zarm-web
Version:
基于 React 的桌面端UI库
476 lines (405 loc) • 14.8 kB
JavaScript
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;