UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

256 lines 13.9 kB
import { __assign } from "tslib"; import * as React from 'react'; import { TextField } from '../TextField'; import { KeyCodes } from '../../../Utilities'; import { clearNext, clearPrev, clearRange, DEFAULT_MASK_FORMAT_CHARS, getLeftFormatIndex, getMaskDisplay, getRightFormatIndex, insertString, parseMask, } from './inputMask'; import { useConst, useIsomorphicLayoutEffect } from '@fluentui/react-hooks'; var COMPONENT_NAME = 'MaskedTextField'; var useComponentRef = function (componentRef, internalState, textField) { React.useImperativeHandle(componentRef, function () { return ({ get value() { var value = ''; for (var i = 0; i < internalState.maskCharData.length; i++) { if (!internalState.maskCharData[i].value) { return undefined; } value += internalState.maskCharData[i].value; } return value; }, get selectionStart() { return textField.current && textField.current.selectionStart !== null ? textField.current.selectionStart : -1; }, get selectionEnd() { return textField.current && textField.current.selectionEnd ? textField.current.selectionEnd : -1; }, focus: function () { textField.current && textField.current.focus(); }, blur: function () { textField.current && textField.current.blur(); }, select: function () { textField.current && textField.current.select(); }, setSelectionStart: function (value) { textField.current && textField.current.setSelectionStart(value); }, setSelectionEnd: function (value) { textField.current && textField.current.setSelectionEnd(value); }, setSelectionRange: function (start, end) { textField.current && textField.current.setSelectionRange(start, end); }, }); }, [internalState, textField]); }; export var DEFAULT_MASK_CHAR = '_'; export var MaskedTextField = React.forwardRef(function (props, ref) { var textField = React.useRef(null); var componentRef = props.componentRef, onFocus = props.onFocus, onBlur = props.onBlur, onMouseDown = props.onMouseDown, onMouseUp = props.onMouseUp, onChange = props.onChange, onPaste = props.onPaste, onKeyDown = props.onKeyDown, mask = props.mask, _a = props.maskChar, maskChar = _a === void 0 ? DEFAULT_MASK_CHAR : _a, _b = props.maskFormat, maskFormat = _b === void 0 ? DEFAULT_MASK_FORMAT_CHARS : _b, value = props.value; var internalState = useConst(function () { return ({ maskCharData: parseMask(mask, maskFormat), isFocused: false, moveCursorOnMouseUp: false, changeSelectionData: null, }); }); /** The index into the rendered value of the first unfilled format character */ var _c = React.useState(), maskCursorPosition = _c[0], setMaskCursorPosition = _c[1]; /** * The mask string formatted with the input value. * This is what is displayed inside the TextField * @example * `Phone Number: 12_ - 4___` */ var _d = React.useState(function () { return getMaskDisplay(mask, internalState.maskCharData, maskChar); }), displayValue = _d[0], setDisplayValue = _d[1]; var setValue = React.useCallback(function (newValue) { var valueIndex = 0; var charDataIndex = 0; while (valueIndex < newValue.length && charDataIndex < internalState.maskCharData.length) { // Test if the next character in the new value fits the next format character var testVal = newValue[valueIndex]; if (internalState.maskCharData[charDataIndex].format.test(testVal)) { internalState.maskCharData[charDataIndex].value = testVal; charDataIndex++; } valueIndex++; } }, [internalState]); var handleFocus = React.useCallback(function (ev) { onFocus === null || onFocus === void 0 ? void 0 : onFocus(ev); internalState.isFocused = true; // Move the cursor position to the leftmost unfilled position for (var i = 0; i < internalState.maskCharData.length; i++) { if (!internalState.maskCharData[i].value) { setMaskCursorPosition(internalState.maskCharData[i].displayIndex); break; } } }, [internalState, onFocus]); var handleBlur = React.useCallback(function (ev) { onBlur === null || onBlur === void 0 ? void 0 : onBlur(ev); internalState.isFocused = false; internalState.moveCursorOnMouseUp = true; }, [internalState, onBlur]); var handleMouseDown = React.useCallback(function (ev) { onMouseDown === null || onMouseDown === void 0 ? void 0 : onMouseDown(ev); if (!internalState.isFocused) { internalState.moveCursorOnMouseUp = true; } }, [internalState, onMouseDown]); var handleMouseUp = React.useCallback(function (ev) { onMouseUp === null || onMouseUp === void 0 ? void 0 : onMouseUp(ev); // Move the cursor on mouseUp after focusing the textField if (internalState.moveCursorOnMouseUp) { internalState.moveCursorOnMouseUp = false; // Move the cursor position to the rightmost unfilled position for (var i = 0; i < internalState.maskCharData.length; i++) { if (!internalState.maskCharData[i].value) { setMaskCursorPosition(internalState.maskCharData[i].displayIndex); break; } } } }, [internalState, onMouseUp]); var handleInputChange = React.useCallback(function (ev, inputValue) { if (internalState.changeSelectionData === null && textField.current) { internalState.changeSelectionData = { changeType: 'default', selectionStart: textField.current.selectionStart !== null ? textField.current.selectionStart : -1, selectionEnd: textField.current.selectionEnd !== null ? textField.current.selectionEnd : -1, }; } if (!internalState.changeSelectionData) { return; } // The initial value of cursorPos does not matter var cursorPos = 0; var _a = internalState.changeSelectionData, changeType = _a.changeType, selectionStart = _a.selectionStart, selectionEnd = _a.selectionEnd; if (changeType === 'textPasted') { var charsSelected = selectionEnd - selectionStart; var charCount = inputValue.length + charsSelected - displayValue.length; var startPos = selectionStart; // eslint-disable-next-line deprecation/deprecation var pastedString = inputValue.substr(startPos, charCount); // Clear any selected characters if (charsSelected) { internalState.maskCharData = clearRange(internalState.maskCharData, selectionStart, charsSelected); } cursorPos = insertString(internalState.maskCharData, startPos, pastedString); } else if (changeType === 'delete' || changeType === 'backspace') { // isDel is true If the characters are removed LTR, otherwise RTL var isDel = changeType === 'delete'; var charCount = selectionEnd - selectionStart; if (charCount) { // charCount is > 0 if range was deleted internalState.maskCharData = clearRange(internalState.maskCharData, selectionStart, charCount); cursorPos = getRightFormatIndex(internalState.maskCharData, selectionStart); } else { // If charCount === 0, there was no selection and a single character was deleted if (isDel) { internalState.maskCharData = clearNext(internalState.maskCharData, selectionStart); cursorPos = getRightFormatIndex(internalState.maskCharData, selectionStart); } else { internalState.maskCharData = clearPrev(internalState.maskCharData, selectionStart); cursorPos = getLeftFormatIndex(internalState.maskCharData, selectionStart); } } } else if (inputValue.length > displayValue.length) { // This case is if the user added characters var charCount = inputValue.length - displayValue.length; var startPos = selectionEnd - charCount; // eslint-disable-next-line deprecation/deprecation var enteredString = inputValue.substr(startPos, charCount); cursorPos = insertString(internalState.maskCharData, startPos, enteredString); } else if (inputValue.length <= displayValue.length) { /** * This case is reached only if the user has selected a block of 1 or more * characters and input a character replacing the characters they've selected. */ var charCount = 1; var selectCount = displayValue.length + charCount - inputValue.length; var startPos = selectionEnd - charCount; // eslint-disable-next-line deprecation/deprecation var enteredString = inputValue.substr(startPos, charCount); // Clear the selected range internalState.maskCharData = clearRange(internalState.maskCharData, startPos, selectCount); // Insert the printed character cursorPos = insertString(internalState.maskCharData, startPos, enteredString); } internalState.changeSelectionData = null; var newValue = getMaskDisplay(mask, internalState.maskCharData, maskChar); setDisplayValue(newValue); setMaskCursorPosition(cursorPos); // Perform onChange after input has been processed. Return value is expected to be the displayed text onChange === null || onChange === void 0 ? void 0 : onChange(ev, newValue); }, [displayValue.length, internalState, mask, maskChar, onChange]); var handleKeyDown = React.useCallback(function (ev) { onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(ev); internalState.changeSelectionData = null; if (textField.current && textField.current.value) { // eslint-disable-next-line deprecation/deprecation var keyCode = ev.keyCode, ctrlKey = ev.ctrlKey, metaKey = ev.metaKey; // Ignore ctrl and meta keydown if (ctrlKey || metaKey) { return; } // On backspace or delete, store the selection and the keyCode if (keyCode === KeyCodes.backspace || keyCode === KeyCodes.del) { var selectionStart = ev.target.selectionStart; var selectionEnd = ev.target.selectionEnd; // Check if backspace or delete press is valid. if (!(keyCode === KeyCodes.backspace && selectionEnd && selectionEnd > 0) && !(keyCode === KeyCodes.del && selectionStart !== null && selectionStart < textField.current.value.length)) { return; } internalState.changeSelectionData = { changeType: keyCode === KeyCodes.backspace ? 'backspace' : 'delete', selectionStart: selectionStart !== null ? selectionStart : -1, selectionEnd: selectionEnd !== null ? selectionEnd : -1, }; } } }, [internalState, onKeyDown]); var handlePaste = React.useCallback(function (ev) { onPaste === null || onPaste === void 0 ? void 0 : onPaste(ev); var selectionStart = ev.target.selectionStart; var selectionEnd = ev.target.selectionEnd; // Store the paste selection range internalState.changeSelectionData = { changeType: 'textPasted', selectionStart: selectionStart !== null ? selectionStart : -1, selectionEnd: selectionEnd !== null ? selectionEnd : -1, }; }, [internalState, onPaste]); // Updates the display value if mask or value props change. React.useEffect(function () { internalState.maskCharData = parseMask(mask, maskFormat); value !== undefined && setValue(value); setDisplayValue(getMaskDisplay(mask, internalState.maskCharData, maskChar)); // eslint-disable-next-line react-hooks/exhaustive-deps -- Should only update when mask or value changes. }, [mask, value]); // Run before browser paint to avoid flickering from selection reset. useIsomorphicLayoutEffect(function () { // Move the cursor to position before paint. if (maskCursorPosition !== undefined && textField.current) { textField.current.setSelectionRange(maskCursorPosition, maskCursorPosition); } }, [maskCursorPosition]); // Run after browser paint. React.useEffect(function () { // Move the cursor to the start of the mask format after values update. if (internalState.isFocused && maskCursorPosition !== undefined && textField.current) { textField.current.setSelectionRange(maskCursorPosition, maskCursorPosition); } }); useComponentRef(componentRef, internalState, textField); return (React.createElement(TextField, __assign({}, props, { elementRef: ref, onFocus: handleFocus, onBlur: handleBlur, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onChange: handleInputChange, onKeyDown: handleKeyDown, onPaste: handlePaste, value: displayValue || '', componentRef: textField }))); }); MaskedTextField.displayName = COMPONENT_NAME; //# sourceMappingURL=MaskedTextField.js.map