774 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import _extends from 'babel-runtime/helpers/extends';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
var _class, _temp;
import React, { Children, Component } from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { dom, events, focus, func, guid, KEYCODE, support } from '../util';
import overlayManager from './manager';
import Gateway from './gateway';
import Position from './position';
import findNode from './utils/find-node';
var saveLastFocusNode = focus.saveLastFocusNode,
getFocusNodeList = focus.getFocusNodeList,
backLastFocusNode = focus.backLastFocusNode;
var makeChain = func.makeChain,
noop = func.noop,
bindCtx = func.bindCtx;
var isScrollDisplay = function isScrollDisplay(element) {
try {
var scrollbarStyle = window.getComputedStyle(element, '::-webkit-scrollbar');
return !scrollbarStyle || scrollbarStyle.getPropertyValue('display') !== 'none';
} catch (e) {
// ignore error for firefox
}
return true;
};
var hasScroll = function hasScroll() {
var doc = document.documentElement;
return doc.scrollHeight > doc.clientHeight && dom.scrollbar().width > 0 && isScrollDisplay(document.documentElement) && isScrollDisplay(document.body);
};
var prefixes = ['-webkit-', '-moz-', '-o-', 'ms-', ''];
var getStyleProperty = function getStyleProperty(node, name) {
var style = window.getComputedStyle(node);
var ret = '';
for (var i = 0; i < prefixes.length; i++) {
ret = style.getPropertyValue(prefixes[i] + name);
if (ret) {
break;
}
}
return ret;
};
var modals = [];
var bodyOverflowY = void 0,
bodyPaddingRight = void 0;
/**
* Overlay
* */
var Overlay = (_temp = _class = function (_Component) {
_inherits(Overlay, _Component);
function Overlay(props) {
_classCallCheck(this, Overlay);
var _this = _possibleConstructorReturn(this, _Component.call(this, props));
_this.saveContentRef = function (ref) {
_this.contentRef = ref;
};
_this.saveGatewayRef = function (ref) {
_this.gatewayRef = ref;
};
_this.state = {
visible: props.visible,
status: 'none',
animation: _this.getAnimation(props)
};
_this.lastAlign = props.align;
bindCtx(_this, ['handlePosition', 'handleAnimateEnd', 'handleDocumentKeyDown', 'handleDocumentClick', 'handleMaskClick', 'beforeOpen', 'beforeClose']);
_this.timeoutMap = {};
return _this;
}
Overlay.prototype.componentWillMount = function componentWillMount() {
if (this.props.visible) {
this.beforeOpen();
this.props.beforeOpen();
if (this.state.animation && support.animation) {
this.enter();
}
}
};
Overlay.prototype.componentDidMount = function componentDidMount() {
this.componentDidUpdate({ visible: false });
this.addDocumentEvents();
overlayManager.addOverlay(this);
if (this.state.visible) {
this._isMounted = true;
}
};
Overlay.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
if (!this._isMounted && nextProps.visible) {
this._isMounted = true;
}
var willOpen = !this.props.visible && nextProps.visible;
var willClose = this.props.visible && !nextProps.visible;
if (nextProps.align !== this.props.align) {
this.lastAlign = this.props.align;
}
if (willOpen) {
this.beforeOpen();
nextProps.beforeOpen();
} else if (willClose) {
this.beforeClose();
nextProps.beforeClose();
}
if (nextProps.animation || nextProps.animation === false) {
this.setState({
animation: nextProps.animation
});
}
if (nextProps.animation !== false && support.animation) {
if (willOpen) {
this.enter();
} else if (willClose) {
this.leave();
}
} else {
this.setState({
visible: nextProps.visible
});
}
};
Overlay.prototype.componentDidUpdate = function componentDidUpdate(prevProps) {
var _this2 = this;
var open = !prevProps.visible && this.props.visible;
var close = prevProps.visible && !this.props.visible;
if (this.state.animation && support.animation) {
if (open || close) {
this.addAnimationEvents();
}
} else {
var wrapperNode = this.getWrapperNode();
if (open) {
setTimeout(function () {
_this2.props.onOpen();
_this2.props.afterOpen();
dom.addClass(wrapperNode, 'opened');
overlayManager.addOverlay(_this2);
});
} else if (close) {
this.props.onClose();
this.props.afterClose();
dom.removeClass(wrapperNode, 'opened');
overlayManager.removeOverlay(this);
}
this.setFocusNode();
}
};
Overlay.prototype.componentWillUnmount = function componentWillUnmount() {
this._isDestroyed = true;
this._isMounted = false;
overlayManager.removeOverlay(this);
this.removeDocumentEvents();
if (this.focusTimeout) {
clearTimeout(this.focusTimeout);
}
if (this._animation) {
this._animation.off();
this._animation = null;
}
this.beforeClose();
};
Overlay.prototype.getAnimation = function getAnimation(props) {
if (props.animation === false) {
return false;
}
if (props.animation) {
return props.animation;
}
return this.getAnimationByAlign(props.align);
};
Overlay.prototype.getAnimationByAlign = function getAnimationByAlign(align) {
switch (align[0]) {
case 't':
return {
in: 'expandInDown',
out: 'expandOutUp'
};
case 'b':
return {
in: 'expandInUp',
out: 'expandOutDown'
};
default:
return {
in: 'expandInDown',
out: 'expandOutUp'
};
}
};
Overlay.prototype.addAnimationEvents = function addAnimationEvents() {
var _this3 = this;
setTimeout(function () {
var node = _this3.getContentNode();
if (node) {
var id = guid();
_this3._animation = events.on(node, support.animation.end, _this3.handleAnimateEnd.bind(_this3, id));
var animationDelay = parseFloat(getStyleProperty(node, 'animation-delay')) || 0;
var animationDuration = parseFloat(getStyleProperty(node, 'animation-duration')) || 0;
var time = animationDelay + animationDuration;
if (time) {
_this3.timeoutMap[id] = setTimeout(function () {
_this3.handleAnimateEnd(id);
}, time * 1000 + 200);
}
}
});
};
Overlay.prototype.handlePosition = function handlePosition(config) {
var align = config.align.join(' ');
if (!('animation' in this.props) && this.props.needAdjust && this.lastAlign !== align) {
this.setState({
animation: this.getAnimationByAlign(align)
});
}
var status = this.state.status;
if (status === 'mounting') {
this.setState({
status: 'entering'
});
}
this.lastAlign = align;
};
Overlay.prototype.handleAnimateEnd = function handleAnimateEnd(id) {
if (this.timeoutMap[id]) {
clearTimeout(this.timeoutMap[id]);
}
delete this.timeoutMap[id];
if (this._animation) {
this._animation.off();
this._animation = null;
}
if (!this._isMounted) {
return;
}
if (this.state.status === 'leaving') {
this.setState({
visible: false,
status: 'none'
});
this.onLeaved();
} else if (this.state.status === 'entering') {
this.setState({
status: 'none'
});
this.onEntered();
}
};
Overlay.prototype.enter = function enter() {
var _this4 = this;
this.setState({
visible: true,
status: 'mounting'
}, function () {
// NOTE: setState callback (second argument) now fires immediately after componentDidMount / componentDidUpdate instead of after all components have rendered.
setTimeout(function () {
if (!_this4._isDestroyed) {
_this4.onEntering();
}
});
});
};
Overlay.prototype.leave = function leave() {
this.setState({
status: 'leaving'
});
this.onLeaving();
};
Overlay.prototype.onEntering = function onEntering() {
var wrapperNode = this.getWrapperNode();
dom.addClass(wrapperNode, 'opened');
this.props.onOpen();
};
Overlay.prototype.onLeaving = function onLeaving() {
var wrapperNode = this.getWrapperNode();
dom.removeClass(wrapperNode, 'opened');
this.props.onClose();
};
Overlay.prototype.onEntered = function onEntered() {
overlayManager.addOverlay(this);
this.setFocusNode();
this.props.afterOpen();
};
Overlay.prototype.onLeaved = function onLeaved() {
overlayManager.removeOverlay(this);
this.setFocusNode();
this.props.afterClose();
};
Overlay.prototype.beforeOpen = function beforeOpen() {
if (this.props.disableScroll) {
if (modals.length === 0) {
var style = {
overflowY: 'hidden'
};
var body = document.body;
bodyOverflowY = body.style.overflowY;
if (hasScroll()) {
bodyPaddingRight = body.style.paddingRight;
style.paddingRight = dom.getStyle(body, 'paddingRight') + dom.scrollbar().width + 'px';
}
dom.setStyle(body, style);
}
modals.push(this);
}
};
Overlay.prototype.beforeClose = function beforeClose() {
if (this.props.disableScroll) {
var index = modals.indexOf(this);
if (index > -1) {
if (modals.length === 1) {
var style = {
overflowY: bodyOverflowY
};
if (hasScroll()) {
style.paddingRight = bodyPaddingRight;
}
dom.setStyle(document.body, style);
bodyOverflowY = undefined;
bodyPaddingRight = undefined;
}
modals.splice(index, 1);
}
}
};
Overlay.prototype.setFocusNode = function setFocusNode() {
var _this5 = this;
if (!this.props.autoFocus) {
return;
}
if (this.state.visible && !this._hasFocused) {
saveLastFocusNode();
// 这个时候很可能上一个弹层的关闭事件还未触发,导致焦点已经回到触发的元素
// 这里延时处理一下,延时的时间为 document.click 捕获触发的延时时间
this.focusTimeout = setTimeout(function () {
var node = _this5.getContentNode();
if (node) {
var focusNodeList = getFocusNodeList(node);
if (focusNodeList.length) {
focusNodeList[0].focus();
}
_this5._hasFocused = true;
}
}, 100);
} else if (!this.state.visible && this._hasFocused) {
backLastFocusNode();
this._hasFocused = false;
}
};
Overlay.prototype.getContent = function getContent() {
return this.contentRef;
};
Overlay.prototype.getContentNode = function getContentNode() {
try {
return findDOMNode(this.contentRef);
} catch (err) {
return null;
}
};
Overlay.prototype.getWrapperNode = function getWrapperNode() {
return this.gatewayRef ? this.gatewayRef.getChildNode() : null;
};
Overlay.prototype.addDocumentEvents = function addDocumentEvents() {
if (this.props.canCloseByEsc) {
this._keydownEvents = events.on(document, 'keydown', this.handleDocumentKeyDown);
}
if (this.props.canCloseByOutSideClick) {
this._clickEvents = events.on(document, 'click', this.handleDocumentClick);
this._touchEvents = events.on(document, 'touchend', this.handleDocumentClick);
}
};
Overlay.prototype.removeDocumentEvents = function removeDocumentEvents() {
if (this._keydownEvents) {
this._keydownEvents.off();
this._keydownEvents = null;
}
if (this._clickEvents) {
this._clickEvents.off();
this._clickEvents = null;
}
if (this._touchEvents) {
this._touchEvents.off();
this._touchEvents = null;
}
};
Overlay.prototype.handleDocumentKeyDown = function handleDocumentKeyDown(e) {
if (this.state.visible && e.keyCode === KEYCODE.ESC && overlayManager.isCurrentOverlay(this)) {
this.props.onRequestClose('keyboard', e);
}
};
Overlay.prototype.handleDocumentClick = function handleDocumentClick(e) {
var _this6 = this;
if (this.state.visible) {
var safeNode = this.props.safeNode;
var safeNodes = Array.isArray(safeNode) ? [].concat(safeNode) : [safeNode];
safeNodes.unshift(function () {
return _this6.getWrapperNode();
});
for (var i = 0; i < safeNodes.length; i++) {
var node = findNode(safeNodes[i], this.props);
// HACK: 如果触发点击的节点是弹层内部的节点,并且在被点击后立即销毁,那么此时无法使用 node.contains(e.target)
// 来判断此时点击的节点是否是弹层内部的节点,额外判断
if (node && (node === e.target || node.contains(e.target) || e.target !== document && !document.documentElement.contains(e.target))) {
return;
}
}
this.props.onRequestClose('docClick', e);
}
};
Overlay.prototype.handleMaskClick = function handleMaskClick(e) {
if (this.props.canCloseByMask) {
this.props.onRequestClose('maskClick', e);
}
};
// 兼容过去的用法: this.popupRef.getInstance().overlay.getInstance().getContentNode()
Overlay.prototype.getInstance = function getInstance() {
return this;
};
Overlay.prototype.render = function render() {
var _props = this.props,
prefix = _props.prefix,
className = _props.className,
style = _props.style,
propChildren = _props.children,
target = _props.target,
align = _props.align,
offset = _props.offset,
container = _props.container,
hasMask = _props.hasMask,
needAdjust = _props.needAdjust,
autoFit = _props.autoFit,
beforePosition = _props.beforePosition,
onPosition = _props.onPosition,
wrapperStyle = _props.wrapperStyle,
rtl = _props.rtl,
propShouldUpdatePosition = _props.shouldUpdatePosition,
cache = _props.cache,
wrapperClassName = _props.wrapperClassName,
onMaskMouseEnter = _props.onMaskMouseEnter,
onMaskMouseLeave = _props.onMaskMouseLeave,
maskClass = _props.maskClass,
isChildrenInMask = _props.isChildrenInMask;
var _state = this.state,
stateVisible = _state.visible,
status = _state.status,
animation = _state.animation;
var children = stateVisible || cache && this._isMounted ? propChildren : null;
if (children) {
var _classnames, _classnames2;
var child = Children.only(children);
// if chlild is a functional component wrap in a component to allow a ref to be set
if (typeof child.type === 'function' && !(child.type.prototype instanceof Component)) {
child = React.createElement(
'div',
{ role: 'none' },
child
);
}
var childClazz = classnames((_classnames = {}, _classnames[prefix + 'overlay-inner'] = true, _classnames[animation.in] = status === 'entering' || status === 'mounting', _classnames[animation.out] = status === 'leaving', _classnames[child.props.className] = !!child.props.className, _classnames[className] = !!className, _classnames));
if (typeof child.ref === 'string') {
throw new Error('Can not set ref by string in Overlay, use function instead.');
}
children = React.cloneElement(child, {
className: childClazz,
style: _extends({}, child.props.style, style),
ref: makeChain(this.saveContentRef, child.ref),
'aria-hidden': !stateVisible && cache && this._isMounted,
onClick: this.props.onClick
});
if (align) {
var shouldUpdatePosition = status === 'leaving' ? false : propShouldUpdatePosition;
children = React.createElement(Position, {
children: children,
target: target,
align: align,
offset: offset,
autoFit: autoFit,
container: container,
needAdjust: needAdjust,
beforePosition: beforePosition,
onPosition: makeChain(this.handlePosition, onPosition),
shouldUpdatePosition: shouldUpdatePosition,
rtl: rtl
});
}
var wrapperClazz = classnames([prefix + 'overlay-wrapper', wrapperClassName]);
var newWrapperStyle = _extends({}, {
display: stateVisible ? '' : 'none'
}, wrapperStyle);
var maskClazz = classnames((_classnames2 = {}, _classnames2[prefix + 'overlay-backdrop'] = true, _classnames2[maskClass] = !!maskClass, _classnames2));
children = React.createElement(
'div',
{
className: wrapperClazz,
style: newWrapperStyle,
dir: rtl ? 'rtl' : undefined
},
hasMask ? React.createElement(
'div',
{
className: maskClazz,
onClick: this.handleMaskClick,
onMouseEnter: onMaskMouseEnter,
onMouseLeave: onMaskMouseLeave,
dir: rtl ? 'rtl' : undefined
},
isChildrenInMask && children
) : null,
!isChildrenInMask && children
);
}
return React.createElement(Gateway, _extends({ container: container, target: target, children: children }, {
ref: this.saveGatewayRef
}));
};
return Overlay;
}(Component), _class.propTypes = {
prefix: PropTypes.string,
pure: PropTypes.bool,
rtl: PropTypes.bool,
className: PropTypes.string,
style: PropTypes.object,
/**
* 弹层内容
*/
children: PropTypes.any,
/**
* 是否显示弹层
*/
visible: PropTypes.bool,
/**
* 弹层请求关闭时触发事件的回调函数
* @param {String} type 弹层关闭的来源
* @param {Object} e DOM 事件
*/
onRequestClose: PropTypes.func,
/**
* 弹层定位的参照元素
*/
target: PropTypes.any,
/**
* 弹层相对于参照元素的定位, 详见开发指南的[定位部分](#定位)
*/
align: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
/**
* 弹层相对于trigger的定位的微调, 接收数组[hoz, ver], 表示弹层在 left / top 上的增量
* e.g. [100, 100] 表示往右(RTL 模式下是往左) 、下分布偏移100px
*/
offset: PropTypes.array,
/**
* 渲染组件的容器,如果是函数需要返回 ref如果是字符串则是该 DOM 的 id也可以直接传入 DOM 节点
*/
container: PropTypes.any,
/**
* 是否显示遮罩
*/
hasMask: PropTypes.bool,
/**
* 是否支持 esc 按键关闭弹层
*/
canCloseByEsc: PropTypes.bool,
/**
* 点击弹层外的区域是否关闭弹层,不显示遮罩时生效
*/
canCloseByOutSideClick: PropTypes.bool,
/**
* 点击遮罩区域是否关闭弹层,显示遮罩时生效
*/
canCloseByMask: PropTypes.bool,
/**
* 弹层打开前触发事件的回调函数
*/
beforeOpen: PropTypes.func,
/**
* 弹层打开时触发事件的回调函数
*/
onOpen: PropTypes.func,
/**
* 弹层打开后触发事件的回调函数, 如果有动画,则在动画结束后触发
*/
afterOpen: PropTypes.func,
/**
* 弹层关闭前触发事件的回调函数
*/
beforeClose: PropTypes.func,
/**
* 弹层关闭时触发事件的回调函数
*/
onClose: PropTypes.func,
/**
* 弹层关闭后触发事件的回调函数, 如果有动画,则在动画结束后触发
*/
afterClose: PropTypes.func,
/**
* 弹层定位完成前触发的事件
*/
beforePosition: PropTypes.func,
/**
* 弹层定位完成时触发的事件
* @param {Object} config 定位的参数
* @param {Array} config.align 对齐方式,如 ['cc', 'cc'](如果开启 needAdjust可能和预先设置的 align 不同)
* @param {Number} config.top 距离视口顶部距离
* @param {Number} config.left 距离视口左侧距离
* @param {Object} node 定位参照的容器节点
*/
onPosition: PropTypes.func,
/**
* 是否在每次弹层重新渲染后强制更新定位信息,一般用于弹层内容区域大小发生变化时,仍需保持原来的定位方式
*/
shouldUpdatePosition: PropTypes.bool,
/**
* 弹层打开时是否让其中的元素自动获取焦点
*/
autoFocus: PropTypes.bool,
/**
* 当弹层由于页面滚动等情况不在可视区域时,是否自动调整定位以出现在可视区域
*/
needAdjust: PropTypes.bool,
/**
* 是否禁用页面滚动
*/
disableScroll: PropTypes.bool,
/**
* 隐藏时是否保留子节点
*/
cache: PropTypes.bool,
/**
* 安全节点,当点击 document 的时候,如果包含该节点则不会关闭弹层,如果是函数需要返回 ref如果是字符串则是该 DOM 的 id也可以直接传入 DOM 节点,或者以上值组成的数组
*/
safeNode: PropTypes.any,
/**
* 弹层的根节点的样式类
*/
wrapperClassName: PropTypes.string,
/**
* 弹层的根节点的内联样式
*/
wrapperStyle: PropTypes.object,
/**
* 配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false则不播放动画。 请参考 Animate 组件的文档获取可用的动画名
* @default { in: 'expandInDown', out: 'expandOutUp' }
*/
animation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
onMaskMouseEnter: PropTypes.func,
onMaskMouseLeave: PropTypes.func,
onClick: PropTypes.func,
maskClass: PropTypes.string,
isChildrenInMask: PropTypes.bool
}, _class.defaultProps = {
prefix: 'next-',
pure: false,
visible: false,
onRequestClose: noop,
target: Position.VIEWPORT,
align: 'tl bl',
offset: [0, 0],
hasMask: false,
canCloseByEsc: true,
canCloseByOutSideClick: true,
canCloseByMask: true,
beforeOpen: noop,
onOpen: noop,
afterOpen: noop,
beforeClose: noop,
onClose: noop,
afterClose: noop,
beforePosition: noop,
onPosition: noop,
onMaskMouseEnter: noop,
onMaskMouseLeave: noop,
shouldUpdatePosition: false,
autoFocus: false,
needAdjust: true,
disableScroll: false,
cache: false,
isChildrenInMask: false,
onClick: function onClick(e) {
return e.stopPropagation();
},
maskClass: ''
}, _temp);
Overlay.displayName = 'Overlay';
export { Overlay as default };