Skip to content

Commit a5f9a72

Browse files
author
Kashish Grover
committed
fix: fix link not appearing, general improvements
1 parent df8b2e0 commit a5f9a72

File tree

4 files changed

+154
-78
lines changed

4 files changed

+154
-78
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = {
2020
rules: {
2121
'react/jsx-props-no-spreading': ['off'],
2222
'react/jsx-filename-extension': ['off'],
23-
'no-use-before-define': ['off']
23+
'no-use-before-define': ['off'],
24+
'operator-linebreak': ['off']
2425
},
2526
};

.prettierrc.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
module.exports = {
2-
bracketSpacing: false,
2+
arrowParens: 'always',
3+
bracketSpacing: true,
34
jsxBracketSameLine: true,
5+
printWidth: 100,
46
singleQuote: true,
5-
trailingComma: 'all',
7+
tabWidth: 2,
8+
trailingComma: 'always',
9+
useTabs: false,
610
};

src/SeeMore/SeeMore.js

Lines changed: 58 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React from 'react';
22
import { Text, PanResponder } from 'react-native';
3-
import reactNativeTextSize from 'react-native-text-size';
43
import PropTypes from 'prop-types';
5-
import debounce from 'lodash/debounce';
4+
import SeeMoreUtil from './SeeMoreUtil';
65

76
class SeeMore extends React.Component {
87
panResponder = PanResponder.create({
@@ -13,9 +12,9 @@ class SeeMore extends React.Component {
1312
onPanResponderRelease: () => this.handleLinkReleased(),
1413
});
1514

16-
setDebouncedWidth;
17-
18-
// Map of containerWidth to truncationIndex so that we don't calculate it each time
15+
/**
16+
* Map of containerWidth and truncationIndex so that we don't calculate it each time
17+
*/
1918
containerWidthToTruncationIndexMap;
2019

2120
constructor(props) {
@@ -26,90 +25,63 @@ class SeeMore extends React.Component {
2625
isShowingMore: false,
2726
truncationIndex: undefined,
2827
};
29-
30-
this.setDebouncedWidth = debounce((e) => {
31-
this.findTruncationIndex(e.nativeEvent.layout.width);
32-
}, 100);
3328
}
3429

30+
isExpanded = () => {
31+
const { isShowingMore } = this.state;
32+
return isShowingMore;
33+
};
34+
3535
onLayout = (e) => {
3636
// e.persist() keeps the original synthetic event intact
3737
e.persist();
38-
this.setDebouncedWidth(e);
38+
this.findAndUpdateTruncationIndex(e.nativeEvent.layout.width);
39+
};
40+
41+
findAndUpdateTruncationIndex = async (containerWidth) => {
42+
const truncationIndex = await this.findTruncationIndex(containerWidth);
43+
this.setState({ truncationIndex });
3944
};
4045

4146
findTruncationIndex = async (containerWidth) => {
4247
if (
43-
this.containerWidthToTruncationIndexMap
44-
&& this.containerWidthToTruncationIndexMap[containerWidth]
48+
this.containerWidthToTruncationIndexMap &&
49+
this.containerWidthToTruncationIndexMap[containerWidth]
4550
) {
46-
this.setState({ truncationIndex: this.containerWidthToTruncationIndexMap[containerWidth] });
47-
return;
51+
return this.containerWidthToTruncationIndexMap[containerWidth];
4852
}
4953

5054
const {
5155
children: text,
5256
style: { fontSize, fontFamily, fontWeight },
53-
seeMoreText,
5457
numberOfLines,
58+
seeMoreText,
5559
} = this.props;
5660

57-
const { width: textWidth } = await reactNativeTextSize.measure({
61+
const truncationIndex = await SeeMoreUtil.getTruncationIndex(
5862
text,
63+
numberOfLines,
5964
fontSize,
6065
fontFamily,
6166
fontWeight,
62-
});
63-
64-
const textWidthLimit = containerWidth * numberOfLines;
65-
66-
if (textWidth < textWidthLimit) {
67-
this.setState({ truncationIndex: undefined });
68-
return;
69-
}
70-
71-
const { width: seeMoreTextWidth } = await reactNativeTextSize.measure({
72-
text: ` ...${seeMoreText}`,
73-
fontSize,
74-
fontFamily,
75-
fontWeight,
76-
});
77-
78-
const truncatedWidth = textWidthLimit - 2 * seeMoreTextWidth;
79-
80-
let index = 0;
81-
let start = 0;
82-
let end = text.length - 1;
83-
84-
while (start <= end) {
85-
const middle = start + (end - start) / 2;
86-
// eslint-disable-next-line no-await-in-loop
87-
const { width: partialWidth } = await reactNativeTextSize.measure({
88-
text: text.slice(0, middle),
89-
fontSize,
90-
fontFamily,
91-
fontWeight,
92-
});
93-
if (Math.abs(truncatedWidth - partialWidth) <= 10) {
94-
index = middle;
95-
break;
96-
} else if (partialWidth > truncatedWidth) {
97-
end = middle - 1;
98-
} else {
99-
start = middle + 1;
100-
}
101-
}
102-
103-
const truncationIndex = Math.floor(index);
67+
containerWidth,
68+
seeMoreText,
69+
);
10470

105-
// Map truncation index to width so that we don't calculate it again
10671
this.containerWidthToTruncationIndexMap = {
10772
...this.containerWidthToTruncationIndexMap,
10873
[containerWidth]: truncationIndex,
10974
};
110-
this.setState({ truncationIndex });
75+
76+
return truncationIndex;
11177
};
11278

79+
collapse() {
80+
return new Promise((resolve) => {
81+
this.setState({ isShowingMore: false }, () => resolve());
82+
});
83+
}
84+
11385
handleLinkPressed() {
11486
this.setState({
11587
isLinkPressed: true,
@@ -130,36 +102,47 @@ class SeeMore extends React.Component {
130102
});
131103
}
132104

133-
render() {
105+
renderSeeMoreSeeLessLink() {
134106
const { isLinkPressed, isShowingMore, truncationIndex } = this.state;
135107
const {
136108
children: text,
137-
numberOfLines,
138109
linkColor,
139110
linkPressedColor,
140111
seeMoreText,
141112
seeLessText,
142113
} = this.props;
143114
const isTruncable = truncationIndex < text.length;
144115

116+
if (!isTruncable) {
117+
return null;
118+
}
119+
120+
return (
121+
<Text
122+
{...this.props}
123+
{...this.panResponder.panHandlers}
124+
style={{ color: isLinkPressed ? linkPressedColor : linkColor }}
125+
>
126+
{isShowingMore ? null : <Text {...this.props}>...</Text>}
127+
{isShowingMore ? ` ${seeLessText}` : ` ${seeMoreText}`}
128+
</Text>
129+
);
130+
}
131+
132+
render() {
133+
const { isShowingMore, truncationIndex } = this.state;
134+
const { children: text, numberOfLines } = this.props;
135+
145136
return (
146137
<Text
147138
onLayout={isShowingMore ? undefined : this.onLayout}
148139
numberOfLines={isShowingMore ? undefined : numberOfLines}
140+
{...this.panResponder.panHandlers}
149141
>
150-
<Text {...this.props}>{isShowingMore ? text : text.slice(0, truncationIndex)}</Text>
151-
{isTruncable ? (
152-
<>
153-
{isShowingMore ? null : <Text {...this.props}>...</Text>}
154-
<Text
155-
{...this.props}
156-
{...this.panResponder.panHandlers}
157-
style={{ color: isLinkPressed ? linkPressedColor : linkColor }}
158-
>
159-
{isShowingMore ? ` ${seeLessText}` : ` ${seeMoreText}`}
160-
</Text>
161-
</>
162-
) : null}
142+
<Text {...this.props}>
143+
{isShowingMore ? text : text.slice(0, truncationIndex)}
144+
</Text>
145+
{this.renderSeeMoreSeeLessLink()}
163146
</Text>
164147
);
165148
}

src/SeeMore/SeeMoreUtil.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { PixelRatio } from 'react-native';
2+
import reactNativeTextSize from 'react-native-text-size';
3+
4+
/**
5+
* When difference between partialTextWidth and widthLimit is less than
6+
* this value, we mark the truncation index.
7+
*/
8+
const DIFFERENCE_THRESHOLD = 10;
9+
10+
/**
11+
* Finds the point where the text will be truncated, leaving enough space to show
12+
* the "read more" link
13+
*
14+
* @param text {string} Text for which you need to find the truncation index
15+
* @param numberOfLines {number} Number of lines being displayed
16+
* @param fontSize {number} Font size
17+
* @param fontFamily {string} Font family
18+
* @param fontWeight {string} Font weight
19+
* @param containerWidth {number} Width of the container in which the text will be contained
20+
* @param seeMoreText {string} See more text
21+
*/
22+
async function getTruncationIndex(
23+
text,
24+
numberOfLines,
25+
fontSize,
26+
fontFamily,
27+
fontWeight,
28+
containerWidth,
29+
seeMoreText,
30+
) {
31+
const scaledFontSize = Math.round(fontSize * PixelRatio.getFontScale());
32+
33+
const { width: totalTextWidth } = await reactNativeTextSize.measure({
34+
text,
35+
fontSize: scaledFontSize,
36+
fontFamily,
37+
fontWeight,
38+
});
39+
40+
/**
41+
* Max possible width of the text when it is collapsed.
42+
* 10 is approx value of white space width per line.
43+
*/
44+
const widthLimit = (containerWidth - 10) * numberOfLines;
45+
46+
if (totalTextWidth < widthLimit) {
47+
return undefined;
48+
}
49+
50+
let index = 0;
51+
let start = 0;
52+
let end = text.length - 1;
53+
54+
while (start <= end) {
55+
const middle = start + (end - start) / 2;
56+
// eslint-disable-next-line no-await-in-loop
57+
const { width: partialTextWidth } = await reactNativeTextSize.measure({
58+
text: text.slice(0, middle),
59+
fontSize: scaledFontSize,
60+
fontFamily,
61+
fontWeight,
62+
});
63+
if (Math.abs(widthLimit - partialTextWidth) <= DIFFERENCE_THRESHOLD) {
64+
index = middle;
65+
break;
66+
} else if (partialTextWidth > widthLimit) {
67+
end = middle - 1;
68+
} else {
69+
start = middle + 1;
70+
}
71+
}
72+
73+
let truncationIndex = Math.floor(index) - (seeMoreText.length + 10);
74+
75+
// If there is a new line character before this truncation index, this will break
76+
// So we find the first new line character before truncationIndex and set that as the
77+
// new truncation index
78+
const newLineCharacterIndex = text.slice(0, truncationIndex).indexOf('\n');
79+
if (newLineCharacterIndex > -1) {
80+
truncationIndex = newLineCharacterIndex;
81+
}
82+
83+
return truncationIndex;
84+
}
85+
86+
export default {
87+
getTruncationIndex,
88+
};

0 commit comments

Comments
 (0)