x-spreadsheet,一个基于浏览器的轻量级的js表格。地址:github.com/myliang/x-s… ,但它没有插入图片功能,今天我就来说一下我的实现方法。
关于插入图片,我的思路是2种:canvas渲染、dom挂载。
为了简单,我直接使用了第1种:canvas渲染。下面说一下具体步骤:
- 在工具栏插入图片按钮,效果如下:
这个图标我们可以直接从作者的sprite.svg获取,省事还样式统一。
首先,在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(),
...
}
- 为插入图片按钮绑定事件
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);
},
},
...
};
- 完成渲染,主要在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;
}
打完,收功。