mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat: modify pivot style & add operation menu to pivot item (#1726)
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
|
||||||
|
import { StyledCollapseItem } from '../shared-styles';
|
||||||
|
|
||||||
|
export const EmptyItem = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return <StyledCollapseItem disable={true}>{t('No item')}</StyledCollapseItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyItem;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { MuiCollapse } from '@affine/component';
|
import { MuiCollapse } from '@affine/component';
|
||||||
import { useTranslation } from '@affine/i18n';
|
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -8,13 +7,13 @@ import { useMemo } from 'react';
|
|||||||
import { workspacePreferredModeAtom } from '../../../../atoms';
|
import { workspacePreferredModeAtom } from '../../../../atoms';
|
||||||
import type { FavoriteListProps } from '../index';
|
import type { FavoriteListProps } from '../index';
|
||||||
import { StyledCollapseItem } from '../shared-styles';
|
import { StyledCollapseItem } from '../shared-styles';
|
||||||
|
import EmptyItem from './empty-item';
|
||||||
export const FavoriteList = ({
|
export const FavoriteList = ({
|
||||||
pageMeta,
|
pageMeta,
|
||||||
openPage,
|
openPage,
|
||||||
showList,
|
showList,
|
||||||
}: FavoriteListProps) => {
|
}: FavoriteListProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
|
||||||
const record = useAtomValue(workspacePreferredModeAtom);
|
const record = useAtomValue(workspacePreferredModeAtom);
|
||||||
|
|
||||||
const favoriteList = useMemo(
|
const favoriteList = useMemo(
|
||||||
@@ -60,9 +59,7 @@ export const FavoriteList = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{favoriteList.length === 0 && (
|
{favoriteList.length === 0 && <EmptyItem />}
|
||||||
<StyledCollapseItem disable={true}>{t('No item')}</StyledCollapseItem>
|
|
||||||
)}
|
|
||||||
</MuiCollapse>
|
</MuiCollapse>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
MenuItem,
|
||||||
|
MuiClickAwayListener,
|
||||||
|
PureMenu,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
DeleteTemporarilyIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
MoveToIcon,
|
||||||
|
PenIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from '@blocksuite/icons';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { toast } from '../../../../utils';
|
||||||
|
|
||||||
|
export const OperationButton = ({
|
||||||
|
onAdd,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
onAdd: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const copyUrl = useCallback(() => {
|
||||||
|
const workspaceId = router.query.workspaceId;
|
||||||
|
navigator.clipboard.writeText(window.location.href);
|
||||||
|
toast(t('Copied link to clipboard'));
|
||||||
|
}, [router.query.workspaceId, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiClickAwayListener
|
||||||
|
onClickAway={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
ref={ref => setAnchorEl(ref)}
|
||||||
|
size="small"
|
||||||
|
className="operation-button"
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
</IconButton>
|
||||||
|
<PureMenu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
placement="bottom-start"
|
||||||
|
open={open && anchorEl !== null}
|
||||||
|
zIndex={11111}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
onAdd();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Add a subpage inside')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<MoveToIcon />} disabled={true}>
|
||||||
|
{t('Move to')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<PenIcon />} disabled={true}>
|
||||||
|
{t('Rename')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<DeleteTemporarilyIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
onDelete();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Move to Trash')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
disabled={true}
|
||||||
|
// onClick={() => {
|
||||||
|
// const workspaceId = router.query.workspaceId;
|
||||||
|
// navigator.clipboard.writeText(window.location.href);
|
||||||
|
// toast(t('Copied link to clipboard'));
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
{t('Copy Link')}
|
||||||
|
</MenuItem>
|
||||||
|
</PureMenu>
|
||||||
|
</div>
|
||||||
|
</MuiClickAwayListener>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MuiCollapse, TreeView } from '@affine/component';
|
import { MuiCollapse, TreeView } from '@affine/component';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
import { ArrowDownSmallIcon, FolderIcon } from '@blocksuite/icons';
|
import { ArrowDownSmallIcon, PivotsIcon } from '@blocksuite/icons';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@@ -9,6 +9,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '../../../../hooks/use-blocksuite-workspace-helper';
|
||||||
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
|
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
|
||||||
import type { RemWorkspace } from '../../../../shared';
|
import type { RemWorkspace } from '../../../../shared';
|
||||||
|
import EmptyItem from '../favorite/empty-item';
|
||||||
import { StyledCollapseButton, StyledListItem } from '../shared-styles';
|
import { StyledCollapseButton, StyledListItem } from '../shared-styles';
|
||||||
import type { TreeNode } from './types';
|
import type { TreeNode } from './types';
|
||||||
import { flattenToTree } from './utils';
|
import { flattenToTree } from './utils';
|
||||||
@@ -197,6 +198,11 @@ export const Pivot = ({
|
|||||||
|
|
||||||
const [showPivot, setShowPivot] = useState(true);
|
const [showPivot, setShowPivot] = useState(true);
|
||||||
|
|
||||||
|
const isPivotEmpty = useMemo(
|
||||||
|
() => allMetas.filter(meta => !meta.trash).length === 0,
|
||||||
|
[allMetas]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledListItem>
|
<StyledListItem>
|
||||||
@@ -208,7 +214,7 @@ export const Pivot = ({
|
|||||||
>
|
>
|
||||||
<ArrowDownSmallIcon />
|
<ArrowDownSmallIcon />
|
||||||
</StyledCollapseButton>
|
</StyledCollapseButton>
|
||||||
<FolderIcon />
|
<PivotsIcon />
|
||||||
{t('Pivots')}
|
{t('Pivots')}
|
||||||
</StyledListItem>
|
</StyledListItem>
|
||||||
|
|
||||||
@@ -220,11 +226,15 @@ export const Pivot = ({
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PivotInternal
|
{isPivotEmpty ? (
|
||||||
currentWorkspace={currentWorkspace}
|
<EmptyItem />
|
||||||
openPage={openPage}
|
) : (
|
||||||
allMetas={allMetas}
|
<PivotInternal
|
||||||
/>
|
currentWorkspace={currentWorkspace}
|
||||||
|
openPage={openPage}
|
||||||
|
allMetas={allMetas}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MuiCollapse>
|
</MuiCollapse>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { ArrowDownSmallIcon, EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import {
|
|
||||||
ArrowDownSmallIcon,
|
|
||||||
EdgelessIcon,
|
|
||||||
// DeleteTemporarilyIcon,
|
|
||||||
// PlusIcon,
|
|
||||||
MoreVerticalIcon,
|
|
||||||
PageIcon,
|
|
||||||
} from '@blocksuite/icons';
|
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
import { workspacePreferredModeAtom } from '../../../../atoms';
|
import { workspacePreferredModeAtom } from '../../../../atoms';
|
||||||
import { StyledCollapseButton, StyledCollapseItem } from '../shared-styles';
|
import { StyledCollapseButton, StyledCollapseItem } from '../shared-styles';
|
||||||
|
import { OperationButton } from './OperationButton';
|
||||||
import type { TreeNode } from './types';
|
import type { TreeNode } from './types';
|
||||||
|
|
||||||
export const TreeNodeRender: TreeNode['render'] = (
|
export const TreeNodeRender: TreeNode['render'] = (
|
||||||
node,
|
node,
|
||||||
{ onAdd, onDelete, collapsed, setCollapsed },
|
{ isOver, onAdd, onDelete, collapsed, setCollapsed },
|
||||||
extendProps
|
extendProps
|
||||||
) => {
|
) => {
|
||||||
const { openPage, pageMeta } = extendProps as {
|
const { openPage, pageMeta } = extendProps as {
|
||||||
@@ -37,6 +30,7 @@ export const TreeNodeRender: TreeNode['render'] = (
|
|||||||
}
|
}
|
||||||
openPage(node.id);
|
openPage(node.id);
|
||||||
}}
|
}}
|
||||||
|
isOver={isOver}
|
||||||
active={active}
|
active={active}
|
||||||
>
|
>
|
||||||
<StyledCollapseButton
|
<StyledCollapseButton
|
||||||
@@ -51,37 +45,7 @@ export const TreeNodeRender: TreeNode['render'] = (
|
|||||||
</StyledCollapseButton>
|
</StyledCollapseButton>
|
||||||
{record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
{record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
||||||
<span>{node.title || 'Untitled'}</span>
|
<span>{node.title || 'Untitled'}</span>
|
||||||
<IconButton
|
<OperationButton onAdd={onAdd} onDelete={onDelete} />
|
||||||
size="small"
|
|
||||||
className="operation-button"
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoreVerticalIcon />
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
{/*<IconButton*/}
|
|
||||||
{/* onClick={e => {*/}
|
|
||||||
{/* e.stopPropagation();*/}
|
|
||||||
{/* onAdd();*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* size="small"*/}
|
|
||||||
{/* className="operation-button"*/}
|
|
||||||
{/*>*/}
|
|
||||||
{/* <PlusIcon />*/}
|
|
||||||
{/*</IconButton>*/}
|
|
||||||
{/*<IconButton*/}
|
|
||||||
{/* onClick={e => {*/}
|
|
||||||
{/* e.stopPropagation();*/}
|
|
||||||
|
|
||||||
{/* onDelete();*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* size="small"*/}
|
|
||||||
{/* className="operation-button"*/}
|
|
||||||
{/*>*/}
|
|
||||||
{/* <DeleteTemporarilyIcon />*/}
|
|
||||||
{/*</IconButton>*/}
|
|
||||||
</StyledCollapseItem>
|
</StyledCollapseItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
import { alpha, displayFlex, styled, textEllipsis } from '@affine/component';
|
||||||
|
|
||||||
export const StyledListItem = styled('div')<{
|
export const StyledListItem = styled('div')<{
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
@@ -53,10 +53,11 @@ export const StyledCollapseButton = styled('button')<{
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyledCollapseItem = styled('button')<{
|
export const StyledCollapseItem = styled('div')<{
|
||||||
disable?: boolean;
|
disable?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}>(({ disable = false, active = false, theme }) => {
|
isOver?: boolean;
|
||||||
|
}>(({ disable = false, active = false, theme, isOver }) => {
|
||||||
return {
|
return {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
@@ -70,6 +71,7 @@ export const StyledCollapseItem = styled('button')<{
|
|||||||
? theme.colors.primaryColor
|
? theme.colors.primaryColor
|
||||||
: theme.colors.textColor,
|
: theme.colors.textColor,
|
||||||
cursor: disable ? 'not-allowed' : 'pointer',
|
cursor: disable ? 'not-allowed' : 'pointer',
|
||||||
|
background: isOver ? alpha(theme.colors.primaryColor, 0.06) : '',
|
||||||
|
|
||||||
span: {
|
span: {
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
@@ -83,7 +85,7 @@ export const StyledCollapseItem = styled('button')<{
|
|||||||
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
|
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||||
},
|
},
|
||||||
'.operation-button': {
|
'.operation-button': {
|
||||||
display: 'none',
|
visibility: 'hidden',
|
||||||
},
|
},
|
||||||
|
|
||||||
':hover': disable
|
':hover': disable
|
||||||
@@ -91,7 +93,7 @@ export const StyledCollapseItem = styled('button')<{
|
|||||||
: {
|
: {
|
||||||
backgroundColor: theme.colors.hoverBackground,
|
backgroundColor: theme.colors.hoverBackground,
|
||||||
'.operation-button': {
|
'.operation-button': {
|
||||||
display: 'flex',
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const StyledSliderBar = styled('div')<{ show: boolean }>(
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import {
|
import {
|
||||||
|
alpha,
|
||||||
createTheme as createMuiTheme,
|
createTheme as createMuiTheme,
|
||||||
css,
|
css,
|
||||||
keyframes,
|
keyframes,
|
||||||
@@ -11,7 +12,7 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import type { AffineTheme } from './types';
|
import type { AffineTheme } from './types';
|
||||||
|
|
||||||
export { css, keyframes, styled };
|
export { alpha, css, keyframes, styled };
|
||||||
|
|
||||||
export const ThemeProvider = ({
|
export const ThemeProvider = ({
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ export type IconMenuProps = PropsWithChildren<{
|
|||||||
isDir?: boolean;
|
isDir?: boolean;
|
||||||
icon?: ReactElement;
|
icon?: ReactElement;
|
||||||
iconSize?: [number, number];
|
iconSize?: [number, number];
|
||||||
|
disabled?: boolean;
|
||||||
}> &
|
}> &
|
||||||
HTMLAttributes<HTMLButtonElement>;
|
HTMLAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
|
export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
|
||||||
({ isDir = false, icon, iconSize, children, ...props }, ref) => {
|
({ isDir = false, icon, iconSize, children, ...props }, ref) => {
|
||||||
const [iconWidth, iconHeight] = iconSize || [16, 16];
|
const [iconWidth, iconHeight] = iconSize || [20, 20];
|
||||||
return (
|
return (
|
||||||
<StyledMenuItem ref={ref} {...props}>
|
<StyledMenuItem ref={ref} {...props}>
|
||||||
{icon &&
|
{icon &&
|
||||||
@@ -19,7 +20,7 @@ export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
|
|||||||
width: iconWidth,
|
width: iconWidth,
|
||||||
height: iconHeight,
|
height: iconHeight,
|
||||||
style: {
|
style: {
|
||||||
marginRight: 14,
|
marginRight: 12,
|
||||||
...icon.props?.style,
|
...icon.props?.style,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
|||||||
20
packages/component/src/ui/menu/PureMenu.tsx
Normal file
20
packages/component/src/ui/menu/PureMenu.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
import type { PurePopperProps } from '../popper';
|
||||||
|
import { PurePopper } from '../popper';
|
||||||
|
import { StyledMenuWrapper } from './styles';
|
||||||
|
|
||||||
|
export const PureMenu = ({
|
||||||
|
children,
|
||||||
|
placement,
|
||||||
|
width,
|
||||||
|
...otherProps
|
||||||
|
}: PurePopperProps & { width?: CSSProperties['width'] }) => {
|
||||||
|
return (
|
||||||
|
<PurePopper placement={placement} {...otherProps}>
|
||||||
|
<StyledMenuWrapper width={width} placement={placement}>
|
||||||
|
{children}
|
||||||
|
</StyledMenuWrapper>
|
||||||
|
</PurePopper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './Menu';
|
export * from './Menu';
|
||||||
// export { StyledMenuItem as MenuItem } from './styles';
|
// export { StyledMenuItem as MenuItem } from './styles';
|
||||||
export * from './MenuItem';
|
export * from './MenuItem';
|
||||||
|
export * from './PureMenu';
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export const StyledArrow = styled(ArrowRightSmallIcon)({
|
|||||||
|
|
||||||
export const StyledMenuItem = styled('button')<{
|
export const StyledMenuItem = styled('button')<{
|
||||||
isDir?: boolean;
|
isDir?: boolean;
|
||||||
}>(({ theme, isDir = false }) => {
|
disabled?: boolean;
|
||||||
|
}>(({ theme, isDir = false, disabled = false }) => {
|
||||||
return {
|
return {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
@@ -39,10 +40,25 @@ export const StyledMenuItem = styled('button')<{
|
|||||||
cursor: isDir ? 'pointer' : '',
|
cursor: isDir ? 'pointer' : '',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
color: theme.colors.textColor,
|
color: disabled ? theme.colors.disableColor : theme.colors.textColor,
|
||||||
':hover': {
|
svg: {
|
||||||
color: theme.colors.primaryColor,
|
color: disabled ? theme.colors.disableColor : theme.colors.iconColor,
|
||||||
backgroundColor: theme.colors.hoverBackground,
|
|
||||||
},
|
},
|
||||||
|
...(disabled
|
||||||
|
? {
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
|
||||||
|
':hover': disabled
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
color: theme.colors.primaryColor,
|
||||||
|
backgroundColor: theme.colors.hoverBackground,
|
||||||
|
svg: {
|
||||||
|
color: theme.colors.primaryColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getArrowStyle = (
|
const getArrowStyle = (
|
||||||
placement: PopperArrowProps['placement'],
|
placement: PopperArrowProps['placement'] = 'bottom',
|
||||||
backgroundColor: CSSProperties['backgroundColor']
|
backgroundColor: CSSProperties['backgroundColor']
|
||||||
) => {
|
) => {
|
||||||
if (placement.indexOf('bottom') === 0) {
|
if (placement.indexOf('bottom') === 0) {
|
||||||
@@ -72,7 +72,7 @@ const getArrowStyle = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StyledArrow = styled('span')<{
|
const StyledArrow = styled('span')<{
|
||||||
placement: PopperArrowProps['placement'];
|
placement?: PopperArrowProps['placement'];
|
||||||
}>(({ placement, theme }) => {
|
}>(({ placement, theme }) => {
|
||||||
return {
|
return {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
import PopperUnstyled from '@mui/base/PopperUnstyled';
|
import PopperUnstyled from '@mui/base/PopperUnstyled';
|
||||||
import Grow from '@mui/material/Grow';
|
import Grow from '@mui/material/Grow';
|
||||||
|
import type { CSSProperties, PointerEvent } from 'react';
|
||||||
import {
|
import {
|
||||||
cloneElement,
|
cloneElement,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -33,6 +34,8 @@ export const Popper = ({
|
|||||||
popperHandlerRef,
|
popperHandlerRef,
|
||||||
onClick,
|
onClick,
|
||||||
onClickAway,
|
onClickAway,
|
||||||
|
onPointerEnter,
|
||||||
|
onPointerLeave,
|
||||||
...popperProps
|
...popperProps
|
||||||
}: PopperProps) => {
|
}: PopperProps) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<VirtualElement>();
|
const [anchorEl, setAnchorEl] = useState<VirtualElement>();
|
||||||
@@ -58,7 +61,8 @@ export const Popper = ({
|
|||||||
);
|
);
|
||||||
}, [trigger]);
|
}, [trigger]);
|
||||||
|
|
||||||
const onPointerEnterHandler = () => {
|
const onPointerEnterHandler = (e: PointerEvent<HTMLDivElement>) => {
|
||||||
|
onPointerEnter?.(e);
|
||||||
if (!hasHoverTrigger || visibleControlledByParent) {
|
if (!hasHoverTrigger || visibleControlledByParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -69,7 +73,9 @@ export const Popper = ({
|
|||||||
}, pointerEnterDelay);
|
}, pointerEnterDelay);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerLeaveHandler = () => {
|
const onPointerLeaveHandler = (e: PointerEvent<HTMLDivElement>) => {
|
||||||
|
onPointerLeave?.(e);
|
||||||
|
|
||||||
if (!hasHoverTrigger || visibleControlledByParent) {
|
if (!hasHoverTrigger || visibleControlledByParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +157,7 @@ export const Popper = ({
|
|||||||
onPointerLeave={onPointerLeaveHandler}
|
onPointerLeave={onPointerLeaveHandler}
|
||||||
style={popoverStyle}
|
style={popoverStyle}
|
||||||
className={popoverClassName}
|
className={popoverClassName}
|
||||||
onClick={e => {
|
onClick={() => {
|
||||||
if (hasClickTrigger && !visibleControlledByParent) {
|
if (hasClickTrigger && !visibleControlledByParent) {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
@@ -178,11 +184,11 @@ const Container = styled('div')({
|
|||||||
display: 'contents',
|
display: 'contents',
|
||||||
});
|
});
|
||||||
|
|
||||||
const BasicStyledPopper = styled(PopperUnstyled, {
|
export const BasicStyledPopper = styled(PopperUnstyled, {
|
||||||
shouldForwardProp: (propName: string) =>
|
shouldForwardProp: (propName: string) =>
|
||||||
!['zIndex'].some(name => name === propName),
|
!['zIndex'].some(name => name === propName),
|
||||||
})<{
|
})<{
|
||||||
zIndex?: number;
|
zIndex?: CSSProperties['zIndex'];
|
||||||
}>(({ zIndex, theme }) => {
|
}>(({ zIndex, theme }) => {
|
||||||
return {
|
return {
|
||||||
zIndex: zIndex ?? theme.zIndex.popover,
|
zIndex: zIndex ?? theme.zIndex.popover,
|
||||||
|
|||||||
67
packages/component/src/ui/popper/PurePopper.tsx
Normal file
67
packages/component/src/ui/popper/PurePopper.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { PopperUnstyledProps } from '@mui/base/PopperUnstyled';
|
||||||
|
import Grow from '@mui/material/Grow';
|
||||||
|
import type { CSSProperties, PropsWithChildren } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { PopperArrow } from './PopoverArrow';
|
||||||
|
import { BasicStyledPopper } from './Popper';
|
||||||
|
import { PopperWrapper } from './styles';
|
||||||
|
|
||||||
|
export type PurePopperProps = {
|
||||||
|
zIndex?: CSSProperties['zIndex'];
|
||||||
|
|
||||||
|
offset?: [number, number];
|
||||||
|
|
||||||
|
showArrow?: boolean;
|
||||||
|
} & PopperUnstyledProps &
|
||||||
|
PropsWithChildren;
|
||||||
|
|
||||||
|
export const PurePopper = (props: PurePopperProps) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
zIndex,
|
||||||
|
offset,
|
||||||
|
showArrow = false,
|
||||||
|
modifiers = [],
|
||||||
|
placement,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
const [arrowRef, setArrowRef] = useState<HTMLElement | null>();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return (
|
||||||
|
<BasicStyledPopper
|
||||||
|
zIndex={zIndex}
|
||||||
|
transition
|
||||||
|
modifiers={[
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arrow',
|
||||||
|
enabled: showArrow,
|
||||||
|
options: {
|
||||||
|
element: arrowRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...modifiers,
|
||||||
|
]}
|
||||||
|
placement={placement}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{({ TransitionProps }) => (
|
||||||
|
<Grow {...TransitionProps}>
|
||||||
|
<PopperWrapper>
|
||||||
|
{showArrow && (
|
||||||
|
<PopperArrow placement={placement} ref={setArrowRef} />
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</PopperWrapper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</BasicStyledPopper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './interface';
|
export * from './interface';
|
||||||
export * from './Popper';
|
export * from './Popper';
|
||||||
|
export * from './PurePopper';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export type PopperHandler = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PopperArrowProps = {
|
export type PopperArrowProps = {
|
||||||
placement: PopperPlacementType;
|
placement?: PopperPlacementType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PopperProps = {
|
export type PopperProps = {
|
||||||
|
|||||||
7
packages/component/src/ui/popper/styles.ts
Normal file
7
packages/component/src/ui/popper/styles.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { styled } from '../../styles';
|
||||||
|
|
||||||
|
export const PopperWrapper = styled('div')(() => {
|
||||||
|
return {
|
||||||
|
position: 'relative',
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StyledCollapse,
|
StyledCollapse,
|
||||||
StyledNodeLine,
|
StyledNodeLine,
|
||||||
StyledTreeNodeContainer,
|
StyledTreeNodeContainer,
|
||||||
StyledTreeNodeItem,
|
StyledTreeNodeWrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import type { Node, NodeLIneProps, TreeNodeProps } from './types';
|
import type {
|
||||||
|
Node,
|
||||||
|
NodeLIneProps,
|
||||||
|
TreeNodeItemProps,
|
||||||
|
TreeNodeProps,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
const NodeLine = <N,>({
|
const NodeLine = <N,>({
|
||||||
node,
|
node,
|
||||||
@@ -39,30 +44,21 @@ const NodeLine = <N,>({
|
|||||||
|
|
||||||
return <StyledNodeLine ref={drop} show={isOver && allowDrop} isTop={isTop} />;
|
return <StyledNodeLine ref={drop} show={isOver && allowDrop} isTop={isTop} />;
|
||||||
};
|
};
|
||||||
|
const TreeNodeItem = <N,>({
|
||||||
export const TreeNode = <N,>({
|
|
||||||
node,
|
node,
|
||||||
index,
|
allowDrop,
|
||||||
allDrop = true,
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TreeNodeProps<N>) => {
|
}: TreeNodeItemProps<N>) => {
|
||||||
const { onAdd, onDelete, onDrop, indent } = otherProps;
|
const { onAdd, onDelete, onDrop } = otherProps;
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
|
||||||
type: 'node',
|
|
||||||
item: node,
|
|
||||||
collect: monitor => ({
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const [{ canDrop, isOver }, drop] = useDrop(
|
const [{ canDrop, isOver }, drop] = useDrop(
|
||||||
() => ({
|
() => ({
|
||||||
accept: 'node',
|
accept: 'node',
|
||||||
drop: (item: Node<N>, monitor) => {
|
drop: (item: Node<N>, monitor) => {
|
||||||
const didDrop = monitor.didDrop();
|
const didDrop = monitor.didDrop();
|
||||||
if (didDrop || item.id === node.id || !allDrop) {
|
if (didDrop || item.id === node.id || !allowDrop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDrop?.(item, node, {
|
onDrop?.(item, node, {
|
||||||
@@ -73,42 +69,75 @@ export const TreeNode = <N,>({
|
|||||||
},
|
},
|
||||||
collect: monitor => ({
|
collect: monitor => ({
|
||||||
isOver: monitor.isOver(),
|
isOver: monitor.isOver(),
|
||||||
canDrop: monitor.canDrop(),
|
canDrop: monitor.canDrop() && allowDrop,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
[onDrop, allDrop]
|
[onDrop, allowDrop]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOver && canDrop) {
|
||||||
|
setCollapsed(false);
|
||||||
|
}
|
||||||
|
}, [isOver, canDrop]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={drop}>
|
||||||
|
{node.render?.(node, {
|
||||||
|
isOver: !!(isOver && canDrop),
|
||||||
|
onAdd: () => onAdd?.(node),
|
||||||
|
onDelete: () => onDelete?.(node),
|
||||||
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TreeNode = <N,>({
|
||||||
|
node,
|
||||||
|
index,
|
||||||
|
allowDrop = true,
|
||||||
|
...otherProps
|
||||||
|
}: TreeNodeProps<N>) => {
|
||||||
|
const { indent } = otherProps;
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
|
type: 'node',
|
||||||
|
item: node,
|
||||||
|
collect: monitor => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTreeNodeContainer ref={drag} isDragging={isDragging}>
|
<StyledTreeNodeContainer ref={drag} isDragging={isDragging}>
|
||||||
<StyledTreeNodeItem
|
<StyledTreeNodeWrapper>
|
||||||
ref={drop}
|
|
||||||
isOver={isOver && !isDragging}
|
|
||||||
canDrop={canDrop}
|
|
||||||
>
|
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<NodeLine
|
<NodeLine
|
||||||
node={node}
|
node={node}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
allowDrop={!isDragging && allDrop}
|
allowDrop={!isDragging && allowDrop}
|
||||||
isTop={true}
|
isTop={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{node.render?.(node, {
|
<TreeNodeItem
|
||||||
onAdd: () => onAdd?.(node),
|
node={node}
|
||||||
onDelete: () => onDelete?.(node),
|
index={index}
|
||||||
collapsed,
|
allowDrop={allowDrop}
|
||||||
setCollapsed,
|
collapsed={collapsed}
|
||||||
})}
|
setCollapsed={setCollapsed}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
{(!node.children?.length || collapsed) && (
|
{(!node.children?.length || collapsed) && (
|
||||||
<NodeLine
|
<NodeLine
|
||||||
node={node}
|
node={node}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
allowDrop={!isDragging && allDrop}
|
allowDrop={!isDragging && allowDrop}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledTreeNodeItem>
|
</StyledTreeNodeWrapper>
|
||||||
|
|
||||||
<StyledCollapse in={!collapsed} indent={indent}>
|
<StyledCollapse in={!collapsed} indent={indent}>
|
||||||
{node.children &&
|
{node.children &&
|
||||||
node.children.map((childNode, index) => (
|
node.children.map((childNode, index) => (
|
||||||
@@ -116,7 +145,7 @@ export const TreeNode = <N,>({
|
|||||||
key={childNode.id}
|
key={childNode.id}
|
||||||
node={childNode}
|
node={childNode}
|
||||||
index={index}
|
index={index}
|
||||||
allDrop={isDragging ? false : allDrop}
|
allowDrop={isDragging ? false : allowDrop}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import MuiCollapse from '@mui/material/Collapse';
|
import MuiCollapse from '@mui/material/Collapse';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
import { styled } from '../../styles';
|
import { alpha, styled } from '../../styles';
|
||||||
|
|
||||||
export const StyledCollapse = styled(MuiCollapse)<{
|
export const StyledCollapse = styled(MuiCollapse)<{
|
||||||
indent?: CSSProperties['paddingLeft'];
|
indent?: CSSProperties['paddingLeft'];
|
||||||
@@ -10,12 +10,8 @@ export const StyledCollapse = styled(MuiCollapse)<{
|
|||||||
paddingLeft: indent,
|
paddingLeft: indent,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
export const StyledTreeNodeItem = styled('div')<{
|
export const StyledTreeNodeWrapper = styled('div')(() => {
|
||||||
isOver?: boolean;
|
|
||||||
canDrop?: boolean;
|
|
||||||
}>(({ isOver, canDrop, theme }) => {
|
|
||||||
return {
|
return {
|
||||||
background: isOver && canDrop ? theme.colors.hoverBackground : '',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -23,6 +19,7 @@ export const StyledTreeNodeContainer = styled('div')<{ isDragging: boolean }>(
|
|||||||
({ isDragging, theme }) => {
|
({ isDragging, theme }) => {
|
||||||
return {
|
return {
|
||||||
background: isDragging ? theme.colors.hoverBackground : '',
|
background: isDragging ? theme.colors.hoverBackground : '',
|
||||||
|
// opacity: isDragging ? 0.4 : 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -32,11 +29,14 @@ export const StyledNodeLine = styled('div')<{ show: boolean; isTop?: boolean }>(
|
|||||||
return {
|
return {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '0',
|
left: '0',
|
||||||
...(isTop ? { top: '0' } : { bottom: '0' }),
|
...(isTop ? { top: '-1px' } : { bottom: '-1px' }),
|
||||||
width: '100%',
|
width: '100%',
|
||||||
paddingTop: '3px',
|
paddingTop: '2x',
|
||||||
borderBottom: '3px solid',
|
borderTop: '2px solid',
|
||||||
borderColor: show ? theme.colors.primaryColor : 'transparent',
|
borderColor: show ? theme.colors.primaryColor : 'transparent',
|
||||||
|
boxShadow: show
|
||||||
|
? `0px 0px 8px ${alpha(theme.colors.primaryColor, 0.35)}`
|
||||||
|
: 'none',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type Node<N> = {
|
|||||||
render?: (
|
render?: (
|
||||||
node: Node<N>,
|
node: Node<N>,
|
||||||
eventsAndStatus: {
|
eventsAndStatus: {
|
||||||
|
isOver: boolean;
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
@@ -33,9 +34,14 @@ type CommonProps<N> = {
|
|||||||
export type TreeNodeProps<N> = {
|
export type TreeNodeProps<N> = {
|
||||||
node: Node<N>;
|
node: Node<N>;
|
||||||
index: number;
|
index: number;
|
||||||
allDrop?: boolean;
|
allowDrop?: boolean;
|
||||||
} & CommonProps<N>;
|
} & CommonProps<N>;
|
||||||
|
|
||||||
|
export type TreeNodeItemProps<N> = {
|
||||||
|
collapsed: boolean;
|
||||||
|
setCollapsed: (collapsed: boolean) => void;
|
||||||
|
} & TreeNodeProps<N>;
|
||||||
|
|
||||||
export type TreeViewProps<N> = {
|
export type TreeViewProps<N> = {
|
||||||
data: Node<N>[];
|
data: Node<N>[];
|
||||||
} & CommonProps<N>;
|
} & CommonProps<N>;
|
||||||
|
|||||||
@@ -194,5 +194,8 @@
|
|||||||
"Please make sure you are online": "Please make sure you are online",
|
"Please make sure you are online": "Please make sure you are online",
|
||||||
"Workspace Owner": "Workspace Owner",
|
"Workspace Owner": "Workspace Owner",
|
||||||
"Members": "Members",
|
"Members": "Members",
|
||||||
"Pivots": "Pivots"
|
"Pivots": "Pivots",
|
||||||
|
"Add a subpage inside": "Add a subpage inside",
|
||||||
|
"Rename": "Rename",
|
||||||
|
"Move to": "Move to"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user