如何为textarea中替换为星号的文本实现Ctrl+Z撤销功能?
解决textarea替换文本后Ctrl+Z撤销失效的问题
嘿,这个问题我碰到过!直接修改textarea的value属性时,浏览器不会把这个操作加入它的原生编辑历史栈,所以默认的Ctrl+Z(或Cmd+Z)就没法识别到要撤销的内容。下面给你两个实用的解决思路:
方法一:利用浏览器原生编辑命令(简单高效)
使用document.execCommand('insertText')来替换选中的文本,这个操作会被浏览器自动记录到撤销栈里,原生撤销快捷键就能正常工作了。
修改后的完整代码
$(document).ready(function () { closePopUp(); var newpost = $('#newpost'); const txtarea = document.getElementById("mytextarea"); // 给替换按钮添加点击事件 newpost.find('div').on('click', function() { // 确保textarea获得焦点 txtarea.focus(); // 生成和选中文本长度一致的星号 const selectedLength = txtarea.selectionEnd - txtarea.selectionStart; const stars = '*'.repeat(selectedLength); // 用原生命令插入星号,浏览器会自动记录撤销步骤 document.execCommand('insertText', false, stars); }); $('#mytextarea').on('select', function (e) { var start = txtarea.selectionStart; var finish = txtarea.selectionEnd; // 只有选中文本时才显示弹窗 if (start !== finish) { newpost.offset(getCursorXY(txtarea, start, 20)).show(); newpost.find('div').text('replace with stars'); } else { newpost.hide(); } }); }); // 你的getCursorXY函数保持不变 const getCursorXY = (input, selectionPoint, offset) => { const { offsetLeft: inputX, offsetTop: inputY } = input; const div = document.createElement('div'); const copyStyle = getComputedStyle(input); for (const prop of copyStyle) { div.style[prop] = copyStyle[prop]; } const swap = '.'; const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value; const textContent = inputValue.substr(0, selectionPoint); div.textContent = textContent; if (input.tagName === 'TEXTAREA') div.style.height = 'auto'; if (input.tagName === 'INPUT') div.style.width = 'auto'; const span = document.createElement('span'); span.textContent = inputValue.substr(selectionPoint) || '.'; div.appendChild(span); document.body.appendChild(div); const { offsetLeft: spanX, offsetTop: spanY } = span; document.body.removeChild(div); return { left: inputX + spanX, top: inputY + spanY + offset, }; }
优点:代码改动极小,完全依赖浏览器原生机制,无需手动维护历史记录。
注意:虽然execCommand在部分文档中标记为“废弃”,但insertText这个命令在现代浏览器中依然被广泛支持,足够满足这个场景的需求。
方法二:手动维护撤销栈(灵活可控)
如果需要更精细的撤销控制(比如多步撤销、自定义撤销逻辑),可以手动维护一个历史状态栈,捕获撤销快捷键来恢复之前的内容。
修改后的完整代码
$(document).ready(function () { closePopUp(); var newpost = $('#newpost'); const txtarea = document.getElementById("mytextarea"); // 初始化撤销栈,保存每次修改前的文本和光标状态 const undoStack = []; // 保存当前状态到撤销栈 function saveUndoState() { undoStack.push({ value: txtarea.value, selectionStart: txtarea.selectionStart, selectionEnd: txtarea.selectionEnd }); // 限制栈的大小,避免内存占用过高 if (undoStack.length > 50) { undoStack.shift(); } } // 替换文本的逻辑,替换前先保存状态 newpost.find('div').on('click', function() { saveUndoState(); const start = txtarea.selectionStart; const finish = txtarea.selectionEnd; const stars = '*'.repeat(finish - start); // 直接替换文本 txtarea.value = txtarea.value.slice(0, start) + stars + txtarea.value.slice(finish); // 恢复光标到替换后的位置 txtarea.selectionStart = txtarea.selectionEnd = start + stars.length; txtarea.focus(); }); // 监听撤销快捷键 txtarea.addEventListener('keydown', function(e) { // 兼容Windows的Ctrl+Z和Mac的Cmd+Z if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); // 阻止默认行为,避免冲突 if (undoStack.length > 0) { const lastState = undoStack.pop(); // 恢复之前的文本和光标状态 txtarea.value = lastState.value; txtarea.selectionStart = lastState.selectionStart; txtarea.selectionEnd = lastState.selectionEnd; txtarea.focus(); } } }); $('#mytextarea').on('select', function (e) { var start = txtarea.selectionStart; var finish = txtarea.selectionEnd; if (start !== finish) { newpost.offset(getCursorXY(txtarea, start, 20)).show(); newpost.find('div').text('replace with stars'); } else { newpost.hide(); } }); }); // 你的getCursorXY函数保持不变 const getCursorXY = (input, selectionPoint, offset) => { const { offsetLeft: inputX, offsetTop: inputY } = input; const div = document.createElement('div'); const copyStyle = getComputedStyle(input); for (const prop of copyStyle) { div.style[prop] = copyStyle[prop]; } const swap = '.'; const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value; const textContent = inputValue.substr(0, selectionPoint); div.textContent = textContent; if (input.tagName === 'TEXTAREA') div.style.height = 'auto'; if (input.tagName === 'INPUT') div.style.width = 'auto'; const span = document.createElement('span'); span.textContent = inputValue.substr(selectionPoint) || '.'; div.appendChild(span); document.body.appendChild(div); const { offsetLeft: spanX, offsetTop: spanY } = span; document.body.removeChild(div); return { left: inputX + spanX, top: inputY + spanY + offset, }; }
优点:完全自定义撤销逻辑,支持多步撤销,不受浏览器原生机制限制。
注意:需要自己处理历史状态的保存和恢复,代码量稍大。
内容的提问来源于stack exchange,提问作者anub
相关产品推荐
相关产品推荐

