552 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
		
		
			
		
	
	
			552 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
|  | /** | ||
|  |  * @fileoverview editable 插件 | ||
|  |  */ | ||
|  | const config = require('./config') | ||
|  | const Parser = require('../parser') | ||
|  | 
 | ||
|  | function Editable (vm) { | ||
|  |   this.vm = vm | ||
|  |   this.editHistory = [] // 历史记录
 | ||
|  |   this.editI = -1 // 历史记录指针
 | ||
|  |   vm._mask = [] // 蒙版被点击时进行的操作
 | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 移动历史记录指针 | ||
|  |    * @param {Number} num 移动距离 | ||
|  |    */ | ||
|  |   const move = num => { | ||
|  |     const item = this.editHistory[this.editI + num] | ||
|  |     if (item) { | ||
|  |       this.editI += num | ||
|  |       vm.setData({ | ||
|  |         [item.key]: item.value | ||
|  |       }) | ||
|  |     } | ||
|  |   } | ||
|  |   vm.undo = () => move(-1) // 撤销
 | ||
|  |   vm.redo = () => move(1) // 重做
 | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 更新记录 | ||
|  |    * @param {String} path 路径 | ||
|  |    * @param {*} oldVal 旧值 | ||
|  |    * @param {*} newVal 新值 | ||
|  |    * @param {Boolean} set 是否更新到视图 | ||
|  |    * @private | ||
|  |    */ | ||
|  |   vm._editVal = (path, oldVal, newVal, set) => { | ||
|  |     // 当前指针后的内容去除
 | ||
|  |     while (this.editI < this.editHistory.length - 1) { | ||
|  |       this.editHistory.pop() | ||
|  |     } | ||
|  | 
 | ||
|  |     // 最多存储 30 条操作记录
 | ||
|  |     while (this.editHistory.length > 30) { | ||
|  |       this.editHistory.pop() | ||
|  |       this.editI-- | ||
|  |     } | ||
|  | 
 | ||
|  |     const last = this.editHistory[this.editHistory.length - 1] | ||
|  |     if (!last || last.key !== path) { | ||
|  |       if (last) { | ||
|  |         // 去掉上一次的新值
 | ||
|  |         this.editHistory.pop() | ||
|  |         this.editI-- | ||
|  |       } | ||
|  |       // 存入这一次的旧值
 | ||
|  |       this.editHistory.push({ | ||
|  |         key: path, | ||
|  |         value: oldVal | ||
|  |       }) | ||
|  |       this.editI++ | ||
|  |     } | ||
|  | 
 | ||
|  |     // 存入本次的新值
 | ||
|  |     this.editHistory.push({ | ||
|  |       key: path, | ||
|  |       value: newVal | ||
|  |     }) | ||
|  |     this.editI++ | ||
|  | 
 | ||
|  |     // 更新到视图
 | ||
|  |     if (set) { | ||
|  |       vm.setData({ | ||
|  |         [path]: newVal | ||
|  |       }) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 获取菜单项 | ||
|  |    * @private | ||
|  |    */ | ||
|  |   vm._getItem = function (node, up, down) { | ||
|  |     let items | ||
|  |     let i | ||
|  |     if (node === 'color') { | ||
|  |       return config.color | ||
|  |     } | ||
|  |     if (node.name === 'img') { | ||
|  |       items = config.img.slice(0) | ||
|  |       if (!vm.getSrc) { | ||
|  |         i = items.indexOf('换图') | ||
|  |         if (i !== -1) { | ||
|  |           items.splice(i, 1) | ||
|  |         } | ||
|  |         i = items.indexOf('超链接') | ||
|  |         if (i !== -1) { | ||
|  |           items.splice(i, 1) | ||
|  |         } | ||
|  |         i = items.indexOf('预览图') | ||
|  |         if (i !== -1) { | ||
|  |           items.splice(i, 1) | ||
|  |         } | ||
|  |       } | ||
|  |       i = items.indexOf('禁用预览') | ||
|  |       if (i !== -1 && node.attrs.ignore) { | ||
|  |         items[i] = '启用预览' | ||
|  |       } | ||
|  |     } else if (node.name === 'a') { | ||
|  |       items = config.link.slice(0) | ||
|  |       if (!vm.getSrc) { | ||
|  |         i = items.indexOf('更换链接') | ||
|  |         if (i !== -1) { | ||
|  |           items.splice(i, 1) | ||
|  |         } | ||
|  |       } | ||
|  |     } else if (node.name === 'video' || node.name === 'audio') { | ||
|  |       items = config.media.slice(0) | ||
|  |       i = items.indexOf('封面') | ||
|  |       if (!vm.getSrc && i !== -1) { | ||
|  |         items.splice(i, 1) | ||
|  |       } | ||
|  |       i = items.indexOf('循环') | ||
|  |       if (node.attrs.loop && i !== -1) { | ||
|  |         items[i] = '不循环' | ||
|  |       } | ||
|  |       i = items.indexOf('自动播放') | ||
|  |       if (node.attrs.autoplay && i !== -1) { | ||
|  |         items[i] = '不自动播放' | ||
|  |       } | ||
|  |     } else if (node.name === 'card') { | ||
|  |       items = config.card.slice(0) | ||
|  |     } else { | ||
|  |       items = config.node.slice(0) | ||
|  |     } | ||
|  |     if (!up) { | ||
|  |       i = items.indexOf('上移') | ||
|  |       if (i !== -1) { | ||
|  |         items.splice(i, 1) | ||
|  |       } | ||
|  |     } | ||
|  |     if (!down) { | ||
|  |       i = items.indexOf('下移') | ||
|  |       if (i !== -1) { | ||
|  |         items.splice(i, 1) | ||
|  |       } | ||
|  |     } | ||
|  |     return items | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 显示 tooltip | ||
|  |    * @param {object} obj | ||
|  |    * @private | ||
|  |    */ | ||
|  |   vm._tooltip = function (obj) { | ||
|  |     vm.setData({ | ||
|  |       tooltip: { | ||
|  |         top: obj.top, | ||
|  |         items: obj.items | ||
|  |       } | ||
|  |     }) | ||
|  |     vm._tooltipcb = obj.success | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 显示滚动条 | ||
|  |    * @param {object} obj | ||
|  |    * @private | ||
|  |    */ | ||
|  |   vm._slider = function (obj) { | ||
|  |     vm.setData({ | ||
|  |       slider: { | ||
|  |         min: obj.min, | ||
|  |         max: obj.max, | ||
|  |         value: obj.value, | ||
|  |         top: obj.top | ||
|  |       } | ||
|  |     }) | ||
|  |     vm._slideringcb = obj.changing | ||
|  |     vm._slidercb = obj.change | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 显示颜色选择 | ||
|  |    * @param {object} obj | ||
|  |    * @private | ||
|  |    */ | ||
|  |   vm._color = function (obj) { | ||
|  |     vm.setData({ | ||
|  |       color: { | ||
|  |         items: obj.items, | ||
|  |         top: obj.top | ||
|  |       } | ||
|  |     }) | ||
|  |     vm._colorcb = obj.success | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 点击蒙版 | ||
|  |    * @private | ||
|  |    */ | ||
|  |   vm._maskTap = function () { | ||
|  |     // 隐藏所有悬浮窗
 | ||
|  |     while (this._mask.length) { | ||
|  |       (this._mask.pop())() | ||
|  |     } | ||
|  |     const data = {} | ||
|  |     if (this.data.tooltip) { | ||
|  |       data.tooltip = null | ||
|  |     } | ||
|  |     if (this.data.slider) { | ||
|  |       data.slider = null | ||
|  |     } | ||
|  |     if (this.data.color) { | ||
|  |       data.color = null | ||
|  |     } | ||
|  |     if (this.data.tooltip || this.data.slider || this.data.color) { | ||
|  |       this.setData(data) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 插入节点 | ||
|  |    * @param {Object} node | ||
|  |    */ | ||
|  |   function insert (node) { | ||
|  |     if (vm._edit) { | ||
|  |       vm._edit.insert(node) | ||
|  |     } else { | ||
|  |       const nodes = vm.data.nodes.slice(0) | ||
|  |       nodes.push(node) | ||
|  |       vm._editVal('nodes', vm.data.nodes, nodes, true) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入指定 html 内容 | ||
|  |    * @param {String} html 内容 | ||
|  |    */ | ||
|  |   vm.insertHtml = html => { | ||
|  |     this.inserting = true | ||
|  |     const arr = new Parser(vm).parse(html) | ||
|  |     this.inserting = undefined | ||
|  |     for (let i = 0; i < arr.length; i++) { | ||
|  |       insert(arr[i]) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入图片 | ||
|  |    */ | ||
|  |   vm.insertImg = function () { | ||
|  |     vm.getSrc && vm.getSrc('img').then(src => { | ||
|  |       if (typeof src === 'string') { | ||
|  |         src = [src] | ||
|  |       } | ||
|  |       const parser = new Parser(vm) | ||
|  |       for (let i = 0; i < src.length; i++) { | ||
|  |         insert({ | ||
|  |           name: 'img', | ||
|  |           attrs: { | ||
|  |             src: parser.getUrl(src[i]) | ||
|  |           } | ||
|  |         }) | ||
|  |       } | ||
|  |     }).catch(() => { }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入一个链接 | ||
|  |    */ | ||
|  |   vm.insertLink = function () { | ||
|  |     vm.getSrc && vm.getSrc('link').then(url => { | ||
|  |       insert({ | ||
|  |         name: 'a', | ||
|  |         attrs: { | ||
|  |           href: url | ||
|  |         }, | ||
|  |         children: [{ | ||
|  |           type: 'text', | ||
|  |           text: url | ||
|  |         }] | ||
|  |       }) | ||
|  |     }).catch(() => { }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入一个表格 | ||
|  |    * @param {Number} rows 行数 | ||
|  |    * @param {Number} cols 列数 | ||
|  |    */ | ||
|  |   vm.insertTable = function (rows, cols) { | ||
|  |     const table = { | ||
|  |       name: 'table', | ||
|  |       attrs: { | ||
|  |         style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray' | ||
|  |       }, | ||
|  |       children: [] | ||
|  |     } | ||
|  |     for (let i = 0; i < rows; i++) { | ||
|  |       const tr = { | ||
|  |         name: 'tr', | ||
|  |         attrs: {}, | ||
|  |         children: [] | ||
|  |       } | ||
|  |       for (let j = 0; j < cols; j++) { | ||
|  |         tr.children.push({ | ||
|  |           name: 'td', | ||
|  |           attrs: { | ||
|  |             style: 'padding:2px;border:1px solid gray' | ||
|  |           }, | ||
|  |           children: [{ | ||
|  |             type: 'text', | ||
|  |             text: '' | ||
|  |           }] | ||
|  |         }) | ||
|  |       } | ||
|  |       table.children.push(tr) | ||
|  |     } | ||
|  |     insert(table) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 插入视频/音频 | ||
|  |    * @param {Object} node | ||
|  |    */ | ||
|  |   function insertMedia (node) { | ||
|  |     if (typeof node.src === 'string') { | ||
|  |       node.src = [node.src] | ||
|  |     } | ||
|  |     const parser = new Parser(vm) | ||
|  |     // 拼接主域名
 | ||
|  |     for (let i = 0; i < node.src.length; i++) { | ||
|  |       node.src[i] = parser.getUrl(node.src[i]) | ||
|  |     } | ||
|  |     insert({ | ||
|  |       name: 'div', | ||
|  |       attrs: { | ||
|  |         style: 'text-align:center' | ||
|  |       }, | ||
|  |       children: [node] | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入一个视频 | ||
|  |    */ | ||
|  |   vm.insertVideo = function () { | ||
|  |     vm.getSrc && vm.getSrc('video').then(src => { | ||
|  |       insertMedia({ | ||
|  |         name: 'video', | ||
|  |         attrs: { | ||
|  |           controls: 'T' | ||
|  |         }, | ||
|  |         src | ||
|  |       }) | ||
|  |     }).catch(() => { }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入一个音频 | ||
|  |    */ | ||
|  |   vm.insertAudio = function () { | ||
|  |     vm.getSrc && vm.getSrc('audio').then(attrs => { | ||
|  |       let src | ||
|  |       if (attrs.src) { | ||
|  |         src = attrs.src | ||
|  |         attrs.src = undefined | ||
|  |       } else { | ||
|  |         src = attrs | ||
|  |         attrs = {} | ||
|  |       } | ||
|  |       attrs.controls = 'T' | ||
|  |       insertMedia({ | ||
|  |         name: 'audio', | ||
|  |         attrs, | ||
|  |         src | ||
|  |       }) | ||
|  |     }).catch(() => { }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 在光标处插入一段文本 | ||
|  |    */ | ||
|  |   vm.insertText = function () { | ||
|  |     insert({ | ||
|  |       name: 'p', | ||
|  |       attrs: {}, | ||
|  |       children: [{ | ||
|  |         type: 'text', | ||
|  |         text: '' | ||
|  |       }] | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 清空内容 | ||
|  |    */ | ||
|  |   vm.clear = function () { | ||
|  |     vm._maskTap() | ||
|  |     vm._edit = undefined | ||
|  |     vm.setData({ | ||
|  |       nodes: [{ | ||
|  |         name: 'p', | ||
|  |         attrs: {}, | ||
|  |         children: [{ | ||
|  |           type: 'text', | ||
|  |           text: '' | ||
|  |         }] | ||
|  |       }] | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * @description 获取编辑后的 html | ||
|  |    */ | ||
|  |   vm.getContent = function () { | ||
|  |     let html = ''; | ||
|  |     // 递归遍历获取
 | ||
|  |     (function traversal (nodes, table) { | ||
|  |       for (let i = 0; i < nodes.length; i++) { | ||
|  |         let item = nodes[i] | ||
|  |         if (item.type === 'text') { | ||
|  |           // 编码实体
 | ||
|  |           html += item.text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>').replace(/\xa0/g, ' ') | ||
|  |         } else { | ||
|  |           // 还原被转换的 svg
 | ||
|  |           if (item.name === 'img' && (item.attrs.src || '').includes('data:image/svg+xml;utf8,')) { | ||
|  |             html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"') | ||
|  |             continue | ||
|  |           } else if (item.name === 'video' || item.name === 'audio') { | ||
|  |             // 还原 video 和 audio 的 source
 | ||
|  |             if (item.src.length > 1) { | ||
|  |               item.children = [] | ||
|  |               for (let j = 0; j < item.src.length; j++) { | ||
|  |                 item.children.push({ | ||
|  |                   name: 'source', | ||
|  |                   attrs: { | ||
|  |                     src: item.src[j] | ||
|  |                   } | ||
|  |                 }) | ||
|  |               } | ||
|  |             } else { | ||
|  |               item.attrs.src = item.src[0] | ||
|  |             } | ||
|  |           } else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') { | ||
|  |             // 还原滚动层
 | ||
|  |             item = item.children[0] | ||
|  |           } | ||
|  |           // 还原 table
 | ||
|  |           if (item.name === 'table') { | ||
|  |             table = item.attrs | ||
|  |             if ((item.attrs.style || '').includes('display:grid')) { | ||
|  |               item.attrs.style = item.attrs.style.split('display:grid')[0] | ||
|  |               const children = [{ | ||
|  |                 name: 'tr', | ||
|  |                 attrs: {}, | ||
|  |                 children: [] | ||
|  |               }] | ||
|  |               for (let j = 0; j < item.children.length; j++) { | ||
|  |                 item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '') | ||
|  |                 if (item.children[j].r !== children.length) { | ||
|  |                   children.push({ | ||
|  |                     name: 'tr', | ||
|  |                     attrs: {}, | ||
|  |                     children: [item.children[j]] | ||
|  |                   }) | ||
|  |                 } else { | ||
|  |                   children[children.length - 1].children.push(item.children[j]) | ||
|  |                 } | ||
|  |               } | ||
|  |               item.children = children | ||
|  |             } | ||
|  |           } | ||
|  |           html += '<' + item.name | ||
|  |           for (const attr in item.attrs) { | ||
|  |             let val = item.attrs[attr] | ||
|  |             if (!val) continue | ||
|  |             // bool 型省略值
 | ||
|  |             if (val === 'T' || val === true) { | ||
|  |               html += ' ' + attr | ||
|  |               continue | ||
|  |             } else if (item.name[0] === 't' && attr === 'style' && table) { | ||
|  |               // 取消为了显示 table 添加的 style
 | ||
|  |               val = val.replace(/;*display:table[^;]*/, '') | ||
|  |               if (table.border) { | ||
|  |                 val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '') | ||
|  |               } | ||
|  |               if (table.cellpadding) { | ||
|  |                 val = val.replace(/padding[^;]+;*/g, '') | ||
|  |               } | ||
|  |               if (!val) continue | ||
|  |             } | ||
|  |             html += ' ' + attr + '="' + val.replace(/"/g, '"') + '"' | ||
|  |           } | ||
|  |           html += '>' | ||
|  |           if (item.children) { | ||
|  |             traversal(item.children, table) | ||
|  |             html += '</' + item.name + '>' | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     })(vm.data.nodes) | ||
|  | 
 | ||
|  |     // 其他插件处理
 | ||
|  |     for (let i = vm.plugins.length; i--;) { | ||
|  |       if (vm.plugins[i].onGetContent) { | ||
|  |         html = vm.plugins[i].onGetContent(html) || html | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return html | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Editable.prototype.onUpdate = function (content, config) { | ||
|  |   if (this.vm.properties.editable) { | ||
|  |     this.vm._maskTap() | ||
|  |     config.entities.amp = '&' | ||
|  |     if (!this.inserting) { | ||
|  |       this.vm._edit = undefined | ||
|  |       if (!content) { | ||
|  |         setTimeout(() => { | ||
|  |           this.vm.setData({ | ||
|  |             nodes: [{ | ||
|  |               name: 'p', | ||
|  |               attrs: {}, | ||
|  |               children: [{ | ||
|  |                 type: 'text', | ||
|  |                 text: '' | ||
|  |               }] | ||
|  |             }] | ||
|  |           }) | ||
|  |         }, 0) | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Editable.prototype.onParse = function (node) { | ||
|  |   // 空白单元格可编辑
 | ||
|  |   if (this.vm.properties.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) { | ||
|  |     node.children.push({ | ||
|  |       type: 'text', | ||
|  |       text: '' | ||
|  |     }) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = Editable |