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 分钟前
组件通信与设计模式
前端
冴羽5 分钟前
SvelteKit 最新中文文档教程(10)—— 部署 Cloudflare Pages 和 Cloudflare Workers
前端·javascript·svelte
fridayCodeFly8 分钟前
v-form标签里的:rules有什么作用。如何定义。
前端·javascript·vue.js
前端_学习之路17 分钟前
axios--源码解析
java·开发语言·javascript·ajax
xixixin_18 分钟前
【uniapp】内容瀑布流
java·前端·uni-app
计算机毕设定制辅导-无忧学长24 分钟前
响应式 Web 设计:HTML 与 CSS 协同学习的进度(二)
前端·css·html
yzp011225 分钟前
html方法收集
前端·javascript·html
paradoxaaa_28 分钟前
VUE2导出el-table数据为excel并且按字段分多个sheet
javascript·vue.js·excel
Flower#29 分钟前
C . Serval and The Formula【Codeforces Round 1011 (Div. 2)】
c语言·开发语言·c++·算法
martian66542 分钟前
Java高并发容器的内核解析:从无锁算法到分段锁的架构演进
java·开发语言