目前公司使用的是Ant Design 3.0, DatePicker mode="year" 時(shí)不支持 disabledDate 屬性碾盐。
找到一篇模擬YearPicker的文章泉粉,但是不完全滿足我的需求洪囤,在那篇文章的基礎(chǔ)上進(jìn)行了改造路克。
代碼如下:
YearPicker.js
/**
* 使用方法
* 引入:
* import YearPicker from "@/common/widget/YearPicker";//路徑按照自己的來(lái)
<YearPicker
value={value}
disabled={false} // 是否禁用時(shí)間控件
disabledDate={timeLimit} // 禁用日期,參考disableDate 計(jì)算方式
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();
}
}
// 初始化數(shù)據(jù)處理
initData = (defaultValue) => {
const decade = parseInt(defaultValue / 10, 10) * 10;
const start = decade - 1;
const end = decade + 10;
this.getYearsArr(start, end);
};
// 獲取年份范圍數(shù)組
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(() => {
// 展示彈窗時(shí)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;/*這部分根據(jù)你自己的容器樣式辜荠,我這個(gè)地方是因?yàn)楣媒M件的原因需要設(shè)置*/
#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 計(jì)算方式(也可用于禁用日期)
disabledDateBeforeToday = (current, format) => { // 禁止今年以前的年份(不包含今年)
return current && current < moment(moment().startOf('day').format(format));
}
disabledDateAfterToday = (current) => { // 禁止今年之后的年份(不包含今年)
return current && current >= moment().endOf('day');
}
問(wèn)題1:本來(lái)關(guān)閉彈窗用的是document綁定事件,但是當(dāng)在一個(gè)頁(yè)面里存在多個(gè)YearPicker枚抵,打開(kāi)其中一個(gè)選擇彈窗线欲,再點(diǎn)擊其他YearPicker,會(huì)同時(shí)打開(kāi)多個(gè)彈窗汽摹,所以使用 ReactDOM.createPortal 將整個(gè)選擇的組件與input框隔離成獨(dú)立的部分李丰,采用透明全屏遮罩層的方式,檢測(cè)input在窗口中的位置來(lái)設(shè)置展示組件的位置逼泣,這樣就可以點(diǎn)擊任意位置關(guān)閉組件趴泌,且只出現(xiàn)一個(gè)彈窗。
問(wèn)題2:由于React版本問(wèn)題拉庶,ReactDOM.createPortal 不支持嗜憔。使用ReactDOM.unstable_renderSubtreeIntoContainer 將 YearPicker 加在 body 下。
借鑒文章:
時(shí)間選擇控件YearPicker(基于React氏仗,antd)
React如何將組件渲染到指定節(jié)點(diǎn)—ReactDOM.createPortal