background

It is necessary to make a date picker that only contains the date and year. However, in most date pickers at present, finger sliding uses the Better Scroll plug-in as the mobile scroll plug-in, so it is necessary to install the better Scroll plug-in first, which will undoubtedly increase the volume of the project, so we decide to implement a date picker that does not use plug-ins.

rendering

First, attach the effect drawing

implementation

Implementation approach

  • Do not use scroll plug-ins
  • Use Touchmove to monitor finger sliding
  • Show the year and month in two lists
  • The list is fixed with three rows showing the values of the previous year, current year, and next year
  • When sliding, by judging the direction of finger sliding to change the value, sliding to select the date

The core code

Layout to show

Use two <ul> lists to show year and month;

In <ul>, three <li> are used to show the past, present and future respectively.

The layout code is shown below, and the CSS style code is posted later in the full code.

<div className="date-picker-warpper"> <div className="date-picker-title"> Select the date </div> <div ClassName ="date-selected">{this.state.years. Active} year {this.handleDateselected (this.state.months.active)} month </div> <div className="change-date"> <ul className="date-year" id="yearBar"> <li>{this.state.years.top}</li> <li className="active"> {this.state.years. Active}< span className="year"> year </span> </li> <li>{this.state.years. Bottom}</li> </ul> <ul className="date-month" id="monthBar"> <li>{this.state.months.top}</li> <li className="active"> Months. active} <span className="month"> month </span> </li> <li>{this.state.months.bottom}</li> </ul> </div> <div className="date-picker-button"> <button className="cancel" onClick={(e) => { this.props.handleDatePickerClickCancle(); }} > cancel < / button > < button className = "determine" onClick = {(e) = > {this. Props. HandleDetermineClick (dateString); </button> </div>Copy the code

When the layout is complete, it looks as shown below

Listening to the sliding

  • Add touchMove to the year list and month list
  • Judge the direction of finger sliding by the change of screenY before and after sliding
  • According to the sliding direction, increase or decrease the corresponding value
  • No operation if the sliding distance is less than 20px
yearBar.addEventListener("touchmove", event =>
        this.handleDateTouchMove(event, "year")
    );
monthBar.addEventListener("touchmove", event =>
    this.handleDateTouchMove(event, "month")
);
Copy the code
handleDateTouchMove(event, touchType) { event.stopPropagation(); event.preventDefault(); let screenY = event.changedTouches[0].screenY; let stateObj = this.state[`${touchType}s`]; let targets = Object.assign({}, stateObj), targetChangeY = this[`${touchType}ChangeY`]; If (math. abs(screeny-targetChangey) <= 20) {return; } / / change / / judgment finger sliding direction of the current control the if (screenY - targetChangeY > 0) {/ / finger down the targets. The top = this. DatePickerSubtraction (touchType. targets.top, 'top'); targets.active = this.datePickerSubtraction(touchType, targets.active, 'active'); targets.bottom = this.datePickerSubtraction(touchType, targets.bottom, 'bottom'); } else {// swipe the targets. Top = this.datepickeraddition (touchType, targets. Top, 'top'); targets.active = this.datePickerAddition(touchType, targets.active, 'active'); targets.bottom = this.datePickerAddition(touchType, targets.bottom, 'bottom'); } if (touchType === "month") {let direction = screeny-targetChangey > 0? "down" : "up" let updateYear = this.yearUpdate({ currYear: this.state.years.active, currMonth: this.state.months.active, direction: direction }); if (updateYear) { this.setState({ years: updateYear }); targets = this.initMonths(updateYear.active, direction); } } this.setState({ [`${touchType}s`]: targets }); ChangeY this[' ${touchType}ChangeY '] = screenY; }Copy the code

Calculate the values that should be displayed

From the parameters passed in, calculate the value that should be displayed

  • Uniform numeric format, add 0 to <10
  • Judge critical value
Traction (type, val, dateType) {val = parseInt(val); // Add (type, val, dateType) {val = parseInt(val); if (type == 'year') { if (val == this.nowYear - this.props.yearLimit) { val = this.nowYear; } else { val--; } if (dateType == "active") this.initMonths(val); } else if (type == 'month') { if (val == 1) { val = this.maxMonth; } else if (val <= 10) { val = `0${val - 1}`; } else { val--; } } return `${val}`; } // The value datePickerAddition(type, val, dateType) {val = parseInt(val); if (type == 'year') { if (val == this.nowYear) { val = val - this.props.yearLimit; } else { val++; } if (dateType == "active") this.initMonths(val); } else if (type == 'month') { if (val >= this.maxMonth) { val = "01"; } else if (val < 9) { val = `0${val + 1}`; } else { val = val + 1; } } return `${val}`; }Copy the code

The sliding effect is shown below:

Handling Monthly and Monthly Linkage

  • If the current is 1 month && finger decline, then need to subtract years
  • If the current is the largest month && slip on the finger, you need to subtract years
YearUpdate ({currYear, currMonth, direction}) {// yearUpdate({currYear, currMonth, direction}) const isJan = currMonth == "01"? true : false, isDec = currMonth == this.maxMonth ? true : false; // If the current month is January, If need to reduce (isJan && direction = = "down") {const active = this. DatePickerSubtraction (" year ", currYear, "active"), top = this.datePickerSubtraction("year", active, "bottom"); return { top, active, bottom: currYear }; } else if (isDec && direction == "up") {// If (isDec && direction == "up") { Const active = this.datepickeraddition ("year", currYear, "active"), bottom = this.datepickeraddition ("year", active, "top"); return { top: currYear, active, bottom }; } return null; }Copy the code

The maximum month here is not necessarily December; if the year is the current date year, the maximum month is the current date month

    if (this.activeYear == this.nowYear) {
        this.maxMonth = this.nowMonth;
    } else {
        this.maxMonth = 12;
    }
Copy the code

The monthly and monthly linkage effect is shown in the figure below:

The complete code

Attach the complete code

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class DatePickerComponent extends Component {

    constructor(props) {
        super(props);
        this.now = new Date(),
        this.nowYear = this.now.getFullYear(),
        this.nowMonth = this.now.getMonth() + 1 > 9 ? this.now.getMonth() + 1 : `0${this.now.getMonth() + 1}`;
        this.maxMonth = this.nowMonth;
        this.activeYear = this.nowYear;
        this.yearChangeY = 0;
        this.monthChangeY = 0;
        this.state = {
            datePickerPanelFlag: 'none',
            years: {
                top: this.datePickerSubtraction("year", this.nowYear, 'top'),
                active: this.nowYear,
                bottom: this.datePickerAddition("year", this.nowYear, 'bottom')
            },
            months: {
                top: this.datePickerSubtraction("month", this.nowMonth, 'top'),
                active: this.nowMonth,
                bottom: this.datePickerAddition("month", this.nowMonth, 'bottom')
            },
        };
    }

    // 计算时间控件下滑时,应该展示的值
    datePickerSubtraction(type, val, dateType) {
        val = parseInt(val);
        if (type == 'year') {
            if (val == this.nowYear - this.props.yearLimit) {
                val = this.nowYear;
            } else {
                val--;
            }
            if (dateType == "active") this.initMonths(val);
        } else if (type == 'month') {
            if (val == 1) {
                val = this.maxMonth;
            } else if (val <= 10) {
                val = `0${val - 1}`;
            } else {
                val--;
            }
        }
        return `${val}`;
    }

    // 计算时间控件上滑时,应该展示的值
    datePickerAddition(type, val, dateType) {
        val = parseInt(val);
        if (type == 'year') {
            if (val == this.nowYear) {
                val = val - this.props.yearLimit;
            } else {
                val++;
            }
            if (dateType == "active") this.initMonths(val);
        } else if (type == 'month') {
            if (val >= this.maxMonth) {
                val = "01";
            } else if (val < 9) {
                val = `0${val + 1}`;
            } else {
                val = val + 1;
            }
        }
        return `${val}`;
    }
    //初始化日期选择器月份值
    initMonths(activeYear, direction) {
        if (!this.state) return;

        this.activeYear = activeYear;
        if (this.activeYear == this.nowYear) {
            this.maxMonth = this.nowMonth;
        } else {
            this.maxMonth = 12;
        }
        let stateMonths = this.state.months,
            months = Object.assign({}, stateMonths);
        if (direction && direction === 'up') {
            months.active = '01';
            months.top = this.maxMonth;
            months.bottom = '02';
        } else {
            months.active = this.maxMonth;
            months.top = this.maxMonth - 1;
            months.top = months.top > 9 ? months.top : `0${months.top}`;
            months.bottom = '01';
        }
        this.setState({
            months: months,
        })
        return months;
    }

    // 日期选择器年的关联变化
    yearUpdate({
        currYear,
        currMonth,
        direction
    }) {

        // 年份关联变化
        const isJan = currMonth == "01" ? true : false,
            isDec = currMonth == this.maxMonth ? true : false;
        // 若当前为1月,则需减年
        if (isJan && direction == "down") {
            const active = this.datePickerSubtraction("year", currYear, "active"),
                top = this.datePickerSubtraction("year", active, "bottom");

            return {
                top,
                active,
                bottom: currYear
            };
        } else if (isDec && direction == "up") {
            // 若当前为最大月,则需加年
            const active = this.datePickerAddition("year", currYear, "active"),
                bottom = this.datePickerAddition("year", active, "top");

            return {
                top: currYear,
                active,
                bottom
            };
        }

        return null;
    }

    datePickerScrollInit() {
        const yearBar = document.getElementById('yearBar'),
            monthBar = document.getElementById('monthBar');
        // 绑定事件
        yearBar.addEventListener("touchmove", event =>
            this.handleDateTouchMove(event, "year")
        );
        monthBar.addEventListener("touchmove", event =>
            this.handleDateTouchMove(event, "month")
        );
        yearBar.addEventListener("touchstart", event => {
            this.yearChangeY = event.changedTouches[0].screenY;
        });
        monthBar.addEventListener("touchstart", event => {
            this.monthChangeY = event.changedTouches[0].screenY;
        });
    }
    handleDateTouchMove(event, touchType) {
        event.stopPropagation();
        event.preventDefault();
        let screenY = event.changedTouches[0].screenY;
        let stateObj = this.state[`${touchType}s`];
        let targets = Object.assign({}, stateObj),
            targetChangeY = this[`${touchType}ChangeY`];
        // 滑动距离小于20px不操作
        if (Math.abs(screenY - targetChangeY) <= 20) {
            return;
        }
        // 当前控件的变化
        // 判断手指滑动方向
        if (screenY - targetChangeY > 0) {
            // 手指下滑
            targets.top = this.datePickerSubtraction(touchType, targets.top, 'top');
            targets.active = this.datePickerSubtraction(touchType, targets.active, 'active');
            targets.bottom = this.datePickerSubtraction(touchType, targets.bottom, 'bottom');
        } else {
            // 手指上滑
            targets.top = this.datePickerAddition(touchType, targets.top, 'top');
            targets.active = this.datePickerAddition(touchType, targets.active, 'active');
            targets.bottom = this.datePickerAddition(touchType, targets.bottom, 'bottom');
        }
        if (touchType === "month") { // 年份关联变化
            let direction = screenY - targetChangeY > 0 ? "down" : "up"
            let updateYear = this.yearUpdate({
                currYear: this.state.years.active,
                currMonth: this.state.months.active,
                direction: direction
            });
            if (updateYear) {
                this.setState({
                    years: updateYear
                });
                targets = this.initMonths(updateYear.active, direction);
            }
        }


        this.setState({
            [`${touchType}s`]: targets
        });
        // 将当前滑动后的 screenY 赋值给对应的 ChangeY
        this[`${touchType}ChangeY`] = screenY;
    }

    handleDateSelected(value) {
        if (value) {
            value = String(value);
            return value.replace(/\b(0+)/gi, "");
        }
    }

    dateReceive() {
        let { defaultYear, defaultMonth } = this.props;
        this.activeYear = defaultYear;
        let stateMonths = this.state.months,
            stateYears = this.state.years,
            months = Object.assign({}, stateMonths),
            years = Object.assign({}, stateYears);
        years.active = defaultYear;
        if (years.active != this.nowYear) this.maxMonth = 12;
        years.top = this.datePickerSubtraction("year", defaultYear, 'top');
        years.bottom = this.datePickerAddition("year", defaultYear, 'bottom');
        months.active = defaultMonth;
        months.top = this.datePickerSubtraction("month", defaultMonth, 'top');
        months.bottom = this.datePickerAddition("month", defaultMonth, 'bottom');
        this.setState({
            years: years,
            months: months,
        })
    }

    handleBody() {
        document.body.style.overflow = 'hidden';
        document.getElementsByTagName('html')[0].style.overflow = 'hidden';
    }

    handleBodycancel() {
        document.body.style.overflow = '';
        document.getElementsByTagName('html')[0].style.overflow = '';
    }

    listenModalClick() {
        let modal = document.getElementById("modal");
        modal.addEventListener("click", e => {
            if (e.target.className !== "ui-modal") return;
            this.props.handleDatePickerClickCancle();
        }, false)
    }

    componentDidMount() {
        this.datePickerScrollInit();
        this.dateReceive();
        this.listenModalClick();
        this.handleBody();
    }
    componentWillUnmount() {
        this.handleBodycancel();
    }


    render() {
        const { datePickerPanelFlag } = this.props;
        let dateString = this.state.years.active + this.state.months.active;
        let datePickerPanel = <div className="ui-modal" id="modal" >
            <div className="date-picker-warpper">
                <div className="date-picker-title">选择日期</div>
                <div className="date-selected">{this.state.years.active}年{this.handleDateSelected(this.state.months.active)}月</div>
                <div className="change-date">
                    <ul className="date-year" id="yearBar">
                        <li>{this.state.years.top}</li>
                        <li className="active">
                            {this.state.years.active}
                            <span className="year">年</span>
                        </li>
                        <li>{this.state.years.bottom}</li>
                    </ul>
                    <ul className="date-month" id="monthBar">
                        <li>{this.state.months.top}</li>
                        <li className="active">
                            {this.state.months.active}
                            <span className="month">月</span>
                        </li>
                        <li>{this.state.months.bottom}</li>
                    </ul>
                </div>
                <div className="date-picker-button">
                    <button className="cancel" onClick={(e) => { this.props.handleDatePickerClickCancle(); }} >取消</button>
                    <button className="determine" onClick={(e) => { this.props.handleDetermineClick(dateString); }} >确定</button>
                </div>
            </div>
        </div>;
        return (<div>
            {datePickerPanel}
        </div>
        );
    }
}

DatePickerComponent.PropTypes = {
    datePickerPanelFlag: PropTypes.string.datePickerPanelFlag,
    defaultYear: PropTypes.string.defaultYear,
    defaultMonth: PropTypes.string.defaultMonth,
    yearLimit: PropTypes.number.yearLimit
};

export default DatePickerComponent;
Copy the code

style

@defaultSize: 16 .date-picker-warpper { width: 100%; height: unit(369/@defaultSize, rem); background: rgba(255, 255, 255, 1); border-radius: unit(19/@defaultSize, rem) unit(19/@defaultSize, rem) 0px 0px; position: fixed; bottom: 0; .date-picker-title { text-align: center; height: unit(22/@defaultSize, rem); margin-top: unit(27/@defaultSize, rem); font-size: unit(16/@defaultSize, rem); font-weight: bold; color: rgba(51, 59, 76, 1); line-height: unit(22/@defaultSize, rem); } .date-selected { text-align: center; margin-top: unit(5/@defaultSize, rem); margin-bottom: unit(17/@defaultSize, rem); font-size: unit(13/@defaultSize, rem); color: rgba(51, 59, 76, 1); line-height: unit(18/@defaultSize, rem); height: unit(18/@defaultSize, rem); } .change-date { display: flex; height: unit(174/@defaultSize, rem); justify-content: space-between; .date-year, .date-month { width: 50%; position: relative; color: rgba(178, 181, 187, 1); text-align: center; font-size: unit(17/@defaultSize, rem); li { line-height: rem(58px); span { display: inline-block; color: rgba(17, 110, 255, 1); font-size: unit(10/@defaultSize, rem); vertical-align: text-top; margin-left: unit(5/@defaultSize, rem); } } } .date-year { li { padding: unit(16/@defaultSize, rem) 0 unit(15/@defaultSize, rem) unit(60/@defaultSize, rem); } .active { font-size: unit(19/@defaultSize, rem); color: rgba(17, 110, 255, 1); padding: unit(16/@defaultSize, rem) 0 unit(15/@defaultSize, rem) unit(75/@defaultSize, rem); } } .date-month { li { padding: unit(16/@defaultSize, rem) unit(60/@defaultSize, rem) unit(15/@defaultSize, rem) 0; } .active { font-size: unit(19/@defaultSize, rem); color: rgba(17, 110, 255, 1); padding: unit(16/@defaultSize, rem) unit(42/@defaultSize, rem) unit(15/@defaultSize, rem) 0; } } } .date-text { line-height: unit(58/@defaultSize, rem); height: unit(58/@defaultSize, rem); font-size: unit(17/@defaultSize, rem); color: rgba(178, 181, 187, 1); } .date-text-current { line-height: unit(58/@defaultSize, rem); height: unit(58/@defaultSize, rem); font-size: unit(19/@defaultSize, rem); color: rgba(17, 110, 255, 1); } .date-unit { font-size: unit(10/@defaultSize, rem); color: rgba(17, 110, 255, 1); line-height: unit(13/@defaultSize, rem); } .date-picker-button { button { text-align: center; width: unit(147/@defaultSize, rem); height: unit(47/@defaultSize, rem); line-height: unit(47/@defaultSize, rem); background: rgba(245, 245, 246, 1); border-radius: unit(23/@defaultSize, rem); font-size: unit(15/@defaultSize, rem); } .cancel { position: absolute; left: unit(27/@defaultSize, rem); } .determine { position: absolute; right: unit(27/@defaultSize, rem); color: rgba(17, 110, 255, 1); }}}Copy the code

use

The < DatePickerComponent handleDatePickerClickCancle = {this. HandleDatePickerClickCancle. Bind (this)} / / click cancel HandleDetermineClick = {this. HandleDetermineClick. Bind (this)} / / click ok DatePickerPanelFlag = {this. State. DatePickerPanelFlag} / / control show hidden defaultYear = {this. DefaultYear} / / the default display DefaultMonth ={this.defaultMonth}// default display month yearLimit={5}// yearLimit />Copy the code

conclusion

Using this method to realize the date picker on the mobile terminal, although avoiding the use of plug-ins, it will lead to feel deviation when sliding, and the user experience needs to be improved.