react-native-controlled-mentions
Version:
Fully controlled React Native mentions component
389 lines • 16.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.replaceMentionValues = exports.getValueFromParts = exports.parseValue = exports.getMentionValue = exports.generateMentionPart = exports.generatePlainTextPart = exports.generateValueWithAddedSuggestion = exports.generateValueFromPartsAndChangedText = exports.getMentionPartSuggestionKeywords = exports.isMentionPartType = exports.defaultMentionTextStyle = exports.mentionRegEx = void 0;
const diff_1 = require("diff");
// @ts-ignore the lib do not have TS declarations yet
const string_prototype_matchall_1 = __importDefault(require("string.prototype.matchall"));
/**
* RegEx grouped results. Example - "@[Full Name](123abc)"
* We have 4 groups here:
* - The whole original string - "@[Full Name](123abc)"
* - Mention trigger - "@"
* - Name - "Full Name"
* - Id - "123abc"
*/
const mentionRegEx = /((.)\[([^[]*)]\(([^(^)]*)\))/gi;
exports.mentionRegEx = mentionRegEx;
const defaultMentionTextStyle = { fontWeight: 'bold', color: 'blue' };
exports.defaultMentionTextStyle = defaultMentionTextStyle;
const defaultPlainStringGenerator = ({ trigger }, { name }) => `${trigger}${name}`;
const isMentionPartType = (partType) => {
return partType.trigger != null;
};
exports.isMentionPartType = isMentionPartType;
const getPartIndexByCursor = (parts, cursor, isIncludeEnd) => {
return parts.findIndex(one => cursor >= one.position.start && isIncludeEnd ? cursor <= one.position.end : cursor < one.position.end);
};
/**
* The method for getting parts between two cursor positions.
* ```
* | part1 | part2 | part3 |
* a b c|d e f g h i j h k|l m n o
* ```
* We will get 3 parts here:
* 1. Part included 'd'
* 2. Part included 'efghij'
* 3. Part included 'hk'
* Cursor will move to position after 'k'
*
* @param parts full part list
* @param cursor current cursor position
* @param count count of characters that didn't change
*/
const getPartsInterval = (parts, cursor, count) => {
const newCursor = cursor + count;
const currentPartIndex = getPartIndexByCursor(parts, cursor);
const currentPart = parts[currentPartIndex];
const newPartIndex = getPartIndexByCursor(parts, newCursor, true);
const newPart = parts[newPartIndex];
let partsInterval = [];
if (!currentPart || !newPart) {
return partsInterval;
}
// Push whole first affected part or sub-part of the first affected part
if (currentPart.position.start === cursor && currentPart.position.end <= newCursor) {
partsInterval.push(currentPart);
}
else {
partsInterval.push(generatePlainTextPart(currentPart.text.substr(cursor - currentPart.position.start, count)));
}
if (newPartIndex > currentPartIndex) {
// Concat fully included parts
partsInterval = partsInterval.concat(parts.slice(currentPartIndex + 1, newPartIndex));
// Push whole last affected part or sub-part of the last affected part
if (newPart.position.end === newCursor && newPart.position.start >= cursor) {
partsInterval.push(newPart);
}
else {
partsInterval.push(generatePlainTextPart(newPart.text.substr(0, newCursor - newPart.position.start)));
}
}
return partsInterval;
};
/**
* Function for getting object with keyword for each mention part type
*
* If keyword is undefined then we don't tracking mention typing and shouldn't show suggestions.
* If keyword is not undefined (even empty string '') then we are tracking mention typing.
*
* Examples where @name is just plain text yet, not mention:
* '|abc @name dfg' - keyword is undefined
* 'abc @| dfg' - keyword is ''
* 'abc @name| dfg' - keyword is 'name'
* 'abc @na|me dfg' - keyword is 'na'
* 'abc @|name dfg' - keyword is against ''
* 'abc @name |dfg' - keyword is 'name '
* 'abc @name dfg|' - keyword is 'name dfg'
* 'abc @name dfg |' - keyword is undefined (we have more than one space)
* 'abc @name dfg he|' - keyword is undefined (we have more than one space)
*/
const getMentionPartSuggestionKeywords = (parts, plainText, selection, partTypes) => {
const keywordByTrigger = {};
partTypes.filter(isMentionPartType).forEach(({ trigger, allowedSpacesCount = 1, }) => {
keywordByTrigger[trigger] = undefined;
// Check if we don't have selection range
if (selection.end != selection.start) {
return;
}
// Find the part with the cursor
const part = parts.find(one => selection.end > one.position.start && selection.end <= one.position.end);
// Check if the cursor is not in mention type part
if (part == null || part.data != null) {
return;
}
const triggerIndex = plainText.lastIndexOf(trigger, selection.end);
// Return undefined in case when:
if (
// - the trigger index is not event found
triggerIndex == -1
// - the trigger index is out of found part with selection cursor
|| triggerIndex < part.position.start
// - the trigger is not at the beginning and we don't have space or new line before trigger
|| (triggerIndex > 0 && !/[\s\n]/gi.test(plainText[triggerIndex - 1]))) {
return;
}
// Looking for break lines and spaces between the current cursor and trigger
let spacesCount = 0;
for (let cursor = selection.end - 1; cursor >= triggerIndex; cursor -= 1) {
// Mention cannot have new line
if (plainText[cursor] === '\n') {
return;
}
// Incrementing space counter if the next symbol is space
if (plainText[cursor] === ' ') {
spacesCount += 1;
// Check maximum allowed spaces in trigger word
if (spacesCount > allowedSpacesCount) {
return;
}
}
}
keywordByTrigger[trigger] = plainText.substring(triggerIndex + 1, selection.end);
});
return keywordByTrigger;
};
exports.getMentionPartSuggestionKeywords = getMentionPartSuggestionKeywords;
/**
* Generates new value when we changing text.
*
* @param parts full parts list
* @param originalText original plain text
* @param changedText changed plain text
*/
const generateValueFromPartsAndChangedText = (parts, originalText, changedText) => {
const changes = diff_1.diffChars(originalText, changedText);
let newParts = [];
let cursor = 0;
changes.forEach(change => {
switch (true) {
/**
* We should:
* - Move cursor forward on the changed text length
*/
case change.removed: {
cursor += change.count;
break;
}
/**
* We should:
* - Push new part to the parts with that new text
*/
case change.added: {
newParts.push(generatePlainTextPart(change.value));
break;
}
/**
* We should concat parts that didn't change.
* - In case when we have only one affected part we should push only that one sub-part
* - In case we have two affected parts we should push first
*/
default: {
if (change.count !== 0) {
newParts = newParts.concat(getPartsInterval(parts, cursor, change.count));
cursor += change.count;
}
break;
}
}
});
return getValueFromParts(newParts);
};
exports.generateValueFromPartsAndChangedText = generateValueFromPartsAndChangedText;
/**
* Method for adding suggestion to the parts and generating value. We should:
* - Find part with plain text where we were tracking mention typing using selection state
* - Split the part to next parts:
* -* Before new mention
* -* With new mention
* -* After mention with space at the beginning
* - Generate new parts array and convert it to value
*
* @param parts - full part list
* @param mentionType - actually the mention type
* @param plainText - current plain text
* @param selection - current selection
* @param suggestion - suggestion that should be added
*/
const generateValueWithAddedSuggestion = (parts, mentionType, plainText, selection, suggestion) => {
var _a;
const currentPartIndex = parts.findIndex(one => selection.end >= one.position.start && selection.end <= one.position.end);
const currentPart = parts[currentPartIndex];
if (!currentPart) {
return;
}
const triggerPartIndex = currentPart.text.lastIndexOf(mentionType.trigger, selection.end - currentPart.position.start);
const newMentionPartPosition = {
start: triggerPartIndex,
end: selection.end - currentPart.position.start,
};
const isInsertSpaceToNextPart = mentionType.isInsertSpaceAfterMention
// Cursor is at the very end of parts or text row
&& (plainText.length === selection.end || ((_a = parts[currentPartIndex]) === null || _a === void 0 ? void 0 : _a.text.startsWith('\n', newMentionPartPosition.end)));
return getValueFromParts([
...parts.slice(0, currentPartIndex),
// Create part with string before mention
generatePlainTextPart(currentPart.text.substring(0, newMentionPartPosition.start)),
generateMentionPart(mentionType, Object.assign({ original: getMentionValue(mentionType.trigger, suggestion), trigger: mentionType.trigger }, suggestion)),
// Create part with rest of string after mention and add a space if needed
generatePlainTextPart(`${isInsertSpaceToNextPart ? ' ' : ''}${currentPart.text.substring(newMentionPartPosition.end)}`),
...parts.slice(currentPartIndex + 1),
]);
};
exports.generateValueWithAddedSuggestion = generateValueWithAddedSuggestion;
/**
* Method for generating part for plain text
*
* @param text - plain text that will be added to the part
* @param positionOffset - position offset from the very beginning of text
*/
const generatePlainTextPart = (text, positionOffset = 0) => ({
text,
position: {
start: positionOffset,
end: positionOffset + text.length,
},
});
exports.generatePlainTextPart = generatePlainTextPart;
/**
* Method for generating part for mention
*
* @param mentionPartType
* @param mention - mention data
* @param positionOffset - position offset from the very beginning of text
*/
const generateMentionPart = (mentionPartType, mention, positionOffset = 0) => {
const text = mentionPartType.getPlainString
? mentionPartType.getPlainString(mention)
: defaultPlainStringGenerator(mentionPartType, mention);
return {
text,
position: {
start: positionOffset,
end: positionOffset + text.length,
},
partType: mentionPartType,
data: mention,
};
};
exports.generateMentionPart = generateMentionPart;
/**
* Generates part for matched regex result
*
* @param partType - current part type (pattern or mention)
* @param result - matched regex result
* @param positionOffset - position offset from the very beginning of text
*/
const generateRegexResultPart = (partType, result, positionOffset = 0) => ({
text: result[0],
position: {
start: positionOffset,
end: positionOffset + result[0].length,
},
partType,
});
/**
* Method for generation mention value that accepts mention regex
*
* @param trigger
* @param suggestion
*/
const getMentionValue = (trigger, suggestion) => `${trigger}[${suggestion.name}](${suggestion.id})`;
exports.getMentionValue = getMentionValue;
const getMentionDataFromRegExMatchResult = ([, original, trigger, name, id]) => ({
original,
trigger,
name,
id,
});
/**
* Recursive function for deep parse MentionInput's value and get plainText with parts
*
* @param value - the MentionInput's value
* @param partTypes - All provided part types
* @param positionOffset - offset from the very beginning of plain text
*/
const parseValue = (value, partTypes, positionOffset = 0) => {
if (value == null) {
value = '';
}
let plainText = '';
let parts = [];
// We don't have any part types so adding just plain text part
if (partTypes.length === 0) {
plainText += value;
parts.push(generatePlainTextPart(value, positionOffset));
}
else {
const [partType, ...restPartTypes] = partTypes;
const regex = isMentionPartType(partType) ? mentionRegEx : partType.pattern;
const matches = Array.from(string_prototype_matchall_1.default(value !== null && value !== void 0 ? value : '', regex));
// In case when we didn't get any matches continue parsing value with rest part types
if (matches.length === 0) {
return parseValue(value, restPartTypes, positionOffset);
}
// In case when we have some text before matched part parsing the text with rest part types
if (matches[0].index != 0) {
const text = value.substr(0, matches[0].index);
const plainTextAndParts = parseValue(text, restPartTypes, positionOffset);
parts = parts.concat(plainTextAndParts.parts);
plainText += plainTextAndParts.plainText;
}
// Iterating over all found pattern matches
for (let i = 0; i < matches.length; i++) {
const result = matches[i];
if (isMentionPartType(partType)) {
const mentionData = getMentionDataFromRegExMatchResult(result);
// Matched pattern is a mention and the mention doesn't match current mention type
// We should parse the mention with rest part types
if (mentionData.trigger !== partType.trigger) {
const plainTextAndParts = parseValue(mentionData.original, restPartTypes, positionOffset + plainText.length);
parts = parts.concat(plainTextAndParts.parts);
plainText += plainTextAndParts.plainText;
}
else {
const part = generateMentionPart(partType, mentionData, positionOffset + plainText.length);
parts.push(part);
plainText += part.text;
}
}
else {
const part = generateRegexResultPart(partType, result, positionOffset + plainText.length);
parts.push(part);
plainText += part.text;
}
// Check if the result is not at the end of whole value so we have a text after matched part
// We should parse the text with rest part types
if ((result.index + result[0].length) !== value.length) {
// Check if it is the last result
const isLastResult = i === matches.length - 1;
// So we should to add the last substring of value after matched mention
const text = value.slice(result.index + result[0].length, isLastResult ? undefined : matches[i + 1].index);
const plainTextAndParts = parseValue(text, restPartTypes, positionOffset + plainText.length);
parts = parts.concat(plainTextAndParts.parts);
plainText += plainTextAndParts.plainText;
}
}
}
// Exiting from parseValue
return {
plainText,
parts,
};
};
exports.parseValue = parseValue;
/**
* Function for generation value from parts array
*
* @param parts
*/
const getValueFromParts = (parts) => parts
.map(item => (item.data ? item.data.original : item.text))
.join('');
exports.getValueFromParts = getValueFromParts;
/**
* Replace all mention values in value to some specified format
*
* @param value - value that is generated by MentionInput component
* @param replacer - function that takes mention object as parameter and returns string
*/
const replaceMentionValues = (value, replacer) => value.replace(mentionRegEx, (fullMatch, original, trigger, name, id) => replacer({
original,
trigger,
name,
id,
}));
exports.replaceMentionValues = replaceMentionValues;
//# sourceMappingURL=utils.js.map