js之mvcExcel(一)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章