UNPKG

@alifd/next

Version:

A configurable component library for web built on React.

769 lines (657 loc) 25.8 kB
'use strict'; exports.__esModule = true; exports.default = undefined; var _extends2 = require('babel-runtime/helpers/extends'); var _extends3 = _interopRequireDefault(_extends2); var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); var _inherits2 = require('babel-runtime/helpers/inherits'); var _inherits3 = _interopRequireDefault(_inherits2); var _class, _temp; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _reactDom = require('react-dom'); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _classnames2 = require('classnames'); var _classnames3 = _interopRequireDefault(_classnames2); var _util = require('../util'); var _manager = require('./manager'); var _manager2 = _interopRequireDefault(_manager); var _gateway = require('./gateway'); var _gateway2 = _interopRequireDefault(_gateway); var _position = require('./position'); var _position2 = _interopRequireDefault(_position); var _findNode = require('./utils/find-node'); var _findNode2 = _interopRequireDefault(_findNode); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var saveLastFocusNode = _util.focus.saveLastFocusNode, getFocusNodeList = _util.focus.getFocusNodeList, backLastFocusNode = _util.focus.backLastFocusNode; var makeChain = _util.func.makeChain, noop = _util.func.noop, bindCtx = _util.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 && _util.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) { (0, _inherits3.default)(Overlay, _Component); function Overlay(props) { (0, _classCallCheck3.default)(this, Overlay); var _this = (0, _possibleConstructorReturn3.default)(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 && _util.support.animation) { this.enter(); } } }; Overlay.prototype.componentDidMount = function componentDidMount() { this.componentDidUpdate({ visible: false }); this.addDocumentEvents(); _manager2.default.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 (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 && _util.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 && _util.support.animation) { if (open || close) { this.addAnimationEvents(); } } else { var wrapperNode = this.getWrapperNode(); if (open) { setTimeout(function () { _this2.props.onOpen(); _this2.props.afterOpen(); _util.dom.addClass(wrapperNode, 'opened'); _manager2.default.addOverlay(_this2); }); } else if (close) { this.props.onClose(); this.props.afterClose(); _util.dom.removeClass(wrapperNode, 'opened'); _manager2.default.removeOverlay(this); } this.setFocusNode(); } }; Overlay.prototype.componentWillUnmount = function componentWillUnmount() { this._isDestroyed = true; this._isMounted = false; _manager2.default.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 = (0, _util.guid)(); _this3._animation = _util.events.on(node, _util.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) }); } 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: 'entering' }, 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(); _util.dom.addClass(wrapperNode, 'opened'); this.props.onOpen(); }; Overlay.prototype.onLeaving = function onLeaving() { var wrapperNode = this.getWrapperNode(); _util.dom.removeClass(wrapperNode, 'opened'); this.props.onClose(); }; Overlay.prototype.onEntered = function onEntered() { _manager2.default.addOverlay(this); this.setFocusNode(); this.props.afterOpen(); }; Overlay.prototype.onLeaved = function onLeaved() { _manager2.default.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 = _util.dom.getStyle(body, 'paddingRight') + _util.dom.scrollbar().width + 'px'; } _util.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; } _util.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() { return (0, _reactDom.findDOMNode)(this.contentRef); }; Overlay.prototype.getWrapperNode = function getWrapperNode() { return this.gatewayRef ? this.gatewayRef.getChildNode() : null; }; Overlay.prototype.addDocumentEvents = function addDocumentEvents() { if (this.props.canCloseByEsc) { this._keydownEvents = _util.events.on(document, 'keydown', this.handleDocumentKeyDown); } if (this.props.canCloseByOutSideClick) { this._clickEvents = _util.events.on(document, 'click', this.handleDocumentClick); this._touchEvents = _util.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 === _util.KEYCODE.ESC && _manager2.default.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 = (0, _findNode2.default)(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, 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; 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; var child = _react.Children.only(children); var childClazz = (0, _classnames3.default)((_classnames = {}, _classnames[prefix + 'overlay-inner'] = true, _classnames[animation.in] = status === 'entering', _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 = _react2.default.cloneElement(child, { className: childClazz, style: (0, _extends3.default)({}, child.props.style, style), ref: makeChain(this.saveContentRef, child.ref), 'aria-hidden': !stateVisible && cache && this._isMounted }); if (align) { var shouldUpdatePosition = status === 'leaving' ? false : propShouldUpdatePosition; children = _react2.default.createElement(_position2.default, { children: children, target: target, align: align, offset: offset, needAdjust: needAdjust, beforePosition: beforePosition, onPosition: makeChain(this.handlePosition, onPosition), shouldUpdatePosition: shouldUpdatePosition, rtl: rtl }); } var wrapperClazz = (0, _classnames3.default)([prefix + 'overlay-wrapper', wrapperClassName]); var newWrapperStyle = (0, _extends3.default)({}, { display: stateVisible ? '' : 'none' }, wrapperStyle); children = _react2.default.createElement( 'div', { className: wrapperClazz, style: newWrapperStyle, dir: rtl ? 'rtl' : undefined }, hasMask ? _react2.default.createElement('div', { className: prefix + 'overlay-backdrop', onClick: this.handleMaskClick, onMouseEnter: onMaskMouseEnter, onMouseLeave: onMaskMouseLeave, dir: rtl ? 'rtl' : undefined }) : null, children ); } return _react2.default.createElement(_gateway2.default, (0, _extends3.default)({ container: container, target: target, children: children }, { ref: this.saveGatewayRef })); }; return Overlay; }(_react.Component), _class.propTypes = { prefix: _propTypes2.default.string, pure: _propTypes2.default.bool, rtl: _propTypes2.default.bool, className: _propTypes2.default.string, style: _propTypes2.default.object, /** * 弹层内容 */ children: _propTypes2.default.any, /** * 是否显示弹层 */ visible: _propTypes2.default.bool, /** * 弹层请求关闭时触发事件的回调函数 * @param {String} type 弹层关闭的来源 * @param {Object} e DOM 事件 */ onRequestClose: _propTypes2.default.func, /** * 弹层定位的参照元素 */ target: _propTypes2.default.any, /** * 弹层相对于参照元素的定位, 详见开发指南的[定位部分](#定位) */ align: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.bool]), /** * 弹层相对于参照元素定位的微调 */ offset: _propTypes2.default.array, /** * 渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点 */ container: _propTypes2.default.any, /** * 是否显示遮罩 */ hasMask: _propTypes2.default.bool, /** * 是否支持 esc 按键关闭弹层 */ canCloseByEsc: _propTypes2.default.bool, /** * 点击弹层外的区域是否关闭弹层,不显示遮罩时生效 */ canCloseByOutSideClick: _propTypes2.default.bool, /** * 点击遮罩区域是否关闭弹层,显示遮罩时生效 */ canCloseByMask: _propTypes2.default.bool, /** * 弹层打开前触发事件的回调函数 */ beforeOpen: _propTypes2.default.func, /** * 弹层打开时触发事件的回调函数 */ onOpen: _propTypes2.default.func, /** * 弹层打开后触发事件的回调函数, 如果有动画,则在动画结束后触发 */ afterOpen: _propTypes2.default.func, /** * 弹层关闭前触发事件的回调函数 */ beforeClose: _propTypes2.default.func, /** * 弹层关闭时触发事件的回调函数 */ onClose: _propTypes2.default.func, /** * 弹层关闭后触发事件的回调函数, 如果有动画,则在动画结束后触发 */ afterClose: _propTypes2.default.func, /** * 弹层定位完成前触发的事件 */ beforePosition: _propTypes2.default.func, /** * 弹层定位完成时触发的事件 * @param {Object} config 定位的参数 * @param {Array} config.align 对齐方式,如 ['cc', 'cc'](如果开启 needAdjust,可能和预先设置的 align 不同) * @param {Number} config.top 距离视口顶部距离 * @param {Number} config.left 距离视口左侧距离 * @param {Object} node 定位参照的容器节点 */ onPosition: _propTypes2.default.func, /** * 是否在每次弹层重新渲染后强制更新定位信息,一般用于弹层内容区域大小发生变化时,仍需保持原来的定位方式 */ shouldUpdatePosition: _propTypes2.default.bool, /** * 弹层打开时是否让其中的元素自动获取焦点 */ autoFocus: _propTypes2.default.bool, /** * 当弹层由于页面滚动等情况不在可视区域时,是否自动调整定位以出现在可视区域 */ needAdjust: _propTypes2.default.bool, /** * 是否禁用页面滚动 */ disableScroll: _propTypes2.default.bool, /** * 隐藏时是否保留子节点 */ cache: _propTypes2.default.bool, /** * 安全节点,当点击 document 的时候,如果包含该节点则不会关闭弹层,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点,或者以上值组成的数组 */ safeNode: _propTypes2.default.any, /** * 弹层的根节点的样式类 */ wrapperClassName: _propTypes2.default.string, /** * 弹层的根节点的内联样式 */ wrapperStyle: _propTypes2.default.object, /** * 配置动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画。 请参考 Animate 组件的文档获取可用的动画名 * @default { in: 'expandInDown', out: 'expandOutUp' } */ animation: _propTypes2.default.oneOfType([_propTypes2.default.object, _propTypes2.default.bool]), onMaskMouseEnter: _propTypes2.default.func, onMaskMouseLeave: _propTypes2.default.func }, _class.defaultProps = { prefix: 'next-', pure: false, visible: false, onRequestClose: noop, target: _position2.default.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 }, _temp); Overlay.displayName = 'Overlay'; exports.default = Overlay; module.exports = exports['default'];