UNPKG

zent

Version:

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

331 lines (264 loc) 7.58 kB
import 'react'; import capitalize from 'lodash/capitalize'; import throttle from 'lodash/throttle'; import uniq from 'lodash/uniq'; import isBrowser from 'utils/isBrowser'; import PropTypes from 'prop-types'; import Trigger, { PopoverTriggerPropTypes } from './Trigger'; const MOUSE_EVENT_WHITE_LIST = [ 'down', 'up', 'move', 'over', 'out', 'enter', 'leave' ]; function isMouseEventSuffix(suffix) { return MOUSE_EVENT_WHITE_LIST.indexOf(suffix) !== -1; } // Hover识别的状态 const HoverState = { Init: 1, // Leave识别开始必须先由内出去 Started: 2, // 延迟等待中 Pending: 3, Finish: 255 }; /** * 创建一个新state,每个state是一次性的,识别完成后需要创建一个新的state * * @param {string} name state的名称 * @param {function} onFinish 识别成功时的回调函数 */ const makeState = (name, onFinish, initState = HoverState.Init) => { let state = initState; return { transit(nextState) { // console.log(`${name}: ${state} -> ${nextState}`); // eslint-disable-line state = nextState; if (state === HoverState.Finish) { onFinish(); } }, is(st) { return st === state; }, name }; }; function forEachHook(hooks, action) { if (!hooks) { return; } if (!isBrowser) return; const hookNames = Object.keys(hooks); hookNames.forEach(hookName => { const eventName = isMouseEventSuffix(hookName) ? `mouse${hookName}` : hookName; if (action === 'install') { window.addEventListener(eventName, hooks[hookName], true); } else if (action === 'uninstall') { window.removeEventListener(eventName, hooks[hookName], true); } }); } function makeRecognizer(state, options) { const recognizer = { ...options, destroy() { if (!state.is(HoverState.Finish)) { forEachHook(recognizer.global, 'uninstall'); // console.log(`destroy ${state.name}`); // eslint-disable-line } } }; forEachHook(recognizer.global, 'install'); return recognizer; } /** * 进入和离开的识别是独立的recognizer,每个recognizer可以绑定任意`onmouse***`事件。 * 组件内部只需要提供识别完成后的回调函数,不需要知道recognizer的细节。 * * local下的事件是直接绑定在trigger上的 * global下的事件是绑定在window上的capture事件 */ /** * 进入状态的识别 */ function makeHoverEnterRecognizer({ enterDelay, onEnter }) { const state = makeState('enter', onEnter); let timerId; const recognizer = makeRecognizer(state, { local: { enter() { state.transit(HoverState.Pending); timerId = setTimeout(() => { state.transit(HoverState.Finish); forEachHook(recognizer.global, 'uninstall'); }, enterDelay); }, leave() { if (timerId) { clearTimeout(timerId); timerId = undefined; state.transit(HoverState.Init); } } } }); return recognizer; } /** * 离开状态的识别 */ function makeHoverLeaveRecognizer({ leaveDelay, onLeave, isOutSide, quirk }) { const state = makeState('leave', onLeave); let recognizer; let timerId; const gotoFinishState = () => { state.transit(HoverState.Finish); forEachHook(recognizer.global, 'uninstall'); }; recognizer = makeRecognizer(state, { global: { move: throttle(evt => { const { target } = evt; if (isOutSide(target)) { if (!quirk && !state.is(HoverState.Started)) { return; } state.transit(HoverState.Pending); timerId = setTimeout(gotoFinishState, leaveDelay); } else { if (state.is(HoverState.Init)) { state.transit(HoverState.Started); return; } if (!state.is(HoverState.Pending)) { return; } if (timerId) { clearTimeout(timerId); timerId = undefined; state.transit(HoverState.Started); } } }, 16), // 页面失去焦点的时候强制关闭,否则会出现必须先移动进来再出去才能关闭的问题 blur: evt => { // 确保事件来自 window // React 的事件系统会 bubble blur事件,但是原生的是不会 bubble 的。 // https://github.com/facebook/react/issues/6410#issuecomment-292895495 const target = evt.target || evt.srcElement; if (target !== window) { return; } if (timerId) { clearTimeout(timerId); timerId = undefined; } gotoFinishState(); } } }); return recognizer; } function callHook(recognizer, namespace, hookName, ...args) { const ns = recognizer && recognizer[namespace]; if (ns && ns[hookName]) ns[hookName](...args); } function destroyRecognizer(recognizer) { if (recognizer) { recognizer.destroy(); } } export default class PopoverHoverTrigger extends Trigger { static propTypes = { ...PopoverTriggerPropTypes, showDelay: PropTypes.number, hideDelay: PropTypes.number, isOutside: PropTypes.func, quirk: PropTypes.bool }; static defaultProps = { showDelay: 150, hideDelay: 150, quirk: false }; open = () => { this.props.open(); }; close = () => { this.props.close(); }; state = { enterRecognizer: null, leaveRecognizer: null }; makeEnterRecognizer() { const { showDelay, quirk } = this.props; return makeHoverEnterRecognizer({ enterDelay: showDelay, onEnter: this.open, quirk }); } makeLeaveRecognizer() { const { quirk, hideDelay, isOutsideStacked } = this.props; return makeHoverLeaveRecognizer({ leaveDelay: hideDelay, onLeave: this.close, isOutSide: isOutsideStacked, quirk }); } getTriggerProps(child) { const { enterRecognizer, leaveRecognizer } = this.state; const enterHooks = (enterRecognizer && enterRecognizer.local) || {}; const leaveHooks = (leaveRecognizer && leaveRecognizer.local) || {}; const eventNames = uniq( [].concat(Object.keys(enterHooks), Object.keys(leaveHooks)) ).map(name => `onMouse${capitalize(name)}`); const eventNameToHookName = eventName => eventName.slice('onMouse'.length).toLowerCase(); return eventNames.reduce((events, evtName) => { const hookName = eventNameToHookName(evtName); events[evtName] = evt => { callHook(enterRecognizer, 'local', hookName); callHook(leaveRecognizer, 'local', hookName); this.triggerEvent(child, evtName, evt); }; return events; }, {}); } cleanup() { // ensure global events are removed destroyRecognizer(this.state.enterRecognizer); destroyRecognizer(this.state.leaveRecognizer); } initRecognizers(props) { props = props || this.props; const { contentVisible } = props; this.cleanup(); this.setState({ enterRecognizer: contentVisible ? null : this.makeEnterRecognizer(), leaveRecognizer: contentVisible ? this.makeLeaveRecognizer() : null }); } componentWillUnmount() { this.cleanup(); } componentDidMount() { this.initRecognizers(); } componentWillReceiveProps(nextProps) { const { contentVisible } = nextProps; // visibility changed, create new recognizers if (contentVisible !== this.props.contentVisible) { this.initRecognizers(nextProps); } } }