@fawazahmed/react-native-read-more
Advanced tools
Comparing version 1.1.5 to 2.0.0
@@ -11,2 +11,9 @@ import React, {memo, useState, useEffect, useCallback} from 'react'; | ||
} from 'react-native'; | ||
import { | ||
childrenToTextChildren, | ||
getText, | ||
insertAt, | ||
linesToCharacters, | ||
childrenObjectsToChildren, | ||
} from './helper'; | ||
@@ -21,3 +28,3 @@ if (Platform.OS === 'android') { | ||
300, | ||
LayoutAnimation.Types.easeInEaseOut, | ||
LayoutAnimation.Types.easeOut, | ||
LayoutAnimation.Properties.opacity, | ||
@@ -43,6 +50,10 @@ ); | ||
expandOnly, | ||
seeMoreOverlapCount, | ||
debounceSeeMoreCalc, | ||
...restProps | ||
}) => { | ||
const [textHeight, setTextHeight] = useState(0); | ||
const [hiddenTextHeight, setHiddenTextHeight] = useState(0); | ||
const [additionalProps, setAdditionalProps] = useState({}); | ||
// hiddenTextHeightOne comes from hidden component one | ||
const [hiddenTextHeightOne, setHiddenTextHeightOne] = useState(0); | ||
// hiddenTextHeightWithSeeLess comes from hidden component two | ||
const [ | ||
@@ -52,20 +63,85 @@ hiddenTextHeightWithSeeLess, | ||
] = useState(0); | ||
// textHeight and textWidth comes from hidden component three | ||
const [textHeight, setTextHeight] = useState(0); | ||
const [textWidth, setTextWidth] = useState(0); | ||
// lineOfImpact comes from hidden component four | ||
const [lineOfImpact, setLineOfImpact] = useState({}); | ||
const [lines, setLines] = useState([]); | ||
const [truncatedLineOfImpact, setTruncatedLineOfImpact] = useState(''); | ||
const [truncatedLineOfImpactWidth, setTruncatedLineOfImpactWidth] = useState( | ||
0, | ||
); | ||
const [seeMoreRightPadding, setSeeMoreRightPadding] = useState(0); | ||
// mount or unmount hidden components | ||
const [mountHiddenTextOne, setMountHiddenTextOne] = useState(true); | ||
const [mountHiddenTextTwo, setMountHiddenTextTwo] = useState(true); | ||
const [mountHiddenTextThree, setMountHiddenTextThree] = useState(true); | ||
const [mountHiddenTextFour, setMountHiddenTextFour] = useState(true); | ||
const [mountHiddenTextFive, setMountHiddenTextFive] = useState(false); | ||
// initial measurement is in progress | ||
const [isMeasuring, setIsMeasuring] = useState(true); | ||
// logic decisioning params | ||
const [seeMore, setSeeMore] = useState(false); | ||
const [collapsed, setCollapsed] = useState(true); | ||
const [afterCollapsed, setAfterCollapsed] = useState(true); | ||
// copy of children with only text | ||
const [collapsedChildren, setCollapsedChildren] = useState( | ||
childrenToTextChildren(children, TextComponent), | ||
); | ||
const [measuredCollapsedChildren, setMeasuredCollapsedChildren] = useState( | ||
null, | ||
); | ||
// width of see more component | ||
const [seeMoreWidth, setSeeMoreWidth] = useState(0); | ||
const onTextLayout = useCallback( | ||
const onSeeMoreViewLayout = useCallback( | ||
({ | ||
nativeEvent: { | ||
layout: {height}, | ||
layout: {width}, | ||
}, | ||
}) => { | ||
setSeeMoreWidth(width); | ||
}, | ||
[setSeeMoreWidth], | ||
); | ||
const onLayoutHiddenTextFive = useCallback( | ||
({ | ||
nativeEvent: { | ||
layout: {width}, | ||
}, | ||
}) => { | ||
setMountHiddenTextFive(false); | ||
setTruncatedLineOfImpactWidth(width); | ||
}, | ||
[setTruncatedLineOfImpactWidth, setMountHiddenTextFive], | ||
); | ||
const onTextLayoutHiddenTextFour = useCallback( | ||
({nativeEvent: {lines: _lines}}) => { | ||
const _lineOfImpact = _lines[numberOfLines - 1]; | ||
setLineOfImpact(_lineOfImpact); | ||
setLines(_lines); | ||
}, | ||
[numberOfLines, setLineOfImpact, setLines], | ||
); | ||
const onLayoutHiddenTextFour = useCallback(() => { | ||
setMountHiddenTextFour(false); | ||
}, [setMountHiddenTextFour]); | ||
const onLayoutHiddenTextThree = useCallback( | ||
({ | ||
nativeEvent: { | ||
layout: {height, width}, | ||
}, | ||
}) => { | ||
setTextHeight(height); | ||
setTextWidth(width); | ||
setMountHiddenTextThree(false); | ||
}, | ||
[setTextHeight], | ||
[setTextHeight, setTextWidth, setMountHiddenTextThree], | ||
); | ||
const onHiddenTextLayout = useCallback( | ||
const onHiddenTextLayoutOne = useCallback( | ||
({ | ||
@@ -76,6 +152,6 @@ nativeEvent: { | ||
}) => { | ||
setHiddenTextHeight(height); | ||
setHiddenTextHeightOne(height); | ||
setMountHiddenTextOne(false); | ||
}, | ||
[setHiddenTextHeight, setMountHiddenTextOne], | ||
[setHiddenTextHeightOne, setMountHiddenTextOne], | ||
); | ||
@@ -99,9 +175,131 @@ | ||
const measureSeeMoreLine = useCallback(() => { | ||
if ( | ||
!seeMore || | ||
!textWidth || | ||
!numberOfLines || | ||
!seeMoreWidth || | ||
!lineOfImpact?.text | ||
) { | ||
setTruncatedLineOfImpact(''); | ||
setTruncatedLineOfImpactWidth(0); | ||
return setMeasuredCollapsedChildren(null); | ||
} | ||
// if line of impact | ||
// use number fo lines - 1 lines with wrap text and clip ellipsis | ||
// another text component with line of impact | ||
// width will be total width - see more width | ||
// show this ^^ on collapsed state | ||
const linesTillImpact = Array(numberOfLines) | ||
.fill({}) | ||
.map((_e, index) => lines[index]); | ||
const charactersBeforeSeeMore = linesToCharacters(linesTillImpact); | ||
const charactersLengthTillSeeMore = charactersBeforeSeeMore.trim().length; | ||
const seeMoreTextLength = | ||
`${ellipsis} ${seeMoreText}`.length + seeMoreOverlapCount; | ||
const textBreakPosition = charactersLengthTillSeeMore - seeMoreTextLength; | ||
const trimmedLineOfImpact = lineOfImpact.text.trim(); | ||
const _truncatedLineOfImpact = trimmedLineOfImpact.substring( | ||
0, | ||
trimmedLineOfImpact.length - seeMoreTextLength, | ||
); | ||
setTruncatedLineOfImpact(_truncatedLineOfImpact); | ||
// go to this position and insert a line break | ||
let charactersToTraverse = textBreakPosition; | ||
let nodeFound = false; | ||
const modifiedChildrenObjects = getText(children, TextComponent).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; | ||
return _childObject; | ||
}, | ||
); | ||
if (nodeFound) { | ||
return setMeasuredCollapsedChildren( | ||
childrenObjectsToChildren(modifiedChildrenObjects), | ||
); | ||
} | ||
return setMeasuredCollapsedChildren(null); | ||
}, [ | ||
children, | ||
TextComponent, | ||
textWidth, | ||
numberOfLines, | ||
seeMore, | ||
seeMoreWidth, | ||
lineOfImpact, | ||
lines, | ||
ellipsis, | ||
seeMoreText, | ||
seeMoreOverlapCount, | ||
]); | ||
const textProps = afterCollapsed | ||
? { | ||
numberOfLines, | ||
ellipsizeMode: 'clip', | ||
} | ||
: {}; | ||
const commonHiddenComponentProps = { | ||
...additionalProps, | ||
style: StyleSheet.flatten([ | ||
Array.isArray(style) ? StyleSheet.flatten(style) : style, | ||
styles.hiddenTextAbsolute, | ||
]), | ||
}; | ||
const hiddenComponentPropsLineOfImpact = { | ||
...additionalProps, | ||
style: StyleSheet.flatten([ | ||
Array.isArray(style) ? StyleSheet.flatten(style) : style, | ||
styles.hiddenTextAbsoluteCompact, | ||
]), | ||
}; | ||
const seeMoreBackgroundStyle = | ||
isMeasuring || lineOfImpact?.text ? {} : {backgroundColor}; | ||
const seeMoreContainerStyle = [ | ||
styles.seeMoreContainer, | ||
seeMoreBackgroundStyle, | ||
{ | ||
marginRight: seeMoreRightPadding, | ||
}, | ||
]; | ||
useEffect(() => { | ||
if (!hiddenTextHeight || !textHeight) { | ||
if (!hiddenTextHeightOne || !textHeight) { | ||
return; | ||
} | ||
setSeeMore(hiddenTextHeight > textHeight); | ||
}, [textHeight, hiddenTextHeight]); | ||
setSeeMore(hiddenTextHeightOne > textHeight); | ||
}, [textHeight, hiddenTextHeightOne]); | ||
@@ -124,5 +322,12 @@ useEffect(() => { | ||
useEffect(() => { | ||
setMountHiddenTextOne(true); | ||
setMountHiddenTextTwo(true); | ||
const handle = setTimeout(() => { | ||
setMountHiddenTextOne(true); | ||
setMountHiddenTextTwo(true); | ||
setMountHiddenTextThree(true); | ||
setMountHiddenTextFour(true); | ||
}, debounceSeeMoreCalc); | ||
return () => clearTimeout(handle); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [ | ||
// re calc if any of these params change | ||
numberOfLines, | ||
@@ -137,18 +342,81 @@ style, | ||
ellipsis, | ||
allowFontScaling, | ||
additionalProps, | ||
]); | ||
const textProps = afterCollapsed | ||
? { | ||
onLayout: onTextLayout, | ||
numberOfLines, | ||
ellipsizeMode: 'tail', | ||
useEffect(() => { | ||
const checkIfStillMeasuring = () => { | ||
if ( | ||
!mountHiddenTextOne && | ||
!mountHiddenTextTwo && | ||
!mountHiddenTextThree && | ||
!mountHiddenTextFour | ||
) { | ||
setIsMeasuring(false); | ||
if (animate) { | ||
LayoutAnimation.configureNext(readmoreAnimation); | ||
} | ||
} | ||
: {}; | ||
}; | ||
const additionalProps = {}; | ||
if (allowFontScaling !== undefined) { | ||
additionalProps.allowFontScaling = allowFontScaling; | ||
} | ||
const handler = setTimeout(checkIfStillMeasuring, debounceSeeMoreCalc); | ||
return () => clearTimeout(handler); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [ | ||
mountHiddenTextOne, | ||
mountHiddenTextTwo, | ||
mountHiddenTextThree, | ||
mountHiddenTextFour, | ||
]); | ||
// a map of additional props to be passed down | ||
// in hidden text components other than style | ||
// for accurate measurements | ||
useEffect(() => { | ||
const _additionalProps = {}; | ||
// pick selected params | ||
if (allowFontScaling !== undefined) { | ||
_additionalProps.allowFontScaling = allowFontScaling; | ||
} | ||
setAdditionalProps(_additionalProps); | ||
}, [allowFontScaling]); | ||
useEffect(() => { | ||
const handle = setTimeout(measureSeeMoreLine, debounceSeeMoreCalc); | ||
return () => clearTimeout(handle); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [measureSeeMoreLine]); | ||
useEffect(() => { | ||
const _textChildren = childrenToTextChildren(children, TextComponent); | ||
setCollapsedChildren(_textChildren); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [children]); | ||
useEffect(() => { | ||
if (!truncatedLineOfImpact) { | ||
return; | ||
} | ||
setMountHiddenTextFive(true); | ||
}, [truncatedLineOfImpact]); | ||
useEffect(() => { | ||
console.log('padding', truncatedLineOfImpactWidth, seeMoreWidth, textWidth); | ||
if (!truncatedLineOfImpactWidth || !seeMoreWidth || !textWidth) { | ||
setSeeMoreRightPadding(0); | ||
return; | ||
} | ||
const _seeMoreRightPadding = | ||
textWidth - truncatedLineOfImpactWidth - seeMoreWidth; | ||
if (_seeMoreRightPadding > 0) { | ||
setSeeMoreRightPadding(_seeMoreRightPadding); | ||
if (animate) { | ||
LayoutAnimation.configureNext(readmoreAnimation); | ||
} | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [truncatedLineOfImpactWidth, seeMoreWidth, textWidth]); | ||
return ( | ||
@@ -159,9 +427,5 @@ <View style={wrapperStyle}> | ||
<TextComponent | ||
{...additionalProps} | ||
style={StyleSheet.flatten([ | ||
Array.isArray(style) ? StyleSheet.flatten(style) : style, | ||
styles.hiddenTextAbsolute, | ||
])} | ||
{...commonHiddenComponentProps} | ||
ellipsizeMode={'clip'} | ||
onLayout={onHiddenTextLayout}> | ||
onLayout={onHiddenTextLayoutOne}> | ||
{children || ''} | ||
@@ -173,12 +437,39 @@ </TextComponent> | ||
<TextComponent | ||
{...additionalProps} | ||
style={StyleSheet.flatten([ | ||
Array.isArray(style) ? StyleSheet.flatten(style) : style, | ||
styles.hiddenTextAbsolute, | ||
])} | ||
{...commonHiddenComponentProps} | ||
onLayout={onHiddenSeeLessTextLayoutTwo}> | ||
{children || ''} | ||
{` ${seeLessText}`} | ||
{/* 3 spaces before see less are intentional */} | ||
{` ${seeLessText}`} | ||
</TextComponent> | ||
)} | ||
{/* to remove all flickers add another hidden component with collapsed children to get seeMore and all hidden components to use collapsed children only */} | ||
{mountHiddenTextThree && ( | ||
<TextComponent | ||
{...commonHiddenComponentProps} | ||
numberOfLines={numberOfLines} | ||
onLayout={onLayoutHiddenTextThree}> | ||
{collapsedChildren || ''} | ||
{/* no see less here since it's in collapsed state replicating original component */} | ||
</TextComponent> | ||
)} | ||
{/* extract line of impact -> see more line */} | ||
{mountHiddenTextFour && ( | ||
<TextComponent | ||
{...commonHiddenComponentProps} | ||
numberOfLines={numberOfLines + 1} | ||
onLayout={onLayoutHiddenTextFour} | ||
onTextLayout={onTextLayoutHiddenTextFour}> | ||
{collapsedChildren || ''} | ||
{/* no see less here since it's in collapsed state replicating original component */} | ||
</TextComponent> | ||
)} | ||
{/* extract width of line of impact without see more line */} | ||
{mountHiddenTextFive && ( | ||
<TextComponent | ||
{...hiddenComponentPropsLineOfImpact} | ||
onLayout={onLayoutHiddenTextFive}> | ||
{truncatedLineOfImpact} | ||
</TextComponent> | ||
)} | ||
{/* actual text component */} | ||
<TextComponent | ||
@@ -189,3 +480,5 @@ {...additionalProps} | ||
{...textProps}> | ||
{children || ''} | ||
{isMeasuring || (seeMore && collapsed) | ||
? measuredCollapsedChildren || collapsedChildren || '' | ||
: children || ''} | ||
{seeMore && !collapsed && !expandOnly && ( | ||
@@ -197,3 +490,3 @@ <TextComponent | ||
style={seeLessStyle}> | ||
{hiddenTextHeightWithSeeLess > hiddenTextHeight ? '\n' : ' '} | ||
{hiddenTextHeightWithSeeLess > hiddenTextHeightOne ? '\n' : ' '} | ||
{seeLessText} | ||
@@ -203,4 +496,5 @@ </TextComponent> | ||
</TextComponent> | ||
{seeMore && collapsed && afterCollapsed && ( | ||
<View style={[styles.seeMoreContainer, {backgroundColor}]}> | ||
{/* See more component */} | ||
{seeMore && collapsed && !!collapsedChildren && !isMeasuring && ( | ||
<View style={seeMoreContainerStyle} onLayout={onSeeMoreViewLayout}> | ||
<TextComponent | ||
@@ -210,4 +504,11 @@ {...additionalProps} | ||
onPress={toggle} | ||
style={style}> | ||
{`${ellipsis}`} | ||
</TextComponent> | ||
<TextComponent | ||
{...additionalProps} | ||
{...restProps} | ||
onPress={toggle} | ||
style={[style, seeMoreStyle]}> | ||
{`${ellipsis} ${seeMoreText}`} | ||
{` ${seeMoreText}`} | ||
</TextComponent> | ||
@@ -230,3 +531,11 @@ </View> | ||
color: 'transparent', | ||
display: 'none', | ||
}, | ||
hiddenTextAbsoluteCompact: { | ||
position: 'absolute', | ||
left: 0, | ||
top: 0, | ||
color: 'transparent', | ||
display: 'none', | ||
}, | ||
seeMoreContainer: { | ||
@@ -273,2 +582,4 @@ position: 'absolute', | ||
expandOnly: PropTypes.bool, | ||
seeMoreOverlapCount: PropTypes.number, | ||
debounceSeeMoreCalc: PropTypes.number, | ||
}; | ||
@@ -292,4 +603,6 @@ | ||
expandOnly: false, | ||
seeMoreOverlapCount: 1, | ||
debounceSeeMoreCalc: 300, | ||
}; | ||
export default memo(ReadMore); |
{ | ||
"name": "@fawazahmed/react-native-read-more", | ||
"version": "1.1.5", | ||
"version": "2.0.0", | ||
"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", |
![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) | ||
### Imp: v2.0.0 is exprimental, if you see issues please report as gituhb issue and downgrade to v1.1.5 | ||
#### Please :star: it, thanks :thumbsup: | ||
@@ -4,0 +6,0 @@ # react-native-read-more |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
505026
66
730
116