UNPKG

react-fatless-form

Version:

A lightweight React form package designed for simplicity that simplifies form handling and validation without unnecessary complexity or bloat.

859 lines (829 loc) 112 kB
'use strict'; var React = require('react'); var ReactDOM = require('react-dom'); class FeedbackManager { constructor() { this.listeners = new Set(); this.feedbacks = []; this.timeouts = new Map(); this.addFeedback = (message, options = {}) => { var _a; const id = Date.now(); const feedback = { id, message, type: options.type || "toast", variant: options.variant || "info", autoDismiss: (_a = options.autoDismiss) !== null && _a !== undefined ? _a : true, duration: options.duration || 5000, onClose: options.onClose, isFadingOut: false, }; this.feedbacks = [...this.feedbacks, feedback]; this.notifyListeners(); if (feedback.autoDismiss) { const timeout = setTimeout(() => this.startFadeOut(id), feedback.duration); this.timeouts.set(id, timeout); } }; this.startFadeOut = (id) => { this.feedbacks = this.feedbacks.map((f) => f.id === id ? Object.assign(Object.assign({}, f), { isFadingOut: true }) : f); this.notifyListeners(); const timeout = setTimeout(() => this.removeFeedback(id), 300); this.timeouts.set(id, timeout); }; this.removeFeedback = (id) => { const feedback = this.feedbacks.find((f) => f.id === id); if (feedback === null || feedback === undefined ? undefined : feedback.onClose) feedback.onClose(); this.feedbacks = this.feedbacks.filter((f) => f.id !== id); this.timeouts.delete(id); this.notifyListeners(); }; this.notifyListeners = () => { this.listeners.forEach((listener) => listener(this.feedbacks)); }; this.subscribe = (listener) => { this.listeners.add(listener); return () => this.listeners.delete(listener); }; } } /** * FeedbackManager * * A class for managing feedback notifications, such as toasts or alerts, with support for auto-dismissal, customizable durations, and fade-out animations. * This utility helps centralize feedback handling in applications, offering a subscription-based model for real-time updates. * * ### Features: * - Supports feedback types: "toast" and "alert". * - Four visual variants: "info", "success", "error", and "warning". * - Auto-dismiss functionality with configurable durations. * - Handles fade-out animations before removal. * - Easy integration with UI through a FeedbackContainer component. * * ### Usage: * * #### Importing and Instantiating: * ```typescript * import { feedbackManager } from 'react-fatless-form'; * ``` * * #### Adding Feedback: * ```typescript * feedbackManager.addFeedback("Operation successful!", { * type: "toast", * variant: "success", * autoDismiss: true, * duration: 5000, * onClose: () => console.log("Feedback closed!"), * }); * ``` * * #### Subscribing to Feedback Updates: * ```typescript * const unsubscribe = feedbackManager.subscribe((feedbacks) => { * console.log("Current feedbacks:", feedbacks); * }); * * // Unsubscribe when no longer needed * unsubscribe(); * ``` * * #### Mounting the FeedbackContainer: * The `FeedbackContainer` component listens for updates to feedback notifications and renders them appropriately. * Add it once to your application, typically in your app's root component. * * ```tsx * import { FeedbackContainer } from 'react-fatless-form'; * * function App() { * return ( * <div> * <YourMainContent /> * <FeedbackContainer /> * </div> * ); * } * ``` * * `FeedbackContainer` uses `ReactDOM.createPortal` to render feedback notifications at the root of the `document.body`. * Ensure it is included in your component tree to visualize feedback. * * #### Example Integration with UI: * ```tsx * import { feedbackManager } from 'react-fatless-form'; * * const App = () => { * const handleClick = () => { * feedbackManager.addFeedback("This is a success message!", { * type: "toast", * variant: "success", * duration: 3000, * }); * }; * * return ( * <div> * <button onClick={handleClick}>Show Feedback</button> * <FeedbackContainer /> * </div> * ); * }; * ``` * * ### API: * #### Methods: * - `addFeedback(message: string, options?: FeedbackOptions): void` * - Adds a new feedback to the list. * - `message` (string): The feedback message to display. * - `options` (FeedbackOptions): Optional configurations. * - `type` ("toast" | "alert"): Type of feedback (default: "toast"). * - `variant` ("info" | "success" | "error" | "warning"): Visual variant (default: "info"). * - `autoDismiss` (boolean): Whether feedback should dismiss automatically (default: true). * - `duration` (number): Auto-dismiss duration in milliseconds (default: 5000ms). * - `onClose` (function): Callback to execute on feedback removal. * * - `removeFeedback(id: number): void` * - Removes feedback immediately and triggers its `onClose` callback if provided. * * - `subscribe(listener: (feedbacks: Feedback[]) => void): () => void` * - Registers a listener for feedback updates. * - Returns an unsubscribe function to stop listening. * * #### Internal Methods: * - `startFadeOut(id: number): void` * - Initiates the fade-out animation before removing feedback. * * - `notifyListeners(): void` * - Notifies all registered listeners of feedback updates. * * ### Types: * - `FeedbackVariant`: * - `"info"`, `"success"`, `"error"`, `"warning"` * - `Feedback`: * - `id` (number): Unique identifier. * - `message` (string): Feedback content. * - `type` ("toast" | "alert"): Feedback type. * - `variant` (FeedbackVariant): Feedback variant. * - `autoDismiss` (boolean): Whether to auto-dismiss. * - `duration` (number): Auto-dismiss duration in milliseconds. * - `onClose` (function): Callback when feedback is closed. * - `isFadingOut` (boolean): Whether the feedback is fading out. * - `FeedbackOptions`: * - Optional properties for `addFeedback` method. */ const feedbackManager = new FeedbackManager(); function useFeedback() { const [feedbacks, setFeedbacks] = React.useState([]); React.useEffect(() => { const listener = (newFeedbacks) => setFeedbacks(newFeedbacks); const unsubscribe = feedbackManager.subscribe(listener); return () => { unsubscribe(); }; }, []); return feedbacks; } function styleInject(css, ref) { if ( ref === undefined ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z$9 = ".Feedback-module_alert-container__RnydS,.Feedback-module_toast-container__yxUI6{animation:Feedback-module_fade-in__F-If5 .3s ease;display:flex;gap:5px;position:fixed;right:5px;z-index:9999}.Feedback-module_alert-container__RnydS{flex-direction:column;left:5px;top:5px}.Feedback-module_toast-container__yxUI6{bottom:5px;flex-direction:column-reverse}.Feedback-module_alert__OllrK,.Feedback-module_toast__3xnv4{align-items:center;border:1px solid #fff;border-radius:8px;display:flex;padding:15px 20px}.Feedback-module_toast__3xnv4{max-width:450px;min-width:350px}.Feedback-module_info__1ncyU{background:linear-gradient(180deg,#d1ecf1,#fff)}.Feedback-module_success__hQASX{background:linear-gradient(180deg,#d4edda,#fff)}.Feedback-module_warning__Ac-rP{background:linear-gradient(180deg,#fff3cd,#fff)}.Feedback-module_error__VTPq6{background:linear-gradient(180deg,#f8d7da,#fff)}.Feedback-module_icon-error__mna7F,.Feedback-module_icon-info__CDmBm,.Feedback-module_icon-success__FabnY,.Feedback-module_icon-warning__lXS-P{align-items:center;border-radius:50%;color:#fff;display:flex;height:20px;justify-content:center;width:20px}.Feedback-module_icon-info__CDmBm{background-color:#27bcfd}.Feedback-module_icon-success__FabnY{background-color:#00d27a}.Feedback-module_icon-warning__lXS-P{background-color:#f5803e}.Feedback-module_icon-error__mna7F{background-color:#e63757}.Feedback-module_icon-container__SW5oN{align-items:center;background-color:#f5f5f5;border:1px solid #fff;border-radius:50%;box-shadow:0 0 8px rgba(0,0,0,.1);display:flex;height:40px;justify-content:center;max-width:40px;width:40px;width:100%}.Feedback-module_message-container__7ahjX{flex:1;padding-left:20px}.Feedback-module_message-heading__6nKUc{font-size:1rem;font-weight:700}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-info__VOoGF{color:#27bcfd}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-success__PHY1e{color:#00d27a}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-warning__V36sc{color:#f5803e}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-error__TT2iz{color:#e63757}.Feedback-module_message-body__6kaEv{color:#404040;font-size:.9rem}.Feedback-module_btn-close__397Jb{background:none;border:none;color:#333;cursor:pointer;font-size:16px;margin-left:auto;opacity:.7;position:relative;transition:opacity .2s}.Feedback-module_btn-close__397Jb:hover{opacity:1}@keyframes Feedback-module_fade-in__F-If5{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes Feedback-module_fade-out__1WY6R{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-10px)}}.Feedback-module_fade-out__1WY6R{animation:Feedback-module_fade-out__1WY6R .3s ease forwards}"; var styles$8 = {"alert-container":"Feedback-module_alert-container__RnydS","toast-container":"Feedback-module_toast-container__yxUI6","fade-in":"Feedback-module_fade-in__F-If5","alert":"Feedback-module_alert__OllrK","toast":"Feedback-module_toast__3xnv4","info":"Feedback-module_info__1ncyU","success":"Feedback-module_success__hQASX","warning":"Feedback-module_warning__Ac-rP","error":"Feedback-module_error__VTPq6","icon-info":"Feedback-module_icon-info__CDmBm","icon-success":"Feedback-module_icon-success__FabnY","icon-warning":"Feedback-module_icon-warning__lXS-P","icon-error":"Feedback-module_icon-error__mna7F","icon-container":"Feedback-module_icon-container__SW5oN","message-container":"Feedback-module_message-container__7ahjX","message-heading":"Feedback-module_message-heading__6nKUc","heading-info":"Feedback-module_heading-info__VOoGF","heading-success":"Feedback-module_heading-success__PHY1e","heading-warning":"Feedback-module_heading-warning__V36sc","heading-error":"Feedback-module_heading-error__TT2iz","message-body":"Feedback-module_message-body__6kaEv","btn-close":"Feedback-module_btn-close__397Jb","fade-out":"Feedback-module_fade-out__1WY6R"}; styleInject(css_248z$9); const getIcon = (variant) => { const icons = { success: React.createElement("span", { className: styles$8.icon }, "\u2713"), error: React.createElement("span", { className: styles$8.icon }, "\u2715"), warning: React.createElement("span", { className: styles$8.icon }, "!"), info: React.createElement("span", { className: styles$8.icon }, "\u2139"), }; return icons[variant] || icons.info; }; function FeedbackContainer() { const feedbacks = useFeedback(); const alerts = feedbacks.filter(({ type }) => type === "alert"); const toasts = feedbacks.filter(({ type }) => type === "toast"); return ReactDOM.createPortal(React.createElement(React.Fragment, null, React.createElement("div", { className: styles$8["alert-container"] }, alerts.map(({ id, variant, message, isFadingOut }) => (React.createElement("div", { key: id, className: `${styles$8.alert} ${styles$8[variant]} ${isFadingOut ? styles$8["fade-out"] : ""}` }, React.createElement("div", { className: styles$8["icon-container"] }, React.createElement("div", { className: styles$8[`icon-${variant}`] }, getIcon(variant))), React.createElement("div", { className: styles$8["message-container"] }, React.createElement("p", { className: styles$8["message-heading"] }, variant.replace(/^\w/, c => c.toUpperCase())), React.createElement("p", { className: styles$8["message-body"] }, message)), React.createElement("button", { type: "button", className: styles$8["btn-close"], "aria-label": "Close", onClick: () => feedbackManager.startFadeOut(id) }, "\u2715"))))), React.createElement("div", { className: styles$8["toast-container"] }, toasts.map(({ id, variant, message, isFadingOut }) => (React.createElement("div", { key: id, className: `${styles$8.toast} ${styles$8[variant]} ${isFadingOut ? styles$8["fade-out"] : ""}` }, React.createElement("div", { className: styles$8["icon-container"] }, React.createElement("div", { className: styles$8[`icon-${variant}`] }, getIcon(variant))), React.createElement("div", { className: styles$8["message-container"] }, React.createElement("p", { className: `${styles$8["message-heading"]} ${styles$8[`heading-${variant}`]}` }, variant.replace(/^\w/, c => c.toUpperCase())), React.createElement("p", { className: styles$8["message-body"] }, message)), React.createElement("button", { type: "button", className: styles$8["btn-close"], "aria-label": "Close", onClick: () => feedbackManager.startFadeOut(id) }, "\u2715")))))), document.body); } const FormContext = React.createContext(null); function FormProvider({ children, form }) { return React.createElement(FormContext.Provider, { value: form }, children); } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; function useFormContext() { const context = React.useContext(FormContext); if (!context) { throw new Error("useFormContext must be used within a FormProvider"); } return context; } function useField(name) { const form = useFormContext(); const value = form.values[name]; const error = form.errors[name]; const touched = form.touched[name]; const onFocus = () => { if (!touched) { form.setFieldTouched(name, true); } }; const onBlur = () => { if (!touched) { form.setFieldTouched(name, true); } if (error) { form.setFieldError(name, error); } }; const onChange = React.useCallback((e) => { if (error) { form.setFieldError(name, ''); } if (typeof e === 'boolean') { form.setFieldValue(name, e); } else if (e instanceof Date) { form.setFieldValue(name, e); } else if (typeof e === 'string' || typeof e === 'number') { form.setFieldValue(name, e); } else if (Array.isArray(e)) { const selectedValues = e.map((option) => { if (typeof option === 'object' && option !== null && 'value' in option) { return option.value; } return option; }); // @ts-ignore form.setFieldArrayValue(name, selectedValues); } else if (e && 'target' in e) { const target = e.target; if (target instanceof HTMLSelectElement) { if (target.multiple) { const selectedValues = Array.from(target.selectedOptions, (option) => option.value); form.setFieldArrayValue(name, selectedValues); } else { form.setFieldValue(name, target.value); } } else if (target instanceof HTMLInputElement) { if (target.type === 'checkbox') { // Handle checkbox inputs form.setFieldValue(name, target.checked); } else { const value = e.target.value; // If it's a valid number string, convert to number const numeric = Number(value); if (value === '') { form.setFieldValue(name, null); } else if (!isNaN(numeric) && value.trim() !== '') { form.setFieldValue(name, numeric); } else { form.setFieldValue(name, value); // it's a string } } } else { form.setFieldValue(name, target.value); } } else if (e instanceof FileList) { form.setFieldValue(name, e ? Array.from(e) : []); } else { form.setFieldValue(name, e); } }, [error]); return { touched, value, error, onFocus, onBlur, onChange }; } var css_248z$8 = ".Switch-module_switch__xyu7V{display:inline-block;height:22px;position:relative;width:38px}.Switch-module_switch__xyu7V input{height:0;opacity:0;width:0}.Switch-module_slider__SngdL{background-color:#ccc;bottom:0;cursor:pointer;left:0;position:absolute;right:0;top:0;-webkit-transition:.4s;transition:.4s}.Switch-module_slider__SngdL:before{background-color:#fff;bottom:4px;content:\"\";height:14px;left:5px;position:absolute;-webkit-transition:.4s;transition:.4s;width:14px}input:checked+.Switch-module_slider__SngdL{background-color:#0078d4}input:focus+.Switch-module_slider__SngdL{box-shadow:0 0 1px #0078d4}input:checked+.Switch-module_slider__SngdL:before{-webkit-transform:translateX(14px);-ms-transform:translateX(14px);transform:translateX(14px)}.Switch-module_slider__SngdL.Switch-module_round__0IxC3{border-radius:34px}.Switch-module_slider__SngdL.Switch-module_round__0IxC3:before{border-radius:50%}"; var styles$7 = {"switch":"Switch-module_switch__xyu7V","slider":"Switch-module_slider__SngdL","round":"Switch-module_round__0IxC3"}; styleInject(css_248z$8); /** * A reusable Checkbox component for handling single and multiple checkbox inputs. * * This component supports two modes: * 1. **Single Checkbox**: For a single boolean value, with optional slider styles. * 2. **Multiple Checkboxes**: For selecting multiple values from a list of options. * * ## Props * * - **SingleCheckboxProps**: * - `name` (string): The name attribute for the checkbox input. * - `label` (string, optional): The label displayed next to the checkbox. * - `value` (boolean): The current checked state of the checkbox. * - `onChange` (function): A callback triggered when the checkbox state changes. * - `slider` ('rectangular' | 'rounded', optional): If provided, renders the checkbox as a switch. * - `rectangular`: Renders a rectangular switch. * - `rounded`: Renders a rounded switch. * * - **MultipleCheckboxProps**: * - `name` (string): The name attribute for the group of checkboxes. * - `label` (string, optional): A label displayed above the group of checkboxes. * - `value` (array of strings or numbers): The current selected values. * - `onChange` (function): A callback triggered with the updated array of selected values. * - `options` (array of SelectOption): The options to display as checkboxes, where each option has: * - `label` (string): The display label for the checkbox. * - `value` (string or number): The value associated with the checkbox. * * ## Behavior * * - **Single Checkbox**: * - If `options` is not provided, it renders a single checkbox. * - If `slider` is provided, the checkbox is styled as a switch. * - **Multiple Checkboxes**: * - If `options` is provided and has at least one item, it renders a list of checkboxes. * - If `options` is empty, the component renders nothing. * * ## Examples * * ### Single Checkbox (Default) * * ```tsx * <Checkbox * name="acceptTerms" * label="Accept Terms and Conditions" * value={true} * onChange={(checked) => console.log(checked)} * /> * ``` * * ### Single Checkbox (Slider) * * ```tsx * <Checkbox * name="darkMode" * label="Enable Dark Mode" * value={false} * onChange={(checked) => console.log(checked)} * slider="rounded" * /> * ``` * * ### Multiple Checkboxes * * ```tsx * <Checkbox * name="preferences" * label="Choose Preferences" * value={['option1']} * onChange={(selected) => console.log(selected)} * options={[ * { label: 'Option 1', value: 'option1' }, * { label: 'Option 2', value: 'option2' }, * ]} * /> * ``` * * @param {CheckboxInputType} props - The props to configure the checkbox component. * * @returns {JSX.Element | null} The rendered checkbox component. */ function Checkbox(props) { const { name, label } = props; // Single checkbox case if (!('options' in props)) { const { value, onChange, slider } = props; const inputElement = (React.createElement("input", { id: name, name: name, type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) })); const labelElement = label && React.createElement("span", { style: { marginLeft: '8px', cursor: "pointer" } }, label); if (!slider) { return (React.createElement("label", { htmlFor: name }, inputElement, labelElement)); } const sliderClass = `${styles$7.slider} ${slider === 'rounded' ? styles$7.round : ''}`; return (React.createElement("div", null, React.createElement("label", { className: styles$7.switch, htmlFor: name }, inputElement, React.createElement("span", { className: sliderClass })), labelElement)); } // Multiple checkboxes case const { options, value = [], onChange } = props; if (!options || options.length === 0) { return null; } return (React.createElement("div", null, label && React.createElement("label", null, label), React.createElement("div", null, options.map((option, index) => (React.createElement("label", { key: `${option.value}-${index}`, style: { display: "block", margin: "4px 0", cursor: "pointer" } }, React.createElement("input", { id: `${name}-${option.value}`, type: "checkbox", name: name, checked: value.includes(option.value), onChange: (e) => onChange(e.target.checked ? [...value, option.value] : value.filter((v) => v !== option.value)) }), option.label)))))); } /** * Custom hook to calculate label width and dynamically set * styles for masking the parent's border behind the label. * * @param label - The label text. * @returns [labelRef, dynamicStyles] */ function useLabelStyle(label) { const labelRef = React.useRef(null); const [labelWidth, setLabelWidth] = React.useState(0); const [parentBackground, setParentBackground] = React.useState("transparent"); React.useEffect(() => { if (labelRef.current) { // Measure label width const rect = labelRef.current.getBoundingClientRect(); setLabelWidth(rect.width); // Find nearest ancestor with non-transparent background const findNonTransparentBackground = (element) => { while (element) { const backgroundColor = getComputedStyle(element).backgroundColor; // Check for non-transparent background if (backgroundColor !== "rgba(0, 0, 0, 0)" && backgroundColor !== "transparent") { return backgroundColor; } element = element.parentElement; } // Default to body background or white if none found const bodyBackground = getComputedStyle(document.body).backgroundColor; return bodyBackground === "rgba(0, 0, 0, 0)" ? "#fff" : bodyBackground; }; const parentElement = labelRef.current.parentElement; const backgroundColor = findNonTransparentBackground(parentElement); setParentBackground(backgroundColor); } }, [label]); const dynamicStyles = { "--label-width": `${labelWidth}px`, "--label-offset": "10px", "--mask-background": parentBackground, }; return [labelRef, dynamicStyles]; } var css_248z$7 = ".Global-module_inputWrapper__F9Q9V{align-items:center;border:1px solid #ccc;border-radius:4px;display:flex;height:56px;margin-bottom:10px;position:relative;transition:border-color .3s ease;width:100%}.Global-module_inputWrapper__F9Q9V.Global-module_selectBoxOpen__ByTM6,.Global-module_inputWrapper__F9Q9V:not(.Global-module_inputWrapperError__L-xXY):focus-within{border-color:#0078d4}.Global-module_inputWrapper__F9Q9V.Global-module_selectBoxOpen__ByTM6:not(.Global-module_inputWrapperError__L-xXY) .Global-module_label__Kvs6m,.Global-module_inputWrapper__F9Q9V:not(.Global-module_inputWrapperError__L-xXY):focus-within .Global-module_label__Kvs6m{color:#0078d4}.Global-module_inputWrapper__F9Q9V:before{background-color:var(--mask-background);content:\"\";height:1px;left:var(--label-offset,10px);position:absolute;top:-1px;width:var(--label-width,50px);z-index:1}.Global-module_inputWrapper__F9Q9V.Global-module_inputWrapperError__L-xXY{border:1px solid #b80000}.Global-module_label__Kvs6m{background-color:transparent;color:grey;font-size:.8rem;margin-left:var(--label-offset,10px);padding:0 4px;position:absolute;top:-10px;transition:color .3s ease;z-index:2}.Global-module_rightIcon__mrj0Y{align-items:center;background:none;border:none;cursor:pointer;display:inline-flex;font-size:18px;justify-content:center;position:absolute;right:2px;top:50%;transform:translateY(-50%);z-index:2}.Global-module_rightIcon__mrj0Y span{font-size:1.5rem;line-height:1;transform-origin:center;transition:transform .3s ease-in-out}.Global-module_rightIcon__mrj0Y:hover span{transform:scale(1.2)}.Global-module_label__Kvs6m.Global-module_errorLabel__TLZP7{color:#b80000}.Global-module_errorMessage__kNxOf{color:#b80000;font-size:.8rem;margin-top:1px}"; var globalStyles = {"inputWrapper":"Global-module_inputWrapper__F9Q9V","inputWrapperError":"Global-module_inputWrapperError__L-xXY","selectBoxOpen":"Global-module_selectBoxOpen__ByTM6","label":"Global-module_label__Kvs6m","rightIcon":"Global-module_rightIcon__mrj0Y","errorLabel":"Global-module_errorLabel__TLZP7","errorMessage":"Global-module_errorMessage__kNxOf"}; styleInject(css_248z$7); var css_248z$6 = ".DateInput-module_dateInput__bYAdI{background-color:transparent;border:none;cursor:pointer;font-size:.8rem;padding:8px;position:relative;width:100%;z-index:1}.DateInput-module_dateInput__bYAdI:focus{outline:none}time.DateInput-module_icon__xKmsO{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#fff;border-radius:.6em;box-shadow:0 1px 0 #bdbdbd,0 2px 0 #fff,0 3px 0 #bdbdbd,0 4px 0 #fff,0 5px 0 #bdbdbd,0 0 0 1px #bdbdbd;display:block;font-size:1em;height:7em;margin:2em auto;overflow:hidden;position:relative;-webkit-transform:rotate(0deg) skewY(0deg);transform:rotate(0deg) skewY(0deg);-webkit-transform-origin:50% 10%;transform-origin:50% 10%;width:7em}.DateInput-module_timePicker__EpNUE{background:#fff;border:1px solid #ccc;border-radius:0 4px 4px 0;box-shadow:0 4px 8px rgba(0,0,0,.1);display:flex;flex-direction:column;height:100%;left:99%;overflow-y:auto;position:absolute;scrollbar-color:#ccc transparent;scrollbar-width:thin;top:-1px;width:90px;z-index:10}.DateInput-module_timeHeader__z3e81{align-items:center;background:#fff;border-bottom:1px solid #ccc;color:#333;display:flex;font-size:.7rem;justify-content:space-between;padding:8px;position:sticky;top:0;z-index:1}.DateInput-module_timeOption__ZJwGo{background:transparent;border:none;cursor:pointer;font-size:.8rem;padding:8px;text-align:center}.DateInput-module_timeOption__ZJwGo:not(.DateInput-module_timeOptionSelected__uAtnc):hover{background:#e0e0e0}.DateInput-module_timeOption__ZJwGo:active{background:#ddd}time.DateInput-module_icon__xKmsO *{display:block;font-size:1em;font-style:normal;font-weight:700;text-align:center;width:100%}time.DateInput-module_icon__xKmsO strong{background-color:#fd9f1b;border-bottom:1px dashed #f37302;box-shadow:0 2px 0 #fd9f1b;color:#fff;padding:.4em 0;position:absolute;top:0}time.DateInput-module_icon__xKmsO em{bottom:.3em;color:#fd9f1b;position:absolute}time.DateInput-module_icon__xKmsO span{color:#2f2f2f;font-size:2.8em;letter-spacing:-.05em;padding-top:.8em;width:100%}time.DateInput-module_icon__xKmsO:focus,time.DateInput-module_icon__xKmsO:hover{-webkit-animation:DateInput-module_swing__-zagu .6s ease-out;animation:DateInput-module_swing__-zagu .6s ease-out}.DateInput-module_calendarDropdown__RfMqi{background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 4px 8px rgba(0,0,0,.1);left:0;opacity:0;padding:16px;position:absolute;top:calc(100% + 2px);width:300px;z-index:10}.DateInput-module_calendarDropdown__RfMqi.DateInput-module_open__FnLHL{opacity:1;transform:translateY(0)}.DateInput-module_calendarHeader__WMjnb{align-items:center;display:flex;justify-content:space-between;margin-bottom:8px}.DateInput-module_calendarHeader__WMjnb select{border:1px solid #ccc;border-radius:4px;padding:4px}.DateInput-module_daysOfWeek__KqX6i,.DateInput-module_days__0E6SG{display:grid;gap:4px;grid-template-columns:repeat(7,1fr)}.DateInput-module_dayLabel__-w6e8{font-size:.8rem;font-weight:400;text-align:center}.DateInput-module_day__TMSXL{background:#f9f9f9;border:none;border-radius:4px;cursor:pointer;padding:8px;text-align:center}.DateInput-module_day__TMSXL:not(.DateInput-module_daySelected__HVDzA):hover{background:#e0e0e0}.DateInput-module_daySelected__HVDzA,.DateInput-module_timeOptionSelected__uAtnc{background:#0078d4;color:#fff}@-webkit-keyframes DateInput-module_swing__-zagu{0%{-webkit-transform:rotate(0deg) skewY(0deg)}20%{-webkit-transform:rotate(12deg) skewY(4deg)}60%{-webkit-transform:rotate(-9deg) skewY(-3deg)}80%{-webkit-transform:rotate(6deg) skewY(-2deg)}to{-webkit-transform:rotate(0deg) skewY(0deg)}}@keyframes DateInput-module_swing__-zagu{0%{transform:rotate(0deg) skewY(0deg)}20%{transform:rotate(12deg) skewY(4deg)}60%{transform:rotate(-9deg) skewY(-3deg)}80%{transform:rotate(6deg) skewY(-2deg)}to{transform:rotate(0deg) skewY(0deg)}}"; var styles$6 = {"dateInput":"DateInput-module_dateInput__bYAdI","icon":"DateInput-module_icon__xKmsO","timePicker":"DateInput-module_timePicker__EpNUE","timeHeader":"DateInput-module_timeHeader__z3e81","timeOption":"DateInput-module_timeOption__ZJwGo","timeOptionSelected":"DateInput-module_timeOptionSelected__uAtnc","swing":"DateInput-module_swing__-zagu","calendarDropdown":"DateInput-module_calendarDropdown__RfMqi","open":"DateInput-module_open__FnLHL","calendarHeader":"DateInput-module_calendarHeader__WMjnb","daysOfWeek":"DateInput-module_daysOfWeek__KqX6i","days":"DateInput-module_days__0E6SG","dayLabel":"DateInput-module_dayLabel__-w6e8","day":"DateInput-module_day__TMSXL","daySelected":"DateInput-module_daySelected__HVDzA"}; styleInject(css_248z$6); /** * A customizable `DateInput` component for selecting or entering dates in a user-friendly manner. * * This component includes a text input field and an optional dropdown calendar for selecting dates. * It supports disabling specific dates through `minDate` and `maxDate` props, which allow developers * to control the range of selectable dates. Users can interact with the component via text input * (formatted as `DD/MM/YYYY`) or by selecting a date directly from the calendar. * * ## Features: * - **Customizable Styling**: Accepts an optional `className` prop for styling the input field. * - **Calendar Dropdown**: Provides an intuitive calendar interface for selecting dates. * - **Date Validation**: Ensures that only valid dates are accepted, with error messages for invalid inputs. * - **Disabled State**: Fully disables interactions when the `disabled` prop is `true`. * - **Date Restrictions**: Use `minDate` and `maxDate` to enforce a selectable date range. * - **Keyboard and Mouse Support**: Allows users to input dates manually or pick from the calendar dropdown. * - **Time Picker (Optional)**: Includes an optional time picker for selecting time along with the date. * * ## Example Usage: * ```tsx * <DateInput * value={new Date()} // Pre-select today's date * onChange={(date) => console.log('Selected Date:', date)} // Handle date selection * minDate={new Date()} // Restrict to no past dates * maxDate={new Date(2080, 11, 31)} // Allow dates only up to Dec 31, 2080 * className="custom-date-input" // Add custom styling to the input field * /> * ``` * * ## Props: * - `value?: Date` * - The initial selected date for the input field. Defaults to `null`. * - Example: `new Date(2023, 11, 25)` for December 25, 2023. * * - `onChange?: (value: Date) => void` * - Callback function triggered when the selected date changes. * - Receives the newly selected `Date` object as an argument. * - Example: `(date) => console.log(date.toISOString())`. * * - `disabled?: boolean` * - Whether the input and calendar dropdown should be disabled. * - Defaults to `false`. * - Example: `disabled={true}` disables all interactions. * * - `minDate?: Date` * - The earliest date that can be selected. * - Defaults to `undefined` (no restriction). * - Example: `minDate={new Date()}` prevents selecting past dates. * * - `maxDate?: Date` * - The latest date that can be selected. * - Defaults to `undefined` (no restriction). * - Example: `maxDate={new Date(2033, 11, 31)}` restricts selection to dates on or before December 31, 2033. * * - `timePicker?: boolean` * - Whether to include a time picker along with the date input. * * - `rightIcon?: JSX.Element` * - An optional icon or element to display on the right side of the input field. * * - `className?: string` * - An optional CSS class to apply custom styles to the input field. * - Example: `className="custom-date-input"`. * * ## Notes: * - Ensure the `minDate` and `maxDate` props are valid `Date` objects. Invalid dates may cause unexpected behavior. * - The component handles date parsing and validation for manual input but requires users to input dates in `DD/MM/YYYY` format. * - Custom styles applied via the `className` prop will merge with the component's default styles. */ function DateInput({ value, onChange = () => { }, disabled = false, minDate, maxDate, timePicker = false, className, style, placeholder = 'DD/MM/YYYY', rightIcon, label = '', error = '', }) { var _a, _b; const [isCalendarOpen, setIsCalendarOpen] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); const inputRef = React.useRef(null); const [inputValue, setInputValue] = React.useState(value ? value.toLocaleDateString('en-GB') : ''); const [currentMonth, setCurrentMonth] = React.useState((_a = value === null || value === undefined ? undefined : value.getMonth()) !== null && _a !== undefined ? _a : new Date().getMonth()); const [currentYear, setCurrentYear] = React.useState((_b = value === null || value === undefined ? undefined : value.getFullYear()) !== null && _b !== undefined ? _b : new Date().getFullYear()); const [labelRef, dynamicStyles] = useLabelStyle(label); const isDateDisabled = (date) => { if (disabled) return true; if (minDate && date < new Date(minDate.setHours(0, 0, 0, 0))) return true; if (maxDate && date > new Date(maxDate.setHours(23, 59, 59, 999))) return true; return false; }; const validateAndPropagateDate = (formattedValue) => { const [day, month, year] = formattedValue.split('/').map(Number); if (day && month && year && formattedValue.length === 10) { const date = new Date(year, month - 1, day); if (date.getDate() === day && date.getMonth() === month - 1 && date.getFullYear() === year && !isDateDisabled(date)) { setErrorMessage(''); onChange(date); } else { setErrorMessage(isDateDisabled(date) ? `Date must be ${minDate ? `after ${minDate.toLocaleDateString('en-GB')}` : ''} ${maxDate ? `and before ${maxDate.toLocaleDateString('en-GB')}` : ''}.` : 'Invalid date.'); onChange(null); } } else { setErrorMessage(''); onChange(null); } }; const handleInputChange = (e) => { const rawValue = e.target.value.replace(/[^0-9]/g, ''); let formattedValue = ''; e.target.selectionStart || 0; if (rawValue.length > 0) { formattedValue += rawValue.slice(0, 2); } if (rawValue.length > 2) { formattedValue += '/' + rawValue.slice(2, 4); } if (rawValue.length > 4) { formattedValue += '/' + rawValue.slice(4, 8); } setInputValue(formattedValue); validateAndPropagateDate(formattedValue); }; React.useEffect(() => { if (value) { setInputValue(timePicker ? value.toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour12: true, hour: '2-digit', minute: '2-digit', hourCycle: 'h12', }).replace(' am', ' AM').replace(' pm', ' PM') : value.toLocaleDateString('en-GB')); } else { setInputValue(''); } }, [value]); const toggleCalendar = () => { if (!disabled) { setIsCalendarOpen((prev) => !prev); } }; const handleDateChange = (date) => { if (!isDateDisabled(date)) { onChange(date); setErrorMessage(''); !timePicker && setIsCalendarOpen(false); } }; const handleTimeChange = (time) => { if (value) { const [hour, minute, period] = time.split(/[: ]/); const hours24 = period === "PM" ? (Number(hour) % 12) + 12 : Number(hour) % 12; const updatedDate = new Date(value); updatedDate.setHours(hours24, Number(minute), 0, 0); onChange(updatedDate); setIsCalendarOpen(false); } }; const handleOutsideClick = (event) => { var _a, _b; if (!((_a = inputRef.current) === null || _a === undefined ? undefined : _a.contains(event.target)) && !((_b = dropdownRef.current) === null || _b === undefined ? undefined : _b.contains(event.target))) { setIsCalendarOpen(false); } }; const dropdownRef = React.useRef(null); const timeOptionRefs = React.useRef([]); React.useEffect(() => { if (isCalendarOpen) { document.addEventListener('mousedown', handleOutsideClick); } else { document.removeEventListener('mousedown', handleOutsideClick); } return () => { document.removeEventListener('mousedown', handleOutsideClick); }; }, [isCalendarOpen]); const generateCalendarDays = () => { const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay(); const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); const days = []; for (let i = 0; i < firstDayOfMonth; i++) { days.push(React.createElement("div", { key: `empty-${i}`, className: styles$6.empty })); } for (let day = 1; day <= daysInMonth; day++) { const date = new Date(currentYear, currentMonth, day); const isSelected = value && value.getDate() === day && value.getMonth() === currentMonth && value.getFullYear() === currentYear; const isDisabled = isDateDisabled(date); days.push(React.createElement("button", { key: day, className: `${styles$6.day} ${isSelected ? styles$6.daySelected : ''} ${isDisabled ? styles$6.dayDisabled : ''}`, onClick: () => handleDateChange(date), disabled: isDisabled, type: 'button' }, day)); } return days; }; const renderCalendar = () => (React.createElement("div", { className: `${styles$6.calendarDropdown} ${isCalendarOpen ? styles$6.open : ''}`, ref: dropdownRef, role: "dialog" }, React.createElement("div", { className: styles$6.calendarHeader }, React.createElement("select", { value: currentMonth, onChange: (e) => setCurrentMonth(Number(e.target.value)), disabled: disabled, role: "combobox", "aria-label": "month" }, Array.from({ length: 12 }, (_, index) => React.createElement("option", { key: index, value: index }, new Date(0, index).toLocaleString('default', { month: 'long' })))), React.createElement("select", { value: currentYear, onChange: (e) => setCurrentYear(Number(e.target.value)), disabled: disabled, role: "combobox", "aria-label": "year" }, Array.from({ length: 20 }, (_, index) => new Date().getFullYear() - 10 + index).map((year) => (React.createElement("option", { key: year, value: year }, year))))), React.createElement("div", { className: styles$6.daysOfWeek }, ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (React.createElement("div", { key: day, className: styles$6.dayLabel }, day)))), React.createElement("div", { className: styles$6.days }, generateCalendarDays()), timePicker && renderTimePicker())); React.useEffect(() => { var _a; if (isCalendarOpen && timePicker && value) { const selectedTimeIndex = value.getHours() * 12 + Math.floor(value.getMinutes() / 5); (_a = timeOptionRefs.current[selectedTimeIndex]) === null || _a === undefined ? undefined : _a.scrollIntoView({ behavior: 'instant', block: 'center', }); } }, [isCalendarOpen, timePicker, value]); const renderTimePicker = () => { if (!timePicker) return null; const times = Array.from({ length: 288 }, (_, i) => { const totalMinutes = i * 5; const hour24 = Math.floor(totalMinutes / 60); const hour = hour24 % 12 || 12; const minute = totalMinutes % 60; const period = hour24 < 12 ? "AM" : "PM"; const formattedTime = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} ${period}`; const isSelected = value && value.getHours() === hour24 && value.getMinutes() === minute; return (React.createElement("button", { key: i, ref: (el) => { if (el) timeOptionRefs.current[i] = el; }, onClick: () => handleTimeChange(formattedTime), className: `${styles$6.timeOption} ${isSelected ? styles$6.timeOptionSelected : ""}`, type: "button" }, formattedTime)); }); return (React.createElement("div", { className: styles$6.timePicker }, React.createElement("div", { className: styles$6.timeHeader }, "Select Time"), React.createElement("div", { style: { borderRadius: "4px" } }, times))); }; return (React.createElement(React.Fragment, null, React.createElement("div", { className: `${globalStyles.inputWrapper} ${(error || errorMessage) ? globalStyles.inputWrapperError : ''}`, style: Object.assign(Object.assign({}, dynamicStyles), style) }, React.createElement("label", { ref: labelRef, className: `${globalStyles.label} ${(error || errorMessage) ? globalStyles.errorLabel : ''}` }, label), React.createElement("input", { ref: inputRef, type: "text", value: inputValue, onChange: handleInputChange, placeholder: placeholder, disabled: disabled, className: `${styles$6.dateInput} ${className || ''}`, onFocus: () => setIsCalendarOpen(true) }), React.createElement("button", { type: "button", onClick: toggleCalendar, disabled: disabled, className: globalStyles.rightIcon }, React.createElement("span", null, rightIcon || '🗓️')), isCalendarOpen && renderCalendar()), (error || errorMessage) && React.createElement("div", { className: globalStyles.errorMessage }, error || errorMessage))); } var css_248z$5 = ".DefaultInput-module_defaultInput__blRSH{background-color:transparent;border:none;height:56px;padding:8px;position:relative;width:100%;z-index:1}.DefaultInput-module_defaultInput__blRSH:focus{outline:none}"; var styles$5 = {"defaultInput":"DefaultInput-module_defaultInput__blRSH"}; styleInject(css_248z$5); const DefaultInput = React.forwardRef((_a, ref) => { var { name, type, label, error = '', className = '', style = {} } = _a, rest = __rest(_a, ["name", "type", "label", "error", "className", "style"]); const [labelRef, dynamicStyles] = useLabelStyle(label); return (React.createElement(React.Fragment, null, React.createElement("div", { className: `${globalStyles.inputWrapper} ${className} ${error ? globalStyles.inputWrapperError : ""}`, style: Object.assign(Object.assign({}, dynamicStyles), style) }, React.createElement("label", { ref: labelRef, className: `${globalStyles.label} ${error ? globalStyles.errorLabel : ""}` }, label), React.createElement("input", Object.assign({ id: name, name: name, type: type }, rest, { ref: ref, className: styles$5.defaultInput }))), error && React.createElement("p", { className: globalStyles.errorMessage }, error))); }); function useDragAndDrop({ handleFiles }) { const [isDragging, setIsDragging] = React.useState(false); const handleDragOver = (e) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = () => { setIsDragging(false); }; const handleDrop = (e) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files) handleFiles(e.dataTransfer.files); }; return { isDragging, handleDragOver, handleDragLeave, handleDrop }; } function useFileHandler({ accept, multiple, value, onChange }) { const handleFiles = (files) => { const currentFiles = value ? Array.from(value) : []; const validFiles = Array.from(files).filter((file) => { if (!accept) return true; const acceptedTypes = accept.split(",").map((type) => type.trim().toLowerCase()); return acceptedTypes.some((type) => file.type === type || file.name.toLowerCase().endsWith(type.replace("*", ""))); }); const newFiles = validFiles.filter((newFile) => !currentFiles.some((currentFile) => currentFile.name === newFile.name)); const updatedFiles = multiple ? [...currentFiles, ...newFiles] : newFiles; const dataTransfer = new DataTransfer(); updatedFiles.forEach((file) => dataTransfer.items.add(file)); onChange(dataTransfer.files); }; const fileArray = value ? Array.from(value) : []; return