vue2使用wangeditor编辑器图文直接粘贴

235 阅读5分钟

第二次写文章,如有错误请指正,多多担待,菜鸟一枚,话不多说,直接开始;

摘要:

本文主要解决wangeditor富文本编辑器不支持直接粘贴图文混合图片不显示的情况,本文章主要支持word,wps,网页,其它来源暂无考虑进去;使用本代码可能存在安全漏洞,望各位悉知,自行斟酌;

安装使用:

安装:个人推荐使用yarn

yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save

yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save

使用:局部导入注册

<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
  components: { Editor, Toolbar },
}
</script>

使用:样式引入

<style src="@wangeditor/editor/dist/css/style.css"></style>

vue2模板部分:

<template>
  <div style="border: 1px solid #ccc">
    <Toolbar
      style="border-bottom: 1px solid #ccc"
      :editor="editor"
      :defaultConfig="toolbarConfig"
      :mode="mode"
    />
    <Editor
      style="height: 500px; overflow-y: hidden"
      v-model="editorHtml"
      :defaultConfig="editorConfig"
      :mode="mode"
      @onCreated="onCreated"
      @customPaste="customPaste"
    />
  </div>
</template>

接下来需要写methods和data:

export default {
  components: { Editor, Toolbar },
  data(){
      return:{
          editor: null,
          timeoutId: [], // 存储setTimeout的ID
          editorHtml: '',
          toolbarConfig: {},
          editorConfig: { placeholder: '请输入内容...' },
          mode: 'default', // or 'simple'
          value: '',
          imgParent: ['span', 'font','a','picture'] // 图片可能存在的直接祖先行元素
     }
  }  
}

备注:data函数返回值中目前包含了下文可能用到的属性;

export default {
  components: { Editor, Toolbar },
  data(){
      return:{
          editor: null,
          timeoutId: [], // 存储setTimeout的ID
          editorHtml: '',
          toolbarConfig: {},
          editorConfig: { placeholder: '请输入内容...' },
          mode: 'default', // or 'simple'
          value: '',
          imgParent: ['span', 'font','a','picture'] // 图片可能存在的直接祖先行元素
     }
  },
  
  methods: {
    onCreated(editor) {
      this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
    },
    // 粘贴方法,此事件可以覆盖编辑器原本的Paste
    // 粘贴方法
    customPaste(editor, event, callback) {
      event.preventDefault()
      let image = event.clipboardData.items[0].getAsFile() // 获取粘贴的图片文件对象
      let html = event.clipboardData.getData('text/html') // 获取粘贴的 html
      let rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
      let timeoutId
      if (rtf) {
        let imgs = this.findAllImageElementsWithLocalSource(html)
        let imgs1 = this.replaceImagesFileSourceWithInlineRepresentation(
          imgs,
          this.extractImageDataFromRtf(rtf)
        )
        this.value.querySelectorAll('img').forEach((img, index) => {
          if (imgs1.length > 0) {
            img.setAttribute('src', imgs1[index].src)
          } else {
            // 将base64转换为文件对象
            // let boldFile = this.convertBase64ToBlob(img.src)
            // img.setAttribute('src', URL.createObjectURL(boldFile))
            img.setAttribute('src', img.src)
          }
          img.style.width = this.imgWidth(img)
          img.style.height = this.imgHeight(img)
          if (
            parseFloat(img.style.height) <= 20 &&
            parseFloat(img.style.width) > parseFloat(img.style.height)
          ) {
            img.style.width = 'auto'
          } else if (
            parseFloat(img.style.width) <= 20 &&
            parseFloat(img.style.height) > parseFloat(img.style.width)
          ) {
            img.style.height = 'auto'
          }
          this.findParent(img)
          return img
        })
        timeoutId = setTimeout(() => {
          editor.dangerouslyInsertHtml(this.value.outerHTML)
        }, 100)
      } else if (html && !rtf) {
        const div = document.createElement('div')
        div.innerHTML = html
        const imgs = div.querySelectorAll('img')
        imgs.forEach((img) => {
          let newSrc = img.src
          img.onerror = () => {
            // if (img.src.startsWith('data:image/')) {
            //   let boldFile = this.convertBase64ToBlob(img.src)
            //   newSrc = URL.createObjectURL(boldFile)
            // } else
            if (img.src.startsWith('http')) {
              const cleanSrc = img.src.split('?')[0]
              // 构造Base64地址;        **这里要换成自己的服务器转换地址**
              newSrc = `https://aaaa.bbbb.com/download/img/${btoa(
                cleanSrc
              )}`
            }
            if (this.editorHtml?.includes(img.src)) {
              this.editorHtml = this.editorHtml.replace(img.src, newSrc)
            }
            img.setAttribute('src', newSrc)
            img.onerror = null
          }
          img.style.width = this.imgWidth(img)
          img.style.height = this.imgHeight(img)
          if (
            parseFloat(img.style.height) <= 20 &&
            parseFloat(img.style.width) > parseFloat(img.style.height)
          ) {
            img.style.width = 'auto'
          } else if (
            parseFloat(img.style.width) <= 20 &&
            parseFloat(img.style.height) > parseFloat(img.style.width)
          ) {
            img.style.height = 'auto'
          }

          this.findParent(img)

          return img
        })
        timeoutId = setTimeout(() => {
          editor.dangerouslyInsertHtml(div.innerHTML)
        }, 100)
      } else if (image && image.type.startsWith('image/')) {
        // let img = new Image()
        // 转换为base64地址
        let reader = new FileReader()
        reader.readAsDataURL(image)
        reader.onload = (e) => {
          let base64 = e.target.result
          let img = new Image()
          img.src = base64
          editor.dangerouslyInsertHtml(img.outerHTML)
        }
        // img.src = URL.createObjectURL(image)
        // editor.dangerouslyInsertHtml(img.outerHTML)
      }
      this.timeoutId.push(timeoutId)
      callback(true) // 返回值(注意,vue 事件的返回值,不能用 return)
    },
  }
}

备注:editor.dangerouslyInsertHtml();此方法具有安全隐患;

完整代码:

<template>
  <div style="border: 1px solid #ccc">
    <Toolbar
      style="border-bottom: 1px solid #ccc"
      :editor="editor"
      :defaultConfig="toolbarConfig"
      :mode="mode"
    />
    <Editor
      style="height: 500px; overflow-y: hidden"
      v-model="editorHtml"
      :defaultConfig="editorConfig"
      :mode="mode"
      @onCreated="onCreated"
      @customPaste="customPaste"
    />
  </div>
</template>
<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
  components: { Editor, Toolbar },
  data() {
    return {
      editor: null,
      timeoutId: [], // 存储setTimeout的ID
      editorHtml: '',
      toolbarConfig: {},
      editorConfig: { placeholder: '请输入内容...' },
      mode: 'default', // or 'simple'
      value: '',
      imgParent: ['span', 'font', 'a', 'picture'] // 图片可能存在的直接祖先行元素
    }
  },
  methods: {
    onCreated(editor) {
      this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
    },
    // 粘贴方法
    customPaste(editor, event, callback) {
      event.preventDefault()
      let image = event.clipboardData.items[0].getAsFile() // 获取粘贴的图片文件对象
      let html = event.clipboardData.getData('text/html') // 获取粘贴的 html
      let rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
      let timeoutId
      if (rtf) {
        let imgs = this.findAllImageElementsWithLocalSource(html)
        let imgs1 = this.replaceImagesFileSourceWithInlineRepresentation(
          imgs,
          this.extractImageDataFromRtf(rtf)
        )
        this.value.querySelectorAll('img').forEach((img, index) => {
          if (imgs1.length > 0) {
            img.setAttribute('src', imgs1[index].src)
          } else {
            // 将base64转换为文件对象
            // let boldFile = this.convertBase64ToBlob(img.src)
            // img.setAttribute('src', URL.createObjectURL(boldFile))
            img.setAttribute('src', img.src)
          }
          img.style.width = this.imgWidth(img)
          img.style.height = this.imgHeight(img)
          if (
            parseFloat(img.style.height) <= 20 &&
            parseFloat(img.style.width) > parseFloat(img.style.height)
          ) {
            img.style.width = 'auto'
          } else if (
            parseFloat(img.style.width) <= 20 &&
            parseFloat(img.style.height) > parseFloat(img.style.width)
          ) {
            img.style.height = 'auto'
          }
          this.findParent(img)
          return img
        })
        timeoutId = setTimeout(() => {
          editor.dangerouslyInsertHtml(this.value.outerHTML)
        }, 100)
      } else if (html && !rtf) {
        const div = document.createElement('div')
        div.innerHTML = html
        const imgs = div.querySelectorAll('img')
        imgs.forEach((img) => {
          let newSrc = img.src
          img.onerror = () => {
            // if (img.src.startsWith('data:image/')) {
            //   let boldFile = this.convertBase64ToBlob(img.src)
            //   newSrc = URL.createObjectURL(boldFile)
            // } else
            if (img.src.startsWith('http')) {
              const cleanSrc = img.src.split('?')[0]
              // 构造Base64地址
              newSrc = `https://aaa.bbb.com/download/img/${btoa(
                cleanSrc
              )}`
            }
            if (this.editorHtml?.includes(img.src)) {
              this.editorHtml = this.editorHtml.replace(img.src, newSrc)
            }
            img.setAttribute('src', newSrc)
            img.onerror = null
          }
          img.style.width = this.imgWidth(img)
          img.style.height = this.imgHeight(img)
          if (
            parseFloat(img.style.height) <= 20 &&
            parseFloat(img.style.width) > parseFloat(img.style.height)
          ) {
            img.style.width = 'auto'
          } else if (
            parseFloat(img.style.width) <= 20 &&
            parseFloat(img.style.height) > parseFloat(img.style.width)
          ) {
            img.style.height = 'auto'
          }

          this.findParent(img)

          return img
        })
        timeoutId = setTimeout(() => {
          editor.dangerouslyInsertHtml(div.innerHTML)
        }, 100)
      } else if (image && image.type.startsWith('image/')) {
        // let img = new Image()
        // 转换为base64地址
        let reader = new FileReader()
        reader.readAsDataURL(image)
        reader.onload = (e) => {
          let base64 = e.target.result
          let img = new Image()
          img.src = base64
          editor.dangerouslyInsertHtml(img.outerHTML)
        }
        // img.src = URL.createObjectURL(image)
        // editor.dangerouslyInsertHtml(img.outerHTML)
      }
      this.timeoutId.push(timeoutId)
      callback(true) // 返回值(注意,vue 事件的返回值,不能用 return)
    },
    // 查找图片的父元素
    findParent(element) {
      let parent = element.parentElement
      // 向上遍历,检查祖先元素
      while (parent && parent.tagName.toLowerCase() !== 'body') {
        if (this.imgParent.includes(parent.tagName.toLowerCase())) {
          const newDiv = document.createElement('div')
          // 将原有内容迁移到新的 div 中
          while (parent.firstChild) {
            newDiv.appendChild(parent.firstChild)
          }
          // 用新的 div 替换掉原来的 span 或 font
          parent.parentElement.replaceChild(newDiv, parent)
          parent = newDiv
        } else {
          parent = parent.parentElement
        }
      }
    },
    imgWidth(img) {
      const width = img.width || 0 // 获取 img 实际宽度
      const styleWidth = img.style.width ? parseFloat(img.style.width) || 0 : 0 // 获取样式中的宽度并转换为数字
      const maxWidth = img.style.maxWidth
        ? parseFloat(img.style.maxWidth) || 0
        : 0 // 获取样式中的最大宽度并转换为数字

      // 获取根元素的字体大小以计算rem的像素值
      const rootFontSize =
        parseFloat(getComputedStyle(document.documentElement).fontSize) || 1 // 默认值为1以避免NaN

      // 转换为像素,如果是百分比则乘以父元素的宽度
      const convertedStyleWidth =
        img.style.width && img.style.width.endsWith('%')
          ? (parseFloat(img.style.width) / 100) *
            (img.parentElement.clientWidth || 0)
          : img.style.width && img.style.width.endsWith('rem')
          ? parseFloat(img.style.width) * rootFontSize
          : styleWidth

      const convertedMaxWidth =
        img.style.maxWidth && img.style.maxWidth.endsWith('%')
          ? (parseFloat(img.style.maxWidth) / 100) *
            (img.parentElement.clientWidth || 0)
          : img.style.maxWidth && img.style.maxWidth.endsWith('rem')
          ? parseFloat(img.style.maxWidth) * rootFontSize
          : maxWidth

      // 找到最小不为零的值
      const values = [width, convertedStyleWidth, convertedMaxWidth].filter(
        (v) => !isNaN(v) && v > 0
      )
      console.log(width, convertedStyleWidth, convertedMaxWidth)

      return values.length > 0 ? Math.min(...values) + 'px' : 'auto'
    },
    imgHeight(img) {
      const height = img.height || 0 // 获取 img 实际高度
      const styleHeight = img.style.height
        ? parseFloat(img.style.height) || 0
        : 0 // 获取样式中的高度并转换为数字
      const maxHeight = img.style.maxHeight
        ? parseFloat(img.style.maxHeight) || 0
        : 0 // 获取样式中的最大高度并转换为数字

      // 获取根元素的字体大小以计算rem的像素值
      const rootFontSize =
        parseFloat(getComputedStyle(document.documentElement).fontSize) || 1 // 默认值为1以避免NaN

      // 转换为像素,如果是百分比则乘以父元素的高度
      const convertedStyleHeight =
        img.style.height && img.style.height.endsWith('%')
          ? (parseFloat(img.style.height) / 100) *
            (img.parentElement.clientHeight || 0)
          : img.style.height && img.style.height.endsWith('rem')
          ? parseFloat(img.style.height) * rootFontSize
          : styleHeight

      const convertedMaxHeight =
        img.style.maxHeight && img.style.maxHeight.endsWith('%')
          ? (parseFloat(img.style.maxHeight) / 100) *
            (img.parentElement.clientHeight || 0)
          : img.style.maxHeight && img.style.maxHeight.endsWith('rem')
          ? parseFloat(img.style.maxHeight) * rootFontSize
          : maxHeight

      // 找到最小不为零的值
      const values = [height, convertedStyleHeight, convertedMaxHeight].filter(
        (v) => !isNaN(v) && v > 0
      )
      console.log(height, convertedStyleHeight, convertedMaxHeight)

      return values.length > 0 ? Math.min(...values) + 'px' : 'auto'
    },

    _convertHexToBase64(hexString) {
      return btoa(
        hexString
          .match(/\w{2}/g)
          .map((char) => {
            return String.fromCharCode(parseInt(char, 16))
          })
          .join('')
      )
    }, // 将base64转换为文件对象
    convertBase64ToBlob(base64) {
      const base64Arr = base64.split(',')
      let imgtype = ''
      let base64String = ''
      if (base64Arr.length > 1) {
        // 如果是图片base64,去掉头信息
        base64String = base64Arr[1]
        imgtype = base64Arr[0].substring(
          base64Arr[0].indexOf(':') + 1,
          base64Arr[0].indexOf(';')
        )
      } // 将base64解码
      var bytes = atob(base64String)
      var bytesCode = new ArrayBuffer(bytes.length) // 转换为类型化数组
      var byteArray = new Uint8Array(bytesCode) // 将base64转换为ascii码
      for (var i = 0; i < bytes.length; i++) {
        byteArray[i] = bytes.charCodeAt(i)
      } // 生成Blob对象(文件对象)
      return new Blob([byteArray], { type: imgtype })
    },
    findAllImageElementsWithLocalSource(documentFragment) {
      const div = document.createElement('div')
      div.innerHTML = documentFragment
      this.value = div
      const imgs = []
      div.querySelectorAll('img').forEach((img) => {
        if (img.src.startsWith('file://')) {
          imgs.push(img)
        }
      })
      return imgs
    },

    extractImageDataFromRtf(rtfData) {
      if (!rtfData) {
        return []
      } // 旧的写法 // const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/ // 新删减后的写法
      const regexPictureHeader = /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/
      const regexPicture = new RegExp(
        '(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}',
        'g'
      )
      const images = rtfData.match(regexPicture)
      const result = []
      if (images) {
        for (const image of images) {
          let imageType = false
          if (image.includes('\\pngblip')) {
            imageType = 'image/png'
          } else if (image.includes('\\jpegblip')) {
            imageType = 'image/jpeg'
          }
          if (imageType) {
            result.push({
              hex: image
                .replace(regexPictureHeader, '')
                .replace(/[^\da-fA-F]/g, ''),
              type: imageType
            })
          }
        }
      }
      return result
    },
    replaceImagesFileSourceWithInlineRepresentation(
      imageElements,
      imagesHexSources
    ) {
      // Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.
      if (imageElements.length === imagesHexSources.length) {
        for (let i = 0; i < imageElements.length; i++) {
          const newSrc = `data:${
            imagesHexSources[i].type
          };base64,${this._convertHexToBase64(imagesHexSources[i].hex)}`
          // let boldFile = this.convertBase64ToBlob(newSrc)
          // imageElements[i].setAttribute('src', URL.createObjectURL(boldFile))
          imageElements[i].setAttribute('src', newSrc)
        }
      }
      return imageElements
    }
  },
  beforeDestroy() {
    const editor = this.editor
    if (editor == null) return
    editor.destroy() // 组件销毁时,及时销毁编辑器
    // 清除定时器
    if (this.timeoutId.length > 0) {
      this.timeoutId.forEach((timeoutId) => {
        clearTimeout(timeoutId)
      })
    }
  }
}
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>

总结:

部分方法是借鉴网友的,感谢被我借鉴的网友,记不清啦!jym如有问题请留言