guestbell-forms 3.0.103 → 3.0.105
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/build/components/base/input/useBaseInput.d.ts +44 -0
- package/build/components/base/input/useBaseInput.js +181 -0
- package/build/components/base/input/useBaseInput.js.map +1 -0
- package/build/components/tags/Tags.d.ts +1 -1
- package/build/components/tags/Tags.js +28 -15
- package/build/components/tags/Tags.js.map +1 -1
- package/build/dist/guestbell-forms.css +9 -10
- package/build/dist/guestbell-forms.css.map +1 -1
- package/build/dist/guestbell-forms.min.css +1 -1
- package/build/dist/report.html +2 -2
- package/build/scss/components/tags/tags.scss +9 -8
- package/package.json +1 -1
- package/src/lib/components/base/input/useBaseInput.tsx +239 -0
- package/src/lib/components/tags/Tags.tsx +46 -24
- package/src/lib/scss/components/tags/tags.scss +9 -8
- package/src/stories/TagsDemo.tsx +9 -5
@@ -0,0 +1,239 @@
|
|
1
|
+
// experimental and unfinished and unused for now
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
3
|
+
import {
|
4
|
+
AllowedHtmlElements,
|
5
|
+
BaseTranslations,
|
6
|
+
ValidationError,
|
7
|
+
} from './BaseInput';
|
8
|
+
import * as Validators from '../../../validators';
|
9
|
+
import { FormContextState } from '../../form/FormContext';
|
10
|
+
import guid from '../../utils/Guid';
|
11
|
+
|
12
|
+
interface UseBaseInputProps<TranslationsT extends BaseTranslations> {
|
13
|
+
value?: string;
|
14
|
+
required?: boolean;
|
15
|
+
validators?: string[];
|
16
|
+
customValidators?: Validators.IBaseValidator[];
|
17
|
+
onTheFlightValidate?: (value: string) => boolean;
|
18
|
+
onChange?: (
|
19
|
+
event: React.ChangeEvent<AllowedHtmlElements>,
|
20
|
+
isValid: boolean
|
21
|
+
) => void;
|
22
|
+
onBlur?: () => void;
|
23
|
+
onFocus?: () => void;
|
24
|
+
errors?: ValidationError[];
|
25
|
+
infoText?: string;
|
26
|
+
touchOn?: 'focus' | 'blur';
|
27
|
+
showValidation?: boolean;
|
28
|
+
disabled?: boolean;
|
29
|
+
ignoreContext?: boolean;
|
30
|
+
formContext?: FormContextState;
|
31
|
+
translations?: TranslationsT;
|
32
|
+
validationName?: string;
|
33
|
+
label?: string | JSX.Element;
|
34
|
+
}
|
35
|
+
|
36
|
+
export function useBaseInput<
|
37
|
+
TranslationsT extends BaseTranslations = BaseTranslations
|
38
|
+
>(props: UseBaseInputProps<TranslationsT>) {
|
39
|
+
const {
|
40
|
+
value: initialValue = '',
|
41
|
+
required = false,
|
42
|
+
validators = [],
|
43
|
+
customValidators = [],
|
44
|
+
onTheFlightValidate,
|
45
|
+
onChange,
|
46
|
+
onBlur,
|
47
|
+
onFocus,
|
48
|
+
//errors: propErrors = [],
|
49
|
+
//infoText,
|
50
|
+
touchOn = 'focus',
|
51
|
+
//showValidation = true,
|
52
|
+
disabled = false,
|
53
|
+
ignoreContext = false,
|
54
|
+
formContext,
|
55
|
+
translations,
|
56
|
+
validationName,
|
57
|
+
label,
|
58
|
+
} = props;
|
59
|
+
|
60
|
+
const [value, setValue] = useState<string>(initialValue);
|
61
|
+
const [touched, setTouched] = useState<boolean>(false);
|
62
|
+
const [isValid, setIsValid] = useState<boolean>(true);
|
63
|
+
const [errors, setErrors] = useState<ValidationError[]>([]);
|
64
|
+
const [focused, setFocused] = useState<boolean>(false);
|
65
|
+
const [componentDisabled, setComponentDisabled] = useState<boolean>(disabled);
|
66
|
+
|
67
|
+
const inputRef = useRef<AllowedHtmlElements>(null);
|
68
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
69
|
+
const componentId = useRef(guid());
|
70
|
+
|
71
|
+
const handleValidation = useCallback(
|
72
|
+
(currentValue: string, initializing = false) => {
|
73
|
+
let validationErrors: ValidationError[] = [];
|
74
|
+
let validationStatus = true;
|
75
|
+
|
76
|
+
if (required && !currentValue) {
|
77
|
+
validationErrors.push(
|
78
|
+
translations?.required || 'This field is required'
|
79
|
+
);
|
80
|
+
validationStatus = false;
|
81
|
+
} else if (validators) {
|
82
|
+
validators.forEach((validator) => {
|
83
|
+
let isValidForValidator = false;
|
84
|
+
switch (validator) {
|
85
|
+
case 'email':
|
86
|
+
isValidForValidator = new Validators.EmailValidator().Validate(
|
87
|
+
currentValue,
|
88
|
+
required,
|
89
|
+
(error) => validationErrors.push(error)
|
90
|
+
);
|
91
|
+
break;
|
92
|
+
// Add other validators as needed
|
93
|
+
default:
|
94
|
+
throw new Error(`Validator ${validator} not implemented`);
|
95
|
+
}
|
96
|
+
if (!isValidForValidator) validationStatus = false;
|
97
|
+
});
|
98
|
+
}
|
99
|
+
|
100
|
+
customValidators.forEach((customValidator) => {
|
101
|
+
const isValidForCustomValidator = customValidator.Validate(
|
102
|
+
currentValue,
|
103
|
+
required,
|
104
|
+
(error) => validationErrors.push(error)
|
105
|
+
);
|
106
|
+
if (!isValidForCustomValidator) validationStatus = false;
|
107
|
+
});
|
108
|
+
|
109
|
+
setErrors(validationErrors);
|
110
|
+
setIsValid(validationStatus);
|
111
|
+
|
112
|
+
if (!initializing && !ignoreContext) {
|
113
|
+
formContext?.updateCallback(componentId.current, {
|
114
|
+
isValid: validationStatus,
|
115
|
+
errors: validationErrors,
|
116
|
+
});
|
117
|
+
}
|
118
|
+
|
119
|
+
return { isValid: validationStatus, errors: validationErrors };
|
120
|
+
},
|
121
|
+
[
|
122
|
+
required,
|
123
|
+
validators,
|
124
|
+
customValidators,
|
125
|
+
formContext,
|
126
|
+
translations,
|
127
|
+
ignoreContext,
|
128
|
+
]
|
129
|
+
);
|
130
|
+
|
131
|
+
const handleValueChange = useCallback(
|
132
|
+
(newValue: string) => {
|
133
|
+
if (!onTheFlightValidate || onTheFlightValidate(newValue)) {
|
134
|
+
const result = handleValidation(newValue);
|
135
|
+
setValue(newValue);
|
136
|
+
onChange?.(
|
137
|
+
{
|
138
|
+
target: { value: newValue },
|
139
|
+
} as React.ChangeEvent<AllowedHtmlElements>,
|
140
|
+
result.isValid
|
141
|
+
);
|
142
|
+
}
|
143
|
+
},
|
144
|
+
[handleValidation, onTheFlightValidate, onChange]
|
145
|
+
);
|
146
|
+
|
147
|
+
const handleFocus = useCallback(() => {
|
148
|
+
if (!disabled) {
|
149
|
+
onFocus?.();
|
150
|
+
setFocused(true);
|
151
|
+
if (touchOn === 'focus') setTouched(true);
|
152
|
+
}
|
153
|
+
}, [disabled, onFocus, touchOn]);
|
154
|
+
|
155
|
+
const handleBlur = useCallback(() => {
|
156
|
+
onBlur?.();
|
157
|
+
setFocused(false);
|
158
|
+
if (touchOn === 'blur') setTouched(true);
|
159
|
+
}, [onBlur, touchOn]);
|
160
|
+
|
161
|
+
const scrollTo = useCallback(() => {
|
162
|
+
containerRef.current?.scrollIntoView({ behavior: 'smooth' });
|
163
|
+
}, []);
|
164
|
+
|
165
|
+
const setValid = useCallback(() => {
|
166
|
+
setIsValid(true);
|
167
|
+
setErrors([]);
|
168
|
+
formContext?.updateCallback(componentId.current, {
|
169
|
+
isValid: true,
|
170
|
+
errors: [],
|
171
|
+
});
|
172
|
+
}, [formContext]);
|
173
|
+
|
174
|
+
const setInvalid = useCallback(
|
175
|
+
(validationErrors: ValidationError[] = []) => {
|
176
|
+
setIsValid(false);
|
177
|
+
setErrors(validationErrors);
|
178
|
+
formContext?.updateCallback(componentId.current, {
|
179
|
+
isValid: false,
|
180
|
+
errors: validationErrors,
|
181
|
+
});
|
182
|
+
},
|
183
|
+
[formContext]
|
184
|
+
);
|
185
|
+
|
186
|
+
const disableComponent = useCallback(() => {
|
187
|
+
setComponentDisabled(true);
|
188
|
+
}, []);
|
189
|
+
|
190
|
+
const enableComponent = useCallback(() => {
|
191
|
+
setComponentDisabled(false);
|
192
|
+
}, []);
|
193
|
+
|
194
|
+
useEffect(() => {
|
195
|
+
handleValidation(value, true);
|
196
|
+
|
197
|
+
if (!ignoreContext && formContext) {
|
198
|
+
formContext.subscribe(componentId.current, {
|
199
|
+
componentApi: {
|
200
|
+
disableComponent,
|
201
|
+
enableComponent,
|
202
|
+
focus: () => inputRef.current?.focus(),
|
203
|
+
scrollTo,
|
204
|
+
touch: () => setTouched(true),
|
205
|
+
unTouch: () => setTouched(false),
|
206
|
+
},
|
207
|
+
validation: {
|
208
|
+
isValid,
|
209
|
+
errors,
|
210
|
+
name: validationName || label,
|
211
|
+
},
|
212
|
+
});
|
213
|
+
}
|
214
|
+
|
215
|
+
return () => {
|
216
|
+
if (!ignoreContext) formContext?.unSubscribe(componentId.current);
|
217
|
+
};
|
218
|
+
}, [formContext, ignoreContext, handleValidation, value]);
|
219
|
+
|
220
|
+
return {
|
221
|
+
value,
|
222
|
+
setValue: handleValueChange,
|
223
|
+
isValid,
|
224
|
+
errors,
|
225
|
+
touched,
|
226
|
+
focused,
|
227
|
+
inputRef,
|
228
|
+
containerRef,
|
229
|
+
handleFocus,
|
230
|
+
handleBlur,
|
231
|
+
scrollTo,
|
232
|
+
setValid,
|
233
|
+
setInvalid,
|
234
|
+
disableComponent,
|
235
|
+
enableComponent,
|
236
|
+
setTouched,
|
237
|
+
componentDisabled,
|
238
|
+
};
|
239
|
+
}
|
@@ -176,7 +176,10 @@ export class TagsRaw<
|
|
176
176
|
}
|
177
177
|
}
|
178
178
|
|
179
|
-
public componentDidUpdate(
|
179
|
+
public componentDidUpdate(
|
180
|
+
oldProps: TagsProps<IdT, T> & InjectedProps,
|
181
|
+
oldState: TagsState<IdT, T>
|
182
|
+
) {
|
180
183
|
if (
|
181
184
|
oldProps.tags !== this.props.tags ||
|
182
185
|
oldProps.validators !== this.props.validators ||
|
@@ -185,8 +188,14 @@ export class TagsRaw<
|
|
185
188
|
) {
|
186
189
|
this.handleErrors(this.props.tags);
|
187
190
|
}
|
188
|
-
if (
|
189
|
-
this.
|
191
|
+
if (
|
192
|
+
this.state.suggestionsVisible &&
|
193
|
+
(oldProps.tags !== this.props.tags ||
|
194
|
+
oldProps.existingTags !== this.props.existingTags ||
|
195
|
+
this.state.value !== oldState.value ||
|
196
|
+
this.state.suggestionsVisible !== oldState.suggestionsVisible)
|
197
|
+
) {
|
198
|
+
this.fetchExistingTags(this.state.value);
|
190
199
|
}
|
191
200
|
}
|
192
201
|
|
@@ -450,7 +459,6 @@ export class TagsRaw<
|
|
450
459
|
if (!this.state.suggestionsVisible) {
|
451
460
|
this.props.onSuggestionsOpened?.();
|
452
461
|
}
|
453
|
-
this.fetchExistingTags(this.state.value);
|
454
462
|
this.setState(
|
455
463
|
() => ({ textIsFocused: true, suggestionsVisible: true, touched: true }),
|
456
464
|
() => this.handleErrors()
|
@@ -493,13 +501,11 @@ export class TagsRaw<
|
|
493
501
|
this.props.tags.concat(suggestions[this.state.preselectedSuggestion])
|
494
502
|
);
|
495
503
|
this.setState({ value: '', preselectedSuggestion: undefined }, () => {
|
496
|
-
this.fetchExistingTags();
|
497
504
|
this.handleErrors();
|
498
505
|
});
|
499
506
|
} else if (existingTag) {
|
500
507
|
this.props.onTagsChanged(this.props.tags.concat(existingTag));
|
501
508
|
this.setState({ value: '' }, () => {
|
502
|
-
this.fetchExistingTags();
|
503
509
|
this.handleErrors();
|
504
510
|
});
|
505
511
|
} else if (this.props.allowNew) {
|
@@ -537,13 +543,15 @@ export class TagsRaw<
|
|
537
543
|
? false
|
538
544
|
: this.state.suggestionsVisible,
|
539
545
|
});
|
546
|
+
const newTags = newTag
|
547
|
+
? this.props.tags
|
548
|
+
? this.props.tags.concat(newTag)
|
549
|
+
: [newTag]
|
550
|
+
: this.props.tags;
|
540
551
|
if (newTag) {
|
541
|
-
this.props.onTagsChanged(
|
542
|
-
this.props.tags ? this.props.tags.concat(newTag) : [newTag]
|
543
|
-
);
|
552
|
+
this.props.onTagsChanged(newTags);
|
544
553
|
}
|
545
554
|
this.setState({ value: '', textErrors: [] }, () => {
|
546
|
-
this.fetchExistingTags();
|
547
555
|
this.handleErrors();
|
548
556
|
});
|
549
557
|
};
|
@@ -602,7 +610,6 @@ export class TagsRaw<
|
|
602
610
|
}),
|
603
611
|
() => this.handleErrors()
|
604
612
|
);
|
605
|
-
this.fetchExistingTags(e.target.value);
|
606
613
|
};
|
607
614
|
|
608
615
|
private handleErrors = (tags: T[] = this.props.tags) => {
|
@@ -653,15 +660,30 @@ export class TagsRaw<
|
|
653
660
|
() => this.setState(() => ({ fetchingExistingTags: true })),
|
654
661
|
this.props.loadingDelayMs
|
655
662
|
);
|
656
|
-
this.props
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
663
|
+
const prom = this.props.fetchExistingTags(startsWith, this.props.tags);
|
664
|
+
if (prom) {
|
665
|
+
prom
|
666
|
+
.then((fetchedExistingTags) => {
|
667
|
+
if (fetchedExistingTags) {
|
668
|
+
clearTimeout(timer);
|
669
|
+
this.setState(() => ({
|
670
|
+
fetchedExistingTags,
|
671
|
+
fetchingExistingTags: false,
|
672
|
+
}));
|
673
|
+
}
|
674
|
+
})
|
675
|
+
.catch(() => {
|
676
|
+
clearTimeout(timer);
|
677
|
+
this.setState(() => ({
|
678
|
+
fetchingExistingTags: false,
|
679
|
+
}));
|
680
|
+
});
|
681
|
+
} else {
|
682
|
+
clearTimeout(timer);
|
683
|
+
this.setState(() => ({
|
684
|
+
fetchingExistingTags: false,
|
685
|
+
}));
|
686
|
+
}
|
665
687
|
}
|
666
688
|
}
|
667
689
|
|
@@ -702,11 +724,11 @@ export class TagsRaw<
|
|
702
724
|
circular={true}
|
703
725
|
blank={true}
|
704
726
|
onClick={this.tagRemoveClick(tag)}
|
705
|
-
className="
|
727
|
+
className="tags-input__tag__removeButton p-0"
|
706
728
|
Component={TagButtonComponent}
|
707
729
|
preventsDefault={false}
|
708
730
|
>
|
709
|
-
<PlusIcon />
|
731
|
+
<PlusIcon className="transform-rotate--45" />
|
710
732
|
</Button>
|
711
733
|
)}
|
712
734
|
</>
|
@@ -727,9 +749,9 @@ export class TagsRaw<
|
|
727
749
|
);
|
728
750
|
}
|
729
751
|
return (
|
730
|
-
<
|
752
|
+
<span onClick={this.tagClick(tag)} className={className} key={index}>
|
731
753
|
{body}
|
732
|
-
</
|
754
|
+
</span>
|
733
755
|
);
|
734
756
|
}
|
735
757
|
|
@@ -12,26 +12,27 @@
|
|
12
12
|
}
|
13
13
|
.tags-input__tag {
|
14
14
|
position: relative;
|
15
|
-
display: inline-flex;
|
16
|
-
align-items: center;
|
17
|
-
padding-left: 0.25rem;
|
18
15
|
margin-right: 0.25rem;
|
19
|
-
margin-bottom: 0.125rem;
|
20
|
-
margin-top: 0.125rem;
|
21
16
|
&.tags-input__tag-chip {
|
22
17
|
border: 1px solid;
|
23
18
|
border-radius: 1.5rem;
|
24
19
|
}
|
25
20
|
}
|
21
|
+
.tags-input__tag__removeButton {
|
22
|
+
margin-left: 2px;
|
23
|
+
svg {
|
24
|
+
vertical-align: -0.4rem;
|
25
|
+
}
|
26
|
+
}
|
26
27
|
.tags-input__tags__wrapper {
|
27
28
|
display: flex;
|
28
29
|
align-items: center;
|
30
|
+
line-height: 1.75;
|
29
31
|
}
|
30
32
|
.tags-input__tag__wrapper {
|
31
|
-
display: inline-flex;
|
32
|
-
flex-wrap: wrap;
|
33
33
|
padding-top: 0.25rem;
|
34
|
-
padding-
|
34
|
+
padding-left: 0.25rem;
|
35
|
+
padding-right: 0.25rem;
|
35
36
|
}
|
36
37
|
&.tags-input--readOnly {
|
37
38
|
border-bottom-color: transparent !important;
|
package/src/stories/TagsDemo.tsx
CHANGED
@@ -38,15 +38,19 @@ export function TagsDemo(props: TagsProps) {
|
|
38
38
|
<Tags
|
39
39
|
title="Tags suggestions"
|
40
40
|
label="Choose or create tags"
|
41
|
-
fetchExistingTags={(startsWith) =>
|
42
|
-
new Promise<Tag[]>((resolve) =>
|
41
|
+
fetchExistingTags={(startsWith, tags) => {
|
42
|
+
return new Promise<Tag[]>((resolve) =>
|
43
43
|
setTimeout(
|
44
44
|
() =>
|
45
|
-
resolve(
|
45
|
+
resolve(
|
46
|
+
existingTags
|
47
|
+
.filter((e) => e.name.includes(startsWith))
|
48
|
+
.filter((e) => !tags.find((t) => t.id === e.id))
|
49
|
+
),
|
46
50
|
1000
|
47
51
|
)
|
48
|
-
)
|
49
|
-
}
|
52
|
+
);
|
53
|
+
}}
|
50
54
|
suggestionsEmptyComponent={'Not found'}
|
51
55
|
tags={tags}
|
52
56
|
onTagsChanged={setTags}
|