mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 23:01:30 +08:00
perf(sheets): replace object matrix queries with range operations (#7470)
This commit is contained in:
@@ -21,7 +21,6 @@ test('cells rendering after scrolling', async () => {
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const canvas = page.locator(SHEET_MAIN_CANVAS_ID);
|
||||
// TODO(@ai-review): Verify fixed-count wheel input and stable scroll frames eliminate CI flakiness without hiding merged-cell repaint regressions.
|
||||
await canvas.evaluate(async (element: HTMLCanvasElement) => {
|
||||
const scroll = async (deltaY: number) => {
|
||||
for (let elapsed = 0; elapsed < 1000; elapsed += 30) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { describe, expect, it } from 'vitest';
|
||||
import { Range } from '../../sheets/range';
|
||||
import { AbsoluteRefType } from '../../sheets/typedef';
|
||||
import { ObjectMatrix } from '../object-matrix';
|
||||
import { multiSubtractMultiRanges } from '../object-matrix-query';
|
||||
import { Rectangle } from '../rectangle';
|
||||
|
||||
function rangesToMatrix(ranges: IRange[]) {
|
||||
@@ -34,6 +33,13 @@ function rangesToMatrix(ranges: IRange[]) {
|
||||
return matrix.getMatrix();
|
||||
}
|
||||
|
||||
function subtractedRangesToMatrix(ranges: IRange[], rangesToSubtract: IRange[]) {
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
ranges.forEach((range) => Range.foreach(range, (row, col) => matrix.setValue(row, col, 1)));
|
||||
rangesToSubtract.forEach((range) => Range.foreach(range, (row, col) => matrix.realDeleteValue(row, col)));
|
||||
return matrix.getMatrix();
|
||||
}
|
||||
|
||||
const cellToRange = (row: number, col: number) => ({ startRow: row, endRow: row, startColumn: col, endColumn: col } as IRange);
|
||||
describe('test "Rectangle"', () => {
|
||||
it('test "subtract"', () => {
|
||||
@@ -134,8 +140,7 @@ describe('multiSubtractMulti', () => {
|
||||
const ranges2: IRange[] = [
|
||||
{ startColumn: 2, endColumn: 4, startRow: 2, endRow: 4 },
|
||||
];
|
||||
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
|
||||
});
|
||||
|
||||
it('should handle subtracting multiple ranges from a single range', () => {
|
||||
@@ -146,8 +151,7 @@ describe('multiSubtractMulti', () => {
|
||||
{ startColumn: 2, endColumn: 5, startRow: 2, endRow: 5 },
|
||||
{ startColumn: 6, endColumn: 8, startRow: 6, endRow: 8 },
|
||||
];
|
||||
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
|
||||
});
|
||||
|
||||
it('should handle non-overlapping subtraction ranges', () => {
|
||||
@@ -157,8 +161,7 @@ describe('multiSubtractMulti', () => {
|
||||
const ranges2: IRange[] = [
|
||||
{ startColumn: 6, endColumn: 8, startRow: 6, endRow: 8 },
|
||||
];
|
||||
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
|
||||
});
|
||||
|
||||
it('should handle subtraction ranges that completely overlap', () => {
|
||||
@@ -168,8 +171,7 @@ describe('multiSubtractMulti', () => {
|
||||
const ranges2: IRange[] = [
|
||||
{ startColumn: 1, endColumn: 5, startRow: 1, endRow: 5 },
|
||||
];
|
||||
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
|
||||
});
|
||||
|
||||
it('should handle empty ranges', () => {
|
||||
@@ -177,8 +179,7 @@ describe('multiSubtractMulti', () => {
|
||||
const ranges2: IRange[] = [
|
||||
{ startColumn: 2, endColumn: 4, startRow: 2, endRow: 4 },
|
||||
];
|
||||
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
|
||||
});
|
||||
|
||||
it('should handle empty subtraction ranges', () => {
|
||||
@@ -186,7 +187,6 @@ describe('multiSubtractMulti', () => {
|
||||
{ startColumn: 1, endColumn: 5, startRow: 1, endRow: 5 },
|
||||
];
|
||||
const ranges2: IRange[] = [];
|
||||
const expected: IRange[] = multiSubtractMultiRanges(ranges1, ranges2);
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(rangesToMatrix(expected));
|
||||
expect(rangesToMatrix(Rectangle.subtractMulti(ranges1, ranges2))).toEqual(subtractedRangesToMatrix(ranges1, ranges2));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,6 @@ export {
|
||||
numfmt,
|
||||
} from './numfmt';
|
||||
export * from './object-matrix';
|
||||
export { queryObjectMatrix } from './object-matrix-query';
|
||||
export * from './random-id';
|
||||
export { moveRangeByOffset, splitIntoGrid } from './range';
|
||||
export * from './rectangle';
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '../sheets/typedef';
|
||||
import type { Nullable } from './types';
|
||||
import { Range } from '../sheets/range';
|
||||
import { ObjectMatrix } from './object-matrix';
|
||||
import { Rectangle } from './rectangle';
|
||||
|
||||
function maximalRectangle<T>(matrix: T[][], match: (val: T) => boolean) {
|
||||
if (matrix.length === 0 || matrix[0].length === 0) return null;
|
||||
|
||||
const heights = new Array(matrix[0].length).fill(0);
|
||||
let maxArea = 0;
|
||||
let maxRect = null;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[0].length; col++) {
|
||||
heights[col] = match(matrix[row][col]) ? heights[col] + 1 : 0;
|
||||
}
|
||||
|
||||
const areaWithRect = largestRectangleArea(heights);
|
||||
if (areaWithRect.area > maxArea) {
|
||||
maxArea = areaWithRect.area;
|
||||
// Adjust the rectangle's top row to the current row minus the height plus one
|
||||
maxRect = {
|
||||
startColumn: areaWithRect.start,
|
||||
startRow: row - areaWithRect.height + 1,
|
||||
endColumn: areaWithRect.end,
|
||||
endRow: row,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return maxRect;
|
||||
}
|
||||
|
||||
function largestRectangleArea(heights: number[]) {
|
||||
const stack: number[] = [];
|
||||
let maxArea = 0;
|
||||
let maxRect = { area: 0, height: 0, start: 0, end: 0 };
|
||||
let index = 0;
|
||||
|
||||
while (index < heights.length) {
|
||||
if (stack.length === 0 || heights[index] >= heights[stack[stack.length - 1]]) {
|
||||
stack.push(index++);
|
||||
} else {
|
||||
const height = heights[stack.pop()!];
|
||||
const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1;
|
||||
if (height * width > maxArea) {
|
||||
maxArea = height * width;
|
||||
maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const height = heights[stack.pop()!];
|
||||
const width = stack.length === 0 ? index : index - stack[stack.length - 1] - 1;
|
||||
if (height * width > maxArea) {
|
||||
maxArea = height * width;
|
||||
maxRect = { area: maxArea, height, start: stack.length === 0 ? 0 : stack[stack.length - 1] + 1, end: index - 1 };
|
||||
}
|
||||
}
|
||||
|
||||
return maxRect;
|
||||
}
|
||||
|
||||
function resetMatrix<T>(matrix: Nullable<T>[][], range: IRange) {
|
||||
Range.foreach(range, (row, col) => {
|
||||
matrix[row][col] = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this function could cause memory out of use in large range.
|
||||
*/
|
||||
export function queryObjectMatrix<T>(matrix: ObjectMatrix<T>, match: (value: T) => boolean) {
|
||||
const arrayMatrix = matrix.toFullArray();
|
||||
const results: IRange[] = [];
|
||||
while (true) {
|
||||
const rectangle = maximalRectangle(arrayMatrix, match);
|
||||
if (!rectangle) {
|
||||
break;
|
||||
}
|
||||
|
||||
results.push(rectangle);
|
||||
resetMatrix(arrayMatrix, rectangle);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function multiSubtractMultiRanges(ranges1: IRange[], ranges2: IRange[]): IRange[] {
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
|
||||
ranges1.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
});
|
||||
|
||||
ranges2.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
matrix.setValue(row, col, 0);
|
||||
});
|
||||
});
|
||||
|
||||
return Rectangle.mergeRanges(queryObjectMatrix(matrix, (value) => value === 1));
|
||||
}
|
||||
+237
-2
@@ -14,7 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
|
||||
import { IUniverInstanceService, Range, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
AddConditionalRuleMutation,
|
||||
CFRuleType,
|
||||
@@ -24,16 +26,19 @@ import {
|
||||
SetConditionalRuleMutation,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import { COPY_TYPE, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createCfUiTestBed } from '../../__tests__/create-cf-ui-test-bed';
|
||||
import { ConditionalFormattingCopyPasteController } from '../cf.copy-paste.controller';
|
||||
|
||||
describe('ConditionalFormattingCopyPasteController', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('creates copy-paste mutations from the real rule model and view model', async () => {
|
||||
vi.spyOn(Range, 'foreach').mockImplementation(() => {
|
||||
throw new Error('must not enumerate cells');
|
||||
});
|
||||
const testBed = createCfUiTestBed();
|
||||
testBed.injector.add([ConditionalFormattingCopyPasteController]);
|
||||
testBed.injector.get(ConditionalFormattingCopyPasteController);
|
||||
@@ -145,4 +150,234 @@ describe('ConditionalFormattingCopyPasteController', () => {
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps identical overlapping source rules distinct when copying', async () => {
|
||||
const testBed = createCfUiTestBed();
|
||||
testBed.injector.add([ConditionalFormattingCopyPasteController]);
|
||||
testBed.injector.get(ConditionalFormattingCopyPasteController);
|
||||
const config: IConditionFormattingRule['rule'] = {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notContainsText,
|
||||
value: 'A1',
|
||||
style: { bg: { rgb: '#0f0' } },
|
||||
};
|
||||
|
||||
for (const cfId of ['cf-source-1', 'cf-source-2']) {
|
||||
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: {
|
||||
cfId,
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
|
||||
stopIfTrue: false,
|
||||
rule: config,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const hook = testBed.getClipboardHook()!;
|
||||
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
const result = hook.onPasteCells(
|
||||
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
|
||||
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [1], cols: [1] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
const copiedRuleIds = result.redos.flatMap((mutation) => {
|
||||
if (mutation.id !== AddConditionalRuleMutation.id && mutation.id !== SetConditionalRuleMutation.id) {
|
||||
return [];
|
||||
}
|
||||
const rule = (mutation.params as { rule: IConditionFormattingRule }).rule;
|
||||
return rule.ranges.some((range) => range.startRow <= 1 && range.endRow >= 1 && range.startColumn <= 1 && range.endColumn >= 1)
|
||||
? [rule.cfId]
|
||||
: [];
|
||||
});
|
||||
expect(new Set(copiedRuleIds)).toEqual(new Set(['cf-source-1', 'cf-source-2']));
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('does not reuse a target rule with different stop-if-true semantics', async () => {
|
||||
const testBed = createCfUiTestBed();
|
||||
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
|
||||
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
|
||||
testBed.injector.add([ConditionalFormattingCopyPasteController]);
|
||||
testBed.injector.get(ConditionalFormattingCopyPasteController);
|
||||
const config: IConditionFormattingRule['rule'] = {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notContainsText,
|
||||
value: 'A1',
|
||||
style: { bg: { rgb: '#0f0' } },
|
||||
};
|
||||
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: {
|
||||
cfId: 'cf-source',
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
|
||||
stopIfTrue: true,
|
||||
rule: config,
|
||||
},
|
||||
});
|
||||
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: 'sheet2',
|
||||
rule: {
|
||||
cfId: 'cf-target',
|
||||
ranges: [{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 }],
|
||||
stopIfTrue: false,
|
||||
rule: config,
|
||||
},
|
||||
});
|
||||
|
||||
const hook = testBed.getClipboardHook()!;
|
||||
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
const result = hook.onPasteCells(
|
||||
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
|
||||
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
const addedRule = result.redos.find((mutation) => mutation.id === AddConditionalRuleMutation.id)?.params as { rule: IConditionFormattingRule } | undefined;
|
||||
expect(addedRule?.rule.stopIfTrue).toBe(true);
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: 'matching semantics', targetValue: 'source' },
|
||||
{ name: 'different semantics', targetValue: 'target' },
|
||||
])('isolates source and target rules with colliding IDs during cross-sheet cut: $name', async ({ targetValue }) => {
|
||||
const testBed = createCfUiTestBed();
|
||||
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
|
||||
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
|
||||
testBed.injector.add([ConditionalFormattingCopyPasteController]);
|
||||
testBed.injector.get(ConditionalFormattingCopyPasteController);
|
||||
const createRule = (value: string, ranges: IConditionFormattingRule['ranges']): IConditionFormattingRule => ({
|
||||
cfId: 'cf-collision',
|
||||
ranges,
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notContainsText,
|
||||
value,
|
||||
style: { bg: { rgb: value === 'source' ? '#0f0' : '#f00' } },
|
||||
},
|
||||
});
|
||||
const sourceRule = createRule('source', [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }]);
|
||||
const targetRule = createRule(targetValue, [
|
||||
{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
|
||||
{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
|
||||
]);
|
||||
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: sourceRule,
|
||||
});
|
||||
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: 'sheet2',
|
||||
rule: targetRule,
|
||||
});
|
||||
|
||||
const hook = testBed.getClipboardHook()!;
|
||||
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
const result = hook.onPasteCells(
|
||||
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
|
||||
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.CUT, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
) as { redos: Array<{ id: string; params: object }>; undos: Array<{ id: string; params: object }> };
|
||||
|
||||
for (const mutation of result.redos) {
|
||||
await testBed.commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-collision')).toBeFalsy();
|
||||
expect(testBed.ruleModel.getRule(testBed.unitId, 'sheet2', 'cf-collision')?.ranges).toEqual([
|
||||
{ startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
|
||||
]);
|
||||
const copiedRules = testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')?.filter((rule) => (
|
||||
rule.cfId !== 'cf-collision' && rule.rule.type === CFRuleType.highlightCell && 'value' in rule.rule && rule.rule.value === 'source'
|
||||
));
|
||||
expect(copiedRules).toHaveLength(1);
|
||||
expect(copiedRules?.[0].ranges).toEqual([{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }]);
|
||||
|
||||
for (const mutation of result.undos) {
|
||||
await testBed.commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
expect(testBed.ruleModel.getRule(testBed.unitId, testBed.subUnitId, 'cf-collision')).toEqual(sourceRule);
|
||||
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')).toEqual([targetRule]);
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('preserves source rule priority when copying across worksheets', async () => {
|
||||
const testBed = createCfUiTestBed();
|
||||
const workbook = testBed.get(IUniverInstanceService).getUnit<Workbook>(testBed.unitId, UniverInstanceType.UNIVER_SHEET)!;
|
||||
workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'sheet2' });
|
||||
testBed.injector.add([ConditionalFormattingCopyPasteController]);
|
||||
testBed.injector.get(ConditionalFormattingCopyPasteController);
|
||||
for (const value of ['low', 'high']) {
|
||||
await testBed.commandService.executeCommand(AddConditionalRuleMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: {
|
||||
cfId: `cf-${value}`,
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
|
||||
stopIfTrue: false,
|
||||
rule: {
|
||||
type: CFRuleType.highlightCell,
|
||||
subType: CFSubRuleType.text,
|
||||
operator: CFTextOperator.notContainsText,
|
||||
value,
|
||||
style: { bg: { rgb: value === 'high' ? '#0f0' : '#f00' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const hook = testBed.getClipboardHook()!;
|
||||
hook.onBeforeCopy(testBed.unitId, testBed.subUnitId, {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
const result = hook.onPasteCells(
|
||||
{ unitId: testBed.unitId, subUnitId: testBed.subUnitId, range: { rows: [0], cols: [0] } },
|
||||
{ unitId: testBed.unitId, subUnitId: 'sheet2', range: { rows: [1], cols: [1] } },
|
||||
null,
|
||||
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
) as { redos: Array<{ id: string; params: object }>; undos: Array<{ id: string; params: object }> };
|
||||
for (const mutation of result.redos) {
|
||||
await testBed.commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
|
||||
const copiedValues = testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')?.map((rule) => (
|
||||
rule.rule.type === CFRuleType.highlightCell && 'value' in rule.rule ? rule.rule.value : undefined
|
||||
));
|
||||
expect(copiedValues).toEqual(['high', 'low']);
|
||||
for (const mutation of result.undos) {
|
||||
await testBed.commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
expect(testBed.ruleModel.getSubunitRules(testBed.unitId, 'sheet2')).toEqual([]);
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
+45
-125
@@ -17,12 +17,11 @@
|
||||
import type { IMutationInfo, IRange, Workbook } from '@univerjs/core';
|
||||
import type { IDiscreteRange, ISheetAutoFillHook } from '@univerjs/sheets';
|
||||
import type { IDeleteConditionalRuleMutationParams, ISetConditionalRuleMutationParams } from '@univerjs/sheets-conditional-formatting';
|
||||
import { Disposable, Inject, Injector, IUniverInstanceService, Range, Rectangle, UniverInstanceType } from '@univerjs/core';
|
||||
import { Disposable, getIntersectRange, Inject, Injector, IUniverInstanceService, Rectangle, UniverInstanceType } from '@univerjs/core';
|
||||
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
|
||||
import {
|
||||
ConditionalFormattingRangeTransformService,
|
||||
ConditionalFormattingRuleModel,
|
||||
ConditionalFormattingViewModel,
|
||||
DeleteConditionalRuleMutation,
|
||||
DeleteConditionalRuleMutationUndoFactory,
|
||||
SetConditionalRuleMutation,
|
||||
@@ -42,7 +41,6 @@ export class ConditionalFormattingAutoFillController extends Disposable {
|
||||
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(IAutoFillService) private _autoFillService: IAutoFillService,
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
|
||||
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
|
||||
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
|
||||
) {
|
||||
super();
|
||||
@@ -54,117 +52,6 @@ export class ConditionalFormattingAutoFillController extends Disposable {
|
||||
private _initAutoFill() {
|
||||
const noopReturnFunc = () => ({ redos: [], undos: [] });
|
||||
|
||||
const loopFunc = (
|
||||
sourceStartCell: { row: number; col: number },
|
||||
targetStartCell: { row: number; col: number },
|
||||
relativeRange: IRange,
|
||||
rangeMap: Map<string, IRange[]>,
|
||||
rangeDeltaMap: Map<string, IRangeDelta>,
|
||||
mapFunc: (row: number, col: number) => ({ row: number; col: number })
|
||||
) => {
|
||||
const unitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
|
||||
const subUnitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
|
||||
if (!unitId || !subUnitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getRangeDelta = (cfId: string) => {
|
||||
let rangeDelta = rangeDeltaMap.get(cfId);
|
||||
if (!rangeDelta) {
|
||||
rangeDelta = { add: [], remove: [] };
|
||||
rangeDeltaMap.set(cfId, rangeDelta);
|
||||
}
|
||||
return rangeDelta;
|
||||
};
|
||||
const ensureRuleRanges = (cfId: string) => {
|
||||
if (rangeMap.has(cfId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
|
||||
if (!rule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rangeMap.set(cfId, rule.ranges);
|
||||
return true;
|
||||
};
|
||||
const sourceRange = {
|
||||
startRow: sourceStartCell.row,
|
||||
startColumn: sourceStartCell.col,
|
||||
endColumn: sourceStartCell.col,
|
||||
endRow: sourceStartCell.row,
|
||||
};
|
||||
const targetRange = {
|
||||
startRow: targetStartCell.row,
|
||||
startColumn: targetStartCell.col,
|
||||
endColumn: targetStartCell.col,
|
||||
endRow: targetStartCell.row,
|
||||
};
|
||||
|
||||
Range.foreach(relativeRange, (row, col) => {
|
||||
const sourcePositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
sourceRange
|
||||
);
|
||||
const targetPositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
targetRange
|
||||
);
|
||||
const { row: sourceRow, col: sourceCol } = mapFunc(sourcePositionRange.startRow, sourcePositionRange.startColumn);
|
||||
const sourceCellCf = this._conditionalFormattingViewModel.getCellCfs(
|
||||
unitId,
|
||||
subUnitId,
|
||||
sourceRow,
|
||||
sourceCol
|
||||
);
|
||||
const { row: targetRow, col: targetCol } = mapFunc(targetPositionRange.startRow, targetPositionRange.startColumn);
|
||||
const targetCellCf = this._conditionalFormattingViewModel.getCellCfs(
|
||||
unitId,
|
||||
subUnitId,
|
||||
targetRow,
|
||||
targetCol
|
||||
);
|
||||
if (targetCellCf) {
|
||||
targetCellCf.forEach((cf) => {
|
||||
if (!ensureRuleRanges(cf.cfId)) {
|
||||
return;
|
||||
}
|
||||
getRangeDelta(cf.cfId).remove.push({
|
||||
startRow: targetRow,
|
||||
endRow: targetRow,
|
||||
startColumn: targetCol,
|
||||
endColumn: targetCol,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (sourceCellCf) {
|
||||
sourceCellCf.forEach((cf) => {
|
||||
if (!ensureRuleRanges(cf.cfId)) {
|
||||
return;
|
||||
}
|
||||
getRangeDelta(cf.cfId).add.push({
|
||||
startRow: targetRow,
|
||||
endRow: targetRow,
|
||||
startColumn: targetCol,
|
||||
endColumn: targetCol,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const generalApplyFunc = (sourceRange: IDiscreteRange, targetRange: IDiscreteRange) => {
|
||||
const unitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getUnitId();
|
||||
const subUnitId = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getActiveSheet()?.getSheetId();
|
||||
@@ -177,18 +64,51 @@ export class ConditionalFormattingAutoFillController extends Disposable {
|
||||
return noopReturnFunc();
|
||||
}
|
||||
|
||||
const virtualRange = virtualizeDiscreteRanges([sourceRange, targetRange]);
|
||||
const [vSourceRange, vTargetRange] = virtualRange.ranges;
|
||||
const { mapFunc } = virtualRange;
|
||||
|
||||
const sourceStartCell = {
|
||||
row: vSourceRange.startRow,
|
||||
col: vSourceRange.startColumn,
|
||||
};
|
||||
|
||||
const virtualization = virtualizeDiscreteRanges([sourceRange, targetRange]);
|
||||
const [vSourceRange, vTargetRange] = virtualization.ranges;
|
||||
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
|
||||
repeats.forEach((repeat) => {
|
||||
loopFunc(sourceStartCell, repeat.repeatStartCell, repeat.relativeRange, rangeMap, rangeDeltaMap, mapFunc);
|
||||
const targetRanges = repeats.flatMap((repeat) => virtualization.mapRange(Rectangle.getPositionRange(repeat.relativeRange, {
|
||||
startRow: repeat.repeatStartCell.row,
|
||||
endRow: repeat.repeatStartCell.row,
|
||||
startColumn: repeat.repeatStartCell.col,
|
||||
endColumn: repeat.repeatStartCell.col,
|
||||
})));
|
||||
const getRangeDelta = (cfId: string) => {
|
||||
let rangeDelta = rangeDeltaMap.get(cfId);
|
||||
if (!rangeDelta) {
|
||||
rangeDelta = { add: [], remove: [] };
|
||||
rangeDeltaMap.set(cfId, rangeDelta);
|
||||
}
|
||||
return rangeDelta;
|
||||
};
|
||||
const rules = this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId) ?? [];
|
||||
|
||||
rules.forEach((rule) => {
|
||||
if (Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
|
||||
rangeMap.set(rule.cfId, rule.ranges);
|
||||
getRangeDelta(rule.cfId).remove.push(...targetRanges);
|
||||
}
|
||||
|
||||
const sourceRanges = rule.ranges.flatMap((range) => {
|
||||
const projected = virtualization.projectRange(range);
|
||||
const intersected = projected && getIntersectRange(projected, vSourceRange);
|
||||
return intersected ? [Rectangle.getRelativeRange(intersected, vSourceRange)] : [];
|
||||
});
|
||||
const additions = repeats.flatMap((repeat) => sourceRanges.flatMap((range) => {
|
||||
const copiedRange = getIntersectRange(range, repeat.relativeRange);
|
||||
return copiedRange
|
||||
? virtualization.mapRange(Rectangle.getPositionRange(copiedRange, {
|
||||
startRow: repeat.repeatStartCell.row,
|
||||
endRow: repeat.repeatStartCell.row,
|
||||
startColumn: repeat.repeatStartCell.col,
|
||||
endColumn: repeat.repeatStartCell.col,
|
||||
}))
|
||||
: [];
|
||||
}));
|
||||
if (additions.length) {
|
||||
rangeMap.set(rule.cfId, rule.ranges);
|
||||
getRangeDelta(rule.cfId).add.push(...additions);
|
||||
}
|
||||
});
|
||||
rangeDeltaMap.forEach((rangeDelta, cfId) => {
|
||||
const ranges = rangeMap.get(cfId);
|
||||
|
||||
+97
-140
@@ -17,7 +17,6 @@
|
||||
import type { IRange, Nullable } from '@univerjs/core';
|
||||
import type {
|
||||
IAddConditionalRuleMutationParams,
|
||||
IConditionalFormattingRuleConfig,
|
||||
IConditionFormattingRule,
|
||||
IDeleteConditionalRuleMutationParams,
|
||||
ISetConditionalRuleMutationParams,
|
||||
@@ -28,10 +27,7 @@ import {
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
Range,
|
||||
Rectangle,
|
||||
Tools,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
@@ -42,7 +38,6 @@ import {
|
||||
AddConditionalRuleMutationUndoFactory,
|
||||
ConditionalFormattingRangeTransformService,
|
||||
ConditionalFormattingRuleModel,
|
||||
ConditionalFormattingViewModel,
|
||||
DeleteConditionalRuleMutation,
|
||||
DeleteConditionalRuleMutationUndoFactory,
|
||||
SetConditionalRuleMutation,
|
||||
@@ -52,11 +47,11 @@ import {
|
||||
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
|
||||
|
||||
interface ICopyInfoType {
|
||||
matrix: ObjectMatrix<string[]>;
|
||||
rules: Map<string, IRange[]>;
|
||||
info: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
cfMap: Record<string, IConditionalFormattingRuleConfig>;
|
||||
cfMap: Record<string, Pick<IConditionFormattingRule, 'rule' | 'stopIfTrue'>>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,7 +68,6 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
|
||||
@Inject(ISheetClipboardService) private _sheetClipboardService: ISheetClipboardService,
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
|
||||
@Inject(Injector) private _injector: Injector,
|
||||
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
|
||||
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
|
||||
) {
|
||||
@@ -98,10 +92,10 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
|
||||
}
|
||||
|
||||
private _collectConditionalRule(unitId: string, subUnitId: string, range: IRange) {
|
||||
const matrix = new ObjectMatrix<string[]>();
|
||||
const cfMap: Record<string, IConditionalFormattingRuleConfig> = {};
|
||||
const rules = new Map<string, IRange[]>();
|
||||
const cfMap: Record<string, Pick<IConditionFormattingRule, 'rule' | 'stopIfTrue'>> = {};
|
||||
this._copyInfo = {
|
||||
matrix,
|
||||
rules,
|
||||
info: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
@@ -115,28 +109,25 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
|
||||
if (!discreteRange) {
|
||||
return;
|
||||
}
|
||||
const { rows, cols } = discreteRange;
|
||||
const cfIdSet: Set<string> = new Set();
|
||||
rows.forEach((row, rowIndex) => {
|
||||
cols.forEach((col, colIndex) => {
|
||||
const cellCfList = this._conditionalFormattingViewModel.getCellCfs(unitId, subUnitId, row, col);
|
||||
if (!cellCfList) {
|
||||
return;
|
||||
}
|
||||
cellCfList.forEach((item) => cfIdSet.add(item.cfId));
|
||||
matrix.setValue(rowIndex, colIndex, cellCfList.map((item) => item.cfId));
|
||||
const { projectRange } = virtualizeDiscreteRanges([discreteRange]);
|
||||
this._conditionalFormattingRuleModel.getSubunitRules(unitId, subUnitId)?.forEach((rule) => {
|
||||
const projectedRanges = rule.ranges.flatMap((ruleRange) => {
|
||||
const projected = projectRange(ruleRange);
|
||||
return projected ? [projected] : [];
|
||||
});
|
||||
});
|
||||
cfIdSet.forEach((cfId) => {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(unitId, subUnitId, cfId);
|
||||
if (rule) {
|
||||
cfMap[cfId] = rule.rule;
|
||||
if (projectedRanges.length) {
|
||||
rules.set(rule.cfId, projectedRanges.length > 1 ? Rectangle.mergeRanges(projectedRanges) : projectedRanges);
|
||||
cfMap[rule.cfId] = { rule: rule.rule, stopIfTrue: rule.stopIfTrue };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _generateConditionalFormattingMutations(pasteFrom: ISheetDiscreteRangeLocation, pasteTo: ISheetDiscreteRangeLocation, payload: ICopyPastePayload) {
|
||||
const copyInfo = this._copyInfo;
|
||||
if (!copyInfo) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
const { unitId: copyUnitId, subUnitId: copySubUnitId, range: copyRange } = pasteFrom;
|
||||
const { unitId: pastedUnitId, subUnitId: pastedSubUnitId, range: pastedRange } = pasteTo;
|
||||
const { copyType = COPY_TYPE.COPY } = payload;
|
||||
@@ -152,147 +143,112 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pastedRange]);
|
||||
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
|
||||
const effectedConditionalFormattingRuleRanges: Record<string, {
|
||||
const sourceVirtualization = virtualizeDiscreteRanges([copyRange]);
|
||||
const sourceVirtualRange = sourceVirtualization.ranges[0];
|
||||
const targetVirtualization = virtualizeDiscreteRanges([pastedRange]);
|
||||
const targetVirtualRange = targetVirtualization.ranges[0];
|
||||
const repeatRange = getRepeatRange(sourceVirtualRange, targetVirtualRange, true);
|
||||
const targetRanges = targetVirtualization.mapRange(targetVirtualRange);
|
||||
const isSameSheet = pastedUnitId === copyUnitId && pastedSubUnitId === copySubUnitId;
|
||||
const effectedConditionalFormattingRuleRanges = new Map<string, {
|
||||
cfId: string;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
ranges: IRange[];
|
||||
add: IRange[];
|
||||
remove: IRange[];
|
||||
}> = {};
|
||||
}>();
|
||||
const getEffectKey = (unitId: string, subUnitId: string, cfId: string) => JSON.stringify([unitId, subUnitId, cfId]);
|
||||
|
||||
// 1. delete the conditional formatting rules in the pasted range.
|
||||
Range.foreach(vPastedRange, (row, col) => {
|
||||
const { row: realRow, col: realCol } = mapFunc(row, col);
|
||||
const cellCfList = this._conditionalFormattingViewModel.getCellCfs(pastedUnitId, pastedSubUnitId, realRow, realCol);
|
||||
if (cellCfList) {
|
||||
cellCfList.forEach((item) => {
|
||||
if (!effectedConditionalFormattingRuleRanges[item.cfId]) {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(pastedUnitId, pastedSubUnitId, item.cfId);
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
effectedConditionalFormattingRuleRanges[item.cfId] = {
|
||||
unitId: pastedUnitId,
|
||||
subUnitId: pastedSubUnitId,
|
||||
ranges: rule.ranges,
|
||||
add: [],
|
||||
remove: [],
|
||||
};
|
||||
}
|
||||
const current = effectedConditionalFormattingRuleRanges[item.cfId];
|
||||
current.remove.push({
|
||||
startRow: realRow,
|
||||
endRow: realRow,
|
||||
startColumn: realCol,
|
||||
endColumn: realCol,
|
||||
});
|
||||
});
|
||||
this._conditionalFormattingRuleModel.getSubunitRules(pastedUnitId, pastedSubUnitId)?.forEach((rule) => {
|
||||
if (!Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
|
||||
return;
|
||||
}
|
||||
effectedConditionalFormattingRuleRanges.set(getEffectKey(pastedUnitId, pastedSubUnitId, rule.cfId), {
|
||||
cfId: rule.cfId,
|
||||
unitId: pastedUnitId,
|
||||
subUnitId: pastedSubUnitId,
|
||||
ranges: rule.ranges,
|
||||
add: [],
|
||||
remove: targetRanges,
|
||||
});
|
||||
});
|
||||
|
||||
// 2. if it is cut from another worksheet, need to delete the conditional formatting rules in the copy range.
|
||||
if (copyType === COPY_TYPE.CUT && (pastedUnitId !== copyUnitId || pastedSubUnitId !== copySubUnitId)) {
|
||||
Range.foreach(vCopyRange, (row, col) => {
|
||||
const { row: realRow, col: realCol } = mapFunc(row, col);
|
||||
const cellCfList = this._conditionalFormattingViewModel.getCellCfs(copyUnitId, copySubUnitId, realRow, realCol);
|
||||
if (cellCfList) {
|
||||
cellCfList.forEach((item) => {
|
||||
if (!effectedConditionalFormattingRuleRanges[item.cfId]) {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(copyUnitId, copySubUnitId, item.cfId);
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
effectedConditionalFormattingRuleRanges[item.cfId] = {
|
||||
unitId: copyUnitId,
|
||||
subUnitId: copySubUnitId,
|
||||
ranges: rule.ranges,
|
||||
add: [],
|
||||
remove: [],
|
||||
};
|
||||
}
|
||||
const current = effectedConditionalFormattingRuleRanges[item.cfId];
|
||||
current.remove.push({
|
||||
startRow: realRow,
|
||||
endRow: realRow,
|
||||
startColumn: realCol,
|
||||
endColumn: realCol,
|
||||
});
|
||||
});
|
||||
const sourceRanges = sourceVirtualization.mapRange(sourceVirtualRange);
|
||||
copyInfo.rules.forEach((_ranges, cfId) => {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(copyUnitId, copySubUnitId, cfId);
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
effectedConditionalFormattingRuleRanges.set(getEffectKey(copyUnitId, copySubUnitId, cfId), {
|
||||
cfId,
|
||||
unitId: copyUnitId,
|
||||
subUnitId: copySubUnitId,
|
||||
ranges: rule.ranges,
|
||||
add: [],
|
||||
remove: sourceRanges,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { matrix, info } = this._copyInfo as ICopyInfoType;
|
||||
const waitAddRule: IConditionFormattingRule[] = [];
|
||||
const { rules, info } = copyInfo;
|
||||
const waitAddRule = new Map<string, IConditionFormattingRule>();
|
||||
const cacheCfIdMap: Record<string, IConditionFormattingRule> = {};
|
||||
|
||||
// 3. generate the new conditional formatting rules based on the copy range's conditional formatting rules and the paste position.
|
||||
const getCurrentSheetCfRule = (copyRangeCfId: string) => {
|
||||
const oldRule = info?.cfMap[copyRangeCfId];
|
||||
const targetRule = [...(this._conditionalFormattingRuleModel.getSubunitRules(pastedUnitId, pastedSubUnitId) || []), ...waitAddRule].find((rule) => {
|
||||
return Tools.diffValue(rule.rule, oldRule);
|
||||
});
|
||||
|
||||
if (targetRule) {
|
||||
cacheCfIdMap[copyRangeCfId] = targetRule;
|
||||
return targetRule;
|
||||
} else {
|
||||
const rule: IConditionFormattingRule = {
|
||||
rule: oldRule,
|
||||
cfId: this._conditionalFormattingRuleModel.createCfId(pastedUnitId, pastedSubUnitId),
|
||||
ranges: [],
|
||||
stopIfTrue: false,
|
||||
};
|
||||
cacheCfIdMap[copyRangeCfId] = rule;
|
||||
waitAddRule.push(rule);
|
||||
return rule;
|
||||
if (isSameSheet) {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(pastedUnitId, pastedSubUnitId, copyRangeCfId);
|
||||
if (rule) {
|
||||
cacheCfIdMap[copyRangeCfId] = rule;
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
const rule: IConditionFormattingRule = {
|
||||
rule: oldRule.rule,
|
||||
cfId: this._conditionalFormattingRuleModel.createCfId(pastedUnitId, pastedSubUnitId),
|
||||
ranges: [],
|
||||
stopIfTrue: oldRule.stopIfTrue,
|
||||
};
|
||||
cacheCfIdMap[copyRangeCfId] = rule;
|
||||
waitAddRule.set(rule.cfId, rule);
|
||||
return rule;
|
||||
};
|
||||
|
||||
repeatRange.forEach((item) => {
|
||||
matrix &&
|
||||
matrix.forValue((row, col, copyRangeCfIdList) => {
|
||||
const range = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
},
|
||||
item.startRange
|
||||
);
|
||||
|
||||
const { row: _row, col: _col } = mapFunc(range.startRow, range.startColumn);
|
||||
|
||||
copyRangeCfIdList.forEach((cfId) => {
|
||||
const rule = cacheCfIdMap[cfId] || getCurrentSheetCfRule(cfId);
|
||||
if (!effectedConditionalFormattingRuleRanges[rule.cfId]) {
|
||||
effectedConditionalFormattingRuleRanges[rule.cfId] = {
|
||||
unitId: pastedUnitId,
|
||||
subUnitId: pastedSubUnitId,
|
||||
ranges: rule.ranges,
|
||||
add: [],
|
||||
remove: [],
|
||||
};
|
||||
}
|
||||
const current = effectedConditionalFormattingRuleRanges[rule.cfId];
|
||||
current.add.push({
|
||||
startRow: _row,
|
||||
endRow: _row,
|
||||
startColumn: _col,
|
||||
endColumn: _col,
|
||||
});
|
||||
});
|
||||
const sourceRuleEntries = Array.from(rules.entries());
|
||||
if (!isSameSheet) {
|
||||
// AddRule prepends, so emit lower-priority cross-sheet clones first.
|
||||
sourceRuleEntries.reverse();
|
||||
}
|
||||
sourceRuleEntries.forEach(([cfId, sourceRanges]) => {
|
||||
const rule = cacheCfIdMap[cfId] || getCurrentSheetCfRule(cfId);
|
||||
const effectKey = getEffectKey(pastedUnitId, pastedSubUnitId, rule.cfId);
|
||||
if (!effectedConditionalFormattingRuleRanges.has(effectKey)) {
|
||||
effectedConditionalFormattingRuleRanges.set(effectKey, {
|
||||
cfId: rule.cfId,
|
||||
unitId: pastedUnitId,
|
||||
subUnitId: pastedSubUnitId,
|
||||
ranges: rule.ranges,
|
||||
add: [],
|
||||
remove: [],
|
||||
});
|
||||
}
|
||||
const current = effectedConditionalFormattingRuleRanges.get(effectKey)!;
|
||||
current.add.push(...repeatRange.flatMap((item) => sourceRanges.flatMap((sourceRange) => (
|
||||
targetVirtualization.mapRange(Rectangle.getPositionRange(sourceRange, item.startRange))
|
||||
))));
|
||||
});
|
||||
|
||||
const redos = [];
|
||||
const undos = [];
|
||||
|
||||
for (const cfId in effectedConditionalFormattingRuleRanges) {
|
||||
const { unitId, subUnitId, ranges: sourceRanges, add, remove } = effectedConditionalFormattingRuleRanges[cfId];
|
||||
for (const effect of effectedConditionalFormattingRuleRanges.values()) {
|
||||
const { cfId, unitId, subUnitId, ranges: sourceRanges, add, remove } = effect;
|
||||
const ranges = this._conditionalFormattingRangeTransformService.applyRangeDelta(sourceRanges, remove, add);
|
||||
|
||||
if (!ranges.length) {
|
||||
@@ -303,14 +259,15 @@ export class ConditionalFormattingCopyPasteController extends Disposable {
|
||||
};
|
||||
redos.push({ id: DeleteConditionalRuleMutation.id, params: deleteParams });
|
||||
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, deleteParams));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitAddRule.some((rule) => rule.cfId === cfId)) {
|
||||
const rule = waitAddRule.find((rule) => rule.cfId === cfId) as IConditionFormattingRule;
|
||||
const waitAdd = waitAddRule.get(cfId);
|
||||
if (waitAdd) {
|
||||
const addParams: IAddConditionalRuleMutationParams = {
|
||||
unitId: pastedUnitId,
|
||||
subUnitId: pastedSubUnitId,
|
||||
rule: { ...rule, ranges },
|
||||
rule: { ...waitAdd, ranges },
|
||||
};
|
||||
redos.push({ id: AddConditionalRuleMutation.id, params: addParams });
|
||||
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, addParams));
|
||||
|
||||
+59
-136
@@ -17,18 +17,18 @@
|
||||
import type { IMutationInfo, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type {
|
||||
IAddConditionalRuleMutationParams,
|
||||
IConditionFormattingRule,
|
||||
IDeleteConditionalRuleMutationParams,
|
||||
ISetConditionalRuleMutationParams,
|
||||
} from '@univerjs/sheets-conditional-formatting';
|
||||
import type { IFormatPainterHook } from '@univerjs/sheets-ui';
|
||||
import { Disposable, Inject, Injector, IUniverInstanceService, Range, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
|
||||
import { Disposable, getIntersectRange, Inject, Injector, IUniverInstanceService, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import {
|
||||
AddConditionalRuleMutation,
|
||||
AddConditionalRuleMutationUndoFactory,
|
||||
ConditionalFormattingRangeTransformService,
|
||||
ConditionalFormattingRuleModel,
|
||||
ConditionalFormattingViewModel,
|
||||
DeleteConditionalRuleMutation,
|
||||
DeleteConditionalRuleMutationUndoFactory,
|
||||
SetConditionalRuleMutation,
|
||||
@@ -101,7 +101,6 @@ export class ConditionalFormattingPainterController extends Disposable {
|
||||
@Inject(IFormatPainterService) private _formatPainterService: IFormatPainterService,
|
||||
@Inject(SheetsSelectionsService) private _sheetsSelectionsService: SheetsSelectionsService,
|
||||
@Inject(ConditionalFormattingRuleModel) private _conditionalFormattingRuleModel: ConditionalFormattingRuleModel,
|
||||
@Inject(ConditionalFormattingViewModel) private _conditionalFormattingViewModel: ConditionalFormattingViewModel,
|
||||
@Inject(ConditionalFormattingRangeTransformService) private _conditionalFormattingRangeTransformService: ConditionalFormattingRangeTransformService
|
||||
|
||||
) {
|
||||
@@ -114,108 +113,6 @@ export class ConditionalFormattingPainterController extends Disposable {
|
||||
private _initFormattingPainter() {
|
||||
const noopReturnFunc = () => ({ redos: [], undos: [] });
|
||||
|
||||
const loopFunc = (
|
||||
sourceStartCell: { row: number; col: number },
|
||||
targetStartCell: { row: number; col: number },
|
||||
relativeRange: IRange,
|
||||
rangeMap: Map<string, IRange[]>,
|
||||
rangeDeltaMap: Map<string, IRangeDelta>,
|
||||
config: {
|
||||
targetUnitId: string;
|
||||
targetSubUnitId: string;
|
||||
}
|
||||
) => {
|
||||
const { unitId: sourceUnitId, subUnitId: sourceSubUnitId } = this._painterConfig!;
|
||||
const { targetUnitId, targetSubUnitId } = config;
|
||||
|
||||
const getRangeDelta = (cfId: string) => {
|
||||
let rangeDelta = rangeDeltaMap.get(cfId);
|
||||
if (!rangeDelta) {
|
||||
rangeDelta = { add: [], remove: [] };
|
||||
rangeDeltaMap.set(cfId, rangeDelta);
|
||||
}
|
||||
return rangeDelta;
|
||||
};
|
||||
const sourceRange = {
|
||||
startRow: sourceStartCell.row,
|
||||
startColumn: sourceStartCell.col,
|
||||
endColumn: sourceStartCell.col,
|
||||
endRow: sourceStartCell.row,
|
||||
};
|
||||
const targetRange = {
|
||||
startRow: targetStartCell.row,
|
||||
startColumn: targetStartCell.col,
|
||||
endColumn: targetStartCell.col,
|
||||
endRow: targetStartCell.row,
|
||||
};
|
||||
|
||||
Range.foreach(relativeRange, (row, col) => {
|
||||
const sourcePositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
sourceRange
|
||||
);
|
||||
const targetPositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
targetRange
|
||||
);
|
||||
|
||||
const sourceCellCf = this._conditionalFormattingViewModel.getCellCfs(
|
||||
sourceUnitId,
|
||||
sourceSubUnitId,
|
||||
sourcePositionRange.startRow,
|
||||
sourcePositionRange.startColumn
|
||||
);
|
||||
|
||||
const targetCellCf = this._conditionalFormattingViewModel.getCellCfs(
|
||||
targetUnitId,
|
||||
targetSubUnitId,
|
||||
targetPositionRange.startRow,
|
||||
targetPositionRange.startColumn
|
||||
);
|
||||
|
||||
if (targetCellCf) {
|
||||
targetCellCf.forEach((cf) => {
|
||||
if (!rangeMap.has(cf.cfId)) {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cf.cfId);
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
rangeMap.set(cf.cfId, rule.ranges);
|
||||
}
|
||||
getRangeDelta(cf.cfId).remove.push({
|
||||
startRow: targetPositionRange.startRow,
|
||||
endRow: targetPositionRange.startRow,
|
||||
startColumn: targetPositionRange.startColumn,
|
||||
endColumn: targetPositionRange.startColumn,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (sourceCellCf) {
|
||||
sourceCellCf.forEach((cf) => {
|
||||
if (!rangeMap.has(cf.cfId)) {
|
||||
return;
|
||||
}
|
||||
getRangeDelta(cf.cfId).add.push({
|
||||
startRow: targetPositionRange.startRow,
|
||||
endRow: targetPositionRange.startRow,
|
||||
startColumn: targetPositionRange.startColumn,
|
||||
endColumn: targetPositionRange.startColumn,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
const generalApplyFunc = (targetUnitId: string, targetSubUnitId: string, targetRange: IRange) => {
|
||||
const { range: sourceRange, unitId: sourceUnitId, subUnitId: sourceSubUnitId } = this._painterConfig!;
|
||||
@@ -228,23 +125,53 @@ export class ConditionalFormattingPainterController extends Disposable {
|
||||
if (!targetUnitId || !targetSubUnitId || !sourceUnitId || !sourceSubUnitId) {
|
||||
return noopReturnFunc();
|
||||
}
|
||||
const ruleList = this._conditionalFormattingRuleModel.getSubunitRules(sourceUnitId, sourceSubUnitId) ?? [];
|
||||
ruleList?.forEach((rule) => {
|
||||
const { ranges, cfId } = rule;
|
||||
if (ranges.some((range) => Rectangle.intersects(sourceRange, range))) {
|
||||
rangeMap.set(cfId, isSkipSheet ? [] : ranges);
|
||||
const repeats = repeatByRange(sourceRange, targetRange);
|
||||
const targetRanges = repeats.map((repeat) => Rectangle.getPositionRange(repeat.repeatRelativeRange, repeat.startRange));
|
||||
const getRangeDelta = (cfId: string) => {
|
||||
let rangeDelta = rangeDeltaMap.get(cfId);
|
||||
if (!rangeDelta) {
|
||||
rangeDelta = { add: [], remove: [] };
|
||||
rangeDeltaMap.set(cfId, rangeDelta);
|
||||
}
|
||||
return rangeDelta;
|
||||
};
|
||||
|
||||
const targetRuleList = this._conditionalFormattingRuleModel.getSubunitRules(targetUnitId, targetSubUnitId) ?? [];
|
||||
const waitAddRule = new Map<string, IConditionFormattingRule>();
|
||||
targetRuleList.forEach((rule) => {
|
||||
if (Rectangle.doAnyRangesIntersect(rule.ranges, targetRanges)) {
|
||||
rangeMap.set(rule.cfId, rule.ranges);
|
||||
getRangeDelta(rule.cfId).remove.push(...targetRanges);
|
||||
}
|
||||
});
|
||||
|
||||
const sourceStartCell = {
|
||||
row: sourceRange.startRow,
|
||||
col: sourceRange.startColumn,
|
||||
};
|
||||
|
||||
const repeats = repeatByRange(sourceRange, targetRange);
|
||||
|
||||
repeats.forEach((repeat) => {
|
||||
loopFunc(sourceStartCell, { row: repeat.startRange.startRow, col: repeat.startRange.startColumn }, repeat.repeatRelativeRange, rangeMap, rangeDeltaMap, { targetUnitId, targetSubUnitId });
|
||||
const sourceRuleList = this._conditionalFormattingRuleModel.getSubunitRules(sourceUnitId, sourceSubUnitId) ?? [];
|
||||
const sourceRules = isSkipSheet ? [...sourceRuleList].reverse() : sourceRuleList;
|
||||
sourceRules.forEach((rule) => {
|
||||
const sourceRanges = rule.ranges.flatMap((range) => {
|
||||
const intersected = getIntersectRange(range, sourceRange);
|
||||
return intersected ? [Rectangle.getRelativeRange(intersected, sourceRange)] : [];
|
||||
});
|
||||
const additions = repeats.flatMap((repeat) => sourceRanges.flatMap((range) => {
|
||||
const copiedRange = getIntersectRange(range, repeat.repeatRelativeRange);
|
||||
return copiedRange ? [Rectangle.getPositionRange(copiedRange, repeat.startRange)] : [];
|
||||
}));
|
||||
if (additions.length) {
|
||||
let targetCfId = rule.cfId;
|
||||
if (isSkipSheet) {
|
||||
targetCfId = this._conditionalFormattingRuleModel.createCfId(targetUnitId, targetSubUnitId);
|
||||
waitAddRule.set(targetCfId, {
|
||||
...Tools.deepClone(rule),
|
||||
cfId: targetCfId,
|
||||
ranges: [],
|
||||
});
|
||||
rangeMap.set(targetCfId, []);
|
||||
}
|
||||
if (!rangeMap.has(targetCfId)) {
|
||||
rangeMap.set(targetCfId, rule.ranges);
|
||||
}
|
||||
getRangeDelta(targetCfId).add.push(...additions);
|
||||
}
|
||||
});
|
||||
rangeDeltaMap.forEach((rangeDelta, cfId) => {
|
||||
const ranges = rangeMap.get(cfId);
|
||||
@@ -283,25 +210,21 @@ export class ConditionalFormattingPainterController extends Disposable {
|
||||
undos.push(...DeleteConditionalRuleMutationUndoFactory(this._injector, params));
|
||||
}
|
||||
} else {
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cfId);
|
||||
if (!rule) {
|
||||
const waitAdd = waitAddRule.get(cfId);
|
||||
if (waitAdd) {
|
||||
if (ranges.length) {
|
||||
const sourceRule = this._conditionalFormattingRuleModel.getRule(sourceUnitId, sourceSubUnitId, cfId);
|
||||
if (sourceRule) {
|
||||
const params: IAddConditionalRuleMutationParams = {
|
||||
unitId: targetUnitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
rule: {
|
||||
...Tools.deepClone(sourceRule),
|
||||
cfId: this._conditionalFormattingRuleModel.createCfId(targetUnitId, targetSubUnitId),
|
||||
ranges,
|
||||
},
|
||||
};
|
||||
redos.push({ id: AddConditionalRuleMutation.id, params });
|
||||
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, params));
|
||||
}
|
||||
const params: IAddConditionalRuleMutationParams = {
|
||||
unitId: targetUnitId,
|
||||
subUnitId: targetSubUnitId,
|
||||
rule: { ...waitAdd, ranges },
|
||||
};
|
||||
redos.push({ id: AddConditionalRuleMutation.id, params });
|
||||
undos.push(AddConditionalRuleMutationUndoFactory(this._injector, params));
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const rule = this._conditionalFormattingRuleModel.getRule(targetUnitId, targetSubUnitId, cfId);
|
||||
if (rule) {
|
||||
if (ranges.length) {
|
||||
const params: ISetConditionalRuleMutationParams = {
|
||||
unitId: targetUnitId,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { ISheetClipboardHook } from '@univerjs/sheets-ui';
|
||||
import {
|
||||
Disposable,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
toDisposable,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDataValidationPlugin } from '@univerjs/data-validation';
|
||||
import { IAutoFillService, UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { SheetDataValidationModel, UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
|
||||
import { ISheetClipboardService } from '@univerjs/sheets-ui';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 200_000,
|
||||
columnCount: 2_000,
|
||||
cellData: {},
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Sheet2',
|
||||
rowCount: 100,
|
||||
columnCount: 100,
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class TestSheetClipboardService extends Disposable {
|
||||
private _hooks: ISheetClipboardHook[] = [];
|
||||
|
||||
addClipboardHook(hook: ISheetClipboardHook) {
|
||||
this._hooks.push(hook);
|
||||
return toDisposable(() => {
|
||||
this._hooks = this._hooks.filter((item) => item !== hook);
|
||||
});
|
||||
}
|
||||
|
||||
getHooks() {
|
||||
return this._hooks;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDvUiTestBed() {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
univer.registerPlugin(UniverDataValidationPlugin);
|
||||
univer.registerPlugin(UniverSheetsPlugin, { notExecuteFormula: true });
|
||||
univer.registerPlugin(UniverSheetsDataValidationPlugin);
|
||||
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, Tools.deepClone(TEST_WORKBOOK_DATA));
|
||||
get(IUniverInstanceService).focusUnit(workbook.getUnitId());
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
get(LocaleService).load({ enUS });
|
||||
get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
injector.add([ISheetClipboardService, { useClass: TestSheetClipboardService as never }]);
|
||||
const clipboardService = get(ISheetClipboardService) as unknown as TestSheetClipboardService;
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
workbook,
|
||||
commandService: get(ICommandService),
|
||||
dataValidationModel: get(SheetDataValidationModel),
|
||||
autoFillService: get(IAutoFillService),
|
||||
getClipboardHook: () => clipboardService.getHooks()[0],
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
};
|
||||
}
|
||||
+100
-98
@@ -14,120 +14,122 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DataValidationType } from '@univerjs/core';
|
||||
import { AUTO_FILL_APPLY_TYPE, AutoFillTools } from '@univerjs/sheets';
|
||||
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations } from '@univerjs/sheets-data-validation';
|
||||
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { ISheetAutoFillHook } from '@univerjs/sheets';
|
||||
import { DataValidationType, Direction, Range } from '@univerjs/core';
|
||||
import { AddDataValidationMutation } from '@univerjs/data-validation';
|
||||
import { AUTO_FILL_APPLY_TYPE } from '@univerjs/sheets';
|
||||
import { DATA_VALIDATION_PLUGIN_NAME } from '@univerjs/sheets-data-validation';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createDvUiTestBed } from '../../__tests__/create-dv-ui-test-bed';
|
||||
import { DataValidationAutoFillController } from '../dv-auto-fill.controller';
|
||||
|
||||
vi.mock('@univerjs/sheets-ui', async (importActual) => {
|
||||
const actual = await importActual<typeof import('@univerjs/sheets-ui')>();
|
||||
return {
|
||||
...actual,
|
||||
virtualizeDiscreteRanges: vi.fn(() => ({
|
||||
ranges: [
|
||||
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
|
||||
{ startRow: 1, startColumn: 1, endRow: 1, endColumn: 1 },
|
||||
],
|
||||
mapFunc: (row: number, col: number) => ({ row, col }),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@univerjs/sheets-data-validation', async (importActual) => {
|
||||
const actual = await importActual<typeof import('@univerjs/sheets-data-validation')>();
|
||||
return {
|
||||
...actual,
|
||||
getDataValidationDiffMutations: vi.fn(() => ({ redoMutations: ['redo-dv'], undoMutations: ['undo-dv'] })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@univerjs/sheets', async (importActual) => {
|
||||
const actual = await importActual<typeof import('@univerjs/sheets')>();
|
||||
return {
|
||||
...actual,
|
||||
AutoFillTools: {
|
||||
...actual.AutoFillTools,
|
||||
getAutoFillRepeatRange: vi.fn(() => [{
|
||||
repeatStartCell: { row: 1, col: 1 },
|
||||
relativeRange: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
}]),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('DataValidationAutoFillController', () => {
|
||||
it('registers a hook and disables series fill when checkbox validation exists in source cells', () => {
|
||||
let hook: { id: string; onBeforeFillData: (location: { unitId: string; subUnitId: string; source: { rows: number[]; cols: number[] } }) => void } | undefined;
|
||||
const autoFillService = {
|
||||
addHook: vi.fn((registeredHook) => {
|
||||
hook = registeredHook;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
setDisableApplyType: vi.fn(),
|
||||
};
|
||||
let testBed: ReturnType<typeof createDvUiTestBed>;
|
||||
let hook: ISheetAutoFillHook;
|
||||
|
||||
const controller = new DataValidationAutoFillController(
|
||||
autoFillService as never,
|
||||
{
|
||||
getRuleByLocation: vi.fn(() => ({ type: DataValidationType.CHECKBOX })),
|
||||
} as never,
|
||||
{} as never
|
||||
);
|
||||
beforeEach(() => {
|
||||
testBed = createDvUiTestBed();
|
||||
testBed.injector.add([DataValidationAutoFillController]);
|
||||
testBed.injector.get(DataValidationAutoFillController);
|
||||
const registeredHook = testBed.autoFillService.getAllHooks().find((item) => item.id === DATA_VALIDATION_PLUGIN_NAME);
|
||||
if (!registeredHook) {
|
||||
throw new Error('Data validation autofill hook was not registered');
|
||||
}
|
||||
hook = registeredHook;
|
||||
});
|
||||
|
||||
expect(controller).toBeTruthy();
|
||||
expect(hook?.id).toBe(DATA_VALIDATION_PLUGIN_NAME);
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
hook!.onBeforeFillData({
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
source: { rows: [0], cols: [0] },
|
||||
it('disables series fill for checkbox rules without querying each source cell', async () => {
|
||||
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: {
|
||||
uid: 'checkbox-rule',
|
||||
type: DataValidationType.CHECKBOX,
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
|
||||
},
|
||||
});
|
||||
vi.spyOn(testBed.dataValidationModel, 'getRuleByLocation').mockImplementation(() => {
|
||||
throw new Error('must not query validation by cell');
|
||||
});
|
||||
|
||||
expect(autoFillService.setDisableApplyType).toHaveBeenCalledWith(AUTO_FILL_APPLY_TYPE.SERIES, true);
|
||||
hook.onBeforeFillData?.({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
source: {
|
||||
rows: Array.from({ length: 100_000 }, (_, index) => index),
|
||||
cols: Array.from({ length: 1_000 }, (_, index) => index),
|
||||
},
|
||||
target: { rows: [100_000], cols: [0] },
|
||||
}, Direction.DOWN);
|
||||
|
||||
expect(testBed.autoFillService.menu.find((item) => item.value === AUTO_FILL_APPLY_TYPE.SERIES)?.disable).toBe(true);
|
||||
});
|
||||
|
||||
it('builds diff mutations for copy and format autofill, and skips unsupported apply types', () => {
|
||||
let hook: { onFillData: (location: { unitId: string; subUnitId: string; source: { rows: number[]; cols: number[]; startRow: number; endRow: number; startColumn: number; endColumn: number }; target: { rows: number[]; cols: number[]; startRow: number; endRow: number; startColumn: number; endColumn: number } }, direction: unknown, applyType: AUTO_FILL_APPLY_TYPE) => { redos: unknown[]; undos: unknown[] } } | undefined;
|
||||
const ruleMatrixCopy = {
|
||||
addRangeRules: vi.fn(),
|
||||
diff: vi.fn(() => 'diffs'),
|
||||
};
|
||||
it('ignores checkbox rules that only cover filtered-out source rows', async () => {
|
||||
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: {
|
||||
uid: 'filtered-checkbox-rule',
|
||||
type: DataValidationType.CHECKBOX,
|
||||
ranges: [{ startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 }],
|
||||
},
|
||||
});
|
||||
vi.spyOn(testBed.dataValidationModel, 'getRuleByLocation').mockImplementation(() => {
|
||||
throw new Error('must not query validation by cell');
|
||||
});
|
||||
|
||||
const controller = new DataValidationAutoFillController(
|
||||
{
|
||||
addHook: vi.fn((registeredHook) => {
|
||||
hook = registeredHook;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
setDisableApplyType: vi.fn(),
|
||||
} as never,
|
||||
{
|
||||
getRuleByLocation: vi.fn(() => null),
|
||||
getRuleObjectMatrix: vi.fn(() => ({ clone: vi.fn(() => ruleMatrixCopy) })),
|
||||
getRuleIdByLocation: vi.fn(() => 'rule-1'),
|
||||
getRules: vi.fn(() => ['existing-rule']),
|
||||
} as never,
|
||||
{} as never
|
||||
);
|
||||
hook.onBeforeFillData?.({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
source: { rows: [0, 2], cols: [0] },
|
||||
target: { rows: [3], cols: [0] },
|
||||
}, Direction.DOWN);
|
||||
|
||||
expect(controller).toBeTruthy();
|
||||
expect(testBed.autoFillService.menu.find((item) => item.value === AUTO_FILL_APPLY_TYPE.SERIES)?.disable).toBe(false);
|
||||
});
|
||||
|
||||
it('executes and undoes real validation mutations without enumerating autofill cells', async () => {
|
||||
await testBed.commandService.executeCommand(AddDataValidationMutation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
rule: {
|
||||
uid: 'decimal-rule',
|
||||
type: DataValidationType.DECIMAL,
|
||||
formula1: '1',
|
||||
ranges: [{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }],
|
||||
},
|
||||
});
|
||||
const foreach = vi.spyOn(Range, 'foreach').mockImplementation(() => {
|
||||
throw new Error('must not enumerate cells');
|
||||
});
|
||||
const location = {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
source: { rows: [0], cols: [0], startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
target: { rows: [1], cols: [1], startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
source: { rows: [0], cols: [0] },
|
||||
target: { rows: [1], cols: [0] },
|
||||
};
|
||||
const result = hook!.onFillData(location, 'down', AUTO_FILL_APPLY_TYPE.ONLY_FORMAT);
|
||||
|
||||
expect(vi.mocked(virtualizeDiscreteRanges)).toHaveBeenCalledWith([location.source, location.target]);
|
||||
expect(vi.mocked(AutoFillTools.getAutoFillRepeatRange)).toHaveBeenCalled();
|
||||
expect(ruleMatrixCopy.addRangeRules).toHaveBeenCalledWith([{ id: 'rule-1', ranges: [{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 }] }]);
|
||||
expect(vi.mocked(getDataValidationDiffMutations)).toHaveBeenCalledWith('book-1', 'sheet-1', 'diffs', {}, 'patched', true);
|
||||
expect(result).toEqual({ redos: ['redo-dv'], undos: ['undo-dv'] });
|
||||
const result = hook.onFillData?.(location, Direction.DOWN, AUTO_FILL_APPLY_TYPE.COPY);
|
||||
if (!result) {
|
||||
throw new Error('Data validation autofill did not return mutations');
|
||||
}
|
||||
foreach.mockRestore();
|
||||
for (const mutation of result.redos) {
|
||||
await testBed.commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 0, 0)?.uid).toBe('decimal-rule');
|
||||
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 1, 0)?.uid).toBe('decimal-rule');
|
||||
|
||||
expect(hook!.onFillData(location, 'down', AUTO_FILL_APPLY_TYPE.NO_FORMAT)).toEqual({ redos: [], undos: [] });
|
||||
for (const mutation of result.undos) {
|
||||
await testBed.commandService.executeCommand(mutation.id, mutation.params);
|
||||
}
|
||||
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 0, 0)?.uid).toBe('decimal-rule');
|
||||
expect(testBed.dataValidationModel.getRuleByLocation(testBed.unitId, testBed.subUnitId, 1, 0)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import type { IAutoFillLocation, ISheetAutoFillHook } from '@univerjs/sheets';
|
||||
import { DataValidationType, Disposable, Inject, Injector, ObjectMatrix, queryObjectMatrix, Range, Rectangle } from '@univerjs/core';
|
||||
import { DataValidationType, Disposable, getIntersectRange, Inject, Injector, Rectangle } from '@univerjs/core';
|
||||
import { AUTO_FILL_APPLY_TYPE, AutoFillTools, IAutoFillService } from '@univerjs/sheets';
|
||||
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
|
||||
import { virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
|
||||
@@ -42,58 +43,44 @@ export class DataValidationAutoFillController extends Disposable {
|
||||
|
||||
const virtualRange = virtualizeDiscreteRanges([sourceRange, targetRange]);
|
||||
const [vSourceRange, vTargetRange] = virtualRange.ranges;
|
||||
const { mapFunc } = virtualRange;
|
||||
const sourceStartCell = {
|
||||
row: vSourceRange.startRow,
|
||||
col: vSourceRange.startColumn,
|
||||
};
|
||||
const { mapRange, projectRange } = virtualRange;
|
||||
const repeats = AutoFillTools.getAutoFillRepeatRange(vSourceRange, vTargetRange);
|
||||
const additionMatrix = new ObjectMatrix();
|
||||
const additionRules = new Set<string>();
|
||||
repeats.forEach((repeat) => {
|
||||
const targetStartCell = repeat.repeatStartCell;
|
||||
const relativeRange = repeat.relativeRange;
|
||||
const sourceRange = {
|
||||
startRow: sourceStartCell.row,
|
||||
startColumn: sourceStartCell.col,
|
||||
endColumn: sourceStartCell.col,
|
||||
endRow: sourceStartCell.row,
|
||||
};
|
||||
const targetRange = {
|
||||
startRow: targetStartCell.row,
|
||||
startColumn: targetStartCell.col,
|
||||
endColumn: targetStartCell.col,
|
||||
endRow: targetStartCell.row,
|
||||
};
|
||||
Range.foreach(relativeRange, (row, col) => {
|
||||
const sourcePositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
sourceRange
|
||||
);
|
||||
const { row: sourceRow, col: sourceCol } = mapFunc(sourcePositionRange.startRow, sourcePositionRange.startColumn);
|
||||
// if ruleId exists, set more dv rules, if not, clear dv rules.
|
||||
const ruleId = this._sheetDataValidationModel.getRuleIdByLocation(unitId, subUnitId, sourceRow, sourceCol) || '';
|
||||
const targetPositionRange = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
endRow: row,
|
||||
},
|
||||
targetRange
|
||||
);
|
||||
const { row: targetRow, col: targetCol } = mapFunc(targetPositionRange.startRow, targetPositionRange.startColumn);
|
||||
const additionsByRuleId = new Map<string, IRange[]>();
|
||||
additionsByRuleId.set('', repeats.flatMap((repeat) => mapRange(Rectangle.getPositionRange(repeat.relativeRange, {
|
||||
startRow: repeat.repeatStartCell.row,
|
||||
endRow: repeat.repeatStartCell.row,
|
||||
startColumn: repeat.repeatStartCell.col,
|
||||
endColumn: repeat.repeatStartCell.col,
|
||||
}))));
|
||||
|
||||
additionMatrix.setValue(targetRow, targetCol, ruleId);
|
||||
additionRules.add(ruleId);
|
||||
this._sheetDataValidationModel.getRules(unitId, subUnitId).forEach((rule) => {
|
||||
const relativeSourceRanges = rule.ranges.flatMap((range) => {
|
||||
const projected = projectRange(range);
|
||||
const intersected = projected && getIntersectRange(projected, vSourceRange);
|
||||
return intersected
|
||||
? [Rectangle.getRelativeRange(intersected, vSourceRange)]
|
||||
: [];
|
||||
});
|
||||
const targetRanges = repeats.flatMap((repeat) => relativeSourceRanges.flatMap((sourceRange) => {
|
||||
const copiedRange = getIntersectRange(sourceRange, repeat.relativeRange);
|
||||
if (!copiedRange) {
|
||||
return [];
|
||||
}
|
||||
return mapRange(Rectangle.getPositionRange(copiedRange, {
|
||||
startRow: repeat.repeatStartCell.row,
|
||||
endRow: repeat.repeatStartCell.row,
|
||||
startColumn: repeat.repeatStartCell.col,
|
||||
endColumn: repeat.repeatStartCell.col,
|
||||
}));
|
||||
}));
|
||||
if (targetRanges.length) {
|
||||
additionsByRuleId.set(rule.uid, targetRanges);
|
||||
}
|
||||
});
|
||||
const additions = Array.from(additionRules).map((id) => ({ id, ranges: queryObjectMatrix(additionMatrix, (value) => value === id) }));
|
||||
const additions = Array.from(additionsByRuleId, ([id, ranges]) => ({
|
||||
id,
|
||||
ranges: ranges.length > 1 ? Rectangle.mergeRanges(ranges) : ranges,
|
||||
}));
|
||||
ruleMatrixCopy.addRangeRules(additions);
|
||||
const diffs = ruleMatrixCopy.diff(this._sheetDataValidationModel.getRules(unitId, subUnitId));
|
||||
const { redoMutations, undoMutations } = getDataValidationDiffMutations(unitId, subUnitId, diffs, this._injector, 'patched', applyType === AUTO_FILL_APPLY_TYPE.ONLY_FORMAT);
|
||||
@@ -106,14 +93,12 @@ export class DataValidationAutoFillController extends Disposable {
|
||||
id: DATA_VALIDATION_PLUGIN_NAME,
|
||||
onBeforeFillData: (location) => {
|
||||
const { source: sourceRange, unitId, subUnitId } = location;
|
||||
for (const row of sourceRange.rows) {
|
||||
for (const col of sourceRange.cols) {
|
||||
const dv = this._sheetDataValidationModel.getRuleByLocation(unitId, subUnitId, row, col);
|
||||
if (dv && dv.type === DataValidationType.CHECKBOX) {
|
||||
this._autoFillService.setDisableApplyType(AUTO_FILL_APPLY_TYPE.SERIES, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { projectRange } = virtualizeDiscreteRanges([sourceRange]);
|
||||
const hasCheckbox = this._sheetDataValidationModel.getRules(unitId, subUnitId).some((rule) => (
|
||||
rule.type === DataValidationType.CHECKBOX && rule.ranges.some((range) => projectRange(range) !== null)
|
||||
));
|
||||
if (hasCheckbox) {
|
||||
this._autoFillService.setDisableApplyType(AUTO_FILL_APPLY_TYPE.SERIES, true);
|
||||
}
|
||||
},
|
||||
onFillData: (location, direction, applyType) => {
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
import type { IRange, ISheetDataValidationRule, Nullable } from '@univerjs/core';
|
||||
import type { ICopyPastePayload, IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
|
||||
import { Disposable, Inject, Injector, IUniverInstanceService, ObjectMatrix, queryObjectMatrix, Rectangle } from '@univerjs/core';
|
||||
import { Disposable, Inject, Injector, IUniverInstanceService, Rectangle } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, rangeToDiscreteRange } from '@univerjs/sheets';
|
||||
import { DATA_VALIDATION_PLUGIN_NAME, getDataValidationDiffMutations, SheetDataValidationModel } from '@univerjs/sheets-data-validation';
|
||||
import { COPY_TYPE, getRepeatRange, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE, virtualizeDiscreteRanges } from '@univerjs/sheets-ui';
|
||||
|
||||
interface ICopyInfoType {
|
||||
matrix: ObjectMatrix<string>;
|
||||
rules: Map<string, IRange[]>;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
@@ -61,11 +61,11 @@ export class DataValidationCopyPasteController extends Disposable {
|
||||
}
|
||||
|
||||
private _collect(unitId: string, subUnitId: string, range: IRange) {
|
||||
const matrix = new ObjectMatrix<string>();
|
||||
const rules = new Map<string, IRange[]>();
|
||||
this._copyInfo = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
matrix,
|
||||
rules,
|
||||
};
|
||||
|
||||
const discreteRange = this._injector.invoke((accessor) => {
|
||||
@@ -74,17 +74,24 @@ export class DataValidationCopyPasteController extends Disposable {
|
||||
if (!discreteRange) {
|
||||
return;
|
||||
}
|
||||
const { rows, cols } = discreteRange;
|
||||
rows.forEach((row, rowIndex) => {
|
||||
cols.forEach((col, colIndex) => {
|
||||
const ruleId = this._sheetDataValidationModel.getRuleIdByLocation(unitId, subUnitId, row, col);
|
||||
matrix.setValue(rowIndex, colIndex, ruleId ?? '');
|
||||
const { projectRange } = virtualizeDiscreteRanges([discreteRange]);
|
||||
this._sheetDataValidationModel.getRules(unitId, subUnitId).forEach((rule) => {
|
||||
const projectedRanges = rule.ranges.flatMap((ruleRange) => {
|
||||
const projected = projectRange(ruleRange);
|
||||
return projected ? [projected] : [];
|
||||
});
|
||||
if (projectedRanges.length) {
|
||||
rules.set(rule.uid, projectedRanges.length > 1 ? Rectangle.mergeRanges(projectedRanges) : projectedRanges);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _generateMutations(pasteFrom: ISheetDiscreteRangeLocation, pasteTo: ISheetDiscreteRangeLocation, payload: ICopyPastePayload) {
|
||||
const copyInfo = this._copyInfo;
|
||||
if (!copyInfo) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
const { unitId: copyUnitId, subUnitId: copySubUnitId, range: copyRange } = pasteFrom;
|
||||
const { unitId: pastedUnitId, subUnitId: pastedSubUnitId, range: pastedRange } = pasteTo;
|
||||
const { copyType = COPY_TYPE.COPY } = payload;
|
||||
@@ -100,41 +107,29 @@ export class DataValidationCopyPasteController extends Disposable {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const sourceVirtualRange = virtualizeDiscreteRanges([copyRange]).ranges[0];
|
||||
const targetVirtualization = virtualizeDiscreteRanges([pastedRange]);
|
||||
const targetVirtualRange = targetVirtualization.ranges[0];
|
||||
const repeatRange = getRepeatRange(sourceVirtualRange, targetVirtualRange, true);
|
||||
const clearTargetRanges = targetVirtualization.mapRange(targetVirtualRange);
|
||||
const getTargetRanges = (sourceRanges: IRange[]) => repeatRange.flatMap(({ startRange }) => sourceRanges.flatMap((sourceRange) => (
|
||||
targetVirtualization.mapRange(Rectangle.getPositionRange(sourceRange, startRange))
|
||||
)));
|
||||
|
||||
if (pastedUnitId !== copyUnitId || pastedSubUnitId !== copySubUnitId) {
|
||||
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(pastedUnitId, pastedSubUnitId).clone();
|
||||
const additionMatrix = new ObjectMatrix();
|
||||
const addRules = new Set<string>();
|
||||
|
||||
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pastedRange]);
|
||||
|
||||
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
|
||||
const additionRules: Map<string, ISheetDataValidationRule> = new Map();
|
||||
const additions = [{ id: '', ranges: clearTargetRanges }];
|
||||
|
||||
repeatRange.forEach(({ startRange }) => {
|
||||
this._copyInfo?.matrix.forValue((row, col, ruleId) => {
|
||||
const range = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
},
|
||||
startRange
|
||||
);
|
||||
const transformedRuleId = `${copySubUnitId}-${ruleId}`;
|
||||
const oldRule = this._sheetDataValidationModel.getRuleById(copyUnitId, copySubUnitId, ruleId);
|
||||
|
||||
if (!this._sheetDataValidationModel.getRuleById(pastedUnitId, pastedSubUnitId, transformedRuleId) && oldRule) {
|
||||
additionRules.set(transformedRuleId, { ...oldRule, uid: transformedRuleId });
|
||||
}
|
||||
|
||||
const { row: startRow, col: startColumn } = mapFunc(range.startRow, range.startColumn);
|
||||
addRules.add(transformedRuleId);
|
||||
additionMatrix.setValue(startRow, startColumn, transformedRuleId);
|
||||
});
|
||||
copyInfo.rules.forEach((ranges, ruleId) => {
|
||||
const transformedRuleId = `${copySubUnitId}-${ruleId}`;
|
||||
const oldRule = this._sheetDataValidationModel.getRuleById(copyUnitId, copySubUnitId, ruleId);
|
||||
if (!this._sheetDataValidationModel.getRuleById(pastedUnitId, pastedSubUnitId, transformedRuleId) && oldRule) {
|
||||
additionRules.set(transformedRuleId, { ...oldRule, uid: transformedRuleId });
|
||||
}
|
||||
additions.push({ id: transformedRuleId, ranges: getTargetRanges(ranges) });
|
||||
});
|
||||
|
||||
const additions = Array.from(addRules).map((id) => ({ id, ranges: queryObjectMatrix(additionMatrix, (value) => value === id) }));
|
||||
ruleMatrix.addRangeRules(additions);
|
||||
|
||||
const { redoMutations, undoMutations } = getDataValidationDiffMutations(
|
||||
@@ -149,17 +144,10 @@ export class DataValidationCopyPasteController extends Disposable {
|
||||
if (copyType === COPY_TYPE.CUT) {
|
||||
// Delete rules in copy range
|
||||
const copySheetRuleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(copyUnitId, copySubUnitId).clone();
|
||||
const deleteRangeStartCell = mapFunc(vCopyRange.startRow, vCopyRange.startColumn);
|
||||
const deleteRangeEndCell = mapFunc(vCopyRange.endRow, vCopyRange.endColumn);
|
||||
copySheetRuleMatrix.addRangeRules([
|
||||
{
|
||||
id: '',
|
||||
ranges: [{
|
||||
startRow: deleteRangeStartCell.row,
|
||||
endRow: deleteRangeEndCell.row,
|
||||
startColumn: deleteRangeStartCell.col,
|
||||
endColumn: deleteRangeEndCell.col,
|
||||
}],
|
||||
ranges: virtualizeDiscreteRanges([copyRange]).mapRange(sourceVirtualRange),
|
||||
},
|
||||
]);
|
||||
const { redoMutations: cutRedos, undoMutations: cutUndos } = getDataValidationDiffMutations(
|
||||
@@ -180,31 +168,10 @@ export class DataValidationCopyPasteController extends Disposable {
|
||||
};
|
||||
} else {
|
||||
const ruleMatrix = this._sheetDataValidationModel.getRuleObjectMatrix(copyUnitId, copySubUnitId).clone();
|
||||
const additionMatrix = new ObjectMatrix();
|
||||
const additionRules = new Set<string>();
|
||||
|
||||
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pastedRange]);
|
||||
|
||||
const repeatRange = getRepeatRange(vCopyRange, vPastedRange, true);
|
||||
|
||||
repeatRange.forEach(({ startRange }) => {
|
||||
this._copyInfo?.matrix.forValue((row, col, ruleId) => {
|
||||
const range = Rectangle.getPositionRange(
|
||||
{
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
},
|
||||
startRange
|
||||
);
|
||||
const { row: startRow, col: startColumn } = mapFunc(range.startRow, range.startColumn);
|
||||
additionMatrix.setValue(startRow, startColumn, ruleId);
|
||||
additionRules.add(ruleId);
|
||||
});
|
||||
});
|
||||
|
||||
const additions = Array.from(additionRules).map((id) => ({ id, ranges: queryObjectMatrix(additionMatrix, (value) => value === id) }));
|
||||
const additions = [
|
||||
{ id: '', ranges: clearTargetRanges },
|
||||
...Array.from(copyInfo.rules, ([id, ranges]) => ({ id, ranges: getTargetRanges(ranges) })),
|
||||
];
|
||||
ruleMatrix.addRangeRules(additions);
|
||||
const { redoMutations, undoMutations } = getDataValidationDiffMutations(
|
||||
pastedUnitId,
|
||||
|
||||
@@ -38,4 +38,25 @@ describe('range-tools', () => {
|
||||
expect(mapFunc(0, 0)).toEqual({ row: 5, col: 10 });
|
||||
expect(mapFunc(2, 2)).toEqual({ row: 9, col: 30 });
|
||||
});
|
||||
|
||||
it('maps rectangles between virtual and discrete coordinates', () => {
|
||||
const result = virtualizeDiscreteRanges([{
|
||||
rows: [5, 6, 9],
|
||||
cols: [10, 11, 20],
|
||||
}]);
|
||||
|
||||
expect(result).toHaveProperty('mapRange');
|
||||
expect(result.mapRange({ startRow: 0, endRow: 2, startColumn: 0, endColumn: 2 })).toEqual([
|
||||
{ startRow: 5, endRow: 6, startColumn: 10, endColumn: 11 },
|
||||
{ startRow: 5, endRow: 6, startColumn: 20, endColumn: 20 },
|
||||
{ startRow: 9, endRow: 9, startColumn: 10, endColumn: 11 },
|
||||
{ startRow: 9, endRow: 9, startColumn: 20, endColumn: 20 },
|
||||
]);
|
||||
expect(result.projectRange({ startRow: 6, endRow: 9, startColumn: 11, endColumn: 20 })).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 2,
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,9 +17,58 @@
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import type { IDiscreteRange } from '@univerjs/sheets';
|
||||
|
||||
interface ILine {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
function groupConsecutive(values: number[], start: number, end: number): ILine[] {
|
||||
const groups: ILine[] = [];
|
||||
for (let index = start; index <= end; index++) {
|
||||
const value = values[index];
|
||||
const previous = groups[groups.length - 1];
|
||||
if (previous && value === previous.end + 1) {
|
||||
previous.end = value;
|
||||
} else {
|
||||
groups.push({ start: value, end: value });
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function projectLine(values: number[], start: number, end: number): ILine | null {
|
||||
let low = 0;
|
||||
let high = values.length;
|
||||
while (low < high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
if (values[middle] < start) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
const first = low;
|
||||
if (first === values.length || values[first] > end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
high = values.length;
|
||||
while (low < high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
if (values[middle] <= end) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
return { start: first, end: low - 1 };
|
||||
}
|
||||
|
||||
export function virtualizeDiscreteRanges(ranges: IDiscreteRange[]): {
|
||||
ranges: IRange[];
|
||||
mapFunc: (row: number, col: number) => { row: number; col: number };
|
||||
mapRange: (range: IRange) => IRange[];
|
||||
projectRange: (range: IRange) => IRange | null;
|
||||
} {
|
||||
let totalRows: number[] = [];
|
||||
let totalCols: number[] = [];
|
||||
@@ -51,5 +100,22 @@ export function virtualizeDiscreteRanges(ranges: IDiscreteRange[]): {
|
||||
col: totalCols[col],
|
||||
}
|
||||
),
|
||||
mapRange: (range) => {
|
||||
const rowGroups = groupConsecutive(totalRows, range.startRow, range.endRow);
|
||||
const columnGroups = groupConsecutive(totalCols, range.startColumn, range.endColumn);
|
||||
return rowGroups.flatMap((row) => columnGroups.map((column) => ({
|
||||
startRow: row.start,
|
||||
endRow: row.end,
|
||||
startColumn: column.start,
|
||||
endColumn: column.end,
|
||||
})));
|
||||
},
|
||||
projectRange: (range) => {
|
||||
const row = projectLine(totalRows, range.startRow, range.endRow);
|
||||
const column = projectLine(totalCols, range.startColumn, range.endColumn);
|
||||
return row && column
|
||||
? { startRow: row.start, endRow: row.end, startColumn: column.start, endColumn: column.end }
|
||||
: null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import type {
|
||||
} from '../../../basics';
|
||||
import type { IMoveRangeMutationParams } from '../../../commands/mutations/move-range.mutation';
|
||||
import type { IMoveColumnsMutationParams } from '../../../commands/mutations/move-rows-cols.mutation';
|
||||
import { Direction, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, RANGE_TYPE } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Direction, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, Range, RANGE_TYPE } from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { InsertColMutation, InsertRowMutation } from '../../../commands/mutations/insert-row-col.mutation';
|
||||
import { MoveRangeMutation } from '../../../commands/mutations/move-range.mutation';
|
||||
import { MoveColsMutation } from '../../../commands/mutations/move-rows-cols.mutation';
|
||||
@@ -71,6 +71,18 @@ const formatRanges = (ranges: IRange[]) =>
|
||||
.map((range) => [range.startRow, range.endRow, range.startColumn, range.endColumn] as const)
|
||||
.sort((prev, aft) => prev[0] - aft[0] || prev[2] - aft[2] || prev[1] - aft[1] || prev[3] - aft[3]);
|
||||
|
||||
const formatCoveredCells = (ranges: IRange[]) => {
|
||||
const cells = new Set<string>();
|
||||
ranges.forEach((range) => {
|
||||
for (let row = range.startRow; row <= range.endRow; row++) {
|
||||
for (let column = range.startColumn; column <= range.endColumn; column++) {
|
||||
cells.add(`${row}:${column}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return Array.from(cells).sort();
|
||||
};
|
||||
|
||||
const selectionDeps = (range?: IRange) => ({
|
||||
selectionManagerService: {
|
||||
getCurrentSelections: () => range ? [{ range }] : [],
|
||||
@@ -78,6 +90,50 @@ const selectionDeps = (range?: IRange) => ({
|
||||
} as any);
|
||||
|
||||
describe('ref range util behavior coverage', () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('transforms large ranges without enumerating individual cells', () => {
|
||||
const foreach = vi.spyOn(Range, 'foreach').mockImplementation(() => {
|
||||
throw new Error('must not enumerate cells');
|
||||
});
|
||||
const targetRange = r(0, MAX_ROW_COUNT - 1, 0, MAX_COLUMN_COUNT - 1);
|
||||
|
||||
expect(() => handleMoveRowsCommon({
|
||||
id: EffectRefRangId.MoveRowsCommandId,
|
||||
params: { fromRange: r(2, 3, 0, MAX_COLUMN_COUNT - 1), toRange: r(8, 9, 0, MAX_COLUMN_COUNT - 1) },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleMoveColsCommon({
|
||||
id: EffectRefRangId.MoveColsCommandId,
|
||||
params: { fromRange: r(0, MAX_ROW_COUNT - 1, 2, 3), toRange: r(0, MAX_ROW_COUNT - 1, 8, 9) },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleReorderRangeCommon({
|
||||
id: EffectRefRangId.ReorderRangeCommandId,
|
||||
params: { unitId: 'unit', subUnitId: 'sheet', range: r(2, 3, 0, MAX_COLUMN_COUNT - 1), order: { 2: 3, 3: 2 } },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleMoveRangeCommon({
|
||||
id: EffectRefRangId.MoveRangeCommandId,
|
||||
params: { fromRange: r(2, 3, 2, 3), toRange: r(8, 9, 8, 9) },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleInsertRangeMoveDownCommon({
|
||||
id: EffectRefRangId.InsertRangeMoveDownCommandId,
|
||||
params: { range: r(2, 3, 2, 3) },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleInsertRangeMoveRightCommon({
|
||||
id: EffectRefRangId.InsertRangeMoveRightCommandId,
|
||||
params: { range: r(2, 3, 2, 3) },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleDeleteRangeMoveLeftCommon({
|
||||
id: EffectRefRangId.DeleteRangeMoveLeftCommandId,
|
||||
params: { range: r(2, 3, 2, 3) },
|
||||
}, targetRange)).not.toThrow();
|
||||
expect(() => handleDeleteRangeMoveUpCommon({
|
||||
id: EffectRefRangId.DeleteRangeMoveUpCommandId,
|
||||
params: { range: r(2, 3, 2, 3) },
|
||||
}, targetRange)).not.toThrow();
|
||||
|
||||
foreach.mockRestore();
|
||||
});
|
||||
|
||||
describe('range type normalization', () => {
|
||||
it('expands typed and inferred column/row/all ranges for internal calculation', () => {
|
||||
expect(handleRangeTypeInput(r(Number.NaN, Number.NaN, 2, 4))).toEqual(r(0, MAX_ROW_COUNT - 1, 2, 4));
|
||||
@@ -182,19 +238,19 @@ describe('ref range util behavior coverage', () => {
|
||||
});
|
||||
|
||||
it('removes deleted cells and moves the trailing cells into the gap', () => {
|
||||
expect(formatRanges(handleDeleteRangeMoveLeftCommon({
|
||||
expect(formatCoveredCells(handleDeleteRangeMoveLeftCommon({
|
||||
id: EffectRefRangId.DeleteRangeMoveLeftCommandId,
|
||||
params: { range: r(1, 2, 2, 3) },
|
||||
}, r(0, 3, 1, 5)))).toEqual(formatRanges([
|
||||
}, r(0, 3, 1, 5)))).toEqual(formatCoveredCells([
|
||||
r(0, 3, 1, 3),
|
||||
r(0, 0, 4, 5),
|
||||
r(3, 3, 4, 5),
|
||||
]));
|
||||
|
||||
expect(formatRanges(handleDeleteRangeMoveUpCommon({
|
||||
expect(formatCoveredCells(handleDeleteRangeMoveUpCommon({
|
||||
id: EffectRefRangId.DeleteRangeMoveUpCommandId,
|
||||
params: { range: r(2, 3, 1, 2) },
|
||||
}, r(1, 5, 0, 3)))).toEqual(formatRanges([
|
||||
}, r(1, 5, 0, 3)))).toEqual(formatCoveredCells([
|
||||
r(1, 3, 0, 3),
|
||||
r(4, 5, 0, 0),
|
||||
r(4, 5, 3, 3),
|
||||
@@ -293,10 +349,10 @@ describe('ref range util behavior coverage', () => {
|
||||
});
|
||||
|
||||
it('uses common range transformations for split range operations', () => {
|
||||
expect(formatRanges(handleCommonDefaultRangeChangeWithEffectRefCommands(r(0, 3, 1, 5), {
|
||||
expect(formatCoveredCells(handleCommonDefaultRangeChangeWithEffectRefCommands(r(0, 3, 1, 5), {
|
||||
id: EffectRefRangId.DeleteRangeMoveLeftCommandId,
|
||||
params: { range: r(1, 2, 2, 3) },
|
||||
}))).toEqual(formatRanges([
|
||||
}))).toEqual(formatCoveredCells([
|
||||
r(0, 3, 1, 3),
|
||||
r(0, 0, 4, 5),
|
||||
r(3, 3, 4, 5),
|
||||
|
||||
@@ -66,6 +66,18 @@ const countRange = ([a, b, c, d]: readonly [number, number, number, number]) =>
|
||||
|
||||
const formatRanges = (ranges: IRange[]) => ranges.map((range) => [range.startRow, range.endRow, range.startColumn, range.endColumn] as const).sort((prev, aft) => countRange(prev) - countRange(aft));
|
||||
|
||||
const formatCoveredCells = (ranges: IRange[]) => {
|
||||
const cells = new Set<string>();
|
||||
ranges.forEach((range) => {
|
||||
for (let row = range.startRow; row <= range.endRow; row++) {
|
||||
for (let column = range.startColumn; column <= range.endColumn; column++) {
|
||||
cells.add(`${row}:${column}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return Array.from(cells).sort();
|
||||
};
|
||||
|
||||
describe('test ref-range move', () => {
|
||||
describe('range type and effect-range helpers', () => {
|
||||
it('normalizes row, column, all, and NaN range coordinates', () => {
|
||||
@@ -535,7 +547,7 @@ describe('test ref-range move', () => {
|
||||
targetRange1_2
|
||||
);
|
||||
|
||||
expect(resRange1_1).toEqual(
|
||||
expect(formatRanges(resRange1_1)).toEqual(formatRanges(
|
||||
[
|
||||
{
|
||||
endColumn: 10,
|
||||
@@ -550,8 +562,8 @@ describe('test ref-range move', () => {
|
||||
startRow: 2,
|
||||
},
|
||||
]
|
||||
);
|
||||
expect(resRange1_2).toEqual([
|
||||
));
|
||||
expect(formatRanges(resRange1_2)).toEqual(formatRanges([
|
||||
{
|
||||
endColumn: 10,
|
||||
endRow: 22,
|
||||
@@ -564,7 +576,7 @@ describe('test ref-range move', () => {
|
||||
startColumn: 0,
|
||||
startRow: 6,
|
||||
},
|
||||
]);
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1621,8 +1633,8 @@ describe('test ref-range move', () => {
|
||||
}]
|
||||
)
|
||||
);
|
||||
expect(formatRanges(res2)).toEqual(
|
||||
formatRanges(
|
||||
expect(formatCoveredCells(res2)).toEqual(
|
||||
formatCoveredCells(
|
||||
[{
|
||||
endColumn: 10,
|
||||
endRow: 10,
|
||||
|
||||
@@ -44,7 +44,7 @@ import type {
|
||||
IRemoveRowColCommand,
|
||||
IReorderRangeCommand,
|
||||
} from './type';
|
||||
import { Direction, getIntersectRange, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, mergeIntervals, ObjectMatrix, queryObjectMatrix, Range, RANGE_TYPE, Rectangle } from '@univerjs/core';
|
||||
import { Direction, getIntersectRange, IUniverInstanceService, MAX_COLUMN_COUNT, MAX_ROW_COUNT, mergeIntervals, ObjectMatrix, RANGE_TYPE, Rectangle } from '@univerjs/core';
|
||||
import { DeleteRangeMoveLeftCommand } from '../../commands/commands/delete-range-move-left.command';
|
||||
import { DeleteRangeMoveUpCommand } from '../../commands/commands/delete-range-move-up.command';
|
||||
import { InsertRangeMoveDownCommand } from '../../commands/commands/insert-range-move-down.command';
|
||||
@@ -120,6 +120,67 @@ interface ILine {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface ILineTransform extends ILine {
|
||||
offset: number;
|
||||
}
|
||||
|
||||
function mergeAndSortRanges(ranges: IRange[]): IRange[] {
|
||||
const merged = ranges.length > 1 ? Rectangle.mergeRanges(ranges) : ranges;
|
||||
return sortRanges(merged);
|
||||
}
|
||||
|
||||
function sortRanges(ranges: IRange[]): IRange[] {
|
||||
return ranges.sort((a, b) => a.startRow - b.startRow || a.startColumn - b.startColumn || a.endRow - b.endRow || a.endColumn - b.endColumn);
|
||||
}
|
||||
|
||||
function translateRange(range: IRange, rowOffset: number, columnOffset: number): IRange {
|
||||
return {
|
||||
startRow: range.startRow + rowOffset,
|
||||
endRow: range.endRow + rowOffset,
|
||||
startColumn: range.startColumn + columnOffset,
|
||||
endColumn: range.endColumn + columnOffset,
|
||||
};
|
||||
}
|
||||
|
||||
function moveRangeAlongAxis(targetRange: IRange, from: number, count: number, to: number, isRow: boolean): IRange[] {
|
||||
let transforms: ILineTransform[];
|
||||
if (from > to) {
|
||||
transforms = [
|
||||
{ start: Number.NEGATIVE_INFINITY, end: to - 1, offset: 0 },
|
||||
{ start: to, end: from - 1, offset: count },
|
||||
{ start: from, end: from + count - 1, offset: to - from },
|
||||
{ start: from + count, end: Number.POSITIVE_INFINITY, offset: 0 },
|
||||
];
|
||||
} else {
|
||||
if (from + count > to) {
|
||||
throw new Error('Invalid move operation');
|
||||
}
|
||||
transforms = [
|
||||
{ start: Number.NEGATIVE_INFINITY, end: from - 1, offset: 0 },
|
||||
{ start: from, end: from + count - 1, offset: to - from - count },
|
||||
{ start: from + count, end: to - 1, offset: -count },
|
||||
{ start: to, end: Number.POSITIVE_INFINITY, offset: 0 },
|
||||
];
|
||||
}
|
||||
|
||||
const rangeStart = isRow ? targetRange.startRow : targetRange.startColumn;
|
||||
const rangeEnd = isRow ? targetRange.endRow : targetRange.endColumn;
|
||||
const ranges = transforms.flatMap(({ start, end, offset }) => {
|
||||
const intersectStart = Math.max(rangeStart, start);
|
||||
const intersectEnd = Math.min(rangeEnd, end);
|
||||
if (intersectStart > intersectEnd) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const range = isRow
|
||||
? { startRow: intersectStart, endRow: intersectEnd, startColumn: targetRange.startColumn, endColumn: targetRange.endColumn }
|
||||
: { startRow: targetRange.startRow, endRow: targetRange.endRow, startColumn: intersectStart, endColumn: intersectEnd };
|
||||
return [translateRange(range, isRow ? offset : 0, isRow ? 0 : offset)];
|
||||
});
|
||||
|
||||
return mergeAndSortRanges(ranges);
|
||||
}
|
||||
/**
|
||||
* see docs/tldr/ref-range/move-rows-cols.tldr
|
||||
*/
|
||||
@@ -276,17 +337,7 @@ export const handleMoveRowsCommon = (params: IMoveRowsCommand, targetRange: IRan
|
||||
const count = fromRange.endRow - fromRange.startRow + 1;
|
||||
const toRow = toRange.startRow;
|
||||
|
||||
const matrix = new ObjectMatrix();
|
||||
|
||||
Range.foreach(targetRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
|
||||
matrix.moveRows(fromRow, count, toRow);
|
||||
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
const res = queryObjectMatrix(matrix, (value) => value === 1);
|
||||
return res;
|
||||
return moveRangeAlongAxis(targetRange, fromRow, count, toRow, true);
|
||||
};
|
||||
|
||||
export const handleReorderRangeCommon = (param: IReorderRangeCommand, targetRange: IRange) => {
|
||||
@@ -294,26 +345,25 @@ export const handleReorderRangeCommon = (param: IReorderRangeCommand, targetRang
|
||||
if (!range || !order) {
|
||||
return [targetRange];
|
||||
}
|
||||
const matrix = new ObjectMatrix();
|
||||
Range.foreach(targetRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
const overwrittenRows: IRange[] = [];
|
||||
const additions: IRange[] = [];
|
||||
const startColumn = Math.max(range.startColumn, targetRange.startColumn);
|
||||
const endColumn = Math.min(range.endColumn, targetRange.endColumn);
|
||||
|
||||
const cacheMatrix = new ObjectMatrix();
|
||||
Range.foreach(range, (row, col) => {
|
||||
if (Object.prototype.hasOwnProperty.call(order, row)) {
|
||||
const targetRow = order[row];
|
||||
const cloneCell = matrix.getValue(targetRow, col) ?? 0;
|
||||
cacheMatrix.setValue(row, col, cloneCell);
|
||||
Object.keys(order).forEach((key) => {
|
||||
const row = Number(key);
|
||||
if (row < range.startRow || row > range.endRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
overwrittenRows.push({ startRow: row, endRow: row, startColumn: range.startColumn, endColumn: range.endColumn });
|
||||
const sourceRow = order[row];
|
||||
if (sourceRow >= targetRange.startRow && sourceRow <= targetRange.endRow && startColumn <= endColumn) {
|
||||
additions.push({ startRow: row, endRow: row, startColumn, endColumn });
|
||||
}
|
||||
});
|
||||
|
||||
cacheMatrix.forValue((row, col, cellData) => {
|
||||
matrix.setValue(row, col, cellData);
|
||||
});
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
const res = queryObjectMatrix(matrix, (value) => value === 1);
|
||||
return res;
|
||||
return mergeAndSortRanges([...Rectangle.subtractMulti([targetRange], overwrittenRows), ...additions]);
|
||||
};
|
||||
|
||||
export const handleMoveCols = (params: IMoveColsCommand, targetRange: IRange): IOperator[] => {
|
||||
@@ -355,15 +405,7 @@ export const handleMoveColsCommon = (params: IMoveColsCommand, targetRange: IRan
|
||||
const count = fromRange.endColumn - fromRange.startColumn + 1;
|
||||
const toCol = toRange.startColumn;
|
||||
|
||||
const matrix = new ObjectMatrix();
|
||||
|
||||
Range.foreach(targetRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
|
||||
matrix.moveColumns(fromCol, count, toCol);
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
return queryObjectMatrix(matrix, (value) => value === 1);
|
||||
return moveRangeAlongAxis(targetRange, fromCol, count, toCol, false);
|
||||
};
|
||||
|
||||
export const handleMoveRange = (param: IMoveRangeCommand, targetRange: IRange) => {
|
||||
@@ -414,41 +456,13 @@ export const handleMoveRangeCommon = (param: IMoveRangeCommand, targetRange: IRa
|
||||
return [positionRange];
|
||||
}
|
||||
|
||||
const matrix = new ObjectMatrix();
|
||||
|
||||
Range.foreach(targetRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
|
||||
const fromMatrix = new ObjectMatrix();
|
||||
const loopFromRange = getIntersectRange(fromRange, targetRange);
|
||||
|
||||
loopFromRange && Range.foreach(loopFromRange, (row, col) => {
|
||||
if (matrix.getValue(row, col)) {
|
||||
matrix.setValue(row, col, undefined);
|
||||
fromMatrix.setValue(row, col, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const columnOffset = toRange.startColumn - fromRange.startColumn;
|
||||
const rowOffset = toRange.startRow - fromRange.startRow;
|
||||
const movedRange = getIntersectRange(fromRange, targetRange);
|
||||
const remainingRanges = Rectangle.subtractMulti([targetRange], [fromRange, toRange]);
|
||||
const translatedRanges = movedRange ? [translateRange(movedRange, rowOffset, columnOffset)] : [];
|
||||
|
||||
const loopToRange = {
|
||||
startColumn: toRange.startColumn - columnOffset,
|
||||
endColumn: toRange.endColumn - columnOffset,
|
||||
startRow: toRange.startRow - rowOffset,
|
||||
endRow: toRange.endRow - rowOffset,
|
||||
};
|
||||
|
||||
loopToRange && Range.foreach(loopToRange, (row, col) => {
|
||||
const targetRow = row + rowOffset;
|
||||
const targetCol = col + columnOffset;
|
||||
matrix.setValue(targetRow, targetCol, fromMatrix.getValue(row, col) ?? 0);
|
||||
});
|
||||
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
const res = queryObjectMatrix(matrix, (value) => value === 1);
|
||||
return res;
|
||||
return mergeAndSortRanges([...remainingRanges, ...translatedRanges]);
|
||||
};
|
||||
|
||||
// see docs/tldr/ref-range/remove-rows-cols.tldr
|
||||
@@ -769,19 +783,7 @@ export const handleInsertRangeMoveDownCommon = (param: IInsertRangeMoveDownComma
|
||||
return [targetRange];
|
||||
}
|
||||
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
noMoveRanges.forEach((noMoveRange) => {
|
||||
Range.foreach(noMoveRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
});
|
||||
|
||||
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
|
||||
matrix.setValue(row + moveCount, col, 1);
|
||||
});
|
||||
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
return queryObjectMatrix(matrix, (v) => v === 1);
|
||||
return sortRanges([...noMoveRanges, translateRange(targetMoveRange, moveCount, 0)]);
|
||||
};
|
||||
|
||||
export const handleInsertRangeMoveRight = (param: IInsertRangeMoveRightCommand, targetRange: IRange) => {
|
||||
@@ -820,19 +822,7 @@ export const handleInsertRangeMoveRightCommon = (param: IInsertRangeMoveRightCom
|
||||
return [targetRange];
|
||||
}
|
||||
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
noMoveRanges.forEach((noMoveRange) => {
|
||||
Range.foreach(noMoveRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
});
|
||||
|
||||
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
|
||||
matrix.setValue(row, col + moveCount, 1);
|
||||
});
|
||||
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
return queryObjectMatrix(matrix, (v) => v === 1);
|
||||
return sortRanges([...noMoveRanges, translateRange(targetMoveRange, 0, moveCount)]);
|
||||
};
|
||||
|
||||
export const handleDeleteRangeMoveLeft = (param: IDeleteRangeMoveLeftCommand, targetRange: IRange) => {
|
||||
@@ -872,34 +862,18 @@ export const handleDeleteRangeMoveLeftCommon = (param: IDeleteRangeMoveLeftComma
|
||||
};
|
||||
|
||||
const moveCount = range.endColumn - range.startColumn + 1;
|
||||
// this range need delete
|
||||
const targetDeleteRange = getIntersectRange(range, targetRange);
|
||||
|
||||
const noMoveRanges = Rectangle.subtract(targetRange, rightRange);
|
||||
const targetMoveRange = getIntersectRange(rightRange, targetRange);
|
||||
|
||||
if (!targetDeleteRange && !targetMoveRange) {
|
||||
if (!targetMoveRange) {
|
||||
return [targetRange];
|
||||
}
|
||||
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
const movedRanges = targetMoveRange
|
||||
? Rectangle.subtract(targetMoveRange, range).map((target) => translateRange(target, 0, -moveCount))
|
||||
: [];
|
||||
|
||||
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
|
||||
matrix.setValue(row, col - moveCount, 1);
|
||||
});
|
||||
|
||||
targetDeleteRange && Range.foreach(targetDeleteRange, (row, col) => {
|
||||
matrix.setValue(row, col - moveCount, 0);
|
||||
});
|
||||
|
||||
noMoveRanges.forEach((noMoveRange) => {
|
||||
Range.foreach(noMoveRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
return queryObjectMatrix(matrix, (v) => v === 1);
|
||||
return mergeAndSortRanges([...noMoveRanges, ...movedRanges]);
|
||||
};
|
||||
|
||||
export const handleDeleteRangeMoveUp = (param: IDeleteRangeMoveUpCommand, targetRange: IRange) => {
|
||||
@@ -935,34 +909,18 @@ export const handleDeleteRangeMoveUpCommon = (param: IDeleteRangeMoveUpCommand,
|
||||
};
|
||||
|
||||
const moveCount = range.endRow - range.startRow + 1;
|
||||
// this range need delete
|
||||
const targetDeleteRange = getIntersectRange(range, targetRange);
|
||||
|
||||
const noMoveRanges = Rectangle.subtract(targetRange, bottomRange);
|
||||
const targetMoveRange = getIntersectRange(bottomRange, targetRange);
|
||||
|
||||
if (!targetDeleteRange && !targetMoveRange) {
|
||||
if (!targetMoveRange) {
|
||||
return [targetRange];
|
||||
}
|
||||
|
||||
const matrix = new ObjectMatrix<number>();
|
||||
const movedRanges = targetMoveRange
|
||||
? Rectangle.subtract(targetMoveRange, range).map((target) => translateRange(target, -moveCount, 0))
|
||||
: [];
|
||||
|
||||
targetMoveRange && Range.foreach(targetMoveRange, (row, col) => {
|
||||
matrix.setValue(row - moveCount, col, 1);
|
||||
});
|
||||
|
||||
targetDeleteRange && Range.foreach(targetDeleteRange, (row, col) => {
|
||||
matrix.setValue(row - moveCount, col, 0);
|
||||
});
|
||||
|
||||
noMoveRanges.forEach((noMoveRange) => {
|
||||
Range.foreach(noMoveRange, (row, col) => {
|
||||
matrix.setValue(row, col, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO@zhangw try to remove queryObjectMatrix, this could case memory out of use in large range.
|
||||
return queryObjectMatrix(matrix, (v) => v === 1);
|
||||
return mergeAndSortRanges([...noMoveRanges, ...movedRanges]);
|
||||
};
|
||||
|
||||
export const handleRemoveRowCommon = (param: IRemoveRowColCommandInterceptParams, targetRange: IRange) => {
|
||||
|
||||
Reference in New Issue
Block a user