mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-30 00:29:46 +08:00
Merge branch 'master' of https://github.com/toeverything/AFFiNE into feature/page-tree-code-style
This commit is contained in:
@@ -18,6 +18,12 @@ interface AffineEditorProps {
|
||||
scrollBlank?: boolean;
|
||||
|
||||
isWhiteboard?: boolean;
|
||||
|
||||
scrollContainer?: HTMLElement;
|
||||
scrollController?: {
|
||||
lockScroll: () => void;
|
||||
unLockScroll: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
function _useConstantWithDispose(
|
||||
@@ -53,13 +59,32 @@ function _useConstantWithDispose(
|
||||
}
|
||||
|
||||
export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
|
||||
({ workspace, rootBlockId, scrollBlank = true, isWhiteboard }, ref) => {
|
||||
(
|
||||
{
|
||||
workspace,
|
||||
rootBlockId,
|
||||
scrollBlank = true,
|
||||
isWhiteboard,
|
||||
scrollController,
|
||||
scrollContainer,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const editor = _useConstantWithDispose(
|
||||
workspace,
|
||||
rootBlockId,
|
||||
isWhiteboard
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainer) {
|
||||
editor.scrollManager.scrollContainer = scrollContainer;
|
||||
}
|
||||
if (scrollController) {
|
||||
editor.scrollManager.scrollController = scrollController;
|
||||
}
|
||||
}, [editor, scrollContainer, scrollController]);
|
||||
|
||||
useImperativeHandle(ref, () => editor);
|
||||
|
||||
return (
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorFallback } from './components/error-fallback';
|
||||
import { ZoomBar } from './components/zoom-bar';
|
||||
import { CommandPanel } from './components/command-panel';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
|
||||
export interface TldrawProps extends TldrawAppCtorProps {
|
||||
/**
|
||||
@@ -132,6 +133,9 @@ export function Tldraw({
|
||||
tools,
|
||||
}: TldrawProps) {
|
||||
const [sId, set_sid] = React.useState(id);
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
|
||||
// Create a new app when the component mounts.
|
||||
const [app, setApp] = React.useState(() => {
|
||||
@@ -140,6 +144,7 @@ export function Tldraw({
|
||||
callbacks,
|
||||
commands,
|
||||
getSession,
|
||||
editorShapeInitSize,
|
||||
tools,
|
||||
});
|
||||
return app;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StrokeLineStyleConfig } from './stroke-line-style-config';
|
||||
import { Group, UnGroup } from './GroupOperation';
|
||||
import { DeleteShapes } from './DeleteOperation';
|
||||
import { Lock, Unlock } from './LockOperation';
|
||||
import { FrameFillColorConfig } from './FrameFillColorConfig';
|
||||
|
||||
export const CommandPanel: FC<{ app: TldrawApp }> = ({ app }) => {
|
||||
const state = app.useStore();
|
||||
@@ -51,6 +52,13 @@ export const CommandPanel: FC<{ app: TldrawApp }> = ({ app }) => {
|
||||
shapes={config.fill.selectedShapes}
|
||||
/>
|
||||
) : null,
|
||||
frameFill: config.frameFill.selectedShapes.length ? (
|
||||
<FrameFillColorConfig
|
||||
key="fill"
|
||||
app={app}
|
||||
shapes={config.frameFill.selectedShapes}
|
||||
/>
|
||||
) : null,
|
||||
font: config.font.selectedShapes.length ? (
|
||||
<FontSizeConfig
|
||||
key="font"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { FC } from 'react';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import type { TDShape } from '@toeverything/components/board-types';
|
||||
import {
|
||||
Popover,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import {
|
||||
ShapeColorNoneIcon,
|
||||
ShapeColorDuotoneIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { countBy, maxBy } from '@toeverything/utils';
|
||||
import { getShapeIds } from './utils';
|
||||
import { Palette } from '../palette';
|
||||
|
||||
interface BorderColorConfigProps {
|
||||
app: TldrawApp;
|
||||
shapes: TDShape[];
|
||||
}
|
||||
|
||||
type ColorType = 'none' | string;
|
||||
|
||||
const _colors: ColorType[] = [
|
||||
'rgba(255, 133, 137, 0.5)',
|
||||
'rgba(255, 159, 101, 0.5)',
|
||||
'rgba(255, 251, 69, 0.5)',
|
||||
'rgba(64, 255, 138, 0.5)',
|
||||
'rgba(26, 252, 255, 0.5)',
|
||||
'rgba(198, 156, 255, 0.5)',
|
||||
'rgba(255, 143, 224, 0.5)',
|
||||
'rgba(152, 172, 189, 0.5)',
|
||||
'rgba(216, 226, 248, 0.5)',
|
||||
];
|
||||
|
||||
const _getIconRenderColor = (shapes: TDShape[]) => {
|
||||
const counted = countBy(shapes, shape => shape.style.fill);
|
||||
const max = maxBy(Object.entries(counted), ([c, n]) => n);
|
||||
return max[0];
|
||||
};
|
||||
|
||||
export const FrameFillColorConfig: FC<BorderColorConfigProps> = ({
|
||||
app,
|
||||
shapes,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const setFillColor = (color: ColorType) => {
|
||||
app.style(
|
||||
{ fill: color, isFilled: color !== 'none' },
|
||||
getShapeIds(shapes)
|
||||
);
|
||||
};
|
||||
|
||||
const iconColor = _getIconRenderColor(shapes);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="hover"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<Palette
|
||||
colors={_colors}
|
||||
selected={iconColor}
|
||||
onSelect={setFillColor}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Tooltip content="Frame Background Color" placement="top-start">
|
||||
<IconButton>
|
||||
{iconColor === 'none' ? (
|
||||
<ShapeColorNoneIcon />
|
||||
) : (
|
||||
<ShapeColorDuotoneIcon
|
||||
style={{
|
||||
color: iconColor,
|
||||
border:
|
||||
iconColor === '#FFFFFF'
|
||||
? `1px solid ${theme.affine.palette.tagHover}`
|
||||
: 0,
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ interface Config {
|
||||
type:
|
||||
| 'stroke'
|
||||
| 'fill'
|
||||
| 'frameFill'
|
||||
| 'font'
|
||||
| 'group'
|
||||
| 'ungroup'
|
||||
@@ -22,6 +23,10 @@ const _createInitConfig = (): Record<Config['type'], Config> => {
|
||||
type: 'fill',
|
||||
selectedShapes: [],
|
||||
},
|
||||
frameFill: {
|
||||
type: 'frameFill',
|
||||
selectedShapes: [],
|
||||
},
|
||||
stroke: {
|
||||
type: 'stroke',
|
||||
selectedShapes: [],
|
||||
@@ -64,6 +69,7 @@ const _isSupportStroke = (shape: TDShape): boolean => {
|
||||
TDShapeType.Pencil,
|
||||
TDShapeType.Laser,
|
||||
TDShapeType.Highlight,
|
||||
TDShapeType.Draw,
|
||||
TDShapeType.Arrow,
|
||||
TDShapeType.Line,
|
||||
].some(type => type === shape.type);
|
||||
@@ -91,6 +97,10 @@ const _isSupportFont = (shape: TDShape): boolean => {
|
||||
].some(type => type === shape.type);
|
||||
};
|
||||
|
||||
const _isSupportFrameFill = (shape: TDShape): boolean => {
|
||||
return shape.type === TDShapeType.Frame;
|
||||
};
|
||||
|
||||
export const useConfig = (app: TldrawApp): Record<Config['type'], Config> => {
|
||||
const state = app.useStore();
|
||||
const selectedShapes = TLDR.get_selected_shapes(state, app.currentPageId);
|
||||
@@ -105,6 +115,9 @@ export const useConfig = (app: TldrawApp): Record<Config['type'], Config> => {
|
||||
if (_isSupportFont(cur)) {
|
||||
acc.font.selectedShapes.push(cur);
|
||||
}
|
||||
if (_isSupportFrameFill(cur)) {
|
||||
acc.frameFill.selectedShapes.push(cur);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
_createInitConfig()
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TldrawPatch,
|
||||
TDShape,
|
||||
TDStatus,
|
||||
TDShapeType,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
@@ -75,6 +76,10 @@ export class RotateSession extends BaseSession {
|
||||
app: { currentPageId, currentPoint, shiftKey },
|
||||
} = this;
|
||||
|
||||
const filteredShapes = initialShapes.filter(
|
||||
shape => shape.shape.type !== TDShapeType.Editor
|
||||
);
|
||||
|
||||
const shapes: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
let directionDelta =
|
||||
@@ -85,7 +90,7 @@ export class RotateSession extends BaseSession {
|
||||
}
|
||||
|
||||
// Update the shapes
|
||||
initialShapes.forEach(({ center, shape }) => {
|
||||
filteredShapes.forEach(({ center, shape }) => {
|
||||
const { rotation = 0 } = shape;
|
||||
let shapeDelta = 0;
|
||||
|
||||
|
||||
+17
-43
@@ -1,12 +1,9 @@
|
||||
import * as React from 'react';
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { Utils, SVGContainer } from '@tldraw/core';
|
||||
import {
|
||||
FrameShape,
|
||||
DashStyle,
|
||||
TDShapeType,
|
||||
TDMeta,
|
||||
GHOSTED_OPACITY,
|
||||
LABEL_POINT,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TDShapeUtil } from '../TDShapeUtil';
|
||||
import {
|
||||
@@ -14,14 +11,13 @@ import {
|
||||
getShapeStyle,
|
||||
getBoundsRectangle,
|
||||
transformRectangle,
|
||||
getFontStyle,
|
||||
transformSingleRectangle,
|
||||
} from '../shared';
|
||||
import { DrawFrame } from './components/draw-frame';
|
||||
import { Frame } from './components/Frame';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
type T = FrameShape;
|
||||
type E = HTMLDivElement;
|
||||
type E = SVGSVGElement;
|
||||
|
||||
export class FrameUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Frame as const;
|
||||
@@ -56,10 +52,7 @@ export class FrameUtil extends TDShapeUtil<T, E> {
|
||||
(
|
||||
{
|
||||
shape,
|
||||
isEditing,
|
||||
isBinding,
|
||||
isSelected,
|
||||
isGhost,
|
||||
meta,
|
||||
bounds,
|
||||
events,
|
||||
@@ -70,21 +63,20 @@ export class FrameUtil extends TDShapeUtil<T, E> {
|
||||
) => {
|
||||
const { id, size, style } = shape;
|
||||
return (
|
||||
<FullWrapper ref={ref} {...events}>
|
||||
<SVGContainer
|
||||
id={shape.id + '_svg'}
|
||||
opacity={1}
|
||||
fill={'#fff'}
|
||||
>
|
||||
<DrawFrame
|
||||
id={id}
|
||||
style={style}
|
||||
size={size}
|
||||
isSelected={isSelected}
|
||||
isDarkMode={meta.isDarkMode}
|
||||
/>
|
||||
</SVGContainer>
|
||||
</FullWrapper>
|
||||
<SVGContainer
|
||||
ref={ref}
|
||||
{...events}
|
||||
id={shape.id + '_svg'}
|
||||
opacity={1}
|
||||
>
|
||||
<Frame
|
||||
id={id}
|
||||
style={style}
|
||||
size={size}
|
||||
isSelected={isSelected}
|
||||
isDarkMode={meta.isDarkMode}
|
||||
/>
|
||||
</SVGContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -121,27 +113,9 @@ export class FrameUtil extends TDShapeUtil<T, E> {
|
||||
override transform = transformRectangle;
|
||||
|
||||
override transformSingle = transformSingleRectangle;
|
||||
|
||||
override hitTestPoint = (shape: T, point: number[]): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
override hitTestLineSegment = (
|
||||
shape: T,
|
||||
A: number[],
|
||||
B: number[]
|
||||
): boolean => {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
const FullWrapper = styled('div')({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'.tl-fill-hitarea': {
|
||||
fill: '#F7F9FF',
|
||||
},
|
||||
'.tl-stroke-hitarea': {
|
||||
fill: '#F7F9FF',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
interface RectangleSvgProps {
|
||||
id: string;
|
||||
style: ShapeStyles;
|
||||
isSelected: boolean;
|
||||
size: number[];
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const Frame = React.memo(function DashedRectangle({
|
||||
id,
|
||||
style,
|
||||
size,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
}: RectangleSvgProps) {
|
||||
const { strokeWidth, fill } = getShapeStyle(style, isDarkMode);
|
||||
|
||||
const _fill = fill && fill !== 'none' ? fill : '#F7F9FF';
|
||||
|
||||
const sw = 1 + strokeWidth * 1.618;
|
||||
|
||||
const w = Math.max(0, size[0] - sw / 2);
|
||||
const h = Math.max(0, size[1] - sw / 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
fill={_fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
interface RectangleSvgProps {
|
||||
id: string;
|
||||
style: ShapeStyles;
|
||||
isSelected: boolean;
|
||||
size: number[];
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const DrawFrame = React.memo(function DashedRectangle({
|
||||
id,
|
||||
style,
|
||||
size,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
}: RectangleSvgProps) {
|
||||
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
|
||||
|
||||
const sw = 1 + strokeWidth * 1.618;
|
||||
|
||||
const w = Math.max(0, size[0] - sw / 2);
|
||||
const h = Math.max(0, size[1] - sw / 2);
|
||||
|
||||
return (
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
export * from './frame-util';
|
||||
export * from './FrameUtil';
|
||||
|
||||
@@ -73,6 +73,7 @@ import { StateManager } from './manager/state-manager';
|
||||
import { getClipboard, setClipboard } from './idb-clipboard';
|
||||
import type { Commands } from './types/commands';
|
||||
import type { BaseTool } from './types/tool';
|
||||
import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core';
|
||||
|
||||
const uuid = Utils.uniqueId();
|
||||
|
||||
@@ -178,6 +179,7 @@ export interface TldrawAppCtorProps {
|
||||
getSession: (type: SessionType) => {
|
||||
new (app: TldrawApp, ...args: any[]): BaseSessionType;
|
||||
};
|
||||
editorShapeInitSize?: number;
|
||||
commands: Commands;
|
||||
tools: Record<string, { new (app: TldrawApp): BaseTool }>;
|
||||
}
|
||||
@@ -223,6 +225,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
fileSystemHandle: FileSystemHandle | null = null;
|
||||
|
||||
editorShapeInitSize = MIN_PAGE_WIDTH;
|
||||
|
||||
viewport = Utils.getBoundsFromPoints([
|
||||
[0, 0],
|
||||
[100, 100],
|
||||
@@ -285,6 +289,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
return acc;
|
||||
}, {} as Record<string, BaseTool>);
|
||||
this.currentTool = this.tools['select'];
|
||||
|
||||
if (props.editorShapeInitSize) {
|
||||
this.editorShapeInitSize = props.editorShapeInitSize;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Internal -------------------- */
|
||||
|
||||
@@ -18,6 +18,7 @@ export class EditorTool extends BaseTool {
|
||||
const {
|
||||
currentPoint,
|
||||
currentGrid,
|
||||
editorShapeInitSize,
|
||||
settings: { showGrid },
|
||||
appState: { currentPageId, currentStyle },
|
||||
document: { id: workspace },
|
||||
@@ -47,6 +48,7 @@ export class EditorTool extends BaseTool {
|
||||
? Vec.snap(currentPoint, currentGrid)
|
||||
: currentPoint,
|
||||
style: { ...currentStyle },
|
||||
size: [editorShapeInitSize, 200],
|
||||
workspace,
|
||||
});
|
||||
// In order to make the cursor just positioned at the beginning of the first line, it needs to be adjusted according to the padding newShape.point = Vec.sub(newShape.point, [50, 30]);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import style9 from 'style9';
|
||||
import {
|
||||
MuiButton as Button,
|
||||
MuiCollapse as Collapse,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||
import {
|
||||
ArrowDropDownIcon,
|
||||
ArrowRightIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
|
||||
const styles = style9.create({
|
||||
ligoButton: {
|
||||
textTransform: 'none',
|
||||
const StyledContainer = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
background: '#f5f7f8',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,29 +29,32 @@ export type CollapsibleTitleProps = {
|
||||
};
|
||||
|
||||
export function CollapsibleTitle(props: CollapsibleTitleProps) {
|
||||
const { className, style, children, title, initialOpen = true } = props;
|
||||
const { children, title, initialOpen = true } = props;
|
||||
|
||||
const [open, setOpen] = useState(initialOpen);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
startIcon={
|
||||
open ? (
|
||||
<ArrowDropDownIcon sx={{ color: '#566B7D' }} />
|
||||
) : (
|
||||
<ArrowRightIcon sx={{ color: '#566B7D' }} />
|
||||
)
|
||||
}
|
||||
onClick={() => setOpen(prev => !prev)}
|
||||
sx={{ color: '#566B7D', textTransform: 'none' }}
|
||||
className={clsx(styles('ligoButton'), className)}
|
||||
style={style}
|
||||
disableElevation
|
||||
disableRipple
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
<StyledContainer onClick={() => setOpen(prev => !prev)}>
|
||||
{open ? (
|
||||
<ArrowDropDownIcon sx={{ color: '#566B7D' }} />
|
||||
) : (
|
||||
<ArrowRightIcon sx={{ color: '#566B7D' }} />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
color: '#98ACBD',
|
||||
textTransform: 'none',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</StyledContainer>
|
||||
{children ? (
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{children}
|
||||
|
||||
@@ -121,11 +121,11 @@ const isLinkActive = (editor: ReactEditor) => {
|
||||
|
||||
const LinkStyledTooltip = styled(({ className, ...props }: MuiTooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(() => ({
|
||||
))(({ theme }) => ({
|
||||
[`& .${muiTooltipClasses.tooltip}`]: {
|
||||
backgroundColor: '#fff',
|
||||
color: '#4C6275',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
fontSize: '14px',
|
||||
},
|
||||
[`& .MuiTooltip-tooltipPlacementBottom`]: {
|
||||
@@ -412,8 +412,7 @@ export const LinkModal = memo((props: LinkModalProps) => {
|
||||
visible && (
|
||||
<>
|
||||
<LinkBehavior onMousedown={handle_mouse_down} rects={rects} />
|
||||
<div
|
||||
className={styles('linkModalContainer')}
|
||||
<LinkModalContainer
|
||||
style={{
|
||||
top: top + height + GAP_BETWEEN_CONTENT_AND_MODAL,
|
||||
left,
|
||||
@@ -431,7 +430,7 @@ export const LinkModal = memo((props: LinkModalProps) => {
|
||||
autoComplete="off"
|
||||
ref={inputEl}
|
||||
/>
|
||||
</div>
|
||||
</LinkModalContainer>
|
||||
</>
|
||||
),
|
||||
body
|
||||
@@ -491,19 +490,20 @@ const LinkBehavior = (props: {
|
||||
);
|
||||
};
|
||||
|
||||
const LinkModalContainer = styled('div')(({ theme }) => ({
|
||||
position: 'fixed',
|
||||
width: '354px',
|
||||
height: '40px',
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
borderRadius: '4px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
zIndex: '1',
|
||||
}));
|
||||
|
||||
const styles = style9.create({
|
||||
linkModalContainer: {
|
||||
position: 'fixed',
|
||||
width: '354px',
|
||||
height: '40px',
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
zIndex: '1',
|
||||
},
|
||||
linkModalContainerIcon: {
|
||||
width: '16px',
|
||||
margin: '0 16px 0 4px',
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, BulletIcon, NumberType } from './data';
|
||||
@@ -188,7 +188,7 @@ export const BulletView: FC<CreateView> = ({ block, editor }) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<List>
|
||||
<BulletLeft>
|
||||
<BulletIcon numberType={properties.numberType} />
|
||||
@@ -206,7 +206,7 @@ export const BulletView: FC<CreateView> = ({ block, editor }) => {
|
||||
/>
|
||||
</div>
|
||||
</List>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
|
||||
@@ -50,7 +50,7 @@ import { Option, Select } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
interface CreateCodeView extends CreateView {
|
||||
@@ -163,7 +163,7 @@ export const CodeView: FC<CreateCodeView> = ({ block, editor }) => {
|
||||
editor.selectionManager.activePreviousNode(block.id, 'start');
|
||||
};
|
||||
return (
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<CodeBlock
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
@@ -222,6 +222,6 @@ export const CodeView: FC<CreateCodeView> = ({ block, editor }) => {
|
||||
handleKeyArrowUp={handleKeyArrowUp}
|
||||
/>
|
||||
</CodeBlock>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
@@ -33,7 +33,7 @@ export const EmbedLinkView: FC<EmbedLinkView> = props => {
|
||||
};
|
||||
|
||||
return (
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<LinkContainer>
|
||||
{embedLinkUrl ? (
|
||||
<SourceView
|
||||
@@ -53,6 +53,6 @@ export const EmbedLinkView: FC<EmbedLinkView> = props => {
|
||||
/>
|
||||
)}
|
||||
</LinkContainer>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
import { SourceView } from '../../components/source-view';
|
||||
@@ -30,7 +30,7 @@ export const FigmaView: FC<FigmaView> = ({ block, editor }) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
return (
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<LinkContainer>
|
||||
{figmaUrl ? (
|
||||
<SourceView
|
||||
@@ -52,6 +52,6 @@ export const FigmaView: FC<FigmaView> = ({ block, editor }) => {
|
||||
/>
|
||||
)}
|
||||
</LinkContainer>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ type GridHandleProps = {
|
||||
blockId: string;
|
||||
enabledAddItem: boolean;
|
||||
draggable: boolean;
|
||||
alertHandleId: string;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const GridHandle: FC<GridHandleProps> = function ({
|
||||
@@ -21,6 +23,8 @@ export const GridHandle: FC<GridHandleProps> = function ({
|
||||
onDrag,
|
||||
onMouseDown,
|
||||
draggable,
|
||||
alertHandleId,
|
||||
onMouseEnter,
|
||||
}) {
|
||||
const [isMouseDown, setIsMouseDown] = useState<boolean>(false);
|
||||
const handleMouseDown: React.MouseEventHandler<HTMLDivElement> = e => {
|
||||
@@ -44,16 +48,17 @@ export const GridHandle: FC<GridHandleProps> = function ({
|
||||
editor.selectionManager.setActivatedNodeId(textBlock.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = e => {
|
||||
onMouseEnter && onMouseEnter(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<GridHandleContainer
|
||||
style={
|
||||
isMouseDown
|
||||
? {
|
||||
backgroundColor: '#3E6FDB',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
isMouseDown={isMouseDown}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
isAlert={alertHandleId === blockId}
|
||||
>
|
||||
{enabledAddItem ? (
|
||||
<AddGridHandle
|
||||
@@ -67,7 +72,10 @@ export const GridHandle: FC<GridHandleProps> = function ({
|
||||
);
|
||||
};
|
||||
|
||||
const GridHandleContainer = styled('div')(({ theme }) => ({
|
||||
const GridHandleContainer = styled('div')<{
|
||||
isMouseDown: boolean;
|
||||
isAlert: boolean;
|
||||
}>(({ theme, isMouseDown, isAlert }) => ({
|
||||
position: 'relative',
|
||||
width: '10px',
|
||||
flexGrow: '0',
|
||||
@@ -78,11 +86,15 @@ const GridHandleContainer = styled('div')(({ theme }) => ({
|
||||
borderRadius: '1px',
|
||||
backgroundClip: 'content-box',
|
||||
' &:hover': {
|
||||
backgroundColor: theme.affine.palette.primary,
|
||||
backgroundColor: isAlert ? 'red' : theme.affine.palette.primary,
|
||||
[`.${GRID_ADD_HANDLE_NAME}`]: {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
...(isMouseDown &&
|
||||
(isAlert
|
||||
? { backgroundColor: 'red' }
|
||||
: { backgroundColor: theme.affine.palette.primary })),
|
||||
}));
|
||||
|
||||
const AddGridHandle = styled('div')(({ theme }) => ({
|
||||
|
||||
@@ -31,6 +31,7 @@ export const Grid: FC<CreateView> = function (props) {
|
||||
const gridItemCountRef = useRef<number>();
|
||||
const originalLeftWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
|
||||
const originalRightWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
|
||||
const [alertHandleId, setAlertHandleId] = useState<string>(null);
|
||||
|
||||
const getLeftRightGridItemDomByIndex = (index: number) => {
|
||||
const gridItems = Array.from(gridContainerRef.current?.children).filter(
|
||||
@@ -117,7 +118,7 @@ export const Grid: FC<CreateView> = function (props) {
|
||||
itemDom.style.width = width;
|
||||
};
|
||||
|
||||
const handleDragGrid = (e: MouseEvent, index: number) => {
|
||||
const handleDragGrid = async (e: MouseEvent, index: number) => {
|
||||
setIsOnDrag(true);
|
||||
window.getSelection().removeAllRanges();
|
||||
if (!isSetMouseUp.current) {
|
||||
@@ -165,39 +166,47 @@ export const Grid: FC<CreateView> = function (props) {
|
||||
setItemWidth(leftGrid, newLeft);
|
||||
setItemWidth(rightGrid, newRight);
|
||||
updateDbWidth(leftBlockId, newLeft, rightBlockId, newRight);
|
||||
[leftBlockId, rightBlockId].forEach(async blockId => {
|
||||
if (await checkGridItemHasOverflow(blockId)) {
|
||||
setAlertHandleId(leftBlockId);
|
||||
} else {
|
||||
setAlertHandleId(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const children = (
|
||||
<>
|
||||
{block.childrenIds.map((id, i) => {
|
||||
return (
|
||||
<GridItem
|
||||
style={{
|
||||
transition: isOnDrag
|
||||
? 'none'
|
||||
: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
onMouseDown={event => handleMouseDown(event, i)}
|
||||
blockId={id}
|
||||
enabledAddItem={
|
||||
block.childrenIds.length < MAX_ITEM_COUNT
|
||||
}
|
||||
draggable={i !== block.childrenIds.length - 1}
|
||||
/>
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
const checkGridItemHasOverflow = async (blockId: string) => {
|
||||
let isOverflow = false;
|
||||
const block = await editor.getBlockById(blockId);
|
||||
if (block) {
|
||||
const blockDom = block.dom;
|
||||
if (blockDom) {
|
||||
block.dom.style.overflow = 'scroll';
|
||||
if (block.dom.clientWidth !== block.dom.scrollWidth) {
|
||||
isOverflow = true;
|
||||
}
|
||||
blockDom.style.overflow = 'visible';
|
||||
}
|
||||
}
|
||||
return isOverflow;
|
||||
};
|
||||
|
||||
const handleHandleMouseEnter = (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
index: number
|
||||
) => {
|
||||
const leftBlockId = block.childrenIds[index];
|
||||
const rightBlockId = block.childrenIds[index + 1];
|
||||
[leftBlockId, rightBlockId].forEach(async blockId => {
|
||||
if (await checkGridItemHasOverflow(blockId)) {
|
||||
setAlertHandleId(leftBlockId);
|
||||
} else {
|
||||
setAlertHandleId(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -206,7 +215,35 @@ export const Grid: FC<CreateView> = function (props) {
|
||||
ref={gridContainerRef}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
{children}
|
||||
{block.childrenIds.map((id, i) => {
|
||||
return (
|
||||
<GridItem
|
||||
style={{
|
||||
transition: isOnDrag
|
||||
? 'none'
|
||||
: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
onMouseDown={event => handleMouseDown(event, i)}
|
||||
blockId={id}
|
||||
enabledAddItem={
|
||||
block.childrenIds.length < MAX_ITEM_COUNT
|
||||
}
|
||||
onMouseEnter={event =>
|
||||
handleHandleMouseEnter(event, i)
|
||||
}
|
||||
alertHandleId={alertHandleId}
|
||||
draggable={i !== block.childrenIds.length - 1}
|
||||
/>
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
</GridContainer>
|
||||
{isOnDrag
|
||||
? ReactDOM.createPortal(<GridMask />, window.document.body)
|
||||
|
||||
@@ -60,7 +60,7 @@ const GroupContainer = styled('div')<{ isSelect?: boolean }>(
|
||||
({ isSelect, theme }) => ({
|
||||
background: theme.affine.palette.white,
|
||||
border: '2px solid rgba(236,241,251,.5)',
|
||||
padding: '15px 12px',
|
||||
padding: `15px 16px 0 16px`,
|
||||
borderRadius: '10px',
|
||||
...(isSelect
|
||||
? {
|
||||
@@ -69,7 +69,7 @@ const GroupContainer = styled('div')<{ isSelect?: boolean }>(
|
||||
}
|
||||
: {
|
||||
'&:hover': {
|
||||
boxShadow: '0px 1px 10px rgb(152 172 189 / 60%)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -2,11 +2,11 @@ import { styled } from '@toeverything/components/ui';
|
||||
import type { ComponentPropsWithRef, MouseEvent } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const StyledPanel = styled('div')(() => ({
|
||||
const StyledPanel = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
borderRadius: 10,
|
||||
padding: '12px 24px',
|
||||
}));
|
||||
|
||||
@@ -41,6 +41,7 @@ const getKanbanColor = (
|
||||
return DEFAULT_COLOR;
|
||||
}
|
||||
if (
|
||||
group.type === PropertyType.Status ||
|
||||
group.type === PropertyType.Select ||
|
||||
group.type === PropertyType.MultiSelect ||
|
||||
group.type === DEFAULT_GROUP_ID
|
||||
|
||||
@@ -25,7 +25,7 @@ const AddCard = ({ group }: { group: KanbanGroup }) => {
|
||||
const { addCard } = useKanban();
|
||||
const handleClick = useCallback(async () => {
|
||||
await addCard(group);
|
||||
}, [addCard]);
|
||||
}, [addCard, group]);
|
||||
return <AddCardWrapper onClick={handleClick}>+</AddCardWrapper>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { KanbanCard } from '@toeverything/components/editor-core';
|
||||
import { RenderBlock, useKanban } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
RenderBlock,
|
||||
useKanban,
|
||||
useRefPage,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
|
||||
const CardContent = styled('div')({
|
||||
margin: '20px',
|
||||
@@ -58,18 +63,24 @@ export const CardItem = ({
|
||||
block: KanbanCard['block'];
|
||||
}) => {
|
||||
const { addSubItem } = useKanban();
|
||||
const { openSubPage } = useRefPage();
|
||||
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
|
||||
const onAddItem = async () => {
|
||||
await addSubItem(block);
|
||||
};
|
||||
|
||||
const onClickCard = async () => {
|
||||
showKanbanRefPageFlag && openSubPage(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardContainer>
|
||||
<CardContainer onClick={onClickCard}>
|
||||
<CardContent>
|
||||
<RenderBlock blockId={id} />
|
||||
</CardContent>
|
||||
<CardActions onClick={onAddItem}>
|
||||
<PlusIcon />
|
||||
<span>Add item</span>
|
||||
<span>Add a sub-block</span>
|
||||
</CardActions>
|
||||
</CardContainer>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
useCurrentView,
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
@@ -143,13 +143,13 @@ export const ImageView: FC<ImageView> = ({ block, editor }) => {
|
||||
type: 'link',
|
||||
});
|
||||
};
|
||||
const handle_click = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handle_click = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
//TODO clear active selection
|
||||
// document.getElementsByTagName('body')[0].click();
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopPropagation();
|
||||
editor.selectionManager.setSelectedNodesIds([block.id]);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
await editor.selectionManager.setSelectedNodesIds([block.id]);
|
||||
await editor.selectionManager.activeNodeByNodeId(block.id, 'end');
|
||||
};
|
||||
const down_file = () => {
|
||||
if (down_ref) {
|
||||
@@ -158,7 +158,7 @@ export const ImageView: FC<ImageView> = ({ block, editor }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<ImageBlock>
|
||||
<div ref={resize_box}>
|
||||
{imgUrl ? (
|
||||
@@ -229,6 +229,6 @@ export const ImageView: FC<ImageView> = ({ block, editor }) => {
|
||||
</div> */}
|
||||
</div>
|
||||
</ImageBlock>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,9 +19,8 @@ import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { List } from '../../components/style-container';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
|
||||
@@ -185,7 +184,7 @@ export const NumberedView: FC<CreateView> = ({ block, editor }) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<List>
|
||||
<div className={'checkBoxContainer'}>
|
||||
{getNumber(properties.numberType, number)}.
|
||||
@@ -203,7 +202,7 @@ export const NumberedView: FC<CreateView> = ({ block, editor }) => {
|
||||
/>
|
||||
</div>
|
||||
</List>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
supportChildren,
|
||||
unwrapGroup,
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
@@ -99,7 +99,7 @@ export const TextView: FC<CreateTextView> = ({
|
||||
if (!parentBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const preParent = await parentBlock.previousSibling();
|
||||
if (Protocol.Block.Type.group === parentBlock.type) {
|
||||
const children = await block.children();
|
||||
const preNode = await block.physicallyPerviousSibling();
|
||||
@@ -129,34 +129,19 @@ export const TextView: FC<CreateTextView> = ({
|
||||
'start'
|
||||
);
|
||||
if (block.blockProvider.isEmpty()) {
|
||||
block.remove();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// TODO remove timing problem
|
||||
const prevGroupBlock = await parentBlock.previousSibling();
|
||||
|
||||
if (!prevGroupBlock) {
|
||||
const childrenBlock = await parentBlock.children();
|
||||
if (childrenBlock.length) {
|
||||
if (children.length) {
|
||||
await parentBlock.append(...children);
|
||||
}
|
||||
await block.remove();
|
||||
return true;
|
||||
const parentChild = await parentBlock.children();
|
||||
if (
|
||||
parentBlock.type ===
|
||||
Protocol.Block.Type.group &&
|
||||
!parentChild.length
|
||||
) {
|
||||
await editor.selectionManager.setSelectedNodesIds(
|
||||
[preParent?.id ?? editor.getRootBlockId()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parentBlock.remove();
|
||||
return true;
|
||||
}
|
||||
if (prevGroupBlock.type !== Protocol.Block.Type.group) {
|
||||
unwrapGroup(parentBlock);
|
||||
return true;
|
||||
}
|
||||
|
||||
mergeGroup(prevGroupBlock, parentBlock);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -231,7 +216,7 @@ export const TextView: FC<CreateTextView> = ({
|
||||
selected={isSelect}
|
||||
className={containerClassName}
|
||||
>
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<TextBlock
|
||||
block={block}
|
||||
type={block.type}
|
||||
@@ -242,7 +227,7 @@ export const TextView: FC<CreateTextView> = ({
|
||||
handleConvert={handleConvert}
|
||||
handleTab={onTab}
|
||||
/>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import type { AsyncBlock } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
AsyncBlock,
|
||||
useCurrentView,
|
||||
useLazyIframe,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
FC,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { SCENE_CONFIG } from '../../blocks/group/config';
|
||||
import { BlockPreview } from './BlockView';
|
||||
import { formatUrl } from './format-url';
|
||||
|
||||
@@ -15,7 +27,18 @@ export interface Props {
|
||||
}
|
||||
|
||||
const getHost = (url: string) => new URL(url).host;
|
||||
|
||||
const MouseMaskContainer = styled('div')({
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
backgroundColor: 'transparent',
|
||||
'&:hover': {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
});
|
||||
const LinkContainer = styled('div')<{
|
||||
isSelected: boolean;
|
||||
}>(({ theme, isSelected }) => {
|
||||
@@ -38,12 +61,28 @@ const LinkContainer = styled('div')<{
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const _getLinkStyle = (scene: string) => {
|
||||
switch (scene) {
|
||||
case SCENE_CONFIG.PAGE:
|
||||
return {
|
||||
width: '420px',
|
||||
height: '198px',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
width: '252px',
|
||||
height: '126px',
|
||||
};
|
||||
}
|
||||
};
|
||||
const SourceViewContainer = styled('div')<{
|
||||
isSelected: boolean;
|
||||
}>(({ theme, isSelected }) => {
|
||||
scene: string;
|
||||
}>(({ theme, isSelected, scene }) => {
|
||||
return {
|
||||
..._getLinkStyle(scene),
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
background: isSelected ? 'rgba(152, 172, 189, 0.1)' : 'transparent',
|
||||
padding: '8px',
|
||||
@@ -52,32 +91,96 @@ const SourceViewContainer = styled('div')<{
|
||||
height: '100%',
|
||||
border: '1px solid #EAEEF2',
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
userSelect: 'none',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const LazyIframe = ({
|
||||
src,
|
||||
delay = 3000,
|
||||
fallback,
|
||||
}: {
|
||||
src: string;
|
||||
delay?: number;
|
||||
fallback?: ReactNode;
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const timer = useRef<number>();
|
||||
|
||||
useEffect(() => {
|
||||
// Hide iframe when the src changed
|
||||
setShow(false);
|
||||
}, [src]);
|
||||
|
||||
const onLoad = () => {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = window.setTimeout(() => {
|
||||
// Prevent iframe scrolling parent container
|
||||
// Remove the delay after the issue is resolved
|
||||
// See W3C https://github.com/w3c/csswg-drafts/issues/7134
|
||||
// See https://forum.figma.com/t/prevent-figmas-embed-code-from-automatically-scrolling-to-it-on-page-load/26029/6
|
||||
setShow(true);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onMouseDown={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
style={{ display: show ? 'block' : 'none', height: '100%' }}
|
||||
>
|
||||
<iframe src={src} onLoad={onLoad} />
|
||||
</div>
|
||||
{!show && fallback}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Loading = styled('div')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
lineHeight: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid #EAEEF2',
|
||||
};
|
||||
});
|
||||
|
||||
const LoadingContiner = () => {
|
||||
return <Loading>loading...</Loading>;
|
||||
};
|
||||
|
||||
export const SourceView: FC<Props> = props => {
|
||||
const { link, isSelected, block, editorElement } = props;
|
||||
const src = formatUrl(link);
|
||||
const openTabOnBrowser = () => {
|
||||
window.open(link, '_blank');
|
||||
};
|
||||
// let iframeShow = useLazyIframe(src, 3000, iframeContainer);
|
||||
const [currentView] = useCurrentView();
|
||||
const { type } = currentView;
|
||||
if (src?.startsWith('http')) {
|
||||
return (
|
||||
<LinkContainer
|
||||
isSelected={isSelected}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
onClick={openTabOnBrowser}
|
||||
>
|
||||
<p>{getHost(src)}</p>
|
||||
<p>{src}</p>
|
||||
</LinkContainer>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<SourceViewContainer isSelected={isSelected} scene={type}>
|
||||
<MouseMaskContainer />
|
||||
|
||||
<LazyIframe
|
||||
src={src}
|
||||
fallback={LoadingContiner()}
|
||||
></LazyIframe>
|
||||
</SourceViewContainer>
|
||||
</div>
|
||||
);
|
||||
} else if (src?.startsWith('affine')) {
|
||||
return (
|
||||
<SourceViewContainer
|
||||
isSelected={isSelected}
|
||||
style={{ padding: '0' }}
|
||||
scene={type}
|
||||
>
|
||||
<BlockPreview
|
||||
block={block}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
RenderBlock,
|
||||
useCurrentView,
|
||||
useOnSelect,
|
||||
WrapperWithPendantAndDragDrop,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type {
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import style9 from 'style9';
|
||||
import { SCENE_CONFIG } from '../blocks/group/config';
|
||||
import { BlockContainer } from '../components/BlockContainer';
|
||||
|
||||
@@ -30,29 +29,15 @@ const TreeView = forwardRef<
|
||||
{ lastItem?: boolean } & ComponentPropsWithRef<'div'>
|
||||
>(({ lastItem, children, onClick, ...restProps }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={treeStyles('treeWrapper')} {...restProps}>
|
||||
<div className={treeStyles('treeView')}>
|
||||
<div
|
||||
className={treeStyles({
|
||||
line: true,
|
||||
verticalLine: true,
|
||||
lastItemVerticalLine: lastItem,
|
||||
})}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<div
|
||||
className={treeStyles({
|
||||
line: true,
|
||||
horizontalLine: true,
|
||||
lastItemHorizontalLine: lastItem,
|
||||
})}
|
||||
onClick={onClick}
|
||||
/>
|
||||
{lastItem && <div className={treeStyles('lastItemRadius')} />}
|
||||
</div>
|
||||
<TreeWrapper ref={ref} {...restProps}>
|
||||
<StyledTreeView>
|
||||
<VerticalLine last={lastItem} onClick={onClick} />
|
||||
<HorizontalLine last={lastItem} onClick={onClick} />
|
||||
{lastItem && <LastItemRadius />}
|
||||
</StyledTreeView>
|
||||
{/* maybe need a child wrapper */}
|
||||
{children}
|
||||
</div>
|
||||
</TreeWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,10 +56,7 @@ const ChildrenView = ({
|
||||
const isKanbanScene = currentView.type === SCENE_CONFIG.KANBAN;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles('children')}
|
||||
style={{ ...(!isKanbanScene && { marginLeft: indent }) }}
|
||||
>
|
||||
<Children style={{ ...(!isKanbanScene && { marginLeft: indent }) }}>
|
||||
{childrenIds.map((childId, idx) => {
|
||||
if (isKanbanScene) {
|
||||
return (
|
||||
@@ -94,7 +76,7 @@ const ChildrenView = ({
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Children>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -104,9 +86,7 @@ const CollapsedNode = forwardRef<
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<TreeView ref={ref} lastItem={true} {...props}>
|
||||
<div className={treeStyles('collapsed')} onClick={props.onClick}>
|
||||
···
|
||||
</div>
|
||||
<Collapsed onClick={props.onClick}>···</Collapsed>
|
||||
</TreeView>
|
||||
);
|
||||
});
|
||||
@@ -146,11 +126,11 @@ export const withTreeViewChildren = (
|
||||
editor={props.editor}
|
||||
block={block}
|
||||
selected={isSelect}
|
||||
className={styles('wrapper')}
|
||||
className={Wrapper.toString()}
|
||||
>
|
||||
<WrapperWithPendantAndDragDrop editor={editor} block={block}>
|
||||
<div className={styles('node')}>{creator(props)}</div>
|
||||
</WrapperWithPendantAndDragDrop>
|
||||
<BlockPendantProvider block={block}>
|
||||
<div>{creator(props)}</div>
|
||||
</BlockPendantProvider>
|
||||
|
||||
{collapsed && (
|
||||
<CollapsedNode
|
||||
@@ -170,93 +150,79 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
node: {},
|
||||
const Wrapper = styled('div')({ display: 'flex', flexDirection: 'column' });
|
||||
|
||||
children: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
const Children = Wrapper;
|
||||
|
||||
const TREE_COLOR = '#D5DFE6';
|
||||
// TODO determine the position of the horizontal line by the type of the item
|
||||
const ITEM_POINT_HEIGHT = '12.5px'; // '50%'
|
||||
|
||||
const TreeWrapper = styled('div')({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const treeColor = '#D5DFE6';
|
||||
// TODO determine the position of the horizontal line by the type of the item
|
||||
const itemPointHeight = '12.5px'; // '50%'
|
||||
const StyledTreeView = styled('div')({
|
||||
position: 'absolute',
|
||||
left: '-21px',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const treeStyles = style9.create({
|
||||
treeWrapper: {
|
||||
position: 'relative',
|
||||
},
|
||||
const Line = styled('div')({
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: TREE_COLOR,
|
||||
// somehow tldraw would override this
|
||||
boxSizing: 'content-box!important' as any,
|
||||
// See [Can I add background color only for padding?](https://stackoverflow.com/questions/14628601/can-i-add-background-color-only-for-padding)
|
||||
backgroundClip: 'content-box',
|
||||
backgroundOrigin: 'content-box',
|
||||
// Increase click hot spot
|
||||
padding: '10px',
|
||||
});
|
||||
|
||||
treeView: {
|
||||
position: 'absolute',
|
||||
left: '-21px',
|
||||
height: '100%',
|
||||
},
|
||||
line: {
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: treeColor,
|
||||
boxSizing: 'content-box',
|
||||
// See [Can I add background color only for padding?](https://stackoverflow.com/questions/14628601/can-i-add-background-color-only-for-padding)
|
||||
backgroundClip: 'content-box',
|
||||
backgroundOrigin: 'content-box',
|
||||
// Increase click hot spot
|
||||
padding: '10px',
|
||||
},
|
||||
verticalLine: {
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
transform: 'translate(-50%, 0)',
|
||||
},
|
||||
horizontalLine: {
|
||||
width: '16px',
|
||||
height: '1px',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
top: itemPointHeight,
|
||||
transform: 'translate(0, -50%)',
|
||||
},
|
||||
noItemHorizontalLine: {
|
||||
display: 'none',
|
||||
},
|
||||
const VerticalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
|
||||
width: '1px',
|
||||
height: last ? ITEM_POINT_HEIGHT : '100%',
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
transform: 'translate(-50%, 0)',
|
||||
|
||||
lastItemHorizontalLine: {
|
||||
opacity: 0,
|
||||
},
|
||||
lastItemVerticalLine: {
|
||||
height: itemPointHeight,
|
||||
opacity: 0,
|
||||
},
|
||||
lastItemRadius: {
|
||||
boxSizing: 'content-box',
|
||||
position: 'absolute',
|
||||
left: '-0.5px',
|
||||
top: 0,
|
||||
height: itemPointHeight,
|
||||
bottom: '50%',
|
||||
width: '16px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderLeftColor: treeColor,
|
||||
borderBottomColor: treeColor,
|
||||
borderTop: 'none',
|
||||
borderRight: 'none',
|
||||
borderRadius: '0 0 0 3px',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
opacity: last ? 0 : 'unset',
|
||||
}));
|
||||
|
||||
collapsed: {
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
color: '#B9CAD5',
|
||||
},
|
||||
const HorizontalLine = styled(Line)<{ last: boolean }>(({ last }) => ({
|
||||
width: '16px',
|
||||
height: '1px',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
top: ITEM_POINT_HEIGHT,
|
||||
transform: 'translate(0, -50%)',
|
||||
opacity: last ? 0 : 'unset',
|
||||
}));
|
||||
|
||||
const Collapsed = styled('div')({
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
color: '#B9CAD5',
|
||||
});
|
||||
|
||||
const LastItemRadius = styled('div')({
|
||||
boxSizing: 'content-box',
|
||||
position: 'absolute',
|
||||
left: '-0.5px',
|
||||
top: 0,
|
||||
height: ITEM_POINT_HEIGHT,
|
||||
bottom: '50%',
|
||||
width: '16px',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderLeftColor: TREE_COLOR,
|
||||
borderBottomColor: TREE_COLOR,
|
||||
borderTop: 'none',
|
||||
borderRight: 'none',
|
||||
borderRadius: '0 0 0 3px',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
const StyledBorder = styled('div')({
|
||||
|
||||
+3
-15
@@ -1,9 +1,8 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import type { Column } from '@toeverything/datasource/db-service';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
|
||||
export const RootContext = createContext<{
|
||||
const RootContext = createContext<{
|
||||
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;
|
||||
@@ -14,6 +13,8 @@ export const RootContext = createContext<{
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
};
|
||||
@@ -22,16 +23,3 @@ export const useEditor = () => {
|
||||
* @deprecated
|
||||
*/
|
||||
export const BlockContext = createContext<AsyncBlock>(null as any);
|
||||
|
||||
/**
|
||||
* Context of column information
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const ColumnsContext = createContext<{
|
||||
fromId: string;
|
||||
columns: Column[];
|
||||
}>({
|
||||
fromId: '',
|
||||
columns: [],
|
||||
});
|
||||
@@ -2,14 +2,14 @@ import type { BlockEditor } from './editor';
|
||||
import { styled, usePatchNodes } from '@toeverything/components/ui';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { RootContext } from './contexts';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
import {
|
||||
Protocol,
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { addNewGroup } from './recast-block';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
|
||||
interface RenderRootProps {
|
||||
@@ -151,7 +151,7 @@ export const RenderRoot: FC<PropsWithChildren<RenderRootProps>> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<RootContext.Provider value={{ editor, editorElement }}>
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isWhiteboard={editor.isWhiteboard}
|
||||
ref={ref => {
|
||||
@@ -183,7 +183,7 @@ export const RenderRoot: FC<PropsWithChildren<RenderRootProps>> = ({
|
||||
{editor.isWhiteboard ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</RootContext.Provider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -199,24 +199,32 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
mouseMoved.current = false;
|
||||
return;
|
||||
}
|
||||
const lastBlock = await editor.getRootLastChildrenBlock();
|
||||
const rootBlock = await editor.getBlockById(
|
||||
editor.getRootBlockId()
|
||||
);
|
||||
if (!rootBlock) {
|
||||
throw new Error('root block is not found');
|
||||
}
|
||||
|
||||
const lastGroupBlock = await editor.getRootLastChildrenBlock();
|
||||
const lastRootChildren = await rootBlock.lastChild();
|
||||
// If last block is not a group
|
||||
// create a group with a empty text
|
||||
if (lastGroupBlock.type !== 'group') {
|
||||
addNewGroup(editor, lastBlock, true);
|
||||
if (lastRootChildren == null) {
|
||||
appendNewGroup(editor, rootBlock, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastGroupBlock.childrenIds.length > 1) {
|
||||
addNewGroup(editor, lastBlock, true);
|
||||
if (
|
||||
lastRootChildren.type !== Protocol.Block.Type.group ||
|
||||
lastRootChildren.childrenIds.length > 1
|
||||
) {
|
||||
addNewGroup(editor, lastRootChildren, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the **only** block in the group is text and is empty
|
||||
// active the text block
|
||||
const theGroupChildBlock = await lastGroupBlock.firstChild();
|
||||
const theGroupChildBlock = await lastRootChildren.firstChild();
|
||||
|
||||
if (
|
||||
theGroupChildBlock &&
|
||||
@@ -229,7 +237,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
return;
|
||||
}
|
||||
// else create a new group
|
||||
addNewGroup(editor, lastBlock, true);
|
||||
addNewGroup(editor, lastRootChildren, true);
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AsyncBlock, BlockEditor } from '../editor';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { BlockPendantProvider } from '../block-pendant';
|
||||
import { DragDropWrapper } from '../drag-drop-wrapper';
|
||||
|
||||
type BlockContentWrapperProps = {
|
||||
block: AsyncBlock;
|
||||
editor: BlockEditor;
|
||||
children: ReactElement | null;
|
||||
};
|
||||
|
||||
export const WrapperWithPendantAndDragDrop: FC<BlockContentWrapperProps> =
|
||||
function ({ block, children, editor }) {
|
||||
return (
|
||||
<DragDropWrapper block={block} editor={editor}>
|
||||
<BlockPendantProvider block={block}>
|
||||
{children}
|
||||
</BlockPendantProvider>
|
||||
</DragDropWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './BlockContentWrapper';
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { PendantPopover } from './pendant-popover';
|
||||
@@ -11,74 +10,68 @@ interface BlockTagProps {
|
||||
block: AsyncBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Need to be refactored
|
||||
*/
|
||||
export const BlockPendantProvider: FC<PropsWithChildren<BlockTagProps>> = ({
|
||||
block,
|
||||
children,
|
||||
}) => {
|
||||
const [container, setContainer] = useState<HTMLElement>(null);
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
return (
|
||||
<Container ref={(dom: HTMLElement) => setContainer(dom)}>
|
||||
<Container>
|
||||
{children}
|
||||
{container && (
|
||||
<PendantPopover
|
||||
block={block}
|
||||
container={container}
|
||||
onVisibleChange={visible => {
|
||||
setIsHover(visible);
|
||||
}}
|
||||
>
|
||||
<StyledTriggerLine
|
||||
className="triggerLine"
|
||||
isHover={isHover}
|
||||
/>
|
||||
</PendantPopover>
|
||||
)}
|
||||
|
||||
<PendantPopover block={block}>
|
||||
<StyledTriggerLine />
|
||||
</PendantPopover>
|
||||
|
||||
<PendantRender block={block} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const Container = styled('div')({
|
||||
export const LINE_GAP = 16;
|
||||
const TAG_GAP = 4;
|
||||
|
||||
const StyledTriggerLine = styled('div')({
|
||||
padding: `${TAG_GAP}px 0`,
|
||||
width: '100px',
|
||||
cursor: 'default',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
position: 'relative',
|
||||
padding: '4px',
|
||||
'&:hover .triggerLine::before': {
|
||||
display: 'flex',
|
||||
|
||||
'::before': {
|
||||
content: "''",
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: '#dadada',
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '4px',
|
||||
},
|
||||
'::after': {
|
||||
content: "''",
|
||||
width: '0',
|
||||
height: '2px',
|
||||
background: '#aac4d5',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '4px',
|
||||
transition: 'width .3s',
|
||||
},
|
||||
});
|
||||
|
||||
const StyledTriggerLine = styled('div')<{ isHover: boolean }>(({ isHover }) => {
|
||||
return {
|
||||
padding: '4px 0',
|
||||
width: '100px',
|
||||
cursor: 'default',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
position: 'relative',
|
||||
|
||||
'::before': {
|
||||
content: "''",
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: '#dadada',
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '4px',
|
||||
const Container = styled('div')({
|
||||
position: 'relative',
|
||||
paddingBottom: `${LINE_GAP - TAG_GAP * 2}px`,
|
||||
'&:hover': {
|
||||
[StyledTriggerLine.toString()]: {
|
||||
'&::before': {
|
||||
display: 'flex',
|
||||
},
|
||||
'&::after': {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
'::after': {
|
||||
content: "''",
|
||||
width: isHover ? '100%' : '0',
|
||||
height: '2px',
|
||||
background: '#aac4d5',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '4px',
|
||||
transition: 'width .3s',
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+26
-15
@@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useRef, useEffect, useState } from 'react';
|
||||
import { getPendantHistory } from '../utils';
|
||||
import {
|
||||
getRecastItemValue,
|
||||
RecastMetaProperty,
|
||||
@@ -30,22 +29,22 @@ export const PendantHistoryPanel = ({
|
||||
|
||||
const [history, setHistory] = useState<RecastBlockValue[]>([]);
|
||||
const popoverHandlerRef = useRef<{ [key: string]: PopperHandler }>({});
|
||||
const { getValueHistory } = getRecastItemValue(block);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const currentBlockValues = getRecastItemValue(block).getAllValue();
|
||||
const allProperties = getProperties();
|
||||
const missProperties = allProperties.filter(
|
||||
const missValues = getProperties().filter(
|
||||
property => !currentBlockValues.find(v => v.id === property.id)
|
||||
);
|
||||
const pendantHistory = getPendantHistory({
|
||||
const valueHistory = getValueHistory({
|
||||
recastBlockId: recastBlock.id,
|
||||
});
|
||||
const historyMap = missProperties.reduce<{
|
||||
[key: RecastPropertyId]: string;
|
||||
const historyMap = missValues.reduce<{
|
||||
[key: RecastPropertyId]: string[];
|
||||
}>((history, property) => {
|
||||
if (pendantHistory[property.id]) {
|
||||
history[property.id] = pendantHistory[property.id];
|
||||
if (valueHistory[property.id]) {
|
||||
history[property.id] = valueHistory[property.id];
|
||||
}
|
||||
|
||||
return history;
|
||||
@@ -54,18 +53,30 @@ export const PendantHistoryPanel = ({
|
||||
const blockHistory = (
|
||||
await Promise.all(
|
||||
Object.entries(historyMap).map(
|
||||
async ([propertyId, blockId]) => {
|
||||
const latestValueBlock = (
|
||||
await groupBlock.children()
|
||||
).find((block: AsyncBlock) => block.id === blockId);
|
||||
async ([propertyId, blockIds]) => {
|
||||
const blocks = await groupBlock.children();
|
||||
const latestChangeBlock = blockIds
|
||||
.reverse()
|
||||
.reduce<AsyncBlock>((block, id) => {
|
||||
if (!block) {
|
||||
return blocks.find(
|
||||
block => block.id === id
|
||||
);
|
||||
}
|
||||
return block;
|
||||
}, null);
|
||||
|
||||
return getRecastItemValue(
|
||||
latestValueBlock
|
||||
).getValue(propertyId as RecastPropertyId);
|
||||
if (latestChangeBlock) {
|
||||
return getRecastItemValue(
|
||||
latestChangeBlock
|
||||
).getValue(propertyId as RecastPropertyId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)
|
||||
)
|
||||
).filter(v => v);
|
||||
|
||||
setHistory(blockHistory);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ModifyPanelContentProps } from './types';
|
||||
import { StyledDivider, StyledPopoverSubTitle } from '../StyledComponent';
|
||||
import { BasicSelect } from './Select';
|
||||
import { InformationProperty, InformationValue } from '../../recast-block';
|
||||
import { genInitialOptions, getPendantIconsConfigByName } from '../utils';
|
||||
import { generateInitialOptions, getPendantIconsConfigByName } from '../utils';
|
||||
|
||||
export default (props: ModifyPanelContentProps) => {
|
||||
const { onPropertyChange, onValueChange, initialValue, property } = props;
|
||||
@@ -38,7 +38,7 @@ export default (props: ModifyPanelContentProps) => {
|
||||
}}
|
||||
initialOptions={
|
||||
propProperty?.emailOptions ||
|
||||
genInitialOptions(
|
||||
generateInitialOptions(
|
||||
property?.type,
|
||||
getPendantIconsConfigByName('Email')
|
||||
)
|
||||
@@ -66,7 +66,7 @@ export default (props: ModifyPanelContentProps) => {
|
||||
}}
|
||||
initialOptions={
|
||||
propProperty?.phoneOptions ||
|
||||
genInitialOptions(
|
||||
generateInitialOptions(
|
||||
property?.type,
|
||||
getPendantIconsConfigByName('Phone')
|
||||
)
|
||||
@@ -94,7 +94,7 @@ export default (props: ModifyPanelContentProps) => {
|
||||
}}
|
||||
initialOptions={
|
||||
propProperty?.locationOptions ||
|
||||
genInitialOptions(
|
||||
generateInitialOptions(
|
||||
property?.type,
|
||||
getPendantIconsConfigByName('Location')
|
||||
)
|
||||
|
||||
@@ -18,7 +18,9 @@ export default ({
|
||||
user: { username, nickname, photo },
|
||||
} = useUserAndSpaces();
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState(initialValue?.value);
|
||||
const [selectedValue, setSelectedValue] = useState(
|
||||
initialValue?.value || ''
|
||||
);
|
||||
const [focus, setFocus] = useState(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from '@toeverything/components/ui';
|
||||
import { HighLightIconInput } from './IconInput';
|
||||
import { PendantConfig, IconNames, OptionIdType, OptionType } from '../types';
|
||||
import { genBasicOption } from '../utils';
|
||||
import { generateBasicOption } from '../utils';
|
||||
|
||||
type OptionItemType = {
|
||||
option: OptionType;
|
||||
@@ -66,7 +66,7 @@ export const BasicSelect = ({
|
||||
const [selectIds, setSelectIds] = useState<OptionIdType[]>(initialValue);
|
||||
|
||||
const insertOption = (insertId: OptionIdType) => {
|
||||
const newOption = genBasicOption({
|
||||
const newOption = generateBasicOption({
|
||||
index: options.length + 1,
|
||||
iconConfig,
|
||||
});
|
||||
|
||||
+8
-9
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Input, Option, Select, Tooltip } from '@toeverything/components/ui';
|
||||
import { HelpCenterIcon } from '@toeverything/components/icons';
|
||||
import { AsyncBlock } from '../../editor';
|
||||
@@ -15,13 +14,13 @@ import {
|
||||
StyledPopoverSubTitle,
|
||||
StyledPopoverWrapper,
|
||||
} from '../StyledComponent';
|
||||
import { genInitialOptions, getPendantConfigByType } from '../utils';
|
||||
import {
|
||||
generateRandomFieldName,
|
||||
generateInitialOptions,
|
||||
getPendantConfigByType,
|
||||
} from '../utils';
|
||||
import { useOnCreateSure } from './hooks';
|
||||
|
||||
const upperFirst = (str: string) => {
|
||||
return `${str[0].toUpperCase()}${str.slice(1)}`;
|
||||
};
|
||||
|
||||
export const CreatePendantPanel = ({
|
||||
block,
|
||||
onSure,
|
||||
@@ -35,7 +34,7 @@ export const CreatePendantPanel = ({
|
||||
|
||||
useEffect(() => {
|
||||
selectedOption &&
|
||||
setFieldName(upperFirst(`${selectedOption.type}#${nanoid(4)}`));
|
||||
setFieldName(generateRandomFieldName(selectedOption.type));
|
||||
}, [selectedOption]);
|
||||
|
||||
return (
|
||||
@@ -45,7 +44,7 @@ export const CreatePendantPanel = ({
|
||||
<Select
|
||||
width={284}
|
||||
placeholder="Search for a field type"
|
||||
value={selectedOption}
|
||||
value={selectedOption ?? null}
|
||||
onChange={(selectedValue: PendantOptions) => {
|
||||
setSelectedOption(selectedValue);
|
||||
}}
|
||||
@@ -93,7 +92,7 @@ export const CreatePendantPanel = ({
|
||||
<PendantModifyPanel
|
||||
type={selectedOption.type}
|
||||
// Select, MultiSelect, Status use this props as initial property
|
||||
initialOptions={genInitialOptions(
|
||||
initialOptions={generateInitialOptions(
|
||||
selectedOption.type,
|
||||
getPendantConfigByType(selectedOption.type)
|
||||
)}
|
||||
|
||||
+4
-3
@@ -4,11 +4,11 @@ import { HelpCenterIcon } from '@toeverything/components/icons';
|
||||
import { PendantModifyPanel } from '../pendant-modify-panel';
|
||||
import type { AsyncBlock } from '../../editor';
|
||||
import {
|
||||
getRecastItemValue,
|
||||
type RecastBlockValue,
|
||||
type RecastMetaProperty,
|
||||
} from '../../recast-block';
|
||||
import { getPendantConfigByType } from '../utils';
|
||||
import { usePendant } from '../use-pendant';
|
||||
import {
|
||||
StyledPopoverWrapper,
|
||||
StyledOperationLabel,
|
||||
@@ -42,7 +42,8 @@ export const UpdatePendantPanel = ({
|
||||
}: Props) => {
|
||||
const pendantOption = pendantOptions.find(v => v.type === property.type);
|
||||
const iconConfig = getPendantConfigByType(property.type);
|
||||
const { removePendant } = usePendant(block);
|
||||
const { removeValue } = getRecastItemValue(block);
|
||||
|
||||
const Icon = IconMap[iconConfig.iconName];
|
||||
const [fieldName, setFieldName] = useState(property.name);
|
||||
const onUpdateSure = useOnUpdateSure({ block, property });
|
||||
@@ -108,7 +109,7 @@ export const UpdatePendantPanel = ({
|
||||
onDelete={
|
||||
hasDelete
|
||||
? async () => {
|
||||
await removePendant(property);
|
||||
await removeValue(property.id);
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import {
|
||||
genSelectOptionId,
|
||||
getRecastItemValue,
|
||||
type InformationProperty,
|
||||
type MultiSelectProperty,
|
||||
type RecastMetaProperty,
|
||||
type SelectOption,
|
||||
type SelectProperty,
|
||||
useRecastBlock,
|
||||
useRecastBlockMeta,
|
||||
useSelectProperty,
|
||||
SelectValue,
|
||||
MultiSelectValue,
|
||||
StatusValue,
|
||||
InformationValue,
|
||||
TextValue,
|
||||
DateValue,
|
||||
} from '../../recast-block';
|
||||
import { type AsyncBlock } from '../../editor';
|
||||
import { usePendant } from '../use-pendant';
|
||||
import {
|
||||
type OptionType,
|
||||
PendantTypes,
|
||||
@@ -41,8 +48,8 @@ const genOptionWithId = (options: OptionType[] = []) => {
|
||||
export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
const { addProperty } = useRecastBlockMeta();
|
||||
const { createSelect } = useSelectProperty();
|
||||
const { setPendant } = usePendant(block);
|
||||
|
||||
const recastBlock = useRecastBlock();
|
||||
const { setValue } = getRecastItemValue(block);
|
||||
return async ({
|
||||
type,
|
||||
fieldName,
|
||||
@@ -79,7 +86,14 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
tempSelectedId: newValue,
|
||||
});
|
||||
|
||||
await setPendant(newProperty, selectedId);
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: selectedId,
|
||||
} as SelectValue | MultiSelectValue | StatusValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else if (type === PendantTypes.Information) {
|
||||
const emailOptions = genOptionWithId(newPropertyItem.emailOptions);
|
||||
|
||||
@@ -97,26 +111,33 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
locationOptions,
|
||||
} as Omit<InformationProperty, 'id'>);
|
||||
|
||||
await setPendant(newProperty, {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions,
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions,
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions,
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
});
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions,
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions,
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions,
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
},
|
||||
} as InformationValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else {
|
||||
// TODO: Color and background should use pendant config, but ui is not design now
|
||||
const iconConfig = getPendantConfigByType(type);
|
||||
@@ -129,8 +150,14 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
color: iconConfig.color as CSSProperties['color'],
|
||||
iconName: iconConfig.iconName,
|
||||
});
|
||||
|
||||
await setPendant(newProperty, newValue);
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: newValue,
|
||||
} as TextValue | DateValue,
|
||||
recastBlock.id
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -144,8 +171,9 @@ export const useOnUpdateSure = ({
|
||||
property: RecastMetaProperty;
|
||||
}) => {
|
||||
const { updateSelect } = useSelectProperty();
|
||||
const { setPendant } = usePendant(block);
|
||||
const { updateProperty } = useRecastBlockMeta();
|
||||
const { setValue } = getRecastItemValue(block);
|
||||
const recastBlock = useRecastBlock();
|
||||
|
||||
return async ({
|
||||
type,
|
||||
@@ -199,7 +227,14 @@ export const useOnUpdateSure = ({
|
||||
tempSelectedId: newValue,
|
||||
});
|
||||
|
||||
await setPendant(selectProperty, selectedId);
|
||||
await setValue(
|
||||
{
|
||||
id: selectProperty.id,
|
||||
type: selectProperty.type,
|
||||
value: selectedId,
|
||||
} as SelectValue | MultiSelectValue | StatusValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else if (type === PendantTypes.Information) {
|
||||
// const { emailOptions, phoneOptions, locationOptions } =
|
||||
// property as InformationProperty;
|
||||
@@ -231,28 +266,42 @@ export const useOnUpdateSure = ({
|
||||
locationOptions,
|
||||
} as InformationProperty);
|
||||
|
||||
await setPendant(newProperty, {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
});
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
},
|
||||
} as InformationValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else {
|
||||
await setPendant(property, newValue);
|
||||
await setValue(
|
||||
{
|
||||
id: property.id,
|
||||
type: property.type,
|
||||
value: newValue,
|
||||
} as TextValue | DateValue,
|
||||
recastBlock.id
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldName !== property.name) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useRef } from 'react';
|
||||
import { FC, useRef } from 'react';
|
||||
import { AsyncBlock } from '../../editor';
|
||||
import { PendantHistoryPanel } from '../pendant-history-panel';
|
||||
import {
|
||||
@@ -21,8 +21,6 @@ export const PendantPopover: FC<
|
||||
pointerEnterDelay={300}
|
||||
pointerLeaveDelay={200}
|
||||
placement="bottom-start"
|
||||
// visible={true}
|
||||
// trigger="click"
|
||||
content={
|
||||
<PendantHistoryPanel
|
||||
block={block}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
MuiZoom,
|
||||
MuiFade,
|
||||
Popover,
|
||||
PopperHandler,
|
||||
styled,
|
||||
@@ -100,16 +100,15 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
|
||||
);
|
||||
})}
|
||||
{hasAddBtn ? (
|
||||
<MuiZoom in={showAddBtn}>
|
||||
<MuiFade in={showAddBtn}>
|
||||
<div>
|
||||
<AddPendantPopover
|
||||
block={block}
|
||||
iconStyle={{ marginTop: 4 }}
|
||||
container={blockRenderContainerRef.current}
|
||||
trigger="click"
|
||||
/>
|
||||
</div>
|
||||
</MuiZoom>
|
||||
</MuiFade>
|
||||
) : null}
|
||||
</BlockPendantContainer>
|
||||
);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { removePropertyValueRecord, setPendantHistory } from './utils';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import {
|
||||
getRecastItemValue,
|
||||
RecastMetaProperty,
|
||||
useRecastBlock,
|
||||
} from '../recast-block';
|
||||
|
||||
export const usePendant = (block: AsyncBlock) => {
|
||||
// const { getProperties, removeProperty } = useRecastBlockMeta();
|
||||
const recastBlock = useRecastBlock();
|
||||
const { getValue, setValue, removeValue } = getRecastItemValue(block);
|
||||
// const { updateSelect } = useSelectProperty();
|
||||
|
||||
const setPendant = async (property: RecastMetaProperty, newValue: any) => {
|
||||
const nv = {
|
||||
id: property.id,
|
||||
type: property.type,
|
||||
value: newValue,
|
||||
};
|
||||
await setValue(nv);
|
||||
setPendantHistory({
|
||||
recastBlockId: recastBlock.id,
|
||||
blockId: block.id,
|
||||
propertyId: property.id,
|
||||
});
|
||||
};
|
||||
|
||||
const removePendant = async (property: RecastMetaProperty) => {
|
||||
await removeValue(property.id);
|
||||
removePropertyValueRecord({
|
||||
recastBlockId: block.id,
|
||||
propertyId: property.id,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
setPendant,
|
||||
removePendant,
|
||||
};
|
||||
};
|
||||
@@ -1,84 +1,7 @@
|
||||
import {
|
||||
PropertyType,
|
||||
RecastBlockValue,
|
||||
RecastPropertyId,
|
||||
SelectOption,
|
||||
} from '../recast-block';
|
||||
import { OptionIdType, OptionType } from './types';
|
||||
import { PropertyType, SelectOption } from '../recast-block';
|
||||
import { OptionIdType, OptionType, PendantConfig, PendantTypes } from './types';
|
||||
import { pendantConfig } from './config';
|
||||
import { PendantConfig, PendantTypes } from './types';
|
||||
type Props = {
|
||||
recastBlockId: string;
|
||||
blockId: string;
|
||||
propertyId: RecastPropertyId;
|
||||
};
|
||||
|
||||
type StorageMap = {
|
||||
[recastBlockId: string]: {
|
||||
[propertyId: RecastPropertyId]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_NAME = 'TEMPORARY_PENDANT_DATA';
|
||||
|
||||
const ensureLocalStorage = () => {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_NAME);
|
||||
if (!data) {
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify({}));
|
||||
}
|
||||
};
|
||||
|
||||
export const setPendantHistory = ({
|
||||
recastBlockId,
|
||||
blockId,
|
||||
propertyId,
|
||||
}: Props) => {
|
||||
ensureLocalStorage();
|
||||
const data: StorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
|
||||
if (!data[recastBlockId]) {
|
||||
data[recastBlockId] = {};
|
||||
}
|
||||
const propertyValueRecord = data[recastBlockId];
|
||||
propertyValueRecord[propertyId] = blockId;
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const getPendantHistory = ({
|
||||
recastBlockId,
|
||||
}: {
|
||||
recastBlockId: string;
|
||||
}) => {
|
||||
ensureLocalStorage();
|
||||
const data: StorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
|
||||
return data[recastBlockId] ?? {};
|
||||
};
|
||||
|
||||
export const removePropertyValueRecord = ({
|
||||
recastBlockId,
|
||||
propertyId,
|
||||
}: {
|
||||
recastBlockId: string;
|
||||
propertyId: RecastPropertyId;
|
||||
}) => {
|
||||
ensureLocalStorage();
|
||||
const data: StorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
if (!data[recastBlockId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete data[recastBlockId][propertyId];
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
/**
|
||||
* In select pendant panel, use mock options instead of use `createSelect` when add or delete option
|
||||
@@ -107,7 +30,7 @@ export const getOfficialSelected = ({
|
||||
.map(id => {
|
||||
return tempOptions.findIndex((o: OptionType) => o.id === id);
|
||||
})
|
||||
.filter(index => index != -1);
|
||||
.filter(index => index !== -1);
|
||||
selectedId = selectedIndex.map((index: number) => {
|
||||
return options[index].id;
|
||||
});
|
||||
@@ -130,7 +53,7 @@ export const getPendantIconsConfigByName = (
|
||||
return pendantConfig[pendantName];
|
||||
};
|
||||
|
||||
export const genBasicOption = ({
|
||||
export const generateBasicOption = ({
|
||||
index,
|
||||
iconConfig,
|
||||
name = '',
|
||||
@@ -159,22 +82,22 @@ export const genBasicOption = ({
|
||||
/**
|
||||
* Status Pendant is a Select Pendant built-in some options
|
||||
* **/
|
||||
export const genInitialOptions = (
|
||||
export const generateInitialOptions = (
|
||||
type: PendantTypes,
|
||||
iconConfig: PendantConfig
|
||||
) => {
|
||||
if (type === PendantTypes.Status) {
|
||||
return [
|
||||
genBasicOption({ index: 0, iconConfig, name: 'No Started' }),
|
||||
genBasicOption({
|
||||
generateBasicOption({ index: 0, iconConfig, name: 'No Started' }),
|
||||
generateBasicOption({
|
||||
index: 1,
|
||||
iconConfig,
|
||||
name: 'In Progress',
|
||||
}),
|
||||
genBasicOption({ index: 2, iconConfig, name: 'Complete' }),
|
||||
generateBasicOption({ index: 2, iconConfig, name: 'Complete' }),
|
||||
];
|
||||
}
|
||||
return [genBasicOption({ index: 0, iconConfig })];
|
||||
return [generateBasicOption({ index: 0, iconConfig })];
|
||||
};
|
||||
|
||||
export const checkPendantForm = (
|
||||
@@ -222,3 +145,10 @@ export const checkPendantForm = (
|
||||
|
||||
return { passed: true, message: 'Check passed !' };
|
||||
};
|
||||
|
||||
const upperFirst = (str: string) => {
|
||||
return `${str[0].toUpperCase()}${str.slice(1)}`;
|
||||
};
|
||||
|
||||
export const generateRandomFieldName = (type: PendantTypes) =>
|
||||
upperFirst(`${type}#${nanoid(4)}`);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { AsyncBlock, BlockEditor } from '../editor';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface DragDropWrapperProps {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
children: ReactElement | null;
|
||||
}
|
||||
|
||||
export function DragDropWrapper({
|
||||
children,
|
||||
editor,
|
||||
block,
|
||||
}: DragDropWrapperProps) {
|
||||
const handlerDragOver: React.DragEventHandler<HTMLDivElement> = event => {
|
||||
event.preventDefault();
|
||||
if (block.dom) {
|
||||
editor.getHooks().afterOnNodeDragOver(event, {
|
||||
blockId: block.id,
|
||||
dom: block.dom,
|
||||
rect: block.dom?.getBoundingClientRect(),
|
||||
type: block.type,
|
||||
properties: block.getProperties(),
|
||||
});
|
||||
}
|
||||
};
|
||||
return <div onDragOver={handlerDragOver}>{children}</div>;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './DragDropWrapper';
|
||||
@@ -12,6 +12,7 @@ enum DragType {
|
||||
}
|
||||
|
||||
const DRAG_STATE_CHANGE_EVENT_KEY = 'dragStateChange';
|
||||
const MAX_GRID_BLOCK_FLOOR = 3;
|
||||
export class DragDropManager {
|
||||
private _editor: Editor;
|
||||
private _enabled: boolean;
|
||||
@@ -231,6 +232,17 @@ export class DragDropManager {
|
||||
if (!(await this._canBeDrop(event))) {
|
||||
direction = BlockDropPlacement.none;
|
||||
}
|
||||
if (
|
||||
direction === BlockDropPlacement.left ||
|
||||
direction === BlockDropPlacement.right
|
||||
) {
|
||||
const path = await this._editor.getBlockPath(blockId);
|
||||
const gridBlocks = path.filter(block => block.type === 'grid');
|
||||
// limit grid block floor counts
|
||||
if (gridBlocks.length >= MAX_GRID_BLOCK_FLOOR) {
|
||||
direction = BlockDropPlacement.none;
|
||||
}
|
||||
}
|
||||
this._setBlockDragDirection(direction);
|
||||
return direction;
|
||||
}
|
||||
|
||||
@@ -340,7 +340,20 @@ export class Editor implements Virgo {
|
||||
const rootBlockId = this.getRootBlockId();
|
||||
const rootBlock = await this.getBlockById(rootBlockId);
|
||||
const blockList: Array<AsyncBlock> = rootBlock ? [rootBlock] : [];
|
||||
const children = (await rootBlock?.children()) || [];
|
||||
return [...blockList, ...(await this.getOffspring(rootBlockId))];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* get all offspring of block
|
||||
* @param {string} id
|
||||
* @return {*}
|
||||
* @memberof Editor
|
||||
*/
|
||||
async getOffspring(id: string) {
|
||||
const block = await this.getBlockById(id);
|
||||
const blockList: Array<AsyncBlock> = [];
|
||||
const children = (await block?.children()) || [];
|
||||
for (const block of children) {
|
||||
if (!block) {
|
||||
continue;
|
||||
@@ -354,15 +367,6 @@ export class Editor implements Virgo {
|
||||
return blockList;
|
||||
}
|
||||
|
||||
async getRootLastChildrenBlock(rootBlockId = this.getRootBlockId()) {
|
||||
const rootBlock = await this.getBlockById(rootBlockId);
|
||||
if (!rootBlock) {
|
||||
throw new Error('root block is not found');
|
||||
}
|
||||
const lastChildren = await rootBlock.lastChild();
|
||||
return lastChildren ?? rootBlock;
|
||||
}
|
||||
|
||||
async getLastBlock(rootBlockId = this.getRootBlockId()) {
|
||||
const rootBlock = await this.getBlockById(rootBlockId);
|
||||
if (!rootBlock) {
|
||||
@@ -379,6 +383,20 @@ export class Editor implements Virgo {
|
||||
return lastBlock;
|
||||
}
|
||||
|
||||
async getBlockPath(id: string) {
|
||||
const block = await this.getBlockById(id);
|
||||
if (!block) {
|
||||
return [];
|
||||
}
|
||||
const path = [block];
|
||||
let parent = await block.parent();
|
||||
while (parent) {
|
||||
path.unshift(parent);
|
||||
parent = await parent.parent();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
async getBlockByPoint(point: Point) {
|
||||
const blockList = await this.getBlockList();
|
||||
|
||||
|
||||
@@ -35,20 +35,6 @@ export class KeyboardManager {
|
||||
}
|
||||
this.handler_map = {};
|
||||
|
||||
// WARNING: Remove the filter of hotkeys, the input event of input/select/textarea will be filtered out by default
|
||||
// When there is a problem with the input of the text component, you need to pay attention to this
|
||||
const old_filter = HotKeys.filter;
|
||||
HotKeys.filter = event => {
|
||||
let parent = (event.target as Element).parentElement;
|
||||
while (parent) {
|
||||
if (parent === editor.container) {
|
||||
return old_filter(event);
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
HotKeys.setScope('editor');
|
||||
|
||||
// this.init_common_shortcut_cb();
|
||||
|
||||
@@ -113,13 +113,6 @@ export class Hooks implements HooksRunner, PluginHooks {
|
||||
this._runHook(HookType.ON_ROOTNODE_DRAG_OVER_CAPTURE, e);
|
||||
}
|
||||
|
||||
public afterOnNodeDragOver(
|
||||
e: React.DragEvent<Element>,
|
||||
node: BlockDomInfo
|
||||
): void {
|
||||
this._runHook(HookType.AFTER_ON_NODE_DRAG_OVER, e, node);
|
||||
}
|
||||
|
||||
public onSearch(): void {
|
||||
this._runHook(HookType.ON_SEARCH);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export class ScrollManager {
|
||||
|
||||
constructor(editor: BlockEditor) {
|
||||
this._editor = editor;
|
||||
(window as any).scrollManager = this;
|
||||
}
|
||||
|
||||
private _updateScrollInfo(left: number, top: number) {
|
||||
@@ -111,6 +110,7 @@ export class ScrollManager {
|
||||
}
|
||||
|
||||
public emitScrollEvent(event: UIEvent) {
|
||||
this.scrollContainer = event.target as HTMLElement;
|
||||
this._scrollDirection = this._getScrollDirection();
|
||||
this._scrollMoveOffset = Math.abs(
|
||||
this.scrollContainer.scrollTop - this._scrollRecord[0]
|
||||
|
||||
@@ -177,7 +177,6 @@ export enum HookType {
|
||||
ON_ROOTNODE_DRAG_END = 'onRootNodeDragEnd',
|
||||
ON_ROOTNODE_DRAG_OVER_CAPTURE = 'onRootNodeDragOverCapture',
|
||||
ON_ROOTNODE_DROP = 'onRootNodeDrop',
|
||||
AFTER_ON_NODE_DRAG_OVER = 'afterOnNodeDragOver',
|
||||
BEFORE_COPY = 'beforeCopy',
|
||||
BEFORE_CUT = 'beforeCut',
|
||||
ON_ROOTNODE_SCROLL = 'onRootNodeScroll',
|
||||
@@ -219,10 +218,6 @@ export interface HooksRunner {
|
||||
onRootNodeDragEnd: (e: React.DragEvent<Element>) => void;
|
||||
onRootNodeDragLeave: (e: React.DragEvent<Element>) => void;
|
||||
onRootNodeDrop: (e: React.DragEvent<Element>) => void;
|
||||
afterOnNodeDragOver: (
|
||||
e: React.DragEvent<Element>,
|
||||
node: BlockDomInfo
|
||||
) => void;
|
||||
beforeCopy: (e: ClipboardEvent) => void;
|
||||
beforeCut: (e: ClipboardEvent) => void;
|
||||
onRootNodeScroll: (e: React.UIEvent) => void;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { noop, Point } from '@toeverything/utils';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEditor } from './Contexts';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockEditor,
|
||||
@@ -5,9 +8,6 @@ import {
|
||||
SelectionInfo,
|
||||
SelectionSettingsMap,
|
||||
} from './editor';
|
||||
import { noop, Point } from '@toeverything/utils';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { RootContext } from './contexts';
|
||||
|
||||
function useRequestReRender() {
|
||||
const [, setUpdateCounter] = useState(0);
|
||||
@@ -56,7 +56,7 @@ function useRequestReRender() {
|
||||
export const useBlock = (blockId: string) => {
|
||||
const [block, setBlock] = useState<AsyncBlock>();
|
||||
const requestReRender = useRequestReRender();
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
if (!blockId) {
|
||||
return undefined;
|
||||
@@ -95,7 +95,7 @@ export const useOnSelect = (
|
||||
blockId: string,
|
||||
cb: (isSelect: boolean) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.observe(blockId, SelectEventTypes.onSelect, cb);
|
||||
return () => {
|
||||
@@ -117,7 +117,7 @@ export const useOnSelectActive = (
|
||||
blockId: string,
|
||||
cb: (position: Point | undefined) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.observe(blockId, SelectEventTypes.active, cb);
|
||||
return () => {
|
||||
@@ -139,7 +139,7 @@ export const useOnSelectSetSelection = <T extends keyof SelectionSettingsMap>(
|
||||
blockId: string,
|
||||
cb: (args: SelectionSettingsMap[T]) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.observe(
|
||||
blockId,
|
||||
@@ -162,7 +162,7 @@ export const useOnSelectSetSelection = <T extends keyof SelectionSettingsMap>(
|
||||
* @export
|
||||
*/
|
||||
export const useOnSelectChange = (cb: (info: SelectionInfo) => void) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.onSelectionChange(cb);
|
||||
return () => {
|
||||
@@ -177,7 +177,7 @@ export const useOnSelectChange = (cb: (info: SelectionInfo) => void) => {
|
||||
* @export
|
||||
*/
|
||||
export const useOnSelectEnd = (cb: (info: SelectionInfo) => void) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.onSelectEnd(cb);
|
||||
return () => {
|
||||
@@ -195,7 +195,7 @@ export const useOnSelectStartWith = (
|
||||
blockId: string,
|
||||
cb: (args: MouseEvent) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.mouseManager.onSelectStartWith(blockId, cb);
|
||||
return () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { ColumnsContext, RootContext } from './contexts';
|
||||
export { RenderRoot, MIN_PAGE_WIDTH } from './RenderRoot';
|
||||
export * from './render-block';
|
||||
export * from './hooks';
|
||||
@@ -15,7 +14,6 @@ export * from './kanban/types';
|
||||
|
||||
export * from './utils';
|
||||
|
||||
export * from './drag-drop-wrapper';
|
||||
export * from './block-content-wrapper';
|
||||
|
||||
export * from './editor';
|
||||
|
||||
export { RefPageProvider, useRefPage } from './ref-page';
|
||||
|
||||
@@ -6,10 +6,15 @@ import {
|
||||
PropertyType,
|
||||
RecastBlockValue,
|
||||
RecastMetaProperty,
|
||||
RecastPropertyId,
|
||||
} from '../recast-block/types';
|
||||
import type { DefaultGroup, KanbanGroup } from './types';
|
||||
import { DEFAULT_GROUP_ID } from './types';
|
||||
import {
|
||||
generateInitialOptions,
|
||||
generateRandomFieldName,
|
||||
getPendantIconsConfigByName,
|
||||
} from '../block-pendant/utils';
|
||||
import { SelectOption } from '../recast-block';
|
||||
|
||||
/**
|
||||
* - If the `groupBy` is `SelectProperty` or `MultiSelectProperty`, return `(Multi)SelectProperty.options`.
|
||||
@@ -23,6 +28,7 @@ export const getGroupOptions = async (
|
||||
return [];
|
||||
}
|
||||
switch (groupBy.type) {
|
||||
case PropertyType.Status:
|
||||
case PropertyType.Select:
|
||||
case PropertyType.MultiSelect: {
|
||||
return groupBy.options.map(option => ({
|
||||
@@ -51,15 +57,13 @@ const isValueBelongOption = (
|
||||
option: KanbanGroup
|
||||
) => {
|
||||
switch (propertyValue.type) {
|
||||
case PropertyType.Select: {
|
||||
case PropertyType.Select:
|
||||
case PropertyType.Status: {
|
||||
return propertyValue.value === option.id;
|
||||
}
|
||||
case PropertyType.MultiSelect: {
|
||||
return propertyValue.value.some(i => i === option.id);
|
||||
}
|
||||
// case PropertyType.Text: {
|
||||
// TOTODO:DO support this type
|
||||
// }
|
||||
default: {
|
||||
console.error(propertyValue, option);
|
||||
throw new Error('Not support group by type');
|
||||
@@ -96,40 +100,67 @@ export const calcCardGroup = (
|
||||
/**
|
||||
* Set group value for the card block
|
||||
*/
|
||||
export const moveCardToGroup = async (
|
||||
groupById: RecastPropertyId,
|
||||
cardBlock: RecastItem,
|
||||
group: KanbanGroup
|
||||
) => {
|
||||
export const moveCardToGroup = async ({
|
||||
groupBy,
|
||||
cardBlock,
|
||||
group,
|
||||
recastBlock,
|
||||
}: {
|
||||
groupBy: RecastMetaProperty;
|
||||
cardBlock: RecastItem;
|
||||
group: KanbanGroup;
|
||||
recastBlock: RecastBlock;
|
||||
}) => {
|
||||
const { setValue, removeValue } = getRecastItemValue(cardBlock);
|
||||
let success = false;
|
||||
if (group.id === DEFAULT_GROUP_ID) {
|
||||
success = await removeValue(groupById);
|
||||
success = await removeValue(groupBy.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (group.type) {
|
||||
case PropertyType.Select: {
|
||||
success = await setValue({
|
||||
id: groupById,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
});
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PropertyType.Status: {
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PropertyType.MultiSelect: {
|
||||
success = await setValue({
|
||||
id: groupById,
|
||||
type: group.type,
|
||||
value: [group.id],
|
||||
});
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: [group.id],
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PropertyType.Text: {
|
||||
success = await setValue({
|
||||
id: groupById,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
});
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -194,14 +225,18 @@ export const genDefaultGroup = (groupBy: RecastMetaProperty): DefaultGroup => ({
|
||||
items: [],
|
||||
});
|
||||
|
||||
export const DEFAULT_GROUP_BY_PROPERTY = {
|
||||
name: 'Status',
|
||||
options: [
|
||||
{ name: 'No Started', color: '#E53535', background: '#FFCECE' },
|
||||
{ name: 'In Progress', color: '#A77F1A', background: '#FFF5AB' },
|
||||
{ name: 'Complete', color: '#3C8867', background: '#C5FBE0' },
|
||||
],
|
||||
};
|
||||
export const generateDefaultGroupByProperty = (): {
|
||||
name: string;
|
||||
options: Omit<SelectOption, 'id'>[];
|
||||
type: PropertyType.Status;
|
||||
} => ({
|
||||
name: generateRandomFieldName(PropertyType.Status),
|
||||
type: PropertyType.Status,
|
||||
options: generateInitialOptions(
|
||||
PropertyType.Status,
|
||||
getPendantIconsConfigByName(PropertyType.Status)
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Unwrap blocks from the grid recursively.
|
||||
|
||||
@@ -7,6 +7,7 @@ export const useKanbanGroup = (groupBy: RecastMetaProperty) => {
|
||||
const { updateSelect } = useSelectProperty();
|
||||
|
||||
switch (groupBy.type) {
|
||||
case PropertyType.Status:
|
||||
case PropertyType.MultiSelect:
|
||||
case PropertyType.Select: {
|
||||
const {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useEditor } from '../contexts';
|
||||
import { useEditor } from '../Contexts';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { useRecastView } from '../recast-block';
|
||||
import { useRecastBlock } from '../recast-block/Context';
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
import { supportChildren } from '../utils';
|
||||
import {
|
||||
calcCardGroup,
|
||||
DEFAULT_GROUP_BY_PROPERTY,
|
||||
genDefaultGroup,
|
||||
generateDefaultGroupByProperty,
|
||||
getCardGroup,
|
||||
getGroupOptions,
|
||||
moveCardToAfter,
|
||||
@@ -48,6 +48,7 @@ export const useRecastKanbanGroupBy = () => {
|
||||
// Add other type groupBy support
|
||||
const supportedGroupBy = getProperties().filter(
|
||||
prop =>
|
||||
prop.type === PropertyType.Status ||
|
||||
prop.type === PropertyType.Select ||
|
||||
prop.type === PropertyType.MultiSelect
|
||||
);
|
||||
@@ -88,7 +89,8 @@ export const useRecastKanbanGroupBy = () => {
|
||||
// TODO: support other property type
|
||||
if (
|
||||
groupByProperty.type !== PropertyType.Select &&
|
||||
groupByProperty.type !== PropertyType.MultiSelect
|
||||
groupByProperty.type !== PropertyType.MultiSelect &&
|
||||
groupByProperty.type !== PropertyType.Status
|
||||
) {
|
||||
console.warn('Not support groupBy type', groupByProperty);
|
||||
|
||||
@@ -134,7 +136,7 @@ export const useInitKanbanEffect = ():
|
||||
}
|
||||
// 3. no group by, no properties
|
||||
// create a new property and set it as group by
|
||||
const prop = await createSelect(DEFAULT_GROUP_BY_PROPERTY);
|
||||
const prop = await createSelect(generateDefaultGroupByProperty());
|
||||
await setGroupBy(prop.id);
|
||||
};
|
||||
|
||||
@@ -197,7 +199,12 @@ export const useRecastKanban = () => {
|
||||
beforeBlock: string | null,
|
||||
afterBlock: string | null
|
||||
) => {
|
||||
await moveCardToGroup(groupBy.id, child, kanbanMap[id]);
|
||||
await moveCardToGroup({
|
||||
groupBy,
|
||||
cardBlock: child,
|
||||
group: kanbanMap[id],
|
||||
recastBlock,
|
||||
});
|
||||
if (beforeBlock) {
|
||||
const block = await editor.getBlockById(
|
||||
beforeBlock
|
||||
@@ -286,7 +293,12 @@ export const useKanban = () => {
|
||||
);
|
||||
if (isChangedGroup) {
|
||||
// 1.2 Move to the target group
|
||||
await moveCardToGroup(groupBy.id, targetCard, targetGroup);
|
||||
await moveCardToGroup({
|
||||
groupBy,
|
||||
cardBlock: targetCard,
|
||||
group: targetGroup,
|
||||
recastBlock,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Reorder the card
|
||||
@@ -324,7 +336,12 @@ export const useKanban = () => {
|
||||
}
|
||||
recastBlock.append(newBlock);
|
||||
const newCard = newBlock as unknown as RecastItem;
|
||||
await moveCardToGroup(groupBy.id, newCard, group);
|
||||
await moveCardToGroup({
|
||||
groupBy,
|
||||
cardBlock: newCard,
|
||||
group,
|
||||
recastBlock,
|
||||
});
|
||||
},
|
||||
[editor, groupBy.id, recastBlock]
|
||||
);
|
||||
|
||||
@@ -46,7 +46,10 @@ export type DefaultGroup = KanbanGroupBase & {
|
||||
|
||||
type SelectGroup = KanbanGroupBase &
|
||||
SelectOption & {
|
||||
type: PropertyType.Select | PropertyType.MultiSelect;
|
||||
type:
|
||||
| PropertyType.Select
|
||||
| PropertyType.MultiSelect
|
||||
| PropertyType.Status;
|
||||
};
|
||||
|
||||
type TextGroup = KanbanGroupBase & {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -47,7 +48,7 @@ export const RecastBlockProvider = ({
|
||||
|
||||
return (
|
||||
<RecastBlockContext.Provider value={block}>
|
||||
{children}
|
||||
<RefPageProvider>{children}</RefPageProvider>
|
||||
</RecastBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -60,7 +61,7 @@ export const useRecastBlock = () => {
|
||||
const recastBlock = useContext(RecastBlockContext);
|
||||
if (!recastBlock) {
|
||||
throw new Error(
|
||||
'Failed to find recastBlock! Please use the hook under `RecastTableProvider`.'
|
||||
'Failed to find recastBlock! Please use the hook under `RecastBlockProvider`.'
|
||||
);
|
||||
}
|
||||
return recastBlock;
|
||||
|
||||
@@ -49,22 +49,3 @@ const SomeBlock = () => {
|
||||
return <div>...</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Scene
|
||||
|
||||
**Notice: The scene API will refactor at next version.**
|
||||
|
||||
```tsx
|
||||
const SomeBlock = () => {
|
||||
const { scene, setScene, setPage, setTable, setKanban } =
|
||||
useRecastBlockScene();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>Scene: {scene}</div>
|
||||
<button onClick={setPage}>list</button>
|
||||
<button onClick={setKanban}>kanban</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -32,7 +32,7 @@ export const mergeGroup = async (...groups: AsyncBlock[]) => {
|
||||
);
|
||||
}
|
||||
|
||||
await mergeGroupProperties(...(groups as RecastBlock[]));
|
||||
await mergeGroupProperties(...(groups as unknown as RecastBlock[]));
|
||||
|
||||
const [headGroup, ...restGroups] = groups;
|
||||
// Add all children to the head group
|
||||
@@ -174,7 +174,7 @@ export const splitGroup = async (
|
||||
}
|
||||
|
||||
splitGroupProperties(
|
||||
group as RecastBlock,
|
||||
group as unknown as RecastBlock,
|
||||
newGroupBlock as unknown as RecastBlock
|
||||
);
|
||||
await group.after(newGroupBlock);
|
||||
@@ -185,6 +185,22 @@ export const splitGroup = async (
|
||||
return newGroupBlock;
|
||||
};
|
||||
|
||||
export const appendNewGroup = async (
|
||||
editor: BlockEditor,
|
||||
parentBlock: AsyncBlock,
|
||||
active = false
|
||||
) => {
|
||||
const newGroupBlock = await createGroupWithEmptyText(editor);
|
||||
await parentBlock.append(newGroupBlock);
|
||||
if (active) {
|
||||
// Active text block
|
||||
await editor.selectionManager.activeNodeByNodeId(
|
||||
newGroupBlock.childrenIds[0]
|
||||
);
|
||||
}
|
||||
return newGroupBlock;
|
||||
};
|
||||
|
||||
export const addNewGroup = async (
|
||||
editor: BlockEditor,
|
||||
previousBlock: AsyncBlock,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { RecastPropertyId } from './types';
|
||||
|
||||
// TODO: The logic for keeping history should be supported by the network layer
|
||||
type Props = {
|
||||
recastBlockId: string;
|
||||
blockId: string;
|
||||
propertyId: RecastPropertyId;
|
||||
};
|
||||
|
||||
type HistoryStorageMap = {
|
||||
[recastBlockId: string]: {
|
||||
[propertyId: RecastPropertyId]: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_NAME = 'TEMPORARY_HISTORY_DATA';
|
||||
|
||||
const ensureLocalStorage = () => {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_NAME);
|
||||
if (!data) {
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify({}));
|
||||
}
|
||||
};
|
||||
const ensureHistoryAtom = (
|
||||
data: HistoryStorageMap,
|
||||
recastBlockId: string,
|
||||
propertyId: RecastPropertyId
|
||||
): HistoryStorageMap => {
|
||||
if (!data[recastBlockId]) {
|
||||
data[recastBlockId] = {};
|
||||
}
|
||||
if (!data[recastBlockId][propertyId]) {
|
||||
data[recastBlockId][propertyId] = [];
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const setHistory = ({ recastBlockId, blockId, propertyId }: Props) => {
|
||||
ensureLocalStorage();
|
||||
const data: HistoryStorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
ensureHistoryAtom(data, recastBlockId, propertyId);
|
||||
const propertyHistory = data[recastBlockId][propertyId];
|
||||
|
||||
if (propertyHistory.includes(blockId)) {
|
||||
const idIndex = propertyHistory.findIndex(id => id === blockId);
|
||||
propertyHistory.splice(idIndex, 1);
|
||||
}
|
||||
|
||||
propertyHistory.push(blockId);
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const getHistory = ({ recastBlockId }: { recastBlockId: string }) => {
|
||||
ensureLocalStorage();
|
||||
const data: HistoryStorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
|
||||
return data[recastBlockId] ?? {};
|
||||
};
|
||||
|
||||
export const removeHistory = ({
|
||||
recastBlockId,
|
||||
blockId,
|
||||
propertyId,
|
||||
}: Props) => {
|
||||
ensureLocalStorage();
|
||||
const data: HistoryStorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
ensureHistoryAtom(data, recastBlockId, propertyId);
|
||||
|
||||
const propertyHistory = data[recastBlockId][propertyId];
|
||||
|
||||
if (propertyHistory.includes(blockId)) {
|
||||
const idIndex = propertyHistory.findIndex(id => id === blockId);
|
||||
propertyHistory.splice(idIndex, 1);
|
||||
}
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SelectProperty,
|
||||
TABLE_VALUES_KEY,
|
||||
} from './types';
|
||||
import { getHistory, removeHistory, setHistory } from './history';
|
||||
|
||||
/**
|
||||
* Generate a unique id for a property
|
||||
@@ -240,7 +241,13 @@ export const getRecastItemValue = (block: RecastItem | AsyncBlock) => {
|
||||
return props[id];
|
||||
};
|
||||
|
||||
const setValue = (newValue: RecastBlockValue) => {
|
||||
const setValue = (newValue: RecastBlockValue, recastBlockId: string) => {
|
||||
setHistory({
|
||||
recastBlockId: recastBlockId,
|
||||
blockId: block.id,
|
||||
propertyId: newValue.id,
|
||||
});
|
||||
|
||||
return recastItem.setProperty(TABLE_VALUES_KEY, {
|
||||
...props,
|
||||
[newValue.id]: newValue,
|
||||
@@ -249,22 +256,30 @@ export const getRecastItemValue = (block: RecastItem | AsyncBlock) => {
|
||||
|
||||
const removeValue = (propertyId: RecastPropertyId) => {
|
||||
const { [propertyId]: omitted, ...restProps } = props;
|
||||
|
||||
removeHistory({
|
||||
recastBlockId: block.id,
|
||||
propertyId: propertyId,
|
||||
blockId: block.id,
|
||||
});
|
||||
|
||||
return recastItem.setProperty(TABLE_VALUES_KEY, restProps);
|
||||
};
|
||||
return { getAllValue, getValue, setValue, removeValue };
|
||||
|
||||
const getValueHistory = getHistory;
|
||||
|
||||
return { getAllValue, getValue, setValue, removeValue, getValueHistory };
|
||||
};
|
||||
|
||||
const isSelectLikeProperty = (
|
||||
metaProperty?: RecastMetaProperty
|
||||
): metaProperty is SelectProperty | MultiSelectProperty => {
|
||||
if (
|
||||
!metaProperty ||
|
||||
(metaProperty.type !== PropertyType.Select &&
|
||||
metaProperty.type !== PropertyType.MultiSelect)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
): metaProperty is SelectProperty | MultiSelectProperty | StatusProperty => {
|
||||
return (
|
||||
metaProperty &&
|
||||
(metaProperty.type === PropertyType.Status ||
|
||||
metaProperty.type === PropertyType.Select ||
|
||||
metaProperty.type === PropertyType.MultiSelect)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -312,7 +327,7 @@ export const useSelectProperty = () => {
|
||||
};
|
||||
|
||||
const updateSelect = (
|
||||
selectProperty: SelectProperty | MultiSelectProperty
|
||||
selectProperty: StatusProperty | SelectProperty | MultiSelectProperty
|
||||
) => {
|
||||
// if (typeof selectProperty === 'string') {
|
||||
// const maybeSelectProperty = getProperty(selectProperty);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
import { MutableRefObject, useCallback, useEffect, useState } from 'react';
|
||||
import { useRecastBlock } from './Context';
|
||||
import {
|
||||
KanbanView,
|
||||
@@ -50,7 +50,33 @@ export const useCurrentView = () => {
|
||||
);
|
||||
return [currentView, setCurrentView] as const;
|
||||
};
|
||||
export const useLazyIframe = (
|
||||
link: string,
|
||||
timers: number,
|
||||
container: MutableRefObject<HTMLElement>
|
||||
) => {
|
||||
const [iframeShow, setIframeShow] = useState(false);
|
||||
useEffect(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = link;
|
||||
iframe.onload = () => {
|
||||
setTimeout(() => {
|
||||
// Prevent iframe from scrolling parent container
|
||||
// TODO W3C https://github.com/w3c/csswg-drafts/issues/7134
|
||||
// https://forum.figma.com/t/prevent-figmas-embed-code-from-automatically-scrolling-to-it-on-page-load/26029/6
|
||||
setIframeShow(true);
|
||||
}, timers);
|
||||
};
|
||||
if (container?.current) {
|
||||
container.current.appendChild(iframe);
|
||||
}
|
||||
return () => {
|
||||
iframe.remove();
|
||||
};
|
||||
}, [link, container]);
|
||||
|
||||
return iframeShow;
|
||||
};
|
||||
export const useRecastView = () => {
|
||||
const recastBlock = useRecastBlock();
|
||||
const recastViews =
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui';
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RenderBlock } from '../render-block';
|
||||
|
||||
const Dialog = styled('div')({
|
||||
flex: 1,
|
||||
width: '880px',
|
||||
margin: '72px auto',
|
||||
background: '#fff',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
borderRadius: '10px',
|
||||
padding: '72px 120px',
|
||||
overflow: 'scroll',
|
||||
});
|
||||
|
||||
const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
const { closeSubPage } = useRefPage();
|
||||
|
||||
return createPortal(
|
||||
<MuiBackdrop
|
||||
open={open}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(58, 76, 92, 0.4)',
|
||||
zIndex: theme.affine.zIndex.popover,
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
</MuiBackdrop>,
|
||||
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
const ModalPage = ({ blockId }: { blockId: string | null }) => {
|
||||
return (
|
||||
<Modal open={!!blockId}>
|
||||
{blockId && <RenderBlock blockId={blockId} />}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const RefPageContext = createContext<
|
||||
ReturnType<typeof useState<string | null>> | undefined
|
||||
>(undefined);
|
||||
|
||||
export const RefPageProvider = ({ children }: { children: ReactNode }) => {
|
||||
const state = useState<string | null>();
|
||||
const [blockId, setBlockId] = state;
|
||||
|
||||
return (
|
||||
<RefPageContext.Provider value={state}>
|
||||
{children}
|
||||
<ModalPage blockId={blockId ?? null} />
|
||||
</RefPageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useRefPage = () => {
|
||||
const context = useContext(RefPageContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'Wrap your app inside of a `SubPageProvider` to have access to the hook context!'
|
||||
);
|
||||
}
|
||||
const [blockId, setBlockId] = context;
|
||||
const openSubPage = (blockId: string) => {
|
||||
setBlockId(blockId);
|
||||
};
|
||||
const closeSubPage = () => {
|
||||
setBlockId(null);
|
||||
};
|
||||
|
||||
return { blockId, open: !!blockId, openSubPage, closeSubPage };
|
||||
};
|
||||
|
||||
// export const openSubPage = () => {};
|
||||
@@ -0,0 +1 @@
|
||||
export { useRefPage, RefPageProvider } from './ModalPage';
|
||||
@@ -1,8 +1,8 @@
|
||||
import { styled, Theme } from '@toeverything/components/ui';
|
||||
import { FC, useContext, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { FC, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
|
||||
// import { RenderChildren } from './RenderChildren';
|
||||
import { RootContext } from '../contexts';
|
||||
import { useEditor } from '../Contexts';
|
||||
import { useBlock } from '../hooks';
|
||||
|
||||
interface RenderBlockProps {
|
||||
@@ -14,7 +14,7 @@ export const RenderBlock: FC<RenderBlockProps> = ({
|
||||
blockId,
|
||||
hasContainer = true,
|
||||
}) => {
|
||||
const { editor, editorElement } = useContext(RootContext);
|
||||
const { editor, editorElement } = useEditor();
|
||||
const { block } = useBlock(blockId);
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ const StyledContainerForAddCommentContainer = styled('div')(({ theme }) => {
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
boxShadow: theme.affine.shadows.shadowSxDownLg,
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
backgroundColor: theme.affine.palette.white,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ const RootContainer = styled('div')(({ theme }) => {
|
||||
width: 352,
|
||||
maxHeight: 525,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
backgroundColor: '#fff',
|
||||
padding: '8px 4px',
|
||||
};
|
||||
|
||||
@@ -242,25 +242,29 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
|
||||
onKeyUpCapture={handleKeyup}
|
||||
ref={commandMenuContentRef}
|
||||
>
|
||||
<MuiClickAwayListener onClickAway={handleClickAway}>
|
||||
<div>
|
||||
<CommandMenuContainer
|
||||
editor={editor}
|
||||
hooks={hooks}
|
||||
style={{
|
||||
...commandMenuPosition,
|
||||
...style,
|
||||
}}
|
||||
isShow={show}
|
||||
blockId={blockId}
|
||||
onSelected={handleSelected}
|
||||
onclose={handleClose}
|
||||
searchBlocks={searchBlocks}
|
||||
types={types}
|
||||
categories={categories}
|
||||
/>
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
{show ? (
|
||||
<MuiClickAwayListener onClickAway={handleClickAway}>
|
||||
<div>
|
||||
<CommandMenuContainer
|
||||
editor={editor}
|
||||
hooks={hooks}
|
||||
style={{
|
||||
...commandMenuPosition,
|
||||
...style,
|
||||
}}
|
||||
isShow={show}
|
||||
blockId={blockId}
|
||||
onSelected={handleSelected}
|
||||
onclose={handleClose}
|
||||
searchBlocks={searchBlocks}
|
||||
types={types}
|
||||
categories={categories}
|
||||
/>
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HandleParentIcon } from '@toeverything/components/icons';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Point } from '@toeverything/utils';
|
||||
|
||||
export const ICON_WIDTH = 24;
|
||||
export const ICON_WIDTH = 16;
|
||||
|
||||
type DragItemProps = {
|
||||
isShow: boolean;
|
||||
@@ -30,17 +30,21 @@ export const DragItem = function ({
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDiv = styled('div')({
|
||||
const StyledDiv = styled('div')(({ theme }) => ({
|
||||
padding: '0',
|
||||
display: 'inlineFlex',
|
||||
display: 'inline-flex',
|
||||
width: `${ICON_WIDTH}px`,
|
||||
height: `${ICON_WIDTH}px`,
|
||||
height: '20px',
|
||||
cursor: 'grab',
|
||||
'& svg': {
|
||||
fontSize: '20px',
|
||||
marginLeft: '-2px',
|
||||
},
|
||||
':hover': {
|
||||
backgroundColor: '#edeef0',
|
||||
backgroundColor: '#F5F7F8',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
const StyledButton = styled('div')({
|
||||
padding: '0',
|
||||
|
||||
@@ -75,13 +75,13 @@ export const InlineMenuContainer = ({ editor }: InlineMenuContainerProps) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
const ToolbarContainer = styled('div')({
|
||||
const ToolbarContainer = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 12px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
backgroundColor: '#fff',
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { useState, useEffect, FC } from 'react';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
FC,
|
||||
type MouseEvent,
|
||||
type DragEvent,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Virgo,
|
||||
BlockDomInfo,
|
||||
PluginHooks,
|
||||
BlockDropPlacement,
|
||||
LINE_GAP,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Button } from '@toeverything/components/common';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
@@ -14,7 +23,7 @@ import { distinctUntilChanged, Subject } from 'rxjs';
|
||||
import { HandleChildIcon } from '@toeverything/components/icons';
|
||||
import { MENU_WIDTH } from './menu-config';
|
||||
|
||||
const MENU_BUTTON_OFFSET = 12;
|
||||
const MENU_BUTTON_OFFSET = 4;
|
||||
|
||||
export type LineInfoSubject = Subject<
|
||||
| {
|
||||
@@ -64,13 +73,13 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
|
||||
};
|
||||
const bottomLineStyle = {
|
||||
...horizontalLineStyle,
|
||||
top: intersectionRect.bottom + 1 - rootRect.y,
|
||||
top: intersectionRect.bottom + 1 - rootRect.y - LINE_GAP,
|
||||
};
|
||||
|
||||
const verticalLineStyle = {
|
||||
...lineStyle,
|
||||
width: 2,
|
||||
height: intersectionRect.height,
|
||||
height: intersectionRect.height - LINE_GAP,
|
||||
top: intersectionRect.y - rootRect.y,
|
||||
};
|
||||
const leftLineStyle = {
|
||||
@@ -93,10 +102,10 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
|
||||
}
|
||||
|
||||
function DragComponent(props: {
|
||||
children: React.ReactNode;
|
||||
style: React.CSSProperties;
|
||||
onDragStart: (event: React.DragEvent<Element>) => void;
|
||||
onDragEnd: (event: React.DragEvent<Element>) => void;
|
||||
children: ReactNode;
|
||||
style: CSSProperties;
|
||||
onDragStart: (event: DragEvent<Element>) => void;
|
||||
onDragEnd: (event: DragEvent<Element>) => void;
|
||||
}) {
|
||||
const { style, children, onDragStart, onDragEnd } = props;
|
||||
return (
|
||||
@@ -122,7 +131,7 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
|
||||
const [block, setBlock] = useState<BlockDomInfo | undefined>();
|
||||
const [line, setLine] = useState<LineInfo | undefined>(undefined);
|
||||
|
||||
const handleDragStart = async (event: React.DragEvent<Element>) => {
|
||||
const handleDragStart = async (event: DragEvent<Element>) => {
|
||||
event.stopPropagation();
|
||||
setVisible(false);
|
||||
|
||||
@@ -138,12 +147,12 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: React.DragEvent<Element>) => {
|
||||
const handleDragEnd = (event: DragEvent<Element>) => {
|
||||
event.preventDefault();
|
||||
setLine(undefined);
|
||||
};
|
||||
|
||||
const onClick = (event: React.MouseEvent) => {
|
||||
const onClick = (event: MouseEvent<Element>) => {
|
||||
if (block == null) return;
|
||||
const currentTarget = event.currentTarget;
|
||||
editor.selection.setSelectedNodesIds([block.blockId]);
|
||||
@@ -200,11 +209,10 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left:
|
||||
Math.min(
|
||||
block.rect.left -
|
||||
MENU_WIDTH -
|
||||
MENU_BUTTON_OFFSET
|
||||
) - rootRect.left,
|
||||
block.rect.left -
|
||||
MENU_WIDTH -
|
||||
MENU_BUTTON_OFFSET -
|
||||
rootRect.left,
|
||||
top: block.rect.top - rootRect.top,
|
||||
opacity: visible ? 1 : 0,
|
||||
zIndex: 1,
|
||||
@@ -239,15 +247,19 @@ const Draggable = styled(Button)({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
width: '16px',
|
||||
height: '22px',
|
||||
'& svg': {
|
||||
fontSize: '20px',
|
||||
marginLeft: '-2px',
|
||||
},
|
||||
':hover': {
|
||||
backgroundColor: '#edeef0',
|
||||
backgroundColor: '#F5F7F8',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
|
||||
const LigoLeftMenu = styled('div')({
|
||||
backgroundColor: 'transparent',
|
||||
marginRight: '4px',
|
||||
// marginRight: '4px',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BlockDomInfo, HookType } from '@toeverything/framework/virgo';
|
||||
import React, { StrictMode } from 'react';
|
||||
import { StrictMode } from 'react';
|
||||
import { BasePlugin } from '../../base-plugin';
|
||||
import { ignoreBlockTypes } from './menu-config';
|
||||
import { LineInfoSubject, LeftMenuDraggable } from './LeftMenuDraggable';
|
||||
@@ -37,7 +37,7 @@ export class LeftMenuPlugin extends BasePlugin {
|
||||
);
|
||||
this.sub.add(
|
||||
this.hooks
|
||||
.get(HookType.AFTER_ON_NODE_DRAG_OVER)
|
||||
.get(HookType.ON_ROOTNODE_DRAG_OVER)
|
||||
.subscribe(this._handleDragOverBlockNode)
|
||||
);
|
||||
this.sub.add(
|
||||
@@ -65,24 +65,30 @@ export class LeftMenuPlugin extends BasePlugin {
|
||||
private _onDrop = () => {
|
||||
this._lineInfo.next(undefined);
|
||||
};
|
||||
private _handleDragOverBlockNode = async ([event, blockInfo]: [
|
||||
React.DragEvent<Element>,
|
||||
BlockDomInfo
|
||||
]) => {
|
||||
const { type, dom, blockId } = blockInfo;
|
||||
private _handleDragOverBlockNode = async (
|
||||
event: React.DragEvent<Element>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
if (this.editor.dragDropManager.isDragBlock(event)) {
|
||||
if (ignoreBlockTypes.includes(type)) {
|
||||
return;
|
||||
}
|
||||
const direction =
|
||||
await this.editor.dragDropManager.checkBlockDragTypes(
|
||||
event,
|
||||
dom,
|
||||
blockId
|
||||
);
|
||||
this._lineInfo.next({ direction, blockInfo });
|
||||
}
|
||||
if (!this.editor.dragDropManager.isDragBlock(event)) return;
|
||||
const block = await this.editor.getBlockByPoint(
|
||||
new Point(event.clientX, event.clientY)
|
||||
);
|
||||
if (block == null || ignoreBlockTypes.includes(block.type)) return;
|
||||
const direction = await this.editor.dragDropManager.checkBlockDragTypes(
|
||||
event,
|
||||
block.dom,
|
||||
block.id
|
||||
);
|
||||
this._lineInfo.next({
|
||||
direction,
|
||||
blockInfo: {
|
||||
blockId: block.id,
|
||||
dom: block.dom,
|
||||
rect: block.dom.getBoundingClientRect(),
|
||||
type: block.type,
|
||||
properties: block.getProperties(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _handleMouseMove = async (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
commonListContainer,
|
||||
} from '@toeverything/components/common';
|
||||
import { domToRect } from '@toeverything/utils';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
import { QueryResult } from '../../search';
|
||||
|
||||
@@ -152,13 +153,12 @@ export const ReferenceMenuContainer = ({
|
||||
}, [hooks, handle_key_down]);
|
||||
|
||||
return isShow ? (
|
||||
<div
|
||||
<RootContainer
|
||||
ref={menu_ref}
|
||||
className={styles('rootContainer')}
|
||||
onKeyDownCapture={handle_key_down}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles('contentContainer')}>
|
||||
<ContentContainer>
|
||||
<CommonList
|
||||
items={
|
||||
searchBlocks?.map(
|
||||
@@ -169,24 +169,23 @@ export const ReferenceMenuContainer = ({
|
||||
currentItem={current_item}
|
||||
setCurrentItem={set_current_item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentContainer>
|
||||
</RootContainer>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
rootContainer: {
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
maxHeight: 525,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
backgroundColor: '#fff',
|
||||
padding: '8px 4px',
|
||||
},
|
||||
contentContainer: {
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
maxHeight: 493,
|
||||
},
|
||||
});
|
||||
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',
|
||||
}));
|
||||
|
||||
@@ -141,8 +141,7 @@ export const PlaceholderPanel = (props: PlaceholderPanelProps) => {
|
||||
setOpen(false);
|
||||
props.onClickTips();
|
||||
};
|
||||
const templateList: Array<TemplateMeta> =
|
||||
TemplateFactory.defaultTemplateList;
|
||||
const templateList = TemplateFactory.defaultTemplateList;
|
||||
const handleNewFromTemplate = async (template: TemplateMeta) => {
|
||||
const pageId = await editor.getRootBlockId();
|
||||
const newPage = await services.api.editorBlock.getBlock(
|
||||
@@ -162,33 +161,30 @@ export const PlaceholderPanel = (props: PlaceholderPanelProps) => {
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<PlaceholderPanelContainer
|
||||
style={{
|
||||
top: point.y + 'px',
|
||||
left: point.x + 'px',
|
||||
display: open ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<EmptyPageTipContainer onClick={handleClickEmptyPage}>
|
||||
Press Enter to continue with an empty page, or pick a
|
||||
template
|
||||
</EmptyPageTipContainer>
|
||||
<div style={{ marginTop: '4px', marginLeft: '-8px' }}>
|
||||
{templateList.map((template, index) => {
|
||||
return (
|
||||
<TemplateItemContainer
|
||||
key={index}
|
||||
onClick={() => {
|
||||
handleNewFromTemplate(template);
|
||||
}}
|
||||
>
|
||||
<BaseButton>{template.name}</BaseButton>
|
||||
</TemplateItemContainer>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PlaceholderPanelContainer>
|
||||
</>
|
||||
<PlaceholderPanelContainer
|
||||
style={{
|
||||
top: point.y + 'px',
|
||||
left: point.x + 'px',
|
||||
display: open ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<EmptyPageTipContainer onClick={handleClickEmptyPage}>
|
||||
Press Enter to continue with an empty page, or pick a template
|
||||
</EmptyPageTipContainer>
|
||||
<div style={{ marginTop: '4px', marginLeft: '-8px' }}>
|
||||
{templateList.map((template, index) => {
|
||||
return (
|
||||
<TemplateItemContainer
|
||||
key={index}
|
||||
onClick={() => {
|
||||
handleNewFromTemplate(template);
|
||||
}}
|
||||
>
|
||||
<BaseButton>{template.name}</BaseButton>
|
||||
</TemplateItemContainer>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PlaceholderPanelContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TransitionsModal,
|
||||
MuiBox as Box,
|
||||
MuiBox,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { Virgo, BlockEditor } from '@toeverything/framework/virgo';
|
||||
import { throttle } from '@toeverything/utils';
|
||||
@@ -21,26 +22,6 @@ const styles = style9.create({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
search: {
|
||||
margin: '0.5em',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0px 1px 10px rgb(152 172 189 / 60%)',
|
||||
padding: '16px 32px',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
result: {
|
||||
margin: '0.5em',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0px 1px 10px rgb(152 172 189 / 60%)',
|
||||
padding: '16px 32px',
|
||||
borderRadius: '10px',
|
||||
transitionProperty: 'max-height',
|
||||
transitionDuration: '300ms',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDelay: '0ms',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
},
|
||||
resultItem: {
|
||||
width: '100%',
|
||||
},
|
||||
@@ -96,15 +77,14 @@ export const Search = (props: SearchProps) => {
|
||||
}}
|
||||
>
|
||||
<Box className={styles('wrapper')}>
|
||||
<input
|
||||
className={styles('search')}
|
||||
<SearchInput
|
||||
autoFocus
|
||||
value={search}
|
||||
onChange={e => set_search(e.target.value)}
|
||||
/>
|
||||
<MuiBox
|
||||
<ResultContainer
|
||||
sx={{ maxHeight: `${result.length * 28 + 32 + 20}px` }}
|
||||
className={styles('result', {
|
||||
className={styles({
|
||||
resultHide: !result.length,
|
||||
})}
|
||||
>
|
||||
@@ -119,8 +99,30 @@ export const Search = (props: SearchProps) => {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</MuiBox>
|
||||
</ResultContainer>
|
||||
</Box>
|
||||
</TransitionsModal>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchInput = styled('input')(({ theme }) => ({
|
||||
margin: '0.5em',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
padding: '16px 32px',
|
||||
borderRadius: '10px',
|
||||
}));
|
||||
|
||||
const ResultContainer = styled(MuiBox)(({ theme }) => ({
|
||||
margin: '0.5em',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
padding: '16px 32px',
|
||||
borderRadius: '10px',
|
||||
transitionProperty: 'max-height',
|
||||
transitionDuration: '300ms',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDelay: '0ms',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
}));
|
||||
|
||||
@@ -20,7 +20,7 @@ const IconWrapper = styled('div')<Pick<StatusIconProps, 'mode'>>(
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '5px',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
color: theme.affine.palette.primary,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.affine.palette.white,
|
||||
|
||||
@@ -94,7 +94,7 @@ const StyledContainerForCommentItem = styled('div', {
|
||||
transition: 'left 150ms ease-in-out',
|
||||
backgroundColor: theme.affine.palette.white,
|
||||
'&:hover': {
|
||||
boxShadow: theme.affine.shadows.shadowSxDownLg,
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
importWorkspace,
|
||||
exportWorkspace,
|
||||
useWorkspaceAndPageId,
|
||||
useReadingMode,
|
||||
// useReadingMode,
|
||||
clearWorkspace,
|
||||
} from './util';
|
||||
|
||||
@@ -63,20 +63,20 @@ export const useSettings = (): SettingItem[] => {
|
||||
const { workspaceId, pageId } = useWorkspaceAndPageId();
|
||||
const navigate = useNavigate();
|
||||
const settingFlags = useSettingFlags();
|
||||
const { toggleReadingMode, readingMode } = useReadingMode();
|
||||
// const { toggleReadingMode, readingMode } = useReadingMode();
|
||||
|
||||
const settings: SettingItem[] = [
|
||||
{
|
||||
type: 'switch',
|
||||
name: 'Reading Mode',
|
||||
value: readingMode,
|
||||
onChange: () => {
|
||||
toggleReadingMode();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
// {
|
||||
// type: 'switch',
|
||||
// name: 'Reading Mode',
|
||||
// value: readingMode,
|
||||
// onChange: () => {
|
||||
// toggleReadingMode();
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// type: 'separator',
|
||||
// },
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Duplicate Page',
|
||||
|
||||
@@ -12,17 +12,22 @@ import { useNavigate } from 'react-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
const StyledWrapper = styled('div')({
|
||||
margin: '0 16px 0 32px',
|
||||
paddingLeft: '12px',
|
||||
span: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
'.item': {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
ustifyContent: 'space-between',
|
||||
padding: '7px 0px',
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: '20px',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
background: '#f5f7f8',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
},
|
||||
'.itemButton': {
|
||||
padding: 0,
|
||||
@@ -31,6 +36,7 @@ const StyledWrapper = styled('div')({
|
||||
'.itemLeft': {
|
||||
color: '#4c6275',
|
||||
marginRight: '20px',
|
||||
cursor: 'pointer',
|
||||
span: {
|
||||
fontSize: 14,
|
||||
},
|
||||
@@ -44,34 +50,39 @@ const StyledWrapper = styled('div')({
|
||||
},
|
||||
});
|
||||
|
||||
const StyledItemContent = styled('div')({
|
||||
width: '100%',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export const Activities = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, currentSpaceId } = useUserAndSpaces();
|
||||
const [recentPages, setRecentPages] = useState([]);
|
||||
const userId = user?.id;
|
||||
|
||||
/* temporarily remove:show recently viewed documents */
|
||||
// const fetchRecentPages = useCallback(async () => {
|
||||
// if (!userId || !currentSpaceId) {
|
||||
// return;
|
||||
// }
|
||||
// const recent_pages = await services.api.userConfig.getRecentPages(
|
||||
// currentSpaceId,
|
||||
// userId
|
||||
// );
|
||||
// setRecentPages(recent_pages);
|
||||
// }, [userId, currentSpaceId]);
|
||||
|
||||
// useEffect(() => {
|
||||
// (async () => {
|
||||
// await fetchRecentPages();
|
||||
// })();
|
||||
// }, [fetchRecentPages]);
|
||||
|
||||
/* show recently edit documents */
|
||||
const getRecentEditPages = async (state, block) => {
|
||||
console.log(state, await block.children());
|
||||
};
|
||||
const getRecentEditPages = useCallback(async () => {
|
||||
if (!userId || !currentSpaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recentEditPages =
|
||||
(await services.api.userConfig.getRecentEditedPages(
|
||||
currentSpaceId
|
||||
)) || [];
|
||||
|
||||
setRecentPages(recentEditPages);
|
||||
}, [currentSpaceId, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await getRecentEditPages();
|
||||
})();
|
||||
}, [getRecentEditPages]);
|
||||
|
||||
useEffect(() => {
|
||||
let unobserve: () => void;
|
||||
@@ -90,12 +101,12 @@ export const Activities = () => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<List>
|
||||
{recentPages.map(({ id, title, lastOpenTime }) => {
|
||||
<List style={{ padding: '0px' }}>
|
||||
{recentPages.map(item => {
|
||||
const { id, title, updated } = item;
|
||||
return (
|
||||
<ListItem className="item" key={id}>
|
||||
<ListItemButton
|
||||
className="itemButton"
|
||||
<StyledItemContent
|
||||
onClick={() => {
|
||||
navigate(`/${currentSpaceId}/${id}`);
|
||||
}}
|
||||
@@ -106,11 +117,11 @@ export const Activities = () => {
|
||||
/>
|
||||
<ListItemText
|
||||
className="itemRight"
|
||||
primary={formatDistanceToNow(lastOpenTime, {
|
||||
primary={formatDistanceToNow(updated, {
|
||||
includeSeconds: true,
|
||||
})}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</StyledItemContent>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -44,7 +44,7 @@ export type DndTreeProps = {
|
||||
*/
|
||||
export function DndTree(props: DndTreeProps) {
|
||||
const {
|
||||
indentationWidth = 16,
|
||||
indentationWidth = 12,
|
||||
collapsible,
|
||||
removable,
|
||||
showDragIndicator,
|
||||
|
||||
@@ -101,9 +101,6 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
<ArrowDropDownIcon />
|
||||
))}
|
||||
</Action>
|
||||
{/*<Action>*/}
|
||||
{/* <DocumentIcon />*/}
|
||||
{/*</Action>*/}
|
||||
|
||||
<div className={styles['ItemContent']}>
|
||||
<span
|
||||
@@ -161,7 +158,6 @@ export function Action({
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
// cursor,
|
||||
'--fill': active?.fill,
|
||||
'--background': active?.background,
|
||||
} as CSSProperties
|
||||
|
||||
+4
-16
@@ -4,6 +4,10 @@
|
||||
list-style: none;
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
background: #f5f7f8;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&.clone {
|
||||
display: inline-block;
|
||||
@@ -43,7 +47,6 @@
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #2389ff;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
> * {
|
||||
@@ -69,7 +72,6 @@
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
color: #4c6275;
|
||||
}
|
||||
|
||||
@@ -81,7 +83,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
background-color: #fff;
|
||||
color: #4c6275;
|
||||
padding-right: 0.5rem;
|
||||
overflow: hidden;
|
||||
@@ -96,11 +97,6 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f5f7f8;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.Text {
|
||||
@@ -167,14 +163,6 @@
|
||||
background-color: transparent;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action-background, rgba(0, 0, 0, 0.05));
|
||||
|
||||
svg {
|
||||
fill: #6f7b88;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
flex: 0 0 auto;
|
||||
margin: auto;
|
||||
|
||||
@@ -133,7 +133,7 @@ export function Cascader(props: CascaderProps) {
|
||||
const MenuPaper = styled('div')(({ theme }) => ({
|
||||
fontFamily: 'PingFang SC',
|
||||
background: '#FFF',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
borderRadius: '10px 0px 10px 10px',
|
||||
color: '#4C6275',
|
||||
fontWeight: '400',
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Avatar,
|
||||
Backdrop,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
tooltipClasses,
|
||||
Typography,
|
||||
Zoom,
|
||||
Fade,
|
||||
} from '@mui/material';
|
||||
|
||||
export { alpha } from '@mui/system';
|
||||
@@ -233,7 +235,16 @@ export const MuiInput = Input;
|
||||
*/
|
||||
export const MuiZoom = Zoom;
|
||||
|
||||
/**
|
||||
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
|
||||
*/
|
||||
export const MuiFade = Fade;
|
||||
|
||||
/**
|
||||
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
|
||||
*/
|
||||
export const MuiRadio = Radio;
|
||||
/**
|
||||
* @deprecated It is not recommended to use Mui directly, because the design will not refer to Mui's interaction logic.
|
||||
*/
|
||||
export const MuiBackdrop = Backdrop;
|
||||
|
||||
+3
-3
@@ -12,14 +12,14 @@ const border_radius_map: Record<PopoverContainerProps['direction'], string> = {
|
||||
export const PopoverContainer = styled('div')<
|
||||
Pick<PopoverContainerProps, 'direction'>
|
||||
>(({ theme, direction, style }) => {
|
||||
const shadow = theme.affine.shadows.shadowSxDownLg;
|
||||
const shadow = theme.affine.shadows.shadow1;
|
||||
const white = theme.affine.palette.white;
|
||||
|
||||
const border_radius =
|
||||
const borderRadius =
|
||||
border_radius_map[direction] || border_radius_map['left-top'];
|
||||
return {
|
||||
boxShadow: shadow,
|
||||
borderRadius: border_radius,
|
||||
borderRadius: borderRadius,
|
||||
padding: '8px 4px',
|
||||
backgroundColor: white,
|
||||
...style,
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MuiPopperPlacementType as PopperPlacementType } from '../mui';
|
||||
import React, { forwardRef, type PropsWithChildren } from 'react';
|
||||
import { type PopperHandler, Popper } from '../popper';
|
||||
import { PopoverContainer } from './container';
|
||||
import { PopoverContainer } from './Container';
|
||||
import type { PopoverProps, PopoverDirection } from './interface';
|
||||
|
||||
export const placementToContainerDirection: Record<
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './Popover';
|
||||
export * from './interface';
|
||||
export { PopoverContainer } from './container';
|
||||
export { PopoverContainer } from './Container';
|
||||
@@ -117,7 +117,7 @@ const StyledListbox = styled('ul')(({ theme }) => ({
|
||||
background: '#fff',
|
||||
borderRadius: '10px',
|
||||
overflow: 'auto',
|
||||
boxShadow: theme.affine.shadows.shadowSxDownLg,
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
}));
|
||||
|
||||
const StyledPopper = styled(PopperUnstyled)`
|
||||
|
||||
@@ -70,7 +70,7 @@ interface Typography {
|
||||
|
||||
interface Shadows {
|
||||
none: 'none';
|
||||
shadowSxDownLg: string;
|
||||
shadow1: string;
|
||||
}
|
||||
|
||||
type StringWithNone = [
|
||||
@@ -225,7 +225,7 @@ export const Theme = {
|
||||
},
|
||||
shadows: {
|
||||
none: 'none',
|
||||
shadowSxDownLg: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
shadow1: '0px 1px 5px rgba(152, 172, 189, 0.2)',
|
||||
},
|
||||
border: ['none'],
|
||||
spacing: {
|
||||
|
||||
@@ -13,7 +13,7 @@ const groupTemplateMap = {
|
||||
grid: gridTemplate,
|
||||
} as GroupTemplateMap;
|
||||
|
||||
const defaultTemplateList = [
|
||||
const defaultTemplateList: Array<TemplateMeta> = [
|
||||
{
|
||||
name: 'New From Quick Start',
|
||||
groupKeys: ['todolist'],
|
||||
@@ -22,10 +22,10 @@ const defaultTemplateList = [
|
||||
{ name: 'New From Blog', groupKeys: ['blog'] },
|
||||
{ name: ' New Todolist', groupKeys: ['todolist'] },
|
||||
{ name: ' New Empty Page', groupKeys: ['empty'] },
|
||||
] as const;
|
||||
];
|
||||
|
||||
const TemplateFactory = {
|
||||
defaultTemplateList: defaultTemplateList,
|
||||
defaultTemplateList,
|
||||
generatePageTemplateByGroupKeys(props: TemplateMeta): Template {
|
||||
const newTitle = props.name || 'Get Started with AFFiNE';
|
||||
const keys: GroupTemplateKeys[] = props.groupKeys || [];
|
||||
|
||||
@@ -3,43 +3,44 @@ import { ServiceBaseClass } from '../base';
|
||||
import { ObserveCallback, ReturnUnobserve } from '../database';
|
||||
import { PageTree } from './page-tree';
|
||||
import { PageConfigItem } from './types';
|
||||
import type { QueryIndexMetadata } from '@toeverything/datasource/jwt';
|
||||
|
||||
/** Operate the user configuration at the workspace level */
|
||||
export class UserConfig extends ServiceBaseClass {
|
||||
private async fetch_recent_pages(
|
||||
private async _fetchRecentPages(
|
||||
workspace: string
|
||||
): Promise<Record<string, Array<PageConfigItem>>> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const recent_work_pages =
|
||||
workspace_db_block.getDecoration<
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
const recentWorkPages =
|
||||
workspaceDbBlock.getDecoration<
|
||||
Record<string, Array<PageConfigItem>>
|
||||
>(RECENT_PAGES) || {};
|
||||
return recent_work_pages;
|
||||
return recentWorkPages;
|
||||
}
|
||||
|
||||
private async save_recent_pages(
|
||||
private async _saveRecentPages(
|
||||
workspace: string,
|
||||
recentPages: Record<string, Array<PageConfigItem>>
|
||||
) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
workspace_db_block.setDecoration(RECENT_PAGES, recentPages);
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
workspaceDbBlock.setDecoration(RECENT_PAGES, recentPages);
|
||||
}
|
||||
|
||||
async getUserInitialPage(
|
||||
workspace: string,
|
||||
userId: string
|
||||
): Promise<string> {
|
||||
const recent_pages = await this.getRecentPages(workspace, userId);
|
||||
if (recent_pages.length > 0) {
|
||||
return recent_pages[0].id;
|
||||
const recentPages = await this.getRecentPages(workspace, userId);
|
||||
if (recentPages.length > 0) {
|
||||
return recentPages[0].id;
|
||||
}
|
||||
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const new_page = await db.get('page');
|
||||
const newPage = await db.get('page');
|
||||
|
||||
await this.get_dependency(PageTree).addPage(workspace, new_page.id);
|
||||
await this.addRecentPage(workspace, userId, new_page.id);
|
||||
return new_page.id;
|
||||
await this.get_dependency(PageTree).addPage(workspace, newPage.id);
|
||||
await this.addRecentPage(workspace, userId, newPage.id);
|
||||
return newPage.id;
|
||||
}
|
||||
|
||||
async getRecentPages(
|
||||
@@ -47,13 +48,10 @@ export class UserConfig extends ServiceBaseClass {
|
||||
userId: string,
|
||||
topNumber = 5
|
||||
): Promise<PageConfigItem[]> {
|
||||
const recent_work_pages = await this.fetch_recent_pages(workspace);
|
||||
const recent_pages = (recent_work_pages[userId] || []).slice(
|
||||
0,
|
||||
topNumber
|
||||
);
|
||||
const recentWorkPages = await this._fetchRecentPages(workspace);
|
||||
const recentPages = (recentWorkPages[userId] || []).slice(0, topNumber);
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
for (const item of recent_pages) {
|
||||
for (const item of recentPages) {
|
||||
const page = await db.get(item.id as 'page');
|
||||
item.title =
|
||||
page
|
||||
@@ -61,39 +59,39 @@ export class UserConfig extends ServiceBaseClass {
|
||||
?.value?.map(v => v.text)
|
||||
.join('') || 'Untitled';
|
||||
}
|
||||
return recent_pages;
|
||||
return recentPages;
|
||||
}
|
||||
|
||||
async addRecentPage(workspace: string, userId: string, pageId: string) {
|
||||
const recent_work_pages = await this.fetch_recent_pages(workspace);
|
||||
let recent_pages = recent_work_pages[userId] || [];
|
||||
recent_pages = recent_pages.filter(item => item.id !== pageId);
|
||||
recent_pages.unshift({
|
||||
const recentWorkPages = await this._fetchRecentPages(workspace);
|
||||
let recentPages = recentWorkPages[userId] || [];
|
||||
recentPages = recentPages.filter(item => item.id !== pageId);
|
||||
recentPages.unshift({
|
||||
id: pageId,
|
||||
lastOpenTime: Date.now(),
|
||||
});
|
||||
recent_work_pages[userId] = recent_pages;
|
||||
await this.save_recent_pages(workspace, recent_work_pages);
|
||||
recentWorkPages[userId] = recentPages;
|
||||
await this._saveRecentPages(workspace, recentWorkPages);
|
||||
}
|
||||
|
||||
async removePage(workspace: string, pageId: string) {
|
||||
const recent_work_pages = await this.fetch_recent_pages(workspace);
|
||||
for (const key in recent_work_pages) {
|
||||
recent_work_pages[key] = recent_work_pages[key].filter(
|
||||
const recentWorkPages = await this._fetchRecentPages(workspace);
|
||||
for (const key in recentWorkPages) {
|
||||
recentWorkPages[key] = recentWorkPages[key].filter(
|
||||
item => item.id !== pageId
|
||||
);
|
||||
}
|
||||
await this.save_recent_pages(workspace, recent_work_pages);
|
||||
await this._saveRecentPages(workspace, recentWorkPages);
|
||||
}
|
||||
|
||||
async observe(
|
||||
{ workspace }: { workspace: string },
|
||||
callback: ObserveCallback
|
||||
): Promise<ReturnUnobserve> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
const unobserveWorkspace = await this._observe(
|
||||
workspace,
|
||||
workspace_db_block.id,
|
||||
workspaceDbBlock.id,
|
||||
(states, block) => {
|
||||
callback(states, block);
|
||||
}
|
||||
@@ -105,24 +103,40 @@ export class UserConfig extends ServiceBaseClass {
|
||||
}
|
||||
|
||||
async unobserve({ workspace }: { workspace: string }) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
await this._unobserve(workspace, workspace_db_block.id);
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
await this._unobserve(workspace, workspaceDbBlock.id);
|
||||
}
|
||||
|
||||
async getWorkspaceName(workspace: string): Promise<string> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
const workspaceName =
|
||||
workspace_db_block.getDecoration<string>(WORKSPACE_CONFIG) || '';
|
||||
workspaceDbBlock.getDecoration<string>(WORKSPACE_CONFIG) || '';
|
||||
return workspaceName;
|
||||
}
|
||||
|
||||
async getWorkspaceId(workspace: string): Promise<string> {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
return workspace_db_block.id;
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
return workspaceDbBlock.id;
|
||||
}
|
||||
|
||||
async setWorkspaceName(workspace: string, workspaceName: string) {
|
||||
const workspace_db_block = await this.getWorkspaceDbBlock(workspace);
|
||||
workspace_db_block.setDecoration(WORKSPACE_CONFIG, workspaceName);
|
||||
const workspaceDbBlock = await this.getWorkspaceDbBlock(workspace);
|
||||
workspaceDbBlock.setDecoration(WORKSPACE_CONFIG, workspaceName);
|
||||
}
|
||||
|
||||
async getRecentEditedPages(workspace: string) {
|
||||
const db = await this.database.getDatabase(workspace);
|
||||
const recentEditedPages =
|
||||
(await db.queryBlocks({
|
||||
$sort: 'lastUpdated',
|
||||
$desc: false /* sort rule: true(default)(ASC), or false(DESC) */,
|
||||
$limit: 4,
|
||||
flavor: 'page',
|
||||
} as QueryIndexMetadata)) || [];
|
||||
|
||||
return recentEditedPages.map(item => {
|
||||
item['title'] = item.content || 'Untitled';
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
## Usage
|
||||
|
||||
- set provider
|
||||
- Set token at environment variable
|
||||
- The key can be obtained from the [Feature Flag Portal](https://portal.featureflag.co/account-settings/projects)
|
||||
|
||||
```shell
|
||||
# .env.local
|
||||
AFFINE_FEATURE_FLAG_TOKEN=XXXXXXX
|
||||
```
|
||||
|
||||
- Set provider
|
||||
|
||||
```tsx
|
||||
import { FeatureFlagsProvider } from '@toeverything/datasource/feature-flags';
|
||||
@@ -42,7 +50,8 @@ const App = () => {
|
||||
|
||||
**When entering development mode feature flag will NOT be updated in real time**
|
||||
|
||||
- `activateFfcDevMode()` play with feature flags locally
|
||||
- `activateFfcDevMode(PASSWORD)` play with feature flags locally
|
||||
- The `devModePassword` can be obtained from `src/config.ts`
|
||||
- `quitFfcDevMode()` quit dev mode
|
||||
|
||||
## Running unit tests
|
||||
|
||||
@@ -8,4 +8,18 @@ export const config: IOption = {
|
||||
// id: 'the user's unique identifier'
|
||||
// }
|
||||
devModePassword: '-',
|
||||
enableDataSync: !!process.env['AFFINE_FEATURE_FLAG_TOKEN'],
|
||||
// bootstrap: [
|
||||
// {
|
||||
// // the feature flag key
|
||||
// id: 'flag',
|
||||
// // the feature flag value
|
||||
// variation: false,
|
||||
// // the variation data type, string is used if not provided
|
||||
// variationType: VariationDataType.boolean,
|
||||
// variationOptions: [],
|
||||
// timestamp: 0,
|
||||
// sendToExperiment: false,
|
||||
// },
|
||||
// ],
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
"buffer": "^6.0.3",
|
||||
"debug": "^4.3.4",
|
||||
"fast-sort": "^3.2.0",
|
||||
"fflate": "^0.7.3",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"immer": "^9.0.15",
|
||||
|
||||
@@ -3,29 +3,29 @@ import { Array as YArray, Map as YMap } from 'yjs';
|
||||
import { RemoteKvService } from '@toeverything/datasource/remote-kv';
|
||||
|
||||
export class YjsRemoteBinaries {
|
||||
readonly #binaries: YMap<YArray<ArrayBuffer>>; // binary instance
|
||||
readonly #remote_storage?: RemoteKvService;
|
||||
private readonly _binaries: YMap<YArray<ArrayBuffer>>; // binary instance
|
||||
private readonly _remoteStorage?: RemoteKvService;
|
||||
|
||||
constructor(binaries: YMap<YArray<ArrayBuffer>>, remote_token?: string) {
|
||||
this.#binaries = binaries;
|
||||
this._binaries = binaries;
|
||||
if (remote_token) {
|
||||
this.#remote_storage = new RemoteKvService(remote_token);
|
||||
this._remoteStorage = new RemoteKvService(remote_token);
|
||||
} else {
|
||||
console.warn(`Remote storage is not ready`);
|
||||
}
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
return this.#binaries.has(name);
|
||||
return this._binaries.has(name);
|
||||
}
|
||||
|
||||
async get(name: string): Promise<YArray<ArrayBuffer> | undefined> {
|
||||
if (this.#binaries.has(name)) {
|
||||
return this.#binaries.get(name);
|
||||
if (this._binaries.has(name)) {
|
||||
return this._binaries.get(name);
|
||||
} else {
|
||||
// TODO: Remote Load
|
||||
try {
|
||||
const file = await this.#remote_storage?.instance.getBuffData(
|
||||
const file = await this._remoteStorage?.instance.getBuffData(
|
||||
name
|
||||
);
|
||||
console.log(file);
|
||||
@@ -38,16 +38,16 @@ export class YjsRemoteBinaries {
|
||||
}
|
||||
|
||||
async set(name: string, binary: YArray<ArrayBuffer>) {
|
||||
if (!this.#binaries.has(name)) {
|
||||
if (!this._binaries.has(name)) {
|
||||
console.log(name, 'name');
|
||||
if (binary.length === 1) {
|
||||
this.#binaries.set(name, binary);
|
||||
if (this.#remote_storage) {
|
||||
this._binaries.set(name, binary);
|
||||
if (this._remoteStorage) {
|
||||
// TODO: Remote Save, if there is an object with the same name remotely, the upload is skipped, because the file name is the hash of the file content
|
||||
const has_file = this.#remote_storage.instance.exist(name);
|
||||
const has_file = this._remoteStorage.instance.exist(name);
|
||||
if (!has_file) {
|
||||
const upload_file = new File(binary.toArray(), name);
|
||||
await this.#remote_storage.instance
|
||||
await this._remoteStorage.instance
|
||||
.upload(upload_file)
|
||||
.catch(err => {
|
||||
throw new Error(`${err} upload error`);
|
||||
|
||||
@@ -32,49 +32,51 @@ type YjsBlockInstanceProps = {
|
||||
};
|
||||
|
||||
export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
readonly #id: string;
|
||||
readonly #block: YMap<unknown>;
|
||||
readonly #binary?: YArray<ArrayBuffer>;
|
||||
readonly #children: YArray<string>;
|
||||
readonly #set_block: (
|
||||
private readonly _id: string;
|
||||
private readonly _block: YMap<unknown>;
|
||||
private readonly _binary?: YArray<ArrayBuffer>;
|
||||
private readonly _children: YArray<string>;
|
||||
private readonly _setBlock: (
|
||||
id: string,
|
||||
block: BlockItem<YjsContentOperation>
|
||||
) => Promise<void>;
|
||||
readonly #get_updated: (id: string) => number | undefined;
|
||||
readonly #get_creator: (id: string) => string | undefined;
|
||||
readonly #get_block_instance: (id: string) => YjsBlockInstance | undefined;
|
||||
readonly #children_listeners: Map<string, BlockListener>;
|
||||
readonly #content_listeners: Map<string, BlockListener>;
|
||||
private readonly _getUpdated: (id: string) => number | undefined;
|
||||
private readonly _getCreator: (id: string) => string | undefined;
|
||||
private readonly _getBlockInstance: (
|
||||
id: string
|
||||
) => YjsBlockInstance | undefined;
|
||||
private readonly _childrenListeners: Map<string, BlockListener>;
|
||||
private readonly _contentListeners: Map<string, BlockListener>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
#children_map: Map<string, number>;
|
||||
_childrenMap: Map<string, number>;
|
||||
|
||||
constructor(props: YjsBlockInstanceProps) {
|
||||
this.#id = props.id;
|
||||
this.#block = props.block;
|
||||
this.#binary = props.binary;
|
||||
this._id = props.id;
|
||||
this._block = props.block;
|
||||
this._binary = props.binary;
|
||||
|
||||
this.#children = props.block.get('children') as YArray<string>;
|
||||
this.#children_map = getMapFromYArray(this.#children);
|
||||
this.#set_block = props.setBlock;
|
||||
this.#get_updated = props.getUpdated;
|
||||
this.#get_creator = props.getCreator;
|
||||
this.#get_block_instance = props.getBlockInstance;
|
||||
this._children = props.block.get('children') as YArray<string>;
|
||||
this._childrenMap = getMapFromYArray(this._children);
|
||||
this._setBlock = props.setBlock;
|
||||
this._getUpdated = props.getUpdated;
|
||||
this._getCreator = props.getCreator;
|
||||
this._getBlockInstance = props.getBlockInstance;
|
||||
|
||||
this.#children_listeners = new Map();
|
||||
this.#content_listeners = new Map();
|
||||
this._childrenListeners = new Map();
|
||||
this._contentListeners = new Map();
|
||||
|
||||
const content = this.#block.get('content') as YMap<unknown>;
|
||||
const content = this._block.get('content') as YMap<unknown>;
|
||||
|
||||
this.#children.observe(event =>
|
||||
ChildrenListenerHandler(this.#children_listeners, event)
|
||||
this._children.observe(event =>
|
||||
ChildrenListenerHandler(this._childrenListeners, event)
|
||||
);
|
||||
content?.observeDeep(events =>
|
||||
ContentListenerHandler(this.#content_listeners, events)
|
||||
ContentListenerHandler(this._contentListeners, events)
|
||||
);
|
||||
// TODO: flavor needs optimization
|
||||
this.#block.observeDeep(events =>
|
||||
ContentListenerHandler(this.#content_listeners, events)
|
||||
this._block.observeDeep(events =>
|
||||
ContentListenerHandler(this._contentListeners, events)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,85 +101,85 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
}
|
||||
|
||||
addChildrenListener(name: string, listener: BlockListener): void {
|
||||
this.#children_listeners.set(name, listener);
|
||||
this._childrenListeners.set(name, listener);
|
||||
}
|
||||
|
||||
removeChildrenListener(name: string): void {
|
||||
this.#children_listeners.delete(name);
|
||||
this._childrenListeners.delete(name);
|
||||
}
|
||||
|
||||
addContentListener(name: string, listener: BlockListener): void {
|
||||
this.#content_listeners.set(name, listener);
|
||||
this._contentListeners.set(name, listener);
|
||||
}
|
||||
|
||||
removeContentListener(name: string): void {
|
||||
this.#content_listeners.delete(name);
|
||||
this._contentListeners.delete(name);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.#id;
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get content(): YjsContentOperation {
|
||||
if (this.type === BlockTypes.block) {
|
||||
const content = this.#block.get('content');
|
||||
const content = this._block.get('content');
|
||||
if (content instanceof YAbstractType) {
|
||||
return new YjsContentOperation(content);
|
||||
} else {
|
||||
throw new Error(`Invalid content type: ${typeof content}`);
|
||||
}
|
||||
} else if (this.type === BlockTypes.binary && this.#binary) {
|
||||
return new YjsContentOperation(this.#binary);
|
||||
} else if (this.type === BlockTypes.binary && this._binary) {
|
||||
return new YjsContentOperation(this._binary);
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid content type: ${this.type}, ${this.#block.get(
|
||||
`Invalid content type: ${this.type}, ${this._block.get(
|
||||
'content'
|
||||
)}, ${this.#binary}`
|
||||
)}, ${this._binary}`
|
||||
);
|
||||
}
|
||||
|
||||
get type(): BlockItem<YjsContentOperation>['type'] {
|
||||
return this.#block.get(
|
||||
return this._block.get(
|
||||
'type'
|
||||
) as BlockItem<YjsContentOperation>['type'];
|
||||
}
|
||||
|
||||
get flavor(): BlockItem<YjsContentOperation>['flavor'] {
|
||||
return this.#block.get(
|
||||
return this._block.get(
|
||||
'flavor'
|
||||
) as BlockItem<YjsContentOperation>['flavor'];
|
||||
}
|
||||
|
||||
// TODO: bad case. Need to optimize.
|
||||
setFlavor(flavor: BlockItem<YjsContentOperation>['flavor']) {
|
||||
this.#block.set('flavor', flavor);
|
||||
this._block.set('flavor', flavor);
|
||||
}
|
||||
|
||||
get created(): BlockItem<YjsContentOperation>['created'] {
|
||||
return this.#block.get(
|
||||
return this._block.get(
|
||||
'created'
|
||||
) as BlockItem<YjsContentOperation>['created'];
|
||||
}
|
||||
|
||||
get updated(): number {
|
||||
return this.#get_updated(this.#id) || this.created;
|
||||
return this._getUpdated(this._id) || this.created;
|
||||
}
|
||||
|
||||
get creator(): string | undefined {
|
||||
return this.#get_creator(this.#id);
|
||||
return this._getCreator(this._id);
|
||||
}
|
||||
|
||||
get children(): string[] {
|
||||
return this.#children.toArray();
|
||||
return this._children.toArray();
|
||||
}
|
||||
|
||||
getChildren(ids?: (string | undefined)[]): YjsBlockInstance[] {
|
||||
const query_ids = ids?.filter((id): id is string => !!id) || [];
|
||||
const exists_ids = this.#children.map(id => id);
|
||||
const exists_ids = this._children.map(id => id);
|
||||
const filter_ids = query_ids.length ? query_ids : exists_ids;
|
||||
return exists_ids
|
||||
.filter(id => filter_ids.includes(id))
|
||||
.map(id => this.#get_block_instance(id))
|
||||
.map(id => this._getBlockInstance(id))
|
||||
.filter((v): v is YjsBlockInstance => !!v);
|
||||
}
|
||||
|
||||
@@ -196,7 +198,7 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
return pos;
|
||||
}
|
||||
} else if (before) {
|
||||
const current_pos = this.#children_map.get(before || '');
|
||||
const current_pos = this._childrenMap.get(before || '');
|
||||
if (
|
||||
typeof current_pos === 'number' &&
|
||||
Number.isInteger(current_pos)
|
||||
@@ -207,7 +209,7 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
}
|
||||
}
|
||||
} else if (after) {
|
||||
const current_pos = this.#children_map.get(after || '');
|
||||
const current_pos = this._childrenMap.get(after || '');
|
||||
if (
|
||||
typeof current_pos === 'number' &&
|
||||
Number.isInteger(current_pos)
|
||||
@@ -227,44 +229,44 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
): Promise<void> {
|
||||
const content = block[GET_BLOCK_ITEM]();
|
||||
if (content) {
|
||||
const lastIndex = this.#children_map.get(block.id);
|
||||
const lastIndex = this._childrenMap.get(block.id);
|
||||
if (typeof lastIndex === 'number') {
|
||||
this.#children.delete(lastIndex);
|
||||
this.#children_map = getMapFromYArray(this.#children);
|
||||
this._children.delete(lastIndex);
|
||||
this._childrenMap = getMapFromYArray(this._children);
|
||||
}
|
||||
|
||||
const position = this.position_calculator(
|
||||
this.#children_map.size,
|
||||
this._childrenMap.size,
|
||||
pos
|
||||
);
|
||||
if (typeof position === 'number') {
|
||||
this.#children.insert(position, [block.id]);
|
||||
this._children.insert(position, [block.id]);
|
||||
} else {
|
||||
this.#children.push([block.id]);
|
||||
this._children.push([block.id]);
|
||||
}
|
||||
await this.#set_block(block.id, content);
|
||||
this.#children_map = getMapFromYArray(this.#children);
|
||||
await this._setBlock(block.id, content);
|
||||
this._childrenMap = getMapFromYArray(this._children);
|
||||
}
|
||||
}
|
||||
|
||||
removeChildren(ids: (string | undefined)[]): Promise<string[]> {
|
||||
return new Promise(resolve => {
|
||||
if (this.#children.doc) {
|
||||
transact(this.#children.doc, () => {
|
||||
if (this._children.doc) {
|
||||
transact(this._children.doc, () => {
|
||||
const failed = [];
|
||||
for (const id of ids) {
|
||||
let idx = -1;
|
||||
for (const block_id of this.#children) {
|
||||
for (const block_id of this._children) {
|
||||
idx += 1;
|
||||
if (block_id === id) {
|
||||
this.#children.delete(idx);
|
||||
this._children.delete(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (id) failed.push(id);
|
||||
}
|
||||
|
||||
this.#children_map = getMapFromYArray(this.#children);
|
||||
this._childrenMap = getMapFromYArray(this._children);
|
||||
resolve(failed);
|
||||
});
|
||||
} else {
|
||||
@@ -274,7 +276,7 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
}
|
||||
|
||||
public scopedHistory(scope: any[]): HistoryManager {
|
||||
return new YjsHistoryManager(this.#block, scope);
|
||||
return new YjsHistoryManager(this._block, scope);
|
||||
}
|
||||
|
||||
[GET_BLOCK_ITEM]() {
|
||||
@@ -283,7 +285,7 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
|
||||
return {
|
||||
type: this.type,
|
||||
flavor: this.flavor,
|
||||
children: this.#children.slice(),
|
||||
children: this._children.slice(),
|
||||
created: this.created,
|
||||
content: this.content,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user