@jstarpl/react-contextmenu
Version:
Context Menu implemented in React
282 lines (246 loc) • 8.79 kB
JavaScript
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import assign from 'object-assign';
import { hideMenu } from './actions';
import AbstractMenu from './AbstractMenu';
import { callIfExists, cssClasses, hasOwnProp, store } from './helpers';
import listener from './globalEventListener';
export default class SubMenu extends AbstractMenu {
constructor(props) {
super(props);
_defineProperty(this, "getMenuPosition", () => {
const {
innerWidth,
innerHeight
} = window;
const rect = this.subMenu.getBoundingClientRect();
const position = {};
if (rect.bottom > innerHeight) {
position.bottom = 0;
} else {
position.top = 0;
}
if (rect.right < innerWidth) {
position.left = '100%';
} else {
position.right = '100%';
}
return position;
});
_defineProperty(this, "getRTLMenuPosition", () => {
const {
innerHeight
} = window;
const rect = this.subMenu.getBoundingClientRect();
const position = {};
if (rect.bottom > innerHeight) {
position.bottom = 0;
} else {
position.top = 0;
} // eslint-disable-next-line no-magic-numbers
if (rect.left < 0) {
position.left = '100%';
} else {
position.right = '100%';
}
return position;
});
_defineProperty(this, "hideMenu", e => {
e.preventDefault();
this.hideSubMenu(e);
});
_defineProperty(this, "hideSubMenu", e => {
// avoid closing submenus of a different menu tree
if (e.detail && e.detail.id && this.menu && e.detail.id !== this.menu.id) {
return;
}
if (this.props.forceOpen) {
this.props.forceClose();
}
this.setState({
visible: false,
selectedItem: null
});
this.unregisterHandlers();
});
_defineProperty(this, "handleClick", event => {
event.preventDefault();
if (this.props.disabled) return;
callIfExists(this.props.onClick, event, assign({}, this.props.data, store.data), store.target);
if (!this.props.onClick || this.props.preventCloseOnClick) return;
hideMenu();
});
_defineProperty(this, "handleMouseEnter", () => {
if (this.closetimer) clearTimeout(this.closetimer);
if (this.props.disabled || this.state.visible) return;
this.opentimer = setTimeout(() => this.setState({
visible: true,
selectedItem: null
}), this.props.hoverDelay);
});
_defineProperty(this, "handleMouseLeave", () => {
if (this.opentimer) clearTimeout(this.opentimer);
if (!this.state.visible) return;
this.closetimer = setTimeout(() => this.setState({
visible: false,
selectedItem: null
}), this.props.hoverDelay);
});
_defineProperty(this, "menuRef", c => {
this.menu = c;
});
_defineProperty(this, "subMenuRef", c => {
this.subMenu = c;
});
_defineProperty(this, "registerHandlers", () => {
document.removeEventListener('keydown', this.props.parentKeyNavigationHandler);
document.addEventListener('keydown', this.handleKeyNavigation);
});
_defineProperty(this, "unregisterHandlers", dismounting => {
document.removeEventListener('keydown', this.handleKeyNavigation);
if (!dismounting) {
document.addEventListener('keydown', this.props.parentKeyNavigationHandler);
}
});
this.state = assign({}, this.state, {
visible: false
});
}
componentDidMount() {
this.listenId = listener.register(() => {}, this.hideSubMenu);
}
getSubMenuType() {
// eslint-disable-line class-methods-use-this
return SubMenu;
}
shouldComponentUpdate(nextProps, nextState) {
this.isVisibilityChange = (this.state.visible !== nextState.visible || this.props.forceOpen !== nextProps.forceOpen) && !(this.state.visible && nextProps.forceOpen) && !(this.props.forceOpen && nextState.visible);
return true;
}
componentDidUpdate() {
if (!this.isVisibilityChange) return;
if (this.props.forceOpen || this.state.visible) {
const wrapper = window.requestAnimationFrame || setTimeout;
wrapper(() => {
const styles = this.props.rtl ? this.getRTLMenuPosition() : this.getMenuPosition();
this.subMenu.style.removeProperty('top');
this.subMenu.style.removeProperty('bottom');
this.subMenu.style.removeProperty('left');
this.subMenu.style.removeProperty('right');
if (hasOwnProp(styles, 'top')) this.subMenu.style.top = styles.top;
if (hasOwnProp(styles, 'left')) this.subMenu.style.left = styles.left;
if (hasOwnProp(styles, 'bottom')) this.subMenu.style.bottom = styles.bottom;
if (hasOwnProp(styles, 'right')) this.subMenu.style.right = styles.right;
this.subMenu.classList.add(cssClasses.menuVisible);
this.registerHandlers();
this.setState({
selectedItem: null
});
});
} else {
const cleanup = () => {
this.subMenu.removeEventListener('transitionend', cleanup);
this.subMenu.style.removeProperty('bottom');
this.subMenu.style.removeProperty('right');
this.subMenu.style.top = 0;
this.subMenu.style.left = '100%';
this.unregisterHandlers();
};
this.subMenu.addEventListener('transitionend', cleanup);
this.subMenu.classList.remove(cssClasses.menuVisible);
}
}
componentWillUnmount() {
if (this.listenId) {
listener.unregister(this.listenId);
}
if (this.opentimer) clearTimeout(this.opentimer);
if (this.closetimer) clearTimeout(this.closetimer);
this.unregisterHandlers(true);
}
render() {
const {
children,
attributes,
disabled,
title,
selected
} = this.props;
const {
visible
} = this.state;
const menuProps = {
ref: this.menuRef,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
className: cx(cssClasses.menuItem, cssClasses.subMenu, attributes.listClassName),
style: {
position: 'relative'
}
};
const menuItemProps = {
className: cx(cssClasses.menuItem, attributes.className, {
[cx(cssClasses.menuItemDisabled, attributes.disabledClassName)]: disabled,
[cx(cssClasses.menuItemActive, attributes.visibleClassName)]: visible,
[cx(cssClasses.menuItemSelected, attributes.selectedClassName)]: selected
}),
onMouseMove: this.props.onMouseMove,
onMouseOut: this.props.onMouseOut,
onClick: this.handleClick
};
const subMenuProps = {
ref: this.subMenuRef,
style: {
position: 'absolute',
transition: 'opacity 1ms',
// trigger transitionend event
top: 0,
left: '100%'
},
className: cx(cssClasses.menu, this.props.className)
};
return (
/*#__PURE__*/
// eslint-disable-next-line react/jsx-props-no-spreading
React.createElement("nav", _extends({}, menuProps, {
role: "menuitem",
tabIndex: "-1",
"aria-haspopup": "true"
}), /*#__PURE__*/React.createElement("div", _extends({}, attributes, menuItemProps), title), /*#__PURE__*/React.createElement("nav", _extends({}, subMenuProps, {
role: "menu",
tabIndex: "-1"
}), this.renderChildren(children)))
);
}
}
_defineProperty(SubMenu, "propTypes", {
children: PropTypes.node.isRequired,
attributes: PropTypes.object,
title: PropTypes.node.isRequired,
className: PropTypes.string,
disabled: PropTypes.bool,
hoverDelay: PropTypes.number,
rtl: PropTypes.bool,
selected: PropTypes.bool,
onMouseMove: PropTypes.func,
onMouseOut: PropTypes.func,
forceOpen: PropTypes.bool,
forceClose: PropTypes.func,
parentKeyNavigationHandler: PropTypes.func
});
_defineProperty(SubMenu, "defaultProps", {
disabled: false,
hoverDelay: 500,
attributes: {},
className: '',
rtl: false,
selected: false,
onMouseMove: () => null,
onMouseOut: () => null,
forceOpen: false,
forceClose: () => null,
parentKeyNavigationHandler: () => null
});