I'm developing a custom date input format to enhance user experience in scenarios where birthdays and expiration dates are inputted. Traditional date fields and date picker libraries often fall short in offering intuitive user interactions. My script has been designed to:
- Enforce a month value range from 01 to 12.
- Ensure day values stay within the 01 to 31 range.
- Set the year value boundaries between 1900 and 2100.
- If two digits year starting with 0 or 3 is entered year is assumed 1900s.
- If two digits year starting with 4 - 9 is entered year is assumed to be 2000s.
- Automatically advance from the month to day section when the first digit of the month exceeds 1.
- Move the cursor to the year section when the initial digit of the day goes beyond 3.
- Recognize a manually entered slash (/), and, if needed, precede it with a leading zero before transitioning to the next segment.
- Filter out non-numeric inputs, with the sole exception being a manually entered slash (/).
- Adjust a 00 entry for month or day to 01 for better consistency.
- Permit the backspace functionality to clear fields without default autofill interruptions.
Added:
- Form validation using pattern attribute, thanks morbusg
- Allowed for pasting ISO yyyy-mm-dd formats, along with mm/dd/yyyy and m/d/yyyy, thanks J-H
My goal is to optimize user interactions, ensuring the underlying code is robust, reliable, and widely compatible across browsers.
const dateInputs = document.querySelectorAll('.date-input');
dateInputs.forEach(dateInput => {
let lastValue = ""; // To keep track of the previous value for backspace detection
dateInput.addEventListener('input', function() {
let val = this.value;
// If backspacing, skip the rest of the formatting
if (lastValue && lastValue.length > val.length) {
lastValue = val;
return;
}
if (val.endsWith('/')) {
val = val.slice(0, -1); // Remove the manually entered slash
if (val.length === 1) {
val = '0' + val + '/';
} else if (val.length === 4) {
val = val.slice(0, 3) + '0' + val.slice(3) + '/';
}
}
val = val.replace(/\D/g, ''); // Remove any non-digit characters
// Apply the MM/DD/YYYY format with constraints
if (val.length === 1 && parseInt(val, 10) > 1) {
val = '0' + val + '/';
} else if (val.length >= 2) {
let month = parseInt(val.substring(0, 2), 10);
if (month === 0) month = 1;
if (month > 12) month = 12;
val = (month < 10 ? '0' + month : month) + '/' + val.substring(2);
}
if (val.length === 4 && parseInt(val.substring(3, 4), 10) > 3) {
val = val.substring(0, 3) + '0' + val.substring(3) + '/';
} else if (val.length >= 5) {
let day = parseInt(val.substring(3, 5), 10);
if (day === 0) day = 1;
if (day > 31) day = 31;
val = val.substring(0, 3) + (day < 10 ? '0' + day : day) + '/' + val.substring(5);
}
if (val.length === 7) {
const yearStart = parseInt(val.substring(6, 7), 10);
if (yearStart >= 4 && yearStart <= 9) { // If between 4 - 9 assume 1900's
val = val.substring(0, 6) + '19' + val.substring(6);
} else if (yearStart === 0 || yearStart === 3) { // If between 0 or 3 assume 2000's
val = val.substring(0, 6) + '20' + val.substring(6);
}
} else if (val.length >= 10) {
let year = parseInt(val.substring(6, 10), 10);
if (year < 1900) {
year = 1900;
} else if (year > 2100) {
year = 2100;
}
val = val.substring(0, 6) + year;
}
this.value = val;
lastValue = val;
dateInput.addEventListener('paste', function(e) {
e.preventDefault();
const clipboardData = e.clipboardData || window.clipboardData;
let pastedData = clipboardData.getData('Text');
if (isISOFormat(pastedData)) {
this.value = convertFromISOFormat(pastedData);
}
else if (isShortDateFormat(pastedData)) {
this.value = convertFromShortFormat(pastedData);
}
else if (isStandardDateFormat(pastedData)) {
this.value = pastedData;
}
else {
// If it doesn't match any of the allowed formats, don't set the value.
this.value = "";
alert("Please paste a date in the correct format!");
return;
}
let event = new Event('input', {
'bubbles': true,
'cancelable': true
});
dateInput.dispatchEvent(event); // manually dispatch input event to trigger your input handler after paste
});
});
// Prevent users from entering non-digit characters, except slash
dateInput.addEventListener('keydown', function(e) {
if (!isAllowedDateKeyEvent(e)) {
e.preventDefault();
}
});
});
// Check if it's in yyyy-mm-dd format
function isISOFormat(date) {
const isoPattern = /^\d{4}-\d{2}-\d{2}$/;
return isoPattern.test(date);
}
// Convert yyyy-mm-dd to MM/DD/YYYY
function convertFromISOFormat(isoDate) {
const parts = isoDate.split('-');
return `${parts[1]}/${parts[2]}/${parts[0]}`;
}
// Check if it's in m/d/yyyy format
function isShortDateFormat(date) {
const shortDatePattern = /^\d{1,2}\/\d{1,2}\/\d{4}$/;
return shortDatePattern.test(date);
}
// Convert m/d/yyyy to MM/DD/YYYY
function convertFromShortFormat(shortDate) {
const parts = shortDate.split('/');
const month = parts[0].padStart(2, '0');
const day = parts[1].padStart(2, '0');
const year = parts[2];
return `${month}/${day}/${year}`;
}
// Check if it's in mm/dd/yyyy format
function isStandardDateFormat(date) {
const standardDatePattern = /^\d{2}\/\d{2}\/\d{4}$/;
return standardDatePattern.test(date);
}
function isAllowedDateKeyEvent(e) {
let charCode = e.keyCode;
// Allow CTRL key or CMD key on Mac (e.metaKey)
if (e.ctrlKey || e.metaKey) {
return true;
}
if (charCode > 31 && (charCode < 48 || charCode > 57) && charCode !== 37 && charCode !== 39 && charCode !== 8 && charCode !== 46 && charCode !== 191) {
return false;
}
return true;
}
<input type="text" class="date-input" placeholder="mm/dd/yyyy" pattern="\d{2}/\d{2}/\d{4}" inputmode="numeric">
3" impressed me as trouble, since a pre-determined (paste) string of valid user input characters would be treated differently based on such details. I had in mind unconditionally accepting 1 input char, then 1 msec later "fix it up". \$\endgroup\$