Web: Components: ContextMenu: Refactoring

This commit is contained in:
Ilya Oleshko 2021-11-20 00:17:25 +03:00
parent 56ecbc95ea
commit c10944f8fa
2 changed files with 281 additions and 314 deletions

View File

@ -1,266 +1,11 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import DomHelpers from "../utils/domHelpers";
import ObjectUtils from "../utils/objectUtils";
import { classNames } from "../utils/classNames";
import { CSSTransition } from "react-transition-group";
import { ReactSVG } from "react-svg";
import Portal from "../portal";
import StyledContextMenu from "./styled-context-menu";
import ArrowIcon from "./svg/arrow.right.react.svg";
class ContextMenuSub extends Component {
constructor(props) {
super(props);
this.state = {
activeItem: null,
};
this.onEnter = this.onEnter.bind(this);
this.submenuRef = React.createRef();
}
onItemMouseEnter(e, item) {
if (item.disabled) {
e.preventDefault();
return;
}
this.setState({
activeItem: item,
});
}
onItemClick(item, e) {
if (item.disabled) {
e.preventDefault();
return;
}
if (!item.url) {
e.preventDefault();
}
if (item.onClick) {
item.onClick({
originalEvent: e,
action: item.action,
});
}
if (!item.items) {
this.props.onLeafClick(e);
}
}
position() {
const parentItem = this.submenuRef.current.parentElement;
const containerOffset = DomHelpers.getOffset(
this.submenuRef.current.parentElement
);
const viewport = DomHelpers.getViewport();
const sublistWidth = this.submenuRef.current.offsetParent
? this.submenuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(this.submenuRef.current);
const itemOuterWidth = DomHelpers.getOuterWidth(parentItem.children[0]);
this.submenuRef.current.style.top = "0px";
if (
parseInt(containerOffset.left, 10) + itemOuterWidth + sublistWidth >
viewport.width - DomHelpers.calculateScrollbarWidth()
) {
this.submenuRef.current.style.left = -1 * sublistWidth + "px";
} else {
this.submenuRef.current.style.left = itemOuterWidth + "px";
}
}
onEnter() {
this.position();
}
isActive() {
return this.props.root || !this.props.resetMenu;
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.resetMenu === true) {
return {
activeItem: null,
};
}
return null;
}
componentDidUpdate() {
if (this.isActive()) {
this.position();
}
}
renderSeparator(index) {
return (
<li
key={"separator_" + index}
className="p-menu-separator not-selectable"
role="separator"
></li>
);
}
renderSubmenu(item) {
if (item.items) {
return (
<ContextMenuSub
model={item.items}
resetMenu={item !== this.state.activeItem}
onLeafClick={this.props.onLeafClick}
/>
);
}
return null;
}
renderMenuitem(item, index) {
if (item.disabled) return; //TODO: Not render disabled items
const active = this.state.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,
});
const iconClassName = classNames("p-menuitem-icon", {
"p-disabled": item.disabled,
});
const submenuIconClassName = "p-submenu-icon";
const icon = item.icon && (
<ReactSVG
wrapper="span"
className={iconClassName}
src={item.icon}
></ReactSVG>
);
const label = item.label && (
<span className="p-menuitem-text not-selectable">{item.label}</span>
);
const submenuIcon = item.items && (
<ArrowIcon className={submenuIconClassName} />
);
const submenu = this.renderSubmenu(item);
const dataKeys = Object.fromEntries(
Object.entries(item).filter((el) => el[0].indexOf("data-") === 0)
);
const onClick = (e) => {
this.onItemClick(item, e);
};
let content = (
<a
href={item.url || "#"}
className={linkClassName}
target={item.target}
{...dataKeys}
onClick={onClick}
role="menuitem"
>
{icon}
{label}
{submenuIcon}
</a>
);
if (item.template) {
const defaultContentOptions = {
onClick,
className: linkClassName,
labelClassName: "p-menuitem-text",
iconClassName,
submenuIconClassName,
element: content,
props: this.props,
active,
};
content = ObjectUtils.getJSXElement(
item.template,
item,
defaultContentOptions
);
}
return (
<li
key={item.label + "_" + index}
role="none"
className={className}
style={item.style}
onMouseEnter={(event) => this.onItemMouseEnter(event, item)}
>
{content}
{submenu}
</li>
);
}
renderItem(item, index) {
if (!item) return null;
if (item.isSeparator) return this.renderSeparator(index);
else return this.renderMenuitem(item, index);
}
renderMenu() {
if (this.props.model) {
return this.props.model.map((item, index) => {
return this.renderItem(item, index);
});
}
return null;
}
render() {
const className = classNames({ "p-submenu-list": !this.props.root });
const submenu = this.renderMenu();
const isActive = this.isActive();
return (
<CSSTransition
nodeRef={this.submenuRef}
classNames="p-contextmenusub"
in={isActive}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit={true}
onEnter={this.onEnter}
>
<ul ref={this.submenuRef} className={`${className} not-selectable`}>
{submenu}
</ul>
</CSSTransition>
);
}
}
ContextMenuSub.propTypes = {
model: PropTypes.any,
root: PropTypes.bool,
className: PropTypes.string,
resetMenu: PropTypes.bool,
onLeafClick: PropTypes.func,
};
ContextMenuSub.defaultProps = {
model: null,
root: false,
className: null,
resetMenu: false,
onLeafClick: null,
};
import SubMenu from "./sub-components/sub-menu";
class ContextMenu extends Component {
constructor(props) {
@ -272,34 +17,22 @@ class ContextMenu extends Component {
resetMenu: false,
};
this.onMenuClick = this.onMenuClick.bind(this);
this.onLeafClick = this.onLeafClick.bind(this);
this.onMenuMouseEnter = this.onMenuMouseEnter.bind(this);
this.onEnter = this.onEnter.bind(this);
this.onEntered = this.onEntered.bind(this);
this.onExit = this.onExit.bind(this);
this.onExited = this.onExited.bind(this);
this.menuRef = React.createRef();
}
onMenuClick() {
onMenuClick = () => {
this.setState({
resetMenu: false,
});
}
};
onMenuMouseEnter() {
onMenuMouseEnter = () => {
this.setState({
resetMenu: false,
});
}
show(e) {
if (!(e instanceof Event)) {
e.persist();
}
};
show = (e) => {
e.stopPropagation();
e.preventDefault();
@ -314,7 +47,7 @@ class ContextMenu extends Component {
}
});
}
}
};
componentDidUpdate(prevProps, prevState) {
if (this.state.visible && prevState.reshow !== this.state.reshow) {
@ -323,7 +56,6 @@ class ContextMenu extends Component {
{
visible: false,
reshow: false,
rePosition: false,
resetMenu: true,
},
() => this.show(event)
@ -331,20 +63,16 @@ class ContextMenu extends Component {
}
}
hide(e) {
if (!(e instanceof Event)) {
e.persist();
}
hide = (e) => {
this.currentEvent = e;
this.setState({ visible: false, reshow: false }, () => {
if (this.props.onHide) {
this.props.onHide(this.currentEvent);
}
});
}
};
onEnter() {
onEnter = () => {
if (this.props.autoZIndex) {
this.menuRef.current.style.zIndex = String(
this.props.baseZIndex + DomHelpers.generateZIndex()
@ -352,22 +80,22 @@ class ContextMenu extends Component {
}
this.position(this.currentEvent);
}
};
onEntered() {
onEntered = () => {
this.bindDocumentListeners();
}
};
onExit() {
onExit = () => {
this.currentEvent = null;
this.unbindDocumentListeners();
}
};
onExited() {
onExited = () => {
DomHelpers.revertZIndex();
}
};
position(event) {
position = (event) => {
if (event) {
const rects = this.props.containerRef?.current.getBoundingClientRect();
@ -412,9 +140,9 @@ class ContextMenu extends Component {
this.menuRef.current.style.left = left + "px";
this.menuRef.current.style.top = top + "px";
}
}
};
onLeafClick(e) {
onLeafClick = (e) => {
this.setState({
resetMenu: true,
});
@ -422,9 +150,9 @@ class ContextMenu extends Component {
this.hide(e);
e.stopPropagation();
}
};
isOutsideClicked(e) {
isOutsideClicked = (e) => {
return (
this.menuRef &&
this.menuRef.current &&
@ -433,19 +161,19 @@ class ContextMenu extends Component {
this.menuRef.current.contains(e.target)
)
);
}
};
bindDocumentListeners() {
bindDocumentListeners = () => {
this.bindDocumentResizeListener();
this.bindDocumentClickListener();
}
};
unbindDocumentListeners() {
unbindDocumentListeners = () => {
this.unbindDocumentResizeListener();
this.unbindDocumentClickListener();
}
};
bindDocumentClickListener() {
bindDocumentClickListener = () => {
if (!this.documentClickListener) {
this.documentClickListener = (e) => {
if (this.isOutsideClicked(e)) {
@ -461,9 +189,9 @@ class ContextMenu extends Component {
document.addEventListener("click", this.documentClickListener);
document.addEventListener("mousedown", this.documentClickListener);
}
}
};
bindDocumentContextMenuListener() {
bindDocumentContextMenuListener = () => {
if (!this.documentContextMenuListener) {
this.documentContextMenuListener = (e) => {
this.show(e);
@ -474,9 +202,9 @@ class ContextMenu extends Component {
this.documentContextMenuListener
);
}
}
};
bindDocumentResizeListener() {
bindDocumentResizeListener = () => {
if (!this.documentResizeListener) {
this.documentResizeListener = (e) => {
if (this.state.visible) {
@ -486,17 +214,17 @@ class ContextMenu extends Component {
window.addEventListener("resize", this.documentResizeListener);
}
}
};
unbindDocumentClickListener() {
unbindDocumentClickListener = () => {
if (this.documentClickListener) {
document.removeEventListener("click", this.documentClickListener);
document.removeEventListener("mousedown", this.documentClickListener);
this.documentClickListener = null;
}
}
};
unbindDocumentContextMenuListener() {
unbindDocumentContextMenuListener = () => {
if (this.documentContextMenuListener) {
document.removeEventListener(
"contextmenu",
@ -504,14 +232,14 @@ class ContextMenu extends Component {
);
this.documentContextMenuListener = null;
}
}
};
unbindDocumentResizeListener() {
unbindDocumentResizeListener = () => {
if (this.documentResizeListener) {
window.removeEventListener("resize", this.documentResizeListener);
this.documentResizeListener = null;
}
}
};
componentDidMount() {
if (this.props.global) {
@ -526,7 +254,7 @@ class ContextMenu extends Component {
DomHelpers.revertZIndex();
}
renderContextMenu() {
renderContextMenu = () => {
const className = classNames(
"p-contextmenu p-component",
this.props.className
@ -553,7 +281,7 @@ class ContextMenu extends Component {
onClick={this.onMenuClick}
onMouseEnter={this.onMenuMouseEnter}
>
<ContextMenuSub
<SubMenu
model={this.props.model}
root
resetMenu={this.state.resetMenu}
@ -563,7 +291,7 @@ class ContextMenu extends Component {
</CSSTransition>
</StyledContextMenu>
);
}
};
render() {
const element = this.renderContextMenu();

View File

@ -0,0 +1,239 @@
import React, { useRef, useState, useEffect } from "react";
import PropTypes from "prop-types";
import DomHelpers from "../../utils/domHelpers";
import ObjectUtils from "../../utils/objectUtils";
import { classNames } from "../../utils/classNames";
import { CSSTransition } from "react-transition-group";
import { ReactSVG } from "react-svg";
import ArrowIcon from "../svg/arrow.right.react.svg";
const SubMenu = (props) => {
const { onLeafClick, root, resetMenu, model } = props;
const [activeItem, setActiveItem] = useState(null);
const subMenuRef = useRef();
const onItemMouseEnter = (e, item) => {
if (item.disabled) {
e.preventDefault();
return;
}
setActiveItem(item);
};
const onItemClick = (e, item) => {
const { disabled, url, onClick, items, action } = item;
if (disabled) {
e.preventDefault();
return;
}
if (!url) {
e.preventDefault();
}
if (onClick) {
onClick({
originalEvent: e,
action: action,
});
}
if (!items) {
onLeafClick(e);
}
};
const position = () => {
const parentItem = subMenuRef.current.parentElement;
const containerOffset = DomHelpers.getOffset(
subMenuRef.current.parentElement
);
const viewport = DomHelpers.getViewport();
const subListWidth = subMenuRef.current.offsetParent
? subMenuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(subMenuRef.current);
const itemOuterWidth = DomHelpers.getOuterWidth(parentItem.children[0]);
subMenuRef.current.style.top = "0px";
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 = () => {
position();
};
const isActive = () => {
return root || !resetMenu;
};
useEffect(() => {
if (isActive()) {
position();
}
});
const renderSeparator = (index) => (
<li
key={"separator_" + index}
className="p-menu-separator not-selectable"
role="separator"
></li>
);
const renderSubMenu = (item) => {
if (item.items) {
return (
<SubMenu
model={item.items}
resetMenu={item !== activeItem}
onLeafClick={onLeafClick}
/>
);
}
return null;
};
const renderMenuitem = (item, index) => {
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,
});
const iconClassName = classNames("p-menuitem-icon", {
"p-disabled": item.disabled,
});
const subMenuIconClassName = "p-submenu-icon";
const icon = item.icon && (
<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 && (
<ArrowIcon className={subMenuIconClassName} />
);
const subMenu = renderSubMenu(item);
const dataKeys = Object.fromEntries(
Object.entries(item).filter((el) => el[0].indexOf("data-") === 0)
);
const onClick = (e) => {
onItemClick(e, item);
};
let content = (
<a
href={item.url || "#"}
className={linkClassName}
target={item.target}
{...dataKeys}
onClick={onClick}
role="menuitem"
>
{icon}
{label}
{subMenuIcon}
</a>
);
if (item.template) {
const defaultContentOptions = {
onClick,
className: linkClassName,
labelClassName: "p-menuitem-text",
iconClassName,
subMenuIconClassName,
element: content,
props: props,
active,
};
content = ObjectUtils.getJSXElement(
item.template,
item,
defaultContentOptions
);
}
return (
<li
key={item.label + "_" + index}
role="none"
className={className}
style={item.style}
onMouseEnter={(e) => onItemMouseEnter(e, item)}
>
{content}
{subMenu}
</li>
);
};
const renderItem = (item, index) => {
if (!item) return null;
if (item.isSeparator) return renderSeparator(index);
else return renderMenuitem(item, index);
};
const renderMenu = () => {
if (model) {
return model.map((item, index) => {
return renderItem(item, index);
});
}
return null;
};
const className = classNames({ "p-submenu-list": !root });
const submenu = renderMenu();
const active = isActive();
return (
<CSSTransition
nodeRef={subMenuRef}
classNames="p-contextmenusub"
in={active}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit={true}
onEnter={onEnter}
>
<ul ref={subMenuRef} className={`${className} not-selectable`}>
{submenu}
</ul>
</CSSTransition>
);
};
SubMenu.propTypes = {
model: PropTypes.any,
root: PropTypes.bool,
className: PropTypes.string,
resetMenu: PropTypes.bool,
onLeafClick: PropTypes.func,
};
SubMenu.defaultProps = {
model: null,
root: false,
className: null,
resetMenu: false,
onLeafClick: null,
};
export default SubMenu;