UNPKG

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
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;