這一期,我使用面向對象的風格來重構我上版的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完整代碼
但是後續的代碼更新也在這個網址中,下期見。