2021-03-26 13:03:51 +00:00
|
|
|
import React, { Component } from "react";
|
2020-10-16 13:16:01 +00:00
|
|
|
import PropTypes from "prop-types";
|
2021-03-26 13:03:51 +00:00
|
|
|
import DomHelpers from "../utils/domHelpers";
|
|
|
|
import { classNames } from "../utils/classNames";
|
|
|
|
import { CSSTransition } from "react-transition-group";
|
|
|
|
import Portal from "../portal";
|
|
|
|
import StyledContextMenu from "./styled-context-menu";
|
2021-11-19 21:17:25 +00:00
|
|
|
import SubMenu from "./sub-components/sub-menu";
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2022-03-04 17:55:28 +00:00
|
|
|
import { isMobile, isMobileOnly } from "react-device-detect";
|
|
|
|
import {
|
|
|
|
isMobile as isMobileUtils,
|
|
|
|
isTablet as isTabletUtils,
|
|
|
|
} from "../utils/device";
|
|
|
|
|
|
|
|
import Backdrop from "../backdrop";
|
|
|
|
import Text from "../text";
|
|
|
|
import { ReactSVG } from "react-svg";
|
|
|
|
|
2021-03-26 13:03:51 +00:00
|
|
|
class ContextMenu extends Component {
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
visible: false,
|
|
|
|
reshow: false,
|
|
|
|
resetMenu: false,
|
2022-03-02 12:15:03 +00:00
|
|
|
model: null,
|
2022-03-04 17:55:28 +00:00
|
|
|
changeView: false,
|
2021-03-26 13:03:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.menuRef = React.createRef();
|
|
|
|
}
|
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onMenuClick = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.setState({
|
|
|
|
resetMenu: false,
|
|
|
|
});
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onMenuMouseEnter = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.setState({
|
|
|
|
resetMenu: false,
|
|
|
|
});
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
show = (e) => {
|
2022-03-10 07:09:10 +00:00
|
|
|
if (this.props.getContextModel) {
|
|
|
|
const model = this.props.getContextModel();
|
2022-03-03 09:56:51 +00:00
|
|
|
this.setState({ model });
|
|
|
|
}
|
2022-03-02 12:15:03 +00:00
|
|
|
|
2021-03-26 13:03:51 +00:00
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
this.currentEvent = e;
|
|
|
|
|
|
|
|
if (this.state.visible) {
|
2022-05-27 14:44:10 +00:00
|
|
|
!isMobileOnly && this.setState({ reshow: true });
|
2021-03-26 13:03:51 +00:00
|
|
|
} else {
|
|
|
|
this.setState({ visible: true }, () => {
|
|
|
|
if (this.props.onShow) {
|
|
|
|
this.props.onShow(this.currentEvent);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
|
|
if (this.state.visible && prevState.reshow !== this.state.reshow) {
|
|
|
|
let event = this.currentEvent;
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
visible: false,
|
|
|
|
reshow: false,
|
|
|
|
resetMenu: true,
|
2022-03-04 17:55:28 +00:00
|
|
|
changeView: false,
|
2021-03-26 13:03:51 +00:00
|
|
|
},
|
|
|
|
() => this.show(event)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
hide = (e) => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.currentEvent = e;
|
2022-04-20 13:05:24 +00:00
|
|
|
|
|
|
|
this.props.onHide && this.props.onHide(e);
|
|
|
|
this.setState({
|
|
|
|
visible: false,
|
|
|
|
reshow: false,
|
|
|
|
changeView: false,
|
2021-03-26 13:03:51 +00:00
|
|
|
});
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onEnter = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (this.props.autoZIndex) {
|
|
|
|
this.menuRef.current.style.zIndex = String(
|
|
|
|
this.props.baseZIndex + DomHelpers.generateZIndex()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.position(this.currentEvent);
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onEntered = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.bindDocumentListeners();
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onExit = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.currentEvent = null;
|
|
|
|
this.unbindDocumentListeners();
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onExited = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
DomHelpers.revertZIndex();
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
position = (event) => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (event) {
|
2021-11-19 14:59:10 +00:00
|
|
|
const rects = this.props.containerRef?.current.getBoundingClientRect();
|
|
|
|
|
|
|
|
let left = rects ? rects.left : event.pageX + 1;
|
|
|
|
let top = rects ? rects.top : event.pageY + 1;
|
2021-03-26 13:03:51 +00:00
|
|
|
let width = this.menuRef.current.offsetParent
|
|
|
|
? this.menuRef.current.offsetWidth
|
|
|
|
: DomHelpers.getHiddenElementOuterWidth(this.menuRef.current);
|
|
|
|
let height = this.menuRef.current.offsetParent
|
|
|
|
? this.menuRef.current.offsetHeight
|
|
|
|
: DomHelpers.getHiddenElementOuterHeight(this.menuRef.current);
|
|
|
|
let viewport = DomHelpers.getViewport();
|
|
|
|
|
2022-03-04 17:55:28 +00:00
|
|
|
if ((isMobile || isTabletUtils()) && height > 483) {
|
|
|
|
this.setState({ changeView: true });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((isMobileOnly || isMobileUtils()) && height > 210) {
|
|
|
|
this.setState({ changeView: true });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-26 13:03:51 +00:00
|
|
|
//flip
|
|
|
|
if (left + width - document.body.scrollLeft > viewport.width) {
|
|
|
|
left -= width;
|
|
|
|
}
|
|
|
|
|
|
|
|
//flip
|
|
|
|
if (top + height - document.body.scrollTop > viewport.height) {
|
|
|
|
top -= height;
|
|
|
|
}
|
|
|
|
|
|
|
|
//fit
|
|
|
|
if (left < document.body.scrollLeft) {
|
|
|
|
left = document.body.scrollLeft;
|
|
|
|
}
|
|
|
|
|
|
|
|
//fit
|
|
|
|
if (top < document.body.scrollTop) {
|
|
|
|
top = document.body.scrollTop;
|
|
|
|
}
|
|
|
|
|
2021-11-19 14:59:10 +00:00
|
|
|
if (this.props.containerRef) {
|
|
|
|
top += rects.height + 4;
|
|
|
|
|
|
|
|
if (this.props.scaled) {
|
|
|
|
this.menuRef.current.style.width = rects.width + "px";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-26 13:03:51 +00:00
|
|
|
this.menuRef.current.style.left = left + "px";
|
|
|
|
this.menuRef.current.style.top = top + "px";
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
onLeafClick = (e) => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.setState({
|
|
|
|
resetMenu: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.hide(e);
|
|
|
|
|
|
|
|
e.stopPropagation();
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
isOutsideClicked = (e) => {
|
2021-03-26 13:03:51 +00:00
|
|
|
return (
|
|
|
|
this.menuRef &&
|
|
|
|
this.menuRef.current &&
|
|
|
|
!(
|
|
|
|
this.menuRef.current.isSameNode(e.target) ||
|
|
|
|
this.menuRef.current.contains(e.target)
|
|
|
|
)
|
|
|
|
);
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
bindDocumentListeners = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.bindDocumentResizeListener();
|
|
|
|
this.bindDocumentClickListener();
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
unbindDocumentListeners = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
this.unbindDocumentResizeListener();
|
|
|
|
this.unbindDocumentClickListener();
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
bindDocumentClickListener = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (!this.documentClickListener) {
|
|
|
|
this.documentClickListener = (e) => {
|
2021-10-11 09:56:17 +00:00
|
|
|
if (this.isOutsideClicked(e)) {
|
|
|
|
//TODO: (&& e.button !== 2) restore after global usage
|
2021-03-26 13:03:51 +00:00
|
|
|
this.hide(e);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
resetMenu: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener("click", this.documentClickListener);
|
2021-10-10 20:29:01 +00:00
|
|
|
document.addEventListener("mousedown", this.documentClickListener);
|
2021-03-26 13:03:51 +00:00
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
bindDocumentContextMenuListener = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (!this.documentContextMenuListener) {
|
|
|
|
this.documentContextMenuListener = (e) => {
|
|
|
|
this.show(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener(
|
|
|
|
"contextmenu",
|
|
|
|
this.documentContextMenuListener
|
|
|
|
);
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
bindDocumentResizeListener = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (!this.documentResizeListener) {
|
|
|
|
this.documentResizeListener = (e) => {
|
|
|
|
if (this.state.visible) {
|
|
|
|
this.hide(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener("resize", this.documentResizeListener);
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
unbindDocumentClickListener = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (this.documentClickListener) {
|
|
|
|
document.removeEventListener("click", this.documentClickListener);
|
2021-10-10 20:29:01 +00:00
|
|
|
document.removeEventListener("mousedown", this.documentClickListener);
|
2021-03-26 13:03:51 +00:00
|
|
|
this.documentClickListener = null;
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
unbindDocumentContextMenuListener = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (this.documentContextMenuListener) {
|
|
|
|
document.removeEventListener(
|
|
|
|
"contextmenu",
|
|
|
|
this.documentContextMenuListener
|
|
|
|
);
|
|
|
|
this.documentContextMenuListener = null;
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
unbindDocumentResizeListener = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
if (this.documentResizeListener) {
|
|
|
|
window.removeEventListener("resize", this.documentResizeListener);
|
|
|
|
this.documentResizeListener = null;
|
|
|
|
}
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
if (this.props.global) {
|
|
|
|
this.bindDocumentContextMenuListener();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
this.unbindDocumentListeners();
|
|
|
|
this.unbindDocumentContextMenuListener();
|
|
|
|
|
|
|
|
DomHelpers.revertZIndex();
|
|
|
|
}
|
|
|
|
|
2021-11-19 21:17:25 +00:00
|
|
|
renderContextMenu = () => {
|
2021-03-26 13:03:51 +00:00
|
|
|
const className = classNames(
|
|
|
|
"p-contextmenu p-component",
|
|
|
|
this.props.className
|
|
|
|
);
|
|
|
|
|
2022-03-04 17:55:28 +00:00
|
|
|
const changeView = this.state.changeView;
|
|
|
|
|
2021-03-26 13:03:51 +00:00
|
|
|
return (
|
2022-03-04 17:55:28 +00:00
|
|
|
<>
|
|
|
|
<StyledContextMenu changeView={changeView}>
|
|
|
|
<CSSTransition
|
|
|
|
nodeRef={this.menuRef}
|
|
|
|
classNames="p-contextmenu"
|
|
|
|
in={this.state.visible}
|
|
|
|
timeout={{ enter: 250, exit: 0 }}
|
|
|
|
unmountOnExit
|
|
|
|
onEnter={this.onEnter}
|
|
|
|
onEntered={this.onEntered}
|
|
|
|
onExit={this.onExit}
|
|
|
|
onExited={this.onExited}
|
2021-03-30 07:42:10 +00:00
|
|
|
>
|
2022-03-04 17:55:28 +00:00
|
|
|
<div
|
|
|
|
ref={this.menuRef}
|
|
|
|
id={this.props.id}
|
|
|
|
className={className}
|
|
|
|
style={this.props.style}
|
|
|
|
onClick={this.onMenuClick}
|
|
|
|
onMouseEnter={this.onMenuMouseEnter}
|
|
|
|
>
|
|
|
|
{changeView && (
|
|
|
|
<div className="contextmenu-header">
|
|
|
|
<div className="icon-wrapper">
|
|
|
|
<ReactSVG
|
|
|
|
src={this.props.header.icon}
|
|
|
|
className="drop-down-item_icon"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<Text className="text" truncate={true}>
|
|
|
|
{this.props.header.title}
|
|
|
|
</Text>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<SubMenu
|
2022-03-05 14:52:01 +00:00
|
|
|
model={
|
2022-03-10 07:09:10 +00:00
|
|
|
this.props.getContextModel
|
|
|
|
? this.state.model
|
|
|
|
: this.props.model
|
2022-03-05 14:52:01 +00:00
|
|
|
}
|
2022-03-04 17:55:28 +00:00
|
|
|
root
|
|
|
|
resetMenu={this.state.resetMenu}
|
|
|
|
onLeafClick={this.onLeafClick}
|
|
|
|
changeView={changeView}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</CSSTransition>
|
|
|
|
</StyledContextMenu>
|
|
|
|
</>
|
2020-10-16 13:16:01 +00:00
|
|
|
);
|
2021-11-19 21:17:25 +00:00
|
|
|
};
|
2021-03-26 13:03:51 +00:00
|
|
|
|
|
|
|
render() {
|
|
|
|
const element = this.renderContextMenu();
|
|
|
|
|
2022-03-04 17:55:28 +00:00
|
|
|
return (
|
|
|
|
<>
|
2022-05-04 12:32:38 +00:00
|
|
|
{this.props.withBackdrop && (
|
2022-05-04 12:38:35 +00:00
|
|
|
<Backdrop
|
|
|
|
visible={this.state.visible}
|
|
|
|
withBackground={false}
|
2022-05-11 09:16:39 +00:00
|
|
|
withoutBlur={true}
|
2022-05-04 12:38:35 +00:00
|
|
|
/>
|
2022-05-04 12:32:38 +00:00
|
|
|
)}
|
2022-03-04 17:55:28 +00:00
|
|
|
<Portal element={element} appendTo={this.props.appendTo} />
|
|
|
|
</>
|
|
|
|
);
|
2021-03-26 13:03:51 +00:00
|
|
|
}
|
2019-08-29 13:00:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ContextMenu.propTypes = {
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Unique identifier of the element */
|
2019-11-27 13:06:48 +00:00
|
|
|
id: PropTypes.string,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** An array of menuitems */
|
2021-03-26 13:03:51 +00:00
|
|
|
model: PropTypes.array,
|
2022-03-04 17:55:28 +00:00
|
|
|
/** An object of header with icon and label */
|
|
|
|
header: PropTypes.object,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Inline style of the component */
|
2021-03-26 13:03:51 +00:00
|
|
|
style: PropTypes.object,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Style class of the component */
|
2021-03-26 13:03:51 +00:00
|
|
|
className: PropTypes.string,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Attaches the menu to document instead of a particular item */
|
2021-03-26 13:03:51 +00:00
|
|
|
global: PropTypes.bool,
|
2022-03-04 17:55:28 +00:00
|
|
|
/** Tell when context menu was render with backdrop */
|
|
|
|
withBackdrop: PropTypes.bool,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Base zIndex value to use in layering */
|
2021-03-26 13:03:51 +00:00
|
|
|
autoZIndex: PropTypes.bool,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Whether to automatically manage layering */
|
2021-03-26 13:03:51 +00:00
|
|
|
baseZIndex: PropTypes.number,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** DOM element instance where the menu should be mounted */
|
2021-03-26 13:03:51 +00:00
|
|
|
appendTo: PropTypes.any,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Callback to invoke when a popup menu is shown */
|
2021-03-26 13:03:51 +00:00
|
|
|
onShow: PropTypes.func,
|
2021-04-01 10:22:37 +00:00
|
|
|
/** Callback to invoke when a popup menu is hidden */
|
2021-03-26 13:03:51 +00:00
|
|
|
onHide: PropTypes.func,
|
2021-11-19 14:59:10 +00:00
|
|
|
/** If you want to display relative to another component */
|
|
|
|
containerRef: PropTypes.any,
|
|
|
|
/** Scale with by container component*/
|
|
|
|
scaled: PropTypes.bool,
|
2022-03-10 07:09:10 +00:00
|
|
|
getContextModel: PropTypes.func,
|
2019-08-29 13:00:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
ContextMenu.defaultProps = {
|
2021-03-26 13:03:51 +00:00
|
|
|
id: null,
|
|
|
|
style: null,
|
|
|
|
className: null,
|
|
|
|
global: false,
|
|
|
|
autoZIndex: true,
|
|
|
|
baseZIndex: 0,
|
|
|
|
appendTo: null,
|
|
|
|
onShow: null,
|
|
|
|
onHide: null,
|
2021-11-19 14:59:10 +00:00
|
|
|
scaled: false,
|
|
|
|
containerRef: null,
|
2019-08-29 13:00:01 +00:00
|
|
|
};
|
|
|
|
|
2020-10-16 13:16:01 +00:00
|
|
|
export default ContextMenu;
|