chayns-components
Version:
A set of beautiful React components for developing chayns® applications.
385 lines (377 loc) • 14.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.default = void 0;
var _clsx = _interopRequireDefault(require("clsx"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _react = _interopRequireWildcard(require("react"));
var _server = require("react-dom/server");
var _Bubble = _interopRequireDefault(require("../../react-chayns-bubble/component/Bubble"));
var _Icon = _interopRequireDefault(require("../../react-chayns-icon/component/Icon"));
var _TextString = _interopRequireDefault(require("../../react-chayns-textstring/component/TextString"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* @component {./docs.md}
*/
/**
* Gives people access to additional functionality related to onscreen items
* without cluttering the interface.
*/
const ContextMenu = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
let {
parent,
position: userPosition = null,
minWidth,
maxWidth,
className,
items = [],
stopPropagation = false,
childrenStyle,
childrenClassName,
showTriggerBackground = false,
children = /*#__PURE__*/_react.default.createElement(_Icon.default, {
icon: "ts-ellipsis_v"
}),
style,
onChildrenClick,
disableDialog = false,
onLayerClick,
onShow,
onHide,
removeParentSpace = false,
coordinates,
positionOnChildren = 1,
arrowDistance = 0,
customOpenDialog
} = _ref;
const bubbleRef = (0, _react.useRef)();
const childrenRef = (0, _react.useRef)();
const [isBubbleShown, setIsBubbleShown] = (0, _react.useState)(false);
const [position, setPosition] = (0, _react.useState)(null);
const [x, setX] = (0, _react.useState)(0);
const [y, setY] = (0, _react.useState)(0);
const itemsHaveIcons = items.some(_ref2 => {
let {
icon
} = _ref2;
return typeof icon === 'string' ? icon : icon === null || icon === void 0 ? void 0 : icon.iconName;
});
/**
* Sync bubble with `isBubbleShown` state
*/
(0, _react.useEffect)(() => {
if (!bubbleRef.current) return;
if (isBubbleShown) {
bubbleRef.current.show();
onShow === null || onShow === void 0 ? void 0 : onShow();
} else {
bubbleRef.current.hide();
onHide === null || onHide === void 0 ? void 0 : onHide();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isBubbleShown]);
/**
* Update position of the bubble
*/
(0, _react.useEffect)(() => {
let newX = (coordinates === null || coordinates === void 0 ? void 0 : coordinates.x) || 0;
let top = (coordinates === null || coordinates === void 0 ? void 0 : coordinates.y) || 0;
let bottom = (coordinates === null || coordinates === void 0 ? void 0 : coordinates.y) || 0;
if (!coordinates && childrenRef.current) {
const rect = childrenRef.current.getBoundingClientRect();
if (positionOnChildren === ContextMenu.positionOnChildren.LEFT) {
newX = rect.left + arrowDistance;
} else if (positionOnChildren === ContextMenu.positionOnChildren.CENTER) {
newX = rect.left + rect.width / 2;
} else if (positionOnChildren === ContextMenu.positionOnChildren.RIGHT) {
newX = rect.left + rect.width - arrowDistance;
}
top = rect.top;
bottom = rect.bottom;
}
let newPosition = userPosition;
if (newPosition === null) {
const shouldBeLeft = newX > window.innerWidth / 2;
const shouldBeTop = (top + bottom) / 2 > (document.body.offsetHeight || window.innerHeight) / 2;
if (shouldBeLeft && shouldBeTop) {
newPosition = ContextMenu.position.TOP_LEFT;
} else if (!shouldBeLeft && shouldBeTop) {
newPosition = ContextMenu.position.TOP_RIGHT;
} else if (shouldBeLeft && !shouldBeTop) {
newPosition = ContextMenu.position.BOTTOM_LEFT;
} else {
newPosition = ContextMenu.position.BOTTOM_RIGHT;
}
}
let newY = _Bubble.default.isPositionBottom(newPosition) ? bottom : top;
if (removeParentSpace) {
const parentRect = (parent || document.getElementsByClassName('tapp')[0] || document.body).getBoundingClientRect();
newX -= parentRect.left;
newY -= parentRect.top;
}
if (chayns.env.isApp) {
chayns.getWindowMetrics().then(_ref3 => {
let {
pageYOffset
} = _ref3;
setPosition(newPosition);
setX(newX);
setY(newY + pageYOffset);
});
} else {
setPosition(newPosition);
setX(newX);
setY(newY);
}
}, [arrowDistance, coordinates, parent, positionOnChildren, removeParentSpace, userPosition, isBubbleShown]);
/**
* Show the menu, either by opening a dialog or by showing the bubble.
*/
const show = (0, _react.useCallback)(async () => {
if (!disableDialog && (chayns.env.isMobile || chayns.env.isTablet)) {
const iconFallback = items.some(_ref4 => {
let {
icon
} = _ref4;
return typeof icon === 'string' ? icon : icon === null || icon === void 0 ? void 0 : icon.iconName;
}) ? ' ' : '';
const {
buttonType,
selection
} = await (customOpenDialog !== null && customOpenDialog !== void 0 ? customOpenDialog : chayns.dialog.select)({
type: 2,
list: items.map((_ref5, index) => {
let {
text,
icon,
stringName
} = _ref5;
return {
// eslint-disable-next-line no-nested-ternary
name: stringName ? _TextString.default.getTextString(stringName, null, text) : /*#__PURE__*/_react.default.isValidElement(text) ? (0, _server.renderToStaticMarkup)(text) : text,
value: index,
icon: (typeof icon === 'string' ? icon : (icon === null || icon === void 0 ? void 0 : icon.iconName) && `fa-${icon === null || icon === void 0 ? void 0 : icon.iconName}`) || iconFallback
};
}),
buttons: []
});
if (buttonType === 1 && selection !== null && selection !== void 0 && selection[0]) {
items[selection[0].value].onClick();
} else if (buttonType === -1 && onLayerClick) {
onLayerClick();
}
} else {
setIsBubbleShown(true);
}
}, [disableDialog, items, onLayerClick, customOpenDialog]);
const hide = (0, _react.useCallback)(() => {
setIsBubbleShown(false);
}, []);
/**
* Expose the `show` and `hide` methods via ref.
*/
(0, _react.useImperativeHandle)(ref, () => ({
show,
hide
}), [hide, show]);
(0, _react.useEffect)(() => {
window.addEventListener('blur', hide);
return () => {
window.removeEventListener('blur', hide);
};
}, [hide]);
/**
* Hides the bubble when is it opened and the page is clicked.
*/
// eslint-disable-next-line consistent-return
(0, _react.useEffect)(() => {
if (isBubbleShown) {
const handleLayerClick = event => {
if (isBubbleShown) {
if (onLayerClick) {
onLayerClick(event);
} else {
hide();
}
}
};
document.addEventListener('click', handleLayerClick, true);
return () => document.removeEventListener('click', handleLayerClick, true);
}
}, [hide, isBubbleShown, onLayerClick]);
function handleChildrenClick(event) {
if (onChildrenClick) {
onChildrenClick(event);
} else {
show();
}
if (stopPropagation) {
event.stopPropagation();
}
}
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_Bubble.default, {
coordinates: {
x,
y
},
parent: parent,
position: position,
style: {
minWidth,
maxWidth,
...style
},
key: "bubble",
ref: bubbleRef,
className: className
}, /*#__PURE__*/_react.default.createElement("ul", null, items.map((item, index) => /*#__PURE__*/_react.default.createElement("li", {
className: (0, _clsx.default)('context-menu__item', item.className),
onClick: e => {
if (stopPropagation) {
e.stopPropagation();
}
item.onClick(e);
},
key:
// eslint-disable-next-line react/no-array-index-key
item.stringName || (item.text.props && item.text.props.stringName ? item.text.props.stringName : item.text) + index
}, item.icon || itemsHaveIcons ? /*#__PURE__*/_react.default.createElement("div", {
className: "context-menu__item__icon"
}, item.icon && /*#__PURE__*/_react.default.createElement(_Icon.default, {
icon: item.icon
})) : null, item.stringName ? /*#__PURE__*/_react.default.createElement(_TextString.default, {
stringName: item.stringName
}, /*#__PURE__*/_react.default.createElement("div", {
className: "context-menu__item__text"
}, item.text)) : /*#__PURE__*/_react.default.createElement("div", {
className: "context-menu__item__text"
}, item.text))))), !coordinates && /*#__PURE__*/_react.default.createElement("div", {
key: "cc__contextMenu__children",
ref: childrenRef,
onClick: handleChildrenClick,
style: childrenStyle,
className: (0, _clsx.default)("accordion--no-trigger context-menu__children", childrenClassName, showTriggerBackground && 'image-tool')
}, children));
});
ContextMenu.position = _Bubble.default.position;
ContextMenu.positionOnChildren = {
LEFT: 0,
CENTER: 1,
RIGHT: 2
};
ContextMenu.propTypes = {
/**
* This callback will be called when the `ContextMenu` is shown and the user
* clicks away from it.
*/
onLayerClick: _propTypes.default.func,
/**
* This callback will be called when the context menu becomes visible
*/
onShow: _propTypes.default.func,
/**
* This callback will be called when the `ContextMenu` hides
*/
onHide: _propTypes.default.func,
/**
* The coordinates at which the context menu will get rendered.
*/
coordinates: _propTypes.default.shape({
x: _propTypes.default.number.isRequired,
y: _propTypes.default.number.isRequired
}),
/**
* The action items inside of the context menu. Their shape should look like
* this: `{ className: <string>, onClick: <function>, text: <string>,
* icon: <string> }, stringName: <string>`.
*/
items: _propTypes.default.arrayOf(_propTypes.default.shape({
className: _propTypes.default.string,
onClick: _propTypes.default.func,
text: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.node]).isRequired,
stringName: _propTypes.default.string,
icon: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.instanceOf(Object)])
})),
/**
* This specifies where the menu will appear relative to the components
* provided in the `children`-prop. Possible values are: `0` for top left,
* `1` for bottom left, `2` for bottom right, `3` for top right, `4` for top
* center and `5` for bottom center.
*/
position: _propTypes.default.number,
/**
* The position of the arrow relative to the children. Possible values are
* `0` for left, `1` for center and `2` for right.
*/
positionOnChildren: _propTypes.default.number,
/**
* Specifies the DOM node the context menu will be rendered into.
*/
parent: typeof Element !== 'undefined' ? _propTypes.default.instanceOf(Element) : () => {},
/**
* The React node that the context menu refers to.
*/
children: _propTypes.default.node,
/**
* Called when the `onclick`-event is triggered on the children.
*/
onChildrenClick: _propTypes.default.func,
/**
* A React style object that will be applied to the children wrapper.
*/
childrenStyle: _propTypes.default.objectOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number])),
/**
* A classname string that will be applied to the children wrapper.
*/
childrenClassName: _propTypes.default.string,
/**
* Wether to stop propagation of click events on the children elements.
*/
stopPropagation: _propTypes.default.bool,
/**
* The minimum width of the context menu.
*/
minWidth: _propTypes.default.number,
/**
* The maximum width of the context menu.
*/
maxWidth: _propTypes.default.number,
/**
* Adds a white background to the children wrapper, for when it would
* otherwise be difficult to view (e.g. on images or video).
*/
showTriggerBackground: _propTypes.default.bool,
/**
* A classname string applied to the Bubble component.
*/
className: _propTypes.default.string,
/**
* A React style object that is applied to the Bubble component.
*/
// eslint-disable-next-line react/forbid-prop-types
style: _propTypes.default.object,
/**
* Removes the parent padding to the page borders from the context menu
* position. This is needed when the parent is padded and relatively
* positioned.
*/
removeParentSpace: _propTypes.default.bool,
/**
* Disables the use of a dialog on mobile.
*/
disableDialog: _propTypes.default.bool,
/**
* Custom open dialog function for mobile. If not provided, the default chayns dialog will be used.
*/
customOpenDialog: _propTypes.default.func,
/**
* Adjust the distance of the arrow and the end of the children. This only
* applies if `positionOnChildren` is set to left (`0`) or right (`2`).
*/
arrowDistance: _propTypes.default.number
};
ContextMenu.displayName = 'ContextMenu';
var _default = ContextMenu;
exports.default = _default;
//# sourceMappingURL=ContextMenu.js.map