UNPKG

@gravityforms/components

Version:

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

235 lines (215 loc) 8.01 kB
import { parse, format, templateParser, parseDigit, templateFormatter } from 'input-format'; import { AsYouType } from 'libphonenumber-js'; const ANDROID_USER_AGENT_REG_EXP = /Android/i; /** * @function isAndroid * @description Checks if the user agent is Android. * Copied from the original function `isAndroid` from `input-format` package. * The original function is not exported, so it was copied here. * * @since 5.5.0 * * @return {boolean} Whether the user agent is Android. */ export const isAndroid = () => { // `navigator` is not defined when running mocha tests. if ( typeof navigator !== 'undefined' ) { return ANDROID_USER_AGENT_REG_EXP.test( navigator.userAgent ); } }; /** * @function getCaretPosition * @description Gets the caret position. * Copied from the original function `getCaretPosition` from `input-format` package. * The original function is not exported, so it was copied here. * * @since 5.5.0 * * @param {HTMLElement} element The element to get the caret position from. * * @return {number} The caret position. */ export const getCaretPosition = ( element ) => { return element.selectionStart; }; /** * @function setCaretPosition * @description Sets the caret position. * Copied from the original function `setCaretPosition` from `input-format` package. * The original function is not exported, so it was copied here. * * @since 5.5.0 * * @param {HTMLElement} element The element to set the caret position on. * @param {number} caretPosition The caret position to set. */ export const setCaretPosition = ( element, caretPosition ) => { // Sanity check if ( caretPosition === undefined ) { return; } // Set caret position. // There has been an issue with caret positioning on Android devices. // https://github.com/catamphetamine/input-format/issues/2 // I was revisiting this issue and looked for similar issues in other libraries. // For example, there's [`text-mask`](https://github.com/text-mask/text-mask) library. // They've had exactly the same issue when the caret seemingly refused to be repositioned programmatically. // The symptoms were the same: whenever the caret passed through a non-digit character of a mask (a whitespace, a bracket, a dash, etc), it looked as if it placed itself one character before its correct position. // https://github.com/text-mask/text-mask/issues/300 // They seem to have found a basic fix for it: calling `input.setSelectionRange()` in a timeout rather than instantly for Android devices. // https://github.com/text-mask/text-mask/pull/400/files // I've implemented the same workaround here. if ( isAndroid() ) { setTimeout( () => element.setSelectionRange( caretPosition, caretPosition ), 0 ); } else { element.setSelectionRange( caretPosition, caretPosition ); } }; /** * @function edit * @description Edits text based on the operation. * Copied from the original function `edit` from `input-format` package. * The original function is not exported, so it was copied here. * * @param {string} value The value to edit. * @param {number} caret The caret position. * @param {string} operation The operation to perform, one of `Backspace` or `Delete`. * * @return {object} The edited value and the caret position. */ export const edit = ( value, caret, operation ) => { // Edits text `value` (if `operation` is passed) and repositions the `caret` if needed. // // Example: // // value - '88005553535' // caret - 2 // starting from 0; is positioned before the first zero // operation - 'Backspace' // // Returns // { // value: '8005553535' // caret: 1 // } // // Currently supports just 'Delete' and 'Backspace' operations // switch ( operation ) { case 'Backspace': // If there exists the previous character, // then erase it and reposition the caret. if ( caret > 0 ) { // Remove the previous character value = value.slice( 0, caret - 1 ) + value.slice( caret ); // Position the caret where the previous (erased) character was caret--; } break; case 'Delete': // Remove current digit (if any) value = value.slice( 0, caret ) + value.slice( caret + 1 ); break; } return { value, caret }; }; /** * @function parseDigitWithPlus * @description Parses a digit with a plus sign. * * @since 5.5.0 * * @param {boolean} international Whether the phone number is in international format. * * @return {Function} The function to parse a digit with a plus sign. */ export const parseDigitWithPlus = ( international ) => ( character, value ) => { // Leading plus is allowed if ( international && character === '+' && ! value ) { return character; } // Digits are allowed return parseDigit( character ); }; /** * @function getFormattedInputText * @description Gets the formatted input text. * Modified from the original function `formatInputText` from `input-format` package. * * @since 5.5.0 * * @param {string} inputValue The input value. * @param {number} caretPosition The caret position. * @param {string} country The country code. * @param {boolean} international Whether the phone number is in international format. * @param {string|undefined} operation The operation to perform, one of `Backspace` or `Delete`. * * @return {object} The formatted input text, caret position, and the AsYouType instance. */ export const getFormattedInputText = ( inputValue, caretPosition, country, international, operation, ) => { const asYouType = new AsYouType( country ); asYouType.input( inputValue ); const parser = templateParser( asYouType.getTemplate(), parseDigitWithPlus( international ) ); // Parse input value. // Get the `value` and `caret` position. let { value, caret } = parse( inputValue, caretPosition, parser ); // If a user performed an operation ("Backspace", "Delete") // then apply that operation and get the new `value` and `caret` position. if ( operation ) { const newValueAndCaret = edit( value, caret, operation ); value = newValueAndCaret.value; caret = newValueAndCaret.caret; } asYouType.reset(); asYouType.input( value ); const formatter = templateFormatter( asYouType.getTemplate(), 'x', true ); // Format the `value` and reposition the caret accordingly. return { asYouType, formatted: format( value, caret, formatter ), }; }; /** * @function getTextAfterEraseSelection * @description Gets the text after erasing the selection. * Modified from the original function `eraseSelection` from `input-format` package. * The original function is not exported, so it was copied here. * * @since 5.5.0 * * @param {string} text The text to erase the selection from. * @param {object} selection The selection to erase. The selection is an object with `start` and `end` properties. * * @return {string} The text after erasing the selection. */ export const getTextAfterEraseSelection = ( text, selection ) => { const start = Math.min( selection.start, selection.end ); const end = Math.max( selection.start, selection.end ); return text.slice( 0, start ) + text.slice( end ); }; /** * @function getSelection * @description Gets the selection. * Copied from the original function `getSelection` from `input-format` package. * The original function is not exported, so it was copied here. * * @since 5.5.0 * * @param {HTMLElement} input The input element. * * @return {object|undefined} The selection object with `start` and `end` properties. */ export const getSelection = ( input ) => { // If no selection, return nothing if ( input.selectionStart === input.selectionEnd ) { return; } const start = Math.min( input.selectionStart, input.selectionEnd ); const end = Math.max( input.selectionStart, input.selectionEnd ); return { start, end }; };