div用contenteditable属性写一个输入框且敏感词显示

需求:输入框中有输入内容,如有敏感词则需要标记

下面这个也可以,有用点个赞

el-input输入框敏感词关键词高亮标红_el-input 内容高亮显示-CSDN博客

直接给代码

ContentEditable.vue

复制代码
<template>
  <div
    ref="editableDiv"
    contenteditable="true"
    @input="handleInput"
    @keydown="handleKeyDown"
    @compositionstart="handleCompositionStart"
    @compositionend="handleCompositionEnd"
    :class="['editable-box ', { empty: true }]"
    :data-placeholder="placeholder"
  ></div>
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      default: ""
    },
    placeholder: {
      type: String,
      default: "请输入内容"
    },
    keywords: {
      type: Array,
      default: () => []
    }
  },

  data: () => ({
    lastHTML: "",
    isComposing: false,
    isFocused: false
  }),

  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        console.log(newVal, this.$refs?.editableDiv?.innerHTML)
        if (newVal !== this.cleanContent(this.$refs?.editableDiv?.innerHTML)) {
          this.$nextTick(() => {
            this.safeUpdate(newVal)
          })
        }
      }
    }
  },

  mounted() {
    this.$refs.editableDiv.addEventListener("focus", () => (this.isFocused = true))
    this.$refs.editableDiv.addEventListener("blur", () => (this.isFocused = false))
  },

  methods: {
    // 安全更新内容(带光标保护)
    safeUpdate(newVal) {
      if (!this.$refs.editableDiv) return
      const selection = window.getSelection()
      const hadFocus = this.isFocused
      const { range, offset } = this.saveCursorPosition()

      // 更新内容
      this.lastHTML = this.highlightKeywords(newVal)
      if (this.$refs.editableDiv) {
        this.$refs.editableDiv.innerHTML = this.lastHTML
      }

      // 恢复光标
      if (hadFocus) {
        this.$nextTick(() => {
          this.restoreCursorPosition(range, offset)
          if (!this.isFocused) this.$refs.editableDiv.focus()
        })
      }
    },

    // 智能光标保存
    saveCursorPosition() {
      const sel = window.getSelection()
      if (!sel.rangeCount) return { range: null, offset: 0 }

      const range = sel.getRangeAt(0)
      const preRange = range.cloneRange()
      preRange.selectNodeContents(this.$refs.editableDiv)
      preRange.setEnd(range.startContainer, range.startOffset)
      return {
        range: range,
        offset: preRange.toString().length
      }
    },

    // 精准光标恢复
    restoreCursorPosition(originalRange, targetOffset) {
      const walker = document.createTreeWalker(this.$refs.editableDiv, NodeFilter.SHOW_TEXT, null, false)

      let cumulative = 0
      let targetNode = null
      let targetPos = 0

      while (walker.nextNode()) {
        const node = walker.currentNode
        const len = node.textContent.length

        if (cumulative + len >= targetOffset) {
          targetNode = node
          targetPos = targetOffset - cumulative
          break
        }
        cumulative += len
      }

      const range = document.createRange()
      if (targetNode) {
        range.setStart(targetNode, Math.min(targetPos, targetNode.textContent.length))
      } else {
        range.selectNodeContents(this.$refs.editableDiv)
        range.collapse(false)
      }
      range.collapse(true)

      const sel = window.getSelection()
      sel.removeAllRanges()
      sel.addRange(range)
    },
    removeSpaces(text) {
      return text.replace(/\s/g, "")
    },

    // 安全更新内容(保持光标位置)
    updateContent(newText) {
      const el = this.$refs.editableDiv
      const selection = window.getSelection()
      const range = selection.getRangeAt(0)

      // 保存光标状态
      const anchorNode = range.startContainer
      const anchorOffset = range.startOffset

      // 更新内容
      el.textContent = newText

      // 恢复光标
      const newRange = document.createRange()
      newRange.setStart(anchorNode, Math.min(anchorOffset, newText.length))
      newRange.collapse(true)

      selection.removeAllRanges()
      selection.addRange(newRange)
    },
    handleInput(e) {
      if (this.isComposing) return
      const originalText = e.target.textContent
      const processedText = this.removeSpaces(originalText)
      if (processedText === this.lastHTML) return

      if (originalText !== processedText) {
        this.$nextTick(() => {
          this.updateContent(processedText)
        })
      }
      const cleanText = this.cleanContent(processedText)
      this.lastHTML = processedText
      this.$emit("input", cleanText)
      this.safeUpdate(cleanText)
    },
    // 输入处理(优化防抖)
    handleInput1(e) {
      if (this.isComposing) return
      const currentHTML = e.target.innerHTML
      if (currentHTML === this.lastHTML) return
      const cleanText = this.cleanContent(currentHTML)
      this.lastHTML = currentHTML
      this.$emit("input", cleanText)
      this.safeUpdate(cleanText)
    },

    // 关键词高亮处理
    highlightKeywords(text) {
      const div = document.createElement("div")
      div.textContent = text
      const rawText = div.innerHTML

      return this.keywords.reduce((html, keyword) => {
        const escaped = this.escapeRegExp(keyword)
        const regex = new RegExp(`(${escaped})`, "gi")
        return html.replace(regex, '<mark class="keyword-highlight">$1</mark>')
      }, rawText)
    },

    // 清理HTML标签
    cleanContent(html) {
      const div = document.createElement("div")
      div.innerHTML = html
      return div.textContent
    },

    // 转义正则特殊字符
    escapeRegExp(str) {
      return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
    },

    // 中文输入法处理
    handleCompositionStart() {
      this.isComposing = true
    },

    handleCompositionEnd() {
      this.isComposing = false
      this.handleInput({ target: this.$refs.editableDiv })
    },

    // 按键处理(防止回车换行)
    handleKeyDown(e) {
      if (e.key === "Enter") {
        e.preventDefault()
        this.$emit("enter")
      }
      if (e.key === " " || e.keyCode === 32) {
        e.preventDefault()
        return false
      }
    }
  }
}
</script>

<style>
.editable-box {
  min-height: 40px;
  padding: 8px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  line-height: 1.5;
  text-align: left;
  white-space: pre-wrap;
  outline: none;

  &:focus {
    border-color: #409eff;
    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
  }

  &.empty:empty::before {
    content: attr(data-placeholder);
    color: #c0c4cc;
    pointer-events: none;
  }
}

.keyword-highlight {
  background-color: #fff3d8;
  padding: 0 2px;
  border-radius: 2px;
}
</style>

父组件调用

复制代码
<template>
  <div>
    <!-- 组件使用 -->
    <ContentEditable
      v-model="content"
      :keywords="['Vue', 'Element UI']"
      placeholder="输入包含关键词的内 容"
      @enter="submit"
    />

    <!-- 显示原始文本 -->
    <div>当前内容:{{ content }}</div>
  </div>
</template>

<script>
import ContentEditable from "../components/ContentEditable.vue"
export default {
  name: "App",
  components: { ContentEditable },
  filters: {},
  data() {
    return {
      content: "尝试输入包含Vue或Element UI的文本",
      keywords: ["Vue", "Element UI"]
    }
  },
  computed: {},
  watch: {},
  methods: {
    submit() {}
  }
}
</script>
<style></style>
相关推荐
阿珊和她的猫2 小时前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
fouryears_234174 小时前
Flutter InheritedWidget 详解:从生命周期到数据流动的完整解析
开发语言·flutter·客户端·dart
我好喜欢你~5 小时前
C#---StopWatch类
开发语言·c#
加班是不可能的,除非双倍日工资6 小时前
css预编译器实现星空背景图
前端·css·vue3
lifallen6 小时前
Java Stream sort算子实现:SortedOps
java·开发语言
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi7 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip7 小时前
vite和webpack打包结构控制
前端·javascript
excel7 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
cui__OaO8 小时前
Linux软件编程--线程
linux·开发语言·线程·互斥锁·死锁·信号量·嵌入式学习