UNPKG

zent

Version:

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

385 lines (321 loc) 9.79 kB
/** * 设计: * * Popover组件只是一个壳子,负责组装Trigger和Content。 * * 弹层实际的打开/关闭都是Content完成的,而什么情况打开弹层是Trigger控制的。 * * Popover 组件是一个递归的组件,支持嵌套。 * * * context context * ------> ------> * Popover Popover child Popover grand-child ...... * <------ <------ * isOutsideStacked isOutsideStacked * */ import React, { Component, PureComponent, Children } from 'react'; import ReactDOM from 'react-dom'; import cx from 'classnames'; import noop from 'lodash/noop'; import uniqueId from 'lodash/uniqueId'; import isFunction from 'lodash/isFunction'; import isBoolean from 'lodash/isBoolean'; import isPromise from 'utils/isPromise'; import PropTypes from 'prop-types'; import kindOf from 'utils/kindOf'; import getWidth from 'utils/getWidth'; import PopoverContent from './Content'; import PopoverTrigger from './trigger/Trigger'; const SKIPPED = () => {}; function handleBeforeHook(beforeFn, arity, continuation, escape) { // 有参数,传入continuation,由外部去控制何时调用 // escapse 用来终止 onChange 操作 if (arity === 1) { return beforeFn(continuation); } else if (arity >= 2) { return beforeFn(continuation, escape); } // 无参数,如果返回Promise那么resolve后调用continuation, reject 的话调用 escape; // 如果返回不是Promise,直接调用Promise const mayBePromise = beforeFn(); if (!isPromise(mayBePromise) && mayBePromise !== SKIPPED) { return continuation(); } mayBePromise.then(continuation, escape); } export const PopoverContextType = { _zentPopover: PropTypes.shape({ close: PropTypes.func.isRequired, open: PropTypes.func.isRequired, getContentNode: PropTypes.func.isRequired, getTriggerNode: PropTypes.func.isRequired, // 用于维护 Popover 栈,处理嵌套的问题 registerDescendant: PropTypes.func, unregisterDescendant: PropTypes.func }) }; export default class Popover extends (PureComponent || 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 contextTypes = PopoverContextType; static childContextTypes = PopoverContextType; getChildContext() { return { _zentPopover: { close: this.close, open: this.open, getContentNode: this.getPopoverNode, getTriggerNode: this.getTriggerNode, registerDescendant: this.registerDescendant, unregisterDescendant: this.unregisterDescendant } }; } registerDescendant = popover => { this.descendants.push(popover); }; unregisterDescendant = popover => { const idx = this.descendants.indexOf(popover); this.descendants.splice(idx, 1); }; constructor(props) { super(props); // id用来唯一标识popover实例 this.id = uniqueId(`${props.prefix}-popover-internal-id-`); // 记录 Popover 子孙 this.descendants = []; if (!this.isVisibilityControlled(props)) { this.state = { visible: false }; } this.isUnmounted = 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); }; const escapse = () => { this.pendingOnBeforeHook = false; }; if (this.isVisibilityControlled(props)) { if (this.pendingOnBeforeHook || props.visible === visible) { return; } handleBeforeHook( onBefore, beforeHook.length, () => { props.onVisibleChange(visible); this.pendingOnBeforeHook = false; }, escapse ); } else { if (this.pendingOnBeforeHook || state.visible === visible) { return; } handleBeforeHook( onBefore, beforeHook.length, () => { this.safeSetState({ visible }); this.pendingOnBeforeHook = false; }, escapse ); } }; 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); }; injectIsOutsideSelf = impl => { this.isOutsideSelf = impl; }; // Popover up in the tree will call this method to see if the node lies outside isOutsideStacked = node => { if (this.isOutsideSelf) { // 在自身内部,肯定不在外面 if (!this.isOutsideSelf(node)) { return false; } } // 问下面的 Popover 是否在外面 if (this.descendants.some(popover => !popover.isOutsideStacked(node))) { return false; } return true; }; 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 (kindOf(type, PopoverTrigger)) { state.trigger = c; } else if (kindOf(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 }; } safeSetState(updater, callback) { if (!this.isUnmounted) { return this.setState(updater, callback); } } componentDidMount() { const { _zentPopover: popover } = this.context || {}; if (popover && popover.registerDescendant) { popover.registerDescendant(this); } 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(); } } componentWillUnmount() { const { _zentPopover: popover } = this.context || {}; if (popover && popover.unregisterDescendant) { popover.unregisterDescendant(this); } this.isUnmounted = true; } render() { const { trigger, content } = this.validateChildren(); const { display, prefix, className, wrapperClassName, containerSelector, position, cushion, width } = this.props; const visible = this.getVisible(); return ( <div style={{ display, ...getWidth(width) }} 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, isOutsideStacked: this.isOutsideStacked, injectIsOutsideSelf: this.injectIsOutsideSelf })} {React.cloneElement(content, { prefix, className, id: this.id, getContentNode: this.getPopoverNode, getAnchor: this.getTriggerNode, visible, cushion, containerSelector, placement: position })} </div> ); } }