import { IStoreState, IColumn, IRow, ICellNN, ID, FormulaManagerConfig, ICell, } from '../../index.data'; import * as R from 'rambda'; import { idMap, dissoc, sliceBy, dropIndex, insertIndex, isPlainObject } from 'valor-app-utils'; import { recalculate } from './recalculate'; import { uuid } from './utils'; import { SpreadSheetRuntime } from '../../RuntimeContext'; import { cloneElement } from 'react'; import { IRowWithCell } from '../../SpreadSheetProvider/index.data'; import { IFormulaRef_RelativePosition, updateFormula } from '../formula_support'; function copyCell( cell: ICellNN, count: number, copyAttrs: ('value' | 'formula' | 'locked')[], rowId: ID, id?: ID, ): ICellNN[] { return R.range(0, count) .map(i => ({ ...cell, id: id || uuid(), formula: copyAttrs.indexOf('formula') >= 0 ? cell.formula : undefined, formulaDisabled: copyAttrs.indexOf('formula') >= 0 ? cell.formulaDisabled : false, locked: copyAttrs.indexOf('locked') >= 0 ? cell.locked : false, error: null, meta: null, rowId, value: copyAttrs.indexOf('value') >= 0 ? cell.value : cell.dataType === 'number' ? 0 : '', })) .map(it => dissoc(it, ['rowspan', 'colspan'])); } function createCell(proto: Partial, rowId: ID, count: number, id?: ID): ICellNN[] { return R.range(0, count).map(i => ({ id: id || uuid(), rowId, type: 'text', dataType: 'string', value: proto.dataType === 'number' ? 0 : '', formulaDisabled: false, error: null, meta: null, ...proto, })); } /** * 在第j列插入空列, 空列为column * @param state * @param j * @param column * @param cellsToFill 在插列完毕后, 可以将cellsToFill从上到下填入cell * @param forceMergePolicy : 在第4列插入, 第1行的第1个单元格colspan=3, 则插入后, 不新增单元格, 将colspan设为4 * 插入前: 1 2 3 4 * __ _ __ _ __ | _ | _ * __ | __ | __ | _ | _ * __ | __ | __ | _ | _ * 若autoMerge=[]: * 插入后: 1 2 3 4(new) 5 * __ _ __ _ __ | _ | _ | _ * __ | __ | __ | _ | _ | _ * __ | __ | __ | _ | _ | _ * 若autoMerge=[left]: * 插入后: 1 2 3 4(new) 5 * __ _ __ _ __ _ _ | _ | _ * __ | __ | __ | _ | _ | _ * __ | __ | __ | _ | _ | _ * 注意: 无论autoMerge为何, 如果在第2列插入, 则无论如何1234都会被合并 * * @param runtime * @param testing 如果为true, 则将生成 randomid1 这样的cellid, 供测试 */ export function insertColumn( state: IStoreState, j: number, column: IColumn, cellsToFill: Partial[], forceMergePolicy: 'left' | null, // 向right合并很不常见, 并且需要删除单元格, 有点麻烦, 忽略 copyCellAttrs: ('value' | 'formula' | 'locked')[], //新创建的cell要复制相邻 cell 的哪些属性 runtime: SpreadSheetRuntime, testing?: boolean, ): { newState: Partial; updatedStaleCells: ICellNN[] } { const updatedColumns = [ ...state.columns.slice(0, j), { ...column, id: state.columns.length, j }, ...state.columns.slice(j).map(it => ({ ...it, j: it.j! + 1 })), ]; const { updatedCells, insertedCells, updatedRows, baseCell2newCell } = state.rows.reduce( (acc, row, i) => { const { insertedCell, updatedCell, updatedRow, sourceCell } = insertCell( row, cellsToFill[i] as any, acc.prevRowColspans, state.cells, j, forceMergePolicy, // 如果提供了模板单元格, 则仅从左侧单元格复制shape R.isEmpty(cellsToFill) ? copyCellAttrs : ['value', 'formula', 'locked'], testing, ); const updatedCells = updatedCell ? acc.updatedCells.concat(updatedCell) : acc.updatedCells; const insertedCells = insertedCell ? acc.insertedCells.concat( // 为何copy { ...insertedCell, ...cellsToFill[i] }? // 是否insertedCell不存在, 则cellsToFill[i]一定不存在? // 不一定: // ----------------------- // | 大标题 | // |_____ |_______________ // | | | // 右侧插列(不forceMerge) => 填入特定的cellsToFill => 合并第一行 // 第一步: 右侧插列: 此时, 由于并不forceMerge, 所以右侧第一行会产生新的 "大标题" 单元格 ( 因为是复制左侧 ): // ----------------------- ------------- // | 大标题 | 大标题 | // |_____ |_____________________________ // | | | | // 第二步: 填入cellsToFill, 此时, 由于最终第一行需要合并, 所以cellsToFills[0]应该是null // 这就造成了 insertedCell/cellsToFill并不同时为空 // copyCell({ ...insertedCell, ...cellsToFill[i] }, 1, copyCellAttrs, insertedCell.id)[0], insertedCell, ) : acc.insertedCells; const updatedRows = acc.updatedRows.concat(updatedRow); const prevRowColspans = updatedRow.cellIds!.map(cellId => cellId ? ( insertedCells.concat(updatedCells.map(it => it.new)).find(it => it.id === cellId) || state.cells[cellId] ).colspan || 1 : 0, ); return { updatedCells, insertedCells, updatedRows, prevRowColspans, baseCell2newCell: sourceCell && sourceCell.id && insertedCell ? Object.assign({}, acc.baseCell2newCell, { [sourceCell.id + '']: insertedCell.id }) : acc.baseCell2newCell, }; }, { updatedCells: [] as { old: ICellNN; new: ICellNN }[], insertedCells: [] as ICellNN[], updatedRows: [] as IRow[], prevRowColspans: R.repeat(1, state.columns.length + 1), baseCell2newCell: {} as Record, }, ); const newStateAfterInsert = { rows: updatedRows, columns: updatedColumns, cells: Object.assign( {}, state.cells, idMap(updatedCells.map(it => it.new)), idMap(insertedCells), ), } as IStoreState; /**** 修改公式 */ const updatedFormulaCells = updateFormula(state, newStateAfterInsert, baseCell2newCell); for (let i = 0; i < updatedFormulaCells.length; i++) { const cell = updatedFormulaCells[i]; newStateAfterInsert.cells[cell.id].formula = cell.formula; } /** 修改公式完成 */ const calculatedStatePatch = runtime.batchMode ? Object.assign({}, newStateAfterInsert) : recalculate( { ...state, ...newStateAfterInsert }, { force: true, config: runtime && runtime.formulaManager.config }, ); return { newState: { rows: updatedRows, columns: updatedColumns, ...calculatedStatePatch }, // 有哪些单元格的colspan受影响了, 需要记下来, 以便下次撤销时需要减回来 updatedStaleCells: updatedCells.map(it => it.old), }; } export function deleteColumn( state: IStoreState, j: number, runtime: SpreadSheetRuntime, testing?: boolean, ) { const updatedColumns = [ ...state.columns.slice(0, j), ...state.columns.slice(j + 1).map(it => ({ ...it, j: it.j! - 1 })), ]; const { deletedCells, insertedCells, updatedCells, updatedRows } = state.rows.reduce( (acc, row, i) => { const { insertedCell, deletedCell, updatedCell, updatedRow } = deleteCell( row, state.cells, j, testing, ); return { updatedCells: updatedCell ? acc.updatedCells.concat(updatedCell) : acc.updatedCells, insertedCells: acc.insertedCells.concat(insertedCell), deletedCells: acc.deletedCells.concat(deletedCell), updatedRows: acc.updatedRows.concat(updatedRow), }; }, { updatedCells: [] as { old: ICellNN; new: ICellNN }[], insertedCells: [] as ICell[], // 按顺序返回从第1行到第N行的deleteCell, 可能为null, 顺序不能乱, 便于撤销 deletedCells: [] as ICell[], updatedRows: [] as IRow[], }, ); const newStateAfterDelete = { rows: updatedRows, columns: updatedColumns, cells: Object.assign( {}, dissoc(state.cells, deletedCells.filter(Boolean).map(it => it!.id + '')), idMap(updatedCells.map(it => it.new)), idMap(insertedCells.filter(Boolean)), ), }; const calculatedStatePatch = runtime.batchMode ? Object.assign({}, newStateAfterDelete) : recalculate( { ...state, ...newStateAfterDelete }, { force: true, config: runtime && runtime.formulaManager.config }, ); return { newState: { rows: updatedRows, columns: updatedColumns, ...calculatedStatePatch }, // 有哪些单元格的colspan受影响了, 需要记下来, 以便下次撤销时需要减回来 updatedStaleCells: updatedCells.map(it => it.old), // 有哪些单元被删除了, 下次撤销时要恢复过来 deletedCells, // 有哪些单元格被创建了, 下次撤销要替换为null insertedCells, }; } /** * 往row的第j列, 插入一个空白cell * 如果j-1 与 old_j 已经合并, 则插入后j-1 与 j 与 j+1(即old_j) 都将合并 * 如果j-2 与 j-1 已经合并, 但j-1与old_j并未合则, 则: * 若forceMergePolicy==='left', 则 三者合并 * 若forceMergePolicy===null, 则 没有任何合并 * @param row: 要插入cell的行 * @param j: 要在第j列插入 * @param baseCell: 如果为null, 则复制closestCell, 否则复制baseCell ( 可从外部提供 ) * @param cellMap * @param forceMergePolicy * @param copyCellAttrs * @param testing */ export function insertCell( row: IRow, baseCell: ICell, updatedPreRowColspans: number[], cellMap: Record, j: number, forceMergePolicy: 'left' | null, copyCellAttrs: ('value' | 'formula' | 'locked')[], testing?: boolean, ) { const leftClosestSolidCellId = R.last(row.cellIds!.slice(0, j).filter(Boolean)); const leftClosestSolidCell = leftClosestSolidCellId ? cellMap[leftClosestSolidCellId!] : null; const leftClosestSolidCellIndex = !leftClosestSolidCellId ? -1 : row.cellIds!.findIndex(cellId => cellId === leftClosestSolidCellId); const rightClosestSolidCellId = R.head(row.cellIds!.slice(j).filter(Boolean)); const rightClosestSolidCell = rightClosestSolidCellId ? cellMap[rightClosestSolidCellId!] : null; const rightClosestSolidCellIndex = !rightClosestSolidCellId ? -1 : row.cellIds!.findIndex(cellId => cellId === rightClosestSolidCellId); if ( leftClosestSolidCell && // 必须是合并单元格 (leftClosestSolidCell.colspan || 1) >= 2 && // 以下的理解: // row2.cellIds = [31, null, 33, null] // 在j=1处插入时, 一定合被合并 // 在j=2处插入时, [33,null]会被挤到右侧, 此时若forceMerge, 则会被合并 (leftClosestSolidCellIndex + (leftClosestSolidCell.colspan || 1) - 1 >= j || (leftClosestSolidCellIndex + (leftClosestSolidCell.colspan || 1) - 1 === j - 1 && forceMergePolicy)) ) { // 1. 产生主动合并:新加的单元格将被同行左单元格合并, 更新左单元格即可 if (!leftClosestSolidCell) throw new Error('未考虑到的情形'); if (!leftClosestSolidCell.colspan) throw new Error('未考虑到'); const updatedCell = { ...leftClosestSolidCell, colspan: leftClosestSolidCell.colspan + 1 }; return { insertedCell: null, updatedCell: { old: leftClosestSolidCell, new: updatedCell }, updatedRow: { ...row, cellIds: [...row.cellIds!.slice(0, j), null, ...row.cellIds!.slice(j)], }, sourceCell: null, }; } else if ( (!leftClosestSolidCell || (leftClosestSolidCell && (leftClosestSolidCell.colspan || 1) >= 2 && // 以下的理解: // row2.cellIds = [31, 32, 33, null] // row3.cellIds = [41, null, null, null], 41.cospan=2 // 可以断定: 33.rowspan=2, 33.colspan=2 // 因此,1.在row3, j=3处插入时, 因为row2已处理, 33.colspan已变成3, 所以这里无需处理 leftClosestSolidCellIndex + (leftClosestSolidCell.colspan || 1) - 1 < j - 1)) && // 但2. 在row3, j=4处插入时, 必须看上一行的的插入结果, 如果上一行j列插入了null单元格, 表示被合并, 本行也需要被合并 updatedPreRowColspans && updatedPreRowColspans[j] === 0 // 最上面的!leftClosestSolidCell是考虑以下的理解: // row2.cellIds = [31, null, 33, null] // row3.cellIds = [null, null, null, null] // 对row3行进行插入, 尤其是最后一个单元格, 同样也需要考虑上行 ) { // 2. 产生被动合并(上行已合并过了): 新加的单元格被上面的行合并, 不做操作 return { insertedCell: null, updatedCell: null, updatedRow: { ...row, cellIds: [...row.cellIds!.slice(0, j), null, ...row.cellIds!.slice(j)], }, sourceCell: null, }; } else { // 3. 新加的单元格不会合并, 应创建新单元格 const id = testing ? `randomid${row.id}` : uuid(); const closestCell = j === 0 ? rightClosestSolidCell : leftClosestSolidCell; // 由于肯定会创建新单元格, 所以只要有baseCell就要用上 const insertedCell = baseCell || closestCell ? copyCell((baseCell || closestCell)!, 1, copyCellAttrs, row.id, id)[0] : createCell( row.type === 'body' ? { type: 'numeric', dataType: 'number', value: 0, rowId: row.id } : { type: 'text', dataType: 'string', value: '', rowId: row.id }, row.id, 1, id, )[0]; return { insertedCell: insertedCell, updatedCell: null, updatedRow: { ...row, cellIds: [...row.cellIds!.slice(0, j), id, ...row.cellIds!.slice(j)] }, sourceCell: baseCell || closestCell || null, }; } } /** * 从row中, 删除第j列的单元格. 已考虑合并单元格的影响 */ export function deleteCell( row: IRow, cellMap: Record, j: number, testing?: boolean, ): { updatedCell: { old: ICellNN; new: ICellNN } | null; deletedCell: ICellNN | null; updatedRow: IRow; insertedCell: ICellNN | null; } { const leftClosestSolidCellId = R.last(row.cellIds!.slice(0, j).filter(Boolean)); const leftClosestSolidCell = leftClosestSolidCellId ? cellMap[leftClosestSolidCellId!] : null; const leftClosestSolidCellIndex = !leftClosestSolidCellId ? -1 : row.cellIds!.findIndex(cellId => cellId === leftClosestSolidCellId); const currentCell = row.cellIds![j] ? cellMap[row.cellIds![j]!] : null; if (currentCell && (currentCell.colspan || 1) >= 2) { // 第j格是合并单元格的主格, 需要在原j+1格相应创建新单元格的情形 const insertedCell = { ...copyCell(currentCell, 1, [], row.id, testing ? `randomid${row.id}` : uuid())[0], colspan: currentCell.colspan! - 1, value: currentCell.value, }; const updatedRow = { ...row, cellIds: R.update(j, insertedCell.id, dropIndex(row.cellIds!, j)), }; return { insertedCell, deletedCell: currentCell!, updatedCell: null, updatedRow, }; } else if ( leftClosestSolidCell && leftClosestSolidCellIndex + (leftClosestSolidCell.colspan || 1) - 1 >= j ) { // 第j格被左单元格合并, 需要修改左格的colspan const updatedRow = { ...row, cellIds: dropIndex(row.cellIds!, j) }; return { insertedCell: null, deletedCell: null, updatedCell: { old: leftClosestSolidCell, new: { ...leftClosestSolidCell, colspan: leftClosestSolidCell.colspan! - 1 }, }, updatedRow, }; } else { // 第j格被顶部合并, 或第j格是ICellNN且cospan=1, 只需删除这个单元格, 没有别的动作 const updatedRow = { ...row, cellIds: dropIndex(row.cellIds!, j) }; return { insertedCell: null, deletedCell: currentCell ? currentCell : null, updatedCell: null, updatedRow, }; } }