增加用于快捷向Scheme添加颜色的上下文菜单。
This commit is contained in:
parent
1db89e57cc
commit
036b9fead6
42
src/components/ContextMenu.module.css
Normal file
42
src/components/ContextMenu.module.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
@layer components {
|
||||
.context_menu_locationer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.action_icon {
|
||||
background-color: transparent;
|
||||
&:hover {
|
||||
background-color: var(--color-neutral-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-neutral-active);
|
||||
}
|
||||
}
|
||||
.menu_body {
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
background-color: var(--color-wumeizi);
|
||||
color: var(--color-yudubai);
|
||||
border: 1px solid var(--color-xuanqing);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding-block: var(--spacing-xs);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 300;
|
||||
.menu_item {
|
||||
width: 100%;
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
&:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-active);
|
||||
}
|
||||
}
|
||||
hr {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
363
src/components/ContextMenu.tsx
Normal file
363
src/components/ContextMenu.tsx
Normal file
|
@ -0,0 +1,363 @@
|
|||
import { SwatchEntry } from 'color-module';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { capitalize, size } from 'lodash-es';
|
||||
import { FC, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MaterialDesign2SchemeSource } from '../material-2-scheme';
|
||||
import {
|
||||
MaterialDesign3DynamicSchemeSource,
|
||||
MaterialDesign3SchemeSource,
|
||||
} from '../material-3-scheme';
|
||||
import { QSchemeSource } from '../q-scheme';
|
||||
import { currentPickedColor } from '../stores/colors';
|
||||
import { activeSchemeAtom, useActiveScheme, useUpdateScheme } from '../stores/schemes';
|
||||
import { SwatchSchemeSource } from '../swatch_scheme';
|
||||
import { ActionIcon } from './ActionIcon';
|
||||
import styles from './ContextMenu.module.css';
|
||||
import { NotificationType, useNotification } from './Notifications';
|
||||
|
||||
interface ContextMenuItemProps {
|
||||
color: string;
|
||||
afterClick?: () => void;
|
||||
}
|
||||
|
||||
const SetPickerMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||
const setCurrentPicker = useSetAtom(currentPickedColor);
|
||||
const handleClickAction = useCallback(() => {
|
||||
setCurrentPicker(color);
|
||||
afterClick?.();
|
||||
}, [afterClick, color]);
|
||||
|
||||
return (
|
||||
<div className={styles.menu_item} onClick={handleClickAction}>
|
||||
Set to default Picker
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const QSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||
const { showToast } = useNotification();
|
||||
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||
const updateSchemeContent = useCallback(
|
||||
(content: keyof QSchemeSource) => {
|
||||
updateScheme((prev) => {
|
||||
prev.schemeStorage.source[content] = color;
|
||||
return prev;
|
||||
});
|
||||
showToast(
|
||||
NotificationType.SUCCESS,
|
||||
`${capitalize(content)} color in active scheme updated.`,
|
||||
'tabler:settings-up',
|
||||
3000,
|
||||
);
|
||||
afterClick?.();
|
||||
},
|
||||
[color, activeSchemeId, updateScheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('primary')}>
|
||||
Set as Primary color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('secondary')}>
|
||||
Set as Secondary color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('tertiary')}>
|
||||
Set as Tertiary color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('accent')}>
|
||||
Set as Accent color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('danger')}>
|
||||
Set as Danger color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('success')}>
|
||||
Set as Success color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('warning')}>
|
||||
Set as Warn color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('info')}>
|
||||
Set as Info color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('foreground')}>
|
||||
Set as Foreground color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('background')}>
|
||||
Set as Background color
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SwatchSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||
const { showToast } = useNotification();
|
||||
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||
const addColorEntry = useCallback(() => {
|
||||
updateScheme((prev) => {
|
||||
const source = prev.schemeStorage.source as SwatchSchemeSource;
|
||||
|
||||
const colorAmount = source.colors.length;
|
||||
const newEntry = new SwatchEntry(`Custom Color ${colorAmount + 1}`, color);
|
||||
source.colors.push(newEntry.toJsValue());
|
||||
|
||||
return prev;
|
||||
});
|
||||
|
||||
showToast(NotificationType.SUCCESS, 'New color entry added.', 'tabler:settings-up', 3000);
|
||||
|
||||
afterClick?.();
|
||||
}, [color, activeSchemeId, updateScheme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<div className={styles.menu_item} onClick={addColorEntry}>
|
||||
Add to swatch color
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Material2SchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||
const { showToast } = useNotification();
|
||||
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||
const updateSchemeColor = useCallback(
|
||||
(content: keyof MaterialDesign2SchemeSource) => {
|
||||
updateScheme((prev) => {
|
||||
prev.schemeStorage.source[content] = color;
|
||||
return prev;
|
||||
});
|
||||
showToast(
|
||||
NotificationType.SUCCESS,
|
||||
`${capitalize(content)} color in active scheme updated.`,
|
||||
'tabler:settings-up',
|
||||
3000,
|
||||
);
|
||||
afterClick?.();
|
||||
},
|
||||
[color, activeSchemeId, updateScheme],
|
||||
);
|
||||
const addToCustomColor = useCallback(() => {
|
||||
updateScheme((prev) => {
|
||||
const source = prev.schemeStorage.source as MaterialDesign2SchemeSource;
|
||||
const colorAmount = size(source.custom_colors);
|
||||
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
|
||||
return prev;
|
||||
});
|
||||
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
|
||||
afterClick?.();
|
||||
}, [color, activeSchemeId, updateScheme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeColor('primary')}>
|
||||
Set as Primary color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeColor('secondary')}>
|
||||
Set as Secondary color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeColor('error')}>
|
||||
Set as Error color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => addToCustomColor()}>
|
||||
Add to Custom colors
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Material3SchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
|
||||
const { showToast } = useNotification();
|
||||
const activeSchemeId = useAtomValue(activeSchemeAtom);
|
||||
const updateScheme = useUpdateScheme(activeSchemeId);
|
||||
const updateSchemeContent = useCallback(
|
||||
(content: keyof MaterialDesign3SchemeSource) => {
|
||||
updateScheme((prev) => {
|
||||
prev.schemeStorage.source[content] = color;
|
||||
return prev;
|
||||
});
|
||||
|
||||
showToast(
|
||||
NotificationType.SUCCESS,
|
||||
`${capitalize(content)} color in active scheme updated.`,
|
||||
'tabler:settings-up',
|
||||
3000,
|
||||
);
|
||||
|
||||
afterClick?.();
|
||||
},
|
||||
[color, activeSchemeId, updateScheme],
|
||||
);
|
||||
const addCustomColor = useCallback(() => {
|
||||
updateScheme((prev) => {
|
||||
const source = prev.schemeStorage.source as
|
||||
| MaterialDesign3DynamicSchemeSource
|
||||
| MaterialDesign3SchemeSource;
|
||||
const colorAmount = size(source.custom_colors);
|
||||
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
|
||||
return prev;
|
||||
});
|
||||
|
||||
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
|
||||
|
||||
afterClick?.();
|
||||
}, [color, activeSchemeId, updateScheme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('source')}>
|
||||
Set as Source color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={() => updateSchemeContent('error')}>
|
||||
Set as Error color
|
||||
</div>
|
||||
<div className={styles.menu_item} onClick={addCustomColor}>
|
||||
Add to Custom colors
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContextMenuProps {
|
||||
color: string;
|
||||
}
|
||||
|
||||
const ContextMenu: FC<ContextMenuProps> = ({ color }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 });
|
||||
const [renderPosition, setRenderPosition] = useState({ x: 0, y: 0 });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const activeScheme = useActiveScheme();
|
||||
|
||||
const handleOpenMenu = () => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
if (triggerRef.current && containerRef.current) {
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const x = triggerRect.left - containerRect.left;
|
||||
const y = triggerRect.bottom - containerRect.top;
|
||||
|
||||
setInitialPosition({ x, y });
|
||||
setRenderPosition({ x, y });
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
const handleCloseMenu = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
const handleLeaveClose = useCallback(
|
||||
(evt: MouseEvent<HTMLDivElement>) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
const relatedTarget = evt.relatedTarget as Node | null;
|
||||
|
||||
if (menuRef.current && menuRef.current.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerRef.current && triggerRef.current.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleCloseMenu();
|
||||
},
|
||||
[handleCloseMenu, isOpen],
|
||||
);
|
||||
|
||||
const schemeMenu = useMemo(() => {
|
||||
const sharedProps: ContextMenuItemProps = {
|
||||
color,
|
||||
afterClick: handleCloseMenu,
|
||||
};
|
||||
switch (activeScheme?.type) {
|
||||
case 'q_scheme':
|
||||
return <QSchemeMenu {...sharedProps} />;
|
||||
case 'swatch_scheme':
|
||||
return <SwatchSchemeMenu {...sharedProps} />;
|
||||
case 'material_2':
|
||||
return <Material2SchemeMenu {...sharedProps} />;
|
||||
case 'material_3':
|
||||
case 'material_3_dynamic':
|
||||
return <Material3SchemeMenu {...sharedProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [activeScheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && menuRef.current && containerRef.current && triggerRef.current) {
|
||||
const menuElemenet = menuRef.current;
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const menuHeight = menuElemenet.offsetHeight;
|
||||
const menuWidth = menuElemenet.offsetWidth;
|
||||
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
const viewportX = containerRect.left + initialPosition.x;
|
||||
const viewportY = containerRect.top + initialPosition.y;
|
||||
|
||||
let adjustedX = initialPosition.x;
|
||||
let adjustedY = initialPosition.y;
|
||||
|
||||
if (viewportX + menuWidth > viewportWidth) {
|
||||
adjustedX = initialPosition.x - menuWidth + triggerRect.width;
|
||||
if (containerRect.left + adjustedX < 0) {
|
||||
adjustedX = -containerRect.left + 5; // 留5px边距
|
||||
}
|
||||
}
|
||||
|
||||
if (viewportY + menuHeight > viewportHeight) {
|
||||
adjustedY = initialPosition.y - menuHeight - triggerRect.height;
|
||||
if (containerRect.top + adjustedY < 0) {
|
||||
adjustedY = -containerRect.top + 5; // 留5px边距
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustedX !== renderPosition.x || adjustedY !== renderPosition.y) {
|
||||
setRenderPosition({ x: adjustedX, y: adjustedY });
|
||||
}
|
||||
}
|
||||
}, [isOpen, initialPosition, renderPosition.x, renderPosition.y]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.context_menu_locationer}
|
||||
ref={containerRef}
|
||||
onMouseLeave={handleLeaveClose}>
|
||||
<ActionIcon
|
||||
icon="tabler:dots-vertical"
|
||||
extendClassName={styles.action_icon}
|
||||
onClick={handleOpenMenu}
|
||||
ref={triggerRef}
|
||||
/>
|
||||
{isOpen && (
|
||||
<div
|
||||
className={styles.menu_body}
|
||||
ref={menuRef}
|
||||
style={{ top: renderPosition.y, left: renderPosition.x }}>
|
||||
<SetPickerMenu color={color} afterClick={handleCloseMenu} />
|
||||
{schemeMenu}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
Loading…
Reference in New Issue
Block a user