#Application
View Compiled
html, body
margin 0
padding 0
width 100%
height 100%
color #fff
background linear-gradient(to left top, #333, #111)
font-family "Lato", sans-serif
#Application
width 100%
height 100%
display flex
align-items center
justify-content center
.DateInput
.DateInput__help
text-align center
transition 0.2s ease-out all
margin-top 2rem
.DateInput__is-in
opacity 1
.DateInput__is-out
opacity 0
.DateInput__container
width 100%
.DateInput__separator
display inline-block
font-size 250%
font-weight 100
text-align center
.DateInput__group
width calc(100% / 6.5)
display inline-block
label
display block
text-align center
color #fff
margin-bottom 0.5rem
input
padding-top 0.5rem
user-select none
display block
margin 0 auto
width 100%
text-align center
font-size 250%
border 0
outline 0
background transparent
color #fff
border-top 2px solid transparent
font-weight 200
input::selection
background transparent
input:focus
border-top 2px solid #fff
transition .4s ease-out all
View Compiled
const KEYS = {
BACKSPACE: 8,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
NUM_0: 48,
NUM_1: 49,
NUM_2: 50,
NUM_3: 51,
NUM_4: 52,
NUM_5: 53,
NUM_6: 54,
NUM_7: 55,
NUM_8: 56,
NUM_9: 57,
NUMPAD_0: 96,
NUMPAD_1: 97,
NUMPAD_2: 98,
NUMPAD_3: 99,
NUMPAD_4: 100,
NUMPAD_5: 101,
NUMPAD_6: 102,
NUMPAD_7: 103,
NUMPAD_8: 104,
NUMPAD_9: 105
};
function getCharacter(keyCode) {
switch(keyCode) {
default: return ".";
case KEYS.NUM_0: case KEYS.NUMPAD_0: return "0";
case KEYS.NUM_1: case KEYS.NUMPAD_1: return "1";
case KEYS.NUM_2: case KEYS.NUMPAD_2: return "2";
case KEYS.NUM_3: case KEYS.NUMPAD_3: return "3";
case KEYS.NUM_4: case KEYS.NUMPAD_4: return "4";
case KEYS.NUM_5: case KEYS.NUMPAD_5: return "5";
case KEYS.NUM_6: case KEYS.NUMPAD_6: return "6";
case KEYS.NUM_7: case KEYS.NUMPAD_7: return "7";
case KEYS.NUM_8: case KEYS.NUMPAD_8: return "8";
case KEYS.NUM_9: case KEYS.NUMPAD_9: return "9";
}
}
function getDestructuredDate(date) {
const y = date.getFullYear();
const m = date.getMonth();
const d = date.getDate();
const h = date.getHours();
const i = date.getMinutes();
const s = date.getSeconds();
return { y, m, d, h, i, s };
}
function getLabels() {
const y = /es(-[A-Z]{2})?/.test(navigator.language) ? "Año": "Year";
const m = /es(-[A-Z]{2})?/.test(navigator.language) ? "Mes": "Month";
const d = /es(-[A-Z]{2})?/.test(navigator.language) ? "Día": "Day";
const h = /es(-[A-Z]{2})?/.test(navigator.language) ? "Hora": "Hour";
const i = /es(-[A-Z]{2})?/.test(navigator.language) ? "Min.": "Min.";
const s = /es(-[A-Z]{2})?/.test(navigator.language) ? "Seg.": "Sec.";
return { y, m, d, h, i, s };
}
function setDate(date, y = date.getFullYear(), m = date.getMonth(), d = date.getDate(), h = date.getHours(), i = date.getMinutes(), s = date.getSeconds()) {
return new Date(y, m, d, h, i, s);
}
function getDatePart(date, part) {
switch(part) {
default: throw new Error("Invalid part name");
case "year": return date.getFullYear();
case "month": return date.getMonth();
case "day": return date.getDate();
case "hours": return date.getHours();
case "minutes": return date.getMinutes();
case "seconds": return date.getSeconds();
}
}
function setDatePart(date, part, value) {
const y = (part === "year" ? value : date.getFullYear());
const m = (part === "month" ? value : date.getMonth());
const d = (part === "day" ? value : date.getDate());
const h = (part === "hours" ? value : date.getHours());
const i = (part === "minutes" ? value : date.getMinutes());
const s = (part === "seconds" ? value : date.getSeconds());
return new Date(y,m,d,h,i,s);
}
function addDate(date, ay, am, ad, ah, ai, as) {
const { y, m, d, h, i, s } = getDestructuredDate(date);
return new Date(y + ay,m + am,d + ad,h + ah,i + ai,s + as);
}
function incrementYear(date,q = 1) { return addDate(date,q,0,0,0,0,0); }
function decrementYear(date,q = 1) { return addDate(date,-q,0,0,0,0,0); }
function incrementMonth(date,q = 1) { return addDate(date,0,q,0,0,0,0); }
function decrementMonth(date,q = 1) { return addDate(date,0,-q,0,0,0,0); }
function incrementDay(date,q = 1) { return addDate(date,0,0,q,0,0,0); }
function decrementDay(date,q = 1) { return addDate(date,0,0,-q,0,0,0); }
function incrementHours(date,q = 1) { return addDate(date,0,0,0,q,0,0); }
function decrementHours(date,q = 1) { return addDate(date,0,0,0,-q,0,0); }
function incrementMinutes(date,q = 1) { return addDate(date,0,0,0,0,q,0); }
function decrementMinutes(date,q = 1) { return addDate(date,0,0,0,0,-q,0); }
function incrementSeconds(date,q = 1) { return addDate(date,0,0,0,0,0,q); }
function decrementSeconds(date,q = 1) { return addDate(date,0,0,0,0,0,-q); }
function incrementDate(date, part, q = 1) {
switch(part) {
default: throw new Error("Invalid part name");
case "year": return incrementYear(date, q);
case "month": return incrementMonth(date, q);
case "day": return incrementDay(date, q);
case "hours": return incrementHours(date, q);
case "minutes": return incrementMinutes(date, q);
case "seconds": return incrementSeconds(date, q);
}
}
function decrementDate(date, part, q = 1) {
switch(part) {
default: throw new Error("Invalid part name");
case "year": return decrementYear(date, q);
case "month": return decrementMonth(date, q);
case "day": return decrementDay(date, q);
case "hours": return decrementHours(date, q);
case "minutes": return decrementMinutes(date, q);
case "seconds": return decrementSeconds(date, q);
}
}
function modifyDate(date, part, increment = true) {
if (increment) {
return incrementDate(date, part);
} else {
return decrementDate(date, part);
}
}
const DateInput = React.createClass({
getDefaultProps() {
return {
helpTime: 5000,
helpTexts: [
"You can use ← and → keys to move between fields",
"You can use ↓ and ↑ keys to change current value",
"Press left mouse button over a number and drag up and down to change current value"
]
};
},
getInitialState() {
return {
activeElement: null,
date: new Date(),
selectionRange: 0,
helpIndex: 0,
helpIsVisible: false,
isDragging: false
};
},
handleCopy(e) {
e.preventDefault();
const data = this.state.date.toLocaleString();
const datetime = this.state.date.toISOString();
e.clipboardData.setData("text/plain", data);
e.clipboardData.setData("text/html", `<time datetime="${datetime}">${data}</time>`);
},
handlePaste(e) {
const data = e.clipboardData.getData("text/plain");
const newDate = Date.parse(data);
if (isNaN(newDate)) {
e.preventDefault();
} else {
this.setState({
date: new Date(newDate)
});
}
},
focusPrevious() {
switch (this.state.activeElement) {
case this.refs.year: break;
case this.refs.month: this.setState({ activeElement: this.refs.year, selectionRange: this.refs.year.value.length }); break;
case this.refs.day: this.setState({ activeElement: this.refs.month, selectionRange: this.refs.month.value.length }); break;
case this.refs.hours: this.setState({ activeElement: this.refs.day, selectionRange: this.refs.day.value.length }); break;
case this.refs.minutes: this.setState({ activeElement: this.refs.hours, selectionRange: this.refs.hours.value.length }); break;
case this.refs.seconds: this.setState({ activeElement: this.refs.minutes, selectionRange: this.refs.minutes.value.length }); break;
}
},
focusNext() {
switch (this.state.activeElement) {
case this.refs.year: this.setState({ activeElement: this.refs.month, selectionRange: 0 }); break;
case this.refs.month: this.setState({ activeElement: this.refs.day, selectionRange: 0 }); break;
case this.refs.day: this.setState({ activeElement: this.refs.hours, selectionRange: 0 }); break;
case this.refs.hours: this.setState({ activeElement: this.refs.minutes, selectionRange: 0 }); break;
case this.refs.minutes: this.setState({ activeElement: this.refs.seconds, selectionRange: 0 }); break;
case this.refs.seconds: break;
}
},
focusFirst() {
this.setState({ activeElement: this.refs.year, selectionRange: 0 });
},
focusLast() {
this.setState({ activeElement: this.refs.seconds, selectionRange: 0 });
},
isYearActive() { return this.state.activeElement === this.refs.year; },
isMonthActive() { return this.state.activeElement === this.refs.month; },
isDayActive() { return this.state.activeElement === this.refs.day; },
isHoursActive() { return this.state.activeElement === this.refs.hours; },
isMinutesActive() { return this.state.activeElement === this.refs.minutes; },
isSecondsActive() { return this.state.activeElement === this.refs.seconds; },
handleKey(e) {
if (e.keyCode === KEYS.DOWN || e.keyCode === KEYS.UP) {
e.preventDefault();
this.setState({ date: modifyDate(this.state.date, e.target.name, e.keyCode === KEYS.UP) });
} else if (e.keyCode === KEYS.LEFT || e.keyCode === KEYS.RIGHT) {
if (this.state.selectionRange === 0 && e.keyCode === KEYS.LEFT) {
this.focusPrevious();
} else if (this.state.selectionRange === e.target.value.length && e.keyCode === KEYS.RIGHT) {
this.focusNext();
} else {
this.setState({
selectionRange: e.target.selectionStart + (e.keyCode === KEYS.LEFT ? -1 : 1)
});
}
} else if (e.keyCode === KEYS.HOME || e.keyCode === KEYS.END) {
if (e.keyCode === KEYS.HOME) {
this.focusFirst();
} else if (e.keyCode === KEYS.END) {
this.focusLast();
}
} else if (e.keyCode === KEYS.BACKSPACE) {
if (this.state.selectionRange === 0) {
this.focusPrevious();
} else {
this.setState({
selectionRange: Math.max(this.state.selectionRange - 1, 0)
});
}
} else {
const character = getCharacter(e.keyCode);
if (!/^[0-9]$/.test(character)) {
return;
}
const value = e.target.value;
const selectionStart = e.target.selectionStart;
const selectionEnd = e.target.selectionEnd;
let newValue = parseInt(value.substr(0,selectionStart) + character + value.substr(selectionStart + 1));
if (this.state.activeElement === this.refs.year && String(newValue).length > 4
|| this.state.activeElement === this.refs.month && String(newValue).length > 2
|| this.state.activeElement === this.refs.day && String(newValue).length > 2
|| this.state.activeElement === this.refs.hours && String(newValue).length > 2
|| this.state.activeElement === this.refs.minutes && String(newValue).length > 2
|| this.state.activeElement === this.refs.seconds && String(newValue).length > 2) {
return;
}
if (this.isMonthActive()) {
if (newValue > 12) {
if (this.state.selectionRange === 0) {
newValue = 10;
} else {
newValue = 12;
}
}
newValue--;
}
if (this.isDayActive()) {
if (newValue > 31) {
if (this.state.selectionRange === 0) {
newValue = 30;
} else {
newValue = 31;
}
}
}
if (this.isHoursActive()) {
if (newValue > 23) {
if (this.state.selectionRange === 0) {
newValue = 20;
} else {
newValue = 23;
}
}
}
if (this.isMinutesActive()
|| this.isSecondsActive()) {
if (newValue > 59) {
if (this.state.selectionRange === 0) {
newValue = 50;
} else {
newValue = 59;
}
}
}
let newSelectionRange = selectionStart + 1;
if (this.state.activeElement === this.refs.year && this.state.selectionRange + 1 === 4
|| this.state.activeElement === this.refs.month && this.state.selectionRange + 1 === 2
|| this.state.activeElement === this.refs.day && this.state.selectionRange + 1 === 2
|| this.state.activeElement === this.refs.hours && this.state.selectionRange + 1 === 2
|| this.state.activeElement === this.refs.minutes && this.state.selectionRange + 1 === 2) {
this.focusNext();
newSelectionRange = 0;
}
this.setState({
date: setDatePart(this.state.date, e.target.name, newValue),
selectionRange: newSelectionRange
});
}
},
format(value) {
let string = String(value);
if (string.length === 1) {
return "0" + string;
} else {
return string;
}
},
delayed(fn) {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(fn, 0);
},
hideHelp() {
this.setState({ helpIsVisible: false });
this.helpTimeout = setTimeout(this.changeHelp, 200);
},
showHelp() {
this.setState({ helpIsVisible: true });
this.helpTimeout = setTimeout(this.hideHelp, this.props.helpTime);
},
changeHelp() {
this.setState({
helpIndex: (this.state.helpIndex + 1) % this.props.helpTexts.length
});
this.helpTimeout = setTimeout(this.showHelp, 200);
},
componentDidMount() {
this.hideHelp = this.hideHelp.bind(this);
this.showHelp = this.showHelp.bind(this);
this.changeHelp = this.changeHelp.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.changeHelp();
},
componentWillUnmount() {
clearTimeout(this.helpTimeout);
this.helpTimeout = null;
},
componentDidUpdate(prevProps, prevState) {
if (this.state.activeElement) {
if (this.state.activeElement !== prevState.activeElement) {
this.state.activeElement.focus();
}
this.delayed(() => {
this.state.activeElement.setSelectionRange(this.state.selectionRange,this.state.selectionRange);
this.timeout = null;
});
}
},
hasFocus() {
return document.activeElement === this.refs.year
|| document.activeElement === this.refs.month
|| document.activeElement === this.refs.day
|| document.activeElement === this.refs.hours
|| document.activeElement === this.refs.minutes
|| document.activeElement === this.refs.seconds;
},
handleBlur(e) {
if (this.blurTimeout) {
clearTimeout(this.blurTimeout);
this.blurTimeout = null;
}
this.blurTimeout = setTimeout(() => {
if (document.activeElement === null
|| (document.activeElement !== this.refs.year
&& document.activeElement !== this.refs.month
&& document.activeElement !== this.refs.day
&& document.activeElement !== this.refs.hours
&& document.activeElement !== this.refs.minutes
&& document.activeElement !== this.refs.seconds)) {
this.setState({
activeElement: null,
selectionRange: 0
});
}
}, 0);
},
handleMouseDown(e) {
e.preventDefault();
if (this.state.isDragging === false && e.button === 0) {
this.setState({
isDragging: true,
activeElement: e.target,
selectionRange: 0
});
this.current = this.start = e.clientY;
document.addEventListener("mouseup", this.handleMouseUp);
document.addEventListener("mouseleave", this.handleMouseUp);
document.addEventListener("mousemove", this.handleMouseMove);
window.requestAnimationFrame(this.handleFrame);
}
},
handleMouseUp(e) {
if (this.state.isDragging === true) {
this.setState({ isDragging: false });
document.removeEventListener("mouseup", this.handleMouseUp);
document.removeEventListener("mouseleave", this.handleMouseUp);
document.removeEventListener("mousemove", this.handleMouseMove);
}
},
getDirection(value) {
if (value > 0) {
return 1;
} else if (value < 0) {
return -1;
}
return 0;
},
handleFrame(now) {
if (this.now === undefined) {
this.now = now;
}
const diff = Math.round((this.current - this.start) / (window.innerHeight * 0.01));
const dir = this.getDirection(diff);
const cadence = Math.max(50, Math.min(200, 500 - (Math.abs(diff) * 10)));
if (dir !== 0) {
if (now - this.now > cadence) {
this.now = now;
const value = parseInt(this.state.activeElement.value);
const newValue = value - dir; // we need to invert direction.
this.setState({
date: setDatePart(this.state.date, this.state.activeElement.name, newValue),
});
}
}
if (this.state.isDragging) {
window.requestAnimationFrame(this.handleFrame);
}
},
handleMouseMove(e) {
this.current = e.clientY;
},
isHelpVisible() {
return this.hasFocus() && this.state.helpIsVisible;
},
renderHelp() {
const classes = "DateInput__help " + (this.isHelpVisible() ? "DateInput__is-in" : "DateInput__is-out");
const help = this.props.helpTexts[this.state.helpIndex];
return (
<div className={classes}>{help}</div>
);
},
render() {
const labels = getLabels();
const date = getDestructuredDate(this.state.date);
const help = this.renderHelp();
return (
<div className="DateInput">
<div className="DateInput__container">
<div className="DateInput__group">
<label for="year">{labels.y}</label>
<input ref="year"
type="text"
name="year"
id="year"
onMouseDown={this.handleMouseDown}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyDown={this.handleKey}
onPaste={this.handlePaste}
onCopy={this.handleCopy}
value={this.format(date.y)} />
</div>
<div className="DateInput__separator">
/
</div>
<div className="DateInput__group">
<label for="month">{labels.m}</label>
<input ref="month"
type="text"
name="month"
id="month"
onMouseDown={this.handleMouseDown}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyDown={this.handleKey}
onPaste={this.handlePaste}
onCopy={this.handleCopy}
value={this.format(date.m + 1)} />
</div>
<div className="DateInput__separator">
/
</div>
<div className="DateInput__group">
<label for="day">{labels.d}</label>
<input ref="day"
type="text"
name="day"
id="day"
onMouseDown={this.handleMouseDown}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyDown={this.handleKey}
onPaste={this.handlePaste}
onCopy={this.handleCopy}
value={this.format(date.d)} />
</div>
<div className="DateInput__group">
<label for="hours">{labels.h}</label>
<input ref="hours"
type="text"
name="hours"
id="hours"
onMouseDown={this.handleMouseDown}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyDown={this.handleKey}
onPaste={this.handlePaste}
onCopy={this.handleCopy}
value={this.format(date.h)} />
</div>
<div className="DateInput__separator">
:
</div>
<div className="DateInput__group">
<label for="minutes">{labels.i}</label>
<input ref="minutes"
type="text"
name="minutes"
id="minutes"
onMouseDown={this.handleMouseDown}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyDown={this.handleKey}
onPaste={this.handlePaste}
onCopy={this.handleCopy}
value={this.format(date.i)} />
</div>
<div className="DateInput__separator">
:
</div>
<div className="DateInput__group">
<label for="seconds">{labels.s}</label>
<input ref="seconds"
type="text"
name="seconds"
id="seconds"
onMouseDown={this.handleMouseDown}
onBlur={this.handleBlur}
onClick={this.handleClick}
onKeyDown={this.handleKey}
onPaste={this.handlePaste}
onCopy={this.handleCopy}
value={this.format(date.s)} />
</div>
</div>
{help}
</div>
);
}
});
ReactDOM.render(
<DateInput />,
document.querySelector("#Application")
);
View Compiled
This Pen doesn't use any external CSS resources.