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.