import "./style.pcss"; import { getDebouncedFn } from "@web3r/flowerkit/fn"; import type { FC, ReactElement, ChangeEvent, ClipboardEvent, FocusEvent, } from "react"; import { useImperativeHandle, useState, useEffect, useRef, useMemo, useCallback, } from "react"; import { IMaskInput } from "react-imask"; import type { ReactMaskProps } from "react-imask"; import type { IMaskedInput, TMaskedCallbackArg, TInstance, TMaskedInputProps, TMaskedRef, TMaskOptions, } from "./config/types"; import { getCurrentMask, getCurrentValue } from "./lib"; import { constants } from "../../constants"; import type { IComponentBaseProps } from "../../types"; import { useCN } from "../../utils/react/useCN"; import { useFormValidation } from "../../utils/react/useFormValidation"; import { useSSGRender } from "../../utils/react/useSSGRender"; import { Input } from "../input"; const MaskedControl: FC & { onAccept: unknown; onComplete: unknown; onPaste: unknown; onKeyDown: unknown; defaultValue: IMaskedInput["value"]; }> = (props = {}): ReactElement => { const { getCN } = useCN("formInput"); const computedProps = useMemo(() => { const extraClasses: IComponentBaseProps["extraClasses"] = {}; const currentProps: TMaskedInputProps = { ...props, ...((props?.mask ?? {}) as object), } as TMaskedInputProps; // readonly prop if (currentProps.isReadOnly) { currentProps.readOnly = true; extraClasses.isReadOnly = true; } delete currentProps.isReadOnly; // disabled prop if (currentProps.isDisabled) { currentProps.disabled = true; extraClasses.isDisabled = true; } delete currentProps.isDisabled; // inputMode prop if (!currentProps.inputMode) { delete currentProps.inputMode; } // form prop if (!currentProps.form) { delete currentProps.form; } // name prop if (!currentProps.name) { delete currentProps.name; } // id prop if (!currentProps.id) { delete currentProps.id; } // classname if (currentProps.inputExtraClasses) { currentProps.className = getCN("", { ...extraClasses, ...(props?.inputExtraClasses ?? {}), }); delete currentProps.inputExtraClasses; } // extra attrs if (currentProps.inputExtraAttrs) { Object.entries(currentProps.inputExtraAttrs) .forEach(([ key, value ]: [ string, unknown ]) => { currentProps[key as keyof TMaskedInputProps] = value; }); delete currentProps.inputExtraAttrs; } return currentProps; }, [ getCN, props ]); return ; }; /** * Компонент маски * @returns {ReactElement} */ const MaskedInput: FC = (props): ReactElement => { const { baseClass = "maskedInput", extraAttrs = {}, extraClasses = {}, children = null, isDisabled = false, isReadOnly = false, isRequired = false, value = "", mask, name = "", onBlur = null, onChange = null, onFocus = null, onPaste = null, onInput = null, onKeyDown = null, placeholder = "", title = "", form = "", id = "", inputMode, onMount = null, onUnmount = null, inputExtraAttrs = {}, inputExtraClasses = {}, ref, } = props || {}; // Classnames const { getCN } = useCN(baseClass); // Ref const containerRef = useRef(null); const instanceRef = useRef(null); const inputRef = useRef(null); // States const [ inputValue, setInputValue ] = useState(() => getCurrentValue(value)); const [ isInputDisabled, setIsInputDisabled ] = useState(isDisabled); const [ isInputReadOnly, setIsInputReadOnly ] = useState(isReadOnly); const [ isInputRequired, setIsInputRequired ] = useState(isRequired); const [ currentMask, setCurrentMask ] = useState(() => getCurrentMask(mask)); // Prev states const prevMask = useRef(currentMask); const prevValue = useRef(inputValue); // State actions const stateActions = useMemo(() => ({ setInputValue, setIsInputDisabled, setIsInputReadOnly, setIsInputRequired, setCurrentMask, }), []); // Updater const syncInstance = getDebouncedFn(() => { const instance = instanceRef?.current?.maskRef; if (instance) { instance.updateValue(); instance.updateControl(); } }, 300); // Callback arg const getCbArg = useCallback<((value: IMaskedInput["value"], event?: ChangeEvent | FocusEvent | ClipboardEvent) => TMaskedCallbackArg)>((value, event) => { return { el: containerRef.current, ref: instanceRef, utils: null, event: event || null, getData: () => ({ value, prevValue: prevValue.current, syncInstance, }), stateActions, }; }, [ stateActions, syncInstance ]); const emitChange = useMemo(() => getDebouncedFn((nextValue: string, event?: ChangeEvent) => { if (typeof onChange === "function") { const arg = getCbArg(nextValue, event); onChange(arg); } }, 300), [ getCbArg, onChange ]); const emitInput = useMemo(() => getDebouncedFn((nextValue: string, event?: ChangeEvent) => { if (typeof onInput === "function") { const arg = getCbArg(nextValue, event); onInput(arg); } }, 300), [ getCbArg, onInput ]); // onChange const handleChange = useCallback((value: string, mask: TInstance, event?: ChangeEvent) => { const nextValue = value || ""; setInputValue((prev) => (prev !== nextValue ? nextValue : prev)); emitChange(nextValue, event); }, [ emitChange ]); const handleInput = useCallback((value: string, mask: TInstance, event?: ChangeEvent) => { const nextValue = value || ""; setInputValue((prev) => (prev !== nextValue ? nextValue : prev)); emitInput(nextValue, event); }, [ emitInput ]); // onBlur const handleBlur = useCallback((event: FocusEvent) => { if (typeof onBlur === "function") { const arg = getCbArg((event.target as HTMLInputElement).value, event); onBlur(arg); } }, [ getCbArg, onBlur ]); // onFocus const handleFocus = useCallback((event: FocusEvent) => { if (typeof onFocus === "function") { const arg = getCbArg((event.target as HTMLInputElement).value, event); onFocus(arg); } }, [ getCbArg, onFocus ]); // onPaste const handlePaste = useCallback((event: ClipboardEvent) => { if (typeof onPaste === "function") { const arg = getCbArg((event.target as HTMLInputElement).value, event); onPaste(arg); } }, [ getCbArg, onPaste ]); // onKeyDown const handleKeyDown = useCallback((event: ClipboardEvent) => { if (typeof onKeyDown === "function") { const arg = getCbArg((event.target as HTMLInputElement).value, event); onKeyDown(arg); } }, [ getCbArg, onKeyDown ]); // Validation useFormValidation(inputRef, (e) => { containerRef?.current?.classList.toggle(constants.stateClasses.invalid, !e.detail.isValid); }); // Ref useImperativeHandle(ref, () => { return getCbArg(inputValue) as TMaskedRef; }, [ getCbArg, inputValue ]); useEffect(() => { if (typeof onMount === "function") { onMount(instanceRef); } return () => { if (typeof onUnmount === "function") { onUnmount(instanceRef); } }; }, []); // Change `mask` or `value` useEffect(() => { // value const newValue = getCurrentValue(value); if (newValue !== prevValue.current) { prevValue.current = newValue; setInputValue(newValue); } // mask const newMask = getCurrentMask(mask); if (newMask !== prevMask.current) { prevMask.current = newMask; setCurrentMask(newMask); } }, [ mask, value ]); // Sync useEffect(() => { if (inputValue !== prevValue.current) { prevValue.current = inputValue; syncInstance(); } }, [ inputValue ]); // Classnames const classMods = useMemo(() => ({ isDisabled: !!isInputDisabled, isReadOnly: !!isInputReadOnly, isRequired: !!isInputRequired, ...extraClasses, }), [ extraClasses, isInputDisabled, isInputReadOnly, isInputRequired ]); const SSGComponent = useSSGRender("data-js-masked-input", props); if (SSGComponent) { return SSGComponent; } return (
{children}
); }; export type { IMaskedInput, TMaskedCallbackArg, TMaskedRef, TMaskOptions, }; export { MaskedInput, };