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.
@@ -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(oldProps: TagsProps<IdT, T> & InjectedProps) {
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 (oldProps.tags !== this.props.tags && this.state.suggestionsVisible) {
189
- this.fetchExistingTags();
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="ml-1 transform-rotate--45 line-height--0 p-0"
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
- <div onClick={this.tagClick(tag)} className={className} key={index}>
745
+ <span onClick={this.tagClick(tag)} className={className} key={index}>
731
746
  {body}
732
- </div>
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-bottom: 0.125rem;
34
+ padding-left: 0.25rem;
35
+ padding-right: 0.25rem;
35
36
  }
36
37
  &.tags-input--readOnly {
37
38
  border-bottom-color: transparent !important;
@@ -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(existingTags.filter((e) => e.name.includes(startsWith))),
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}