UNPKG

zent

Version:

一套前端设计语言和基于React的实现

276 lines (226 loc) 7.25 kB
/** * 设计: * * Popover组件只是一个壳子,负责组装Trigger和Content。 * * 弹层实际的打开/关闭都是Content完成的,而什么情况打开弹层是Trigger控制的。 */ import React, { Component, PropTypes, Children } from 'react'; import ReactDOM from 'react-dom'; import cx from 'zent-utils/classnames'; import noop from 'zent-utils/lodash/noop'; import uniqueId from 'zent-utils/lodash/uniqueId'; import isFunction from 'zent-utils/lodash/isFunction'; import isBoolean from 'zent-utils/lodash/isBoolean'; import isPromise from 'zent-utils/isPromise'; import PopoverContent from './Content'; import PopoverTrigger from './trigger/Trigger'; const SKIPPED = () => {}; function instanceOf(MaybeDerive, Base) { return MaybeDerive === Base || MaybeDerive.prototype instanceof Base; } function handleBeforeHook(beforeFn, arity, continuation) { // 有参数,传入continuation,由外部去控制何时调用 if (arity >= 1) { return beforeFn(continuation); } // 无参数,如果返回Promise那么resolve后调用continuation;如果返回不是Promise,直接调用Promise const mayBePromise = beforeFn(); if (!isPromise(mayBePromise) && mayBePromise !== SKIPPED) { return continuation(); } mayBePromise.then(continuation); } export const PopoverContextType = { popover: PropTypes.shape({ close: PropTypes.func.isRequired, open: PropTypes.func.isRequired, getContentNode: PropTypes.func.isRequired, getTriggerNode: PropTypes.func.isRequired }) }; export default class Popover extends Component { static propTypes = { prefix: PropTypes.string, className: PropTypes.string, // custom classname for trigger wrapper wrapperClassName: PropTypes.string, // container的display属性 display: PropTypes.string, // position strategy position: PropTypes.func.isRequired, // 定位时的偏移量 cushion: PropTypes.number, // 只有用户触发的打开/关闭才会触发这两个毁掉 onBeforeClose: PropTypes.func, onBeforeShow: PropTypes.func, // 不管打开/关闭时如何触发的都会被调用 onClose: PropTypes.func, onShow: PropTypes.func, // defaults to body containerSelector: PropTypes.string, children: PropTypes.node.isRequired, // 两个必须一起出现 visible: PropTypes.bool, onVisibleChange: PropTypes.func }; static defaultProps = { prefix: 'zent', className: '', wrapperClassName: '', display: 'block', onBeforeClose: noop, onBeforeShow: noop, onClose: noop, onShow: noop, cushion: 0, containerSelector: 'body' }; static childContextTypes = PopoverContextType; getChildContext() { return { popover: { close: this.close, open: this.open, getContentNode: this.getPopoverNode, getTriggerNode: this.getTriggerNode } }; } constructor(props) { super(props); // id用来唯一标识popover实例 this.id = uniqueId(`${props.prefix}-popover-internal-id-`); if (!this.isVisibilityControlled(props)) { this.state = { visible: false }; } } isVisibilityControlled(props) { const { visible, onVisibleChange } = props || this.props; const hasOnChange = isFunction(onVisibleChange); const hasVisible = isBoolean(visible); if (hasVisible && !hasOnChange || hasOnChange && !hasVisible) { throw new Error('visible and onVisibleChange must be used together'); } return hasVisible && hasOnChange; } getVisible = (props, state) => { if (this.isVisibilityControlled(props)) { props = props || this.props; return props.visible; } state = state || this.state; return state.visible; }; setVisible = (visible, props, state) => { props = props || this.props; state = state || this.state; const beforeHook = visible ? props.onBeforeShow : props.onBeforeClose; const onBefore = (...args) => { // 确保pending的时候不会触发多次beforeHook if (this.pendingOnBeforeHook) { return SKIPPED; } this.pendingOnBeforeHook = true; return beforeHook(...args); }; if (this.isVisibilityControlled(props)) { if (this.pendingOnBeforeHook || props.visible === visible) { return; } handleBeforeHook(onBefore, beforeHook.length, () => { props.onVisibleChange(visible); this.pendingOnBeforeHook = false; }); } else { if (this.pendingOnBeforeHook || state.visible === visible) { return; } handleBeforeHook(onBefore, beforeHook.length, () => { this.setState({ visible }); this.pendingOnBeforeHook = false; }); } } getPopoverNode = () => { return document.querySelector(`.${this.id}`); } onTriggerRefChange = (triggerInstance) => { this.triggerNode = ReactDOM.findDOMNode(triggerInstance); }; getTriggerNode = () => { return this.triggerNode; }; open = () => { this.setVisible(true); }; close = () => { this.setVisible(false); }; validateChildren() { const { children } = this.props; const childArray = Children.toArray(children); if (childArray.length !== 2) { throw new Error('There must be one and only one trigger and content in Popover'); } const { trigger, content } = childArray.reduce((state, c) => { const type = c.type; if (instanceOf(type, PopoverTrigger)) { state.trigger = c; } else if (instanceOf(type, PopoverContent)) { state.content = c; } return state; }, { trigger: null, content: null }); if (!trigger) { throw new Error('Missing trigger in Popover'); } if (!content) { throw new Error('Missing content in Popover'); } return { trigger, content }; } componentDidMount() { if (this.isVisibilityControlled() && this.props.visible) { this.props.onShow(); } } componentDidUpdate(prevProps, prevState) { const visible = this.getVisible(); if (visible !== this.getVisible(prevProps, prevState)) { const afterHook = visible ? this.props.onShow : this.props.onClose; afterHook(); } } render() { const { trigger, content } = this.validateChildren(); const { display, prefix, className, wrapperClassName, containerSelector, position, cushion } = this.props; const visible = this.getVisible(); return ( <div style={{ display }} className={cx(`${prefix}-popover-wrapper`, wrapperClassName)}> {React.cloneElement(trigger, { prefix, contentVisible: visible, onTriggerRefChange: this.onTriggerRefChange, getTriggerNode: this.getTriggerNode, getContentNode: this.getPopoverNode, open: this.open, close: this.close })} {React.cloneElement(content, { prefix, className, id: this.id, getContentNode: this.getPopoverNode, getAnchor: this.getTriggerNode, visible, cushion, containerSelector, placement: position })} </div> ); } }