/** * FormValidation (https://formvalidation.io) * The best validation library for JavaScript * (c) 2013 - 2020 Nguyen Huu Phuoc */ import { Localization, ValidateInput, ValidateOptions, ValidateResult } from '../core/Core'; import format from '../utils/format'; import isValidDate from '../utils/isValidDate'; type CompareCallback = () => (string | Date); export interface DateOptions extends ValidateOptions { // The date format. Default is MM/DD/YYYY // The format can be: // - date: Consist of DD, MM, YYYY parts which are separated by the separator option // - date and time: The time can consist of h, m, s parts which are separated by : // - date, time and A (indicating AM or PM) format: string; // The maximum date max?: string | Date | CompareCallback; // The minimum date min?: string | Date | CompareCallback; // Use to separate the date, month, and year. By default, it is / separator?: string; } export interface DateLocalization extends Localization { date: { default: string, max: string, min: string, range: string, }; } export default function date() { /** * Return a date object after parsing the date string * * @param {string} input The date to parse * @param {string[]} inputFormat The date format * The format can be: * - date: Consist of DD, MM, YYYY parts which are separated by the separator option * - date and time: The time can consist of h, m, s parts which are separated by : * @param {string} separator The separator used to separate the date, month, and year * @return {Date} * @private */ const parseDate = (input: string, inputFormat: string[], separator: string) => { // Ensure that the format must consist of year, month and day patterns const yearIndex = inputFormat.indexOf('YYYY'); const monthIndex = inputFormat.indexOf('MM'); const dayIndex = inputFormat.indexOf('DD'); if (yearIndex === -1 || monthIndex === -1 || dayIndex === -1) { return null; } const sections = (input as string).split(' '); const dateSection = sections[0].split(separator); if (dateSection.length < 3) { return null; } const d = new Date( parseInt(dateSection[yearIndex], 10), parseInt(dateSection[monthIndex], 10) - 1, parseInt(dateSection[dayIndex], 10), ); if (sections.length > 1) { const timeSection = sections[1].split(':'); d.setHours(timeSection.length > 0 ? parseInt(timeSection[0], 10) : 0); d.setMinutes(timeSection.length > 1 ? parseInt(timeSection[1], 10) : 0); d.setSeconds(timeSection.length > 2 ? parseInt(timeSection[2], 10) : 0); } return d; }; /** * Format date * * @param {Date} input The date object to format * @param {string} inputFormat The date format * The format can consist of the following tokens: * d Day of the month without leading zeros (1 through 31) * dd Day of the month with leading zeros (01 through 31) * m Month without leading zeros (1 through 12) * mm Month with leading zeros (01 through 12) * yy Last two digits of year (for example: 14) * yyyy Full four digits of year (for example: 2014) * h Hours without leading zeros (1 through 12) * hh Hours with leading zeros (01 through 12) * H Hours without leading zeros (0 through 23) * HH Hours with leading zeros (00 through 23) * M Minutes without leading zeros (0 through 59) * MM Minutes with leading zeros (00 through 59) * s Seconds without leading zeros (0 through 59) * ss Seconds with leading zeros (00 through 59) * @return {string} * @private */ const formatDate = (input: Date, inputFormat: string) => { const dateFormat = inputFormat .replace(/Y/g, 'y') .replace(/M/g, 'm') .replace(/D/g, 'd') .replace(/:m/g, ':M') .replace(/:mm/g, ':MM') .replace(/:S/, ':s') .replace(/:SS/, ':ss'); const d = input.getDate(); const dd = d < 10 ? `0${d}` : d; const m = input.getMonth() + 1; const mm = m < 10 ? `0${m}` : m; const yy = `${input.getFullYear()}`.substr(2); const yyyy = input.getFullYear(); const h = input.getHours() % 12 || 12; const hh = h < 10 ? `0${h}` : h; const H = input.getHours(); const HH = H < 10 ? `0${H}` : H; const M = input.getMinutes(); const MM = M < 10 ? `0${M}` : M; const s = input.getSeconds(); const ss = s < 10 ? `0${s}` : s; const replacer = { H: `${H}`, HH: `${HH}`, M: `${M}`, MM: `${MM}`, d: `${d}`, dd: `${dd}`, h: `${h}`, hh: `${hh}`, m: `${m}`, mm: `${mm}`, s: `${s}`, ss: `${ss}`, yy: `${yy}`, yyyy: `${yyyy}`, }; return dateFormat.replace(/d{1,4}|m{1,4}|yy(?:yy)?|([HhMs])\1?|"[^"]*"|'[^']*'/g, (match) => { return replacer[match] ? replacer[match] : match.slice(1, match.length - 1); }); }; return { validate(input: ValidateInput): ValidateResult { if (input.value === '') { return { meta: { date: null, }, valid: true, }; } const opts = Object.assign({}, { // Force the format to `YYYY-MM-DD` as the default browser behaviour when using type="date" attribute format: (input.element && input.element.getAttribute('type') === 'date') ? 'YYYY-MM-DD' : 'MM/DD/YYYY', message: '', }, input.options); const message = input.l10n ? input.l10n.date.default : opts.message; const invalidResult = { message: `${message}`, meta: { date: null, }, valid: false, }; const formats = opts.format.split(' '); const timeFormat = (formats.length > 1) ? formats[1] : null; const amOrPm = (formats.length > 2) ? formats[2] : null; const sections = input.value.split(' '); const dateSection = sections[0]; const timeSection = (sections.length > 1) ? sections[1] : null; if (formats.length !== sections.length) { return invalidResult; } // Determine the separator const separator = opts.separator || ((dateSection.indexOf('/') !== -1) ? '/' : ((dateSection.indexOf('-') !== -1) ? '-' : ((dateSection.indexOf('.') !== -1) ? '.' : '/'))); if (separator === null || dateSection.indexOf(separator) === -1) { return invalidResult; } // Determine the date const dateStr = dateSection.split(separator); const dateFormat = formats[0].split(separator); if (dateStr.length !== dateFormat.length) { return invalidResult; } const yearStr = dateStr[dateFormat.indexOf('YYYY')]; const monthStr = dateStr[dateFormat.indexOf('MM')]; const dayStr = dateStr[dateFormat.indexOf('DD')]; if (!/^\d+$/.test(yearStr) || !/^\d+$/.test(monthStr) || !/^\d+$/.test(dayStr) || yearStr.length > 4 || monthStr.length > 2 || dayStr.length > 2 ) { return invalidResult; } const year = parseInt(yearStr, 10); const month = parseInt(monthStr, 10); const day = parseInt(dayStr, 10); if (!isValidDate(year, month, day)) { return invalidResult; } // Determine the time const d = new Date(year, month - 1, day); if (timeFormat) { const hms = timeSection.split(':'); if (timeFormat.split(':').length !== hms.length) { return invalidResult; } const h = hms.length > 0 ? (hms[0].length <= 2 && /^\d+$/.test(hms[0]) ? parseInt(hms[0], 10) : -1) : 0; const m = hms.length > 1 ? (hms[1].length <= 2 && /^\d+$/.test(hms[1]) ? parseInt(hms[1], 10) : -1) : 0; const s = hms.length > 2 ? (hms[2].length <= 2 && /^\d+$/.test(hms[2]) ? parseInt(hms[2], 10) : -1) : 0; if (h === -1 || m === -1 || s === -1) { return invalidResult; } // Validate seconds if (s < 0 || s > 60) { return invalidResult; } // Validate hours if (h < 0 || h >= 24 || (amOrPm && h > 12)) { return invalidResult; } // Validate minutes if (m < 0 || m > 59) { return invalidResult; } d.setHours(h); d.setMinutes(m); d.setSeconds(s); } // Validate day, month, and year const minOption = (typeof opts.min === 'function') ? opts.min() : opts.min; const min = (minOption instanceof Date) ? (minOption as Date) : (minOption ? parseDate(minOption as string, dateFormat, separator) : d); const maxOption = (typeof opts.max === 'function') ? opts.max() : opts.max; const max = (maxOption instanceof Date) ? (maxOption as Date) : (maxOption ? parseDate(maxOption as string, dateFormat, separator) : d); // In order to avoid displaying a date string like "Mon Dec 08 2014 19:14:12 GMT+0000 (WET)" const minOptionStr = (minOption instanceof Date) ? formatDate(min, opts.format) : (minOption as string); const maxOptionStr = (maxOption instanceof Date) ? formatDate(max, opts.format) : (maxOption as string); switch (true) { case (!!minOptionStr && !maxOptionStr): return { message: format(input.l10n ? input.l10n.date.min : message, minOptionStr), meta: { date: d, }, valid: d.getTime() >= min.getTime(), }; case (!!maxOptionStr && !minOptionStr): return { message: format(input.l10n ? input.l10n.date.max : message, maxOptionStr), meta: { date: d, }, valid: d.getTime() <= max.getTime(), }; case (!!maxOptionStr && !!minOptionStr): return { message: format(input.l10n ? input.l10n.date.range : message, [minOptionStr, maxOptionStr]), meta: { date: d, }, valid: d.getTime() <= max.getTime() && d.getTime() >= min.getTime(), }; default: return { message: `${message}`, meta: { date: d, }, valid: true, }; } }, }; }