rn-mentions
Version:
Mentions textbox for React Native. Works on both ios and android
180 lines (160 loc) • 4.65 kB
JavaScript
import React from "react";
import { Text, StyleSheet, Linking } from "react-native";
/*
* ranges: object or array in the form of {start:, end:, style:}
* matches: object or array in the form of {text:/regex:, style:}
* enabledLinkTypes: array of FORMATTED_LINK_MATCH_TYPE
*
* */
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
const emailRegex = /\S+@\S+\.\S+/g;
const hashRegex = /#\S+/g;
export const FORMATTED_LINK_MATCH_TYPE = {
URL: urlRegex,
EMAIL: emailRegex,
HASH: hashRegex
};
const openUrl = url => {
Linking.canOpenURL(url).then(supported => {
if (supported) {
Linking.openURL(url);
} else {
console.warn("Don't know how to open URI: " + url);
}
});
};
const FormattedText = ({
children,
ranges = [],
matches = [],
enabledLinkTypes = [],
...props
}) => {
if (ranges && !Array.isArray(ranges)) {
ranges = [ranges];
}
if (matches && !Array.isArray(matches)) {
matches = [matches];
}
if (enabledLinkTypes && !Array.isArray(enabledLinkTypes)) {
enabledLinkTypes = [enabledLinkTypes];
}
// convert enabledLinkTypes array to normal match object
for (const match of enabledLinkTypes) {
let m = { regex: match.type };
if (match.style) {
m.style = match.style;
} else {
m.style = FormattedStyles.link;
}
if (match.onPress) {
m.onPress = match.onPress;
} else {
// setup default link handling for url and email if onPress is absent
switch (match.type) {
case FORMATTED_LINK_MATCH_TYPE.URL:
m.onPress = openUrl;
break;
case FORMATTED_LINK_MATCH_TYPE.EMAIL:
m.onPress = text => {
openUrl(`mailto:${text}`);
};
break;
}
}
matches.push(m);
}
// construct ranges from matches
matches.forEach(match => {
// generalize text param to regex
if (match.text) {
match.regex = new RegExp("\\b" + match.text + "\\b", "g");
}
if (match.regex) {
let m;
while ((m = match.regex.exec(children)) != null) {
ranges.push({
start: m.index,
end: m.index + m[0].length,
style: match.style,
onPress: match.onPress
});
}
} else {
console.warn(
`${match} in match array doesn't have either a regex or text`
);
}
});
// remove ranges that don't have enough meta data
const filteredFormats = ranges.filter(
s => s.hasOwnProperty("start") && s.hasOwnProperty("end")
);
if (ranges.length === 0) {
// console.warn("no format found");
return <Text {...props}>{children}</Text>;
}
// sort formats by range, check if there is overlap
const sortedFormats = filteredFormats.sort((a, b) => a.start - b.start);
for (let i = 0; i < sortedFormats.length; i++) {
if (i > 0 && sortedFormats[i].start < sortedFormats[i - 1].end) {
console.warn(
`Overlap found between ${JSON.stringify(
sortedFormats[i - 1]
)}(${children.substring(
sortedFormats[i - 1].start,
sortedFormats[i - 1].end
)}) and ${JSON.stringify(sortedFormats[i])}(${children.substring(
sortedFormats[i].start,
sortedFormats[i].end
)})`
);
return <Text {...props}>{children}</Text>;
}
}
// build formatted string
const result = [];
sortedFormats.forEach((format, index) => {
// add start of string if needed
if (index === 0 && format.start > 0) {
result.push(children.substring(0, format.start));
}
// add gab between two ranges
if (index > 0 && format.start > sortedFormats[index - 1].end) {
result.push(
children.substring(sortedFormats[index - 1].end, format.start)
);
}
const sub = children.substring(format.start, format.end);
result.push(
<Text
style={format.style || FormattedStyles.default}
onPress={
format.onPress
? () => {
format.onPress(sub, format.start, format.end);
}
: null
}
key={index}
>
{sub}
</Text>
);
// add end of string if needed
if (index === sortedFormats.length - 1 && format.end < children.length) {
result.push(children.substring(format.end, children.length));
}
});
return <Text {...props}>{result}</Text>;
};
const FormattedStyles = StyleSheet.create({
default: {
fontWeight: "600"
},
link: {
color: "#005daa",
textDecorationLine: "underline"
}
});
export default FormattedText;