index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <template>
  2. <div class="ibps-ueditor" v-on="listeners">
  3. <script ref="script" :name="name" type="text/plain" />
  4. </div>
  5. </template>
  6. <script>
  7. // 参考代码 https://github.com/HaoChuan9421/vue-ueditor-wrap 的v2.4.2版本[2019-05-30]
  8. import LoadEvent from './utils/Event.js'
  9. import Debounce from './utils/Debounce.js'
  10. import PopupManager from '@/utils/popup'
  11. import defaultConfig from './ueditor.config'
  12. export default {
  13. name: 'ibps-ueditor',
  14. inject: {
  15. elForm: {
  16. default: ''
  17. },
  18. elFormItem: {
  19. default: ''
  20. }
  21. },
  22. props: {
  23. // v-model 实现方式
  24. mode: {
  25. type: String,
  26. default: 'observer',
  27. validator: function(value) {
  28. // 1. observer 借助 MutationObserver API https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
  29. // 2. listener 借助 UEditor 的 contentChange 事件 https://ueditor.baidu.com/doc/#UE.Editor:contentChange
  30. return ['observer', 'listener'].indexOf(value) !== -1
  31. }
  32. },
  33. value: {
  34. type: String,
  35. default: ''
  36. },
  37. config: {
  38. type: Object,
  39. default: function() {
  40. return {}
  41. }
  42. },
  43. init: {
  44. type: Function,
  45. default: function() {
  46. return () => {}
  47. }
  48. },
  49. readonly: {
  50. type: Boolean,
  51. default: false
  52. },
  53. destroy: {
  54. type: Boolean,
  55. default: false
  56. },
  57. name: {
  58. type: String,
  59. default: ''
  60. },
  61. observerDebounceTime: {
  62. type: Number,
  63. default: 50,
  64. validator: function(value) {
  65. return value >= 20
  66. }
  67. },
  68. observerOptions: {
  69. type: Object,
  70. default: function() {
  71. // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit
  72. return {
  73. attributes: true, // 是否监听 DOM 元素的属性变化
  74. attributeFilter: ['src', 'style', 'type', 'name'], // 只有在该数组中的属性值的变化才会监听
  75. characterData: true, // 是否监听文本节点
  76. childList: true, // 是否监听子节点
  77. subtree: true // 是否监听后代元素
  78. }
  79. }
  80. },
  81. // 本组件提供对普通 Vue 项目和 Nuxt 项目开箱即用的支持,但如果是自己搭建的 Vue SSR 项目,可能需要自行区分是客户端还是服务端环境并跳过环境检测,直接初始化
  82. forceInit: {
  83. type: Boolean,
  84. default: false
  85. },
  86. zIndex: {
  87. type: Number
  88. }
  89. },
  90. data() {
  91. return {
  92. editor: null,
  93. id: null,
  94. status: 0,
  95. initValue: '',
  96. defaultConfig: defaultConfig()
  97. }
  98. },
  99. computed: {
  100. mixedConfig() {
  101. return Object.assign({
  102. readonly: this.readonly,
  103. zIndex: this.getZindex()
  104. }, this.defaultConfig, this.config)
  105. },
  106. listeners() {
  107. return {
  108. ...this.$listeners
  109. }
  110. }
  111. },
  112. // v-model语法糖实现
  113. watch: {
  114. value: {
  115. handler(value) {
  116. // 修复值为空无法双向绑定的问题
  117. if (value === null || value === undefined) {
  118. value = ''
  119. }
  120. // 0: 尚未初始化 1: 开始初始化但尚未ready 2 初始化完成并已ready
  121. switch (this.status) {
  122. case 0:
  123. this.status = 1
  124. this.initValue = value;
  125. // 判断执行环境是服务端还是客户端,这里的 process.client 是 Nuxt 添加的环境变量
  126. (this.forceInit || (typeof process !== 'undefined' && process.client) || typeof window !== 'undefined') && this._checkDependencies().then(() => {
  127. this.$refs.script ? this._initEditor() : this.$nextTick(() => this._initEditor())
  128. })
  129. break
  130. case 1:
  131. this.initValue = value
  132. break
  133. case 2:
  134. this._setContent(value)
  135. break
  136. default:
  137. break
  138. }
  139. },
  140. immediate: true
  141. }
  142. },
  143. deactivated() {
  144. this.editor && this.editor.removeListener('contentChange', this.contentChangeHandler)
  145. this.observer && this.observer.disconnect()
  146. },
  147. beforeDestroy() {
  148. if (this.destroy && this.editor && this.editor.destroy) {
  149. this.editor.destroy()
  150. }
  151. if (this.observer && this.observer.disconnect) {
  152. this.observer.disconnect()
  153. }
  154. },
  155. methods: {
  156. // 添加自定义按钮(自定义按钮,自定义弹窗等操作从 2.2.0 版本开始不再考虑直接集成,这会使得组件和 UEditor 过度耦合,但为了兼容一些老版用户的写法,这个方法依然保留)
  157. registerButton({ name, icon, tip, handler, index, UE = window.UE }) {
  158. UE.registerUI(name, (editor, name) => {
  159. editor.registerCommand(name, {
  160. execCommand: () => {
  161. handler(editor, name)
  162. }
  163. })
  164. const btn = new UE.ui.Button({
  165. name,
  166. title: tip,
  167. cssRules: `background-image: url(${icon}) !important;background-size: cover;`,
  168. onclick() {
  169. editor.execCommand(name)
  170. }
  171. })
  172. editor.addListener('selectionchange', () => {
  173. const state = editor.queryCommandState(name)
  174. if (state === -1) {
  175. btn.setDisabled(true)
  176. btn.setChecked(false)
  177. } else {
  178. btn.setDisabled(false)
  179. btn.setChecked(state)
  180. }
  181. })
  182. return btn
  183. }, index, this.id)
  184. },
  185. // 实例化编辑器
  186. _initEditor() {
  187. this.$refs.script.id = this.id = 'ibps_editor_' + Math.random().toString(16).slice(-6) // 这么做是为了支持 Vue SSR,因为如果把 id 属性放在 data 里会导致服务端和客户端分别计算该属性的值,而造成 id 不匹配无法初始化的 BUG
  188. this.init()
  189. this.$emit('before-init', this.id, this.mixedConfig)
  190. this.$emit('beforeInit', this.id, this.mixedConfig) // 虽然这个驼峰的写法会导致使用 DOM 模版时出现监听事件自动转小写的 BUG,但如果经过编译的话并不会有这个问题,为了兼容历史版本,不做删除,参考 https://vuejs.org/v2/guide/components-custom-events.html#Event-Names
  191. this.editor = window.UE.getEditor(this.id, this.mixedConfig)
  192. this.editor.addListener('ready', () => {
  193. if (this.status === 2) { // 使用 keep-alive 组件会出现这种情况
  194. this.editor.setContent(this.value)
  195. } else {
  196. this.status = 2
  197. this.$emit('ready', this.editor)
  198. if (this.initValue) {
  199. this.editor.setContent(this.initValue)
  200. }
  201. }
  202. if (this.mode === 'observer' && window.MutationObserver) {
  203. this._observerChangeListener()
  204. } else {
  205. this._normalChangeListener()
  206. }
  207. })
  208. // zxh 增加click 事件
  209. this.editor.addListener('click', (editor) => {
  210. this.$emit('click', editor)
  211. })
  212. },
  213. // 检测依赖,确保 UEditor 资源文件已加载完毕
  214. _checkDependencies() {
  215. return new Promise((resolve, reject) => {
  216. // 判断ueditor.config.js和ueditor.all.js是否均已加载(仅加载完ueditor.config.js时UE对象和UEDITOR_CONFIG对象存在,仅加载完ueditor.all.js时UEDITOR_CONFIG对象存在,但为空对象)
  217. const scriptsLoaded = !!window.UE && !!window.UEDITOR_CONFIG && Object.keys(window.UEDITOR_CONFIG).length !== 0 && !!window.UE.getEditor
  218. if (scriptsLoaded) {
  219. resolve()
  220. } else if (window['$loadEnv']) { // 利用订阅发布,确保同时渲染多个组件时,不会重复创建script标签
  221. window['$loadEnv'].on('scriptsLoaded', () => {
  222. resolve()
  223. })
  224. } else {
  225. window['$loadEnv'] = new LoadEvent()
  226. // 如果在其他地方只引用ueditor.all.min.js,在加载ueditor.config.js之后仍需要重新加载ueditor.all.min.js,所以必须确保ueditor.config.js已加载
  227. this._loadConfig().then(() => this._loadCore()).then(() => {
  228. resolve()
  229. window['$loadEnv'].emit('scriptsLoaded')
  230. })
  231. }
  232. })
  233. },
  234. _loadConfig() {
  235. return new Promise((resolve, reject) => {
  236. if (window.UE && window.UEDITOR_CONFIG && Object.keys(window.UEDITOR_CONFIG).length !== 0) {
  237. resolve()
  238. return
  239. }
  240. const configScript = document.createElement('script')
  241. configScript.type = 'text/javascript'
  242. configScript.src = this.mixedConfig.UEDITOR_HOME_URL + 'ueditor.config.js'
  243. document.getElementsByTagName('head')[0].appendChild(configScript)
  244. configScript.onload = function() {
  245. if (window.UE && window.UEDITOR_CONFIG && Object.keys(window.UEDITOR_CONFIG).length !== 0) {
  246. resolve()
  247. } else {
  248. console.error('加载ueditor.config.js失败,请检查您的配置地址UEDITOR_HOME_URL填写是否正确!\n', configScript.src)
  249. }
  250. }
  251. })
  252. },
  253. _loadCore() {
  254. return new Promise((resolve, reject) => {
  255. if (window.UE && window.UE.getEditor) {
  256. resolve()
  257. return
  258. }
  259. const coreScript = document.createElement('script')
  260. coreScript.type = 'text/javascript'
  261. coreScript.src = this.mixedConfig.UEDITOR_HOME_URL + 'ueditor.all.min.js'
  262. document.getElementsByTagName('head')[0].appendChild(coreScript)
  263. coreScript.onload = function() {
  264. if (window.UE && window.UE.getEditor) {
  265. resolve()
  266. } else {
  267. console.error('加载ueditor.all.min.js失败,请检查您的配置地址UEDITOR_HOME_URL填写是否正确!\n', coreScript.src)
  268. }
  269. }
  270. })
  271. },
  272. // 设置内容
  273. _setContent(value) {
  274. if (this.editor) {
  275. value === this.editor.getContent() || this.editor.setContent(value)
  276. }
  277. },
  278. contentChangeHandler() {
  279. this.$emit('input', this.editor.getContent())
  280. },
  281. execCommand(cmd, params) {
  282. if (this.editor) {
  283. this.editor.execCommand(cmd, params)
  284. }
  285. },
  286. // 基于 UEditor 的 contentChange 事件
  287. _normalChangeListener() {
  288. if (this.editor) {
  289. this.editor.addListener('contentChange', this.contentChangeHandler)
  290. }
  291. },
  292. // 基于 MutationObserver API
  293. _observerChangeListener() {
  294. const changeHandle = (mutationsList) => {
  295. if (this.editor.document.getElementById('baidu_pastebin')) {
  296. return
  297. }
  298. this.$emit('input', this.editor.getContent())
  299. }
  300. // 函数防抖
  301. this.observer = new MutationObserver(Debounce(changeHandle, this.observerDebounceTime))
  302. this.observer.observe(this.editor.body, this.observerOptions)
  303. },
  304. /**
  305. * zxh 修复zindex 不是最高的被遮住
  306. */
  307. getZindex() {
  308. if (this.zIndex) {
  309. return this.zIndex
  310. }
  311. const zIndex = PopupManager.getZIndex()
  312. return zIndex - 1
  313. }
  314. }
  315. }
  316. </script>