模擬 AntD YearPicker

目前公司使用的是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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吉捶,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子廓鞠,更是在濱河造成了極大的恐慌帚稠,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件床佳,死亡現(xiàn)場(chǎng)離奇詭異滋早,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)砌们,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門杆麸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人浪感,你說(shuō)我怎么就攤上這事昔头。” “怎么了影兽?”我有些...
    開(kāi)封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵揭斧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我峻堰,道長(zhǎng)讹开,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任捐名,我火速辦了婚禮旦万,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镶蹋。我一直安慰自己成艘,他們只是感情好赏半,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著淆两,像睡著了一般断箫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琼腔,一...
    開(kāi)封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天瑰枫,我揣著相機(jī)與錄音踱葛,去河邊找鬼丹莲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛尸诽,可吹牛的內(nèi)容都是我干的甥材。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼性含,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼洲赵!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起商蕴,我...
    開(kāi)封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤叠萍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后绪商,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苛谷,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年格郁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腹殿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡例书,死狀恐怖锣尉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情决采,我是刑警寧澤自沧,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站树瞭,受9級(jí)特大地震影響拇厢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜移迫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一旺嬉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厨埋,春花似錦邪媳、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)迅涮。三九已至,卻和暖如春徽龟,著一層夾襖步出監(jiān)牢的瞬間叮姑,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工据悔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留传透,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓极颓,卻偏偏與公主長(zhǎng)得像朱盐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子菠隆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容