react-native-transformer-text-input 0.1.0-alpha.0
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/LICENSE +20 -0
- package/README.md +141 -0
- package/RNTransformerTextInput.podspec +35 -0
- package/android/build.gradle +96 -0
- package/android/gradle.properties +5 -0
- package/android/spotless.gradle +19 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TextState.kt +15 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputDecoratorView.kt +160 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputDecoratorViewManager.kt +44 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputJni.kt +25 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputModule.kt +22 -0
- package/android/src/main/java/com/appandflow/transformertextinput/TransformerTextInputPackage.kt +53 -0
- package/android/src/main/jni/CMakeLists.txt +62 -0
- package/android/src/main/jni/TransformerTextInputJni.cpp +94 -0
- package/android/src/main/jni/rntti.h +17 -0
- package/cpp/TransformerTextInputDecoratorViewComponentDescriptor.h +16 -0
- package/cpp/TransformerTextInputDecoratorViewShadowNode.cpp +21 -0
- package/cpp/TransformerTextInputDecoratorViewShadowNode.h +40 -0
- package/cpp/TransformerTextInputRuntime.cpp +86 -0
- package/cpp/TransformerTextInputRuntime.h +31 -0
- package/ios/TransformerTextInputDecoratorView.h +9 -0
- package/ios/TransformerTextInputDecoratorView.mm +256 -0
- package/ios/TransformerTextInputModule.h +8 -0
- package/ios/TransformerTextInputModule.mm +28 -0
- package/lib/module/NativeTransformerTextInputModule.js +5 -0
- package/lib/module/NativeTransformerTextInputModule.js.map +1 -0
- package/lib/module/Transformer.js +15 -0
- package/lib/module/Transformer.js.map +1 -0
- package/lib/module/TransformerTextInput.js +86 -0
- package/lib/module/TransformerTextInput.js.map +1 -0
- package/lib/module/TransformerTextInputDecoratorViewNativeComponent.ts +31 -0
- package/lib/module/formatters/phone-number.js +315 -0
- package/lib/module/formatters/phone-number.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/registry.js +83 -0
- package/lib/module/registry.js.map +1 -0
- package/lib/module/selection.js +48 -0
- package/lib/module/selection.js.map +1 -0
- package/lib/module/utils/useMergeRefs.js +49 -0
- package/lib/module/utils/useMergeRefs.js.map +1 -0
- package/lib/module/utils/useRefEffect.js +37 -0
- package/lib/module/utils/useRefEffect.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeTransformerTextInputModule.d.ts +7 -0
- package/lib/typescript/src/NativeTransformerTextInputModule.d.ts.map +1 -0
- package/lib/typescript/src/Transformer.d.ts +19 -0
- package/lib/typescript/src/Transformer.d.ts.map +1 -0
- package/lib/typescript/src/TransformerTextInput.d.ts +247 -0
- package/lib/typescript/src/TransformerTextInput.d.ts.map +1 -0
- package/lib/typescript/src/TransformerTextInputDecoratorViewNativeComponent.d.ts +12 -0
- package/lib/typescript/src/TransformerTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/formatters/phone-number.d.ts +18 -0
- package/lib/typescript/src/formatters/phone-number.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/registry.d.ts +17 -0
- package/lib/typescript/src/registry.d.ts.map +1 -0
- package/lib/typescript/src/selection.d.ts +4 -0
- package/lib/typescript/src/selection.d.ts.map +1 -0
- package/lib/typescript/src/utils/useMergeRefs.d.ts +20 -0
- package/lib/typescript/src/utils/useMergeRefs.d.ts.map +1 -0
- package/lib/typescript/src/utils/useRefEffect.d.ts +24 -0
- package/lib/typescript/src/utils/useRefEffect.d.ts.map +1 -0
- package/package.json +199 -0
- package/react-native.config.js +13 -0
- package/src/NativeTransformerTextInputModule.ts +10 -0
- package/src/Transformer.ts +32 -0
- package/src/TransformerTextInput.tsx +147 -0
- package/src/TransformerTextInputDecoratorViewNativeComponent.ts +31 -0
- package/src/formatters/phone-number.ts +327 -0
- package/src/index.tsx +10 -0
- package/src/registry.ts +120 -0
- package/src/selection.ts +62 -0
- package/src/utils/useMergeRefs.ts +59 -0
- package/src/utils/useRefEffect.ts +42 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { Transformer, type Selection } from '../Transformer';
|
|
2
|
+
|
|
3
|
+
export type PhoneNumberTransformerOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Country code for phone number formatting.
|
|
6
|
+
* Currently only 'US' is supported.
|
|
7
|
+
* @default 'US'
|
|
8
|
+
*/
|
|
9
|
+
country?: 'US';
|
|
10
|
+
/**
|
|
11
|
+
* Enable debug logging for transformer operations.
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Check if char is a digit
|
|
18
|
+
const isDigit = (char: string | undefined): boolean => {
|
|
19
|
+
'worklet';
|
|
20
|
+
return char !== undefined && char >= '0' && char <= '9';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Count national digits before a position
|
|
24
|
+
// Always skips the first "1" digit when we have/will have +1 prefix
|
|
25
|
+
const countNationalDigitsBefore = (text: string, pos: number): number => {
|
|
26
|
+
'worklet';
|
|
27
|
+
let count = 0;
|
|
28
|
+
let skippedCountryOne = false;
|
|
29
|
+
for (let i = 0; i < Math.min(pos, text.length); i++) {
|
|
30
|
+
if (isDigit(text[i])) {
|
|
31
|
+
// Skip the "1" in "+1" prefix
|
|
32
|
+
if (
|
|
33
|
+
!skippedCountryOne &&
|
|
34
|
+
text[i] === '1' &&
|
|
35
|
+
i > 0 &&
|
|
36
|
+
text[i - 1] === '+'
|
|
37
|
+
) {
|
|
38
|
+
skippedCountryOne = true;
|
|
39
|
+
} else {
|
|
40
|
+
count++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return count;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Find position in formatted text after N national digits
|
|
48
|
+
const positionAfterNationalDigit = (
|
|
49
|
+
text: string,
|
|
50
|
+
targetCount: number,
|
|
51
|
+
): number => {
|
|
52
|
+
'worklet';
|
|
53
|
+
if (targetCount <= 0) {
|
|
54
|
+
// Position after "+1 " prefix if present, else 0
|
|
55
|
+
if (text.startsWith('+1 ')) return 3;
|
|
56
|
+
if (text.startsWith('+1')) return 2;
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
let count = 0;
|
|
60
|
+
let skippedCountryOne = false;
|
|
61
|
+
for (let i = 0; i < text.length; i++) {
|
|
62
|
+
if (isDigit(text[i])) {
|
|
63
|
+
if (
|
|
64
|
+
!skippedCountryOne &&
|
|
65
|
+
text[i] === '1' &&
|
|
66
|
+
i > 0 &&
|
|
67
|
+
text[i - 1] === '+'
|
|
68
|
+
) {
|
|
69
|
+
skippedCountryOne = true;
|
|
70
|
+
} else {
|
|
71
|
+
count++;
|
|
72
|
+
if (count === targetCount) return i + 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return text.length;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Format national digits into US phone format
|
|
80
|
+
const formatUSPhoneNumber = (digits: string): string => {
|
|
81
|
+
'worklet';
|
|
82
|
+
const area = digits.slice(0, 3);
|
|
83
|
+
const exchange = digits.slice(3, 6);
|
|
84
|
+
const subscriber = digits.slice(6, 10);
|
|
85
|
+
|
|
86
|
+
let formatted = '+1 ';
|
|
87
|
+
|
|
88
|
+
if (area.length > 0) {
|
|
89
|
+
formatted += '(' + area;
|
|
90
|
+
if (area.length === 3) {
|
|
91
|
+
formatted += ') ';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (exchange.length > 0) {
|
|
96
|
+
formatted += exchange;
|
|
97
|
+
if (exchange.length === 3 && subscriber.length > 0) {
|
|
98
|
+
formatted += '-';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (subscriber.length > 0) {
|
|
103
|
+
formatted += subscriber;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return formatted;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export class PhoneNumberTransformer extends Transformer {
|
|
110
|
+
constructor({
|
|
111
|
+
country = 'US',
|
|
112
|
+
debug = false,
|
|
113
|
+
}: PhoneNumberTransformerOptions = {}) {
|
|
114
|
+
if (country !== 'US') {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[PhoneNumberTransformer] Country "${country}" is not supported. Only "US" is currently supported.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const worklet = (input: {
|
|
121
|
+
value: string;
|
|
122
|
+
previousValue: string;
|
|
123
|
+
selection: Selection;
|
|
124
|
+
previousSelection: Selection;
|
|
125
|
+
}) => {
|
|
126
|
+
'worklet';
|
|
127
|
+
|
|
128
|
+
const { value, selection, previousValue, previousSelection } = input;
|
|
129
|
+
|
|
130
|
+
// Extract all digits from input
|
|
131
|
+
const allDigits = value.replace(/\D/g, '');
|
|
132
|
+
|
|
133
|
+
// Empty input - clear everything
|
|
134
|
+
if (allDigits.length === 0) {
|
|
135
|
+
return { value: '', selection: { start: 0, end: 0 } };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get national digits - strip leading "1" if enabled since we always add +1 prefix
|
|
139
|
+
const nationalDigits = (
|
|
140
|
+
allDigits.startsWith('1') ? allDigits.slice(1) : allDigits
|
|
141
|
+
).slice(0, 10);
|
|
142
|
+
|
|
143
|
+
// If no national digits left, check if user is deleting
|
|
144
|
+
if (nationalDigits.length === 0) {
|
|
145
|
+
// If value got shorter (user is deleting), clear everything
|
|
146
|
+
if (value.length < previousValue.length) {
|
|
147
|
+
if (debug) {
|
|
148
|
+
console.log('[PhoneNumberTransformer] -> case: deleted to empty');
|
|
149
|
+
}
|
|
150
|
+
return { value: '', selection: { start: 0, end: 0 } };
|
|
151
|
+
}
|
|
152
|
+
// Otherwise user just typed "1", show the prefix
|
|
153
|
+
return { value: '+1 ', selection: { start: 3, end: 3 } };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const formatted = formatUSPhoneNumber(nationalDigits);
|
|
157
|
+
|
|
158
|
+
// Calculate cursor position
|
|
159
|
+
const isCaret = selection.start === selection.end;
|
|
160
|
+
const cursorAtEnd = selection.end >= value.length;
|
|
161
|
+
const prevCursorAtEnd = previousSelection.end >= previousValue.length;
|
|
162
|
+
|
|
163
|
+
// Count national digits
|
|
164
|
+
const digitsBeforeCursor = countNationalDigitsBefore(
|
|
165
|
+
value,
|
|
166
|
+
selection.start,
|
|
167
|
+
);
|
|
168
|
+
const prevDigitsBeforeCursor = countNationalDigitsBefore(
|
|
169
|
+
previousValue,
|
|
170
|
+
previousSelection.start,
|
|
171
|
+
);
|
|
172
|
+
const totalDigits = countNationalDigitsBefore(value, value.length);
|
|
173
|
+
const prevTotalDigits = countNationalDigitsBefore(
|
|
174
|
+
previousValue,
|
|
175
|
+
previousValue.length,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const isDeleting = totalDigits < prevTotalDigits;
|
|
179
|
+
const isInserting = totalDigits > prevTotalDigits;
|
|
180
|
+
|
|
181
|
+
// Detect deletion of formatting character (e.g., deleting space, dash, or paren)
|
|
182
|
+
// User expects this to delete the digit before cursor
|
|
183
|
+
const deletedFormattingChar =
|
|
184
|
+
isCaret &&
|
|
185
|
+
value.length < previousValue.length &&
|
|
186
|
+
totalDigits === prevTotalDigits &&
|
|
187
|
+
totalDigits > 0;
|
|
188
|
+
|
|
189
|
+
// Log for debugging
|
|
190
|
+
if (debug) {
|
|
191
|
+
console.log('[PhoneNumberTransformer]', {
|
|
192
|
+
input: { value, selection },
|
|
193
|
+
prev: { previousValue, previousSelection },
|
|
194
|
+
digits: { nationalDigits, totalDigits, prevTotalDigits },
|
|
195
|
+
cursor: { digitsBeforeCursor, prevDigitsBeforeCursor },
|
|
196
|
+
flags: {
|
|
197
|
+
isCaret,
|
|
198
|
+
cursorAtEnd,
|
|
199
|
+
prevCursorAtEnd,
|
|
200
|
+
isDeleting,
|
|
201
|
+
isInserting,
|
|
202
|
+
deletedFormattingChar,
|
|
203
|
+
},
|
|
204
|
+
output: formatted,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Case 0: Deleted a formatting char - remove the digit before cursor
|
|
209
|
+
if (deletedFormattingChar) {
|
|
210
|
+
// Figure out which digit to remove: the one before the cursor
|
|
211
|
+
const digitToRemove = digitsBeforeCursor;
|
|
212
|
+
|
|
213
|
+
if (digitToRemove <= 0) {
|
|
214
|
+
// Cursor was before all digits, nothing to delete
|
|
215
|
+
if (debug) {
|
|
216
|
+
console.log(
|
|
217
|
+
'[PhoneNumberTransformer] -> case: deleted formatting char but cursor before all digits',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return { value: formatted, selection: { start: 3, end: 3 } };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Remove the digit at position (digitToRemove - 1) from nationalDigits
|
|
224
|
+
const trimmedDigits =
|
|
225
|
+
nationalDigits.slice(0, digitToRemove - 1) +
|
|
226
|
+
nationalDigits.slice(digitToRemove);
|
|
227
|
+
|
|
228
|
+
if (debug) {
|
|
229
|
+
console.log(
|
|
230
|
+
'[PhoneNumberTransformer] -> case: deleted formatting char',
|
|
231
|
+
{
|
|
232
|
+
digitToRemove,
|
|
233
|
+
trimmedDigits,
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (trimmedDigits.length === 0) {
|
|
239
|
+
return { value: '', selection: { start: 0, end: 0 } };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const trimmedFormatted = formatUSPhoneNumber(trimmedDigits);
|
|
243
|
+
|
|
244
|
+
// Position cursor where the deleted digit was
|
|
245
|
+
const newCursorDigits = digitToRemove - 1;
|
|
246
|
+
const newPos = positionAfterNationalDigit(
|
|
247
|
+
trimmedFormatted,
|
|
248
|
+
newCursorDigits,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
value: trimmedFormatted,
|
|
253
|
+
selection: { start: newPos, end: newPos },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Case 1: Typing at end (most common)
|
|
258
|
+
if (isCaret && cursorAtEnd && prevCursorAtEnd) {
|
|
259
|
+
if (debug) {
|
|
260
|
+
console.log('[PhoneNumberTransformer] -> case: typing at end');
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
value: formatted,
|
|
264
|
+
selection: { start: formatted.length, end: formatted.length },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Case 2: Deletion - use current cursor position (previous is stale if user moved cursor)
|
|
269
|
+
if (isDeleting && isCaret) {
|
|
270
|
+
const newPos = positionAfterNationalDigit(
|
|
271
|
+
formatted,
|
|
272
|
+
digitsBeforeCursor,
|
|
273
|
+
);
|
|
274
|
+
if (debug) {
|
|
275
|
+
console.log('[PhoneNumberTransformer] -> case: deletion', {
|
|
276
|
+
digitsBeforeCursor,
|
|
277
|
+
newPos,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
value: formatted,
|
|
282
|
+
selection: { start: newPos, end: newPos },
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Case 3: Insertion in middle - use current cursor position
|
|
287
|
+
if (isInserting && isCaret) {
|
|
288
|
+
const newPos = positionAfterNationalDigit(
|
|
289
|
+
formatted,
|
|
290
|
+
digitsBeforeCursor,
|
|
291
|
+
);
|
|
292
|
+
if (debug) {
|
|
293
|
+
console.log('[PhoneNumberTransformer] -> case: insertion', {
|
|
294
|
+
digitsBeforeCursor,
|
|
295
|
+
newPos,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
value: formatted,
|
|
300
|
+
selection: { start: newPos, end: newPos },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Case 4: Default / selection handling
|
|
305
|
+
const clampedDigits = Math.min(digitsBeforeCursor, nationalDigits.length);
|
|
306
|
+
const newStart = positionAfterNationalDigit(formatted, clampedDigits);
|
|
307
|
+
const endDigits = countNationalDigitsBefore(value, selection.end);
|
|
308
|
+
const clampedEndDigits = Math.min(endDigits, nationalDigits.length);
|
|
309
|
+
const newEnd = positionAfterNationalDigit(formatted, clampedEndDigits);
|
|
310
|
+
|
|
311
|
+
if (debug) {
|
|
312
|
+
console.log('[PhoneNumberTransformer] -> case: default', {
|
|
313
|
+
clampedDigits,
|
|
314
|
+
newStart,
|
|
315
|
+
clampedEndDigits,
|
|
316
|
+
newEnd,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
value: formatted,
|
|
321
|
+
selection: { start: newStart, end: newEnd },
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
super(worklet);
|
|
326
|
+
}
|
|
327
|
+
}
|
package/src/index.tsx
ADDED
package/src/registry.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runOnUI } from 'react-native-worklets';
|
|
2
|
+
import NativeTransformerTextInputModule from './NativeTransformerTextInputModule';
|
|
3
|
+
import { type Selection, type Transformer } from './Transformer';
|
|
4
|
+
import { computeUncontrolledSelection, validateSelection } from './selection';
|
|
5
|
+
|
|
6
|
+
type TransformerWrapper = (
|
|
7
|
+
input: string,
|
|
8
|
+
selectionStart: number,
|
|
9
|
+
selectionEnd: number,
|
|
10
|
+
) => { value: string; selection: Selection };
|
|
11
|
+
|
|
12
|
+
type ReactNativeTextInputTransformerRegistry = {
|
|
13
|
+
register(id: number, transformer: TransformerWrapper): void;
|
|
14
|
+
unregister(transformerId: number): void;
|
|
15
|
+
get(transformerId: number): TransformerWrapper | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
var __rntti_registerTransformerRegistry:
|
|
20
|
+
| ReactNativeTextInputTransformerRegistry
|
|
21
|
+
| undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let initialized = false;
|
|
25
|
+
|
|
26
|
+
function initializeIfNeeded() {
|
|
27
|
+
if (initialized) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Important that `runOnUI` is called first to make sure the UI runtime is initialized.
|
|
32
|
+
runOnUI(() => {
|
|
33
|
+
'worklet';
|
|
34
|
+
|
|
35
|
+
const transformersMap = new Map<number, TransformerWrapper>();
|
|
36
|
+
|
|
37
|
+
globalThis.__rntti_registerTransformerRegistry = {
|
|
38
|
+
register(id, transformer) {
|
|
39
|
+
transformersMap.set(id, transformer);
|
|
40
|
+
},
|
|
41
|
+
unregister(transformerId) {
|
|
42
|
+
transformersMap.delete(transformerId);
|
|
43
|
+
},
|
|
44
|
+
get(transformerId) {
|
|
45
|
+
return transformersMap.get(transformerId);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
|
|
50
|
+
NativeTransformerTextInputModule.install();
|
|
51
|
+
|
|
52
|
+
initialized = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Start counting ids at 1 to avoid using 0 as it is the default int value.
|
|
56
|
+
let currentId = 1;
|
|
57
|
+
|
|
58
|
+
export function registerTransformer(transformer: Transformer): number {
|
|
59
|
+
initializeIfNeeded();
|
|
60
|
+
|
|
61
|
+
const id = currentId++;
|
|
62
|
+
const worklet = transformer.worklet;
|
|
63
|
+
|
|
64
|
+
runOnUI(() => {
|
|
65
|
+
'worklet';
|
|
66
|
+
|
|
67
|
+
let previousValue: string | null = null;
|
|
68
|
+
let previousSelection: Selection | null = null;
|
|
69
|
+
|
|
70
|
+
const transformerWrapper: TransformerWrapper = (
|
|
71
|
+
value,
|
|
72
|
+
selectionStart,
|
|
73
|
+
selectionEnd,
|
|
74
|
+
) => {
|
|
75
|
+
const result = worklet({
|
|
76
|
+
value,
|
|
77
|
+
previousValue: previousValue ?? value,
|
|
78
|
+
selection: { start: selectionStart, end: selectionEnd },
|
|
79
|
+
previousSelection: previousSelection ?? {
|
|
80
|
+
start: selectionStart,
|
|
81
|
+
end: selectionEnd,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const newValue = result?.value ?? value;
|
|
85
|
+
let newSelection: Selection;
|
|
86
|
+
if (result?.selection != null) {
|
|
87
|
+
newSelection = result.selection;
|
|
88
|
+
validateSelection(newSelection, newValue.length);
|
|
89
|
+
} else {
|
|
90
|
+
newSelection = computeUncontrolledSelection(
|
|
91
|
+
value,
|
|
92
|
+
newValue,
|
|
93
|
+
selectionStart,
|
|
94
|
+
selectionEnd,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
previousValue = newValue;
|
|
98
|
+
previousSelection = newSelection;
|
|
99
|
+
return {
|
|
100
|
+
value: newValue,
|
|
101
|
+
selection: newSelection,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
globalThis.__rntti_registerTransformerRegistry?.register(
|
|
106
|
+
id,
|
|
107
|
+
transformerWrapper,
|
|
108
|
+
);
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
return id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function unregisterTransformer(transformerId: number) {
|
|
115
|
+
runOnUI(() => {
|
|
116
|
+
'worklet';
|
|
117
|
+
|
|
118
|
+
global.__rntti_registerTransformerRegistry?.unregister(transformerId);
|
|
119
|
+
})();
|
|
120
|
+
}
|
package/src/selection.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Selection } from './Transformer';
|
|
2
|
+
|
|
3
|
+
export const computeUncontrolledSelection = (
|
|
4
|
+
oldValue: string,
|
|
5
|
+
newValue: string,
|
|
6
|
+
selectionStart: number,
|
|
7
|
+
selectionEnd: number,
|
|
8
|
+
): Selection => {
|
|
9
|
+
'worklet';
|
|
10
|
+
const oldLength = oldValue.length;
|
|
11
|
+
const newLength = newValue.length;
|
|
12
|
+
const delta = newLength - oldLength;
|
|
13
|
+
let rawStart: number;
|
|
14
|
+
let rawEnd: number;
|
|
15
|
+
if (selectionStart === selectionEnd) {
|
|
16
|
+
if (selectionEnd >= oldLength) {
|
|
17
|
+
rawStart = newLength;
|
|
18
|
+
rawEnd = newLength;
|
|
19
|
+
} else {
|
|
20
|
+
const next = selectionEnd + delta;
|
|
21
|
+
rawStart = next;
|
|
22
|
+
rawEnd = next;
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
rawStart = selectionStart + delta;
|
|
26
|
+
rawEnd = selectionEnd + delta;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
rawStart < 0 ||
|
|
31
|
+
rawEnd < 0 ||
|
|
32
|
+
rawStart > newLength ||
|
|
33
|
+
rawEnd > newLength ||
|
|
34
|
+
rawStart > rawEnd
|
|
35
|
+
) {
|
|
36
|
+
return { start: newLength, end: newLength };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { start: rawStart, end: rawEnd };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const validateSelection = (
|
|
43
|
+
selection: Selection,
|
|
44
|
+
valueLength: number,
|
|
45
|
+
) => {
|
|
46
|
+
'worklet';
|
|
47
|
+
if (selection.start < 0 || selection.end < 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[rntti] Returned selection must be non-negative. Received start=${selection.start}, end=${selection.end}, valueLength=${valueLength}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (selection.end < selection.start) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`[rntti] Returned selection end must be >= selection start. Received start=${selection.start}, end=${selection.end}, valueLength=${valueLength}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (selection.start > valueLength || selection.end > valueLength) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`[rntti] Returned selection is out of bounds for the returned value. Received start=${selection.start}, end=${selection.end}, valueLength=${valueLength}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import useRefEffect from './useRefEffect';
|
|
9
|
+
import { useCallback } from 'react';
|
|
10
|
+
import type { MutableRefObject } from 'react';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Constructs a new ref that forwards new values to each of the given refs. The
|
|
14
|
+
* given refs will always be invoked in the order that they are supplied.
|
|
15
|
+
*
|
|
16
|
+
* WARNING: A known problem of merging refs using this approach is that if any
|
|
17
|
+
* of the given refs change, the returned callback ref will also be changed. If
|
|
18
|
+
* the returned callback ref is supplied as a `ref` to a React element, this may
|
|
19
|
+
* lead to problems with the given refs being invoked more times than desired.
|
|
20
|
+
*/
|
|
21
|
+
type RefWithCleanup<T> =
|
|
22
|
+
| ((instance: T | null) => void | (() => void))
|
|
23
|
+
| MutableRefObject<T | null>
|
|
24
|
+
| null
|
|
25
|
+
| undefined;
|
|
26
|
+
|
|
27
|
+
export default function useMergeRefs<Instance>(
|
|
28
|
+
...refs: ReadonlyArray<RefWithCleanup<Instance>>
|
|
29
|
+
): (instance: Instance | null) => void {
|
|
30
|
+
const refEffect = useCallback(
|
|
31
|
+
(current: Instance) => {
|
|
32
|
+
const cleanups: Array<void | (() => void)> = refs.map((ref) => {
|
|
33
|
+
if (ref == null) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (typeof ref === 'function') {
|
|
37
|
+
const cleanup = ref(current);
|
|
38
|
+
return typeof cleanup === 'function'
|
|
39
|
+
? cleanup
|
|
40
|
+
: () => {
|
|
41
|
+
ref(null);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
ref.current = current;
|
|
45
|
+
return () => {
|
|
46
|
+
ref.current = null;
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
for (const cleanup of cleanups) {
|
|
52
|
+
cleanup?.();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
[...refs], // eslint-disable-line react-hooks/exhaustive-deps
|
|
57
|
+
);
|
|
58
|
+
return useRefEffect(refEffect);
|
|
59
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useRef } from 'react';
|
|
9
|
+
|
|
10
|
+
type CallbackRef<T> = (instance: T | null) => void;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Constructs a callback ref that provides similar semantics as `useEffect`. The
|
|
14
|
+
* supplied `effect` callback will be called with non-null component instances.
|
|
15
|
+
* The `effect` callback can also optionally return a cleanup function.
|
|
16
|
+
*
|
|
17
|
+
* When a component is updated or unmounted, the cleanup function is called. The
|
|
18
|
+
* `effect` callback will then be called again, if applicable.
|
|
19
|
+
*
|
|
20
|
+
* When a new `effect` callback is supplied, the previously returned cleanup
|
|
21
|
+
* function will be called before the new `effect` callback is called with the
|
|
22
|
+
* same instance.
|
|
23
|
+
*
|
|
24
|
+
* WARNING: The `effect` callback should be stable (e.g. using `useCallback`).
|
|
25
|
+
*/
|
|
26
|
+
export default function useRefEffect<TInstance>(
|
|
27
|
+
effect: (instance: TInstance) => (() => void) | void,
|
|
28
|
+
): CallbackRef<TInstance> {
|
|
29
|
+
const cleanupRef = useRef<(() => void) | void>(undefined);
|
|
30
|
+
return useCallback(
|
|
31
|
+
(instance: null | TInstance) => {
|
|
32
|
+
if (cleanupRef.current) {
|
|
33
|
+
cleanupRef.current();
|
|
34
|
+
cleanupRef.current = undefined;
|
|
35
|
+
}
|
|
36
|
+
if (instance != null) {
|
|
37
|
+
cleanupRef.current = effect(instance);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
[effect],
|
|
41
|
+
);
|
|
42
|
+
}
|