X-Spreadsheet扩展插入图片功能

1,101 阅读3分钟

x-spreadsheet,一个基于浏览器的轻量级的js表格。地址:github.com/myliang/x-s… ,但它没有插入图片功能,今天我就来说一下我的实现方法。

关于插入图片,我的思路是2种:canvas渲染、dom挂载。

为了简单,我直接使用了第1种:canvas渲染。下面说一下具体步骤:

  1. 在工具栏插入图片按钮,效果如下:

微信图片_20220715121322.png

这个图标我们可以直接从作者的sprite.svg获取,省事还样式统一。

微信图片_20220715121327.png

首先,在src/component/toolbar新建一个文件,比如chart.js

import IconItem from './icon-item';

/**
 * 插入图片按钮。
 * @ignore
 * @class
 */
class Chart extends IconItem {
  constructor() {
    super('chart', undefined, undefined);
  }
}

export default Chart;

这样就完成了一个工具栏图标组件的定义。

注意:这里构造函数第一个参数必须是chart,只有这样才能匹配上相关样式,正确定位到sprite.svg中的图标。

定义完组件后,将其添加到工具栏,这个简单,直接引用即可,参考代码如下:

import Chart from './chart';

class Toolbar {
   constructor(data, widthFn, isHide = false) {
     ...
     buildDivider(),
     [(this.chartEl = new Chart())],
     buildDivider(),
     ...
   }
  1. 为插入图片按钮绑定事件

src/component/sheet.js

import { fileOpen } from 'browser-fs-access';

/**
 * 工具栏事件。
 * @ignore
 * @param type 触发事件的按钮类型
 * @param value
 */
function toolbarChange(type, value) {
  switch (type) {
    case 'chart':
      uploadImage.call(this, type);
      break;
    ...
   }
   ...
 }
 
/**
 * 实现图片上传功能的整合。
 * @ignore
 * @param type
 */
async function uploadImage(type) {
  try {
    const blob = await fileOpen({
      description: 'Image files',
      mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'],
      extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
    });
    // 将图片上传到指定的服务器。
    const { data } = this;
    const { settings } = data;
    const { upload } = settings; // 扩展默认配置
    const { url, method, name, success } = upload;

    const formData = new FormData();
    formData.append(name, blob);

    const response = await fetch(url, {
      method,
      body: formData,
    });
    if (success && typeof success === 'function') {
      success(response);
    }
    const json = await response.json();
    // 具体结构要后台接口提供。
    // const { code, message, data: imageUrl } = json;
    // if (code !== 0) {
    //   throw new Error(message);
    // }
    const { thumbUrl: imageUrl } = json;
    // 其实是设置该单元格 type: 'image', value: imageUrl,在后面进行渲染。
    // 因为 setSelectedCellAttr 只能设置一个值,所以这里需要先设置 type,再设置 value。
    // 因为原渲染内容使用 text,我们既需要地址,又不像渲染 text,所以使用 value。
    data.setSelectedCellAttr('type', 'image' || type); // 设置类型,方便后面的渲染。
    data.setSelectedCellAttr('value', imageUrl); // 设置图片地址。方面后面使用地址渲染。
    sheetReset.call(this);
  } catch (e) {
    console.error(e);
  }
}

src/core/data_proxy.js下的默认配置可自由扩展,比如我的:

const defaultSettings = {
  locale: 'zh',
  mode: 'edit', // edit | read
  // 配置上传
  upload: {
    url: '',
    method: 'POST',
    headers: {},
    params: {},
    name: 'file',
    success: (res) => {
      console.log(res);
    },
  },
  ...
};
  1. 完成渲染,主要在src/component/table.js与src/canvas/draw.js文件中

src/component/table.js

/**
 * 渲染单元格
 * @ignore
 * @param draw 绘制 canvas 工具类
 * @param {DataProxy} data 为 data-proxy 生成的对象
 * @param {number} rindex 行坐标, 0 开始
 * @param {number} cindex 列坐标, 0 开始
 * @param {number} yoffset y 轴偏移量
 */
export function renderCell(draw, data, rindex, cindex, yoffset = 0) {
  ...
  draw.rect(dbox, () => {
    // 在文本之前添加空白占位符方便绘制特殊图形:例如圆形、方形等等
    if (['text', 'radio', 'checkbox', 'date', 'select', 'inputGroup', 'image'].includes(cell.type)) {
      // 在这里传递一下行坐标与列坐标的宽度,方便异步加载图片时使用
      let fixedIndexWidth = cols.indexWidth;
      let fixedIndexHeight = rows.height;
      draw.geometry(cell, dbox, { fixedIndexWidth, fixedIndexHeight });
    }
    
    ... 其它原代码
  }
  ...
}

src/canvas.draw.js

  /**
   * 绘制图形。
   * 在这里本方法参考text方法的逻辑,当单元格为 radio, checkbox, date 时,在文字前添加相应的图形。
   * @param {Object} cell - 单元格
   * @param {Object} box - DrawBox
   * @param {Object} fixedIndexWidth - 行坐标宽度
   * @param {Object} fixedIndexHeight - 列坐标高度
   * @returns {Draw} CanvasRenderingContext2D 实例
   */
  geometry(cell, box, { fixedIndexWidth, fixedIndexHeight }) {
    const { type } = cell;

    switch (type) {
      case 'text':
        this.fillInput(box);
        break;
      case 'radio':
        this.fillRadio(box, cell);
        break;
      case 'checkbox':
        this.fillCheckbox(box, cell);
        break;
      case 'inputGroup':
        this.fillInputGroup(box, cell);
        break;
      case 'date':
        this.fillDate(box);
        break;
      case 'select':
        this.fillSelect(box);
        break;
      case 'image':
        this.fillImage(box, cell, { fixedIndexWidth, fixedIndexHeight });
        break;
      default:
    }

    return this;
  }
  
  
  /**
   * 画图片。
   * @param {*} box - 一个 DrawBox 对象
   * @param {string} src - 图片的路径
   * @param {Object} fixedIndexWidth - 行坐标宽度
   * @param {Object} fixedIndexHeight - 列坐标高度
   */
  fillImage(box, { value: src }, { fixedIndexWidth, fixedIndexHeight }) {
    const img = new Image();
    img.src = src;
    img.onload = () => {
      this.ctx.save();
      const { x, y, width, height } = box;
      // 计算左上角位置,为什么translate没有生效呢?因为异步……
      const sx = x + fixedIndexWidth;
      const sy = y + fixedIndexHeight;
      this.ctx.drawImage(img, npx(sx), npx(sy), npx(width), npx(height));
      this.ctx.restore();
    };

    return this;
  }

打完,收功。