实现文本框中变量作为单个不可分割实体的JavaScript方案咨询
实现文本框中变量作为单个不可分割实体的JavaScript方案咨询
嘿,这个需求我之前做模板编辑器的时候碰到过,其实不用纠结什么高大上的术语,咱们直接把这种需求叫做「将模板变量视为单个不可拆分的编辑单元」就行,实现起来核心就是监听文本框的编辑事件,针对性拦截或调整操作逻辑。我给你捋捋具体的实现思路和代码示例:
核心需求拆解
咱们要实现的效果总结下来就是三点:
- 变量(比如
${productName})不能被拆开来编辑,不能在中间插字符、删单个字符 - 光标紧贴变量边缘时(右侧按Backspace、左侧按Delete),一键删除整个变量
- 就算选中变量的一部分,按删除键也得删掉整个变量
具体实现步骤&代码
我以最常用的<textarea>为例来写示例,单行<input>的逻辑是一样的,只是少了换行处理:
1. HTML结构
先整个基础的文本框:
<textarea id="templateEditor" rows="4" cols="50">The name of the product is ${productName}</textarea>
2. JavaScript核心逻辑
const editor = document.getElementById('templateEditor'); // 匹配${变量名}的正则,变量名允许字母、数字、下划线 const variablePattern = /\$\{[a-zA-Z0-9_]+\}/g; // 保存上一次的有效内容,用来兜底恢复 let lastValidContent = editor.value; // 辅助函数:判断当前光标/选中区域是否关联某个变量 function getTargetVariable() { const content = editor.value; const startPos = editor.selectionStart; const endPos = editor.selectionEnd; let match; // 遍历所有匹配到的变量 while ((match = variablePattern.exec(content)) !== null) { const varStart = match.index; const varEnd = varStart + match[0].length; const varText = match[0]; // 几种需要处理的情况: // 1. 光标紧贴变量右侧(}后面) // 2. 光标紧贴变量左侧($前面) // 3. 光标落在变量内部 // 4. 选中了变量的一部分 const isNearEdge = startPos === varEnd || startPos === varStart; const isInsideVar = startPos > varStart && startPos < varEnd; const isPartialSelect = startPos > varStart && endPos < varEnd; if (isNearEdge || isInsideVar || isPartialSelect) { return { start: varStart, end: varEnd, text: varText }; } } return null; } // 监听键盘事件:处理退格、删除、输入拦截 editor.addEventListener('keydown', (e) => { const targetVar = getTargetVariable(); if (!targetVar) return; const { key } = e; const { start: varStart, end: varEnd } = targetVar; const isFullSelect = editor.selectionStart === varStart && editor.selectionEnd === varEnd; // 处理退格键:光标在变量右侧或内部/部分选中时,删除整个变量 if (key === 'Backspace') { if (editor.selectionStart === varEnd || !isFullSelect) { e.preventDefault(); // 拼接删除变量后的新内容 const newContent = editor.value.slice(0, varStart) + editor.value.slice(varEnd); editor.value = newContent; // 把光标定位到变量原来的起始位置 editor.selectionStart = editor.selectionEnd = varStart; lastValidContent = newContent; return; } } // 处理删除键:光标在变量左侧或内部/部分选中时,删除整个变量 if (key === 'Delete') { if (editor.selectionStart === varStart || !isFullSelect) { e.preventDefault(); const newContent = editor.value.slice(0, varStart) + editor.value.slice(varEnd); editor.value = newContent; editor.selectionStart = editor.selectionEnd = varStart; lastValidContent = newContent; return; } } // 阻止在变量内部输入任何字符(方向键除外,允许光标移动) const allowedKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']; if (!allowedKeys.includes(key) && editor.selectionStart > varStart && editor.selectionStart < varEnd) { e.preventDefault(); } }); // 监听粘贴事件:防止把内容粘到变量内部 editor.addEventListener('paste', (e) => { const targetVar = getTargetVariable(); if (targetVar && editor.selectionStart > targetVar.start && editor.selectionStart < targetVar.end) { e.preventDefault(); } }); // 兜底的input事件:防止某些绕过keydown的输入方式(比如拖拽文本) editor.addEventListener('input', () => { // 检查当前内容里的变量是否都是完整且未被修改的 const currentVars = [...editor.value.matchAll(variablePattern)].map(m => m[0]); const lastVars = [...lastValidContent.matchAll(variablePattern)].map(m => m[0]); // 如果变量数量不对或者内容被篡改,恢复到上一次的有效状态 if (currentVars.length !== lastVars.length || !currentVars.every((v, i) => v === lastVars[i])) { editor.value = lastValidContent; } else { lastValidContent = editor.value; } });
额外优化建议
- 如果你的变量名允许特殊字符(比如连字符、中文),直接调整
variablePattern正则里的字符集就行 - 要是用的是
contenteditable的富文本框,核心逻辑不变,但获取光标位置的方式要换成Range和Selection API - 可以加个小优化:当光标在变量左侧按右箭头时,直接跳到变量右侧;在右侧按左箭头直接跳到左侧,让变量的「单个实体」感更强
备注:内容来源于stack exchange,提问作者Archit Arora




