Vue中級指南-02 如何在Vue項目使用富文本

富文本

富文本是管理後臺一個核心的功能,但同時又是一個有不少坑的地方。在選擇富文本的過程當中我也走了很多的彎路,市面上常見的富文本都基本用過了,最終權衡了一下選擇了Tinymce。css

這裏在簡述一下推薦使用 tinymce 的緣由:tinymce 是一家老牌作富文本的公司(這裏也推薦 ckeditor,也是一家一直作富文本的公司,新版本很不錯),它的產品經受了市場的承認,不論是文檔仍是配置的自由度都很好。在使用富文本的時候有一點也很關鍵就是複製格式化,以前在用一款韓國人作的富文本 summernote 被它的格式化坑的死去活來,但 tinymce 的去格式化至關的好,它還有一些增值服務(付費插件),最好用的就是powerpaste,很是的強大,支持從 word 裏面複製各類東西,並且還幫你順帶格式化了。富文本還有一點也很關鍵,就是拓展性。樓主用 tinymce 寫了好幾個插件,學習成本和容易度都不錯,很方便拓展。最後一點就是文檔很完善,基本你想獲得的配置項,它都有。tinymce 也支持按需加載,你能夠經過它官方的 build 頁定製本身須要的 plugins。html

以上內容引自vue-element-admin做者官網前端

Tinymce的使用方法

目前採用全局引用的方式。代碼地址:static/tinymce (static 目錄下的文件不會被打包), 在 index.html 中引入。並確保它的引入順序在你的app.js以前!vue

  • 第一步 咱們須要去官網下載他的源文件,或者引入在線地址
  • 第二步 因爲TinyMCE容許經過CSS選擇器識別可替換元素,所以惟一的要求是傳遞包含selectorto 的對象tinymce.init()。
在這個例子中,替換<textarea id='mytextarea'>
經過使選擇器與TinyMCE的5.0編輯器實例'#mytextarea'來tinymce.init()。
<!DOCTYPE html>
<html>
<head>
  <script src='https://cloud.tinymce.com/5/tinymce.min.js?apiKey=your_API_key'></script>
  <script>
  tinymce.init({
    selector: '#mytextarea'
  });
  </script>
</head>

<body>
<h1>TinyMCE Quick Start Guide</h1>
  <form method="post">
    <textarea id="mytextarea">Hello, World!</textarea>
  </form>
</body>
</html>
複製代碼
  • 第三步 使用表單POST保存內容,當form被提交後,TinyMCE的5.0主編模仿一個普通的HTML行爲textarea過程當中POST。在用戶的表單處理程序中,提交的內容能夠與從常規建立的內容相同的方式處理textarea。

項目實戰

因爲目前使用 npm 安裝 Tinymce 方法比較複雜並且還有一些問題(往後可能會採用該模式)且會大大增長編譯的時間因此暫時不許備採用。因此這裏直接去官網下載源文件保存在項目static文件夾中git

使用

因爲富文本不適合雙向數據流,因此只會 watch 傳入富文本的內容一次變化,以後傳入內容的變化就不會再監聽了,若是以後還有改變富文本內容的需求。程序員

能夠經過 this.refs.xxx.setContent() 手動來設置。github

  • 官網下載源文件放在static文件下如圖

  • 在index.html頁面中引入 tinymce.min.js文件, 確保它的引入順序在你的app.js以前!如圖

  • 在component文件夾內建立一個Tinymce文件夾用於封裝咱們要使用的富文本組件, 目錄如圖

其中兩個js文件是對應的工具欄列表等其餘插件功能, index.vue是封裝好的模板,components中的組件是封裝的圖片上傳的彈窗功能

plugins.js文件的代碼以下web

const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']

export default plugins
複製代碼

toolbar.js文件的代碼以下npm

const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']

export default toolbar
複製代碼

index.vue組件的代碼以下api

<template>
  <div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
    <textarea :id="tinymceId" class="tinymce-textarea" />
    <div class="editor-custom-btn-container">
      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
    </div>
  </div>
</template>

<script>
// 導入圖片上傳的組件
import editorImage from './components/editorImage'
// 導入富文本插件
import plugins from './plugins'
// 導入富文本工具欄
import toolbar from './toolbar'

export default {
  name: 'Tinymce',
  components: { editorImage },
  props: {
    id: {
      type: String,
      default: function() {
        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
      }
    },
    value: {
      type: String,
      default: ''
    },
    toolbar: {
      type: Array,
      required: false,
      default() {
        return []
      }
    },
    menubar: {
      type: String,
      default: 'file edit insert view format table'
    },
    height: {
      type: Number,
      required: false,
      default: 360
    }
  },
  data() {
    return {
      hasChange: false,
      hasInit: false,
      tinymceId: this.id,
      fullscreen: false,
      languageTypeList: {
        'en': 'en',
        'zh': 'zh_CN'
      }
    }
  },
  computed: {
    language() {
      return 'zh_CN'
    }
  },
  watch: {
    value(val) {
      if (!this.hasChange && this.hasInit) {
        this.$nextTick(() =>
          window.tinymce.get(this.tinymceId).setContent(val || ''))
      }
    },
    language() {
      this.destroyTinymce()
      this.$nextTick(() => this.initTinymce())
    }
  },
  mounted() {
    this.initTinymce()
  },
  activated() {
    this.initTinymce()
  },
  deactivated() {
    this.destroyTinymce()
  },
  destroyed() {
    this.destroyTinymce()
  },
  methods: {
    initTinymce() {
      const _this = this
      window.tinymce.init({
        language: this.language,
        selector: `#${this.tinymceId}`,
        height: this.height,
        body_class: 'panel-body ',
        object_resizing: false,
        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
        menubar: this.menubar,
        plugins: plugins,
        end_container_on_empty_block: true,
        powerpaste_word_import: 'clean',
        code_dialog_height: 450,
        code_dialog_width: 1000,
        advlist_bullet_styles: 'square',
        advlist_number_styles: 'default',
        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
        default_link_target: '_blank',
        link_title: false,
        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
        init_instance_callback: editor => {
          if (_this.value) {
            editor.setContent(_this.value)
          }
          _this.hasInit = true
          editor.on('NodeChange Change KeyUp SetContent', () => {
            this.hasChange = true
            this.$emit('input', editor.getContent())
          })
        },
        setup(editor) {
          editor.on('FullscreenStateChanged', (e) => {
            _this.fullscreen = e.state
          })
        }
        // 整合七牛上傳
        // images_dataimg_filter(img) {
        //   setTimeout(() => {
        //     const $image = $(img);
        //     $image.removeAttr('width');
        //     $image.removeAttr('height');
        //     if ($image[0].height && $image[0].width) {
        //       $image.attr('data-wscntype', 'image');
        //       $image.attr('data-wscnh', $image[0].height);
        //       $image.attr('data-wscnw', $image[0].width);
        //       $image.addClass('wscnph');
        //     }
        //   }, 0);
        //   return img
        // },
        // images_upload_handler(blobInfo, success, failure, progress) {
        //   progress(0);
        //   const token = _this.$store.getters.token;
        //   getToken(token).then(response => {
        //     const url = response.data.qiniu_url;
        //     const formData = new FormData();
        //     formData.append('token', response.data.qiniu_token);
        //     formData.append('key', response.data.qiniu_key);
        //     formData.append('file', blobInfo.blob(), url);
        //     upload(formData).then(() => {
        //       success(url);
        //       progress(100);
        //     })
        //   }).catch(err => {
        //     failure('出現未知問題,刷新頁面,或者聯繫程序員')
        //     console.log(err);
        //   });
        // },
      })
    },
    destroyTinymce() {
      const tinymce = window.tinymce.get(this.tinymceId)
      if (this.fullscreen) {
        tinymce.execCommand('mceFullScreen')
      }

      if (tinymce) {
        tinymce.destroy()
      }
    },
    setContent(value) {
      window.tinymce.get(this.tinymceId).setContent(value)
    },
    getContent() {
      window.tinymce.get(this.tinymceId).getContent()
    },
    imageSuccessCBK(arr) {
      const _this = this
      arr.forEach(v => {
        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
      })
    }
  }
}
</script>

<style scoped>
.tinymce-container {
  position: relative;
  line-height: normal;
}
.tinymce-container>>>.mce-fullscreen {
  z-index: 10000;
}
.tinymce-textarea {
  visibility: hidden;
  z-index: -1;
}
.editor-custom-btn-container {
  position: absolute;
  right: 4px;
  top: 4px;
  /*z-index: 2005;*/
}
.fullscreen .editor-custom-btn-container {
  z-index: 10000;
  position: fixed;
}
.editor-upload-btn {
  display: inline-block;
}
</style>

複製代碼

圖片上傳組件editorImage的代碼以下

<template>
  <div class="upload-container">
    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
      上傳圖片
    </el-button>
    <el-dialog :visible.sync="dialogVisible">
      <el-upload
        :multiple="true"
        :file-list="fileList"
        :show-file-list="true"
        :on-remove="handleRemove"
        :on-success="handleSuccess"
        :before-upload="beforeUpload"
        class="editor-slide-upload"
        action="https://httpbin.org/post"
        list-type="picture-card"
      >
        <el-button size="small" type="primary">
          點擊上傳
        </el-button>
      </el-upload>
      <el-button @click="dialogVisible = false">
        取 消
      </el-button>
      <el-button type="primary" @click="handleSubmit">
        確 定
      </el-button>
    </el-dialog>
  </div>
</template>

<script>
// import { getToken } from 'api/qiniu'

export default {
  name: 'EditorSlideUpload',
  props: {
    color: {
      type: String,
      default: '#1890ff'
    }
  },
  data() {
    return {
      dialogVisible: false,
      listObj: {},
      fileList: []
    }
  },
  methods: {
    checkAllSuccess() {
      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
    },
    handleSubmit() {
      const arr = Object.keys(this.listObj).map(v => this.listObj[v])
      if (!this.checkAllSuccess()) {
        this.$message('請等待全部圖片上傳成功 或 出現了網絡問題,請刷新頁面從新上傳!')
        return
      }
      this.$emit('successCBK', arr)
      this.listObj = {}
      this.fileList = []
      this.dialogVisible = false
    },
    handleSuccess(response, file) {
      const uid = file.uid
      const objKeyArr = Object.keys(this.listObj)
      for (let i = 0, len = objKeyArr.length; i < len; i++) {
        if (this.listObj[objKeyArr[i]].uid === uid) {
          this.listObj[objKeyArr[i]].url = response.files.file
          this.listObj[objKeyArr[i]].hasSuccess = true
          return
        }
      }
    },
    handleRemove(file) {
      const uid = file.uid
      const objKeyArr = Object.keys(this.listObj)
      for (let i = 0, len = objKeyArr.length; i < len; i++) {
        if (this.listObj[objKeyArr[i]].uid === uid) {
          delete this.listObj[objKeyArr[i]]
          return
        }
      }
    },
    beforeUpload(file) {
      const _self = this
      const _URL = window.URL || window.webkitURL
      const fileName = file.uid
      this.listObj[fileName] = {}
      return new Promise((resolve, reject) => {
        const img = new Image()
        img.src = _URL.createObjectURL(file)
        img.onload = function() {
          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
        }
        resolve(true)
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.editor-slide-upload {
  margin-bottom: 20px;
  /deep/ .el-upload--picture-card {
    width: 100%;
  }
}
</style>

複製代碼

以上代碼直接複製到本地就可以直接使用

  • 接下來在須要使用富文本組件的地方引用tinymce文件下的index.vue文件便可,如圖使用

當form被提交後,TinyMCE的5.0主編模仿一個普通的HTML行爲textarea過程當中POST。在用戶的表單處理程序中,提交的內容能夠與從常規建立的內容相同的方式處理textarea。

這裏定義了一個add方法 在富文本內編輯內容後,點擊提交按鈕便可獲取到postForm.content的內容(富文本編輯的內容)

當咱們把富文本的內容傳遞後臺後,若是有二次編輯的需求,後臺又將數據原封不動的傳遞給前端,如何將傳遞回來的數據放置在富文本中呢?

使用方式:
調用tinymce組件中的setContent()方法便可;
例如經過chage方法調用
change () {
      this.$refs.Tinymce.setContent("'<h1>我是後臺傳遞回來的數據,要放在富文本中二次編輯內容<h1>'")
    }
複製代碼

封裝的代碼都是看vue-element-admin開源項目的,具體代碼還在學習當中,歡迎提出寶貴意見

Vue入門指南(快速上手vue)

相關文章
相關標籤/搜索