Merge branch 'develop' into fix/clipboard

This commit is contained in:
QiShaoXuan
2022-08-09 11:28:20 +08:00
169 changed files with 4639 additions and 1929 deletions

View File

@@ -3,10 +3,10 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@authing/react-ui-components": "^3.1.23",
"@authing/react-ui-components": "^3.1.39",
"@emotion/styled": "^11.9.3"
},
"devDependencies": {
"authing-js-sdk": "^4.23.33"
"authing-js-sdk": "^4.23.35"
}
}

View File

@@ -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 (

View File

@@ -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;

View File

@@ -21,9 +21,7 @@ export const ZoomBar: FC = () => {
const zoom = app.useStore(zoomSelector);
return (
<div
style={{ position: 'absolute', right: 10, bottom: 10, zIndex: 200 }}
>
<ZoomBarContainer>
<MiniMapContainer>
<MiniMap />
</MiniMapContainer>
@@ -52,10 +50,18 @@ export const ZoomBar: FC = () => {
<UnfoldMoreIcon style={{ transform: 'rotateZ(90deg)' }} />
</IconButton>
</div>
</div>
</ZoomBarContainer>
);
};
const ZoomBarContainer = styled('div')({
position: 'absolute',
right: 10,
bottom: 10,
zIndex: 200,
userSelect: 'none',
});
const MiniMapContainer = styled('div')({
display: 'flex',
justifyContent: 'flex-end',

View File

@@ -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;

View File

@@ -133,6 +133,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
<HTMLContainer ref={ref} {...events}>
<Container
ref={containerRef}
editing={isEditing}
onPointerDown={stopPropagation}
onMouseEnter={activateIfEditing}
onDragEnter={activateIfEditing}
@@ -248,15 +249,15 @@ export class EditorUtil extends TDShapeUtil<T, E> {
const PADDING = 16;
// const MIN_CONTAINER_HEIGHT = 200;
const Container = styled('div')({
const Container = styled('div')<{ editing: boolean }>(({ editing }) => ({
pointerEvents: 'all',
position: 'relative',
width: '100%',
});
userSelect: editing ? 'unset' : 'none',
}));
const Mask = styled('div')({
position: 'absolute',
userSelect: 'none',
top: 0,
left: 0,
bottom: 0,

View File

@@ -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 -------------------- */

View File

@@ -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]);

View File

@@ -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}

View File

@@ -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',

View File

@@ -8,21 +8,21 @@
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-html": "~6.1.0",
"@codemirror/lang-java": "~6.0.0",
"@codemirror/lang-javascript": "~6.0.1",
"@codemirror/lang-javascript": "~6.0.2",
"@codemirror/lang-json": "~6.0.0",
"@codemirror/lang-lezer": "~6.0.0",
"@codemirror/lang-markdown": "~6.0.0",
"@codemirror/lang-markdown": "~6.0.1",
"@codemirror/lang-php": "~6.0.0",
"@codemirror/lang-python": "~6.0.0",
"@codemirror/lang-python": "~6.0.1",
"@codemirror/lang-rust": "~6.0.0",
"@codemirror/lang-sql": "~6.0.0",
"@codemirror/lang-sql": "~6.1.0",
"@codemirror/lang-xml": "~6.0.0",
"@codemirror/language": "^6.2.0",
"@codemirror/language": "^6.2.1",
"@codemirror/legacy-modes": "~6.1.0",
"@codemirror/next": "^0.16.0",
"@codemirror/state": "^6.1.0",
"@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0",
"@codemirror/view": "^6.0.2",
"@codemirror/view": "^6.2.0",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
@@ -31,6 +31,7 @@
"@mui/system": "^5.8.6",
"code-example": "^3.3.6",
"codemirror": "6.0.1",
"codemirror-lang-elixir": "^3.0.0",
"keymap": "link:@codemirror/next/keymap",
"nanoid": "^4.0.0",
"react-resizable": "^3.0.4",

View File

@@ -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>

View File

@@ -1,10 +1,9 @@
import { FC, useState, useMemo, useRef, useEffect } from 'react';
import { FC, useState, useRef, useEffect } from 'react';
import { StyleWithAtRules } from 'style9';
import { CreateView } from '@toeverything/framework/virgo';
import CodeMirror from './CodeMirror';
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
import { styled } from '@toeverything/components/ui';
import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined';
import { javascript } from '@codemirror/lang-javascript';
import { html } from '@codemirror/lang-html';
@@ -32,6 +31,7 @@ import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
import { elixir } from 'codemirror-lang-elixir';
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
import { perl } from '@codemirror/legacy-modes/mode/perl';
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
@@ -45,12 +45,11 @@ import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { julia } from '@codemirror/legacy-modes/mode/julia';
import { r } from '@codemirror/legacy-modes/mode/r';
import { Extension } from '@codemirror/state';
// import { Select } from '../../components/select';
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 {
@@ -87,6 +86,7 @@ const langs: Record<string, any> = {
brainfuck: () => StreamLanguage.define(brainfuck),
stylus: () => StreamLanguage.define(stylus),
erlang: () => StreamLanguage.define(erlang),
elixir: () => StreamLanguage.define(elixir),
nginx: () => StreamLanguage.define(nginx),
perl: () => StreamLanguage.define(perl),
ruby: () => StreamLanguage.define(ruby),
@@ -100,10 +100,8 @@ const langs: Record<string, any> = {
julia: () => StreamLanguage.define(julia),
dockerfile: () => StreamLanguage.define(dockerFile),
r: () => StreamLanguage.define(r),
// clike: () => StreamLanguage.define(clike),
// clike: () => clike({ }),
};
const DEFAULT_LANG = 'javascript';
const CodeBlock = styled('div')(({ theme }) => ({
backgroundColor: '#F2F5F9',
padding: '8px 24px',
@@ -118,7 +116,7 @@ const CodeBlock = styled('div')(({ theme }) => ({
flexWrap: 'wrap',
justifyContent: 'space-between',
},
'.delete-block': {
'.copy-block': {
padding: '6px 10px',
backgroundColor: '#fff',
borderRadius: theme.affine.shape.borderRadius,
@@ -130,29 +128,27 @@ const CodeBlock = styled('div')(({ theme }) => ({
}));
export const CodeView: FC<CreateCodeView> = ({ block, editor }) => {
const initValue: string = block.getProperty('text')?.value?.[0]?.text;
const langType: string = block.getProperty('lang')?.value?.[0]?.text;
const [mode, setMode] = useState('javascript');
const langType: string = block.getProperty('lang');
const [extensions, setExtensions] = useState<Extension[]>();
const codeMirror = useRef();
useOnSelect(block.id, (is_select: boolean) => {
const codeMirror = useRef<ReactCodeMirrorRef>();
useOnSelect(block.id, (_is_select: boolean) => {
if (codeMirror.current) {
//@ts-ignore
codeMirror?.current?.view?.focus();
}
});
const onChange = (value: any, codeEditor: any) => {
const onChange = (value: string) => {
block.setProperty('text', {
value: [{ text: value }],
});
};
useEffect(() => {
handleLangChange(langType ? langType : 'javascript');
}, []);
function handleLangChange(lang: string) {
const handleLangChange = (lang: string) => {
block.setProperty('lang', lang);
setMode(lang);
setExtensions([langs[lang]()]);
}
};
useEffect(() => {
handleLangChange(langType ? langType : DEFAULT_LANG);
}, []);
const copyCode = () => {
copyToClipboard(initValue);
};
@@ -163,7 +159,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();
@@ -171,19 +167,12 @@ export const CodeView: FC<CreateCodeView> = ({ block, editor }) => {
>
<div className="operation">
<div className="select">
{/* <Select
label="Lang"
options={Object.keys(langs)}
value={mode}
onChange={evn => handleLangChange(evn.target.value)}
/> */}
<Select
width={128}
placeholder="Search for a field type"
value={mode}
value={langType || DEFAULT_LANG}
listboxStyle={{ maxHeight: '400px' }}
onChange={(selectedValue: string) => {
// setSelectedOption(selectedValue);
handleLangChange(selectedValue);
}}
>
@@ -197,17 +186,8 @@ export const CodeView: FC<CreateCodeView> = ({ block, editor }) => {
</Select>
</div>
<div>
<div className="delete-block" onClick={copyCode}>
<div className="copy-block" onClick={copyCode}>
Copy
{/* <DeleteSweepOutlinedIcon
className="delete-icon"
fontSize="small"
sx={{
color: 'rgba(0,0,0,.5)',
cursor: 'pointer',
'&:hover': { color: 'rgba(0,0,0,.9)' },
}}
/> */}
</div>
</div>
</div>
@@ -222,6 +202,6 @@ export const CodeView: FC<CreateCodeView> = ({ block, editor }) => {
handleKeyArrowUp={handleKeyArrowUp}
/>
</CodeBlock>
</WrapperWithPendantAndDragDrop>
</BlockPendantProvider>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -2,13 +2,13 @@ import { FC, useEffect, useLayoutEffect, useRef } from 'react';
import { ChildrenView } from '@toeverything/framework/virgo';
import { styled } from '@toeverything/components/ui';
import { sleep } from '@toeverything/utils';
import { GRID_ITEM_MIN_WIDTH, GRID_PROPERTY_KEY, removePercent } from '../grid';
import { GRID_PROPERTY_KEY, removePercent } from '../grid';
export const GRID_ITEM_CLASS_NAME = 'grid-item';
export const GRID_ITEM_CONTENT_CLASS_NAME = `${GRID_ITEM_CLASS_NAME}-content`;
export const GridItem: FC<ChildrenView> = function (props) {
const { children, block } = props;
const { children, block, editor } = props;
const RENDER_DELAY_TIME = 100;
const ref = useRef<HTMLDivElement>();
@@ -25,6 +25,7 @@ export const GridItem: FC<ChildrenView> = function (props) {
const checkAndRefreshWidth = async () => {
const currentWidth = block.getProperty(GRID_PROPERTY_KEY);
const gridItemMinWidth = editor.configManager.grid.gridItemMinWidth;
if (currentWidth) {
setWidth(currentWidth);
} else if (!block.dom?.style.width) {
@@ -64,26 +65,23 @@ export const GridItem: FC<ChildrenView> = function (props) {
if new width less then min width,
set min width and next block will be fix width
*/
if (newWidth < GRID_ITEM_MIN_WIDTH) {
needFixWidth += GRID_ITEM_MIN_WIDTH - newWidth;
newWidth = GRID_ITEM_MIN_WIDTH;
if (newWidth < gridItemMinWidth) {
needFixWidth += gridItemMinWidth - newWidth;
newWidth = gridItemMinWidth;
}
// if can fix width, fix width
if (
newWidth > GRID_ITEM_MIN_WIDTH &&
needFixWidth
) {
if (newWidth > gridItemMinWidth && needFixWidth) {
if (
newWidth - needFixWidth >=
GRID_ITEM_MIN_WIDTH
gridItemMinWidth
) {
newWidth = newWidth - needFixWidth;
needFixWidth = 0;
} else {
needFixWidth =
needFixWidth -
(newWidth - GRID_ITEM_MIN_WIDTH);
newWidth = GRID_ITEM_MIN_WIDTH;
(newWidth - gridItemMinWidth);
newWidth = gridItemMinWidth;
}
}
if (index === children.length - 2) {

View File

@@ -12,10 +12,8 @@ import { debounce, domToRect, Point } from '@toeverything/utils';
import clsx from 'clsx';
import { Protocol } from '@toeverything/datasource/db-service';
const MAX_ITEM_COUNT = 6;
const DB_UPDATE_DELAY = 50;
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
export const GRID_ITEM_MIN_WIDTH = 100 / MAX_ITEM_COUNT;
export const GRID_PROPERTY_KEY = 'gridItemWidth';
export function removePercent(str: string) {
@@ -24,13 +22,14 @@ export function removePercent(str: string) {
export const Grid: FC<CreateView> = function (props) {
const { block, editor } = props;
const gridItemMinWidth = editor.configManager.grid.gridItemMinWidth;
const [isOnDrag, setIsOnDrag] = useState<boolean>(false);
const isSetMouseUp = useRef<boolean>(false);
const gridContainerRef = useRef<HTMLDivElement>();
const mouseStartPoint = useRef<Point>();
const gridItemCountRef = useRef<number>();
const originalLeftWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
const originalRightWidth = useRef<number>(GRID_ITEM_MIN_WIDTH);
const originalLeftWidth = useRef<number>(gridItemMinWidth);
const originalRightWidth = useRef<number>(gridItemMinWidth);
const [alertHandleId, setAlertHandleId] = useState<string>(null);
const getLeftRightGridItemDomByIndex = (index: number) => {
@@ -126,8 +125,8 @@ export const Grid: FC<CreateView> = function (props) {
editor.mouseManager.onMouseupEventOnce(() => {
setIsOnDrag(false);
isSetMouseUp.current = false;
originalLeftWidth.current = GRID_ITEM_MIN_WIDTH;
originalRightWidth.current = GRID_ITEM_MIN_WIDTH;
originalLeftWidth.current = gridItemMinWidth;
originalRightWidth.current = gridItemMinWidth;
mouseStartPoint.current = null;
});
} else {
@@ -153,12 +152,12 @@ export const Grid: FC<CreateView> = function (props) {
const newLeftWidth = originalLeftWidth.current - xDistance;
let newLeftPercent = (newLeftWidth / containerWidth) * 100;
let newRightPercent = Number(totalWidth) - newLeftPercent;
if (newLeftPercent < GRID_ITEM_MIN_WIDTH) {
newLeftPercent = GRID_ITEM_MIN_WIDTH;
newRightPercent = totalWidth - GRID_ITEM_MIN_WIDTH;
} else if (newRightPercent < GRID_ITEM_MIN_WIDTH) {
newRightPercent = GRID_ITEM_MIN_WIDTH;
newLeftPercent = totalWidth - GRID_ITEM_MIN_WIDTH;
if (newLeftPercent < gridItemMinWidth) {
newLeftPercent = gridItemMinWidth;
newRightPercent = totalWidth - gridItemMinWidth;
} else if (newRightPercent < gridItemMinWidth) {
newRightPercent = gridItemMinWidth;
newLeftPercent = totalWidth - gridItemMinWidth;
}
//XXX first change dom style is for animation speed, maybe not a good idea
const newLeft = `${newLeftPercent}%`;
@@ -213,6 +212,7 @@ export const Grid: FC<CreateView> = function (props) {
<GridContainer
className={clsx({ [GRID_ON_DRAG_CLASS]: isOnDrag })}
ref={gridContainerRef}
gridItemMinWidth={gridItemMinWidth}
isOnDrag={isOnDrag}
>
{block.childrenIds.map((id, i) => {
@@ -233,7 +233,8 @@ export const Grid: FC<CreateView> = function (props) {
onMouseDown={event => handleMouseDown(event, i)}
blockId={id}
enabledAddItem={
block.childrenIds.length < MAX_ITEM_COUNT
block.childrenIds.length <
editor.configManager.grid.maxGridItemCount
}
onMouseEnter={event =>
handleHandleMouseEnter(event, i)
@@ -252,24 +253,25 @@ export const Grid: FC<CreateView> = function (props) {
);
};
const GridContainer = styled('div')<{ isOnDrag: boolean }>(
({ isOnDrag, theme }) => ({
position: 'relative',
display: 'flex',
alignItems: 'stretch',
borderRadius: '10px',
border: '1px solid #FFF',
minWidth: `${GRID_ITEM_MIN_WIDTH}%`,
[`&:hover .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
const GridContainer = styled('div')<{
isOnDrag: boolean;
gridItemMinWidth: number;
}>(({ isOnDrag, theme, gridItemMinWidth }) => ({
position: 'relative',
display: 'flex',
alignItems: 'stretch',
borderRadius: '10px',
border: '1px solid #FFF',
minWidth: `${gridItemMinWidth}%`,
[`&:hover .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
},
...(isOnDrag && {
[`& .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
},
...(isOnDrag && {
[`& .${GRID_ITEM_CONTENT_CLASS_NAME}`]: {
borderColor: theme.affine.palette.borderColor,
},
}),
})
);
}),
}));
const GridMask = styled('div')({
position: 'fixed',

View File

@@ -3,7 +3,7 @@ import { Protocol } from '@toeverything/datasource/db-service';
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
import { GridItem } from '../grid-item/GridItem';
import { GridRender } from './GridRender';
export { GRID_ITEM_MIN_WIDTH, GRID_PROPERTY_KEY, removePercent } from './Grid';
export { GRID_PROPERTY_KEY, removePercent } from './Grid';
export class GridBlock extends BaseView {
public override selectable = false;

View File

@@ -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,
},
}),
})

View File

@@ -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',
}));

View File

@@ -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

View File

@@ -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>;
};

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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}

View File

@@ -126,7 +126,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
// block = await editor.commands.blockCommands.createNextBlock(block.id,)
const on_text_view_active = useCallback(
(point: CursorTypes, rang_form?: 'up' | 'down') => {
(point: CursorTypes) => {
// TODO code to be optimized
if (textRef.current) {
const end_selection = textRef.current.getEndSelection();
@@ -146,7 +146,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
.getElementsByClassName('text-paragraph')[0]
.getBoundingClientRect();
if (rang_form === 'up') {
if (blockTop > blockDomStyle.top) {
blockTop = blockDomStyle.bottom - 5;
} else {
blockTop = blockDomStyle.top + 5;
@@ -319,7 +319,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
if (nowPosition.top === startPosition.top) {
editor.selectionManager.activePreviousNode(
block.id,
new Point(nowPosition.left, nowPosition.top - 20)
new Point(nowPosition.left, nowPosition.top)
);
return true;
@@ -357,17 +357,14 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
// The specific amount of TODO needs to be determined after subsequent padding
editor.selectionManager.activeNextNode(
block.id,
new Point(nowPosition.left, nowPosition.bottom + 20)
new Point(nowPosition.left, nowPosition.bottom)
);
return true;
} else {
if (prePosition?.bottom === endPosition.bottom) {
editor.selectionManager.activeNextNode(
block.id,
new Point(
prePosition.left,
prePosition?.bottom + 20
)
new Point(prePosition.left, prePosition?.bottom)
);
return true;
} else {

View File

@@ -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')({

View File

@@ -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: [],
});

View File

@@ -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]
);

View File

@@ -187,7 +187,6 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
)
)
);
const scrollDirections = getScrollDirections(
endPointRef.current,
scrollManager.verticalScrollTriggerDistance,
@@ -204,6 +203,7 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
mouseType.current = 'up';
startPointBlock.current = null;
setShow(false);
setRect(Rect.fromLTRB(0, 0, 0, 0));
scrollManager.stopAutoScroll();
};

View File

@@ -1,22 +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;
};
// TODO: remove
export const WrapperWithPendantAndDragDrop: FC<BlockContentWrapperProps> =
function ({ block, children, editor }) {
return (
<DragDropWrapper block={block} editor={editor}>
<BlockPendantProvider block={block}>
{children}
</BlockPendantProvider>
</DragDropWrapper>
);
};

View File

@@ -1 +0,0 @@
export * from './BlockContentWrapper';

View File

@@ -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);
};

View File

@@ -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')
)

View File

@@ -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 (

View File

@@ -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,
});

View File

@@ -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)
)}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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>
);

View File

@@ -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,
};
};

View File

@@ -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)}`);

View File

@@ -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>;
}

View File

@@ -1 +0,0 @@
export * from './DragDropWrapper';

View File

@@ -159,6 +159,33 @@ export class BlockCommands {
return [];
}
public async moveInNewGridItem(
blockId: string,
gridItemId: string,
isBefore = false
) {
const block = await this._editor.getBlockById(blockId);
if (block) {
const gridItemBlock = await this._editor.createBlock(
Protocol.Block.Type.gridItem
);
const targetGridItemBlock = await this._editor.getBlockById(
gridItemId
);
await block.remove();
await gridItemBlock.append(block);
if (targetGridItemBlock && gridItemBlock) {
if (isBefore) {
await targetGridItemBlock.before(gridItemBlock);
} else {
await targetGridItemBlock.after(gridItemBlock);
}
}
return gridItemBlock;
}
return undefined;
}
public async splitGroupFromBlock(blockId: string) {
const block = await this._editor.getBlockById(blockId);
await splitGroup(this._editor, block);

View File

@@ -0,0 +1,27 @@
import { BlockEditor } from '../..';
/**
*
* the global config for the editor
* @class GridConfig
*/
export class GridConfig {
private _maxGridItemCount = 6;
private _editor: BlockEditor;
constructor(editor: BlockEditor) {
this._editor = editor;
}
get maxGridItemCount() {
return this._maxGridItemCount;
}
set maxGridItemCount(value) {
this._maxGridItemCount = value;
}
get gridItemMinWidth() {
return 100 / this.maxGridItemCount;
}
}

View File

@@ -0,0 +1,23 @@
import { BlockEditor } from '../..';
import { GridConfig } from './grid';
// TODO: if config be complex, add children config abstract
/**
*
* the global config for the editor
* @class EditorConfig
*/
export class EditorConfig {
private _maxGridItemCount = 6;
private _editor: BlockEditor;
private _grid: GridConfig;
constructor(editor: BlockEditor) {
this._editor = editor;
this._grid = new GridConfig(editor);
}
get grid() {
return this._grid;
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { domToRect, Point } from '@toeverything/utils';
import { AsyncBlock } from '../..';
import { GridDropType } from '../commands/types';
@@ -5,6 +6,7 @@ import { Editor } from '../editor';
import { BlockDropPlacement, GroupDirection } from '../types';
// TODO: Evaluate implementing custom events with Rxjs
import EventEmitter from 'eventemitter3';
import { Protocol } from '@toeverything/datasource/db-service';
enum DragType {
dragBlock = 'dragBlock',
@@ -86,6 +88,7 @@ export class DragDropManager {
while (curr !== this._editor.getRootBlockId()) {
if (curr === blockId) return false;
const block = await this._editor.getBlockById(curr);
if (!block) return false;
curr = block.parentId;
}
return true;
@@ -114,6 +117,48 @@ export class DragDropManager {
: GridDropType.right
);
}
if (
[
BlockDropPlacement.outerLeft,
BlockDropPlacement.outerRight,
].includes(this._blockDragDirection)
) {
const targetBlock = await this._editor.getBlockById(
this._blockDragTargetId
);
if (targetBlock.type !== Protocol.Block.Type.grid) {
await this._editor.commands.blockCommands.createLayoutBlock(
blockId,
this._blockDragTargetId,
this._blockDragDirection ===
BlockDropPlacement.outerLeft
? GridDropType.left
: GridDropType.right
);
}
if (targetBlock.type === Protocol.Block.Type.grid) {
const gridItems = await targetBlock.children();
if (
BlockDropPlacement.outerRight ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[gridItems.length - 1].id
);
}
if (
BlockDropPlacement.outerLeft ===
this._blockDragDirection
) {
await this._editor.commands.blockCommands.moveInNewGridItem(
blockId,
gridItems[0].id,
true
);
}
}
}
}
}
@@ -209,6 +254,93 @@ export class DragDropManager {
);
}
/**
*
* check if drag block is out of blocks and return direction
* @param {React.DragEvent<Element>} event
* @return {
* direction: BlockDropPlacement.none, // none, outerLeft, outerRight
* block: undefined, // the block in the same clientY
* isOuter: false, // if is drag over outer
* }
*
* @memberof DragDropManager
*/
public async checkOuterBlockDragTypes(event: React.DragEvent<Element>) {
const { clientX, clientY } = event;
const mousePoint = new Point(clientX, clientY);
const rootBlock = await this._editor.getBlockById(
this._editor.getRootBlockId()
);
let direction = BlockDropPlacement.none;
const rootBlockRect = domToRect(rootBlock.dom);
let targetBlock: AsyncBlock | undefined;
let typesInfo = {
direction: BlockDropPlacement.none,
block: undefined,
isOuter: false,
} as {
direction: BlockDropPlacement;
block: AsyncBlock | undefined;
isOuter: boolean;
};
if (rootBlockRect.isPointLeft(mousePoint)) {
direction = BlockDropPlacement.outerLeft;
typesInfo.isOuter = true;
}
if (rootBlockRect.isPointRight(mousePoint)) {
direction = BlockDropPlacement.outerRight;
typesInfo.isOuter = true;
}
if (direction !== BlockDropPlacement.none) {
const blockList = await this._editor.getBlockListByLevelOrder();
targetBlock = blockList.find(block => {
const domRect = domToRect(block.dom);
const pointChecker =
direction === BlockDropPlacement.outerLeft
? domRect.isPointLeft.bind(domRect)
: domRect.isPointRight.bind(domRect);
return (
block.type !== Protocol.Block.Type.page &&
block.type !== Protocol.Block.Type.group &&
pointChecker(mousePoint)
);
});
if (targetBlock) {
if (targetBlock.type !== Protocol.Block.Type.grid) {
this._setBlockDragDirection(direction);
this._setBlockDragTargetId(targetBlock.id);
typesInfo = {
direction,
block: targetBlock,
isOuter: true,
};
}
if (targetBlock.type === Protocol.Block.Type.grid) {
const children = await targetBlock.children();
if (
children.length <
this._editor.configManager.grid.maxGridItemCount
) {
typesInfo = {
direction,
block: targetBlock,
isOuter: true,
};
}
}
}
}
if (
typesInfo.direction !== BlockDropPlacement.none &&
typesInfo.block
) {
this._setBlockDragTargetId(targetBlock.id);
}
this._setBlockDragDirection(typesInfo.direction);
return typesInfo;
}
public async checkBlockDragTypes(
event: React.DragEvent<Element>,
blockDom: HTMLElement,
@@ -216,10 +348,25 @@ export class DragDropManager {
) {
const { clientX, clientY } = event;
this._setBlockDragTargetId(blockId);
const path = await this._editor.getBlockPath(blockId);
const mousePoint = new Point(clientX, clientY);
const rect = domToRect(blockDom);
/**
* IMP: compute the level of the target block
* future feature drag drop has level support do not delete
* const levelUnderGrid = Array.from(path)
.reverse()
.findIndex(block => block.type === Protocol.Block.Type.gridItem);
const levelUnderGroup = Array.from(path)
.reverse()
.findIndex(block => block.type === Protocol.Block.Type.group);
const blockLevel =
levelUnderGrid > 0 ? levelUnderGrid : levelUnderGroup;
console.log({ blockLevel, levelUnderGrid, levelUnderGroup });
*
*/
let direction = BlockDropPlacement.bottom;
if (mousePoint.x - rect.left <= this._dragBlockHotDistance) {
direction = BlockDropPlacement.left;
}
@@ -236,9 +383,10 @@ export class DragDropManager {
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
const gridBlocks = path.filter(
block => block.type === Protocol.Block.Type.grid
);
// limit grid block floor counts, when drag block to init grid
if (gridBlocks.length >= MAX_GRID_BLOCK_FLOOR) {
direction = BlockDropPlacement.none;
}

View File

@@ -3,6 +3,8 @@ export enum BlockDropPlacement {
right = 'right',
top = 'top',
bottom = 'bottom',
outerLeft = 'outer-left',
outerRight = 'outer-right',
none = 'none',
}

View File

@@ -35,6 +35,7 @@ import { BrowserClipboard } from './clipboard/browser-clipboard';
import { ClipboardPopulator } from './clipboard/clipboard-populator';
import { BlockHelper } from './block/block-helper';
import { DragDropManager } from './drag-drop';
import { EditorConfig } from './config';
export interface EditorCtorProps {
workspace: string;
@@ -56,6 +57,7 @@ export class Editor implements Virgo {
public dragDropManager = new DragDropManager(this);
public commands = new EditorCommands(this);
public blockHelper = new BlockHelper(this);
public configManager = new EditorConfig(this);
public bdCommands: Commands;
public ui_container?: HTMLDivElement;
public version = '0.0.1';
@@ -343,6 +345,23 @@ export class Editor implements Virgo {
return [...blockList, ...(await this.getOffspring(rootBlockId))];
}
async getBlockListByLevelOrder() {
const rootBlockId = this.getRootBlockId();
const rootBlock = await this.getBlockById(rootBlockId);
const blockList: Array<AsyncBlock> = [];
let nextToVisit: Array<AsyncBlock> = rootBlock ? [rootBlock] : [];
while (nextToVisit.length) {
let next: Array<AsyncBlock> = [];
for (const block of nextToVisit) {
const children = await block.children();
blockList.push(block);
next = next.concat(children);
}
nextToVisit = next;
}
return blockList;
}
/**
*
* get all offspring of block
@@ -367,15 +386,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) {

View File

@@ -7,12 +7,6 @@ export * from './commands/types';
export { Editor as BlockEditor } from './editor';
export * from './selection';
export { BlockDropPlacement, HookType, GroupDirection } from './types';
export type {
BlockDomInfo,
Plugin,
PluginCreator,
PluginHooks,
Virgo,
} from './types';
export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types';
export { BaseView, getTextHtml, getTextProperties } from './views/base-view';
export type { ChildrenView, CreateView } from './views/base-view';

View File

@@ -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();

View File

@@ -1,6 +1,5 @@
import { DragEvent } from 'react';
import { Observable, Subject } from 'rxjs';
import { HooksRunner, HookType, BlockDomInfo, PluginHooks } from '../types';
import { HooksRunner, HookType, PluginHooks } from '../types';
export class Hooks implements HooksRunner, PluginHooks {
private _subject: Record<string, Subject<unknown>> = {};
@@ -113,13 +112,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);
}

View File

@@ -30,7 +30,6 @@ export class ScrollManager {
constructor(editor: BlockEditor) {
this._editor = editor;
(window as any).scrollManager = this;
}
private _updateScrollInfo(left: number, top: number) {

View File

@@ -73,6 +73,7 @@ export interface Virgo {
getBlockById(blockId: string): Promise<AsyncBlock | null>;
setHotKeysScope(scope?: string): void;
getBlockList: () => Promise<AsyncBlock[]>;
getBlockListByLevelOrder: () => Promise<AsyncBlock[]>;
// removeBlocks: () => void;
storageManager: StorageManager | undefined;
selection: VirgoSelection;
@@ -177,20 +178,11 @@ 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',
}
export interface BlockDomInfo {
blockId: string;
dom: HTMLElement;
type: BlockFlavorKeys;
rect: DOMRect;
properties: Record<string, unknown>;
}
// Editor's various callbacks, used in Editor
export interface HooksRunner {
init: () => void;
@@ -219,10 +211,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;

View File

@@ -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 () => {

View File

@@ -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';

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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]
);

View File

@@ -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 & {

View File

@@ -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;

View File

@@ -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>
</>
);
};
```

View File

@@ -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,

View File

@@ -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));
};

View File

@@ -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);

View File

@@ -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 =

View File

@@ -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 = () => {};

View File

@@ -0,0 +1 @@
export { useRefPage, RefPageProvider } from './ModalPage';

View File

@@ -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);

View File

@@ -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,
};
});

View File

@@ -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',
};

View File

@@ -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>
);
};

View File

@@ -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',
});
}));

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useMemo } from 'react';
import { Virgo, PluginHooks } from '@toeverything/framework/virgo';
import { Cascader, CascaderItemProps } from '@toeverything/components/ui';
import { TurnIntoMenu } from './TurnIntoMenu';
@@ -18,42 +18,43 @@ interface LeftMenuProps {
}
export function LeftMenu(props: LeftMenuProps) {
const { editor, anchorEl, hooks, blockId } = props;
const menu: CascaderItemProps[] = [
{
title: 'Delete',
callback: () => {
editor.commands.blockCommands.removeBlock(blockId);
const { editor, anchorEl, hooks, blockId, onClose } = props;
const menu: CascaderItemProps[] = useMemo(
() => [
{
title: 'Delete',
callback: () => {
editor.commands.blockCommands.removeBlock(blockId);
},
shortcut: 'Del',
icon: <DeleteCashBinIcon />,
},
shortcut: 'Del',
icon: <DeleteCashBinIcon />,
},
{
title: 'Turn into',
subItems: [],
children: (
<TurnIntoMenu
editor={editor}
hooks={hooks}
blockId={blockId}
onClose={() => {
props.onClose();
editor.selection.setSelectedNodesIds([]);
}}
/>
),
icon: <TurnIntoIcon />,
},
{
title: 'Divide Here As A New Group',
icon: <UngroupIcon />,
callback: () => {
editor.commands.blockCommands.splitGroupFromBlock(blockId);
{
title: 'Turn into',
subItems: [],
children: (
<TurnIntoMenu
editor={editor}
hooks={hooks}
blockId={blockId}
onClose={() => {
onClose();
editor.selection.setSelectedNodesIds([]);
}}
/>
),
icon: <TurnIntoIcon />,
},
},
].filter(v => v);
const [menuList, setMenuList] = useState<CascaderItemProps[]>(menu);
{
title: 'Divide Here As A New Group',
icon: <UngroupIcon />,
callback: () => {
editor.commands.blockCommands.splitGroupFromBlock(blockId);
},
},
],
[editor, hooks, blockId, onClose]
);
// const filterItems = (
// value: string,
@@ -90,7 +91,7 @@ export function LeftMenu(props: LeftMenuProps) {
<>
{props.children}
<Cascader
items={menuList}
items={menu}
anchorEl={anchorEl}
placement="bottom-start"
open={Boolean(anchorEl)}

View File

@@ -6,14 +6,15 @@ import {
type DragEvent,
type ReactNode,
type CSSProperties,
useCallback,
} from 'react';
import {
Virgo,
BlockDomInfo,
PluginHooks,
BlockDropPlacement,
LINE_GAP,
AsyncBlock,
} from '@toeverything/framework/virgo';
import { Button } from '@toeverything/components/common';
import { styled } from '@toeverything/components/ui';
@@ -25,6 +26,11 @@ import { MENU_WIDTH } from './menu-config';
const MENU_BUTTON_OFFSET = 4;
export interface BlockDomInfo {
block: AsyncBlock;
rect: DOMRect;
}
export type LineInfoSubject = Subject<
| {
direction: BlockDropPlacement;
@@ -52,7 +58,6 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
return null;
}
const { direction, blockInfo } = lineInfo;
const finalDirection = direction;
const lineStyle = {
zIndex: 2,
position: 'absolute' as const,
@@ -91,14 +96,14 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
left: intersectionRect.right + 10 - rootRect.x,
};
const styleMap = {
left: leftLineStyle,
right: rightLineStyle,
top: topLineStyle,
bottom: bottomLineStyle,
[BlockDropPlacement.left]: leftLineStyle,
[BlockDropPlacement.right]: rightLineStyle,
[BlockDropPlacement.top]: topLineStyle,
[BlockDropPlacement.bottom]: bottomLineStyle,
[BlockDropPlacement.outerLeft]: leftLineStyle,
[BlockDropPlacement.outerRight]: rightLineStyle,
};
return (
<div className="editor-menu-line" style={styleMap[finalDirection]} />
);
return <div className="editor-menu-line" style={styleMap[direction]} />;
}
function DragComponent(props: {
@@ -139,11 +144,11 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
if (block == null) return;
setRootRect(editor.container.getBoundingClientRect());
const dragImage = await editor.blockHelper.getBlockDragImg(
block.blockId
block.block.id
);
if (dragImage) {
event.dataTransfer.setDragImage(dragImage, -50, -10);
editor.dragDropManager.setDragBlockInfo(event, block.blockId);
editor.dragDropManager.setDragBlockInfo(event, block.block.id);
}
};
@@ -155,16 +160,18 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
const onClick = (event: MouseEvent<Element>) => {
if (block == null) return;
const currentTarget = event.currentTarget;
editor.selection.setSelectedNodesIds([block.blockId]);
editor.selection.setSelectedNodesIds([block.block.id]);
setVisible(true);
setAnchorEl(currentTarget);
};
const onClose = useCallback(() => setAnchorEl(undefined), [setAnchorEl]);
useEffect(() => {
const sub = blockInfo
.pipe(
distinctUntilChanged(
(prev, curr) => prev?.blockId === curr?.blockId
(prev, curr) => prev?.block.id === curr?.block.id
)
)
.subscribe(block => {
@@ -186,7 +193,7 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
setRootRect(editor.container.getBoundingClientRect());
setLine(prev => {
if (
prev?.blockInfo.blockId !== blockInfo.blockId ||
prev?.blockInfo.block.id !== blockInfo.block.id ||
prev?.direction !== direction
) {
return {
@@ -225,8 +232,8 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
anchorEl={anchorEl}
editor={props.editor}
hooks={props.hooks}
onClose={() => setAnchorEl(undefined)}
blockId={block.blockId}
onClose={onClose}
blockId={block.block.id}
>
<Draggable onClick={onClick}>
<HandleChildIcon />

View File

@@ -1,12 +1,16 @@
import { BlockDomInfo, HookType } from '@toeverything/framework/virgo';
import { HookType, BlockDropPlacement } from '@toeverything/framework/virgo';
import { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin';
import { ignoreBlockTypes } from './menu-config';
import { LineInfoSubject, LeftMenuDraggable } from './LeftMenuDraggable';
import {
LineInfoSubject,
LeftMenuDraggable,
BlockDomInfo,
} from './LeftMenuDraggable';
import { PluginRenderRoot } from '../../utils';
import { Subject } from 'rxjs';
import { Subject, throttleTime } from 'rxjs';
import { domToRect, last, Point } from '@toeverything/utils';
const DRAG_THROTTLE_DELAY = 150;
export class LeftMenuPlugin extends BasePlugin {
private _mousedown?: boolean;
private _root?: PluginRenderRoot;
@@ -35,11 +39,7 @@ export class LeftMenuPlugin extends BasePlugin {
.get(HookType.ON_ROOTNODE_MOUSE_UP)
.subscribe(this._handleMouseUp)
);
this.sub.add(
this.hooks
.get(HookType.AFTER_ON_NODE_DRAG_OVER)
.subscribe(this._handleDragOverBlockNode)
);
this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_MOUSE_LEAVE).subscribe(() => {
this._hideLeftMenu();
@@ -60,29 +60,63 @@ export class LeftMenuPlugin extends BasePlugin {
this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_DROP).subscribe(this._onDrop)
);
this.sub.add(
this.hooks
.get(HookType.ON_ROOTNODE_DRAG_OVER)
.pipe(throttleTime(DRAG_THROTTLE_DELAY))
.subscribe(this._handleRootNodeDragover)
);
}
private _handleRootNodeDragover = async (
event: React.DragEvent<Element>
) => {
event.preventDefault();
if (this.editor.dragDropManager.isDragBlock(event)) {
const { direction, block, isOuter } =
await this.editor.dragDropManager.checkOuterBlockDragTypes(
event
);
if (direction !== BlockDropPlacement.none && block && block.dom) {
this._lineInfo.next({
direction,
blockInfo: {
block,
rect: block.dom.getBoundingClientRect(),
},
});
} else if (!isOuter) {
this._handleDragOverBlockNode(event);
} else {
this._lineInfo.next(undefined);
}
}
};
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: {
block,
rect: block.dom.getBoundingClientRect(),
},
});
};
private _handleMouseMove = async (
@@ -129,11 +163,8 @@ export class LeftMenuPlugin extends BasePlugin {
}
}
this._blockInfo.next({
blockId: node.id,
dom: node.dom,
block: node,
rect: node.dom.getBoundingClientRect(),
type: node.type,
properties: node.getProperties(),
});
};

View File

@@ -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',
}));

View File

@@ -116,7 +116,7 @@ export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
};
const handle_close = () => {
editor.blockHelper.removeSearchSlash(block_id);
block_id && editor.blockHelper.removeSearchSlash(block_id);
};
return (

View File

@@ -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',
}));

View File

@@ -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,

View File

@@ -35,7 +35,7 @@ export const Switcher = () => {
active={pageViewMode === DocMode.doc}
onClick={() => switchToPageView(DocMode.doc)}
>
Doc
Paper
</StatusText>
<StatusTrack
mode={pageViewMode}
@@ -51,7 +51,7 @@ export const Switcher = () => {
active={pageViewMode === DocMode.board}
onClick={() => switchToPageView(DocMode.board)}
>
Board
Edgeless
</StatusText>
</StyledContainerForSwitcher>
);

View File

@@ -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,
},
};
});

View File

@@ -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',

View File

@@ -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>
);
})}

View File

@@ -44,7 +44,7 @@ export type DndTreeProps = {
*/
export function DndTree(props: DndTreeProps) {
const {
indentationWidth = 16,
indentationWidth = 12,
collapsible,
removable,
showDragIndicator,

View File

@@ -3,9 +3,9 @@ import React, {
type CSSProperties,
type HTMLAttributes,
} from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import cx from 'clsx';
import { CloseIcon, DocumentIcon } from '@toeverything/components/common';
import { CloseIcon } from '@toeverything/components/common';
import {
ArrowDropDownIcon,
ArrowRightIcon,
@@ -62,7 +62,6 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
ref
) => {
const { workspace_id } = useParams();
const navigate = useNavigate();
const BooleanPageTreeItemMoreActions = useFlag(
'BooleanPageTreeItemMoreActions',
true
@@ -101,20 +100,15 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
<ArrowDropDownIcon />
))}
</Action>
{/*<Action>*/}
{/* <DocumentIcon />*/}
{/*</Action>*/}
<div className={styles['ItemContent']}>
<span
<Link
className={styles['Text']}
{...handleProps}
onClick={() => {
navigate(`/${workspace_id}/${pageId}`);
}}
to={`/${workspace_id}/${pageId}`}
>
{value}
</span>
</Link>
{BooleanPageTreeItemMoreActions && (
<MoreActions
workspaceId={workspace_id}
@@ -161,7 +155,6 @@ export function Action({
style={
{
...style,
// cursor,
'--fill': active?.fill,
'--background': active?.background,
} as CSSProperties

View File

@@ -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 {
@@ -109,6 +105,9 @@
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
appearance: none;
color: unset;
text-decoration: none;
}
.Count {
@@ -167,14 +166,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;

View File

@@ -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',

View File

@@ -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;

View File

@@ -12,7 +12,7 @@ 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 borderRadius =

View File

@@ -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)`

View File

@@ -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: {