UNPKG

e-virt-table

Version:

A powerful data table based on canvas. You can use it as data grid、Microsoft Excel or Google sheets. It supports virtual scroll、cell edit etc.

713 lines 28.4 kB
import Row from './Row'; import { TreeUtil } from './TreeUtil'; import { getLayoutScrollerTrackSize } from './ScrollbarMode'; export default class Body { constructor(ctx) { Object.defineProperty(this, "resizeTarget", { enumerable: true, configurable: true, writable: true, value: null }); //调整行大小的目标 Object.defineProperty(this, "dragSource", { enumerable: true, configurable: true, writable: true, value: null }); //拖拽行的源 Object.defineProperty(this, "dragTarget", { enumerable: true, configurable: true, writable: true, value: null }); //拖拽行的目标 Object.defineProperty(this, "dragRowDiff", { enumerable: true, configurable: true, writable: true, value: 0 }); //拖拽行的差值 Object.defineProperty(this, "dragPosition", { enumerable: true, configurable: true, writable: true, value: 'none' }); //拖拽行的目标行索引 Object.defineProperty(this, "isMouseDown", { enumerable: true, configurable: true, writable: true, value: false }); // 是否按下 Object.defineProperty(this, "resizeDiff", { enumerable: true, configurable: true, writable: true, value: 0 }); // 是否移动中 Object.defineProperty(this, "clientY", { enumerable: true, configurable: true, writable: true, value: 0 }); // 鼠标按下时的y轴位置 Object.defineProperty(this, "ctx", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "x", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "y", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "width", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "height", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "headIndex", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "tailIndex", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "isResizing", { enumerable: true, configurable: true, writable: true, value: false }); //是否正在调整大小 Object.defineProperty(this, "renderRows", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "visibleRows", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "visibleHeight", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "visibleWidth", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "containerRect", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "data", { enumerable: true, configurable: true, writable: true, value: [] }); this.ctx = ctx; this.init(); this.initTree(); this.initSelection(); this.initResizeRow(); this.initDragRow(); } init() { const { canvasElement, header, footer, database, config: { FOOTER_FIXED, HEIGHT, EMPTY_BODY_HEIGHT = 0, MAX_HEIGHT = 0, ENABLE_OFFSET_HEIGHT = 0, OFFSET_HEIGHT = 0, FOOTER_POSITION, }, } = this.ctx; const layoutScrollerTrackSize = getLayoutScrollerTrackSize(this.ctx.config); if (!header.width) { return; } this.x = 0; if (FOOTER_POSITION === 'top' && FOOTER_FIXED) { this.y = header.height + footer.height; //更新body的y轴位置 } else { this.y = header.height; } const { data, sumHeight } = database.getData(); // 更新高度 this.height = sumHeight; // 更新数据 this.data = data; // 更新宽度 this.width = header.width; this.visibleWidth = this.ctx.stageWidth - layoutScrollerTrackSize; // 底部高度 const footerHeight = this.ctx.footer.height; this.ctx.isEmpty = !this.data.length; if (!this.data.length && !HEIGHT) { this.height = EMPTY_BODY_HEIGHT; } else if (!this.data.length && HEIGHT) { // 如果有设置高度的情况,空数据时,高度也要保持为设置的高度 this.height = HEIGHT - header.height - footerHeight - layoutScrollerTrackSize; } let containerHeight = this.height + header.height + layoutScrollerTrackSize; containerHeight += footerHeight; let stageHeight = containerHeight; const windowInnerHeight = window.innerHeight; const { top } = this.containerRect || this.ctx.containerElement.getBoundingClientRect(); if (windowInnerHeight > top && ENABLE_OFFSET_HEIGHT && !HEIGHT) { const visibleHeight = windowInnerHeight - top; const _stageHeight = visibleHeight - OFFSET_HEIGHT; if (_stageHeight > header.height + layoutScrollerTrackSize) { stageHeight = _stageHeight; } else if (containerHeight > MAX_HEIGHT) { stageHeight = MAX_HEIGHT; } } else if (this.data.length && HEIGHT) { stageHeight = HEIGHT; } else if (this.data.length && MAX_HEIGHT && containerHeight > MAX_HEIGHT) { stageHeight = MAX_HEIGHT; } if (stageHeight > 0) { this.ctx.stageHeight = Math.floor(stageHeight); this.ctx.stageElement.style.height = `${this.ctx.stageHeight}px`; } // 可视区高度 let _visibleHeight = this.ctx.stageHeight - header.height - layoutScrollerTrackSize; // 底部高度,如果是固定底部,可视区减上底部高度 if (FOOTER_FIXED) { this.visibleHeight = _visibleHeight - footerHeight; } else { this.visibleHeight = _visibleHeight; } if (!this.data.length) { this.height = this.visibleHeight; } this.ctx.body.x = this.x; this.ctx.body.y = this.y; this.ctx.body.width = this.width; this.ctx.body.height = this.height; this.ctx.body.visibleWidth = this.visibleWidth; this.ctx.body.visibleHeight = this.visibleHeight; this.ctx.body.data = data; const dpr = window.devicePixelRatio || 1; // 获取设备像素比 const canvasWidth = this.ctx.stageWidth * dpr; const canvasHeight = this.ctx.stageHeight * dpr; canvasElement.width = Math.round(canvasWidth); canvasElement.height = Math.round(canvasHeight); const isEmpty = !this.data.length ? 'empty' : 'not-empty'; this.ctx.emit('emptyChange', { isEmpty, type: isEmpty, headerHeight: header.height, bodyHeight: this.height, footerHeight, width: this.width, height: !this.data.length ? EMPTY_BODY_HEIGHT + footerHeight : 0, }); // 外层容器样式 const _cssWidth = Math.round((canvasElement.width / dpr) * 10000) / 10000; const _cssHeight = Math.round((canvasElement.height / dpr) * 10000) / 10000; this.ctx.canvasElement.setAttribute('style', `height:${_cssHeight}px;width:${_cssWidth}px;`); this.ctx.paint.scale(dpr); } initTree() { this.ctx.on('cellClick', (cell, e) => { if (this.ctx.stageElement.style.cursor !== 'pointer') { return; } if (!cell.isImageInside('tree', e)) { return; } const row = this.ctx.database.getRowForRowKey(cell.rowKey); const { expand = false, expandLazy = false } = row || {}; const { EXPAND_LAZY, EXPAND_LAZY_METHOD } = this.ctx.config; // 懒加载且有懒加载方法,不是展开的不是已经加载过的 if (EXPAND_LAZY && EXPAND_LAZY_METHOD && !expand && !expandLazy) { if (typeof EXPAND_LAZY_METHOD === 'function') { this.ctx.database.expandLoading(cell.rowKey, true); const expandLazyMethod = EXPAND_LAZY_METHOD; expandLazyMethod({ row: cell.row, rowIndex: cell.rowIndex, colIndex: cell.colIndex, column: cell.column, value: cell.getValue(), }) .then((res) => { this.ctx.database.setExpandChildren(cell.rowKey, res); this.ctx.database.expandLoading(cell.rowKey, false); this.ctx.emit('expandChange', this.ctx.database.getExpandRowKeys()); }) .catch((err) => { this.ctx.database.expandLoading(cell.rowKey, false); console.error(err); }); } // 懒加载 } else { const isExpand = this.ctx.database.getIsExpand(cell.rowKey); this.ctx.database.expandItem(cell.rowKey, !isExpand); this.ctx.emit('expandChange', this.ctx.database.getExpandRowKeys()); } }); } initSelection() { this.ctx.on('cellClick', (cell, e) => { if (this.ctx.stageElement.style.cursor !== 'pointer') { return; } if (!cell.isImageInside('selection', e)) { return; } const selectable = this.ctx.database.getRowSelectable(cell.rowKey); if (!selectable) { return; } this.ctx.database.toggleRowSelection(cell.rowKey, cell.type); }); } // 调整行的高度 initResizeRow() { this.ctx.on('resize', () => { if (!this.ctx.config.ENABLE_RESIZE_ROW) { return; } this.containerRect = this.ctx.containerElement.getBoundingClientRect(); }); this.ctx.on('resizeObserver', () => { if (!this.ctx.config.ENABLE_RESIZE_ROW) { return; } this.containerRect = this.ctx.containerElement.getBoundingClientRect(); }); // 鼠标移动 this.ctx.on('mouseup', () => { if (!this.ctx.config.ENABLE_RESIZE_ROW) { return; } this.isMouseDown = false; if (this.resizeDiff !== 0 && this.resizeTarget) { // 调整宽度 this.resizeRow(this.resizeTarget, this.resizeDiff); } this.resizeTarget = null; this.resizeDiff = 0; this.isResizing = false; //加个延时,修复拖动时,开始编辑的问题 setTimeout(() => { this.ctx.rowResizing = false; }, 0); this.clientY = 0; }); this.ctx.on('mousedown', (e) => { if (!this.ctx.config.ENABLE_RESIZE_ROW) { return; } if (!this.ctx.isTarget(e)) { return; } this.clientY = e.clientY; if (this.resizeTarget) { this.isResizing = true; // 传递给上下文,防止其他事件触发,行调整大小时,不触发选择器 this.ctx.rowResizing = true; } else { this.isResizing = false; this.ctx.rowResizing = false; } this.isMouseDown = true; }); this.ctx.on('mousemove', (e) => { if (!this.ctx.config.ENABLE_RESIZE_ROW) { return; } // 编辑中不触发mousemove if (this.ctx.editing) return; const { offsetY, offsetX } = this.ctx.getOffset(e); const y = offsetY; const x = offsetX; const clientY = e.clientY; const { stageHeight, scrollY, config: { RESIZE_ROW_MIN_HEIGHT = 0 }, } = this.ctx; if (this.isResizing && this.resizeTarget) { const resizeTargetHeight = this.resizeTarget.height; let diff = clientY - this.clientY; const { calculatedHeight } = this.resizeTarget; const minHeight = calculatedHeight === -1 ? RESIZE_ROW_MIN_HEIGHT : calculatedHeight; if (diff + resizeTargetHeight < minHeight) { diff = -(resizeTargetHeight - minHeight); } this.resizeDiff = diff; this.ctx.emit('draw'); } else { this.resizeTarget = null; // 按下时不改变样式,有可能是多选表头 if (this.isMouseDown) { return; } // 鼠标移动时,判断是否在行的范围内 if (x < 0 || x > this.ctx.body.visibleWidth || y < 0 || y > this.ctx.header.visibleHeight + this.ctx.body.visibleHeight) { if (this.ctx.stageElement.style.cursor === 'row-resize') { // 恢复默认样式 this.ctx.stageElement.style.cursor = 'default'; } return; } // 如果是拖动选择 if (this.ctx.stageElement.style.cursor === 'crosshair') { return; } if (this.ctx.stageElement.style.cursor === 'row-resize') { // 恢复默认样式 this.ctx.stageElement.style.cursor = 'default'; } for (let i = 0; i < this.renderRows.length; i++) { const row = this.renderRows[i]; const isYRange = y > row.y - scrollY + row.height - 1.5 && y < row.y - scrollY + row.height + 1.5 && y < stageHeight - 4; if (isYRange) { for (let j = 0; j < row.cells.length; j++) { const cell = row.cells[j]; if (x > cell.drawX + 10 && x < cell.drawX + cell.width - 10 && cell.rowspan === 1 //没有被合并的单元格 ) { this.ctx.stageElement.style.cursor = 'row-resize'; this.resizeTarget = row; } } } } } }); } initDragRow() { this.ctx.on('cellMousedown', (cell, e) => { if (!this.ctx.config.ENABLE_DRAG_ROW) { return; } if (!cell.isImageInside('drag', e)) { return; } this.dragSource = cell; // 收起行 this.ctx.database.expandItem(cell.rowKey, false); this.ctx.dragRowIng = true; this.dragRowDiff = this.ctx.mouseY - cell.drawY; this.ctx.stageElement.style.cursor = 'grabbing'; this.ctx.clearSelector(); this.ctx.emit('dragRowMouseDown', cell); this.ctx.emit('draw'); }); this.ctx.on('mouseup', async () => { if (!this.ctx.dragRowIng) { return; } if (this.dragSource && this.dragTarget && this.ctx.stageElement.style.cursor !== 'not-allowed') { const dragSource = this.dragSource; const dragTarget = this.dragTarget; const params = { source: dragSource, target: dragTarget, position: this.dragPosition, }; this.ctx.emit('dragRowChange', params); let isDragRow = true; const { BEFORE_DRAG_ROW_METHOD } = this.ctx.config; // 判断是不是函数 if (typeof BEFORE_DRAG_ROW_METHOD === 'function') { // 判断的是不是Promise isDragRow = await BEFORE_DRAG_ROW_METHOD(params); } if (isDragRow && !this.ctx.config.ENABLE_DRAG_ROW_CUSTOM) { const originalData = this.ctx.database.getOriginalData(); const { TREE_CHILDREN_KEY, ROW_KEY } = this.ctx.config; const treeUtil = new TreeUtil(originalData, { key: ROW_KEY, childrenKey: TREE_CHILDREN_KEY, }); treeUtil.treeMove(dragSource.rowKey, dragTarget.rowKey, this.dragPosition); this.ctx.emit('loadData', treeUtil.getTree()); } } if (!this.dragTarget) { this.ctx.stageElement.style.cursor = 'grab'; } this.ctx.dragRowIng = false; this.dragSource = null; this.dragTarget = null; this.dragPosition = 'after'; this.dragRowDiff = 0; this.ctx.emit('draw'); }); this.ctx.on('mousemove', (e) => { if (!this.ctx.dragRowIng) { return; } const { offsetY } = this.ctx.getOffset(e); const { drawY } = this.dragTarget; if (drawY < offsetY || drawY > offsetY) { this.ctx.startAdjustPosition(e); } else { this.ctx.stopAdjustPosition(); } }); this.ctx.on('cellMouseenter', (cell) => { if (!this.ctx.dragRowIng) { return; } this.dragTarget = cell; const { ENABLE_DRAG_ROW_CROSS_LEVEL } = this.ctx.config; const dragSource = this.dragSource; const dragTarget = this.dragTarget; // 不启用跨级拖拽时,判断是否在同一级 if (!ENABLE_DRAG_ROW_CROSS_LEVEL && dragSource && dragTarget && dragSource.parentRowKey !== dragTarget.parentRowKey) { this.ctx.stageElement.style.cursor = 'not-allowed'; return; } this.ctx.stageElement.style.cursor = 'grabbing'; this.dragTarget = cell; const { drawY, visibleHeight } = cell; const scopeHeight = ENABLE_DRAG_ROW_CROSS_LEVEL ? visibleHeight / 3 : visibleHeight / 2; if (this.ctx.mouseY <= drawY + scopeHeight) { this.dragPosition = 'before'; } else if (this.ctx.mouseY > drawY + visibleHeight - scopeHeight) { this.dragPosition = 'after'; } else { this.dragPosition = 'inside'; } const params = { dragSource: this.dragSource, dragTarget: this.dragTarget, dragPosition: this.dragPosition, }; this.ctx.emit('dragRowMove', params); this.ctx.emit('draw'); }); } resizeRow(row, diff) { const { rowIndex, height, rowKey, data } = row; this.ctx.database.setRowHeight(rowIndex, height + diff); this.init(); this.ctx.emit('draw'); this.ctx.emit('resizeRowChange', { rowIndex, oldHeight: height, height: height + diff, rowKey, row: data, data: this.data, }); } drawTipLine() { if (this.isResizing && this.resizeTarget) { const { stageWidth, scrollY, config: { RESIZE_ROW_LINE_COLOR }, } = this.ctx; const resizeTargetDrawY = this.resizeTarget.y - scrollY; const resizeTargetHeight = this.resizeTarget.height; const y = resizeTargetDrawY + resizeTargetHeight + this.resizeDiff - 0.5; const poins = [0, y - 0.5, stageWidth, y - 0.5]; this.ctx.paint.drawLine(poins, { borderColor: RESIZE_ROW_LINE_COLOR, borderWidth: 1, }); } } // 拖拽提示线绘制 drawDragRowTip() { if (this.ctx.dragRowIng && this.dragTarget && this.dragSource) { const { stageWidth, config: { DRAG_ROW_TIP_LINE_COLOR, DRAG_TIP_BG_COLOR }, } = this.ctx; const rh = this.dragSource.visibleHeight; // 提示背景 this.ctx.paint.drawRect(0, this.ctx.mouseY - this.dragRowDiff, this.visibleWidth, rh, { fillColor: DRAG_TIP_BG_COLOR, borderWidth: 0, borderColor: 'transparent', }); const dragTargetDrawY = this.dragTarget.drawY; const dragTargetHeight = this.dragTarget.height; // 默认上方 let y = dragTargetDrawY; if (['before', 'after'].includes(this.dragPosition)) { if (this.dragPosition === 'after') { y = dragTargetDrawY + dragTargetHeight; } const poins = [0, y - 0.5, stageWidth, y - 0.5]; this.ctx.paint.drawLine(poins, { borderColor: DRAG_ROW_TIP_LINE_COLOR, borderWidth: 1.2, }); } // 中间位置 if (['inside'].includes(this.dragPosition)) { y = dragTargetDrawY + dragTargetHeight / 2; } // 倒三角形 const trianglePoins = [0, y - 4, 6, y, 0, y + 4, 0, y - 4]; this.ctx.paint.drawLine(trianglePoins, { borderColor: DRAG_ROW_TIP_LINE_COLOR, borderWidth: 1.2, fillColor: DRAG_ROW_TIP_LINE_COLOR, }); } } drawFixedShadow() { const { fixedLeftWidth, fixedRightWidth, scrollX, header, stageWidth, config: { HEADER_BG_COLOR, FIXED_COLUMN_SHADOW_COLOR, FIXED_COLUMN_SHADOW_FADE_COLOR, FIXED_COLUMN_SHADOW_WIDTH, }, } = this.ctx; const layoutScrollerTrackSize = getLayoutScrollerTrackSize(this.ctx.config); if (scrollX > 0 && fixedLeftWidth !== 0 && !this.ctx.isEmpty) { this.ctx.paint.drawShadow(this.x, this.y, fixedLeftWidth, this.height, { fillColor: HEADER_BG_COLOR, side: 'right', shadowWidth: FIXED_COLUMN_SHADOW_WIDTH, colorStart: FIXED_COLUMN_SHADOW_COLOR, colorEnd: FIXED_COLUMN_SHADOW_FADE_COLOR, }); } // 右边阴影 if (scrollX < Math.floor(header.width - stageWidth - 1) && fixedRightWidth !== layoutScrollerTrackSize && !this.ctx.isEmpty) { const x = header.width - (this.x + this.width) + stageWidth - fixedRightWidth; this.ctx.paint.drawShadow(x, this.y, fixedRightWidth, this.height, { fillColor: HEADER_BG_COLOR, side: 'left', shadowWidth: FIXED_COLUMN_SHADOW_WIDTH, colorStart: FIXED_COLUMN_SHADOW_FADE_COLOR, colorEnd: FIXED_COLUMN_SHADOW_COLOR, }); } } binarySearch(list, value) { let start = 0; let end = list.length - 1; let tempIndex = -1; while (start <= end) { let midIndex = Math.floor((start + end) / 2); let midValue = list[midIndex].bottom; if (midValue === value) { return midIndex; } else if (midValue < value) { start = midIndex + 1; } else { tempIndex = midIndex; // 记录当前的 midIndex end = midIndex - 1; } } return tempIndex; } update() { this.init(); const { header, database, scrollY, config: { CELL_HEIGHT }, } = this.ctx; const offset = scrollY; const { data, positions } = database.getData(); // 更新最大行数 this.ctx.maxRowIndex = data.length - 1; let _headIndex = this.binarySearch(positions, offset); let _tailIndex = this.binarySearch(positions, offset + this.visibleHeight); if (_tailIndex === -1) { _tailIndex = this.ctx.maxRowIndex; } // 解决性能问题,设置数据时又设置滚动条可能导致虚拟滚动计算错误 if (_headIndex === -1 && _tailIndex === this.ctx.maxRowIndex) { const buffer = Math.floor(this.visibleHeight / CELL_HEIGHT); _headIndex = this.ctx.maxRowIndex - buffer; } this.headIndex = Math.max(0, _headIndex); this.tailIndex = Math.min(this.ctx.maxRowIndex, _tailIndex + 1); this.visibleRows = data.slice(this.headIndex, this.tailIndex + 1); this.ctx.body.headIndex = this.headIndex; this.ctx.body.tailIndex = this.tailIndex; this.ctx.body.visibleRows = this.visibleRows; const rows = []; for (let i = 0; i < this.visibleRows.length; i++) { const index = this.headIndex + i; const data = this.visibleRows[i]; const { height, top } = this.ctx.database.getPositionForRowIndex(index); const row = new Row(this.ctx, index, 0, top + this.y, header.width, height, data); rows.push(row); } this.renderRows = rows; this.ctx.body.renderRows = rows; } updateAutoHeight() { const rows = this.ctx.body.renderRows; const hasAutoHeight = rows.some((row) => row.calculatedHeightCells.length > 0); // 如果没有计算格子,不更新 if (!hasAutoHeight) { return false; } // 更新计算高度 rows.forEach((row) => { row.updateCalculatedHeight(); }); // 如果有计算格子,重新计算行高 const heights = rows.map((row) => ({ height: row.calculatedHeight, rowIndex: row.rowIndex, })); return this.ctx.database.setBatchCalculatedRowHeight(heights); } draw() { // 容器背景 this.renderRows.forEach((row) => { row.drawContainer(); }); this.renderRows.forEach((row) => { row.drawHorizontalBorder(); }); this.renderRows.forEach((row) => { row.drawCenter(); }); this.drawFixedShadow(); this.renderRows.forEach((row) => { row.drawFixedContainer(); }); this.renderRows.forEach((row) => { row.drawFixedHorizontalBorder(); }); this.renderRows.forEach((row) => { row.drawFixed(); }); this.drawTipLine(); } } //# sourceMappingURL=Body.js.map