新版Excel(完結版)

上篇博客講述了此mvc版本的Excel的model層實現,這裏我繼續上篇博客的內容,把剩下的內容敘述完。最終效果:新版Excel
首先,我先描述下次版本的Excel在功能上與上個版本的相同點與不同點:

  • 在表的結構上,此次表爲動態固定表,也是Excel表格的模式,即行列大小是固定的,所謂的增刪行列就是增加並刪除或者刪除並增加,具體效果可以自行嘗試。
  • 此次的選擇效果除了上次的cell的多選效果外,還新增了行列的多選效果。
  • 在編輯效果上除了上次支持的enter鍵以及鼠標焦點切換來確定輸入內容外,此次新增了tab鍵確定輸入內容的效果。
  • 此次在resize功能上除了本身支持的resize效果外還擴展了反向壓縮resize的效果。
  • 最重要的一個新增功能,上述的行列增刪功能以及resize功能都支持多選操作的效果!

主要功能就這些,還有一些邊界的細節功能我在這就不敘述了,大家有興趣可以自己玩玩。
下面是此版項目的目錄結構:
在這裏插入圖片描述
除了上述模塊外,還有一個單元測試模塊未顯示,由於此版項目代碼量很多,所以在這裏我不再給出全部的源碼,大家可以通過此github鏈接下載完整的源代碼:新版Excel源碼
我先講解此mvc項目的運轉流程:
首先最外層的函數接口是main.js文件,此文件調用controller層的initController.js文件的initsheet函數,此函數只做了兩件事,調用model層的表格數據初始化函數,然後調用將model層數據傳給view層來初始化UI頁面。

/* eslint-disable max-len */
import sheet from './sheet.js';
import initTable from '../views/initView.js';
import constants from '../utils/constant.js';

export default function initSheet() {
  sheet.init(constants.rowLength, constants.colLength);
  initTable(sheet.corner, sheet.rowHeaders, sheet.colHeaders, sheet.selectRange.selectType,
    sheet.activeCellCoordinate, sheet.selectRange.selectUpperLeftCoordinate, sheet.selectRange.selectBottomRightCoordinate);
}

然後,在view層的初始化函數裏綁定此次項目所做功能所需的一些事件,而事件的具體實現都交給controller層來管理。

/* eslint-disable max-len */
import portray from './portray.js';

import RowHeaderController from '../controllers/rowHeaderController.js';
import ColHeaderController from '../controllers/colHeaderController.js';
import cornerClickHandler from '../controllers/cornerController.js';
import Cell from '../controllers/cellController.js';
import Resize from '../controllers/resizeController.js';
/* eslint-disable max-len */
// time out event resize
const colHeaderController = new ColHeaderController();
const rowHeaderController = new RowHeaderController();
const cell = new Cell();
const resize = new Resize();
function createCorner(corner) {
  const thCorner = document.createElement('th');
  thCorner.innerText = corner.text;
  thCorner.classList.add('corner');
  thCorner.addEventListener('click', (e) => {
    cornerClickHandler(e);
  }, false);
  return thCorner;
}

function createColHeader(colHeader) {
  const thColHeader = document.createElement('th');
  const resizeE = document.createElement('div');
  const span = document.createElement('span');
  thColHeader.classList.add('colHeader');
  resizeE.classList.add('resizeE');
  thColHeader.appendChild(span);
  thColHeader.appendChild(resizeE);
  thColHeader.children[0].innerText = colHeader.text;
  resizeE.addEventListener('mousedown', resize.resizeColHeaderDownHandler, false);
  thColHeader.style.width = `${colHeader.width}px`;
  thColHeader.addEventListener('click', (e) => {
    ColHeaderController.colHeaderClickHandler(e);
  }, false);
  thColHeader.addEventListener('mousedown', (e) => {
    colHeaderController.colHeaderDownHandler(e);
  }, false);
  thColHeader.addEventListener('mouseup', (e) => {
    colHeaderController.colHeaderUpHandler(e);
  }, false);
  thColHeader.addEventListener('mousemove', (e) => {
    colHeaderController.colHeaderMoveHandler(e);
  }, false);
  thColHeader.addEventListener('contextmenu', (e) => {
    ColHeaderController.colHeaderMenuHandler(e);
  }, false);
  return thColHeader;
}

function createRowHeader(rowHeader) {
  const tdRowHeader = document.createElement('td');
  const resizeS = document.createElement('div');
  const span = document.createElement('span');
  tdRowHeader.classList.add('rowHeader');
  resizeS.classList.add('resizeS');
  tdRowHeader.appendChild(span);
  tdRowHeader.appendChild(resizeS);
  tdRowHeader.children[0].innerText = rowHeader.text;
  tdRowHeader.style.height = `${rowHeader.height}px`;
  resizeS.addEventListener('mousedown', resize.resizeRowHeaderDownHandler, false);
  tdRowHeader.addEventListener('click', (e) => {
    RowHeaderController.rowHeaderClickHandler(e);
  }, false);
  tdRowHeader.addEventListener('mousedown', (e) => {
    rowHeaderController.rowHeaderDownHandler(e);
  }, false);
  tdRowHeader.addEventListener('mouseup', (e) => {
    rowHeaderController.rowHeaderUpHandler(e);
  }, false);
  tdRowHeader.addEventListener('mousemove', (e) => {
    rowHeaderController.rowHeaderMoveHandler(e);
  }, false);
  tdRowHeader.addEventListener('contextmenu', (e) => {
    RowHeaderController.rowHeaderMenuHandler(e);
  }, false);
  return tdRowHeader;
}

function createCell(dataIndex, colWidth) {
  const tdCell = document.createElement('td');
  tdCell.style.maxWidth = `${colWidth}px`;
  tdCell.classList.add('cell');
  tdCell.setAttribute('data-index', dataIndex);
  tdCell.addEventListener('click', (e) => {
    Cell.cellClickHandler(e);
  }, false);
  tdCell.addEventListener('dblclick', (e) => {
    cell.cellDbClickHandler(e);
  }, false);
  tdCell.addEventListener('mousedown', (e) => {
    cell.cellDownHandler(e);
  }, false);
  tdCell.addEventListener('mousemove', (e) => {
    cell.cellMoveHandler(e);
  }, false);
  return tdCell;
}
function BindButtonEvent() {
  const addButton = document.getElementsByClassName('add')[0];
  const removeButton = document.getElementsByClassName('remove')[0];
  addButton.addEventListener('click', () => {
    colHeaderController.addColHeaderHandler();
  }, false);
  removeButton.addEventListener('click', () => {
    colHeaderController.removeColHeaderHandler();
  }, false);
  addButton.addEventListener('click', () => {
    rowHeaderController.addRowHeaderHandler();
  }, false);
  removeButton.addEventListener('click', () => {
    rowHeaderController.removeRowHeaderHandler();
  }, false);
}
export default function initTable(corner, rowHeaders, colHeaders,
  selectType, activeCellCoordinate, selectUpperLeftCoordinate, selectBottomRightCoordinate) {
  const body = document.getElementsByTagName('body')[0];
  const table = document.createElement('table');
  table.classList.add('table');
  body.appendChild(table);
  const trFirst = document.createElement('tr');
  trFirst.appendChild(createCorner(corner));
  colHeaders.forEach((colHeader) => trFirst.appendChild(createColHeader(colHeader)));
  table.appendChild(trFirst);
  for (let i = 0; i < rowHeaders.length; i++) {
    const trOther = document.createElement('tr');
    trOther.appendChild(createRowHeader(rowHeaders[i]));
    for (let j = 0; j < colHeaders.length; j++) {
      trOther.appendChild(createCell(colHeaders[j].text, colHeaders[j].width));
    }
    table.appendChild(trOther);
  }
  document.addEventListener('mouseup', (e) => {
    cell.cellUpHandler(e);
  }, false);
  const bigFrame = document.createElement('div');
  bigFrame.classList.add('bigFrame');
  const smallFrame = document.createElement('div');
  smallFrame.classList.add('smallFrame');
  table.appendChild(bigFrame);
  table.appendChild(smallFrame);
  portray(selectType, selectUpperLeftCoordinate, selectBottomRightCoordinate, activeCellCoordinate);
  BindButtonEvent();
}

每次我們在前端ui頁面觸發事件後,controller層來執行具體的事件邏輯,然後更改model層的數據,最後傳遞給view層來更新頁面。這不就是最簡單的mvc的核心流程思想嗎
至於每個模塊裏的每個文件是幹什麼的,從文件名應該就能理解,這裏我就不多講解。
最後,我講解一下反向壓縮resize的算法思想:
首先,我們明白最基本的resize功能的實現邏輯,無非就三步:在down事件裏記錄下初識位置,在move的時候根據初始值來確定該變量並及時更新,最後再up裏重置數據。
由於反向壓縮resize操作會改變其它元素的數據,所以下move時候的及時更新就很有可能會出現重複計算的問題,所以我在這裏採取的解決方案是通過每次在down的時候深拷貝一份model層的原始數據,在move層裏使用深拷貝的那份數據進行邏輯操作,如此一來實時更新model層數據也便無後顧之憂。不得不說,此方案雖然不高效但是的確能解決問題。
到此,此項目的講解就結束了,如果有看源碼不明白的地方或者對源碼有更好的簡潔,歡迎評論區留言,我會及時回覆。

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