前端开发:记录一次自定义输入框的经历

1 篇文章 0 订阅
1 篇文章 0 订阅

前言:因项目需要,输入框内需要输入文字和带样式的文字,首先考虑使用现成的富文本编辑器,发现不满足需求,且富文本编辑器功能太多,项目只需要能输入带样式的文字即可,因此在此基础上修改太过冗余。通过搜索发现,可以将div标签设置contenteditable属性,将普通的标签变成输入框。因此,开启自定义输入框的旅程~

先看最终的成果:
在这里插入图片描述上述输入框原始文本内容为

<span class="tag-pause" contenteditable="false">2秒</span>你好<span class="tag-liaison" contenteditable="false">连读</span><span class="tag-breath" contenteditable="false">换气</span><span class="tag-action" contenteditable="false">左手向左上介绍</span>

处理后

[p2000]你好,<动作:左手向左上介绍>

接下来是开发步骤:

首先,定义div标签
1)v-html 绑定富文本内容
2)contenteditable 将div标签置为可编辑标签
3)@input 输入监听
4)@click 点击输入框监听

<div
    class="custom-input"
    id="customInput"
    ref="customInputRef"
    v-html="textHtml"
    placeholder="请输入需要虚拟主播播报的内容……"
    contenteditable
    @input="inputText"
    @click="handleCustomInputClick">
</div>

然后,默认将光标设置到该标签上,并且记录当前光标

this.$nextTick(() => {
    // 初始化光标
    this.initSelection();
});
// 初始化光标
initSelection() {
    let input = document.getElementById("customInput");
    var selection = window.getSelection();
    let range = document.createRange();
    range.setStart(input, 0);
    range.setEnd(input, 0);
    selection.removeAllRanges();
    selection.addRange(range);
    this.lastRange = selection.getRangeAt(0);
}

点击输入区域,同样需要记录光标位置

// 点击输入区域,记录光标位置
handleCustomInputClick() {
    let selection = window.getSelection();
    this.lastRange = selection.getRangeAt(0);
}

输入普通文本,监听input回调(@input=“inputText”),记录输入时的光标

// 监听输入框内容
inputText() {
    let selection = window.getSelection();
    this.lastRange = selection.getRangeAt(0);
}

插入标签

// 插入标签
insertTag(text, className) {
    // 不能连续添加动作
    if (className == "tag-action") {
        const node = this.getAdjacentNodeClasses(this.$refs.customInputRef);
        if (node) {
            if (node.before == "tag-action" || node.after == "tag-action") {
                this.$message.warning("不能连续添加动作");
                return;
              }
        }
    }
    //插入标签
    setTimeout(() => {
        // 输入框获取焦点
        let input = document.getElementById("customInput");
        input.focus();
        // 获取光标位置
        let selection = window.getSelection();
        if (this.lastRange) {
          selection.removeAllRanges();
          selection.addRange(this.lastRange);
        }
        let range = selection.getRangeAt(0);
        // 创建span标签,设置标签内部文本、class名称
        const span = document.createElement("span");
        span.innerHTML = text;
        span.className = className;// 标签样式通过设置的class设置
        span.setAttribute("contenteditable", false);
        // 插入标签节点到输入框,并将光标设置到标签节点后
        range.insertNode(span);
        range.setStartAfter(span);
        range.setEndAfter(span);
        // 记录光标位置
        selection.removeAllRanges();
        selection.addRange(range);
        this.lastRange = selection.getRangeAt(0);
    }, 100);
}



其中,插入标签时要求不能连续插入动作标签,因此,当检测到标签class名称为"tag-action"时,需要判断光标前后的内容是否为动作标签。

// 获取光标前后紧邻的节点className
getAdjacentNodeClasses(editableElement) {
    editableElement.focus();
    let selection = window.getSelection();
    if (this.lastRange) {
        selection.removeAllRanges();
        selection.addRange(this.lastRange);
    }
    let range = selection.getRangeAt(0);
    if (
        !selection.rangeCount ||
        !selection.getRangeAt(0).intersectsNode(editableElement)
    ) {
        return { before: null, after: null };
    }
    let beforeNode = null;
    let afterNode = null;
    // 尝试获取光标前的节点
    if (range.startOffset > 0) {
        let currentNode = range.startContainer;
        let offset = range.startOffset;
        // 如果当前节点是文本节点
        if (currentNode.nodeType === Node.ELEMENT_NODE) {
          let childNodes = currentNode.childNodes;
          if (childNodes && childNodes.length >= offset) {
            beforeNode = childNodes[offset - 1];
          } else {
            beforeNode = null;
          }
        }
      }

    // 尝试获取光标后的节点
    if (
        range.endOffset <
        (range.endContainer.nodeType === Node.TEXT_NODE
          ? range.endContainer.nodeValue.length
          : range.endContainer.childNodes.length)
      ) {
        let currentNode = range.endContainer;
        let offset = range.endOffset;

        // 如果当前节点是文本节点
        if (currentNode.nodeType === Node.ELEMENT_NODE) {
          let childNodes = currentNode.childNodes;
          if (childNodes && childNodes.length > offset) {
            afterNode = childNodes[offset];
          } else {
            afterNode = null;
          }
        }
    }

    // 返回前后节点的class(如果存在)
    return {
        before:
          beforeNode && beforeNode.nodeType === Node.ELEMENT_NODE
            ? beforeNode.className
            : null,
        after:
          afterNode && afterNode.nodeType === Node.ELEMENT_NODE
            ? afterNode.className
            : null,
      };
}

当选择无动作时,需要将输入框内所有的动作标签都清除

// 清除动作标签
removeActionTag() {
    // 选择所有具有类名tag-action的span元素
    var elements = document.querySelectorAll("span.tag-action");
    // 遍历并移除这些元素
    elements.forEach(function (element) {
        element.remove();
    });
}

获取输入框内的内容

let text = this.$refs.customInputRef.innerHTML

通过上面这种方式获取的输入框内容是原生的带标签样式的,最终我们需要将其处理成想要的内容。在这个项目中,需要做如下处理:
1)连读标签所在的地方需删除它附近的标点符号。
2)换气标签处理成逗号。
3)停顿标签处理成对应的值。

extractTextFromHTML(htmlString) {
      if (htmlString === undefined) {
        return "";
      }
      // 创建一个临时的DOM元素来容纳HTML字符串
      var tempDiv = document.createElement("div");
      tempDiv.innerHTML = htmlString;

      // 使用TreeWalker遍历DOM树(可选,但这里我们可以简单地使用childNodes)
      var textContent = "";
      var nodes = tempDiv.childNodes;
      for (var i = 0; i < nodes.length; i++) {
        if (nodes[i].nodeType === Node.TEXT_NODE) {
          // 只处理文本节点
          textContent += nodes[i].nodeValue;
        } else if (nodes[i].nodeType === Node.ELEMENT_NODE) {
          if (nodes[i].innerHTML == "连读") {
            textContent += '<span class="tag-liaison" contenteditable="false"></span>';
          } else if (nodes[i].innerHTML == "换气") {
            textContent += '<span class="tag-breath" contenteditable="false"></span>';
          } else if (nodes[i].className == "tag-pause") {
            // 停顿处理为对应的value
            textContent += this.convertPauseTime(nodes[i].innerHTML);
          } else if (nodes[i].className == "tag-action") {
            // 动作处理为<动作:动作名称>
            textContent += "<动作:" + nodes[i].innerHTML + ">";
          }
        }
      }
      // 先删除连读标签附近的标点符号
      textContent = this.removePunctuationAroundTagLiaison(textContent.trim());
      // 再将换气标签处理为逗号
      textContent = textContent.replace(
        /<span class="tag-breath" contenteditable="false">(.*?)<\/span>/g,
        ","
      );
      console.log("删除处理后", textContent);

      // 返回去除HTML标签后的文本内容
      return textContent; // 使用trim()去除可能的首尾空白字符
},
convertPauseTime(label) {
    const dict = this.pauseTimeOptions.find((item) => item.label === String(label));
    return dict !== undefined ? dict.value : "";
},
removePunctuationAroundTagLiaison(text) {
    // 正则表达式匹配 class="tag-liaison" 的 span 标签及其前后的标点符号
    // 注意:这个正则表达式假设标点符号是直接与 span 标签相邻的,或者通过空白字符分隔
    const regex = /([^\w\s<>]*)<span class="tag-liaison" contenteditable="false">(.*?)<\/span>([^\w\s<>]*)/g;
    // 定义要删除的标点符号集合
    const punctuation = /[, , 、。. ! !]/g;
    // 替换匹配的文本,删除 span 标签前后的标点符号
    let result = text.replace(regex, (match, prefix, content, suffix) => {
        // 去除 prefix 和 suffix 中的标点符号
        prefix = prefix.replace(punctuation, "");
        suffix = suffix.replace(punctuation, "");
        // 去除 content 两侧的空白字符(如果需要)
        content = content.trim();
        // 返回处理后的文本
        return prefix + content + suffix;
    });
    return result;
}

const PAUSE_TIME_OPTIONS = [
    {
        label: "0.5秒",
        value: "[p500]",
    },
    {
        label: "1秒",
        value: "[p1000]",
    },
    {
        label: "2秒",
        value: "[p2000]",
    },
];

清空输入框内的内容

this.$refs.customInputRef.innerHTML = "";

以上,就是自定义输入框的全过程。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值