guestbell-forms 3.0.103 → 3.0.104
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 +18 -13
- 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 +31 -16
- 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) => {
|
@@ -656,9 +663,17 @@ export class TagsRaw<
|
|
656
663
|
this.props
|
657
664
|
.fetchExistingTags(startsWith, this.props.tags)
|
658
665
|
.then((fetchedExistingTags) => {
|
666
|
+
if (fetchedExistingTags) {
|
667
|
+
clearTimeout(timer);
|
668
|
+
this.setState(() => ({
|
669
|
+
fetchedExistingTags,
|
670
|
+
fetchingExistingTags: false,
|
671
|
+
}));
|
672
|
+
}
|
673
|
+
})
|
674
|
+
.catch(() => {
|
659
675
|
clearTimeout(timer);
|
660
676
|
this.setState(() => ({
|
661
|
-
fetchedExistingTags,
|
662
677
|
fetchingExistingTags: false,
|
663
678
|
}));
|
664
679
|
});
|
@@ -702,11 +717,11 @@ export class TagsRaw<
|
|
702
717
|
circular={true}
|
703
718
|
blank={true}
|
704
719
|
onClick={this.tagRemoveClick(tag)}
|
705
|
-
className="
|
720
|
+
className="tags-input__tag__removeButton p-0"
|
706
721
|
Component={TagButtonComponent}
|
707
722
|
preventsDefault={false}
|
708
723
|
>
|
709
|
-
<PlusIcon />
|
724
|
+
<PlusIcon className="transform-rotate--45" />
|
710
725
|
</Button>
|
711
726
|
)}
|
712
727
|
</>
|
@@ -727,9 +742,9 @@ export class TagsRaw<
|
|
727
742
|
);
|
728
743
|
}
|
729
744
|
return (
|
730
|
-
<
|
745
|
+
<span onClick={this.tagClick(tag)} className={className} key={index}>
|
731
746
|
{body}
|
732
|
-
</
|
747
|
+
</span>
|
733
748
|
);
|
734
749
|
}
|
735
750
|
|
@@ -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}
|