import { IStoreState, IRow, ICell, ID, IRowDimensions, FormulaManagerConfig, } from '../../index.data'; import { IRowWithCell } from '../../SpreadSheetProvider/index.data'; import { treeArray as ta, idMap, dissoc } from 'valor-app-utils'; import { normalizeRows, normalizeCellIdMap } from '../../common/helper'; import { RootNodeId } from 'valor-app-utils/dist/tree/interface'; import { tryEnableFormula as normalizeRowAfterOperation } from './cell'; import { recalculate } from './recalculate'; import * as R from 'rambda'; import { SimpleNodeContext } from 'valor-app-utils/dist/tree/context'; import { getHostRows, getRow, getRowi, getPureTreeContext } from '../selectors'; import { getDecendantIndexes } from 'valor-app-utils/dist/treeArray'; import { Store } from 'unistore'; import equals from 'fast-deep-equal'; import { restoreCellRefIfNeeded } from '../formula_support/postProcess'; import { replaceById, updateBy } from 'valor-app-utils/dist/array'; import { tryRealizeAbstractFormulaForCell } from '../../cellTypes/formula/FormulaEditor/NamedFormulaEditor/AbstractCellMapper/helper'; import { SpreadSheetRuntime } from '../../RuntimeContext'; /** * 可插入一行, 或一个子树 * 注意: 调用此方法前, 必须保证参数是正常的 ( 可以正常插入, 并且rowsToInsert一定不空... ) * 本方法不检查参数!!! * @param state * @param type * @param baseI 基准行 * @param rowsToInsert * @param index 插入为基准行的第index个子 */ export function insertTreeRow( state: IStoreState, type: 'sibling' | 'child', baseI: number, // 鼠标选中的那一行 rowsToInsert: IRowWithCell[], index: number | undefined, helpers: { normalizeRow: (rows: IRow[], row: IRowWithCell) => IRowWithCell; }, runtime: SpreadSheetRuntime, ) { // 2. 插入操作, 获得操作前后的state // 无论插入子, 还是兄弟, 都需要插入在 最后一个子孙节点之后 const insertAtIndex = getInsertAtIndex(state, type as any, baseI, index); const rowsToInsert_normalized: IRow[] = normalizeRows( { rows: rowsToInsert, columns: state.columns }, insertAtIndex, ); const cellsToInsert_normalized = normalizeCellIdMap( { rows: rowsToInsert, columns: state.columns }, insertAtIndex, ); const f = type === 'sibling' ? ta.createSiblingItem : ta.createChildItem; const [newRows, newTreeContext_] = (f as any)(state.rows, rowsToInsert_normalized, baseI, index); const newTreeContext = getPureTreeContext({ rows: newRows } as IStoreState, newTreeContext_); // 要注意: 返回的顺序是正常的, 但row.i未改变, 因此需要同步一下 // const newRows = amendRowI(newRows_); // const newCells_ = amendCellI(newRows, { ...state.cells, ...cellsToInsert_normalized }); // 尝试实现单元格的抽象公式 ( 仅当该单元格中, 所有引用到的单元格能完美匹配时才实现 ) const newCells = R.map( (cell, id) => tryRealizeAbstractFormulaForCell(state, cell), Object.assign({}, state.cells, cellsToInsert_normalized), ); // 3. 修改formulaDisabled, 比如 非叶结点, subChildren对应的单元格就要设置 formulaDisabled=false const parentRowId = newTreeContext[rowsToInsert[0].id].parentId; let affectedRowIds = [...rowsToInsert.map(it => it.id)]; affectedRowIds = parentRowId === RootNodeId ? affectedRowIds : [parentRowId, ...affectedRowIds]; // 对受影响的行, 校正formulaDisabled并重算 return amendFormulaDisabledAndRecaculate( { ...state, rows: newRows, cells: newCells, treeContext: newTreeContext }, affectedRowIds, inferHostRowsByInsert( state, parentRowId === RootNodeId ? -1 : getRowi(state, parentRowId), rowsToInsert.map(it => it.id), ), helpers, runtime && runtime.formulaManager.config, runtime && runtime.batchMode, ); } export function getInsertAtIndex(state: IStoreState, type: 'sibling', baseI: number): number; export function getInsertAtIndex( state: IStoreState, type: 'child', baseI: number, index?: number, ): number; /** * 获取要插入的位置 * @param state * @param type * @param baseI 基准行 * @param index 仅对type==='child'有意义, 比如 要插入为 baseI的第0行, 或第1行( 如果不指定, 则插入到最后一行 ) */ export function getInsertAtIndex( state: IStoreState, type: 'sibling' | 'child', baseI: number, index?: number, ) { if (type === 'sibling') { return ta.getLastDecendantIndex(state.rows, baseI) + 1; } else if (type === 'child') { // const _treeContext = ({ // ...state.treeContext, // [RootNodeId]: { // level: 0, // parentId: null, // childrenIds: state.rows.filter(row => row.level === 1).map(row => row.id), // }, // } as any) as Record; const _treeContext = state.treeContext; const childrenIds = _treeContext[baseI < 0 ? RootNodeId : state.rows[baseI].id].childrenIds; const _index = R.is(Number, index) ? index! : (childrenIds || []).length; return _index >= (childrenIds || []).length ? ta.getLastDecendantIndex(state.rows, baseI) + 1 : // : state.rows.find(row => row.id === childrenIds[_index])!.i!; getRowi(state, state.rows.find(row => row.id === childrenIds[_index])!.id); } } /** * 可删除一行, 或一棵子树 * 注意: 调用此方法前, 必须保证参数正常 * 本方法不检查参数, 假定删除可正常进行 */ export function deleteTreeRow( state: IStoreState, i: number, helpers: { normalizeRow: (rows: IRow[], row: IRowWithCell) => IRowWithCell; }, runtime: SpreadSheetRuntime, ) { const rowId = state.rows[i].id; const parentRowId = state.treeContext[rowId].parentId; const [newRows, newTreeContext_] = ta.deleteItem(state.rows, i); const newTreeContext = getPureTreeContext({ rows: newRows } as IStoreState, newTreeContext_); // 要注意: 返回的顺序是正常的, 但row.i未改变, 因此需要同步一下 // const newRows = amendRowI(newRows_); const newCellIds = (R.flatten( newRows.map(row => row.cellIds!).filter(Boolean), ) as any) as string[]; // const newCells = amendCellI(newRows, R.pick(newCellIds, state.cells)); const newCells = R.pick(newCellIds, state.cells); return amendFormulaDisabledAndRecaculate( { ...state, rows: newRows, cells: newCells, treeContext: newTreeContext }, [parentRowId].filter(it => it !== '-1'), inferHostRowsByDelete(state, i), helpers, runtime && runtime.formulaManager.config, runtime && runtime.batchMode, ); } /** * 对受影响的行, 校正formulaDisabled并重算 * @param state * @param affectedRowIds_formula 需要重新确定公式的行 * @param affectedRowIds_deps 受操作影响, 需要重新计算的行(应包含前者) */ export function amendFormulaDisabledAndRecaculate( state: IStoreState, affectedRowIds_formula: ID[], affectedRowIds_deps: ID[], helpers: { normalizeRow: (rows: IRow[], row: IRowWithCell) => IRowWithCell; }, formulaConfig: FormulaManagerConfig, batchMode: boolean, ) { const normalizedResults = normalizeRowAfterOperation(state, affectedRowIds_formula, helpers); // 合并 formulaDisabled 有修改的 cells const formulaFulfilledState = { ...state, cells: Object.assign({}, state.cells, normalizedResults.cells), rows: updateBy(state.rows, normalizedResults.rows, (o, n) => o.id === n.id, (o, n) => n), }; // 4. 整体计算, 并生效 const calculatedStatePatch = batchMode ? Object.assign({}, formulaFulfilledState) : recalculate(formulaFulfilledState, { targetRowIds: affectedRowIds_deps, force: false, config: formulaConfig, }); // 此处不要{...state}, 因为不需要返回整个state const cellRefRestored = restoreCellRefIfNeeded(state, calculatedStatePatch); return { ...cellRefRestored, rows: formulaFulfilledState.rows, treeContext: state.treeContext }; // 若忽略recalculate, 则可: // return { ...formulaFulfilledState, rows: state.rows, treeContext: state.treeContext } as any; // TODO } /** * 对行 升级或降级 * 本方法不校验参数 */ export function pushTreeRow( state: IStoreState, type: 'left' | 'right', i: number, helpers: { normalizeRow: (rows: IRow[], row: IRowWithCell) => IRowWithCell; }, runtime: SpreadSheetRuntime, ) { const oldParentRowId = state.treeContext[state.rows[i].id].parentId; const f = type === 'left' ? ta.pushItemLeft : ta.pushItemRight; const [newRows, newTreeContext_] = f(state.rows, i); const newTreeContext = getPureTreeContext({ rows: newRows } as IStoreState, newTreeContext_); const newParentRowId = newTreeContext[state.rows[i].id].parentId; // 如果父未变, 表示不可升降级 if (oldParentRowId === newParentRowId) return null; const stateAfterPush = { ...state, rows: newRows, treeContext: newTreeContext }; return amendFormulaDisabledAndRecaculate( stateAfterPush, [oldParentRowId, newParentRowId].filter(it => it !== '-1'), inferHostRowsByPush(state, stateAfterPush, i, newParentRowId), helpers, runtime && runtime.formulaManager.config, runtime && runtime.batchMode, ); } /** * 上移下移行 * 本方法不检验参数 * 不需要重置formulaDisabled */ export function moveTreeRow(state: IStoreState, type: 'up' | 'down', i: number) { const f = type === 'up' ? ta.moveUp : ta.moveDown; const [newRows, newTreeContext_] = f(state.rows, i); const newTreeContext = getPureTreeContext({ rows: newRows } as IStoreState, newTreeContext_); // 要注意: 返回的顺序是正常的, 但row.i未改变, 因此需要同步一下 // const newRows = amendRowI(newRows_); // const newCells = amendCellI(newRows, state.cells); // 判断实际没有发生移动的情形 const row = state.rows[i]; if (getRowi(state, row.id) === getRowi({ rows: newRows } as IStoreState, row.id)) { return null; } // const newRowDimensions = getNewRowDimensions(newRows.map(it => it.id), state.rowDimensions); return { rows: newRows, treeContext: newTreeContext, // rowDimensions: newRowDimensions, }; } /** * 由于rows顺序发生改变, 需要重算 rowDimensions * 方法是: 重新根据 rowIds 的顺序, 计算row.top * @param rowDimensions 修改之前的rowDimensions */ /* export function getNewRowDimensions(rowIds: ID[], rowDimensions: IRowDimensions): IRowDimensions { const result = rowIds.reduce( ({ top, accRowDimensions }: { top: number; accRowDimensions: IRowDimensions }, rowId) => ({ top: top + rowDimensions[rowId].height!, accRowDimensions: Object.assign({}, accRowDimensions, { [rowId]: { ...rowDimensions[rowId], top: top }, }), }), { top: 0, accRowDimensions: rowDimensions }, ); return result['accRowDimensions']; } */ /** * 根据 rows 的自然排序, 修正 row.i * @param rows 待修正的rows ( 中间状态, 自然排序是正确的 , 但row.i 是不正确的) */ /* 不再使用 export function amendRowI(rows: IRow[]): IRow[] { // 若不变, 则维持引用关系 return rows.map((row, i) => (row.i === i ? row : Object.assign({}, row, { i }))); } */ /** * 根据 row 的变化, 修正 row.cells 里的 cell.i * @param rows 修正后的row * @param cells 待修正的cells */ /* 不需要此方法了 export function amendCellI(rows: IRow[], cells: Record): Record { // TODO: 这一步应该会被优化掉 return rows.reduce( (acc, row) => row['cellIds']!.filter(Boolean).reduce( (acc1, cellId) => acc[cellId!]!.i === row.i ? acc1 : { ...acc1, [cellId!]: { ...cells[cellId!]!, i: row.i } }, acc, ), cells, ); } */ // #region 行A的增删改, 将由公式传播, 造成其它行的变动, 这里推导出有哪些行将发生变动 // 插入时, 哪些行 (内部单元格的value) 会变化 // 例: 要插入一个子树 A->A1,A2 // A, A1,A2 当然需要重算 // 假定: A1&A2 仅引起 A 重算 ( 这条假设若不成立, 则下述逻辑不成立) // A将引起哪些已有行重算? // 1. 若A插入后有兄弟, 并且兄弟引起 K 重算, 则A也引起K重算(对应sumSibling/sumChildren等场合. 若K的公式是加法, 则A不必要重算, 会多算一行) // 2. 若A插入后没有兄弟, 则只能假定 A 将引起 A.parent重算, 所以 获取parent的影响对象 export function inferHostRowsByInsert(state: IStoreState, parentRowIndex: number, insertIds: ID[]) { const parentRow = parentRowIndex < 0 ? null : state.rows[parentRowIndex]; const parentRowId = parentRowIndex < 0 ? RootNodeId : parentRow!.id; // const childrenIds = // parentRowId === RootNodeId // ? state.rows.filter(it => it.level === 1).map(it => it.id) // : state.treeContext[parentRowId].childrenIds; const childrenIds = state.treeContext[parentRowId].childrenIds; const result = childrenIds.length > 0 ? getHostRows(state, getRowi(state, childrenIds[0])) : [parentRowId].concat(getHostRows(state, parentRowIndex)); return R.uniq(insertIds.concat(result)).filter(it => it !== RootNodeId); } // 删除时, 哪些行 (内部单元格的value) 会变化 // @param i 要删除的行序号 export function inferHostRowsByDelete(state: IStoreState, i: number) { const id = state.rows[i].id; const decendantIndexes = getDecendantIndexes(state.rows, i); const decendantIds = decendantIndexes .map(ii => state.rows[ii]) .filter(Boolean) .map(row => row.id); const deletedIds = decendantIds.concat([id]); // 注意最后一步要过滤掉i行, 因为i行将被删除, 自然不受影响 return R.uniq((R.flatten( [...decendantIndexes, i].map(k => getHostRows(state, k)), ) as any) as ID[]).filter(it => deletedIds.indexOf(it) < 0); } // Push时引起哪些行变化 // 例: 一个子树 A (A1 (A11 A12) A2 (A21 A22)) // 第一种情况: A1升级 => 1) 先删除A1, 2) 再将A1插入到A的平级 // 第二种情况: A2降级 => 1) 先删除A2, 2) 再将A2插入为A12的平级 // 删除较简单, 但插入时, parent不同 // 总之 就是先删除 再插入 export function inferHostRowsByPush( stateBeforePush: IStoreState, stateAfterPush: IStoreState, i: number, newParentRowId: ID, ) { const [rowsAfterDelete, treeContextAfterDelete] = ta.deleteItem(stateBeforePush.rows, i); const stateAfterDelete = { ...stateBeforePush, rows: rowsAfterDelete, treeContext: treeContextAfterDelete, }; // 注: 为何要用newState? // A, A1, A2, 设A1升级, 则A2在升级后变成A1的下级, 所以A2也应跟着计算 const decendantIndexes = getDecendantIndexes(stateAfterPush.rows, i); const decendantIds = decendantIndexes.map(k => stateAfterPush.rows[k]).map(row => row.id); const result = inferHostRowsByDelete(stateBeforePush, i).concat( // 模拟插入前, 要先移除 inferHostRowsByInsert( stateAfterDelete, newParentRowId === RootNodeId ? -1 : getRowi(stateBeforePush, newParentRowId), [stateBeforePush.rows[i].id].concat(decendantIds), ), ); return R.uniq(result); } // #endregion // #region 删除行时, 同时修改selection // 此方法未测试 export function ensureSelectionWhenTreeDelete( store: Store, deleteId: ID, runtime: SpreadSheetRuntime, action: any, ) { runtime.fsmService.send({ type: 'SELECT.NONE' }); const state = store.getState(); const indexInParent = state.treeContext[deleteId].index; const parentId = state.treeContext[deleteId].parentId; const parentChildrenIds = state.treeContext[parentId].childrenIds; const nextRowId = parentChildrenIds[indexInParent + 1] || parentChildrenIds[indexInParent - 1] || parentId; action(); if (nextRowId !== RootNodeId) { // 上面的rows已被删除, state正确, 但页面上的行并没有刷新, 所以需要setTimeout // 这里的疑问: 为何页面没有刷新, 貌似是同步模式啊 setTimeout(() => { runtime.setSheetDimensions(); runtime.fsmService.send({ type: 'SELECT.ROW', selectedRow: nextRowId }); }, 0); } } // 此方法对外, 将保证选择正确 export function deleteTreeRowOperation( store: Store, deleteI: number, helpers: { normalizeRow: (rows: IRow[], row: IRowWithCell) => IRowWithCell; }, runtime: SpreadSheetRuntime, ) { const deleteId = store.getState().rows[deleteI].id; ensureSelectionWhenTreeDelete(store, deleteId, runtime!, () => { // 注意不要直接用state, 因上面已更新了state const newState = deleteTreeRow(store.getState(), deleteI, helpers, runtime); store.setState(Object.assign(newState, { readyDimensions: true })); }); } /** * patch某个row * 暂时不允许修改row.cells * 目前仅有的需求: 修改additions */ export function setRowData(state: IStoreState, id: ID, rowData: Partial, replace = false) { const rowIndex = state.rows.findIndex(it => it.id === id); const oldRow = getRow(state, id); const newRow: IRow = replace ? ({ ...rowData } as IRow) : { ...oldRow, ...rowData }; if (R.equals(dissoc(oldRow, ['id', 'cellIds']), dissoc(newRow, ['id', 'cellIds']))) return {}; // Todo: 不稳健, 未考虑合并单元格后撤销的影响 return { rows: [...state.rows.slice(0, rowIndex), newRow, ...state.rows.slice(rowIndex + 1)] }; }