Textarea-光标-有趣的实践

文章讲述了在textarea中处理光标插入特定格式字符串的需求,包括不允许在已有字符串内插入、整体删除目标字符串等要求。通过获取光标位置、正则匹配目标字符串以及监听键盘事件来实现这些功能。在keydown事件中处理Backspace、Delete、方向键操作,确保符合要求的光标行为。同时,处理点击事件以调整光标位置。文章强调了需求分析、问题解决和边界条件处理的重要性。
摘要由CSDN通过智能技术生成

最近在项目开发中,遇到这样一个需求,在textarea中(这里我们用的ant-design)在光标处插入目标字符串,字符串类似于模板字符串格式,通过${}包裹。

对目标字符串有几点要求:

1、不能在已有的目标字符串中再插入,鼠标点击或者说按键操作(上下左右)使光标在目标字符串范围内,光标要自动定位到目标字符串尾部;

2、删除目标字符串时只能整体删除,例如删除目标字符串中一个字符,需要将整个目标字符串删除,通过鼠标进行范围选取时,要将有交集的目标字符串都删除。

 

接下来,我们就开始处理。

首先,在光标处进行内容插入的话,得知道光标的位置如何获取。 下面我是用ref的形式,获取到标签元素,再得到标签的selectionStart,selectionEnd这两个属性,分别代表光标的开始结束位置,在不进行鼠标批量选择时,这两个通常在一个位置。

/** 获取光标的位置 */
const getInsertIndex = () => { 
  selectionStartIndex.value = (textareaRef.value?.$el as HTMLTextAreaElement)?.selectionStart || 0;
  selectionEndIndex.value = (textareaRef.value?.$el as HTMLTextAreaElement)?.selectionEnd || 0;
};

 在获取到了光标的位置后,插入操作也就简单了,通过模板字符串的形式,进行拼接。

对于2个要求。这个该如何实现呢?
1、首先,得确定目标字符串在整个字符串中下标的范围,我们知道目标字符串有一个特点,就是用${}进行包裹,所以我们可以通过正则+matchAll来获取所有的信息。注意,正则在书写时,$ { }这三个字符需要转义。防止正则贪婪特性在匹配时造成匹配失败问题,需要做一个限制,${和}中间的字符需要排除这三个字符。因为matchAll得到的是伪数组,所以需要转成真正的数组来进行遍历操作。这里我们就得到了所有的信息。(type,key,url是我其它业务逻辑所需参数,可以不关注)

/** 找出光标范围内的变量和短链 */
const findKeyIndex = (target: string) => {
  // eslint-disable-next-line no-useless-escape
  const reg = /\$\{([^\$\{\}])+\}/g;
  const str = target;
  const result = str.matchAll(reg);
  return Array.from(result).map((item) => {
    const { type, key, url } = checkUrlOrLabel(item[0]);
    return {
      start: item.index as number,
      end: item.index as number + item[0].length,
      type, // 短链还是变量
      key, // 短链为uid, 变量为名称
      url,
    };
  });
};

这时得到了textarea中目标字符串的下标信息,根据下标信息处理光标。

监听keydown事件,监听各种按键操作

const keyList = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
const handleDeleteKey = (val: KeyboardEvent) => {
  // 需要处理backspace,delete,上,下,左,右这几个按键
  if (!keyList.includes(val.key)) { // 当不是目标键时,直接放过
    return;
  }
  getInsertIndex();
  // 删除时,若是删除变量和短链
  // 判断删除的内容是否与变量或者短链有交集
  const list = findKeyIndex(props.value);
  if (val.key === 'Backspace') {
    keyBackspace(list);
  } else if (val.key === 'Delete') {
    keyDelete(list);
  } else {
    keyDirection();
  }
};

2、处理鼠标点击

这个我们直接在组件上绑定click事件来处理,首先,得到光标开始结束下标。看是否光标落在目标字符串范围内,在的话就将光标结束位置设置为目标字符串的结束位置;不在则不处理

/** 点击时获取光标的位置,若在变量或者短链范围内 */
const handleClick = () => {
  getInsertIndex();
  const list = findKeyIndex(props.value);
  // eslint-disable-next-line consistent-return
  list.forEach((item) => {
    if (item.start === selectionStartIndex.value) {
      setStartIndex(item.start);
      setEndIndex(item.start);
      return false;
    }
    if (item.start < selectionStartIndex.value && selectionStartIndex.value <= item.end) {
      // 移动光标到变量或者短链结尾处
      setStartIndex(item.end);
      setEndIndex(item.end);
      return false;
    }
  });
};

3、处理Backspace键操作

删除操作的话我们只需要设置光标的开始结束位置,不用额外操作字符串,浏览器会自动删除字符串。这里的逻辑为找到光标范围内有交集的目标字符串集合,再根据集合确定删除内容的范围:

(1)若是result的长度为0,说明没有交集,直接删除

(2)若是result有数据,则取result第一项的start和最后一项的end与光标开始位置和光标结束位置进行大小比较,若是start小于光标开始位置,则将光标开始位置赋值start;同理end也需要处理。(其实这里需要注意result只有一项和有多项的问题,但是一项和多项最后都要回归到start和end的判断处理,所以不用分开处理)

const keyBackspace = (list: Result[]) => {
  const result = list.filter((item) => (
    selectionStartIndex.value >= item.start && selectionStartIndex.value <= item.end)
    || (selectionEndIndex.value >= item.start && selectionEndIndex.value <= item.end)
    || (selectionStartIndex.value <= item.start && selectionEndIndex.value >= item.end));
  if (result.length) { // 说明与短链或者变量有交集
    // 根据光标的开始和结束返回,判断有哪些
    const { start } = result[0];
    const { end } = result[result.length - 1]; // 移动光标,删除相关内容
    if (start < selectionStartIndex.value) {
      setStartIndex(start);
    }
    if (end > selectionEndIndex.value) {
      setEndIndex(end);
    }
  }
};

4、处理Delete键,Delete键有两种情况:

(1)光标开始位置和结束位置一致,此时删除操作为向后删除一个字符

(2)光标开始位置结束位置不一致,此时代表批量选择了,删除操作就是将光标范围内的数据删除。此时的操作就和Baskspace键一致。

所以代码逻辑就要进行判断

const keyDelete = (list: Result[]) => {
  if (selectionStartIndex.value === selectionEndIndex.value) { // 没有进行范围选择,判断光标后一个字符是否在目标范围内
    const nextIndex = selectionStartIndex.value + 1;
    const target = list.find((item) => item.start <= nextIndex && nextIndex <= item.end);
    // 设置光标
    if (target) {
      setEndIndex(target.end);
    }
  } else {
    // 若是范围的话,就和Backsapce按键操作一致
    keyBackspace(list);
  }
};

5、处理上下左右键('ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown')

这个操作的话,就是判断光标移动之后的位置是否在目标字符串范围内。此时的操作就和点击事件一致了。注意点:若是在keydown监听事件直接调用的话,光标位置还没有变化,此时处理就不生效,所以我这加了一个setTimeout,一开始我是使用async await nextTick()方式,但是并不生效,因为组件根本没有更新

const keyDirection = () => {
  setTimeout(() => handleClick(), 0);
};

到这基本要求算是达到了。

总结:虽然这个功能比较简单,但是其中需要考虑的点也是比较多的。其实我在开发中也并不是一下子就可以考虑到所有的点,也是在实践的过程中不断发现完善的。其实这个过程也是我们开发工作的缩影吧。明确需求,拆分需求,边界问题,逐一解决,逐一完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值