flet-box 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Input.js
|
|
3
|
+
* @description Text input with label, validation and controlled/uncontrolled value.
|
|
4
|
+
*/
|
|
5
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
|
+
import { colors } from "../utils/themes.js";
|
|
7
|
+
import { Icon } from "./Icon.js";
|
|
8
|
+
import { TextInputValidator } from "../utils/TextInputValidator.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} InputProps
|
|
12
|
+
* @property {string} [value=""] - Initial value of the input field.
|
|
13
|
+
* @property {string} [placeholder=""] - Placeholder text shown when the field is empty.
|
|
14
|
+
* @property {string} [type="text"] - HTML input type (e.g. "text", "password", "email").
|
|
15
|
+
* @property {string} [label] - Label text rendered above the input.
|
|
16
|
+
* @property {boolean|string} [error=false] - If truthy, renders the input in error state. A string value is used as the error message.
|
|
17
|
+
* @property {boolean} [disabled=false] - Disables interaction with the input.
|
|
18
|
+
* @property {boolean} [readonly=false] - Makes the input read-only (focusable but not editable).
|
|
19
|
+
* @property {"small"|"medium"|"large"} [size="medium"] - Controls padding and font size.
|
|
20
|
+
* @property {"outlined"|"filled"|"underlined"} [variant="outlined"] - Visual style of the input border.
|
|
21
|
+
* @property {boolean} [fullWidth=false] - When true, the input stretches to fill its container.
|
|
22
|
+
* @property {number} [borderRadius=24] - Border radius of the input wrapper in pixels.
|
|
23
|
+
* @property {string} [iconLeft] - Material icon name to show on the left side.
|
|
24
|
+
* @property {string} [iconRight] - Material icon name to show on the right side.
|
|
25
|
+
* @property {string} [iconColor] - Color of the icons.
|
|
26
|
+
* @property {number} [iconSize=20] - Size of the icons in pixels.
|
|
27
|
+
* @property {Function} [onIconPress] - Called when either icon is pressed.
|
|
28
|
+
* @property {"none"|"letters"|"numbers"|"email"|"alphanumeric"|"safe"|"custom"} [validation="none"] - Validation mode applied on each keystroke and on blur.
|
|
29
|
+
* @property {number} [maxLength] - Maximum number of characters allowed.
|
|
30
|
+
* @property {boolean} [required=false] - Marks the field as required; validation fails on empty values.
|
|
31
|
+
* @property {string} [customPattern] - RegExp string used when validation is "custom".
|
|
32
|
+
* @property {Function} [onValidated] - Called with (isValid, message) after every validation run.
|
|
33
|
+
* @property {boolean} [showValidationMessage=true] - Whether to display the inline validation message below the input.
|
|
34
|
+
* @property {boolean} [showValidationIcon=true] - Whether to show the ✓/✗ icon inside the input.
|
|
35
|
+
* @property {Function} [onInput] - Called with (value, event) on every keystroke.
|
|
36
|
+
* @property {Function} [onChange] - Called with (value, event) after filtering/validation.
|
|
37
|
+
* @property {Function} [onFocus] - Called when the input receives focus.
|
|
38
|
+
* @property {Function} [onBlur] - Called when the input loses focus and final validation runs.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A fully-featured text input widget with built-in validation, icons, and theme support.
|
|
43
|
+
*
|
|
44
|
+
* Supports multiple visual variants (outlined, filled, underlined), configurable sizes,
|
|
45
|
+
* optional left/right icons, and a rich validation system driven by `TextInputValidator`.
|
|
46
|
+
* The public API exposed on the returned element allows programmatic control of the value,
|
|
47
|
+
* validation state, focus, and blur.
|
|
48
|
+
*
|
|
49
|
+
* @param {InputProps} props
|
|
50
|
+
* @returns {HTMLElement} A `<div>` acting as the input container, augmented with public methods:
|
|
51
|
+
* - `getValue()` → current string value
|
|
52
|
+
* - `setValue(newValue)` → updates value and reruns validation
|
|
53
|
+
* - `isValid()` → returns whether the current value passes validation
|
|
54
|
+
* - `validate()` → runs validation and updates the UI, returns `{ valid, message }`
|
|
55
|
+
* - `reset()` → clears the value
|
|
56
|
+
* - `focus()` / `blur()` → delegate to the native `<input>`
|
|
57
|
+
* - `_input` → reference to the underlying native `<input>` element
|
|
58
|
+
*/
|
|
59
|
+
export const Input = (props) => {
|
|
60
|
+
// ========== PROPS ==========
|
|
61
|
+
const {
|
|
62
|
+
value = "",
|
|
63
|
+
placeholder = "",
|
|
64
|
+
type = "text",
|
|
65
|
+
label,
|
|
66
|
+
error = false,
|
|
67
|
+
disabled = false,
|
|
68
|
+
readonly = false,
|
|
69
|
+
size = "medium",
|
|
70
|
+
variant = "outlined",
|
|
71
|
+
fullWidth = false,
|
|
72
|
+
borderRadius = 24,
|
|
73
|
+
|
|
74
|
+
iconLeft,
|
|
75
|
+
iconRight,
|
|
76
|
+
iconColor = colors.textSecondary,
|
|
77
|
+
iconSize = 20,
|
|
78
|
+
onIconPress,
|
|
79
|
+
|
|
80
|
+
validation = "none",
|
|
81
|
+
maxLength,
|
|
82
|
+
required = false,
|
|
83
|
+
customPattern,
|
|
84
|
+
onValidated,
|
|
85
|
+
showValidationMessage = true,
|
|
86
|
+
showValidationIcon = true,
|
|
87
|
+
|
|
88
|
+
onInput,
|
|
89
|
+
onChange,
|
|
90
|
+
onFocus,
|
|
91
|
+
onBlur,
|
|
92
|
+
...rest
|
|
93
|
+
} = props;
|
|
94
|
+
|
|
95
|
+
// ========== INTERNAL STATE ==========
|
|
96
|
+
let currentValue = value;
|
|
97
|
+
let isValid = true;
|
|
98
|
+
let validationMessage = "";
|
|
99
|
+
|
|
100
|
+
// DOM element references — populated during the build phase below
|
|
101
|
+
let inputElement = null;
|
|
102
|
+
let validationIcon = null;
|
|
103
|
+
let errorMessageElement = null;
|
|
104
|
+
let inputWrapper = null;
|
|
105
|
+
let container = null;
|
|
106
|
+
|
|
107
|
+
// Whether the native input currently holds focus. Used to keep the focus
|
|
108
|
+
// ring (a box-shadow) in sync with validation colors without ever changing
|
|
109
|
+
// the border width — changing the width would resize the box and shift layout.
|
|
110
|
+
const isFocused = () => document.activeElement === inputElement;
|
|
111
|
+
|
|
112
|
+
// ========== SIZES ==========
|
|
113
|
+
// Maps the `size` prop to concrete CSS values so the rest of the code stays size-agnostic.
|
|
114
|
+
const sizes = {
|
|
115
|
+
small: { padding: "6px 12px", fontSize: "12px", iconSize: 16 },
|
|
116
|
+
medium: { padding: "10px 14px", fontSize: "14px", iconSize: 20 },
|
|
117
|
+
large: { padding: "14px 18px", fontSize: "16px", iconSize: 24 },
|
|
118
|
+
};
|
|
119
|
+
const sz = sizes[size] || sizes.medium;
|
|
120
|
+
|
|
121
|
+
// ========== VALIDATION ==========
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validates a value against the current validation rules.
|
|
125
|
+
* Does NOT mutate state — just returns the result so it can be used
|
|
126
|
+
* both inline (on input) and on blur.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} val - The value to validate.
|
|
129
|
+
* @returns {{ valid: boolean, message: string }}
|
|
130
|
+
*/
|
|
131
|
+
const validateValue = (val) => {
|
|
132
|
+
let valid = true;
|
|
133
|
+
let message = "";
|
|
134
|
+
|
|
135
|
+
if (required && !val) {
|
|
136
|
+
valid = false;
|
|
137
|
+
message = "This field is required";
|
|
138
|
+
} else if (val && valid) {
|
|
139
|
+
switch (validation) {
|
|
140
|
+
case "letters":
|
|
141
|
+
if (!TextInputValidator.isOnlyLetters(val)) {
|
|
142
|
+
valid = false;
|
|
143
|
+
message = "Only letters allowed";
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case "numbers":
|
|
147
|
+
if (!TextInputValidator.isOnlyNumbers(val)) {
|
|
148
|
+
valid = false;
|
|
149
|
+
message = "Only numbers allowed";
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case "email":
|
|
153
|
+
if (!TextInputValidator.isEmail(val)) {
|
|
154
|
+
valid = false;
|
|
155
|
+
message = "Invalid email address";
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
case "alphanumeric":
|
|
159
|
+
if (!/^[a-zA-Z0-9\s]+$/.test(val)) {
|
|
160
|
+
valid = false;
|
|
161
|
+
message = "Only letters and numbers allowed";
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case "safe":
|
|
165
|
+
if (!TextInputValidator.isSafe(val)) {
|
|
166
|
+
valid = false;
|
|
167
|
+
message = "Contains unsafe characters";
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case "custom":
|
|
171
|
+
if (customPattern && !new RegExp(customPattern).test(val)) {
|
|
172
|
+
valid = false;
|
|
173
|
+
message = "Invalid format";
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { valid, message };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Strips disallowed characters from a raw input value based on the active
|
|
183
|
+
* validation mode. This is a live filter applied on every keystroke so the
|
|
184
|
+
* user cannot type invalid characters in the first place.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} val - Raw value from the input event.
|
|
187
|
+
* @returns {string} Filtered value safe to store and display.
|
|
188
|
+
*/
|
|
189
|
+
const filterValue = (val) => {
|
|
190
|
+
if (!val) return val;
|
|
191
|
+
switch (validation) {
|
|
192
|
+
case "letters":
|
|
193
|
+
return TextInputValidator.onlyLetters(val);
|
|
194
|
+
case "numbers":
|
|
195
|
+
return TextInputValidator.onlyNumbers(val);
|
|
196
|
+
case "email":
|
|
197
|
+
return TextInputValidator.filterEmail(val);
|
|
198
|
+
case "alphanumeric":
|
|
199
|
+
return TextInputValidator.onlyAlphanumeric(val);
|
|
200
|
+
case "safe":
|
|
201
|
+
return TextInputValidator.safeText(val);
|
|
202
|
+
default:
|
|
203
|
+
return val;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// ========== UPDATE VALIDATION UI ==========
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Syncs the visible validation state (border color, icon, error message) with
|
|
211
|
+
* the current `isValid` / `validationMessage` / `error` prop values.
|
|
212
|
+
* Should be called after every state change that could affect the UI.
|
|
213
|
+
*
|
|
214
|
+
* @param {boolean} valid - Whether the current value passes validation.
|
|
215
|
+
*/
|
|
216
|
+
const updateValidationUI = (valid) => {
|
|
217
|
+
if (!inputWrapper) return;
|
|
218
|
+
|
|
219
|
+
// Border color logic:
|
|
220
|
+
// - error prop takes precedence over internal validation
|
|
221
|
+
// - green border when valid and a non-trivial validation mode is active
|
|
222
|
+
let borderColorValue = colors.border;
|
|
223
|
+
if (error) {
|
|
224
|
+
borderColorValue = colors.danger;
|
|
225
|
+
} else if (!valid && currentValue) {
|
|
226
|
+
borderColorValue = colors.danger;
|
|
227
|
+
} else if (currentValue && valid && validation !== "none") {
|
|
228
|
+
borderColorValue = colors.success;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The border keeps a fixed 1px width in every state; focus thickness is
|
|
232
|
+
// drawn as a 1px box-shadow ring instead. This changes the look on focus
|
|
233
|
+
// without altering the element's box size, so nothing gets pushed around.
|
|
234
|
+
if (variant === "outlined") {
|
|
235
|
+
inputWrapper.style.borderColor = borderColorValue;
|
|
236
|
+
inputWrapper.style.boxShadow = isFocused()
|
|
237
|
+
? `0 0 0 1px ${borderColorValue}`
|
|
238
|
+
: "none";
|
|
239
|
+
} else if (variant === "underlined") {
|
|
240
|
+
inputWrapper.style.borderBottomColor = borderColorValue;
|
|
241
|
+
inputWrapper.style.boxShadow = isFocused()
|
|
242
|
+
? `inset 0 -1px 0 0 ${borderColorValue}`
|
|
243
|
+
: "none";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (showValidationIcon && validationIcon) {
|
|
247
|
+
if (currentValue && validation !== "none") {
|
|
248
|
+
validationIcon.textContent = isValid ? "✓" : "✗";
|
|
249
|
+
validationIcon.style.color = isValid ? colors.success : colors.danger;
|
|
250
|
+
validationIcon.style.display = "flex";
|
|
251
|
+
} else {
|
|
252
|
+
validationIcon.style.display = "none";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (errorMessageElement) {
|
|
257
|
+
if (validationMessage && showValidationMessage && !isValid && !error) {
|
|
258
|
+
errorMessageElement.textContent = validationMessage;
|
|
259
|
+
errorMessageElement.style.color = colors.danger;
|
|
260
|
+
errorMessageElement.style.display = "block";
|
|
261
|
+
} else if (error) {
|
|
262
|
+
errorMessageElement.textContent =
|
|
263
|
+
error === true ? "Invalid value" : error;
|
|
264
|
+
errorMessageElement.style.color = colors.danger;
|
|
265
|
+
errorMessageElement.style.display = "block";
|
|
266
|
+
} else {
|
|
267
|
+
errorMessageElement.style.display = "none";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Central handler for every input event. Filters the raw value, enforces
|
|
274
|
+
* `maxLength`, runs validation, updates the UI, and fires all relevant
|
|
275
|
+
* callbacks in the correct order.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} val - The raw value from the native input event.
|
|
278
|
+
* @param {Event} event - The originating DOM event (passed through to callbacks).
|
|
279
|
+
*/
|
|
280
|
+
const handleInput = (val, event) => {
|
|
281
|
+
let filtered = filterValue(val);
|
|
282
|
+
if (maxLength) filtered = filtered.slice(0, maxLength);
|
|
283
|
+
|
|
284
|
+
currentValue = filtered;
|
|
285
|
+
if (inputElement) {
|
|
286
|
+
inputElement.value = filtered;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result = validateValue(filtered);
|
|
290
|
+
isValid = result.valid;
|
|
291
|
+
validationMessage = result.message;
|
|
292
|
+
|
|
293
|
+
updateValidationUI(isValid);
|
|
294
|
+
|
|
295
|
+
if (onInput) onInput(filtered, event);
|
|
296
|
+
if (onValidated) onValidated(isValid, validationMessage);
|
|
297
|
+
if (onChange) onChange(filtered, event);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// ========== BUILD UI ==========
|
|
301
|
+
|
|
302
|
+
// --- Main container — uses WidgetFactory so it inherits layout helpers ---
|
|
303
|
+
container = WidgetFactory({
|
|
304
|
+
tag: "div",
|
|
305
|
+
widgetName: "Input",
|
|
306
|
+
display: "inline-flex",
|
|
307
|
+
flexDirection: "column",
|
|
308
|
+
gap: 4,
|
|
309
|
+
width: fullWidth ? "100%" : "auto",
|
|
310
|
+
disableTransform: true,
|
|
311
|
+
pointerEvents: "auto",
|
|
312
|
+
touchAction: "manipulation",
|
|
313
|
+
...rest,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// --- Optional label rendered above the input ---
|
|
317
|
+
if (label) {
|
|
318
|
+
const requiredMark = required ? " *" : "";
|
|
319
|
+
const labelEl = WidgetFactory({
|
|
320
|
+
tag: "label",
|
|
321
|
+
text: label + requiredMark,
|
|
322
|
+
fontSize: 12,
|
|
323
|
+
fontWeight: 500,
|
|
324
|
+
color: error ? colors.danger : colors.textSecondary,
|
|
325
|
+
marginBottom: 2,
|
|
326
|
+
cursor: "text",
|
|
327
|
+
});
|
|
328
|
+
// Clicking the label focuses the native input — mirrors native <label for> behavior
|
|
329
|
+
labelEl.addEventListener("click", () => {
|
|
330
|
+
inputElement?.focus();
|
|
331
|
+
});
|
|
332
|
+
container.appendChild(labelEl);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- Wrapper — contains icons + native input, owns the border/fill style ---
|
|
336
|
+
let wrapperStyles = {
|
|
337
|
+
display: "flex",
|
|
338
|
+
alignItems: "center",
|
|
339
|
+
width: "100%",
|
|
340
|
+
transition: "all 0.2s ease",
|
|
341
|
+
pointerEvents: "auto",
|
|
342
|
+
touchAction: "manipulation",
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (variant === "filled") {
|
|
346
|
+
wrapperStyles.backgroundColor = colors.gray100;
|
|
347
|
+
wrapperStyles.borderRadius =
|
|
348
|
+
typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
|
|
349
|
+
wrapperStyles.border = "none";
|
|
350
|
+
} else if (variant === "underlined") {
|
|
351
|
+
wrapperStyles.backgroundColor = "transparent";
|
|
352
|
+
wrapperStyles.borderBottom = `1px solid ${error ? colors.danger : colors.border}`;
|
|
353
|
+
wrapperStyles.borderRadius = "0";
|
|
354
|
+
} else {
|
|
355
|
+
// Default: outlined
|
|
356
|
+
wrapperStyles.backgroundColor = "transparent";
|
|
357
|
+
wrapperStyles.borderRadius =
|
|
358
|
+
typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
|
|
359
|
+
wrapperStyles.border = `1px solid ${error ? colors.danger : colors.border}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
inputWrapper = WidgetFactory({
|
|
363
|
+
tag: "div",
|
|
364
|
+
disableTransform: true,
|
|
365
|
+
...wrapperStyles,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// --- Optional left icon ---
|
|
369
|
+
if (iconLeft) {
|
|
370
|
+
const iconEl = Icon({
|
|
371
|
+
name: iconLeft,
|
|
372
|
+
size: iconSize || sz.iconSize,
|
|
373
|
+
color: iconColor,
|
|
374
|
+
marginLeft: "12px",
|
|
375
|
+
flexShrink: 0,
|
|
376
|
+
pointerEvents: "auto",
|
|
377
|
+
});
|
|
378
|
+
if (onIconPress) iconEl.onclick = onIconPress;
|
|
379
|
+
inputWrapper.appendChild(iconEl);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --- Native <input> element ---
|
|
383
|
+
// Using a real <input> (not a div) is critical for:
|
|
384
|
+
// - Mobile keyboard triggering
|
|
385
|
+
// - Accessibility (label association, screen readers)
|
|
386
|
+
// - Browser autofill support
|
|
387
|
+
inputElement = document.createElement("input");
|
|
388
|
+
inputElement.type = type;
|
|
389
|
+
inputElement.placeholder = placeholder || "";
|
|
390
|
+
inputElement.disabled = disabled || false;
|
|
391
|
+
inputElement.readOnly = readonly || false;
|
|
392
|
+
inputElement.value = currentValue;
|
|
393
|
+
|
|
394
|
+
inputElement.style.flex = "1";
|
|
395
|
+
inputElement.style.padding = sz.padding;
|
|
396
|
+
inputElement.style.fontSize = sz.fontSize;
|
|
397
|
+
inputElement.style.border = "none";
|
|
398
|
+
inputElement.style.outline = "none";
|
|
399
|
+
inputElement.style.backgroundColor = "transparent";
|
|
400
|
+
inputElement.style.fontFamily = "inherit";
|
|
401
|
+
inputElement.style.width = "100%";
|
|
402
|
+
inputElement.style.boxSizing = "border-box";
|
|
403
|
+
inputElement.style.touchAction = "manipulation";
|
|
404
|
+
inputElement.style.userSelect = "text";
|
|
405
|
+
inputElement.style.color = disabled ? colors.textDisabled : colors.text;
|
|
406
|
+
inputElement.style.cursor = disabled ? "not-allowed" : "text";
|
|
407
|
+
|
|
408
|
+
// tabIndex ensures the element participates in keyboard navigation
|
|
409
|
+
inputElement.tabIndex = 0;
|
|
410
|
+
|
|
411
|
+
// --- Input events ---
|
|
412
|
+
inputElement.addEventListener("input", (e) => {
|
|
413
|
+
handleInput(e.target.value, e);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
inputElement.addEventListener("focus", (e) => {
|
|
417
|
+
// Highlight the border on focus to signal the active field.
|
|
418
|
+
// The border width stays at 1px; the thicker focus look comes from a
|
|
419
|
+
// 1px box-shadow ring so focusing never resizes the box or shifts layout.
|
|
420
|
+
if (variant === "outlined") {
|
|
421
|
+
inputWrapper.style.borderColor = colors.primary;
|
|
422
|
+
inputWrapper.style.boxShadow = `0 0 0 1px ${colors.primary}`;
|
|
423
|
+
} else if (variant === "underlined") {
|
|
424
|
+
inputWrapper.style.borderBottomColor = colors.primary;
|
|
425
|
+
inputWrapper.style.boxShadow = `inset 0 -1px 0 0 ${colors.primary}`;
|
|
426
|
+
}
|
|
427
|
+
if (onFocus) onFocus(e);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
inputElement.addEventListener("blur", (e) => {
|
|
431
|
+
// Restore normal border and run final validation when the user leaves the field
|
|
432
|
+
if (variant === "outlined") {
|
|
433
|
+
inputWrapper.style.borderColor = error ? colors.danger : colors.border;
|
|
434
|
+
inputWrapper.style.boxShadow = "none";
|
|
435
|
+
} else if (variant === "underlined") {
|
|
436
|
+
inputWrapper.style.borderBottomColor = error
|
|
437
|
+
? colors.danger
|
|
438
|
+
: colors.border;
|
|
439
|
+
inputWrapper.style.boxShadow = "none";
|
|
440
|
+
}
|
|
441
|
+
const result = validateValue(currentValue);
|
|
442
|
+
isValid = result.valid;
|
|
443
|
+
validationMessage = result.message;
|
|
444
|
+
updateValidationUI(isValid);
|
|
445
|
+
if (onBlur) onBlur(e);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
inputWrapper.appendChild(inputElement);
|
|
449
|
+
|
|
450
|
+
// --- Inline validation icon (✓ / ✗) shown inside the input on the right ---
|
|
451
|
+
if (showValidationIcon && validation !== "none") {
|
|
452
|
+
validationIcon = document.createElement("span");
|
|
453
|
+
validationIcon.textContent = "";
|
|
454
|
+
validationIcon.style.width = "24px";
|
|
455
|
+
validationIcon.style.fontSize = "16px";
|
|
456
|
+
validationIcon.style.display = "none";
|
|
457
|
+
validationIcon.style.alignItems = "center";
|
|
458
|
+
validationIcon.style.justifyContent = "center";
|
|
459
|
+
validationIcon.style.marginRight = "8px";
|
|
460
|
+
validationIcon.style.flexShrink = "0";
|
|
461
|
+
inputWrapper.appendChild(validationIcon);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// --- Optional right icon ---
|
|
465
|
+
if (iconRight) {
|
|
466
|
+
const iconEl = Icon({
|
|
467
|
+
name: iconRight,
|
|
468
|
+
size: iconSize || sz.iconSize,
|
|
469
|
+
color: iconColor,
|
|
470
|
+
marginRight: "12px",
|
|
471
|
+
flexShrink: 0,
|
|
472
|
+
pointerEvents: "auto",
|
|
473
|
+
});
|
|
474
|
+
if (onIconPress) iconEl.onclick = onIconPress;
|
|
475
|
+
inputWrapper.appendChild(iconEl);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
container.appendChild(inputWrapper);
|
|
479
|
+
|
|
480
|
+
// --- Error message element rendered below the input ---
|
|
481
|
+
errorMessageElement = document.createElement("span");
|
|
482
|
+
errorMessageElement.textContent = "";
|
|
483
|
+
errorMessageElement.style.fontSize = "11px";
|
|
484
|
+
errorMessageElement.style.color = colors.danger;
|
|
485
|
+
errorMessageElement.style.marginTop = "2px";
|
|
486
|
+
errorMessageElement.style.display = "none";
|
|
487
|
+
container.appendChild(errorMessageElement);
|
|
488
|
+
|
|
489
|
+
// ========== FORCE FOCUS (mobile and desktop) ==========
|
|
490
|
+
// Clicking anywhere in the wrapper (not just on the <input>) should focus it.
|
|
491
|
+
// This matters most on mobile where touch targets need to be larger.
|
|
492
|
+
inputWrapper.addEventListener("click", (e) => {
|
|
493
|
+
if (e.target !== inputElement) {
|
|
494
|
+
inputElement.focus();
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
inputWrapper.addEventListener(
|
|
499
|
+
"touchstart",
|
|
500
|
+
(e) => {
|
|
501
|
+
if (e.target !== inputElement) {
|
|
502
|
+
inputElement.focus();
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
{ passive: true },
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
container.addEventListener("click", (e) => {
|
|
509
|
+
if (e.target === container || e.target === label) {
|
|
510
|
+
inputElement.focus();
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ========== PUBLIC API ==========
|
|
515
|
+
// These methods are attached directly to the container DOM node so callers
|
|
516
|
+
// can interact with the input without holding a separate reference.
|
|
517
|
+
|
|
518
|
+
/** @returns {string} The current value of the input. */
|
|
519
|
+
container.getValue = () => inputElement?.value || "";
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Programmatically sets the input value and reruns validation.
|
|
523
|
+
* @param {string} newValue - The value to set.
|
|
524
|
+
*/
|
|
525
|
+
container.setValue = (newValue) => {
|
|
526
|
+
currentValue = newValue;
|
|
527
|
+
if (inputElement) {
|
|
528
|
+
inputElement.value = newValue;
|
|
529
|
+
}
|
|
530
|
+
const result = validateValue(newValue);
|
|
531
|
+
isValid = result.valid;
|
|
532
|
+
validationMessage = result.message;
|
|
533
|
+
updateValidationUI(isValid);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/** @returns {boolean} Whether the current value passes all validation rules. */
|
|
537
|
+
container.isValid = () => validateValue(currentValue).valid;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Runs validation against the current value and updates the UI.
|
|
541
|
+
* Useful for triggering validation on form submit without waiting for blur.
|
|
542
|
+
* @returns {{ valid: boolean, message: string }}
|
|
543
|
+
*/
|
|
544
|
+
container.validate = () => {
|
|
545
|
+
const result = validateValue(currentValue);
|
|
546
|
+
updateValidationUI(result.valid);
|
|
547
|
+
return result;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/** Clears the input value (equivalent to `setValue("")`). */
|
|
551
|
+
container.reset = () => container.setValue("");
|
|
552
|
+
|
|
553
|
+
/** Focuses the native input element. */
|
|
554
|
+
container.focus = () => inputElement?.focus();
|
|
555
|
+
|
|
556
|
+
/** Removes focus from the native input element. */
|
|
557
|
+
container.blur = () => inputElement?.blur();
|
|
558
|
+
|
|
559
|
+
// Direct reference to the native <input> for advanced use cases
|
|
560
|
+
container._input = inputElement;
|
|
561
|
+
|
|
562
|
+
return container;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
export default Input;
|