I have created a simple text editor with a "replace all" feature. It's meant to be part of a larger project, so it needs to be robust and work reliably across various scenarios. Please review the code for quality, error handling, and any potential edge cases I might have missed.
const editor = document.getElementById('editor');
const findInput = document.getElementById('findInput');
const replaceInput = document.getElementById('replaceInput');
const matchCaseToggle = document.getElementById('matchCaseToggle');
const wholeWordToggle = document.getElementById('wholeWordToggle');
const regexModeToggle = document.getElementById('regexModeToggle');
const replaceButton = document.getElementById('replaceButton');
const undoButton = document.getElementById('undoButton');
const replaceFeedback = document.getElementById('replaceFeedback');
let previousEditorValue = null;
// Interpret some common escape sequences in the replacement string
function interpretEscapeSequences(str) {
const escapeMap = {
'\\n': '\n', // Newline
'\\r': '\r', // Carriage return
'\\t': '\t', // Tab
'\\\\': '\\', // Backslash
};
return str.replace(/\\[nrt\\]/g, match => escapeMap[match]);
}
function isActive(button) {
return button.classList.contains('active');
}
function deactivate(button) {
button.classList.remove('active');
}
function updatePlaceholder() {
findInput.placeholder = isActive(regexModeToggle) ? 'pattern or /pattern/flags' : '';
}
function clearFeedback() {
if (replaceFeedback.value) {
replaceFeedback.value = '';
}
}
function clearUndoState() {
if (previousEditorValue !== null) {
previousEditorValue = null;
undoButton.disabled = true;
}
}
for (const textField of [editor, findInput, replaceInput]) {
textField.addEventListener('input', function() {
clearFeedback();
if (this === editor) {
clearUndoState();
}
});
}
for (const button of document.querySelectorAll('.toggle')) {
button.addEventListener('click', function() {
this.classList.toggle('active');
if (this === matchCaseToggle || this === wholeWordToggle) {
deactivate(regexModeToggle);
} else if (this === regexModeToggle) {
deactivate(matchCaseToggle);
deactivate(wholeWordToggle);
}
updatePlaceholder();
clearFeedback();
});
}
replaceButton.addEventListener('click', function() {
const currentEditorValue = editor.value;
const findInputValue = findInput.value;
const replaceInputValue = replaceInput.value;
const isCaseSensitive = isActive(matchCaseToggle);
const isWholeWord = isActive(wholeWordToggle);
const isRegex = isActive(regexModeToggle);
if (!findInputValue) {
replaceFeedback.value = 'No find term';
return;
}
// Prepare pattern and flags for RegExp constructor based on find input and options
let pattern;
let flags = 'g'; // Global flag is always needed to replace all
let regex;
if (isRegex) {
const regexLiteralMatch = findInputValue.match(/^\/(.*)\/(.*)$/);
if (regexLiteralMatch) {
[, pattern, flags] = regexLiteralMatch;
if (!flags.includes('g')) {
flags += 'g';
}
} else {
pattern = findInputValue;
}
} else {
pattern = findInputValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (isWholeWord) {
// Unicode-aware word boundaries (mimics \b) with 'u' flag for non-English characters
pattern = `(?<![\\p{L}\\p{N}_])${pattern}(?![\\p{L}\\p{N}_])`;
flags += 'u';
}
if (!isCaseSensitive) {
flags += 'i';
}
}
try {
regex = new RegExp(pattern, flags);
} catch (error) {
replaceFeedback.value = error.message;
return;
}
const matches = currentEditorValue.match(regex);
if (matches) {
const matchCount = matches.length;
previousEditorValue = currentEditorValue;
// Use function replacement in non-regex mode to avoid interpreting $ patterns
editor.value = currentEditorValue.replace(regex, isRegex ? interpretEscapeSequences(replaceInputValue) : () => replaceInputValue);
replaceFeedback.innerHTML = `<span class="number">${matchCount}</span> ${matchCount === 1 ? 'replacement' : 'replacements'}`;
undoButton.disabled = false;
} else {
replaceFeedback.value = 'No matches';
}
});
undoButton.addEventListener('click', function() {
editor.value = previousEditorValue;
replaceFeedback.value = 'Replacement undone';
clearUndoState();
});
#editor {
width: 100%;
height: 100px;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0;
}
#replaceFeedback {
color: #666;
min-height: 1.2em;
}
.findOptions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.toggle {
padding: 0.25rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
background: #f0f0f0;
cursor: pointer;
transition: all 0.2s;
}
.toggle.active {
background: #007bff;
color: white;
border-color: #007bff;
}
#replaceButton {
background: #28a745;
color: white;
}
.number {
font-family: monospace;
}
<textarea id="editor"></textarea>
<div class="controls">
<label for="findInput">Find</label>
<input type="text" id="findInput">
<label for="replaceInput">Replace</label>
<input type="text" id="replaceInput">
<div class="findOptions">
<button type="button" class="toggle" id="matchCaseToggle">Match Case</button>
<button type="button" class="toggle" id="wholeWordToggle">Whole Word</button>
<button type="button" class="toggle" id="regexModeToggle">Regex</button>
</div>
<button type="button" id="replaceButton">Replace All</button>
<button type="button" id="undoButton" disabled>Undo</button>
</div>
<output for="editor findInput matchCaseToggle wholeWordToggle regexModeToggle" id="replaceFeedback"></output>