当前位置:   article > 正文

Vue3项目中集成最强的tinymce富文本编辑器

tinymce beforesetcontent

前言

tinymce是目前公认的最好的富文本编辑器。本篇文章将详细说明在vue3项目中如何集成tinymce,并将 tinymce封装成组件使用

最终效果

正文

1. 安装依赖,

这里我们使用了tinymce5,最新已经到tinymce6了。不同版本之间插件上会有差异

npm install tinymce@5.10.2

2. 安装汉化包

我们在 vue3 项目的public/resource/ 目录下创建文件夹 tinymce,在tinymce下再创建 langs文件夹用来存放语言汉化包

我们将 这个中文包https://gitee.com/shuiche/tinymce-vue3/blob/master/langs/zh_CN.js下载后放到langs文件夹下

3. 迁移ui皮肤文件

我们在 node_modules/tinymce 中找到 skins 文件夹,将其复制到 public/resource/tinymce/ 里

4. 封装组件

在src/components/ 下新建 Tinymce 文件夹。在 Tinymce/ 文件夹下新建3个文件: helper.js , tinymce.js, Tinymce.vue

内容分别是:

helper.js

  1. // ==========helper.js==========
  2. const validEvents = [
  3. 'onActivate',
  4. 'onAddUndo',
  5. 'onBeforeAddUndo',
  6. 'onBeforeExecCommand',
  7. 'onBeforeGetContent',
  8. 'onBeforeRenderUI',
  9. 'onBeforeSetContent',
  10. 'onBeforePaste',
  11. 'onBlur',
  12. 'onChange',
  13. 'onClearUndos',
  14. 'onClick',
  15. 'onContextMenu',
  16. 'onCopy',
  17. 'onCut',
  18. 'onDblclick',
  19. 'onDeactivate',
  20. 'onDirty',
  21. 'onDrag',
  22. 'onDragDrop',
  23. 'onDragEnd',
  24. 'onDragGesture',
  25. 'onDragOver',
  26. 'onDrop',
  27. 'onExecCommand',
  28. 'onFocus',
  29. 'onFocusIn',
  30. 'onFocusOut',
  31. 'onGetContent',
  32. 'onHide',
  33. 'onInit',
  34. 'onKeyDown',
  35. 'onKeyPress',
  36. 'onKeyUp',
  37. 'onLoadContent',
  38. 'onMouseDown',
  39. 'onMouseEnter',
  40. 'onMouseLeave',
  41. 'onMouseMove',
  42. 'onMouseOut',
  43. 'onMouseOver',
  44. 'onMouseUp',
  45. 'onNodeChange',
  46. 'onObjectResizeStart',
  47. 'onObjectResized',
  48. 'onObjectSelected',
  49. 'onPaste',
  50. 'onPostProcess',
  51. 'onPostRender',
  52. 'onPreProcess',
  53. 'onProgressState',
  54. 'onRedo',
  55. 'onRemove',
  56. 'onReset',
  57. 'onSaveContent',
  58. 'onSelectionChange',
  59. 'onSetAttrib',
  60. 'onSetContent',
  61. 'onShow',
  62. 'onSubmit',
  63. 'onUndo',
  64. 'onVisualAid'
  65. ]
  66. const isValidKey = (key) => validEvents.indexOf(key) !== -1
  67. export const bindHandlers = (initEvent, listeners, editor) => {
  68. Object.keys(listeners)
  69. .filter(isValidKey)
  70. .forEach((key) => {
  71. const handler = listeners[key]
  72. if (typeof handler === 'function') {
  73. if (key === 'onInit') {
  74. handler(initEvent, editor)
  75. } else {
  76. editor.on(key.substring(2), (e) => handler(e, editor))
  77. }
  78. }
  79. })
  80. }

tinymce.js

  1. // ==========tinymce.js==========
  2. // Any plugins you want to setting has to be imported
  3. // Detail plugins list see https://www.tinymce.com/docs/plugins/
  4. // Custom builds see https://www.tinymce.com/download/custom-builds/
  5. // colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
  6. export const plugins = [
  7. 'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus template textpattern visualblocks visualchars wordcount'
  8. ]
  9. export const toolbar = [
  10. 'fontsizeselect lineheight searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample',
  11. 'hr bullist numlist link preview anchor pagebreak insertdatetime media forecolor backcolor fullscreen'
  12. ]
  1. // ==========Tinymce.vue==========
  2. <template>
  3. <div class="prefixCls" :style="{ width: containerWidth }">
  4. <textarea
  5. :id="tinymceId"
  6. ref="elRef"
  7. :style="{ visibility: 'hidden' }"
  8. ></textarea>
  9. </div>
  10. </template>
  11. <script setup>
  12. import tinymce from 'tinymce/tinymce'
  13. import 'tinymce/themes/silver'
  14. import 'tinymce/icons/default/icons'
  15. import 'tinymce/plugins/advlist'
  16. import 'tinymce/plugins/anchor'
  17. import 'tinymce/plugins/autolink'
  18. import 'tinymce/plugins/autosave'
  19. import 'tinymce/plugins/code'
  20. import 'tinymce/plugins/codesample'
  21. import 'tinymce/plugins/directionality'
  22. import 'tinymce/plugins/fullscreen'
  23. import 'tinymce/plugins/hr'
  24. import 'tinymce/plugins/insertdatetime'
  25. import 'tinymce/plugins/link'
  26. import 'tinymce/plugins/lists'
  27. import 'tinymce/plugins/media'
  28. import 'tinymce/plugins/nonbreaking'
  29. import 'tinymce/plugins/noneditable'
  30. import 'tinymce/plugins/pagebreak'
  31. import 'tinymce/plugins/paste'
  32. import 'tinymce/plugins/preview'
  33. import 'tinymce/plugins/print'
  34. import 'tinymce/plugins/save'
  35. import 'tinymce/plugins/searchreplace'
  36. import 'tinymce/plugins/spellchecker'
  37. import 'tinymce/plugins/tabfocus'
  38. import 'tinymce/plugins/template'
  39. import 'tinymce/plugins/textpattern'
  40. import 'tinymce/plugins/visualblocks'
  41. import 'tinymce/plugins/visualchars'
  42. import 'tinymce/plugins/wordcount'
  43. // import 'tinymce/plugins/table';
  44. import { computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, defineProps, defineEmits, getCurrentInstance } from 'vue'
  45. import { toolbar, plugins } from './tinymce'
  46. import { buildShortUUID } from '@/utils/uuid'
  47. import { bindHandlers } from './helper'
  48. import { onMountedOrActivated } from '@/hooks/core/onMountedOrActivated'
  49. import { isNumber } from '@/utils/is'
  50. const props = defineProps({
  51. options: {
  52. type: Object,
  53. default: () => {}
  54. },
  55. value: {
  56. type: String
  57. },
  58. toolbar: {
  59. type: Array,
  60. default: toolbar
  61. },
  62. plugins: {
  63. type: Array,
  64. default: plugins
  65. },
  66. modelValue: {
  67. type: String
  68. },
  69. height: {
  70. type: [Number, String],
  71. required: false,
  72. default: 400
  73. },
  74. width: {
  75. type: [Number, String],
  76. required: false,
  77. default: 'auto'
  78. },
  79. showImageUpload: {
  80. type: Boolean,
  81. default: true
  82. }
  83. })
  84. const emits = defineEmits(['change', 'update:modelValue', 'inited', 'init-error'])
  85. const { attrs } = getCurrentInstance()
  86. const tinymceId = ref(buildShortUUID('tiny-vue'))
  87. const containerWidth = computed(() => {
  88. const width = props.width
  89. if (isNumber(width)) {
  90. return `${width}px`
  91. }
  92. return width
  93. })
  94. const editorRef = ref(null)
  95. const fullscreen = ref(false)
  96. const elRef = ref(null)
  97. const tinymceContent = computed(() => props.modelValue)
  98. const initOptions = computed(() => {
  99. const { height, options, toolbar, plugins } = props
  100. const publicPath = '/'
  101. return {
  102. selector: `#${unref(tinymceId)}`,
  103. height,
  104. toolbar,
  105. menubar: 'file edit insert view format table',
  106. plugins,
  107. language_url: '/resource/tinymce/langs/zh_CN.js',
  108. language: 'zh_CN',
  109. branding: false,
  110. default_link_target: '_blank',
  111. link_title: false,
  112. object_resizing: false,
  113. auto_focus: true,
  114. skin: 'oxide',
  115. skin_url: '/resource/tinymce/skins/ui/oxide',
  116. content_css: '/resource/tinymce/skins/ui/oxide/content.min.css',
  117. ...options,
  118. setup: (editor) => {
  119. editorRef.value = editor
  120. editor.on('init', (e) => initSetup(e))
  121. }
  122. }
  123. })
  124. const disabled = computed(() => {
  125. const { options } = props
  126. const getdDisabled = options && Reflect.get(options, 'readonly')
  127. const editor = unref(editorRef)
  128. if (editor) {
  129. editor.setMode(getdDisabled ? 'readonly' : 'design')
  130. }
  131. return getdDisabled ?? false
  132. })
  133. watch(
  134. () => attrs.disabled,
  135. () => {
  136. const editor = unref(editorRef)
  137. if (!editor) {
  138. return
  139. }
  140. editor.setMode(attrs.disabled ? 'readonly' : 'design')
  141. }
  142. )
  143. onMountedOrActivated(() => {
  144. if (!initOptions.value.inline) {
  145. tinymceId.value = buildShortUUID('tiny-vue')
  146. }
  147. nextTick(() => {
  148. setTimeout(() => {
  149. initEditor()
  150. }, 30)
  151. })
  152. })
  153. onBeforeUnmount(() => {
  154. destory()
  155. })
  156. onDeactivated(() => {
  157. destory()
  158. })
  159. function destory () {
  160. if (tinymce !== null) {
  161. // tinymce?.remove?.(unref(initOptions).selector!);
  162. }
  163. }
  164. function initSetup (e) {
  165. const editor = unref(editorRef)
  166. if (!editor) {
  167. return
  168. }
  169. const value = props.modelValue || ''
  170. editor.setContent(value)
  171. bindModelHandlers(editor)
  172. bindHandlers(e, attrs, unref(editorRef))
  173. }
  174. function initEditor () {
  175. const el = unref(elRef)
  176. if (el) {
  177. el.style.visibility = ''
  178. }
  179. tinymce
  180. .init(unref(initOptions))
  181. .then((editor) => {
  182. emits('inited', editor)
  183. })
  184. .catch((err) => {
  185. emits('init-error', err)
  186. })
  187. }
  188. function setValue (editor, val, prevVal) {
  189. if (
  190. editor &&
  191. typeof val === 'string' &&
  192. val !== prevVal &&
  193. val !== editor.getContent({ format: attrs.outputFormat })
  194. ) {
  195. editor.setContent(val)
  196. }
  197. }
  198. function bindModelHandlers (editor) {
  199. const modelEvents = attrs.modelEvents ? attrs.modelEvents : null
  200. const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents
  201. watch(
  202. () => props.modelValue,
  203. (val, prevVal) => {
  204. setValue(editor, val, prevVal)
  205. }
  206. )
  207. watch(
  208. () => props.value,
  209. (val, prevVal) => {
  210. setValue(editor, val, prevVal)
  211. },
  212. {
  213. immediate: true
  214. }
  215. )
  216. editor.on(normalizedEvents || 'change keyup undo redo', () => {
  217. const content = editor.getContent({ format: attrs.outputFormat })
  218. emits('update:modelValue', content)
  219. emits('change', content)
  220. })
  221. editor.on('FullscreenStateChanged', (e) => {
  222. fullscreen.value = e.state
  223. })
  224. }
  225. function handleImageUploading (name) {
  226. const editor = unref(editorRef)
  227. if (!editor) {
  228. return
  229. }
  230. editor.execCommand('mceInsertContent', false, getUploadingImgName(name))
  231. const content = editor?.getContent() ?? ''
  232. setValue(editor, content)
  233. }
  234. function handleDone (name, url) {
  235. const editor = unref(editorRef)
  236. if (!editor) {
  237. return
  238. }
  239. const content = editor?.getContent() ?? ''
  240. const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? ''
  241. setValue(editor, val)
  242. }
  243. function getUploadingImgName (name) {
  244. return `[uploading:${name}]`
  245. }
  246. </script>
  247. <style lang="scss" scoped>
  248. .prefixCls{
  249. position: relative;
  250. line-height: normal;
  251. }
  252. textarea {
  253. z-index: -1;
  254. visibility: hidden;
  255. }
  256. </style>

在Tinymce中引入了两个外部函数和一个hook,具体内容是:

  1. // ==== isNumber 函数====
  2. const toString = Object.prototype.toString
  3. export function is (val, type) {
  4. return toString.call(val) === `[object ${type}]`
  5. }
  6. export function isNumber (val) {
  7. return is(val, 'Number')
  8. }
  9. // ==== buildShortUUID 函数====
  10. export function buildShortUUID (prefix = '') {
  11. const time = Date.now()
  12. const random = Math.floor(Math.random() * 1000000000)
  13. unique++
  14. return prefix + '_' + random + unique + String(time)
  15. }
  16. // ==== onMountedOrActivated hook====
  17. import { nextTick, onMounted, onActivated } from 'vue'
  18. export function onMountedOrActivated (hook) {
  19. let mounted
  20. onMounted(() => {
  21. hook()
  22. nextTick(() => {
  23. mounted = true
  24. })
  25. })
  26. onActivated(() => {
  27. if (mounted) {
  28. hook()
  29. }
  30. })
  31. }

使用组件

使用时非常简单,我们用 v-model 和 @change 即可使用。

  1. <Tinymce v-model="content" @change="handleChange" width="100%" />
  2. // ......
  3. let content = ref('')
  4. function handleChange (item) {
  5. console.log('change', item)
  6. }

后记

如有问题,可以留言沟通

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/635003
推荐阅读
相关标签
  

闽ICP备14008679号