目前公司使用的是Ant Design 3.0, DatePicker mode="year" 时不支持 disabledDate 属性。
找到一篇模拟YearPicker的文章,但是不完全满足我的需求,在那篇文章的基础上进行了改造。
代码如下:
YearPicker.js
/**
* 使用方法
* 引入:
* import YearPicker from "@/common/widget/YearPicker";//路径按照自己的来
<YearPicker
value={value}
disabled={false} // 是否禁用时间控件
disabledDate={timeLimit} // 禁用日期,参考disableDate 计算方式
callback={this.onChange} // DatePicker onChange 事件
onBlur={this.onBlur} // 用于弹窗Input onBlur 事件
/>
*/
import React, { Component } from 'react';
import moment from 'moment';
import { Icon } from 'antd';
import Portal from './Portal';
import './YearPicker.less';
class YearPicker extends Component {
static getDerivedStateFromProps(nextProps) {
if ('value' in nextProps) {
return {
selectedyear: nextProps.value && nextProps.value != 'undefined'
? (nextProps.value._isAMomentObject ? nextProps.value.format('YYYY') : nextProps.value)
: '',
};
}
return {
value: '',
};
}
state = {
isShow: false,
selectedyear: this.props.value || null,
listInputVal: '',
years: [],
}
componentWillMount() {
// document.removeEventListener('click', this.documentClick);
}
componentDidMount() {
// document.addEventListener('click', this.documentClick, false);
}
documentClick = (e) => {
const { isShow } = this.state;
const clsName = e.target.className;
if (
clsName && typeof clsName == 'string' && clsName.indexOf('calendarX') === -1
&& e.target.tagName !== 'BUTTON'
&& isShow
) {
this.hide();
}
}
// 初始化数据处理
initData = (defaultValue) => {
const decade = parseInt(defaultValue / 10, 10) * 10;
const start = decade - 1;
const end = decade + 10;
this.getYearsArr(start, end);
};
// 获取年份范围数组
getYearsArr = (start, end) => {
const arr = [];
for (let i = start; i <= end; i++) {
arr.push(Number(i));
}
this.setState({
years: arr,
});
};
// 获取日历Input所在位置
getPosOfInput = (ele) => {
const pos = ele.getBoundingClientRect();
const { top, left } = pos;
return { left, top: top || 0 };
}
// 显示日历年组件
show = (e) => {
const { left, top } = this.getPosOfInput(e.target);
const { selectedyear } = this.state;
this.initData(selectedyear || new Date().getFullYear());
this.setState({
isShow: true, left, top, listInputVal: selectedyear,
});
setTimeout(() => {
// 展示弹窗时focus到input
const inputFocus = document.getElementById('year-picker-id').getElementsByClassName('calendarX-modal-input');
if (inputFocus && inputFocus[0]) inputFocus[0].focus();
}, 50);
};
// 隐藏日期年组件
hide = () => {
this.setState({ isShow: false });
};
// 向前的年份
prev = () => {
const { years } = this.state;
if (years[0] <= 1970) {
return;
}
this.getNewYearRangestartAndEnd('prev');
};
// 向后的年份
next = () => {
this.getNewYearRangestartAndEnd('next');
};
// 获取新的年份
getNewYearRangestartAndEnd = (type) => {
const { years } = this.state;
const start = Number(years[0]);
const end = Number(years[years.length - 1]);
let newstart;
let newend;
if (type == 'prev') {
newstart = parseInt(start - 10, 10);
newend = parseInt(end - 10, 10);
}
if (type == 'next') {
newstart = parseInt(start + 10, 10);
newend = parseInt(end + 10, 10);
}
this.getYearsArr(newstart, newend);
};
// 选中某一年
selects = (e) => {
const val = Number(e.target.value);
this.hide();
if (this.props.callback) {
this.props.callback(String(val));
}
};
getContainer = (domId = 'c-modal') => {
const _this = this;
const domContainer = document.createElement('div');
domContainer.id = domId;
domContainer.style.position = 'absolute';
domContainer.style.top = '0';
domContainer.style.left = '0';
domContainer.style.width = '100%';
domContainer.style.height = '100%';
document.getElementsByTagName('body')[0].appendChild(domContainer);
domContainer.onclick = (e) => {
if (e.target == e.currentTarget) {
_this.hide();
}
};
return domContainer;
}
listInputChange = (e) => {
if (e && e.target) {
const val = e.target.value;
this.setState({ listInputVal: val });
if (val && /^([0-9]{4})$/.test(val)) {
this.inputBlur(e);
this.initData(val);
}
}
}
EnterKey = (e) => {
if (e.keyCode == 13) {
this.hide();
this.inputBlur(e);
}
}
inputBlur = (e) => {
if (this.props.onBlur) this.props.onBlur(e);
}
render() {
const {
isShow, years, selectedyear, top, left, listInputVal,
} = this.state;
const { disabledDate, disabled } = this.props;
return (
<div className="calendarX-wrap">
<div className="calendarX-input">
<input
className="calendarX-value"
placeholder=""
onFocus={this.show}
value={selectedyear}
readOnly
disabled={disabled}
/>
<Icon type="calendar" className="calendarX-icon" />
{selectedyear && (
<Icon
type="close-circle"
theme="filled"
className="close-circle-icon"
onClick={() => {
if (this.props.callback) {
this.props.callback(null);
}
}}
/>
)}
</div>
{isShow ? (
<Portal getContainer={() => this.getContainer('year-picker-id')}>
<div style={{ position: 'absolute', left, top }}>
<List
data={years}
value={selectedyear}
prev={this.prev}
next={this.next}
cback={this.selects}
disabledDate={disabledDate}
inputChange={this.listInputChange}
listInputVal={listInputVal}
EnterKey={this.EnterKey}
inputBlur={this.inputBlur}
/>
</div>
</Portal>
) : (
''
)}
</div>
);
}
}
const List = (props) => {
const {
data, value, prev, next, cback, disabledDate, inputChange,
listInputVal, EnterKey, inputBlur,
} = props;
const start = data && data[1];
const end = data && data[data.length - 2];
return (
<>
<div className="calendarX-container">
<div className="calendarX-input-wrap">
<div className="calendarX-date-input-wrap">
<input
className="calendarX-modal-input"
placeholder=""
value={listInputVal}
onChange={inputChange}
onKeyDown={EnterKey}
onBlur={inputBlur}
/>
</div>
</div>
<div className="calendarX-head-year">
<Icon
type="double-left"
className="calendarX-btn prev-btn"
title=""
onClick={prev}
/>
<span className="calendarX-year-range">{`${start}-${end}`}</span>
<Icon
type="double-right"
className="calendarX-btn next-btn"
title=""
onClick={next}
/>
</div>
<div className="calendarX-body-year">
<ul className="calendarX-year-ul">
{data.map((item, index) => {
const isDisabled = disabledDate && disabledDate(moment(String(item)));
const isFirst = index == 0;
const isLast = index == data.length - 1;
return (
<li
key={index}
title={item}
className={
`${item == value
? 'calendarX-year-li calendarX-year-selected'
: 'calendarX-year-li'}${isFirst ? ' calendarX-year-last-decade-li'
: (isLast ? ' calendarX-year-next-decade-li' : '')}${
isDisabled ? ' calendarX-year-li-disabled' : ''
}`
}
>
<button
type="button"
onClick={(e) => {
if (isDisabled) { return; }
if (isFirst) { prev(); return; }
if (isLast) { next(); return; }
cback(e);
}}
value={item}
>
{item}
</button>
</li>
);
},
)}
</ul>
</div>
</div>
</>
);
};
export default YearPicker;
YearPicker.less
@focuscolor: #108ee9;
@bordercolor: #d9d9d9;/*这部分根据你自己的容器样式,我这个地方是因为公用组件的原因需要设置*/
#wrapper .toolbar {
overflow: inherit !important;
}
#wrapper .toolbar > div:after {
content: "";
display: block;
visibility: hidden;
width: 0;
clear: both;
}
/*---以下为必备样式----*/
:global {
.calendarX-wrap {
position: relative;
.calendarX-input {
width: 100%;
position: relative;
cursor: pointer;
.calendarX-icon {
position: absolute;
right: 10px;
top: 50%;
margin-top: -7px;
color: rgba(0, 0, 0, 0.25);
}
&:hover {
.close-circle-icon {
display: inline-block;
transition: all 0.3s;
}
}
.close-circle-icon {
display: none;
position: absolute;
right: 10px;
top: 50%;
margin-top: -7px;
color: rgba(0, 0, 0, 0.25);
transition: all 0.3s;
background-color: #fff;
}
input {
width: 100%;
height: 32px;
border: 1px solid @bordercolor;
border-radius: 4px;
font-size: 14px;
outline: none;
display: block;
padding: 4px 11px;
transition: all 0.3s;
&:hover:not(:disabled),
&:active:not(:disabled) {
border-color: #40a9ff;
}
&:disabled {
color: rgba(0, 0, 0, 0.25);
background-color: #f5f5f5;
cursor: not-allowed;
opacity: 1;
}
}
}
}
.calendarX-container {
position: relative;
width: 280px;
font-size: 14px;
line-height: 1.5;
text-align: left;
list-style: none;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #fff;
border-radius: 4px;
outline: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 999;
}
.calendarX-head-year {
height: 40px;
line-height: 40px;
text-align: center;
width: 100%;
position: relative;
border-bottom: 1px solid #e8e8e8;
.calendarX-year-range {
padding: 0 2px;
display: inline-block;
color: rgba(0, 0, 0, 0.85);
line-height: 34px;
}
.calendarX-btn {
position: absolute;
top: 0;
color: #aaa;
padding: 0 5px;
font-size: 12px;
display: inline-block;
line-height: 34px;
cursor: pointer;
&:hover {
color: @focuscolor;
}
}
.prev-btn {
left: 7px;
}
.next-btn {
right: 7px;
}
}
.calendarX-body-year {
width: 100%;
height: 218px;
.calendarX-year-ul {
list-style: none;
.calendarX-year-li {
float: left;
text-align: center;
width: 92px;
> button {
cursor: pointer;
outline: none;
border: 0;
display: inline-block;
margin: 0 auto;
color: rgba(0, 0, 0, 0.65);
background: transparent;
text-align: center;
height: 24px;
line-height: 24px;
padding: 0 8px;
border-radius: 4px;
transition: background 0.3s ease;
margin: 14px 0;
&:hover {
color: @focuscolor;
}
}
&::before {
}
&.calendarX-year-li-disabled {
position: relative;
cursor: not-allowed;
&::before {
background: rgba(0, 0, 0, 0.04);
position: absolute;
top: 50%;
right: 0;
left: 0;
z-index: 1;
height: 24px;
transform: translateY(-50%);
transition: all 0.3s;
content: '';
}
> button {
color: rgba(0, 0, 0, 0.25);
}
}
}
.calendarX-year-selected {
> button {
background: #108ee9;
color: #fff !important;
&:hover {
color: #fff;
}
}
}
.calendarX-year-last-decade-li, .calendarX-year-next-decade-li {
> button {
color: rgba(0, 0, 0, 0.25);
}
}
}
}
.calendarX-input-wrap {
height: 34px;
padding: 6px 10px;
border-bottom: 1px solid #e8e8e8;
.calendarX-input {
width: 100%;
height: 22px;
color: rgba(0, 0, 0, 0.65);
background: #fff;
border: 0;
outline: 0;
cursor: auto;
}
}
.calendarX-modal-input {
width: 100%;
height: 22px;
color: rgba(0, 0, 0, 0.65);
background: #fff;
border: 0;
outline: 0;
cursor: auto;
}
}
Portal.js
import React from 'react';
import ReactDOM from 'react-dom';
/**
* @function getContainer 渲染组件的父组件
* @param children 需要渲染的组件
* @export
* @class Portal
* @extends {React.Component}
*/
export default class Portal extends React.Component {
componentDidMount() {
this.createContainer();
}
componentDidUpdate() {
// React版本较低,不使用ReactDOM.createPortal
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
this.props.children,
this._container,
);
}
componentWillUnmount() {
this.removeContainer();
}
createContainer() {
this._container = this.props.getContainer();
this.forceUpdate();
}
removeContainer() {
if (this._container) {
this._container.parentNode.removeChild(this._container);
}
}
render() {
return null;
}
}
disableDate 计算方式(也可用于禁用日期)
disabledDateBeforeToday = (current, format) => { // 禁止今年以前的年份(不包含今年)
return current && current < moment(moment().startOf('day').format(format));
}
disabledDateAfterToday = (current) => { // 禁止今年之后的年份(不包含今年)
return current && current >= moment().endOf('day');
}
问题1:本来关闭弹窗用的是document绑定事件,但是当在一个页面里存在多个YearPicker,打开其中一个选择弹窗,再点击其他YearPicker,会同时打开多个弹窗,所以使用 ReactDOM.createPortal 将整个选择的组件与input框隔离成独立的部分,采用透明全屏遮罩层的方式,检测input在窗口中的位置来设置展示组件的位置,这样就可以点击任意位置关闭组件,且只出现一个弹窗。
问题2:由于React版本问题,ReactDOM.createPortal 不支持。使用ReactDOM.unstable_renderSubtreeIntoContainer 将 YearPicker 加在 body 下。
借鉴文章:
时间选择控件YearPicker(基于React,antd)
React如何将组件渲染到指定节点—ReactDOM.createPortal