@fawazahmed/react-native-read-more
Advanced tools
Comparing version 2.1.0 to 2.1.1
@@ -52,4 +52,2 @@ import React, {memo, useState, useEffect, useCallback} from 'react'; | ||
const [textWidth, setTextWidth] = useState(0); | ||
// lineOfImpact comes from hidden component four | ||
// const [lineOfImpact, setLineOfImpact] = useState({}); | ||
const [truncatedLineOfImpact, setTruncatedLineOfImpact] = useState(''); | ||
@@ -110,27 +108,26 @@ const [truncatedLineOfImpactWidth, setTruncatedLineOfImpactWidth] = useState( | ||
}, | ||
[setHiddenTextLinesWithSeeLess, setMountHiddenTextTwo], | ||
[ | ||
setHiddenTextLinesWithSeeLess, | ||
setMountHiddenTextTwo, | ||
setMountHiddenTextFour, | ||
], | ||
); | ||
const onLayoutActualTextComponent = useCallback( | ||
({ | ||
nativeEvent: { | ||
layout: {width}, | ||
}, | ||
}) => { | ||
setTextWidth(width); | ||
(event) => { | ||
const _event = event; // clone event | ||
const _width = _event?.nativeEvent?.layout?.width || 0; | ||
setTextWidth(_width); | ||
setMountHiddenTextFour(false); | ||
if (Platform.OS === 'android') { | ||
setMountHiddenTextSix(true); | ||
} | ||
}, | ||
[setTextWidth], | ||
[setTextWidth, setMountHiddenTextFour], | ||
); | ||
const onTextLayoutActualTextComponent = useCallback( | ||
({nativeEvent: {lines: _lines}}) => { | ||
if (!collapsed) { | ||
return; | ||
(event) => { | ||
const _event = event; // clone event | ||
if (collapsed) { | ||
const _lines = _event?.nativeEvent?.lines || []; | ||
setCollapsedLines(_lines); | ||
} | ||
setCollapsedLines(_lines); | ||
}, | ||
@@ -181,2 +178,29 @@ [setCollapsedLines, collapsed], | ||
const updateLineOfImpact = useCallback( | ||
(_text = '', resetCollapsedChildren = true) => { | ||
setHideEllipsis(!_text?.length); | ||
setTruncatedLineOfImpact(_text || ''); | ||
if (!_text?.length) { | ||
// reset width if no text | ||
// otherwise an effect will update the width | ||
setTruncatedLineOfImpactWidth(0); | ||
setReconciledLineOfImpactWidth(0); | ||
setSeeMoreRightPadding(0); | ||
setIsMeasured(true); | ||
} | ||
if (resetCollapsedChildren) { | ||
setCollapsedChildren(null); | ||
} | ||
}, | ||
[ | ||
setHideEllipsis, | ||
setTruncatedLineOfImpact, | ||
setTruncatedLineOfImpactWidth, | ||
setCollapsedChildren, | ||
setIsMeasured, | ||
], | ||
); | ||
const measureSeeMoreLine = useCallback(() => { | ||
@@ -187,3 +211,4 @@ if ( | ||
!collapsedLines.length || | ||
!seeMore | ||
!seeMore || | ||
!seeMoreWidth | ||
) { | ||
@@ -212,2 +237,3 @@ return; | ||
const availableWidth = textWidth - seeMoreWidth; | ||
const _trimmedText = _lineOfImpact?.text?.trimEnd?.(); | ||
@@ -218,100 +244,90 @@ // calculate how many characters to cut off if any | ||
// case 1 | ||
// if no text after right trim | ||
// hide ellipsis | ||
// move see more to beginning | ||
if (_lineOfImpact.text.trim().length === 0) { | ||
_lineOfImpact.width = 0; | ||
_lineOfImpact.text = ''; | ||
setHideEllipsis(true); | ||
} else { | ||
setHideEllipsis(false); | ||
if (!_trimmedText?.length) { | ||
return updateLineOfImpact(_trimmedText); | ||
} | ||
// todo | ||
// right trim and width adjustment | ||
// case 2 | ||
// text is there but no need to put \n | ||
// enough space for see more text on right side | ||
if (_lineOfImpact.width < availableWidth) { | ||
return updateLineOfImpact(_trimmedText); | ||
} | ||
// setLineOfImpact(_lineOfImpact); | ||
const seeMoreTextLength = | ||
`${ellipsis} ${seeMoreText}`.length + seeMoreOverlapCount; | ||
if (_lineOfImpact.width < availableWidth) { | ||
// if no need to cutoff, simply calculate see more right padding | ||
const _seeMoreRightPadding = | ||
textWidth - _lineOfImpact.width - seeMoreWidth; | ||
if (_seeMoreRightPadding > 0) { | ||
setTruncatedLineOfImpact(''); | ||
setTruncatedLineOfImpactWidth(0); | ||
setReconciledLineOfImpact(''); | ||
setReconciledLineOfImpactWidth(0); | ||
setSeeMoreRightPadding(_seeMoreRightPadding); | ||
// todo: remove this | ||
if (animate) { | ||
LayoutAnimation.configureNext(readmoreAnimation); | ||
} | ||
} | ||
} else { | ||
// todo | ||
// determine point, traverse through nodes | ||
// create collapsed children with spaces at the point | ||
const seeMoreTextLength = | ||
`${ellipsis} ${seeMoreText}`.length + seeMoreOverlapCount; | ||
const linesTillImpact = Array(_lineOfImpact.index + 1) | ||
.fill({}) | ||
.map((_e, index) => lines[index]); | ||
const charactersBeforeSeeMore = linesToCharacters(linesTillImpact); | ||
const charactersLengthTillSeeMore = charactersBeforeSeeMore.length; | ||
// text break position for collapsed text | ||
const textBreakPosition = charactersLengthTillSeeMore - seeMoreTextLength; | ||
// case 3 | ||
// many spaces at the end of text | ||
// so still no need to cutoff the text at end with \n | ||
const spaceDifference = _lineOfImpact?.text?.length - _trimmedText?.length; | ||
if (spaceDifference >= seeMoreTextLength) { | ||
return updateLineOfImpact(_trimmedText); | ||
} | ||
const _truncatedText = _lineOfImpact.text.substr( | ||
0, | ||
_lineOfImpact.text.length - seeMoreTextLength, | ||
); | ||
// case 4 | ||
// create collapsed children with \n at the point | ||
const linesTillImpact = Array(_lineOfImpact.index + 1) | ||
.fill({}) | ||
.map((_e, index) => lines[index]); | ||
const charactersBeforeSeeMore = linesToCharacters(linesTillImpact); | ||
const charactersLengthTillSeeMore = | ||
charactersBeforeSeeMore?.trimEnd?.()?.length || 0; | ||
// text break position for collapsed text | ||
const textBreakPosition = charactersLengthTillSeeMore - seeMoreTextLength; | ||
if (truncatedLineOfImpact !== _truncatedText) { | ||
setTruncatedLineOfImpact(_truncatedText); | ||
} | ||
const _truncatedText = | ||
_trimmedText | ||
?.substr(0, _trimmedText.length - seeMoreTextLength) | ||
?.trimEnd?.() || ''; | ||
// go to this position and insert spaces | ||
let charactersToTraverse = textBreakPosition; | ||
let nodeFound = false; | ||
const modifiedChildrenObjects = getText(children, TextComponent, true) | ||
?.map((_childObject) => { | ||
if (nodeFound) { | ||
return _childObject; | ||
} | ||
if (_childObject.content.length > charactersToTraverse) { | ||
// this node is the one | ||
nodeFound = true; | ||
const childContent = insertAt( | ||
_childObject.content, | ||
'\n', | ||
charactersToTraverse, | ||
); | ||
return { | ||
type: _childObject?.type, | ||
content: childContent, | ||
child: | ||
_childObject?.type === 'string' | ||
? childContent | ||
: React.cloneElement( | ||
_childObject, | ||
_childObject.props, | ||
childContent, | ||
), | ||
}; | ||
} | ||
charactersToTraverse = | ||
charactersToTraverse - _childObject.content.length; | ||
// go to this position and insert \n | ||
let charactersToTraverse = textBreakPosition; | ||
let nodeFound = false; | ||
const modifiedChildrenObjects = getText(children, TextComponent, true) | ||
?.map((_childObject) => { | ||
if (nodeFound) { | ||
return _childObject; | ||
}) | ||
?.map((_updatedObjects) => { | ||
return _updatedObjects.child; | ||
}); | ||
} | ||
if (_childObject.content.length > charactersToTraverse) { | ||
// this node is the one | ||
nodeFound = true; | ||
const childContent = insertAt( | ||
_childObject.content, | ||
'\n', | ||
charactersToTraverse, | ||
); | ||
return { | ||
type: _childObject?.type, | ||
content: childContent, | ||
child: | ||
_childObject?.type === 'string' | ||
? childContent | ||
: React.cloneElement( | ||
_childObject, | ||
_childObject.props, | ||
childContent, | ||
), | ||
}; | ||
} | ||
charactersToTraverse = | ||
charactersToTraverse - _childObject.content.length; | ||
if (nodeFound) { | ||
setCollapsedChildren(modifiedChildrenObjects); | ||
} | ||
return _childObject; | ||
}) | ||
?.map((_updatedObjects) => { | ||
return _updatedObjects.child; | ||
}); | ||
if (nodeFound) { | ||
setCollapsedChildren(modifiedChildrenObjects); | ||
return updateLineOfImpact(_truncatedText, false); | ||
} | ||
setIsMeasured(true); | ||
// todo: inform user | ||
// error case | ||
return updateLineOfImpact(_trimmedText); | ||
}, [ | ||
@@ -324,9 +340,8 @@ numberOfLines, | ||
seeMoreWidth, | ||
animate, | ||
ellipsis, | ||
seeMoreText, | ||
seeMoreOverlapCount, | ||
truncatedLineOfImpact, | ||
children, | ||
TextComponent, | ||
updateLineOfImpact, | ||
]); | ||
@@ -359,8 +374,8 @@ | ||
const seeMoreTextHidingStyle = !isMeasured | ||
? {color: 'transparent'} | ||
: { | ||
backgroundColor: 'transparent', | ||
}; | ||
? styles.transparentColor | ||
: styles.transparentBackground; | ||
const seeMoreContainerStyle = [ | ||
styles.seeMoreContainer, | ||
hideEllipsis | ||
? styles.seeMoreContainerEllpisisHidden | ||
: styles.seeMoreContainer, | ||
{ | ||
@@ -381,3 +396,2 @@ marginRight: seeMoreRightPadding, | ||
const callback = collapsed ? onCollapse : onExpand; | ||
setAfterCollapsed(collapsed); | ||
if (animate) { | ||
@@ -388,2 +402,3 @@ LayoutAnimation.configureNext(readmoreAnimation, callback); | ||
} | ||
setAfterCollapsed(collapsed); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
@@ -394,2 +409,5 @@ }, [collapsed]); | ||
const handle = setTimeout(() => { | ||
// to commence measurement chain | ||
// we should mount component 1 | ||
// also reset isMeasured | ||
setMountHiddenTextOne(true); | ||
@@ -413,6 +431,6 @@ }, debounceSeeMoreCalc); | ||
// a map of additional props to be passed down | ||
// in hidden text components other than style | ||
// for accurate measurements | ||
useEffect(() => { | ||
// a map of additional props to be passed down | ||
// in hidden text components other than style | ||
// for accurate measurements | ||
const _additionalProps = {}; | ||
@@ -429,17 +447,13 @@ | ||
useEffect(() => { | ||
if (mountHiddenTextOne || mountHiddenTextTwo || mountHiddenTextFour) { | ||
if (mountHiddenTextTwo && !seeMoreWidth && collapsedLines?.length) { | ||
return; | ||
} | ||
// only start measurement after component 2 is unmounted and see more width is calculated | ||
// since component 1 mounts -> unmounts -> mounts component 2 | ||
// then component 2 unmounts itself | ||
// and then all measurement params are available | ||
const handle = setTimeout(measureSeeMoreLine, debounceSeeMoreCalc); | ||
return () => clearTimeout(handle); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [ | ||
lines, | ||
numberOfLines, | ||
collapsedLines, | ||
seeMore, | ||
mountHiddenTextOne, | ||
mountHiddenTextTwo, | ||
mountHiddenTextFour, | ||
]); | ||
}, [mountHiddenTextTwo, seeMoreWidth, collapsedLines]); | ||
@@ -452,29 +466,41 @@ useEffect(() => { | ||
setMountHiddenTextThree(true); | ||
if (Platform.OS === 'android') { | ||
setMountHiddenTextSix(true); | ||
} | ||
}, [truncatedLineOfImpact]); | ||
useEffect(() => { | ||
if (!truncatedLineOfImpactWidth) { | ||
if ( | ||
!(truncatedLineOfImpactWidth || reconciledLineOfImpactWidth) || | ||
!seeMoreWidth || | ||
!textWidth | ||
) { | ||
return; | ||
} | ||
const padding = textWidth - truncatedLineOfImpactWidth - seeMoreWidth; | ||
setSeeMoreRightPadding(padding); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [truncatedLineOfImpactWidth]); | ||
const _width = | ||
reconciledLineOfImpactWidth || truncatedLineOfImpactWidth || 0; | ||
useEffect(() => { | ||
if (!reconciledLineOfImpactWidth || !seeMoreWidth || !textWidth) { | ||
// setSeeMoreRightPadding(0); | ||
return; | ||
let _seeMoreRightPadding = textWidth - _width - seeMoreWidth; | ||
_seeMoreRightPadding = _seeMoreRightPadding < 0 ? 0 : _seeMoreRightPadding; | ||
setSeeMoreRightPadding(_seeMoreRightPadding); | ||
/* | ||
// shut down animation while measuring | ||
// so that it doesn't effects other UI animations | ||
if (animate && isMeasured) { | ||
LayoutAnimation.configureNext(readmoreAnimation); | ||
} | ||
const _seeMoreRightPadding = | ||
textWidth - reconciledLineOfImpactWidth - seeMoreWidth; | ||
if (_seeMoreRightPadding > 0) { | ||
setSeeMoreRightPadding(_seeMoreRightPadding); | ||
if (animate) { | ||
LayoutAnimation.configureNext(readmoreAnimation); | ||
} | ||
} | ||
*/ | ||
setIsMeasured(true); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [reconciledLineOfImpactWidth, seeMoreWidth, textWidth]); | ||
}, [ | ||
truncatedLineOfImpactWidth, | ||
reconciledLineOfImpactWidth, | ||
seeMoreWidth, | ||
textWidth, | ||
]); | ||
@@ -519,3 +545,3 @@ return ( | ||
)} | ||
{/* extract line of impact with measured children */} | ||
{/* extract line of impact with collapsed children for remeasurement of right padding on android */} | ||
{mountHiddenTextSix && ( | ||
@@ -562,17 +588,22 @@ <TextComponent | ||
<View style={seeMoreContainerStyle} onLayout={onSeeMoreViewLayout}> | ||
{!hideEllipsis && ( | ||
<TextComponent | ||
key={`${isMeasured}-${hideEllipsis}`} | ||
{...additionalProps} | ||
{...restProps} | ||
onPress={toggle} | ||
style={[ | ||
style, | ||
seeMoreTextHidingStyle, | ||
hideEllipsis ? styles.transparentColor : {}, | ||
]}> | ||
{`${ellipsis} `} | ||
</TextComponent> | ||
)} | ||
<TextComponent | ||
key={`${isMeasured}`} | ||
{...additionalProps} | ||
{...restProps} | ||
onPress={toggle} | ||
style={[style, seeMoreTextHidingStyle]}> | ||
{`${hideEllipsis ? '' : ellipsis}`} | ||
</TextComponent> | ||
<TextComponent | ||
{...additionalProps} | ||
{...restProps} | ||
onPress={toggle} | ||
style={[style, seeMoreStyle, seeMoreTextHidingStyle]}> | ||
{` ${seeMoreText}`} | ||
{hideEllipsis ? ' ' : ''} | ||
{seeMoreText} | ||
</TextComponent> | ||
@@ -611,2 +642,8 @@ </View> | ||
}, | ||
seeMoreContainerEllpisisHidden: { | ||
position: 'absolute', | ||
left: 0, | ||
bottom: 0, | ||
flexDirection: 'row', | ||
}, | ||
seeMoreButton: { | ||
@@ -624,2 +661,12 @@ flexDirection: 'row', | ||
}, | ||
transparentBackground: { | ||
backgroundColor: 'transparent', | ||
}, | ||
transparentColor: { | ||
color: 'transparent', | ||
}, | ||
hiddenEllpisisText: { | ||
color: 'transparent', | ||
position: 'absolute', | ||
}, | ||
}); | ||
@@ -649,2 +696,4 @@ | ||
debounceSeeMoreCalc: PropTypes.number, | ||
onLayout: PropTypes.func, | ||
onTextLayout: PropTypes.func, | ||
}; | ||
@@ -667,3 +716,3 @@ | ||
expandOnly: false, | ||
seeMoreOverlapCount: 1, | ||
seeMoreOverlapCount: 2, | ||
debounceSeeMoreCalc: 300, | ||
@@ -670,0 +719,0 @@ allowFontScaling: Platform.select({ |
{ | ||
"name": "@fawazahmed/react-native-read-more", | ||
"version": "2.1.0", | ||
"version": "2.1.1", | ||
"description": "A simple react native library to show large blocks of text in a condensed manner with the ability to collapse and expand.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -73,3 +73,3 @@ ![NPM Downloads](https://img.shields.io/npm/dw/@fawazahmed/react-native-read-more) ![NPM License](https://img.shields.io/npm/l/@fawazahmed/react-native-read-more) ![NPM Version](https://img.shields.io/npm/v/@fawazahmed/react-native-read-more) | ||
| `animate` | `bool` | no | defaults to `true` => applies a subtle animation to see more and see less text, not the complete text itself | ||
| `backgroundColor` | `string` | no | (deprecated) defaults to `white` => supply `backgroundColor` if your background color is something other than white | ||
| `backgroundColor` | `string` | no | (removed in v2.1.0 and above) defaults to `white` => supply `backgroundColor` if your background color is something other than white | ||
| `customTextComponent` | `React component` | no | defaults to `Text` | ||
@@ -79,3 +79,3 @@ | `expandOnly` | `bool` | no | defaults to `false` => hide see less option similar to a linkedIn post | ||
| `onCollapse` | `func` | no | optional callback executed when collapsed | ||
| `preserveLinebreaks` | `bool` | no | (deprecated) defaults to `false` => preserves `\n` in the content while in the collapsed state. This prop is in experimental stage. | ||
| `preserveLinebreaks` | `bool` | no | (removed in v2.1.0 and above) defaults to `false` => preserves `\n` in the content while in the collapsed state. This prop is in experimental stage. | ||
@@ -117,1 +117,5 @@ Any additional props are passed down to underlying `Text` component. | ||
refer to jest docs [here](https://jestjs.io/docs/en/tutorial-react-native#transformignorepatterns-customization) and github [issue](https://github.com/fawaz-ahmed/react-native-read-more/issues/19) | ||
### Known issues | ||
`Android only` if `numberOfLines` with a value of `1` is passed down as a prop, text in android devices will overlap at the end of line. This is an issue in `react-native` where text from other lines concatenates into the first one even if we add `\n` to the first line, where the lines returned from `onTextLayout` indicates a different response. | ||
To overcome this issue, use `numberOfLines` greater than `1`. |
509022
833
119