Skip to content

Feat/Text highlight #1514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions demo/src/screens/componentScreens/TextScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ class TextScreen extends Component {
<Text text50R highlightString={'danc'} highlightStyle={{fontWeight: '200', color: Colors.grey20}}>
Dancing in The Dark
</Text>
<Text text60R highlightString={['dancing', 'da']} highlightStyle={{color: Colors.green30}}>
Dancing in The Dark
</Text>
</View>
{this.renderDivider()}
<View padding-20 centerH>
Expand Down
15 changes: 13 additions & 2 deletions generatedTypes/src/components/text/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export declare type TextProps = RNTextProps & TypographyModifiers & ColorsModifi
/**
* Substring to highlight
*/
highlightString?: string;
highlightString?: string | string[];
/**
* Custom highlight style for highlight string
*/
Expand All @@ -42,7 +42,18 @@ declare type PropsTypes = BaseComponentInjectedProps & ForwardRefInjectedProps &
declare class Text extends PureComponent<PropsTypes> {
static displayName: string;
private TextContainer;
getTextPartsByHighlight(targetString?: string, highlightString?: string): string[];
getPartsByHighlight(targetString: string | undefined, highlightString: string | string[]): {
string: string;
shouldHighlight: boolean;
}[];
getTextPartsByHighlight(targetString?: string, highlightString?: string): {
string: string;
shouldHighlight: boolean;
}[];
getArrayPartsByHighlight(targetString?: string, highlightString?: string[]): {
string: string;
shouldHighlight: boolean;
}[];
renderText(children: any): any;
render(): JSX.Element;
}
Expand Down
18 changes: 9 additions & 9 deletions src/components/text/__tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,53 @@ describe('Text', () => {
it('should return the whole string as a single part when highlight string is empty', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('Playground Screen', '');
expect(result).toEqual(['Playground Screen']);
expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]);
});
it('should return the whole string as a single part when highlight string dont match', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('Playground Screen', 'aaa');
expect(result).toEqual(['Playground Screen']);
expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]);
});
it('should break text to parts according to highlight string', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('Playground Screen', 'Scr');
expect(result).toEqual(['Playground ', 'Scr', 'een']);
expect(result).toEqual([{string: 'Playground ', shouldHighlight: false}, {string: 'Scr', shouldHighlight: true}, {string: 'een', shouldHighlight: false}]);
});

it('should handle case when highlight repeats more than once', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('Dancing in the Dark', 'Da');
expect(result).toEqual(['Da', 'ncing in the ', 'Da', 'rk']);
expect(result).toEqual([{string: 'Da', shouldHighlight: true}, {string: 'ncing in the ', shouldHighlight: false}, {string: 'Da', shouldHighlight: true}, {string: 'rk', shouldHighlight: false}]);
});

it('should handle ignore case-sensetive', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('Dancing in the Dark', 'da');
expect(result).toEqual(['Da', 'ncing in the ', 'Da', 'rk']);
expect(result).toEqual([{string: 'Da', shouldHighlight: true}, {string: 'ncing in the ', shouldHighlight: false}, {string: 'Da', shouldHighlight: true}, {string: 'rk', shouldHighlight: false}]);
});

it('Should handle special characters @', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('@ancing in the @ark', '@a');
expect(result).toEqual(['@a', 'ncing in the ', '@a', 'rk']);
expect(result).toEqual([{string: '@a', shouldHighlight: true}, {string: 'ncing in the ', shouldHighlight: false}, {string: '@a', shouldHighlight: true}, {string: 'rk', shouldHighlight: false}]);
});

it('Should handle special characters !', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('!ancing in the !ark', '!a');
expect(result).toEqual(['!a', 'ncing in the ', '!a', 'rk']);
expect(result).toEqual([{string: '!a', shouldHighlight: true}, {string: 'ncing in the ', shouldHighlight: false}, {string: '!a', shouldHighlight: true}, {string: 'rk', shouldHighlight: false}]);
});

it('Should handle special characters starts with @', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('[email protected]', '@wix');
expect(result).toEqual(['uilib', '@wix', '.com']);
expect(result).toEqual([{string: 'uilib', shouldHighlight: false}, {string: '@wix', shouldHighlight: true}, {string: '.com', shouldHighlight: false}]);
});

it('Should handle empty string .', () => {
const uut = new Text({});
const result = uut.getTextPartsByHighlight('@ancing in the @ark', '');
expect(result).toEqual(['@ancing in the @ark']);
expect(result).toEqual([{string: '@ancing in the @ark', shouldHighlight: false}]);
});
});
});
140 changes: 95 additions & 45 deletions src/components/text/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,37 @@ import {
} from '../../commons/new';
import {Colors} from 'style';

export type TextProps = RNTextProps & TypographyModifiers & ColorsModifiers & MarginModifiers & {
/**
* color of the text
*/
color?: string;
/**
* whether to center the text (using textAlign)
*/
center?: boolean;
/**
* whether to change the text to uppercase
*/
uppercase?: boolean;
/**
* Substring to highlight
*/
highlightString?: string;
/**
* Custom highlight style for highlight string
*/
highlightStyle?: TextStyle;
/**
* Use Animated.Text as a container
*/
animated?: boolean;
textAlign?: string;
style?: StyleProp<TextStyle | Animated.AnimatedProps<TextStyle>>;
}
export type TextProps = RNTextProps &
TypographyModifiers &
ColorsModifiers &
MarginModifiers & {
/**
* color of the text
*/
color?: string;
/**
* whether to center the text (using textAlign)
*/
center?: boolean;
/**
* whether to change the text to uppercase
*/
uppercase?: boolean;
/**
* Substring to highlight
*/
highlightString?: string | string[];
/**
* Custom highlight style for highlight string
*/
highlightStyle?: TextStyle;
/**
* Use Animated.Text as a container
*/
animated?: boolean;
textAlign?: string;
style?: StyleProp<TextStyle | Animated.AnimatedProps<TextStyle>>;
};
export type TextPropTypes = TextProps; //TODO: remove after ComponentPropTypes deprecation;

type PropsTypes = BaseComponentInjectedProps & ForwardRefInjectedProps & TextProps;
Expand All @@ -62,50 +65,97 @@ class Text extends PureComponent<PropsTypes> {
// this._root.setNativeProps(nativeProps); // eslint-disable-line
// }

getTextPartsByHighlight(targetString = '', highlightString = '') {
if (_.isEmpty(highlightString.trim())) {
return [targetString];
getPartsByHighlight(targetString = '', highlightString: string | string[]) {
if (typeof highlightString === 'string') {
if (_.isEmpty(highlightString.trim())) {
return [{string: targetString, shouldHighlight: false}];
}
return this.getTextPartsByHighlight(targetString, highlightString);
} else {
return this.getArrayPartsByHighlight(targetString, highlightString);
}
}

getTextPartsByHighlight(targetString = '', highlightString = '') {
if (highlightString === '') {
return [{string: targetString, shouldHighlight: false}];
}
const textParts = [];
let highlightIndex;

do {
highlightIndex = targetString.toLowerCase().indexOf(highlightString.toLowerCase());
if (highlightIndex !== -1) {
if (highlightIndex > 0) {
textParts.push(targetString.substring(0, highlightIndex));
textParts.push({string: targetString.substring(0, highlightIndex), shouldHighlight: false});
}
textParts.push(targetString.substr(highlightIndex, highlightString.length));
textParts.push({string: targetString.substr(highlightIndex, highlightString.length), shouldHighlight: true});
targetString = targetString.substr(highlightIndex + highlightString.length);
} else {
textParts.push(targetString);
textParts.push({string: targetString, shouldHighlight: false});
}
} while (highlightIndex !== -1);

return textParts;
}

getArrayPartsByHighlight(targetString = '', highlightString = ['']) {
const target = _.toLower(targetString);
const indices = [];
let index = 0;
let lastWordLength = 0;
for (let j = 0; j < highlightString.length; j++) {
const word = _.toLower(highlightString[j]);
const targetSuffix = target.substring(index + lastWordLength);
const i = targetSuffix.indexOf(word);
if (i >= 0) {
const newIndex = index + lastWordLength + i;
indices.push({start: index + lastWordLength + i, end: index + lastWordLength + i + word.length});
index = newIndex;
lastWordLength = word.length;
} else {
break;
}
}
const parts = [];
for (let k = 0; k < indices.length; k++) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using a forEach or even a map in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can't use map or forEach here since in every iteration I use the current index and the next one (k and k+1) which is not possible with those methods..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, map and forEach that comes with javascript don't retrieve the Index..
But using lodash's _.forEach or _.map, you also have access to the index

_.map(array, (item, index) => console.log(index))
_.forEach(array, (item, index) => console.log(index))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, but I need the item and the next item..
I can use it that way:
_.forEach(array, (item, index) => console.log(item, array[index + 1]))
but I don't think it makes that more clear..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I have one last suggestion, let me know what you think (:

What if instead of keeping the indices as couples (which I personally find confusing) you'll keep them as objects of {start, end}, so indices looks like this

[{start: 2, end: 6}, {start: 10, end: 12}, ...]

I think it'll make a easier to understand (:
WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, done :)

if (k === 0 && indices[k].start !== 0) {
parts.push({string: targetString.substring(0, indices[k].start), shouldHighlight: false});
}
parts.push({string: targetString.substring(indices[k].start, indices[k].end), shouldHighlight: true});
if (k === indices.length - 1) {
parts.push({string: targetString.substring(indices[k].end), shouldHighlight: false});
} else {
parts.push({string: targetString.substring(indices[k].end, indices[k + 1].start), shouldHighlight: false});
}
}
return parts;
}

renderText(children: any): any {
const {highlightString, highlightStyle} = this.props;

if (!_.isEmpty(highlightString)) {
if (_.isArray(children)) {
return _.map(children, (child) => {
return _.map(children, child => {
return this.renderText(child);
});
}

if (_.isString(children)) {
const textParts = this.getTextPartsByHighlight(children, highlightString);
return _.map(textParts, (text, index) => {
const shouldHighlight = _.lowerCase(text) === _.lowerCase(highlightString);
return (
<RNText key={index} style={shouldHighlight ? [styles.highlight, highlightStyle] : styles.notHighlight}>
{text}
</RNText>
);
});
const textParts = highlightString && this.getPartsByHighlight(children, highlightString);
return (
textParts &&
_.map(textParts, (text, index) => {
return (
<RNText
key={index}
style={text.shouldHighlight ? [styles.highlight, highlightStyle] : styles.notHighlight}
>
{text.string}
</RNText>
);
})
);
}
}
return children;
Expand Down