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