Skip to content
This repository has been archived by the owner on Jun 24, 2021. It is now read-only.

Commit

Permalink
Merge pull request #9 from Astrocoders/patched-highlights
Browse files Browse the repository at this point in the history
feature/highlights
  • Loading branch information
georgelima authored Apr 8, 2019
2 parents 29acbf6 + c2cf522 commit 264136a
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 23 deletions.
136 changes: 120 additions & 16 deletions Demo/SelectableText.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,129 @@
import React from "react";
import { Text, requireNativeComponent, Platform } from "react-native";
import React from 'react'
import { Text, requireNativeComponent, Platform } from 'react-native'
import { v4 } from 'uuid'
import memoize from 'fast-memoize'

const RNSelectableText = requireNativeComponent("RNSelectableText");
const RNSelectableText = requireNativeComponent('RNSelectableText')

export const SelectableText = ({ onSelection, value, children, ...props }) => {
/**
* numbers: array({start: int, end: int, id: string})
*/
const combineHighlights = memoize(numbers => {
return numbers
.sort((a, b) => a.start - b.start || a.end - b.end)
.reduce(function(combined, next) {
if (!combined.length || combined[combined.length - 1].end < next.start) combined.push(next)
else {
var prev = combined.pop()
combined.push({
start: prev.start,
end: Math.max(prev.end, next.end),
id: next.id,
})
}
return combined
}, [])
})

/**
* value: string
* highlights: array({start: int, end: int, id: any})
*/
const mapHighlightsRanges = (value, highlights) => {
const combinedHighlights = combineHighlights(highlights)

if (combinedHighlights.length === 0) return [{ isHighlight: false, text: value }]

const data = [{ isHighlight: false, text: value.slice(0, combinedHighlights[0].start) }]

combinedHighlights.forEach(({ start, end }, idx) => {
data.push({
isHighlight: true,
text: value.slice(start, end),
})

if (combinedHighlights[idx + 1]) {
data.push({
isHighlight: false,
text: value.slice(end, combinedHighlights[idx + 1].start),
})
}
})

data.push({
isHighlight: false,
text: value.slice(combinedHighlights[combinedHighlights.length - 1].end, value.length),
})

return data.filter(x => x.text)
}

/**
* Props
* ...TextProps
* onSelection: ({ content: string, eventType: string, selectionStart: int, selectionEnd: int }) => void
* children: ReactNode
* highlights: array({ id, start, end })
* highlightColor: string
* onHighlightPress: string => void
*/
export const SelectableText = ({ onSelection, onHighlightPress, value, children, ...props }) => {
const onSelectionNative = ({
nativeEvent: { content, eventType, selectionStart, selectionEnd }
nativeEvent: { content, eventType, selectionStart, selectionEnd },
}) => {
onSelection &&
onSelection({ content, eventType, selectionStart, selectionEnd });
};
onSelection && onSelection({ content, eventType, selectionStart, selectionEnd })
}

const onHighlightPressNative = onHighlightPress
? Platform.OS === 'ios'
? ({ nativeEvent: { clickedRangeStart, clickedRangeEnd } }) => {
if (!props.highlights || props.highlights.length === 0) return

const mergedHighlights = combineHighlights(props.highlights)

const hightlightInRange = mergedHighlights.find(
({ start, end }) => clickedRangeStart >= start - 1 && clickedRangeEnd <= end + 1,
)

if (hightlightInRange) {
onHighlightPress(hightlightInRange.id)
}
}
: onHighlightPress
: () => {}

return Platform.OS === "ios" ? (
return (
<RNSelectableText
{...props}
value={children ? children : value}
onHighlightPress={onHighlightPressNative}
selectable
onSelection={onSelectionNative}
/>
) : (
<RNSelectableText {...props} selectable onSelection={onSelectionNative}>
{children ? children : <Text selectable>{value}</Text>}
>
<Text selectable key={v4()}>
{props.highlights && props.highlights.length > 0
? mapHighlightsRanges(value, props.highlights).map(({ id, isHighlight, text }) => (
<Text
key={v4()}
selectable
style={
isHighlight
? {
backgroundColor: props.highlightColor,
}
: {}
}
onPress={() => {
if (isHighlight) {
onHighlightPress && onHighlightPress(id)
}
}}
>
{text}
</Text>
))
: value}
{props.appendToChildren ? props.appendToChildren : null}
</Text>
</RNSelectableText>
);
};
)
}
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# react-native-selectable-text

## Demo
Expand Down Expand Up @@ -43,7 +44,7 @@ import { SelectableText } from "react-native-selectable-text";
#### iOS

1. In XCode, in the project navigator, right click `Libraries``Add Files to [your project's name]`
2. Go to `node_modules``react-native-selectable-text` and add `RNSelectableText.xcodeproj`
2. Go to `node_modules``@astrocoders/react-native-selectable-text` and add `RNSelectableText.xcodeproj`
3. In XCode, in the project navigator, select your project. Add `libRNSelectableText.a` to your project's `Build Phases``Link Binary With Libraries`
4. Run your project (`Cmd+R`)<

Expand All @@ -57,9 +58,21 @@ import { SelectableText } from "react-native-selectable-text";
2. Append the following lines to `android/settings.gradle`:
```
include ':react-native-selectable-text'
project(':react-native-selectable-text').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-selectable-text/android')
project(':react-native-selectable-text').projectDir = new File(rootProject.projectDir, '../node_modules/@astrocoders/react-native-selectable-text/android')
```
3. Insert the following lines inside the dependencies block in `android/app/build.gradle`:
```
compile project(':react-native-selectable-text')
```

## Props
| name | description | type | default |
|--|--|--|--|
| **value** | text content | string | "" |
| **onSelection** | Called when the user taps in a item of the selection menu | ({ eventType: string, content: string, selectionStart: int, selectionEnd: int }) => void | () => {} |
| **menuItems** | context menu items | array(string) | [] |
| **style** | additional styles to be applied to text | Object | null |
| **highlights** | array of text ranges that should be highlighted with an optional id | array({ id: string, start: int, end: int }) | [] |
| **highlightColor** | highlight color |string | null |
| **onHighlightPress** | called when the user taps the highlight |(id: string) => void | () => {} |
| **appendToChildren** | element to be added in the last line of text | ReactNode | null |
1 change: 1 addition & 0 deletions ios/RNSelectableTextManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonnull, nonatomic, copy) NSString *value;
@property (nonatomic, copy) RCTDirectEventBlock onSelection;
@property (nullable, nonatomic, copy) NSArray<NSString *> *menuItems;
@property (nonatomic, copy) RCTDirectEventBlock onHighlightPress;

@end

Expand Down
1 change: 1 addition & 0 deletions ios/RNSelectableTextManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onSelection, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(menuItems, NSArray);
RCT_EXPORT_VIEW_PROPERTY(value, NSString);
RCT_EXPORT_VIEW_PROPERTY(onHighlightPress, RCTDirectEventBlock)

#pragma mark - Multiline <TextInput> (aka TextView) specific properties

Expand Down
1 change: 1 addition & 0 deletions ios/RNSelectableTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonnull, nonatomic, copy) NSString *value;
@property (nonatomic, copy) RCTDirectEventBlock onSelection;
@property (nullable, nonatomic, copy) NSArray<NSString *> *menuItems;
@property (nonatomic, copy) RCTDirectEventBlock onHighlightPress;

@end

Expand Down
41 changes: 37 additions & 4 deletions ios/RNSelectableTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,24 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
gesture.enabled = NO;
}
}

[self addSubview:_backedTextInputView];

UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPressGesture.minimumPressDuration = 0.15;

UITapGestureRecognizer *tapGesture = [ [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapGesture.numberOfTapsRequired = 2;

UITapGestureRecognizer *singleTapGesture = [ [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
singleTapGesture.numberOfTapsRequired = 1;

[_backedTextInputView addGestureRecognizer:longPressGesture];
[_backedTextInputView addGestureRecognizer:tapGesture];
[_backedTextInputView addGestureRecognizer:singleTapGesture];

[self setUserInteractionEnabled:true];
[self setUserInteractionEnabled:YES];
}

return self;
}

Expand Down Expand Up @@ -87,6 +90,28 @@ -(void) _handleGesture
[menuController setMenuVisible:YES animated:YES];
}

-(void) handleSingleTap: (UITapGestureRecognizer *) gesture
{
CGPoint pos = [gesture locationInView:_backedTextInputView];
pos.y += _backedTextInputView.contentOffset.y;

UITextPosition *tapPos = [_backedTextInputView closestPositionToPoint:pos];
UITextRange *word = [_backedTextInputView.tokenizer rangeEnclosingPosition:tapPos withGranularity:(UITextGranularityWord) inDirection:UITextLayoutDirectionRight];

UITextPosition* beginning = _backedTextInputView.beginningOfDocument;

UITextPosition *selectionStart = word.start;
UITextPosition *selectionEnd = word.end;

const NSInteger location = [_backedTextInputView offsetFromPosition:beginning toPosition:selectionStart];
const NSInteger endLocation = [_backedTextInputView offsetFromPosition:beginning toPosition:selectionEnd];

self.onHighlightPress(@{
@"clickedRangeStart": @(location),
@"clickedRangeEnd": @(endLocation),
});
}

-(void) handleLongPress: (UILongPressGestureRecognizer *) gesture
{
CGPoint pos = [gesture locationInView:_backedTextInputView];
Expand Down Expand Up @@ -144,6 +169,8 @@ - (void)tappedMenuItem:(NSString *)eventType
@"selectionStart": @(start),
@"selectionEnd": @(selection.end)
});

[_backedTextInputView setSelectedTextRange:nil notifyDelegate:false];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
Expand Down Expand Up @@ -181,4 +208,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
return NO;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
[_backedTextInputView setSelectedTextRange:nil notifyDelegate:true];
return [super hitTest:point withEvent:event];
}

@end
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@astrocoders/react-native-selectable-text",
"version": "1.1.1",
"version": "1.2.0",
"description": "",
"main": "index.js",
"scripts": {
Expand All @@ -13,5 +13,9 @@
"license": "MIT",
"peerDependencies": {
"react-native": "^0.41.2"
},
"dependencies": {
"fast-memoize": "^2.5.1",
"uuid": "^3.3.2"
}
}
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


fast-memoize@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.1.tgz#c3519241e80552ce395e1a32dcdde8d1fd680f5d"
integrity sha512-xdmw296PCL01tMOXx9mdJSmWY29jQgxyuZdq0rEHMu+Tpe1eOEtCycoG6chzlcrWsNgpZP7oL8RiQr7+G6Bl6g==

uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==

0 comments on commit 264136a

Please sign in to comment.