这一期,我使用面向对象的风格来重构我上版的Excel代码。并且这一次基于最近的面向对象的calculator的基础上使用MVC的框架来进行实现。这篇博客,我将讲解最为重要的model层的实现思路。
model层,即模型层,用于创建项目所需模型以及管理模型数据。
以excel表格为例,初步分析,整个表格就是一个模型,但是这划分的细度显然不够。
再细想,我们不难得出四类模型:corner,cell,colHeader,rowHeader。
下面给出这四类模型的代码:
export default class Corner {
constructor(text) {
this.text = text;
}
}
export default class Cell {
constructor() {
this.text = '';
}
}
export default class ColHeader {
constructor(text) {
this.text = text;
this.width = 64;
}
}
export default class RowHeader {
constructor(text) {
this.text = text;
this.height = 25;
}
}
这代码结构为何如此简单,连基本的事件绑定都没有,这怎么可能实现前端的那个页面效果?
对,没错,model层仅仅这点代码肯定是不够的。
首先,为什么不在这里不绑定事件?因为事件是ui效果,属于view层的管理,不应该在这里绑定。
没有事件绑定,这些模型有何用处?原理很简单,model层,我可以通过把前端Excel拆解成四部分,每一部分的后台数据都在model层进行管理,所以完整的model层还需要一个管理类!
Sheet:
/* eslint-disable max-len */
import RowHeader from './rowHeader.js';
import ColHeader from './colHeader.js';
import Cell from './cell.js';
import Corner from './corner.js';
import SelectRange from './selectRange.js';
import Coordinate from './coordinate.js';
export default class Sheet {
constructor() {
this._onDataChangedCallback = null;
this.rowHeaders = [];
this.colHeaders = [];
this.cells = []; // Two-dimensional array
this.corner = null;
this.activeCellCoordinate = new Coordinate(0, 0);
this.boundaryCellCoordinate = new Coordinate(-1, -1);
this.selectRange = new SelectRange(); // Vice Business
}
init(rowCount, colCount) {
this.corner = new Corner('/');
for (let i = 0; i < rowCount; i++) {
this.rowHeaders.push(new RowHeader(`${i + 1}`));
}
for (let i = 0; i < colCount; i++) {
this.colHeaders.push(new ColHeader(String.fromCharCode('A'.charCodeAt() + i)));
}
for (let i = 0; i < colCount; i++) {
this.cells[i] = [];
for (let j = 0; j < rowCount; j++) {
this.cells[i].push(new Cell());
}
}
}
initEvent(onDataChangedCallback) {
this._onDataChangedCallback = onDataChangedCallback;
}
onDataChange(data) {
if (this._onDataChangedCallback != null) {
this._onDataChangedCallback(data);
}
}
changeSelectRange(selectType, startColIndex, startRowIndex, endColIndex, endRowIndex, activeCellColIndex, activeCellRowIndex) {
if (this.selectRange.equal(selectType, startColIndex, startRowIndex, endColIndex, endRowIndex)
&& this.activeCellCoordinate.colIndex === activeCellColIndex && this.activeCellCoordinate.rowIndex === activeCellRowIndex) {
this.onDataChange({ result: false });
return;
}
this.selectRange.selectType = selectType;
this.selectRange.selectUpperLeftCoordinate.colIndex = startColIndex;
this.selectRange.selectUpperLeftCoordinate.rowIndex = startRowIndex;
this.selectRange.selectBottomRightCoordinate.colIndex = endColIndex;
this.selectRange.selectBottomRightCoordinate.rowIndex = endRowIndex;
this.activeCellCoordinate.colIndex = activeCellColIndex;
this.activeCellCoordinate.rowIndex = activeCellRowIndex;
this.onDataChange({ result: true });
}
updateCellText(colIndex, rowIndex, text) {
this.activeCellCoordinate.colIndex = colIndex;
this.activeCellCoordinate.rowIndex = rowIndex;
this.cells[colIndex][rowIndex].text = text;
this.boundaryCellCoordinate.colIndex = Math.max(this.boundaryCellCoordinate.colIndex, colIndex);
this.boundaryCellCoordinate.rowIndex = Math.max(this.boundaryCellCoordinate.rowIndex, rowIndex);
}
// final status
changeColWidth(index, count, width) {
if (count === 1) {
let actualWidth = width + this.colHeaders[index].width;
if (actualWidth >= 0) {
this.colHeaders[index].width = width;
} else {
this.colHeaders[index].width = 0;
let i = index - 1;
while (i >= 0 && actualWidth < 0) {
actualWidth += this.colHeaders[i].width;
this.colHeaders[i].width = actualWidth >= 0 ? actualWidth : 0;
i--;
}
}
} else {
const actualWidth = width < 0 ? 0 : width;
for (let i = index; i < index + count; i++) {
this.colHeaders[i].width = actualWidth;
}
}
}
changeRowHeight(index, count, height) {
if (count === 1) {
let actualHeight = height + this.rowHeaders[index].height;
if (actualHeight >= 0) {
this.rowHeaders[index].height = height;
} else {
this.rowHeaders[index].height = 0;
let i = index - 1;
while (i >= 0 && actualHeight < 0) {
actualHeight += this.rowHeaders[i].height;
this.rowHeaders[i].height = actualHeight >= 0 ? actualHeight : 0;
i--;
}
}
} else {
const actualHeight = height < 0 ? 0 : height;
for (let i = index; i < index + count; i++) {
this.rowHeaders[i].height = actualHeight;
}
}
}
createRow() {
const newCells = [];
for (let j = 0; j < this.colHeaders.length; j++) {
newCells[j] = new Cell();
}
return newCells;
}
resetRowText() {
for (let i = 0; i < this.rowHeaders.length; i++) {
this.rowHeaders[i].text = `${i + 1}`;
}
}
addRows(index, count, height) {
if (this.boundaryCellCoordinate.rowIndex + count >= this.rowHeaders.length) {
throw new 'This behavior will delete existing data!'();
}
if (index <= this.boundaryCellCoordinate.rowIndex) {
this.boundaryCellCoordinate.rowIndex += count;
}
for (let i = 0; i < count; i++) {
this.cells.splice(index, 0, this.createRow());
this.cells.pop();
const newRowHeader = new RowHeader('1');
newRowHeader.height = height;
this.rowHeaders.splice(index, 0, newRowHeader);
this.rowHeaders.pop();
}
this.resetRowText();
}
getNewRowIndex(index) {
for (let j = index - 1; j >= 0; j--) {
for (let i = 0; i < this.colHeaders.length; i++) {
if (this.cells[i][j].text !== '') {
return j;
}
}
}
return -1;
}
removeRows(index, count) {
if (this.boundaryCellCoordinate.rowIndex >= index && this.boundaryCellCoordinate.rowIndex < index + count) {
this.boundaryCellCoordinate.rowIndex = this.getNewRowIndex(index);
this.boundaryCellCoordinate.colIndex = this.getNewColIndex(index);
}
for (let i = 0; i < count; i++) {
this.cells.splice(index, 1);
this.cells.push(this.createRow());
this.rowHeaders.splice(index, 1);
this.rowHeaders.push(new RowHeader('1'));
}
this.resetRowText();
}
resetColText() {
for (let i = 0; i < this.colHeaders.length; i++) {
this.colHeaders[i].text = String.fromCharCode('A'.charCodeAt() + i);
}
}
addCols(index, count, width) {
if (this.boundaryCellCoordinate.colIndex + count >= this.colHeaders.length) {
throw new 'This behavior will delete existing data!'();
}
if (index <= this.boundaryCellCoordinate.colIndex) {
this.boundaryCellCoordinate.colIndex += count;
}
for (let i = 0; i < count; i++) {
for (let j = 0; j < this.rowHeaders.length; j++) {
this.cells[j].splice(index, 0, new Cell());
this.cells[j].pop();
}
const newColHeader = new ColHeader('A');
newColHeader.width = width;
this.colHeaders.splice(index, 0, newColHeader);
this.colHeaders.pop();
}
this.resetColText();
}
getNewColIndex(index) {
for (let i = index - 1; i >= 0; i--) {
for (let j = 0; j < this.removeRows.length; j++) {
if (this.cells[i][j].text !== '') {
return i;
}
}
}
return -1;
}
// index most left index
removeCols(index, count) {
if (this.boundaryCellCoordinate.colIndex >= index && this.boundaryCellCoordinate.colIndex < index + count) {
this.boundaryCellCoordinate.rowIndex = this.getNewRowIndex(index);
this.boundaryCellCoordinate.colIndex = this.getNewColIndex(index);
}
for (let i = 0; i < count; i++) {
for (let j = 0; j < this.rowHeaders.length; j++) {
this.cells[j].splice(index, 1);
this.cells[j].push(new Cell());
}
this.colHeaders.splice(index, 1);
this.colHeaders.push(new ColHeader('A'));
}
this.resetColText();
}
}
Coordinate:
export default class Coordinate {
constructor(colIndex, rowIndex) {
this.colIndex = colIndex;
this.rowIndex = rowIndex;
}
}
SelectRange :
/* eslint-disable max-len */
import Coordinate from './coordinate.js';
export default class SelectRange {
constructor() {
this.selectType = 'cell';
this.selectUpperLeftCoordinate = new Coordinate(0, 0);
this.selectBottomRightCoordinate = new Coordinate(0, 0);
}
equal(selectType, startColIndex, startRowIndex, endColIndex, endRowIndex) {
return selectType === this.selectType
&& startColIndex === this.selectUpperLeftCoordinate.colIndex && startRowIndex === this.selectBottomRightCoordinate.rowIndex
&& endColIndex === this.selectBottomRightCoordinate.colIndex && endRowIndex === this.selectBottomRightCoordinate.rowIndex;
}
}
sheet类的代码有点长,我做点解释:
首先看类的属性,除了我上述提到的四个基本模型外,
Coordinate :记录二维坐标轴里点的坐标
_onDataChangedCallback:用于与controller层进行交互的事件回调函数
activeCellCoordinate:用于记录记录支持编辑的方框的坐标,即那个白色的小方框
boundaryCellCoordinate:用于记录有数据的最外边的cell坐标(在add函数里被用到)
selectRange:选择范围,记录选择的类型以及大方框的坐标(左上角和右下角)
除了这些属性外,还有一些model层进行数据管理必备的函数:
表格的初始化;回调函数的控制;更换选择区域;更新cell元素的文本;行列resize的相应数据更新;增删行列的数据更新。
到此,model层就以搭建完成,view层的功能就是简单来说就是页面渲染,属于这个模块的任务有表格的创建以及各类事件的绑定,还有页面显示效果的更新。而controller层的功能就是具体实现view层所绑定的事件然后改变model层的数据最后再返回给view层更新页面显示。
这次除了代码类型的改变外,还在上次的基础上新增了一些其它的功能,目前view层和controller层的代码还未完全实现,所以具体功能这一部分留到下一期。
这里我给出此项目的github源码地址(还未更新完整):Excel完整代码
但是后续的代码更新也在这个网址中,下期见。