{"version":3,"file":"maskedInput.mjs","names":[],"sources":["../../../../package/common/maskedInput/maskedInput.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from 'react';\nimport type React from 'react';\n\nimport { mergeRefs } from '../utils/mergeRefs';\nimport { firstEmptyCaret, isBlank, maskChange, toMask } from './mask';\nimport type { MaskedInputProps } from './maskedInput.types';\n\n/**\n * A text input under a fixed digit mask, used by `DatePicker` and `TimeInput` through `Input`'s\n * `customInput`. Internal: consumers reach it through those two.\n *\n * Controlled, with one exception. An empty field shows nothing until it is focused, and then shows\n * the bare mask, so a form full of `__.__.____` does not greet someone who has typed nothing. That\n * mask is display only and is never reported through `onChange`, or every untouched date field\n * would submit a value made of underscores.\n */\nfunction MaskedInput(props: MaskedInputProps) {\n  const {\n    mask,\n    maskPlaceholder = '_',\n    value = '',\n    onChange,\n    beforeMaskedStateChange,\n    onFocus,\n    onBlur,\n    onClick,\n    ref,\n    ...rest\n  } = props;\n\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [isFocused, setIsFocused] = useState(false);\n  /**\n   * Where the caret belongs after the value the parent holds comes back to us. Set during the\n   * change and applied on the next paint, because the browser puts the caret at the end of any\n   * value it did not compute itself.\n   */\n  const caretRef = useRef<number | null>(null);\n\n  useLayoutEffect(() => {\n    if (caretRef.current === null) return;\n    inputRef.current?.setSelectionRange(caretRef.current, caretRef.current);\n    caretRef.current = null;\n  });\n\n  const blank = isBlank(value, mask);\n  // Everything below assumes a value exactly as long as the mask. The prop is whatever the parent\n  // holds, which on a first render can be a date from somewhere else entirely, so it is put\n  // through the mask rather than trusted.\n  const display = blank ? (isFocused ? toMask('', mask, maskPlaceholder) : '') : toMask(value, mask, maskPlaceholder);\n\n  const setCaret = (position: number) => {\n    caretRef.current = position;\n    inputRef.current?.setSelectionRange(position, position);\n  };\n\n  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const input = event.target;\n    const raw = input.value;\n    const nextState = maskChange(display, raw, input.selectionStart ?? raw.length, mask, maskPlaceholder);\n\n    const state = beforeMaskedStateChange\n      ? beforeMaskedStateChange({ previousState: { value: display, selection: null }, nextState })\n      : nextState;\n\n    const next = toMask(state.value, mask, maskPlaceholder);\n    const caret = state.selection?.start ?? firstEmptyCaret(next, mask, maskPlaceholder);\n\n    // Nothing came of it — a letter typed into an empty field, or a separator the mask already\n    // draws. React re-renders nothing when the value it holds has not changed, so the caret has\n    // to be put back here; the layout effect will not run.\n    if (next === display) {\n      input.value = display;\n      setCaret(caret);\n      return;\n    }\n\n    caretRef.current = caret;\n    onChange?.(isBlank(next, mask) ? '' : next);\n  };\n\n  const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {\n    setIsFocused(true);\n    // Only when the mask is about to appear. The field showed nothing a moment ago, so whatever\n    // caret the browser picked was measured against an empty string. A field with something in it\n    // keeps the caret where the user put it, and `handleClick` pulls it back only if it landed\n    // past the last free slot.\n    if (blank) caretRef.current = 0;\n    onFocus?.(event);\n  };\n\n  /**\n   * A click can leave the caret past every slot, because a mask is as wide empty as full. Runs on\n   * click rather than on focus: the browser sets the selection on mouse *up*, after focus, so at\n   * focus time there is nothing reliable to read.\n   */\n  const handleClick = (event: React.MouseEvent<HTMLInputElement>) => {\n    const input = event.currentTarget;\n    const limit = firstEmptyCaret(display, mask, maskPlaceholder);\n    // A drag that selected something is left alone; so is a caret already inside the filled part.\n    if (input.selectionStart === input.selectionEnd && (input.selectionStart ?? 0) > limit) {\n      input.setSelectionRange(limit, limit);\n    }\n    onClick?.(event);\n  };\n\n  const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n    setIsFocused(false);\n    onBlur?.(event);\n  };\n\n  return (\n    <input\n      {...rest}\n      ref={mergeRefs(inputRef, ref)}\n      value={display}\n      onChange={handleChange}\n      onFocus={handleFocus}\n      onBlur={handleBlur}\n      onClick={handleClick}\n      inputMode='numeric'\n    />\n  );\n}\n\nexport default MaskedInput;\n"],"mappings":"gQAgBA,SAAS,EAAY,EAAyB,CAC5C,GAAM,CACJ,OACA,kBAAkB,IAClB,QAAQ,GACR,WACA,0BACA,UACA,SACA,UACA,MACA,GAAG,GACD,EAEE,EAAW,EAAyB,IAAI,EACxC,CAAC,EAAW,GAAgB,EAAS,EAAK,EAM1C,EAAW,EAAsB,IAAI,EAE3C,MAAsB,CAChB,EAAS,UAAY,OACzB,EAAS,SAAS,kBAAkB,EAAS,QAAS,EAAS,OAAO,EACtE,EAAS,QAAU,KACrB,CAAC,EAED,IAAM,EAAQ,EAAQ,EAAO,CAAI,EAI3B,EAAU,EAAS,EAAY,EAAO,GAAI,EAAM,CAAe,EAAI,GAAM,EAAO,EAAO,EAAM,CAAe,EAE5G,EAAY,GAAqB,CACrC,EAAS,QAAU,EACnB,EAAS,SAAS,kBAAkB,EAAU,CAAQ,CACxD,EAEM,EAAgB,GAA+C,CACnE,IAAM,EAAQ,EAAM,OACd,EAAM,EAAM,MACZ,EAAY,EAAW,EAAS,EAAK,EAAM,gBAAkB,EAAI,OAAQ,EAAM,CAAe,EAE9F,EAAQ,EACV,EAAwB,CAAE,cAAe,CAAE,MAAO,EAAS,UAAW,IAAK,EAAG,WAAU,CAAC,EACzF,EAEE,EAAO,EAAO,EAAM,MAAO,EAAM,CAAe,EAChD,EAAQ,EAAM,WAAW,OAAS,EAAgB,EAAM,EAAM,CAAe,EAKnF,GAAI,IAAS,EAAS,CACpB,EAAM,MAAQ,EACd,EAAS,CAAK,EACd,MACF,CAEA,EAAS,QAAU,EACnB,IAAW,EAAQ,EAAM,CAAI,EAAI,GAAK,CAAI,CAC5C,EAEM,EAAe,GAA8C,CACjE,EAAa,EAAI,EAKb,IAAO,EAAS,QAAU,GAC9B,IAAU,CAAK,CACjB,EAOM,EAAe,GAA8C,CACjE,IAAM,EAAQ,EAAM,cACd,EAAQ,EAAgB,EAAS,EAAM,CAAe,EAExD,EAAM,iBAAmB,EAAM,eAAiB,EAAM,gBAAkB,GAAK,GAC/E,EAAM,kBAAkB,EAAO,CAAK,EAEtC,IAAU,CAAK,CACjB,EAEM,EAAc,GAA8C,CAChE,EAAa,EAAK,EAClB,IAAS,CAAK,CAChB,EAEA,OACE,EAAC,QAAD,CACE,GAAI,EACJ,IAAK,EAAU,EAAU,CAAG,EAC5B,MAAO,EACP,SAAU,EACV,QAAS,EACT,OAAQ,EACR,QAAS,EACT,UAAU,SACX,CAAA,CAEL"}