From 16bf6e4949e42b9666855cce177aea0d1fc60310 Mon Sep 17 00:00:00 2001 From: Victor Trinh Date: Mon, 11 Nov 2024 10:25:16 -0500 Subject: [PATCH 1/2] Copy function into repo --- packages/components/package.json | 3 +- .../src/date-input/src/useMaskedInput.ts | 4 +- .../src/utils/adjustCaretPosition.js | 269 ++++++++++++++++++ .../src/date-input/src/utils/conformToMask.js | 249 ++++++++++++++++ .../src/date-input/src/utils/constants.js | 2 + .../src/utils/createTextMarkInputElement.js | 199 +++++++++++++ .../src/date-input/src/utils/utilities.js | 54 ++++ pnpm-lock.yaml | 157 ++++++---- 8 files changed, 879 insertions(+), 58 deletions(-) create mode 100644 packages/components/src/date-input/src/utils/adjustCaretPosition.js create mode 100644 packages/components/src/date-input/src/utils/conformToMask.js create mode 100644 packages/components/src/date-input/src/utils/constants.js create mode 100644 packages/components/src/date-input/src/utils/createTextMarkInputElement.js create mode 100644 packages/components/src/date-input/src/utils/utilities.js diff --git a/packages/components/package.json b/packages/components/package.json index 16ba21cb3..673891157 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -48,7 +48,6 @@ "@hopper-ui/icons": "^2.8.2", "@popperjs/core": "2.11.8", "react-is": "18.3.1", - "text-mask-core": "5.1.2", "use-debounce": "10.0.4" }, "devDependencies": { @@ -82,8 +81,8 @@ "react-aria-components": "^1.2.1", "react-test-renderer": "18.3.1", "resize-observer-polyfill": "1.5.1", - "ts-node": "10.9.2", "ts-jest": "29.2.5", + "ts-node": "10.9.2", "tsup": "8.3.0", "typescript": "5.4.5" }, diff --git a/packages/components/src/date-input/src/useMaskedInput.ts b/packages/components/src/date-input/src/useMaskedInput.ts index b8280ad80..1864c0f5d 100644 --- a/packages/components/src/date-input/src/useMaskedInput.ts +++ b/packages/components/src/date-input/src/useMaskedInput.ts @@ -1,4 +1,4 @@ -import textMaskCore from "text-mask-core"; +import createTextMaskInputElement from "./utils/createTextMarkInputElement.js"; import { isNil } from "../../shared/index.ts"; import { useCallback, useEffect, useRef } from "react"; @@ -15,7 +15,7 @@ export function useMaskedInput({ useEffect(() => { if (!isNil(inputElement)) { - maskRef.current = textMaskCore.createTextMaskInputElement({ + maskRef.current = createTextMaskInputElement({ guide: false, inputElement, mask diff --git a/packages/components/src/date-input/src/utils/adjustCaretPosition.js b/packages/components/src/date-input/src/utils/adjustCaretPosition.js new file mode 100644 index 000000000..444eb1696 --- /dev/null +++ b/packages/components/src/date-input/src/utils/adjustCaretPosition.js @@ -0,0 +1,269 @@ +const defaultArray = []; +const emptyString = ""; + +export default function adjustCaretPosition({ + previousConformedValue = emptyString, + previousPlaceholder = emptyString, + currentCaretPosition = 0, + conformedValue, + rawValue, + placeholderChar, + placeholder, + indexesOfPipedChars = defaultArray, + caretTrapIndexes = defaultArray +}) { + if (currentCaretPosition === 0 || !rawValue.length) { return 0; } + + // Store lengths for faster performance? + const rawValueLength = rawValue.length; + const previousConformedValueLength = previousConformedValue.length; + const placeholderLength = placeholder.length; + const conformedValueLength = conformedValue.length; + + // This tells us how long the edit is. If user modified input from `(2__)` to `(243__)`, + // we know the user in this instance pasted two characters + const editLength = rawValueLength - previousConformedValueLength; + + // If the edit length is positive, that means the user is adding characters, not deleting. + const isAddition = editLength > 0; + + // This is the first raw value the user entered that needs to be conformed to mask + const isFirstRawValue = previousConformedValueLength === 0; + + // A partial multi-character edit happens when the user makes a partial selection in their + // input and edits that selection. That is going from `(123) 432-4348` to `() 432-4348` by + // selecting the first 3 digits and pressing backspace. + // + // Such cases can also happen when the user presses the backspace while holding down the ALT + // key. + const isPartialMultiCharEdit = editLength > 1 && !isAddition && !isFirstRawValue; + + // This algorithm doesn't support all cases of multi-character edits, so we just return + // the current caret position. + // + // This works fine for most cases. + if (isPartialMultiCharEdit) { return currentCaretPosition; } + + // For a mask like (111), if the `previousConformedValue` is (1__) and user attempts to enter + // `f` so the `rawValue` becomes (1f__), the new `conformedValue` would be (1__), which is the + // same as the original `previousConformedValue`. We handle this case differently for caret + // positioning. + const possiblyHasRejectedChar = isAddition && ( + previousConformedValue === conformedValue || + conformedValue === placeholder + ); + + let startingSearchIndex = 0; + let trackRightCharacter; + let targetChar; + + if (possiblyHasRejectedChar) { + startingSearchIndex = currentCaretPosition - editLength; + } else { + // At this point in the algorithm, we want to know where the caret is right before the raw input + // has been conformed, and then see if we can find that same spot in the conformed input. + // + // We do that by seeing what character lies immediately before the caret, and then look for that + // same character in the conformed input and place the caret there. + + // First, we need to normalize the inputs so that letter capitalization between raw input and + // conformed input wouldn't matter. + const normalizedConformedValue = conformedValue.toLowerCase(); + const normalizedRawValue = rawValue.toLowerCase(); + + // Then we take all characters that come before where the caret currently is. + const leftHalfChars = normalizedRawValue.substr(0, currentCaretPosition).split(emptyString); + + // Now we find all the characters in the left half that exist in the conformed input + // This step ensures that we don't look for a character that was filtered out or rejected by `conformToMask`. + const intersection = leftHalfChars.filter(char => normalizedConformedValue.indexOf(char) !== -1); + + // The last character in the intersection is the character we want to look for in the conformed + // value and the one we want to adjust the caret close to + targetChar = intersection[intersection.length - 1]; + + // Calculate the number of mask characters in the previous placeholder + // from the start of the string up to the place where the caret is + const previousLeftMaskChars = previousPlaceholder + .substr(0, intersection.length) + .split(emptyString) + .filter(char => char !== placeholderChar) + .length; + + // Calculate the number of mask characters in the current placeholder + // from the start of the string up to the place where the caret is + const leftMaskChars = placeholder + .substr(0, intersection.length) + .split(emptyString) + .filter(char => char !== placeholderChar) + .length; + + // Has the number of mask characters up to the caret changed? + const masklengthChanged = leftMaskChars !== previousLeftMaskChars; + + // Detect if `targetChar` is a mask character and has moved to the left + const targetIsMaskMovingLeft = ( + previousPlaceholder[intersection.length - 1] !== undefined && + placeholder[intersection.length - 2] !== undefined && + previousPlaceholder[intersection.length - 1] !== placeholderChar && + previousPlaceholder[intersection.length - 1] !== placeholder[intersection.length - 1] && + previousPlaceholder[intersection.length - 1] === placeholder[intersection.length - 2] + ); + + // If deleting and the `targetChar` `is a mask character and `masklengthChanged` is true + // or the mask is moving to the left, we can't use the selected `targetChar` any longer + // if we are not at the end of the string. + // In this case, change tracking strategy and track the character to the right of the caret. + if ( + !isAddition && + (masklengthChanged || targetIsMaskMovingLeft) && + previousLeftMaskChars > 0 && + placeholder.indexOf(targetChar) > -1 && + rawValue[currentCaretPosition] !== undefined + ) { + trackRightCharacter = true; + targetChar = rawValue[currentCaretPosition]; + } + + // It is possible that `targetChar` will appear multiple times in the conformed value. + // We need to know not to select a character that looks like our target character from the placeholder or + // the piped characters, so we inspect the piped characters and the placeholder to see if they contain + // characters that match our target character. + + // If the `conformedValue` got piped, we need to know which characters were piped in so that when we look for + // our `targetChar`, we don't select a piped char by mistake + const pipedChars = indexesOfPipedChars.map(index => normalizedConformedValue[index]); + + // We need to know how many times the `targetChar` occurs in the piped characters. + const countTargetCharInPipedChars = pipedChars.filter(char => char === targetChar).length; + + // We need to know how many times it occurs in the intersection + const countTargetCharInIntersection = intersection.filter(char => char === targetChar).length; + + // We need to know if the placeholder contains characters that look like + // our `targetChar`, so we don't select one of those by mistake. + const countTargetCharInPlaceholder = placeholder + .substr(0, placeholder.indexOf(placeholderChar)) + .split(emptyString) + .filter((char, index) => ( + // Check if `char` is the same as our `targetChar`, so we account for it + char === targetChar && + + // but also make sure that both the `rawValue` and placeholder don't have the same character at the same + // index because if they are equal, that means we are already counting those characters in + // `countTargetCharInIntersection` + rawValue[index] !== char + )) + .length; + + // The number of times we need to see occurrences of the `targetChar` before we know it is the one we're looking + // for is: + const requiredNumberOfMatches = ( + countTargetCharInPlaceholder + + countTargetCharInIntersection + + countTargetCharInPipedChars + + // The character to the right of the caret isn't included in `intersection` + // so add one if we are tracking the character to the right + (trackRightCharacter ? 1 : 0) + ); + + // Now we start looking for the location of the `targetChar`. + // We keep looping forward and store the index in every iteration. Once we have encountered + // enough occurrences of the target character, we break out of the loop + // If are searching for the second `1` in `1214`, `startingSearchIndex` will point at `4`. + let numberOfEncounteredMatches = 0; + for (let i = 0; i < conformedValueLength; i++) { + const conformedValueChar = normalizedConformedValue[i]; + + startingSearchIndex = i + 1; + + if (conformedValueChar === targetChar) { + numberOfEncounteredMatches++; + } + + if (numberOfEncounteredMatches >= requiredNumberOfMatches) { + break; + } + } + } + + // At this point, if we simply return `startingSearchIndex` as the adjusted caret position, + // most cases would be handled. However, we want to fast forward or rewind the caret to the + // closest placeholder character if it happens to be in a non-editable spot. That's what the next + // logic is for. + + // In case of addition, we fast forward. + if (isAddition) { + // We want to remember the last placeholder character encountered so that if the mask + // contains more characters after the last placeholder character, we don't forward the caret + // that far to the right. Instead, we stop it at the last encountered placeholder character. + let lastPlaceholderChar = startingSearchIndex; + + for (let i = startingSearchIndex; i <= placeholderLength; i++) { + if (placeholder[i] === placeholderChar) { + lastPlaceholderChar = i; + } + + if ( + // If we're adding, we can position the caret at the next placeholder character. + placeholder[i] === placeholderChar || + + // If a caret trap was set by a mask function, we need to stop at the trap. + caretTrapIndexes.indexOf(i) !== -1 || + + // This is the end of the placeholder. We cannot move any further. Let's put the caret there. + i === placeholderLength + ) { + return lastPlaceholderChar; + } + } + } else { + // In case of deletion, we rewind. + if (trackRightCharacter) { + // Searching for the character that was to the right of the caret + // We start at `startingSearchIndex` - 1 because it includes one character extra to the right + for (let i = startingSearchIndex - 1; i >= 0; i--) { + // If tracking the character to the right of the cursor, we move to the left until + // we found the character and then place the caret right before it + + if ( + // `targetChar` should be in `conformedValue`, since it was in `rawValue`, just + // to the right of the caret + conformedValue[i] === targetChar || + + // If a caret trap was set by a mask function, we need to stop at the trap. + caretTrapIndexes.indexOf(i) !== -1 || + + // This is the beginning of the placeholder. We cannot move any further. + // Let's put the caret there. + i === 0 + ) { + return i; + } + } + } else { + // Searching for the first placeholder or caret trap to the left + + for (let i = startingSearchIndex; i >= 0; i--) { + // If we're deleting, we stop the caret right before the placeholder character. + // For example, for mask `(111) 11`, current conformed input `(456) 86`. If user + // modifies input to `(456 86`. That is, they deleted the `)`, we place the caret + // right after the first `6` + + if ( + // If we're deleting, we can position the caret right before the placeholder character + placeholder[i - 1] === placeholderChar || + + // If a caret trap was set by a mask function, we need to stop at the trap. + caretTrapIndexes.indexOf(i) !== -1 || + + // This is the beginning of the placeholder. We cannot move any further. + // Let's put the caret there. + i === 0 + ) { + return i; + } + } + } + } +} diff --git a/packages/components/src/date-input/src/utils/conformToMask.js b/packages/components/src/date-input/src/utils/conformToMask.js new file mode 100644 index 000000000..9b8bc631a --- /dev/null +++ b/packages/components/src/date-input/src/utils/conformToMask.js @@ -0,0 +1,249 @@ +import { convertMaskToPlaceholder, isArray, processCaretTraps } from "./utilities"; +import { placeholderChar as defaultPlaceholderChar, strFunction } from "./constants"; + +const emptyArray = []; +const emptyString = ""; + +export default function conformToMask(rawValue = emptyString, mask = emptyArray, config = {}) { + if (!isArray(mask)) { + // If someone passes a function as the mask property, we should call the + // function to get the mask array - Normally this is handled by the + // `createTextMaskInputElement:update` function - this allows mask functions + // to be used directly with `conformToMask` + if (typeof mask === strFunction) { + // call the mask function to get the mask array + mask = mask(rawValue, config); + + // mask functions can setup caret traps to have some control over how the caret moves. We need to process + // the mask for any caret traps. `processCaretTraps` will remove the caret traps from the mask + mask = processCaretTraps(mask).maskWithoutCaretTraps; + } else { + throw new Error( + "Text-mask:conformToMask; The mask property must be an array." + ); + } + } + + // These configurations tell us how to conform the mask + const { + guide = true, + previousConformedValue = emptyString, + placeholderChar = defaultPlaceholderChar, + placeholder = convertMaskToPlaceholder(mask, placeholderChar), + currentCaretPosition, + keepCharPositions + } = config; + + // The configs below indicate that the user wants the algorithm to work in *no guide* mode + const suppressGuide = guide === false && previousConformedValue !== undefined; + + // Calculate lengths once for performance + const rawValueLength = rawValue.length; + const previousConformedValueLength = previousConformedValue.length; + const placeholderLength = placeholder.length; + const maskLength = mask.length; + + // This tells us the number of edited characters and the direction in which they were edited (+/-) + const editDistance = rawValueLength - previousConformedValueLength; + + // In *no guide* mode, we need to know if the user is trying to add a character or not + const isAddition = editDistance > 0; + + // Tells us the index of the first change. For (438) 394-4938 to (38) 394-4938, that would be 1 + const indexOfFirstChange = currentCaretPosition + (isAddition ? -editDistance : 0); + + // We're also gonna need the index of last change, which we can derive as follows... + const indexOfLastChange = indexOfFirstChange + Math.abs(editDistance); + + // If `conformToMask` is configured to keep character positions, that is, for mask 111, previous value + // _2_ and raw value 3_2_, the new conformed value should be 32_, not 3_2 (default behavior). That's in the case of + // addition. And in the case of deletion, previous value _23, raw value _3, the new conformed string should be + // __3, not _3_ (default behavior) + // + // The next block of logic handles keeping character positions for the case of deletion. (Keeping + // character positions for the case of addition is further down since it is handled differently.) + // To do this, we want to compensate for all characters that were deleted + if (keepCharPositions === true && !isAddition) { + // We will be storing the new placeholder characters in this variable. + let compensatingPlaceholderChars = emptyString; + + // For every character that was deleted from a placeholder position, we add a placeholder char + for (let i = indexOfFirstChange; i < indexOfLastChange; i++) { + if (placeholder[i] === placeholderChar) { + compensatingPlaceholderChars += placeholderChar; + } + } + + // Now we trick our algorithm by modifying the raw value to make it contain additional placeholder characters + // That way when the we start laying the characters again on the mask, it will keep the non-deleted characters + // in their positions. + rawValue = ( + rawValue.slice(0, indexOfFirstChange) + + compensatingPlaceholderChars + + rawValue.slice(indexOfFirstChange, rawValueLength) + ); + } + + // Convert `rawValue` string to an array, and mark characters based on whether they are newly added or have + // existed in the previous conformed value. Identifying new and old characters is needed for `conformToMask` + // to work if it is configured to keep character positions. + const rawValueArr = rawValue + .split(emptyString) + .map((char, i) => ({ char, isNew: i >= indexOfFirstChange && i < indexOfLastChange })); + + // The loop below removes masking characters from user input. For example, for mask + // `00 (111)`, the placeholder would be `00 (___)`. If user input is `00 (234)`, the loop below + // would remove all characters but `234` from the `rawValueArr`. The rest of the algorithm + // then would lay `234` on top of the available placeholder positions in the mask. + for (let i = rawValueLength - 1; i >= 0; i--) { + const { char } = rawValueArr[i]; + + if (char !== placeholderChar) { + const shouldOffset = i >= indexOfFirstChange && previousConformedValueLength === maskLength; + + if (char === placeholder[(shouldOffset) ? i - editDistance : i]) { + rawValueArr.splice(i, 1); + } + } + } + + // This is the variable that we will be filling with characters as we figure them out + // in the algorithm below + let conformedValue = emptyString; + let someCharsRejected = false; + + // Ok, so first we loop through the placeholder looking for placeholder characters to fill up. + placeholderLoop: for (let i = 0; i < placeholderLength; i++) { + const charInPlaceholder = placeholder[i]; + + // We see one. Let's find out what we can put in it. + if (charInPlaceholder === placeholderChar) { + // But before that, do we actually have any user characters that need a place? + if (rawValueArr.length > 0) { + // We will keep chipping away at user input until either we run out of characters + // or we find at least one character that we can map. + while (rawValueArr.length > 0) { + // Let's retrieve the first user character in the queue of characters we have left + const { char: rawValueChar, isNew } = rawValueArr.shift(); + + // If the character we got from the user input is a placeholder character (which happens + // regularly because user input could be something like (540) 90_-____, which includes + // a bunch of `_` which are placeholder characters) and we are not in *no guide* mode, + // then we map this placeholder character to the current spot in the placeholder + if (rawValueChar === placeholderChar && suppressGuide !== true) { + conformedValue += placeholderChar; + + // And we go to find the next placeholder character that needs filling + continue placeholderLoop; + + // Else if, the character we got from the user input is not a placeholder, let's see + // if the current position in the mask can accept it. + } else if (mask[i].test(rawValueChar)) { + // we map the character differently based on whether we are keeping character positions or not. + // If any of the conditions below are met, we simply map the raw value character to the + // placeholder position. + if ( + keepCharPositions !== true || + isNew === false || + previousConformedValue === emptyString || + guide === false || + !isAddition + ) { + conformedValue += rawValueChar; + } else { + // We enter this block of code if we are trying to keep character positions and none of the conditions + // above is met. In this case, we need to see if there's an available spot for the raw value character + // to be mapped to. If we couldn't find a spot, we will discard the character. + // + // For example, for mask `1111`, previous conformed value `_2__`, raw value `942_2__`. We can map the + // `9`, to the first available placeholder position, but then, there are no more spots available for the + // `4` and `2`. So, we discard them and end up with a conformed value of `92__`. + const rawValueArrLength = rawValueArr.length; + let indexOfNextAvailablePlaceholderChar = null; + + // Let's loop through the remaining raw value characters. We are looking for either a suitable spot, ie, + // a placeholder character or a non-suitable spot, ie, a non-placeholder character that is not new. + // If we see a suitable spot first, we store its position and exit the loop. If we see a non-suitable + // spot first, we exit the loop and our `indexOfNextAvailablePlaceholderChar` will stay as `null`. + for (let i = 0; i < rawValueArrLength; i++) { + const charData = rawValueArr[i]; + + if (charData.char !== placeholderChar && charData.isNew === false) { + break; + } + + if (charData.char === placeholderChar) { + indexOfNextAvailablePlaceholderChar = i; + break; + } + } + + // If `indexOfNextAvailablePlaceholderChar` is not `null`, that means the character is not blocked. + // We can map it. And to keep the character positions, we remove the placeholder character + // from the remaining characters + if (indexOfNextAvailablePlaceholderChar !== null) { + conformedValue += rawValueChar; + rawValueArr.splice(indexOfNextAvailablePlaceholderChar, 1); + + // If `indexOfNextAvailablePlaceholderChar` is `null`, that means the character is blocked. We have to + // discard it. + } else { + i--; + } + } + + // Since we've mapped this placeholder position. We move on to the next one. + continue placeholderLoop; + } else { + someCharsRejected = true; + } + } + } + + // We reach this point when we've mapped all the user input characters to placeholder + // positions in the mask. In *guide* mode, we append the left over characters in the + // placeholder to the `conformedString`, but in *no guide* mode, we don't wanna do that. + // + // That is, for mask `(111)` and user input `2`, we want to return `(2`, not `(2__)`. + if (suppressGuide === false) { + conformedValue += placeholder.substr(i, placeholderLength); + } + + // And we break + break; + + // Else, the charInPlaceholder is not a placeholderChar. That is, we cannot fill it + // with user input. So we just map it to the final output + } else { + conformedValue += charInPlaceholder; + } + } + + // The following logic is needed to deal with the case of deletion in *no guide* mode. + // + // Consider the silly mask `(111) /// 1`. What if user tries to delete the last placeholder + // position? Something like `(589) /// `. We want to conform that to `(589`. Not `(589) /// `. + // That's why the logic below finds the last filled placeholder character, and removes everything + // from that point on. + if (suppressGuide && isAddition === false) { + let indexOfLastFilledPlaceholderChar = null; + + // Find the last filled placeholder position and substring from there + for (let i = 0; i < conformedValue.length; i++) { + if (placeholder[i] === placeholderChar) { + indexOfLastFilledPlaceholderChar = i; + } + } + + if (indexOfLastFilledPlaceholderChar !== null) { + // We substring from the beginning until the position after the last filled placeholder char. + conformedValue = conformedValue.substr(0, indexOfLastFilledPlaceholderChar + 1); + } else { + // If we couldn't find `indexOfLastFilledPlaceholderChar` that means the user deleted + // the first character in the mask. So we return an empty string. + conformedValue = emptyString; + } + } + + return { conformedValue, meta: { someCharsRejected } }; +} diff --git a/packages/components/src/date-input/src/utils/constants.js b/packages/components/src/date-input/src/utils/constants.js new file mode 100644 index 000000000..5879a0403 --- /dev/null +++ b/packages/components/src/date-input/src/utils/constants.js @@ -0,0 +1,2 @@ +export const placeholderChar = "_"; +export const strFunction = "function"; diff --git a/packages/components/src/date-input/src/utils/createTextMarkInputElement.js b/packages/components/src/date-input/src/utils/createTextMarkInputElement.js new file mode 100644 index 000000000..aa97993b4 --- /dev/null +++ b/packages/components/src/date-input/src/utils/createTextMarkInputElement.js @@ -0,0 +1,199 @@ +import adjustCaretPosition from "./adjustCaretPosition"; +import conformToMask from "./conformToMask"; +import { convertMaskToPlaceholder, isString, isNumber, processCaretTraps } from "./utilities"; +import { placeholderChar as defaultPlaceholderChar, strFunction } from "./constants"; + +const emptyString = ""; +const strNone = "none"; +const strObject = "object"; +const isAndroid = typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent); +const defer = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : setTimeout; + +export default function createTextMaskInputElement(config) { + // Anything that we will need to keep between `update` calls, we will store in this `state` object. + const state = { previousConformedValue: undefined, previousPlaceholder: undefined }; + + return { + state, + + // `update` is called by framework components whenever they want to update the `value` of the input element. + // The caller can send a `rawValue` to be conformed and set on the input element. However, the default use-case + // is for this to be read from the `inputElement` directly. + update(rawValue, { + inputElement, + mask: providedMask, + guide, + pipe, + placeholderChar = defaultPlaceholderChar, + keepCharPositions = false, + showMask = false + } = config) { + // if `rawValue` is `undefined`, read from the `inputElement` + if (typeof rawValue === "undefined") { + rawValue = inputElement.value; + } + + // If `rawValue` equals `state.previousConformedValue`, we don't need to change anything. So, we return. + // This check is here to handle controlled framework components that repeat the `update` call on every render. + if (rawValue === state.previousConformedValue) { return; } + + // Text Mask accepts masks that are a combination of a `mask` and a `pipe` that work together. If such a `mask` is + // passed, we destructure it below, so the rest of the code can work normally as if a separate `mask` and a `pipe` + // were passed. + if (typeof providedMask === strObject && providedMask.pipe !== undefined && providedMask.mask !== undefined) { + pipe = providedMask.pipe; + providedMask = providedMask.mask; + } + + // The `placeholder` is an essential piece of how Text Mask works. For a mask like `(111)`, the placeholder would + // be `(___)` if the `placeholderChar` is set to `_`. + let placeholder; + + // We don't know what the mask would be yet. If it is an array, we take it as is, but if it's a function, we will + // have to call that function to get the mask array. + let mask; + + // If the provided mask is an array, we can call `convertMaskToPlaceholder` here once and we'll always have the + // correct `placeholder`. + if (providedMask instanceof Array) { + placeholder = convertMaskToPlaceholder(providedMask, placeholderChar); + } + + // In framework components that support reactivity, it's possible to turn off masking by passing + // `false` for `mask` after initialization. See https://github.com/text-mask/text-mask/pull/359 + if (providedMask === false) { return; } + + // We check the provided `rawValue` before moving further. + // If it's something we can't work with `getSafeRawValue` will throw. + const safeRawValue = getSafeRawValue(rawValue); + + // `selectionEnd` indicates to us where the caret position is after the user has typed into the input + const { selectionEnd: currentCaretPosition } = inputElement; + + // We need to know what the `previousConformedValue` and `previousPlaceholder` is from the previous `update` call + const { previousConformedValue, previousPlaceholder } = state; + + let caretTrapIndexes; + + // If the `providedMask` is a function. We need to call it at every `update` to get the `mask` array. + // Then we also need to get the `placeholder` + if (typeof providedMask === strFunction) { + mask = providedMask(safeRawValue, { currentCaretPosition, previousConformedValue, placeholderChar }); + + // disable masking if `mask` is `false` + if (mask === false) { return; } + + // mask functions can setup caret traps to have some control over how the caret moves. We need to process + // the mask for any caret traps. `processCaretTraps` will remove the caret traps from the mask and return + // the indexes of the caret traps. + const { maskWithoutCaretTraps, indexes } = processCaretTraps(mask); + + mask = maskWithoutCaretTraps; // The processed mask is what we're interested in + caretTrapIndexes = indexes; // And we need to store these indexes because they're needed by `adjustCaretPosition` + + placeholder = convertMaskToPlaceholder(mask, placeholderChar); + + // If the `providedMask` is not a function, we just use it as-is. + } else { + mask = providedMask; + } + + // The following object will be passed to `conformToMask` to determine how the `rawValue` will be conformed + const conformToMaskConfig = { + previousConformedValue, + guide, + placeholderChar, + pipe, + placeholder, + currentCaretPosition, + keepCharPositions + }; + + // `conformToMask` returns `conformedValue` as part of an object for future API flexibility + const { conformedValue } = conformToMask(safeRawValue, mask, conformToMaskConfig); + + // The following few lines are to support the `pipe` feature. + const piped = typeof pipe === strFunction; + + let pipeResults = {}; + + // If `pipe` is a function, we call it. + if (piped) { + // `pipe` receives the `conformedValue` and the configurations with which `conformToMask` was called. + pipeResults = pipe(conformedValue, { rawValue: safeRawValue, ...conformToMaskConfig }); + + // `pipeResults` should be an object. But as a convenience, we allow the pipe author to just return `false` to + // indicate rejection. Or return just a string when there are no piped characters. + // If the `pipe` returns `false` or a string, the block below turns it into an object that the rest + // of the code can work with. + if (pipeResults === false) { + // If the `pipe` rejects `conformedValue`, we use the `previousConformedValue`, and set `rejected` to `true`. + pipeResults = { value: previousConformedValue, rejected: true }; + } else if (isString(pipeResults)) { + pipeResults = { value: pipeResults }; + } + } + + // Before we proceed, we need to know which conformed value to use, the one returned by the pipe or the one + // returned by `conformToMask`. + const finalConformedValue = (piped) ? pipeResults.value : conformedValue; + + // After determining the conformed value, we will need to know where to set + // the caret position. `adjustCaretPosition` will tell us. + const adjustedCaretPosition = adjustCaretPosition({ + previousConformedValue, + previousPlaceholder, + conformedValue: finalConformedValue, + placeholder, + rawValue: safeRawValue, + currentCaretPosition, + placeholderChar, + indexesOfPipedChars: pipeResults.indexesOfPipedChars, + caretTrapIndexes + }); + + // Text Mask sets the input value to an empty string when the condition below is set. It provides a better UX. + const inputValueShouldBeEmpty = finalConformedValue === placeholder && adjustedCaretPosition === 0; + const emptyValue = showMask ? placeholder : emptyString; + const inputElementValue = (inputValueShouldBeEmpty) ? emptyValue : finalConformedValue; + + state.previousConformedValue = inputElementValue; // store value for access for next time + state.previousPlaceholder = placeholder; + + // In some cases, this `update` method will be repeatedly called with a raw value that has already been conformed + // and set to `inputElement.value`. The below check guards against needlessly readjusting the input state. + // See https://github.com/text-mask/text-mask/issues/231 + if (inputElement.value === inputElementValue) { + return; + } + + inputElement.value = inputElementValue; // set the input value + safeSetSelection(inputElement, adjustedCaretPosition); // adjust caret position + } + }; +} + +function safeSetSelection(element, selectionPosition) { + if (document.activeElement === element) { + if (isAndroid) { + defer(() => element.setSelectionRange(selectionPosition, selectionPosition, strNone), 0); + } else { + element.setSelectionRange(selectionPosition, selectionPosition, strNone); + } + } +} + +function getSafeRawValue(inputValue) { + if (isString(inputValue)) { + return inputValue; + } else if (isNumber(inputValue)) { + return String(inputValue); + } else if (inputValue === undefined || inputValue === null) { + return emptyString; + } else { + throw new Error( + "The 'value' provided to Text Mask needs to be a string or a number. The value " + + `received was:\n\n ${JSON.stringify(inputValue)}` + ); + } +} diff --git a/packages/components/src/date-input/src/utils/utilities.js b/packages/components/src/date-input/src/utils/utilities.js new file mode 100644 index 000000000..7fbb44766 --- /dev/null +++ b/packages/components/src/date-input/src/utils/utilities.js @@ -0,0 +1,54 @@ +import { placeholderChar as defaultPlaceholderChar } from "./constants"; + +const emptyArray = []; + +export function convertMaskToPlaceholder(mask = emptyArray, placeholderChar = defaultPlaceholderChar) { + if (!isArray(mask)) { + throw new Error( + "Text-mask:convertMaskToPlaceholder; The mask property must be an array." + ); + } + + if (mask.indexOf(placeholderChar) !== -1) { + throw new Error( + "Placeholder character must not be used as part of the mask. Please specify a character " + + "that is not present in your mask as your placeholder character.\n\n" + + `The placeholder character that was received is: ${JSON.stringify(placeholderChar)}\n\n` + + `The mask that was received is: ${JSON.stringify(mask)}` + ); + } + + return mask.map(char => { + return (char instanceof RegExp) ? placeholderChar : char; + }).join(""); +} + +export function isArray(value) { + return (Array.isArray && Array.isArray(value)) || value instanceof Array; +} + +export function isString(value) { + return typeof value === "string" || value instanceof String; +} + +export function isNumber(value) { + return typeof value === "number" && value.length === undefined && !isNaN(value); +} + +export function isNil(value) { + return typeof value === "undefined" || value === null; +} + +const strCaretTrap = "[]"; +export function processCaretTraps(mask) { + const indexes = []; + + let indexOfCaretTrap; + while(indexOfCaretTrap = mask.indexOf(strCaretTrap), indexOfCaretTrap !== -1) { // eslint-disable-line + indexes.push(indexOfCaretTrap); + + mask.splice(indexOfCaretTrap, 1); + } + + return { maskWithoutCaretTraps: mask, indexes }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de326c5ae..5fabd2d60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -245,9 +245,6 @@ importers: react-is: specifier: 18.3.1 version: 18.3.1 - text-mask-core: - specifier: 5.1.2 - version: 5.1.2 use-debounce: specifier: 10.0.4 version: 10.0.4(react@18.3.1) @@ -299,7 +296,7 @@ importers: version: 8.10.0(eslint@8.57.1)(typescript@5.4.5) '@workleap/swc-configs': specifier: 2.2.3 - version: 2.2.3(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/helpers@0.5.13)(@swc/jest@0.2.36(@swc/core@1.7.36(@swc/helpers@0.5.13)))(browserslist@4.24.2) + version: 2.2.3(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/helpers@0.5.13)(@swc/jest@0.2.36(@swc/core@1.7.36(@swc/helpers@0.5.13)))(browserslist@4.24.0) '@workleap/tsup-configs': specifier: 3.0.6 version: 3.0.6(tsup@8.3.0(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(typescript@5.4.5)(yaml@2.6.0))(typescript@5.4.5) @@ -9824,9 +9821,6 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - text-mask-core@5.1.2: - resolution: {integrity: sha512-VfkCMdmRRZqXgQZFlDMiavm3hzsMzBM23CxHZsaeAYg66ZhXCNJWrFmnJwNy8KF9f74YvAUAuQenxsMCfuvhUw==} - text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -10689,6 +10683,26 @@ snapshots: '@babel/compat-data@7.26.2': {} + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/core@7.26.0(supports-color@9.4.0)': dependencies: '@ampproject/remapping': 2.3.0 @@ -10774,6 +10788,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.25.9(supports-color@9.4.0)': dependencies: '@babel/traverse': 7.25.9(supports-color@9.4.0) @@ -10781,6 +10802,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)(supports-color@9.4.0)': dependencies: '@babel/core': 7.26.0(supports-color@9.4.0) @@ -10896,22 +10926,22 @@ snapshots: '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': @@ -10921,67 +10951,67 @@ snapshots: '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': @@ -11405,6 +11435,18 @@ snapshots: '@babel/parser': 7.26.2 '@babel/types': 7.26.0 + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.25.9(supports-color@9.4.0)': dependencies: '@babel/code-frame': 7.26.2 @@ -11732,7 +11774,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -11811,7 +11853,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -12030,7 +12072,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -14822,7 +14864,7 @@ snapshots: '@typescript-eslint/types': 8.10.0 '@typescript-eslint/typescript-estree': 8.10.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 8.10.0 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 eslint: 8.57.1 optionalDependencies: typescript: 5.4.5 @@ -14896,7 +14938,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -14925,7 +14967,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.10.0 '@typescript-eslint/visitor-keys': 8.10.0 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15168,14 +15210,6 @@ snapshots: '@swc/jest': 0.2.36(@swc/core@1.7.36(@swc/helpers@0.5.13)) browserslist: 4.24.0 - '@workleap/swc-configs@2.2.3(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/helpers@0.5.13)(@swc/jest@0.2.36(@swc/core@1.7.36(@swc/helpers@0.5.13)))(browserslist@4.24.2)': - dependencies: - '@swc/core': 1.7.36(@swc/helpers@0.5.13) - '@swc/helpers': 0.5.13 - optionalDependencies: - '@swc/jest': 0.2.36(@swc/core@1.7.36(@swc/helpers@0.5.13)) - browserslist: 4.24.2 - '@workleap/tsup-configs@3.0.6(tsup@8.3.0(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(typescript@5.4.5)(yaml@2.6.0))(typescript@5.4.5)': dependencies: tsup: 8.3.0(@swc/core@1.7.36(@swc/helpers@0.5.13))(postcss@8.4.47)(typescript@5.4.5)(yaml@2.6.0) @@ -15229,6 +15263,12 @@ snapshots: acorn@8.14.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + agent-base@6.0.2(supports-color@9.4.0): dependencies: debug: 4.3.7(supports-color@9.4.0) @@ -15564,7 +15604,7 @@ snapshots: babel-jest@29.7.0(@babel/core@7.26.0): dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 @@ -15618,7 +15658,7 @@ snapshots: babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) @@ -15637,7 +15677,7 @@ snapshots: babel-preset-jest@29.6.3(@babel/core@7.26.0): dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) @@ -16530,6 +16570,10 @@ snapshots: optionalDependencies: supports-color: 9.4.0 + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.3.7(supports-color@9.4.0): dependencies: ms: 2.1.3 @@ -17433,7 +17477,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.5 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -18477,8 +18521,8 @@ snapshots: http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 - agent-base: 6.0.2(supports-color@9.4.0) - debug: 4.3.7(supports-color@9.4.0) + agent-base: 6.0.2 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -18502,6 +18546,13 @@ snapshots: transitivePeerDependencies: - debug + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1(supports-color@9.4.0): dependencies: agent-base: 6.0.2(supports-color@9.4.0) @@ -18930,7 +18981,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -18940,7 +18991,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -18965,7 +19016,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -19136,7 +19187,7 @@ snapshots: jest-config@29.7.0(@types/node@22.9.0)(ts-node@10.9.2(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@22.9.0)(typescript@5.4.5)): dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) @@ -19383,7 +19434,7 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@babel/generator': 7.26.2 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) @@ -19546,7 +19597,7 @@ snapshots: form-data: 4.0.1 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1(supports-color@9.4.0) + https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.13 parse5: 7.2.1 @@ -22996,8 +23047,6 @@ snapshots: text-hex@1.0.0: {} - text-mask-core@5.1.2: {} - text-table@0.2.0: {} thenify-all@1.6.0: @@ -23157,7 +23206,7 @@ snapshots: typescript: 5.4.5 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.26.0(supports-color@9.4.0) + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) @@ -23251,7 +23300,7 @@ snapshots: cac: 6.7.14 chokidar: 3.6.0 consola: 3.2.3 - debug: 4.3.7(supports-color@9.4.0) + debug: 4.3.7 esbuild: 0.23.1 execa: 5.1.1 joycon: 3.1.1 From c4c2528c874ca90621a2169a105fbd5a776edf1d Mon Sep 17 00:00:00 2001 From: Alexandre Asselin Date: Mon, 11 Nov 2024 10:48:55 -0500 Subject: [PATCH 2/2] convert to TS --- .../scripts/copy-css-index-to-dist.js | 8 -------- .../src/date-input/src/useMaskedInput.ts | 2 +- ...tCaretPosition.js => adjustCaretPosition.ts} | 7 ++++++- .../{conformToMask.js => conformToMask.ts} | 17 +++++++++++------ .../src/date-input/src/utils/constants.js | 2 -- .../src/date-input/src/utils/constants.ts | 7 +++++++ ...Element.js => createTextMarkInputElement.ts} | 16 +++++++++++----- .../src/utils/{utilities.js => utilities.ts} | 8 +++++++- 8 files changed, 43 insertions(+), 24 deletions(-) delete mode 100644 packages/components/scripts/copy-css-index-to-dist.js rename packages/components/src/date-input/src/utils/{adjustCaretPosition.js => adjustCaretPosition.ts} (97%) rename packages/components/src/date-input/src/utils/{conformToMask.js => conformToMask.ts} (95%) delete mode 100644 packages/components/src/date-input/src/utils/constants.js create mode 100644 packages/components/src/date-input/src/utils/constants.ts rename packages/components/src/date-input/src/utils/{createTextMarkInputElement.js => createTextMarkInputElement.ts} (95%) rename packages/components/src/date-input/src/utils/{utilities.js => utilities.ts} (84%) diff --git a/packages/components/scripts/copy-css-index-to-dist.js b/packages/components/scripts/copy-css-index-to-dist.js deleted file mode 100644 index a847db6cb..000000000 --- a/packages/components/scripts/copy-css-index-to-dist.js +++ /dev/null @@ -1,8 +0,0 @@ -const chalk = require("chalk"); -const shell = require("shelljs"); - -const DIST_PATH = "dist"; - -shell.cp("-f", "src/index.css", DIST_PATH); - -console.log(chalk.green("success"), " components index.css copied to dist folder."); diff --git a/packages/components/src/date-input/src/useMaskedInput.ts b/packages/components/src/date-input/src/useMaskedInput.ts index 1864c0f5d..109e51cc2 100644 --- a/packages/components/src/date-input/src/useMaskedInput.ts +++ b/packages/components/src/date-input/src/useMaskedInput.ts @@ -1,4 +1,4 @@ -import createTextMaskInputElement from "./utils/createTextMarkInputElement.js"; +import createTextMaskInputElement from "./utils/createTextMarkInputElement.ts"; import { isNil } from "../../shared/index.ts"; import { useCallback, useEffect, useRef } from "react"; diff --git a/packages/components/src/date-input/src/utils/adjustCaretPosition.js b/packages/components/src/date-input/src/utils/adjustCaretPosition.ts similarity index 97% rename from packages/components/src/date-input/src/utils/adjustCaretPosition.js rename to packages/components/src/date-input/src/utils/adjustCaretPosition.ts index 444eb1696..4a0e961e1 100644 --- a/packages/components/src/date-input/src/utils/adjustCaretPosition.js +++ b/packages/components/src/date-input/src/utils/adjustCaretPosition.ts @@ -1,3 +1,8 @@ +// ******************************************** +// This file is copied from the "text-mask-core" repo: https://github.com/text-mask/text-mask +// It was causing issue since we migrated to ESM, so we copied the file here to avoid the issue. +// ******************************************** + const defaultArray = []; const emptyString = ""; @@ -11,7 +16,7 @@ export default function adjustCaretPosition({ placeholder, indexesOfPipedChars = defaultArray, caretTrapIndexes = defaultArray -}) { +}: any) { if (currentCaretPosition === 0 || !rawValue.length) { return 0; } // Store lengths for faster performance? diff --git a/packages/components/src/date-input/src/utils/conformToMask.js b/packages/components/src/date-input/src/utils/conformToMask.ts similarity index 95% rename from packages/components/src/date-input/src/utils/conformToMask.js rename to packages/components/src/date-input/src/utils/conformToMask.ts index 9b8bc631a..c8511cb1d 100644 --- a/packages/components/src/date-input/src/utils/conformToMask.js +++ b/packages/components/src/date-input/src/utils/conformToMask.ts @@ -1,10 +1,15 @@ -import { convertMaskToPlaceholder, isArray, processCaretTraps } from "./utilities"; -import { placeholderChar as defaultPlaceholderChar, strFunction } from "./constants"; +// ******************************************** +// This file is copied from the "text-mask-core" repo: https://github.com/text-mask/text-mask +// It was causing issue since we migrated to ESM, so we copied the file here to avoid the issue. +// ******************************************** + +import { convertMaskToPlaceholder, isArray, processCaretTraps } from "./utilities.ts"; +import { placeholderChar as defaultPlaceholderChar, strFunction } from "./constants.ts"; const emptyArray = []; const emptyString = ""; -export default function conformToMask(rawValue = emptyString, mask = emptyArray, config = {}) { +export default function conformToMask(rawValue: any = emptyString, mask: any = emptyArray, config: any = {}) { if (!isArray(mask)) { // If someone passes a function as the mask property, we should call the // function to get the mask array - Normally this is handled by the @@ -165,15 +170,15 @@ export default function conformToMask(rawValue = emptyString, mask = emptyArray, // a placeholder character or a non-suitable spot, ie, a non-placeholder character that is not new. // If we see a suitable spot first, we store its position and exit the loop. If we see a non-suitable // spot first, we exit the loop and our `indexOfNextAvailablePlaceholderChar` will stay as `null`. - for (let i = 0; i < rawValueArrLength; i++) { - const charData = rawValueArr[i]; + for (let i2 = 0; i2 < rawValueArrLength; i2++) { + const charData = rawValueArr[i2]; if (charData.char !== placeholderChar && charData.isNew === false) { break; } if (charData.char === placeholderChar) { - indexOfNextAvailablePlaceholderChar = i; + indexOfNextAvailablePlaceholderChar = i2; break; } } diff --git a/packages/components/src/date-input/src/utils/constants.js b/packages/components/src/date-input/src/utils/constants.js deleted file mode 100644 index 5879a0403..000000000 --- a/packages/components/src/date-input/src/utils/constants.js +++ /dev/null @@ -1,2 +0,0 @@ -export const placeholderChar = "_"; -export const strFunction = "function"; diff --git a/packages/components/src/date-input/src/utils/constants.ts b/packages/components/src/date-input/src/utils/constants.ts new file mode 100644 index 000000000..8eb71902c --- /dev/null +++ b/packages/components/src/date-input/src/utils/constants.ts @@ -0,0 +1,7 @@ +// ******************************************** +// This file is copied from the "text-mask-core" repo: https://github.com/text-mask/text-mask +// It was causing issue since we migrated to ESM, so we copied the file here to avoid the issue. +// ******************************************** + +export const placeholderChar = "_"; +export const strFunction = "function"; diff --git a/packages/components/src/date-input/src/utils/createTextMarkInputElement.js b/packages/components/src/date-input/src/utils/createTextMarkInputElement.ts similarity index 95% rename from packages/components/src/date-input/src/utils/createTextMarkInputElement.js rename to packages/components/src/date-input/src/utils/createTextMarkInputElement.ts index aa97993b4..1aee7f389 100644 --- a/packages/components/src/date-input/src/utils/createTextMarkInputElement.js +++ b/packages/components/src/date-input/src/utils/createTextMarkInputElement.ts @@ -1,7 +1,12 @@ -import adjustCaretPosition from "./adjustCaretPosition"; -import conformToMask from "./conformToMask"; -import { convertMaskToPlaceholder, isString, isNumber, processCaretTraps } from "./utilities"; -import { placeholderChar as defaultPlaceholderChar, strFunction } from "./constants"; +// ******************************************** +// This file is copied from the "text-mask-core" repo: https://github.com/text-mask/text-mask +// It was causing issue since we migrated to ESM, so we copied the file here to avoid the issue. +// ******************************************** + +import adjustCaretPosition from "./adjustCaretPosition.ts"; +import conformToMask from "./conformToMask.ts"; +import { convertMaskToPlaceholder, isString, isNumber, processCaretTraps } from "./utilities.ts"; +import { placeholderChar as defaultPlaceholderChar, strFunction } from "./constants.ts"; const emptyString = ""; const strNone = "none"; @@ -115,7 +120,7 @@ export default function createTextMaskInputElement(config) { // The following few lines are to support the `pipe` feature. const piped = typeof pipe === strFunction; - let pipeResults = {}; + let pipeResults: any = {}; // If `pipe` is a function, we call it. if (piped) { @@ -176,6 +181,7 @@ export default function createTextMaskInputElement(config) { function safeSetSelection(element, selectionPosition) { if (document.activeElement === element) { if (isAndroid) { + // @ts-ignore defer(() => element.setSelectionRange(selectionPosition, selectionPosition, strNone), 0); } else { element.setSelectionRange(selectionPosition, selectionPosition, strNone); diff --git a/packages/components/src/date-input/src/utils/utilities.js b/packages/components/src/date-input/src/utils/utilities.ts similarity index 84% rename from packages/components/src/date-input/src/utils/utilities.js rename to packages/components/src/date-input/src/utils/utilities.ts index 7fbb44766..1624d82c5 100644 --- a/packages/components/src/date-input/src/utils/utilities.js +++ b/packages/components/src/date-input/src/utils/utilities.ts @@ -1,4 +1,9 @@ -import { placeholderChar as defaultPlaceholderChar } from "./constants"; +// ******************************************** +// This file is copied from the "text-mask-core" repo: https://github.com/text-mask/text-mask +// It was causing issue since we migrated to ESM, so we copied the file here to avoid the issue. +// ******************************************** + +import { placeholderChar as defaultPlaceholderChar } from "./constants.ts"; const emptyArray = []; @@ -32,6 +37,7 @@ export function isString(value) { } export function isNumber(value) { + // @ts-ignore return typeof value === "number" && value.length === undefined && !isNaN(value); }