DocSpace-client/packages/shared/components/context-menu/sub-components/SubMenu.tsx

436 lines
11 KiB
TypeScript

import React, { useRef, useState, useEffect } from "react";
import { CSSTransition } from "react-transition-group";
import { ReactSVG } from "react-svg";
import { useTheme } from "styled-components";
import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg";
import OutsdideIcon from "PUBLIC_DIR/images/arrow.outside.react.svg";
import {
classNames,
ObjectUtils,
DomHelpers,
isMobile,
isTablet,
} from "../../../utils";
import { ContextMenuSkeleton } from "../../../skeletons/context-menu";
import { Scrollbar, ScrollbarType } from "../../scrollbar";
import { ToggleButton } from "../../toggle-button";
import { SubMenuItem } from "../ContextMenu.styled";
import {
ContextMenuModel,
ContextMenuType,
SeparatorType,
} from "../ContextMenu.types";
const SubMenu = (props: {
model: ContextMenuModel[];
root?: boolean;
className?: string;
resetMenu?: boolean;
onLeafClick?: (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
) => void;
onMobileItemClick?: (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
loadFunc: () => Promise<ContextMenuModel[]>,
) => void;
changeView?: boolean;
onLoad?: () => Promise<ContextMenuModel[]>;
}) => {
const {
onLeafClick,
root,
resetMenu,
changeView,
onMobileItemClick,
onLoad,
} = props;
const [model, setModel] = useState(props?.model);
const [isLoading, setIsLoading] = useState(false);
const [activeItem, setActiveItem] = useState<ContextMenuType | null>(null);
const subMenuRef = useRef<HTMLUListElement>(null);
const theme = useTheme();
const onItemMouseEnter = (e: React.MouseEvent, item: ContextMenuType) => {
if (item.disabled || isTablet() || isMobile()) {
e.preventDefault();
return;
}
setActiveItem(item);
};
const onItemClick = (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
item: ContextMenuType,
) => {
if (item.onLoad) {
e.preventDefault();
if (!isMobile() && !isTablet()) return;
if (isMobile() || isTablet()) onMobileItemClick?.(e, item.onLoad);
else onLeafClick?.(e);
return;
}
const { disabled, url, onClick, items, action } = item;
if (disabled) {
e.preventDefault();
return;
}
if (!url) {
e.preventDefault();
}
if (onClick) {
onClick({
originalEvent: e,
action,
item,
});
}
if (!items) {
onLeafClick?.(e);
} else {
e.stopPropagation();
}
};
const position = () => {
const parentItem = subMenuRef.current?.parentElement;
const containerOffset = DomHelpers.getOffset(parentItem);
const viewport = DomHelpers.getViewport();
const subListWidth = subMenuRef.current?.offsetParent
? subMenuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(subMenuRef.current);
const itemOuterWidth = DomHelpers.getOuterWidth(
parentItem?.children[0] as HTMLElement,
);
const isRtl = theme.interfaceDirection === "rtl";
if (subMenuRef.current) {
subMenuRef.current.style.top = "0px";
if (isRtl) {
if (subListWidth < parseInt(`${containerOffset.left}`, 10)) {
subMenuRef.current.style.left = `${-1 * subListWidth}px`;
} else {
subMenuRef.current.style.left = `${itemOuterWidth}px`;
}
} else if (
parseInt(`${containerOffset.left}`, 10) +
itemOuterWidth +
subListWidth >
viewport.width - DomHelpers.calculateScrollbarWidth()
) {
subMenuRef.current.style.left = `${-1 * subListWidth}px`;
} else {
subMenuRef.current.style.left = `${itemOuterWidth}px`;
}
}
};
const onEnter = async () => {
if (onLoad && model && model.length && model[0].isLoader && !isLoading) {
setIsLoading(true);
const res = await onLoad();
setIsLoading(false);
if (res) setModel(res);
}
position();
};
const isActive = () => {
return !!root || !resetMenu;
};
useEffect(() => {
if (isActive()) {
position();
}
});
const renderSeparator = (index: number, style: React.CSSProperties) => (
<li
key={`separator_${index}`}
className="p-menu-separator not-selectable"
role="separator"
style={style}
/>
);
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 = (
item: ContextMenuType,
index: number,
style: React.CSSProperties,
) => {
if (item.disabled) return;
// TODO: Not render disabled items
const active = activeItem === item;
const className = classNames(
"p-menuitem",
{ "p-menuitem-active": active },
item.className || "",
);
const linkClassName = classNames("p-menuitem-link", "not-selectable", {
"p-disabled": item.disabled || item.disableColor,
});
const iconClassName = classNames("p-menuitem-icon", {
"p-disabled": item.disabled || item.disableColor,
});
const subMenuIconClassName = "p-submenu-icon";
const icon =
item.icon &&
((!item.icon.includes("images/") && !item.icon.includes(".svg")) ||
item.icon.includes("webplugins") ? (
<img
src={item.icon}
alt="plugin icon"
className={iconClassName || ""}
/>
) : (
<ReactSVG
wrapper="span"
className={iconClassName || ""}
src={item.icon}
/>
));
const label = item.label && (
<span className="p-menuitem-text not-selectable">{item.label}</span>
);
const subMenuIcon = (item.items || item.onLoad) && (
<ArrowIcon className={subMenuIconClassName} />
);
const subMenu = renderSubMenu(item);
const dataKeys = Object.fromEntries(
Object.entries(item).filter((el) => el[0].indexOf("data-") === 0),
);
const onClick = (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
) => {
onItemClick(e, item);
};
let content = (
<a
href={item.url || "#"}
className={linkClassName || ""}
target={item.target}
{...dataKeys}
onClick={onClick}
role="menuitem"
>
{icon}
{label}
{subMenuIcon}
{item.isOutsideLink && (
<OutsdideIcon className={subMenuIconClassName} />
)}
</a>
);
if (item.template) {
const defaultContentOptions = {
onClick,
className: linkClassName,
labelClassName: "p-menuitem-text",
iconClassName,
subMenuIconClassName,
element: content,
props,
active,
};
content = ObjectUtils.getJSXElement(
item.template,
item,
defaultContentOptions,
);
}
if (item.withToggle) {
return (
<SubMenuItem
id={item.id}
key={item.key}
role="none"
className={className || ""}
style={{ ...item.style, ...style }}
onMouseEnter={(e) => onItemMouseEnter(e, item)}
>
{content}
{subMenu}
<ToggleButton
isChecked={item.checked || false}
onChange={onClick}
noAnimation
/>
</SubMenuItem>
);
}
return (
<li
id={item.id}
key={item.key}
role="none"
className={className || ""}
style={{ ...item.style, ...style }}
onMouseEnter={(e) => onItemMouseEnter(e, item)}
>
{content}
{subMenu}
</li>
);
};
const renderItem = (
data:
| ContextMenuType
| SeparatorType
| { data: ContextMenuModel[]; index: number; style: React.CSSProperties },
idx: number,
) => {
let item: ContextMenuType | SeparatorType | null =
"data" in data ? null : data;
let index = idx;
let style = {};
if ("data" in data && Array.isArray(data.data)) {
item = data.data[data.index] ? data.data[data.index] : null;
index = data.index;
style = data.style;
}
if (!item) return null;
if (item.isSeparator || !("label" in item))
return (
<React.Fragment key={`fragment_${item.key}`}>
{renderSeparator(index, style)}
</React.Fragment>
);
return (
<React.Fragment key={`fragment_${item.key}`}>
{renderMenuitem(item, index, style)}
</React.Fragment>
);
};
const renderMenu = () => {
if (model) {
if (changeView) {
const newModel = model.filter(
(item: ContextMenuModel) => item && !item.disabled,
);
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 }}
stype={ScrollbarType.mediumBlack}
>
{model.map((item: ContextMenuModel, index: number) => {
if (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) => {
if (item?.disabled) return null;
return renderItem(item, index);
});
}
return null;
};
const className = classNames({ "p-submenu-list": !root });
const submenu = renderMenu();
const active = isActive();
console.log("render", model, active, submenu);
return (
<CSSTransition
nodeRef={subMenuRef}
classNames="p-contextmenusub"
in={active}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit
onEnter={onEnter}
>
<ul ref={subMenuRef} className={`${className} not-selectable`}>
{submenu}
</ul>
</CSSTransition>
);
};
export { SubMenu };