UNPKG

@boindahood/text-truncate-show-more

Version:

A simple React Native component to truncate long text and toggle between "show more" / "show less".

157 lines (153 loc) 6.98 kB
import React, { useCallback, useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; export const TextShowMore = React.memo((props) => { const { children, textStyle, numberOfLines, textShowLessStyle, textShowMoreStyle, textShowLess = 'Show less', textShowMore = 'Show more' } = props; const [expanded, setExpanded] = useState(false); // current satate is expanded or not const [dontNeedToShowMore, setDontNeedToShowMore] = useState(true); // check dont need btn 'show more' const [textNotExpanded, setTextNotExpanded] = useState(); // text show when expanded = false const [textExpanded, setTextExpanded] = useState(); // text show when expanded = true const toggleShowMore = useCallback(() => { setExpanded((prev) => !prev); }, []); const changeTextNotExpanded = useCallback((text) => { setTextNotExpanded(text); }, []); const changeTextExpanded = useCallback((text) => { setTextExpanded(text); }, []); const changeDontNeedToShowMore = useCallback((dontNeed) => { setDontNeedToShowMore(dontNeed); }, []); const renderButtonMoreLess = useCallback(() => { if (dontNeedToShowMore) return; if (expanded) return <Text style={[styles.defaultStyleTextMore, textShowLessStyle]} suppressHighlighting={true} onPress={toggleShowMore}> {textShowLess} </Text>; return <Text style={[styles.defaultStyleTextMore, textShowMoreStyle]} suppressHighlighting={true} onPress={toggleShowMore}> {textShowMore} </Text>; }, [expanded, toggleShowMore, dontNeedToShowMore, textShowMore, textShowLess, textShowMoreStyle, textShowLessStyle]); return <> <Text style={[styles.defaultStyle, textStyle]}> {expanded ? textExpanded : textNotExpanded} {renderButtonMoreLess()} </Text> {/* clone raw text, full line */} <TextCloneCaculate changeTextNotExpanded={changeTextNotExpanded} changeTextExpanded={changeTextExpanded} changeDontNeedToShowMore={changeDontNeedToShowMore} textStyle={textStyle} numberOfLines={numberOfLines} textShowMore={textShowMore} textShowMoreStyle={textShowMoreStyle} textShowLess={textShowLess} textShowLessStyle={textShowLessStyle}>{children}</TextCloneCaculate> </>; }); const TextCloneCaculate = React.memo((props) => { const { numberOfLines, textShowMoreStyle, textShowLessStyle, textShowMore, textShowLess, children, textStyle, changeTextNotExpanded, changeTextExpanded, changeDontNeedToShowMore } = props; const [textLineNormal, setTextLineNormal] = useState(); const [textLastLine, setTextLastLine] = useState(); const prevTextLastLine = React.useRef(''); const rawTextLastLine = React.useRef(''); const onTextLayoutClone = useCallback((e) => { const { lines } = e.nativeEvent; // dont need to show more if (!numberOfLines || (lines === null || lines === void 0 ? void 0 : lines.length) <= numberOfLines) { changeDontNeedToShowMore(true); changeTextNotExpanded(children); return; } // need to show more changeDontNeedToShowMore(false); let textTruncateWillShow = ''; lines.forEach((line, index) => { if (index < numberOfLines - 1) { textTruncateWillShow += line.text; } if (index === numberOfLines - 1) { setTextLastLine(line.text); rawTextLastLine.current = line.text; } }); setTextLineNormal(textTruncateWillShow); }, [numberOfLines, children, changeDontNeedToShowMore, changeTextNotExpanded]); const onTextLayoutNotExpanded = useCallback((e) => { var _a; const { lines } = e.nativeEvent; if (!lines) return; const textLastLine = (_a = lines === null || lines === void 0 ? void 0 : lines[0]) === null || _a === void 0 ? void 0 : _a.text; // caculate last line done if ((lines === null || lines === void 0 ? void 0 : lines.length) === 1) { changeTextNotExpanded(textLineNormal + prevTextLastLine.current.trim() + '...'); return; } // caulate const shorterListString = textLastLine === null || textLastLine === void 0 ? void 0 : textLastLine.split(' ').slice(0, -1); const shorterString = shorterListString.join(' '); setTextLastLine(shorterString); prevTextLastLine.current = textLastLine; }, [textLineNormal, changeTextNotExpanded]); const onTextLayoutExpanded = useCallback((e) => { var _a; const { lines } = e.nativeEvent; // case text show less in 2 lines if (numberOfLines && (lines === null || lines === void 0 ? void 0 : lines.length) > numberOfLines && textShowLess) { const textLastLine = (_a = lines[(lines === null || lines === void 0 ? void 0 : lines.length) - 1]) === null || _a === void 0 ? void 0 : _a.text; const textShowLessIn2Lines = textLastLine.length < textShowLess.length && textShowLess.includes(textLastLine); if (textShowLessIn2Lines) { changeTextExpanded(children + '\n'); return; } } // case happy changeTextExpanded(children); }, [numberOfLines, changeTextExpanded, children]); useEffect(() => { setTextLastLine(rawTextLastLine.current); }, [textShowMore, textShowLess]); return (<View style={styles.viewClone} pointerEvents='none'> {/* raw to get all lines */} <Text numberOfLines={numberOfLines ? numberOfLines + 1 : undefined} onTextLayout={onTextLayoutClone} style={[styles.defaultStyle, textStyle]}> {children} </Text> {/* process state = has show more */} <Text style={[styles.defaultStyle, textStyle]} onTextLayout={onTextLayoutNotExpanded}> {textLastLine}{'...'} <Text style={[styles.defaultStyleTextMore, textShowMoreStyle]}> {textShowMore} </Text> </Text> <View style={styles.viewStroke}/> {/* process state = has show less */} <Text style={[styles.defaultStyle, textStyle]} onTextLayout={onTextLayoutExpanded}> {children} <Text style={[styles.defaultStyleTextMore, textShowLessStyle]}> {textShowLess} </Text> </Text> </View>); }); // styles const styles = StyleSheet.create({ defaultStyle: { fontSize: 14, color: 'black', fontFamily: 'monospace', }, defaultStyleTextMore: { fontSize: 14, color: 'black', fontWeight: 'bold', fontFamily: 'monospace', }, viewClone: { position: 'absolute', top: 0, left: 0, opacity: 0, }, viewStroke: { borderBottomWidth: 1, borderBottomColor: 'gray', width: '100%', height: 1, marginBottom: 10, }, }); export default TextShowMore;