react-tooltip
Version:
react tooltip component
734 lines (599 loc) • 25.8 kB
JavaScript
;
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;