Merge pull request #422 from ONLYOFFICE/feature/new-context-menu

Feature/new context menu
This commit is contained in:
Alexey Safronov 2024-08-14 12:57:58 +04:00 committed by GitHub
commit f1a184f8c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 341 additions and 262 deletions

View File

@ -1256,71 +1256,10 @@ class ContextOptionsStore {
!contextOptions.includes("finalize-version") && !contextOptions.includes("finalize-version") &&
contextOptions.includes("show-version-history"); contextOptions.includes("show-version-history");
const versionActions = isDesktop() const versionActions = onlyShowVersionHistory
? onlyShowVersionHistory ? [
? [
{
id: "option_show-version-history",
key: "show-version-history",
label: t("ShowVersionHistory"),
icon: HistoryReactSvgUrl,
onClick: () =>
this.showVersionHistory(
item.id,
item.security,
item?.requestToken,
),
disabled: false,
},
]
: [
{
id: "option_version",
key: "version",
label: t("VersionHistory"),
icon: HistoryFinalizedReactSvgUrl,
items: [
{
id: "option_finalize-version",
key: "finalize-version",
label: t("FinalizeVersion"),
icon: HistoryFinalizedReactSvgUrl,
onClick: () =>
isEditing
? this.onShowEditingToast(t)
: this.finalizeVersion(item.id, item.security),
disabled: false,
},
{
id: "option_version-history",
key: "show-version-history",
label: t("ShowVersionHistory"),
icon: HistoryReactSvgUrl,
onClick: () =>
this.showVersionHistory(
item.id,
item.security,
item?.requestToken,
),
disabled: false,
},
],
},
]
: [
{ {
id: "option_finalize-version", id: "option_show-version-history",
key: "finalize-version",
label: t("FinalizeVersion"),
icon: HistoryFinalizedReactSvgUrl,
onClick: () =>
isEditing
? this.onShowEditingToast(t)
: this.finalizeVersion(item.id),
disabled: false,
},
{
id: "option_version-history",
key: "show-version-history", key: "show-version-history",
label: t("ShowVersionHistory"), label: t("ShowVersionHistory"),
icon: HistoryReactSvgUrl, icon: HistoryReactSvgUrl,
@ -1332,8 +1271,43 @@ class ContextOptionsStore {
), ),
disabled: false, disabled: false,
}, },
]
: [
{
id: "option_version",
key: "version",
label: t("VersionHistory"),
icon: HistoryFinalizedReactSvgUrl,
items: [
{
id: "option_finalize-version",
key: "finalize-version",
label: t("FinalizeVersion"),
icon: HistoryFinalizedReactSvgUrl,
onClick: () =>
isEditing
? this.onShowEditingToast(t)
: this.finalizeVersion(item.id, item.security),
disabled: false,
},
{
id: "option_version-history",
key: "show-version-history",
label: t("ShowVersionHistory"),
icon: HistoryReactSvgUrl,
onClick: () =>
this.showVersionHistory(
item.id,
item.security,
item?.requestToken,
),
disabled: false,
},
],
},
]; ];
const moveActions = isDesktop()
const moveActions = !isInfoPanel
? [ ? [
{ {
id: "option_move-or-copy", id: "option_move-or-copy",
@ -1815,7 +1789,7 @@ class ContextOptionsStore {
const pluginItems = this.onLoadPlugins(item); const pluginItems = this.onLoadPlugins(item);
if (pluginItems.length > 0) { if (pluginItems.length > 0) {
if (!isDesktop() || pluginItems.length === 1) { if (pluginItems.length === 1) {
pluginItems.forEach((plugin) => { pluginItems.forEach((plugin) => {
options.splice(1, 0, { options.splice(1, 0, {
id: `option_${plugin.key}`, id: `option_${plugin.key}`,

View File

@ -26,21 +26,7 @@
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { Base, TTheme, globalColors } from "../../themes"; import { Base, TTheme, globalColors } from "../../themes";
import { tablet, mobile, getCorrectFourValuesStyle } from "../../utils"; import { mobile, getCorrectFourValuesStyle } from "../../utils";
const styledTabletView = css<{ articleWidth: number }>`
position: fixed;
width: ${(props) => props.theme.newContextMenu.devices.tabletWidth};
max-width: ${(props) => props.theme.newContextMenu.devices.tabletWidth};
max-height: ${(props) => props.theme.newContextMenu.devices.maxHeight};
inset-inline-start: ${(props) =>
props.articleWidth
? `${props.articleWidth}px`
: props.theme.newContextMenu.devices.left};
inset-inline-end: ${(props) => props.theme.newContextMenu.devices.right};
bottom: ${(props) => props.theme.newContextMenu.devices.bottom};
margin: ${(props) => props.theme.newContextMenu.devices.margin};
`;
const styledMobileView = css` const styledMobileView = css`
position: fixed; position: fixed;
@ -97,14 +83,13 @@ const StyledContextMenu = styled.div<{
box-shadow: ${(props) => props.theme.newContextMenu.boxShadow}; box-shadow: ${(props) => props.theme.newContextMenu.boxShadow};
-moz-box-shadow: ${(props) => props.theme.newContextMenu.boxShadow}; -moz-box-shadow: ${(props) => props.theme.newContextMenu.boxShadow};
-webkit-box-shadow: ${(props) => props.theme.newContextMenu.boxShadow}; -webkit-box-shadow: ${(props) => props.theme.newContextMenu.boxShadow};
padding: ${(props) => props.theme.newContextMenu.padding};
@media ${tablet} { .scroll-body {
${(props) => props.changeView && styledTabletView} display: flex;
} flex-direction: column;
justify-content: center;
@media ${mobile} { padding-inline-end: 0 !important;
${(props) => props.changeView && styledMobileView}
} }
} }
@ -205,7 +190,6 @@ const StyledContextMenu = styled.div<{
box-shadow: ${(props) => props.theme.dropDown.boxShadow}; box-shadow: ${(props) => props.theme.dropDown.boxShadow};
-moz-box-shadow: ${(props) => props.theme.dropDown.boxShadow}; -moz-box-shadow: ${(props) => props.theme.dropDown.boxShadow};
-webkit-box-shadow: ${(props) => props.theme.dropDown.boxShadow}; -webkit-box-shadow: ${(props) => props.theme.dropDown.boxShadow};
padding: 4px 0px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -226,7 +210,7 @@ const StyledContextMenu = styled.div<{
position: relative; position: relative;
border: ${(props) => props.theme.dropDownItem.border}; border: ${(props) => props.theme.dropDownItem.border};
margin: ${(props) => props.theme.dropDownItem.margin}; margin: ${(props) => props.theme.dropDownItem.margin};
padding: ${(props) => props.theme.dropDownItem.padding}; padding: 0 16px;
font-family: ${(props) => props.theme.fontFamily}; font-family: ${(props) => props.theme.fontFamily};
font-style: normal; font-style: normal;
background: none; background: none;
@ -241,10 +225,6 @@ const StyledContextMenu = styled.div<{
-webkit-touch-callout: none; -webkit-touch-callout: none;
@media ${tablet} {
padding: 0 16px;
}
&:hover { &:hover {
background-color: ${(props) => background-color: ${(props) =>
props.noHover props.noHover
@ -275,7 +255,7 @@ const StyledContextMenu = styled.div<{
cursor: default !important; cursor: default !important;
margin: ${(props) => props.theme.menuItem.separator.margin}; margin: ${(props) => props.theme.menuItem.separator.margin};
height: ${(props) => props.theme.menuItem.separator.height}; height: ${(props) => props.theme.menuItem.separator.height};
width: ${(props) => props.theme.menuItem.separator.width};
&:hover { &:hover {
cursor: default !important; cursor: default !important;
} }
@ -284,6 +264,14 @@ const StyledContextMenu = styled.div<{
.p-contextmenu .p-menuitem { .p-contextmenu .p-menuitem {
position: relative; position: relative;
margin: ${(props) => props.theme.dropDownItem.margin}; margin: ${(props) => props.theme.dropDownItem.margin};
max-width: calc(-32px + 100vw);
width: fit-content;
min-width: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.p-contextmenu .scroll-body .p-menuitem { .p-contextmenu .scroll-body .p-menuitem {
@ -361,10 +349,47 @@ const StyledContextMenu = styled.div<{
opacity: 1; opacity: 1;
transition: opacity 250ms; transition: opacity 250ms;
} }
.p-contextmenu {
@media ${mobile} {
${(props) => props.changeView && styledMobileView}
}
@media not ${mobile} {
max-width: calc(100vw - 32px);
}
}
.p-contextmenu ul {
@media not ${mobile} {
max-width: calc(100vw - 32px);
}
}
`; `;
StyledContextMenu.defaultProps = { StyledContextMenu.defaultProps = {
theme: Base, theme: Base,
}; };
export const StyledList = styled.ul<{
listHeight: number;
widthSubMenu: null | number;
}>`
& > :first-child {
.scroll-body {
height: ${(props) => `${props.listHeight}px`};
}
}
& > :nth-child(1) {
${(props) =>
props.widthSubMenu &&
css`
.p-menuitem {
max-width: ${`${props.widthSubMenu}px`};
}
`}
}
`;
export { StyledContextMenu }; export { StyledContextMenu };

View File

@ -57,6 +57,8 @@ import {
ContextMenuRefType, ContextMenuRefType,
} from "./ContextMenu.types"; } from "./ContextMenu.types";
const marginBorder = 16; // Indentation from the border of the screen
const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>( const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
(props, ref) => { (props, ref) => {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
@ -65,9 +67,11 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
const [model, setModel] = React.useState<ContextMenuModel[] | null>(null); const [model, setModel] = React.useState<ContextMenuModel[] | null>(null);
const [changeView, setChangeView] = React.useState(false); const [changeView, setChangeView] = React.useState(false);
const [showMobileMenu, setShowMobileMenu] = React.useState(false); const [showMobileMenu, setShowMobileMenu] = React.useState(false);
const [onLoad, setOnLoad] = React.useState< const [mobileSubMenuItems, setMobileSubMenuItems] = React.useState<
undefined | (() => Promise<ContextMenuModel[]>) ContextMenuModel[] | undefined
>(undefined); >([]);
const [mobileHeader, setMobileHeader] = React.useState<string>("");
const [articleWidth, setArticleWidth] = React.useState(0); const [articleWidth, setArticleWidth] = React.useState(0);
const prevReshow = React.useRef(false); const prevReshow = React.useRef(false);
@ -211,7 +215,7 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
? rects.left - currentLeftOffset - currentRightOffset ? rects.left - currentLeftOffset - currentRightOffset
: event.pageX + 1; : event.pageX + 1;
let top = rects ? rects.top : event.pageY + 1; let top = rects ? rects.top : event.pageY + 1;
const width = let width =
menuRef.current && menuRef.current.offsetParent menuRef.current && menuRef.current.offsetParent
? menuRef.current.offsetWidth ? menuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(menuRef.current); : DomHelpers.getHiddenElementOuterWidth(menuRef.current);
@ -221,29 +225,30 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
: DomHelpers.getHiddenElementOuterHeight(menuRef.current); : DomHelpers.getHiddenElementOuterHeight(menuRef.current);
const viewport = DomHelpers.getViewport(); const viewport = DomHelpers.getViewport();
const mobileView =
isMobileUtils() && (height > 210 || ignoreChangeView);
if (!mobileView) {
const options =
menuRef?.current?.getElementsByClassName("p-menuitem");
const optionsWidth: number[] = [];
if (options) {
Array.from(options).forEach((option) =>
optionsWidth.push(option.clientWidth),
);
const widthMaxContent = Math.max(...optionsWidth);
width = widthMaxContent;
}
}
if (theme.interfaceDirection === "rtl" && !rects && left > width) { if (theme.interfaceDirection === "rtl" && !rects && left > width) {
left = event.pageX - width + 1; left = event.pageX - width + 1;
} }
if ( if (mobileView) {
isTabletUtils() &&
(height > 483 ||
(isMobileOnly && window.innerHeight < window.innerWidth))
) {
const article = document.getElementById("article-container");
let currentArticleWidth = 0;
if (article) {
currentArticleWidth = article.offsetWidth;
}
setChangeView(true);
setArticleWidth(currentArticleWidth);
return;
}
if (isMobileUtils() && (height > 210 || ignoreChangeView)) {
setChangeView(true); setChangeView(true);
setArticleWidth(0); setArticleWidth(0);
@ -267,7 +272,8 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
// fit // fit
if (top < document.body.scrollTop) { if (top < document.body.scrollTop) {
top = document.body.scrollTop; if (document.body.scrollTop === 0) top = marginBorder;
else top = document.body.scrollTop;
} }
if (containerRef) { if (containerRef) {
@ -281,8 +287,10 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
} }
} }
if (menuRef.current) { if (menuRef.current) {
menuRef.current.style.left = `${left}px`; menuRef.current.style.left = `${left || marginBorder}px`;
menuRef.current.style.top = `${top}px`; menuRef.current.style.top = `${top}px`;
if (!mobileView) menuRef.current.style.width = `${width}px`;
} }
} }
}; };
@ -410,14 +418,20 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
}; };
}, [documentResizeListener, onHide, visible]); }, [documentResizeListener, onHide, visible]);
const onMobileItemClick = ( const onMobileItemClick = async (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>, e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
label: string,
items?: ContextMenuModel[],
loadFunc?: () => Promise<ContextMenuModel[]>, loadFunc?: () => Promise<ContextMenuModel[]>,
) => { ) => {
e.stopPropagation(); e.stopPropagation();
setShowMobileMenu(true); setShowMobileMenu(true);
if (loadFunc) setOnLoad(loadFunc);
const res = loadFunc ? await loadFunc() : items;
setMobileSubMenuItems(res);
setMobileHeader(label);
}; };
const onBackClick = (e: React.MouseEvent<HTMLDivElement>) => { const onBackClick = (e: React.MouseEvent<HTMLDivElement>) => {
@ -517,7 +531,7 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
)} )}
<Text className="text" truncate dir="auto"> <Text className="text" truncate dir="auto">
{header.title} {showMobileMenu ? mobileHeader : header.title}
</Text> </Text>
</div> </div>
)} )}
@ -527,7 +541,7 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
root root
resetMenu={resetMenu} resetMenu={resetMenu}
onLeafClick={onLeafClick} onLeafClick={onLeafClick}
onLoad={onLoad} mobileSubMenuItems={mobileSubMenuItems}
/> />
) : ( ) : (
<SubMenu <SubMenu
@ -535,8 +549,9 @@ const ContextMenu = React.forwardRef<ContextMenuRefType, ContextMenuProps>(
root root
resetMenu={resetMenu} resetMenu={resetMenu}
onLeafClick={onLeafClick} onLeafClick={onLeafClick}
changeView={changeView}
onMobileItemClick={onMobileItemClick} onMobileItemClick={onMobileItemClick}
changeView={changeView}
withHeader={withHeader}
/> />
)} )}
</div> </div>

View File

@ -44,9 +44,9 @@ const MobileSubMenu = (props: {
onLeafClick: (e: React.MouseEvent) => void; onLeafClick: (e: React.MouseEvent) => void;
root?: boolean; root?: boolean;
resetMenu: boolean; resetMenu: boolean;
onLoad?: () => Promise<ContextMenuModel[]>; mobileSubMenuItems?: ContextMenuModel[];
}) => { }) => {
const { onLeafClick, root, resetMenu, onLoad } = props; const { onLeafClick, root, resetMenu, mobileSubMenuItems } = props;
const [submenu, setSubmenu] = useState<null | ContextMenuModel[]>(null); const [submenu, setSubmenu] = useState<null | ContextMenuModel[]>(null);
@ -91,16 +91,12 @@ const MobileSubMenu = (props: {
} }
}); });
const fetchSubMenu = React.useCallback(async () => {
const res = await onLoad?.();
if (res) setSubmenu(res);
position();
}, [position, setSubmenu, onLoad]);
useEffect(() => { useEffect(() => {
if (onLoad) fetchSubMenu(); if (!mobileSubMenuItems?.length) return;
}, [onLoad, fetchSubMenu]);
setSubmenu(mobileSubMenuItems);
position();
}, [mobileSubMenuItems, mobileSubMenuItems?.length, position]);
const onItemClick = (e: React.MouseEvent, item: ContextMenuType) => { const onItemClick = (e: React.MouseEvent, item: ContextMenuType) => {
const { disabled, url, onClick, items, action } = item; const { disabled, url, onClick, items, action } = item;

View File

@ -33,20 +33,22 @@ import { useTheme } from "styled-components";
import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg"; import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg";
import OutsdideIcon from "PUBLIC_DIR/images/arrow.outside.react.svg"; import OutsdideIcon from "PUBLIC_DIR/images/arrow.outside.react.svg";
import { isMobile as isMobileDevice } from "react-device-detect"; import { isMobile as isMobileDevice } from "react-device-detect";
import { classNames, ObjectUtils, DomHelpers, isMobile } from "../../../utils";
import { classNames, ObjectUtils, DomHelpers } from "../../../utils";
import { ContextMenuSkeleton } from "../../../skeletons/context-menu"; import { ContextMenuSkeleton } from "../../../skeletons/context-menu";
import { Scrollbar } from "../../scrollbar";
import { ToggleButton } from "../../toggle-button"; import { ToggleButton } from "../../toggle-button";
import { Scrollbar } from "../../scrollbar";
import { SubMenuItem } from "../ContextMenu.styled"; import { SubMenuItem, StyledList } from "../ContextMenu.styled";
import { import {
ContextMenuModel, ContextMenuModel,
ContextMenuType, ContextMenuType,
SeparatorType, SeparatorType,
} from "../ContextMenu.types"; } from "../ContextMenu.types";
const submenuListMargin = 4; // Indentation of the second level menu from the first level
const sectionPadding = 16; // Screen margin
const SubMenu = (props: { const SubMenu = (props: {
model: ContextMenuModel[]; model: ContextMenuModel[];
root?: boolean; root?: boolean;
@ -57,23 +59,28 @@ const SubMenu = (props: {
) => void; ) => void;
onMobileItemClick?: ( onMobileItemClick?: (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>, e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
loadFunc: () => Promise<ContextMenuModel[]>, label: string,
items?: ContextMenuModel[],
loadFunc?: () => Promise<ContextMenuModel[]>,
) => void; ) => void;
changeView?: boolean;
onLoad?: () => Promise<ContextMenuModel[]>; onLoad?: () => Promise<ContextMenuModel[]>;
changeView?: boolean;
withHeader?: boolean;
}) => { }) => {
const { const {
onLeafClick, onLeafClick,
root, root,
resetMenu, resetMenu,
changeView,
onMobileItemClick, onMobileItemClick,
onLoad, onLoad,
changeView,
withHeader,
} = props; } = props;
const [model, setModel] = useState(props?.model); const [model, setModel] = useState(props?.model);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [activeItem, setActiveItem] = useState<ContextMenuType | null>(null); const [activeItem, setActiveItem] = useState<ContextMenuType | null>(null);
const [widthSubMenu, setWidthSubMenu] = useState<null | number>(null);
const subMenuRef = useRef<HTMLUListElement>(null); const subMenuRef = useRef<HTMLUListElement>(null);
@ -92,15 +99,19 @@ const SubMenu = (props: {
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>, e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
item: ContextMenuType, item: ContextMenuType,
) => { ) => {
const { url, onClick, items, action } = item; const { disabled, url, onClick, items, action, label } = item;
if (item.onLoad) { if (isMobile() && label && (items || item.onLoad)) {
e.preventDefault(); e.preventDefault();
if (!isMobileDevice) return; if (items) onMobileItemClick?.(e, label as string, items, undefined);
else if (item.onLoad)
onMobileItemClick?.(e, item.onLoad); onMobileItemClick?.(e, label as string, undefined, item.onLoad);
return;
}
if (disabled) {
e.preventDefault();
return; return;
} }
@ -126,7 +137,9 @@ const SubMenu = (props: {
const containerOffset = DomHelpers.getOffset(parentItem); const containerOffset = DomHelpers.getOffset(parentItem);
const viewport = DomHelpers.getViewport(); const viewport = DomHelpers.getViewport();
const subListWidth = subMenuRef.current?.offsetParent const options = subMenuRef.current?.getElementsByClassName("p-menuitem");
let subListWidth = subMenuRef.current?.offsetParent
? subMenuRef.current.offsetWidth ? subMenuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(subMenuRef.current); : DomHelpers.getHiddenElementOuterWidth(subMenuRef.current);
@ -136,42 +149,90 @@ const SubMenu = (props: {
const isRtl = theme.interfaceDirection === "rtl"; const isRtl = theme.interfaceDirection === "rtl";
if (!isMobile() && options) {
const optionsWidth: number[] = [];
Array.from(options).forEach((option) =>
optionsWidth.push(Math.ceil(option.getBoundingClientRect().width)),
);
const widthMaxContent = Math.max(...optionsWidth);
if (root) subListWidth = subListWidth || widthMaxContent;
else if (!subMenuRef?.current?.style.width)
subListWidth = Math.max(subListWidth, widthMaxContent);
}
if (subMenuRef.current) { if (subMenuRef.current) {
subMenuRef.current.style.top = "0px"; let subMenuRefTop = null;
if (!isMobile()) {
if (root) subMenuRef.current.style.width = `${subListWidth}px`;
else if (!subMenuRef?.current?.style.width) {
subMenuRef.current.style.width = `${subListWidth}px`;
}
}
if (!isMobile() && !root) {
const firstList = parentItem?.firstChild as HTMLElement;
const menuItemActive = firstList.querySelector(
".p-menuitem-active",
) as HTMLElement;
const top = menuItemActive.offsetTop;
const scroller = firstList.querySelector(".scroller") as HTMLElement;
const scrollTop = scroller.scrollTop;
const positionActiveItem = top - scrollTop;
subMenuRefTop = positionActiveItem - 2;
subMenuRef.current.style.top = `${subMenuRefTop}px`;
}
const submenuRects = subMenuRef.current.getBoundingClientRect(); const submenuRects = subMenuRef.current.getBoundingClientRect();
if (submenuRects.bottom > viewport.height) { if (submenuRects.bottom > viewport.height && subMenuRefTop) {
const submenuMargin = 16; const submenuMargin = 16;
const topOffset = submenuRects.bottom - viewport.height + submenuMargin;
subMenuRef.current.style.top = `${-1 * topOffset}px`; const topOffset =
subMenuRefTop -
(submenuRects.bottom - viewport.height) -
submenuMargin;
subMenuRef.current.style.top = `${topOffset}px`;
} }
const containerOffsetLeft = parseInt(`${containerOffset.left}`, 10); const containerOffsetLeft = parseInt(`${containerOffset.left}`, 10);
const freeSpaceRight = const freeSpaceRight =
viewport.width - containerOffsetLeft - itemOuterWidth; viewport.width - containerOffsetLeft - itemOuterWidth;
const freeSpaceLeft = containerOffsetLeft; const freeSpaceLeft = containerOffsetLeft;
const submenuListMargin = 4;
const sectionPadding = 17;
if (isRtl) { if (isRtl) {
if (
!root &&
freeSpaceLeft > freeSpaceRight &&
subListWidth > containerOffsetLeft
) {
// If the menu extends beyond the screen
subMenuRef.current.style.width = `${containerOffsetLeft - submenuListMargin - sectionPadding}px`;
}
if ( if (
subListWidth < containerOffsetLeft || subListWidth < containerOffsetLeft ||
(!root && freeSpaceLeft > freeSpaceRight) (!root && freeSpaceLeft > freeSpaceRight)
) { ) {
subMenuRef.current.style.left = `${-1 * subListWidth}px`; subMenuRef.current.style.left = `${-1 * subListWidth}px`;
if (!root && subListWidth > containerOffsetLeft) {
// If the menu extends beyond the screen
const newWidth =
containerOffsetLeft - submenuListMargin - sectionPadding;
subMenuRef.current.style.width = `${newWidth}px`;
setWidthSubMenu(newWidth);
}
} else { } else {
subMenuRef.current.style.left = `${itemOuterWidth}px`; subMenuRef.current.style.left = `${itemOuterWidth}px`;
if (!root) subMenuRef.current.style.marginLeft = `4px`;
if (!root && subListWidth > freeSpaceRight) {
// If the menu extends beyond the screen
const newWidth = freeSpaceRight - 3 * submenuListMargin;
subMenuRef.current.style.width = `${newWidth}px`;
setWidthSubMenu(newWidth);
}
} }
} }
@ -180,8 +241,22 @@ const SubMenu = (props: {
viewport.width - DomHelpers.calculateScrollbarWidth(); viewport.width - DomHelpers.calculateScrollbarWidth();
if (!isRtl) { if (!isRtl) {
if (notEnoughWidthRight && containerOffsetLeft > subListWidth) { if (notEnoughWidthRight && freeSpaceLeft > freeSpaceRight) {
subMenuRef.current.style.left = `${-1 * subListWidth}px`; subMenuRef.current.style.left = `${-1 * subListWidth}px`;
if (!root) subMenuRef.current.style.marginLeft = `-4px`;
if (
notEnoughWidthRight &&
!root &&
subListWidth > containerOffsetLeft
) {
// If the menu extends beyond the screen
const newWidth = containerOffsetLeft - 12;
subMenuRef.current.style.width = `${newWidth}px`;
setWidthSubMenu(newWidth);
}
} else { } else {
subMenuRef.current.style.left = `${itemOuterWidth}px`; subMenuRef.current.style.left = `${itemOuterWidth}px`;
@ -195,6 +270,7 @@ const SubMenu = (props: {
sectionPadding; sectionPadding;
subMenuRef.current.style.width = `${newWidth}px`; subMenuRef.current.style.width = `${newWidth}px`;
setWidthSubMenu(newWidth);
} }
} }
} }
@ -231,29 +307,6 @@ const SubMenu = (props: {
/> />
); );
const renderSubMenu = (item: ContextMenuType) => {
const loaderItem = {
id: "link-loader-option",
key: "link-loader",
isLoader: true,
label: <ContextMenuSkeleton />,
};
if (item.items || item.onLoad) {
return (
<SubMenu
model={item.onLoad ? [loaderItem] : item.items || []}
resetMenu={item !== activeItem}
onLeafClick={onLeafClick}
// onEnter={onEnter}
onLoad={item.onLoad}
/>
);
}
return null;
};
const renderMenuitem = ( const renderMenuitem = (
item: ContextMenuType, item: ContextMenuType,
index: number, index: number,
@ -301,7 +354,7 @@ const SubMenu = (props: {
const subMenuIcon = (item.items || item.onLoad) && ( const subMenuIcon = (item.items || item.onLoad) && (
<ArrowIcon className={subMenuIconClassName} /> <ArrowIcon className={subMenuIconClassName} />
); );
const subMenu = renderSubMenu(item);
const dataKeys = Object.fromEntries( const dataKeys = Object.fromEntries(
Object.entries(item).filter((el) => el[0].indexOf("data-") === 0), Object.entries(item).filter((el) => el[0].indexOf("data-") === 0),
); );
@ -367,7 +420,6 @@ const SubMenu = (props: {
onMouseEnter={(e) => onItemMouseEnter(e, item)} onMouseEnter={(e) => onItemMouseEnter(e, item)}
> >
{content} {content}
{subMenu}
<ToggleButton <ToggleButton
isChecked={item.checked || false} isChecked={item.checked || false}
onChange={onClick} onChange={onClick}
@ -389,7 +441,6 @@ const SubMenu = (props: {
onMouseEnter={(e) => onItemMouseEnter(e, item)} onMouseEnter={(e) => onItemMouseEnter(e, item)}
> >
{content} {content}
{subMenu}
</li> </li>
); );
}; };
@ -428,78 +479,100 @@ const SubMenu = (props: {
}; };
const renderMenu = () => { const renderMenu = () => {
if (model) { if (!model) return null;
if (changeView) {
const newModel = model.filter( return model.map((item: ContextMenuModel, index: number) => {
(item: ContextMenuModel) => item && !item.disabled, if (item?.disabled) return null;
return renderItem(item, index);
});
};
const renderSubMenuLower = () => {
if (!model) return null;
const submenu: JSX.Element[] = [];
const loaderItem = {
id: "link-loader-option",
key: "link-loader",
isLoader: true,
label: <ContextMenuSkeleton />,
};
model.forEach((item) => {
const contextMenuTypeItem = item as ContextMenuType;
if (contextMenuTypeItem?.items || contextMenuTypeItem?.onLoad) {
submenu.push(
<SubMenu
key={`sub-menu_${item.id}`}
model={
contextMenuTypeItem?.onLoad
? [loaderItem]
: contextMenuTypeItem?.items || []
}
resetMenu={item !== activeItem}
onLeafClick={onLeafClick}
onLoad={contextMenuTypeItem?.onLoad}
/>,
); );
const rowHeights: number[] = newModel.map((item: ContextMenuModel) => {
if (!item) return 0;
if (item.isSeparator) return 13;
return 36;
});
// const getItemSize = (index) => rowHeights[index];
const height = rowHeights.reduce((a, b) => a + b);
const viewport = DomHelpers.getViewport();
const listHeight =
height + 61 > viewport.height - 64
? viewport.height - 125
: height + 5;
return (
<Scrollbar style={{ height: listHeight }}>
{model.map((item: ContextMenuModel, index: number) => {
if (!item || item?.disabled) return null;
return renderItem(item, index);
})}
</Scrollbar>
);
// return (
// <VariableSizeList
// height={listHeight}
// width={"auto"}
// itemCount={newModel.length}
// itemSize={getItemSize}
// itemData={newModel}
// outerElementType={CustomScrollbarsVirtualList}
// >
// {renderItem}
// </VariableSizeList>
// );
} }
});
return model.map((item: ContextMenuModel, index: number) => { return submenu;
if (item?.disabled) return null;
return renderItem(item, index);
});
}
return null;
}; };
const className = classNames({ "p-submenu-list": !root }); const className = classNames({ "p-submenu-list": !root });
const submenu = renderMenu(); const submenu = renderMenu();
const active = isActive(); const active = isActive();
const submenuLower = renderSubMenuLower();
return ( if (model.length) {
<CSSTransition const newModel = model.filter(
nodeRef={subMenuRef} (item: ContextMenuModel) => item && !item.disabled,
classNames="p-contextmenusub" );
in={active} const rowHeights: number[] = newModel.map((item: ContextMenuModel) => {
timeout={{ enter: 0, exit: 0 }} if (!item) return 0;
unmountOnExit if (item.isSeparator) return 13;
onEnter={onEnter} return 36;
> });
<ul ref={subMenuRef} className={`${className} not-selectable`}>
{submenu} const height = rowHeights.reduce((a, b) => a + b);
</ul> const viewport = DomHelpers.getViewport();
</CSSTransition> const paddingList = 12;
); const marginsList = 32;
const backdrop = 64;
const header = 55;
const listHeight =
changeView && withHeader
? height + paddingList + header > viewport.height
? viewport.height - backdrop - header - paddingList
: height + paddingList
: height + paddingList + marginsList > viewport.height
? viewport.height - marginsList
: height + paddingList;
return (
<CSSTransition
nodeRef={subMenuRef}
classNames="p-contextmenusub"
in={active}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit
onEnter={onEnter}
>
<StyledList
ref={subMenuRef}
className={`${className} not-selectable`}
listHeight={height + paddingList}
widthSubMenu={widthSubMenu}
>
<Scrollbar style={{ height: listHeight }}>{submenu}</Scrollbar>
{submenuLower}
</StyledList>
</CSSTransition>
);
}
}; };
export { SubMenu }; export { SubMenu };

View File

@ -2511,9 +2511,8 @@ export const getBaseTheme = () => {
}, },
separator: { separator: {
borderBottom: `1px solid ${grayLightMid} !important`, borderBottom: `1px solid ${grayLightMid} !important`,
margin: "6px 16px 6px 16px !important", margin: "6px 16px !important",
height: "1px !important", height: "1px !important",
width: "calc(100% - 32px) !important",
}, },
text: { text: {
header: { header: {
@ -2534,7 +2533,7 @@ export const getBaseTheme = () => {
background: "none", background: "none",
svgFill: black, svgFill: black,
header: { header: {
height: "49px", height: "55px",
borderBottom: `1px solid ${grayLightMid}`, borderBottom: `1px solid ${grayLightMid}`,
marginBottom: "6px", marginBottom: "6px",
}, },
@ -2544,7 +2543,7 @@ export const getBaseTheme = () => {
padding: "0 12px", padding: "0 12px",
mobile: { mobile: {
height: "36px", height: "36px",
padding: "0 16px 6px", padding: "6px 16px",
}, },
}, },
newContextMenu: { newContextMenu: {
@ -2552,7 +2551,6 @@ export const getBaseTheme = () => {
borderRadius: "6px", borderRadius: "6px",
mobileBorderRadius: "6px 6px 0 0", mobileBorderRadius: "6px 6px 0 0",
boxShadow: `0px 8px 16px 0px ${boxShadowColor}`, boxShadow: `0px 8px 16px 0px ${boxShadowColor}`,
padding: "6px 0px",
border: "none", border: "none",
devices: { devices: {
maxHeight: "calc(100vh - 64px)", maxHeight: "calc(100vh - 64px)",

View File

@ -2498,7 +2498,6 @@ const Dark: TTheme = {
borderBottom: `1px solid ${grayDarkStrong} !important`, borderBottom: `1px solid ${grayDarkStrong} !important`,
margin: "6px 16px 6px 16px !important", margin: "6px 16px 6px 16px !important",
height: "1px !important", height: "1px !important",
width: "calc(100% - 32px) !important",
}, },
text: { text: {
header: { header: {
@ -2519,7 +2518,7 @@ const Dark: TTheme = {
background: "none", background: "none",
svgFill: white, svgFill: white,
header: { header: {
height: "49px", height: "55px",
borderBottom: `1px solid ${grayDarkStrong}`, borderBottom: `1px solid ${grayDarkStrong}`,
marginBottom: "6px", marginBottom: "6px",
}, },
@ -2529,7 +2528,7 @@ const Dark: TTheme = {
padding: "0 12px", padding: "0 12px",
mobile: { mobile: {
height: "36px", height: "36px",
padding: "0 16px 6px", padding: "6px 16px",
}, },
}, },
newContextMenu: { newContextMenu: {
@ -2537,7 +2536,6 @@ const Dark: TTheme = {
borderRadius: "6px", borderRadius: "6px",
mobileBorderRadius: "6px 6px 0 0", mobileBorderRadius: "6px 6px 0 0",
boxShadow: `0px 8px 16px 0px ${boxShadowDarkColor}`, boxShadow: `0px 8px 16px 0px ${boxShadowDarkColor}`,
padding: "6px 0px",
border: `1px solid ${grayDarkStrong}`, border: `1px solid ${grayDarkStrong}`,
devices: { devices: {
maxHeight: "calc(100vh - 64px)", maxHeight: "calc(100vh - 64px)",