UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

297 lines (271 loc) 8.6 kB
/** * @constant DEFAULT_DATE_FORMAT * @description Default date format used by the DatePicker utilities. * * @since 5.8.6 * * @type {string} */ const DEFAULT_DATE_FORMAT = 'MM/dd/yyyy'; const TOKEN_REGEX = /(Y+|y+|M+|m+|D+|d+)/g; /** * @function isValidDate * @description Determine whether a value is a valid Date instance. * * @since 5.8.6 * * @param {*} value The value to test. * * @return {boolean} True when the value is a valid date. */ const isValidDate = ( value ) => value instanceof Date && ! Number.isNaN( value.getTime() ); /** * @function replaceTokens * @description Replace all token occurrences in a pattern with provided values. * * @since 5.8.6 * * @param {string} pattern The format pattern to transform. * @param {Array} replacements Array of replacement definitions `{ tokens, value }`. * * @return {string} The formatted string with tokens substituted. */ const replaceTokens = ( pattern, replacements ) => ( replacements.reduce( ( formatted, { tokens, value } ) => ( tokens.reduce( ( acc, token ) => acc.replace( new RegExp( token, 'g' ), value ), formatted ) ), pattern ) ); /** * @function getSegmentsFromFormat * @description Tokenize a format string into literal and token segments. * * @since 5.8.6 * * @param {string} format The format template to parse. * * @return {Array} Ordered list of segment descriptors. */ const getSegmentsFromFormat = ( format ) => { const segments = []; let lastIndex = 0; let match; while ( ( match = TOKEN_REGEX.exec( format ) ) ) { if ( match.index > lastIndex ) { segments.push( { type: 'literal', value: format.slice( lastIndex, match.index ), } ); } segments.push( { type: 'token', value: match[ 0 ], char: match[ 0 ][ 0 ], length: match[ 0 ].length, } ); lastIndex = match.index + match[ 0 ].length; } if ( lastIndex < format.length ) { segments.push( { type: 'literal', value: format.slice( lastIndex ), } ); } return segments; }; /** * @function convertTwoDigitYear * @description Convert a two-digit year into a four-digit year using a mid-century pivot. * * @since 5.8.6 * * @param {number} value The two-digit year. * * @return {number} The expanded four-digit year. */ const convertTwoDigitYear = ( value ) => { const pivot = 50; return value >= 0 && value < pivot ? 2000 + value : 1900 + value; }; /** * @function formatDateWithPattern * @description Format a date according to a tokenized pattern. * * @since 5.8.6 * * @param {string} pattern The desired output pattern. * @param {Date} date The date to format. * * @return {string} The formatted date string. */ export const formatDateWithPattern = ( pattern, date ) => { if ( ! isValidDate( date ) ) { return ''; } const effectivePattern = pattern || DEFAULT_DATE_FORMAT; const yearFull = String( date.getFullYear() ); const replacements = [ { tokens: [ 'YYYY', 'yyyy' ], value: yearFull }, { tokens: [ 'YY', 'yy' ], value: yearFull.slice( -2 ) }, { tokens: [ 'MM', 'mm' ], value: String( date.getMonth() + 1 ).padStart( 2, '0' ) }, { tokens: [ 'DD', 'dd' ], value: String( date.getDate() ).padStart( 2, '0' ) }, ]; return replaceTokens( effectivePattern, replacements ); }; /** * @function formatValueWithFormat * @description Normalize any supported value to a formatted string using a pattern or formatter function. * * @since 5.8.6 * * @param {string|Function} format The format string or formatter function. * @param {*} value The value to format (Date, range, or primitive). * * @return {string} The formatted representation. */ export const formatValueWithFormat = ( format, value ) => { if ( value === null || typeof value === 'undefined' ) { return ''; } if ( typeof format === 'function' ) { const formatted = format( value ); return formatted === null || typeof formatted === 'undefined' ? '' : String( formatted ); } if ( Array.isArray( value ) ) { return value .map( ( item ) => formatValueWithFormat( format, item ) ) .filter( Boolean ) .join( ' - ' ); } if ( isValidDate( value ) ) { return formatDateWithPattern( format, value ); } return String( value ); }; /** * @function maskDateInputValue * @description Apply a format-aware mask to user-entered date text. * * @since 5.8.6 * * @param {string|Function} format The active date format (string tokens only). * @param {string} rawValue The raw user input value. * @param {?number} caretDigitIndex Optional number of digits before the caret. When provided, returns an object containing `{ value, caretPosition }`. * * @return {string|object} The masked input string or `{ value, caretPosition }` when caret tracking is requested. */ export const maskDateInputValue = ( format, rawValue, caretDigitIndex = null ) => { if ( typeof format !== 'string' ) { return caretDigitIndex === null ? rawValue : { value: rawValue, caretPosition: rawValue.length }; } const segments = getSegmentsFromFormat( format ); const digitString = String( rawValue || '' ).replace( /\D/g, '' ); let digitIndex = 0; let result = ''; let previousTokenComplete = false; const trackCaret = typeof caretDigitIndex === 'number' && caretDigitIndex >= 0; let caretPosition = trackCaret && caretDigitIndex === 0 ? 0 : null; for ( const segment of segments ) { if ( segment.type === 'literal' ) { if ( result && previousTokenComplete ) { result += segment.value; } continue; } const requiredLength = segment.length; const availableDigits = digitString.length - digitIndex; if ( availableDigits <= 0 ) { previousTokenComplete = false; break; } const takeLength = Math.min( requiredLength, availableDigits ); const segmentDigits = digitString.slice( digitIndex, digitIndex + takeLength ); const digitsPriorToSegment = digitIndex; result += segmentDigits; if ( trackCaret && caretPosition === null ) { if ( caretDigitIndex <= digitsPriorToSegment ) { caretPosition = result.length - segmentDigits.length; } else if ( caretDigitIndex <= digitsPriorToSegment + segmentDigits.length ) { const withinSegment = Math.max( caretDigitIndex - digitsPriorToSegment, 0 ); caretPosition = ( result.length - segmentDigits.length ) + withinSegment; } } digitIndex += takeLength; previousTokenComplete = takeLength === requiredLength; if ( takeLength < requiredLength ) { break; } } if ( trackCaret && caretPosition === null ) { caretPosition = result.length; } return trackCaret ? { value: result, caretPosition } : result; }; /** * @function parseDateFromMaskedValue * @description Parse a masked date string into a Date instance when valid. * * @since 5.8.6 * * @param {string|Function} format The format string describing the mask. * @param {string} value The masked input value. * * @return {?Date} Parsed date or null when invalid/incomplete. */ export const parseDateFromMaskedValue = ( format, value ) => { if ( typeof format !== 'string' ) { return null; } const segments = getSegmentsFromFormat( format ); let cursor = 0; let year; let month; let day; for ( const segment of segments ) { if ( segment.type === 'literal' ) { const literal = segment.value; if ( value.length < cursor + literal.length ) { return null; } if ( value.slice( cursor, cursor + literal.length ) !== literal ) { return null; } cursor += literal.length; continue; } const expectedLength = segment.length; if ( value.length < cursor + expectedLength ) { return null; } const numericString = value.slice( cursor, cursor + expectedLength ); if ( numericString.length < expectedLength || /\D/.test( numericString ) ) { return null; } const numericValue = parseInt( numericString, 10 ); const tokenChar = segment.char.toUpperCase(); if ( tokenChar === 'Y' ) { year = expectedLength <= 2 ? convertTwoDigitYear( numericValue ) : numericValue; } else if ( tokenChar === 'M' ) { month = numericValue; } else if ( tokenChar === 'D' ) { day = numericValue; } cursor += expectedLength; } if ( cursor !== value.length ) { return null; } if ( typeof year !== 'number' || typeof month !== 'number' || typeof day !== 'number' ) { return null; } const date = new Date( year, month - 1, day ); if ( Number.isNaN( date.getTime() ) ) { return null; } if ( date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day ) { return null; } return date; }; export { DEFAULT_DATE_FORMAT };