index.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <template>
  2. <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
  3. <textarea :id="tinymceId" class="tinymce-textarea" />
  4. <div class="editor-custom-btn-container">
  5. <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
  6. </div>
  7. </div>
  8. </template>
  9. <script>
  10. /**
  11. * docs:
  12. * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
  13. */
  14. import editorImage from './components/EditorImage'
  15. import plugins from './plugins'
  16. import toolbar from './toolbar'
  17. import load from './dynamicLoadScript'
  18. import axios from 'axios';
  19. // import tinymceCDN from '@/assets/tinymce.min.js'
  20. // why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
  21. const tinymceCDN = 'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js'
  22. export default {
  23. name: 'Tinymce',
  24. components: { editorImage },
  25. props: {
  26. id: {
  27. type: String,
  28. default: function() {
  29. return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
  30. }
  31. },
  32. value: {
  33. type: String,
  34. default: ''
  35. },
  36. toolbar: {
  37. type: Array,
  38. required: false,
  39. default() {
  40. return []
  41. }
  42. },
  43. uploadUrl: {
  44. type: String,
  45. required: true
  46. },
  47. headers:{
  48. type: Object,
  49. required: true,
  50. default() {
  51. return {}
  52. }
  53. },
  54. menubar: {
  55. type: String,
  56. default: 'file edit insert view format table'
  57. },
  58. height: {
  59. type: [Number, String],
  60. required: false,
  61. default: 360
  62. },
  63. width: {
  64. type: [Number, String],
  65. required: false,
  66. default: 'auto'
  67. }
  68. },
  69. data() {
  70. return {
  71. hasChange: false,
  72. hasInit: false,
  73. tinymceId: this.id,
  74. fullscreen: false,
  75. languageTypeList: {
  76. 'en': 'en',
  77. 'zh': 'zh_CN',
  78. 'es': 'es_MX',
  79. 'ja': 'ja'
  80. }
  81. }
  82. },
  83. computed: {
  84. containerWidth() {
  85. const width = this.width
  86. if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
  87. return `${width}px`
  88. }
  89. return width
  90. }
  91. },
  92. watch: {
  93. value(val) {
  94. if (!this.hasChange && this.hasInit) {
  95. this.$nextTick(() =>
  96. window.tinymce.get(this.tinymceId).setContent(val || ''))
  97. }
  98. }
  99. },
  100. mounted() {
  101. this.init()
  102. },
  103. activated() {
  104. if (window.tinymce) {
  105. this.initTinymce()
  106. }
  107. },
  108. deactivated() {
  109. this.destroyTinymce()
  110. },
  111. destroyed() {
  112. this.destroyTinymce()
  113. },
  114. methods: {
  115. init() {
  116. // this.initTinymce()
  117. // dynamic load tinymce from cdn
  118. load(tinymceCDN, (err) => {
  119. if (err) {
  120. this.$message.error(err.message)
  121. return
  122. }
  123. this.initTinymce()
  124. })
  125. },
  126. initTinymce() {
  127. const _this = this
  128. window.tinymce.init({
  129. selector: `#${this.tinymceId}`,
  130. language: this.languageTypeList['zh'],
  131. height: this.height,
  132. body_class: 'panel-body',
  133. object_resizing: true,
  134. // extended_valid_elements: 'img[style|class|src|border|alt|title|hspace|vspace|width|height|align|name|loading]',
  135. toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
  136. menubar: this.menubar,
  137. plugins: plugins,
  138. // content_css : "tinymce.css",
  139. entity_encoding : "raw",
  140. remove_linebreaks : false,
  141. forced_root_block: false,
  142. force_br_newlines: true,
  143. // invalid_elements : "p, div, span",
  144. imagetools_toolbar: 'editimage',
  145. end_container_on_empty_block: true,
  146. powerpaste_word_import: 'clean',
  147. paste_merge_formats: false,
  148. code_dialog_height: 500,
  149. code_dialog_width: 900,
  150. advlist_bullet_styles: 'square',
  151. advlist_number_styles: 'default',
  152. imagetools_cors_hosts: ['account.aliyun.com'],
  153. default_link_target: '_blank',
  154. // table_default_styles: {
  155. // width: '80%',
  156. // border: '1',
  157. // cellspacing:"0",
  158. // cellpadding:"0"
  159. // },
  160. automatic_uploads: false,
  161. remove_linebreaks: false,
  162. link_title: true,
  163. relative_urls: false,
  164. paste_data_images: false,
  165. nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
  166. init_instance_callback: editor => {
  167. if (_this.value) {
  168. editor.setContent(_this.value)
  169. }
  170. _this.hasInit = true
  171. editor.on('NodeChange Change KeyUp SetContent', () => {
  172. this.hasChange = true
  173. this.$emit('input', editor.getContent())
  174. })
  175. editor.on('paste', (evt) => {
  176. // 监听粘贴事件
  177. _this.onPaste(evt)
  178. })
  179. },
  180. setup(editor) {
  181. editor.on('FullscreenStateChanged', (e) => {
  182. _this.fullscreen = e.state
  183. })
  184. },
  185. // it will try to keep these URLs intact
  186. // https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
  187. // https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
  188. // convert_urls: true,
  189. // automatic_uploads: true
  190. // 整合七牛上传
  191. // images_upload_handler(blobInfo, success, failure, progress) {
  192. // console.log("images_upload_handler" )
  193. // progress(0);
  194. // const formData = new FormData();
  195. // // formData.append('file', blobInfo.blob() );
  196. // formData.append('file', blobInfo.blob(), blobInfo.filename());
  197. // axios.post(this.uploadUrl, formData).then(res => {
  198. // console.log("res", res)
  199. // success( res.data.filename );
  200. // }) .catch(err => {
  201. // failure("出现未知问题");
  202. // console.log(err);
  203. // });
  204. // }
  205. })
  206. },
  207. destroyTinymce() {
  208. const tinymce = window.tinymce.get(this.tinymceId)
  209. if (this.fullscreen) {
  210. tinymce.execCommand('mceFullScreen')
  211. }
  212. if (tinymce) {
  213. tinymce.destroy()
  214. }
  215. },
  216. onPaste(e) {
  217. var items=e.clipboardData.items;
  218. var item = items[0];
  219. if ( item.kind != 'file' || item.type.indexOf('image/') == -1 ) {
  220. return
  221. }
  222. var file = item.getAsFile();
  223. const formData = new FormData()
  224. formData.append('file', file)
  225. // 上传图片
  226. axios.post(this.uploadUrl, formData, { headers: this.headers }).then(res => {
  227. if (res.data.code === 200) {
  228. window.tinymce.get(this.tinymceId).insertContent(`<img style="width:100%" src="http://smoa.ndjsxh.cn:8888/preview/${res.data.data.filename}" >`)
  229. } else {
  230. this.$message.error('图片上传失败,联系开发人员')
  231. }
  232. })
  233. },
  234. setContent(value) {
  235. window.tinymce.get(this.tinymceId).setContent(value)
  236. },
  237. getContent() {
  238. window.tinymce.get(this.tinymceId).getContent()
  239. },
  240. imageSuccessCBK(arr) {
  241. arr.forEach(v => window.tinymce.get(this.tinymceId).insertContent(`<img style="width:100%" src="${v.url}" >`))
  242. }
  243. }
  244. }
  245. </script>
  246. <style lang="scss" scoped>
  247. .tinymce-container {
  248. position: relative;
  249. line-height: normal;
  250. }
  251. .tinymce-container {
  252. ::v-deep {
  253. .mce-fullscreen {
  254. z-index: 10000;
  255. }
  256. }
  257. }
  258. .tinymce-textarea {
  259. visibility: hidden;
  260. z-index: -1;
  261. }
  262. .editor-custom-btn-container {
  263. position: absolute;
  264. right: 4px;
  265. top: 4px;
  266. /*z-index: 2005;*/
  267. }
  268. .fullscreen .editor-custom-btn-container {
  269. z-index: 10000;
  270. position: fixed;
  271. }
  272. .editor-upload-btn {
  273. display: inline-block;
  274. }
  275. </style>