import { ID, IDimension, IStoreState } from '../index.data'; import { SpreadSheetRuntime } from '../RuntimeContext'; import { getAllCollapsedRowFlags, getSelectionScope } from '../store/selectors'; import * as R from 'rambda'; import { getColumnLeft_exceptFrozen, getFrozenHeight, getFrozenWidth, getRowTop_exceptCollapsedAndFrozen, } from '../store/selectors/dimensions'; export function inFrozenArea(state: IStoreState) { const scope = getSelectionScope(state); const { freezeAt } = state; return scope.toI <= freezeAt.i || scope.toJ <= freezeAt.j; } /** * 根据上述exceeds, 重新设置 scrollX/scrollY */ export function tryMoveSelectedIntoView(context: SpreadSheetRuntime, dim: IDimension) { // if (dim.offsetWidth <= 1 || dim.offsetHeight <= 1) return false; const state = context.store.getState(); const result = getScrollByMoveSelectIntoView(state, dim); const { scrollX: scrollX0, scrollY: scrollY0 } = state; if (Math.abs(result.scrollX - scrollX0) > 10 || Math.abs(result.scrollY - scrollY0) > 0) { // 误差10, 避免无意义的滚动(可能导致重复滚) context!.patchState(Object.assign(result, { readyDimensions: true })); return true; } else { return false; } } // 比如: 当selected隐藏到底部, 被自动滚上来, 此时 selected的底部距viewport底部的距离 const preserveMarginH = (viewport: { w: number }) => Math.min(((1 / 6.0) * viewport.w) | 0, 100); const preserveMarginV = (viewport: { h: number }) => Math.min(((1 / 6.0) * viewport.h) | 0, 100); /** * 计算scrollX/scrollY, 使选中的区域移到屏幕内 * @param state * @param 当前选区的位置, 可以很容易地判断哪个方向在选区外. 这个参数是冗余的, 但避免了重复计算 * @return */ export function getScrollByMoveSelectIntoView( state: IStoreState, dim: IDimension, ): { scrollX: number; scrollY: number } { let scrollX = state.scrollX, scrollY = state.scrollY; const selectionIndex = getSelectionScope(state); const frozenWidth = getFrozenWidth(state); const frozenHeight = getFrozenHeight(state); const rowIndicatorWidth = state.colDimensions[state.columns[0].id!].offsetLeft; const columnIndicatorHeight = state.rowDimensions[state.rows[0].id].offsetTop; // 1. 判断当前显示区域是否完整位于viewport内部 // 几种可能的组合: 左侧在显示区域外, 右侧在显示区域外, 顶部在显示区域外, 底部在显示区域外 const viewport = state.containerBox; const exceeds = { left: -(dim.offsetLeft - frozenWidth - rowIndicatorWidth), // 左侧超出左边界, > 0 表示超出 top: -(dim.offsetTop - frozenHeight - columnIndicatorHeight), // 上侧超出上边界, > 0 表示超出 right: dim.offsetLeft + dim.offsetWidth - viewport.w, // 右侧超出右边界, > 0 表示超出 bottom: dim.offsetTop + dim.offsetHeight - viewport.h, // 下侧超出下边界, > 0 表示超出 }; // 2. 如果不, 则将其移到显示区域内部, 原则: 如果左侧在显示区域外, 则确保: // | 移进来的dim左边界, 离viewport左边界, 不少于min(1/6容器宽,100px) const { fromI, toI, fromJ, toJ } = selectionIndex; // 避免死循环(上次计算出来的selectionBox.bottom移进视口, 选成top移出视口, 反复循环) if ( (exceeds.left > 0 || exceeds.right > 0) && dim.offsetWidth + preserveMarginH(viewport) > viewport.w ) { // 这里应忽略, 避免反复循环 ( 容器宽选择集宽太接近了 ) } else { // 水平向超出 if ( (exceeds.left > 0 && toJ + 1 !== state.columns.length) || // 对于已被滚到左侧的列 (exceeds.left === 0 && dim.offsetWidth === 0) ) { scrollX = getScrollXMadeLeftVisible_whenLeftExceed(state, viewport, fromJ); scrollX = Math.max(0, scrollX); } else if (exceeds.right > 0 && fromJ > 0) { scrollX = getScrollXMadeRightVisible_whenRightExceed(state, viewport, toJ); scrollX = Math.max(0, scrollX); } else { } } // 竖直向超出 if ( /*避免反复上下循环*/ (exceeds.top > 0 || exceeds.bottom > 0) && dim.offsetHeight + preserveMarginV(viewport) > viewport.h ) { // 这里应忽略, 避免反复循环 ( 容器宽选择集宽太接近了 ) } else { if ( (exceeds.top > 0 && toI + 1 !== state.rows.length) || // 对于已被滚到顶部的行 (exceeds.top === 0 && dim.offsetHeight === 0) ) { scrollY = getScrollYMadeTopVisible_whenTopExceed(state, viewport, fromI); scrollY = Math.max(0, scrollY); } else if (exceeds.bottom > 0 && fromI > 0) { scrollY = getScrollYMadeBottomVisible_whenBottomExceed(state, viewport, toI); scrollY = Math.max(0, scrollY); } else { } } return { scrollX, scrollY }; } /** * 当selectedRow底部超出视口时, 重新计算scrollY, 使底部上升到视口中 * @param state * @param viewport * @param _rowIndex * @return 新的scrollY, 此scrollY将使rowIndex代表的行滚动到视口内部 */ export function getScrollYMadeBottomVisible_whenBottomExceed( state: IStoreState, viewport: { w: number; h: number }, _rowIndex: number, ): number { /* ------ A1(100) ===================================== viewport top (viewport.height=200) ------ A2(100) ------ A3(100) ===================================== viewport bottom ------ 此时A3被隐藏50(1/2 row.height) A4(100) ------ A5(100) ------ */ // 上移后情况如下: preserveV = min(200/6, 100) = 33 // 注意按此做法, A2将隐藏33, 显示67, 所以最佳策略是全部显示, 即scrollY=100 /* ------ A1(100) ------ A2隐藏33 A2(100) ===================================== viewport top (viewport.height=200) ------ A2显示67 A3(100) ------ 此时A3底部显示出33px, 由此反推 A4(100) ===================================== viewport bottom ------ A5(100) ------ */ let acc = preserveMarginV(viewport); const collapsedRowFlags = getAllCollapsedRowFlags(state); let rowIdx = _rowIndex; const { rows, rowDimensions, freezeAt } = state; const v_viewport_except_freezed = viewport.h - getFrozenHeight(state); while (rowIdx > freezeAt.i) { if (!collapsedRowFlags[rowIdx]) { const rowId = rows[rowIdx].id; acc += rowDimensions[rowId].height; if (acc > v_viewport_except_freezed) { break; } } rowIdx--; } const topRowIdx = rowIdx + 1; return getRowTop_exceptCollapsedAndFrozen(state, collapsedRowFlags, topRowIdx); } export function getScrollYMadeTopVisible_whenTopExceed( state: IStoreState, viewport: { w: number; h: number }, _rowIndex: number, ): number { const margin = preserveMarginV(viewport); let acc = 0; const collapsedRowFlags = getAllCollapsedRowFlags(state); let rowIdx = _rowIndex - 1; const { rows, rowDimensions, freezeAt } = state; while (rowIdx > freezeAt.i) { if (!collapsedRowFlags[rowIdx]) { const rowId = rows[rowIdx].id; acc += rowDimensions[rowId].height; if (acc > margin) { break; } } rowIdx--; } const topRowIdx = rowIdx; return getRowTop_exceptCollapsedAndFrozen(state, collapsedRowFlags, topRowIdx); } export function getScrollXMadeLeftVisible_whenLeftExceed( state: IStoreState, viewport: { w: number; h: number }, _colIndex: number, ): number { const margin = preserveMarginH(viewport); let acc = 0; let colIndex = _colIndex; const { columns, colDimensions, freezeAt } = state; while (colIndex > freezeAt.j) { const colId = columns[colIndex].id!; acc += colDimensions[colId].width; if (acc > margin) { break; } colIndex--; } const leftColIdx = colIndex; return getColumnLeft_exceptFrozen(state, leftColIdx); } function getScrollXMadeRightVisible_whenRightExceed( state: IStoreState, viewport: { w: number; h: number }, _colIndex: number, ): number { let acc = preserveMarginH(viewport); let colIdx = _colIndex; const { columns, colDimensions, freezeAt } = state; const h_viewport_except_freezed = viewport.w - getFrozenWidth(state); while (colIdx > freezeAt.j) { const colId = columns[colIdx].id!; acc += colDimensions[colId].width; if (acc > h_viewport_except_freezed) { break; } colIdx--; } const leftColIdx = colIdx + 1; return getColumnLeft_exceptFrozen(state, leftColIdx); }