I've created a basic text editor with a "Replace all" feature. However, I'm facing an issue where the output indicates replacements were made even when the text remains unchanged. Here's a simplified version of my code:
const textarea = document.getElementById('textarea');
const searchInput = document.getElementById('search');
const replaceInput = document.getElementById('replace');
const output = document.getElementById('output');
const replaceButton = document.getElementById('replaceAll');
replaceButton.addEventListener('click', () => {
const searchValue = searchInput.value;
const replaceValue = replaceInput.value;
if (!searchValue) {
output.textContent = 'No search term';
return;
}
const regex = new RegExp(searchValue, 'g');
const matches = textarea.value.match(regex);
if (matches) {
const count = matches.length;
textarea.value = textarea.value.replace(regex, replaceValue);
output.textContent = `${count} replacement${count === 1 ? '' : 's'}`;
} else {
output.textContent = 'No matches';
}
});
textarea {
width: 100%;
height: 100px;
}
#output {
color: #666;
}
<textarea id="textarea">apple orange apple orange</textarea>
<input type="text" id="search" placeholder="Search">
<input type="text" id="replace" placeholder="Replace">
<button id="replaceAll">Replace all</button>
<div id="output"></div>
How can I modify the code to correctly handle these edge cases and display "No replacement" when the text remains unchanged after the replacement operation?
One way to do this is using a replacer function instead of a replacement template:
const replaceAndCount = (str, pattern, replacer) => {
let changes = 0;
const result = str.replace(pattern, (...args) => {
const replacement = replacer(...args);
if (replacement !== args[0]) {
changes++;
}
return replacement;
});
return {result, changes};
};
console.log(replaceAndCount("Hello", /[^aeiou]/gi, letter => letter.toUpperCase()));
But then you have to convert replacement templates to replacer functions. For example, here’s an (untested) implementation that’s compatible with the JavaScript default:
const toReplacer = template => {
const namedGroupParts = template.split(/(\$(?:[$&`']|\d{1,2}|<[^>]*>))/);
const noNamedGroupParts = template.split(/(\$(?:[$&`']|\d{1,2}))/);
return (...args) => {
let string = args.pop();
let groups = undefined;
// A final `groups` argument may or may not be present, depending on whether the regex has named groups.
if (typeof string === 'object') {
groups = string;
string = args.pop();
}
const index = args.pop();
const parts = groups != null ? namedGroupParts : noNamedGroupParts;
let result = parts[0];
for (let i = 1; i < parts.length; i += 2) {
switch (parts[i].charAt(1)) {
case '$':
result += '$';
break;
case '&':
result += args[0];
break;
case '`':
result += string.substring(0, index);
break;
case "'":
result += string.substring(index + args[0].length);
break;
case '<':
const groupName = parts[i].slice(2, -1);
if (Object.hasOwn(groups, groupName)) {
result += groups[groupName] ?? '';
}
break;
default:
const groupIndex = parts[i].substring(1);
let gi = Number(groupIndex);
if (1 <= gi && gi < args.length) {
result += args[gi];
break;
}
if (groupIndex.length === 2) {
gi = Number(groupIndex.charAt(0));
if (1 <= gi && gi < args.length) {
result += args[gi] + groupIndex.charAt(1);
break;
}
}
result += parts[i];
}
result += parts[i + 1];
}
return result;
};
};
console.log('foo bar baz'.replace(/ba./g, '($&)'));
console.log('foo bar baz'.replace(/ba./g, toReplacer('($&)')));
(If you drop strict compatibility in edge cases like $12
being interpreted as $012
depending on the actual number of groups, you can simplify it a lot.)
As a side note, on the off chance that this behavior change isn’t motivated by any other problem: existing text editors typically don’t special-case this, and exhibit your original behavior. It’s worth considering whether you need to change it at all.