美文网首页
模拟 AntD YearPicker

模拟 AntD YearPicker

作者: 阳光小羊羊的草场 | 来源:发表于2021-12-13 17:48 被阅读0次

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

    相关文章

      网友评论

          本文标题:模拟 AntD YearPicker

          本文链接:https://www.haomeiwen.com/subject/titsfrtx.html