innoboxrr-react-form-elements 3.4.0 → 3.6.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/package.json +2 -2
- package/src/CheckboxInputComponent.jsx +2 -1
- package/src/CodeInputComponent.jsx +4 -2
- package/src/CountrySelectInputComponent.jsx +28 -15
- package/src/DynamicGroupInputComponent.jsx +56 -50
- package/src/FileDropInputComponent.jsx +12 -2
- package/src/FileInputComponent.jsx +2 -2
- package/src/FqsInputComponent.jsx +1 -1
- package/src/RadioInputComponent.jsx +2 -1
- package/src/SingleCheckboxInputComponent.jsx +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "innoboxrr-react-form-elements",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Gemelo React de innoboxrr-form-elements: los mismos componentes, los mismos nombres y el mismo contrato, con diálogos, drawers, menús, avisos y paleta de comandos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@tinymce/tinymce-react": "^6.3.0",
|
|
54
54
|
"@uiw/react-codemirror": "^4.25.11",
|
|
55
55
|
"@yaireo/tagify": "^4.38.0",
|
|
56
|
-
"innoboxrr-form-core": "^2.
|
|
56
|
+
"innoboxrr-form-core": "^2.8.0",
|
|
57
57
|
"innoboxrr-maskjs": "^2.0.0",
|
|
58
58
|
"react-colorful": "^5.8.1",
|
|
59
59
|
"react-phone-number-input": "^3.4.18",
|
|
@@ -21,13 +21,14 @@ export default function CheckboxInputComponent({
|
|
|
21
21
|
}) {
|
|
22
22
|
const [current, set] = useControlled(value, onChange, '')
|
|
23
23
|
const boxClass = useThemeClass('checkbox', customClass)
|
|
24
|
+
const labelClass = useThemeClass('label')
|
|
24
25
|
|
|
25
26
|
const isGroup = Array.isArray(current)
|
|
26
27
|
const checked = isGroup ? current.includes(val) : Boolean(current)
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<div className="fe-mb">
|
|
30
|
-
<label className=
|
|
31
|
+
<label className={labelClass}>
|
|
31
32
|
<input
|
|
32
33
|
className={boxClass}
|
|
33
34
|
type="checkbox"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useState } from 'react'
|
|
2
2
|
import useControlled from './internal/useControlled.js'
|
|
3
|
+
import useTheme, { joinClasses } from './internal/useTheme.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Gemelo de CodeInputComponent.vue: las casillas de un código de un solo uso.
|
|
@@ -20,6 +21,7 @@ export default function CodeInputComponent({
|
|
|
20
21
|
onChange,
|
|
21
22
|
onComplete,
|
|
22
23
|
}) {
|
|
24
|
+
const theme = useTheme()
|
|
23
25
|
const [code, setCode] = useControlled(value, onChange, '')
|
|
24
26
|
const inputs = useRef([])
|
|
25
27
|
const [chars, setChars] = useState(() => Array.from({ length: fields }, (_, i) => (code ?? '')[i] ?? ''))
|
|
@@ -45,12 +47,12 @@ export default function CodeInputComponent({
|
|
|
45
47
|
<div className={['code-input-container', className].filter(Boolean).join(' ')}>
|
|
46
48
|
{title ? <p className="title">{title}</p> : null}
|
|
47
49
|
|
|
48
|
-
<div className=
|
|
50
|
+
<div className={joinClasses('code-input', theme.codeInput)}>
|
|
49
51
|
{chars.map((char, index) => (
|
|
50
52
|
<input
|
|
51
53
|
key={index}
|
|
52
54
|
ref={(element) => { inputs.current[index] = element }}
|
|
53
|
-
className=
|
|
55
|
+
className={theme.codeCell}
|
|
54
56
|
type="text"
|
|
55
57
|
inputMode="numeric"
|
|
56
58
|
pattern="[0-9]*"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useId, useState } from 'react'
|
|
2
2
|
import PhoneInput, { isValidPhoneNumber, parsePhoneNumber } from 'react-phone-number-input'
|
|
3
|
-
import
|
|
3
|
+
import 'react-phone-number-input/style.css'
|
|
4
|
+
import useTheme, { joinClasses } from './internal/useTheme.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Gemelo de CountrySelectInputComponent.vue.
|
|
@@ -15,29 +16,40 @@ import Field from './internal/Field.jsx'
|
|
|
15
16
|
* emitía `change` en Vue, para que el formulario que ya lo consumía siga
|
|
16
17
|
* funcionando igual.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* La hoja de la librería la importa el componente, igual que el de Vue. Antes
|
|
20
|
+
* la tenía que importar la aplicación y, si no lo hacía, el selector de país y
|
|
21
|
+
* el campo salían como controles nativos sin forma. El aspecto sale del tema
|
|
22
|
+
* (`phone`, `phoneInvalid`).
|
|
21
23
|
*/
|
|
22
24
|
export default function CountrySelectInputComponent({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// Sin valor, cada una sale de su token del tema.
|
|
26
|
+
wrapperClass = null,
|
|
27
|
+
containerClass = null,
|
|
28
|
+
labelClass = null,
|
|
26
29
|
label = '',
|
|
27
30
|
help = null,
|
|
28
31
|
defaultPhone = '',
|
|
29
32
|
defaultCountry = null,
|
|
30
33
|
disabled = false,
|
|
31
34
|
name = 'telephone',
|
|
35
|
+
id = null,
|
|
32
36
|
placeholder = 'Ingresa un número telefónico',
|
|
33
37
|
validators = null,
|
|
34
38
|
onCountryChange,
|
|
35
39
|
...rest
|
|
36
40
|
}) {
|
|
41
|
+
const theme = useTheme()
|
|
42
|
+
|
|
43
|
+
// Con el nombre como id, dos teléfonos en la misma página compartían id y
|
|
44
|
+
// la etiqueta del segundo enfocaba el primero.
|
|
45
|
+
const generatedId = useId()
|
|
46
|
+
const inputId = id ?? `${name}-${generatedId}`
|
|
47
|
+
|
|
37
48
|
const [phone, setPhone] = useState(defaultPhone ? String(defaultPhone) : '')
|
|
38
49
|
const [country, setCountry] = useState(defaultCountry ?? undefined)
|
|
39
50
|
|
|
40
51
|
const valid = Boolean(phone) && isValidPhoneNumber(phone)
|
|
52
|
+
const invalid = ! valid && phone.length !== 0
|
|
41
53
|
|
|
42
54
|
const announce = (nextPhone, nextCountry) => {
|
|
43
55
|
const isValid = Boolean(nextPhone) && isValidPhoneNumber(nextPhone)
|
|
@@ -53,14 +65,15 @@ export default function CountrySelectInputComponent({
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
return (
|
|
56
|
-
<div className={wrapperClass}>
|
|
57
|
-
<
|
|
58
|
-
{label ? <label className={labelClass} htmlFor={name}>{label}</label> : null}
|
|
68
|
+
<div className={wrapperClass ?? theme.field}>
|
|
69
|
+
{label ? <label className={labelClass ?? theme.label} htmlFor={inputId}>{label}</label> : null}
|
|
59
70
|
|
|
71
|
+
<div className={containerClass ?? theme.fieldInner}>
|
|
60
72
|
<PhoneInput
|
|
61
|
-
id={
|
|
73
|
+
id={inputId}
|
|
62
74
|
name={name}
|
|
63
|
-
className={(
|
|
75
|
+
className={joinClasses(theme.phone, invalid && theme.phoneInvalid)}
|
|
76
|
+
aria-invalid={invalid || undefined}
|
|
64
77
|
international
|
|
65
78
|
countryCallingCodeEditable={false}
|
|
66
79
|
defaultCountry={defaultCountry ?? undefined}
|
|
@@ -79,9 +92,9 @@ export default function CountrySelectInputComponent({
|
|
|
79
92
|
announce(clean, country)
|
|
80
93
|
}}
|
|
81
94
|
{...rest} />
|
|
82
|
-
|
|
83
|
-
{help ? <p className="text-sm text-gray-500">{help}</p> : null}
|
|
84
95
|
</div>
|
|
96
|
+
|
|
97
|
+
{help ? <p className="fe-text-muted fe-text-sm">{help}</p> : null}
|
|
85
98
|
</div>
|
|
86
99
|
)
|
|
87
100
|
}
|
|
@@ -21,6 +21,7 @@ import SelectInputComponent from './SelectInputComponent.jsx'
|
|
|
21
21
|
import TextInputComponent from './TextInputComponent.jsx'
|
|
22
22
|
import TextareaInputComponent from './TextareaInputComponent.jsx'
|
|
23
23
|
import useControlled from './internal/useControlled.js'
|
|
24
|
+
import useTheme, { joinClasses } from './internal/useTheme.js'
|
|
24
25
|
|
|
25
26
|
const COMPONENTS = {
|
|
26
27
|
text: TextInputComponent,
|
|
@@ -38,15 +39,20 @@ const COMPONENTS = {
|
|
|
38
39
|
* por su cuenta y trae ordenación **por teclado**, que ni SortableJS ni la API
|
|
39
40
|
* nativa de arrastre dan. Aquí no es un detalle: un formulario que solo se
|
|
40
41
|
* puede reordenar con el ratón no es accesible.
|
|
42
|
+
*
|
|
43
|
+
* El aspecto sale del tema, con el mismo marcado que la rama Vue: cada grupo es
|
|
44
|
+
* una superficie con su barra. Antes eran clases de Tailwind y colores escritos
|
|
45
|
+
* para el modo oscuro que el paquete no declara.
|
|
41
46
|
*/
|
|
42
47
|
function SortableGroup({ id, children }) {
|
|
48
|
+
const theme = useTheme()
|
|
43
49
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
|
|
44
50
|
|
|
45
51
|
return (
|
|
46
52
|
<div
|
|
47
53
|
ref={setNodeRef}
|
|
48
54
|
style={{ transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1 }}
|
|
49
|
-
className=
|
|
55
|
+
className={theme.surface}>
|
|
50
56
|
{children({ attributes, listeners })}
|
|
51
57
|
</div>
|
|
52
58
|
)
|
|
@@ -61,6 +67,7 @@ export default function DynamicGroupInputComponent({
|
|
|
61
67
|
removeButtonLabel = 'Eliminar',
|
|
62
68
|
itemLabel = 'Item',
|
|
63
69
|
}) {
|
|
70
|
+
const theme = useTheme()
|
|
64
71
|
const [current, set] = useControlled(value, onChange, [])
|
|
65
72
|
const nextKey = useRef(0)
|
|
66
73
|
|
|
@@ -113,67 +120,66 @@ export default function DynamicGroupInputComponent({
|
|
|
113
120
|
return (
|
|
114
121
|
<div>
|
|
115
122
|
{label ? (
|
|
116
|
-
<label className=
|
|
123
|
+
<label className={theme.label}>
|
|
117
124
|
{label}
|
|
118
125
|
</label>
|
|
119
126
|
) : null}
|
|
120
127
|
|
|
121
128
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
|
122
129
|
<SortableContext items={keyed.map((group) => group.__key)} strategy={verticalListSortingStrategy}>
|
|
123
|
-
<div className=
|
|
130
|
+
<div className={theme.group}>
|
|
124
131
|
{keyed.map((group, index) => (
|
|
125
132
|
<SortableGroup key={group.__key} id={group.__key}>
|
|
126
133
|
{({ attributes, listeners }) => (
|
|
127
134
|
<>
|
|
128
|
-
<div className=
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
</div>
|
|
135
|
+
<div className={theme.toolbar}>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
aria-label={`Mover ${itemLabel} ${index + 1}`}
|
|
139
|
+
className={joinClasses(theme.iconButton, theme.dragHandle, 'drag-handle')}
|
|
140
|
+
{...attributes}
|
|
141
|
+
{...listeners}>
|
|
142
|
+
<IconComponent name="drag" />
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<h4 className={theme.groupTitle}>
|
|
146
|
+
{itemLabel} #{index + 1}
|
|
147
|
+
</h4>
|
|
148
|
+
|
|
149
|
+
<span className={theme.toolbarSpacer} />
|
|
150
|
+
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label={`Duplicar ${itemLabel} ${index + 1}`}
|
|
154
|
+
className={theme.iconButton}
|
|
155
|
+
onClick={() => set([
|
|
156
|
+
...keyed.slice(0, index + 1),
|
|
157
|
+
{ ...group, __key: `grupo-${nextKey.current++}` },
|
|
158
|
+
...keyed.slice(index + 1),
|
|
159
|
+
])}>
|
|
160
|
+
<IconComponent name="copy" />
|
|
161
|
+
</button>
|
|
162
|
+
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
aria-label={`${removeButtonLabel} ${index + 1}`}
|
|
166
|
+
className={joinClasses(theme.iconButton, theme.iconButtonDanger)}
|
|
167
|
+
onClick={() => set(keyed.filter((_, position) => position !== index))}>
|
|
168
|
+
<IconComponent name="delete" />
|
|
169
|
+
</button>
|
|
170
|
+
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
aria-label={`Expandir ${itemLabel} ${index + 1}`}
|
|
174
|
+
aria-expanded={! group._collapsed}
|
|
175
|
+
className={theme.iconButton}
|
|
176
|
+
onClick={() => updateAt(index, '_collapsed', ! group._collapsed)}>
|
|
177
|
+
<IconComponent name={group._collapsed ? 'up' : 'down'} />
|
|
178
|
+
</button>
|
|
173
179
|
</div>
|
|
174
180
|
|
|
175
181
|
{! group._collapsed ? (
|
|
176
|
-
<div className="
|
|
182
|
+
<div className="fe-card-body">
|
|
177
183
|
{inputsConfig.map((field) => {
|
|
178
184
|
const Component = COMPONENTS[field.type] ?? TextInputComponent
|
|
179
185
|
|
|
@@ -208,7 +214,7 @@ export default function DynamicGroupInputComponent({
|
|
|
208
214
|
|
|
209
215
|
<button
|
|
210
216
|
type="button"
|
|
211
|
-
className=
|
|
217
|
+
className={joinClasses(theme.button, 'fe-mt')}
|
|
212
218
|
onClick={() => set([...keyed, emptyGroup()])}>
|
|
213
219
|
{addButtonLabel}
|
|
214
220
|
</button>
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { useRef, useState } from 'react'
|
|
2
|
+
import IconComponent from './IconComponent.jsx'
|
|
3
|
+
import useTheme from './internal/useTheme.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Gemelo de FileDropInputComponent.vue. `onFilesChange` recibe un array de
|
|
5
7
|
* File, venga de arrastrar o del diálogo.
|
|
8
|
+
*
|
|
9
|
+
* `fe-file-drop` no lo definía ninguna hoja, así que la zona salía sin borde ni
|
|
10
|
+
* resalte. Ahora sale del tema, con el mismo icono y el mismo texto secundario
|
|
11
|
+
* que la rama Vue.
|
|
6
12
|
*/
|
|
7
13
|
export default function FileDropInputComponent({
|
|
8
14
|
multiple = false,
|
|
@@ -11,18 +17,20 @@ export default function FileDropInputComponent({
|
|
|
11
17
|
subText = 'o haz clic para seleccionar los archivos.',
|
|
12
18
|
onFilesChange,
|
|
13
19
|
}) {
|
|
20
|
+
const theme = useTheme()
|
|
14
21
|
const input = useRef(null)
|
|
15
22
|
const [dragging, setDragging] = useState(false)
|
|
16
23
|
|
|
17
24
|
return (
|
|
18
25
|
<div
|
|
19
|
-
className=
|
|
26
|
+
className={theme.fileDrop}
|
|
20
27
|
data-dragging={dragging}
|
|
21
28
|
role="button"
|
|
22
29
|
tabIndex={0}
|
|
23
30
|
onClick={() => input.current?.click()}
|
|
24
31
|
onKeyDown={(event) => {
|
|
25
32
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
33
|
+
event.preventDefault()
|
|
26
34
|
input.current?.click()
|
|
27
35
|
}
|
|
28
36
|
}}
|
|
@@ -36,8 +44,10 @@ export default function FileDropInputComponent({
|
|
|
36
44
|
setDragging(false)
|
|
37
45
|
onFilesChange?.(Array.from(event.dataTransfer.files))
|
|
38
46
|
}}>
|
|
47
|
+
<IconComponent name="media" size={32} />
|
|
48
|
+
|
|
39
49
|
<p>{mainText}</p>
|
|
40
|
-
<p>{subText}</p>
|
|
50
|
+
<p className={theme.fileDropHint}>{subText}</p>
|
|
41
51
|
|
|
42
52
|
<input
|
|
43
53
|
ref={input}
|
|
@@ -167,14 +167,14 @@ export default function FileInputComponent({
|
|
|
167
167
|
</ul>
|
|
168
168
|
|
|
169
169
|
{errors.length ? (
|
|
170
|
-
<ul className="fe-list
|
|
170
|
+
<ul className="fe-list fe-error">
|
|
171
171
|
{errors.map((entry, index) => (
|
|
172
172
|
<li key={`${entry.name}-error-${index}`}>{entry.name}: {entry.errors?.join(', ')}</li>
|
|
173
173
|
))}
|
|
174
174
|
</ul>
|
|
175
175
|
) : null}
|
|
176
176
|
|
|
177
|
-
{overTotal ? <p className="
|
|
177
|
+
{overTotal ? <p className="fe-error">El tamaño total supera el máximo permitido.</p> : null}
|
|
178
178
|
|
|
179
179
|
{! autoUpload && files.length ? (
|
|
180
180
|
<button type="button" className="fe-button" disabled={uploading || overTotal} onClick={upload}>
|
|
@@ -34,7 +34,7 @@ export default function FqsInputComponent({
|
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
36
|
<div>
|
|
37
|
-
<h4>{text.title}</h4>
|
|
37
|
+
<h4 className="fe-group-title fe-mb-sm">{text.title}</h4>
|
|
38
38
|
|
|
39
39
|
{items.map((item, index) => (
|
|
40
40
|
<div key={index} className="fe-card fe-card-sm fe-card-body fe-mb">
|
|
@@ -17,10 +17,11 @@ export default function RadioInputComponent({
|
|
|
17
17
|
}) {
|
|
18
18
|
const [current, set] = useControlled(value, onChange, '')
|
|
19
19
|
const radioClass = useThemeClass('radio', customClass)
|
|
20
|
+
const labelClass = useThemeClass('label')
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<div className="fe-mb">
|
|
23
|
-
<label className=
|
|
24
|
+
<label className={labelClass}>
|
|
24
25
|
<input
|
|
25
26
|
className={radioClass}
|
|
26
27
|
type="radio"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import useThemeClass from './internal/useThemeClass.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Gemelo de SingleCheckboxInputComponent.vue.
|
|
3
5
|
*
|
|
@@ -13,10 +15,11 @@ export default function SingleCheckboxInputComponent({
|
|
|
13
15
|
...rest
|
|
14
16
|
}) {
|
|
15
17
|
const htmlId = `${id}_${label}`
|
|
18
|
+
const labelClass = useThemeClass('label')
|
|
16
19
|
|
|
17
20
|
return (
|
|
18
21
|
<div className="fe-mb">
|
|
19
|
-
<label htmlFor={htmlId} className=
|
|
22
|
+
<label htmlFor={htmlId} className={labelClass}>
|
|
20
23
|
<input
|
|
21
24
|
id={htmlId}
|
|
22
25
|
className="fe-checkbox"
|