富文本编辑器TinyMCE的使用(React Vue)

一,需求与介绍

1.1,需求

    编辑新闻等富有个性化的文本

1.2,介绍

TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。

TinyMCE的优势:

开源可商用,基于LGPL2.1

插件丰富,自带插件基本涵盖日常所需功能

接口丰富,可扩展性强,有能力可以无限拓展功能

界面好看,符合现代审美

提供经典、内联、沉浸无干扰三种模式

对标准支持优秀(自v5开始)

多语言支持,官网可下载几十种语言。

二,配置集成并组件化

2.1,通用配置

1,工具栏toolbar

复制代码

1 // Here is a list of the toolbar

2 // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols

3

4 // 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

5 const toolbar = ['code codesample undo redo restoredraft | cut copy paste pastetext | forecolor backcolor searchreplace bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | formatselect fontselect fontsizeselect | blockquote subscript superscript removeformat | table image media charmap emoticons hr pagebreak insertdatetime print preview']// | fullscreen

6 export default toolbar

复制代码

2,插件plugins

复制代码

1 // Any plugins you want to use has to be imported

2 // Detail plugins list see https://www.tinymce.com/docs/plugins/

3 // Custom builds see https://www.tinymce.com/download/custom-builds/

4

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

6 const plugins = ['print preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount imagetools textpattern help paste emoticons autosave']

7

8 export default plugins

复制代码

3,常用字体配置fonts

复制代码

1 // Any font you want to use has to be imported

2 const fontsizeFormats='12px 14px 16px 18px 24px 36px 48px 56px 72px';

3 const fontFormats= '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats;知乎配置=BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif;小米配置=Helvetica Neue,Helvetica,Arial,Microsoft Yahei,Hiragino Sans GB,Heiti SC,WenQuanYi Micro Hei,sans-serif';

4

5 export default {

6    fontsizeFormats,

7    fontFormats

8 }

复制代码

4,准备标签

1  <div>

2              <textarea id="tinymceId"/>

3            </div>

在textarea外面需要套一层div,否则会产生一些意想不到的问题

5,初始化标签,生成编辑框

复制代码

1  window.tinymce.init({

2      language: 'zh_CN',

3      selector: `#${tinymceId}`,

4      height: height,

5      body_class: 'panel-body ',

6      object_resizing: false,

7      toolbar: toolbar.length > 0 ? toolbar : defaultToolbar,

8      menubar: menubar,

9      plugins: defaultplugins,

10      end_container_on_empty_block: true,

11      fontsize_formats: fontsizeFormats,

12      font_formats: fontFormats,

13      powerpaste_word_import: 'clean',

14      code_dialog_height: 450,

15      code_dialog_width: 1000,

16      advlist_bullet_styles: 'square',

17      advlist_number_styles: 'default',

18      imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],

19      default_link_target: '_blank',

20      link_title: false,

21      nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin

22      init_instance_callback: editor => {

23        if (content) {

24          editor.setContent(content)

25        }

26        editor.on('NodeChange Change KeyUp SetContent', () => {

27        })

28      },

29      setup(editor) {

30        editor.on('FullscreenStateChanged', (e) => {

31        })

32      }

33    })

复制代码

2.2,React组件化

直接放代码

复制代码

  1 import React from 'react';

  2 import { Button } from 'antd';

  3 import PropTypes from 'prop-types';

  4 import styles from './index.less';

  6 import defaultplugins from './plugins';

  7 import defaultToolbar from './toolbar';

  8 import {

  9  fontsizeFormats,

10  fontFormats

11 } from './font';

12 import UploadImage from './UploadImage';

13

14

15 class Tinymce extends React.Component {

16

17  static propTypes = {

18    tinymceId: PropTypes.string,

19    content: PropTypes.string,

20    toolbar: PropTypes.array,

21    menubar: PropTypes.string,

22    height: PropTypes.number,

23    getContent: PropTypes.func,

24  };

25  static defaultProps = {

26    tinymceId: 'react-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + ''),

27    menubar: 'file edit insert view format table',

28    height: 520,

29    toolbar: []

30  };

31  constructor(props) {

32    super(props);

33    this.state = {

34      hasChange: false,

35      hasInit: false,

36      fullscreen: false,

37    };

38  };

39

40  componentDidMount() {

41    this.initTinymce()

42

43  }

44  componentWillUnmount() {

45    this.destroyTinymce()

46  }

47  initTinymce() {

48    const { tinymceId, menubar, height, toolbar, content, getContent } = this.props

49    const _this = this

50    window.tinymce.init({

51      language: 'zh_CN',

52      selector: `#${tinymceId}`,

53      height: height,

54      body_class: 'panel-body ',

55      object_resizing: false,

56      toolbar: toolbar.length > 0 ? toolbar : defaultToolbar,

57      menubar: menubar,

58      plugins: defaultplugins,

59      end_container_on_empty_block: true,

60      fontsize_formats: fontsizeFormats,

61      font_formats: fontFormats,

62      powerpaste_word_import: 'clean',

63      code_dialog_height: 450,

64      code_dialog_width: 1000,

65      advlist_bullet_styles: 'square',

66      advlist_number_styles: 'default',

67      imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],

68      default_link_target: '_blank',

69      link_title: false,

70      nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin

71      init_instance_callback: editor => {

72        if (content) {

73          editor.setContent(content)

74        }

75        _this.setState({

76          hasInit: true

77        })

78        editor.on('NodeChange Change KeyUp SetContent', () => {

79          _this.setState({

80            hasChange: true

81          })

82        })

83      },

84      setup(editor) {

85        editor.on('FullscreenStateChanged', (e) => {

86          _this.setState({

87            fullscreen: e.state

88          })

89        })

90      }

91    })

92  }

93  destroyTinymce() {

94    const { tinymceId } = this.props

95    const { fullscreen } = this.state

96    const tinymce = window.tinymce.get(tinymceId)

97    if (fullscreen) {

98      tinymce.execCommand('mceFullScreen')

99    }

100

101    if (tinymce) {

102      tinymce.destroy()

103    }

104  }

105  // setContent(value) {

106  //  const { tinymceId } = this.props

107  //  window.tinymce.get(tinymceId).setContent(value)

108  // }

109  saveToGetContent() {

110    const { tinymceId, getContent } = this.props

111    if (getContent && typeof getContent === 'function') {

112      getContent(window.tinymce.get(tinymceId).getContent())

113    }

114  }

115

116  /**

117    * 上传图片成功回调

118    *  */

119  imageSuccessCBK(arr) {

120    const { tinymceId } = this.props

121    arr.forEach(v => {

122      window.tinymce.get(tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)

123    })

124  }

125  render() {

126    const { loading, tinymceId } = this.props

127    const { fullscreen } = this.state

128    const header = (

129      <Page.Header breadcrumb={['富文本实例']} title={'富文本实例'} />

130    );

131    return (

132      <div className={styles['tinymce-components']}>

133        <Page header={header} loading={!!loading}>

134

135          <div className={fullscreen ? "tinymce-container mce-fullscreen" : "tinymce-container"}>

136

137            <div>

138              <textarea id={tinymceId} className={styles['tinymce-textarea']} />

139            </div>

140            <div className="editor-custom-btn-container">

141              <UploadImage className="editor-upload-btn" imageSuccessCBK={(arr)=>{this.imageSuccessCBK(arr)}}/>

142            </div>

143            <Button type="primary" onClick={() => { this.saveToGetContent() }}>保存</Button>

144          </div>

145        </Page>

146      </div>

147    );

148  }

149 }

150

151 export default Tinymce;

复制代码

上传图片组件,使用antd的部分组件:

复制代码

  1 import React from 'react';

  2 import { Button, Modal, Icon, Upload, message } from 'antd';

  3 import PropTypes from 'prop-types';

  4 import styles from './index.less';

  5

  6 const Dragger = Upload.Dragger;

  7

  8 class UploadImage extends React.Component {

  9

10  static propTypes = {

11    imageSuccessCBK: PropTypes.func,

12

13  };

14  static defaultProps = {

15

16  };

17  constructor(props) {

18    super(props);

19    this.state = {

20      visible: false,

21      listObj: {}

22    };

23  };

24

25  /**

26    * 显示弹框

27    *

28    *  */

29  showModal = () => {

30    this.setState({

31      visible: true,

32    });

33  }

34

35  /**

36  * 确认

37  *

38  *  */

39  handleOk = (e) => {

40    const { imageSuccessCBK } = this.props

41    const { listObj } = this.state

42    const imagesFileArr = Object.keys(listObj).map(v => listObj[v])

43    imageSuccessCBK(imagesFileArr)

44    this.setState({

45      visible: false,

46      listObj: {},

47      Uploading: false

48    });

49  }

50

51  handleCancel = (e) => {

52    this.setState({

53      visible: false,

54      listObj: {}

55    });

56  }

57  render() {

58    const { loading } = this.props

59    const { visible, listObj, Uploading } = this.state

60    const props = {

61      name: 'file',

62      multiple: true,

63      action: '//jsonplaceholder.typicode.com/posts/',

64      listType: 'picture',

65      onChange: (info) => {

66        const uid = info.file.uid

67        const objKeyArr = Object.keys(listObj)

68        const status = info.file.status;

69        if (status !== 'uploading') {

70          console.log(info.file, info.fileList);

71        }

72        if (status === 'done') {//已成功上传

73          this.setState({

74            Uploading: false,

75          })

76          for (let i = 0, len = objKeyArr.length; i < len; i++) {

77            if (listObj[objKeyArr[i]].uid === uid) {

78              listObj[objKeyArr[i]].url = info.file.thumbUrl

79              listObj[objKeyArr[i]].hasSuccess = true

80              message.success(`${info.file.name} file uploaded successfully.`);

81              return

82            }

83          }

84

85        } else if (status === 'error') {

86          this.setState({

87            Uploading: false,

88          })

89          message.error(`${info.file.name} file upload failed.`);

90        }

91        if (status === 'removed') {//移除上传的

92          for (let i = 0, len = objKeyArr.length; i < len; i++) {

93            if (listObj[objKeyArr[i]].uid === uid) {

94              delete listObj[objKeyArr[i]]

95              message.success(`${info.file.name} file removed successfully.`);

96              return

97            }

98          }

99        }

100      },

101      beforeUpload: (file) => {

102        this.setState({

103          Uploading: true,

104        })

105        const _self = this

106        const _URL = window.URL || window.webkitURL

107        const fileName = file.uid

108        listObj[fileName] = {}

109        return new Promise((resolve, reject) => {

110          const img = new Image()

111          img.src = _URL.createObjectURL(file)

112          img.onload = function () {

113            listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }

114            _self.setState({

115              listObj,

116            })

117          }

118          resolve(true)

119        })

120      },

121    };

122

123    return (

124      <div>

125        <Button

126          style={{ marginTop: 0 }}

127          type="primary"

128          shape="round"

129          icon="upload"

130          onClick={() => { this.showModal() }}>

131          上传

132            </Button>

133        {

134          visible ? <Modal

135            title="上传图片"

136            visible={visible}

137            onCancel={this.handleCancel}

138            footer={[

139              <div key="1">

140                <Button onClick={() => this.handleCancel()} loading={!!Uploading}>取消</Button>

141                <Button type="primary" style={{ marginLeft: 8 }} onClick={() => this.handleOk()} loading={!!Uploading}>

142                  确定

143                </Button>

144              </div>]}

145          >

146            <Dragger {...props}>

147              <p className="ant-upload-drag-icon">

148                <Icon type="inbox" />

149              </p>

150              <p className="ant-upload-text">Click or drag file to this area to upload</p>

151              <p className="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>

152            </Dragger>

153          </Modal> : null

154        }

155      </div>

156    );

157  }

158 }

159

160 export default UploadImage;

复制代码

2.3,Vue组件化

  直接放代码

复制代码

  1 <template>

  2  <div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">

  3    <div>

  4    <textarea :id="tinymceId" class="tinymce-textarea" />

  5    </div>

  6    <div class="editor-custom-btn-container">

  7      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />

  8    </div>

  9  </div>

10 </template>

11

12 <script>

13 import editorImage from './components/EditorImage'

14 import plugins from './plugins'

15 import toolbar from './toolbar'

16 import font from './font';

17

18 export default {

19  name: 'Tinymce',

20  components: { editorImage },

21  props: {

22    id: {

23      type: String,

24      default: function() {

25        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')

26      }

27    },

28    value: {

29      type: String,

30      default: ''

31    },

32    toolbar: {

33      type: Array,

34      required: false,

35      default() {

36        return []

37      }

38    },

39    menubar: {

40      type: String,

41      default: 'file edit insert view format table'

42    },

43    height: {

44      type: Number,

45      required: false,

46      default: 520

47    }

48  },

49  data() {

50    return {

51      hasChange: false,

52      hasInit: false,

53      tinymceId: this.id,

54      fullscreen: false,

55      languageTypeList: {

56        'en': 'en',

57        'zh': 'zh_CN'

58      }

59    }

60  },

61  computed: {

62    language() {

63      return this.languageTypeList[this.$store.getters.language]

64    }

65  },

66  watch: {

67    value(val) {

68      if (!this.hasChange && this.hasInit) {

69        this.$nextTick(() =>

70          window.tinymce.get(this.tinymceId).setContent(val || ''))

71      }

72    },

73    language() {

74      this.destroyTinymce()

75      this.$nextTick(() => this.initTinymce())

76    }

77  },

78  mounted() {

79    this.initTinymce()

80  },

81  activated() {

82    this.initTinymce()

83  },

84  deactivated() {

85    this.destroyTinymce()

86  },

87  destroyed() {

88    this.destroyTinymce()

89  },

90  methods: {

91    initTinymce() {

92      const _this = this

93      window.tinymce.init({

94        language: 'zh_CN',

95        selector: `#${this.tinymceId}`,

96        height: this.height,

97        body_class: 'panel-body ',

98        object_resizing: false,

99        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,

100        menubar: this.menubar,

101        plugins: plugins,

102        end_container_on_empty_block: true,

103        fontsize_formats: font.fontsizeFormats,

104        font_formats: font.fontFormats,

105        powerpaste_word_import: 'clean',

106        code_dialog_height: 450,

107        code_dialog_width: 1000,

108        advlist_bullet_styles: 'square',

109        advlist_number_styles: 'default',

110        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],

111        default_link_target: '_blank',

112        link_title: false,

113        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin

114        init_instance_callback: editor => {

115          if (_this.value) {

116            editor.setContent(_this.value)

117          }

118          _this.hasInit = true

119          editor.on('NodeChange Change KeyUp SetContent', () => {

120            this.hasChange = true

121            this.$emit('input', editor.getContent())

122          })

123        },

124        setup(editor) {

125          editor.on('FullscreenStateChanged', (e) => {

126            _this.fullscreen = e.state

127          })

128        }

129      })

130    },

131    destroyTinymce() {

132      const tinymce = window.tinymce.get(this.tinymceId)

133      if (this.fullscreen) {

134        tinymce.execCommand('mceFullScreen')

135      }

136

137      if (tinymce) {

138        tinymce.destroy()

139      }

140    },

141    setContent(value) {

142      window.tinymce.get(this.tinymceId).setContent(value)

143    },

144    getContent() {

145      window.tinymce.get(this.tinymceId).getContent()

146    },

147    imageSuccessCBK(arr) {

148      const _this = this

149      arr.forEach(v => {

150        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)

151      })

152    }

153  }

154 }

155 </script>

156

157 <style scoped>

158 .tinymce-container {

159  position: relative;

160  line-height: normal;

161 }

162 .tinymce-container>>>.mce-fullscreen {

163  z-index: 10000;

164 }

165 .tinymce-textarea {

166  visibility: hidden;

167  z-index: -1;

168 }

169 .editor-custom-btn-container {

170  position: absolute;

171  right: 4px;

172  top: 4px;

173  /*z-index: 2005;*/

174 }

175 .fullscreen .editor-custom-btn-container {

176  z-index: 10000;

177  position: fixed;

178 }

179 .editor-upload-btn {

180  display: inline-block;

181 }

182 </style>

复制代码

上传图片组件,使用elementUI的部分组件:

复制代码

  1 <template>

  2  <div class="upload-container">

  3    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">

  4      upload

  5    </el-button>

  6    <el-dialog :visible.sync="dialogVisible">

  7      <el-upload

  8        :multiple="true"

  9        :file-list="fileList"

10        :show-file-list="true"

11        :on-remove="handleRemove"

12        :on-success="handleSuccess"

13        :before-upload="beforeUpload"

14        class="editor-slide-upload"

15        action="https://httpbin.org/post"

16        list-type="picture-card"

17      >

18        <el-button size="small" type="primary">

19          Click upload

20        </el-button>

21      </el-upload>

22      <el-button @click="dialogVisible = false">

23        Cancel

24      </el-button>

25      <el-button type="primary" @click="handleSubmit">

26        Confirm

27      </el-button>

28    </el-dialog>

29  </div>

30 </template>

31

32 <script>

33 // import { getToken } from 'api/qiniu'

34

35 export default {

36  name: 'EditorSlideUpload',

37  props: {

38    color: {

39      type: String,

40      default: '#1890ff'

41    }

42  },

43  data() {

44    return {

45      dialogVisible: false,

46      listObj: {},

47      fileList: []

48    }

49  },

50  methods: {

51    checkAllSuccess() {

52      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)

53    },

54    handleSubmit() {

55      const arr = Object.keys(this.listObj).map(v => this.listObj[v])

56      if (!this.checkAllSuccess()) {

57        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')

58        return

59      }

60      this.$emit('successCBK', arr)

61      this.listObj = {}

62      this.fileList = []

63      this.dialogVisible = false

64    },

65    handleSuccess(response, file) {

66      const uid = file.uid

67      const objKeyArr = Object.keys(this.listObj)

68      for (let i = 0, len = objKeyArr.length; i < len; i++) {

69        if (this.listObj[objKeyArr[i]].uid === uid) {

70          this.listObj[objKeyArr[i]].url = response.files.file

71          this.listObj[objKeyArr[i]].hasSuccess = true

72          return

73        }

74      }

75    },

76    handleRemove(file) {

77      const uid = file.uid

78      const objKeyArr = Object.keys(this.listObj)

79      for (let i = 0, len = objKeyArr.length; i < len; i++) {

80        if (this.listObj[objKeyArr[i]].uid === uid) {

81          delete this.listObj[objKeyArr[i]]

82          return

83        }

84      }

85    },

86    beforeUpload(file) {

87      const _self = this

88      const _URL = window.URL || window.webkitURL

89      const fileName = file.uid

90      this.listObj[fileName] = {}

91      return new Promise((resolve, reject) => {

92        const img = new Image()

93        img.src = _URL.createObjectURL(file)

94        img.onload = function() {

95          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }

96        }

97        resolve(true)

98      })

99    }

100  }

101 }

102 </script>

103

104 <style lang="scss" scoped>

105 .editor-slide-upload {

106  margin-bottom: 20px;

107  /deep/ .el-upload--picture-card {

108    width: 100%;

109  }

110 }

111 </style>

复制代码

三,使用

3.1,React

第一步:导入组件

1 import Tinymce from '../../components/Tinymce';

第二步:使用组件

1  <Tinymce

2            content={''}

3              tinymceId='tinymceIdDemo'

4              getContent={(content) => { this.getContent(content) }}

5            />

第三步:获取输入的富文本

复制代码

1  getContent(content) {

2    console.log('content===',content)

3    this.setState({

4      content

5    })

6  }

复制代码

第四步:文本渲染

1  {/* 渲染标签字符串 */}

2            <div dangerouslySetInnerHTML={{ __html: content }}></div>

东莞网站建设www.zg886.cn

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容