You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

实现文本框中变量作为单个不可分割实体的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

火山引擎 最新活动