新版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层数据也便无后顾之忧。不得不说,此方案虽然不高效但是的确能解决问题。
到此,此项目的讲解就结束了,如果有看源码不明白的地方或者对源码有更好的简洁,欢迎评论区留言,我会及时回复。

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