UNPKG

react-tooltip

Version:
734 lines (599 loc) 25.8 kB
'use strict'; var _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; }; var _createClass = 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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _class, _class2, _temp; /* Decorators */ /* Utils */ /* CSS */ var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _classnames = require('classnames'); var _classnames2 = _interopRequireDefault(_classnames); var _staticMethods = require('./decorators/staticMethods'); var _staticMethods2 = _interopRequireDefault(_staticMethods); var _windowListener = require('./decorators/windowListener'); var _windowListener2 = _interopRequireDefault(_windowListener); var _customEvent = require('./decorators/customEvent'); var _customEvent2 = _interopRequireDefault(_customEvent); var _isCapture = require('./decorators/isCapture'); var _isCapture2 = _interopRequireDefault(_isCapture); var _getEffect = require('./decorators/getEffect'); var _getEffect2 = _interopRequireDefault(_getEffect); var _trackRemoval = require('./decorators/trackRemoval'); var _trackRemoval2 = _interopRequireDefault(_trackRemoval); var _getPosition = require('./utils/getPosition'); var _getPosition2 = _interopRequireDefault(_getPosition); var _getTipContent = require('./utils/getTipContent'); var _getTipContent2 = _interopRequireDefault(_getTipContent); var _aria = require('./utils/aria'); var _nodeListToArray = require('./utils/nodeListToArray'); var _nodeListToArray2 = _interopRequireDefault(_nodeListToArray); var _style = require('./style'); var _style2 = _interopRequireDefault(_style); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var ReactTooltip = (0, _staticMethods2.default)(_class = (0, _windowListener2.default)(_class = (0, _customEvent2.default)(_class = (0, _isCapture2.default)(_class = (0, _getEffect2.default)(_class = (0, _trackRemoval2.default)(_class = (_temp = _class2 = function (_React$Component) { _inherits(ReactTooltip, _React$Component); function ReactTooltip(props) { _classCallCheck(this, ReactTooltip); var _this = _possibleConstructorReturn(this, (ReactTooltip.__proto__ || Object.getPrototypeOf(ReactTooltip)).call(this, props)); _this.state = { place: props.place || 'top', // Direction of tooltip desiredPlace: props.place || 'top', type: 'dark', // Color theme of tooltip effect: 'float', // float or fixed show: false, border: false, offset: {}, extraClass: '', html: false, delayHide: 0, delayShow: 0, event: props.event || null, eventOff: props.eventOff || null, currentEvent: null, // Current mouse event currentTarget: null, // Current target of mouse event ariaProps: (0, _aria.parseAria)(props), // aria- and role attributes isEmptyTip: false, disable: false, originTooltip: null, isMultiline: false }; _this.bind(['showTooltip', 'updateTooltip', 'hideTooltip', 'hideTooltipOnScroll', 'getTooltipContent', 'globalRebuild', 'globalShow', 'globalHide', 'onWindowResize', 'mouseOnToolTip']); _this.mount = true; _this.delayShowLoop = null; _this.delayHideLoop = null; _this.delayReshow = null; _this.intervalUpdateContent = null; return _this; } /** * For unify the bind and unbind listener */ _createClass(ReactTooltip, [{ key: 'bind', value: function bind(methodArray) { var _this2 = this; methodArray.forEach(function (method) { _this2[method] = _this2[method].bind(_this2); }); } }, { key: 'componentDidMount', value: function componentDidMount() { var _props = this.props, insecure = _props.insecure, resizeHide = _props.resizeHide; if (insecure) { this.setStyleHeader(); // Set the style to the <link> } this.bindListener(); // Bind listener for tooltip this.bindWindowEvents(resizeHide); // Bind global event for static method } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { this.mount = false; this.clearTimer(); this.unbindListener(); this.removeScrollListener(); this.unbindWindowEvents(); } /** * Return if the mouse is on the tooltip. * @returns {boolean} true - mouse is on the tooltip */ }, { key: 'mouseOnToolTip', value: function mouseOnToolTip() { var show = this.state.show; if (show && this.tooltipRef) { /* old IE or Firefox work around */ if (!this.tooltipRef.matches) { /* old IE work around */ if (this.tooltipRef.msMatchesSelector) { this.tooltipRef.matches = this.tooltipRef.msMatchesSelector; } else { /* old Firefox work around */ this.tooltipRef.matches = this.tooltipRef.mozMatchesSelector; } } return this.tooltipRef.matches(':hover'); } return false; } /** * Pick out corresponded target elements */ }, { key: 'getTargetArray', value: function getTargetArray(id) { var targetArray = void 0; if (!id) { targetArray = document.querySelectorAll('[data-tip]:not([data-for])'); } else { var escaped = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); targetArray = document.querySelectorAll('[data-tip][data-for="' + escaped + '"]'); } // targetArray is a NodeList, convert it to a real array return (0, _nodeListToArray2.default)(targetArray); } /** * Bind listener to the target elements * These listeners used to trigger showing or hiding the tooltip */ }, { key: 'bindListener', value: function bindListener() { var _this3 = this; var _props2 = this.props, id = _props2.id, globalEventOff = _props2.globalEventOff, isCapture = _props2.isCapture; var targetArray = this.getTargetArray(id); targetArray.forEach(function (target) { var isCaptureMode = _this3.isCapture(target); var effect = _this3.getEffect(target); if (target.getAttribute('currentItem') === null) { target.setAttribute('currentItem', 'false'); } _this3.unbindBasicListener(target); if (_this3.isCustomEvent(target)) { _this3.customBindListener(target); return; } target.addEventListener('mouseenter', _this3.showTooltip, isCaptureMode); if (effect === 'float') { target.addEventListener('mousemove', _this3.updateTooltip, isCaptureMode); } target.addEventListener('mouseleave', _this3.hideTooltip, isCaptureMode); }); // Global event to hide tooltip if (globalEventOff) { window.removeEventListener(globalEventOff, this.hideTooltip); window.addEventListener(globalEventOff, this.hideTooltip, isCapture); } // Track removal of targetArray elements from DOM this.bindRemovalTracker(); } /** * Unbind listeners on target elements */ }, { key: 'unbindListener', value: function unbindListener() { var _this4 = this; var _props3 = this.props, id = _props3.id, globalEventOff = _props3.globalEventOff; var targetArray = this.getTargetArray(id); targetArray.forEach(function (target) { _this4.unbindBasicListener(target); if (_this4.isCustomEvent(target)) _this4.customUnbindListener(target); }); if (globalEventOff) window.removeEventListener(globalEventOff, this.hideTooltip); this.unbindRemovalTracker(); } /** * Invoke this before bind listener and unmount the component * it is necessary to invoke this even when binding custom event * so that the tooltip can switch between custom and default listener */ }, { key: 'unbindBasicListener', value: function unbindBasicListener(target) { var isCaptureMode = this.isCapture(target); target.removeEventListener('mouseenter', this.showTooltip, isCaptureMode); target.removeEventListener('mousemove', this.updateTooltip, isCaptureMode); target.removeEventListener('mouseleave', this.hideTooltip, isCaptureMode); } }, { key: 'getTooltipContent', value: function getTooltipContent() { var _props4 = this.props, getContent = _props4.getContent, children = _props4.children; // Generate tooltip content var content = void 0; if (getContent) { if (Array.isArray(getContent)) { content = getContent[0] && getContent[0](this.state.originTooltip); } else { content = getContent(this.state.originTooltip); } } return (0, _getTipContent2.default)(this.state.originTooltip, children, content, this.state.isMultiline); } }, { key: 'isEmptyTip', value: function isEmptyTip(placeholder) { return typeof placeholder === 'string' && placeholder === '' || placeholder === null; } /** * When mouse enter, show the tooltip */ }, { key: 'showTooltip', value: function showTooltip(e, isGlobalCall) { if (isGlobalCall) { // Don't trigger other elements belongs to other ReactTooltip var targetArray = this.getTargetArray(this.props.id); var isMyElement = targetArray.some(function (ele) { return ele === e.currentTarget; }); if (!isMyElement) return; } // Get the tooltip content // calculate in this phrase so that tip width height can be detected var _props5 = this.props, multiline = _props5.multiline, getContent = _props5.getContent; var originTooltip = e.currentTarget.getAttribute('data-tip'); var isMultiline = e.currentTarget.getAttribute('data-multiline') || multiline || false; // If it is focus event or called by ReactTooltip.show, switch to `solid` effect var switchToSolid = e instanceof window.FocusEvent || isGlobalCall; // if it needs to skip adding hide listener to scroll var scrollHide = true; if (e.currentTarget.getAttribute('data-scroll-hide')) { scrollHide = e.currentTarget.getAttribute('data-scroll-hide') === 'true'; } else if (this.props.scrollHide != null) { scrollHide = this.props.scrollHide; } // Make sure the correct place is set var desiredPlace = e.currentTarget.getAttribute('data-place') || this.props.place || 'top'; var effect = switchToSolid && 'solid' || this.getEffect(e.currentTarget); var offset = e.currentTarget.getAttribute('data-offset') || this.props.offset || {}; var result = (0, _getPosition2.default)(e, e.currentTarget, this.tooltipRef, desiredPlace, desiredPlace, effect, offset); if (result.position && this.props.overridePosition) { result.position = this.props.overridePosition(result.position, e.currentTarget, this.tooltipRef, desiredPlace, desiredPlace, effect, offset); } var place = result.isNewState ? result.newState.place : desiredPlace; // To prevent previously created timers from triggering this.clearTimer(); var target = e.currentTarget; var reshowDelay = this.state.show ? target.getAttribute('data-delay-update') || this.props.delayUpdate : 0; var self = this; var updateState = function updateState() { self.setState({ originTooltip: originTooltip, isMultiline: isMultiline, desiredPlace: desiredPlace, place: place, type: target.getAttribute('data-type') || self.props.type || 'dark', effect: effect, offset: offset, html: target.getAttribute('data-html') ? target.getAttribute('data-html') === 'true' : self.props.html || false, delayShow: target.getAttribute('data-delay-show') || self.props.delayShow || 0, delayHide: target.getAttribute('data-delay-hide') || self.props.delayHide || 0, delayUpdate: target.getAttribute('data-delay-update') || self.props.delayUpdate || 0, border: target.getAttribute('data-border') ? target.getAttribute('data-border') === 'true' : self.props.border || false, extraClass: target.getAttribute('data-class') || self.props.class || self.props.className || '', disable: target.getAttribute('data-tip-disable') ? target.getAttribute('data-tip-disable') === 'true' : self.props.disable || false, currentTarget: target }, function () { if (scrollHide) self.addScrollListener(self.state.currentTarget); self.updateTooltip(e); if (getContent && Array.isArray(getContent)) { self.intervalUpdateContent = setInterval(function () { if (self.mount) { var _getContent = self.props.getContent; var placeholder = (0, _getTipContent2.default)(originTooltip, '', _getContent[0](), isMultiline); var isEmptyTip = self.isEmptyTip(placeholder); self.setState({ isEmptyTip: isEmptyTip }); self.updatePosition(); } }, getContent[1]); } }); }; // If there is no delay call immediately, don't allow events to get in first. if (reshowDelay) { this.delayReshow = setTimeout(updateState, reshowDelay); } else { updateState(); } } /** * When mouse hover, update tool tip */ }, { key: 'updateTooltip', value: function updateTooltip(e) { var _this5 = this; var _state = this.state, delayShow = _state.delayShow, disable = _state.disable; var afterShow = this.props.afterShow; var placeholder = this.getTooltipContent(); var delayTime = parseInt(delayShow, 10); var eventTarget = e.currentTarget || e.target; // Check if the mouse is actually over the tooltip, if so don't hide the tooltip if (this.mouseOnToolTip()) { return; } if (this.isEmptyTip(placeholder) || disable) return; // if the tooltip is empty, disable the tooltip var updateState = function updateState() { if (Array.isArray(placeholder) && placeholder.length > 0 || placeholder) { var isInvisible = !_this5.state.show; _this5.setState({ currentEvent: e, currentTarget: eventTarget, show: true }, function () { _this5.updatePosition(); if (isInvisible && afterShow) afterShow(e); }); } }; clearTimeout(this.delayShowLoop); if (delayShow) { this.delayShowLoop = setTimeout(updateState, delayTime); } else { updateState(); } } /* * If we're mousing over the tooltip remove it when we leave. */ }, { key: 'listenForTooltipExit', value: function listenForTooltipExit() { var show = this.state.show; if (show && this.tooltipRef) { this.tooltipRef.addEventListener('mouseleave', this.hideTooltip); } } }, { key: 'removeListenerForTooltipExit', value: function removeListenerForTooltipExit() { var show = this.state.show; if (show && this.tooltipRef) { this.tooltipRef.removeEventListener('mouseleave', this.hideTooltip); } } /** * When mouse leave, hide tooltip */ }, { key: 'hideTooltip', value: function hideTooltip(e, hasTarget) { var _this6 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { isScroll: false }; var disable = this.state.disable; var isScroll = options.isScroll; var delayHide = isScroll ? 0 : this.state.delayHide; var afterHide = this.props.afterHide; var placeholder = this.getTooltipContent(); if (!this.mount) return; if (this.isEmptyTip(placeholder) || disable) return; // if the tooltip is empty, disable the tooltip if (hasTarget) { // Don't trigger other elements belongs to other ReactTooltip var targetArray = this.getTargetArray(this.props.id); var isMyElement = targetArray.some(function (ele) { return ele === e.currentTarget; }); if (!isMyElement || !this.state.show) return; } var resetState = function resetState() { var isVisible = _this6.state.show; // Check if the mouse is actually over the tooltip, if so don't hide the tooltip if (_this6.mouseOnToolTip()) { _this6.listenForTooltipExit(); return; } _this6.removeListenerForTooltipExit(); _this6.setState({ show: false }, function () { _this6.removeScrollListener(); if (isVisible && afterHide) afterHide(e); }); }; this.clearTimer(); if (delayHide) { this.delayHideLoop = setTimeout(resetState, parseInt(delayHide, 10)); } else { resetState(); } } /** * When scroll, hide tooltip */ }, { key: 'hideTooltipOnScroll', value: function hideTooltipOnScroll(event, hasTarget) { this.hideTooltip(event, hasTarget, { isScroll: true }); } /** * Add scroll event listener when tooltip show * automatically hide the tooltip when scrolling */ }, { key: 'addScrollListener', value: function addScrollListener(currentTarget) { var isCaptureMode = this.isCapture(currentTarget); window.addEventListener('scroll', this.hideTooltipOnScroll, isCaptureMode); } }, { key: 'removeScrollListener', value: function removeScrollListener() { window.removeEventListener('scroll', this.hideTooltipOnScroll); } // Calculation the position }, { key: 'updatePosition', value: function updatePosition() { var _this7 = this; var _state2 = this.state, currentEvent = _state2.currentEvent, currentTarget = _state2.currentTarget, place = _state2.place, desiredPlace = _state2.desiredPlace, effect = _state2.effect, offset = _state2.offset; var node = this.tooltipRef; var result = (0, _getPosition2.default)(currentEvent, currentTarget, node, place, desiredPlace, effect, offset); if (result.position && this.props.overridePosition) { result.position = this.props.overridePosition(result.position, currentEvent, currentTarget, node, place, desiredPlace, effect, offset); } if (result.isNewState) { // Switch to reverse placement return this.setState(result.newState, function () { _this7.updatePosition(); }); } // Set tooltip position node.style.left = result.position.left + 'px'; node.style.top = result.position.top + 'px'; } /** * Set style tag in header * in this way we can insert default css */ }, { key: 'setStyleHeader', value: function setStyleHeader() { var head = document.getElementsByTagName('head')[0]; if (!head.querySelector('style[id="react-tooltip"]')) { var tag = document.createElement('style'); tag.id = 'react-tooltip'; tag.innerHTML = _style2.default; /* eslint-disable */ if (typeof __webpack_nonce__ !== 'undefined' && __webpack_nonce__) { tag.setAttribute('nonce', __webpack_nonce__); } /* eslint-enable */ head.insertBefore(tag, head.firstChild); } } /** * CLear all kinds of timeout of interval */ }, { key: 'clearTimer', value: function clearTimer() { clearTimeout(this.delayShowLoop); clearTimeout(this.delayHideLoop); clearTimeout(this.delayReshow); clearInterval(this.intervalUpdateContent); } }, { key: 'render', value: function render() { var _this8 = this; var _state3 = this.state, extraClass = _state3.extraClass, html = _state3.html, ariaProps = _state3.ariaProps, disable = _state3.disable; var placeholder = this.getTooltipContent(); var isEmptyTip = this.isEmptyTip(placeholder); var tooltipClass = (0, _classnames2.default)('__react_component_tooltip', { 'show': this.state.show && !disable && !isEmptyTip }, { 'border': this.state.border }, { 'place-top': this.state.place === 'top' }, { 'place-bottom': this.state.place === 'bottom' }, { 'place-left': this.state.place === 'left' }, { 'place-right': this.state.place === 'right' }, { 'type-dark': this.state.type === 'dark' }, { 'type-success': this.state.type === 'success' }, { 'type-warning': this.state.type === 'warning' }, { 'type-error': this.state.type === 'error' }, { 'type-info': this.state.type === 'info' }, { 'type-light': this.state.type === 'light' }, { 'allow_hover': this.props.delayUpdate }, { 'allow_click': this.props.clickable }); var Wrapper = this.props.wrapper; if (ReactTooltip.supportedWrappers.indexOf(Wrapper) < 0) { Wrapper = ReactTooltip.defaultProps.wrapper; } if (html) { return _react2.default.createElement(Wrapper, _extends({ className: tooltipClass + ' ' + extraClass, id: this.props.id, ref: function ref(_ref) { return _this8.tooltipRef = _ref; } }, ariaProps, { 'data-id': 'tooltip', dangerouslySetInnerHTML: { __html: placeholder } })); } else { return _react2.default.createElement( Wrapper, _extends({ className: tooltipClass + ' ' + extraClass, id: this.props.id }, ariaProps, { ref: function ref(_ref2) { return _this8.tooltipRef = _ref2; }, 'data-id': 'tooltip' }), placeholder ); } } }], [{ key: 'getDerivedStateFromProps', value: function getDerivedStateFromProps(nextProps, prevState) { var ariaProps = prevState.ariaProps; var newAriaProps = (0, _aria.parseAria)(nextProps); var isChanged = Object.keys(newAriaProps).some(function (props) { return newAriaProps[props] !== ariaProps[props]; }); if (!isChanged) { return null; } return _extends({}, prevState, { ariaProps: newAriaProps }); } }]); return ReactTooltip; }(_react2.default.Component), _class2.propTypes = { children: _propTypes2.default.any, place: _propTypes2.default.string, type: _propTypes2.default.string, effect: _propTypes2.default.string, offset: _propTypes2.default.object, multiline: _propTypes2.default.bool, border: _propTypes2.default.bool, insecure: _propTypes2.default.bool, class: _propTypes2.default.string, className: _propTypes2.default.string, id: _propTypes2.default.string, html: _propTypes2.default.bool, delayHide: _propTypes2.default.number, delayUpdate: _propTypes2.default.number, delayShow: _propTypes2.default.number, event: _propTypes2.default.string, eventOff: _propTypes2.default.string, watchWindow: _propTypes2.default.bool, isCapture: _propTypes2.default.bool, globalEventOff: _propTypes2.default.string, getContent: _propTypes2.default.any, afterShow: _propTypes2.default.func, afterHide: _propTypes2.default.func, overridePosition: _propTypes2.default.func, disable: _propTypes2.default.bool, scrollHide: _propTypes2.default.bool, resizeHide: _propTypes2.default.bool, wrapper: _propTypes2.default.string, clickable: _propTypes2.default.bool }, _class2.defaultProps = { insecure: true, resizeHide: true, wrapper: 'div', clickable: false }, _class2.supportedWrappers = ['div', 'span'], _class2.displayName = 'ReactTooltip', _temp)) || _class) || _class) || _class) || _class) || _class) || _class; /* export default not fit for standalone, it will exports {default:...} */ module.exports = ReactTooltip;