periplo-ui 3.56.0 → 3.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Combobox/Combobox.d.ts +2 -0
- package/dist/components/Combobox/Combobox.js +60 -50
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/InputDateMask/InputDate.d.ts +27 -0
- package/dist/components/InputDateMask/InputDate.js +130 -0
- package/dist/components/InputDateMask/InputDate.js.map +1 -0
- package/dist/components/InputDateMask/MaskedInput.d.ts +18 -0
- package/dist/components/InputDateMask/MaskedInput.js +227 -0
- package/dist/components/InputDateMask/MaskedInput.js.map +1 -0
- package/dist/components/InputDateMask/index.d.ts +1 -0
- package/dist/components/InputDateMask/index.js +2 -0
- package/dist/components/InputDateMask/index.js.map +1 -0
- package/dist/components/InputDateMask/manualDateFormat.d.ts +11 -0
- package/dist/components/InputDateMask/manualDateFormat.js +60 -0
- package/dist/components/InputDateMask/manualDateFormat.js.map +1 -0
- package/package.json +1 -1
|
@@ -49,6 +49,8 @@ type ComboboxBaseProps<T> = {
|
|
|
49
49
|
specialOptionsTitle?: string;
|
|
50
50
|
/** Container element to position the combobox relative to. */
|
|
51
51
|
container?: HTMLElement;
|
|
52
|
+
/** Alignment of the dropdown relative to the trigger. Defaults to 'center'. */
|
|
53
|
+
align?: 'start' | 'center' | 'end';
|
|
52
54
|
};
|
|
53
55
|
export type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {
|
|
54
56
|
multiple?: false;
|
|
@@ -32,7 +32,8 @@ const Combobox = (props) => {
|
|
|
32
32
|
multipleOptionsPlaceholder = "Options",
|
|
33
33
|
specialOptions,
|
|
34
34
|
specialOptionsTitle,
|
|
35
|
-
container
|
|
35
|
+
container,
|
|
36
|
+
align
|
|
36
37
|
} = props;
|
|
37
38
|
const normalizedMaxHeight = typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
|
|
38
39
|
const boundedListMaxHeight = `min(${normalizedMaxHeight}, max(120px, calc(var(--radix-popover-content-available-height, 100dvh) - 68px)))`;
|
|
@@ -129,55 +130,64 @@ const Combobox = (props) => {
|
|
|
129
130
|
)
|
|
130
131
|
}
|
|
131
132
|
),
|
|
132
|
-
/* @__PURE__ */ jsx(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
PopoverContent,
|
|
135
|
+
{
|
|
136
|
+
className: cn("overflow-hidden p-0", contentClassName),
|
|
137
|
+
container,
|
|
138
|
+
side: "bottom",
|
|
139
|
+
align,
|
|
140
|
+
children: /* @__PURE__ */ jsxs(Command, { shouldFilter: false, children: [
|
|
141
|
+
/* @__PURE__ */ jsx(
|
|
142
|
+
CommandInput,
|
|
143
|
+
{
|
|
144
|
+
placeholder: searchPlaceholder,
|
|
145
|
+
disabled: loading,
|
|
146
|
+
value: searchTerm,
|
|
147
|
+
onValueChange: setSearchTerm
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
isVirtualized ? /* @__PURE__ */ jsx(
|
|
151
|
+
VirtualizedComboboxList,
|
|
152
|
+
{
|
|
153
|
+
localOptions: filteredOptions,
|
|
154
|
+
loading,
|
|
155
|
+
loadingPlaceholder,
|
|
156
|
+
emptyMessage,
|
|
157
|
+
value: props.value,
|
|
158
|
+
getOptionValue,
|
|
159
|
+
getOptionLabel,
|
|
160
|
+
handleSelect,
|
|
161
|
+
renderOption,
|
|
162
|
+
maxHeight: boundedListMaxHeight,
|
|
163
|
+
hasNextPage,
|
|
164
|
+
loadingMore,
|
|
165
|
+
onLoadMore: loadNextPage
|
|
166
|
+
}
|
|
167
|
+
) : /* @__PURE__ */ jsx(
|
|
168
|
+
StaticComboboxList,
|
|
169
|
+
{
|
|
170
|
+
filteredOptions,
|
|
171
|
+
loading,
|
|
172
|
+
loadingPlaceholder,
|
|
173
|
+
emptyMessage,
|
|
174
|
+
multiple: !!multiple,
|
|
175
|
+
value: props.value,
|
|
176
|
+
getOptionValue,
|
|
177
|
+
getOptionLabel,
|
|
178
|
+
handleSelect,
|
|
179
|
+
renderOption,
|
|
180
|
+
maxHeight: boundedListMaxHeight,
|
|
181
|
+
selectedMultiplePlaceholder,
|
|
182
|
+
multipleOptionsPlaceholder,
|
|
183
|
+
options,
|
|
184
|
+
specialOptions,
|
|
185
|
+
specialOptionsTitle
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] })
|
|
189
|
+
}
|
|
190
|
+
)
|
|
181
191
|
] }),
|
|
182
192
|
typeof error === "string" && /* @__PURE__ */ jsx("span", { className: "text-error-500 text-sm", children: error })
|
|
183
193
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretDown } from '@phosphor-icons/react/dist/ssr/CaretDown'\nimport { X } from '@phosphor-icons/react/dist/ssr/X'\n\nimport { cn } from '../../lib/utils'\nimport { Button, buttonVariants } from '../Button'\nimport { Command, CommandInput } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\n\nimport { StaticComboboxList } from './StaticComboboxList'\nimport { useCombobox } from './useCombobox'\nimport { VirtualizedComboboxList } from './VirtualizedComboboxList'\n\ntype ComboboxBaseProps<T> = {\n /** Unique identifier for the combobox */\n id?: string\n /** Array of options to display in the combobox */\n options: Array<T>\n /** Function to get the unique identifier from an option. */\n getOptionValue: (option: T) => string\n /** Function to get the display text from an option. */\n getOptionLabel: (option: T) => string\n /** Custom render function for options. If not provided, defaults to showing a checkmark and label */\n renderOption?: (option: T, isSelected: boolean) => React.ReactNode\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Additional CSS classes to apply to the popover content */\n contentClassName?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Whether to close the dropdown when an option is selected. Defaults to `true` for single select and `false` for multi-select. */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n /** Custom function to filter options based on search term */\n filterOptions?: (options: Array<T>, searchTerm: string) => Array<T>\n /** Callback function executed when the clear button is clicked. When provided, an X button will appear on hover to clear the selection. */\n onClear?: boolean | (() => void)\n /** Whether the selection can be cleared */\n clearable?: boolean\n /** Whether the combobox is inside a modal */\n modal?: boolean\n /** Placeholder text for the selected multiple options */\n selectedMultiplePlaceholder?: string\n /** Placeholder text for the multiple options */\n multipleOptionsPlaceholder?: string\n /** Special options that appear at the top with their own title. When selected, the combobox switches to single-select mode. */\n specialOptions?: Array<T>\n /** Title for the special options group */\n specialOptionsTitle?: string\n /** Container element to position the combobox relative to. */\n container?: HTMLElement\n}\n\nexport type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {\n multiple?: false\n value?: string\n onChange: (value: string) => void\n /** Custom render function for the selected value display. */\n renderLabel?: (selectedOption: T) => React.ReactNode\n /**\n * Async pagination function.\n * Note: Multiple selection is not supported with virtualization.\n */\n fetchPage?: (params: {\n page: number\n search?: string\n }) => Promise<{ items: Array<T>; hasNextPage: boolean; nextPage: number }>\n}\n\nexport type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {\n multiple: true\n value?: Array<string>\n onChange: (value: Array<string>) => void\n /** Custom render function for the selected value(s) display. */\n renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode\n /** fetchPage is not allowed with multiple selection */\n fetchPage?: never\n}\n\nexport type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example Basic usage\n * ```tsx\n * interface User {\n * id: string\n * name: string\n * email: string\n * }\n *\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * />\n * ```\n *\n * @example Custom filtering\n * ```tsx\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * filterOptions={(options, searchTerm) =>\n * options.filter(user =>\n * user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n * user.email.toLowerCase().includes(searchTerm.toLowerCase())\n * )\n * }\n * />\n * ```\n *\n * @example Virtualized infinite scrolling with pagination\n * ```tsx\n * <Combobox<User>\n * options={[]} // Initial options can be empty\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * fetchPage={async ({ page, search }) => {\n * const response = await api.getUsers({ page, search })\n * return {\n * items: response.users,\n * hasNextPage: response.page < response.totalPages,\n * nextPage: page + 1\n * }\n * }}\n * />\n * ```\n */\nexport const Combobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n id,\n options,\n getOptionValue,\n getOptionLabel,\n searchPlaceholder = 'Search...',\n emptyMessage = 'No results found.',\n className = 'w-60',\n contentClassName,\n disabled = false,\n maxHeight = '300px',\n renderOption,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n multiple,\n onClear,\n modal = false,\n selectedMultiplePlaceholder = 'Selected',\n multipleOptionsPlaceholder = 'Options',\n specialOptions,\n specialOptionsTitle,\n container,\n } = props\n\n const normalizedMaxHeight = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight\n const boundedListMaxHeight = `min(${normalizedMaxHeight}, max(120px, calc(var(--radix-popover-content-available-height, 100dvh) - 68px)))`\n\n const {\n open,\n setOpen,\n searchTerm,\n setSearchTerm,\n setIsHovered,\n hasNextPage,\n loadingMore,\n isVirtualized,\n filteredOptions,\n displayValue,\n hasValue,\n showClearButton,\n handleSelect,\n handleClear,\n loadNextPage,\n } = useCombobox(props)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen} modal={modal}>\n <PopoverTrigger\n asChild\n onPointerDown={(event) => {\n event.stopPropagation()\n if (open) {\n setOpen(false)\n event.preventDefault()\n } else {\n setOpen(true)\n }\n }}\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n }}\n >\n <Button\n id={id}\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n buttonVariants({ variant: 'input', size: 'lg' }),\n 'relative flex justify-between rounded-lg',\n multiple && 'renderLabel' in props && props.renderLabel ? 'h-auto min-h-12' : 'h-12',\n open && 'border-neutral-950',\n disabled && 'cursor-not-allowed',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span\n className={cn(\n 'block',\n !hasValue && 'text-neutral-300',\n !(multiple && 'renderLabel' in props && props.renderLabel) && 'truncate',\n )}\n >\n {displayValue}\n </span>\n <CaretDown\n className={cn(\n 'h-4 w-4 shrink-0 opacity-50 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-50',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 z-10 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className={cn('overflow-hidden p-0', contentClassName)} container={container} side=\"bottom\">\n <Command shouldFilter={false}>\n <CommandInput\n placeholder={searchPlaceholder}\n disabled={loading}\n value={searchTerm}\n onValueChange={setSearchTerm}\n />\n {isVirtualized ? (\n <VirtualizedComboboxList\n localOptions={filteredOptions}\n loading={loading}\n loadingPlaceholder={loadingPlaceholder}\n emptyMessage={emptyMessage}\n value={props.value as string}\n getOptionValue={getOptionValue}\n getOptionLabel={getOptionLabel}\n handleSelect={handleSelect}\n renderOption={renderOption}\n maxHeight={boundedListMaxHeight}\n hasNextPage={hasNextPage}\n loadingMore={loadingMore}\n onLoadMore={loadNextPage}\n />\n ) : (\n <StaticComboboxList\n filteredOptions={filteredOptions}\n loading={loading}\n loadingPlaceholder={loadingPlaceholder}\n emptyMessage={emptyMessage}\n multiple={!!multiple}\n value={props.value}\n getOptionValue={getOptionValue}\n getOptionLabel={getOptionLabel}\n handleSelect={handleSelect}\n renderOption={renderOption}\n maxHeight={boundedListMaxHeight}\n selectedMultiplePlaceholder={selectedMultiplePlaceholder}\n multipleOptionsPlaceholder={multipleOptionsPlaceholder}\n options={options}\n specialOptions={specialOptions}\n specialOptionsTitle={specialOptionsTitle}\n />\n )}\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-error-500 text-sm\">{error}</span>}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretDown } from '@phosphor-icons/react/dist/ssr/CaretDown'\nimport { X } from '@phosphor-icons/react/dist/ssr/X'\n\nimport { cn } from '../../lib/utils'\nimport { Button, buttonVariants } from '../Button'\nimport { Command, CommandInput } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\n\nimport { StaticComboboxList } from './StaticComboboxList'\nimport { useCombobox } from './useCombobox'\nimport { VirtualizedComboboxList } from './VirtualizedComboboxList'\n\ntype ComboboxBaseProps<T> = {\n /** Unique identifier for the combobox */\n id?: string\n /** Array of options to display in the combobox */\n options: Array<T>\n /** Function to get the unique identifier from an option. */\n getOptionValue: (option: T) => string\n /** Function to get the display text from an option. */\n getOptionLabel: (option: T) => string\n /** Custom render function for options. If not provided, defaults to showing a checkmark and label */\n renderOption?: (option: T, isSelected: boolean) => React.ReactNode\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Additional CSS classes to apply to the popover content */\n contentClassName?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Whether to close the dropdown when an option is selected. Defaults to `true` for single select and `false` for multi-select. */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n /** Custom function to filter options based on search term */\n filterOptions?: (options: Array<T>, searchTerm: string) => Array<T>\n /** Callback function executed when the clear button is clicked. When provided, an X button will appear on hover to clear the selection. */\n onClear?: boolean | (() => void)\n /** Whether the selection can be cleared */\n clearable?: boolean\n /** Whether the combobox is inside a modal */\n modal?: boolean\n /** Placeholder text for the selected multiple options */\n selectedMultiplePlaceholder?: string\n /** Placeholder text for the multiple options */\n multipleOptionsPlaceholder?: string\n /** Special options that appear at the top with their own title. When selected, the combobox switches to single-select mode. */\n specialOptions?: Array<T>\n /** Title for the special options group */\n specialOptionsTitle?: string\n /** Container element to position the combobox relative to. */\n container?: HTMLElement\n /** Alignment of the dropdown relative to the trigger. Defaults to 'center'. */\n align?: 'start' | 'center' | 'end'\n}\n\nexport type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {\n multiple?: false\n value?: string\n onChange: (value: string) => void\n /** Custom render function for the selected value display. */\n renderLabel?: (selectedOption: T) => React.ReactNode\n /**\n * Async pagination function.\n * Note: Multiple selection is not supported with virtualization.\n */\n fetchPage?: (params: {\n page: number\n search?: string\n }) => Promise<{ items: Array<T>; hasNextPage: boolean; nextPage: number }>\n}\n\nexport type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {\n multiple: true\n value?: Array<string>\n onChange: (value: Array<string>) => void\n /** Custom render function for the selected value(s) display. */\n renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode\n /** fetchPage is not allowed with multiple selection */\n fetchPage?: never\n}\n\nexport type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example Basic usage\n * ```tsx\n * interface User {\n * id: string\n * name: string\n * email: string\n * }\n *\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * />\n * ```\n *\n * @example Custom filtering\n * ```tsx\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * filterOptions={(options, searchTerm) =>\n * options.filter(user =>\n * user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n * user.email.toLowerCase().includes(searchTerm.toLowerCase())\n * )\n * }\n * />\n * ```\n *\n * @example Virtualized infinite scrolling with pagination\n * ```tsx\n * <Combobox<User>\n * options={[]} // Initial options can be empty\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * fetchPage={async ({ page, search }) => {\n * const response = await api.getUsers({ page, search })\n * return {\n * items: response.users,\n * hasNextPage: response.page < response.totalPages,\n * nextPage: page + 1\n * }\n * }}\n * />\n * ```\n */\nexport const Combobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n id,\n options,\n getOptionValue,\n getOptionLabel,\n searchPlaceholder = 'Search...',\n emptyMessage = 'No results found.',\n className = 'w-60',\n contentClassName,\n disabled = false,\n maxHeight = '300px',\n renderOption,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n multiple,\n onClear,\n modal = false,\n selectedMultiplePlaceholder = 'Selected',\n multipleOptionsPlaceholder = 'Options',\n specialOptions,\n specialOptionsTitle,\n container,\n align,\n } = props\n\n const normalizedMaxHeight = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight\n const boundedListMaxHeight = `min(${normalizedMaxHeight}, max(120px, calc(var(--radix-popover-content-available-height, 100dvh) - 68px)))`\n\n const {\n open,\n setOpen,\n searchTerm,\n setSearchTerm,\n setIsHovered,\n hasNextPage,\n loadingMore,\n isVirtualized,\n filteredOptions,\n displayValue,\n hasValue,\n showClearButton,\n handleSelect,\n handleClear,\n loadNextPage,\n } = useCombobox(props)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen} modal={modal}>\n <PopoverTrigger\n asChild\n onPointerDown={(event) => {\n event.stopPropagation()\n if (open) {\n setOpen(false)\n event.preventDefault()\n } else {\n setOpen(true)\n }\n }}\n onClick={(event) => {\n event.preventDefault()\n event.stopPropagation()\n }}\n >\n <Button\n id={id}\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n buttonVariants({ variant: 'input', size: 'lg' }),\n 'relative flex justify-between rounded-lg',\n multiple && 'renderLabel' in props && props.renderLabel ? 'h-auto min-h-12' : 'h-12',\n open && 'border-neutral-950',\n disabled && 'cursor-not-allowed',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span\n className={cn(\n 'block',\n !hasValue && 'text-neutral-300',\n !(multiple && 'renderLabel' in props && props.renderLabel) && 'truncate',\n )}\n >\n {displayValue}\n </span>\n <CaretDown\n className={cn(\n 'h-4 w-4 shrink-0 opacity-50 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-50',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 z-10 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n className={cn('overflow-hidden p-0', contentClassName)}\n container={container}\n side=\"bottom\"\n align={align}\n >\n <Command shouldFilter={false}>\n <CommandInput\n placeholder={searchPlaceholder}\n disabled={loading}\n value={searchTerm}\n onValueChange={setSearchTerm}\n />\n {isVirtualized ? (\n <VirtualizedComboboxList\n localOptions={filteredOptions}\n loading={loading}\n loadingPlaceholder={loadingPlaceholder}\n emptyMessage={emptyMessage}\n value={props.value as string}\n getOptionValue={getOptionValue}\n getOptionLabel={getOptionLabel}\n handleSelect={handleSelect}\n renderOption={renderOption}\n maxHeight={boundedListMaxHeight}\n hasNextPage={hasNextPage}\n loadingMore={loadingMore}\n onLoadMore={loadNextPage}\n />\n ) : (\n <StaticComboboxList\n filteredOptions={filteredOptions}\n loading={loading}\n loadingPlaceholder={loadingPlaceholder}\n emptyMessage={emptyMessage}\n multiple={!!multiple}\n value={props.value}\n getOptionValue={getOptionValue}\n getOptionLabel={getOptionLabel}\n handleSelect={handleSelect}\n renderOption={renderOption}\n maxHeight={boundedListMaxHeight}\n selectedMultiplePlaceholder={selectedMultiplePlaceholder}\n multipleOptionsPlaceholder={multipleOptionsPlaceholder}\n options={options}\n specialOptions={specialOptions}\n specialOptionsTitle={specialOptionsTitle}\n />\n )}\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-error-500 text-sm\">{error}</span>}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DateInputFormat } from './manualDateFormat';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
export type InputDateProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
endContent?: React.ReactElement;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
error?: boolean | string;
|
|
8
|
+
errorMessage?: string;
|
|
9
|
+
inputClassName?: string;
|
|
10
|
+
inputFormat?: DateInputFormat;
|
|
11
|
+
onClear?: boolean | (() => void);
|
|
12
|
+
value?: string;
|
|
13
|
+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
14
|
+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'>;
|
|
15
|
+
declare const InputDate: React.ForwardRefExoticComponent<{
|
|
16
|
+
className?: string;
|
|
17
|
+
endContent?: React.ReactElement;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
error?: boolean | string;
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
inputClassName?: string;
|
|
22
|
+
inputFormat?: DateInputFormat;
|
|
23
|
+
onClear?: boolean | (() => void);
|
|
24
|
+
value?: string;
|
|
25
|
+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
26
|
+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "type"> & React.RefAttributes<HTMLInputElement>>;
|
|
27
|
+
export { InputDate };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { CalendarBlank } from '@phosphor-icons/react/dist/ssr/CalendarBlank';
|
|
3
|
+
import { WarningCircle } from '@phosphor-icons/react/dist/ssr/WarningCircle';
|
|
4
|
+
import { X } from '@phosphor-icons/react/dist/ssr/X';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { MaskedInput } from './MaskedInput.js';
|
|
7
|
+
import { cn } from '../../lib/utils.js';
|
|
8
|
+
|
|
9
|
+
const InputDate = React.forwardRef(
|
|
10
|
+
({
|
|
11
|
+
className,
|
|
12
|
+
endContent,
|
|
13
|
+
disabled,
|
|
14
|
+
error,
|
|
15
|
+
errorMessage,
|
|
16
|
+
value: controlledValue,
|
|
17
|
+
onChange: controlledOnChange,
|
|
18
|
+
inputClassName,
|
|
19
|
+
onClear = true,
|
|
20
|
+
inputFormat,
|
|
21
|
+
"aria-invalid": ariaInvalid,
|
|
22
|
+
...props
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const [internalValue, setInternalValue] = React.useState(controlledValue ?? "");
|
|
25
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
setInternalValue(controlledValue ?? "");
|
|
28
|
+
}, [controlledValue]);
|
|
29
|
+
const handleChange = (event) => {
|
|
30
|
+
setInternalValue(event.target.value);
|
|
31
|
+
controlledOnChange?.(event);
|
|
32
|
+
};
|
|
33
|
+
const handleMaskedChange = (newValue) => {
|
|
34
|
+
setInternalValue(newValue);
|
|
35
|
+
controlledOnChange?.({
|
|
36
|
+
target: { value: newValue }
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const handleClear = () => {
|
|
40
|
+
handleChange({ target: { value: "" } });
|
|
41
|
+
if (typeof onClear === "function") onClear();
|
|
42
|
+
};
|
|
43
|
+
const hasValue = Boolean(internalValue && internalValue.trim() !== "");
|
|
44
|
+
const showClearIcon = Boolean(onClear && hasValue && isHovered && !disabled);
|
|
45
|
+
const resolvedErrorMessage = typeof error === "string" ? error : errorMessage;
|
|
46
|
+
const hasError = Boolean(error ?? resolvedErrorMessage);
|
|
47
|
+
const renderCalendarIcon = () => /* @__PURE__ */ jsx(CalendarBlank, { size: 20 });
|
|
48
|
+
const renderErrorIcon = () => {
|
|
49
|
+
if (!hasError) return null;
|
|
50
|
+
return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
51
|
+
WarningCircle,
|
|
52
|
+
{
|
|
53
|
+
"data-testid": "exclaim-icon",
|
|
54
|
+
className: cn(
|
|
55
|
+
"text-error-500 transition-opacity duration-150",
|
|
56
|
+
showClearIcon ? "opacity-0" : "opacity-100"
|
|
57
|
+
),
|
|
58
|
+
size: 18
|
|
59
|
+
}
|
|
60
|
+
) });
|
|
61
|
+
};
|
|
62
|
+
const renderClearIcon = () => {
|
|
63
|
+
if (disabled || !onClear) return null;
|
|
64
|
+
return /* @__PURE__ */ jsx(
|
|
65
|
+
"div",
|
|
66
|
+
{
|
|
67
|
+
className: cn(
|
|
68
|
+
"absolute right-3 z-10 flex h-full w-10 items-center justify-end rounded-r-lg",
|
|
69
|
+
endContent || error ? "bg-transparent" : "bg-gradient-to-l from-white from-60% via-white/80 via-80% to-transparent transition-opacity duration-150",
|
|
70
|
+
showClearIcon ? "opacity-100" : "opacity-0"
|
|
71
|
+
),
|
|
72
|
+
children: /* @__PURE__ */ jsx(
|
|
73
|
+
X,
|
|
74
|
+
{
|
|
75
|
+
"data-testid": "clear-button",
|
|
76
|
+
className: cn(
|
|
77
|
+
"h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150",
|
|
78
|
+
showClearIcon ? "opacity-100 hover:opacity-70" : "opacity-0"
|
|
79
|
+
),
|
|
80
|
+
onClick: handleClear
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-1", children: [
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "flex w-full items-center", children: /* @__PURE__ */ jsxs(
|
|
88
|
+
"label",
|
|
89
|
+
{
|
|
90
|
+
"data-testid": "input-date-mask-control",
|
|
91
|
+
className: cn(
|
|
92
|
+
"relative flex h-12 w-full items-center rounded-lg border border-neutral-200 bg-white px-3 transition-colors focus-within:border-neutral-950",
|
|
93
|
+
disabled && "bg-neutral-50",
|
|
94
|
+
hasError && "border-error-400 focus-within:border-error-700",
|
|
95
|
+
className
|
|
96
|
+
),
|
|
97
|
+
onMouseEnter: () => setIsHovered(true),
|
|
98
|
+
onMouseLeave: () => setIsHovered(false),
|
|
99
|
+
children: [
|
|
100
|
+
renderCalendarIcon(),
|
|
101
|
+
/* @__PURE__ */ jsx(
|
|
102
|
+
MaskedInput,
|
|
103
|
+
{
|
|
104
|
+
ref,
|
|
105
|
+
inputFormat,
|
|
106
|
+
placeholder: inputFormat,
|
|
107
|
+
"aria-invalid": ariaInvalid ?? hasError,
|
|
108
|
+
disabled,
|
|
109
|
+
value: internalValue,
|
|
110
|
+
onChange: handleMaskedChange,
|
|
111
|
+
className: cn(
|
|
112
|
+
"w-full bg-transparent px-2 outline-0 transition-colors placeholder:text-neutral-300 disabled:cursor-not-allowed disabled:opacity-50",
|
|
113
|
+
inputClassName
|
|
114
|
+
),
|
|
115
|
+
...props
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
renderErrorIcon(),
|
|
119
|
+
renderClearIcon()
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
) }),
|
|
123
|
+
resolvedErrorMessage && /* @__PURE__ */ jsx("span", { className: "text-error-500 text-sm", children: resolvedErrorMessage })
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
InputDate.displayName = "InputDate";
|
|
128
|
+
|
|
129
|
+
export { InputDate };
|
|
130
|
+
//# sourceMappingURL=InputDate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InputDate.js","sources":["../../../src/components/InputDateMask/InputDate.tsx"],"sourcesContent":["import { CalendarBlank } from '@phosphor-icons/react/dist/ssr/CalendarBlank'\nimport { WarningCircle } from '@phosphor-icons/react/dist/ssr/WarningCircle'\nimport { X } from '@phosphor-icons/react/dist/ssr/X'\nimport * as React from 'react'\n\nimport { type DateInputFormat } from './manualDateFormat'\nimport { MaskedInput } from './MaskedInput'\n\nimport { cn } from '@/lib/utils'\n\nexport type InputDateProps = {\n className?: string\n endContent?: React.ReactElement\n disabled?: boolean\n error?: boolean | string\n errorMessage?: string\n inputClassName?: string\n inputFormat?: DateInputFormat\n onClear?: boolean | (() => void)\n value?: string\n onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void\n} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'>\n\nconst InputDate = React.forwardRef<HTMLInputElement, InputDateProps>(\n (\n {\n className,\n endContent,\n disabled,\n error,\n errorMessage,\n value: controlledValue,\n onChange: controlledOnChange,\n inputClassName,\n onClear = true,\n inputFormat,\n 'aria-invalid': ariaInvalid,\n ...props\n },\n ref,\n ) => {\n const [internalValue, setInternalValue] = React.useState(controlledValue ?? '')\n const [isHovered, setIsHovered] = React.useState(false)\n\n React.useEffect(() => {\n setInternalValue(controlledValue ?? '')\n }, [controlledValue])\n\n const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n setInternalValue(event.target.value)\n controlledOnChange?.(event)\n }\n\n const handleMaskedChange = (newValue: string) => {\n setInternalValue(newValue)\n controlledOnChange?.({\n target: { value: newValue },\n } as React.ChangeEvent<HTMLInputElement>)\n }\n\n const handleClear = () => {\n handleChange({ target: { value: '' } } as React.ChangeEvent<HTMLInputElement>)\n if (typeof onClear === 'function') onClear()\n }\n\n const hasValue = Boolean(internalValue && internalValue.trim() !== '')\n const showClearIcon = Boolean(onClear && hasValue && isHovered && !disabled)\n const resolvedErrorMessage = typeof error === 'string' ? error : errorMessage\n const hasError = Boolean(error ?? resolvedErrorMessage)\n\n const renderCalendarIcon = () => <CalendarBlank size={20} />\n\n const renderErrorIcon = () => {\n if (!hasError) return null\n return (\n <div>\n <WarningCircle\n data-testid=\"exclaim-icon\"\n className={cn(\n 'text-error-500 transition-opacity duration-150',\n showClearIcon ? 'opacity-0' : 'opacity-100',\n )}\n size={18}\n />\n </div>\n )\n }\n\n const renderClearIcon = () => {\n if (disabled || !onClear) return null\n return (\n <div\n className={cn(\n 'absolute right-3 z-10 flex h-full w-10 items-center justify-end rounded-r-lg',\n endContent || error\n ? 'bg-transparent'\n : 'bg-gradient-to-l from-white from-60% via-white/80 via-80% to-transparent transition-opacity duration-150',\n showClearIcon ? 'opacity-100' : 'opacity-0',\n )}\n >\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearIcon ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n </div>\n )\n }\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <div className=\"flex w-full items-center\">\n <label\n data-testid=\"input-date-mask-control\"\n className={cn(\n 'relative flex h-12 w-full items-center rounded-lg border border-neutral-200 bg-white px-3 transition-colors focus-within:border-neutral-950',\n disabled && 'bg-neutral-50',\n hasError && 'border-error-400 focus-within:border-error-700',\n className,\n )}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {renderCalendarIcon()}\n <MaskedInput\n ref={ref}\n inputFormat={inputFormat}\n placeholder={inputFormat}\n aria-invalid={ariaInvalid ?? hasError}\n disabled={disabled}\n value={internalValue}\n onChange={handleMaskedChange}\n className={cn(\n 'w-full bg-transparent px-2 outline-0 transition-colors placeholder:text-neutral-300 disabled:cursor-not-allowed disabled:opacity-50',\n inputClassName,\n )}\n {...props}\n />\n {renderErrorIcon()}\n {renderClearIcon()}\n </label>\n </div>\n {resolvedErrorMessage && <span className=\"text-error-500 text-sm\">{resolvedErrorMessage}</span>}\n </div>\n )\n },\n)\n\nInputDate.displayName = 'InputDate'\n\nexport { InputDate }\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export type DateInputFormat = 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD';
|
|
3
|
+
type Props = {
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
|
7
|
+
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
|
8
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
'aria-invalid'?: React.InputHTMLAttributes<HTMLInputElement>['aria-invalid'];
|
|
15
|
+
inputFormat?: DateInputFormat;
|
|
16
|
+
};
|
|
17
|
+
export declare const MaskedInput: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLInputElement>>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import React__default from 'react';
|
|
4
|
+
import { normalizeManualInput, parseManualDateString } from './manualDateFormat.js';
|
|
5
|
+
import { cn } from '../../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
const FORMAT_CONFIG = {
|
|
8
|
+
"DD/MM/YYYY": {
|
|
9
|
+
template: "DD/MM/YYYY",
|
|
10
|
+
order: ["day", "month", "year"],
|
|
11
|
+
placeholders: { 0: "D", 1: "D", 3: "M", 4: "M", 6: "Y", 7: "Y", 8: "Y", 9: "Y" },
|
|
12
|
+
separators: [2, 5]
|
|
13
|
+
},
|
|
14
|
+
"MM/DD/YYYY": {
|
|
15
|
+
template: "MM/DD/YYYY",
|
|
16
|
+
order: ["month", "day", "year"],
|
|
17
|
+
placeholders: { 0: "M", 1: "M", 3: "D", 4: "D", 6: "Y", 7: "Y", 8: "Y", 9: "Y" },
|
|
18
|
+
separators: [2, 5]
|
|
19
|
+
},
|
|
20
|
+
"YYYY/MM/DD": {
|
|
21
|
+
template: "YYYY/MM/DD",
|
|
22
|
+
order: ["year", "month", "day"],
|
|
23
|
+
placeholders: { 0: "Y", 1: "Y", 2: "Y", 3: "Y", 5: "M", 6: "M", 8: "D", 9: "D" },
|
|
24
|
+
separators: [4, 7]
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const getSegmentIndex = (pos, s1, s2) => {
|
|
28
|
+
if (pos < s1) return 0;
|
|
29
|
+
if (pos < s2) return 1;
|
|
30
|
+
return 2;
|
|
31
|
+
};
|
|
32
|
+
const getFirstPosOfSegment = (segmentIndex, separators) => {
|
|
33
|
+
const [sep1, sep2] = separators;
|
|
34
|
+
if (segmentIndex === 0) return 0;
|
|
35
|
+
if (segmentIndex === 1) return sep1 + 1;
|
|
36
|
+
return sep2 + 1;
|
|
37
|
+
};
|
|
38
|
+
const applyFirstDigitAutopad = (newChars, targetPos, digit, segmentType) => {
|
|
39
|
+
const shouldPadDay = segmentType === "day" && Number.parseInt(digit) > 3;
|
|
40
|
+
const shouldPadMonth = segmentType === "month" && Number.parseInt(digit) > 1;
|
|
41
|
+
if (shouldPadDay || shouldPadMonth) {
|
|
42
|
+
newChars[targetPos] = "0";
|
|
43
|
+
newChars[targetPos + 1] = digit;
|
|
44
|
+
return targetPos + 1;
|
|
45
|
+
}
|
|
46
|
+
return targetPos;
|
|
47
|
+
};
|
|
48
|
+
const validateSecondDigit = (newChars, targetPos, segmentType) => {
|
|
49
|
+
const firstPos = targetPos - 1;
|
|
50
|
+
if (segmentType === "day") {
|
|
51
|
+
const day = Number.parseInt(newChars[firstPos] + newChars[targetPos]);
|
|
52
|
+
return newChars[firstPos] === "D" || day <= 31;
|
|
53
|
+
}
|
|
54
|
+
if (segmentType === "month") {
|
|
55
|
+
const month = Number.parseInt(newChars[firstPos] + newChars[targetPos]);
|
|
56
|
+
return newChars[firstPos] === "M" || month <= 12;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const validateYearDigit = (newChars, targetPos, digit, yearFirstPos) => {
|
|
61
|
+
const yearDigitIndex = targetPos - yearFirstPos;
|
|
62
|
+
if (yearDigitIndex === 0) {
|
|
63
|
+
return digit === "1" || digit === "2";
|
|
64
|
+
}
|
|
65
|
+
if (yearDigitIndex === 1) {
|
|
66
|
+
const firstDigit = newChars[yearFirstPos];
|
|
67
|
+
if (firstDigit === "1") return digit === "9";
|
|
68
|
+
if (firstDigit === "2") return digit === "0";
|
|
69
|
+
}
|
|
70
|
+
if (yearDigitIndex === 2) {
|
|
71
|
+
const twoDigits = newChars[yearFirstPos] + newChars[yearFirstPos + 1];
|
|
72
|
+
if (twoDigits === "20") return Number.parseInt(digit) <= 5;
|
|
73
|
+
}
|
|
74
|
+
if (yearDigitIndex === 3) {
|
|
75
|
+
const yearStr = newChars.slice(yearFirstPos, yearFirstPos + 3).join("") + digit;
|
|
76
|
+
const year = Number.parseInt(yearStr, 10);
|
|
77
|
+
return !Number.isNaN(year) && year >= 1922 && year <= 2300;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
const MaskedInput = React__default.forwardRef(
|
|
82
|
+
({ value, onChange, placeholder, inputFormat = "DD/MM/YYYY", onBlur, onFocus, onKeyDown, className, ...rest }, ref) => {
|
|
83
|
+
const inputRef = React__default.useRef(null);
|
|
84
|
+
React__default.useImperativeHandle(ref, () => inputRef.current);
|
|
85
|
+
const config = FORMAT_CONFIG[inputFormat];
|
|
86
|
+
const [sep1, sep2] = config.separators;
|
|
87
|
+
const maxPos = 9;
|
|
88
|
+
const getNextPos = (pos) => {
|
|
89
|
+
const next = pos + 1;
|
|
90
|
+
if (next === sep1 || next === sep2) return next + 1;
|
|
91
|
+
return Math.min(next, maxPos + 1);
|
|
92
|
+
};
|
|
93
|
+
const handleDelete = (pos, isDelete) => {
|
|
94
|
+
const input = inputRef.current;
|
|
95
|
+
if (!input) return;
|
|
96
|
+
let targetPos = isDelete ? pos : pos - 1;
|
|
97
|
+
if (targetPos === sep1 || targetPos === sep2) {
|
|
98
|
+
targetPos = isDelete ? targetPos + 1 : targetPos - 1;
|
|
99
|
+
}
|
|
100
|
+
if (targetPos < 0 || targetPos > maxPos) return;
|
|
101
|
+
const chars = (value || config.template).split("");
|
|
102
|
+
chars[targetPos] = config.placeholders[targetPos] ?? chars[targetPos];
|
|
103
|
+
const newValue = chars.join("");
|
|
104
|
+
onChange(newValue === config.template ? "" : newValue);
|
|
105
|
+
requestAnimationFrame(() => {
|
|
106
|
+
input.setSelectionRange(targetPos, targetPos);
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const handleDigit = (pos, digit) => {
|
|
110
|
+
const input = inputRef.current;
|
|
111
|
+
if (!input) return;
|
|
112
|
+
let targetPos = pos;
|
|
113
|
+
if (targetPos === sep1 || targetPos === sep2) targetPos++;
|
|
114
|
+
if (targetPos > maxPos) return;
|
|
115
|
+
const chars = (value || config.template).split("");
|
|
116
|
+
const newChars = [...chars];
|
|
117
|
+
newChars[targetPos] = digit;
|
|
118
|
+
const segmentIndex = getSegmentIndex(targetPos, sep1, sep2);
|
|
119
|
+
const segmentType = config.order[segmentIndex];
|
|
120
|
+
const firstPosOfSegment = getFirstPosOfSegment(segmentIndex, config.separators);
|
|
121
|
+
const isFirstDigit = targetPos === firstPosOfSegment;
|
|
122
|
+
const isSecondDigit = targetPos === firstPosOfSegment + 1;
|
|
123
|
+
if (isFirstDigit) {
|
|
124
|
+
targetPos = applyFirstDigitAutopad(newChars, targetPos, digit, segmentType);
|
|
125
|
+
}
|
|
126
|
+
if (isSecondDigit && !validateSecondDigit(newChars, targetPos, segmentType)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (segmentType === "year" && !validateYearDigit(newChars, targetPos, digit, firstPosOfSegment)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
newChars[targetPos] = digit;
|
|
133
|
+
const nextPos = getNextPos(targetPos);
|
|
134
|
+
const result = newChars.join("");
|
|
135
|
+
onChange(result === config.template ? "" : result);
|
|
136
|
+
requestAnimationFrame(() => {
|
|
137
|
+
input.setSelectionRange(nextPos, nextPos);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const handleBlur = (event) => {
|
|
141
|
+
const trimmed = (value || "").trim();
|
|
142
|
+
if (trimmed) {
|
|
143
|
+
const normalized = normalizeManualInput(trimmed, inputFormat);
|
|
144
|
+
const parsed = parseManualDateString(normalized, inputFormat);
|
|
145
|
+
if (!parsed) {
|
|
146
|
+
onChange("");
|
|
147
|
+
} else if (normalized !== value) {
|
|
148
|
+
onChange(normalized);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
onBlur?.(event);
|
|
152
|
+
};
|
|
153
|
+
const handleKeyDown = (event) => {
|
|
154
|
+
onKeyDown?.(event);
|
|
155
|
+
const input = inputRef.current;
|
|
156
|
+
if (!input) return;
|
|
157
|
+
const pos = input.selectionStart;
|
|
158
|
+
if (pos === null) return;
|
|
159
|
+
if (event.key === "Tab" || event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "Enter" || event.key === "ArrowUp" || event.key === "ArrowDown" || event.ctrlKey || event.metaKey) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
164
|
+
handleDelete(pos, event.key === "Delete");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!/^\d$/.test(event.key)) return;
|
|
168
|
+
handleDigit(pos, event.key);
|
|
169
|
+
};
|
|
170
|
+
const handleClick = () => {
|
|
171
|
+
const input = inputRef.current;
|
|
172
|
+
if (!input) return;
|
|
173
|
+
const pos = input.selectionStart ?? 0;
|
|
174
|
+
const getTargetPos = (position) => {
|
|
175
|
+
if (position === sep1) return position - 1;
|
|
176
|
+
if (position === sep2) return position - 1;
|
|
177
|
+
return position;
|
|
178
|
+
};
|
|
179
|
+
requestAnimationFrame(() => {
|
|
180
|
+
input.setSelectionRange(getTargetPos(pos), getTargetPos(pos));
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
const renderOverlay = () => {
|
|
184
|
+
if (!value) return null;
|
|
185
|
+
const display = value.padEnd(10, " ");
|
|
186
|
+
const template = config.template;
|
|
187
|
+
return /* @__PURE__ */ jsx(
|
|
188
|
+
"div",
|
|
189
|
+
{
|
|
190
|
+
"aria-hidden": "true",
|
|
191
|
+
className: "pointer-events-none absolute inset-0 flex items-center px-2 text-base select-none",
|
|
192
|
+
children: display.split("").map((char, index) => {
|
|
193
|
+
const isPlaceholderChar = template[index] !== "/" && char === template[index];
|
|
194
|
+
const stableKey = `${template[index]}-${index}`;
|
|
195
|
+
return /* @__PURE__ */ jsx("span", { className: isPlaceholderChar ? "text-neutral-300" : "text-neutral-900", children: char }, stableKey);
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex w-full items-center", children: [
|
|
201
|
+
/* @__PURE__ */ jsx(
|
|
202
|
+
"input",
|
|
203
|
+
{
|
|
204
|
+
ref: inputRef,
|
|
205
|
+
type: "text",
|
|
206
|
+
autoComplete: "off",
|
|
207
|
+
value: value || "",
|
|
208
|
+
placeholder: placeholder ?? inputFormat,
|
|
209
|
+
onKeyDown: handleKeyDown,
|
|
210
|
+
onClick: handleClick,
|
|
211
|
+
onChange: () => {
|
|
212
|
+
},
|
|
213
|
+
onBlur: handleBlur,
|
|
214
|
+
onFocus,
|
|
215
|
+
size: 1,
|
|
216
|
+
className: cn(className, value ? "text-transparent caret-neutral-900" : ""),
|
|
217
|
+
...rest
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
renderOverlay()
|
|
221
|
+
] });
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
MaskedInput.displayName = "MaskedInput";
|
|
225
|
+
|
|
226
|
+
export { MaskedInput };
|
|
227
|
+
//# sourceMappingURL=MaskedInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MaskedInput.js","sources":["../../../src/components/InputDateMask/MaskedInput.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\n\nimport { normalizeManualInput, parseManualDateString } from './manualDateFormat'\n\nimport { cn } from '@/lib/utils'\n\nexport type DateInputFormat = 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD'\n\ntype Props = {\n value: string\n onChange: (value: string) => void\n onBlur?: React.FocusEventHandler<HTMLInputElement>\n onFocus?: React.FocusEventHandler<HTMLInputElement>\n onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>\n placeholder?: string\n disabled?: boolean\n id?: string\n name?: string\n className?: string\n 'aria-invalid'?: React.InputHTMLAttributes<HTMLInputElement>['aria-invalid']\n inputFormat?: DateInputFormat\n}\n\ntype SegmentType = 'day' | 'month' | 'year'\n\ntype FormatConfig = {\n template: string\n order: [SegmentType, SegmentType, SegmentType]\n placeholders: Record<number, string>\n separators: [number, number]\n}\n\nconst FORMAT_CONFIG: Record<DateInputFormat, FormatConfig> = {\n 'DD/MM/YYYY': {\n template: 'DD/MM/YYYY',\n order: ['day', 'month', 'year'],\n placeholders: { 0: 'D', 1: 'D', 3: 'M', 4: 'M', 6: 'Y', 7: 'Y', 8: 'Y', 9: 'Y' },\n separators: [2, 5],\n },\n 'MM/DD/YYYY': {\n template: 'MM/DD/YYYY',\n order: ['month', 'day', 'year'],\n placeholders: { 0: 'M', 1: 'M', 3: 'D', 4: 'D', 6: 'Y', 7: 'Y', 8: 'Y', 9: 'Y' },\n separators: [2, 5],\n },\n 'YYYY/MM/DD': {\n template: 'YYYY/MM/DD',\n order: ['year', 'month', 'day'],\n placeholders: { 0: 'Y', 1: 'Y', 2: 'Y', 3: 'Y', 5: 'M', 6: 'M', 8: 'D', 9: 'D' },\n separators: [4, 7],\n },\n}\n\nconst getSegmentIndex = (pos: number, s1: number, s2: number): number => {\n if (pos < s1) return 0\n if (pos < s2) return 1\n return 2\n}\n\nconst getFirstPosOfSegment = (segmentIndex: number, separators: [number, number]): number => {\n const [sep1, sep2] = separators\n if (segmentIndex === 0) return 0\n if (segmentIndex === 1) return sep1 + 1\n return sep2 + 1\n}\n\nconst applyFirstDigitAutopad = (\n newChars: Array<string>,\n targetPos: number,\n digit: string,\n segmentType: SegmentType,\n): number => {\n const shouldPadDay = segmentType === 'day' && Number.parseInt(digit) > 3\n const shouldPadMonth = segmentType === 'month' && Number.parseInt(digit) > 1\n\n if (shouldPadDay || shouldPadMonth) {\n newChars[targetPos] = '0'\n newChars[targetPos + 1] = digit\n return targetPos + 1\n }\n return targetPos\n}\n\nconst validateSecondDigit = (newChars: Array<string>, targetPos: number, segmentType: SegmentType): boolean => {\n const firstPos = targetPos - 1\n if (segmentType === 'day') {\n const day = Number.parseInt(newChars[firstPos] + newChars[targetPos])\n return newChars[firstPos] === 'D' || day <= 31\n }\n if (segmentType === 'month') {\n const month = Number.parseInt(newChars[firstPos] + newChars[targetPos])\n return newChars[firstPos] === 'M' || month <= 12\n }\n return true\n}\n\nconst validateYearDigit = (\n newChars: Array<string>,\n targetPos: number,\n digit: string,\n yearFirstPos: number,\n): boolean => {\n const yearDigitIndex = targetPos - yearFirstPos\n\n if (yearDigitIndex === 0) {\n return digit === '1' || digit === '2'\n }\n\n if (yearDigitIndex === 1) {\n const firstDigit = newChars[yearFirstPos]\n if (firstDigit === '1') return digit === '9'\n if (firstDigit === '2') return digit === '0'\n }\n\n if (yearDigitIndex === 2) {\n const twoDigits = newChars[yearFirstPos] + newChars[yearFirstPos + 1]\n if (twoDigits === '20') return Number.parseInt(digit) <= 5\n }\n\n if (yearDigitIndex === 3) {\n const yearStr = newChars.slice(yearFirstPos, yearFirstPos + 3).join('') + digit\n const year = Number.parseInt(yearStr, 10)\n return !Number.isNaN(year) && year >= 1922 && year <= 2300\n }\n\n return true\n}\n\nexport const MaskedInput = React.forwardRef<HTMLInputElement, Props>(\n (\n { value, onChange, placeholder, inputFormat = 'DD/MM/YYYY', onBlur, onFocus, onKeyDown, className, ...rest },\n ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n React.useImperativeHandle(ref, () => inputRef.current!)\n\n const config = FORMAT_CONFIG[inputFormat]\n const [sep1, sep2] = config.separators\n\n const maxPos = 9\n\n const getNextPos = (pos: number): number => {\n const next = pos + 1\n if (next === sep1 || next === sep2) return next + 1\n return Math.min(next, maxPos + 1)\n }\n\n const handleDelete = (pos: number, isDelete: boolean) => {\n const input = inputRef.current\n if (!input) return\n\n let targetPos = isDelete ? pos : pos - 1\n if (targetPos === sep1 || targetPos === sep2) {\n targetPos = isDelete ? targetPos + 1 : targetPos - 1\n }\n if (targetPos < 0 || targetPos > maxPos) return\n\n const chars = (value || config.template).split('')\n chars[targetPos] = config.placeholders[targetPos] ?? chars[targetPos]\n\n const newValue = chars.join('')\n onChange(newValue === config.template ? '' : newValue)\n\n requestAnimationFrame(() => {\n input.setSelectionRange(targetPos, targetPos)\n })\n }\n\n const handleDigit = (pos: number, digit: string) => {\n const input = inputRef.current\n if (!input) return\n\n let targetPos = pos\n if (targetPos === sep1 || targetPos === sep2) targetPos++\n if (targetPos > maxPos) return\n\n const chars = (value || config.template).split('')\n const newChars = [...chars]\n newChars[targetPos] = digit\n\n const segmentIndex = getSegmentIndex(targetPos, sep1, sep2)\n const segmentType = config.order[segmentIndex]\n const firstPosOfSegment = getFirstPosOfSegment(segmentIndex, config.separators)\n const isFirstDigit = targetPos === firstPosOfSegment\n const isSecondDigit = targetPos === firstPosOfSegment + 1\n\n if (isFirstDigit) {\n targetPos = applyFirstDigitAutopad(newChars, targetPos, digit, segmentType)\n }\n\n if (isSecondDigit && !validateSecondDigit(newChars, targetPos, segmentType)) {\n return\n }\n\n if (segmentType === 'year' && !validateYearDigit(newChars, targetPos, digit, firstPosOfSegment)) {\n return\n }\n\n newChars[targetPos] = digit\n const nextPos = getNextPos(targetPos)\n const result = newChars.join('')\n onChange(result === config.template ? '' : result)\n\n requestAnimationFrame(() => {\n input.setSelectionRange(nextPos, nextPos)\n })\n }\n\n const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n const trimmed = (value || '').trim()\n if (trimmed) {\n const normalized = normalizeManualInput(trimmed, inputFormat)\n const parsed = parseManualDateString(normalized, inputFormat)\n if (!parsed) {\n onChange('')\n } else if (normalized !== value) {\n onChange(normalized)\n }\n }\n onBlur?.(event)\n }\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n onKeyDown?.(event)\n\n const input = inputRef.current\n if (!input) return\n\n const pos = input.selectionStart\n if (pos === null) return\n\n if (\n event.key === 'Tab' ||\n event.key === 'ArrowLeft' ||\n event.key === 'ArrowRight' ||\n event.key === 'Enter' ||\n event.key === 'ArrowUp' ||\n event.key === 'ArrowDown' ||\n event.ctrlKey ||\n event.metaKey\n ) {\n return\n }\n\n event.preventDefault()\n\n if (event.key === 'Backspace' || event.key === 'Delete') {\n handleDelete(pos, event.key === 'Delete')\n return\n }\n\n if (!/^\\d$/.test(event.key)) return\n handleDigit(pos, event.key)\n }\n\n const handleClick = () => {\n const input = inputRef.current\n if (!input) return\n\n const pos = input.selectionStart ?? 0\n\n const getTargetPos = (position: number): number => {\n if (position === sep1) return position - 1\n if (position === sep2) return position - 1\n return position\n }\n\n requestAnimationFrame(() => {\n input.setSelectionRange(getTargetPos(pos), getTargetPos(pos))\n })\n }\n const renderOverlay = () => {\n if (!value) return null\n\n const display = value.padEnd(10, ' ')\n const template = config.template\n\n return (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 flex items-center px-2 text-base select-none\"\n >\n {display.split('').map((char, index) => {\n const isPlaceholderChar = template[index] !== '/' && char === template[index]\n\n const stableKey = `${template[index]}-${index}`\n return (\n <span key={stableKey} className={isPlaceholderChar ? 'text-neutral-300' : 'text-neutral-900'}>\n {char}\n </span>\n )\n })}\n </div>\n )\n }\n\n return (\n <div className=\"relative flex w-full items-center\">\n <input\n ref={inputRef}\n type=\"text\"\n autoComplete=\"off\"\n value={value || ''}\n placeholder={placeholder ?? inputFormat}\n onKeyDown={handleKeyDown}\n onClick={handleClick}\n onChange={() => {}}\n onBlur={handleBlur}\n onFocus={onFocus}\n size={1}\n className={cn(className, value ? 'text-transparent caret-neutral-900' : '')}\n {...rest}\n />\n {renderOverlay()}\n </div>\n )\n },\n)\n\nMaskedInput.displayName = 'MaskedInput'\n"],"names":[],"mappings":";;;;;;AAkCA;AAA6D;AAC7C;AACF;AACoB;AACiD;AAC9D;AACnB;AACc;AACF;AACoB;AACiD;AAC9D;AACnB;AACc;AACF;AACoB;AACiD;AAC9D;AAErB;AAEA;AACE;AACA;AACA;AACF;AAEA;AACE;AACA;AACA;AACA;AACF;AAEA;AAME;AACA;AAEA;AACE;AACA;AACA;AAAmB;AAErB;AACF;AAEA;AACE;AACA;AACE;AACA;AAA4C;AAE9C;AACE;AACA;AAA8C;AAEhD;AACF;AAEA;AAME;AAEA;AACE;AAAkC;AAGpC;AACE;AACA;AACA;AAAyC;AAG3C;AACE;AACA;AAAyD;AAG3D;AACE;AACA;AACA;AAAsD;AAGxD;AACF;AAEO;AAA0B;AAK7B;AACA;AAEA;AACA;AAEA;AAEA;AACE;AACA;AACA;AAAgC;AAGlC;AACE;AACA;AAEA;AACA;AACE;AAAmD;AAErD;AAEA;AACA;AAEA;AACA;AAEA;AACE;AAA4C;AAC7C;AAGH;AACE;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACE;AAA0E;AAG5E;AACE;AAAA;AAGF;AACE;AAAA;AAGF;AACA;AACA;AACA;AAEA;AACE;AAAwC;AACzC;AAGH;AACE;AACA;AACE;AACA;AACA;AACE;AAAW;AAEX;AAAmB;AACrB;AAEF;AAAc;AAGhB;AACE;AAEA;AACA;AAEA;AACA;AAEA;AAUE;AAAA;AAGF;AAEA;AACE;AACA;AAAA;AAGF;AACA;AAA0B;AAG5B;AACE;AACA;AAEA;AAEA;AACE;AACA;AACA;AAAO;AAGT;AACE;AAA4D;AAC7D;AAEH;AACE;AAEA;AACA;AAEA;AACE;AAAC;AAAA;AACa;AACF;AAGR;AAEA;AACA;AAGE;AAEH;AAAA;AACH;AAIJ;AAEI;AAAA;AAAC;AAAA;AACM;AACA;AACQ;AACG;AACY;AACjB;AACF;AACO;AAAC;AACT;AACR;AACM;AACoE;AACtE;AAAA;AACN;AACe;AACjB;AAGN;AAEA;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './InputDate';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DateInputFormat = 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD';
|
|
2
|
+
type DateSegment = 'day' | 'month' | 'year';
|
|
3
|
+
type FormatRow = {
|
|
4
|
+
dateFns: string;
|
|
5
|
+
segmentOrder: [DateSegment, DateSegment, DateSegment];
|
|
6
|
+
};
|
|
7
|
+
export declare const DATE_INPUT_FORMATS: Record<DateInputFormat, FormatRow>;
|
|
8
|
+
export declare const getDateInputDateFns: (format: DateInputFormat) => string;
|
|
9
|
+
export declare function parseManualDateString(raw: string, format: DateInputFormat): Date | undefined;
|
|
10
|
+
export declare function normalizeManualInput(text: string, format: DateInputFormat): string;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parseISO, isValid } from 'date-fns';
|
|
2
|
+
|
|
3
|
+
const DATE_INPUT_FORMATS = {
|
|
4
|
+
"DD/MM/YYYY": {
|
|
5
|
+
dateFns: "dd/MM/yyyy",
|
|
6
|
+
segmentOrder: ["day", "month", "year"]
|
|
7
|
+
},
|
|
8
|
+
"MM/DD/YYYY": {
|
|
9
|
+
dateFns: "MM/dd/yyyy",
|
|
10
|
+
segmentOrder: ["month", "day", "year"]
|
|
11
|
+
},
|
|
12
|
+
"YYYY/MM/DD": {
|
|
13
|
+
dateFns: "yyyy/MM/dd",
|
|
14
|
+
segmentOrder: ["year", "month", "day"]
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const getDateInputDateFns = (format) => DATE_INPUT_FORMATS[format].dateFns;
|
|
18
|
+
function parseManualDateString(raw, format) {
|
|
19
|
+
const trimmed = raw.trim();
|
|
20
|
+
if (!trimmed) return void 0;
|
|
21
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
22
|
+
const parsed = parseISO(trimmed);
|
|
23
|
+
return isValid(parsed) ? parsed : void 0;
|
|
24
|
+
}
|
|
25
|
+
const parts = trimmed.split("/");
|
|
26
|
+
if (parts.length !== 3) return void 0;
|
|
27
|
+
const { segmentOrder } = DATE_INPUT_FORMATS[format];
|
|
28
|
+
const dayIdx = segmentOrder.indexOf("day");
|
|
29
|
+
const monthIdx = segmentOrder.indexOf("month");
|
|
30
|
+
const yearIdx = segmentOrder.indexOf("year");
|
|
31
|
+
if (dayIdx === -1 || monthIdx === -1 || yearIdx === -1) return void 0;
|
|
32
|
+
const day = Number.parseInt(parts[dayIdx], 10);
|
|
33
|
+
const month = Number.parseInt(parts[monthIdx], 10);
|
|
34
|
+
const year = Number.parseInt(parts[yearIdx], 10);
|
|
35
|
+
if (Number.isNaN(day) || Number.isNaN(month) || Number.isNaN(year)) return void 0;
|
|
36
|
+
const date = new Date(year, month - 1, day);
|
|
37
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
return date;
|
|
41
|
+
}
|
|
42
|
+
function normalizeManualInput(text, format) {
|
|
43
|
+
const parts = text.split("/");
|
|
44
|
+
if (parts.length !== 3) return "";
|
|
45
|
+
const { segmentOrder } = DATE_INPUT_FORMATS[format];
|
|
46
|
+
const parsed = { day: null, month: null, year: null };
|
|
47
|
+
parts.forEach((part, index) => {
|
|
48
|
+
const type = segmentOrder[index];
|
|
49
|
+
const isYear = type === "year";
|
|
50
|
+
const digits = part.replaceAll(/[DMY]/g, "").trim();
|
|
51
|
+
if (!digits) return;
|
|
52
|
+
if (isYear && digits.length < 4) return;
|
|
53
|
+
parsed[type] = digits.padStart(isYear ? 4 : 2, "0");
|
|
54
|
+
});
|
|
55
|
+
if (parsed.day == null || parsed.month == null || parsed.year == null) return "";
|
|
56
|
+
return segmentOrder.map((type) => parsed[type]).join("/");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { DATE_INPUT_FORMATS, getDateInputDateFns, normalizeManualInput, parseManualDateString };
|
|
60
|
+
//# sourceMappingURL=manualDateFormat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manualDateFormat.js","sources":["../../../src/components/InputDateMask/manualDateFormat.ts"],"sourcesContent":["import { isValid, parseISO } from 'date-fns'\n\nexport type DateInputFormat = 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD'\n\ntype DateSegment = 'day' | 'month' | 'year'\n\ntype FormatRow = {\n dateFns: string\n segmentOrder: [DateSegment, DateSegment, DateSegment]\n}\n\nexport const DATE_INPUT_FORMATS: Record<DateInputFormat, FormatRow> = {\n 'DD/MM/YYYY': {\n dateFns: 'dd/MM/yyyy',\n segmentOrder: ['day', 'month', 'year'],\n },\n 'MM/DD/YYYY': {\n dateFns: 'MM/dd/yyyy',\n segmentOrder: ['month', 'day', 'year'],\n },\n 'YYYY/MM/DD': {\n dateFns: 'yyyy/MM/dd',\n segmentOrder: ['year', 'month', 'day'],\n },\n}\n\nexport const getDateInputDateFns = (format: DateInputFormat): string => DATE_INPUT_FORMATS[format].dateFns\n\nexport function parseManualDateString(raw: string, format: DateInputFormat): Date | undefined {\n const trimmed = raw.trim()\n if (!trimmed) return undefined\n\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) {\n const parsed = parseISO(trimmed)\n return isValid(parsed) ? parsed : undefined\n }\n\n const parts = trimmed.split('/')\n if (parts.length !== 3) return undefined\n\n const { segmentOrder } = DATE_INPUT_FORMATS[format]\n const dayIdx = segmentOrder.indexOf('day')\n const monthIdx = segmentOrder.indexOf('month')\n const yearIdx = segmentOrder.indexOf('year')\n if (dayIdx === -1 || monthIdx === -1 || yearIdx === -1) return undefined\n\n const day = Number.parseInt(parts[dayIdx], 10)\n const month = Number.parseInt(parts[monthIdx], 10)\n const year = Number.parseInt(parts[yearIdx], 10)\n if (Number.isNaN(day) || Number.isNaN(month) || Number.isNaN(year)) return undefined\n\n const date = new Date(year, month - 1, day)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return undefined\n }\n return date\n}\n\nexport function normalizeManualInput(text: string, format: DateInputFormat): string {\n const parts = text.split('/')\n if (parts.length !== 3) return ''\n\n const { segmentOrder } = DATE_INPUT_FORMATS[format]\n const parsed: Record<DateSegment, string | null> = { day: null, month: null, year: null }\n\n parts.forEach((part, index) => {\n const type = segmentOrder[index]\n const isYear = type === 'year'\n const digits = part.replaceAll(/[DMY]/g, '').trim()\n if (!digits) return\n if (isYear && digits.length < 4) return\n parsed[type] = digits.padStart(isYear ? 4 : 2, '0')\n })\n\n if (parsed.day == null || parsed.month == null || parsed.year == null) return ''\n\n return segmentOrder.map((type) => parsed[type]).join('/')\n}\n"],"names":[],"mappings":";;AAWO,MAAM,kBAAA,GAAyD;AAAA,EACpE,YAAA,EAAc;AAAA,IACZ,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,CAAC,KAAA,EAAO,OAAA,EAAS,MAAM;AAAA,GACvC;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,CAAC,OAAA,EAAS,KAAA,EAAO,MAAM;AAAA,GACvC;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,CAAC,MAAA,EAAQ,OAAA,EAAS,KAAK;AAAA;AAEzC;AAEO,MAAM,mBAAA,GAAsB,CAAC,MAAA,KAAoC,kBAAA,CAAmB,MAAM,CAAA,CAAE;AAE5F,SAAS,qBAAA,CAAsB,KAAa,MAAA,EAA2C;AAC5F,EAAA,MAAM,OAAA,GAAU,IAAI,IAAA,EAAK;AACzB,EAAA,IAAI,CAAC,SAAS,OAAO,MAAA;AAErB,EAAA,IAAI,qBAAA,CAAsB,IAAA,CAAK,OAAO,CAAA,EAAG;AACvC,IAAA,MAAM,MAAA,GAAS,SAAS,OAAO,CAAA;AAC/B,IAAA,OAAO,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,GAAS,MAAA;AAAA;AAGpC,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAE/B,EAAA,MAAM,EAAE,YAAA,EAAa,GAAI,kBAAA,CAAmB,MAAM,CAAA;AAClD,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,OAAA,CAAQ,KAAK,CAAA;AACzC,EAAA,MAAM,QAAA,GAAW,YAAA,CAAa,OAAA,CAAQ,OAAO,CAAA;AAC7C,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,OAAA,CAAQ,MAAM,CAAA;AAC3C,EAAA,IAAI,WAAW,EAAA,IAAM,QAAA,KAAa,EAAA,IAAM,OAAA,KAAY,IAAI,OAAO,MAAA;AAE/D,EAAA,MAAM,MAAM,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,MAAM,GAAG,EAAE,CAAA;AAC7C,EAAA,MAAM,QAAQ,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,QAAQ,GAAG,EAAE,CAAA;AACjD,EAAA,MAAM,OAAO,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM,OAAO,GAAG,EAAE,CAAA;AAC/C,EAAA,IAAI,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,IAAK,MAAA,CAAO,KAAA,CAAM,KAAK,CAAA,IAAK,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,EAAG,OAAO,MAAA;AAE3E,EAAA,MAAM,OAAO,IAAI,IAAA,CAAK,IAAA,EAAM,KAAA,GAAQ,GAAG,GAAG,CAAA;AAC1C,EAAA,IAAI,IAAA,CAAK,WAAA,EAAY,KAAM,IAAA,IAAQ,IAAA,CAAK,QAAA,EAAS,KAAM,KAAA,GAAQ,CAAA,IAAK,IAAA,CAAK,OAAA,EAAQ,KAAM,GAAA,EAAK;AAC1F,IAAA,OAAO,MAAA;AAAA;AAET,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,oBAAA,CAAqB,MAAc,MAAA,EAAiC;AAClF,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC5B,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,EAAA;AAE/B,EAAA,MAAM,EAAE,YAAA,EAAa,GAAI,kBAAA,CAAmB,MAAM,CAAA;AAClD,EAAA,MAAM,SAA6C,EAAE,GAAA,EAAK,MAAM,KAAA,EAAO,IAAA,EAAM,MAAM,IAAA,EAAK;AAExF,EAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,KAAA,KAAU;AAC7B,IAAA,MAAM,IAAA,GAAO,aAAa,KAAK,CAAA;AAC/B,IAAA,MAAM,SAAS,IAAA,KAAS,MAAA;AACxB,IAAA,MAAM,SAAS,IAAA,CAAK,UAAA,CAAW,QAAA,EAAU,EAAE,EAAE,IAAA,EAAK;AAClD,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,IAAI,MAAA,IAAU,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AACjC,IAAA,MAAA,CAAO,IAAI,CAAA,GAAI,MAAA,CAAO,SAAS,MAAA,GAAS,CAAA,GAAI,GAAG,GAAG,CAAA;AAAA,GACnD,CAAA;AAED,EAAA,IAAI,MAAA,CAAO,OAAO,IAAA,IAAQ,MAAA,CAAO,SAAS,IAAA,IAAQ,MAAA,CAAO,IAAA,IAAQ,IAAA,EAAM,OAAO,EAAA;AAE9E,EAAA,OAAO,YAAA,CAAa,IAAI,CAAC,IAAA,KAAS,OAAO,IAAI,CAAC,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC1D;;;;"}
|