-
Notifications
You must be signed in to change notification settings - Fork 76
/
Copy pathindex.tsx
156 lines (142 loc) · 5.59 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
import * as Behaviors from 'hyperview/src/services/behaviors';
import * as Logging from 'hyperview/src/services/logging';
import * as Namespaces from 'hyperview/src/services/namespaces';
import type { HvComponentProps, TextContextType } from 'hyperview/src/types';
import React, { MutableRefObject, useCallback, useEffect, useRef } from 'react';
import {
createProps,
getNameValueFormInputValues,
} from 'hyperview/src/services';
import type { ElementRef } from 'react';
import type { KeyboardTypeOptions } from 'react-native';
import { LOCAL_NAME } from 'hyperview/src/types';
import { TextInput } from 'react-native';
import TinyMask from 'hyperview/src/mask';
import debounce from 'lodash/debounce';
const HvTextField = (props: HvComponentProps) => {
if (props.element.localName === LOCAL_NAME.TEXT_AREA) {
Logging.warn(
'Deprecation notice: `<text-area>` tag is deprecated and will be removed in a future version. Use `<text-field>` with attribute `multiline="true"` as a replacement.',
);
}
if (props.element.getAttribute('hide') === 'true') {
return null;
}
// Extract known attributes into their own variables
const autoFocus = props.element.getAttribute('auto-focus') === 'true';
const debounceTimeMs =
parseInt(props.element.getAttribute('debounce') || '', 10) || 0;
const defaultValue = props.element.getAttribute('value') || undefined;
const editable = props.element.getAttribute('editable') !== 'false';
const keyboardType =
(props.element.getAttribute('keyboard-type') as KeyboardTypeOptions) ||
undefined;
const multiline =
props.element.localName === LOCAL_NAME.TEXT_AREA ||
props.element.getAttribute('multiline') === 'true';
const secureTextEntry = props.element.getAttribute('secure-text') === 'true';
const textContentType =
(props.element.getAttribute('text-content-type') as TextContextType) ||
'none';
// Force email addresses to have no auto-capitalization, others use default
const defaultCapitalization =
keyboardType === 'email-address' ? 'none' : undefined;
const autoCapitalize =
(props.element.getAttribute('auto-capitalize') as
| 'none'
| 'sentences'
| 'words'
| 'characters') || defaultCapitalization;
// Handlers
const setFocus = (focused: boolean) => {
const newElement = props.element.cloneNode(true) as Element;
props.onUpdate(null, 'swap', props.element, { newElement });
if (focused) {
Behaviors.trigger('focus', newElement, props.onUpdate);
} else {
Behaviors.trigger('blur', newElement, props.onUpdate);
}
};
// Create a memoized, debounced function to trigger the "change" behavior
// eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps
const triggerChangeBehaviors = useCallback(
debounce((newElement: Element) => {
Behaviors.trigger('change', newElement, props.onUpdate);
}, debounceTimeMs),
[],
);
// This handler takes care of handling the state, so it shouldn't be debounced
// eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps
const onChangeText = (value: string) => {
const formattedValue = HvTextField.getFormattedValue(props.element, value);
const newElement = props.element.cloneNode(true) as Element;
newElement.setAttribute('value', formattedValue);
props.onUpdate(null, 'swap', props.element, { newElement });
triggerChangeBehaviors(newElement);
};
// eslint-disable-next-line react-hooks/rules-of-hooks
const textInputRef: MutableRefObject<TextInput | null> = useRef(
null as TextInput | null,
);
// eslint-disable-next-line react-hooks/rules-of-hooks
const prevDefaultValue = useRef<string | undefined>(defaultValue);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (prevDefaultValue.current !== defaultValue) {
onChangeText(defaultValue || '');
}
prevDefaultValue.current = defaultValue;
}, [defaultValue, onChangeText]);
const p = {
...createProps(props.element, props.stylesheets, {
...props.options,
focused: textInputRef.current?.isFocused(),
}),
};
return (
<TextInput
{...p}
ref={(ref: ElementRef<typeof TextInput> | null) => {
textInputRef.current = ref;
if (props.options?.registerInputHandler) {
props.options.registerInputHandler(ref);
}
}}
autoCapitalize={autoCapitalize}
autoFocus={autoFocus}
defaultValue={defaultValue}
editable={editable}
keyboardType={keyboardType}
multiline={multiline}
onBlur={() => setFocus(false)}
onChangeText={onChangeText}
onFocus={() => setFocus(true)}
secureTextEntry={secureTextEntry}
textContentType={textContentType}
/>
);
};
HvTextField.namespaceURI = Namespaces.HYPERVIEW;
HvTextField.localName = LOCAL_NAME.TEXT_FIELD;
HvTextField.localNameAliases = [LOCAL_NAME.TEXT_AREA];
HvTextField.getFormInputValues = (
element: Element,
): Array<[string, string]> => {
return getNameValueFormInputValues(element);
};
/**
* Formats the user's input based on element attributes.
* Currently supports the "mask" attribute, which will be applied
* to format the provided value.
*/
HvTextField.getFormattedValue = (element: Element, value: string) => {
if (!element.hasAttribute('mask')) {
return value;
}
const mask = new TinyMask(element.getAttribute('mask'));
// TinyMask returns undefined in some cases (like if the value is an empty string).
// In those situations, we want the formatted value to be an empty string
// (for proper serialization).
return mask.mask(value) || '';
};
export default HvTextField;