在vue3中使用div的contenteditable做了个输入框组件

2,947 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天


因为业务的驱使,需要做一个可编辑的文字组件,即点击就可以编辑,由于之前的同事做的点击之后切换成input的效果看起来不太满意,所以这边我用divcontenteditable属性对组件进行重构

无焦点状态

image.png

焦点状态

image.png


重构过程

到div在contenteditable模式下,内容是html文档而不是纯文字数据,所以进行回显的时候需要使用v-html对保存的内容进行插入

    <div
      ref="editorRef"
      contenteditable
      v-html="title"
    />

只利用div的blur进行数据的修改,因为是实时对数据进行修改的话会导致组件的重复渲染,出现光标错位的问题

<div
  ref="editorRef"
  contenteditable
  v-html="title"
  @blur="handleTitleChange"
/>
<script setup>
const emit = defineEmits(['update:title'])
const handleTitleChange = (e) => {
  context.emit('update:title', e.target.innerHTML)
}
</script>
    

就这样子很简单,其实就完成了一个div的编辑框封装,外部使用方式就是引进组件,然后通过v-model:title来绑定要显示的数据


遇到的棘手问题

输入框的paste事件问题,及复制事件,因为contenteditable模式是html文档内容,复制其他内容过来的时候也会把样式一起复制过来,就会出现内容样式不一致的问题

window.addEventListener('paste', pasteFn)

const pasteFn = (e) => {
  // 先阻止默认的复制事件
  e.preventDefault()
  // 获取复制板的内容
  const paste = (e.clipboardData || window.clipboardData).getData('text/plain')
  // 新建元素标签
  var newNode = document.createElement('span')
  newNode.innerHTML = paste
  // 获取当前光标位置,插入元素
  window.getSelection().getRangeAt(0).insertNode(newNode)
}

通过监听paste 的事件对事件进行重写,只复制内容,不复制样式,就可以避免复制出现的问题

最后附送组件内代码---vue3版本

<template>
  <div :class="['title-warp', `${model}-question`]" :style="{width: `${width}px`}">
    <div class="quill">
      <div class="q-container">
        <div
          :id="keyName"
          ref="editorRef"
          :tabindex="0"
          class="q-editor"
          :style="{fontSize: `${fontSize}px !important`}"
          :contenteditable="model === 'edit'"
          @focus="handleFocus"
          @blur="handleTitleChange"
          v-html="title"
        />
      </div>
    </div>
  </div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
  props: {
    title: {
      type: String,
      default: ''
    },
    fontSize: {
      type: [String, Number],
      default: 14
    },
    width: {
      type: [String, Number],
      default: ''
    },
    keyName: {
      type: String,
      default: ''
    },
    model: {
      type: String,
      default: 'edit'
    }
  },
  setup(props, context) {
    const editorRef = ref()
    const handleTitleChange = (e) => {
      context.emit('update:title', e.target.innerHTML)
      window.removeEventListener('paste', pasteFn)
    }
    const handleFocus = () => {
      window.addEventListener('paste', pasteFn)
    }
    const pasteFn = (e) => {
      e.preventDefault()
      const paste = (e.clipboardData || window.clipboardData).getData('text/plain')
      var newNode = document.createElement('span')
      newNode.innerHTML = paste
      window.getSelection().getRangeAt(0).insertNode(newNode)
    }
    return { editorRef, handleTitleChange, handleFocus }
  }
})
</script>
<style lang="scss" scoped>
.q-editor {
  box-sizing: border-box;
  line-height: 1.42;
  height: 100%;
  outline: none;
  overflow-y: auto;
  tab-size: 4;
  -moz-tab-size: 4;
  padding: 5px 8px;
  text-align: left;
  white-space: pre-wrap;
  word-wrap: break-word;
  border: 1px solid transparent;
  span {
    font-size: 0 !important;
  }
}
.edit-question {
  .q-editor {
    &:focus {
      outline: 0px;
      border: 1px solid rgb(24, 144, 255) !important;
    }
    &:hover {
      border: 1px dashed #aaaaaa;
      transition: all .5s;
    }
  }
}
.title-warp {
  box-sizing: border-box;
  margin: 0px;
  font-variant: tabular-nums;
  list-style: none;
  font-feature-settings: "tnum";
  width: 100%;
  color: rgba(0, 0, 0, 0.65);
  border: 1px solid transparent;
  transition: all 0.3s ease 0s;
  .quill {
    text-align: center;
    .q-container {
      box-sizing: border-box;
      font-family: Helvetica,Arial,sans-serif;
      font-size: 13px;
      height: 100%;
      margin: 0;
      position: relative;
    }
  }
}
</style>

关于作者

一个工作三年,摆烂躺平的前端攻城狮~~~🦁