react-native-input-suggestion
Version:
A customizable React Native TextInput with inline suggestion, auto-complete, and swipe-to-fill or tap-to-fill functionality.
178 lines (177 loc) • 7.38 kB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
import { View, TextInput, Text, StyleSheet, Dimensions, PanResponder, Pressable, } from 'react-native';
const { width } = Dimensions.get('window');
const fontStyleKeys = [
'fontSize',
'fontWeight',
'fontStyle',
'fontFamily',
'letterSpacing',
'textAlign',
'textTransform',
];
const inputStyleKeys = [
'fontSize',
'fontWeight',
'fontStyle',
'fontFamily',
'letterSpacing',
'textAlign',
'textTransform',
'height',
];
const SuggestionInput = ({ value, onChangeText, suggestion, inputTextColor = 'black', suggestionTextColor = 'gray', placeholder = 'Type here...', textStyle, fillType = 'textPress', caseSensitive = false, containerStyle = {}, showFillButton = false, ...props }) => {
// const [value, onChangeText] = useState('');
const inputTextRef = useRef(''); // Holds latest value
const [isFocused, setIsFocused] = useState(false);
const [showSuggestion, setShowSuggestion] = useState(false);
const [fullText, setFullText] = useState(suggestion);
// const fullText = 'An apple is there';
const prevTextRef = useRef('');
const flattenedStyle = StyleSheet.flatten(textStyle) || {};
const restTextStyles = Object.fromEntries(Object.entries(flattenedStyle).filter(([key]) => fontStyleKeys.includes(key)));
const restInputStyles = Object.fromEntries(Object.entries(flattenedStyle).filter(([key]) => inputStyleKeys.includes(key)));
useEffect(() => {
setFullText(suggestion);
}, [suggestion]);
const startsWith = (source, target) => {
if (!caseSensitive) {
return source.toLowerCase().startsWith(target.toLowerCase());
}
return source.startsWith(target);
};
const handleTextChange = (text) => {
const prevText = prevTextRef.current;
const isBackspace = text.length < prevText.length;
onChangeText(text);
inputTextRef.current = text;
prevTextRef.current = text;
if (text.length > 0 && startsWith(fullText, text) && !isBackspace) {
setShowSuggestion(true);
}
else {
setShowSuggestion(false);
}
};
const remainingText = fullText.slice(value.length);
const panResponder = useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (_, gestureState) => {
const dx = gestureState.dx; // Horizontal movement
if (dx > 0) {
const charsToReveal = Math.min(Math.floor(dx / 25), // Estimate: 10px per character
fullText.length - inputTextRef.current.length);
const newText = fullText.slice(0, inputTextRef.current.length + charsToReveal);
onChangeText(newText);
inputTextRef.current = newText; // update ref
}
},
onPanResponderRelease: () => {
// Still show remaining text if not fully filled
const current = inputTextRef.current;
if (current.length > 0 &&
current.length < fullText.length &&
fullText.startsWith(current)) {
setShowSuggestion(true);
}
else {
setShowSuggestion(false);
}
},
})).current;
return (React.createElement(View, { style: [styles.container, containerStyle] },
React.createElement(View, { style: styles.inputRow },
React.createElement(View, { style: styles.inputWrapper },
React.createElement(TextInput, { style: [styles.input, restInputStyles, { color: inputTextColor }], value: value, onChangeText: handleTextChange, placeholder: placeholder, multiline: false, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), autoFocus: true, ...props }),
isFocused && showSuggestion && remainingText.length > 0 && (React.createElement(React.Fragment, null,
fillType == 'textPress' && (React.createElement(Pressable, { style: styles.suggestionOverlay, ...panResponder.panHandlers, onPress: () => {
onChangeText(fullText);
setShowSuggestion(false);
} },
React.createElement(Text, { style: [
styles.suggestionText,
restTextStyles,
{ color: suggestionTextColor },
] },
React.createElement(Text, { style: [styles.transparentText, restTextStyles] }, fullText.slice(0, value.length)),
remainingText),
showFillButton && (React.createElement(Pressable, { onPress: () => {
onChangeText(fullText);
setShowSuggestion(false);
}, style: styles.fillButton },
React.createElement(Text, { style: styles.fillButtonText }, "Fill"))))),
fillType == 'textDrag' && (React.createElement(View, { style: styles.suggestionOverlay, ...panResponder.panHandlers },
React.createElement(Text, { style: [
styles.suggestionText,
restTextStyles,
{ color: suggestionTextColor },
] },
React.createElement(Text, { style: [styles.transparentText, restTextStyles] }, fullText.slice(0, value.length)),
remainingText),
showFillButton && (React.createElement(Pressable, { onPress: () => {
onChangeText(fullText);
setShowSuggestion(false);
}, style: styles.fillButton },
React.createElement(Text, { style: styles.fillButtonText }, "Fill")))))))))));
};
const styles = StyleSheet.create({
container: {
padding: 5,
paddingHorizontal: 10,
borderColor: '#ccc',
borderRadius: 10,
borderWidth: 1,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
position: 'relative',
flex: 1,
},
input: {
fontSize: 15,
color: 'black',
letterSpacing: 0.06,
padding: 0,
width: width,
minHeight: 40,
},
suggestionOverlay: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
},
suggestionText: {
fontSize: 15,
color: 'gray',
letterSpacing: 0.06,
},
transparentText: {
fontSize: 15,
color: 'transparent',
letterSpacing: 0.06,
},
fillButton: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 5,
height: 20,
paddingHorizontal: 6,
alignContent: 'center',
justifyContent: 'center',
marginTop: 4,
marginLeft: 5,
},
fillButtonText: {
fontSize: 12,
color: '#ccc',
},
});
export default SuggestionInput;