UNPKG

zarm-web

Version:
476 lines (405 loc) 14.8 kB
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /* eslint-disable no-bitwise */ import React from 'react'; import { createPortal } from 'react-dom'; import classnames from 'classnames'; import events from '../utils/events'; import throttle from '../utils/throttle'; import domUtil from '../utils/dom'; function getOffsetElem(elem) { let parentElem = elem.parentNode; while (parentElem) { if (parentElem instanceof HTMLElement) { if (parentElem.style.position === 'fixed' || window.getComputedStyle(parentElem).position === 'fixed' || parentElem === document.body) { return parentElem; } parentElem = parentElem.parentNode; } else { break; } } return document.body; } // 获取元素坐标 function getElemPosition(elem, relativeElem = document.body) { let parentElem = elem.offsetParent; const position = { top: elem.offsetTop, left: elem.offsetLeft }; while (relativeElem.contains(parentElem)) { if (parentElem instanceof HTMLElement) { if (relativeElem === parentElem) { return position; } position.top += parentElem.offsetTop; position.left += parentElem.offsetLeft; parentElem = parentElem.offsetParent; } } return position; } // todo [首次不创建] const placementMap = { bottomLeft: 5, bottomCenter: 9, bottomRight: 17, topLeft: 6, topCenter: 10, topRight: 18 }; const defaultProps = { visible: false, isRadius: false, hideOnClick: true, prefixCls: 'ui-dropdown', placement: 'bottomLeft', trigger: 'click', disabled: false, zIndex: 2018 }; const mountedInstance = new Set(); export default class Dropdown extends React.Component { // 隐藏全部的Dropdown static hide() { mountedInstance.forEach(instance => { instance.props.onVisibleChange(false); }); } // 显示全部的Dropdown 除了disable static show() { mountedInstance.forEach(instance => { instance.props.onVisibleChange(true); }); } // 重新计算Dropdown定位 static reposition() { mountedInstance.forEach(instance => { instance.reposition(); }); } // 用于存储已生成的全部实例的Set // private static mountedInstance: Set<Dropdown> = new Set(); // 根据定位点计算定位信息 static calcPosition(placement = 'bottomLeft', width, height, dropWidth, dropHeight) { let top = 0; let left = 0; const placementCode = placementMap[placement]; if (placementCode & 1) { top = height; } else if (placementCode & 2) { top = -dropHeight; } if (placementCode & 8) { left = (width - dropWidth) / 2; } else if (placementCode & 16) { left = width - dropWidth; } return { top, left }; } static createDivBox() { const div = document.createElement('div'); div.style.setProperty('position', 'absolute'); div.style.setProperty('left', '0'); div.style.setProperty('top', '0'); div.style.setProperty('width', 'auto'); return div; } constructor(props) { super(props); this.state = { visible: this.props.visible, positionInfo: { left: 0, top: 0 }, animationState: null, // eslint-disable-next-line react/no-unused-state animationProps: null }; this.DropdownContentEvent = {}; this.triggerEvent = {}; this.onWindowResize = void 0; this.onParentScroll = void 0; this.div = Dropdown.createDivBox(); this.triggerBox = void 0; this.DropdownContent = void 0; this.popContainer = void 0; this.scrollParent = void 0; this.isHoverOnDropContent = false; this.hiddenTimer = void 0; this.triggerBoxOffsetHeight = void 0; this.setEventObject = triggerType => { if (triggerType === 'hover') { this.DropdownContentEvent.onMouseLeave = this.onDropdownContentMouseLeave; this.DropdownContentEvent.onMouseEnter = this.onDropdownContentMouseEnter; this.triggerEvent.onMouseEnter = this.onTrigger; this.triggerEvent.onMouseLeave = this.onTrigger; } else if (triggerType === 'click') { this.triggerEvent.onClick = this.onTrigger; } else if (triggerType === 'contextMenu') { this.triggerEvent.onContextMenu = this.onTrigger; } }; this.onTrigger = e => { // 禁用状态不做任何处理 if (this.props.disabled === true) { return; } const { type } = e; if (type === 'click') { this.props.onVisibleChange(!this.props.visible); } else if (type === 'contextmenu') { e.preventDefault(); this.props.onVisibleChange(!this.props.visible); } else if (type === 'mouseenter') { if (this.props.visible === false) { this.props.onVisibleChange(true); } else if (this.hiddenTimer) { clearTimeout(this.hiddenTimer); } } else if (type === 'mouseleave') { // 缓冲一点一时间给间隙 this.hiddenTimer = setTimeout(() => { // 若当前鼠标在弹出层上 则不消失 if (this.isHoverOnDropContent === false) { this.props.onVisibleChange(false); } }, 300); } }; this.onDropdownContentMouseEnter = () => { // 重新放置时候取消隐藏 if (this.hiddenTimer) { clearTimeout(this.hiddenTimer); } if (this.isHoverOnDropContent === false) { this.isHoverOnDropContent = true; } }; this.onDropdownContentMouseLeave = () => { this.isHoverOnDropContent = false; // 给消失一点缓冲时间 this.hiddenTimer = setTimeout(() => { this.props.onVisibleChange(false); }, 300); }; this.onDocumentClick = e => { const { hideOnClick, onVisibleChange } = this.props; if (this.props.disabled === true || this.state.visible === false) { return; } const target = e.target; // eslint-disable-next-line no-empty if (this.div.contains(target) || this.triggerBox.contains(target)) {} else { // eslint-disable-next-line no-lonely-if if (hideOnClick) { onVisibleChange(false); } } }; this.reposition = () => { if (!this.state.visible || this.props.disabled) { return; } const { left, top } = this.getDropdownPosition(this.props.placement); if (left === this.state.positionInfo.left && top === this.state.positionInfo.top) { return; } this.setState({ positionInfo: { left, top } }); }; this.onAniEnd = e => { if (e.type.toLowerCase().endsWith('animationend')) { this.setState({ visible: this.props.visible, animationState: null }); } }; this.setEventObject(props.trigger); this.onWindowResize = throttle(this.reposition, 300); this.onParentScroll = this.reposition; } componentDidMount() { const { getPopupContainer, visible, placement } = this.props; if (typeof getPopupContainer === 'function') { this.popContainer = getPopupContainer(); this.popContainer.style.position = 'relative'; } else { this.popContainer = getOffsetElem(this.triggerBox); } this.popContainer.appendChild(this.div); if (visible) { const { left, top } = this.getDropdownPosition(placement); this.setState({ positionInfo: { left, top } }); } this.scrollParent = domUtil.getScrollParent(this.triggerBox); events.on(document, 'click', this.onDocumentClick); events.on(window, 'resize', this.onWindowResize); events.on(this.scrollParent, 'scroll', this.onParentScroll); // 存储当前实例,方便静态方法统一处理 mountedInstance.add(this); this.triggerBoxOffsetHeight = this.triggerBox.offsetHeight; } componentWillReceiveProps(nextProps) { const { visible, trigger } = this.props; if (nextProps.visible === visible) { return; } if (nextProps.trigger !== trigger) { this.setEventObject(nextProps.trigger); } if (nextProps.visible) { this.enter(() => { if (nextProps.visible) { this.setState({ positionInfo: this.getDropdownPosition(nextProps.placement) }); } }); } else { this.leave(); } } componentDidUpdate() { const height = this.triggerBox.offsetHeight; if (height !== this.triggerBoxOffsetHeight) { this.reposition(); this.triggerBoxOffsetHeight = height; } } componentWillUnmount() { events.off(document, 'click', this.onDocumentClick); events.off(window, 'click', this.onWindowResize); events.off(this.scrollParent, 'scroll', this.onParentScroll); mountedInstance.delete(this); setTimeout(() => { this.popContainer.removeChild(this.div); }); } // 根据trigger方式不同绑定事件 // 获取元素的定位信息 getDropdownPosition(placement = 'bottomLeft') { const rectInfo = getElemPosition(this.triggerBox, this.popContainer); const { offsetWidth, offsetHeight } = this.DropdownContent; const computerStyle = window.getComputedStyle(this.DropdownContent); const marginTop = parseFloat(this.DropdownContent.style.marginTop || computerStyle.marginTop || '0'); const marginLeft = parseFloat(this.DropdownContent.style.marginLeft || computerStyle.marginLeft || '0'); const { top, left } = Dropdown.calcPosition(placement, this.triggerBox.offsetWidth, this.triggerBox.offsetHeight, offsetWidth, offsetHeight); const offset = placement.startsWith('bottom') ? 5 : -5; let scrollLeft = 0; let scrollTop = 0; if (this.scrollParent !== this.popContainer && this.popContainer.contains(this.scrollParent)) { scrollLeft = domUtil.getScrollLeftValue(this.scrollParent); scrollTop = domUtil.getScrollTopValue(this.scrollParent); } return { left: rectInfo.left + left - marginLeft - scrollLeft, top: rectInfo.top + top - marginTop + offset - scrollTop }; } enter(callback) { this.setState({ visible: true, animationState: 'enter' }, callback); } leave() { this.setState({ visible: true, animationState: 'leave' }); } render() { const _this$props = this.props, { disabled, children, overlay, className, trigger, prefixCls, style, isRadius, placement = 'bottomLeft', zIndex, notRenderInDisabledMode, visible, hideOnClick, onVisibleChange, getPopupContainer, triggerBoxStyle } = _this$props, others = _objectWithoutProperties(_this$props, ["disabled", "children", "overlay", "className", "trigger", "prefixCls", "style", "isRadius", "placement", "zIndex", "notRenderInDisabledMode", "visible", "hideOnClick", "onVisibleChange", "getPopupContainer", "triggerBoxStyle"]); const { positionInfo, animationState, visible: stateVisible } = this.state; // 根据placement判断向上动画还是向下动画 const animationProps = placementMap[placement] & 1 ? 'scaleDown' : 'scaleUp'; const cls = classnames({ [prefixCls]: true, radius: 'radius' in this.props || isRadius, [className]: !!className, [`${animationProps}-${animationState}`]: !!animationState }); const dropdownBoxStyle = _objectSpread({ minWidth: this.triggerBox && this.triggerBox.offsetWidth || 0 }, style, {}, positionInfo, { position: 'absolute', animationDuration: '300ms', // eslint-disable-next-line no-nested-ternary display: disabled ? 'none' : stateVisible ? 'block' : 'none', overflow: 'hidden', zIndex }); return React.createElement(React.Fragment, null, React.createElement("div", _extends({ className: `${prefixCls}-trigger-box`, style: triggerBoxStyle, ref: e => { this.triggerBox = e; } }, this.triggerEvent), children), createPortal(React.createElement("div", _extends({ onAnimationEnd: this.onAniEnd, className: cls, ref: e => { this.DropdownContent = e; }, style: dropdownBoxStyle }, others, this.DropdownContentEvent), notRenderInDisabledMode && disabled ? null : overlay), this.div)); } } Dropdown.defaultProps = defaultProps;