554 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			554 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 = [] // 蒙版被点击时进行的操作
 | 
						|
 | 
						|
  vm._setData = function (path, val) {
 | 
						|
    const paths = path.split('.')
 | 
						|
    let target = vm
 | 
						|
    for (let i = 0; i < paths.length - 1; i++) {
 | 
						|
      target = target[paths[i]]
 | 
						|
    }
 | 
						|
    vm.$set(target, paths.pop(), val)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @description 移动历史记录指针
 | 
						|
   * @param {Number} num 移动距离
 | 
						|
   */
 | 
						|
  const move = num => {
 | 
						|
    setTimeout(() => {
 | 
						|
      const item = this.editHistory[this.editI + num]
 | 
						|
      if (item) {
 | 
						|
        this.editI += num
 | 
						|
        vm._setData(item.key, item.value)
 | 
						|
      }
 | 
						|
    }, 200)
 | 
						|
  }
 | 
						|
  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.$set(vm, 'tooltip', {
 | 
						|
      top: obj.top,
 | 
						|
      items: obj.items
 | 
						|
    })
 | 
						|
    vm._tooltipcb = obj.success
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @description 显示滚动条
 | 
						|
   * @param {object} obj
 | 
						|
   * @private
 | 
						|
   */
 | 
						|
  vm._slider = function (obj) {
 | 
						|
    vm.$set(vm, '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.$set(vm, 'color', {
 | 
						|
      items: obj.items,
 | 
						|
      top: obj.top
 | 
						|
    })
 | 
						|
    vm._colorcb = obj.success
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @description 点击蒙版
 | 
						|
   * @private
 | 
						|
   */
 | 
						|
  vm._maskTap = function () {
 | 
						|
    // 隐藏所有悬浮窗
 | 
						|
    while (vm._mask.length) {
 | 
						|
      (vm._mask.pop())()
 | 
						|
    }
 | 
						|
    if (vm.tooltip) {
 | 
						|
      vm.$set(vm, 'tooltip', null)
 | 
						|
    }
 | 
						|
    if (vm.slider) {
 | 
						|
      vm.$set(vm, 'slider', null)
 | 
						|
    }
 | 
						|
    if (vm.color) {
 | 
						|
      vm.$set(vm, 'color', null)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @description 插入节点
 | 
						|
   * @param {Object} node
 | 
						|
   */
 | 
						|
  function insert (node) {
 | 
						|
    if (vm._edit) {
 | 
						|
      vm._edit.insert(node)
 | 
						|
    } else {
 | 
						|
      const nodes = vm.nodes.slice(0)
 | 
						|
      nodes.push(node)
 | 
						|
      vm._editVal('nodes', vm.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'
 | 
						|
        },
 | 
						|
        children: [],
 | 
						|
        src,
 | 
						|
        // #ifdef APP-PLUS
 | 
						|
        html: `<video src="${src}" style="width:100%;height:100%"></video>`
 | 
						|
        // #endif
 | 
						|
      })
 | 
						|
    }).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,
 | 
						|
        children: [],
 | 
						|
        src
 | 
						|
      })
 | 
						|
    }).catch(() => { })
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @description 在光标处插入一段文本
 | 
						|
   */
 | 
						|
  vm.insertText = function () {
 | 
						|
    insert({
 | 
						|
      name: 'p',
 | 
						|
      attrs: {},
 | 
						|
      children: [{
 | 
						|
        type: 'text',
 | 
						|
        text: ''
 | 
						|
      }]
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @description 清空内容
 | 
						|
   */
 | 
						|
  vm.clear = function () {
 | 
						|
    vm._maskTap()
 | 
						|
    vm._edit = undefined
 | 
						|
    vm.$set(vm, '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 {
 | 
						|
          if (item.name === 'img') {
 | 
						|
            item.attrs.i = ''
 | 
						|
            // 还原被转换的 svg
 | 
						|
            if ((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
 | 
						|
            item = JSON.parse(JSON.stringify(item))
 | 
						|
            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') {
 | 
						|
            item = JSON.parse(JSON.stringify(item))
 | 
						|
            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
 | 
						|
            if (val === 'T' || val === true) {
 | 
						|
              // bool 型省略值
 | 
						|
              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.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.editable) {
 | 
						|
    this.vm._maskTap()
 | 
						|
    config.entities.amp = '&'
 | 
						|
    if (!this.inserting) {
 | 
						|
      this.vm._edit = undefined
 | 
						|
      if (!content) {
 | 
						|
        setTimeout(() => {
 | 
						|
          this.vm.$set(this.vm, 'nodes', [{
 | 
						|
            name: 'p',
 | 
						|
            attrs: {},
 | 
						|
            children: [{
 | 
						|
              type: 'text',
 | 
						|
              text: ''
 | 
						|
            }]
 | 
						|
          }])
 | 
						|
        }, 0)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
Editable.prototype.onParse = function (node) {
 | 
						|
  // 空白单元格可编辑
 | 
						|
  if (this.vm.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
 | 
						|
    node.children.push({
 | 
						|
      type: 'text',
 | 
						|
      text: ''
 | 
						|
    })
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports = Editable
 |