guestbell-forms 3.0.102 → 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,6 +188,15 @@ export class TagsRaw<
185
188
  ) {
186
189
  this.handleErrors(this.props.tags);
187
190
  }
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);
199
+ }
188
200
  }
189
201
 
190
202
  public handleLeaveMobileClick() {
@@ -447,7 +459,6 @@ export class TagsRaw<
447
459
  if (!this.state.suggestionsVisible) {
448
460
  this.props.onSuggestionsOpened?.();
449
461
  }
450
- this.fetchExistingTags(this.state.value);
451
462
  this.setState(
452
463
  () => ({ textIsFocused: true, suggestionsVisible: true, touched: true }),
453
464
  () => this.handleErrors()
@@ -490,13 +501,11 @@ export class TagsRaw<
490
501
  this.props.tags.concat(suggestions[this.state.preselectedSuggestion])
491
502
  );
492
503
  this.setState({ value: '', preselectedSuggestion: undefined }, () => {
493
- this.fetchExistingTags();
494
504
  this.handleErrors();
495
505
  });
496
506
  } else if (existingTag) {
497
507
  this.props.onTagsChanged(this.props.tags.concat(existingTag));
498
508
  this.setState({ value: '' }, () => {
499
- this.fetchExistingTags();
500
509
  this.handleErrors();
501
510
  });
502
511
  } else if (this.props.allowNew) {
@@ -534,13 +543,15 @@ export class TagsRaw<
534
543
  ? false
535
544
  : this.state.suggestionsVisible,
536
545
  });
546
+ const newTags = newTag
547
+ ? this.props.tags
548
+ ? this.props.tags.concat(newTag)
549
+ : [newTag]
550
+ : this.props.tags;
537
551
  if (newTag) {
538
- this.props.onTagsChanged(
539
- this.props.tags ? this.props.tags.concat(newTag) : [newTag]
540
- );
552
+ this.props.onTagsChanged(newTags);
541
553
  }
542
554
  this.setState({ value: '', textErrors: [] }, () => {
543
- this.fetchExistingTags();
544
555
  this.handleErrors();
545
556
  });
546
557
  };
@@ -599,7 +610,6 @@ export class TagsRaw<
599
610
  }),
600
611
  () => this.handleErrors()
601
612
  );
602
- this.fetchExistingTags(e.target.value);
603
613
  };
604
614
 
605
615
  private handleErrors = (tags: T[] = this.props.tags) => {
@@ -653,9 +663,17 @@ export class TagsRaw<
653
663
  this.props
654
664
  .fetchExistingTags(startsWith, this.props.tags)
655
665
  .then((fetchedExistingTags) => {
666
+ if (fetchedExistingTags) {
667
+ clearTimeout(timer);
668
+ this.setState(() => ({
669
+ fetchedExistingTags,
670
+ fetchingExistingTags: false,
671
+ }));
672
+ }
673
+ })
674
+ .catch(() => {
656
675
  clearTimeout(timer);
657
676
  this.setState(() => ({
658
- fetchedExistingTags,
659
677
  fetchingExistingTags: false,
660
678
  }));
661
679
  });
@@ -699,11 +717,11 @@ export class TagsRaw<
699
717
  circular={true}
700
718
  blank={true}
701
719
  onClick={this.tagRemoveClick(tag)}
702
- className="ml-1 transform-rotate--45 line-height--0 p-0"
720
+ className="tags-input__tag__removeButton p-0"
703
721
  Component={TagButtonComponent}
704
722
  preventsDefault={false}
705
723
  >
706
- <PlusIcon />
724
+ <PlusIcon className="transform-rotate--45" />
707
725
  </Button>
708
726
  )}
709
727
  </>
@@ -724,9 +742,9 @@ export class TagsRaw<
724
742
  );
725
743
  }
726
744
  return (
727
- <div onClick={this.tagClick(tag)} className={className} key={index}>
745
+ <span onClick={this.tagClick(tag)} className={className} key={index}>
728
746
  {body}
729
- </div>
747
+ </span>
730
748
  );
731
749
  }
732
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}