模拟 AntD YearPicker

2021-12-13  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读