@tikpage/reactjs-popup
Version:
React Popup Component - Modals,Tooltips and Menus — All in one
788 lines (668 loc) • 24.8 kB
JavaScript
/*!
* @tikpage/reactjs-popup v1.0.10
* (c) 2020-present Youssouf EL AZIZI <youssoufelazizi@gmail.com>
* Released under the MIT License.
*/
;
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var React = _interopDefault(require('react'));
var ReactDOM = _interopDefault(require('react-dom'));
var PropTypes = _interopDefault(require('prop-types'));
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
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;
}
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 _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
}
}
function _iterableToArray(iter) {
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance");
}
/* Algo to calculate position
1. center position for popup content : the center of the trigger will be the center of the content content
so the popup content position will be like this :
top => the y of the center for the trigger element : trigger.top + trigger.height/2
left => the x of the center for the trigger element : trigger.left + trigger.width/2
2. translate position according to the first position attribute passed in the function argument
for example :
position = 'left top'
we need to handle the first argument in the position: 'left' => that's mean we need to translate the popup content according to the X axis by - content.width/2
3.translate position according to the first position attribute passed in the function argument
for example :
position = 'left top'
the second argument 'top' => translate popup content by + content.height*4/5
4. check if calculated position is going out of bounds of wrapper box or not. If yes repeat 1-3 for next position enum. By default wrapper box is window element
*/
function getCoordinatesForPosition(triggerBounding, ContentBounding, position, arrow, _ref) {
var offsetX = _ref.offsetX,
offsetY = _ref.offsetY;
var margin = arrow ? 8 : 0;
var args = position.split(' '); // the step N 1 : center the popup content => ok
var CenterTop = triggerBounding.top + triggerBounding.height / 2;
var CenterLeft = triggerBounding.left + triggerBounding.width / 2;
var height = ContentBounding.height,
width = ContentBounding.width;
var top = CenterTop - height / 2;
var left = CenterLeft - width / 2;
var transform = '';
var arrowTop = '0%';
var arrowLeft = '0%'; // the step N 2 : => ok
switch (args[0]) {
case 'top':
top -= height / 2 + triggerBounding.height / 2 + margin;
transform = "rotate(45deg)";
arrowTop = '100%';
arrowLeft = '50%';
break;
case 'bottom':
top += height / 2 + triggerBounding.height / 2 + margin;
transform = "rotate(225deg)";
arrowLeft = '50%';
break;
case 'left':
left -= width / 2 + triggerBounding.width / 2 + margin;
transform = " rotate(-45deg)";
arrowLeft = '100%';
arrowTop = '50%';
break;
case 'right':
left += width / 2 + triggerBounding.width / 2 + margin;
transform = "rotate(135deg)";
arrowTop = '50%';
break;
default:
}
switch (args[1]) {
case 'top':
top = triggerBounding.top;
arrowTop = "".concat(triggerBounding.height / 2, "px");
break;
case 'bottom':
top = triggerBounding.top - height + triggerBounding.height;
arrowTop = "".concat(height - triggerBounding.height / 2, "px");
break;
case 'left':
left = triggerBounding.left;
arrowLeft = "".concat(triggerBounding.width / 2, "px");
break;
case 'right':
left = triggerBounding.left - width + triggerBounding.width;
arrowLeft = "".concat(width - triggerBounding.width / 2, "px");
break;
default:
}
top = args[0] === 'top' ? top - offsetY : top + offsetY;
left = args[0] === 'left' ? left - offsetX : left + offsetX;
return {
top: top,
left: left,
transform: transform,
arrowLeft: arrowLeft,
arrowTop: arrowTop
};
}
function calculatePosition(triggerBounding, ContentBounding, positions, arrow, _ref2, wrapperBox) {
var offsetX = _ref2.offsetX,
offsetY = _ref2.offsetY;
var bestCoords;
var i = 0;
while (i < positions.length) {
bestCoords = getCoordinatesForPosition(triggerBounding, ContentBounding, positions[i], arrow, {
offsetX: offsetX,
offsetY: offsetY
});
var contentBox = {
top: bestCoords.top,
left: bestCoords.left,
width: ContentBounding.width,
height: ContentBounding.height
};
if (contentBox.top <= wrapperBox.top || contentBox.left <= wrapperBox.left || contentBox.top + contentBox.height >= wrapperBox.top + wrapperBox.height || contentBox.left + contentBox.width >= wrapperBox.left + wrapperBox.width) {
i++;
} else {
break;
}
}
return bestCoords;
}
var styles = {
popupContent: {
tooltip: {
position: 'absolute',
zIndex: '2',
width: '200px',
background: "rgb(255, 255, 255)",
border: "1px solid rgb(187, 187, 187)",
boxShadow: "rgba(0, 0, 0, 0.2) 0px 1px 3px",
padding: '5px'
},
modal: {
position: 'relative',
background: "rgb(255, 255, 255)",
width: '50%',
margin: 'auto',
border: "1px solid rgb(187, 187, 187)",
padding: '5px'
}
},
popupArrow: {
height: '10px',
width: '10px',
position: 'absolute',
background: 'rgb(255, 255, 255)',
transform: 'rotate(45deg)',
margin: '-5px',
zIndex: '-1',
boxShadow: 'rgba(0, 0, 0, 0.2) 1px 1px 1px'
},
overlay: {
tooltip: {
position: 'fixed',
top: '0',
bottom: '0',
left: '0',
right: '0'
},
modal: {
position: 'fixed',
top: '0',
bottom: '0',
left: '0',
right: '0',
background: "rgba(0, 0, 0,0.5)",
display: 'flex',
zIndex: '999'
}
}
};
var BodyEnd =
/*#__PURE__*/
function (_React$PureComponent) {
_inherits(BodyEnd, _React$PureComponent);
function BodyEnd() {
_classCallCheck(this, BodyEnd);
return _possibleConstructorReturn(this, _getPrototypeOf(BodyEnd).apply(this, arguments));
}
_createClass(BodyEnd, [{
key: "componentDidMount",
value: function componentDidMount() {
var _this$props = this.props,
className = _this$props.className,
onClick = _this$props.onClick;
this._popup = document.createElement('div'); //this._popup.onclick = onClick;
this._popup.className = className;
this._popup.style.cssText = 'position: fixed;top: 0;bottom: 0;left: 0;right: 0;background: rgba(0, 0, 0,0.5); display: flex;z-index: 999';
document.body.appendChild(this._popup);
this._render();
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate() {
this._render();
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
ReactDOM.unmountComponentAtNode(this._popup);
document.body.removeChild(this._popup);
}
}, {
key: "_render",
value: function _render() {
ReactDOM.render(this.props.children, this._popup);
}
}, {
key: "render",
value: function render() {
return null;
}
}]);
return BodyEnd;
}(React.PureComponent);
_defineProperty(BodyEnd, "propTypes", {
children: PropTypes.node
});
var POSITION_TYPES = ['top left', 'top center', 'top right', 'right top', 'right center', 'right bottom', 'bottom left', 'bottom center', 'bottom right', 'left top', 'left center', 'left bottom', 'center center'];
var Popup =
/*#__PURE__*/
function (_React$PureComponent) {
_inherits(Popup, _React$PureComponent);
function Popup(props) {
var _this;
_classCallCheck(this, Popup);
_this = _possibleConstructorReturn(this, _getPrototypeOf(Popup).call(this, props));
_defineProperty(_assertThisInitialized(_this), "repositionOnResize", function () {
_this.setPosition();
});
_defineProperty(_assertThisInitialized(_this), "onEscape", function (e) {
if (e.key === 'Escape') _this.closePopup();
});
_defineProperty(_assertThisInitialized(_this), "lockScroll", function () {
var lockScroll = _this.props.lockScroll;
var modal = _this.state.modal;
if (modal && lockScroll)
/* eslint-disable-next-line no-undef */
document.getElementsByTagName('body')[0].style.overflow = 'hidden';
});
_defineProperty(_assertThisInitialized(_this), "resetScroll", function () {
var lockScroll = _this.props.lockScroll;
var modal = _this.state.modal;
if (modal && lockScroll)
/* eslint-disable-next-line no-undef */
document.getElementsByTagName('body')[0].style.overflow = 'auto';
});
_defineProperty(_assertThisInitialized(_this), "togglePopup", function (e) {
// https://reactjs.org/docs/events.html#event-pooling
e.persist();
if (_this.state.isOpen) _this.closePopup(e);else _this.openPopup(e);
});
_defineProperty(_assertThisInitialized(_this), "openPopup", function (e) {
var _this$props = _this.props,
disabled = _this$props.disabled,
onOpen = _this$props.onOpen;
var isOpen = _this.state.isOpen;
if (isOpen || disabled) return;
onOpen(e);
_this.setState({
isOpen: true
}, function () {
_this.setPosition();
_this.lockScroll();
});
});
_defineProperty(_assertThisInitialized(_this), "closePopup", function (e) {
var onClose = _this.props.onClose;
var isOpen = _this.state.isOpen;
if (!isOpen) return;
onClose(e);
_this.setState({
isOpen: false
}, function () {
_this.resetScroll();
});
});
_defineProperty(_assertThisInitialized(_this), "onMouseEnter", function () {
clearTimeout(_this.timeOut);
var mouseEnterDelay = _this.props.mouseEnterDelay;
_this.timeOut = setTimeout(function () {
return _this.openPopup();
}, mouseEnterDelay);
});
_defineProperty(_assertThisInitialized(_this), "onMouseLeave", function () {
clearTimeout(_this.timeOut);
var mouseLeaveDelay = _this.props.mouseLeaveDelay;
_this.timeOut = setTimeout(function () {
return _this.closePopup();
}, mouseLeaveDelay);
});
_defineProperty(_assertThisInitialized(_this), "getTooltipBoundary", function () {
var keepTooltipInside = _this.props.keepTooltipInside;
var boundingBox = {
top: 0,
left: 0,
/* eslint-disable-next-line no-undef */
width: window.innerWidth,
/* eslint-disable-next-line no-undef */
height: window.innerHeight
};
if (typeof keepTooltipInside === 'string') {
/* eslint-disable-next-line no-undef */
var selector = document.querySelector(keepTooltipInside);
if (process.env.NODE_ENV !== 'production') {
if (selector === null) throw new Error("".concat(keepTooltipInside, " selector does not exist : keepTooltipInside must be a valid html selector 'class' or 'Id' or a boolean value"));
}
boundingBox = selector.getBoundingClientRect();
}
return boundingBox;
});
_defineProperty(_assertThisInitialized(_this), "setPosition", function () {
var _this$state = _this.state,
modal = _this$state.modal,
isOpen = _this$state.isOpen;
if (modal || !isOpen) return;
var _this$props2 = _this.props,
arrow = _this$props2.arrow,
position = _this$props2.position,
offsetX = _this$props2.offsetX,
offsetY = _this$props2.offsetY,
keepTooltipInside = _this$props2.keepTooltipInside,
arrowStyle = _this$props2.arrowStyle,
className = _this$props2.className;
var helper = _this.HelperEl.getBoundingClientRect();
var trigger = _this.TriggerEl.getBoundingClientRect();
var content = _this.ContentEl.getBoundingClientRect();
var boundingBox = _this.getTooltipBoundary();
var positions = Array.isArray(position) ? position : [position]; // keepTooltipInside would be activated if the keepTooltipInside exist or the position is Array
if (keepTooltipInside || Array.isArray(position)) positions = [].concat(_toConsumableArray(positions), POSITION_TYPES);
var cords = calculatePosition(trigger, content, positions, arrow, {
offsetX: offsetX,
offsetY: offsetY
}, boundingBox);
_this.ContentEl.style.top = "".concat(cords.top - helper.top, "px");
_this.ContentEl.style.left = "".concat(cords.left - helper.left, "px");
if (arrow) {
_this.ArrowEl.style.transform = cords.transform;
_this.ArrowEl.style['-ms-transform'] = cords.transform;
_this.ArrowEl.style['-webkit-transform'] = cords.transform;
_this.ArrowEl.style.top = arrowStyle.top || cords.arrowTop;
_this.ArrowEl.style.left = arrowStyle.left || cords.arrowLeft;
_this.ArrowEl.classList.add("popup-arrow");
if (className !== '') {
_this.ArrowEl.classList.add("".concat(className, "-arrow"));
}
}
if (
/* eslint-disable-next-line no-undef */
window.getComputedStyle(_this.TriggerEl, null).getPropertyValue('position') === 'static' ||
/* eslint-disable-next-line no-undef */
window.getComputedStyle(_this.TriggerEl, null).getPropertyValue('position') === '') _this.TriggerEl.style.position = 'relative';
});
_defineProperty(_assertThisInitialized(_this), "addWarperAction", function () {
var _this$props3 = _this.props,
contentStyle = _this$props3.contentStyle,
className = _this$props3.className,
on = _this$props3.on;
var modal = _this.state.modal;
var popupContentStyle = modal ? styles.popupContent.modal : styles.popupContent.tooltip;
var childrenElementProps = {
className: "popup-content ".concat(className !== '' ? "".concat(className, "-content") : ''),
style: Object.assign({}, popupContentStyle, contentStyle),
ref: _this.setContentRef,
onClick: function onClick(e) {
e.stopPropagation();
}
};
if (!modal && on.indexOf('hover') >= 0) {
childrenElementProps.onMouseEnter = _this.onMouseEnter;
childrenElementProps.onMouseLeave = _this.onMouseLeave;
}
return childrenElementProps;
});
_defineProperty(_assertThisInitialized(_this), "renderTrigger", function () {
var triggerProps = {
key: 'T',
ref: _this.setTriggerRef
};
var _this$props4 = _this.props,
on = _this$props4.on,
trigger = _this$props4.trigger;
var isOpen = _this.state.isOpen;
var onAsArray = Array.isArray(on) ? on : [on];
for (var i = 0, len = onAsArray.length; i < len; i++) {
switch (onAsArray[i]) {
case 'click':
triggerProps.onClick = _this.togglePopup;
break;
case 'hover':
triggerProps.onMouseEnter = _this.onMouseEnter;
triggerProps.onMouseLeave = _this.onMouseLeave;
break;
case 'focus':
triggerProps.onFocus = _this.onMouseEnter;
break;
default:
}
}
if (typeof trigger === 'function') return !!trigger && React.cloneElement(trigger(isOpen), triggerProps);
return !!trigger && React.cloneElement(trigger, triggerProps);
});
_defineProperty(_assertThisInitialized(_this), "renderContent", function () {
var _this$props5 = _this.props,
arrow = _this$props5.arrow,
arrowStyle = _this$props5.arrowStyle,
children = _this$props5.children;
var _this$state2 = _this.state,
modal = _this$state2.modal,
isOpen = _this$state2.isOpen;
return React.createElement("div", _extends({}, _this.addWarperAction(), {
key: "C"
}), arrow && !modal && React.createElement("div", {
ref: _this.setArrowRef,
style: Object.assign({}, styles.popupArrow, arrowStyle)
}), typeof children === 'function' ? children(_this.closePopup, isOpen) : children);
});
_this.setTriggerRef = function (r) {
return _this.TriggerEl = r;
};
_this.setContentRef = function (r) {
return _this.ContentEl = r;
};
_this.setArrowRef = function (r) {
return _this.ArrowEl = r;
};
_this.setHelperRef = function (r) {
return _this.HelperEl = r;
};
_this.timeOut = 0;
var open = props.open,
_modal = props.modal,
defaultOpen = props.defaultOpen,
_trigger = props.trigger;
_this.state = {
isOpen: open || defaultOpen,
modal: _modal ? true : !_trigger // we create this modal state because the popup can't be a tooltip if the trigger prop doesn't exist
};
return _this;
}
_createClass(Popup, [{
key: "componentDidMount",
value: function componentDidMount() {
var _this$props6 = this.props,
closeOnEscape = _this$props6.closeOnEscape,
defaultOpen = _this$props6.defaultOpen,
repositionOnResize = _this$props6.repositionOnResize;
if (defaultOpen) {
this.setPosition();
this.lockScroll();
}
if (closeOnEscape) {
/* eslint-disable-next-line no-undef */
window.addEventListener('keyup', this.onEscape);
}
if (repositionOnResize) {
/* eslint-disable-next-line no-undef */
window.addEventListener('resize', this.repositionOnResize);
}
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
var _this$props7 = this.props,
open = _this$props7.open,
disabled = _this$props7.disabled;
var isOpen = this.state.isOpen;
if (prevProps.open !== open) {
if (open) this.openPopup();else this.closePopup(undefined, true);
}
if (prevProps.disabled !== disabled && disabled && isOpen) {
this.closePopup();
}
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
// kill any function to execute if the component is unmounted
clearTimeout(this.timeOut);
var _this$props8 = this.props,
closeOnEscape = _this$props8.closeOnEscape,
repositionOnResize = _this$props8.repositionOnResize; // remove events listeners
if (closeOnEscape) {
/* eslint-disable-next-line no-undef */
window.removeEventListener('keyup', this.onEscape);
}
if (repositionOnResize) {
/* eslint-disable-next-line no-undef */
window.removeEventListener('resize', this.repositionOnResize);
}
this.resetScroll();
}
}, {
key: "render",
value: function render() {
var _this$props9 = this.props,
overlayStyle = _this$props9.overlayStyle,
closeOnDocumentClick = _this$props9.closeOnDocumentClick,
className = _this$props9.className,
on = _this$props9.on,
trigger = _this$props9.trigger;
var _this$state3 = this.state,
modal = _this$state3.modal,
isOpen = _this$state3.isOpen;
var overlay = isOpen && !(on.indexOf('hover') >= 0);
var ovStyle = modal ? styles.overlay.modal : styles.overlay.tooltip;
return [this.renderTrigger(), overlay && React.createElement(BodyEnd, {
key: "O",
className: "popup-overlay tikfeed-popup-overlay ".concat(className !== '' ? "".concat(className, "-overlay") : ''),
style: Object.assign({}, ovStyle, overlayStyle),
onClick: closeOnDocumentClick ? this.closePopup : undefined
}, modal && this.renderContent()), isOpen && !modal && this.renderContent()];
}
}]);
return Popup;
}(React.PureComponent);
_defineProperty(Popup, "defaultProps", {
trigger: null,
onOpen: function onOpen() {},
onClose: function onClose() {},
defaultOpen: false,
open: false,
disabled: false,
closeOnDocumentClick: true,
repositionOnResize: true,
closeOnEscape: true,
on: ['click'],
contentStyle: {},
arrowStyle: {},
overlayStyle: {},
className: '',
position: 'bottom center',
modal: false,
lockScroll: false,
arrow: true,
offsetX: 0,
offsetY: 0,
mouseEnterDelay: 100,
mouseLeaveDelay: 100,
keepTooltipInside: false
});
if (process.env.NODE_ENV !== 'production') {
var PropTypes$1 = require('prop-types');
var TRIGGER_TYPES = ['hover', 'click', 'focus'];
Popup.propTypes = {
arrowStyle: PropTypes$1.object,
contentStyle: PropTypes$1.object,
overlayStyle: PropTypes$1.object,
className: PropTypes$1.string,
modal: PropTypes$1.bool,
arrow: PropTypes$1.bool,
closeOnDocumentClick: PropTypes$1.bool,
repositionOnResize: PropTypes$1.bool,
disabled: PropTypes$1.bool,
closeOnEscape: PropTypes$1.bool,
lockScroll: PropTypes$1.bool,
offsetX: PropTypes$1.number,
offsetY: PropTypes$1.number,
mouseEnterDelay: PropTypes$1.number,
mouseLeaveDelay: PropTypes$1.number,
onOpen: PropTypes$1.func,
onClose: PropTypes$1.func,
open: PropTypes$1.bool,
defaultOpen: PropTypes$1.bool,
trigger: PropTypes$1.oneOfType([PropTypes$1.func, PropTypes$1.element]),
// for uncontrolled component we don't need the trigger Element
on: PropTypes$1.oneOfType([PropTypes$1.oneOf(TRIGGER_TYPES), PropTypes$1.arrayOf(PropTypes$1.oneOf(TRIGGER_TYPES))]),
children: PropTypes$1.oneOfType([PropTypes$1.func, PropTypes$1.element, PropTypes$1.string]).isRequired,
position: PropTypes$1.oneOfType([PropTypes$1.oneOf(POSITION_TYPES), PropTypes$1.arrayOf(PropTypes$1.oneOf(POSITION_TYPES))]),
keepTooltipInside: PropTypes$1.oneOfType([PropTypes$1.bool, PropTypes$1.string])
};
}
module.exports = Popup;