上篇博客講述了此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層數據也便無後顧之憂。不得不說,此方案雖然不高效但是的確能解決問題。
到此,此項目的講解就結束了,如果有看源碼不明白的地方或者對源碼有更好的簡潔,歡迎評論區留言,我會及時回覆。