env-autosuggest
Version:
A React component that provides variable autosuggestion functionality with a rich text editor-like interface. This component allows users to type variables within curly braces and get intelligent suggestions based on predefined variables.
202 lines (178 loc) • 7.11 kB
JavaScript
export function removeAllPreceedingCurlyBracesFromTextNode(textContent, searchWord) {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
let textFromRemovedSearchedWord = textContent
if (searchWord !== null) {
textFromRemovedSearchedWord = textContent.slice(0, range.startOffset - searchWord?.length) + textContent.slice(range.startOffset)
}
const splitedStr = textContent.slice(0, range.startOffset);
const lastIndexOfBrace = splitedStr.lastIndexOf("{");
let startIndex = splitedStr.lastIndexOf("{");
while (startIndex >= 0 && textFromRemovedSearchedWord[startIndex] === '{') startIndex--;
const textBefore = textFromRemovedSearchedWord.substring(0, startIndex + 1);
const textAfter = textFromRemovedSearchedWord.substring(lastIndexOfBrace + 1);
return { textBefore, textAfter }
}
export function saveCaretPosition(contentEditableDivRef) {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(contentEditableDivRef);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange?.toString()?.length;
};
export function restoreCaretPosition(contentEditableDivRef, savedPosition) {
const selection = window.getSelection();
const range = document.createRange();
let charCount = 0;
const traverseNodes = (node) => {
if (node?.nodeType === Node.TEXT_NODE) {
const nextCharCount = charCount + node?.length;
if (savedPosition <= nextCharCount) {
range.setStart(node, savedPosition - charCount);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
return true;
}
charCount = nextCharCount;
} else {
for (let i = 0; i < node?.childNodes?.length; i++) {
if (traverseNodes(node?.childNodes[i])) {
return true;
}
}
}
return false;
};
traverseNodes(contentEditableDivRef);
};
export function getLeftCharacterBesideCaret() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const { startContainer, startOffset } = range;
if (startContainer?.nodeType === Node.TEXT_NODE && startOffset > 0) {
return startContainer.textContent[startOffset - 1];
} else if (startOffset === 0 && startContainer.previousSibling) {
const previousNode = startContainer.previousSibling;
if (previousNode?.nodeType === Node.TEXT_NODE) {
return previousNode.textContent[previousNode.textContent.length - 1];
} else if (previousNode?.nodeType === Node.ELEMENT_NODE) {
return previousNode.textContent[previousNode.textContent.length - 1];
}
}
}
return null;
}
export function getTextAfterLastOpenCurlyBrace() {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const currentNode = selection.anchorNode;
const splitedStr = currentNode.wholeText?.slice(0, range.startOffset);
const lastOpenBraceIndex = splitedStr?.lastIndexOf("{");
const text = currentNode.textContent?.slice(lastOpenBraceIndex + 1, range.startOffset);
if (lastOpenBraceIndex !== -1) return text
return null;
}
export function removeOuterCurlyBraces(str) {
if (str.startsWith('{{') && str.endsWith('}}')) return str.slice(2, -2);
return str;
}
export function filterSuggestions(searchWord, suggestions) {
const filteredSuggestions = {};
if (!searchWord) return suggestions;
const lowerCaseSearchWord = searchWord?.toLowerCase();
for (const key in suggestions) {
if (key?.toLowerCase()?.includes(lowerCaseSearchWord)) {
filteredSuggestions[key] = suggestions[key];
}
}
if (Object.keys(filteredSuggestions).length === 0) return {};
return filteredSuggestions;
}
export function createNewHTMLForTooltip(suggestions, variableKey) {
return (
`<div class="__tooltip-container__">
<span>Initial</span> : <span>${suggestions[variableKey].initial}</span>
<br />
<span>Current</span> : <span>${suggestions[variableKey].current}</span>
<br />
<span>Scope</span> : <span>${suggestions[variableKey].scope}</span>
</div>`
)
}
export function isEncodedWithCurlyBraces(str) {
const regex = /^\{\{.*\}\}$/;
return regex.test(str);
}
export function extractInnerTextFromHTML(htmlString) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
return doc.body.innerText.trim();
}
export const setDynamicVariables = (contentEditableDivRef) => {
let resultHtmlString = '';
let dynamicVariables = [];
Array.from(contentEditableDivRef.current?.childNodes)?.forEach((node) => {
resultHtmlString += node.outerHTML;
if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute('text-block')) {
let nodeHTML = node.outerHTML;
let reversedHTML = nodeHTML.split('').reverse().join('');
let reverseOpenIndex = reversedHTML.lastIndexOf('{{');
let reverseCloseIndex = reversedHTML.indexOf('}}');
if (reverseOpenIndex !== -1) {
let openIndex = resultHtmlString.length - reverseOpenIndex;
dynamicVariables.push({
type: 'open',
index: openIndex,
});
}
if (reverseCloseIndex !== -1) {
let closeIndex = resultHtmlString.length - reverseCloseIndex;
dynamicVariables.push({
type: 'close',
index: closeIndex,
});
}
}
});
dynamicVariables.sort((a, b) => a.index - b.index);
let startIndex = 0;
let endIndex = dynamicVariables.length - 1;
let openIndex, closeIndex;
while (startIndex <= endIndex) {
const openTag = dynamicVariables[startIndex];
const closeTag = dynamicVariables[endIndex];
if (openTag.type === 'open' && closeTag.type === 'close') {
openIndex = openTag.index;
closeIndex = closeTag.index;
break;
}
if (openTag.type !== 'open') {
startIndex++;
}
if (closeTag.type !== 'close') {
endIndex--;
}
}
if (openIndex > 0 && closeIndex > 0) {
openIndex = openIndex - 2;
let extractedString = resultHtmlString.substring(openIndex, closeIndex);
extractedString = `<span text-block='true'>${extractedString}</span>`;
let innerText = extractInnerTextFromHTML(extractedString);
innerText = `</span><span id='dynamic' variable-block='true'>${innerText}</span><span text-block='true'>`;
resultHtmlString = resultHtmlString.substring(0, openIndex) + innerText + resultHtmlString.substring(closeIndex);
contentEditableDivRef.current.innerHTML = resultHtmlString;
const dynamicSpan = contentEditableDivRef.current.querySelector('#dynamic');
const selection = window.getSelection();
const range = document.createRange();
range.setStartAfter(dynamicSpan);
range.setEndAfter(dynamicSpan);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
dynamicSpan.removeAttribute('id');
}
};