@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
235 lines (215 loc) • 8.01 kB
JavaScript
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 };
};