x-spreadsheet(一个基于 Web(es6) canvas 构建的轻量级 Excel 开发库)的学习实践:
相关地址
源码目录图
几个重要的类
Spreadsheet
主要入口类,涉及表格初始化、数据初始化等,每个方法最后return this都会返回该对象的引用,支持通过一句语句完成很多操作。
文件目录:src/index.js
class Spreadsheet {
constructor(selectors, options = {}) {
let targetEl = selectors;
this.options = { showBottomBar: true, ...options };
this.sheetIndex = 1;
this.datas = [];
if (typeof selectors === 'string') {
targetEl = document.querySelector(selectors);
}
this.bottombar = this.options.showBottomBar ? new Bottombar(() => {
const d = this.addSheet();
this.sheet.resetData(d);
}, (index) => {
const d = this.datas[index];
this.sheet.resetData(d);
}, () => {
this.deleteSheet();
}, (index, value) => {
this.datas[index].name = value;
}) : null;
this.data = this.addSheet();
const rootEl = h('div', `${cssPrefix}`)
.on('contextmenu', evt => evt.preventDefault());
// create canvas element
targetEl.appendChild(rootEl.el);
this.sheet = new Sheet(rootEl, this.data);
if (this.bottombar !== null) {
rootEl.child(this.bottombar.el);
}
}
addSheet(name, active = true) {} // 新建工作表
deleteSheet() {} // 删除工作表
loadData(data) {} // 加载数据
getData() {} // 获取数据
cellText(ri, ci, text, sheetIndex = 0) { // 设置单元格文字
this.datas[sheetIndex].setCellText(ri, ci, text, 'finished');
return this;
}
cell(ri, ci, sheetIndex = 0) {} // 获取该单元格
cellStyle(ri, ci, sheetIndex = 0) {}
reRender() { // 每次loadData后,都要调用reRender,让页面重新渲染
this.sheet.table.render();
return this;
}
on(eventName, func) {}
validate() {}
change(cb) {}
static locale(lang, message) {}
}
其中初始化配置传入的options如下:
// 见src/index.d.ts
export interface Options {
mode?: 'edit' | 'read'; // read不可编辑、edit可编辑
showToolbar?: boolean; // 是否展示工具条
showGrid?: boolean; // 是否显示网格
showContextmenu?: boolean; // 是否展示右键菜单
showBottomBar?: boolean; // 是否展示底部功能条
view?: { // view视图的宽高配置
height: () => number;
width: () => number;
};
row?: {
len: number; // 限制行数
height: number; // 行高
};
col?: {
len: number; // 列数
width: number; // 列宽
indexWidth: number;
minWidth: number;
};
style?: { // 样式
bgcolor: string;
align: 'left' | 'center' | 'right';
valign: 'top' | 'middle' | 'bottom';
textwrap: boolean;
strike: boolean;
underline: boolean;
color: string;
font: {
name: 'Helvetica';
size: number;
bold: boolean;
italic: false;
};
};
}
Sheet
class Sheet {
constructor(targetEl, data) {}
on(eventName, func) {}
trigger(eventName, ...args) {}
findCell(value) {}
toCell(ri, ci) {}
resetData(data) {}
loadData(data) { // data是一个数组,数组中每个元素(对像)就是渲染在每个sheet的data
const ds = Array.isArray(data) ? data : [data];
if (this.bottombar !== null) {
this.bottombar.clear();
}
if(this.sheet && this.sheet.selector){
this.sheet.selector.hide()
}
this.datas = [];
if (ds.length > 0) {
for (let i = 0; i < ds.length; i += 1) {
const it = ds[i];
const nd = this.addSheet(it.name, i === 0);
nd.setData(it);
if (i === 0) {
this.sheet.resetData(nd);
}
}
}
return this;
}
freeze(ri, ci) { // freeze rows or cols 冻结
const { data } = this;
data.setFreeze(ri, ci);
sheetReset.call(this);
return this;
}
undo() {}
redo() {}
reload() {}
getRect() {}
getTableOffset() {}
}
DataProxy
export default class DataProxy {
constructor(name, settings) {
this.settings = helper.merge(defaultSettings, settings || {});
this.name = name || 'sheet';
this.freeze = [0, 0];
this.styles = [];
this.merges = new Merges();
this.rows = new Rows(this.settings.row);
this.cols = new Cols(this.settings.col);
this.validations = new Validations();
this.hyperlinks = {};
this.comments = {};
this.selector = new Selector();
this.scroll = new Scroll();
this.history = new History(); // 历史记录,存放undo、redo撤销重做的undoItems、redoItems
this.clipboard = new Clipboard(); // 剪切板,存放range、state:clear|copy|cut
this.autoFilter = new AutoFilter();
this.change = () => {};
this.exceptRowSet = new Set();
this.sortedRowMap = new Map();
this.unsortedRowMap = new Map();
this.cellChange = () => {}
}
// ————————校验methods————————
addValidation(mode, ref, validator) {}
removeValidation() {}
getSelectedValidator() {}
getSelectedValidation() {}
// ————————撤销methods————————
canUndo() {}
canRedo() {}
undo() {}
redo() {}
// ————————剪切、复制、粘贴methods————————
copy() {}
copyToSystemClipboard() {}
cut() {}
// what: all | text | format
paste(what = 'all', error = () => { }){}
pasteFromText(txt) {}
clearClipboard() {}
getClipboardRect() {}
// ————————聚焦methods————————
autofill(cellRange, what, error = () => {}) {}
canAutofilter() {}
autofilter() {}
setAutoFilter(ci, order, operator, value) {}
resetAutoFilter() {}
// ————————选中methods————————
calSelectedRangeByEnd(ri, ci) {}
calSelectedRangeByStart(ri, ci) {}
setSelectedCellAttr(property, value) {}
// state: input | finished
setSelectedCellText(text, state = 'input') {}
getSelectedCell() {}
xyInSelectedRect(x, y) {}
getSelectedRect() {}
isSignleSelected() {}
// ————————rect methods————————
getRect(cellRange) {}
getCellRectByXY(x, y) {}
cellRect(ri, ci) {}
// ————————合并 methods————————
canUnmerge() {}
merge() {}
unmerge() {}
eachMergesInView(viewRange, cb) {}
// ————————删除methods————————
deleteCell(what = 'all') {}
// type: row | column
delete(type) {}
// ————————插入methods————————
// type: row | column
insert(type, n = 1) {}
// ————————滚动methods————————
scrollx(x, cb) {}
scrolly(y, cb) {}
// ————————getCell methods————————
getCell(ri, ci) {}
getCellTextOrDefault(ri, ci) {}
// ————————cell style methods————————
getCellStyle(ri, ci) {}
getCellStyleOrDefault(ri, ci) {}
getSelectedCellStyle() {}
defaultStyle() {}
addStyle(nstyle) {}
// ————————编辑methods————————
// state: input | finished
setCellText(ri, ci, text, state) {}
// ————————冻结 methods————————
freezeIsActive() {}
setFreeze(ri, ci) {}
freezeTotalWidth() {}
freezeTotalHeight() {}
freezeViewRange() {}
// ————————设置宽高 methods————————
setRowHeight(ri, height) {}
setColWidth(ci, width) {}
viewHeight() {}
viewWidth() {}
// ————————range methods————————
contentRange() {}
viewRange() {}
// ————————data相关 methods————————
changeData(cb) {}
setData(d) {}
getData() {}
// ————————Rows Cols methods————————
hideRowsOrCols() {}
exceptRowTotalHeight(sri, eri) {}
// type: row | col
// index row-index | col-index
unhideRowsOrCols(type, index) {}
rowEach(min, max, cb) {}
colEach(min, max, cb) {}
}
Draw
文件目录:src/canvas/draw.js 这个类Draw,用canvas上下文封装了一些常用的底层的绘制的方法,如resize、clear、save、restore、text、border等等。
class Draw {
constructor(el, width, height) {
this.el = el;
this.ctx = el.getContext('2d');
this.resize(width, height);
this.ctx.scale(dpr(), dpr());
}
resize(width, height) {}
clear() {}
attr(options) {}
save() {}
restore() {}
beginPath() {}
translate(x, y) {}
scale(x, y) {}
clearRect(x, y, w, h) {}
fillRect(x, y, w, h) {}
fillText(text, x, y) {}
text(mtxt, box, attr = {}, textWrap = true) {}
border(style, color) {}
line(...xys) {}
strokeBorders(box) {}
dropdown(box) {}
error(box) {}
frozen(box) {}
rect(box, dtextcb) {}
}
实践总结
下面这段代码涉及:
- xspreadsheet常用事件监听:
cell-selected、cells-selected、cell-edited等 - 序号列判断
ci === -1 - 单元格编辑状态:input编辑中|finished编辑结束
- 发现的几个xspreadsheet没有处理好的问题
- 初始化配置传styles参数,没有兼容其不传的情况
- 单元格编辑结束,文字为空时,编辑状态state不返回finished
- 给xspreadsheet增加其他事件监听:通过给canvas上的div
const sheetOverlayer = document.querySelector(".x-spreadsheet-overlayer")增加事件监听
const initSetting = rawData => {
const { w, h } = getSheetSize("#selector"); // document.querySelector('#selector').getBoundingClientRect()
const { cols, rows } = createSheetData(rawData, w); // format源数据,处理成符合xspreadsheet的数据
const xs = window
.x_spreadsheet("#calcSheet", {
showToolbar: false,
showContextmenu: false,
showBottomBar: false,
view: {
height: () => h,
width: () => w
},
mode
})
.loadData([
{
styles: [...styleArr], // 库兼容性不好,没有兼容styles不传的情况
cols,
rows
}
])
.on("cell-selected", (cell, ri, ci) => { // cell-selected 选中单元格事件
if (cell) {
// 内容编辑区
if (ci > -1) {
const { text } = cell;
selectedCellInfo.current = buildSelectedCellInfo({
text,
ci,
ri
});
setGuideCellInfo({
pos: `${LETTERS[ci]}${ri + 1}`,
val: text
});
} else {
// 序号列: ci = -1
}
} else {}
})
.on("cells-selected", (cell, parameters) => { // cells-selected 多单元格选中事件
// console.log('cells-selected:', cell, parameters);
})
.on("cell-edited", (cell, ri, ci, state) => { // cell-edited 单元格编辑事件,state编辑状态: input编辑中|finished编辑结束
if (ri >= HEADER_ROWS_NUM) { // HEADER_ROWS_NUM: 列头的行数,列头不给编辑,非编辑区
// 内容编辑区
const cellKey = getCellKey(ci);
const { newValue, oldValue } = cell;
const nState =
newValue !== oldValue && !newValue ? EEditState.finished : state; // fix:单元格在文字为空时state不返回finished的缺陷
onEditFinish(cellKey, nState, newValue, ri, ci, oldValue); // 结束编辑
}
});
// 冻结前两行
xs.sheet.freeze(2, 0);
sheetRef.current = xs;
// 增加其他事件:给canvas上的其他dom元素增加事件监听
const sheetOverlayer = document.querySelector(
".x-spreadsheet-overlayer"
) as HTMLDivElement;
sheetOverlayerRef.current = sheetOverlayer;
if (mode === EPageType.edit) {
sheetOverlayer?.addEventListener("dblclick", onDblClick, true);
sheetOverlayer?.addEventListener("mousedown", onMouseDown, true);
detailWrapperRef?.current.addEventListener( // detailWrapperRef 父元素ref
"contextmenu",
onContextmenu,
true
);
window.addEventListener("paste", onPaste, true);
window.addEventListener("keydown", onKeydown, true);
}
}