import { Ref, ShallowRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { AreaSelectionConfig, AreaSelectionRange, CellKeyGen, ColKeyGen, StkTableColumn, UniqKey, AreaSelectionSetterRange, AreaSelectionSetterOption, } from '../types'; import { VirtualScrollStore, VirtualScrollXStore } from '../useVirtualScroll'; import { getClosestColKey, getClosestTd, getClosestTr, getClosestTrIndex } from '../utils'; import { getCalculatedColWidth } from '../utils/constRefUtils'; import { MY_FN_NAME } from './const'; /** * 单元格区域选择功能 * 支持鼠标拖拽选择、键盘导航、复制粘贴等功能 * en: Cell area selection feature with mouse drag, keyboard navigation, copy-paste, etc. */ export function useAreaSelection
>( props: any, emits: any, tableContainerRef: Ref, dataSourceCopy: ShallowRef, tableHeaderLast: ShallowRef[]>, colKeyGen: ColKeyGen, cellKeyGen: CellKeyGen, scrollTo: (top: number | null, left: number | null) => void, virtualScroll: Ref, virtualScrollX: Ref, getRowIndex: (row: DT) => number, getColumnIndex: (col: StkTableColumn
) => number, ) { /** * 自动滚动:鼠标距容器边缘多少px开始触发 * en: Mouse distance from container edge to start auto scroll */ const EDGE_ZONE = 40; /** * 自动滚动:每帧最大滚动像素 * en: Maximum scroll pixels per frame */ const SCROLL_SPEED_MAX = 15; const POINT_EDGE_OFFSET = 2; const KEY_ARROW_UP = 'ArrowUp'; const KEY_ARROW_DOWN = 'ArrowDown'; const KEY_ARROW_LEFT = 'ArrowLeft'; const KEY_ARROW_RIGHT = 'ArrowRight'; const KEY_TAB = 'Tab'; const KEY_ESCAPE = 'Escape'; const KEY_ESC = 'Esc'; const KEY_C = 'c'; // CSS const CELL_RANGE_SELECTED = 'cell-range-selected'; const CELL_RANGE_TOP = 'cell-range-t'; const CELL_RANGE_BOTTOM = 'cell-range-b'; const CELL_RANGE_LEFT = 'cell-range-l'; const CELL_RANGE_RIGHT = 'cell-range-r'; const selectionRanges = ref([]); const isSelecting = ref(false); /** start cell */ let anchorCell: { rowIndex: number; colIndex: number } | null = null; /** auto scroll rAF id */ let autoScrollRafId = 0; /** * 最后一次鼠标位置(用于边界自动滚动计算) * en: Last mouse position (for boundary auto scroll calculation) */ let lastMouseClientX = 0; let lastMouseClientY = 0; const config = computed(() => { if (typeof props.areaSelection === 'boolean') { const b = props.areaSelection; return { enabled: b, keyboard: b, ctrl: b, shift: b, highlight: { cell: b, row: false } }; } const { highlight: userHighlight, ...restConfig } = props.areaSelection || {}; return { enabled: true, ctrl: true, shift: true, highlight: { cell: true, row: false, ...userHighlight, }, ...restConfig, }; }); /** 是否启用键盘控制选区移动 */ const keyboardEnabled = computed(() => config.value.keyboard); /** 是否启用 Ctrl 多选 */ const ctrlEnabled = computed(() => config.value.ctrl); /** 是否启用 Shift 扩选 */ const shiftEnabled = computed(() => config.value.shift); /** 是否启用单元格高亮 */ const highlightCellEnabled = computed(() => config.value.highlight?.cell); /** 是否启用行高亮 */ const highlightRowEnabled = computed(() => config.value.highlight?.row); /** colKey → absolute index 映射 */ const colKeyToIndexMap = computed(() => { const headers = tableHeaderLast.value; const map = new Map(); for (let i = 0; i < headers.length; i++) { map.set(colKeyGen.value(headers[i]), i); } return map; }); /** * 获取固定列宽度的函数 * 缓存每个固定列位置的累计宽度,查询时直接返回 * @param colIndex 目标列索引 * @returns [leftFixedWidth, rightFixedWidth] */ const getFixedColWidths = computed(() => { const cols = tableHeaderLast.value; // 预计算每个列索引位置对应的左右固定列累计宽度 // 左固定列:从左向右累加,非固定列继承前一个值 const leftWidths: number[] = new Array(cols.length + 1).fill(0); // 右固定列:从右向左累加,非固定列继承后一个值 const rightWidths: number[] = new Array(cols.length + 1).fill(0); let leftSum = 0; for (let i = 0; i < cols.length; i++) { leftWidths[i] = leftSum; if (cols[i]?.fixed === 'left') { leftSum += getCalculatedColWidth(cols[i]); } } leftWidths[cols.length] = leftSum; let rightSum = 0; for (let i = cols.length - 1; i >= 0; i--) { rightWidths[i] = rightSum; if (cols[i]?.fixed === 'right') { rightSum += getCalculatedColWidth(cols[i]); } } return (colIndex: number) => { return [leftWidths[colIndex] ?? 0, rightWidths[colIndex + 1] ?? 0] as const; }; }); /** 根据 selectionRanges 计算所有选区内 cellKey 的并集 */ const selectedCellKeys = computed>(() => { const ranges = selectionRanges.value; if (!ranges.length) return new Set(); const keys = new Set(); const cols = tableHeaderLast.value; const data = dataSourceCopy.value; for (const range of ranges) { const { begin: { row: r1, col: c1 }, end: { row: r2, col: c2 }, } = range.index; const [rStart, rEnd] = r1 < r2 ? [r1, r2] : [r2, r1]; const [cStart, cEnd] = c1 < c2 ? [c1, c2] : [c2, c1]; for (let r = rStart; r <= rEnd; r++) { const row = data[r]; if (!row) continue; for (let c = cStart; c <= cEnd; c++) { const col = cols[c]; if (col) keys.add(cellKeyGen(row, col)); } } } return keys; }); onMounted(() => { addListener(); }); onBeforeUnmount(() => { removeListener(); }); /** * 监听数据行数/列数变化,当行列变少时钳制选区与锚点,避免越界 * en: Watch row/col count changes, clamp selection ranges and anchor to avoid out-of-bounds */ watch([() => dataSourceCopy.value.length, () => tableHeaderLast.value.length], ([rowCount, colCount]) => { if (!config.value.enabled) return; // 钳制锚点 if (anchorCell) { if (rowCount === 0 || colCount === 0) { anchorCell = null; } else { anchorCell.rowIndex = clamp(anchorCell.rowIndex, 0, rowCount - 1); anchorCell.colIndex = clamp(anchorCell.colIndex, 0, colCount - 1); } } if (!selectionRanges.value.length) return; // 行或列为 0 时清空选区 if (rowCount === 0 || colCount === 0) { clearSelectedArea(); emitSelectionChange(); return; } const maxRow = rowCount - 1; const maxCol = colCount - 1; let changed = false; const newRanges: AreaSelectionRange[] = []; for (const range of selectionRanges.value) { const { begin, end } = range.index; const nbRow = clamp(begin.row, 0, maxRow); const nbCol = clamp(begin.col, 0, maxCol); const neRow = clamp(end.row, 0, maxRow); const neCol = clamp(end.col, 0, maxCol); if (nbRow !== begin.row || nbCol !== begin.col || neRow !== end.row || neCol !== end.col) { changed = true; newRanges.push(makeRange(nbRow, nbCol, neRow, neCol)); } else { newRanges.push(range); } } if (changed) { selectionRanges.value = newRanges; emitSelectionChange(); } }); function addListener() { removeListener(); tableContainerRef.value?.addEventListener('keydown', onKeydown); } function removeListener() { tableContainerRef.value?.removeEventListener('keydown', onKeydown); document.removeEventListener('mousemove', onDocumentMouseMove); document.removeEventListener('mouseup', onDocumentMouseUp); stopAutoScroll(); } /** 获取归一化(min/max)后的选区范围 */ function normalizeRange(range: AreaSelectionRange) { const { begin, end } = range.index; return { minRow: Math.min(begin.row, end.row), maxRow: Math.max(begin.row, end.row), minCol: Math.min(begin.col, end.col), maxCol: Math.max(begin.col, end.col), }; } /** * 构造选区范围。begin = 拖拽起点,end = 拖拽终点。 * 同时填充已废弃的 x/y 字段以保证向后兼容。 */ function makeRange(beginRow: number, beginCol: number, endRow: number, endCol: number): AreaSelectionRange { return { index: { x: [beginCol, endCol], y: [beginRow, endRow], begin: { row: beginRow, col: beginCol }, end: { row: endRow, col: endCol }, }, }; } /** 根据colKey获取列的绝对索引 */ function getColIndexByKey(colKey: string | undefined): number { if (!colKey) return -1; return colKeyToIndexMap.value.get(colKey) ?? -1; } /** 获取列的左边距和宽度 * @param colIndex 列的绝对索引 * @returns [left, width] */ function getColPosition(colIndex: number): [number, number] { let left = 0; const cols = tableHeaderLast.value; for (let i = 0; i < cols.length; i++) { const colWidth = getCalculatedColWidth(cols[i]); if (i === colIndex) return [left, colWidth]; left += colWidth; } return [left, 0]; } /** 根据按键计算移动方向 */ function getMovementDelta(key: string, shiftKey: boolean): [number, number] { let rowDelta = 0; let colDelta = 0; switch (key) { case KEY_ARROW_UP: rowDelta = -1; break; case KEY_ARROW_DOWN: rowDelta = 1; break; case KEY_ARROW_LEFT: colDelta = -1; break; case KEY_ARROW_RIGHT: colDelta = 1; break; case KEY_TAB: // Tab: right; Shift+Tab: left colDelta = shiftKey ? -1 : 1; break; } return [rowDelta, colDelta]; } /** 钳制值到指定范围内 */ function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(value, max)); } /** 处理Tab键的换行逻辑 */ function handleTabWrap(row: number, col: number, rawCol: number, rowCount: number, colCount: number): [number, number] { if (rawCol >= colCount) return [Math.min(row + 1, rowCount - 1), 0]; if (rawCol < 0) return [Math.max(row - 1, 0), colCount - 1]; return [row, col]; } /** 计算自动滚动的增量 */ function calculateAutoScrollDelta(mouseX: number, mouseY: number, rect: DOMRect): { deltaX: number; deltaY: number } { const { top, bottom, left, right } = rect; let deltaX = 0; let deltaY = 0; // Y方向 if (mouseY < top + EDGE_ZONE) { const dist = Math.max(0, top + EDGE_ZONE - mouseY); deltaY = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX); } else if (mouseY > bottom - EDGE_ZONE) { const dist = Math.max(0, mouseY - (bottom - EDGE_ZONE)); deltaY = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX); } // X方向 if (mouseX < left + EDGE_ZONE) { const dist = Math.max(0, left + EDGE_ZONE - mouseX); deltaX = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX); } else if (mouseX > right - EDGE_ZONE) { const dist = Math.max(0, mouseX - (right - EDGE_ZONE)); deltaX = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX); } return { deltaX, deltaY }; } /** mousedown 处理:设置锚点,开始拖选 */ function onSelectionMouseDown(e: MouseEvent) { if (!config.value.enabled || e.button !== 0) return; const rowIndex = getClosestTrIndex(e.target as HTMLElement); const colKey = getClosestColKey(e.target as HTMLElement); const colIndex = getColIndexByKey(colKey); if (rowIndex < 0 || colIndex < 0) return; const ctrlKey = e.ctrlKey || e.metaKey; const range: AreaSelectionRange = makeRange(rowIndex, colIndex, rowIndex, colIndex); // Shift 扩选:从锚点扩展到当前位置,更新最后一个区域 if (e.shiftKey && anchorCell && shiftEnabled.value) { const ranges = selectionRanges.value.slice(); const shiftRange: AreaSelectionRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, rowIndex, colIndex); if (ranges.length) { ranges[ranges.length - 1] = shiftRange; } else { ranges.push(shiftRange); } selectionRanges.value = ranges; } else { anchorCell = { rowIndex, colIndex }; if (ctrlKey && ctrlEnabled.value) { // Ctrl multiple selectionRanges.value = selectionRanges.value.concat([range]); } else { // normal click selectionRanges.value = [range]; } } isSelecting.value = true; lastMouseClientX = e.clientX; lastMouseClientY = e.clientY; // 防止拖选时选中文字 // en: Prevent text selection during drag document.addEventListener('mousemove', onDocumentMouseMove); document.addEventListener('mouseup', onDocumentMouseUp); } /** document mousemove 处理:更新选区终点 + 检测边界自动滚动 */ function onDocumentMouseMove(e: MouseEvent) { if (!isSelecting.value) return; lastMouseClientX = e.clientX; lastMouseClientY = e.clientY; // 尝试从当前鼠标位置更新选区 updateSelectionFromEvent(e); // 检测是否需要边界自动滚动 checkAutoScroll(); } /** 从 MouseEvent 目标元素更新选区 */ function updateSelectionFromEvent(e: MouseEvent) { const target = e.target as HTMLElement; if (!target) return; const rowIndex = getClosestTrIndex(target); if (Number.isNaN(rowIndex) || rowIndex < 0) return; const colKey = getClosestColKey(target); const colIndex = getColIndexByKey(colKey); if (colIndex < 0) return; updateSelectionEnd(rowIndex, colIndex); } /** 更新最后一个选区的终点(拖拽过程中) */ function updateSelectionEnd(endRowIndex: number, endColIndex: number) { if (!anchorCell) return; const newRange: AreaSelectionRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, endRowIndex, endColIndex); const ranges = [...selectionRanges.value]; if (ranges.length > 0) { ranges[ranges.length - 1] = newRange; } else { ranges.push(newRange); } selectionRanges.value = ranges; } // ---- 边界自动滚动 ---- /** 检查鼠标是否在容器边缘附近,启动或停止自动滚动 */ function checkAutoScroll() { const container = tableContainerRef.value; if (!container) return; const rect = container.getBoundingClientRect(); const { top, bottom, left, right } = rect; const nearEdge = lastMouseClientY < top + EDGE_ZONE || lastMouseClientY > bottom - EDGE_ZONE || lastMouseClientX < left + EDGE_ZONE || lastMouseClientX > right - EDGE_ZONE; if (nearEdge && !autoScrollRafId) { autoScrollLoop(); } else if (!nearEdge && autoScrollRafId) { stopAutoScroll(); } } /** rAF 循环:边界自动滚动 + 更新选区 */ function autoScrollLoop() { const container = tableContainerRef.value; if (!container || !isSelecting.value) { stopAutoScroll(); return; } const rect = container.getBoundingClientRect(); const { deltaX, deltaY } = calculateAutoScrollDelta(lastMouseClientX, lastMouseClientY, rect); if (deltaX !== 0 || deltaY !== 0) { container.scrollTop += deltaY; container.scrollLeft += deltaX; // 滚动后,在容器内边缘处用 elementFromPoint 找到当前单元格更新选区 updateSelectionFromPoint(container, rect); } // 如果还在拖选且仍需滚动,继续循环 if (isSelecting.value && (deltaX !== 0 || deltaY !== 0)) { autoScrollRafId = requestAnimationFrame(autoScrollLoop); } else { autoScrollRafId = 0; } } /** 将鼠标位置钳制到容器内部,用 elementFromPoint 找到边缘单元格并更新选区 */ function updateSelectionFromPoint(container: HTMLElement, containerRect: DOMRect) { // 获取表头高度,钳制 Y 时跳过表头区域 const thead = container.querySelector('thead'); const { top, bottom, left, right } = containerRect; const headerBottom = thead ? top + thead.offsetHeight : top; const x = Math.max(left + POINT_EDGE_OFFSET, Math.min(lastMouseClientX, right - POINT_EDGE_OFFSET)); const y = Math.max(headerBottom + POINT_EDGE_OFFSET, Math.min(lastMouseClientY, bottom - POINT_EDGE_OFFSET)); const el = document.elementFromPoint(x, y); if (!el) return; const td = getClosestTd(el as HTMLElement); const tr = getClosestTr(el as HTMLElement); if (!td || !tr) return; const rowIndex = getClosestTrIndex(tr); const colKey = getClosestColKey(td); const colIndex = getColIndexByKey(colKey); if (Number.isNaN(rowIndex) || rowIndex < 0 || colIndex < 0) return; updateSelectionEnd(rowIndex, colIndex); } /** 停止自动滚动 */ function stopAutoScroll() { if (autoScrollRafId) { cancelAnimationFrame(autoScrollRafId); autoScrollRafId = 0; } } /** document mouseup 处理:结束拖选 */ function onDocumentMouseUp() { if (!isSelecting.value) return; isSelecting.value = false; stopAutoScroll(); document.removeEventListener('mousemove', onDocumentMouseMove); document.removeEventListener('mouseup', onDocumentMouseUp); // 发出事件 emitSelectionChange(); } function emitSelectionChange() { emits('area-selection-change', selectionRanges.value); } /** 获取 areaSelection 配置中的格式化回调 */ function getFormatCellFn() { const cfg = config.value; return typeof cfg.formatCellForClipboard === 'function' ? cfg.formatCellForClipboard : null; } /** * 复制选区内容到剪贴板,只复制最后一个选区 * en: Copy selected area content to clipboard, only copy the last selected area * @returns {string} text */ function copySelectedArea(): string { const ranges = selectionRanges.value; if (!ranges.length) return ''; const range = ranges[ranges.length - 1]; const { minRow, maxRow, minCol, maxCol } = normalizeRange(range); const data = dataSourceCopy.value; const cols = tableHeaderLast.value; const formatCell = getFormatCellFn(); const lines: string[] = []; for (let r = minRow; r <= maxRow; r++) { const row = data[r]; if (!row) continue; const cells: string[] = []; for (let c = minCol; c <= maxCol; c++) { const col = cols[c]; if (!col) { cells.push(''); continue; } const rawValue = row[col.dataIndex]; cells.push(formatCell ? formatCell(row, col, rawValue) : !rawValue ? '' : String(rawValue)); } lines.push(cells.join('\t')); } const text = lines.join('\n'); navigator.clipboard.writeText(text).catch(() => { console.warn('Failed to copy to clipboard'); }); return text; } function blurCellElement() { // 防止虚拟滚动移除 DOM 时焦点被动丢失,导致后续 keydown 无法冒泡到容器 // en: Prevent focus loss when virtual scroll removes DOM, causing subsequent keydown events to not bubble to the container const container = tableContainerRef.value; const activeEl = document.activeElement as HTMLElement | null; if (container && activeEl && container.contains(activeEl) && activeEl !== container) { container.focus({ preventScroll: true }); } } /** * Ctrl+C / Cmd+C copy * Esc :cancel * Arrow keys / Tab move (when keyboard=true) **/ function onKeydown(e: KeyboardEvent) { if (!config.value.enabled) return; const key = e.key; // Esc :cancel if (key === KEY_ESCAPE || key === KEY_ESC) { blurCellElement(); if (selectionRanges.value.length) { // clearSelectedArea(); // emitSelectionChange(); e.preventDefault(); } return; } // Ctrl/Cmd+C copy if ((e.ctrlKey || e.metaKey) && key === KEY_C && selectionRanges.value.length) { copySelectedArea(); e.preventDefault(); return; } if (!keyboardEnabled.value) return; const isArrowKey = [KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT].includes(key); const isTabKey = key === KEY_TAB; const isNavigationKey = isArrowKey || isTabKey; if (!isNavigationKey) return; e.preventDefault(); const rowCount = dataSourceCopy.value.length; const colCount = tableHeaderLast.value.length; if (rowCount === 0 || colCount === 0) return; // 如果没有选区,默认从第一个单元格开始 // en: If no selection, start from the first cell if (!selectionRanges.value.length) { anchorCell = { rowIndex: 0, colIndex: 0 }; selectionRanges.value = [makeRange(0, 0, 0, 0)]; emitSelectionChange(); scrollToCell(0, 0); return; } // 计算移动方向 const [rowDelta, colDelta] = getMovementDelta(key, e.shiftKey); // Shift 扩展选区,否则移动单格选区 if (e.shiftKey && isArrowKey && shiftEnabled.value) { blurCellElement(); // 扩展选区:保留 begin,更新最后一个区域的 end const ranges = [...selectionRanges.value]; const range = ranges.length > 0 ? ranges[ranges.length - 1] : null; if (!range) return; const { begin, end } = range.index; let newEndRow = end.row + rowDelta; let newEndCol = end.col + colDelta; // 边界检查 newEndRow = clamp(newEndRow, 0, rowCount - 1); newEndCol = clamp(newEndCol, 0, colCount - 1); ranges[ranges.length - 1] = makeRange(begin.row, begin.col, newEndRow, newEndCol); selectionRanges.value = ranges; scrollToCell(newEndRow, newEndCol); } else { blurCellElement(); // 移动单格选区 // 取最后一个区域的 end 位置作为基础,清空旧选区重建 const ranges = selectionRanges.value; const range = ranges.length > 0 ? ranges[ranges.length - 1] : null; const baseRow = range ? normalizeRange(range).minRow : 0; const baseCol = range ? normalizeRange(range).minCol : 0; let newRow = baseRow + rowDelta; let newCol = baseCol + colDelta; // 边界检查(先检查,避免越界) newRow = clamp(newRow, 0, rowCount - 1); newCol = clamp(newCol, 0, colCount - 1); // Tab 换行逻辑:如果到达行尾/行首,换行 if (isTabKey) { // 计算原始未 clamp 的值 const rawCol = baseCol + colDelta; const [tabRow, tabCol] = handleTabWrap(baseRow, newCol, rawCol, rowCount, colCount); newRow = tabRow; newCol = tabCol; } // 更新锚点和选区(移动单格时清空其他区域,仅保留新位置) anchorCell = { rowIndex: newRow, colIndex: newCol }; selectionRanges.value = [makeRange(newRow, newCol, newRow, newCol)]; scrollToCell(newRow, newCol); } emitSelectionChange(); } /** * 滚动到指定单元格,确保其在可视区域内 * @param rowIndex 行索引 * @param colIndex 列索引 */ function scrollToCell(rowIndex: number, colIndex: number) { const container = tableContainerRef.value; if (!container) return; const row = dataSourceCopy.value[rowIndex]; const col = tableHeaderLast.value[colIndex]; if (!row || !col) return; const thead = container.querySelector('thead'); const headerHeight = thead ? thead.offsetHeight : 0; const tfoot = container.querySelector('tfoot'); const footerHeight = tfoot ? tfoot.offsetHeight : 0; const vs = virtualScroll.value; const vsx = virtualScrollX.value; // 是否开启按行滚动模式(experimental.scrollY 模式) const isScrollRowByRow = props.scrollRowByRow; // 计算目标行的位置(基于虚拟滚动数据) const rowHeight = vs.rowHeight; const targetRowTop = rowIndex * rowHeight; const targetRowBottom = targetRowTop + rowHeight; // 计算可视区域 // experimental.scrollY 模式下,容器 scrollTop 始终为 0,需要使用 virtualScroll.scrollTop const visibleTop = isScrollRowByRow ? vs.scrollTop : container.scrollTop; const visibleBottom = visibleTop + vs.containerHeight - headerHeight - footerHeight; // 计算需要的垂直滚动位置 let newScrollTop: number | null = null; if (targetRowTop < visibleTop) { // 目标行在可视区域上方,滚动到使目标行位于顶部 newScrollTop = targetRowTop; } else if (targetRowBottom > visibleBottom) { // 目标行在可视区域下方 newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight - footerHeight); } // 计算目标列的位置 const [targetColLeft, targetColWidth] = getColPosition(colIndex); const targetColRight = targetColLeft + targetColWidth; // 计算可视区域(水平) const visibleLeft = container.scrollLeft; const visibleRight = visibleLeft + vsx.containerWidth; // 计算固定列的宽度(用于检测遮挡) const [leftFixedWidth, rightFixedWidth] = getFixedColWidths.value(colIndex); let newScrollLeft: number | null = null; if (targetColLeft < visibleLeft + leftFixedWidth) { // 目标列在左侧固定列遮挡区域内,需要向左滚动 newScrollLeft = targetColLeft - leftFixedWidth; } else if (targetColRight > visibleRight - rightFixedWidth) { // 目标列在右侧固定列遮挡区域内,需要向右滚动 newScrollLeft = targetColRight - vsx.containerWidth + rightFixedWidth; } if (newScrollTop !== null || newScrollLeft !== null) { scrollTo(newScrollTop, newScrollLeft); } } /** * 判断一个单元格的选区样式类名 * @param cellKey 单元格唯一键 * @param absoluteRowIndex 行在 dataSourceCopy 中的绝对索引 * @param colKey 列唯一键 * @returns 样式类名数组 */ function getAreaSelectionClasses(cellKey: string, absoluteRowIndex: number, colKey: UniqKey): string[] { if (!highlightCellEnabled.value) return []; if (!selectedCellKeys.value.has(cellKey)) return []; const colIndex = colKeyToIndexMap.value.get(colKey); if (colIndex === void 0 || colIndex < 0) return []; const classes: string[] = [CELL_RANGE_SELECTED]; const ranges = selectionRanges.value; if (!ranges.length) return classes; const lastRange = ranges[ranges.length - 1]; const { minRow, maxRow, minCol, maxCol } = normalizeRange(lastRange); // 判断当前单元格是否在最后一个区域内 const isInLastRange = absoluteRowIndex >= minRow && absoluteRowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol; if (isInLastRange) { if (absoluteRowIndex === minRow) classes.push(CELL_RANGE_TOP); if (absoluteRowIndex === maxRow) classes.push(CELL_RANGE_BOTTOM); if (colIndex === minCol) classes.push(CELL_RANGE_LEFT); if (colIndex === maxCol) classes.push(CELL_RANGE_RIGHT); } return classes; } /** * 判断一行的选区样式类名(行高亮) * @param absoluteRowIndex 行在 dataSourceCopy 中的绝对索引 * @returns 样式类名数组 */ function getAreaSelectionRowClasses(absoluteRowIndex: number): string[] { if (!highlightRowEnabled.value) return []; const ranges = selectionRanges.value; if (!ranges.length) return []; // 检查该行是否在任何选区内 for (const range of ranges) { const { begin: { row: r1 }, end: { row: r2 }, } = range.index; const [minRow, maxRow] = r1 < r2 ? [r1, r2] : [r2, r1]; if (absoluteRowIndex >= minRow && absoluteRowIndex <= maxRow) { return ['row-range-selected']; } } return []; } // expose function /** 获取选中的单元格信息 */ function getSelectedArea() { const ranges = selectionRanges.value; if (!ranges.length) return { rows: [] as DT[], cols: [] as StkTableColumn
[], ranges: [] as AreaSelectionRange[] }; const data = dataSourceCopy.value; const cols = tableHeaderLast.value; // 收集所有区域的行和列 const rowSet = new Set(); const colSet = new Set(); for (const range of ranges) { const { minRow, maxRow, minCol, maxCol } = normalizeRange(range); for (let r = minRow; r <= maxRow; r++) rowSet.add(r); for (let c = minCol; c <= maxCol; c++) colSet.add(c); } const sortedRows = [...rowSet].sort((a, b) => a - b); const sortedCols = [...colSet].sort((a, b) => a - b); return { rows: sortedRows.map(i => data[i]).filter(Boolean), cols: sortedCols.map(i => cols[i]).filter(Boolean), ranges: ranges.map(r => ({ ...r })), }; } function clearSelectedArea() { selectionRanges.value = []; isSelecting.value = false; } function setAreaSelection(ranges?: AreaSelectionSetterRange
, option: AreaSelectionSetterOption = {}): AreaSelectionRange[] { if (!config.value.enabled) return selectionRanges.value; const { silent = false, scrollToView = false } = option; const rowCount = dataSourceCopy.value.length; const colCount = tableHeaderLast.value.length; if (rowCount <= 0 || colCount <= 0) { clearSelectedArea(); if (!silent) emitSelectionChange(); return selectionRanges.value; } const maxRow = rowCount - 1; const maxCol = colCount - 1; let beginRow = 0; let endRow = maxRow; let beginCol = 0; let endCol = maxCol; if (ranges) { const begin = ranges.begin; const end = ranges.end ?? begin; beginRow = typeof begin.row === 'number' ? begin.row : getRowIndex(begin.row); endRow = typeof end.row === 'number' ? end.row : getRowIndex(end.row); const beginColInput = typeof begin.col === 'number' ? begin.col : begin.col ? getColumnIndex(begin.col) : void 0; const endColInput = typeof end.col === 'number' ? end.col : end.col ? getColumnIndex(end.col) : void 0; if (beginColInput !== void 0) { beginCol = beginColInput; endCol = endColInput !== void 0 ? endColInput : beginColInput; } else if (endColInput !== void 0) { beginCol = 0; endCol = endColInput; } } beginRow = clamp(beginRow, 0, maxRow); endRow = clamp(endRow, 0, maxRow); beginCol = clamp(beginCol, 0, maxCol); endCol = clamp(endCol, 0, maxCol); selectionRanges.value = [makeRange(beginRow, beginCol, endRow, endCol)]; anchorCell = { rowIndex: beginRow, colIndex: beginCol }; isSelecting.value = false; if (scrollToView) { scrollToCell(endRow, endCol); } if (!silent) emitSelectionChange(); return selectionRanges.value; } return { config, isSelecting, getClass: getAreaSelectionClasses, getRowClass: getAreaSelectionRowClasses, get: getSelectedArea, set: setAreaSelection, clear: clearSelectedArea, copy: copySelectedArea, onMD: onSelectionMouseDown, }; } export const useAreaSelectionName = 'useAreaSelection'; (useAreaSelection as any)[MY_FN_NAME] = useAreaSelectionName;