snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
46
packages/sheets-ui/README.md
Normal file
46
packages/sheets-ui/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# @univerjs/sheets-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-ui)
|
||||
|
||||
`@univerjs/sheets-ui` provides the main spreadsheet UI layer for Univer Sheets, including selection, menus, clipboard, formula bar integration, and rendering interaction services.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-ui` | `UniverSheetsUi` | Yes | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-ui
|
||||
# or
|
||||
npm install @univerjs/sheets-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/sheets-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/sheets-ui/locale/en-US';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
|
||||
univer.registerPlugin(UniverSheetsUIPlugin);
|
||||
|
||||
// Merge EnUS into your Univer locale map when this package contributes UI text.
|
||||
```
|
||||
|
||||
Exported plugin classes:
|
||||
|
||||
- `UniverSheetsUIPlugin`
|
||||
- `UniverSheetsMobileUIPlugin`
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
110
packages/sheets-ui/package.json
Normal file
110
packages/sheets-ui/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Spreadsheet UI layer for Univer Sheets.",
|
||||
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"univer",
|
||||
"sheets",
|
||||
"spreadsheet",
|
||||
"editor",
|
||||
"ui"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.ts",
|
||||
"./facade": "./src/facade/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"require": "./lib/cjs/*",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./locale/*": {
|
||||
"import": "./lib/es/locale/*.js",
|
||||
"require": "./lib/cjs/locale/*.js",
|
||||
"types": "./lib/types/locale/*.d.ts"
|
||||
},
|
||||
"./facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/facade": {
|
||||
"import": "./lib/es/facade.js",
|
||||
"require": "./lib/cjs/facade.js",
|
||||
"types": "./lib/types/facade/index.d.ts"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/engine-formula": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/protocol": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/telemetry": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"@univerjs/themes": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/sheets-ui/postcss.config.mjs
Normal file
1
packages/sheets-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
40
packages/sheets-ui/src/__tests__/embed-tab-anchor.spec.ts
Normal file
40
packages/sheets-ui/src/__tests__/embed-tab-anchor.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 { UniverInstanceType } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getEmbedSheetsTabCustomData } from '../embed-tab-anchor';
|
||||
|
||||
describe('getEmbedSheetsTabCustomData', () => {
|
||||
it('reads the embedded product type used by the Sheet tab marker', () => {
|
||||
expect(getEmbedSheetsTabCustomData({
|
||||
custom: {
|
||||
UNIVER_EMBED_SHEETS_TAB: {
|
||||
version: 1,
|
||||
embedId: 'embed-1',
|
||||
hostAnchorId: 'sheet-1',
|
||||
childType: UniverInstanceType.UNIVER_BOARD,
|
||||
},
|
||||
},
|
||||
})).toEqual({
|
||||
version: 1,
|
||||
embedId: 'embed-1',
|
||||
hostAnchorId: 'sheet-1',
|
||||
childType: UniverInstanceType.UNIVER_BOARD,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { DOCS_VIEW_KEY } from '@univerjs/docs-ui';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getEditorObject } from '../get-editor-object';
|
||||
|
||||
describe('getEditorObject', () => {
|
||||
it('returns undefined when unitId is null or render is missing', () => {
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: () => null,
|
||||
} as unknown as IRenderManagerService;
|
||||
|
||||
expect(getEditorObject(null, renderManagerService)).toBeUndefined();
|
||||
expect(getEditorObject('u-1', renderManagerService)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns editor render parts from render manager', () => {
|
||||
const document = { id: 'doc' };
|
||||
const docBackground = { id: 'bg' };
|
||||
const scene = { id: 'scene' };
|
||||
const engine = { id: 'engine' };
|
||||
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: () => ({
|
||||
mainComponent: document,
|
||||
scene,
|
||||
engine,
|
||||
components: new Map([[DOCS_VIEW_KEY.BACKGROUND, docBackground]]),
|
||||
}),
|
||||
} as unknown as IRenderManagerService;
|
||||
|
||||
const res = getEditorObject('u-1', renderManagerService);
|
||||
expect(res).toEqual({
|
||||
document,
|
||||
docBackground,
|
||||
scene,
|
||||
engine,
|
||||
});
|
||||
});
|
||||
});
|
||||
53
packages/sheets-ui/src/basics/editor/get-editor-object.ts
Normal file
53
packages/sheets-ui/src/basics/editor/get-editor-object.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { DocBackground, Documents, Engine, IRenderManagerService, Scene } from '@univerjs/engine-render';
|
||||
import { DOCS_VIEW_KEY } from '@univerjs/docs-ui';
|
||||
|
||||
export interface IDocObjectParam {
|
||||
document: Documents;
|
||||
docBackground: DocBackground;
|
||||
scene: Scene;
|
||||
engine: Engine;
|
||||
}
|
||||
|
||||
export function getEditorObject(
|
||||
unitId: Nullable<string>,
|
||||
renderManagerService: IRenderManagerService
|
||||
): Nullable<IDocObjectParam> {
|
||||
if (unitId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRender = renderManagerService.getRenderUnitById(unitId);
|
||||
|
||||
if (currentRender == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainComponent, scene, engine, components } = currentRender;
|
||||
|
||||
const document = mainComponent as Documents;
|
||||
const docBackground = components.get(DOCS_VIEW_KEY.BACKGROUND) as DocBackground;
|
||||
|
||||
return {
|
||||
document,
|
||||
docBackground,
|
||||
scene,
|
||||
engine,
|
||||
};
|
||||
}
|
||||
37
packages/sheets-ui/src/basics/editor/range.ts
Normal file
37
packages/sheets-ui/src/basics/editor/range.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { Editor, IRichTextEditorProps } from '@univerjs/docs-ui';
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
export interface IRangeSelectorInstance {
|
||||
editor: Nullable<Editor>;
|
||||
blur: () => void;
|
||||
focus: () => void;
|
||||
changePopupVisible: (visible: boolean) => void;
|
||||
verify: () => boolean;
|
||||
}
|
||||
|
||||
export interface IRangeSelectorProps extends IRichTextEditorProps {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
maxRangeCount?: number;
|
||||
supportAcrossSheet?: boolean;
|
||||
selectorRef?: RefObject<IRangeSelectorInstance>;
|
||||
onVerify?: (res: boolean, rangeText: string) => void;
|
||||
onRangeSelectorDialogVisibleChange?: (visible: boolean) => void;
|
||||
};
|
||||
@@ -0,0 +1,677 @@
|
||||
/**
|
||||
* 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 { Injector, IRange, Univer, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
IConfirmService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
TestConfirmService,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeAllCommand,
|
||||
AddWorksheetMergeCommand,
|
||||
AddWorksheetMergeHorizontalCommand,
|
||||
AddWorksheetMergeMutation,
|
||||
AddWorksheetMergeVerticalCommand,
|
||||
getClearContentMutationParamForRange,
|
||||
InsertColAfterCommand,
|
||||
InsertColBeforeCommand,
|
||||
InsertColByRangeCommand,
|
||||
InsertColCommand,
|
||||
InsertColMutation,
|
||||
InsertRowAfterCommand,
|
||||
InsertRowBeforeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
InsertRowCommand,
|
||||
InsertRowMutation,
|
||||
MergeCellController,
|
||||
MoveColsCommand,
|
||||
MoveColsMutation,
|
||||
MoveRangeMutation,
|
||||
RefRangeService,
|
||||
RemoveColByRangeCommand,
|
||||
RemoveColCommand,
|
||||
RemoveColMutation,
|
||||
RemoveRowByRangeCommand,
|
||||
RemoveRowCommand,
|
||||
RemoveRowMutation,
|
||||
RemoveWorksheetMergeCommand,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
describe('Test add worksheet merge commands', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let selectionManager: SheetsSelectionsService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[IConfirmService, { useClass: TestConfirmService }],
|
||||
[RefRangeService],
|
||||
[MergeCellController],
|
||||
]);
|
||||
univer = testBed.univer;
|
||||
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(AddWorksheetMergeCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeAllCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeVerticalCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeHorizontalCommand);
|
||||
commandService.registerCommand(RemoveWorksheetMergeCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
|
||||
[
|
||||
RemoveColByRangeCommand,
|
||||
RemoveRowByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
InsertColByRangeCommand,
|
||||
].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
|
||||
[
|
||||
InsertRowCommand,
|
||||
InsertRowBeforeCommand,
|
||||
InsertRowAfterCommand,
|
||||
InsertColAfterCommand,
|
||||
InsertColBeforeCommand,
|
||||
InsertColCommand,
|
||||
RemoveRowCommand,
|
||||
RemoveColCommand,
|
||||
MoveColsCommand,
|
||||
|
||||
InsertColMutation,
|
||||
InsertRowMutation,
|
||||
RemoveRowMutation,
|
||||
RemoveColMutation,
|
||||
MoveRangeMutation,
|
||||
MoveColsMutation,
|
||||
|
||||
SetSelectionsOperation,
|
||||
].forEach((c) => commandService.registerCommand(c));
|
||||
|
||||
get(LocaleService).load({});
|
||||
get(MergeCellController);
|
||||
|
||||
selectionManager = get(SheetsSelectionsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// univer.dispose();
|
||||
});
|
||||
|
||||
describe('add merge all', () => {
|
||||
describe('correct situations', () => {
|
||||
it('will merge all cells of the selected range when there is a selected range', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 10, startColumn: 10, endColumn: 10, endRow: 10, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getMerge(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getConfig()
|
||||
.mergeData;
|
||||
}
|
||||
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
|
||||
expect(getMerge()).toStrictEqual([
|
||||
{ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
]);
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getMerge()).toStrictEqual([
|
||||
{ startRow: 0, startColumn: 0, endRow: 5, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fault situations', () => {
|
||||
it('will not apply when there is no selected ranges', async () => {
|
||||
const result = await commandService.executeCommand(AddWorksheetMergeAllCommand.id);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('add merge vertical', () => {
|
||||
describe('correct situations', () => {
|
||||
it('will merge all vertical cells of the selected range when there is a selected range', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 10, startColumn: 10, endColumn: 15, endRow: 10, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getMerge(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getConfig()
|
||||
.mergeData;
|
||||
}
|
||||
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeVerticalCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(6);
|
||||
expect(getMerge()?.[0]).toStrictEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
endRow: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fault situations', () => {
|
||||
it('will not apply when there is no selected ranges', async () => {
|
||||
const result = await commandService.executeCommand(AddWorksheetMergeVerticalCommand.id);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('add merge horizontal', () => {
|
||||
describe('correct situations', () => {
|
||||
it('will merge all horizontal cells of the selected range when there is a selected range', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 10, startColumn: 10, endColumn: 10, endRow: 15, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getMerge(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getConfig()
|
||||
.mergeData;
|
||||
}
|
||||
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeHorizontalCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(6);
|
||||
expect(getMerge()?.[0]).toStrictEqual({ startRow: 0, startColumn: 0, endColumn: 5, endRow: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('fault situations', () => {
|
||||
it('will not apply when there is no selected ranges', async () => {
|
||||
const result = await commandService.executeCommand(AddWorksheetMergeHorizontalCommand.id);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizontal with other merge cell', () => {
|
||||
it('test only the upper left conner has a value', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 10, startColumn: 2, endColumn: 2, endRow: 12, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getMerge(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getConfig()
|
||||
.mergeData;
|
||||
}
|
||||
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(1);
|
||||
commandService.executeCommand(SetRangeValuesMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
cellValue: {
|
||||
10: {
|
||||
2: {
|
||||
v: 1234,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
selectionManager.clear();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 10, startColumn: 1, endColumn: 2, endRow: 12, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeHorizontalCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(3);
|
||||
|
||||
const worksheet = get(IUniverInstanceService).getUnit<Workbook>('test')?.getSheetBySheetId('sheet1');
|
||||
if (worksheet) {
|
||||
expect(worksheet.getCellMatrix().getValue(10, 1)?.v).toBe(1234);
|
||||
expect(worksheet.getCellMatrix().getValue(10, 2)?.v).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel merge', () => {
|
||||
describe('correct situations', () => {
|
||||
it('will cancel the merge of the selected range when there is a selected range', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getMerge(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getConfig()
|
||||
.mergeData;
|
||||
}
|
||||
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeHorizontalCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(6);
|
||||
expect(getMerge()?.[0]).toStrictEqual({ startRow: 0, startColumn: 0, endColumn: 5, endRow: 0 });
|
||||
expect(await commandService.executeCommand(RemoveWorksheetMergeCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(6);
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getMerge()?.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fault situations', () => {
|
||||
it('will not apply when there is no selected ranges', async () => {
|
||||
const result = await commandService.executeCommand(RemoveWorksheetMergeCommand.id);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge cell with set cell data', () => {
|
||||
it('test clear util', () => {
|
||||
const commandService = get(ICommandService);
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) throw new Error('No active sheet found');
|
||||
|
||||
commandService.executeCommand(SetRangeValuesMutation.id, {
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
cellValue: {
|
||||
2: {
|
||||
2: {
|
||||
p: {
|
||||
id: '__INTERNAL_EDITOR__DOCS_NORMAL',
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 82.20805358886719,
|
||||
height: null,
|
||||
},
|
||||
marginTop: 0,
|
||||
marginBottom: 1,
|
||||
marginRight: 2,
|
||||
marginLeft: 2,
|
||||
renderConfig: {
|
||||
horizontalAlign: 0,
|
||||
verticalAlign: 0,
|
||||
centerAngle: 0,
|
||||
vertexAngle: 0,
|
||||
wrapStrategy: 0,
|
||||
},
|
||||
},
|
||||
body: {
|
||||
dataStream: 'hahaha\r\n',
|
||||
textRuns: [
|
||||
{
|
||||
st: 3,
|
||||
ed: 5,
|
||||
ts: {
|
||||
fs: 28,
|
||||
},
|
||||
},
|
||||
],
|
||||
paragraphs: [
|
||||
{ paragraphId: 'para_sheets_ui_fixture_1', startIndex: 6, paragraphStyle: {
|
||||
horizontalAlign: 0,
|
||||
} },
|
||||
],
|
||||
sectionBreaks: [
|
||||
{
|
||||
sectionId: 'section_fixture_1106',
|
||||
startIndex: 7,
|
||||
},
|
||||
],
|
||||
customRanges: [],
|
||||
customDecorations: [],
|
||||
},
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
settings: {
|
||||
zoomRatio: 1,
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_THREAD_COMMENT_PLUGIN',
|
||||
data: '{}',
|
||||
},
|
||||
{
|
||||
name: 'SHEET_AuthzIoMockService_PLUGIN',
|
||||
data: '{}',
|
||||
},
|
||||
],
|
||||
},
|
||||
v: null,
|
||||
f: null,
|
||||
si: null,
|
||||
custom: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const res = getClearContentMutationParamForRange(worksheet, { startRow: 0, startColumn: 2, endRow: 2, endColumn: 2 });
|
||||
expect(res.getValue(0, 2)?.p).toBeTruthy();
|
||||
expect(res.getValue(2, 2)).toBeNull();
|
||||
});
|
||||
|
||||
it('test merge range first cell is blank, second cell is percent number format', async () => {
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) throw new Error('No active sheet found');
|
||||
|
||||
expect(worksheet.getCellRaw(20, 0)).toBeUndefined();
|
||||
expect(worksheet.getCellRaw(20, 1)).toStrictEqual({
|
||||
v: 2,
|
||||
t: 2,
|
||||
s: {
|
||||
n: {
|
||||
pattern: '0%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: {
|
||||
startRow: 20,
|
||||
startColumn: 0,
|
||||
endRow: 21,
|
||||
endColumn: 1,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
},
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
await commandService.executeCommand(AddWorksheetMergeAllCommand.id);
|
||||
|
||||
expect(worksheet.getCellRaw(20, 0)?.v).toStrictEqual(2);
|
||||
expect(worksheet.getCellStyle(20, 0)).toStrictEqual({
|
||||
n: {
|
||||
pattern: '0%',
|
||||
},
|
||||
});
|
||||
expect(worksheet.getCellStyle(20, 1)).toStrictEqual({
|
||||
n: {
|
||||
pattern: '0%',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with insert or remove col', () => {
|
||||
it('will adjust the merge data when insert col and remove col', async () => {
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 10, startColumn: 10, endColumn: 15, endRow: 15, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
const commandService = get(ICommandService);
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) throw new Error('No active sheet found');
|
||||
const mergeData = worksheet.getConfig().mergeData;
|
||||
expect(mergeData.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
|
||||
expect(mergeData.length).toBe(2);
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 7,
|
||||
endColumn: 7,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
expect(await commandService.executeCommand(InsertColAfterCommand.id)).toBeTruthy();
|
||||
expect(worksheet.getMergeData().length).toBe(2);
|
||||
expect(worksheet.getMergeData()[1]).toStrictEqual({ startRow: 10, startColumn: 11, endColumn: 16, endRow: 15, rangeType: 0 });
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 12,
|
||||
endColumn: 13,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
expect(await commandService.executeCommand(InsertColAfterCommand.id)).toBeTruthy();
|
||||
expect(worksheet.getMergeData().length).toBe(2);
|
||||
expect(worksheet.getMergeData()[1]).toStrictEqual({ startRow: 10, startColumn: 11, endColumn: 18, endRow: 15, rangeType: 0 });
|
||||
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 7,
|
||||
endColumn: 7,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
expect(await commandService.executeCommand(RemoveColCommand.id)).toBeTruthy();
|
||||
expect(worksheet.getMergeData().length).toBe(2);
|
||||
expect(worksheet.getMergeData()[1]).toStrictEqual({ startRow: 10, startColumn: 10, endColumn: 17, endRow: 15, rangeType: 0 });
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 12,
|
||||
endColumn: 13,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
},
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
expect(await commandService.executeCommand(RemoveColCommand.id)).toBeTruthy();
|
||||
expect(worksheet.getMergeData().length).toBe(2);
|
||||
expect(worksheet.getMergeData()[1]).toStrictEqual({ startRow: 10, startColumn: 10, endColumn: 15, endRow: 15, rangeType: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with move col', () => {
|
||||
it('will adjust the merge data when move col', async () => {
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endColumn: 5, endRow: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 10, startColumn: 10, endColumn: 15, endRow: 15, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
const commandService = get(ICommandService);
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) throw new Error('No active sheet found');
|
||||
const mergeData = worksheet.getConfig().mergeData;
|
||||
expect(mergeData.length).toBe(0);
|
||||
expect(await commandService.executeCommand(AddWorksheetMergeAllCommand.id)).toBeTruthy();
|
||||
expect(mergeData.length).toBe(2);
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([{
|
||||
primary: null,
|
||||
style: null,
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 18,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: 18,
|
||||
rangeType: 2,
|
||||
},
|
||||
}]);
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 18,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: 18,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 7,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: 7,
|
||||
rangeType: 2,
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(worksheet.getMergeData().length).toBe(2);
|
||||
expect(worksheet.getMergeData()[1]).toStrictEqual({ startRow: 10, startColumn: 11, endColumn: 16, endRow: 15, rangeType: 0 });
|
||||
|
||||
selectionManager.clearCurrentSelections();
|
||||
selectionManager.addSelections([{
|
||||
primary: null,
|
||||
style: null,
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 6,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: 8,
|
||||
rangeType: 2,
|
||||
},
|
||||
}]);
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 6,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: 8,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 17,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: 19,
|
||||
rangeType: 2,
|
||||
},
|
||||
})).toBeTruthy();
|
||||
expect(worksheet.getMergeData().length).toBe(2);
|
||||
expect(worksheet.getMergeData()[1]).toStrictEqual({ startRow: 10, startColumn: 8, endColumn: 13, endRow: 15, rangeType: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,960 @@
|
||||
/**
|
||||
* 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 { ICellData, Injector, IRange, IStyleData, Nullable, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
CellValueType,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
RedoCommand,
|
||||
set,
|
||||
ThemeService,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { EditorService, IEditorService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
AddWorksheetMergeMutation,
|
||||
AUTO_FILL_APPLY_TYPE,
|
||||
AutoClearContentCommand,
|
||||
AutoFillCommand,
|
||||
AutoFillController,
|
||||
AutoFillService,
|
||||
IAutoFillService,
|
||||
RefillCommand,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { IPlatformService, IShortcutService, PlatformService, ShortcutService } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AutoFillRenderController, AutoFillUIController, detectAutoFillRange } from '../../../controllers/auto-fill-ui.controller';
|
||||
import { createRenderTestBed } from '../../../controllers/render-controllers/__tests__/render-test-bed';
|
||||
import { EditorBridgeService, IEditorBridgeService } from '../../../services/editor-bridge.service';
|
||||
import { ISheetSelectionRenderService } from '../../../services/selection/base-selection-render.service';
|
||||
import { SheetSelectionRenderService } from '../../../services/selection/selection-render.service';
|
||||
import { SheetSkeletonManagerService } from '../../../services/sheet-skeleton-manager.service';
|
||||
import { SheetsRenderService } from '../../../services/sheets-render.service';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
class mockSheetsRenderService {
|
||||
registerSkeletonChangingMutations(_id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('AutoFillRenderController', () => {
|
||||
it('routes fill events to the workbook owned by the render context', () => {
|
||||
const selectionFilled$ = new Subject<IRange | null>();
|
||||
const getSelectionControls = vi.fn(() => [{
|
||||
model: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
},
|
||||
selectionFilled$,
|
||||
fillControl: {
|
||||
onDblclick$: {
|
||||
subscribeEvent: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
},
|
||||
onPointerDown$: {
|
||||
subscribeEvent: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
},
|
||||
},
|
||||
}]);
|
||||
const testBed = createRenderTestBed({
|
||||
dependencies: [
|
||||
[ISheetSelectionRenderService, { useValue: { getSelectionControls } }],
|
||||
[IEditorBridgeService, {
|
||||
useValue: {
|
||||
isVisible: () => ({ visible: false }),
|
||||
},
|
||||
}],
|
||||
],
|
||||
});
|
||||
const executeCommand = vi.spyOn(testBed.commandService, 'executeCommand').mockResolvedValue(true);
|
||||
const controller = testBed.injector.createInstance(AutoFillRenderController, testBed.context);
|
||||
|
||||
selectionFilled$.next({
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 2,
|
||||
endRow: 5,
|
||||
});
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 2,
|
||||
endRow: 5,
|
||||
},
|
||||
unitId: testBed.context.unitId,
|
||||
subUnitId: testBed.sheet.getActiveSheet().getSheetId(),
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
const TEST_WORKBOOK_DATA = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
// Single Number
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
// Single Number. descending
|
||||
1: {
|
||||
0: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
// Extend Number
|
||||
2: {
|
||||
0: {
|
||||
v: '第 1',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
1: {
|
||||
v: '第 2',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
// Chinese Number
|
||||
3: {
|
||||
0: {
|
||||
v: '一',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
1: {
|
||||
v: '三',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
// Chinese Week
|
||||
4: {
|
||||
0: {
|
||||
v: '星期一',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
1: {
|
||||
v: '星期三',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
// Loop Series
|
||||
5: {
|
||||
0: {
|
||||
v: '甲',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
1: {
|
||||
v: '乙',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
// Other String
|
||||
6: {
|
||||
0: {
|
||||
v: 'copy only',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
1: {
|
||||
v: 'no series',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
// Mixed Mode
|
||||
7: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
2: {
|
||||
v: '第 1',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
3: {
|
||||
v: '第 2',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
// db click to fill
|
||||
10: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
1: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
2: {
|
||||
v: 3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
3: {
|
||||
v: 4,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
11: {
|
||||
0: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
2: {
|
||||
v: 3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
12: {
|
||||
0: {
|
||||
v: 3,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
13: {
|
||||
0: {
|
||||
v: 4,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
},
|
||||
// left direction & equal ratio & styles
|
||||
14: {
|
||||
2: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
s: '_aRLOe',
|
||||
},
|
||||
3: {
|
||||
v: 4,
|
||||
t: CellValueType.NUMBER,
|
||||
},
|
||||
4: {
|
||||
v: 8,
|
||||
t: CellValueType.NUMBER,
|
||||
s: '3UpAbI',
|
||||
},
|
||||
},
|
||||
16: {
|
||||
2: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
s: '_aRLOe',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [
|
||||
{
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
startColumn: 5,
|
||||
endColumn: 7,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
createdTime: '',
|
||||
creator: '',
|
||||
lastModifiedBy: '',
|
||||
locale: LocaleType.ZH_CN,
|
||||
modifiedTime: '',
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {
|
||||
_aRLOe: {
|
||||
bg: {
|
||||
rgb: '#ccc',
|
||||
},
|
||||
ht: 2,
|
||||
vt: 2,
|
||||
},
|
||||
'3UpAbI': {
|
||||
bg: {
|
||||
rgb: '#eee',
|
||||
},
|
||||
},
|
||||
},
|
||||
timeZone: '',
|
||||
};
|
||||
|
||||
describe('Test auto fill rules in controller', () => {
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let themeService: ThemeService;
|
||||
|
||||
let getValues: (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
) => Array<Array<Nullable<ICellData>>> | undefined;
|
||||
|
||||
let getStyles: (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
) => Array<Array<Nullable<IStyleData>>> | undefined;
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA, [
|
||||
[DocSelectionManagerService],
|
||||
[ISheetSelectionRenderService, { useClass: SheetSelectionRenderService }],
|
||||
[SheetSkeletonService],
|
||||
[IAutoFillService, { useClass: AutoFillService }],
|
||||
[IShortcutService, { useClass: ShortcutService }],
|
||||
[IPlatformService, { useClass: PlatformService }],
|
||||
[IEditorBridgeService, { useClass: EditorBridgeService }],
|
||||
[IEditorService, { useClass: EditorService }],
|
||||
[IRenderManagerService, { useClass: RenderManagerService }],
|
||||
[SheetsRenderService, { useClass: mockSheetsRenderService }],
|
||||
[SheetSkeletonManagerService],
|
||||
[AutoFillController],
|
||||
[AutoFillUIController],
|
||||
]);
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
themeService = get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
get(AutoFillUIController);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
commandService.registerCommand(RefillCommand);
|
||||
commandService.registerCommand(AutoClearContentCommand);
|
||||
commandService.registerCommand(AutoFillCommand);
|
||||
|
||||
getValues = (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
): Array<Array<Nullable<ICellData>>> | undefined =>
|
||||
get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(startRow, startColumn, endRow, endColumn)
|
||||
.getValues();
|
||||
|
||||
getStyles = (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number
|
||||
): Array<Array<Nullable<IStyleData>>> | undefined => {
|
||||
const values = getValues(startRow, startColumn, endRow, endColumn);
|
||||
const styles = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)?.getStyles();
|
||||
if (values && styles) {
|
||||
return values.map((row) => row.map((cell) => styles.getStyleByCell(cell)));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('auto fill', () => {
|
||||
describe('auto fill the numbers', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test number
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 1,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 3,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(0, 2)?.v).toBe(3);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(0, 3)?.v).toBe(4);
|
||||
// test number. descending
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 3,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(1, 2)?.v).toBe(0);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(1, 3)?.v).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the extend numbers', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test extend number
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 2,
|
||||
startColumn: 0,
|
||||
endRow: 2,
|
||||
endColumn: 1,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 2,
|
||||
startColumn: 0,
|
||||
endRow: 2,
|
||||
endColumn: 3,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(2, 2)?.v).toBe('第 3');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(2, 3)?.v).toBe('第 4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the chinese numbers', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test chinese number
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 3,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 3,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 4,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(3, 3)?.v).toBe('五');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(3, 4)?.v).toBe('日');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the chinese week', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
|
||||
// test chinese week
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 4,
|
||||
startColumn: 0,
|
||||
endRow: 4,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 4,
|
||||
startColumn: 0,
|
||||
endRow: 4,
|
||||
endColumn: 4,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(4, 3)?.v).toBe('星期五');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(4, 4)?.v).toBe('星期日');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the loop series', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test loop series
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 5,
|
||||
startColumn: 0,
|
||||
endRow: 5,
|
||||
endColumn: 1,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 5,
|
||||
startColumn: 0,
|
||||
endRow: 5,
|
||||
endColumn: 3,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(5, 2)?.v).toBe('丙');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(5, 3)?.v).toBe('丁');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the other string', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test other string
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 6,
|
||||
startColumn: 0,
|
||||
endRow: 6,
|
||||
endColumn: 1,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 6,
|
||||
startColumn: 0,
|
||||
endRow: 6,
|
||||
endColumn: 3,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(6, 2)?.v).toBe('copy only');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(6, 3)?.v).toBe('no series');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the mixed mode', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test mixed mode
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 7,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 3,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 7,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 7,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 4)?.v).toBe(3);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 5)?.v).toBe(4);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 6)?.v).toBe('第 3');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 7)?.v).toBe('第 4');
|
||||
|
||||
// undo redo
|
||||
await commandService.executeCommand(UndoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 4)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 5)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 6)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 7)?.v).toBe(undefined);
|
||||
|
||||
await commandService.executeCommand(RedoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 4)?.v).toBe(3);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 5)?.v).toBe(4);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 6)?.v).toBe('第 3');
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(7, 7)?.v).toBe('第 4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill the merged cell', async () => {
|
||||
it('test primary', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
|
||||
const selectionManagerService = get(SheetsSelectionsService);
|
||||
selectionManagerService.addSelections([{
|
||||
style: null,
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 5,
|
||||
endRow: 2,
|
||||
endColumn: 7,
|
||||
},
|
||||
primary: {
|
||||
startRow: 0,
|
||||
startColumn: 5,
|
||||
endRow: 2,
|
||||
endColumn: 7,
|
||||
actualColumn: 5,
|
||||
actualRow: 0,
|
||||
isMerged: true,
|
||||
isMergedMainCell: true,
|
||||
},
|
||||
}]);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 0,
|
||||
startColumn: 5,
|
||||
endRow: 2,
|
||||
endColumn: 7,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 0,
|
||||
startColumn: 5,
|
||||
endRow: 5,
|
||||
endColumn: 7,
|
||||
},
|
||||
});
|
||||
expect(workbook.getActiveSheet()?.getMergeData().length).toBe(2);
|
||||
expect(selectionManagerService.getCurrentLastSelection()?.primary).toEqual(expect.objectContaining({
|
||||
startRow: 0,
|
||||
startColumn: 5,
|
||||
endRow: 2,
|
||||
endColumn: 7,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill clear', async () => {
|
||||
it('test primary will move to upper left corner', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
|
||||
const selectionManagerService = get(SheetsSelectionsService);
|
||||
selectionManagerService.addSelections([{
|
||||
style: null,
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 5,
|
||||
endColumn: 2,
|
||||
},
|
||||
// primary not in the upper left corner, but after fill clear will move to upper left corner
|
||||
primary: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 0,
|
||||
actualColumn: 0,
|
||||
actualRow: 1,
|
||||
isMerged: true,
|
||||
isMergedMainCell: true,
|
||||
},
|
||||
}]);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 5,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 2,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(4, 0)?.v).toBeFalsy();
|
||||
expect(selectionManagerService.getCurrentLastSelection()?.primary).toEqual(expect.objectContaining({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 0,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill range is auto detected', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test other string
|
||||
const sourceRange: IRange = {
|
||||
startRow: 10,
|
||||
startColumn: 1,
|
||||
endRow: 10,
|
||||
endColumn: 1,
|
||||
};
|
||||
const worksheet = workbook.getSheetBySheetId('sheet1');
|
||||
if (!worksheet) throw new Error('Worksheet sheet1 does not exist');
|
||||
await commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange,
|
||||
targetRange: detectAutoFillRange(sourceRange, worksheet),
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(2);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(2);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(2);
|
||||
|
||||
// undo redo
|
||||
await commandService.executeCommand(UndoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(undefined);
|
||||
|
||||
await commandService.executeCommand(RedoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(11, 1)?.v).toBe(2);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(12, 1)?.v).toBe(2);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(13, 1)?.v).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill in left direction', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test other string
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 14,
|
||||
startColumn: 2,
|
||||
endRow: 14,
|
||||
endColumn: 4,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 14,
|
||||
startColumn: 0,
|
||||
endRow: 14,
|
||||
endColumn: 4,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 0)?.v).toBe(0.5);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 1)?.v).toBe(1);
|
||||
// undo redo
|
||||
await commandService.executeCommand(UndoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 0)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 1)?.v).toBe(undefined);
|
||||
|
||||
await commandService.executeCommand(RedoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 0)?.v).toBe(0.5);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 1)?.v).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill with equal ratio & style', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// equal ratio
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 14,
|
||||
startColumn: 2,
|
||||
endRow: 14,
|
||||
endColumn: 4,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 14,
|
||||
startColumn: 2,
|
||||
endRow: 14,
|
||||
endColumn: 7,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 5)?.v).toBe(16);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 6)?.v).toBe(32);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 7)?.v).toBe(64);
|
||||
const styles = getStyles(14, 5, 14, 7);
|
||||
expect(styles && styles[0][0]).toStrictEqual({
|
||||
bg: {
|
||||
rgb: '#ccc',
|
||||
},
|
||||
ht: 2,
|
||||
vt: 2,
|
||||
});
|
||||
expect(styles && styles[0][1]).toStrictEqual(undefined);
|
||||
expect(styles && styles[0][2]).toStrictEqual({
|
||||
bg: {
|
||||
rgb: '#eee',
|
||||
},
|
||||
});
|
||||
|
||||
// undo redo
|
||||
await commandService.executeCommand(UndoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 5)?.v).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 6)?.v).toBe(undefined);
|
||||
|
||||
await commandService.executeCommand(RedoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 5)?.v).toBe(16);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 6)?.v).toBe(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill without format', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// equal ratio
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 14,
|
||||
startColumn: 2,
|
||||
endRow: 14,
|
||||
endColumn: 3,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 14,
|
||||
startColumn: 2,
|
||||
endRow: 14,
|
||||
endColumn: 4,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 4)?.v).toBe(6);
|
||||
const styles = getStyles(14, 4, 14, 4);
|
||||
expect(styles && styles[0][0]).toStrictEqual({
|
||||
bg: {
|
||||
rgb: '#ccc',
|
||||
},
|
||||
ht: 2,
|
||||
vt: 2,
|
||||
});
|
||||
|
||||
commandService.executeCommand(RefillCommand.id, { type: AUTO_FILL_APPLY_TYPE.NO_FORMAT });
|
||||
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 4)?.v).toBe(6);
|
||||
const styles_refill = getStyles(14, 4, 14, 4);
|
||||
expect(styles_refill && styles_refill[0][0]).toStrictEqual({
|
||||
bg: {
|
||||
rgb: '#eee',
|
||||
},
|
||||
});
|
||||
|
||||
// undo redo
|
||||
await commandService.executeCommand(UndoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 4)?.v).toBe(8);
|
||||
const styles_undo = getStyles(14, 4, 14, 4);
|
||||
expect(styles_undo && styles_undo[0][0]).toStrictEqual({
|
||||
bg: {
|
||||
rgb: '#eee',
|
||||
},
|
||||
});
|
||||
|
||||
// redo
|
||||
await commandService.executeCommand(RedoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(14, 4)?.v).toBe(6);
|
||||
const styles_redo = getStyles(14, 4, 14, 4);
|
||||
expect(styles_redo && styles_redo[0][0]).toStrictEqual({
|
||||
bg: {
|
||||
rgb: '#eee',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto fill from single cell', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
// test right
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 16,
|
||||
startColumn: 2,
|
||||
endRow: 16,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 16,
|
||||
startColumn: 2,
|
||||
endRow: 16,
|
||||
endColumn: 4,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 3)?.v).toBe(2);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 4)?.v).toBe(2);
|
||||
// test left
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 16,
|
||||
startColumn: 2,
|
||||
endRow: 16,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 16,
|
||||
startColumn: 0,
|
||||
endRow: 16,
|
||||
endColumn: 2,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 1)?.v).toBe(2);
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(16, 0)?.v).toBe(2);
|
||||
// test up
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 16,
|
||||
startColumn: 2,
|
||||
endRow: 16,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 15,
|
||||
startColumn: 2,
|
||||
endRow: 16,
|
||||
endColumn: 2,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(15, 2)?.v).toBe(2);
|
||||
// test down
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startRow: 16,
|
||||
startColumn: 2,
|
||||
endRow: 16,
|
||||
endColumn: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startRow: 16,
|
||||
startColumn: 2,
|
||||
endRow: 17,
|
||||
endColumn: 2,
|
||||
},
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet1')?.getCell(17, 2)?.v).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 { IClipboardInterfaceService } from '@univerjs/ui';
|
||||
import { ICommandService, Injector, IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SetWorksheetActiveOperation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { expect, it, vi } from 'vitest';
|
||||
import { clipboardTestBed } from '../../../services/clipboard/__tests__/clipboard-test-bed';
|
||||
import { PREDEFINED_HOOK_NAME_PASTE } from '../../../services/clipboard/clipboard.service';
|
||||
import { SheetPasteCommand } from '../clipboard.command';
|
||||
|
||||
it.each([false, true])('silently cancels a delayed paste after target disposal (replacement: %s)', async (replace) => {
|
||||
let release = () => {};
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const getType = vi.fn(async () => new Blob(['stale paste'], { type: 'text/plain' }));
|
||||
const boundary: IClipboardInterfaceService = {
|
||||
supportClipboard: true,
|
||||
write: async () => {},
|
||||
writeText: async () => {},
|
||||
readText: async () => '',
|
||||
read: vi.fn(async () => {
|
||||
await gate;
|
||||
return [{ presentationStyle: 'unspecified' as const, types: ['text/plain'], getType }];
|
||||
}),
|
||||
};
|
||||
const { univer, get, sheet } = clipboardTestBed(undefined, undefined, boundary);
|
||||
try {
|
||||
const commands = get(ICommandService);
|
||||
for (const command of [SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetActiveOperation, SetWorksheetRowAutoHeightMutation]) {
|
||||
commands.registerCommand(command);
|
||||
}
|
||||
get(SheetsSelectionsService).setSelections('test', 'sheet1', [{
|
||||
range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
primary: null,
|
||||
style: null,
|
||||
}]);
|
||||
const snapshot = Tools.deepClone(sheet.getSnapshot());
|
||||
const pending = SheetPasteCommand.handler(get(Injector), { value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE });
|
||||
expect(boundary.read).toHaveBeenCalledOnce();
|
||||
expect(get(IUniverInstanceService).disposeUnit(sheet.getUnitId())).toBe(true);
|
||||
const replacement = replace
|
||||
? univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, snapshot)
|
||||
: null;
|
||||
const before = replacement ? JSON.stringify(replacement.getSnapshot()) : null;
|
||||
release();
|
||||
expect(await pending).toBe(false);
|
||||
expect(getType).not.toHaveBeenCalled();
|
||||
expect(replacement ? JSON.stringify(replacement.getSnapshot()) : null).toEqual(before);
|
||||
} finally {
|
||||
release();
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* 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 { IAccessor, IRange, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IClipboardInterfaceService } from '@univerjs/ui';
|
||||
import {
|
||||
ICommandService,
|
||||
IConfirmService,
|
||||
Injector,
|
||||
IPermissionService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
RedoCommand,
|
||||
UndoCommand,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeMutation,
|
||||
DeleteRangeMoveLeftCommand,
|
||||
DeleteRangeMoveUpCommand,
|
||||
InsertRangeMoveDownCommand,
|
||||
InsertRangeMoveRightCommand,
|
||||
MoveRangeMutation,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SetWorksheetActiveOperation,
|
||||
SetWorksheetColWidthMutation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SetWorksheetRowHeightMutation,
|
||||
SheetsSelectionsService,
|
||||
WorkbookCopyPermission,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetSetCellStylePermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
} from '@univerjs/sheets';
|
||||
import * as sheets from '@univerjs/sheets';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { clipboardTestBed } from '../../../services/clipboard/__tests__/clipboard-test-bed';
|
||||
import { ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE } from '../../../services/clipboard/clipboard.service';
|
||||
import {
|
||||
SheetCopyCommand,
|
||||
SheetCutCommand,
|
||||
SheetOptionalPasteCommand,
|
||||
SheetPasteBesidesBorderCommand,
|
||||
SheetPasteColWidthCommand,
|
||||
SheetPasteCommand,
|
||||
SheetPasteFormatCommand,
|
||||
SheetPasteShortKeyCommand,
|
||||
SheetPasteValueCommand,
|
||||
} from '../clipboard.command';
|
||||
import { DeleteRangeMoveLeftConfirmCommand } from '../delete-range-move-left-confirm.command';
|
||||
import { DeleteRangeMoveUpConfirmCommand } from '../delete-range-move-up-confirm.command';
|
||||
import { InsertRangeMoveDownConfirmCommand } from '../insert-range-move-down-confirm.command';
|
||||
import { InsertRangeMoveRightConfirmCommand } from '../insert-range-move-right-confirm.command';
|
||||
|
||||
function createAccessor(pairs: Array<[unknown, unknown]>): IAccessor {
|
||||
const map = new Map<unknown, unknown>(pairs);
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (!map.has(token)) {
|
||||
throw new Error(`Unknown token: ${String(token)}`);
|
||||
}
|
||||
return map.get(token);
|
||||
},
|
||||
} as unknown as IAccessor;
|
||||
}
|
||||
|
||||
function createWorksheet(options?: {
|
||||
rowCount?: number;
|
||||
colCount?: number;
|
||||
filteredRows?: number[];
|
||||
merges?: IRange[];
|
||||
}): Worksheet {
|
||||
const filtered = new Set(options?.filteredRows ?? []);
|
||||
return {
|
||||
getRowCount: () => options?.rowCount ?? 10,
|
||||
getColumnCount: () => options?.colCount ?? 8,
|
||||
getRowFiltered: (row: number) => filtered.has(row),
|
||||
getMergeData: () => options?.merges ?? [],
|
||||
} as unknown as Worksheet;
|
||||
}
|
||||
|
||||
function mockSheetCommandTarget(worksheet: Worksheet): void {
|
||||
vi.spyOn(sheets, 'getSheetCommandTarget').mockReturnValue({
|
||||
workbook: {} as unknown as Workbook,
|
||||
worksheet,
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
});
|
||||
}
|
||||
|
||||
function createSelectionService(range?: IRange | null) {
|
||||
return {
|
||||
getCurrentSelections: () => (range ? [{ range }] : range === null ? null : [{ range: undefined }]),
|
||||
};
|
||||
}
|
||||
|
||||
describe('insert/delete range confirm commands', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('InsertRangeMoveDownConfirm handles filtered rows, merge confirm and direct execute', async () => {
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const confirm = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
const locale = { t: (key: string) => key };
|
||||
const rowSelection = { startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 } as IRange;
|
||||
|
||||
const filteredWorksheet = createWorksheet({ filteredRows: [2] });
|
||||
mockSheetCommandTarget(filteredWorksheet);
|
||||
const accessorFiltered = createAccessor([
|
||||
[IConfirmService, { confirm }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[LocaleService, locale],
|
||||
[SheetsSelectionsService, createSelectionService(rowSelection)],
|
||||
[IUniverInstanceService, {}],
|
||||
]);
|
||||
expect(await InsertRangeMoveDownConfirmCommand.handler(accessorFiltered)).toBe(false);
|
||||
expect(executeCommand).not.toHaveBeenCalled();
|
||||
|
||||
const mergeWorksheet = createWorksheet({
|
||||
merges: [{ startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 } as IRange],
|
||||
});
|
||||
mockSheetCommandTarget(mergeWorksheet);
|
||||
const accessorMerge = createAccessor([
|
||||
[IConfirmService, { confirm }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[LocaleService, locale],
|
||||
[SheetsSelectionsService, createSelectionService(rowSelection)],
|
||||
[IUniverInstanceService, {}],
|
||||
]);
|
||||
|
||||
expect(await InsertRangeMoveDownConfirmCommand.handler(accessorMerge)).toBe(true);
|
||||
expect(await InsertRangeMoveDownConfirmCommand.handler(accessorMerge)).toBe(true);
|
||||
expect(executeCommand).toHaveBeenCalledWith(InsertRangeMoveDownCommand.id);
|
||||
|
||||
const noMergeWorksheet = createWorksheet();
|
||||
mockSheetCommandTarget(noMergeWorksheet);
|
||||
expect(await InsertRangeMoveDownConfirmCommand.handler(accessorMerge)).toBe(true);
|
||||
expect(executeCommand).toHaveBeenCalledWith(InsertRangeMoveDownCommand.id);
|
||||
});
|
||||
|
||||
it('InsertRangeMoveRightConfirm executes directly and handles merge confirm', async () => {
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const confirm = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
const locale = { t: (key: string) => key };
|
||||
const range = { startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 } as IRange;
|
||||
const accessor = createAccessor([
|
||||
[IConfirmService, { confirm }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[LocaleService, locale],
|
||||
[SheetsSelectionsService, createSelectionService(range)],
|
||||
[IUniverInstanceService, {}],
|
||||
]);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet());
|
||||
expect(await InsertRangeMoveRightConfirmCommand.handler(accessor)).toBe(true);
|
||||
expect(executeCommand).toHaveBeenCalledWith(InsertRangeMoveRightCommand.id);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet({
|
||||
merges: [{ startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 } as IRange],
|
||||
}));
|
||||
expect(await InsertRangeMoveRightConfirmCommand.handler(accessor)).toBe(true);
|
||||
expect(await InsertRangeMoveRightConfirmCommand.handler(accessor)).toBe(true);
|
||||
});
|
||||
|
||||
it('DeleteRangeMoveUpConfirm handles filter/merge/no-merge branches', async () => {
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const confirm = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
const locale = { t: (key: string) => key };
|
||||
const range = { startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 } as IRange;
|
||||
const accessor = createAccessor([
|
||||
[IConfirmService, { confirm }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[LocaleService, locale],
|
||||
[SheetsSelectionsService, createSelectionService(range)],
|
||||
[IUniverInstanceService, {}],
|
||||
]);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet({ filteredRows: [2] }));
|
||||
expect(await DeleteRangeMoveUpConfirmCommand.handler(accessor)).toBe(false);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet({
|
||||
merges: [{ startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 } as IRange],
|
||||
}));
|
||||
expect(await DeleteRangeMoveUpConfirmCommand.handler(accessor)).toBe(true);
|
||||
expect(await DeleteRangeMoveUpConfirmCommand.handler(accessor)).toBe(true);
|
||||
expect(executeCommand).toHaveBeenCalledWith(DeleteRangeMoveUpCommand.id);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet());
|
||||
expect(await DeleteRangeMoveUpConfirmCommand.handler(accessor)).toBe(true);
|
||||
});
|
||||
|
||||
it('DeleteRangeMoveLeftConfirm handles merge/no-merge and guard branches', async () => {
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const confirm = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
const locale = { t: (key: string) => key };
|
||||
const range = { startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 } as IRange;
|
||||
const accessor = createAccessor([
|
||||
[IConfirmService, { confirm }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[LocaleService, locale],
|
||||
[SheetsSelectionsService, createSelectionService(range)],
|
||||
[IUniverInstanceService, {}],
|
||||
]);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet({
|
||||
merges: [{ startRow: 0, endRow: 3, startColumn: 0, endColumn: 2 } as IRange],
|
||||
}));
|
||||
expect(await DeleteRangeMoveLeftConfirmCommand.handler(accessor)).toBe(true);
|
||||
expect(await DeleteRangeMoveLeftConfirmCommand.handler(accessor)).toBe(true);
|
||||
|
||||
mockSheetCommandTarget(createWorksheet());
|
||||
expect(await DeleteRangeMoveLeftConfirmCommand.handler(accessor)).toBe(true);
|
||||
expect(executeCommand).toHaveBeenCalledWith(DeleteRangeMoveLeftCommand.id);
|
||||
|
||||
const noSelectionAccessor = createAccessor([
|
||||
[IConfirmService, { confirm }],
|
||||
[ICommandService, { executeCommand }],
|
||||
[LocaleService, locale],
|
||||
[SheetsSelectionsService, createSelectionService(null)],
|
||||
[IUniverInstanceService, {}],
|
||||
]);
|
||||
expect(await DeleteRangeMoveLeftConfirmCommand.handler(noSelectionAccessor)).toBe(false);
|
||||
|
||||
vi.spyOn(sheets, 'getSheetCommandTarget').mockReturnValue(null);
|
||||
expect(await DeleteRangeMoveLeftConfirmCommand.handler(accessor)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard command branches', () => {
|
||||
it.each(['copy', 'cut'] as const)('uses the internal %s cache without system clipboard support and restores history', async (mode) => {
|
||||
const boundary: IClipboardInterfaceService = {
|
||||
supportClipboard: false,
|
||||
write: vi.fn(async () => {}),
|
||||
writeText: vi.fn(async () => {}),
|
||||
readText: vi.fn(async () => ''),
|
||||
read: vi.fn(async () => []),
|
||||
};
|
||||
const { univer, get, sheet } = clipboardTestBed(undefined, undefined, boundary);
|
||||
try {
|
||||
const accessor = get(Injector);
|
||||
const commands = get(ICommandService);
|
||||
for (const command of [
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SetWorksheetActiveOperation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SetWorksheetRowHeightMutation,
|
||||
SetWorksheetColWidthMutation,
|
||||
AddWorksheetMergeMutation,
|
||||
RemoveWorksheetMergeMutation,
|
||||
MoveRangeMutation,
|
||||
]) {
|
||||
commands.registerCommand(command);
|
||||
}
|
||||
const select = (column: number) => get(SheetsSelectionsService).setSelections('test', 'sheet1', [{
|
||||
range: { startRow: 0, endRow: 0, startColumn: column, endColumn: column },
|
||||
primary: null,
|
||||
style: null,
|
||||
}]);
|
||||
select(0);
|
||||
expect(await SheetPasteCommand.handler(accessor, { value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE })).toBe(false);
|
||||
commands.syncExecuteCommand(SetRangeValuesMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
cellValue: { 0: { 0: { v: 'cached-value', s: { bl: 1 } } } },
|
||||
});
|
||||
const copyCommand = mode === 'copy' ? SheetCopyCommand : SheetCutCommand;
|
||||
expect(await copyCommand.handler(accessor)).toBe(true);
|
||||
select(1);
|
||||
expect(await SheetPasteCommand.handler(accessor, { value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE })).toBe(true);
|
||||
const worksheet = sheet.getSheetBySheetId('sheet1')!;
|
||||
expect(worksheet.getCell(0, 1)?.v).toBe('cached-value');
|
||||
expect(worksheet.getCellStyle(0, 1)?.bl).toBe(1);
|
||||
expect(worksheet.getCell(0, 0)?.v).toBe(mode === 'copy' ? 'cached-value' : undefined);
|
||||
if (mode === 'copy') {
|
||||
expect(await SheetOptionalPasteCommand.handler(accessor, { type: 'SPECIAL_PASTE_VALUE' })).toBe(true);
|
||||
expect(worksheet.getCell(0, 1)?.v).toBe('cached-value');
|
||||
expect(worksheet.getCellStyle(0, 1)?.bl).not.toBe(1);
|
||||
}
|
||||
expect(await commands.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(worksheet.getCell(0, 0)?.v).toBe('cached-value');
|
||||
expect(worksheet.getCell(0, 1)?.v).toBeUndefined();
|
||||
expect(await commands.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(worksheet.getCell(0, 1)?.v).toBe('cached-value');
|
||||
expect(boundary.read).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps menu paste data and history on the selection captured before the system read', async () => {
|
||||
let releaseRead = () => {};
|
||||
const readGate = new Promise<void>((resolve) => {
|
||||
releaseRead = resolve;
|
||||
});
|
||||
const boundary: IClipboardInterfaceService = {
|
||||
supportClipboard: true,
|
||||
write: vi.fn(async () => {}),
|
||||
writeText: vi.fn(async () => {}),
|
||||
readText: async () => 'system-value',
|
||||
read: async () => {
|
||||
await readGate;
|
||||
return [{
|
||||
presentationStyle: 'unspecified',
|
||||
types: ['text/plain'],
|
||||
getType: async () => new Blob(['system-value'], { type: 'text/plain' }),
|
||||
}];
|
||||
},
|
||||
};
|
||||
const { univer, get, sheet } = clipboardTestBed(undefined, undefined, boundary);
|
||||
try {
|
||||
const accessor = get(Injector);
|
||||
const commands = get(ICommandService);
|
||||
for (const command of [SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetActiveOperation, SetWorksheetRowAutoHeightMutation]) {
|
||||
commands.registerCommand(command);
|
||||
}
|
||||
const select = (column: number) => get(SheetsSelectionsService).setSelections('test', 'sheet1', [{
|
||||
range: { startRow: 0, endRow: 0, startColumn: column, endColumn: column },
|
||||
primary: null,
|
||||
style: null,
|
||||
}]);
|
||||
select(0);
|
||||
expect(await SheetPasteShortKeyCommand.handler(accessor, { textContent: 'source-value' })).toBe(true);
|
||||
expect(await SheetCopyCommand.handler(accessor)).toBe(true);
|
||||
expect(boundary.write).toHaveBeenCalled();
|
||||
expect(await SheetCutCommand.handler(accessor)).toBe(true);
|
||||
select(1);
|
||||
const pendingPaste = SheetPasteCommand.handler(accessor, { value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE });
|
||||
select(2);
|
||||
releaseRead();
|
||||
expect(await pendingPaste).toBe(true);
|
||||
const worksheet = sheet.getSheetBySheetId('sheet1')!;
|
||||
expect(worksheet.getCell(0, 1)?.v).toBe('system-value');
|
||||
expect(worksheet.getCell(0, 0)?.v).toBe('source-value');
|
||||
expect(worksheet.getCell(0, 2)?.v).toBeUndefined();
|
||||
expect(await commands.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(worksheet.getCell(0, 1)?.v).toBeUndefined();
|
||||
} finally {
|
||||
releaseRead();
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: 'cell value', PermissionPoint: WorksheetSetCellValuePermission },
|
||||
{ name: 'cell style', PermissionPoint: WorksheetSetCellStylePermission },
|
||||
])('rejects menu paste when $name permission is revoked during the system read', async ({ PermissionPoint }) => {
|
||||
let releaseRead = () => {};
|
||||
const readGate = new Promise<void>((resolve) => {
|
||||
releaseRead = resolve;
|
||||
});
|
||||
const getType = vi.fn(async () => new Blob(['forbidden'], { type: 'text/plain' }));
|
||||
const boundary: IClipboardInterfaceService = {
|
||||
supportClipboard: true,
|
||||
write: vi.fn(async () => {}),
|
||||
writeText: vi.fn(async () => {}),
|
||||
readText: vi.fn(async () => ''),
|
||||
read: vi.fn(async () => {
|
||||
await readGate;
|
||||
return [{ presentationStyle: 'unspecified' as const, types: ['text/plain'], getType }];
|
||||
}),
|
||||
};
|
||||
const { univer, get, sheet } = clipboardTestBed(undefined, undefined, boundary);
|
||||
try {
|
||||
get(SheetsSelectionsService).setSelections('test', 'sheet1', [{
|
||||
range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
primary: null,
|
||||
style: null,
|
||||
}]);
|
||||
const permissions = get(IPermissionService);
|
||||
const point = new PermissionPoint('test', 'sheet1');
|
||||
if (!permissions.getPermissionPoint(point.id)) {
|
||||
permissions.addPermissionPoint(point);
|
||||
}
|
||||
permissions.updatePermissionPoint(point.id, true);
|
||||
const before = JSON.stringify(sheet.getSnapshot());
|
||||
const history = get(IUndoRedoService);
|
||||
const beforeHistory = history.getUndoRedoStatus('test');
|
||||
const pendingPaste = SheetPasteCommand.handler(get(Injector), { value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE });
|
||||
expect(boundary.read).toHaveBeenCalledOnce();
|
||||
permissions.updatePermissionPoint(point.id, false);
|
||||
releaseRead();
|
||||
await expect(pendingPaste).rejects.toThrow('have no permission');
|
||||
expect(getType).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(sheet.getSnapshot())).toBe(before);
|
||||
expect(history.getUndoRedoStatus('test')).toEqual(beforeHistory);
|
||||
} finally {
|
||||
releaseRead();
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('checks real clipboard permissions without calling the system boundary or changing cells', async () => {
|
||||
const boundary: IClipboardInterfaceService = {
|
||||
supportClipboard: true,
|
||||
write: vi.fn(async () => {}),
|
||||
writeText: vi.fn(async () => {}),
|
||||
readText: vi.fn(async () => ''),
|
||||
read: vi.fn(async () => []),
|
||||
};
|
||||
const { univer, get, sheet } = clipboardTestBed(undefined, undefined, boundary);
|
||||
try {
|
||||
const accessor = get(Injector);
|
||||
get(ISheetClipboardService);
|
||||
get(SheetsSelectionsService).setSelections('test', 'sheet1', [{
|
||||
range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
primary: null,
|
||||
style: null,
|
||||
}]);
|
||||
const permissions = get(IPermissionService);
|
||||
for (const point of [new WorkbookCopyPermission('test'), new WorkbookEditablePermission('test')]) {
|
||||
if (!permissions.getPermissionPoint(point.id)) {
|
||||
permissions.addPermissionPoint(point);
|
||||
}
|
||||
permissions.updatePermissionPoint(point.id, false);
|
||||
}
|
||||
const before = JSON.stringify(sheet.getSnapshot());
|
||||
await expect(SheetCopyCommand.handler(accessor)).rejects.toThrow('have no permission');
|
||||
await expect(SheetCutCommand.handler(accessor)).rejects.toThrow('have no permission');
|
||||
await expect(SheetPasteCommand.handler(accessor, { value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE })).rejects.toThrow('have no permission');
|
||||
await expect(SheetPasteShortKeyCommand.handler(accessor, { textContent: 'forbidden' })).rejects.toThrow('have no permission');
|
||||
expect(boundary.read).not.toHaveBeenCalled();
|
||||
expect(boundary.write).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(sheet.getSnapshot())).toBe(before);
|
||||
} finally {
|
||||
univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('routes special paste commands to SheetPasteCommand', async () => {
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const accessor = createAccessor([
|
||||
[ICommandService, { executeCommand }],
|
||||
]);
|
||||
|
||||
expect(await SheetPasteValueCommand.handler(accessor)).toBe(true);
|
||||
expect(await SheetPasteFormatCommand.handler(accessor)).toBe(true);
|
||||
expect(await SheetPasteColWidthCommand.handler(accessor)).toBe(true);
|
||||
expect(await SheetPasteBesidesBorderCommand.handler(accessor)).toBe(true);
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
|
||||
});
|
||||
expect(executeCommand).toHaveBeenCalledWith(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
});
|
||||
expect(executeCommand).toHaveBeenCalledWith(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
});
|
||||
expect(executeCommand).toHaveBeenCalledWith(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_BESIDES_BORDER,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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 { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext } from '@univerjs/engine-render';
|
||||
import {
|
||||
BooleanNumber,
|
||||
CellValueType,
|
||||
DisposableCollection,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
|
||||
import { LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import {
|
||||
BorderStyleManagerService,
|
||||
IRefSelectionsService,
|
||||
RangeProtectionRuleModel,
|
||||
SheetInterceptorService,
|
||||
|
||||
SheetsSelectionsService,
|
||||
WorkbookPermissionService,
|
||||
WorksheetPermissionService,
|
||||
WorksheetProtectionPointModel,
|
||||
WorksheetProtectionRuleModel,
|
||||
} from '@univerjs/sheets';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import enUS from '../../../locale/en-US';
|
||||
import { ISheetSelectionRenderService } from '../../../services/selection/base-selection-render.service';
|
||||
import { SheetSelectionRenderService } from '../../../services/selection/selection-render.service';
|
||||
|
||||
const getTestWorkbookDataDemo = (): IWorkbookData => {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'A2',
|
||||
},
|
||||
},
|
||||
// 20 test merge cell, do not change value here
|
||||
20: {
|
||||
1: {
|
||||
v: 2,
|
||||
t: CellValueType.NUMBER,
|
||||
s: {
|
||||
n: {
|
||||
pattern: '0%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
columnData: {
|
||||
1: {
|
||||
hd: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
rowData: {
|
||||
1: {
|
||||
hd: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
};
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: Workbook;
|
||||
}
|
||||
|
||||
export function createCommandTestBed(
|
||||
workbookData?: IWorkbookData,
|
||||
dependencies?: Dependency[],
|
||||
renderDependencies?: Dependency[]
|
||||
): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([BorderStyleManagerService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([WorksheetPermissionService]);
|
||||
injector.add([WorksheetProtectionPointModel]);
|
||||
injector.add([WorkbookPermissionService]);
|
||||
injector.add([WorksheetProtectionRuleModel]);
|
||||
injector.add([RangeProtectionRuleModel]);
|
||||
injector.add([IRefSelectionsService, { useClass: SheetsSelectionsService }]);
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const snapshot = Tools.deepClone(workbookData || getTestWorkbookDataDemo());
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, snapshot);
|
||||
|
||||
if (!dependencies || !dependencies.find((d) => d[0] === ISheetSelectionRenderService)) {
|
||||
const context: IRenderContext<Workbook> = {
|
||||
unitId: sheet.getUnitId(),
|
||||
unit: sheet,
|
||||
type: UniverInstanceType.UNIVER_SHEET,
|
||||
engine: new DisposableCollection() as any,
|
||||
scene: new DisposableCollection() as any,
|
||||
mainComponent: null as any,
|
||||
components: new Map(),
|
||||
isMainScene: true,
|
||||
activated$: new BehaviorSubject(true),
|
||||
activate: () => { },
|
||||
deactivate: () => { },
|
||||
};
|
||||
|
||||
injector.add([ISheetSelectionRenderService, { useFactory: () => injector.createInstance(SheetSelectionRenderService, context) }]);
|
||||
}
|
||||
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
const logService = injector.get(ILogService);
|
||||
|
||||
logService.setLogLevel(LogLevel.VERBOSE); // change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
const localeService = injector.get(LocaleService);
|
||||
localeService.load({ enUS });
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import { Disposable, DisposableCollection, ICommandService, IUniverInstanceService, LocaleType, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { CancelFrozenCommand, SetFrozenMutation, SetSelectionsOperation, SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { SheetScrollManagerService } from '../../../services/scroll-manager.service';
|
||||
import { SelectAllService } from '../../../services/select-all/select-all.service';
|
||||
import { SheetSkeletonManagerService } from '../../../services/sheet-skeleton-manager.service';
|
||||
import { ShortcutExperienceService } from '../../../services/shortcut-experience.service';
|
||||
import {
|
||||
SetColumnFrozenCommand,
|
||||
SetFirstColumnFrozenCommand,
|
||||
SetFirstRowFrozenCommand,
|
||||
SetRowFrozenCommand,
|
||||
SetSelectionFrozenCommand,
|
||||
} from '../set-frozen.command';
|
||||
import { ExpandSelectionCommand, MoveSelectionCommand, SelectAllCommand } from '../set-selection.command';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
export const SELECTION_WITH_EMPTY_CELLS_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
2: {
|
||||
v: 'C1',
|
||||
},
|
||||
3: {
|
||||
v: '',
|
||||
},
|
||||
4: {
|
||||
v: '',
|
||||
},
|
||||
5: {
|
||||
v: 'F1',
|
||||
},
|
||||
6: {
|
||||
v: '', // merged to F1
|
||||
},
|
||||
7: {
|
||||
v: '',
|
||||
},
|
||||
8: {
|
||||
v: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [{ startRow: 0, startColumn: 5, endColumn: 6, endRow: 1 }],
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export const SELECTION_WITH_MERGED_CELLS_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
2: {
|
||||
v: 'C1',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 'A2',
|
||||
},
|
||||
1: {
|
||||
v: '', // merged to B1
|
||||
},
|
||||
2: {
|
||||
v: 'C2',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [{ startRow: 0, startColumn: 1, endRow: 1, endColumn: 1 }],
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export const SIMPLE_SELECTION_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'A2',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 'B1',
|
||||
},
|
||||
1: {
|
||||
v: 'B2',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [{ startRow: 3, startColumn: 4, endRow: 4, endColumn: 4 }],
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export function createSelectionCommandTestBed(workbookData?: IWorkbookData) {
|
||||
const { univer, get, sheet } = createCommandTestBed(workbookData || SIMPLE_SELECTION_WORKBOOK_DATA, [
|
||||
[ShortcutExperienceService],
|
||||
[SelectAllService],
|
||||
[IRenderManagerService, { useClass: RenderManagerService }],
|
||||
]);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
[MoveSelectionCommand, ExpandSelectionCommand, SelectAllCommand, SetSelectionsOperation].forEach((c) => {
|
||||
commandService.registerCommand(c);
|
||||
});
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFrozenCommandTestBed(workbookData?: IWorkbookData) {
|
||||
const { univer, get, sheet } = createCommandTestBed(workbookData || SIMPLE_SELECTION_WORKBOOK_DATA, [
|
||||
[SheetScrollManagerService],
|
||||
[ShortcutExperienceService],
|
||||
[IRenderManagerService, { useClass: RenderManagerService }],
|
||||
]);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
[
|
||||
SetSelectionFrozenCommand,
|
||||
SetRowFrozenCommand,
|
||||
SetColumnFrozenCommand,
|
||||
SetFirstRowFrozenCommand,
|
||||
SetFirstColumnFrozenCommand,
|
||||
CancelFrozenCommand,
|
||||
SetSelectionsOperation,
|
||||
SetFrozenMutation,
|
||||
].forEach((c) => {
|
||||
commandService.registerCommand(c);
|
||||
});
|
||||
|
||||
const unitId = sheet.getUnitId();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([SheetSkeletonService]);
|
||||
// NOTE: this is a hack. Please refer to ./services/clipboard/__tests__/clipboard-test-bed.ts
|
||||
const mockSheetSkService = new SheetSkeletonService(injector, get(IUniverInstanceService));
|
||||
const fakeSheetSkeletonManagerService = new SheetSkeletonManagerService({
|
||||
unit: sheet,
|
||||
unitId,
|
||||
type: UniverInstanceType.UNIVER_SHEET,
|
||||
|
||||
engine: null as any,
|
||||
scene: null as any,
|
||||
mainComponent: null as any,
|
||||
components: null as any,
|
||||
isMainScene: true,
|
||||
activated$: new BehaviorSubject(true),
|
||||
activate: () => {},
|
||||
deactivate: () => {},
|
||||
}, injector.get(SheetSkeletonService));
|
||||
|
||||
injector.add([SheetSkeletonManagerService, { useValue: fakeSheetSkeletonManagerService }]);
|
||||
injector.get(IRenderManagerService).addRender(unitId, {
|
||||
unitId,
|
||||
type: UniverInstanceType.UNIVER_SHEET,
|
||||
engine: new Disposable() as any,
|
||||
scene: new DisposableCollection() as any,
|
||||
mainComponent: null as any,
|
||||
components: new Map(),
|
||||
isMainScene: true,
|
||||
with: injector.get.bind(injector),
|
||||
activated$: new BehaviorSubject(true),
|
||||
activate: () => {},
|
||||
deactivate: () => {},
|
||||
isDisposed: () => false,
|
||||
});
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import { ICommandService, LocaleType } from '@univerjs/core';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { SetBorderCommand, SetBorderPositionCommand, SetRangeValuesMutation, SetSelectionsOperation } from '@univerjs/sheets';
|
||||
|
||||
import { SelectAllService } from '../../../services/select-all/select-all.service';
|
||||
import { ShortcutExperienceService } from '../../../services/shortcut-experience.service';
|
||||
import { ExpandSelectionCommand, MoveSelectionCommand, SelectAllCommand } from '../set-selection.command';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
export const SELECTION_WITH_EMPTY_CELLS_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
2: {
|
||||
v: 'C1',
|
||||
},
|
||||
3: {
|
||||
v: '',
|
||||
},
|
||||
4: {
|
||||
v: '',
|
||||
},
|
||||
5: {
|
||||
v: 'F1',
|
||||
},
|
||||
6: {
|
||||
v: '', // merged to F1
|
||||
},
|
||||
7: {
|
||||
v: '',
|
||||
},
|
||||
8: {
|
||||
v: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [{ startRow: 0, startColumn: 5, endColumn: 6, endRow: 1 }],
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* A1 | B1 | C1
|
||||
* ---| |----
|
||||
* A2 | | C2
|
||||
*
|
||||
*/
|
||||
export const MERGED_CELLS_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'B1',
|
||||
},
|
||||
2: {
|
||||
v: 'C1',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 'A2',
|
||||
},
|
||||
1: {
|
||||
v: '', // merged to B1
|
||||
},
|
||||
2: {
|
||||
v: 'C2',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [{ startRow: 0, startColumn: 1, endRow: 1, endColumn: 1 }],
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export const SIMPLE_SELECTION_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'A2',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 'B1',
|
||||
},
|
||||
1: {
|
||||
v: 'B2',
|
||||
},
|
||||
},
|
||||
},
|
||||
mergeData: [{ startRow: 3, startColumn: 4, endRow: 4, endColumn: 4 }],
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export function createSelectionCommandTestBed(workbookData?: IWorkbookData) {
|
||||
const { univer, get, sheet } = createCommandTestBed(workbookData || SIMPLE_SELECTION_WORKBOOK_DATA, [
|
||||
[ShortcutExperienceService],
|
||||
[SelectAllService],
|
||||
[IRenderManagerService, { useClass: RenderManagerService }],
|
||||
]);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
[MoveSelectionCommand, ExpandSelectionCommand, SelectAllCommand, SetBorderPositionCommand, SetBorderCommand, SetRangeValuesMutation, SetSelectionsOperation].forEach((c) => {
|
||||
commandService.registerCommand(c);
|
||||
});
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 { Injector, IRange, Univer, Workbook } from '@univerjs/core';
|
||||
import { ICommandService, IConfirmService, IUniverInstanceService, LocaleService, RANGE_TYPE, TestConfirmService, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeAllCommand,
|
||||
AddWorksheetMergeCommand,
|
||||
AddWorksheetMergeHorizontalCommand,
|
||||
AddWorksheetMergeMutation,
|
||||
AddWorksheetMergeVerticalCommand,
|
||||
RemoveWorksheetMergeCommand,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetColHiddenCommand,
|
||||
SetColHiddenMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetRowHiddenCommand,
|
||||
SetRowHiddenMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { HideColConfirmCommand, HideRowConfirmCommand } from '../hide-row-col-confirm.command';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
describe('Test hide row col confirm commands', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[IConfirmService, { useClass: TestConfirmService }],
|
||||
]);
|
||||
univer = testBed.univer;
|
||||
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(AddWorksheetMergeCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeAllCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeVerticalCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeHorizontalCommand);
|
||||
commandService.registerCommand(RemoveWorksheetMergeCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
commandService.registerCommand(HideColConfirmCommand);
|
||||
commandService.registerCommand(HideRowConfirmCommand);
|
||||
commandService.registerCommand(SetRowHiddenCommand);
|
||||
commandService.registerCommand(SetRowHiddenMutation);
|
||||
commandService.registerCommand(SetColHiddenCommand);
|
||||
commandService.registerCommand(SetColHiddenMutation);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
|
||||
get(LocaleService).load({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('Hide row', () => {
|
||||
it('Will apply when select some rows', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: Number.NaN, endRow: 1, endColumn: Number.NaN, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getHiddenRows(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRowManager()
|
||||
.getHiddenRows();
|
||||
}
|
||||
|
||||
expect(getHiddenRows()).toStrictEqual([]);
|
||||
expect(await commandService.executeCommand(HideRowConfirmCommand.id)).toBeTruthy();
|
||||
expect(getHiddenRows()).toStrictEqual([
|
||||
{
|
||||
endColumn: 0,
|
||||
endRow: 1,
|
||||
rangeType: RANGE_TYPE.ROW,
|
||||
startColumn: 0,
|
||||
startRow: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Will not apply when select all rows', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: Number.NaN, endRow: 999, endColumn: Number.NaN, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getHiddenRows(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRowManager()
|
||||
.getHiddenRows();
|
||||
}
|
||||
|
||||
expect(getHiddenRows()).toStrictEqual([]);
|
||||
expect(await commandService.executeCommand(HideRowConfirmCommand.id)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hide col', () => {
|
||||
it('Will apply when select some cols', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: Number.NaN, startColumn: 1, endRow: Number.NaN, endColumn: 1, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getHiddenCols(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getColumnManager()
|
||||
.getHiddenCols();
|
||||
}
|
||||
|
||||
expect(getHiddenCols()).toStrictEqual([]);
|
||||
expect(await commandService.executeCommand(HideColConfirmCommand.id)).toBeTruthy();
|
||||
expect(getHiddenCols()).toStrictEqual([
|
||||
{
|
||||
endColumn: 1,
|
||||
endRow: 0,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
startColumn: 1,
|
||||
startRow: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Will not apply when select all cols', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: Number.NaN, startColumn: 0, endRow: Number.NaN, endColumn: 19, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getHiddenCols(): IRange[] | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getColumnManager()
|
||||
.getHiddenCols();
|
||||
}
|
||||
|
||||
expect(getHiddenCols()).toStrictEqual([]);
|
||||
expect(await commandService.executeCommand(HideColConfirmCommand.id)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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 { Injector, IWorkbookData, Univer, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
IConfirmService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
TestConfirmService,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
MoveRangeCommand,
|
||||
MoveRangeMutation,
|
||||
SetSelectionsOperation,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MoveRangeConfirmCommand } from '../move-range-confirm.command';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const FROM_RANGE = { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 };
|
||||
const TO_RANGE = { startRow: 0, endRow: 0, startColumn: 1, endColumn: 1 };
|
||||
|
||||
describe('MoveRangeConfirmCommand', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let workbook: Workbook;
|
||||
let commandService: ICommandService;
|
||||
let confirmService: TestConfirmService<unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(createWorkbookData(), [
|
||||
[IConfirmService, { useClass: TestConfirmService }],
|
||||
]);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
workbook = testBed.sheet;
|
||||
commandService = get(ICommandService);
|
||||
confirmService = get(IConfirmService) as TestConfirmService<unknown>;
|
||||
get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
[MoveRangeCommand, MoveRangeConfirmCommand, MoveRangeMutation, SetSelectionsOperation].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('does not overwrite target content when confirmation is canceled', async () => {
|
||||
const confirm = vi.spyOn(confirmService, 'confirm').mockResolvedValue(false);
|
||||
|
||||
expect(await commandService.executeCommand(MoveRangeConfirmCommand.id, {
|
||||
fromRange: FROM_RANGE,
|
||||
toRange: TO_RANGE,
|
||||
})).toBeFalsy();
|
||||
expect(confirm).toHaveBeenCalledWith({
|
||||
id: MoveRangeConfirmCommand.id,
|
||||
title: { title: 'Warning' },
|
||||
children: {
|
||||
title: 'There is already data in the target area. This action will overwrite the existing data. Do you want to continue?',
|
||||
},
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'OK',
|
||||
});
|
||||
expect(getCellValue(0, 0)).toBe('source');
|
||||
expect(getCellValue(0, 1)).toBe('target');
|
||||
});
|
||||
|
||||
it('overwrites target content after confirmation', async () => {
|
||||
const confirm = vi.spyOn(confirmService, 'confirm').mockResolvedValue(true);
|
||||
|
||||
expect(await commandService.executeCommand(MoveRangeConfirmCommand.id, {
|
||||
fromRange: FROM_RANGE,
|
||||
toRange: TO_RANGE,
|
||||
})).toBeTruthy();
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(getCellValue(0, 0)).toBeUndefined();
|
||||
expect(getCellValue(0, 1)).toBe('source');
|
||||
});
|
||||
|
||||
function getCellValue(row: number, column: number) {
|
||||
return workbook.getActiveSheet().getCellRaw(row, column)?.v;
|
||||
}
|
||||
});
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'source' },
|
||||
1: { v: 'target' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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 { Injector, Univer, Workbook } from '@univerjs/core';
|
||||
import { ICommandService, IConfirmService, IUniverInstanceService, LocaleService, RANGE_TYPE, TestConfirmService, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeAllCommand,
|
||||
AddWorksheetMergeCommand,
|
||||
AddWorksheetMergeHorizontalCommand,
|
||||
AddWorksheetMergeMutation,
|
||||
AddWorksheetMergeVerticalCommand,
|
||||
InsertColByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
RemoveColByRangeCommand,
|
||||
RemoveColCommand,
|
||||
RemoveColMutation,
|
||||
RemoveRowByRangeCommand,
|
||||
RemoveRowCommand,
|
||||
RemoveRowMutation,
|
||||
RemoveWorksheetMergeCommand,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { RemoveColConfirmCommand, RemoveRowConfirmCommand } from '../remove-row-col-confirm.command';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
describe('Test remove row col confirm commands', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[IConfirmService, { useClass: TestConfirmService }],
|
||||
]);
|
||||
univer = testBed.univer;
|
||||
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(AddWorksheetMergeCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeAllCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeVerticalCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeHorizontalCommand);
|
||||
commandService.registerCommand(RemoveWorksheetMergeCommand);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
commandService.registerCommand(RemoveRowConfirmCommand);
|
||||
commandService.registerCommand(RemoveRowCommand);
|
||||
commandService.registerCommand(RemoveRowMutation);
|
||||
commandService.registerCommand(RemoveColConfirmCommand);
|
||||
commandService.registerCommand(RemoveColCommand);
|
||||
commandService.registerCommand(RemoveColMutation);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
|
||||
[
|
||||
RemoveColByRangeCommand,
|
||||
RemoveRowByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
InsertColByRangeCommand,
|
||||
].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
|
||||
get(LocaleService).load({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('Remove row', () => {
|
||||
it('Will apply when select some rows', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: Number.NaN, endRow: 1, endColumn: Number.NaN, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getRowCount(): number | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRowCount();
|
||||
}
|
||||
|
||||
expect(getRowCount()).toBe(1000);
|
||||
expect(await commandService.executeCommand(RemoveRowConfirmCommand.id)).toBeTruthy();
|
||||
expect(getRowCount()).toBe(999);
|
||||
});
|
||||
|
||||
it('Will not apply when select all rows', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: Number.NaN, endRow: 999, endColumn: Number.NaN, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getRowCount(): number | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRowCount();
|
||||
}
|
||||
|
||||
expect(getRowCount()).toBe(1000);
|
||||
expect(await commandService.executeCommand(RemoveRowConfirmCommand.id)).toBeFalsy();
|
||||
expect(getRowCount()).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove col', () => {
|
||||
it('Will apply when select some cols', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: Number.NaN, startColumn: 1, endRow: Number.NaN, endColumn: 1, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getColumnCount(): number | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getColumnCount();
|
||||
}
|
||||
|
||||
expect(getColumnCount()).toBe(20);
|
||||
expect(await commandService.executeCommand(RemoveColConfirmCommand.id)).toBeTruthy();
|
||||
expect(getColumnCount()).toBe(19);
|
||||
});
|
||||
|
||||
it('Will not apply when select all cols', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: Number.NaN, startColumn: 0, endRow: Number.NaN, endColumn: 19, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getColumnCount(): number | undefined {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getColumnCount();
|
||||
}
|
||||
|
||||
expect(getColumnCount()).toBe(20);
|
||||
expect(await commandService.executeCommand(RemoveColConfirmCommand.id)).toBeFalsy();
|
||||
expect(getColumnCount()).toBe(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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 { Direction, RANGE_TYPE } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
checkIfShrink,
|
||||
expandToNextCell,
|
||||
expandToWholeSheet,
|
||||
findNextGapRange,
|
||||
findNextRange,
|
||||
getMergeableSelectionsByType,
|
||||
getStartRange,
|
||||
isAllColumnsCovered,
|
||||
isAllRowsCovered,
|
||||
MergeType,
|
||||
shrinkToNextCell,
|
||||
} from '../utils/selection-utils';
|
||||
|
||||
function createMatrix(hasValue = false) {
|
||||
return {
|
||||
forValue: (cb: (...args: any[]) => void) => {
|
||||
if (hasValue) {
|
||||
cb(0, 0, { v: 1 });
|
||||
}
|
||||
return {
|
||||
getLength: () => (hasValue ? 1 : 0),
|
||||
forValue: (cb2: (...args: any[]) => void) => {
|
||||
if (hasValue) {
|
||||
cb2(0, 0, { v: 1 });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createWorksheet() {
|
||||
return {
|
||||
getRowCount: () => 6,
|
||||
getColumnCount: () => 6,
|
||||
getRowVisible: (row: number) => row !== 1,
|
||||
getColVisible: (col: number) => col !== 1,
|
||||
getMatrixWithMergedCells: (_sr: number, _sc: number, _er: number, _ec: number) => createMatrix(false),
|
||||
};
|
||||
}
|
||||
|
||||
describe('Test selection utils', () => {
|
||||
it('Function isAllRowsCovered', () => {
|
||||
// single range
|
||||
expect(
|
||||
isAllRowsCovered(
|
||||
[{ startRow: 0, endRow: 999, startColumn: Number.NaN, endColumn: Number.NaN }],
|
||||
[{ startRow: 0, endRow: 1, startColumn: Number.NaN, endColumn: Number.NaN }]
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
isAllRowsCovered(
|
||||
[{ startRow: 0, endRow: 999, startColumn: Number.NaN, endColumn: Number.NaN }],
|
||||
[{ startRow: 0, endRow: 999, startColumn: Number.NaN, endColumn: Number.NaN }]
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
// multiple range
|
||||
expect(
|
||||
isAllRowsCovered(
|
||||
[
|
||||
{ startRow: 0, endRow: 999, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
{ startRow: 1000, endRow: 1999, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
],
|
||||
[
|
||||
{ startRow: 100, endRow: 999, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
{ startRow: 1000, endRow: 1500, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
]
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
isAllRowsCovered(
|
||||
[
|
||||
{ startRow: 0, endRow: 999, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
{ startRow: 1000, endRow: 1999, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
],
|
||||
[
|
||||
{ startRow: 0, endRow: 1500, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
{ startRow: 1000, endRow: 1999, startColumn: Number.NaN, endColumn: Number.NaN },
|
||||
]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Function isAllColumnsCovered', () => {
|
||||
// single range
|
||||
expect(
|
||||
isAllColumnsCovered(
|
||||
[{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 999 }],
|
||||
[{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 1 }]
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
isAllColumnsCovered(
|
||||
[{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 999 }],
|
||||
[{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 999 }]
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
// multiple range
|
||||
expect(
|
||||
isAllColumnsCovered(
|
||||
[
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 999 },
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 1000, endColumn: 1999 },
|
||||
],
|
||||
[
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 100, endColumn: 999 },
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 1000, endColumn: 1500 },
|
||||
]
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(
|
||||
isAllColumnsCovered(
|
||||
[
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 999 },
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 1000, endColumn: 1999 },
|
||||
],
|
||||
[
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 0, endColumn: 1500 },
|
||||
{ startRow: Number.NaN, endRow: Number.NaN, startColumn: 1000, endColumn: 1999 },
|
||||
]
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('findNextRange should move with hidden rows/cols and wrap in boundary', () => {
|
||||
const worksheet = createWorksheet();
|
||||
const start = { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 };
|
||||
expect(findNextRange(start as any, Direction.DOWN, worksheet as any)).toEqual({ ...start, startRow: 2, endRow: 2 });
|
||||
expect(findNextRange(start as any, Direction.RIGHT, worksheet as any)).toEqual({ ...start, startColumn: 2, endColumn: 2 });
|
||||
expect(findNextRange(start as any, Direction.LEFT, worksheet as any)).toEqual({ startRow: 5, endRow: 5, startColumn: 5, endColumn: 5 });
|
||||
});
|
||||
|
||||
it('findNextGapRange should continue to gap boundary on empty ranges', () => {
|
||||
const worksheet = createWorksheet();
|
||||
const start = { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 };
|
||||
expect(findNextGapRange(start as any, Direction.RIGHT, worksheet as any)).toEqual({ startRow: 0, endRow: 0, startColumn: 5, endColumn: 5 });
|
||||
expect(findNextGapRange(start as any, Direction.DOWN, worksheet as any)).toEqual({ startRow: 5, endRow: 5, startColumn: 0, endColumn: 0 });
|
||||
});
|
||||
|
||||
it('expand/shrink helpers should return expected ranges', () => {
|
||||
const worksheet = createWorksheet();
|
||||
const start = { startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 };
|
||||
|
||||
expect(expandToNextCell(start as any, Direction.RIGHT, worksheet as any)).toEqual(expect.objectContaining({
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 2,
|
||||
endColumn: 3,
|
||||
}));
|
||||
expect(shrinkToNextCell({ startRow: 2, endRow: 2, startColumn: 2, endColumn: 4 } as any, Direction.RIGHT, worksheet as any)).toEqual(expect.objectContaining({
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 3,
|
||||
endColumn: 4,
|
||||
}));
|
||||
expect(expandToWholeSheet(worksheet as any)).toEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 5,
|
||||
endColumn: 5,
|
||||
rangeType: RANGE_TYPE.ALL,
|
||||
});
|
||||
});
|
||||
|
||||
it('getStartRange and checkIfShrink should follow direction and primary cell', () => {
|
||||
const range = { startRow: 0, endRow: 3, startColumn: 0, endColumn: 3 };
|
||||
const primary = { actualRow: 2, actualColumn: 1, startRow: 2, endRow: 2, startColumn: 1, endColumn: 1 };
|
||||
const worksheet = createWorksheet();
|
||||
|
||||
expect(getStartRange(range as any, primary as any, Direction.DOWN)).toEqual({ ...range, startColumn: 1, endColumn: 1 });
|
||||
expect(getStartRange(range as any, primary as any, Direction.RIGHT)).toEqual({ ...range, startRow: 2, endRow: 2 });
|
||||
expect(checkIfShrink({ range, primary } as any, Direction.DOWN, worksheet as any)).toBe(true);
|
||||
expect(checkIfShrink({ range, primary: { ...primary, startColumn: 0, endColumn: 0 } } as any, Direction.RIGHT, worksheet as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('getMergeableSelectionsByType should filter single-cell ranges by merge mode', () => {
|
||||
const selections = [
|
||||
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 },
|
||||
{ startRow: 0, endRow: 0, startColumn: 0, endColumn: 1 },
|
||||
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
];
|
||||
|
||||
expect(getMergeableSelectionsByType(MergeType.MergeAll, selections as any)?.length).toBe(3);
|
||||
expect(getMergeableSelectionsByType(MergeType.MergeVertical, selections as any)?.length).toBe(2);
|
||||
expect(getMergeableSelectionsByType(MergeType.MergeHorizontal, selections as any)?.length).toBe(2);
|
||||
expect(getMergeableSelectionsByType(MergeType.MergeAll, null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* 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 { Injector, Univer, Workbook } from '@univerjs/core';
|
||||
import type { ISelectionWithCoord, ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import {
|
||||
Disposable,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
ObjectMatrix,
|
||||
RedoCommand,
|
||||
set,
|
||||
ThemeService,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
AddWorksheetMergeMutation,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesCommand,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
} from '@univerjs/sheets';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { FormatPainterController } from '../../../controllers/format-painter/format-painter.controller';
|
||||
import {
|
||||
FormatPainterService,
|
||||
FormatPainterStatus,
|
||||
IFormatPainterService,
|
||||
} from '../../../services/format-painter/format-painter.service';
|
||||
import { IMarkSelectionService } from '../../../services/mark-selection/mark-selection.service';
|
||||
import { ISheetSelectionRenderService } from '../../../services/selection/base-selection-render.service';
|
||||
import { SetFormatPainterOperation } from '../../operations/set-format-painter.operation';
|
||||
import {
|
||||
ApplyFormatPainterCommand,
|
||||
SetInfiniteFormatPainterCommand,
|
||||
SetOnceFormatPainterCommand,
|
||||
} from '../set-format-painter.command';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const TEST_WORKBOOK_DATA = {
|
||||
id: 'workbook-01',
|
||||
sheetOrder: ['sheet-0011'],
|
||||
name: 'UniverSheet Demo',
|
||||
appVersion: '3.0.0-alpha',
|
||||
styles: {
|
||||
yifA1t: {
|
||||
bl: 1,
|
||||
it: 1,
|
||||
ul: {
|
||||
s: 1,
|
||||
},
|
||||
st: {
|
||||
s: 1,
|
||||
},
|
||||
},
|
||||
M5JbP2: {
|
||||
bg: {
|
||||
rgb: '#409f11',
|
||||
},
|
||||
cl: {
|
||||
rgb: '#E30909',
|
||||
},
|
||||
ff: 'Microsoft YaHei',
|
||||
},
|
||||
},
|
||||
sheets: {
|
||||
'sheet-0011': {
|
||||
type: 0,
|
||||
id: 'sheet-0011',
|
||||
name: 'sheet11',
|
||||
columnData: {
|
||||
1: {
|
||||
hd: 0,
|
||||
},
|
||||
},
|
||||
mergeData: [
|
||||
{
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
},
|
||||
],
|
||||
status: 1,
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
s: 'yifA1t',
|
||||
},
|
||||
1: {
|
||||
v: 2,
|
||||
t: 2,
|
||||
s: 'M5JbP2',
|
||||
},
|
||||
2: {
|
||||
v: 3,
|
||||
},
|
||||
3: {
|
||||
v: 1,
|
||||
f: '=SUM(A1)',
|
||||
si: '3e4r5t',
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
0: {
|
||||
v: 4,
|
||||
},
|
||||
2: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
3: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
2: {
|
||||
0: {
|
||||
v: 44,
|
||||
},
|
||||
2: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
3: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
3: {
|
||||
0: {
|
||||
v: 444,
|
||||
},
|
||||
2: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
3: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
4: {
|
||||
2: {
|
||||
v: 1,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createdTime: '',
|
||||
creator: '',
|
||||
lastModifiedBy: '',
|
||||
locale: LocaleType.EN_US,
|
||||
modifiedTime: '',
|
||||
timeZone: '',
|
||||
};
|
||||
|
||||
class MarkSelectionService extends Disposable implements IMarkSelectionService {
|
||||
addShape(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
addShapeWithNoFresh(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
refreshShapes() { /* TODO document why this method 'refreshShapes' is empty */ }
|
||||
|
||||
removeShape(id: string): void { /* TODO document why this method 'removeShape' is empty */ }
|
||||
|
||||
removeAllShapes(): void { /* TODO document why this method 'removeAllShapes' is empty */ }
|
||||
|
||||
getShapeMap(): Map<string, any> {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
describe('Test format painter rules in controller', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let themeService: ThemeService;
|
||||
|
||||
beforeEach(() => {
|
||||
class SheetSelectionRenderService {
|
||||
private readonly _selectionMoveEnd$ = new BehaviorSubject<ISelectionWithCoord[]>([]);
|
||||
readonly selectionMoveEnd$ = this._selectionMoveEnd$.asObservable();
|
||||
}
|
||||
|
||||
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA, [
|
||||
[IMarkSelectionService, { useClass: MarkSelectionService }],
|
||||
[IFormatPainterService, { useClass: FormatPainterService }],
|
||||
[ISheetSelectionRenderService, { useClass: SheetSelectionRenderService }],
|
||||
[IRenderManagerService, { useClass: RenderManagerService }],
|
||||
[FormatPainterController],
|
||||
]);
|
||||
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
themeService = get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
get(FormatPainterController);
|
||||
commandService.registerCommand(SetFormatPainterOperation);
|
||||
commandService.registerCommand(SetInfiniteFormatPainterCommand);
|
||||
commandService.registerCommand(SetOnceFormatPainterCommand);
|
||||
commandService.registerCommand(ApplyFormatPainterCommand);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(SetRangeValuesCommand);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
});
|
||||
|
||||
afterEach(() => univer?.dispose());
|
||||
|
||||
describe('format painter', () => {
|
||||
it('toggles toolbar commands between active mode and off', async () => {
|
||||
const formatPainterService = get(IFormatPainterService);
|
||||
|
||||
expect(formatPainterService.getStatus()).toBe(FormatPainterStatus.OFF);
|
||||
|
||||
expect(await commandService.executeCommand(SetInfiniteFormatPainterCommand.id)).toBeTruthy();
|
||||
expect(formatPainterService.getStatus()).toBe(FormatPainterStatus.INFINITE);
|
||||
|
||||
expect(await commandService.executeCommand(SetInfiniteFormatPainterCommand.id)).toBeTruthy();
|
||||
expect(formatPainterService.getStatus()).toBe(FormatPainterStatus.OFF);
|
||||
|
||||
expect(await commandService.executeCommand(SetOnceFormatPainterCommand.id)).toBeTruthy();
|
||||
expect(formatPainterService.getStatus()).toBe(FormatPainterStatus.ONCE);
|
||||
|
||||
expect(await commandService.executeCommand(SetOnceFormatPainterCommand.id)).toBeTruthy();
|
||||
expect(formatPainterService.getStatus()).toBe(FormatPainterStatus.OFF);
|
||||
});
|
||||
|
||||
describe('format painter the numbers', async () => {
|
||||
it('correct situation', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await commandService.executeCommand(SetOnceFormatPainterCommand.id);
|
||||
await commandService.executeCommand(ApplyFormatPainterCommand.id, {
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
startColumn: 2,
|
||||
endColumn: 3,
|
||||
},
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
});
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 2)?.s).toBe('yifA1t');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 3)?.s).toBe('M5JbP2');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData().length).toBe(3);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[0].startRow).toBe(1);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[1].startRow).toBe(1);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[2].startRow).toBe(3);
|
||||
|
||||
get(IUniverInstanceService).focusUnit('workbook-01');
|
||||
// undo
|
||||
await commandService.executeCommand(UndoCommand.id);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 2)?.s).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 3)?.s).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe(undefined);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData().length).toBe(1);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[0].startRow).toBe(1);
|
||||
//redo
|
||||
await commandService.executeCommand(RedoCommand.id);
|
||||
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 3)?.s).toBe('M5JbP2');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData().length).toBe(3);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[0].startRow).toBe(1);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[1].startRow).toBe(1);
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[2].startRow).toBe(3);
|
||||
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await commandService.executeCommand(SetOnceFormatPainterCommand.id);
|
||||
await commandService.executeCommand(ApplyFormatPainterCommand.id, {
|
||||
range: {
|
||||
startRow: 10,
|
||||
endRow: 13,
|
||||
startColumn: 12,
|
||||
endColumn: 13,
|
||||
},
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
});
|
||||
const mergeData = workbook.getSheetBySheetId('sheet-0011')?.getMergeData();
|
||||
expect(mergeData?.length).toBe(5);
|
||||
expect(mergeData?.[3].startRow).toBe(11);
|
||||
expect(mergeData?.[4].startRow).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
describe('format painter to single cell', () => {
|
||||
it('changes only styles and preserves target formulas through undo and redo', async () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getSheetBySheetId('sheet-0011')!;
|
||||
const originalCell = { ...worksheet.getCell(0, 3)! };
|
||||
get(IUniverInstanceService).focusUnit(workbook.getUnitId());
|
||||
expect(await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(await commandService.executeCommand(SetOnceFormatPainterCommand.id)).toBeTruthy();
|
||||
|
||||
const setRangeValuesParams: ISetRangeValuesMutationParams[] = [];
|
||||
const disposable = commandService.onCommandExecuted((command) => {
|
||||
if (command.id === SetRangeValuesMutation.id) {
|
||||
setRangeValuesParams.push(command.params as ISetRangeValuesMutationParams);
|
||||
}
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(ApplyFormatPainterCommand.id, {
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 3,
|
||||
endColumn: 3,
|
||||
},
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
})).toBeTruthy();
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(setRangeValuesParams.length).toBeGreaterThan(0);
|
||||
for (const params of setRangeValuesParams) {
|
||||
expect(params.trigger).toBe(ApplyFormatPainterCommand.id);
|
||||
new ObjectMatrix(params.cellValue).forValue((_row, _column, cell) => {
|
||||
expect(Object.keys(cell!)).toEqual(['s']);
|
||||
});
|
||||
}
|
||||
expect(worksheet.getCell(0, 3)).toMatchObject({ ...originalCell, s: 'yifA1t' });
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(worksheet.getCell(0, 3)).toMatchObject(originalCell);
|
||||
expect(worksheet.getCell(0, 3)?.s).toBeUndefined();
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(worksheet.getCell(0, 3)).toMatchObject({ ...originalCell, s: 'yifA1t' });
|
||||
});
|
||||
it('will copy whole original styles', async () => {
|
||||
expect(await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(await commandService.executeCommand(SetOnceFormatPainterCommand.id)).toBeTruthy();
|
||||
expect(await commandService.executeCommand(ApplyFormatPainterCommand.id, {
|
||||
range: {
|
||||
startRow: 5,
|
||||
endRow: 5,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
})).toBeTruthy();
|
||||
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(5, 0)?.s).toBe('yifA1t');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(5, 1)?.s).toBe('M5JbP2');
|
||||
});
|
||||
|
||||
describe('format painter from non-style cell to styled cell', () => {
|
||||
it('will clear styles', async () => {
|
||||
expect(await commandService.executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
|
||||
selections: [
|
||||
{
|
||||
range: {
|
||||
startRow: 6,
|
||||
endRow: 6,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(await commandService.executeCommand(SetOnceFormatPainterCommand.id)).toBeTruthy();
|
||||
expect(await commandService.executeCommand(ApplyFormatPainterCommand.id, {
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
},
|
||||
unitId: 'workbook-01',
|
||||
subUnitId: 'sheet-0011',
|
||||
})).toBeTruthy();
|
||||
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) throw new Error('This is an error');
|
||||
expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 0)?.s).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 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 { IFreeze, Injector, IWorkbookData, Univer, Workbook } from '@univerjs/core';
|
||||
import type { IScrollStateWithSearchParam } from '../../../services/scroll-manager.service';
|
||||
import { ICommandService, IUniverInstanceService, RANGE_TYPE, UniverInstanceType } from '@univerjs/core';
|
||||
import { CancelFrozenCommand, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SheetScrollManagerService } from '../../../services/scroll-manager.service';
|
||||
import {
|
||||
SetColumnFrozenCommand,
|
||||
SetFirstColumnFrozenCommand,
|
||||
SetFirstRowFrozenCommand,
|
||||
SetRowFrozenCommand,
|
||||
SetSelectionFrozenCommand,
|
||||
} from '../set-frozen.command';
|
||||
import { createFrozenCommandTestBed } from './create-selection-command-test-bed';
|
||||
|
||||
describe('Test commands used for change selections', () => {
|
||||
let univer: Univer | null = null;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let selectionManagerService: SheetsSelectionsService;
|
||||
let scrollManagerService: SheetScrollManagerService;
|
||||
|
||||
const searchParam = {
|
||||
unitId: 'test',
|
||||
sheetId: 'sheet1',
|
||||
};
|
||||
|
||||
function select(
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
actualRow: number,
|
||||
actualColumn: number,
|
||||
isMerged: boolean,
|
||||
isMergedMainCell: boolean
|
||||
) {
|
||||
selectionManagerService.addSelections([
|
||||
{
|
||||
range: { startRow, startColumn, endRow, endColumn, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: {
|
||||
startRow,
|
||||
startColumn,
|
||||
endRow,
|
||||
endColumn,
|
||||
actualRow,
|
||||
actualColumn,
|
||||
isMerged,
|
||||
isMergedMainCell,
|
||||
},
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const scrollTo = (startRow: number, startColumn: number, offsetX = 0, offsetY = 0) => {
|
||||
const scrollInfo = {
|
||||
sheetViewStartRow: startRow,
|
||||
sheetViewStartColumn: startColumn,
|
||||
offsetX,
|
||||
offsetY,
|
||||
...searchParam,
|
||||
};
|
||||
scrollManagerService.emitRawScrollParam(scrollInfo);
|
||||
scrollManagerService.setValidScrollState(scrollInfo);
|
||||
};
|
||||
|
||||
const getFreeze = () => {
|
||||
const currentService = get(IUniverInstanceService);
|
||||
const workbook = currentService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
return worksheet?.getConfig().freeze;
|
||||
};
|
||||
|
||||
function disposeTestBed() {
|
||||
univer?.dispose();
|
||||
univer = null;
|
||||
}
|
||||
|
||||
function prepareTestBed(snapshot?: IWorkbookData) {
|
||||
const testBed = createFrozenCommandTestBed(snapshot);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
selectionManagerService = get(SheetsSelectionsService);
|
||||
scrollManagerService = get(SheetScrollManagerService);
|
||||
scrollManagerService.setSearchParam({
|
||||
...searchParam,
|
||||
});
|
||||
const scrollState = scrollManagerService.getScrollStateByParam(searchParam);
|
||||
const scrollStateWithSheetId: IScrollStateWithSearchParam = {
|
||||
...scrollState!,
|
||||
...searchParam,
|
||||
};
|
||||
scrollManagerService.emitRawScrollParam(scrollStateWithSheetId);
|
||||
}
|
||||
|
||||
afterEach(disposeTestBed);
|
||||
|
||||
describe('Freeze', () => {
|
||||
beforeEach(() => prepareTestBed());
|
||||
|
||||
it('Should freeze row and col on current cell', async () => {
|
||||
select(2, 2, 2, 2, 2, 2, false, false);
|
||||
|
||||
await commandService.executeCommand(SetSelectionFrozenCommand.id);
|
||||
|
||||
const config = getFreeze();
|
||||
expect(config?.startRow === 2 && config.startColumn === 2).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should keep default freeze config when selection is invalid', async () => {
|
||||
await commandService.executeCommand(CancelFrozenCommand.id);
|
||||
|
||||
const config = getFreeze();
|
||||
expect(config?.startRow === -1 && config.startColumn === -1).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should freeze current row', async () => {
|
||||
select(2, 2, 2, 2, 2, 2, false, false);
|
||||
|
||||
await commandService.executeCommand(SetRowFrozenCommand.id);
|
||||
|
||||
const config = getFreeze();
|
||||
expect(config?.startRow === 2 && config.startColumn === -1).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should freeze current column', async () => {
|
||||
select(2, 2, 2, 2, 2, 2, false, false);
|
||||
|
||||
await commandService.executeCommand(SetColumnFrozenCommand.id);
|
||||
|
||||
const config = getFreeze();
|
||||
expect(config?.startRow === -1 && config.startColumn === 2).toBeTruthy();
|
||||
});
|
||||
|
||||
it('xSplit or ySplit must bigger than 0 if row or column was set', async () => {
|
||||
select(2, 2, 2, 2, 2, 2, false, false);
|
||||
scrollTo(2, 2);
|
||||
let config: IFreeze | undefined;
|
||||
await commandService.executeCommand(SetColumnFrozenCommand.id);
|
||||
config = getFreeze();
|
||||
expect(config?.startRow === -1 && config.startColumn === 2).toBeTruthy();
|
||||
expect(config?.xSplit).toBe(1);
|
||||
|
||||
await commandService.executeCommand(SetRowFrozenCommand.id);
|
||||
config = getFreeze();
|
||||
expect(config?.startRow === 2 && config.startColumn === -1).toBeTruthy();
|
||||
expect(config?.ySplit).toBe(1);
|
||||
|
||||
await commandService.executeCommand(SetSelectionFrozenCommand.id);
|
||||
config = getFreeze();
|
||||
expect(config?.startRow === 2 && config.startColumn === 2).toBeTruthy();
|
||||
expect(config?.ySplit).toBe(1);
|
||||
expect(config?.xSplit).toBe(1);
|
||||
});
|
||||
|
||||
it('Should freeze first row and first column based on current viewport', async () => {
|
||||
scrollTo(4, 6);
|
||||
|
||||
await commandService.executeCommand(SetFirstRowFrozenCommand.id);
|
||||
let config = getFreeze();
|
||||
expect(config?.startRow).toBe(5);
|
||||
expect(config?.startColumn).toBe(-1);
|
||||
expect(config?.ySplit).toBe(1);
|
||||
|
||||
await commandService.executeCommand(SetFirstColumnFrozenCommand.id);
|
||||
config = getFreeze();
|
||||
expect(config?.startRow).toBe(-1);
|
||||
expect(config?.startColumn).toBe(7);
|
||||
expect(config?.xSplit).toBe(1);
|
||||
});
|
||||
|
||||
it('Should not freeze when the whole sheet is selected', async () => {
|
||||
selectionManagerService.setSelections([
|
||||
{
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 19,
|
||||
endColumn: 19,
|
||||
rangeType: RANGE_TYPE.ALL,
|
||||
},
|
||||
primary: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 19,
|
||||
endColumn: 19,
|
||||
actualRow: 0,
|
||||
actualColumn: 0,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(SetSelectionFrozenCommand.id)).resolves.toBe(false);
|
||||
expect(getFreeze()).toEqual({
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
xSplit: 0,
|
||||
ySplit: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should cancel all freeze', async () => {
|
||||
await commandService.executeCommand(CancelFrozenCommand.id);
|
||||
|
||||
const config = getFreeze();
|
||||
expect(config?.startRow === -1 && config.startColumn === -1).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable dot-notation */
|
||||
import type { ISelectionCell, IWorkbookData, Univer, Workbook, Worksheet } from '@univerjs/core';
|
||||
import { BorderType, ICommandService, IConfigService, IContextService, Injector, LocaleService, RANGE_TYPE } from '@univerjs/core';
|
||||
import { SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import {
|
||||
SetBorderPositionCommand,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
createSelectionCommandTestBed,
|
||||
MERGED_CELLS_DATA,
|
||||
} from './create-sheet-skeleton-test-bed';
|
||||
|
||||
describe('Test commands used for change selections', () => {
|
||||
let univer: Univer | null = null;
|
||||
let workbook: Workbook;
|
||||
let worksheet: Worksheet;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let selectionManagerService: SheetsSelectionsService;
|
||||
let spreadsheetSkeleton: SpreadsheetSkeleton;
|
||||
let localeService: LocaleService;
|
||||
let contextService: IContextService;
|
||||
let configService: IConfigService;
|
||||
|
||||
function select(selection: ISelectionCell) {
|
||||
const { startRow, startColumn, endRow, endColumn } = selection;
|
||||
selectionManagerService.setSelections([
|
||||
{
|
||||
range: { startRow, startColumn, endRow, endColumn, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: selection,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function disposeTestBed() {
|
||||
univer?.dispose();
|
||||
univer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare whether all the props of the two border configurations exist in each other.
|
||||
*
|
||||
* @param border1
|
||||
* @param border2
|
||||
* @returns
|
||||
*/
|
||||
const borderLenEqual = (border1: string[], border2: string[]) => {
|
||||
if (border1.length !== border2.length) return false;
|
||||
|
||||
const sortedArr1 = border1.slice().sort();
|
||||
const sortedArr2 = border2.slice().sort();
|
||||
|
||||
for (let i = 0; i < sortedArr1.length; i++) {
|
||||
if (sortedArr1[i] !== sortedArr2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
function prepareSelectionsTestBed(snapshot?: IWorkbookData) {
|
||||
const testBed = createSelectionCommandTestBed(snapshot);
|
||||
univer = testBed.univer;
|
||||
workbook = testBed.sheet;
|
||||
get = testBed.get;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
selectionManagerService = get(SheetsSelectionsService);
|
||||
|
||||
localeService = get(LocaleService);
|
||||
contextService = get(IContextService);
|
||||
configService = get(IConfigService);
|
||||
const injector = get(Injector);
|
||||
|
||||
worksheet = workbook.getActiveSheet()!;
|
||||
spreadsheetSkeleton = new SpreadsheetSkeleton(
|
||||
worksheet,
|
||||
workbook.getStyles(),
|
||||
localeService,
|
||||
contextService,
|
||||
configService,
|
||||
injector
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(disposeTestBed);
|
||||
|
||||
describe('_stylesCache.border in skeleton', () => {
|
||||
beforeEach(() => prepareSelectionsTestBed(MERGED_CELLS_DATA));
|
||||
|
||||
/**
|
||||
* A1 | B1 | C1
|
||||
* ---| |----
|
||||
* A2 | | C2
|
||||
*
|
||||
* When user clicks on C2 and move cursor left twice, A2 should not selected not A1.
|
||||
*/
|
||||
it('_stylesCache.border should be same as worksheet.getRange', async () => {
|
||||
const mergeRangeForB1B3 = { startRow: 0, endRow: 2, startColumn: 1, endColumn: 1 };
|
||||
const rangeForA1 = { startRow: 1, endRow: 1, startColumn: 0, endColumn: 0 };
|
||||
select({ ...mergeRangeForB1B3, actualRow: 0, actualColumn: 1, isMerged: false, isMergedMainCell: true } as ISelectionCell);
|
||||
|
||||
//set BorderType.ALL for B1:B3
|
||||
expect(
|
||||
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.ALL })
|
||||
).toBeTruthy();
|
||||
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](mergeRangeForB1B3.startRow, mergeRangeForB1B3.startColumn, {
|
||||
mergeRange: mergeRangeForB1B3,
|
||||
});
|
||||
|
||||
const borderOfCell01 = spreadsheetSkeleton.stylesCache.border?.getValue(0, 1) || {};
|
||||
let actualBorderList = Object.keys(borderOfCell01);
|
||||
let borderOfRange = worksheet.getRange(0, 1, 0, 1).getBorder();
|
||||
let expectBorderList = Object.keys(borderOfRange);
|
||||
expect(borderLenEqual(actualBorderList, expectBorderList)).toBe(true);
|
||||
|
||||
// set BorderType.NONE for A1
|
||||
spreadsheetSkeleton.resetCache();
|
||||
select({ ...rangeForA1, actualRow: 1, actualColumn: 1, isMerged: false, isMergedMainCell: false } as ISelectionCell);
|
||||
expect(
|
||||
await commandService.executeCommand(SetBorderPositionCommand.id, { value: BorderType.NONE })
|
||||
).toBeTruthy();
|
||||
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](mergeRangeForB1B3.startRow, mergeRangeForB1B3.startColumn, {
|
||||
mergeRange: mergeRangeForB1B3,
|
||||
});
|
||||
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](0, 0, { cacheItem: { bg: true, border: true } });
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](0, 1, { cacheItem: { bg: true, border: true } });
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](1, 0, { cacheItem: { bg: true, border: true } });
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](1, 1, { cacheItem: { bg: true, border: true } });
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](2, 0, { cacheItem: { bg: true, border: true } });
|
||||
spreadsheetSkeleton['_setStylesCacheForOneCell'](2, 1, { cacheItem: { bg: true, border: true } });
|
||||
|
||||
const borderOfCell11 = spreadsheetSkeleton.stylesCache.border?.getValue(1, 1) || {};
|
||||
actualBorderList = Object.keys(borderOfCell11);
|
||||
borderOfRange = worksheet.getRange(1, 1, 1, 1).getBorder();
|
||||
expectBorderList = Object.keys(borderOfRange);
|
||||
expect(borderLenEqual(actualBorderList, expectBorderList)).toBe(true);
|
||||
expect(actualBorderList.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
288
packages/sheets-ui/src/commands/commands/clipboard.command.ts
Normal file
288
packages/sheets-ui/src/commands/commands/clipboard.command.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand, IMultiCommand, Workbook } from '@univerjs/core';
|
||||
import type { ISheetRangeLocation } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { IPasteHookKeyType } from '../../services/clipboard/type';
|
||||
import { CommandType, ICommandService, IPermissionService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
RangeProtectionPermissionEditPoint,
|
||||
RangeProtectionPermissionViewPoint,
|
||||
SheetPermissionCheckController,
|
||||
WorkbookCopyPermission,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetCopyPermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetSetCellStylePermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
WorksheetSetColumnStylePermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { CopyCommand, CutCommand, IClipboardInterfaceService, PasteCommand, SheetPasteShortKeyCommandName } from '@univerjs/ui';
|
||||
import { whenSheetFocused } from '../../controllers/shortcuts/utils';
|
||||
import { ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE } from '../../services/clipboard/clipboard.service';
|
||||
|
||||
const SHEET_CLIPBOARD_PRIORITY = 998;
|
||||
|
||||
export const SheetCopyCommand: IMultiCommand = {
|
||||
id: CopyCommand.id,
|
||||
name: 'sheet.command.copy',
|
||||
type: CommandType.COMMAND,
|
||||
multi: true,
|
||||
priority: SHEET_CLIPBOARD_PRIORITY,
|
||||
preconditions: whenSheetFocused,
|
||||
handler: async (accessor) => {
|
||||
checkSheetClipboardPermission(accessor, CopyCommand.id);
|
||||
const sheetClipboardService = accessor.get(ISheetClipboardService);
|
||||
return sheetClipboardService.copy();
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetCutCommand: IMultiCommand = {
|
||||
id: CutCommand.id,
|
||||
name: 'sheet.command.cut',
|
||||
type: CommandType.COMMAND,
|
||||
multi: true,
|
||||
priority: SHEET_CLIPBOARD_PRIORITY,
|
||||
preconditions: whenSheetFocused,
|
||||
handler: async (accessor) => {
|
||||
checkSheetClipboardPermission(accessor, CutCommand.id);
|
||||
const sheetClipboardService = accessor.get(ISheetClipboardService);
|
||||
return sheetClipboardService.cut();
|
||||
},
|
||||
};
|
||||
|
||||
export interface ISheetPasteParams {
|
||||
value: string;
|
||||
target?: ISheetRangeLocation;
|
||||
}
|
||||
|
||||
export interface ISheetPasteByShortKeyParams {
|
||||
htmlContent?: string;
|
||||
textContent?: string;
|
||||
files?: File[];
|
||||
formulaClipboardPayload?: string;
|
||||
target?: ISheetRangeLocation;
|
||||
}
|
||||
|
||||
export const SheetPasteCommand: IMultiCommand = {
|
||||
id: PasteCommand.id,
|
||||
type: CommandType.COMMAND,
|
||||
multi: true,
|
||||
name: 'sheet.command.paste',
|
||||
priority: SHEET_CLIPBOARD_PRIORITY,
|
||||
preconditions: whenSheetFocused,
|
||||
handler: async (accessor: IAccessor, params: ISheetPasteParams) => {
|
||||
const sheetClipboardService = accessor.get(ISheetClipboardService);
|
||||
const target = params?.target ?? sheetClipboardService.capturePasteTarget();
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const workbook = instanceService.getUnit<Workbook>(target.unitId);
|
||||
const worksheet = workbook?.getSheetBySheetId(target.subUnitId);
|
||||
if (!workbook || !worksheet) {
|
||||
return false;
|
||||
}
|
||||
const pasteParams = { value: params?.value, target };
|
||||
checkSheetClipboardPermission(accessor, PasteCommand.id, pasteParams);
|
||||
// const messageService = accessor.get(IMessageService);
|
||||
|
||||
// TODO: @yuhongz: check if there is excel content in the clipboard, if so
|
||||
// ask users to use shortcuts instead.
|
||||
|
||||
const clipboardInterfaceService = accessor.get(IClipboardInterfaceService);
|
||||
if (clipboardInterfaceService.supportClipboard) {
|
||||
const clipboardItems = await clipboardInterfaceService.read();
|
||||
// A restored unit may reuse the same IDs but must not inherit an old clipboard request.
|
||||
if (instanceService.getUnit(target.unitId) !== workbook || workbook.getSheetBySheetId(target.subUnitId) !== worksheet) {
|
||||
return false;
|
||||
}
|
||||
if (clipboardItems.length !== 0) {
|
||||
checkSheetClipboardPermission(accessor, PasteCommand.id, pasteParams);
|
||||
return sheetClipboardService.paste(clipboardItems[0], params?.value, target);
|
||||
}
|
||||
}
|
||||
|
||||
const lastCopyId = sheetClipboardService.copyContentCache().getLastCopyId();
|
||||
if (lastCopyId) {
|
||||
console.warn('Since the current environment does not support the Clipboard API, we will use the internal copyId to paste the content.');
|
||||
return sheetClipboardService.pasteByCopyId(lastCopyId, params?.value, target);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetPasteShortKeyCommand: ICommand = {
|
||||
id: SheetPasteShortKeyCommandName,
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: ISheetPasteByShortKeyParams) => {
|
||||
const clipboardService = accessor.get(ISheetClipboardService);
|
||||
const target = params.target ?? clipboardService.capturePasteTarget();
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
checkSheetClipboardPermission(accessor, PasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE,
|
||||
target,
|
||||
});
|
||||
const { htmlContent, textContent, files, formulaClipboardPayload } = params;
|
||||
return clipboardService.legacyPaste(htmlContent, textContent, files, formulaClipboardPayload, target);
|
||||
},
|
||||
};
|
||||
|
||||
function checkSheetClipboardPermission(accessor: IAccessor, commandId: string, params?: ISheetPasteParams): void {
|
||||
const permissionCheckController = accessor.get(SheetPermissionCheckController);
|
||||
let permission = true;
|
||||
let errorKey: LocaleKey = 'sheets-ui.permission.dialog.commonErr';
|
||||
|
||||
switch (commandId) {
|
||||
case CopyCommand.id:
|
||||
permission = permissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookCopyPermission],
|
||||
worksheetTypes: [WorksheetCopyPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
});
|
||||
errorKey = 'sheets-ui.permission.dialog.copyErr';
|
||||
break;
|
||||
case CutCommand.id:
|
||||
permission = permissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookCopyPermission, WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetCopyPermission, WorksheetEditPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint, RangeProtectionPermissionEditPoint],
|
||||
});
|
||||
errorKey = 'sheets-ui.permission.dialog.cutErr';
|
||||
break;
|
||||
case PasteCommand.id:
|
||||
permission = checkSheetPastePermission(permissionCheckController, params);
|
||||
errorKey = 'sheets-ui.permission.dialog.pasteErr';
|
||||
break;
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localeService = accessor.get(LocaleService);
|
||||
let errorMsg = localeService.t<LocaleKey>(errorKey);
|
||||
if (commandId === CopyCommand.id || commandId === CutCommand.id) {
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(instanceService);
|
||||
const permissionService = accessor.get(IPermissionService);
|
||||
if (
|
||||
target &&
|
||||
!permissionService.getPermissionPoint(new WorkbookCopyPermission(target.unitId).id)?.value
|
||||
) {
|
||||
errorMsg = localeService.t<LocaleKey>('sheets-ui.permission.dialog.workbookCopyErr');
|
||||
}
|
||||
}
|
||||
|
||||
permissionCheckController.blockExecuteWithoutPermission(errorMsg);
|
||||
}
|
||||
|
||||
function checkSheetPastePermission(
|
||||
permissionCheckController: SheetPermissionCheckController,
|
||||
params?: ISheetPasteParams
|
||||
): boolean {
|
||||
const target = params?.target;
|
||||
const ranges = target ? [target.range] : undefined;
|
||||
if (
|
||||
params?.value === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE ||
|
||||
params?.value === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA ||
|
||||
params?.value === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT
|
||||
) {
|
||||
return permissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetSetCellStylePermission, WorksheetEditPermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}, ranges, target?.unitId, target?.subUnitId);
|
||||
}
|
||||
|
||||
if (params?.value === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH) {
|
||||
return permissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [
|
||||
WorksheetEditPermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
WorksheetSetCellStylePermission,
|
||||
WorksheetSetColumnStylePermission,
|
||||
],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}, ranges, target?.unitId, target?.subUnitId);
|
||||
}
|
||||
|
||||
return permissionCheckController.permissionCheckWithRanges({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetSetCellValuePermission, WorksheetSetCellStylePermission, WorksheetEditPermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}, ranges, target?.unitId, target?.subUnitId);
|
||||
}
|
||||
|
||||
export const SheetPasteValueCommand: ICommand = {
|
||||
id: 'sheet.command.paste-value',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetPasteFormatCommand: ICommand = {
|
||||
id: 'sheet.command.paste-format',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetPasteColWidthCommand: ICommand = {
|
||||
id: 'sheet.command.paste-col-width',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetPasteBesidesBorderCommand: ICommand = {
|
||||
id: 'sheet.command.paste-besides-border',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_BESIDES_BORDER,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetOptionalPasteCommand: ICommand = {
|
||||
id: 'sheet.command.optional-paste',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor, { type }: { type: IPasteHookKeyType }) => {
|
||||
const clipboardService = accessor.get(ISheetClipboardService);
|
||||
|
||||
return clipboardService.rePasteWithPasteType(type);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { ICommand, IRange } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, getIntersectRange, ICommandService, IConfirmService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { DeleteRangeMoveLeftCommand, getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
export const DeleteRangeMoveLeftConfirmCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.delete-range-move-left-confirm',
|
||||
handler: async (accessor) => {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const selection = selectionManagerService.getCurrentSelections();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
let range = selection[0].range;
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
range = { ...range, endColumn: worksheet.getColumnCount() - 1 };
|
||||
|
||||
const getRowLength = (range: IRange) => range.endRow - range.startRow;
|
||||
const mergeData = worksheet.getMergeData().find((mergeRange) => {
|
||||
const interSectedRange = getIntersectRange(mergeRange, range);
|
||||
return interSectedRange ? getRowLength(mergeRange) > getRowLength(interSectedRange) : false;
|
||||
});
|
||||
|
||||
if (!mergeData) {
|
||||
return commandService.executeCommand(DeleteRangeMoveLeftCommand.id);
|
||||
}
|
||||
|
||||
const result = await confirmService.confirm({
|
||||
id: DeleteRangeMoveLeftConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.warning') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.dismantleMergeCellWarning') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (result) {
|
||||
return commandService.executeCommand(DeleteRangeMoveLeftCommand.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 { ICommand, IRange } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, getIntersectRange, ICommandService, IConfirmService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { DeleteRangeMoveUpCommand, getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
export const DeleteRangeMoveUpConfirmCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.delete-range-move-up-confirm',
|
||||
handler: async (accessor) => {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const selection = selectionManagerService.getCurrentSelections();
|
||||
if (!selection) return false;
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
let range = selection[0].range;
|
||||
if (!range) return false;
|
||||
|
||||
range = { ...range, endRow: worksheet.getRowCount() - 1 };
|
||||
|
||||
for (let i = range.startRow; i <= range.endRow; i++) {
|
||||
if (worksheet.getRowFiltered(i)) {
|
||||
const result = await confirmService.confirm({
|
||||
id: DeleteRangeMoveUpConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.filter.confirm.error') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.filter.confirm.notAllowedToInsertRange') },
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getColLength = (range: IRange) => range.endColumn - range.startColumn;
|
||||
const mergeData = worksheet.getMergeData().find((mergeRange) => {
|
||||
const interSectedRange = getIntersectRange(mergeRange, range);
|
||||
return interSectedRange ? getColLength(mergeRange) > getColLength(interSectedRange) : false;
|
||||
});
|
||||
|
||||
if (!mergeData) {
|
||||
return commandService.executeCommand(DeleteRangeMoveUpCommand.id);
|
||||
}
|
||||
|
||||
const result = await confirmService.confirm({
|
||||
id: DeleteRangeMoveUpConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.warning') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.dismantleMergeCellWarning') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (result) {
|
||||
return commandService.executeCommand(DeleteRangeMoveUpCommand.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { SpreadsheetColumnHeader, SpreadsheetRowHeader } from '@univerjs/engine-render';
|
||||
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SHEET_VIEW_KEY } from '../../common/keys';
|
||||
import { SheetSkeletonManagerService } from '../../services/sheet-skeleton-manager.service';
|
||||
|
||||
export interface ISetHeaderSizeParams {
|
||||
size: number;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
|
||||
export const SetRowHeaderWidthCommand: ICommand<ISetHeaderSizeParams> = {
|
||||
id: 'sheet.command.set-row-header-width',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderManagerSrv = accessor.get(IRenderManagerService);
|
||||
if (!renderManagerSrv) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId, size } = params;
|
||||
const render = renderManagerSrv.getRenderUnitById(unitId);
|
||||
if (render) {
|
||||
const skm = renderManagerSrv.getRenderUnitById(unitId)!.with(SheetSkeletonManagerService);
|
||||
skm.setRowHeaderSize(render, subUnitId, size);
|
||||
|
||||
const { components } = render;
|
||||
const renderComponent = components.get(SHEET_VIEW_KEY.ROW);
|
||||
if (renderComponent) {
|
||||
(renderComponent as SpreadsheetRowHeader).setCustomHeader({ headerStyle: { size } }, subUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetColumnHeaderHeightCommand: ICommand<ISetHeaderSizeParams> = {
|
||||
id: 'sheet.command.set-col-header-height',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderManagerSrv = accessor.get(IRenderManagerService);
|
||||
if (!renderManagerSrv) {
|
||||
return false;
|
||||
}
|
||||
const { unitId, subUnitId, size } = params;
|
||||
const render = renderManagerSrv.getRenderUnitById(unitId);
|
||||
if (render) {
|
||||
const skm = renderManagerSrv.getRenderUnitById(unitId)!.with(SheetSkeletonManagerService);
|
||||
skm.setColumnHeaderSize(render, subUnitId, size);
|
||||
const { components } = render;
|
||||
const renderComponent = components.get(SHEET_VIEW_KEY.COLUMN);
|
||||
if (renderComponent) {
|
||||
(renderComponent as SpreadsheetColumnHeader).setCustomHeader({ headerStyle: { size } }, subUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, ICommandService, IConfirmService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
SetColHiddenCommand,
|
||||
SetRowHiddenCommand,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { isAllColumnsCovered, isAllRowsCovered } from './utils/selection-utils';
|
||||
|
||||
export const HideRowConfirmCommand: ICommand = {
|
||||
id: 'sheet.command.hide-row-confirm',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
const ranges = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
|
||||
if (!ranges?.length) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
const allRowRanges = worksheet.getVisibleRows();
|
||||
|
||||
if (isAllRowsCovered(allRowRanges, ranges)) {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
|
||||
await confirmService.confirm({
|
||||
id: 'sheet.confirm.hide-row',
|
||||
title: {
|
||||
title: localeService.t<LocaleKey>('sheets-ui.info.problem'),
|
||||
},
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.rightClick.hideAllRowsAlert') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await commandService.executeCommand(SetRowHiddenCommand.id);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const HideColConfirmCommand: ICommand = {
|
||||
id: 'sheet.command.hide-col-confirm',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
const ranges = selectionManagerService.getCurrentSelections()?.map((s) => s.range);
|
||||
if (!ranges?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
const allColumnRanges = worksheet.getVisibleCols();
|
||||
|
||||
if (isAllColumnsCovered(allColumnRanges, ranges)) {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
|
||||
await confirmService.confirm({
|
||||
id: 'sheet.confirm.hide-col',
|
||||
title: {
|
||||
title: localeService.t<LocaleKey>('sheets-ui.info.problem'),
|
||||
},
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.rightClick.hideAllColumnsAlert') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await commandService.executeCommand(SetColHiddenCommand.id);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 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 { ICommand, Workbook } from '@univerjs/core';
|
||||
import { CommandType, DEFAULT_STYLES, EDITOR_ACTIVATED, ICommandService, IContextService, IUniverInstanceService, ThemeService, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
SetInlineFormatBoldCommand,
|
||||
SetInlineFormatFontFamilyCommand,
|
||||
SetInlineFormatFontSizeCommand,
|
||||
SetInlineFormatItalicCommand,
|
||||
SetInlineFormatStrikethroughCommand,
|
||||
SetInlineFormatSubscriptCommand,
|
||||
SetInlineFormatSuperscriptCommand,
|
||||
SetInlineFormatTextColorCommand,
|
||||
SetInlineFormatUnderlineCommand,
|
||||
} from '@univerjs/docs-ui';
|
||||
import {
|
||||
SetBoldCommand,
|
||||
SetFontFamilyCommand,
|
||||
SetFontSizeCommand,
|
||||
SetItalicCommand,
|
||||
SetStrikeThroughCommand,
|
||||
SetTextColorCommand,
|
||||
SetUnderlineCommand,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { getFontStyleAtCursor } from '../../menu/utils';
|
||||
|
||||
/**
|
||||
* It is used to set the bold style of selections or one cell, need to distinguish between
|
||||
* **selection state** and **edit state**. If you are in the selective state,
|
||||
* you need to set the style on the cell and the style on the rich text(p textRuns) at the same time,
|
||||
* and if it is only in edit state, then you only need to set the style of the rich text(p textRuns)
|
||||
*/
|
||||
export const SetRangeBoldCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-bold',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatBoldCommand.id);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetBoldCommand.id);
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeItalicCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-italic',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatItalicCommand.id);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetItalicCommand.id);
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeUnderlineCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-underline',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatUnderlineCommand.id);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetUnderlineCommand.id);
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeStrickThroughCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-stroke',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatStrikethroughCommand.id);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetStrikeThroughCommand.id);
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeSubscriptCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-subscript',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatSubscriptCommand.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeSuperscriptCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-superscript',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatSuperscriptCommand.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeFontSizeCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-fontsize',
|
||||
handler: async (accessor, params) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatFontSizeCommand.id, params);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetFontSizeCommand.id, params);
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeFontIncreaseCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-font-increase',
|
||||
handler: async (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
const defaultValue = DEFAULT_STYLES.fs;
|
||||
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
|
||||
const params = {
|
||||
value: defaultValue,
|
||||
};
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
if (textRun != null) {
|
||||
const value = textRun.ts?.fs ?? defaultValue;
|
||||
|
||||
params.value = value >= 400 ? 400 : value + 1;
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetInlineFormatFontSizeCommand.id, params);
|
||||
}
|
||||
|
||||
if (primary != null) {
|
||||
const style = worksheet.getComposedCellStyle(primary.startRow, primary.startColumn);
|
||||
const value = style.fs ?? defaultValue;
|
||||
params.value = value >= 400 ? 400 : value + 1;
|
||||
return commandService.executeCommand(SetFontSizeCommand.id, params);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeFontDecreaseCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-font-decrease',
|
||||
handler: async (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
const defaultValue = DEFAULT_STYLES.fs;
|
||||
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const primary = selectionManagerService.getCurrentLastSelection()?.primary;
|
||||
|
||||
const params = {
|
||||
value: defaultValue,
|
||||
};
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
const textRun = getFontStyleAtCursor(accessor);
|
||||
if (textRun != null) {
|
||||
const value = textRun.ts?.fs ?? defaultValue;
|
||||
|
||||
params.value = value <= 6 ? 6 : value - 1;
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetInlineFormatFontSizeCommand.id, params);
|
||||
}
|
||||
|
||||
if (primary != null) {
|
||||
const style = worksheet.getComposedCellStyle(primary.startRow, primary.startColumn);
|
||||
const value = style.fs ?? defaultValue;
|
||||
params.value = value <= 6 ? 6 : value - 1;
|
||||
return commandService.executeCommand(SetFontSizeCommand.id, params);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeFontFamilyCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-font-family',
|
||||
handler: async (accessor, params) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatFontFamilyCommand.id, params);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetFontFamilyCommand.id, params);
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRangeTextColorCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-text-color',
|
||||
handler: async (accessor, params) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatTextColorCommand.id, params);
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetTextColorCommand.id, params);
|
||||
},
|
||||
};
|
||||
|
||||
export const ResetRangeTextColorCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.reset-range-text-color',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const contextService = accessor.get(IContextService);
|
||||
const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
const themeService = accessor.get(ThemeService);
|
||||
|
||||
if (isCellEditorFocus) {
|
||||
return commandService.executeCommand(SetInlineFormatTextColorCommand.id, { value: null });
|
||||
}
|
||||
|
||||
return commandService.executeCommand(SetTextColorCommand.id, {
|
||||
value: themeService.getColorFromTheme('gray.900'),
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 { ICommand, IRange } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, getIntersectRange, ICommandService, IConfirmService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, InsertRangeMoveDownCommand, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
export const InsertRangeMoveDownConfirmCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.insert-range-move-down-confirm',
|
||||
handler: async (accessor) => {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const selection = selectionManagerService.getCurrentSelections();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
let range = selection[0].range;
|
||||
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
range = { ...range, endRow: worksheet.getRowCount() - 1 };
|
||||
|
||||
for (let i = range.startRow; i <= range.endRow; i++) {
|
||||
if (worksheet.getRowFiltered(i)) {
|
||||
const result = await confirmService.confirm({
|
||||
id: InsertRangeMoveDownConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.filter.confirm.error') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.filter.confirm.notAllowedToInsertRange') },
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getColLength = (range: IRange) => range.endColumn - range.startColumn;
|
||||
const mergeData = worksheet.getMergeData().find((mergeRange) => {
|
||||
const interSectedRange = getIntersectRange(mergeRange, range);
|
||||
return interSectedRange ? getColLength(mergeRange) > getColLength(interSectedRange) : false;
|
||||
});
|
||||
|
||||
if (!mergeData) {
|
||||
return commandService.executeCommand(InsertRangeMoveDownCommand.id);
|
||||
}
|
||||
|
||||
const result = await confirmService.confirm({
|
||||
id: InsertRangeMoveDownConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.warning') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.dismantleMergeCellWarning') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (result) {
|
||||
return commandService.executeCommand(InsertRangeMoveDownCommand.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 { ICommand, IRange } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, getIntersectRange, ICommandService, IConfirmService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, InsertRangeMoveRightCommand, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
export const InsertRangeMoveRightConfirmCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.insert-range-move-right-confirm',
|
||||
handler: async (accessor) => {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const selection = selectionManagerService.getCurrentSelections();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
|
||||
let range = selection[0].range;
|
||||
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
range = { ...range, endColumn: worksheet.getColumnCount() - 1 };
|
||||
|
||||
const getRowLength = (range: IRange) => range.endRow - range.startRow;
|
||||
|
||||
const mergeData = worksheet.getMergeData().find((mergeRange) => {
|
||||
const interSectedRange = getIntersectRange(mergeRange, range);
|
||||
return interSectedRange ? getRowLength(mergeRange) > getRowLength(interSectedRange) : false;
|
||||
});
|
||||
|
||||
if (!mergeData) {
|
||||
return commandService.executeCommand(InsertRangeMoveRightCommand.id);
|
||||
}
|
||||
|
||||
const result = await confirmService.confirm({
|
||||
id: InsertRangeMoveRightConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.warning') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.dismantleMergeCellWarning') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (result) {
|
||||
return commandService.executeCommand(InsertRangeMoveRightCommand.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { IMoveRangeCommandParams } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IConfirmService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
Rectangle,
|
||||
} from '@univerjs/core';
|
||||
import { findFirstNonEmptyCell, getSheetCommandTarget, MoveRangeCommand } from '@univerjs/sheets';
|
||||
|
||||
export const MoveRangeConfirmCommand: ICommand<IMoveRangeCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.move-range-confirm',
|
||||
handler: async (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const sourceTarget = getSheetCommandTarget(univerInstanceService, {
|
||||
unitId: params.fromUnitId,
|
||||
subUnitId: params.fromSubUnitId,
|
||||
});
|
||||
const targetTarget = getSheetCommandTarget(univerInstanceService, {
|
||||
unitId: params.toUnitId ?? params.fromUnitId,
|
||||
subUnitId: params.toSubUnitId ?? params.fromSubUnitId,
|
||||
});
|
||||
|
||||
if (!targetTarget) {
|
||||
return commandService.executeCommand(MoveRangeCommand.id, params);
|
||||
}
|
||||
|
||||
const isSameWorksheet = sourceTarget?.unitId === targetTarget.unitId
|
||||
&& sourceTarget.subUnitId === targetTarget.subUnitId;
|
||||
const targetRanges = isSameWorksheet
|
||||
? Rectangle.subtract(params.toRange, params.fromRange)
|
||||
: [params.toRange];
|
||||
const hasTargetContent = targetRanges.some(
|
||||
(range) => findFirstNonEmptyCell(range, targetTarget.worksheet) !== null
|
||||
);
|
||||
|
||||
if (hasTargetContent) {
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const confirmed = await accessor.get(IConfirmService).confirm({
|
||||
id: MoveRangeConfirmCommand.id,
|
||||
title: { title: localeService.t<LocaleKey>('sheets-ui.merge.confirm.warning') },
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.info.overwriteCellContent') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return commandService.executeCommand(MoveRangeCommand.id, params);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 { ICommand, Workbook } from '@univerjs/core';
|
||||
import type { IRangeProtectionRule } from '@univerjs/sheets';
|
||||
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, Rectangle, Tools, UniverInstanceType } from '@univerjs/core';
|
||||
import { AddRangeProtectionMutation, DeleteRangeProtectionMutation, DeleteWorksheetProtectionCommand, RangeProtectionRuleModel, SheetsSelectionsService, WorksheetProtectionRuleModel } from '@univerjs/sheets';
|
||||
import { SheetPermissionOpenPanelOperation } from '../operations/sheet-permission-open-panel.operation';
|
||||
|
||||
export interface IAddRangeProtectionParams {
|
||||
permissionId: string;
|
||||
rule: IRangeProtectionRule;
|
||||
}
|
||||
|
||||
export type ISetRangeProtectionParams = IAddRangeProtectionParams;
|
||||
|
||||
export interface IDeleteRangeProtectionParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
rule: IRangeProtectionRule;
|
||||
}
|
||||
|
||||
export const AddRangeProtectionFromToolbarCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.add-range-protection-from-toolbar',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { showDetail: true });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const AddRangeProtectionFromContextMenuCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.add-range-protection-from-context-menu',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { showDetail: true });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ViewSheetPermissionFromContextMenuCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.view-sheet-permission-from-context-menu',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { showDetail: false });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const AddRangeProtectionFromSheetBarCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.add-range-protection-from-sheet-bar',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { fromSheetBar: true, showDetail: true });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ViewSheetPermissionFromSheetBarCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.view-sheet-permission-from-sheet-bar',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { showDetail: false });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const DeleteRangeProtectionFromContextMenuCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.delete-range-protection-from-context-menu',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const worksheetRuleModel = accessor.get(WorksheetProtectionRuleModel);
|
||||
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet()!;
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
const worksheetRule = worksheetRuleModel.getRule(unitId, subUnitId);
|
||||
|
||||
if (worksheetRule?.permissionId) {
|
||||
return commandService.executeCommand(DeleteWorksheetProtectionCommand.id, { unitId, subUnitId, rule: worksheetRule });
|
||||
} else {
|
||||
const selectRange = selectionManagerService.getCurrentLastSelection()?.range;
|
||||
if (!selectRange) {
|
||||
return false;
|
||||
}
|
||||
const sheetPermissionRuleModal = accessor.get(RangeProtectionRuleModel);
|
||||
|
||||
const subRuleList = sheetPermissionRuleModal.getSubunitRuleList(unitId, subUnitId);
|
||||
const rule = subRuleList.find((item) => {
|
||||
return item.ranges.some((range) => Rectangle.intersects(range, selectRange));
|
||||
});
|
||||
|
||||
if (rule) {
|
||||
const redoMutationParam = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ruleIds: [rule.id],
|
||||
};
|
||||
const result = await commandService.executeCommand(DeleteRangeProtectionMutation.id, redoMutationParam);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations: [{ id: DeleteRangeProtectionMutation.id, params: redoMutationParam }],
|
||||
undoMutations: [{ id: AddRangeProtectionMutation.id, params: { unitId, subUnitId, rules: [rule] } }],
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export const SetRangeProtectionFromContextMenuCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-range-protection-from-context-menu',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const worksheetRuleModel = accessor.get(WorksheetProtectionRuleModel);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet()!;
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
const worksheetRule = worksheetRuleModel.getRule(unitId, subUnitId);
|
||||
if (worksheetRule?.permissionId) {
|
||||
const oldRule = {
|
||||
...worksheetRule,
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: [],
|
||||
};
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { showDetail: true, rule: Tools.deepClone(oldRule), oldRule: Tools.deepClone(oldRule) });
|
||||
return true;
|
||||
} else {
|
||||
const selectRange = selectionManagerService.getCurrentLastSelection()?.range;
|
||||
if (!selectRange) {
|
||||
return false;
|
||||
}
|
||||
const sheetPermissionRuleModal = accessor.get(RangeProtectionRuleModel);
|
||||
const subRuleList = sheetPermissionRuleModal.getSubunitRuleList(unitId, subUnitId);
|
||||
const rule = subRuleList.find((item) => {
|
||||
return item?.ranges?.some((range) => Rectangle.intersects(range, selectRange));
|
||||
});
|
||||
|
||||
if (rule) {
|
||||
const oldRule = {
|
||||
...rule,
|
||||
unitId,
|
||||
subUnitId,
|
||||
};
|
||||
await commandService.executeCommand(SheetPermissionOpenPanelOperation.id, { showDetail: true, rule: Tools.deepClone(oldRule), oldRule: Tools.deepClone(oldRule) });
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IRemoveRowColCommandParams } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, ICommandService, IConfirmService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, RemoveColCommand, RemoveRowCommand, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { isAllColumnsCovered, isAllRowsCovered } from './utils/selection-utils';
|
||||
|
||||
export const RemoveRowConfirmCommand: ICommand = {
|
||||
id: 'sheet.command.remove-row-confirm',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params?: IRemoveRowColCommandParams) => {
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
let range = params?.range;
|
||||
if (!range) {
|
||||
range = selectionManagerService.getCurrentLastSelection()?.range;
|
||||
}
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
const allRowRanges = worksheet.getVisibleRows();
|
||||
|
||||
if (isAllRowsCovered(allRowRanges, [range])) {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
|
||||
await confirmService.confirm({
|
||||
id: 'sheet.confirm.remove-row',
|
||||
title: {
|
||||
title: localeService.t<LocaleKey>('sheets-ui.info.problem'),
|
||||
},
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.rightClick.deleteAllRowsAlert') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await commandService.executeCommand(RemoveRowCommand.id, { range });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const RemoveColConfirmCommand: ICommand = {
|
||||
id: 'sheet.command.remove-col-confirm',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params?: IRemoveRowColCommandParams) => {
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
let range = params?.range;
|
||||
if (!range) {
|
||||
range = selectionManagerService.getCurrentLastSelection()?.range;
|
||||
}
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet } = target;
|
||||
const allColumnRanges = worksheet.getVisibleCols();
|
||||
|
||||
if (isAllColumnsCovered(allColumnRanges, [range])) {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
|
||||
await confirmService.confirm({
|
||||
id: 'sheet.confirm.remove-col',
|
||||
title: {
|
||||
title: localeService.t<LocaleKey>('sheets-ui.info.problem'),
|
||||
},
|
||||
children: { title: localeService.t<LocaleKey>('sheets-ui.rightClick.deleteAllColumnsAlert') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await commandService.executeCommand(RemoveColCommand.id, { range });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IUniverSheetsConfig } from '@univerjs/sheets';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IConfirmService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
countCells,
|
||||
defaultLargeSheetOperationConfig,
|
||||
getSheetCommandTarget,
|
||||
RemoveSheetCommand,
|
||||
SHEETS_PLUGIN_CONFIG_KEY,
|
||||
} from '@univerjs/sheets';
|
||||
|
||||
interface IRemoveSheetConfirmCommandParams {
|
||||
subUnitId: string;
|
||||
}
|
||||
|
||||
export const RemoveSheetConfirmCommand: ICommand = {
|
||||
id: 'sheet.command.remove-sheet-confirm',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: IRemoveSheetConfirmCommandParams) => {
|
||||
const { subUnitId } = params;
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const configService = accessor.get(IConfigService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
// Check if this is a large sheet that needs confirmation
|
||||
const target = getSheetCommandTarget(univerInstanceService, { subUnitId });
|
||||
if (!target) return false;
|
||||
const { worksheet } = target;
|
||||
|
||||
const pluginConfig = configService.getConfig<IUniverSheetsConfig>(SHEETS_PLUGIN_CONFIG_KEY);
|
||||
const largeSheetConfig = {
|
||||
...defaultLargeSheetOperationConfig,
|
||||
...pluginConfig?.largeSheetOperation,
|
||||
};
|
||||
const cellCount = countCells(worksheet.getCellMatrix());
|
||||
const isLargeSheet = cellCount >= largeSheetConfig.largeSheetCellCountThreshold;
|
||||
|
||||
// Only show confirmation dialog for large sheets
|
||||
const result = await confirmService.confirm({
|
||||
id: 'sheet.confirm.remove-sheet',
|
||||
title: {
|
||||
title: localeService.t<LocaleKey>('sheets-ui.sheetConfig.deleteSheet'),
|
||||
},
|
||||
children: { title: isLargeSheet ? localeService.t<LocaleKey>('sheets-ui.sheetConfig.deleteLargeSheetContent') : localeService.t<LocaleKey>('sheets-ui.sheetConfig.deleteSheetContent') },
|
||||
cancelText: localeService.t<LocaleKey>('sheets-ui.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('sheets-ui.button.confirm'),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await commandService.executeCommand(RemoveSheetCommand.id, { subUnitId });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import { CommandType, LocaleService } from '@univerjs/core';
|
||||
import {
|
||||
RangeProtectionPermissionEditPoint,
|
||||
SheetPermissionCheckController,
|
||||
SheetsSelectionsService,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetSetCellStylePermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
WorksheetSetColumnStylePermission,
|
||||
WorksheetSetRowStylePermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { IRepeatLastActionService, RepeatLastActionPermission } from '../../services/repeat-last-action.service';
|
||||
|
||||
const permissionCheckMap = {
|
||||
[RepeatLastActionPermission.Editable]: {
|
||||
permissionTypes: {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
},
|
||||
errorMessageKey: 'sheets-ui.permission.dialog.editErr',
|
||||
},
|
||||
[RepeatLastActionPermission.CellStyle]: {
|
||||
permissionTypes: {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellStylePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
},
|
||||
errorMessageKey: 'sheets-ui.permission.dialog.setStyleErr',
|
||||
},
|
||||
[RepeatLastActionPermission.CellValue]: {
|
||||
permissionTypes: {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellValuePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
},
|
||||
errorMessageKey: 'sheets-ui.permission.dialog.editErr',
|
||||
},
|
||||
[RepeatLastActionPermission.RowStyle]: {
|
||||
permissionTypes: {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetSetRowStylePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
},
|
||||
errorMessageKey: 'sheets-ui.permission.dialog.setRowColStyleErr',
|
||||
},
|
||||
[RepeatLastActionPermission.ColumnStyle]: {
|
||||
permissionTypes: {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetSetColumnStylePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
},
|
||||
errorMessageKey: 'sheets-ui.permission.dialog.setRowColStyleErr',
|
||||
},
|
||||
};
|
||||
|
||||
export const RepeatLastActionCommand: ICommand = {
|
||||
id: 'sheet.command.repeat-last-action',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const selections = selectionManagerService.getCurrentSelections()?.map((selection) => selection.range);
|
||||
if (!selections?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const repeatLastActionService = accessor.get(IRepeatLastActionService);
|
||||
const sheetPermissionCheckController = accessor.get(SheetPermissionCheckController);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const actionPermissionType = repeatLastActionService.getActionPermission();
|
||||
|
||||
if (Array.isArray(actionPermissionType)) {
|
||||
for (const permissionType of actionPermissionType) {
|
||||
if (permissionType === RepeatLastActionPermission.None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const permissionConfig = permissionCheckMap[permissionType];
|
||||
const permission = sheetPermissionCheckController.permissionCheckWithRanges(permissionConfig.permissionTypes);
|
||||
if (!permission) {
|
||||
sheetPermissionCheckController.blockExecuteWithoutPermission(localeService.t(permissionConfig.errorMessageKey));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (actionPermissionType !== RepeatLastActionPermission.None) {
|
||||
const permissionConfig = permissionCheckMap[actionPermissionType];
|
||||
const permission = sheetPermissionCheckController.permissionCheckWithRanges(permissionConfig.permissionTypes);
|
||||
if (!permission) {
|
||||
sheetPermissionCheckController.blockExecuteWithoutPermission(localeService.t(permissionConfig.errorMessageKey));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return await repeatLastActionService.runWithoutRecording(selections);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand, IRange } from '@univerjs/core';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
} from '@univerjs/core';
|
||||
|
||||
import { FormatPainterStatus, IFormatPainterService } from '../../services/format-painter/format-painter.service';
|
||||
import { SetFormatPainterOperation } from '../operations/set-format-painter.operation';
|
||||
|
||||
export interface ISetFormatPainterCommandParams {
|
||||
status: FormatPainterStatus;
|
||||
}
|
||||
|
||||
export const SetInfiniteFormatPainterCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-infinite-format-painter',
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const formatPainterService = accessor.get(IFormatPainterService);
|
||||
const status = formatPainterService.getStatus();
|
||||
let newStatus: FormatPainterStatus;
|
||||
if (status !== FormatPainterStatus.OFF) {
|
||||
newStatus = FormatPainterStatus.OFF;
|
||||
} else {
|
||||
newStatus = FormatPainterStatus.INFINITE;
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return commandService.executeCommand(SetFormatPainterOperation.id, { status: newStatus });
|
||||
},
|
||||
};
|
||||
|
||||
export const SetOnceFormatPainterCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-once-format-painter',
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const formatPainterService = accessor.get(IFormatPainterService);
|
||||
const status = formatPainterService.getStatus();
|
||||
let newStatus: FormatPainterStatus;
|
||||
if (status !== FormatPainterStatus.OFF) {
|
||||
newStatus = FormatPainterStatus.OFF;
|
||||
} else {
|
||||
newStatus = FormatPainterStatus.ONCE;
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SetFormatPainterOperation.id, { status: newStatus });
|
||||
},
|
||||
};
|
||||
|
||||
export interface IApplyFormatPainterCommandParams {
|
||||
subUnitId: string;
|
||||
unitId: string;
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
export const ApplyFormatPainterCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.apply-format-painter',
|
||||
handler: async (accessor: IAccessor, params: IApplyFormatPainterCommandParams) => {
|
||||
const formatPainterService = accessor.get(IFormatPainterService);
|
||||
return formatPainterService.applyFormatPainter(params.unitId, params.subUnitId, params.range);
|
||||
},
|
||||
};
|
||||
184
packages/sheets-ui/src/commands/commands/set-frozen.command.ts
Normal file
184
packages/sheets-ui/src/commands/commands/set-frozen.command.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { ISetFrozenMutationParams } from '@univerjs/sheets';
|
||||
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, RANGE_TYPE } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { getSheetCommandTarget, SetFrozenMutation, SetFrozenMutationFactory, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SheetScrollManagerService } from '../../services/scroll-manager.service';
|
||||
|
||||
export enum SetSelectionFrozenType {
|
||||
RowColumn = 0,
|
||||
Row = 1,
|
||||
Column = 2,
|
||||
FirstRow = 3,
|
||||
FirstColumn = 4,
|
||||
}
|
||||
|
||||
export interface ISetSelectionFrozenCommandParams {
|
||||
type?: SetSelectionFrozenType;
|
||||
}
|
||||
|
||||
export const SetSelectionFrozenCommand: ICommand<ISetSelectionFrozenCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-selection-frozen',
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: async (accessor, params) => {
|
||||
const { type } = params || {};
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
if (!selections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderManagerSrv = accessor.get(IRenderManagerService);
|
||||
const scrollManagerService = renderManagerSrv.getRenderUnitById(unitId)!.with(SheetScrollManagerService);
|
||||
const { sheetViewStartRow = 0, sheetViewStartColumn = 0 } = scrollManagerService.getCurrentScrollState() || {};
|
||||
|
||||
let startRow;
|
||||
let startColumn;
|
||||
let freezedRowCount;
|
||||
let freezedColCount;
|
||||
|
||||
if (type === SetSelectionFrozenType.FirstRow) {
|
||||
startRow = sheetViewStartRow + 1; ;
|
||||
freezedRowCount = 1;
|
||||
startColumn = -1;
|
||||
freezedColCount = 0;
|
||||
} else if (type === SetSelectionFrozenType.FirstColumn) {
|
||||
startRow = -1;
|
||||
freezedRowCount = 0;
|
||||
startColumn = sheetViewStartColumn + 1;
|
||||
freezedColCount = 1;
|
||||
} else {
|
||||
const currentSelection = selections[selections?.length - 1];
|
||||
const { primary, range } = currentSelection;
|
||||
let selectRow = primary?.startRow ?? range.startRow;
|
||||
if (selectRow === 0) {
|
||||
selectRow = 1;
|
||||
}
|
||||
let selectColumn = primary?.startColumn ?? range.startColumn;
|
||||
if (selectColumn === 0) {
|
||||
selectColumn = 1;
|
||||
}
|
||||
const rangeType = range.rangeType;
|
||||
|
||||
if (rangeType === RANGE_TYPE.ROW || type === SetSelectionFrozenType.Row) {
|
||||
// Frozen to Row
|
||||
startRow = selectRow;
|
||||
freezedRowCount = selectRow - sheetViewStartRow;
|
||||
startColumn = -1;
|
||||
freezedColCount = 0;
|
||||
} else if (rangeType === RANGE_TYPE.COLUMN || type === SetSelectionFrozenType.Column) {
|
||||
// Frozen to Column
|
||||
startRow = -1;
|
||||
freezedRowCount = 0;
|
||||
startColumn = selectColumn;
|
||||
freezedColCount = selectColumn - sheetViewStartColumn;
|
||||
} else if (rangeType === RANGE_TYPE.NORMAL) {
|
||||
// Frozen to Range
|
||||
startRow = selectRow;
|
||||
freezedRowCount = selectRow - sheetViewStartRow;
|
||||
startColumn = selectColumn;
|
||||
freezedColCount = selectColumn - sheetViewStartColumn;
|
||||
} else {
|
||||
// Unexpected value
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const redoMutationParams: ISetFrozenMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
startRow,
|
||||
startColumn,
|
||||
xSplit: startColumn > 0 ? Math.max(1, freezedColCount) : freezedColCount,
|
||||
ySplit: startRow > 0 ? Math.max(1, freezedRowCount) : freezedRowCount,
|
||||
};
|
||||
const undoMutationParams: ISetFrozenMutationParams = SetFrozenMutationFactory(accessor, redoMutationParams);
|
||||
|
||||
const result = commandService.syncExecuteCommand(SetFrozenMutation.id, redoMutationParams);
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [{ id: SetFrozenMutation.id, params: undoMutationParams }],
|
||||
redoMutations: [{ id: SetFrozenMutation.id, params: redoMutationParams }],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetRowFrozenCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-row-frozen',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
commandService.executeCommand(SetSelectionFrozenCommand.id, {
|
||||
type: SetSelectionFrozenType.Row,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetColumnFrozenCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-col-frozen',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
commandService.executeCommand(SetSelectionFrozenCommand.id, {
|
||||
type: SetSelectionFrozenType.Column,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetFirstRowFrozenCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-first-row-frozen',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
commandService.executeCommand(SetSelectionFrozenCommand.id, {
|
||||
type: SetSelectionFrozenType.FirstRow,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetFirstColumnFrozenCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-first-column-frozen',
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
commandService.executeCommand(SetSelectionFrozenCommand.id, {
|
||||
type: SetSelectionFrozenType.FirstColumn,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
225
packages/sheets-ui/src/commands/commands/set-scroll.command.ts
Normal file
225
packages/sheets-ui/src/commands/commands/set-scroll.command.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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 { ICommand, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IScrollState } from '../../services/scroll-manager.service';
|
||||
|
||||
import { CommandType, ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { SheetsScrollRenderController } from '../../controllers/render-controllers/scroll.render-controller';
|
||||
import { SheetScrollManagerService } from '../../services/scroll-manager.service';
|
||||
import { SetScrollOperation } from '../operations/scroll.operation';
|
||||
|
||||
export interface ISetScrollRelativeCommandParams {
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
}
|
||||
|
||||
export interface IScrollCommandParams {
|
||||
unitId?: string;
|
||||
sheetId?: string;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
/**
|
||||
* The index of row in spreadsheet.
|
||||
* e.g. if row start 10 at current viewport after freeze, and scroll value is zero, startRow is 0.
|
||||
* e.g. if scrolled about 2 rows, now top is 12, then sheetViewStartRow is 2.
|
||||
*/
|
||||
sheetViewStartRow?: number;
|
||||
|
||||
/**
|
||||
* Not the index of col in spreadsheet, but index of first column in current viewport.
|
||||
* e.g. if col start C at current viewport after freeze, and scroll value is zero, startColumn is 0.
|
||||
* e.g. if scrolled about 2 columns, now left is E, then sheetViewStartColumn is 2.
|
||||
*/
|
||||
sheetViewStartColumn?: number;
|
||||
|
||||
/**
|
||||
* The duration of the scroll animation in milliseconds.
|
||||
*/
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This command is used to manage the scroll by relative offset
|
||||
* Usually triggered by wheel event.
|
||||
* NOT same as ScrollCommand, which is usually triggered by scrollbar.
|
||||
*/
|
||||
export const SetScrollRelativeCommand: ICommand<ISetScrollRelativeCommandParams> = {
|
||||
id: 'sheet.command.set-scroll-relative',
|
||||
type: CommandType.COMMAND,
|
||||
// offsetXY derived from mouse wheel event
|
||||
// this._commandService.executeCommand(SetScrollRelativeCommand.id, { offsetY });
|
||||
handler: async (accessor, params: ISetScrollRelativeCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerSrv = accessor.get(IRenderManagerService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
const scrollManagerService = renderManagerSrv.getRenderUnitById(unitId)!.with(SheetScrollManagerService);
|
||||
const currentScroll = scrollManagerService.getCurrentScrollState();
|
||||
const { offsetX = 0, offsetY = 0 } = params || {};
|
||||
const {
|
||||
sheetViewStartRow = 0,
|
||||
sheetViewStartColumn = 0,
|
||||
offsetX: currentOffsetX = 0,
|
||||
offsetY: currentOffsetY = 0,
|
||||
} = currentScroll || {};
|
||||
// the receiver is scroll.operation.ts
|
||||
// const { xSplit, ySplit } = target.worksheet.getConfig().freeze;
|
||||
|
||||
return commandService.executeCommand(SetScrollOperation.id, {
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
|
||||
// why + ySplit? receiver - ySplit in scroll.operation.ts
|
||||
// sheetViewStartRow: sheetViewStartRow + ySplit,
|
||||
// sheetViewStartColumn: sheetViewStartColumn + xSplit,
|
||||
sheetViewStartRow,
|
||||
sheetViewStartColumn,
|
||||
offsetX: currentOffsetX + offsetX, // currentOffsetX + offsetX may be negative or over max
|
||||
offsetY: currentOffsetY + offsetY,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This command is used to manage the scroll position of the current view by specifying the cell index of the top left cell
|
||||
* Usually triggered by dragging scroll bar and click scroll track or moving selection range.
|
||||
* NOT same as SetScrollRelativeCommand which usually trigger by wheelevent.
|
||||
*/
|
||||
export const ScrollCommand: ICommand<IScrollCommandParams> = {
|
||||
id: 'sheet.command.scroll-view',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerSrv = accessor.get(IRenderManagerService);
|
||||
|
||||
const target = params.unitId
|
||||
? getScrollCommandTargetByParams(univerInstanceService, params)
|
||||
: getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { workbook, worksheet, unitId } = target;
|
||||
const renderUnit = renderManagerSrv.getRenderUnitById(unitId);
|
||||
if (!renderUnit) return false;
|
||||
|
||||
const scrollManagerService = renderUnit.with(SheetScrollManagerService);
|
||||
const currentScroll: Readonly<Nullable<IScrollState>> = scrollManagerService.getCurrentScrollState();
|
||||
|
||||
if (!worksheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { sheetViewStartRow, sheetViewStartColumn, offsetX, offsetY, duration } = params;
|
||||
const {
|
||||
sheetViewStartColumn: currentColumn,
|
||||
sheetViewStartRow: currentRow,
|
||||
offsetX: currentOffsetX,
|
||||
offsetY: currentOffsetY,
|
||||
} = currentScroll || {};
|
||||
const { xSplit, ySplit } = target.worksheet.getConfig().freeze;
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return commandService.syncExecuteCommand(SetScrollOperation.id, {
|
||||
unitId: workbook.getUnitId(),
|
||||
sheetId: worksheet.getSheetId(),
|
||||
// why + ySplit? receiver - ySplit in scroll.operation.ts
|
||||
// sheetViewStartRow: sheetViewStartRow + ySplit,
|
||||
// sheetViewStartColumn: sheetViewStartColumn + xSplit,
|
||||
sheetViewStartRow: sheetViewStartRow ?? (currentRow ?? 0 + ySplit),
|
||||
sheetViewStartColumn: sheetViewStartColumn ?? (currentColumn ?? 0 + xSplit),
|
||||
offsetX: offsetX ?? currentOffsetX,
|
||||
offsetY: offsetY ?? currentOffsetY,
|
||||
duration,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function getScrollCommandTargetByParams(
|
||||
univerInstanceService: IUniverInstanceService,
|
||||
params: Pick<IScrollCommandParams, 'unitId' | 'sheetId'>
|
||||
) {
|
||||
if (!params.unitId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(params.unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = params.sheetId ? workbook?.getSheetBySheetId(params.sheetId) : workbook?.getActiveSheet();
|
||||
if (!workbook || !worksheet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
workbook,
|
||||
worksheet,
|
||||
unitId: params.unitId,
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
};
|
||||
}
|
||||
|
||||
export interface IScrollToCellCommandParams {
|
||||
range: IRange;
|
||||
forceTop?: boolean;
|
||||
forceLeft?: boolean;
|
||||
unitId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command is used to scroll to the specific cell if the target cell is not in the viewport.
|
||||
*/
|
||||
export const ScrollToCellCommand: ICommand<IScrollToCellCommandParams> = {
|
||||
id: 'sheet.command.scroll-to-cell',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const scrollController = renderManagerService
|
||||
.getRenderUnitById(instanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SHEET)!.getUnitId())!
|
||||
.with(SheetsScrollRenderController);
|
||||
return scrollController.scrollToRange(params!.range, params!.forceTop, params!.forceLeft);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This command is reset the scroll position of the current view to 0, 0
|
||||
*/
|
||||
export const ResetScrollCommand: ICommand = {
|
||||
id: 'sheet.command.scroll-view-reset',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { subUnitId, unitId } = target;
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SetScrollOperation.id, {
|
||||
unitId,
|
||||
sheetId: subUnitId,
|
||||
sheetViewStartRow: 0,
|
||||
sheetViewStartColumn: 0,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,533 @@
|
||||
/**
|
||||
* 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 { ICommand, IMutationInfo, IRange } from '@univerjs/core';
|
||||
import type {
|
||||
ISetSelectionsOperationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import { CommandType, Direction, ICommandService, IUniverInstanceService, RANGE_TYPE, Rectangle, sequenceExecute, Tools } from '@univerjs/core';
|
||||
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
expandToContinuousRange,
|
||||
getCellAtRowCol,
|
||||
getSelectionsService,
|
||||
getSheetCommandTarget,
|
||||
SelectionMoveType,
|
||||
SetSelectionsOperation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { SelectAllService } from '../../services/select-all/select-all.service';
|
||||
import { ISheetSelectionRenderService } from '../../services/selection/base-selection-render.service';
|
||||
import { ShortcutExperienceService } from '../../services/shortcut-experience.service';
|
||||
import {
|
||||
checkIfShrink,
|
||||
expandToNextCell,
|
||||
expandToNextGapRange,
|
||||
expandToWholeSheet,
|
||||
findNextGapRange,
|
||||
findNextRange,
|
||||
getStartRange,
|
||||
shrinkToNextCell,
|
||||
shrinkToNextGapRange,
|
||||
} from './utils/selection-utils';
|
||||
|
||||
// TODO@DR-UNIVER: moveStepPage and moveStepEnd implement
|
||||
|
||||
export enum JumpOver {
|
||||
moveStopeOne,
|
||||
moveGap,
|
||||
moveStepPage,
|
||||
moveStepEnd,
|
||||
}
|
||||
|
||||
export interface IMoveSelectionCommandParams {
|
||||
direction: Direction;
|
||||
jumpOver?: JumpOver;
|
||||
nextStep?: number;
|
||||
extra?: string;
|
||||
fromCurrentSelection?: boolean;
|
||||
}
|
||||
|
||||
export interface IMoveSelectionEnterAndTabCommandParams {
|
||||
direction: Direction;
|
||||
keycode: KeyCode;
|
||||
extra?: string;
|
||||
fromCurrentSelection?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move selection (Mainly by keyboard arrow keys, For Tab and Enter key, check @MoveSelectionEnterAndTabCommand)
|
||||
*/
|
||||
export const MoveSelectionCommand: ICommand<IMoveSelectionCommandParams> = {
|
||||
id: 'sheet.command.move-selection',
|
||||
type: CommandType.COMMAND,
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
|
||||
if (!target) return false;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const selection = getSelectionsService(accessor, params.fromCurrentSelection).getCurrentLastSelection();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { direction, jumpOver, extra } = params;
|
||||
let { range, primary } = selection;
|
||||
|
||||
if (extra === 'formula-editor') {
|
||||
// When in formula editor, if the current cell is a merged cell, expand the selection to the merged cell range for calculation.
|
||||
if (range.startRow === range.endRow && range.startColumn === range.endColumn) {
|
||||
const mergedCell = worksheet.getMergedCell(range.startRow, range.startColumn);
|
||||
if (mergedCell) {
|
||||
range = {
|
||||
...range,
|
||||
...mergedCell,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// When in formula editor, try to use the last primary cell in ref selections as the primary cell for calculation.
|
||||
if (!primary) {
|
||||
const lastSelectionPrimary = getSelectionsService(accessor, params.fromCurrentSelection).getCurrentLastSelectionPrimaryCell();
|
||||
if (lastSelectionPrimary && Rectangle.contains(range, lastSelectionPrimary)) {
|
||||
primary = lastSelectionPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startRange = getStartRange(range, primary, direction);
|
||||
|
||||
// the start range is from the primary selection range
|
||||
const next =
|
||||
jumpOver === JumpOver.moveGap
|
||||
? findNextGapRange(startRange, direction, worksheet)
|
||||
: findNextRange(startRange, direction, worksheet);
|
||||
const destRange = getCellAtRowCol(next.startRow, next.startColumn, worksheet);
|
||||
|
||||
if (Rectangle.equals(destRange, startRange)) {
|
||||
return false;
|
||||
}
|
||||
const unitId = workbook.getUnitId();
|
||||
// If there are changes to the selection, clear the start position saved by the tab.
|
||||
// This function works in conjunction with the enter and tab shortcuts.
|
||||
accessor.get(ShortcutExperienceService).remove({
|
||||
unitId,
|
||||
sheetId: worksheet.getSheetId(),
|
||||
keycode: KeyCode.TAB,
|
||||
});
|
||||
|
||||
const selections = [
|
||||
{
|
||||
range: Rectangle.clone(destRange),
|
||||
primary: {
|
||||
startRow: destRange.startRow,
|
||||
startColumn: destRange.startColumn,
|
||||
endRow: destRange.endRow,
|
||||
endColumn: destRange.endColumn,
|
||||
actualRow: next.startRow,
|
||||
actualColumn: next.startColumn,
|
||||
isMerged: destRange.isMerged,
|
||||
isMergedMainCell:
|
||||
destRange.startRow === next.startRow && destRange.startColumn === next.startColumn,
|
||||
},
|
||||
},
|
||||
];
|
||||
const rs = accessor.get(ICommandService).executeCommand(SetSelectionsOperation.id, {
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
selections,
|
||||
type: SelectionMoveType.MOVE_END,
|
||||
extra,
|
||||
fromCurrentSelection: params.fromCurrentSelection,
|
||||
reveal: true,
|
||||
} as ISetSelectionsOperationParams);
|
||||
return rs;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Move selection for enter and tab.
|
||||
*/
|
||||
export const MoveSelectionEnterAndTabCommand: ICommand<IMoveSelectionEnterAndTabCommandParams> = {
|
||||
id: 'sheet.command.move-selection-enter-tab',
|
||||
type: CommandType.COMMAND,
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
|
||||
if (!target) return false;
|
||||
|
||||
const { workbook, worksheet } = target;
|
||||
const selectionsService = getSelectionsService(accessor, params.fromCurrentSelection);
|
||||
const { direction, keycode } = params;
|
||||
const isReverse = direction === Direction.LEFT || direction === Direction.UP;
|
||||
|
||||
const selections = selectionsService.getCurrentSelections().concat();
|
||||
const currentSelectionIndex = selections.findIndex((s) => s.primary);
|
||||
const selection = selections[currentSelectionIndex];
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
// for shift tab or shift enter, the direction should be reversed. so we need find the previous selection.
|
||||
const delta = isReverse ? -1 : 1;
|
||||
const nextSelectionIndex = (currentSelectionIndex + delta + selections.length) % selections.length;
|
||||
const nextSelection = selections[nextSelectionIndex];
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const sheetId = worksheet.getSheetId();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { range } = selection;
|
||||
const primary = selection.primary!;
|
||||
let startRange = getStartRange(range, primary, direction);
|
||||
const shortcutExperienceService = accessor.get(ShortcutExperienceService);
|
||||
|
||||
const shortcutExperienceParam = shortcutExperienceService.getCurrentBySearch({
|
||||
unitId,
|
||||
sheetId,
|
||||
keycode: KeyCode.TAB,
|
||||
});
|
||||
|
||||
let resultRange;
|
||||
|
||||
const { startRow, endRow, startColumn, endColumn } = range;
|
||||
|
||||
// for the last cell in the selection, the next cell should be the next selection's start cell.
|
||||
const isLastCell = isReverse ? primary.startRow === startRow && primary.startColumn === startColumn : primary.endRow === endRow && primary.endColumn === endColumn;
|
||||
// not single cell should be handled by the move-selection active cell
|
||||
if (!Rectangle.equals(range, primary)) {
|
||||
// Handle the situation of moving the active cell within the selection area.
|
||||
shortcutExperienceService.remove({
|
||||
unitId,
|
||||
sheetId,
|
||||
keycode: KeyCode.TAB,
|
||||
});
|
||||
|
||||
const newPrimary = Tools.deepClone(primary);
|
||||
|
||||
const next = isLastCell
|
||||
? nextSelection.range :
|
||||
findNextRange(
|
||||
{
|
||||
startRow: newPrimary.startRow,
|
||||
startColumn: newPrimary.startColumn,
|
||||
endRow: newPrimary.endRow,
|
||||
endColumn: newPrimary.endColumn,
|
||||
},
|
||||
direction,
|
||||
worksheet,
|
||||
{
|
||||
startRow,
|
||||
endRow,
|
||||
startColumn,
|
||||
endColumn,
|
||||
}
|
||||
);
|
||||
|
||||
const useLeftTopAsDest = isLastCell && isReverse;
|
||||
const destRange = useLeftTopAsDest ? getCellAtRowCol(next.endRow, next.endColumn, worksheet) : getCellAtRowCol(next.startRow, next.startColumn, worksheet);
|
||||
|
||||
resultRange = {
|
||||
range: isLastCell ? nextSelection.range : Rectangle.clone(range),
|
||||
primary: {
|
||||
startRow: destRange.startRow,
|
||||
startColumn: destRange.startColumn,
|
||||
endRow: destRange.endRow,
|
||||
endColumn: destRange.endColumn,
|
||||
actualRow: useLeftTopAsDest ? destRange.startRow : next.startRow,
|
||||
actualColumn: useLeftTopAsDest ? destRange.startColumn : next.startColumn,
|
||||
isMerged: destRange.isMerged,
|
||||
isMergedMainCell:
|
||||
destRange.startRow === next.startRow && destRange.startColumn === next.startColumn,
|
||||
},
|
||||
style: isLastCell ? nextSelection.style : selection.style,
|
||||
};
|
||||
} else {
|
||||
// Handle the regular situation of moving the selection area.
|
||||
if (keycode === KeyCode.TAB) {
|
||||
if (direction === Direction.RIGHT && shortcutExperienceParam == null) {
|
||||
shortcutExperienceService.addOrUpdate({
|
||||
unitId,
|
||||
sheetId,
|
||||
keycode: KeyCode.TAB,
|
||||
selection,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const selectionRemain = shortcutExperienceParam?.selection;
|
||||
|
||||
if (selectionRemain != null) {
|
||||
const { range, primary } = selectionRemain;
|
||||
startRange = getStartRange(range, primary, direction);
|
||||
}
|
||||
|
||||
shortcutExperienceService.remove({
|
||||
unitId,
|
||||
sheetId,
|
||||
keycode: KeyCode.TAB,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The start range is from the primary selection range.
|
||||
* If the edited cell is the last row/column of the worksheet, and the submitted operation is Enter/Tab to move down/right, the next selected cell should still be the current cell (not move to the first row/column of the worksheet).
|
||||
*/
|
||||
const isLastRowCell = keycode === KeyCode.ENTER && direction === Direction.DOWN && startRange.endRow === worksheet.getMaxRows() - 1;
|
||||
const isLastColumnCell = keycode === KeyCode.TAB && direction === Direction.RIGHT && startRange.endColumn === worksheet.getMaxColumns() - 1;
|
||||
const next = (isLastRowCell || isLastColumnCell) ? startRange : findNextRange(startRange, direction, worksheet);
|
||||
const destRange = getCellAtRowCol(next.startRow, next.startColumn, worksheet);
|
||||
|
||||
if (Rectangle.equals(destRange, startRange)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
resultRange = {
|
||||
range: Rectangle.clone(destRange),
|
||||
primary: {
|
||||
startRow: destRange.startRow,
|
||||
startColumn: destRange.startColumn,
|
||||
endRow: destRange.endRow,
|
||||
endColumn: destRange.endColumn,
|
||||
actualRow: next.startRow,
|
||||
actualColumn: next.startColumn,
|
||||
isMerged: destRange.isMerged,
|
||||
isMergedMainCell:
|
||||
destRange.startRow === next.startRow && destRange.startColumn === next.startColumn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isLastCell) {
|
||||
selections[currentSelectionIndex].primary = null;
|
||||
selections[nextSelectionIndex] = resultRange;
|
||||
selectionsService.setSelections(unitId, sheetId, [], SelectionMoveType.MOVE_END);
|
||||
} else {
|
||||
selections[currentSelectionIndex] = resultRange;
|
||||
}
|
||||
|
||||
const rs = accessor.get(ICommandService).executeCommand(SetSelectionsOperation.id, {
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
type: SelectionMoveType.MOVE_END,
|
||||
selections,
|
||||
reveal: true,
|
||||
extra: params.extra,
|
||||
fromCurrentSelection: params.fromCurrentSelection,
|
||||
});
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const selectionService = renderManagerService.getRenderUnitById(unitId)?.with(ISheetSelectionRenderService);
|
||||
selectionService?.refreshSelectionMoveEnd();
|
||||
return rs;
|
||||
},
|
||||
};
|
||||
|
||||
export interface IExpandSelectionCommandParams {
|
||||
direction: Direction;
|
||||
jumpOver?: JumpOver;
|
||||
nextStep?: number;
|
||||
extra?: string;
|
||||
}
|
||||
|
||||
// Though the command's name is "expand-selection", it actually does not expand but shrink the selection.
|
||||
// That depends on the position of currentCell and the whole selection.
|
||||
export const ExpandSelectionCommand: ICommand<IExpandSelectionCommandParams> = {
|
||||
id: 'sheet.command.expand-selection',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) return false;
|
||||
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
|
||||
const selection = getSelectionsService(accessor).getCurrentLastSelection();
|
||||
if (!selection) return false;
|
||||
|
||||
const { jumpOver, direction, extra } = params;
|
||||
let { range: startRange, primary } = selection;
|
||||
|
||||
if (extra === 'formula-editor') {
|
||||
// When in formula editor, if the current cell is a merged cell, expand the selection to the merged cell range for calculation.
|
||||
if (startRange.startRow === startRange.endRow && startRange.startColumn === startRange.endColumn) {
|
||||
const mergedCell = worksheet.getMergedCell(startRange.startRow, startRange.startColumn);
|
||||
if (mergedCell) {
|
||||
startRange = {
|
||||
...startRange,
|
||||
...mergedCell,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// When in formula editor, try to use the last primary cell in ref selections as the primary cell for calculation.
|
||||
if (!primary) {
|
||||
const lastSelectionPrimary = getSelectionsService(accessor).getCurrentLastSelectionPrimaryCell();
|
||||
if (lastSelectionPrimary && Rectangle.contains(startRange, lastSelectionPrimary)) {
|
||||
primary = lastSelectionPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isShrink = checkIfShrink({ range: startRange, primary }, direction, worksheet);
|
||||
const destRange = !isShrink
|
||||
? jumpOver === JumpOver.moveGap
|
||||
? expandToNextGapRange(startRange, direction, worksheet)
|
||||
: expandToNextCell(startRange, direction, worksheet)
|
||||
: jumpOver === JumpOver.moveGap
|
||||
? shrinkToNextGapRange(
|
||||
startRange,
|
||||
{ ...Rectangle.clone(primary), rangeType: RANGE_TYPE.NORMAL },
|
||||
direction,
|
||||
worksheet
|
||||
)
|
||||
: shrinkToNextCell(startRange, direction, worksheet);
|
||||
destRange.rangeType = selection.range.rangeType;
|
||||
|
||||
if (Rectangle.equals(destRange, startRange)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return accessor.get(ICommandService).syncExecuteCommand(SetSelectionsOperation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
type: SelectionMoveType.MOVE_END,
|
||||
selections: [
|
||||
{
|
||||
range: destRange,
|
||||
primary, // this remains unchanged
|
||||
},
|
||||
],
|
||||
extra,
|
||||
reveal: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export interface ISelectAllCommandParams {
|
||||
expandToGapFirst?: boolean;
|
||||
loop?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This command expand selection to all neighbor ranges. If there are no neighbor ranges. Select the whole sheet.
|
||||
*/
|
||||
export const SelectAllCommand: ICommand<ISelectAllCommandParams> = {
|
||||
id: 'sheet.command.select-all',
|
||||
type: CommandType.COMMAND,
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: async (accessor, params = { expandToGapFirst: true, loop: false }) => {
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
|
||||
if (!target) return false;
|
||||
const selection = getSelectionsService(accessor).getCurrentLastSelection();
|
||||
if (!selection) return false;
|
||||
|
||||
const selectAllService = accessor.get(SelectAllService);
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const id = `${unitId}|${subUnitId}`;
|
||||
if (id !== selectAllService.selectedRangeWorksheet) {
|
||||
selectAllService.rangesStack = [];
|
||||
selectAllService.selectedRangeWorksheet = id;
|
||||
}
|
||||
|
||||
const maxRow = worksheet.getMaxRows();
|
||||
const maxCol = worksheet.getMaxColumns();
|
||||
const { expandToGapFirst, loop } = params;
|
||||
const { range, primary } = selection;
|
||||
const isWholeSheetSelected =
|
||||
range.endColumn === maxCol - 1 &&
|
||||
range.endRow === maxRow - 1 &&
|
||||
range.startRow === 0 &&
|
||||
range.startColumn === 0;
|
||||
|
||||
if (!selectAllService.rangesStack.some((s) => Rectangle.equals(s, range))) {
|
||||
selectAllService.rangesStack = [];
|
||||
selectAllService.rangesStack.push(range);
|
||||
}
|
||||
|
||||
let destRange: IRange;
|
||||
|
||||
// determined what kind of adjustment it should get
|
||||
if (isWholeSheetSelected) {
|
||||
if (loop) {
|
||||
const currentSelectionIndex = selectAllService.rangesStack.findIndex((s) => Rectangle.equals(s, range));
|
||||
if (currentSelectionIndex !== selectAllService.rangesStack.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
destRange = selectAllService.rangesStack[0];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (expandToGapFirst) {
|
||||
destRange = expandToContinuousRange(range, { left: true, right: true, up: true, down: true }, worksheet);
|
||||
if (Rectangle.equals(destRange, range)) {
|
||||
// already to gaps
|
||||
destRange = expandToWholeSheet(worksheet);
|
||||
}
|
||||
} else {
|
||||
destRange = expandToWholeSheet(worksheet);
|
||||
}
|
||||
|
||||
if (!selectAllService.rangesStack.some((s) => Rectangle.equals(s, destRange))) {
|
||||
selectAllService.rangesStack.push(destRange);
|
||||
}
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
|
||||
redos.push({
|
||||
id: SetSelectionsOperation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
reveal: true,
|
||||
selections: [
|
||||
{
|
||||
range: destRange,
|
||||
primary, // this remains unchanged
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
const interceptor = sheetInterceptorService.onCommandExecute({
|
||||
id: SelectAllCommand.id,
|
||||
params: { range },
|
||||
});
|
||||
|
||||
if (interceptor.redos.length) {
|
||||
redos.push(...interceptor.redos);
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const result = sequenceExecute(redos, commandService);
|
||||
|
||||
return result.result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand, IRange } from '@univerjs/core';
|
||||
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { AutoWidthController } from '../../controllers/auto-width.controller';
|
||||
|
||||
export interface ISetWorksheetColIsAutoWidthCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
ranges?: IRange[]; // For Facade API
|
||||
}
|
||||
|
||||
export const SetWorksheetColAutoWidthCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.set-col-auto-width',
|
||||
handler: (accessor: IAccessor, params?: ISetWorksheetColIsAutoWidthCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
|
||||
let ranges = [];
|
||||
if (params?.ranges) {
|
||||
ranges = [...params.ranges];
|
||||
} else {
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
ranges.push(selections[i].range);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ranges?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const redoMutationParams: Required<ISetWorksheetColIsAutoWidthCommandParams> = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges,
|
||||
};
|
||||
|
||||
// undos redos comes from auto-width.controller
|
||||
const { redos, undos } = accessor.get(AutoWidthController).getUndoRedoParamsOfColWidth(redoMutationParams);
|
||||
|
||||
const result = sequenceExecute([...redos], commandService);
|
||||
if (result.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [...undos],
|
||||
redoMutations: [...redos],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import { CommandType, ICommandService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { SHEET_ZOOM_RANGE } from '../../common/keys';
|
||||
import { SetZoomRatioCommand } from './set-zoom-ratio.command';
|
||||
|
||||
export interface ISetZoomRatioFromToolbarCommandParams {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const SetZoomRatioFromToolbarCommand: ICommand<ISetZoomRatioFromToolbarCommandParams> = {
|
||||
id: 'sheet.command.set-zoom-ratio-from-toolbar',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params || !Number.isFinite(params.value)) return false;
|
||||
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
|
||||
if (!target) return false;
|
||||
|
||||
const zoom = Math.min(SHEET_ZOOM_RANGE[1], Math.max(SHEET_ZOOM_RANGE[0], params.value));
|
||||
return accessor.get(ICommandService).executeCommand(SetZoomRatioCommand.id, {
|
||||
unitId: target.unitId,
|
||||
subUnitId: target.subUnitId,
|
||||
zoomRatio: zoom / 100,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import { CommandType, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, ICommandService, IUniverInstanceService } from '@univerjs/core';
|
||||
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { SHEET_ZOOM_RANGE } from '../../common/keys';
|
||||
import { IEditorBridgeService } from '../../services/editor-bridge.service';
|
||||
import { SetZoomRatioOperation } from '../operations/set-zoom-ratio.operation';
|
||||
|
||||
export interface ISetZoomRatioCommandParams {
|
||||
zoomRatio: number;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
export interface IChangeZoomRatioCommandParams {
|
||||
reset?: boolean;
|
||||
delta: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom
|
||||
*/
|
||||
|
||||
export const ChangeZoomRatioCommand: ICommand<IChangeZoomRatioCommandParams> = {
|
||||
id: 'sheet.command.change-zoom-ratio',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet, subUnitId, unitId } = target;
|
||||
const { delta, reset } = params;
|
||||
|
||||
let zoom = reset ? 100 : Math.round((worksheet.getConfig().zoomRatio + delta) * 100);
|
||||
zoom = Math.max(SHEET_ZOOM_RANGE[0], zoom);
|
||||
zoom = Math.min(SHEET_ZOOM_RANGE[1], zoom);
|
||||
|
||||
const zoomRatio = zoom / 100;
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
const state = editorBridgeService.isVisible();
|
||||
if ((state.unitId === unitId || state.unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) && state.visible) return false;
|
||||
|
||||
return accessor.get(ICommandService).executeCommand(SetZoomRatioOperation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
zoomRatio,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SetZoomRatioCommand: ICommand<ISetZoomRatioCommandParams> = {
|
||||
id: 'sheet.command.set-zoom-ratio',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, zoomRatio } = params;
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
const state = editorBridgeService.isVisible();
|
||||
if ((state.unitId === unitId || state.unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) && state.visible) return false;
|
||||
|
||||
return accessor.get(ICommandService).executeCommand(SetZoomRatioOperation.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
zoomRatio,
|
||||
});
|
||||
},
|
||||
};
|
||||
31
packages/sheets-ui/src/commands/commands/unhide.command.ts
Normal file
31
packages/sheets-ui/src/commands/commands/unhide.command.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
import { ISheetBarService } from '../../services/sheet-bar/sheet-bar.service';
|
||||
|
||||
export const ShowMenuListCommand: ICommand = {
|
||||
id: 'ui-sheet.command.show-menu-list',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const sheetBarService = accessor.get(ISheetBarService);
|
||||
sheetBarService.triggerSheetBarMenu();
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,649 @@
|
||||
/**
|
||||
* 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 { ICellData, IRange, ISelection, ISelectionCell, Nullable, ObjectMatrix, Worksheet } from '@univerjs/core';
|
||||
import { Direction, getReverseDirection, RANGE_TYPE, Rectangle } from '@univerjs/core';
|
||||
import { alignToMergedCellsBorders } from '@univerjs/sheets';
|
||||
|
||||
export enum MergeType {
|
||||
MergeAll = 'mergeAll',
|
||||
MergeVertical = 'mergeVertical',
|
||||
MergeHorizontal = 'mergeHorizontal',
|
||||
}
|
||||
|
||||
export interface IExpandParams {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
up?: boolean;
|
||||
down?: boolean;
|
||||
}
|
||||
|
||||
// TODO@wzhudev: methods in this file should use `worksheet.getCell()` instead of using raw data
|
||||
|
||||
export function findNextRange(
|
||||
startRange: IRange,
|
||||
direction: Direction,
|
||||
worksheet: Worksheet,
|
||||
boundary?: IRange,
|
||||
isFindNext: boolean = true,
|
||||
nextStep: number = 1,
|
||||
isGoBack: boolean = true
|
||||
): IRange {
|
||||
let destRange: IRange = { ...startRange };
|
||||
|
||||
/**
|
||||
* Set the boundaries for moving the selection area.
|
||||
* Here you can customize the boundaries to implement the ability to move the primary within the selection area.
|
||||
*/
|
||||
if (boundary == null) {
|
||||
boundary = {
|
||||
startRow: 0,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
startColumn: 0,
|
||||
endColumn: worksheet.getColumnCount() - 1,
|
||||
};
|
||||
}
|
||||
|
||||
let next: number;
|
||||
switch (direction) {
|
||||
case Direction.UP:
|
||||
next = startRange.startRow - nextStep;
|
||||
while (next > -1 && !worksheet.getRowVisible(next)) {
|
||||
next -= 1;
|
||||
}
|
||||
if (next >= boundary.startRow) {
|
||||
destRange.startRow = next;
|
||||
destRange.endRow = next;
|
||||
} else if (isGoBack) {
|
||||
destRange.startRow = boundary.endRow;
|
||||
destRange.endRow = boundary.endRow;
|
||||
|
||||
if (isFindNext) {
|
||||
destRange = findNextRange(destRange, Direction.LEFT, worksheet, boundary, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.DOWN:
|
||||
next = startRange.endRow + nextStep;
|
||||
while (next < worksheet.getRowCount() && !worksheet.getRowVisible(next)) {
|
||||
next += 1;
|
||||
}
|
||||
if (next <= boundary.endRow) {
|
||||
destRange.startRow = next;
|
||||
destRange.endRow = next;
|
||||
} else if (isGoBack) {
|
||||
destRange.startRow = boundary.startRow;
|
||||
destRange.endRow = boundary.startRow;
|
||||
|
||||
if (isFindNext) {
|
||||
destRange = findNextRange(destRange, Direction.RIGHT, worksheet, boundary, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.LEFT:
|
||||
next = startRange.startColumn - nextStep;
|
||||
while (next > -1 && !worksheet.getColVisible(next)) {
|
||||
next -= 1;
|
||||
}
|
||||
if (next >= boundary.startColumn) {
|
||||
destRange.startColumn = next;
|
||||
destRange.endColumn = next;
|
||||
} else if (isGoBack) {
|
||||
destRange.startColumn = boundary.endColumn;
|
||||
destRange.endColumn = boundary.endColumn;
|
||||
|
||||
if (isFindNext) {
|
||||
destRange = findNextRange(destRange, Direction.UP, worksheet, boundary, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Direction.RIGHT:
|
||||
next = startRange.endColumn + nextStep;
|
||||
while (next < worksheet.getColumnCount() && !worksheet.getColVisible(next)) {
|
||||
next += 1;
|
||||
}
|
||||
if (next <= boundary.endColumn) {
|
||||
destRange.startColumn = next;
|
||||
destRange.endColumn = next;
|
||||
} else if (isGoBack) {
|
||||
destRange.startColumn = boundary.startColumn;
|
||||
destRange.endColumn = boundary.startColumn;
|
||||
|
||||
if (isFindNext) {
|
||||
destRange = findNextRange(destRange, Direction.DOWN, worksheet, boundary, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return destRange;
|
||||
}
|
||||
|
||||
export function findNextGapRange(startRange: IRange, direction: Direction, worksheet: Worksheet): IRange {
|
||||
const destRange = { ...startRange };
|
||||
const { startRow, startColumn, endRow, endColumn } = getEdgeOfRange(startRange, direction, worksheet);
|
||||
|
||||
let currentPositionHasValue = rangeHasValue(worksheet, startRow, startColumn, endRow, endColumn).hasValue;
|
||||
let firstMove = true;
|
||||
let shouldContinue = true;
|
||||
|
||||
while (shouldContinue) {
|
||||
if (Direction.UP === direction) {
|
||||
let next = destRange.startRow - 1;
|
||||
while (next > -1 && !worksheet.getRowVisible(next)) {
|
||||
next -= 1;
|
||||
}
|
||||
if (next === -1) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const { hasValue: nextRangeHasValue, matrix } = rangeHasValue(
|
||||
worksheet,
|
||||
next,
|
||||
destRange.startColumn,
|
||||
next,
|
||||
destRange.endColumn
|
||||
);
|
||||
|
||||
if (currentPositionHasValue && !nextRangeHasValue && !firstMove) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
} else {
|
||||
if (matrix.getLength() !== 0) {
|
||||
// update searching ranges
|
||||
let min = next;
|
||||
matrix.forValue((row) => {
|
||||
min = Math.min(row, min);
|
||||
});
|
||||
destRange.startRow = min;
|
||||
} else {
|
||||
destRange.startRow = next;
|
||||
}
|
||||
|
||||
destRange.endRow = destRange.startRow;
|
||||
|
||||
if (!currentPositionHasValue && nextRangeHasValue) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPositionHasValue = nextRangeHasValue;
|
||||
firstMove = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Direction.DOWN === direction) {
|
||||
let next = destRange.endRow + 1;
|
||||
while (next < worksheet.getRowCount() && !worksheet.getRowVisible(next)) {
|
||||
next += 1;
|
||||
}
|
||||
if (next === worksheet.getRowCount()) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const { hasValue: nextRangeHasValue, matrix } = rangeHasValue(
|
||||
worksheet,
|
||||
next,
|
||||
destRange.startColumn,
|
||||
next,
|
||||
destRange.endColumn
|
||||
);
|
||||
|
||||
if (currentPositionHasValue && !nextRangeHasValue && !firstMove) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
} else {
|
||||
if (matrix.getLength() !== 0) {
|
||||
let max = next;
|
||||
matrix.forValue((row, _, value) => {
|
||||
max = Math.max(row + (value.rowSpan || 1) - 1, max);
|
||||
});
|
||||
destRange.endRow = max;
|
||||
} else {
|
||||
destRange.endRow = next;
|
||||
}
|
||||
|
||||
destRange.startRow = destRange.endRow;
|
||||
|
||||
if (!currentPositionHasValue && nextRangeHasValue) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPositionHasValue = nextRangeHasValue;
|
||||
firstMove = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Direction.LEFT === direction) {
|
||||
let next = destRange.startColumn - 1;
|
||||
while (next > -1 && !worksheet.getColVisible(next)) {
|
||||
next -= 1;
|
||||
}
|
||||
if (next === -1) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const { hasValue: nextRangeHasValue, matrix } = rangeHasValue(
|
||||
worksheet,
|
||||
destRange.startRow,
|
||||
next,
|
||||
destRange.endRow,
|
||||
next
|
||||
);
|
||||
|
||||
if (currentPositionHasValue && !nextRangeHasValue && !firstMove) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
} else {
|
||||
if (matrix.getLength() !== 0) {
|
||||
let min = next;
|
||||
matrix.forValue((_, col) => {
|
||||
min = Math.min(col, min);
|
||||
});
|
||||
destRange.startColumn = min;
|
||||
} else {
|
||||
destRange.startColumn = next;
|
||||
}
|
||||
|
||||
destRange.endColumn = destRange.startColumn;
|
||||
|
||||
if (!currentPositionHasValue && nextRangeHasValue) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPositionHasValue = nextRangeHasValue;
|
||||
firstMove = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Direction.RIGHT === direction) {
|
||||
let next = destRange.endColumn + 1;
|
||||
while (next < worksheet.getColumnCount() && !worksheet.getColVisible(next)) {
|
||||
next += 1;
|
||||
}
|
||||
|
||||
if (next === worksheet.getColumnCount()) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const { hasValue: nextRangeHasValue, matrix } = rangeHasValue(
|
||||
worksheet,
|
||||
destRange.startRow,
|
||||
next,
|
||||
destRange.endRow,
|
||||
next
|
||||
);
|
||||
|
||||
if (currentPositionHasValue && !nextRangeHasValue && !firstMove) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
} else {
|
||||
if (matrix.getLength() !== 0) {
|
||||
let max = next;
|
||||
matrix.forValue((_, col, value) => {
|
||||
max = Math.max(col + (value.colSpan || 1) - 1, max);
|
||||
});
|
||||
destRange.endColumn = max;
|
||||
} else {
|
||||
destRange.endColumn = next;
|
||||
}
|
||||
|
||||
destRange.startColumn = destRange.endColumn;
|
||||
|
||||
if (!currentPositionHasValue && nextRangeHasValue) {
|
||||
shouldContinue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
currentPositionHasValue = nextRangeHasValue;
|
||||
firstMove = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alignToMergedCellsBorders(destRange, worksheet, true);
|
||||
}
|
||||
|
||||
export function findNextRangeExpand(startRange: IRange, direction: Direction, worksheet: Worksheet): IRange {
|
||||
return findNextRange(startRange, direction, worksheet, undefined, false, 1, false);
|
||||
}
|
||||
|
||||
export function expandToNextGapRange(startRange: IRange, direction: Direction, worksheet: Worksheet): IRange {
|
||||
const next = findNextGapRange(startRange, direction, worksheet);
|
||||
return alignToMergedCellsBorders(Rectangle.union(next, startRange), worksheet, true);
|
||||
}
|
||||
|
||||
export function expandToNextCell(startRange: IRange, direction: Direction, worksheet: Worksheet): IRange {
|
||||
const next = findNextRangeExpand(startRange, direction, worksheet);
|
||||
const destRange: IRange = {
|
||||
startRow: Math.min(startRange.startRow, next.startRow),
|
||||
startColumn: Math.min(startRange.startColumn, next.startColumn),
|
||||
endRow: Math.max(startRange.endRow, next.endRow),
|
||||
endColumn: Math.max(startRange.endColumn, next.endColumn),
|
||||
};
|
||||
|
||||
return alignToMergedCellsBorders(Rectangle.union(startRange, destRange), worksheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is considered as a reversed operation of `expandToNextGapCell` but there are slightly differences.
|
||||
* @param startRange
|
||||
* @param direction
|
||||
* @param worksheet
|
||||
*/
|
||||
export function shrinkToNextGapRange(
|
||||
startRange: IRange,
|
||||
anchorRange: IRange,
|
||||
direction: Direction,
|
||||
worksheet: Worksheet
|
||||
): IRange {
|
||||
// use `moveToNextGapCell` reversely to get the next going to cell
|
||||
const reversedDirection = getReverseDirection(direction);
|
||||
const nextGap = findNextGapRange(getEdgeOfRange(startRange, reversedDirection, worksheet), direction, worksheet);
|
||||
|
||||
// if next exceed the startRange we should just expand anchorRange with
|
||||
if (direction === Direction.UP && nextGap.startRow <= startRange.startRow) {
|
||||
return alignToMergedCellsBorders(
|
||||
{ ...anchorRange, startColumn: startRange.startColumn, endColumn: startRange.endColumn },
|
||||
worksheet,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (direction === Direction.DOWN && nextGap.endRow >= startRange.endRow) {
|
||||
return alignToMergedCellsBorders(
|
||||
{ ...anchorRange, startColumn: startRange.startColumn, endColumn: startRange.endColumn },
|
||||
worksheet,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (direction === Direction.LEFT && nextGap.startColumn <= startRange.startColumn) {
|
||||
return alignToMergedCellsBorders(
|
||||
{ ...anchorRange, startRow: startRange.startRow, endRow: startRange.endRow },
|
||||
worksheet,
|
||||
true
|
||||
);
|
||||
}
|
||||
if (direction === Direction.RIGHT && nextGap.endColumn >= startRange.endColumn) {
|
||||
return alignToMergedCellsBorders(
|
||||
{ ...anchorRange, startRow: startRange.startRow, endRow: startRange.endRow },
|
||||
worksheet,
|
||||
true
|
||||
);
|
||||
}
|
||||
return Rectangle.union(Rectangle.clone(anchorRange), nextGap);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is considered as a reversed operation of `expandToNextCell` but there are some slightly differences.
|
||||
* @param startRange
|
||||
* @param direction
|
||||
* @param worksheet
|
||||
*/
|
||||
export function shrinkToNextCell(startRange: IRange, direction: Direction, worksheet: Worksheet): IRange {
|
||||
// use `moveToNextCell` reversely to get the next going to range
|
||||
const reversedDirection = getReverseDirection(direction);
|
||||
const shrinkFromEdge = getEdgeOfRange(startRange, reversedDirection, worksheet);
|
||||
const otherEdge = getEdgeOfRange(startRange, direction, worksheet);
|
||||
const next = findNextRangeExpand(shrinkFromEdge, direction, worksheet);
|
||||
return alignToMergedCellsBorders(Rectangle.union(otherEdge, next), worksheet, false);
|
||||
}
|
||||
|
||||
export function expandToWholeSheet(worksheet: Worksheet): IRange {
|
||||
// DEBT: RANGE_TYPE.ALL should not use startRow, startColumn, endRow, endColumn
|
||||
return {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: worksheet.getRowCount() - 1,
|
||||
endColumn: worksheet.getColumnCount() - 1,
|
||||
rangeType: RANGE_TYPE.ALL,
|
||||
};
|
||||
}
|
||||
|
||||
function getEdgeOfRange(startRange: IRange, direction: Direction, worksheet: Worksheet): IRange {
|
||||
let destRange: IRange;
|
||||
switch (direction) {
|
||||
case Direction.UP:
|
||||
destRange = {
|
||||
startRow: startRange.startRow,
|
||||
startColumn: startRange.startColumn,
|
||||
endRow: startRange.startRow,
|
||||
endColumn: startRange.endColumn,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
};
|
||||
|
||||
break;
|
||||
case Direction.DOWN:
|
||||
destRange = {
|
||||
startRow: startRange.endRow,
|
||||
startColumn: startRange.startColumn,
|
||||
endRow: startRange.endRow,
|
||||
endColumn: startRange.endColumn,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
};
|
||||
break;
|
||||
case Direction.LEFT:
|
||||
destRange = {
|
||||
startRow: startRange.startRow,
|
||||
startColumn: startRange.startColumn,
|
||||
endRow: startRange.endRow,
|
||||
endColumn: startRange.startColumn,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
};
|
||||
break;
|
||||
case Direction.RIGHT:
|
||||
destRange = {
|
||||
startRow: startRange.startRow,
|
||||
startColumn: startRange.endColumn,
|
||||
endRow: startRange.endRow,
|
||||
endColumn: startRange.endColumn,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid direction');
|
||||
}
|
||||
|
||||
return alignToMergedCellsBorders(destRange, worksheet, false);
|
||||
}
|
||||
|
||||
function rangeHasValue(
|
||||
worksheet: Worksheet,
|
||||
row: number,
|
||||
col: number,
|
||||
rowEnd: number,
|
||||
colEnd: number
|
||||
): { hasValue: boolean; matrix: ObjectMatrix<ICellData & { rowSpan?: number; colSpan?: number }> } {
|
||||
let hasValue = false;
|
||||
|
||||
const matrix = worksheet.getMatrixWithMergedCells(row, col, rowEnd, colEnd).forValue((_, __, value) => {
|
||||
if (cellHasValue(value)) {
|
||||
hasValue = true;
|
||||
return false; // stop looping
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasValue,
|
||||
matrix,
|
||||
};
|
||||
}
|
||||
|
||||
export function getStartRange(range: IRange, primary: Nullable<ISelectionCell>, direction: Direction): IRange {
|
||||
const ret = Rectangle.clone(range);
|
||||
|
||||
if (primary == null) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case Direction.UP:
|
||||
case Direction.DOWN:
|
||||
ret.startColumn = ret.endColumn = primary.actualColumn;
|
||||
break;
|
||||
case Direction.LEFT:
|
||||
case Direction.RIGHT:
|
||||
ret.startRow = ret.endRow = primary.actualRow;
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function checkIfShrink(selection: ISelection, direction: Direction, worksheet: Worksheet): boolean {
|
||||
const { primary, range } = selection;
|
||||
|
||||
const startRange: IRange = Rectangle.clone(range);
|
||||
switch (direction) {
|
||||
case Direction.UP:
|
||||
case Direction.DOWN:
|
||||
startRange.startRow = primary?.startRow ?? range.startRow;
|
||||
startRange.endRow = primary?.endRow ?? range.startRow;
|
||||
break;
|
||||
case Direction.LEFT:
|
||||
case Direction.RIGHT:
|
||||
startRange.startColumn = primary?.startColumn ?? range.startColumn;
|
||||
startRange.endColumn = primary?.endColumn ?? range.startColumn;
|
||||
break;
|
||||
}
|
||||
|
||||
const anchorRange = getEdgeOfRange(startRange, direction, worksheet);
|
||||
switch (direction) {
|
||||
case Direction.DOWN:
|
||||
return range.startRow < anchorRange.startRow;
|
||||
case Direction.UP:
|
||||
return range.endRow > anchorRange.endRow;
|
||||
case Direction.LEFT:
|
||||
return anchorRange.endColumn < range.endColumn;
|
||||
case Direction.RIGHT:
|
||||
return anchorRange.startColumn > range.startColumn;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the entire row is selected
|
||||
* @param allRowRanges Range of all rows
|
||||
* @param ranges Range of selected rows
|
||||
* @returns Whether the entire row is selected
|
||||
*/
|
||||
export function isAllRowsCovered(allRowRanges: IRange[], ranges: IRange[]): boolean {
|
||||
// Find the minimum start point and maximum end point in all row ranges
|
||||
let start = allRowRanges[0].startRow;
|
||||
let end = allRowRanges[0].endRow;
|
||||
allRowRanges.forEach((range) => {
|
||||
const { startRow, endRow } = range;
|
||||
start = Math.min(start, startRow);
|
||||
end = Math.max(end, endRow);
|
||||
});
|
||||
|
||||
const covered = new Array(end - start + 1).fill(false);
|
||||
|
||||
// Mark true in ranges
|
||||
ranges.forEach((range) => {
|
||||
const { startRow, endRow } = range;
|
||||
for (let i = Math.max(startRow, start); i <= Math.min(endRow, end); i++) {
|
||||
covered[i - start] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if every range in allRowRanges is covered
|
||||
return allRowRanges.every((range) => {
|
||||
const { startRow, endRow } = range;
|
||||
for (let i = startRow; i <= endRow; i++) {
|
||||
if (!covered[i - start]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the entire column is selected
|
||||
* @param allColumnRanges Range of all columns
|
||||
* @param ranges Range of selected columns
|
||||
* @returns Whether the entire column is selected
|
||||
*/
|
||||
export function isAllColumnsCovered(allColumnRanges: IRange[], ranges: IRange[]): boolean {
|
||||
// Find the minimum start point and maximum end point in all column ranges
|
||||
let start = allColumnRanges[0].startColumn;
|
||||
let end = allColumnRanges[0].endColumn;
|
||||
allColumnRanges.forEach((range) => {
|
||||
const { startColumn, endColumn } = range;
|
||||
start = Math.min(start, startColumn);
|
||||
end = Math.max(end, endColumn);
|
||||
});
|
||||
|
||||
const covered = new Array(end - start + 1).fill(false);
|
||||
|
||||
// Mark true in ranges
|
||||
ranges.forEach((range) => {
|
||||
const { startColumn, endColumn } = range;
|
||||
for (let i = Math.max(startColumn, start); i <= Math.min(endColumn, end); i++) {
|
||||
covered[i - start] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if every range in allColumnRanges is covered
|
||||
return allColumnRanges.every((range) => {
|
||||
const { startColumn, endColumn } = range;
|
||||
for (let i = startColumn; i <= endColumn; i++) {
|
||||
if (!covered[i - start]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function getMergeableSelectionsByType(type: MergeType, selections: Nullable<IRange[]>): Nullable<IRange[]> {
|
||||
if (!selections) return null;
|
||||
if (type === MergeType.MergeAll) {
|
||||
return selections.filter((selection) => {
|
||||
if (selection.startRow === selection.endRow && selection.startColumn === selection.endColumn) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else if (type === MergeType.MergeVertical) {
|
||||
return selections.filter((selection) => {
|
||||
if (selection.startRow === selection.endRow) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else if (type === MergeType.MergeHorizontal) {
|
||||
return selections.filter((selection) => {
|
||||
if (selection.startColumn === selection.endColumn) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return selections;
|
||||
}
|
||||
|
||||
function cellHasValue(cell: ICellData): boolean {
|
||||
return (cell.v !== undefined && cell.v !== null && cell.v !== '') || cell.p !== undefined;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 { ICommand, Workbook } from '@univerjs/core';
|
||||
|
||||
import { CommandType, ICommandService, IUndoRedoService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
|
||||
import { AddWorksheetProtectionCommand, DeleteWorksheetProtectionCommand, WorksheetProtectionRuleModel } from '@univerjs/sheets';
|
||||
|
||||
export const DeleteWorksheetProtectionFormSheetBarCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.delete-worksheet-protection-from-sheet-bar',
|
||||
async handler(accessor, params) {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
|
||||
const worksheetProtectionRuleModel = accessor.get(WorksheetProtectionRuleModel);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const unitId = workbook.getUnitId();
|
||||
|
||||
if (!worksheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
|
||||
const rule = worksheetProtectionRuleModel.getRule(unitId, subUnitId);
|
||||
|
||||
const result = await commandService.executeCommand(DeleteWorksheetProtectionCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const redoMutations = [{ id: DeleteWorksheetProtectionCommand.id, params: { unitId, subUnitId } }];
|
||||
const undoMutations = [{ id: AddWorksheetProtectionCommand.id, params: { unitId, rule } }];
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
redoMutations,
|
||||
undoMutations,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ChangeSheetProtectionFromSheetBarCommand: ICommand = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'sheet.command.change-sheet-protection-from-sheet-bar',
|
||||
async handler(accessor) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
await commandService.executeCommand('sheet-permission.operation.openDialog');
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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 { IUniverInstanceService } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IEditorBridgeService } from '../../../services/editor-bridge.service';
|
||||
import { IFormatPainterService } from '../../../services/format-painter/format-painter.service';
|
||||
import { SetActivateCellEditOperation } from '../activate-cell-edit.operation';
|
||||
import { ScrollToRangeOperation } from '../scroll-to-range.operation';
|
||||
import { SetScrollOperation } from '../scroll.operation';
|
||||
import { SetFormatPainterOperation } from '../set-format-painter.operation';
|
||||
import { SetZoomRatioOperation } from '../set-zoom-ratio.operation';
|
||||
|
||||
function createAccessor(pairs: Array<[unknown, unknown]>) {
|
||||
const map = new Map<unknown, unknown>(pairs);
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (!map.has(token)) {
|
||||
throw new Error(`Unknown token: ${String(token)}`);
|
||||
}
|
||||
return map.get(token);
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('sheets-ui basic operations', () => {
|
||||
it('SetFormatPainterOperation should guard empty params and update service', () => {
|
||||
const setStatus = vi.fn();
|
||||
const accessor = createAccessor([[IFormatPainterService, { setStatus }]]);
|
||||
|
||||
expect(SetFormatPainterOperation.handler(accessor, undefined as any)).toBe(false);
|
||||
expect(SetFormatPainterOperation.handler(accessor, { status: 'ONCE' as any })).toBe(true);
|
||||
expect(setStatus).toHaveBeenCalledWith('ONCE');
|
||||
});
|
||||
|
||||
it('SetActivateCellEditOperation should update current edit cell state', () => {
|
||||
const setEditCell = vi.fn();
|
||||
const accessor = createAccessor([[IEditorBridgeService, { setEditCell }]]);
|
||||
|
||||
expect(SetActivateCellEditOperation.handler(accessor, { unitId: 'u1' } as any)).toBe(true);
|
||||
expect(setEditCell).toHaveBeenCalledWith({ unitId: 'u1' });
|
||||
});
|
||||
|
||||
it('SetScrollOperation should compute final offsets with screen ratios', () => {
|
||||
const emitRawScrollParam = vi.fn();
|
||||
const renderUnit = {
|
||||
with: vi.fn(() => ({ emitRawScrollParam })),
|
||||
engine: { width: 200, height: 100 },
|
||||
};
|
||||
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: vi.fn()
|
||||
.mockReturnValueOnce(renderUnit)
|
||||
.mockReturnValueOnce(renderUnit),
|
||||
};
|
||||
|
||||
const accessor = createAccessor([[IRenderManagerService, renderManagerService]]);
|
||||
|
||||
expect(SetScrollOperation.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
offsetX: 10,
|
||||
offsetY: 20,
|
||||
screenRatioX: 0.5,
|
||||
screenRatioY: 0.25,
|
||||
} as any)).toBe(true);
|
||||
|
||||
expect(emitRawScrollParam).toHaveBeenCalledWith(expect.objectContaining({
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
offsetX: 110,
|
||||
offsetY: 45,
|
||||
}));
|
||||
});
|
||||
|
||||
it('SetScrollOperation should return false for empty params or missing render unit', () => {
|
||||
const accessor = createAccessor([[IRenderManagerService, { getRenderUnitById: vi.fn() }]]);
|
||||
expect(SetScrollOperation.handler(accessor, undefined as any)).toBe(false);
|
||||
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: vi.fn()
|
||||
.mockReturnValueOnce({ with: () => ({ emitRawScrollParam: vi.fn() }) })
|
||||
.mockReturnValueOnce(null),
|
||||
};
|
||||
const accessor2 = createAccessor([[IRenderManagerService, renderManagerService]]);
|
||||
expect(SetScrollOperation.handler(accessor2, { unitId: 'u1', sheetId: 's1' } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('SetZoomRatioOperation should delegate to zoom controller', () => {
|
||||
const updateZoom = vi.fn(() => true);
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: vi.fn(() => ({ with: () => ({ updateZoom }) })),
|
||||
};
|
||||
const accessor = createAccessor([[IRenderManagerService, renderManagerService]]);
|
||||
|
||||
expect(SetZoomRatioOperation.handler(accessor, {
|
||||
unitId: 'u1',
|
||||
subUnitId: 's1',
|
||||
zoomRatio: 1.25,
|
||||
})).toBe(true);
|
||||
expect(updateZoom).toHaveBeenCalledWith('s1', 1.25);
|
||||
|
||||
const accessor2 = createAccessor([[IRenderManagerService, { getRenderUnitById: () => null }]]);
|
||||
expect(SetZoomRatioOperation.handler(accessor2, { unitId: 'u1', subUnitId: 's1', zoomRatio: 1 } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('ScrollToRangeOperation should guard params and call scroll controller', () => {
|
||||
const scrollToRange = vi.fn(() => true);
|
||||
const getRenderUnitById = vi.fn(() => ({ with: () => ({ scrollToRange }) }));
|
||||
const accessor = createAccessor([
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: () => ({ getUnitId: () => 'u1' }) }],
|
||||
[IRenderManagerService, { getRenderUnitById }],
|
||||
]);
|
||||
|
||||
expect(ScrollToRangeOperation.handler(accessor, undefined as any)).toBe(false);
|
||||
|
||||
expect(ScrollToRangeOperation.handler(accessor, {
|
||||
range: { startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 },
|
||||
forceTop: true,
|
||||
forceLeft: false,
|
||||
} as any)).toBe(true);
|
||||
|
||||
expect(scrollToRange).toHaveBeenCalledWith(
|
||||
{ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 },
|
||||
true,
|
||||
false
|
||||
);
|
||||
expect(getRenderUnitById).toHaveBeenCalledWith('u1');
|
||||
|
||||
expect(ScrollToRangeOperation.handler(accessor, {
|
||||
unitId: 'embedded-u1',
|
||||
range: { startRow: 2, endRow: 3, startColumn: 2, endColumn: 3 },
|
||||
} as any)).toBe(true);
|
||||
expect(getRenderUnitById).toHaveBeenLastCalledWith('embedded-u1');
|
||||
|
||||
const missingRenderAccessor = createAccessor([
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: () => ({ getUnitId: () => 'missing' }) }],
|
||||
[IRenderManagerService, { getRenderUnitById: () => null }],
|
||||
]);
|
||||
expect(ScrollToRangeOperation.handler(missingRenderAccessor, {
|
||||
range: { startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 },
|
||||
} as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 { ICommandService, IConfigService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IEditorBridgeService } from '../../../services/editor-bridge.service';
|
||||
import {
|
||||
SetCellEditVisibleArrowOperation,
|
||||
SetCellEditVisibleOperation,
|
||||
SetCellEditVisibleWithF2Operation,
|
||||
} from '../cell-edit.operation';
|
||||
|
||||
function createAccessor(pairs: Array<[unknown, unknown]>) {
|
||||
const map = new Map<unknown, unknown>(pairs);
|
||||
return {
|
||||
get(token: unknown) {
|
||||
if (!map.has(token)) {
|
||||
throw new Error(`Unknown token: ${String(token)}`);
|
||||
}
|
||||
return map.get(token);
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('cell edit operations', () => {
|
||||
it('SetCellEditVisibleOperation guards empty params and disable-edit mode', () => {
|
||||
const changeVisible = vi.fn();
|
||||
const accessor = createAccessor([
|
||||
[IConfigService, { getConfig: vi.fn(() => ({ disableEdit: true })) }],
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: vi.fn(() => ({ getUnitId: () => 'u1' })) }],
|
||||
[IEditorBridgeService, { changeVisible }],
|
||||
]);
|
||||
|
||||
expect(SetCellEditVisibleOperation.handler(accessor, undefined as any)).toBe(false);
|
||||
expect(SetCellEditVisibleOperation.handler(accessor, { visible: true } as any)).toBe(false);
|
||||
expect(changeVisible).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('SetCellEditVisibleOperation returns false when no active workbook', () => {
|
||||
const accessor = createAccessor([
|
||||
[IConfigService, { getConfig: vi.fn(() => ({ disableEdit: false })) }],
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: vi.fn(() => null) }],
|
||||
[IEditorBridgeService, { changeVisible: vi.fn() }],
|
||||
]);
|
||||
|
||||
expect(SetCellEditVisibleOperation.handler(accessor, { visible: false } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('SetCellEditVisibleOperation falls back to workbook unitId', () => {
|
||||
const changeVisible = vi.fn();
|
||||
const accessor = createAccessor([
|
||||
[IConfigService, { getConfig: vi.fn(() => ({ disableEdit: false })) }],
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: vi.fn(() => ({ getUnitId: () => 'workbook-u1' })) }],
|
||||
[IEditorBridgeService, { changeVisible }],
|
||||
]);
|
||||
|
||||
expect(SetCellEditVisibleOperation.handler(accessor, { visible: true } as any)).toBe(true);
|
||||
expect(changeVisible).toHaveBeenCalledWith({ visible: true, unitId: 'workbook-u1' });
|
||||
});
|
||||
|
||||
it('SetCellEditVisibleWithF2Operation respects workbook existence and delegates command execution', () => {
|
||||
const syncExecuteCommand = vi.fn(() => true);
|
||||
const commandAccessor = createAccessor([
|
||||
[ICommandService, { syncExecuteCommand }],
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: vi.fn(() => ({ getUnitId: () => 'wb-u2' })) }],
|
||||
]);
|
||||
|
||||
expect(SetCellEditVisibleWithF2Operation.handler(commandAccessor, { visible: true } as any)).toBe(true);
|
||||
expect(syncExecuteCommand).toHaveBeenCalledWith(SetCellEditVisibleOperation.id, {
|
||||
visible: true,
|
||||
unitId: 'wb-u2',
|
||||
});
|
||||
|
||||
const noWorkbookAccessor = createAccessor([
|
||||
[ICommandService, { syncExecuteCommand: vi.fn() }],
|
||||
[IUniverInstanceService, { getCurrentUnitOfType: vi.fn(() => null) }],
|
||||
]);
|
||||
expect(SetCellEditVisibleWithF2Operation.handler(noWorkbookAccessor, { visible: true } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('SetCellEditVisibleArrowOperation is a no-op event operation', () => {
|
||||
expect(SetCellEditVisibleArrowOperation.handler({} as any, undefined as any)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 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 { Injector, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { UnitObject } from '@univerjs/protocol';
|
||||
import { EditStateEnum, ViewStateEnum } from '@univerjs/sheets';
|
||||
import { IDialogService, ISidebarService } from '@univerjs/ui';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
UNIVER_SHEET_PERMISSION_DIALOG,
|
||||
UNIVER_SHEET_PERMISSION_DIALOG_ID,
|
||||
UNIVER_SHEET_PERMISSION_PANEL,
|
||||
} from '../../../consts/permission';
|
||||
import { SheetPermissionPanelModel } from '../../../services/permission/sheet-permission-panel.model';
|
||||
import { SheetPermissionUserManagerService } from '../../../services/permission/sheet-permission-user-list.service';
|
||||
import { ISheetBarService } from '../../../services/sheet-bar/sheet-bar.service';
|
||||
import { DEFINED_NAME_CONTAINER } from '../../../views/defined-name/component-name';
|
||||
import { RenameSheetOperation } from '../rename-sheet.operation';
|
||||
import { SheetPermissionOpenDialogOperation } from '../sheet-permission-open-dialog.operation';
|
||||
import { SheetPermissionOpenPanelOperation } from '../sheet-permission-open-panel.operation';
|
||||
import { SidebarDefinedNameOperation } from '../sidebar-defined-name.operation';
|
||||
|
||||
class SidebarServiceForTest {
|
||||
readonly openCalls: unknown[] = [];
|
||||
closeCount = 0;
|
||||
|
||||
open(props: unknown) {
|
||||
this.openCalls.push(props);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closeCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class DialogServiceForTest {
|
||||
readonly openCalls: unknown[] = [];
|
||||
readonly closeIds: string[] = [];
|
||||
|
||||
open(props: unknown) {
|
||||
this.openCalls.push(props);
|
||||
}
|
||||
|
||||
close(id: string) {
|
||||
this.closeIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
class SheetBarServiceForTest {
|
||||
readonly renameIds: string[] = [];
|
||||
|
||||
setRenameId(subUnitId: string) {
|
||||
this.renameIds.push(subUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorServiceForTest {
|
||||
}
|
||||
|
||||
class LocaleServiceForTest {
|
||||
t(key: string) {
|
||||
return `localized:${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
class WorkbookForTest {
|
||||
getUnitId() {
|
||||
return 'unit-1';
|
||||
}
|
||||
|
||||
getActiveSheet() {
|
||||
return {
|
||||
getSheetId: () => 'sheet-1',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UniverInstanceWithSheet {
|
||||
getCurrentUnitOfType() {
|
||||
return new WorkbookForTest();
|
||||
}
|
||||
}
|
||||
|
||||
class UniverInstanceWithoutSheet {
|
||||
getCurrentUnitOfType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function addUseClass(injector: Injector, token: unknown, useClass: unknown) {
|
||||
injector.add([token, { useClass }] as any);
|
||||
}
|
||||
|
||||
function createOperationInjector(univerClass: typeof UniverInstanceWithSheet | typeof UniverInstanceWithoutSheet = UniverInstanceWithSheet) {
|
||||
const injector = new Injector();
|
||||
addUseClass(injector, ISidebarService, SidebarServiceForTest);
|
||||
addUseClass(injector, IDialogService, DialogServiceForTest);
|
||||
addUseClass(injector, ISheetBarService, SheetBarServiceForTest);
|
||||
addUseClass(injector, IEditorService, EditorServiceForTest);
|
||||
addUseClass(injector, LocaleService, LocaleServiceForTest);
|
||||
addUseClass(injector, IUniverInstanceService, univerClass);
|
||||
injector.add([SheetPermissionPanelModel]);
|
||||
injector.add([SheetPermissionUserManagerService]);
|
||||
return injector;
|
||||
}
|
||||
|
||||
describe('SidebarDefinedNameOperation', () => {
|
||||
it('returns false when there is no sheet command target', async () => {
|
||||
const injector = createOperationInjector(UniverInstanceWithoutSheet);
|
||||
|
||||
await expect(SidebarDefinedNameOperation.handler(injector, { value: 'open' })).resolves.toBe(false);
|
||||
|
||||
const sidebarService = injector.get(ISidebarService) as unknown as SidebarServiceForTest;
|
||||
expect(sidebarService.openCalls).toEqual([]);
|
||||
expect(sidebarService.closeCount).toBe(0);
|
||||
});
|
||||
|
||||
it('opens sidebar with localized title when value is open', async () => {
|
||||
const injector = createOperationInjector();
|
||||
|
||||
await expect(SidebarDefinedNameOperation.handler(injector, { value: 'open' })).resolves.toBe(true);
|
||||
|
||||
const sidebarService = injector.get(ISidebarService) as unknown as SidebarServiceForTest;
|
||||
expect(sidebarService.openCalls).toEqual([expect.objectContaining({
|
||||
id: DEFINED_NAME_CONTAINER,
|
||||
width: 333,
|
||||
header: { title: 'localized:sheets-ui.definedName.featureTitle' },
|
||||
children: { label: DEFINED_NAME_CONTAINER },
|
||||
})]);
|
||||
(sidebarService.openCalls[0] as { onClose: () => void }).onClose();
|
||||
expect(sidebarService.closeCount).toBe(0);
|
||||
});
|
||||
|
||||
it('closes sidebar for close/default action', async () => {
|
||||
const injector = createOperationInjector();
|
||||
|
||||
await expect(SidebarDefinedNameOperation.handler(injector, { value: 'close' })).resolves.toBe(true);
|
||||
await expect(SidebarDefinedNameOperation.handler(injector, { value: 'other' })).resolves.toBe(true);
|
||||
|
||||
expect((injector.get(ISidebarService) as unknown as SidebarServiceForTest).closeCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sheet bar and permission operations', () => {
|
||||
it('marks the requested sheet as being renamed from the sheet bar', async () => {
|
||||
const injector = createOperationInjector();
|
||||
|
||||
await expect(RenameSheetOperation.handler(injector, { subUnitId: 'sheet-2' })).resolves.toBe(true);
|
||||
await expect(RenameSheetOperation.handler(injector)).resolves.toBe(true);
|
||||
|
||||
expect((injector.get(ISheetBarService) as unknown as SheetBarServiceForTest).renameIds).toEqual(['sheet-2']);
|
||||
});
|
||||
|
||||
it('opens the permission dialog after closing find and replace', async () => {
|
||||
const injector = createOperationInjector();
|
||||
|
||||
await expect(SheetPermissionOpenDialogOperation.handler(injector)).resolves.toBe(true);
|
||||
|
||||
const dialogService = injector.get(IDialogService) as unknown as DialogServiceForTest;
|
||||
expect(dialogService.closeIds).toEqual(['DESKTOP_FIND_REPLACE_DIALOG']);
|
||||
expect(dialogService.openCalls).toEqual([expect.objectContaining({
|
||||
id: UNIVER_SHEET_PERMISSION_DIALOG_ID,
|
||||
title: { title: 'sheets-ui.permission.dialog.allowedPermissionType' },
|
||||
children: { label: UNIVER_SHEET_PERMISSION_DIALOG },
|
||||
width: 393,
|
||||
})]);
|
||||
|
||||
(dialogService.openCalls[0] as { onClose: () => void }).onClose();
|
||||
expect(dialogService.closeIds).toEqual(['DESKTOP_FIND_REPLACE_DIALOG', UNIVER_SHEET_PERMISSION_DIALOG_ID]);
|
||||
});
|
||||
|
||||
it('opens the permission panel with rule context and resets panel state on close', async () => {
|
||||
const injector = createOperationInjector();
|
||||
const model = injector.get(SheetPermissionPanelModel);
|
||||
const userManager = injector.get(SheetPermissionUserManagerService);
|
||||
const rule = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
permissionId: 'permission-1',
|
||||
unitType: UnitObject.SelectRange,
|
||||
id: 'rule-1',
|
||||
ranges: [{ startRow: 0, endRow: 2, startColumn: 0, endColumn: 2 }],
|
||||
viewState: ViewStateEnum.OthersCanView,
|
||||
editState: EditStateEnum.OnlyMe,
|
||||
};
|
||||
userManager.setCanEditUserList([{ id: 'user-1', role: 1, subject: undefined }]);
|
||||
userManager.setSelectUserList([{ id: 'user-1', role: 1, subject: undefined }]);
|
||||
|
||||
await expect(SheetPermissionOpenPanelOperation.handler(injector, {
|
||||
fromSheetBar: true,
|
||||
showDetail: false,
|
||||
rule,
|
||||
oldRule: rule,
|
||||
})).resolves.toBe(true);
|
||||
|
||||
const sidebarService = injector.get(ISidebarService) as unknown as SidebarServiceForTest;
|
||||
expect(model.getVisible()).toBe(true);
|
||||
expect(sidebarService.openCalls).toEqual([expect.objectContaining({
|
||||
header: { title: 'sheets-ui.permission.panel.title' },
|
||||
width: 330,
|
||||
children: {
|
||||
label: UNIVER_SHEET_PERMISSION_PANEL,
|
||||
showDetail: false,
|
||||
fromSheetBar: true,
|
||||
rule,
|
||||
oldRule: rule,
|
||||
},
|
||||
})]);
|
||||
|
||||
(sidebarService.openCalls[0] as { onClose: () => void }).onClose();
|
||||
expect(model.getVisible()).toBe(false);
|
||||
expect(userManager.userList).toEqual([]);
|
||||
expect(userManager.selectUserList).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { IOperation } from '@univerjs/core';
|
||||
import type { ICurrentEditCellParam } from '../../services/editor-bridge.service';
|
||||
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IEditorBridgeService } from '../../services/editor-bridge.service';
|
||||
|
||||
export const SetActivateCellEditOperation: IOperation<ICurrentEditCellParam> = {
|
||||
id: 'sheet.operation.set-activate-cell-edit',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
editorBridgeService.setEditCell(params);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 { IOperation, Workbook } from '@univerjs/core';
|
||||
import type { IUniverSheetsUIConfig } from '../../config/config';
|
||||
|
||||
import type { IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service';
|
||||
import { CommandType, ICommandService, IConfigService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../../config/config';
|
||||
import { IEditorBridgeService } from '../../services/editor-bridge.service';
|
||||
|
||||
export const SetCellEditVisibleOperation: IOperation<IEditorBridgeServiceVisibleParam> = {
|
||||
id: 'sheet.operation.set-cell-edit-visible',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const configService = accessor.get(IConfigService);
|
||||
const disableEdit = configService.getConfig<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY)?.disableEdit;
|
||||
if (disableEdit && params.visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId } = params;
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return false;
|
||||
}
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
editorBridgeService.changeVisible({
|
||||
...params,
|
||||
unitId: unitId ?? workbook.getUnitId(),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const SetCellEditVisibleWithF2Operation: IOperation<IEditorBridgeServiceVisibleParam> = {
|
||||
id: 'sheet.operation.set-cell-edit-visible-f2',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, {
|
||||
...params,
|
||||
unitId: workbook.getUnitId(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* When the editor is not clicked to change the cursor,
|
||||
* the arrow keys will exit editing and move the cell.
|
||||
*/
|
||||
export const SetCellEditVisibleArrowOperation: IOperation<IEditorBridgeServiceVisibleParam> = {
|
||||
id: 'sheet.operation.set-cell-edit-visible-arrow',
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => true,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
import { ISheetBarService } from '../../services/sheet-bar/sheet-bar.service';
|
||||
|
||||
interface IRenameSheetOperationParams {
|
||||
subUnitId: string;
|
||||
}
|
||||
|
||||
export const RenameSheetOperation: ICommand = {
|
||||
id: 'sheet.operation.rename-sheet',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor, params?: IRenameSheetOperationParams) => {
|
||||
const sheetBarService = accessor.get(ISheetBarService);
|
||||
if (params) {
|
||||
sheetBarService.setRenameId(params.subUnitId);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { IScrollToCellCommandParams } from '../commands/set-scroll.command';
|
||||
import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetsScrollRenderController } from '../../controllers/render-controllers/scroll.render-controller';
|
||||
|
||||
export const ScrollToRangeOperation: ICommand<IScrollToCellCommandParams> = {
|
||||
id: 'sheet.operation.scroll-to-range',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const unitId = params.unitId ?? instanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SHEET)?.getUnitId();
|
||||
if (!unitId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderUnit = renderManagerService.getRenderUnitById(unitId);
|
||||
if (!renderUnit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const scrollController = renderUnit.with(SheetsScrollRenderController);
|
||||
|
||||
return scrollController.scrollToRange(params.range, params.forceTop, params.forceLeft);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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 { IOperation } from '@univerjs/core';
|
||||
import type { IScrollStateWithSearchParam } from '../../services/scroll-manager.service';
|
||||
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetScrollManagerService } from '../../services/scroll-manager.service';
|
||||
|
||||
export const SetScrollOperation: IOperation<IScrollStateWithSearchParam> = {
|
||||
id: 'sheet.operation.set-scroll',
|
||||
type: CommandType.OPERATION,
|
||||
|
||||
handler: (accessor, params: IScrollStateWithSearchParam) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// freeze is handled by set-scroll.command.ts
|
||||
const { unitId, sheetId, offsetX = 0, offsetY = 0, sheetViewStartColumn, sheetViewStartRow, duration, screenRatioX, screenRatioY } = params;
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const scrollManagerService = renderManagerService.getRenderUnitById(unitId)!.with(SheetScrollManagerService);
|
||||
const renderUnit = renderManagerService.getRenderUnitById(unitId);
|
||||
|
||||
if (!renderUnit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const engine = renderUnit.engine;
|
||||
const canvasWidth = engine.width;
|
||||
const canvasHeight = engine.height;
|
||||
|
||||
const finalOffsetX = screenRatioX !== undefined ? (canvasWidth * screenRatioX) + offsetX : offsetX;
|
||||
const finalOffsetY = screenRatioY !== undefined ? (canvasHeight * screenRatioY) + offsetY : offsetY;
|
||||
|
||||
scrollManagerService.emitRawScrollParam({
|
||||
unitId,
|
||||
sheetId,
|
||||
offsetX: finalOffsetX,
|
||||
offsetY: finalOffsetY,
|
||||
sheetViewStartRow,
|
||||
sheetViewStartColumn,
|
||||
duration,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { IOperation } from '@univerjs/core';
|
||||
import type { FormatPainterStatus } from '../../services/format-painter/format-painter.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IFormatPainterService } from '../../services/format-painter/format-painter.service';
|
||||
|
||||
export interface ISetFormatPainterOperationParams {
|
||||
status: FormatPainterStatus;
|
||||
}
|
||||
export const SetFormatPainterOperation: IOperation<ISetFormatPainterOperationParams> = {
|
||||
id: 'sheet.operation.set-format-painter',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const formatPainterService = accessor.get(IFormatPainterService);
|
||||
formatPainterService.setStatus(params.status);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { IOperation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetsZoomRenderController } from '../../controllers/render-controllers/zoom.render-controller';
|
||||
|
||||
export interface ISetZoomRatioOperationParams {
|
||||
zoomRatio: number;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}
|
||||
|
||||
export const SetZoomRatioOperation: IOperation<ISetZoomRatioOperationParams> = {
|
||||
id: 'sheet.operation.set-zoom-ratio',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params: ISetZoomRatioOperationParams) => {
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const renderUnit = renderManagerService.getRenderUnitById(params.unitId);
|
||||
if (!renderUnit) return false;
|
||||
|
||||
return renderUnit.with(SheetsZoomRenderController).updateZoom(params.subUnitId, params.zoomRatio);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
import { UNIVER_SHEET_PERMISSION_DIALOG, UNIVER_SHEET_PERMISSION_DIALOG_ID } from '../../consts/permission';
|
||||
|
||||
export const SheetPermissionOpenDialogOperation: ICommand = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'sheet-permission.operation.openDialog',
|
||||
async handler(accessor, _params) {
|
||||
const dialogService = accessor.get(IDialogService);
|
||||
|
||||
dialogService.close('DESKTOP_FIND_REPLACE_DIALOG');
|
||||
|
||||
dialogService.open({
|
||||
id: UNIVER_SHEET_PERMISSION_DIALOG_ID,
|
||||
title: { title: 'sheets-ui.permission.dialog.allowedPermissionType' },
|
||||
children: { label: UNIVER_SHEET_PERMISSION_DIALOG },
|
||||
width: 393,
|
||||
onClose: () => dialogService.close(UNIVER_SHEET_PERMISSION_DIALOG_ID),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { IPermissionPanelRule } from '../../services/permission/sheet-permission-panel.model';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { UNIVER_SHEET_PERMISSION_PANEL } from '../../consts/permission';
|
||||
import { SheetPermissionPanelModel } from '../../services/permission/sheet-permission-panel.model';
|
||||
import { SheetPermissionUserManagerService } from '../../services/permission/sheet-permission-user-list.service';
|
||||
|
||||
export interface IPermissionOpenPanelParam {
|
||||
fromSheetBar?: boolean;
|
||||
showDetail?: boolean;
|
||||
rule?: IPermissionPanelRule;
|
||||
oldRule?: IPermissionPanelRule;
|
||||
}
|
||||
|
||||
export const SheetPermissionOpenPanelOperation: ICommand<IPermissionOpenPanelParam> = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'sheet-permission.operation.openPanel',
|
||||
async handler(accessor, _params = {}) {
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const sheetPermissionPanelModel = accessor.get(SheetPermissionPanelModel);
|
||||
const sheetPermissionUserManagerService = accessor.get(SheetPermissionUserManagerService);
|
||||
|
||||
const { showDetail = true, fromSheetBar = false, rule, oldRule } = _params;
|
||||
|
||||
const sidebarProps = {
|
||||
header: { title: 'sheets-ui.permission.panel.title' },
|
||||
children: {
|
||||
label: UNIVER_SHEET_PERMISSION_PANEL,
|
||||
showDetail,
|
||||
fromSheetBar,
|
||||
rule,
|
||||
oldRule,
|
||||
},
|
||||
width: 330,
|
||||
onClose: () => {
|
||||
sheetPermissionPanelModel.reset();
|
||||
sheetPermissionUserManagerService.reset();
|
||||
},
|
||||
};
|
||||
|
||||
sheetPermissionPanelModel.setVisible(true);
|
||||
sidebarService.open(sidebarProps);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { DEFINED_NAME_CONTAINER } from '../../views/defined-name/component-name';
|
||||
|
||||
export interface IUIComponentCommandParams {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const SidebarDefinedNameOperation: ICommand = {
|
||||
id: 'sidebar.operation.defined-name',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: IUIComponentCommandParams) => {
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId } = target;
|
||||
switch (params.value) {
|
||||
case 'open':
|
||||
sidebarService.open({
|
||||
id: DEFINED_NAME_CONTAINER,
|
||||
header: { title: localeService.t<LocaleKey>('sheets-ui.definedName.featureTitle') },
|
||||
children: { label: DEFINED_NAME_CONTAINER },
|
||||
onClose: () => {
|
||||
},
|
||||
width: 333,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'close':
|
||||
default:
|
||||
sidebarService.close();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
201
packages/sheets-ui/src/common/__tests__/utils.spec.ts
Normal file
201
packages/sheets-ui/src/common/__tests__/utils.spec.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 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 { SHEET_VIEWPORT_KEY } from '@univerjs/engine-render';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
checkCellContentInRange,
|
||||
checkCellContentInRanges,
|
||||
getCellIndexByOffsetWithMerge,
|
||||
getCellRealRange,
|
||||
getHoverCellPosition,
|
||||
getUserListEqual,
|
||||
getViewportByCell,
|
||||
transformBound2OffsetBound,
|
||||
transformPosition2Offset,
|
||||
} from '../utils';
|
||||
|
||||
describe('common utils', () => {
|
||||
it('compares collaborator user lists by user id and role', () => {
|
||||
const list1 = [
|
||||
{ id: 'b', subject: { userID: 'u2' }, role: 'viewer' },
|
||||
{ id: 'a', subject: { userID: 'u1' }, role: 'editor' },
|
||||
] as any;
|
||||
const list2 = [
|
||||
{ id: 'a', subject: { userID: 'u1' }, role: 'editor' },
|
||||
{ id: 'b', subject: { userID: 'u2' }, role: 'viewer' },
|
||||
] as any;
|
||||
const list3 = [{ id: 'a', subject: { userID: 'u1' }, role: 'viewer' }] as any;
|
||||
|
||||
expect(getUserListEqual(list1, list2)).toBe(true);
|
||||
expect(getUserListEqual(list1, list3)).toBe(false);
|
||||
expect(getUserListEqual(list2, [{ id: 'a', subject: { userID: 'u1' }, role: 'viewer' }, { id: 'b', subject: { userID: 'u2' }, role: 'viewer' }] as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('checks whether cell contents exist in single or multiple ranges', () => {
|
||||
const matrixWithValue = {
|
||||
forValue: (cb: (...args: any[]) => void) => {
|
||||
cb(0, 0, { v: 'primary' });
|
||||
cb(0, 1, { v: 'other' });
|
||||
},
|
||||
};
|
||||
const matrixWithoutValue = {
|
||||
forValue: (cb: (...args: any[]) => void) => {
|
||||
cb(0, 0, { v: 'primary' });
|
||||
cb(1, 1, null);
|
||||
},
|
||||
};
|
||||
|
||||
const worksheet = {
|
||||
getMatrixWithMergedCells: (_sr: number, _sc: number, _er: number, ec: number) => (ec > 1 ? matrixWithoutValue : matrixWithValue),
|
||||
cellHasValue: (cell: any) => !!cell?.v,
|
||||
};
|
||||
|
||||
expect(checkCellContentInRange(worksheet as any, { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 } as any)).toBe(true);
|
||||
expect(checkCellContentInRanges(worksheet as any, [
|
||||
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 2 },
|
||||
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
|
||||
] as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('gets cell index by offset with merged-cell fallback', () => {
|
||||
const scene = {
|
||||
getActiveViewportByCoord: () => ({ viewportScrollX: 10, viewportScrollY: 20 }),
|
||||
getAncestorScale: () => ({ scaleX: 2, scaleY: 3 }),
|
||||
};
|
||||
const skeleton = {
|
||||
getCellIndexByOffset: () => ({ row: 4, column: 5 }),
|
||||
worksheet: {
|
||||
getMergedCell: () => ({ startRow: 1, startColumn: 2 }),
|
||||
},
|
||||
};
|
||||
|
||||
const result = getCellIndexByOffsetWithMerge(100, 120, scene as any, skeleton as any)!;
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
row: 4,
|
||||
col: 5,
|
||||
actualRow: 1,
|
||||
actualCol: 2,
|
||||
}));
|
||||
|
||||
const noViewport = {
|
||||
getActiveViewportByCoord: () => null,
|
||||
};
|
||||
expect(getCellIndexByOffsetWithMerge(1, 2, noViewport as any, skeleton as any)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves viewport by cell with and without freeze panes', () => {
|
||||
const scene = {
|
||||
getViewport: (key: string) => key,
|
||||
};
|
||||
const noFreezeSheet = { getFreeze: () => null };
|
||||
expect(getViewportByCell(1, 1, scene as any, noFreezeSheet as any)).toBe(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
|
||||
const freezeSheet = { getFreeze: () => ({ startRow: 2, startColumn: 3 }) };
|
||||
expect(getViewportByCell(3, 3, scene as any, freezeSheet as any)).toBe(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
expect(getViewportByCell(1, 1, scene as any, freezeSheet as any)).toBe(SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP);
|
||||
expect(getViewportByCell(1, 3, scene as any, freezeSheet as any)).toBe(SHEET_VIEWPORT_KEY.VIEW_MAIN_TOP);
|
||||
expect(getViewportByCell(3, 1, scene as any, freezeSheet as any)).toBe(SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT);
|
||||
});
|
||||
|
||||
it('transforms sheet position to viewport offset', () => {
|
||||
const sceneNoViewport = {
|
||||
getAncestorScale: () => ({ scaleX: 2, scaleY: 2 }),
|
||||
getViewport: () => null,
|
||||
};
|
||||
expect(transformPosition2Offset(10, 20, sceneNoViewport as any, {} as any, {} as any)).toEqual({ x: 10, y: 20 });
|
||||
|
||||
const scene = {
|
||||
getAncestorScale: () => ({ scaleX: 2, scaleY: 3 }),
|
||||
getViewport: () => ({ top: 50, left: 40, viewportScrollX: 5, viewportScrollY: 6 }),
|
||||
};
|
||||
const skeleton = {
|
||||
getNoMergeCellWithCoordByIndex: (row: number, col: number) => {
|
||||
if (row === 1 && col === 2) {
|
||||
return { startX: 20, startY: 30 };
|
||||
}
|
||||
return { startX: 70, startY: 90 };
|
||||
},
|
||||
rowHeaderWidth: 10,
|
||||
columnHeaderHeight: 8,
|
||||
};
|
||||
const worksheet = {
|
||||
getFreeze: () => ({ startColumn: 3, startRow: 2, xSplit: 1, ySplit: 1 }),
|
||||
};
|
||||
|
||||
const result = transformPosition2Offset(60, 20, scene as any, skeleton as any, worksheet as any);
|
||||
expect(result).toEqual({ x: 110, y: 76 });
|
||||
|
||||
const bound = transformBound2OffsetBound({ left: 30, top: 20, right: 60, bottom: 70 } as any, scene as any, skeleton as any, worksheet as any);
|
||||
expect(bound.left).toBeTypeOf('number');
|
||||
expect(bound.right).toBeTypeOf('number');
|
||||
expect(bound.top).toBeTypeOf('number');
|
||||
expect(bound.bottom).toBeTypeOf('number');
|
||||
});
|
||||
|
||||
it('gets real cell range and hover cell position with overflow and merge fallback', () => {
|
||||
const overflowRange = { startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 };
|
||||
const skeleton = {
|
||||
overflowCache: {
|
||||
forValue: (cb: (...args: any[]) => void) => cb(1, 1, overflowRange),
|
||||
},
|
||||
getCellWithCoordByIndex: (row: number, col: number) => ({
|
||||
actualRow: row,
|
||||
actualColumn: col,
|
||||
mergeInfo: row === 1 && col === 1 ? { startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 } : null,
|
||||
}),
|
||||
getOffsetByColumn: (col: number) => col * 20,
|
||||
getOffsetByRow: (row: number) => row * 10,
|
||||
getCellIndexByOffset: () => ({ row: 1, column: 1 }),
|
||||
worksheet: { getMergedCell: () => null },
|
||||
};
|
||||
const workbook = { getUnitId: () => 'unit-1' };
|
||||
const worksheet = { getSheetId: () => 'sheet-1' };
|
||||
|
||||
const location = getCellRealRange(workbook as any, worksheet as any, skeleton as any, 2, 2);
|
||||
expect(location.row).toBe(1);
|
||||
expect(location.col).toBe(1);
|
||||
|
||||
const scene = {
|
||||
getActiveViewportByCoord: () => ({ viewportScrollX: 3, viewportScrollY: 4 }),
|
||||
getAncestorScale: () => ({ scaleX: 2, scaleY: 2 }),
|
||||
};
|
||||
const currentRender = { scene };
|
||||
const hover = getHoverCellPosition(
|
||||
currentRender as any,
|
||||
workbook as any,
|
||||
worksheet as any,
|
||||
{ skeleton, sheetId: 'sheet-1' } as any,
|
||||
100,
|
||||
120
|
||||
);
|
||||
expect(hover?.location.row).toBe(1);
|
||||
expect(hover?.overflowLocation.row).toBe(1);
|
||||
expect(hover?.position.startX).toBeTypeOf('number');
|
||||
|
||||
const noCellIndexSkeleton = {
|
||||
...skeleton,
|
||||
getCellIndexByOffset: () => ({ row: 1, column: 1 }),
|
||||
};
|
||||
const noActiveViewportRender = {
|
||||
scene: {
|
||||
getActiveViewportByCoord: () => null,
|
||||
getAncestorScale: () => ({ scaleX: 1, scaleY: 1 }),
|
||||
},
|
||||
};
|
||||
expect(getHoverCellPosition(noActiveViewportRender as any, workbook as any, worksheet as any, { skeleton: noCellIndexSkeleton, sheetId: 'sheet-1' } as any, 1, 2)).toBeNull();
|
||||
});
|
||||
});
|
||||
44
packages/sheets-ui/src/common/keys.ts
Normal file
44
packages/sheets-ui/src/common/keys.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const SHEET_ZOOM_RANGE = [10, 400];
|
||||
|
||||
/**
|
||||
* TODO@ggg: To avoid introducing the range-selector of the sheet-formula-ui package into sheet-ui, use a component key for now
|
||||
*/
|
||||
export const RANGE_SELECTOR_COMPONENT_KEY = 'RANGE_SELECTOR_COMPONENT_KEY';
|
||||
export const EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY = 'EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY';
|
||||
export const EMBEDDING_CELL_EDITOR_COMPONENT_KEY = 'EMBEDDING_CELL_EDITOR_COMPONENT_KEY';
|
||||
// end
|
||||
|
||||
export enum SHEET_VIEW_KEY {
|
||||
MAIN = '__SpreadsheetRender__',
|
||||
ROW = '__SpreadsheetRowHeader__',
|
||||
COLUMN = '__SpreadsheetColumnHeader__',
|
||||
LEFT_TOP = '__SpreadsheetLeftTopPlaceholder__',
|
||||
}
|
||||
|
||||
export const SHEET_COMPONENT_MAIN_LAYER_INDEX = 0;
|
||||
|
||||
export const SHEET_COMPONENT_SELECTION_LAYER_INDEX = 1;
|
||||
|
||||
export const SHEET_COMPONENT_HEADER_LAYER_INDEX = 10;
|
||||
|
||||
export const SHEET_COMPONENT_HEADER_SELECTION_LAYER_INDEX = 11;
|
||||
|
||||
export const SHEET_COMPONENT_UNHIDE_LAYER_INDEX = 12;
|
||||
|
||||
// TODO@wzhudev: there should be a global zIndex layer fo sheet selections // And global zIndex for extension
|
||||
258
packages/sheets-ui/src/common/utils.ts
Normal file
258
packages/sheets-ui/src/common/utils.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 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 { IPosition, IRange, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IBoundRectNoAngle, IRender, Scene, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { ICollaborator } from '@univerjs/protocol';
|
||||
import type { ISheetLocation, ISheetSkeletonManagerParam } from '@univerjs/sheets';
|
||||
import { SHEET_VIEWPORT_KEY, Vector2 } from '@univerjs/engine-render';
|
||||
|
||||
export function getUserListEqual(userList1: ICollaborator[], userList2: ICollaborator[]) {
|
||||
if (userList1.length !== userList2.length) return false;
|
||||
|
||||
const sorted1 = userList1.sort((a, b) => a.id.localeCompare(b.id));
|
||||
const sorted2 = userList2.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
return sorted1.every((user, index) => {
|
||||
return user.subject?.userID === sorted2[index].subject?.userID && user.role === sorted2[index].role;
|
||||
});
|
||||
}
|
||||
|
||||
export function checkCellContentInRanges(worksheet: Worksheet, ranges: IRange[]): boolean {
|
||||
return ranges.some((range) => checkCellContentInRange(worksheet, range));
|
||||
}
|
||||
|
||||
export function checkCellContentInRange(worksheet: Worksheet, range: IRange): boolean {
|
||||
const { startRow, startColumn, endColumn, endRow } = range;
|
||||
const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn);
|
||||
|
||||
let someCellGoingToBeRemoved = false;
|
||||
cellMatrix.forValue((row, col, cellData) => {
|
||||
if (cellData && (row !== startRow || col !== startColumn) && worksheet.cellHasValue(cellData)) {
|
||||
someCellGoingToBeRemoved = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return someCellGoingToBeRemoved;
|
||||
}
|
||||
|
||||
export function getCellIndexByOffsetWithMerge(offsetX: number, offsetY: number, scene: Scene, skeleton: SpreadsheetSkeleton) {
|
||||
const activeViewport = scene.getActiveViewportByCoord(
|
||||
Vector2.FromArray([offsetX, offsetY])
|
||||
);
|
||||
|
||||
if (!activeViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
const scrollXY = {
|
||||
x: activeViewport.viewportScrollX,
|
||||
y: activeViewport.viewportScrollY,
|
||||
};
|
||||
|
||||
const cellIndex = skeleton.getCellIndexByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY);
|
||||
|
||||
// const mergeCell = skeleton.mergeData.find((range) => {
|
||||
// const { startColumn, startRow, endColumn, endRow } = range;
|
||||
// return cellPos.row >= startRow && cellPos.column >= startColumn && cellPos.row <= endRow && cellPos.column <= endColumn;
|
||||
// });
|
||||
const mergeCell = skeleton.worksheet.getMergedCell(cellIndex.row, cellIndex.column);
|
||||
|
||||
const params = {
|
||||
actualRow: mergeCell ? mergeCell.startRow : cellIndex.row,
|
||||
actualCol: mergeCell ? mergeCell.startColumn : cellIndex.column,
|
||||
mergeCell,
|
||||
row: cellIndex.row,
|
||||
col: cellIndex.column,
|
||||
};
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export function getViewportByCell(row: number, column: number, scene: Scene, worksheet: Worksheet) {
|
||||
const freeze = worksheet.getFreeze();
|
||||
if (!freeze || (freeze.startRow <= 0 && freeze.startColumn <= 0)) {
|
||||
return scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
}
|
||||
|
||||
if (row >= freeze.startRow && column >= freeze.startColumn) {
|
||||
return scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
}
|
||||
|
||||
if (row < freeze.startRow && column < freeze.startColumn) {
|
||||
return scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP);
|
||||
}
|
||||
|
||||
if (row < freeze.startRow && column >= freeze.startColumn) {
|
||||
return scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN_TOP);
|
||||
}
|
||||
|
||||
if (row >= freeze.startRow && column < freeze.startColumn) {
|
||||
return scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformBound2OffsetBound(originBound: IBoundRectNoAngle, scene: Scene, skeleton: SpreadsheetSkeleton, worksheet: Worksheet): IBoundRectNoAngle {
|
||||
const topLeft = transformPosition2Offset(originBound.left, originBound.top, scene, skeleton, worksheet);
|
||||
const bottomRight = transformPosition2Offset(originBound.right, originBound.bottom, scene, skeleton, worksheet);
|
||||
|
||||
return {
|
||||
left: topLeft.x,
|
||||
top: topLeft.y,
|
||||
right: bottomRight.x,
|
||||
bottom: bottomRight.y,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformPosition2Offset(x: number, y: number, scene: Scene, skeleton: SpreadsheetSkeleton, worksheet: Worksheet) {
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
const viewMain = scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
if (!viewMain) {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
const freeze = worksheet.getFreeze();
|
||||
const { startColumn, startRow, xSplit, ySplit } = freeze;
|
||||
// freeze start
|
||||
const startSheetView = skeleton.getNoMergeCellWithCoordByIndex(startRow - ySplit, startColumn - xSplit, false);
|
||||
// freeze end
|
||||
const endSheetView = skeleton.getNoMergeCellWithCoordByIndex(startRow, startColumn, false);
|
||||
const { rowHeaderWidth, columnHeaderHeight } = skeleton;
|
||||
const freezeWidth = endSheetView.startX - startSheetView.startX;
|
||||
const freezeHeight = endSheetView.startY - startSheetView.startY;
|
||||
|
||||
const { top, left, viewportScrollX, viewportScrollY } = viewMain;
|
||||
let offsetX: number;
|
||||
// viewMain or viewTop
|
||||
if (x > left) {
|
||||
offsetX = (x - viewportScrollX) * scaleX;
|
||||
} else {
|
||||
offsetX = ((freezeWidth + rowHeaderWidth) - (left - x)) * scaleX;
|
||||
}
|
||||
|
||||
let offsetY: number;
|
||||
if (y > top) {
|
||||
offsetY = (y - viewportScrollY) * scaleY;
|
||||
} else {
|
||||
offsetY = ((freezeHeight + columnHeaderHeight) - (top - y)) * scaleX;
|
||||
}
|
||||
|
||||
return {
|
||||
x: offsetX,
|
||||
y: offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCellRealRange(workbook: Workbook, worksheet: Worksheet, skeleton: SpreadsheetSkeleton, row: number, col: number) {
|
||||
let actualRow = row;
|
||||
let actualCol = col;
|
||||
|
||||
skeleton.overflowCache.forValue((r, c, range) => {
|
||||
if (range.startRow <= actualRow && range.endRow >= actualRow && range.startColumn <= actualCol && range.endColumn >= actualCol) {
|
||||
actualCol = c;
|
||||
actualRow = r;
|
||||
}
|
||||
});
|
||||
|
||||
const actualCell = skeleton.getCellWithCoordByIndex(actualRow, actualCol);
|
||||
|
||||
const location: ISheetLocation = {
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
workbook,
|
||||
worksheet,
|
||||
row: actualCell.actualRow,
|
||||
col: actualCell.actualColumn,
|
||||
};
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
export function getHoverCellPosition(currentRender: IRender, workbook: Workbook, worksheet: Worksheet, skeletonParam: ISheetSkeletonManagerParam, offsetX: number, offsetY: number) {
|
||||
const { scene } = currentRender;
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const { skeleton, sheetId } = skeletonParam;
|
||||
|
||||
const cellIndex = getCellIndexByOffsetWithMerge(offsetX, offsetY, scene, skeleton);
|
||||
|
||||
if (!cellIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { actualCol, actualRow } = cellIndex;
|
||||
const originLocation = {
|
||||
unitId,
|
||||
subUnitId: sheetId,
|
||||
workbook,
|
||||
worksheet,
|
||||
row: actualRow,
|
||||
col: actualCol,
|
||||
};
|
||||
|
||||
skeleton.overflowCache.forValue((r, c, range) => {
|
||||
if (range.startRow <= actualRow && range.endRow >= actualRow && range.startColumn <= actualCol && range.endColumn >= actualCol) {
|
||||
actualCol = c;
|
||||
actualRow = r;
|
||||
}
|
||||
});
|
||||
|
||||
const actualCell = skeleton.getCellWithCoordByIndex(actualRow, actualCol);
|
||||
const location: ISheetLocation = getCellRealRange(workbook, worksheet, skeleton, actualRow, actualCol);
|
||||
let anchorCell: IRange;
|
||||
if (actualCell.mergeInfo) {
|
||||
anchorCell = actualCell.mergeInfo;
|
||||
} else {
|
||||
anchorCell = {
|
||||
startRow: location.row,
|
||||
endRow: location.row,
|
||||
startColumn: location.col,
|
||||
endColumn: location.col,
|
||||
};
|
||||
}
|
||||
|
||||
const activeViewport = scene.getActiveViewportByCoord(
|
||||
Vector2.FromArray([offsetX, offsetY])
|
||||
);
|
||||
|
||||
if (!activeViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
const scrollXY = {
|
||||
x: activeViewport.viewportScrollX,
|
||||
y: activeViewport.viewportScrollY,
|
||||
};
|
||||
|
||||
const position: IPosition = {
|
||||
startX: (skeleton.getOffsetByColumn(anchorCell.startColumn - 1) - scrollXY.x) * scaleX,
|
||||
endX: (skeleton.getOffsetByColumn(anchorCell.endColumn) - scrollXY.x) * scaleX,
|
||||
startY: (skeleton.getOffsetByRow(anchorCell.startRow - 1) - scrollXY.y) * scaleY,
|
||||
endY: (skeleton.getOffsetByRow(anchorCell.endRow) - scrollXY.y) * scaleY,
|
||||
};
|
||||
|
||||
return {
|
||||
position,
|
||||
location: originLocation,
|
||||
overflowLocation: location,
|
||||
};
|
||||
}
|
||||
163
packages/sheets-ui/src/config/config.ts
Normal file
163
packages/sheets-ui/src/config/config.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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 { DependencyOverride } from '@univerjs/core';
|
||||
import type { IScrollBarProps } from '@univerjs/engine-render';
|
||||
import type { ComponentType, MenuConfig } from '@univerjs/ui';
|
||||
import type { IPermissionDetailUserPartProps } from '../views/permission/panel-detail/PermissionDetailUserPart';
|
||||
|
||||
export const SHEETS_UI_PLUGIN_CONFIG_KEY = 'sheets-ui.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsUIConfig {
|
||||
menu?: MenuConfig;
|
||||
disableAutoFocus?: true;
|
||||
override?: DependencyOverride;
|
||||
|
||||
/**
|
||||
* The maximum count of rows triggering auto height. This is used to avoid performance issue.
|
||||
* @default 1000
|
||||
*/
|
||||
maxAutoHeightCount?: number;
|
||||
|
||||
/**
|
||||
* Whether to show the formula bar.
|
||||
*/
|
||||
formulaBar?: boolean;
|
||||
|
||||
/**
|
||||
* The config of the footer.
|
||||
* @default {}
|
||||
*/
|
||||
footer?: false | {
|
||||
/**
|
||||
* Sheet bar is the manager of sub sheets, including add/switch/delete sub sheets.
|
||||
* @default true
|
||||
*/
|
||||
sheetBar?: boolean;
|
||||
|
||||
/**
|
||||
* statistic bar including statistic info current selections, such as count, sum, average, etc.
|
||||
* @default true
|
||||
*/
|
||||
statisticBar?: boolean;
|
||||
|
||||
/**
|
||||
* Including the menus in the footer. such as highlight, gridlines, etc.
|
||||
* @default true
|
||||
*/
|
||||
menus?: boolean;
|
||||
|
||||
/**
|
||||
* Zoom slider is the zoom slider in the footer.
|
||||
* @default true
|
||||
*/
|
||||
zoomSlider?: boolean;
|
||||
|
||||
/**
|
||||
* The config of the add sheet button in the sheet bar.
|
||||
*/
|
||||
addSheetButtonConfig?: {
|
||||
/**
|
||||
* Whether to show the add sheet button in the sheet bar.
|
||||
* When `footer` or `footer.sheetBar` is false, this config will not work because the sheet bar will not be rendered.
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
/**
|
||||
* The default row count of the new sheet created by the add sheet button.
|
||||
* @default 1000
|
||||
*/
|
||||
defaultRowCount?: number;
|
||||
/**
|
||||
* The default column count of the new sheet created by the add sheet button.
|
||||
* @default 20
|
||||
*/
|
||||
defaultColumnCount?: number;
|
||||
};
|
||||
};
|
||||
|
||||
clipboardConfig?: {
|
||||
hidePasteOptions?: boolean;
|
||||
};
|
||||
|
||||
/** The config of the scroll bar. */
|
||||
scrollConfig?: IScrollBarProps;
|
||||
|
||||
/**
|
||||
* Strategy for showing the protected range shadow.
|
||||
* - true or 'always': Show shadow for all protected ranges (default behavior)
|
||||
* - 'non-editable': Only show shadow for ranges that cannot be edited (Edit permission is false)
|
||||
* - 'non-viewable': Only show shadow for ranges that cannot be viewed (View permission is false)
|
||||
* - false or 'none': Never show shadow for protected ranges
|
||||
* @default true
|
||||
*/
|
||||
protectedRangeShadow?: boolean | 'always' | 'non-editable' | 'non-viewable' | 'none';
|
||||
|
||||
/**
|
||||
* The custom component of the protected range user selector.
|
||||
*/
|
||||
protectedRangeUserSelector?: {
|
||||
/**
|
||||
* custom component, should implement the `IPermissionDetailUserPartProps` interface.
|
||||
*/
|
||||
component: ComponentType<IPermissionDetailUserPartProps>;
|
||||
/**
|
||||
* The framework of the component. Must be passed correctly.
|
||||
*/
|
||||
framework: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether to disable the force string alert.
|
||||
* @default false
|
||||
*/
|
||||
disableForceStringAlert?: boolean;
|
||||
/**
|
||||
* Whether to disable the force string mark.
|
||||
* @default false
|
||||
*/
|
||||
disableForceStringMark?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to disable the edit.
|
||||
* @default false
|
||||
*/
|
||||
disableEdit?: boolean;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsUIConfig = {
|
||||
formulaBar: true,
|
||||
protectedRangeShadow: true,
|
||||
maxAutoHeightCount: 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the protectedRangeShadow config to shadow strategy
|
||||
* - true -> 'always'
|
||||
* - false -> 'none'
|
||||
* - string values remain unchanged
|
||||
* @param config The protectedRangeShadow config value
|
||||
* @returns The shadow strategy
|
||||
*/
|
||||
export function convertToShadowStrategy(
|
||||
config?: boolean | 'always' | 'non-editable' | 'non-viewable' | 'none'
|
||||
): 'always' | 'non-editable' | 'non-viewable' | 'none' {
|
||||
if (config === true) return 'always';
|
||||
if (config === false) return 'none';
|
||||
return config || 'always';
|
||||
}
|
||||
51
packages/sheets-ui/src/consts/mobile-context.ts
Normal file
51
packages/sheets-ui/src/consts/mobile-context.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Context key indicating that pinch-to-zoom gesture is in progress on mobile.
|
||||
* Used to prevent selection operations during pinch zoom.
|
||||
*/
|
||||
export const MOBILE_PINCH_ZOOMING = 'MOBILE_PINCH_ZOOMING';
|
||||
|
||||
/**
|
||||
* Context key indicating that selection is being expanded on mobile (dragging fill controls).
|
||||
* Used to prevent scroll/inertia/zoom operations during selection expansion.
|
||||
*/
|
||||
export const MOBILE_EXPANDING_SELECTION = 'MOBILE_EXPANDING_SELECTION';
|
||||
|
||||
/**
|
||||
* Context key indicating that the next selection-end event was triggered by a mobile long press.
|
||||
* Used to ensure the context menu only opens for long-press gestures, not normal taps.
|
||||
*/
|
||||
export const MOBILE_TRIGGER_CONTEXT_MENU = 'MOBILE_TRIGGER_CONTEXT_MENU';
|
||||
|
||||
/** Whether the software keyboard currently occupies the mobile visual viewport. */
|
||||
export const MOBILE_KEYBOARD_VISIBLE = 'MOBILE_KEYBOARD_VISIBLE';
|
||||
|
||||
/** Whether the mobile formula editor currently occupies the immersive full-screen surface. */
|
||||
export const MOBILE_FX_EDITOR_EXPANDED = 'MOBILE_FX_EDITOR_EXPANDED';
|
||||
|
||||
/** Whether the mobile formula function panel is open above the formula editor. */
|
||||
export const MOBILE_FORMULA_FUNCTION_PANEL_OPEN = 'MOBILE_FORMULA_FUNCTION_PANEL_OPEN';
|
||||
|
||||
/** Whether the compact mobile formula operator strip is currently visible. */
|
||||
export const MOBILE_FORMULA_OPERATORS_VISIBLE = 'MOBILE_FORMULA_OPERATORS_VISIBLE';
|
||||
|
||||
/** Height of the compact mobile formula operator strip in pixels. */
|
||||
export const MOBILE_FORMULA_OPERATOR_BAR_HEIGHT = 36;
|
||||
|
||||
/** Submit the compact mobile formula bar without unmounting its input. */
|
||||
export const MOBILE_FORMULA_BAR_SUBMIT_COMMAND_ID = 'sheet.command.mobile-formula-bar-submit';
|
||||
61
packages/sheets-ui/src/consts/permission.ts
Normal file
61
packages/sheets-ui/src/consts/permission.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 { UnitAction } from '@univerjs/protocol';
|
||||
|
||||
export const UNIVER_SHEET_PERMISSION_PLUGIN_NAME = 'UNIVER_SHEET_PERMISSION_PLUGIN';
|
||||
export const UNIVER_SHEET_PERMISSION_PANEL = 'UNIVER_SHEET_PERMISSION_PANEL';
|
||||
export const UNIVER_SHEET_PERMISSION_USER_PART = 'UNIVER_SHEET_PERMISSION_USER_PART';
|
||||
export const UNIVER_SHEET_PERMISSION_PANEL_FOOTER = 'UNIVER_SHEET_PERMISSION_PANEL_FOOTER';
|
||||
export const UNIVER_SHEET_PERMISSION_USER_DIALOG = 'UNIVER_SHEET_PERMISSION_USER_DIALOG';
|
||||
export const UNIVER_SHEET_PERMISSION_DIALOG = 'UNIVER_SHEET_PERMISSION_DIALOG';
|
||||
export const UNIVER_SHEET_PERMISSION_USER_DIALOG_ID = 'UNIVER_SHEET_PERMISSION_USER_DIALOG_ID';
|
||||
export const UNIVER_SHEET_PERMISSION_DIALOG_ID = 'UNIVER_SHEET_PERMISSION_DIALOG_ID';
|
||||
|
||||
export const subUnitPermissionTypeMap: Partial<Record<UnitAction, string>> = {
|
||||
[UnitAction.Copy]: 'Copy',
|
||||
[UnitAction.SetCellStyle]: 'SetCellStyle',
|
||||
[UnitAction.SetCellValue]: 'SetCellValue',
|
||||
[UnitAction.SetRowStyle]: 'SetRowStyle',
|
||||
[UnitAction.SetColumnStyle]: 'SetColumnStyle',
|
||||
[UnitAction.InsertRow]: 'InsertRow',
|
||||
[UnitAction.InsertColumn]: 'InsertColumn',
|
||||
[UnitAction.InsertHyperlink]: 'InsertHyperlink',
|
||||
[UnitAction.DeleteRow]: 'DeleteRow',
|
||||
[UnitAction.DeleteColumn]: 'DeleteColumn',
|
||||
[UnitAction.Sort]: 'Sort',
|
||||
[UnitAction.Filter]: 'Filter',
|
||||
[UnitAction.PivotTable]: 'PivotTable',
|
||||
[UnitAction.EditExtraObject]: 'EditExtraObject',
|
||||
};
|
||||
|
||||
export const defaultWorksheetUnitActionList: UnitAction[] = [
|
||||
UnitAction.Copy,
|
||||
UnitAction.SetCellStyle,
|
||||
UnitAction.SetCellValue,
|
||||
UnitAction.SetRowStyle,
|
||||
UnitAction.SetColumnStyle,
|
||||
UnitAction.InsertRow,
|
||||
UnitAction.InsertColumn,
|
||||
UnitAction.InsertHyperlink,
|
||||
UnitAction.DeleteRow,
|
||||
UnitAction.DeleteColumn,
|
||||
UnitAction.Sort,
|
||||
UnitAction.Filter,
|
||||
UnitAction.PivotTable,
|
||||
UnitAction.EditExtraObject,
|
||||
UnitAction.View,
|
||||
];
|
||||
17
packages/sheets-ui/src/consts/plugin-name.ts
Normal file
17
packages/sheets-ui/src/consts/plugin-name.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const SHEET_UI_PLUGIN_NAME = 'SheetUI';
|
||||
21
packages/sheets-ui/src/consts/ui-name.ts
Normal file
21
packages/sheets-ui/src/consts/ui-name.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum SheetsUIPart {
|
||||
FILTER_PANEL_EMBED_POINT = 'filter-panel-embed-point',
|
||||
SHEETS_FOOTER = 'sheets-footer',
|
||||
FORMULA_AUX = 'formula-aux',
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
BooleanNumber,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
RedoCommand,
|
||||
Tools,
|
||||
UndoCommand,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { CanvasColorService, ICanvasColorService, IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
CancelMarkDirtyRowAutoHeightOperation,
|
||||
MarkDirtyRowAutoHeightOperation,
|
||||
SetColWidthCommand,
|
||||
SetWorksheetColWidthMutation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { SheetSkeletonManagerService } from '../../services/sheet-skeleton-manager.service';
|
||||
import { AutoHeightController } from '../auto-height.controller';
|
||||
|
||||
const RANGE: IRange = { startRow: 1, endRow: 3, startColumn: 0, endColumn: 2 };
|
||||
|
||||
describe('AutoHeightController', () => {
|
||||
let univer: Univer;
|
||||
|
||||
beforeEach(() => {
|
||||
// Happy DOM has no canvas backend; the render and skeleton services remain real.
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(function (this: HTMLCanvasElement) {
|
||||
return { canvas: this, setTransform: vi.fn(), clearRect: vi.fn() } as unknown as CanvasRenderingContext2D;
|
||||
});
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([AutoHeightController]);
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
injector.get(SheetSkeletonService);
|
||||
injector.get(SheetInterceptorService);
|
||||
injector.get(IRenderManagerService).registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetSkeletonManagerService]);
|
||||
injector.get(ICommandService).registerCommand(SetWorksheetRowAutoHeightMutation);
|
||||
injector.get(ICommandService).registerCommand(SetColWidthCommand);
|
||||
injector.get(ICommandService).registerCommand(SetWorksheetColWidthMutation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
for (const current of ['none', 'other-workbook', 'other-worksheet'] as const) {
|
||||
it(`updates only the explicit child worksheet with ${current} current and restores its height on undo`, () => {
|
||||
const injector = univer.__getInjector();
|
||||
const instances = injector.get(IUniverInstanceService);
|
||||
const renderManager = injector.get(IRenderManagerService);
|
||||
const peer = instances.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookSnapshot('peer'),
|
||||
{ makeCurrent: false }
|
||||
);
|
||||
const child = instances.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookSnapshot('child'),
|
||||
{ makeCurrent: false }
|
||||
);
|
||||
renderManager.createRender('peer');
|
||||
renderManager.createRender('child');
|
||||
if (current !== 'none') {
|
||||
instances.setCurrentUnitForType(current === 'other-workbook' ? 'peer' : 'child');
|
||||
}
|
||||
injector.get(AutoHeightController);
|
||||
const worksheet = child.getSheetBySheetId('sheet2')!;
|
||||
const peerBefore = Tools.deepClone(peer.getSnapshot());
|
||||
const otherSheetBefore = Tools.deepClone(child.getSheetBySheetId('sheet1')!.getSnapshot());
|
||||
const rowsBefore = Tools.deepClone(worksheet.getSnapshot().rowData);
|
||||
const result = injector.get(SheetInterceptorService).generateMutationsOfAutoHeight({
|
||||
unitId: 'child',
|
||||
subUnitId: 'sheet2',
|
||||
ranges: [RANGE],
|
||||
});
|
||||
|
||||
expect(result.redos).toEqual([{
|
||||
id: SetWorksheetRowAutoHeightMutation.id,
|
||||
params: { unitId: 'child', subUnitId: 'sheet2', rowsAutoHeightInfo: [{ row: 1, autoHeight: 20 }] },
|
||||
}]);
|
||||
const commands = injector.get(ICommandService);
|
||||
for (const mutation of result.redos) {
|
||||
expect(commands.syncExecuteCommand(mutation.id, mutation.params)).toBe(true);
|
||||
}
|
||||
expect(worksheet.getRowHeight(1)).toBe(20);
|
||||
expect(worksheet.getRowHeight(3)).toBe(50);
|
||||
for (const mutation of result.undos) {
|
||||
expect(commands.syncExecuteCommand(mutation.id, mutation.params)).toBe(true);
|
||||
}
|
||||
expect(worksheet.getSnapshot().rowData).toEqual(rowsBefore);
|
||||
for (const mutation of result.redos) {
|
||||
expect(commands.syncExecuteCommand(mutation.id, mutation.params)).toBe(true);
|
||||
}
|
||||
expect(worksheet.getRowHeight(1)).toBe(20);
|
||||
expect(peer.getSnapshot()).toEqual(peerBefore);
|
||||
expect(child.getSheetBySheetId('sheet1')!.getSnapshot()).toEqual(otherSheetBefore);
|
||||
});
|
||||
}
|
||||
|
||||
for (const current of ['none', 'peer'] as const) {
|
||||
it(`completes explicit column resizing and its undo/redo with ${current} current`, async () => {
|
||||
const injector = univer.__getInjector();
|
||||
const instances = injector.get(IUniverInstanceService);
|
||||
const peer = instances.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookSnapshot('peer'),
|
||||
{ makeCurrent: false }
|
||||
);
|
||||
const child = instances.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookSnapshot('child'),
|
||||
{ makeCurrent: false }
|
||||
);
|
||||
injector.get(IRenderManagerService).createRender('peer');
|
||||
injector.get(IRenderManagerService).createRender('child');
|
||||
if (current === 'peer') {
|
||||
instances.setCurrentUnitForType('peer');
|
||||
instances.focusUnit('peer');
|
||||
}
|
||||
injector.get(AutoHeightController);
|
||||
const target = child.getSheetBySheetId('sheet2')!;
|
||||
const before = Tools.deepClone(target.getSnapshot());
|
||||
const peerBefore = Tools.deepClone(peer.getSnapshot());
|
||||
const commands = injector.get(ICommandService);
|
||||
|
||||
expect(await commands.executeCommand(SetColWidthCommand.id, {
|
||||
unitId: 'child',
|
||||
subUnitId: 'sheet2',
|
||||
ranges: [RANGE],
|
||||
value: 120,
|
||||
})).toBe(true);
|
||||
expect(target.getColumnWidth(1)).toBe(120);
|
||||
expect(target.getRowHeight(1)).toBe(20);
|
||||
expect(peer.getSnapshot()).toEqual(peerBefore);
|
||||
const resized = Tools.deepClone(target.getSnapshot());
|
||||
instances.focusUnit('child');
|
||||
expect(await commands.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(target.getColumnWidth(1)).toBe(before.defaultColumnWidth);
|
||||
expect(target.getSnapshot().rowData).toEqual(before.rowData);
|
||||
expect(await commands.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(target.getSnapshot()).toEqual(resized);
|
||||
expect(peer.getSnapshot()).toEqual(peerBefore);
|
||||
});
|
||||
}
|
||||
|
||||
for (const missing of ['unit', 'worksheet', 'render'] as const) {
|
||||
it(`does not fall back to the current Sheet when the explicit target ${missing} is missing`, () => {
|
||||
const injector = univer.__getInjector();
|
||||
const peer = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookSnapshot('peer'));
|
||||
injector.get(IRenderManagerService).createRender('peer');
|
||||
injector.get(IUniverInstanceService).createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookSnapshot('child'),
|
||||
{ makeCurrent: false }
|
||||
);
|
||||
if (missing !== 'render') {
|
||||
injector.get(IRenderManagerService).createRender('child');
|
||||
}
|
||||
injector.get(AutoHeightController);
|
||||
const before = Tools.deepClone(peer.getSnapshot());
|
||||
const result = injector.get(SheetInterceptorService).generateMutationsOfAutoHeight({
|
||||
unitId: missing === 'unit' ? 'missing' : 'child',
|
||||
subUnitId: missing === 'worksheet' ? 'missing' : 'sheet2',
|
||||
ranges: [RANGE],
|
||||
});
|
||||
expect(result).toEqual({ redos: [], undos: [], preRedos: [], preUndos: [] });
|
||||
expect(peer.getSnapshot()).toEqual(before);
|
||||
});
|
||||
}
|
||||
|
||||
it('preserves implicit current-worksheet calls and filters unchanged or manually sized rows', () => {
|
||||
const injector = univer.__getInjector();
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookSnapshot('current'));
|
||||
workbook.setActiveSheet(workbook.getSheetBySheetId('sheet2')!);
|
||||
injector.get(IRenderManagerService).createRender('current');
|
||||
const result = injector.get(AutoHeightController).getUndoRedoParamsOfAutoHeight([RANGE]);
|
||||
expect(result.redos).toEqual([{
|
||||
id: SetWorksheetRowAutoHeightMutation.id,
|
||||
params: { unitId: 'current', subUnitId: 'sheet2', rowsAutoHeightInfo: [{ row: 1, autoHeight: 20 }] },
|
||||
}]);
|
||||
expect(result.undos).toEqual([{
|
||||
id: SetWorksheetRowAutoHeightMutation.id,
|
||||
params: { unitId: 'current', subUnitId: 'sheet2', rowsAutoHeightInfo: [{ row: 1, autoHeight: 40 }] },
|
||||
}]);
|
||||
});
|
||||
|
||||
it('keeps lazy dirty-row operations paired and unregisters the interceptor on disposal', () => {
|
||||
const injector = univer.__getInjector();
|
||||
univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookSnapshot('current'));
|
||||
injector.get(IRenderManagerService).createRender('current');
|
||||
const controller = injector.get(AutoHeightController);
|
||||
const interceptor = injector.get(SheetInterceptorService);
|
||||
const context = { unitId: 'current', subUnitId: 'sheet2', ranges: [], lazyAutoHeightRanges: [RANGE] };
|
||||
const result = interceptor.generateMutationsOfAutoHeight(context);
|
||||
expect(result.redos).toEqual([{
|
||||
id: MarkDirtyRowAutoHeightOperation.id,
|
||||
params: { unitId: 'current', subUnitId: 'sheet2', ranges: [RANGE], id: expect.any(String) },
|
||||
options: { onlyLocal: true },
|
||||
}]);
|
||||
expect(result.undos).toEqual([{
|
||||
id: CancelMarkDirtyRowAutoHeightOperation.id,
|
||||
params: { unitId: 'current', subUnitId: 'sheet2', id: (result.redos[0].params as { id: string }).id },
|
||||
options: { onlyLocal: true },
|
||||
}]);
|
||||
controller.dispose();
|
||||
expect(interceptor.generateMutationsOfAutoHeight(context)).toEqual({ redos: [], undos: [], preRedos: [], preUndos: [] });
|
||||
});
|
||||
});
|
||||
|
||||
function workbookSnapshot(id: string): IWorkbookData {
|
||||
return {
|
||||
id,
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: id,
|
||||
styles: {},
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Peer sheet',
|
||||
rowCount: 5,
|
||||
columnCount: 3,
|
||||
defaultRowHeight: 30,
|
||||
rowData: { 1: { ah: 60 } },
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Target sheet',
|
||||
rowCount: 5,
|
||||
columnCount: 3,
|
||||
defaultRowHeight: 20,
|
||||
rowData: { 1: { ah: 40 }, 2: { ah: 20 }, 3: { h: 50, ia: BooleanNumber.FALSE } },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 { UnitAction } from '@univerjs/protocol';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CellCustomRenderController } from '../cell-custom-render.controller';
|
||||
|
||||
function createEventSubject() {
|
||||
const subscribers = new Set<(evt: any) => void>();
|
||||
|
||||
return {
|
||||
subscribeEvent: vi.fn((handler: (evt: any) => void) => {
|
||||
subscribers.add(handler);
|
||||
const unsubscribe = vi.fn(() => subscribers.delete(handler));
|
||||
|
||||
return { dispose: unsubscribe, unsubscribe };
|
||||
}),
|
||||
emit: (evt: any) => subscribers.forEach((handler) => handler(evt)),
|
||||
};
|
||||
}
|
||||
|
||||
function createController(options?: { editable?: boolean; selectionEditable?: boolean }) {
|
||||
const pointerDown$ = createEventSubject();
|
||||
const pointerMove$ = createEventSubject();
|
||||
const renderA = {
|
||||
zIndex: 1,
|
||||
isHit: vi.fn(() => true),
|
||||
onPointerDown: vi.fn(),
|
||||
onPointerEnter: vi.fn(),
|
||||
onPointerLeave: vi.fn(),
|
||||
};
|
||||
const renderB = {
|
||||
zIndex: 2,
|
||||
isHit: vi.fn(() => false),
|
||||
onPointerEnter: vi.fn(),
|
||||
onPointerLeave: vi.fn(),
|
||||
};
|
||||
const cellData = {
|
||||
customRender: [renderB, renderA],
|
||||
selectionProtection: [{ [UnitAction.Edit]: options?.selectionEditable ?? true }],
|
||||
};
|
||||
const worksheet = {
|
||||
getSheetId: vi.fn(() => 'sheet-1'),
|
||||
getMergedCell: vi.fn(() => null),
|
||||
getCell: vi.fn(() => cellData),
|
||||
getComposedCellStyleByCellData: vi.fn(() => ({ fs: 12 })),
|
||||
};
|
||||
const skeleton = {
|
||||
worksheet,
|
||||
getCellIndexByOffset: vi.fn(() => ({ row: 1, column: 2 })),
|
||||
getCellWithCoordByIndex: vi.fn(() => ({ startX: 10, startY: 20 })),
|
||||
};
|
||||
const currentSkeleton$ = new Subject<any>();
|
||||
const spreadsheet = {
|
||||
onPointerDown$: pointerDown$,
|
||||
onPointerMove$: pointerMove$,
|
||||
};
|
||||
const scene = {
|
||||
getActiveViewportByCoord: vi.fn(() => ({ viewportScrollX: 5, viewportScrollY: 6 })),
|
||||
getAncestorScale: vi.fn(() => ({ scaleX: 2, scaleY: 2 })),
|
||||
};
|
||||
const controller = new CellCustomRenderController(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
unit: { getActiveSheet: vi.fn(() => worksheet) },
|
||||
} as any,
|
||||
{
|
||||
currentSkeleton$,
|
||||
getCurrentParam: vi.fn(() => ({ skeleton })),
|
||||
} as any,
|
||||
{
|
||||
getRenderUnitById: vi.fn(() => ({ mainComponent: spreadsheet, scene })),
|
||||
} as any,
|
||||
{
|
||||
composePermission: vi.fn(() => [{ value: options?.editable ?? true }]),
|
||||
} as any
|
||||
);
|
||||
|
||||
return { controller, pointerDown$, pointerMove$, renderA, renderB, currentSkeleton$, skeleton, worksheet };
|
||||
}
|
||||
|
||||
describe('CellCustomRenderController', () => {
|
||||
it('dispatches pointer events to hit custom renderers when sheet and cell are editable', async () => {
|
||||
const { controller, pointerDown$, pointerMove$, renderA, renderB } = createController();
|
||||
|
||||
pointerDown$.emit({ offsetX: 20, offsetY: 30 });
|
||||
pointerMove$.emit({ offsetX: 20, offsetY: 30 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 35));
|
||||
renderA.isHit.mockReturnValue(false);
|
||||
pointerMove$.emit({ offsetX: 200, offsetY: 300 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 35));
|
||||
|
||||
expect(renderA.onPointerDown).toHaveBeenCalled();
|
||||
expect(renderA.onPointerEnter).toHaveBeenCalled();
|
||||
expect(renderA.onPointerLeave).toHaveBeenCalled();
|
||||
expect(renderB.onPointerEnter).not.toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not dispatch pointer down when workbook or protected cell is not editable', () => {
|
||||
const nonEditable = createController({ editable: false });
|
||||
nonEditable.pointerDown$.emit({ offsetX: 20, offsetY: 30 });
|
||||
expect(nonEditable.renderA.onPointerDown).not.toHaveBeenCalled();
|
||||
nonEditable.controller.dispose();
|
||||
|
||||
const protectedCell = createController({ selectionEditable: false });
|
||||
protectedCell.pointerDown$.emit({ offsetX: 20, offsetY: 30 });
|
||||
expect(protectedCell.renderA.onPointerDown).not.toHaveBeenCalled();
|
||||
protectedCell.controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* 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 { CellValueType } from '@univerjs/core';
|
||||
import { CURSOR_TYPE, getCurrentTypeOfRenderer } from '@univerjs/engine-render';
|
||||
import { INTERCEPTOR_POINT, SetWorksheetColWidthMutation, TextToNumberCommand, ToggleCellCheckboxCommand } from '@univerjs/sheets';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CellAlertType } from '../../services/cell-alert-manager.service';
|
||||
import { AutoWidthController, createAutoColWidthUndoMutationsByRedos } from '../auto-width.controller';
|
||||
import { SheetCheckboxController } from '../checkbox.controller';
|
||||
import { ForceStringAlertRenderController } from '../force-string-alert-render.controller';
|
||||
import { ForceStringRenderController } from '../force-string-render.controller';
|
||||
|
||||
const mainComponentMock = vi.hoisted(() => ({
|
||||
setCursor: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@univerjs/engine-render', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/engine-render')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
getCurrentTypeOfRenderer: vi.fn(() => ({
|
||||
mainComponent: mainComponentMock,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SheetCheckboxController', () => {
|
||||
it('uses pointer cursor for checkbox bullets and toggles the clicked checkbox paragraph', () => {
|
||||
const currentRichText$ = new Subject<{ bullet?: { startIndex: number } } | null>();
|
||||
const currentClickedCell$ = new Subject<{
|
||||
location: { unitId: string; subUnitId: string; row: number; col: number };
|
||||
bullet?: { startIndex: number };
|
||||
}>();
|
||||
const commandService = { executeCommand: vi.fn() };
|
||||
const controller = new SheetCheckboxController(
|
||||
{ currentRichText$, currentClickedCell$ } as never,
|
||||
{} as never,
|
||||
commandService as never,
|
||||
{} as never
|
||||
);
|
||||
|
||||
currentRichText$.next({ bullet: { startIndex: 3 } });
|
||||
expect(mainComponentMock.setCursor).toHaveBeenCalledWith(CURSOR_TYPE.POINTER);
|
||||
expect(getCurrentTypeOfRenderer).toHaveBeenCalled();
|
||||
currentRichText$.next(null);
|
||||
expect(mainComponentMock.setCursor).toHaveBeenCalledWith(CURSOR_TYPE.AUTO);
|
||||
|
||||
currentClickedCell$.next({
|
||||
location: { unitId: 'unit-1', subUnitId: 'sheet-1', row: 2, col: 4 },
|
||||
bullet: { startIndex: 7 },
|
||||
});
|
||||
currentClickedCell$.next({
|
||||
location: { unitId: 'unit-1', subUnitId: 'sheet-1', row: 3, col: 5 },
|
||||
});
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledTimes(1);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(ToggleCellCheckboxCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 4,
|
||||
paragraphIndex: 7,
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ForceStringRenderController', () => {
|
||||
it('marks numeric-looking string cells unless disabled or formatted as text', () => {
|
||||
let interceptor: typeof INTERCEPTOR_POINT.CELL_CONTENT | undefined;
|
||||
const controller = new ForceStringRenderController(
|
||||
{ unitId: 'unit-1' } as never,
|
||||
{ getCurrentParam: vi.fn(() => ({ skeleton: {} })) } as never,
|
||||
{
|
||||
intercept: vi.fn((point, config) => {
|
||||
expect(point).toBe(INTERCEPTOR_POINT.CELL_CONTENT);
|
||||
interceptor = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as never,
|
||||
{ getConfig: vi.fn(() => ({ disableForceStringMark: false })) } as never
|
||||
);
|
||||
const workbook = {
|
||||
getStyles: vi.fn(() => ({
|
||||
get: vi.fn((styleId: string) => styleId === 'text' ? { n: { pattern: '@' } } : undefined),
|
||||
})),
|
||||
};
|
||||
const rawCell = { v: '123', t: CellValueType.STRING };
|
||||
|
||||
if (!interceptor) {
|
||||
throw new Error('CELL_CONTENT interceptor was not registered');
|
||||
}
|
||||
|
||||
const result = interceptor.handler(rawCell, {
|
||||
rawData: rawCell,
|
||||
workbook,
|
||||
} as never, (cell) => cell);
|
||||
|
||||
expect(result).not.toBe(rawCell);
|
||||
expect(result?.markers?.tl).toEqual({ size: 6, color: '#409f11' });
|
||||
|
||||
expect(interceptor.handler({ ...rawCell, s: 'text' }, {
|
||||
rawData: { ...rawCell, s: 'text' },
|
||||
workbook,
|
||||
} as never, (cell) => cell)).not.toHaveProperty('markers');
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ForceStringAlertRenderController', () => {
|
||||
it('shows the alert only for a numeric-text primary cell in a single selection', () => {
|
||||
const selectionChanged$ = new Subject<Array<{
|
||||
range: { startRow: number; endRow: number; startColumn: number; endColumn: number };
|
||||
primary: { actualRow: number; actualColumn: number };
|
||||
}>>();
|
||||
const cellAlertManagerService = {
|
||||
currentAlert: new Map(),
|
||||
showAlert: vi.fn((alert) => {
|
||||
cellAlertManagerService.currentAlert.set(alert.key, { alert });
|
||||
}),
|
||||
removeAlert: vi.fn((key) => cellAlertManagerService.currentAlert.delete(key)),
|
||||
};
|
||||
const worksheet = {
|
||||
getSheetId: vi.fn(() => 'sheet-1'),
|
||||
getCell: vi.fn((row: number) => row === 1
|
||||
? { v: '123', t: CellValueType.STRING }
|
||||
: { v: '123', t: CellValueType.STRING, s: 'text' }),
|
||||
};
|
||||
const workbook = {
|
||||
activeSheet$: new BehaviorSubject(worksheet),
|
||||
getUnitId: vi.fn(() => 'unit-1'),
|
||||
getActiveSheet: vi.fn(() => worksheet),
|
||||
getStyles: vi.fn(() => ({
|
||||
get: vi.fn((styleId: string) => styleId === 'text' ? { n: { pattern: '@' } } : undefined),
|
||||
})),
|
||||
};
|
||||
let disableForceStringAlert = false;
|
||||
const commandService = {
|
||||
syncExecuteCommand: vi.fn(() => true),
|
||||
};
|
||||
const currentUnit$ = new BehaviorSubject(workbook);
|
||||
const controller = new ForceStringAlertRenderController(
|
||||
{ unit: workbook } as never,
|
||||
{
|
||||
selectionChanged$,
|
||||
currentSelectionParam: { unitId: 'unit-1', sheetId: 'sheet-1' },
|
||||
getCurrentSelections: vi.fn(() => []),
|
||||
} as never,
|
||||
cellAlertManagerService as never,
|
||||
{ t: vi.fn((key: string) => key) } as never,
|
||||
{ getConfig: vi.fn(() => ({ disableForceStringAlert })) } as never,
|
||||
commandService as never,
|
||||
{ getCurrentTypeOfUnit$: vi.fn(() => currentUnit$) } as never
|
||||
);
|
||||
|
||||
const selection = {
|
||||
range: { startRow: 1, endRow: 3, startColumn: 2, endColumn: 4 },
|
||||
primary: { actualRow: 1, actualColumn: 2 },
|
||||
};
|
||||
selectionChanged$.next([selection]);
|
||||
expect(cellAlertManagerService.showAlert).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: CellAlertType.WARNING,
|
||||
title: 'sheets-ui.info.error',
|
||||
message: 'sheets-ui.info.forceStringInfo',
|
||||
key: 'SHEET_FORCE_STRING_ALERT:unit-1',
|
||||
location: { unitId: 'unit-1', subUnitId: 'sheet-1', row: 1, col: 2 },
|
||||
menu: [expect.objectContaining({ label: 'sheets-ui.info.convertToNumber' })],
|
||||
}));
|
||||
|
||||
const alert = cellAlertManagerService.showAlert.mock.calls[0][0];
|
||||
alert.menu[0].onSelect();
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenCalledWith(TextToNumberCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
ranges: [selection.range],
|
||||
});
|
||||
|
||||
selectionChanged$.next([selection, { ...selection }]);
|
||||
expect(cellAlertManagerService.removeAlert).toHaveBeenCalledWith('SHEET_FORCE_STRING_ALERT:unit-1');
|
||||
|
||||
selectionChanged$.next([{
|
||||
range: { startRow: 2, endRow: 2, startColumn: 2, endColumn: 2 },
|
||||
primary: { actualRow: 2, actualColumn: 2 },
|
||||
}]);
|
||||
expect(cellAlertManagerService.showAlert).toHaveBeenCalledTimes(1);
|
||||
|
||||
disableForceStringAlert = true;
|
||||
selectionChanged$.next([selection]);
|
||||
expect(cellAlertManagerService.showAlert).toHaveBeenCalledTimes(1);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('scopes alerts to the current workbook and refreshes them when the active sheet changes', () => {
|
||||
const selection = {
|
||||
range: { startRow: 1, endRow: 1, startColumn: 2, endColumn: 2 },
|
||||
primary: { actualRow: 1, actualColumn: 2 },
|
||||
};
|
||||
const selectionChanged$ = new Subject<typeof selection[]>();
|
||||
const selectionService = {
|
||||
selectionChanged$,
|
||||
currentSelectionParam: { unitId: 'unit-2', sheetId: 'sheet-1' },
|
||||
getCurrentSelections: vi.fn(() => [selection]),
|
||||
};
|
||||
const sheet1 = {
|
||||
getSheetId: vi.fn(() => 'sheet-1'),
|
||||
getCell: vi.fn(() => ({ v: '123', t: CellValueType.STRING })),
|
||||
};
|
||||
const sheet2 = {
|
||||
getSheetId: vi.fn(() => 'sheet-2'),
|
||||
getCell: vi.fn(() => ({ v: 'abc', t: CellValueType.STRING })),
|
||||
};
|
||||
let activeSheet = sheet1;
|
||||
const activeSheet$ = new BehaviorSubject(activeSheet);
|
||||
const workbook = {
|
||||
activeSheet$,
|
||||
getUnitId: vi.fn(() => 'unit-1'),
|
||||
getActiveSheet: vi.fn(() => activeSheet),
|
||||
getStyles: vi.fn(() => ({ get: vi.fn() })),
|
||||
};
|
||||
const cellAlertManagerService = {
|
||||
currentAlert: new Map(),
|
||||
showAlert: vi.fn((alert) => cellAlertManagerService.currentAlert.set(alert.key, { alert })),
|
||||
removeAlert: vi.fn((key) => cellAlertManagerService.currentAlert.delete(key)),
|
||||
};
|
||||
const currentUnit$ = new BehaviorSubject<object>({ getUnitId: () => 'unit-2' });
|
||||
const controller = new ForceStringAlertRenderController(
|
||||
{ unit: workbook } as never,
|
||||
selectionService as never,
|
||||
cellAlertManagerService as never,
|
||||
{ t: vi.fn((key: string) => key) } as never,
|
||||
{ getConfig: vi.fn(() => ({})) } as never,
|
||||
{ syncExecuteCommand: vi.fn(() => true) } as never,
|
||||
{ getCurrentTypeOfUnit$: vi.fn(() => currentUnit$) } as never
|
||||
);
|
||||
|
||||
expect(cellAlertManagerService.showAlert).not.toHaveBeenCalled();
|
||||
|
||||
selectionService.currentSelectionParam = { unitId: 'unit-1', sheetId: 'sheet-1' };
|
||||
currentUnit$.next(workbook);
|
||||
expect(cellAlertManagerService.showAlert).toHaveBeenCalledTimes(1);
|
||||
|
||||
selectionService.currentSelectionParam = { unitId: 'unit-2', sheetId: 'sheet-1' };
|
||||
currentUnit$.next({ getUnitId: () => 'unit-2' });
|
||||
expect(cellAlertManagerService.currentAlert.size).toBe(0);
|
||||
|
||||
activeSheet = sheet2;
|
||||
selectionService.currentSelectionParam = { unitId: 'unit-1', sheetId: 'sheet-2' };
|
||||
currentUnit$.next(workbook);
|
||||
activeSheet$.next(sheet2);
|
||||
expect(cellAlertManagerService.currentAlert.size).toBe(0);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AutoWidthController', () => {
|
||||
it('creates undo params from existing column widths', () => {
|
||||
expect(createAutoColWidthUndoMutationsByRedos({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
ranges: [{ startColumn: 1, endColumn: 3, startRow: 0, endRow: 0 }],
|
||||
colWidth: { 1: 80, 2: 90, 3: 100 },
|
||||
}, {
|
||||
getColumnManager: () => ({
|
||||
getColumnWidth: (col: number) => col * 10,
|
||||
}),
|
||||
} as never)).toEqual({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
ranges: [{ startColumn: 1, endColumn: 3, startRow: 0, endRow: 0 }],
|
||||
colWidth: { 1: 10, 2: 20, 3: 30 },
|
||||
});
|
||||
});
|
||||
|
||||
it('builds auto-width undo and redo mutations from calculated skeleton widths', () => {
|
||||
const worksheet = {
|
||||
getSheetId: () => 'sheet-1',
|
||||
getColumnManager: () => ({
|
||||
getColumnWidth: (col: number) => col === 2 ? 88 : 66,
|
||||
}),
|
||||
};
|
||||
const controller = new AutoWidthController(
|
||||
{
|
||||
getRenderUnitById: vi.fn(() => ({
|
||||
with: vi.fn(() => ({
|
||||
getCurrentParam: vi.fn(() => ({
|
||||
skeleton: {
|
||||
calculateAutoWidthInRange: vi.fn(() => [
|
||||
{ col: 2, width: 120 },
|
||||
{ col: 3, width: 140 },
|
||||
]),
|
||||
},
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
} as never,
|
||||
{
|
||||
getUnit: vi.fn(() => ({
|
||||
getUnitId: () => 'unit-1',
|
||||
getSheetBySheetId: vi.fn(() => worksheet),
|
||||
})),
|
||||
} as never
|
||||
);
|
||||
|
||||
expect(controller.getUndoRedoParamsOfColWidth({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
ranges: [{ startColumn: 2, endColumn: 3, startRow: 0, endRow: 0 }],
|
||||
})).toEqual({
|
||||
undos: [{
|
||||
id: SetWorksheetColWidthMutation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
ranges: [{ startColumn: 2, endColumn: 3, startRow: 0, endRow: 0 }],
|
||||
colWidth: { 2: 88, 3: 66 },
|
||||
},
|
||||
}],
|
||||
redos: [{
|
||||
id: SetWorksheetColWidthMutation.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
ranges: [{ startColumn: 2, endColumn: 3, startRow: 0, endRow: 0 }],
|
||||
colWidth: { 2: 120, 3: 140 },
|
||||
},
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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 { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SHEET_VIEW_KEY } from '../../common/keys';
|
||||
import { HoverRenderController } from '../hover-render.controller';
|
||||
|
||||
function createEventSubject() {
|
||||
const handlers = new Set<(evt: any) => void>();
|
||||
|
||||
return {
|
||||
subscribeEvent: vi.fn((handler: (evt: any) => void) => {
|
||||
handlers.add(handler);
|
||||
return { dispose: vi.fn(() => handlers.delete(handler)) };
|
||||
}),
|
||||
emit: (evt: any) => handlers.forEach((handler) => handler(evt)),
|
||||
};
|
||||
}
|
||||
|
||||
describe('HoverRenderController', () => {
|
||||
it('forwards sheet body and header pointer events to hover manager services', () => {
|
||||
const currentSkeleton$ = new Subject<any>();
|
||||
const validViewportScrollInfo$ = new Subject<void>();
|
||||
const mainComponent = {
|
||||
onPointerEnter$: createEventSubject(),
|
||||
onPointerMove$: createEventSubject(),
|
||||
onPointerDown$: createEventSubject(),
|
||||
onPointerUp$: createEventSubject(),
|
||||
onClick$: createEventSubject(),
|
||||
onDblclick$: createEventSubject(),
|
||||
onPointerLeave$: createEventSubject(),
|
||||
};
|
||||
const rowHeader = {
|
||||
onPointerMove$: createEventSubject(),
|
||||
onPointerDown$: createEventSubject(),
|
||||
onPointerUp$: createEventSubject(),
|
||||
onDblclick$: createEventSubject(),
|
||||
};
|
||||
const colHeader = {
|
||||
onPointerMove$: createEventSubject(),
|
||||
onPointerDown$: createEventSubject(),
|
||||
onPointerUp$: createEventSubject(),
|
||||
onDblclick$: createEventSubject(),
|
||||
};
|
||||
const hoverManagerService = {
|
||||
triggerMouseMove: vi.fn(),
|
||||
triggerPointerDown: vi.fn(),
|
||||
triggerPointerUp: vi.fn(),
|
||||
triggerClick: vi.fn(),
|
||||
triggerDbClick: vi.fn(),
|
||||
triggerRowHeaderMouseMove: vi.fn(),
|
||||
triggerRowHeaderPoniterDown: vi.fn(),
|
||||
triggerRowHeaderPoniterUp: vi.fn(),
|
||||
triggerRowHeaderClick: vi.fn(),
|
||||
triggerRowHeaderDbClick: vi.fn(),
|
||||
triggerColHeaderMouseMove: vi.fn(),
|
||||
triggerColHeaderPoniterDown: vi.fn(),
|
||||
triggerColHeaderPoniterUp: vi.fn(),
|
||||
triggerColHeaderClick: vi.fn(),
|
||||
triggerColHeaderDbClick: vi.fn(),
|
||||
triggerScroll: vi.fn(),
|
||||
};
|
||||
const controller = new HoverRenderController(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
mainComponent,
|
||||
components: new Map([
|
||||
[SHEET_VIEW_KEY.ROW, rowHeader],
|
||||
[SHEET_VIEW_KEY.COLUMN, colHeader],
|
||||
]),
|
||||
} as never,
|
||||
hoverManagerService as never,
|
||||
{
|
||||
getCurrentParam: vi.fn(() => ({ skeleton: {} })),
|
||||
currentSkeleton$,
|
||||
} as never,
|
||||
{ validViewportScrollInfo$ } as never
|
||||
);
|
||||
|
||||
mainComponent.onPointerEnter$.emit({});
|
||||
expect(controller.active).toBe(true);
|
||||
mainComponent.onPointerMove$.emit({ offsetX: 10, offsetY: 20 });
|
||||
mainComponent.onPointerDown$.emit({ offsetX: 11, offsetY: 21 });
|
||||
mainComponent.onPointerUp$.emit({ offsetX: 12, offsetY: 22 });
|
||||
mainComponent.onClick$.emit({ offsetX: 13, offsetY: 23 });
|
||||
mainComponent.onDblclick$.emit({ offsetX: 14, offsetY: 24 });
|
||||
mainComponent.onPointerLeave$.emit({});
|
||||
rowHeader.onPointerUp$.emit({ offsetX: 1, offsetY: 2 });
|
||||
colHeader.onDblclick$.emit({ offsetX: 3, offsetY: 4 });
|
||||
validViewportScrollInfo$.next();
|
||||
|
||||
expect(controller.active).toBe(false);
|
||||
expect(hoverManagerService.triggerMouseMove).toHaveBeenCalledWith('unit-1', { offsetX: 10, offsetY: 20 });
|
||||
expect(hoverManagerService.triggerPointerDown).toHaveBeenCalledWith('unit-1', { offsetX: 11, offsetY: 21 });
|
||||
expect(hoverManagerService.triggerPointerUp).toHaveBeenCalledWith('unit-1', { offsetX: 12, offsetY: 22 });
|
||||
expect(hoverManagerService.triggerClick).toHaveBeenCalledWith('unit-1', 13, 23);
|
||||
expect(hoverManagerService.triggerDbClick).toHaveBeenCalledWith('unit-1', 14, 24);
|
||||
expect(hoverManagerService.triggerRowHeaderPoniterUp).toHaveBeenCalledWith('unit-1', 1, 2);
|
||||
expect(hoverManagerService.triggerRowHeaderClick).toHaveBeenCalledWith('unit-1', 1, 2);
|
||||
expect(hoverManagerService.triggerColHeaderDbClick).toHaveBeenCalledWith('unit-1', 3, 4);
|
||||
expect(hoverManagerService.triggerScroll).toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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 { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import { ICommandService, RANGE_TYPE } from '@univerjs/core';
|
||||
import { SelectionMoveType, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MoveRangeConfirmCommand } from '../../commands/commands/move-range-confirm.command';
|
||||
import { ISheetSelectionRenderService } from '../../services/selection/base-selection-render.service';
|
||||
import { MoveRangeRenderController } from '../move-range.controller';
|
||||
import { createRenderTestBed } from '../render-controllers/__tests__/render-test-bed';
|
||||
|
||||
describe('MoveRangeRenderController', () => {
|
||||
it('executes the confirmation command after dragging a selection to a valid different range', () => {
|
||||
const controlMoveEnd$ = new Subject<{ startRow: number; startColumn: number; endRow: number; endColumn: number }>();
|
||||
const testBed = createRenderTestBed({
|
||||
dependencies: [
|
||||
[ISheetSelectionRenderService, {
|
||||
useValue: {
|
||||
getSelectionControls: () => [{
|
||||
model: {
|
||||
getRange: () => ({
|
||||
startRow: 1,
|
||||
startColumn: 2,
|
||||
endRow: 3,
|
||||
endColumn: 4,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
}),
|
||||
},
|
||||
selectionMoveEnd$: controlMoveEnd$,
|
||||
}],
|
||||
},
|
||||
}],
|
||||
],
|
||||
});
|
||||
const commandService = testBed.injector.get(ICommandService);
|
||||
const executeCommand = vi.spyOn(commandService, 'executeCommand').mockResolvedValue(true);
|
||||
const controller = testBed.injector.createInstance(MoveRangeRenderController, testBed.context);
|
||||
const selectionManagerService = testBed.injector.get(SheetsSelectionsService);
|
||||
const selection: ISelectionWithStyle = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
startColumn: 2,
|
||||
endRow: 3,
|
||||
endColumn: 4,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
},
|
||||
primary: null,
|
||||
style: null,
|
||||
};
|
||||
|
||||
selectionManagerService.setSelections([selection], SelectionMoveType.MOVE_END);
|
||||
controlMoveEnd$.next({ startRow: 5, startColumn: 6, endRow: 7, endColumn: 8 });
|
||||
controlMoveEnd$.next({ startRow: 1, startColumn: 2, endRow: 3, endColumn: 4 });
|
||||
controlMoveEnd$.next({ startRow: -1, startColumn: 2, endRow: 3, endColumn: 4 });
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledTimes(1);
|
||||
expect(executeCommand).toHaveBeenCalledWith(MoveRangeConfirmCommand.id, {
|
||||
fromRange: {
|
||||
startRow: 1,
|
||||
startColumn: 2,
|
||||
endRow: 3,
|
||||
endColumn: 4,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 5,
|
||||
startColumn: 6,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
},
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* 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 { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import {
|
||||
CellValueType,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
RANGE_TYPE,
|
||||
touchDependencies,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel, FUNCTION_NAMES_MATH, FUNCTION_NAMES_STATISTICAL } from '@univerjs/engine-formula';
|
||||
import {
|
||||
INumfmtService,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { IStatusBarService, StatusBarService } from '../../services/status-bar.service';
|
||||
import { StatusBarController } from '../status-bar.controller';
|
||||
|
||||
const unitId = 'test';
|
||||
const sheetId = 'sheet1';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: unitId,
|
||||
sheetOrder: [sheetId],
|
||||
name: '',
|
||||
appVersion: '0.15.4',
|
||||
locale: LocaleType.EN_US,
|
||||
styles: {},
|
||||
sheets: {
|
||||
[sheetId]: {
|
||||
id: sheetId,
|
||||
name: 'Sheet1',
|
||||
rowCount: 10,
|
||||
columnCount: 10,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { t: CellValueType.NUMBER, v: 1 },
|
||||
},
|
||||
1: {
|
||||
0: { t: CellValueType.NUMBER, v: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function createTestBed() {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
([
|
||||
[SheetsSelectionsService],
|
||||
[IStatusBarService, { useClass: StatusBarService }],
|
||||
[INumfmtService, { useValue: { getValue: () => ({ pattern: '0.00' }) } }],
|
||||
[FormulaDataModel, { useValue: { getArrayFormulaCellData: () => undefined } }],
|
||||
[StatusBarController],
|
||||
] as Dependency[]).forEach((d) => this._injector.add(d));
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[StatusBarController],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, TEST_WORKBOOK_DATA);
|
||||
get(SheetsSelectionsService);
|
||||
get(StatusBarController);
|
||||
|
||||
// Trigger SheetsSelectionsService's current workbook stream.
|
||||
get(IUniverInstanceService).focusUnit(unitId);
|
||||
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
commandService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('StatusBarController', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
const testBed = createTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
commandService = testBed.commandService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('updates status bar values on selection changes and cell updates within selection', () => {
|
||||
const workbook = get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getActiveSheet()!;
|
||||
|
||||
const statusBarService = get(IStatusBarService);
|
||||
|
||||
const selection: ISelectionWithStyle = {
|
||||
range: { startRow: 0, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: {
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
actualRow: 0,
|
||||
actualColumn: 0,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
style: null,
|
||||
};
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetSelectionsOperation.id, {
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
selections: [selection],
|
||||
})).toBeTruthy();
|
||||
|
||||
vi.advanceTimersByTime(150);
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
const state1 = statusBarService.getState();
|
||||
expect(state1?.pattern).toBe('0.00');
|
||||
expect(state1?.values.find((v) => v.func === FUNCTION_NAMES_MATH.SUM)?.value).toBe(3);
|
||||
expect(state1?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.MAX)?.value).toBe(2);
|
||||
expect(state1?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.MIN)?.value).toBe(1);
|
||||
expect(state1?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.COUNT)?.value).toBe(2);
|
||||
expect(state1?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.COUNTA)?.value).toBe(2);
|
||||
expect(state1?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.AVERAGE)?.value).toBe(1.5);
|
||||
|
||||
// Update a cell inside the current selection; controller should recalc via commandExecuted hook.
|
||||
expect(commandService.syncExecuteCommand(SetRangeValuesMutation.id, {
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
cellValue: {
|
||||
0: {
|
||||
0: { t: CellValueType.NUMBER, v: 10 },
|
||||
},
|
||||
},
|
||||
})).toBeTruthy();
|
||||
|
||||
vi.advanceTimersByTime(150);
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
const state2 = statusBarService.getState();
|
||||
expect(state2?.values.find((v) => v.func === FUNCTION_NAMES_MATH.SUM)?.value).toBe(12);
|
||||
expect(state2?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.MAX)?.value).toBe(10);
|
||||
expect(state2?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.MIN)?.value).toBe(2);
|
||||
expect(state2?.values.find((v) => v.func === FUNCTION_NAMES_STATISTICAL.AVERAGE)?.value).toBe(6);
|
||||
|
||||
// Business sanity: workbook data actually updated.
|
||||
expect(workbook.getActiveSheet()!.getCellRaw(0, 0)?.v).toBe(10);
|
||||
});
|
||||
});
|
||||
287
packages/sheets-ui/src/controllers/auto-fill-ui.controller.ts
Normal file
287
packages/sheets-ui/src/controllers/auto-fill-ui.controller.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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 { ICommandInfo, IExecutionOptions, IMutationCommonParams, IRange, Nullable, UnitModel, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { IAutoFillLocation, IRemoveSheetMutationParams } from '@univerjs/sheets';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
} from '@univerjs/core';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import {
|
||||
AUTO_FILL_HOOK_TYPE,
|
||||
AutoClearContentCommand,
|
||||
AutoFillCommand,
|
||||
AutoFillController,
|
||||
IAutoFillService,
|
||||
InsertColMutation,
|
||||
InsertRowMutation,
|
||||
MoveColsMutation,
|
||||
MoveRangeMutation,
|
||||
MoveRowsMutation,
|
||||
RefillCommand,
|
||||
RemoveColMutation,
|
||||
RemoveRowMutation,
|
||||
RemoveSheetMutation,
|
||||
SetRangeValuesCommand,
|
||||
SetRangeValuesMutation,
|
||||
SetWorksheetActiveOperation,
|
||||
SetWorksheetColWidthMutation,
|
||||
SetWorksheetRowHeightMutation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { SetCellEditVisibleOperation } from '../commands/operations/cell-edit.operation';
|
||||
import { SetZoomRatioOperation } from '../commands/operations/set-zoom-ratio.operation';
|
||||
import { IEditorBridgeService } from '../services/editor-bridge.service';
|
||||
import { ISheetSelectionRenderService } from '../services/selection/base-selection-render.service';
|
||||
import { SheetsRenderService } from '../services/sheets-render.service';
|
||||
|
||||
export class AutoFillUIController extends Disposable {
|
||||
private _currentLocation: Nullable<IAutoFillLocation> = null;
|
||||
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IAutoFillService private readonly _autoFillService: IAutoFillService,
|
||||
@Inject(AutoFillController) private _autoFillController: AutoFillController,
|
||||
@Inject(SheetsRenderService) private _sheetsRenderService: SheetsRenderService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._initDefaultHook();
|
||||
this._initQuitListener();
|
||||
this._initSkeletonChange();
|
||||
}
|
||||
|
||||
private _initSkeletonChange() {
|
||||
this.disposeWithMe(this._sheetsRenderService.registerSkeletonChangingMutations(AutoFillCommand.id));
|
||||
}
|
||||
|
||||
private _initDefaultHook() {
|
||||
this.disposeWithMe(this._autoFillService.addHook({
|
||||
id: 'default-ui',
|
||||
type: AUTO_FILL_HOOK_TYPE.DEFAULT,
|
||||
priority: 0,
|
||||
onBeforeFillData: (location: IAutoFillLocation) => {
|
||||
this._currentLocation = location;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private _initQuitListener() {
|
||||
const quitCommands = [
|
||||
SetCellEditVisibleOperation.id,
|
||||
AutoClearContentCommand.id,
|
||||
SetRangeValuesCommand.id,
|
||||
SetZoomRatioOperation.id,
|
||||
SetWorksheetActiveOperation.id,
|
||||
MoveRangeMutation.id,
|
||||
RemoveRowMutation.id,
|
||||
RemoveColMutation.id,
|
||||
InsertRowMutation.id,
|
||||
InsertColMutation.id,
|
||||
MoveRowsMutation.id,
|
||||
MoveColsMutation.id,
|
||||
SetWorksheetColWidthMutation.id,
|
||||
SetWorksheetRowHeightMutation.id,
|
||||
SetRangeValuesMutation.id,
|
||||
];
|
||||
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options?: IExecutionOptions) => {
|
||||
const fromCollab = options?.fromCollab;
|
||||
const fromSync = options?.fromSync;
|
||||
const fromFormula = options?.fromFormula;
|
||||
if (
|
||||
quitCommands.includes(command.id)
|
||||
&& !fromCollab
|
||||
&& !fromSync
|
||||
&& !fromFormula
|
||||
&& (command.params as IMutationCommonParams).trigger !== AutoFillCommand.id
|
||||
&& (command.params as IMutationCommonParams).trigger !== RefillCommand.id
|
||||
) {
|
||||
this._quit();
|
||||
}
|
||||
if (command.id === RemoveSheetMutation.id) {
|
||||
if ((command.params as IRemoveSheetMutationParams).unitId === this._currentLocation?.unitId &&
|
||||
(command.params as IRemoveSheetMutationParams).subUnitId === this._currentLocation?.subUnitId) {
|
||||
this._quit();
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.disposeWithMe(this._univerInstanceService.unitDisposed$.subscribe((unit: UnitModel) => {
|
||||
if (unit.getUnitId() === this._currentLocation?.unitId) {
|
||||
this._quit();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _quit() {
|
||||
this._currentLocation = null;
|
||||
this._autoFillController.quit();
|
||||
this._autoFillService.setShowMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoFillRenderController extends Disposable implements IRenderModule {
|
||||
private readonly _selectionControlDisposables = new DisposableCollection();
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(ISheetSelectionRenderService) private readonly _selectionRenderService: ISheetSelectionRenderService,
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initSelectionControlFillChanged();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._selectionControlDisposables.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _initSelectionControlFillChanged(): void {
|
||||
this._updateSelectionControlListeners();
|
||||
this.disposeWithMe(this._selectionManagerService.selectionChanged$.subscribe(() => {
|
||||
this._updateSelectionControlListeners();
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateSelectionControlListeners(): void {
|
||||
// Selection controls are recreated when the selected ranges change.
|
||||
this._selectionControlDisposables.dispose();
|
||||
|
||||
this._selectionRenderService.getSelectionControls().forEach((controlSelection) => {
|
||||
this._selectionControlDisposables.add(controlSelection.selectionFilled$.subscribe((filled) => {
|
||||
if (
|
||||
filled == null ||
|
||||
filled.startColumn === -1 ||
|
||||
filled.startRow === -1 ||
|
||||
filled.endColumn === -1 ||
|
||||
filled.endRow === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceRange: IRange = {
|
||||
startColumn: controlSelection.model.startColumn,
|
||||
endColumn: controlSelection.model.endColumn,
|
||||
startRow: controlSelection.model.startRow,
|
||||
endRow: controlSelection.model.endRow,
|
||||
};
|
||||
const targetRange: IRange = {
|
||||
startColumn: filled.startColumn,
|
||||
endColumn: filled.endColumn,
|
||||
startRow: filled.startRow,
|
||||
endRow: filled.endRow,
|
||||
};
|
||||
|
||||
this._executeAutoFill(sourceRange, targetRange);
|
||||
}));
|
||||
|
||||
// Double click has the same effect as dragging the fill control, but its target range is detected automatically.
|
||||
this._selectionControlDisposables.add(controlSelection.fillControl.onDblclick$.subscribeEvent(() => {
|
||||
const sourceRange: IRange = {
|
||||
startColumn: controlSelection.model.startColumn,
|
||||
endColumn: controlSelection.model.endColumn,
|
||||
startRow: controlSelection.model.startRow,
|
||||
endRow: controlSelection.model.endRow,
|
||||
};
|
||||
this._handleDbClickFill(sourceRange);
|
||||
}));
|
||||
|
||||
this._selectionControlDisposables.add(controlSelection.fillControl.onPointerDown$.subscribeEvent(() => {
|
||||
const visibleState = this._editorBridgeService.isVisible();
|
||||
if (visibleState.visible) {
|
||||
this._commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, {
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.PointerDown,
|
||||
unitId: this._context.unitId,
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private _executeAutoFill(sourceRange: IRange, targetRange: IRange): void {
|
||||
const subUnitId = this._context.unit.getActiveSheet().getSheetId();
|
||||
this._commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange,
|
||||
targetRange,
|
||||
unitId: this._context.unitId,
|
||||
subUnitId,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleDbClickFill(source: IRange) {
|
||||
const worksheet = this._context.unit.getActiveSheet();
|
||||
const selection = detectAutoFillRange(source, worksheet);
|
||||
// double click only works when dest range is longer than source range
|
||||
if (selection.endRow <= source.endRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Double click uses the same fill command as dragging, with a target range calculated from adjacent data.
|
||||
this._executeAutoFill(source, selection);
|
||||
}
|
||||
}
|
||||
|
||||
export function detectAutoFillRange(source: IRange, worksheet: Worksheet): IRange {
|
||||
const { startRow, endRow, startColumn, endColumn } = source;
|
||||
const matrix = worksheet.getCellMatrix();
|
||||
const maxRow = worksheet.getMaxRows();
|
||||
const maxColumn = worksheet.getMaxColumns();
|
||||
let detectEndRow = endRow + 1;
|
||||
// left column first, or consider right column.
|
||||
if (startColumn > 0 && matrix.getValue(detectEndRow, startColumn - 1)?.v != null) {
|
||||
while (matrix.getValue(detectEndRow + 1, startColumn - 1)?.v != null && detectEndRow < maxRow) {
|
||||
detectEndRow += 1;
|
||||
}
|
||||
} else if (endColumn < maxColumn - 1 && matrix.getValue(detectEndRow, endColumn + 1)?.v != null) {
|
||||
while (matrix.getValue(detectEndRow + 1, endColumn + 1)?.v != null && detectEndRow < maxRow) {
|
||||
detectEndRow += 1;
|
||||
}
|
||||
} else {
|
||||
detectEndRow = endRow;
|
||||
}
|
||||
|
||||
// If the fill range contains data, stop filling at the first row of data.
|
||||
for (let i = endRow + 1; i <= detectEndRow; i++) {
|
||||
for (let j = startColumn; j <= endColumn; j++) {
|
||||
if (matrix.getValue(i, j)?.v != null) {
|
||||
detectEndRow = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startColumn,
|
||||
endColumn,
|
||||
startRow,
|
||||
endRow: detectEndRow,
|
||||
};
|
||||
}
|
||||
149
packages/sheets-ui/src/controllers/auto-height.controller.ts
Normal file
149
packages/sheets-ui/src/controllers/auto-height.controller.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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 { IMutationInfo, IRange, ObjectMatrix } from '@univerjs/core';
|
||||
import type { ISetWorksheetRowAutoHeightMutationParams } from '@univerjs/sheets';
|
||||
import { Disposable, generateRandomId, Inject, IUniverInstanceService } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
CancelMarkDirtyRowAutoHeightOperation,
|
||||
getSheetCommandTarget,
|
||||
MarkDirtyRowAutoHeightOperation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SetWorksheetRowAutoHeightMutationFactory,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service';
|
||||
|
||||
export class AutoHeightController extends Disposable {
|
||||
constructor(
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _processLazyAutoHeight(redoUndoItem: { redos: IMutationInfo[]; undos: IMutationInfo[] }, unitId: string, subUnitId: string, lazyAutoHeightRanges?: IRange[]) {
|
||||
if (lazyAutoHeightRanges?.length) {
|
||||
const redo = {
|
||||
id: MarkDirtyRowAutoHeightOperation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: lazyAutoHeightRanges,
|
||||
id: generateRandomId(),
|
||||
},
|
||||
options: {
|
||||
onlyLocal: true,
|
||||
},
|
||||
};
|
||||
const undo = {
|
||||
id: CancelMarkDirtyRowAutoHeightOperation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
id: redo.params.id,
|
||||
},
|
||||
options: {
|
||||
onlyLocal: true,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
redos: [...redoUndoItem.redos, redo],
|
||||
undos: [...redoUndoItem.undos, undo],
|
||||
};
|
||||
}
|
||||
|
||||
return redoUndoItem;
|
||||
}
|
||||
|
||||
getUndoRedoParamsOfAutoHeight(
|
||||
ranges: IRange[],
|
||||
subUnitIdParam?: string,
|
||||
currentCellHeights?: ObjectMatrix<number>,
|
||||
unitIdParam?: string
|
||||
): { redos: IMutationInfo[]; undos: IMutationInfo[] } {
|
||||
const target = getSheetCommandTarget(this._univerInstanceService, { unitId: unitIdParam, subUnitId: subUnitIdParam });
|
||||
if (!target) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, worksheet } = target;
|
||||
const render = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const skeleton = render?.with<SheetSkeletonManagerService>(SheetSkeletonManagerService).ensureSkeleton(subUnitId);
|
||||
if (!skeleton) {
|
||||
return {
|
||||
redos: [],
|
||||
undos: [],
|
||||
};
|
||||
}
|
||||
const rowsAutoHeightInfo = skeleton.calculateAutoHeightInRange(ranges, currentCellHeights);
|
||||
|
||||
const updatedRowsAutoHeightInfo = rowsAutoHeightInfo.filter((info) => {
|
||||
const { row, autoHeight } = info;
|
||||
if (!autoHeight) {
|
||||
return false;
|
||||
}
|
||||
const currentRowHeight = worksheet.getRowHeight(row);
|
||||
if (currentRowHeight === autoHeight) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (updatedRowsAutoHeightInfo.length === 0) {
|
||||
return {
|
||||
redos: [],
|
||||
undos: [],
|
||||
};
|
||||
}
|
||||
|
||||
const redoParams: ISetWorksheetRowAutoHeightMutationParams = {
|
||||
subUnitId,
|
||||
unitId,
|
||||
rowsAutoHeightInfo: updatedRowsAutoHeightInfo,
|
||||
};
|
||||
const undoParams: ISetWorksheetRowAutoHeightMutationParams = SetWorksheetRowAutoHeightMutationFactory(redoParams, worksheet);
|
||||
return {
|
||||
undos: [
|
||||
{
|
||||
id: SetWorksheetRowAutoHeightMutation.id,
|
||||
params: undoParams,
|
||||
},
|
||||
],
|
||||
redos: [
|
||||
{
|
||||
id: SetWorksheetRowAutoHeightMutation.id,
|
||||
params: redoParams,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this.disposeWithMe(this._sheetInterceptorService.interceptAutoHeight({
|
||||
getMutations: (info) => {
|
||||
const { unitId, subUnitId, ranges, autoHeightRanges, lazyAutoHeightRanges, cellHeights } = info;
|
||||
|
||||
const undoRedoItem = this.getUndoRedoParamsOfAutoHeight(autoHeightRanges ?? ranges, subUnitId, cellHeights, unitId);
|
||||
return this._processLazyAutoHeight(undoRedoItem, unitId, subUnitId, lazyAutoHeightRanges);
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
103
packages/sheets-ui/src/controllers/auto-width.controller.ts
Normal file
103
packages/sheets-ui/src/controllers/auto-width.controller.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { IColAutoWidthInfo, IObjectArrayPrimitiveType, Nullable, Worksheet } from '@univerjs/core';
|
||||
import type { RenderManagerService } from '@univerjs/engine-render';
|
||||
import type {
|
||||
ISetWorksheetColWidthMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import type { ISetWorksheetColIsAutoWidthCommandParams } from '../commands/commands/set-worksheet-auto-col-width.command';
|
||||
import { Disposable, Inject, IUniverInstanceService } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
getSheetCommandTarget,
|
||||
SetWorksheetColWidthMutation,
|
||||
} from '@univerjs/sheets';
|
||||
import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service';
|
||||
|
||||
export const createAutoColWidthUndoMutationsByRedos = (
|
||||
params: ISetWorksheetColWidthMutationParams,
|
||||
worksheet: Worksheet
|
||||
): ISetWorksheetColWidthMutationParams => {
|
||||
const { unitId, subUnitId, ranges } = params;
|
||||
const colWidthObj: IObjectArrayPrimitiveType<Nullable<number>> = {};
|
||||
const manager = worksheet.getColumnManager();
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
for (let j = range.startColumn; j < range.endColumn + 1; j++) {
|
||||
colWidthObj[j] = manager.getColumnWidth(j);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges,
|
||||
colWidth: colWidthObj,
|
||||
};
|
||||
};
|
||||
|
||||
export class AutoWidthController extends Disposable {
|
||||
constructor(
|
||||
@IRenderManagerService private readonly _renderManagerService: RenderManagerService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getUndoRedoParamsOfColWidth(params: Required<ISetWorksheetColIsAutoWidthCommandParams>) {
|
||||
const defaultValue = { redos: [], undos: [] };
|
||||
const { _univerInstanceService: univerInstanceService } = this;
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return defaultValue;
|
||||
|
||||
const { unitId, subUnitId, worksheet } = target;
|
||||
const sheetSkeletonService = this._renderManagerService.getRenderUnitById(unitId)!.with<SheetSkeletonManagerService>(SheetSkeletonManagerService);
|
||||
|
||||
if (!sheetSkeletonService.getCurrentParam()) return defaultValue;
|
||||
|
||||
const { skeleton } = sheetSkeletonService.getCurrentParam()!;
|
||||
const colsAutoWidthInfo: IColAutoWidthInfo[] = skeleton.calculateAutoWidthInRange(params.ranges);
|
||||
|
||||
const colWidthObject: IObjectArrayPrimitiveType<Nullable<number>> = {};
|
||||
for (const { col, width } of colsAutoWidthInfo) {
|
||||
colWidthObject[col] = width;
|
||||
}
|
||||
const redoParams: ISetWorksheetColWidthMutationParams = {
|
||||
subUnitId,
|
||||
unitId,
|
||||
ranges: params.ranges,
|
||||
colWidth: colWidthObject,
|
||||
};
|
||||
const undoParams: ISetWorksheetColWidthMutationParams = createAutoColWidthUndoMutationsByRedos(redoParams, worksheet);
|
||||
return {
|
||||
undos: [
|
||||
{
|
||||
id: SetWorksheetColWidthMutation.id,
|
||||
params: undoParams,
|
||||
},
|
||||
],
|
||||
redos: [
|
||||
{
|
||||
id: SetWorksheetColWidthMutation.id,
|
||||
params: redoParams,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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 {
|
||||
ICellCustomRender,
|
||||
ICellDataForSheetInterceptor,
|
||||
ICellRenderContext,
|
||||
Nullable,
|
||||
Workbook,
|
||||
} from '@univerjs/core';
|
||||
import type {
|
||||
IMouseEvent,
|
||||
IPointerEvent,
|
||||
IRenderContext,
|
||||
IRenderModule,
|
||||
RenderManagerService,
|
||||
Spreadsheet,
|
||||
} from '@univerjs/engine-render';
|
||||
import type { ICellPermission, ISheetSkeletonManagerParam } from '@univerjs/sheets';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
fromEventSubject,
|
||||
Inject,
|
||||
IPermissionService,
|
||||
sortRules,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService, Vector2 } from '@univerjs/engine-render';
|
||||
import { UnitAction } from '@univerjs/protocol';
|
||||
import { WorkbookEditablePermission, WorksheetEditPermission } from '@univerjs/sheets';
|
||||
import { throttleTime } from 'rxjs';
|
||||
import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service';
|
||||
|
||||
export class CellCustomRenderController extends Disposable implements IRenderModule {
|
||||
private _enterActiveRender: Nullable<{
|
||||
render: ICellCustomRender;
|
||||
cellContext: ICellRenderContext;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@IRenderManagerService private readonly _renderManagerService: RenderManagerService,
|
||||
@IPermissionService private readonly _permissionService: IPermissionService
|
||||
) {
|
||||
super();
|
||||
this._initEventBinding();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initEventBinding() {
|
||||
const disposableCollection = new DisposableCollection();
|
||||
|
||||
const workbook = this._context.unit;
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
const handleSkeletonChange = (skeletonParam: Nullable<ISheetSkeletonManagerParam>) => {
|
||||
disposableCollection.dispose();
|
||||
if (!skeletonParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = this._context.unitId;
|
||||
const { skeleton } = skeletonParam;
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (currentRender && currentRender.mainComponent) {
|
||||
const spreadsheet = currentRender.mainComponent as Spreadsheet;
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
const getActiveRender = (evt: IPointerEvent | IMouseEvent) => {
|
||||
const { offsetX, offsetY } = evt;
|
||||
const scene = currentRender.scene;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
|
||||
if (!worksheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeViewport = scene.getActiveViewportByCoord(
|
||||
Vector2.FromArray([offsetX, offsetY])
|
||||
);
|
||||
|
||||
if (!activeViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
const scrollXY = {
|
||||
x: activeViewport.viewportScrollX,
|
||||
y: activeViewport.viewportScrollY,
|
||||
};
|
||||
|
||||
const cellPos = skeleton.getCellIndexByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY);
|
||||
|
||||
// const mergeCell = skeleton.mergeData.find((range) => {
|
||||
// const { startColumn, startRow, endColumn, endRow } = range;
|
||||
// return cellPos.row >= startRow && cellPos.column >= startColumn && cellPos.row <= endRow && cellPos.column <= endColumn;
|
||||
// });
|
||||
const mergeCell = skeleton.worksheet.getMergedCell(cellPos.row, cellPos.column);
|
||||
|
||||
const cellIndex = {
|
||||
actualRow: mergeCell ? mergeCell.startRow : cellPos.row,
|
||||
actualCol: mergeCell ? mergeCell.startColumn : cellPos.column,
|
||||
mergeCell,
|
||||
row: cellPos.row,
|
||||
col: cellPos.column,
|
||||
};
|
||||
|
||||
if (!cellIndex || !skeleton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cellData = worksheet.getCell(cellIndex.actualRow, cellIndex.actualCol);
|
||||
if (!cellData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renders = cellData.customRender;
|
||||
|
||||
if (!renders?.length) {
|
||||
return;
|
||||
}
|
||||
const row = cellIndex.actualRow;
|
||||
const col = cellIndex.actualCol;
|
||||
const sortedRenders = renders.sort(sortRules);
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
|
||||
const info: ICellRenderContext = {
|
||||
data: cellData,
|
||||
style: worksheet.getComposedCellStyleByCellData(row, col, cellData),
|
||||
primaryWithCoord: skeleton.getCellWithCoordByIndex(cellIndex.actualRow, cellIndex.actualCol),
|
||||
unitId,
|
||||
subUnitId,
|
||||
row,
|
||||
col,
|
||||
workbook,
|
||||
worksheet,
|
||||
};
|
||||
|
||||
const position = {
|
||||
x: scrollXY.x + (offsetX / scaleX),
|
||||
y: scrollXY.y + (offsetY / scaleY),
|
||||
};
|
||||
|
||||
const activeRender = sortedRenders.find((render) => render.isHit?.(position, info));
|
||||
if (!activeRender) {
|
||||
return;
|
||||
}
|
||||
return [activeRender, info] as const;
|
||||
};
|
||||
|
||||
const disposable = spreadsheet.onPointerDown$.subscribeEvent((evt) => {
|
||||
const activeRenderInfo = getActiveRender(evt);
|
||||
if (activeRenderInfo) {
|
||||
const [activeRender, cellContext] = activeRenderInfo;
|
||||
const { row, col, worksheet, unitId, subUnitId } = cellContext;
|
||||
const sheetEditable = this._permissionService.composePermission(
|
||||
[new WorkbookEditablePermission(unitId).id, new WorksheetEditPermission(unitId, subUnitId).id]
|
||||
)?.every((permission) => permission.value);
|
||||
|
||||
if (!sheetEditable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const permission = (worksheet.getCell(row, col) as (ICellDataForSheetInterceptor & { selectionProtection: ICellPermission[] }))?.selectionProtection?.[0];
|
||||
if (permission?.[UnitAction.Edit] === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
activeRender.onPointerDown?.(cellContext, evt);
|
||||
}
|
||||
});
|
||||
|
||||
const moveDisposable = fromEventSubject(spreadsheet.onPointerMove$).pipe(throttleTime(30)).subscribe((evt) => {
|
||||
const activeRenderInfo = getActiveRender(evt);
|
||||
if (activeRenderInfo) {
|
||||
const [activeRender, cellContext] = activeRenderInfo;
|
||||
if (this._enterActiveRender) {
|
||||
if (this._enterActiveRender.render !== activeRender) {
|
||||
this._enterActiveRender.render.onPointerLeave?.(this._enterActiveRender.cellContext, evt);
|
||||
this._enterActiveRender = {
|
||||
render: activeRender,
|
||||
cellContext,
|
||||
};
|
||||
activeRender.onPointerEnter?.(cellContext, evt);
|
||||
}
|
||||
} else {
|
||||
this._enterActiveRender = {
|
||||
render: activeRender,
|
||||
cellContext,
|
||||
};
|
||||
activeRender.onPointerEnter?.(cellContext, evt);
|
||||
}
|
||||
} else {
|
||||
if (this._enterActiveRender) {
|
||||
this._enterActiveRender.render.onPointerLeave?.(this._enterActiveRender.cellContext, evt);
|
||||
this._enterActiveRender = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
disposable && disposableCollection.add(disposable);
|
||||
moveDisposable && disposableCollection.add(moveDisposable);
|
||||
}
|
||||
};
|
||||
|
||||
this.disposeWithMe(this._sheetSkeletonManagerService.currentSkeleton$.subscribe(handleSkeletonChange));
|
||||
handleSkeletonChange(this._sheetSkeletonManagerService.getCurrentParam());
|
||||
this.disposeWithMe(disposableCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 { Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { filter } from 'rxjs';
|
||||
import { CellPopupManagerService } from '../services/cell-popup-manager.service';
|
||||
import { IEditorBridgeService } from '../services/editor-bridge.service';
|
||||
|
||||
/**
|
||||
* Controller to hide cell popups when entering edit mode.
|
||||
* This ensures that popups don't overlap with the cell editor.
|
||||
*/
|
||||
export class CellPopupEditorController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
|
||||
@Inject(CellPopupManagerService) private readonly _cellPopupManagerService: CellPopupManagerService
|
||||
) {
|
||||
super();
|
||||
this._initEditorVisibleListener();
|
||||
}
|
||||
|
||||
private _initEditorVisibleListener(): void {
|
||||
this.disposeWithMe(
|
||||
this._editorBridgeService.visible$.pipe(
|
||||
filter((params) => params.visible)
|
||||
).subscribe(() => {
|
||||
const editCellState = this._editorBridgeService.getEditLocation();
|
||||
if (editCellState) {
|
||||
this._cellPopupManagerService.hidePopup(
|
||||
editCellState.unitId,
|
||||
editCellState.sheetId,
|
||||
editCellState.row,
|
||||
editCellState.column
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
70
packages/sheets-ui/src/controllers/checkbox.controller.ts
Normal file
70
packages/sheets-ui/src/controllers/checkbox.controller.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { CURSOR_TYPE, getCurrentTypeOfRenderer, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ToggleCellCheckboxCommand } from '@univerjs/sheets';
|
||||
import { HoverManagerService } from '../services/hover-manager.service';
|
||||
|
||||
export class SheetCheckboxController extends Disposable {
|
||||
private _isPointer = false;
|
||||
constructor(
|
||||
@Inject(HoverManagerService) private _hoverManagerService: HoverManagerService,
|
||||
@IUniverInstanceService private _instanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
this._initPointerEvent();
|
||||
this._initHover();
|
||||
}
|
||||
|
||||
private get _mainComponent() {
|
||||
return getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_SHEET, this._instanceService, this._renderManagerService)?.mainComponent;
|
||||
}
|
||||
|
||||
private _initHover() {
|
||||
this.disposeWithMe(this._hoverManagerService.currentRichText$.subscribe((richText) => {
|
||||
if (richText?.bullet) {
|
||||
if (!this._isPointer) {
|
||||
this._mainComponent?.setCursor(CURSOR_TYPE.POINTER);
|
||||
}
|
||||
this._isPointer = true;
|
||||
} else {
|
||||
if (this._isPointer) {
|
||||
this._mainComponent?.setCursor(CURSOR_TYPE.AUTO);
|
||||
}
|
||||
this._isPointer = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _initPointerEvent() {
|
||||
this.disposeWithMe(this._hoverManagerService.currentClickedCell$.subscribe((cell) => {
|
||||
const { location, bullet } = cell;
|
||||
if (!bullet) {
|
||||
return;
|
||||
}
|
||||
this._commandService.executeCommand(ToggleCellCheckboxCommand.id, {
|
||||
unitId: location.unitId,
|
||||
subUnitId: location.subUnitId,
|
||||
row: location.row,
|
||||
col: location.col,
|
||||
paragraphIndex: bullet.startIndex,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsUIConfig } from '../../config/config';
|
||||
import { Disposable, IConfigService, Inject, Injector } from '@univerjs/core';
|
||||
import { BuiltInUIPart, connectInjector, IUIPartsService } from '@univerjs/ui';
|
||||
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../../config/config';
|
||||
import { ClipboardPopupMenu } from '../../views/clipboard/ClipboardPopupMenu';
|
||||
|
||||
export class SheetClipboardUIController extends Disposable {
|
||||
constructor(
|
||||
@Inject(Injector) injector: Injector,
|
||||
@IConfigService configService: IConfigService,
|
||||
@IUIPartsService uiPartsService: IUIPartsService
|
||||
) {
|
||||
super();
|
||||
|
||||
const config = configService.getConfig<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY);
|
||||
if (!config?.clipboardConfig?.hidePasteOptions) {
|
||||
this.disposeWithMe(uiPartsService.registerComponent(
|
||||
BuiltInUIPart.CONTENT,
|
||||
() => connectInjector(ClipboardPopupMenu, injector)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,916 @@
|
||||
/**
|
||||
* 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 {
|
||||
ICellData,
|
||||
ICommandInfo,
|
||||
IDocumentData,
|
||||
IMutationInfo,
|
||||
IObjectArrayPrimitiveType,
|
||||
IObjectMatrixPrimitiveType,
|
||||
IRange,
|
||||
Workbook,
|
||||
Worksheet,
|
||||
} from '@univerjs/core';
|
||||
import type {
|
||||
IInsertColMutationParams,
|
||||
IInsertRowMutationParams,
|
||||
ISetRangeValuesMutationParams,
|
||||
ISetWorksheetColWidthMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import type { IUniverSheetsUIConfig } from '../../config/config';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type {
|
||||
ICellDataWithSpanInfo,
|
||||
IClipboardPropertyItem,
|
||||
ICopyPastePayload,
|
||||
ISheetClipboardHook,
|
||||
ISheetDiscreteRangeLocation,
|
||||
} from '../../services/clipboard/type';
|
||||
import type { IScrollStateWithSearchParam } from '../../services/scroll-manager.service';
|
||||
import {
|
||||
createDocumentModelWithStyle,
|
||||
DEFAULT_WORKSHEET_COLUMN_WIDTH,
|
||||
DEFAULT_WORKSHEET_COLUMN_WIDTH_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
EDITOR_ACTIVATED,
|
||||
extractPureTextFromCell,
|
||||
getNumfmtLocaleTag,
|
||||
getNumfmtParseValueFilter,
|
||||
handleStyleToString,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
Inject,
|
||||
Injector,
|
||||
isFormulaString,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
ObjectMatrix,
|
||||
Optional,
|
||||
RxDisposable,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { convertBodyToHtml, DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService, withCurrentTypeOfRenderer } from '@univerjs/engine-render';
|
||||
import {
|
||||
AddWorksheetMergeCommand,
|
||||
InsertColMutation,
|
||||
InsertRowMutation,
|
||||
MAX_CELL_PER_SHEET_KEY,
|
||||
MoveColsMutation,
|
||||
MoveRangeMutation,
|
||||
MoveRowsMutation,
|
||||
RemoveColMutation,
|
||||
RemoveRowMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetRangeValuesUndoMutationFactory,
|
||||
SetWorksheetColWidthMutation,
|
||||
SetWorksheetColWidthMutationFactory,
|
||||
} from '@univerjs/sheets';
|
||||
import { IClipboardInterfaceService, IMessageService } from '@univerjs/ui';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import {
|
||||
SheetCopyCommand,
|
||||
SheetCutCommand,
|
||||
SheetOptionalPasteCommand,
|
||||
SheetPasteBesidesBorderCommand,
|
||||
SheetPasteColWidthCommand,
|
||||
SheetPasteCommand,
|
||||
SheetPasteFormatCommand,
|
||||
SheetPasteShortKeyCommand,
|
||||
SheetPasteValueCommand,
|
||||
} from '../../commands/commands/clipboard.command';
|
||||
import { SetScrollOperation } from '../../commands/operations/scroll.operation';
|
||||
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../../config/config';
|
||||
import {
|
||||
escapeSpecialCode,
|
||||
FORMULA_CLIPBOARD_MIME_TYPE,
|
||||
ISheetClipboardService,
|
||||
PREDEFINED_HOOK_NAME_COPY,
|
||||
PREDEFINED_HOOK_NAME_PASTE,
|
||||
} from '../../services/clipboard/clipboard.service';
|
||||
import { ISheetEmbedRuntimeFocusCoordinator } from '../../services/sheet-embed-integration.service';
|
||||
import { SheetSkeletonManagerService } from '../../services/sheet-skeleton-manager.service';
|
||||
import { whenSheetEditorFocused } from '../shortcuts/utils';
|
||||
import { RemovePasteMenuCommands } from './const';
|
||||
import {
|
||||
generateBody,
|
||||
getClearAndSetMergeMutations,
|
||||
getDefaultOnPasteCellMutations,
|
||||
getSetCellStyleMutations,
|
||||
getSetCellValueMutations,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* This controller add basic clipboard logic for basic features such as text color / BISU / row widths to the clipboard
|
||||
* service. You can create a similar clipboard controller to add logic for your own features.
|
||||
*/
|
||||
|
||||
const shouldRemoveShapeIds = [
|
||||
InsertColMutation.id,
|
||||
InsertRowMutation.id,
|
||||
RemoveColMutation.id,
|
||||
RemoveRowMutation.id,
|
||||
MoveRangeMutation.id,
|
||||
MoveRowsMutation.id,
|
||||
MoveColsMutation.id,
|
||||
];
|
||||
|
||||
export class SheetClipboardController extends RxDisposable {
|
||||
private _refreshOptionalPaste$ = new Subject();
|
||||
refreshOptionalPaste$ = this._refreshOptionalPaste$.asObservable();
|
||||
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService,
|
||||
@IClipboardInterfaceService private readonly _clipboardInterfaceService: IClipboardInterfaceService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@Inject(LocaleService) private readonly _localService: LocaleService,
|
||||
@Optional(ISheetEmbedRuntimeFocusCoordinator)
|
||||
private readonly _embedRuntimeFocusCoordinator?: ISheetEmbedRuntimeFocusCoordinator
|
||||
) {
|
||||
super();
|
||||
this._init();
|
||||
this._initCommandListener();
|
||||
this._pasteWithDoc();
|
||||
}
|
||||
|
||||
refreshOptionalPaste() {
|
||||
this._refreshOptionalPaste$.next(Math.random());
|
||||
}
|
||||
|
||||
private _pasteWithDoc() {
|
||||
const sheetPasteShortKeyFn = (docSelectionRenderService: DocSelectionRenderService) => {
|
||||
docSelectionRenderService.onPaste$.pipe(takeUntil(this.dispose$)).subscribe(async (config) => {
|
||||
if (!this._isSheetGridPasteTarget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// editor's value should not change and avoid triggering input event
|
||||
config!.event.preventDefault();
|
||||
|
||||
const clipboardEvent = config!.event as ClipboardEvent;
|
||||
const htmlContent = clipboardEvent.clipboardData?.getData('text/html');
|
||||
const textContent = clipboardEvent.clipboardData?.getData('text/plain');
|
||||
const files = this._resolveClipboardFiles(clipboardEvent.clipboardData);
|
||||
const target = this._sheetClipboardService.capturePasteTarget();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const formulaClipboardPayload = await this._readFormulaClipboardPayload();
|
||||
|
||||
await this._commandService.executeCommand(SheetPasteShortKeyCommand.id, {
|
||||
htmlContent,
|
||||
textContent,
|
||||
files,
|
||||
formulaClipboardPayload,
|
||||
target,
|
||||
});
|
||||
});
|
||||
|
||||
docSelectionRenderService?.onKeydown$.subscribe((event) => {
|
||||
if ((event.event as KeyboardEvent).key === 'Escape') {
|
||||
this._sheetClipboardService.removeMarkSelection();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// docSelectionRenderService would init before clipboardService controller when creating a univer.
|
||||
// But when creating a sheet unit again after the previous sheet unit has been disposed, clipboard controller would init before docSelectionRenderService.
|
||||
// In this case, DocSelectionRenderService isn't ready when clipboardService controller init.
|
||||
// So better listening to the created$ event of the renderManagerService to get the DocSelectionRenderService instance.
|
||||
let docSelectionRenderService = this._renderManagerService.getRenderUnitById(DOCS_NORMAL_EDITOR_UNIT_ID_KEY)?.with(DocSelectionRenderService);
|
||||
|
||||
if (docSelectionRenderService) {
|
||||
sheetPasteShortKeyFn(docSelectionRenderService);
|
||||
}
|
||||
this._renderManagerService.created$.subscribe((renderer) => {
|
||||
if (renderer.unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
docSelectionRenderService = this._renderManagerService.getRenderUnitById(DOCS_NORMAL_EDITOR_UNIT_ID_KEY)?.with(DocSelectionRenderService);
|
||||
if (docSelectionRenderService) {
|
||||
sheetPasteShortKeyFn(docSelectionRenderService);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _isSheetGridPasteTarget(): boolean {
|
||||
if (whenSheetEditorFocused(this._contextService)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activeRuntime = this._embedRuntimeFocusCoordinator?.resolveActiveChildSessionRuntimeScope();
|
||||
if (
|
||||
activeRuntime?.childType !== UniverInstanceType.UNIVER_SHEET ||
|
||||
!activeRuntime.childUnitId ||
|
||||
this._contextService.getContextValue(EDITOR_ACTIVATED)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._instanceService
|
||||
.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)
|
||||
?.getUnitId() === activeRuntime.childUnitId;
|
||||
}
|
||||
|
||||
private _resolveClipboardFiles(clipboardData: DataTransfer | null) {
|
||||
if (!clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(clipboardData.items)
|
||||
.map((item) => item.kind === 'file' ? item.getAsFile() : undefined)
|
||||
.filter(Boolean) as File[];
|
||||
|
||||
return files.length > 0 ? files : undefined;
|
||||
}
|
||||
|
||||
private async _readFormulaClipboardPayload(): Promise<string | undefined> {
|
||||
if (!this._clipboardInterfaceService.supportClipboard) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const items = await this._clipboardInterfaceService.read();
|
||||
const item = items.find((clipboardItem) => clipboardItem.types.includes(FORMULA_CLIPBOARD_MIME_TYPE));
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const blob = await item.getType(FORMULA_CLIPBOARD_MIME_TYPE);
|
||||
return blob.text();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
// register sheet clipboard commands
|
||||
[SheetCopyCommand, SheetCutCommand, SheetPasteCommand].forEach((command) =>
|
||||
this.disposeWithMe(this._commandService.registerMultipleCommand(command))
|
||||
);
|
||||
|
||||
[
|
||||
SheetPasteValueCommand,
|
||||
SheetPasteFormatCommand,
|
||||
SheetPasteColWidthCommand,
|
||||
SheetPasteBesidesBorderCommand,
|
||||
SheetPasteShortKeyCommand,
|
||||
SheetOptionalPasteCommand,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
|
||||
// register basic sheet clipboard hooks
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._initCopyingHooks()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._initPastingRowsHook()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._initPastingColumnsHook()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._initPastingHook()));
|
||||
const disposables = this._initSpecialPasteHooks().map((hook) =>
|
||||
this._sheetClipboardService.addClipboardHook(hook)
|
||||
);
|
||||
this.disposeWithMe({ dispose: () => disposables.forEach((d) => d.dispose()) });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initCopyingHooks(): ISheetClipboardHook {
|
||||
const self = this;
|
||||
let currentSheet: Worksheet | null = null;
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_COPY.DEFAULT_COPY,
|
||||
isDefaultHook: true,
|
||||
onBeforeCopy(unitId, subUnitId) {
|
||||
currentSheet = self._getWorksheet(unitId, subUnitId);
|
||||
},
|
||||
onCopyCellContent(row: number, col: number): string {
|
||||
const cell = currentSheet!.getCell(row, col);
|
||||
|
||||
if (cell?.p?.body?.paragraphs || cell?.p?.body?.textRuns) {
|
||||
return convertBodyToHtml(cell.p);
|
||||
}
|
||||
|
||||
const content = extractPureTextFromCell(cell);
|
||||
|
||||
if (content.trim() === '') {
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for generating the copied HTML, so we need to escape special code to avoid breaking the HTML structure.
|
||||
* For example, if the cell value contains <, > or &, it would break the HTML structure and cause the copied content to be incorrect.
|
||||
*/
|
||||
return escapeSpecialCode(content);
|
||||
},
|
||||
onCopyCellStyle: (row: number, col: number, rowSpan?: number, colSpan?: number) => {
|
||||
const properties: IClipboardPropertyItem = {};
|
||||
|
||||
if (rowSpan || colSpan) {
|
||||
properties.rowspan = `${rowSpan || 1}`;
|
||||
properties.colspan = `${colSpan || 1}`;
|
||||
}
|
||||
|
||||
const mergedCellByRowCol = currentSheet!.getMergedCell(row, col);
|
||||
/**
|
||||
* This is for generating the copied HTML, which requires all the styles applied to the cell, including conditional formatting, data validation, number format, etc.
|
||||
* So here we use `getComposedCellStyle` to get the final style of the cell.
|
||||
*/
|
||||
const textStyle = currentSheet!.getComposedCellStyle(row, col);
|
||||
|
||||
let style = '';
|
||||
|
||||
if (textStyle) {
|
||||
style = handleStyleToString(textStyle);
|
||||
}
|
||||
|
||||
if (mergedCellByRowCol) {
|
||||
const endRow = mergedCellByRowCol.endRow;
|
||||
const endColumn = mergedCellByRowCol.endColumn;
|
||||
const lastRange = currentSheet!.getRange(endRow, endColumn);
|
||||
const lastTextStyle = lastRange.getTextStyle();
|
||||
if (lastTextStyle) {
|
||||
const lastStyle = handleStyleToString(lastTextStyle);
|
||||
if (style) {
|
||||
style += lastStyle ? `;${lastStyle}` : '';
|
||||
} else {
|
||||
style = lastStyle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (style) {
|
||||
properties.style = style;
|
||||
}
|
||||
|
||||
return Object.keys(properties).length ? properties : null;
|
||||
},
|
||||
onCopyColumn(col: number) {
|
||||
const sheet = currentSheet!;
|
||||
const width = sheet.getColumnWidth(col);
|
||||
return {
|
||||
width: `${width}`,
|
||||
};
|
||||
},
|
||||
onCopyRow(row: number) {
|
||||
const sheet = currentSheet!;
|
||||
const height = sheet.getRowHeight(row);
|
||||
return {
|
||||
style: `height: ${height}px;`,
|
||||
};
|
||||
},
|
||||
onAfterCopy() {
|
||||
currentSheet = null;
|
||||
},
|
||||
getFilteredOutRows(unitId: string, subUnitId: string, range: IRange) {
|
||||
const worksheet = self._getWorksheet(unitId, subUnitId);
|
||||
if (!worksheet) return [];
|
||||
|
||||
const { startRow, endRow } = range;
|
||||
const res: number[] = [];
|
||||
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
if (worksheet.getRowFiltered(r)) {
|
||||
res.push(r);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _initPastingHook(): ISheetClipboardHook {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE,
|
||||
isDefaultHook: true,
|
||||
onBeforePaste({ range }) {
|
||||
// examine if pasting would cause number of cells to exceed the upper limit
|
||||
// this is not implemented yet
|
||||
const maxConfig = self._configService.getConfig<number>(MAX_CELL_PER_SHEET_KEY);
|
||||
const endRow = range.rows[range.rows.length - 1];
|
||||
const endColumn = range.cols[range.cols.length - 1];
|
||||
if (maxConfig && endRow * endColumn > maxConfig) {
|
||||
self._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: self._localService.t<LocaleKey>('sheets-ui.clipboard.paste.exceedMaxCells'),
|
||||
}); // TODO: show error info
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
onPastePlainText(pasteTo: ISheetDiscreteRangeLocation, text: string, payload: ICopyPastePayload) {
|
||||
return self._onPastePlainText(pasteTo, text, payload);
|
||||
},
|
||||
|
||||
onPasteCells(
|
||||
pasteFrom: ISheetDiscreteRangeLocation,
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
data: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
payload: ICopyPastePayload
|
||||
) {
|
||||
return self._onPasteCells(pasteFrom, pasteTo, data, payload);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _initPastingRowsHook(): ISheetClipboardHook {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE_ROWS,
|
||||
isDefaultHook: true,
|
||||
|
||||
onPasteRows(pasteTo) {
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const { range, unitId, subUnitId } = pasteTo;
|
||||
const { rows, cols } = range;
|
||||
|
||||
if (!rows || !cols || rows.length === 0 || cols.length === 0) {
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
const worksheet = self._getWorksheet(unitId, subUnitId);
|
||||
const worksheetLastRow = worksheet.getMaxRows() - 1;
|
||||
|
||||
const pasteRangeEndRow = rows[rows.length - 1];
|
||||
const pasteRangeStartColumn = cols[0];
|
||||
const pasteRangeEndColumn = cols[cols.length - 1];
|
||||
|
||||
// If the range is outside ot the worksheet's boundary, we should add rows
|
||||
const addingRowsCount = pasteRangeEndRow - worksheetLastRow;
|
||||
if (addingRowsCount > 0) {
|
||||
const insertRowMutationParams: IInsertRowMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: worksheetLastRow + 1,
|
||||
endRow: pasteRangeEndRow,
|
||||
startColumn: pasteRangeStartColumn,
|
||||
endColumn: pasteRangeEndColumn,
|
||||
},
|
||||
};
|
||||
|
||||
redos.push({
|
||||
id: InsertRowMutation.id,
|
||||
params: insertRowMutationParams,
|
||||
});
|
||||
undos.push({
|
||||
id: RemoveRowMutation.id,
|
||||
params: insertRowMutationParams,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _initPastingColumnsHook(): ISheetClipboardHook {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE_COLUMNS,
|
||||
isDefaultHook: true,
|
||||
|
||||
onPasteColumns(pasteTo) {
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const { range, unitId, subUnitId } = pasteTo;
|
||||
const { rows, cols } = range;
|
||||
|
||||
if (!rows || !cols || rows.length === 0 || cols.length === 0) {
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
const worksheet = self._getWorksheet(unitId, subUnitId);
|
||||
const worksheetLastColumn = worksheet.getMaxColumns() - 1;
|
||||
|
||||
const pasteRangeStartRow = rows[0];
|
||||
const pasteRangeEndRow = rows[rows.length - 1];
|
||||
const pasteRangeEndColumn = cols[cols.length - 1];
|
||||
|
||||
// If the range is outside ot the worksheet's boundary, we should add columns
|
||||
const addingColsCount = pasteRangeEndColumn - worksheetLastColumn;
|
||||
if (addingColsCount > 0) {
|
||||
const InsertColMutationParams: IInsertColMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
range: {
|
||||
startRow: pasteRangeStartRow,
|
||||
endRow: pasteRangeEndRow,
|
||||
startColumn: worksheetLastColumn + 1,
|
||||
endColumn: pasteRangeEndColumn,
|
||||
},
|
||||
};
|
||||
|
||||
redos.push({
|
||||
id: InsertColMutation.id,
|
||||
params: InsertColMutationParams,
|
||||
});
|
||||
undos.push({
|
||||
id: RemoveColMutation.id,
|
||||
params: InsertColMutationParams,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _generateDocumentDataModelSnapshot(snapshot: Partial<IDocumentData>) {
|
||||
const currentSkeleton = withCurrentTypeOfRenderer(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
SheetSkeletonManagerService,
|
||||
this._instanceService,
|
||||
this._renderManagerService
|
||||
)?.getCurrentParam();
|
||||
|
||||
if (currentSkeleton == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const documentModel = createDocumentModelWithStyle('', {});
|
||||
const p = documentModel?.getSnapshot();
|
||||
const documentData = { ...p, ...snapshot };
|
||||
documentModel?.reset(documentData);
|
||||
return documentModel?.getSnapshot();
|
||||
}
|
||||
|
||||
private _onPastePlainText(pasteTo: ISheetDiscreteRangeLocation, text: string, payload: ICopyPastePayload) {
|
||||
const { range, unitId, subUnitId } = pasteTo;
|
||||
const workbook = this._instanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const parseOptions = workbook
|
||||
? { locale: getNumfmtLocaleTag(workbook.getSnapshot().locale), dateSystem: workbook.getDateSystem() }
|
||||
: undefined;
|
||||
let cellValue: IObjectMatrixPrimitiveType<ICellData>;
|
||||
if (/\r|\n/.test(text) || Tools.isLegalUrl(text)) {
|
||||
const body = generateBody(text);
|
||||
const p = this._generateDocumentDataModelSnapshot({ body });
|
||||
cellValue = {
|
||||
[range.rows[0]]: {
|
||||
[range.cols[0]]: {
|
||||
p,
|
||||
v: null,
|
||||
f: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (isFormulaString(text)) {
|
||||
cellValue = {
|
||||
[range.rows[0]]: {
|
||||
[range.cols[0]]: {
|
||||
f: text,
|
||||
v: null,
|
||||
p: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const pattern = getNumfmtParseValueFilter(text, parseOptions);
|
||||
if (pattern?.z) {
|
||||
cellValue = {
|
||||
[range.rows[0]]: {
|
||||
[range.cols[0]]: {
|
||||
v: pattern.v,
|
||||
s: {
|
||||
n: {
|
||||
pattern: pattern.z,
|
||||
},
|
||||
},
|
||||
f: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
cellValue = {
|
||||
[range.rows[0]]: {
|
||||
[range.cols[0]]: {
|
||||
v: text,
|
||||
f: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setRangeValuesParams: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue,
|
||||
};
|
||||
|
||||
return {
|
||||
redos: [
|
||||
{
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: setRangeValuesParams,
|
||||
},
|
||||
],
|
||||
undos: [
|
||||
{
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: SetRangeValuesUndoMutationFactory(this._injector, setRangeValuesParams),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private _onPasteCells(
|
||||
pasteFrom: ISheetDiscreteRangeLocation,
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
data: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
payload: ICopyPastePayload
|
||||
): {
|
||||
redos: IMutationInfo[];
|
||||
undos: IMutationInfo[];
|
||||
} {
|
||||
return this._injector.invoke((accessor) => {
|
||||
return getDefaultOnPasteCellMutations(pasteFrom, pasteTo, data, payload, accessor);
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initSpecialPasteHooks() {
|
||||
const self = this;
|
||||
|
||||
const specialPasteValueHook: ISheetClipboardHook = {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
|
||||
specialPasteInfo: {
|
||||
label: 'specialPaste.value',
|
||||
},
|
||||
onPasteCells: (pasteFrom, pasteTo, data) => {
|
||||
return this._injector.invoke((accessor) => {
|
||||
const { redo, undo } = getSetCellValueMutations(pasteTo, pasteFrom, data, accessor);
|
||||
return {
|
||||
redos: [redo],
|
||||
undos: [undo],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
const specialPasteFormatHook: ISheetClipboardHook = {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
specialPasteInfo: {
|
||||
label: 'specialPaste.format',
|
||||
},
|
||||
onPasteCells: (pasteFrom, pasteTo, matrix) => {
|
||||
const redoMutationsInfo: IMutationInfo[] = [];
|
||||
const undoMutationsInfo: IMutationInfo[] = [];
|
||||
|
||||
// Set cell style
|
||||
const { redo, undo } = this._injector.invoke((accessor) => {
|
||||
return getSetCellStyleMutations(
|
||||
pasteTo,
|
||||
pasteFrom,
|
||||
matrix,
|
||||
accessor
|
||||
);
|
||||
});
|
||||
redoMutationsInfo.push(redo);
|
||||
undoMutationsInfo.push(undo);
|
||||
|
||||
// clear and set merge
|
||||
const { undos: mergeUndos, redos: mergeRedos } = this._injector.invoke((accessor) => {
|
||||
return getClearAndSetMergeMutations(
|
||||
pasteTo,
|
||||
matrix,
|
||||
accessor
|
||||
);
|
||||
});
|
||||
redoMutationsInfo.push(...mergeRedos);
|
||||
undoMutationsInfo.push(...mergeUndos);
|
||||
|
||||
return {
|
||||
undos: undoMutationsInfo,
|
||||
redos: redoMutationsInfo,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const specialPasteColWidthHook: ISheetClipboardHook = {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
specialPasteInfo: {
|
||||
label: 'specialPaste.colWidth',
|
||||
},
|
||||
onPasteCells() {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
},
|
||||
onPasteColumns(pasteTo, colProperties, payload) {
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
if (payload.pasteType !== PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH) {
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
const { range, unitId, subUnitId } = pasteTo;
|
||||
const { rows, cols } = range;
|
||||
|
||||
if (!rows || !cols || rows.length === 0 || cols.length === 0) {
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
const worksheet = self._getWorksheet(unitId, subUnitId);
|
||||
const defaultColumnWidth = self._configService.getConfig<number>(DEFAULT_WORKSHEET_COLUMN_WIDTH_KEY) ?? DEFAULT_WORKSHEET_COLUMN_WIDTH;
|
||||
|
||||
const pasteRangeStartRow = rows[0];
|
||||
const pasteRangeEndRow = rows[rows.length - 1];
|
||||
const pasteRangeStartColumn = cols[0];
|
||||
const pasteRangeEndColumn = cols[cols.length - 1];
|
||||
|
||||
const colPropertiesLength = colProperties.length;
|
||||
if (colPropertiesLength > 0) {
|
||||
const colWidth: IObjectArrayPrimitiveType<number> = {};
|
||||
|
||||
for (let c = pasteRangeStartColumn; c <= pasteRangeEndColumn; c++) {
|
||||
const index = (c - pasteRangeStartColumn) % colPropertiesLength;
|
||||
const property = colProperties[index];
|
||||
if (property.width && Number(property.width) !== defaultColumnWidth) {
|
||||
colWidth[c] = Number(property.width);
|
||||
}
|
||||
}
|
||||
|
||||
const setWorksheetColWidthMutationParams: ISetWorksheetColWidthMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: [
|
||||
{
|
||||
startRow: pasteRangeStartRow,
|
||||
endRow: pasteRangeEndRow,
|
||||
startColumn: pasteRangeStartColumn,
|
||||
endColumn: pasteRangeEndColumn,
|
||||
},
|
||||
],
|
||||
colWidth,
|
||||
};
|
||||
|
||||
redos.push({
|
||||
id: SetWorksheetColWidthMutation.id,
|
||||
params: setWorksheetColWidthMutationParams,
|
||||
});
|
||||
undos.unshift({
|
||||
id: SetWorksheetColWidthMutation.id,
|
||||
params: SetWorksheetColWidthMutationFactory(setWorksheetColWidthMutationParams, worksheet),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const specialPasteBesidesBorder: ISheetClipboardHook = {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_BESIDES_BORDER,
|
||||
specialPasteInfo: {
|
||||
label: 'specialPaste.besidesBorder',
|
||||
},
|
||||
onPasteCells: (pasteFrom, pasteTo, matrix, payload) => {
|
||||
const workbook = self._instanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const redoMutationsInfo: IMutationInfo[] = [];
|
||||
const undoMutationsInfo: IMutationInfo[] = [];
|
||||
const { range, unitId, subUnitId } = pasteTo;
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
|
||||
// TODO@Dushusir: undo selection
|
||||
matrix.forValue((row, col, value) => {
|
||||
const style = value.s;
|
||||
if (typeof style === 'object') {
|
||||
const newValue = Tools.deepClone(value);
|
||||
if (newValue.s) {
|
||||
newValue.s = {
|
||||
...style,
|
||||
bd: null,
|
||||
};
|
||||
}
|
||||
valueMatrix.setValue(range.rows[row], range.cols[col], newValue);
|
||||
}
|
||||
});
|
||||
|
||||
// set cell value and style
|
||||
const setValuesMutation: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: valueMatrix.clone(),
|
||||
};
|
||||
|
||||
redoMutationsInfo.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: setValuesMutation,
|
||||
});
|
||||
|
||||
// undo
|
||||
const undoSetValuesMutation: ISetRangeValuesMutationParams = this._injector.invoke(
|
||||
SetRangeValuesUndoMutationFactory,
|
||||
setValuesMutation
|
||||
);
|
||||
|
||||
undoMutationsInfo.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: undoSetValuesMutation,
|
||||
});
|
||||
|
||||
const { undos, redos } = this._injector.invoke((accessor) => {
|
||||
return getClearAndSetMergeMutations(pasteTo, matrix, accessor);
|
||||
});
|
||||
undoMutationsInfo.push(...undos);
|
||||
redoMutationsInfo.push(...redos);
|
||||
|
||||
return {
|
||||
redos: redoMutationsInfo,
|
||||
undos: undoMutationsInfo,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return [specialPasteValueHook, specialPasteFormatHook, specialPasteColWidthHook, specialPasteBesidesBorder];
|
||||
}
|
||||
|
||||
private _getWorksheet(unitId: string, subUnitId: string): Worksheet {
|
||||
const worksheet = this._instanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetId(subUnitId);
|
||||
|
||||
if (!worksheet) {
|
||||
throw new Error(
|
||||
`[SheetClipboardController]: cannot find a worksheet with unitId ${unitId} and subUnitId ${subUnitId}.`
|
||||
);
|
||||
}
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
private _initCommandListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === AddWorksheetMergeCommand.id) {
|
||||
this._sheetClipboardService.removeMarkSelection();
|
||||
} else if (shouldRemoveShapeIds.includes(command.id)) {
|
||||
this._sheetClipboardService.removeMarkSelection();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const sheetsUIConfig = this._configService.getConfig<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY);
|
||||
if (sheetsUIConfig?.clipboardConfig?.hidePasteOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (RemovePasteMenuCommands.includes(command.id)) {
|
||||
this._sheetClipboardService.disposePasteOptionsCache();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetScrollOperation.id) {
|
||||
if (!this._sheetClipboardService.getPasteMenuVisible()) {
|
||||
return;
|
||||
}
|
||||
const params = command.params as IScrollStateWithSearchParam;
|
||||
const scrollUnitId = params.unitId;
|
||||
const pasteOptionsCache = this._sheetClipboardService.getPasteOptionsCache();
|
||||
const menuUnitId = pasteOptionsCache?.target.unitId;
|
||||
if (scrollUnitId === menuUnitId) {
|
||||
this._refreshOptionalPaste$.next(Math.random());
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
37
packages/sheets-ui/src/controllers/clipboard/const.ts
Normal file
37
packages/sheets-ui/src/controllers/clipboard/const.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { UndoCommandId } from '@univerjs/core';
|
||||
import { InsertColMutation, InsertRowMutation, MoveColsMutation, MoveRangeMutation, MoveRowsMutation, RemoveColMutation, RemoveRowMutation, SetRangeValuesCommand, SetSelectionsOperation, SetWorksheetColWidthMutation, SetWorksheetRowHeightMutation } from '@univerjs/sheets';
|
||||
import { SetCellEditVisibleOperation } from '../../commands/operations/cell-edit.operation';
|
||||
import { SetZoomRatioOperation } from '../../commands/operations/set-zoom-ratio.operation';
|
||||
|
||||
export const RemovePasteMenuCommands = [
|
||||
SetCellEditVisibleOperation.id,
|
||||
SetRangeValuesCommand.id,
|
||||
SetZoomRatioOperation.id,
|
||||
MoveRangeMutation.id,
|
||||
RemoveRowMutation.id,
|
||||
RemoveColMutation.id,
|
||||
InsertRowMutation.id,
|
||||
InsertColMutation.id,
|
||||
MoveRowsMutation.id,
|
||||
MoveColsMutation.id,
|
||||
SetWorksheetColWidthMutation.id,
|
||||
SetWorksheetRowHeightMutation.id,
|
||||
SetSelectionsOperation.id,
|
||||
UndoCommandId,
|
||||
];
|
||||
575
packages/sheets-ui/src/controllers/clipboard/utils.ts
Normal file
575
packages/sheets-ui/src/controllers/clipboard/utils.ts
Normal file
@@ -0,0 +1,575 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-lines-per-function */
|
||||
|
||||
import type { IAccessor, ICellData, ICustomRange, IDocumentBody, IMutationInfo, IParagraph, IRange, IStyleData, Nullable } from '@univerjs/core';
|
||||
import type {
|
||||
IAddWorksheetMergeMutationParams,
|
||||
IDiscreteRange,
|
||||
IRemoveWorksheetMergeMutationParams,
|
||||
ISetRangeValuesMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import type { ICellDataWithSpanInfo, ICopyPastePayload, ISheetDiscreteRangeLocation } from '../../services/clipboard/type';
|
||||
import {
|
||||
CellValueType,
|
||||
cloneCellDataMatrix,
|
||||
createParagraphId,
|
||||
createSectionId,
|
||||
CustomRangeType,
|
||||
generateRandomId,
|
||||
getNumfmtLocaleTag,
|
||||
getNumfmtParseValueFilter,
|
||||
isTextFormat,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
Rectangle,
|
||||
Tools,
|
||||
willLoseNumericPrecision,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddMergeUndoMutationFactory,
|
||||
AddWorksheetMergeMutation,
|
||||
discreteRangeToRange,
|
||||
getMoveRangeCommandMutations,
|
||||
getSheetCommandTarget,
|
||||
RemoveMergeUndoMutationFactory,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetRangeValuesUndoMutationFactory,
|
||||
} from '@univerjs/sheets';
|
||||
import { COPY_TYPE } from '../../services/clipboard/type';
|
||||
import { isRichText } from '../editor/editing.render-controller';
|
||||
import { virtualizeDiscreteRanges } from '../utils/range-tools';
|
||||
|
||||
interface IExternalPasteNumberFormatResult {
|
||||
n?: IStyleData['n'];
|
||||
t?: CellValueType;
|
||||
v?: ICellData['v'];
|
||||
}
|
||||
|
||||
// if special paste need append mutations instead of replace the default, it can use this function to generate default mutations.
|
||||
/**
|
||||
*
|
||||
* @param pasteFrom
|
||||
* @param pasteTo
|
||||
* @param data
|
||||
* @param payload
|
||||
* @param accessor
|
||||
*/
|
||||
export function getDefaultOnPasteCellMutations(
|
||||
pasteFrom: ISheetDiscreteRangeLocation,
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
data: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
payload: ICopyPastePayload,
|
||||
accessor: IAccessor
|
||||
) {
|
||||
if (payload.copyType === COPY_TYPE.CUT) {
|
||||
const { undos, redos } = getMoveRangeMutations(pasteFrom, pasteTo, accessor);
|
||||
return { undos, redos };
|
||||
}
|
||||
|
||||
const redoMutationsInfo: IMutationInfo[] = [];
|
||||
const undoMutationsInfo: IMutationInfo[] = [];
|
||||
|
||||
// Set cell value and style
|
||||
const { redo, undo } = getSetCellDataMutations(
|
||||
pasteTo,
|
||||
pasteFrom,
|
||||
data,
|
||||
accessor
|
||||
);
|
||||
redoMutationsInfo.push(redo);
|
||||
undoMutationsInfo.push(undo);
|
||||
|
||||
// clear and add merge
|
||||
const { undos: clearMergeUndos, redos: clearMergeRedos } = getClearAndSetMergeMutations(
|
||||
pasteTo,
|
||||
data,
|
||||
accessor
|
||||
);
|
||||
redoMutationsInfo.push(...clearMergeRedos);
|
||||
undoMutationsInfo.push(...clearMergeUndos);
|
||||
|
||||
return {
|
||||
undos: undoMutationsInfo,
|
||||
redos: redoMutationsInfo,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMoveRangeMutations(
|
||||
from: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
range?: IDiscreteRange;
|
||||
},
|
||||
to: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
range?: IDiscreteRange;
|
||||
},
|
||||
accessor: IAccessor
|
||||
) {
|
||||
const { range: fromDiscreteRange, subUnitId: fromSubUnitId, unitId } = from;
|
||||
const { range: toDiscreteRange, subUnitId: toSubUnitId } = to;
|
||||
const toRange = toDiscreteRange ? discreteRangeToRange(toDiscreteRange) : null;
|
||||
const fromRange = fromDiscreteRange ? discreteRangeToRange(fromDiscreteRange) : null;
|
||||
|
||||
if (!fromRange || !toRange) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
const moveRangeMutations = getMoveRangeCommandMutations(accessor, {
|
||||
fromRange,
|
||||
toRange,
|
||||
fromUnitId: unitId,
|
||||
fromSubUnitId,
|
||||
toUnitId: to.unitId,
|
||||
toSubUnitId,
|
||||
}, {
|
||||
includeAutoHeight: false,
|
||||
});
|
||||
|
||||
return moveRangeMutations
|
||||
? {
|
||||
undos: moveRangeMutations.undos,
|
||||
redos: moveRangeMutations.redos,
|
||||
}
|
||||
: {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
function getExternalPasteNumberFormatResult(
|
||||
value: ICellData['v'],
|
||||
currentCellStyle: Nullable<IStyleData> | undefined,
|
||||
parseOptions?: Parameters<typeof getNumfmtParseValueFilter>[1]
|
||||
): IExternalPasteNumberFormatResult {
|
||||
if (isTextFormat(currentCellStyle?.n?.pattern)) {
|
||||
return {
|
||||
n: currentCellStyle?.n,
|
||||
t: CellValueType.STRING,
|
||||
};
|
||||
}
|
||||
|
||||
const content = String(value).trim();
|
||||
const numfmtValue = getNumfmtParseValueFilter(content, parseOptions);
|
||||
const result: IExternalPasteNumberFormatResult = {};
|
||||
|
||||
if (numfmtValue?.v !== undefined && typeof numfmtValue.v === 'number') {
|
||||
// If the numeric string will lose precision when converted to a number, set the cell type to force string
|
||||
// e.g. 123456789123456789
|
||||
// e.g. 1212121212121212.2345
|
||||
if (!numfmtValue.z && willLoseNumericPrecision(content)) {
|
||||
result.t = CellValueType.FORCE_STRING;
|
||||
} else {
|
||||
result.v = numfmtValue.v;
|
||||
}
|
||||
}
|
||||
|
||||
if (numfmtValue?.z) {
|
||||
result.n = { pattern: numfmtValue.z };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getSetCellDataMutations(
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
pasteFrom: Nullable<ISheetDiscreteRangeLocation>,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
accessor: IAccessor
|
||||
): {
|
||||
redo: IMutationInfo;
|
||||
undo: IMutationInfo;
|
||||
} {
|
||||
const { unitId, subUnitId, range } = pasteTo;
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId, subUnitId });
|
||||
const worksheet = target?.worksheet;
|
||||
const parseOptions = target
|
||||
? { locale: getNumfmtLocaleTag(target.workbook.getSnapshot().locale), dateSystem: target.workbook.getDateSystem() }
|
||||
: undefined;
|
||||
const { mapFunc } = virtualizeDiscreteRanges([range]);
|
||||
|
||||
const cellValueMatrix = new ObjectMatrix<ICellData>();
|
||||
|
||||
matrix.forValue((rowIndex, columnIndex, value) => {
|
||||
const { row, col } = mapFunc(rowIndex, columnIndex);
|
||||
|
||||
const newStyle = { ...(value.s ?? {}) as IStyleData };
|
||||
if (newStyle.bd && Object.keys(newStyle.bd).length === 0) {
|
||||
newStyle.bd = {
|
||||
t: null,
|
||||
r: null,
|
||||
b: null,
|
||||
l: null,
|
||||
};
|
||||
}
|
||||
|
||||
let cellValue: ICellData = {
|
||||
p: null,
|
||||
v: value.v,
|
||||
t: value.t,
|
||||
};
|
||||
|
||||
if (value.p?.body && isRichText(value.p.body)) {
|
||||
cellValue = {
|
||||
p: value.p,
|
||||
v: value.v,
|
||||
};
|
||||
} else if (!pasteFrom && value.v && !newStyle.n?.pattern) {
|
||||
/**
|
||||
* The `pasteFrom` is null, means the data is pasted from outside.
|
||||
* If the value's style has no number format, handle it as follows:
|
||||
* 1. The paste to cell has a number format, google sheets will apply the number format, but excel does not.
|
||||
* Here we only handle the text format, other number format are left unhandled now.
|
||||
* 2. If the above case does not apply, parse the value to check whether it has a number format.
|
||||
*/
|
||||
const currentCellStyle = worksheet?.getCellStyle(row, col);
|
||||
const numfmtResult = getExternalPasteNumberFormatResult(value.v, currentCellStyle, parseOptions);
|
||||
|
||||
if (numfmtResult.n) {
|
||||
newStyle.n = numfmtResult.n;
|
||||
}
|
||||
if (numfmtResult.t !== undefined) {
|
||||
cellValue.t = numfmtResult.t;
|
||||
}
|
||||
if (numfmtResult.v !== undefined) {
|
||||
cellValue.v = numfmtResult.v;
|
||||
}
|
||||
}
|
||||
|
||||
cellValue.s = Object.keys(newStyle).length > 0 ? newStyle : null;
|
||||
|
||||
cellValueMatrix.setValue(row, col, cellValue);
|
||||
});
|
||||
|
||||
const redoParams: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: cloneCellDataMatrix(cellValueMatrix.getMatrix()),
|
||||
isOverrideStyle: true,
|
||||
};
|
||||
const undoParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
|
||||
accessor,
|
||||
redoParams
|
||||
);
|
||||
|
||||
return {
|
||||
redo: {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: redoParams,
|
||||
},
|
||||
undo: {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: undoParams,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getSetCellValueMutations(
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
pasteFrom: Nullable<ISheetDiscreteRangeLocation>,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
accessor: IAccessor
|
||||
): {
|
||||
redo: IMutationInfo;
|
||||
undo: IMutationInfo;
|
||||
} {
|
||||
const { unitId, subUnitId, range } = pasteTo;
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId, subUnitId });
|
||||
const worksheet = target?.worksheet;
|
||||
const parseOptions = target
|
||||
? { locale: getNumfmtLocaleTag(target.workbook.getSnapshot().locale), dateSystem: target.workbook.getDateSystem() }
|
||||
: undefined;
|
||||
const { mapFunc } = virtualizeDiscreteRanges([range]);
|
||||
|
||||
const cellValueMatrix = new ObjectMatrix<ICellData>();
|
||||
|
||||
matrix.forValue((rowIndex, columnIndex, value) => {
|
||||
const { row, col } = mapFunc(rowIndex, columnIndex);
|
||||
|
||||
let cellValue: ICellData = {
|
||||
p: null,
|
||||
v: value.v,
|
||||
t: value.t,
|
||||
};
|
||||
|
||||
if (value.p?.body && isRichText(value.p.body)) {
|
||||
cellValue = {
|
||||
p: value.p,
|
||||
v: value.v,
|
||||
};
|
||||
} else if (value.v && !pasteFrom && !(value.s as IStyleData)?.n?.pattern) {
|
||||
/**
|
||||
* The `pasteFrom` is null, means the data is pasted from outside.
|
||||
* If the value's style has no number format, handle it as follows:
|
||||
* 1. The paste to cell has a number format, google sheets will apply the number format, but excel does not.
|
||||
* Here we only handle the text format, other number format are left unhandled now.
|
||||
* 2. If the above case does not apply, parse the value to check whether it has a number format.
|
||||
*/
|
||||
const currentCellStyle = worksheet?.getCellStyle(row, col);
|
||||
const numfmtResult = getExternalPasteNumberFormatResult(value.v, currentCellStyle, parseOptions);
|
||||
|
||||
if (numfmtResult.t !== undefined) {
|
||||
cellValue.t = numfmtResult.t;
|
||||
}
|
||||
if (numfmtResult.v !== undefined) {
|
||||
cellValue.v = numfmtResult.v;
|
||||
}
|
||||
}
|
||||
|
||||
cellValueMatrix.setValue(row, col, cellValue);
|
||||
});
|
||||
|
||||
const redoParams: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: cloneCellDataMatrix(cellValueMatrix.getMatrix()),
|
||||
};
|
||||
const undoParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
|
||||
accessor,
|
||||
redoParams
|
||||
);
|
||||
|
||||
return {
|
||||
redo: {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: redoParams,
|
||||
},
|
||||
undo: {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: undoParams,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getSetCellStyleMutations(
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
pasteFrom: Nullable<ISheetDiscreteRangeLocation>,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
accessor: IAccessor
|
||||
): {
|
||||
redo: IMutationInfo;
|
||||
undo: IMutationInfo;
|
||||
} {
|
||||
const { unitId, subUnitId, range } = pasteTo;
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId, subUnitId });
|
||||
const worksheet = target?.worksheet;
|
||||
const parseOptions = target
|
||||
? { locale: getNumfmtLocaleTag(target.workbook.getSnapshot().locale), dateSystem: target.workbook.getDateSystem() }
|
||||
: undefined;
|
||||
const { mapFunc } = virtualizeDiscreteRanges([range]);
|
||||
|
||||
const cellValueMatrix = new ObjectMatrix<ICellData>();
|
||||
|
||||
matrix.forValue((rowIndex, columnIndex, value) => {
|
||||
const { row, col } = mapFunc(rowIndex, columnIndex);
|
||||
|
||||
const newStyle = { ...(value.s ?? {}) as IStyleData };
|
||||
if (newStyle.bd && Object.keys(newStyle.bd).length === 0) {
|
||||
newStyle.bd = {
|
||||
t: null,
|
||||
r: null,
|
||||
b: null,
|
||||
l: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!pasteFrom && value.v && !newStyle.n?.pattern) {
|
||||
/**
|
||||
* The `pasteFrom` is null, means the data is pasted from outside.
|
||||
* If the value's style has no number format, handle it as follows:
|
||||
* 1. The paste to cell has a number format, google sheets will apply the number format, but excel does not.
|
||||
* Here we only handle the text format, other number format are left unhandled now.
|
||||
* 2. If the above case does not apply, parse the value to check whether it has a number format.
|
||||
*/
|
||||
const currentCellStyle = worksheet?.getCellStyle(row, col);
|
||||
const numfmtResult = getExternalPasteNumberFormatResult(value.v, currentCellStyle, parseOptions);
|
||||
|
||||
if (numfmtResult.n) {
|
||||
newStyle.n = numfmtResult.n;
|
||||
}
|
||||
}
|
||||
|
||||
cellValueMatrix.setValue(row, col, {
|
||||
s: Object.keys(newStyle).length > 0 ? newStyle : null,
|
||||
});
|
||||
});
|
||||
|
||||
const redoParams: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: cloneCellDataMatrix(cellValueMatrix.getMatrix()),
|
||||
isOverrideStyle: true,
|
||||
};
|
||||
const undoParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
|
||||
accessor,
|
||||
redoParams
|
||||
);
|
||||
|
||||
return {
|
||||
redo: {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: redoParams,
|
||||
},
|
||||
undo: {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: undoParams,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getClearAndSetMergeMutations(
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
accessor: IAccessor
|
||||
) {
|
||||
const redoMutationsInfo: IMutationInfo[] = [];
|
||||
const undoMutationsInfo: IMutationInfo[] = [];
|
||||
|
||||
const { unitId, subUnitId, range } = pasteTo;
|
||||
const { startColumn, startRow, endColumn, endRow } = discreteRangeToRange(range);
|
||||
const mergeRangeData: IRange[] = [];
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
if (value.rowSpan && value.rowSpan > 1) {
|
||||
const colSpan = value.colSpan || 1;
|
||||
const mergeRange = {
|
||||
startRow: startRow + row,
|
||||
endRow: startRow + row + value.rowSpan - 1,
|
||||
startColumn: startColumn + col,
|
||||
endColumn: startColumn + col + colSpan - 1,
|
||||
};
|
||||
mergeRangeData.push(mergeRange);
|
||||
} else if (value.colSpan && value.colSpan > 1) {
|
||||
const rowSpan = value.rowSpan || 1;
|
||||
const mergeRange = {
|
||||
startRow: startRow + row,
|
||||
endRow: startRow + row + rowSpan - 1,
|
||||
startColumn: startColumn + col,
|
||||
endColumn: startColumn + col + value.colSpan - 1,
|
||||
};
|
||||
mergeRangeData.push(mergeRange);
|
||||
}
|
||||
});
|
||||
|
||||
// clear merge
|
||||
// remove current range's all merged Cell
|
||||
// get all merged cells
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), { unitId, subUnitId });
|
||||
if (target && target.worksheet) {
|
||||
const mergeData = target.worksheet.getMergeData();
|
||||
const mergedCellsInRange = mergeData.filter((rect) =>
|
||||
Rectangle.intersects({ startRow, startColumn, endRow, endColumn }, rect)
|
||||
);
|
||||
|
||||
if (mergedCellsInRange.length > 0) {
|
||||
const removeMergeMutationParams: IRemoveWorksheetMergeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: mergedCellsInRange,
|
||||
};
|
||||
redoMutationsInfo.push({
|
||||
id: RemoveWorksheetMergeMutation.id,
|
||||
params: removeMergeMutationParams,
|
||||
});
|
||||
|
||||
const undoRemoveMergeMutationParams: IAddWorksheetMergeMutationParams = RemoveMergeUndoMutationFactory(
|
||||
accessor,
|
||||
removeMergeMutationParams
|
||||
);
|
||||
undoMutationsInfo.push({
|
||||
id: AddWorksheetMergeMutation.id,
|
||||
params: undoRemoveMergeMutationParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mergeRangeData.length > 0) {
|
||||
// set merged cell info
|
||||
const addMergeMutationParams: IAddWorksheetMergeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: mergeRangeData,
|
||||
};
|
||||
redoMutationsInfo.push({
|
||||
id: AddWorksheetMergeMutation.id,
|
||||
params: addMergeMutationParams,
|
||||
});
|
||||
|
||||
// undo
|
||||
const undoAddMergeMutation: IRemoveWorksheetMergeMutationParams = AddMergeUndoMutationFactory(
|
||||
accessor,
|
||||
addMergeMutationParams
|
||||
);
|
||||
undoMutationsInfo.unshift({
|
||||
id: RemoveWorksheetMergeMutation.id,
|
||||
params: undoAddMergeMutation,
|
||||
});
|
||||
}
|
||||
|
||||
return { redos: redoMutationsInfo, undos: undoMutationsInfo };
|
||||
}
|
||||
|
||||
export function generateBody(text: string): IDocumentBody {
|
||||
if (!text.includes('\r') && Tools.isLegalUrl(text)) {
|
||||
const id = generateRandomId();
|
||||
const urlText = `${text}`;
|
||||
const range: ICustomRange = {
|
||||
startIndex: 0,
|
||||
endIndex: urlText.length - 1,
|
||||
rangeId: id,
|
||||
rangeType: CustomRangeType.HYPERLINK,
|
||||
properties: {
|
||||
url: text,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
dataStream: `${urlText}\r\n`,
|
||||
paragraphs: [{
|
||||
startIndex: urlText.length,
|
||||
paragraphId: createParagraphId(new Set()),
|
||||
}],
|
||||
customRanges: [range],
|
||||
};
|
||||
}
|
||||
|
||||
// Convert all \n to \r, because we use \r to indicate paragraph break.
|
||||
let dataStream = text.replace(/(\r\n|\n)/g, '\r');
|
||||
if (!dataStream.endsWith('\r\n')) {
|
||||
dataStream += '\r\n';
|
||||
}
|
||||
const paragraphs: IParagraph[] = [];
|
||||
const existingParagraphIds = new Set<string>();
|
||||
|
||||
for (let i = 0; i < dataStream.length; i++) {
|
||||
if (dataStream[i] === '\r') {
|
||||
paragraphs.push({ startIndex: i, paragraphId: createParagraphId(existingParagraphIds) });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dataStream,
|
||||
paragraphs,
|
||||
sectionBreaks: [{ sectionId: createSectionId(new Set()), startIndex: dataStream.indexOf('\n') }],
|
||||
};
|
||||
}
|
||||
105
packages/sheets-ui/src/controllers/components.controller.ts
Normal file
105
packages/sheets-ui/src/controllers/components.controller.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsUIConfig } from '../config/config';
|
||||
import { Disposable, IConfigService, Inject, Injector } from '@univerjs/core';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import {
|
||||
UNIVER_SHEET_PERMISSION_DIALOG,
|
||||
UNIVER_SHEET_PERMISSION_PANEL,
|
||||
UNIVER_SHEET_PERMISSION_USER_DIALOG,
|
||||
UNIVER_SHEET_PERMISSION_USER_PART,
|
||||
} from '../consts/permission';
|
||||
import { BorderPanel } from '../views/border-panel/BorderPanel';
|
||||
import { BORDER_PANEL_COMPONENT } from '../views/border-panel/interface';
|
||||
import { CELL_ALERT_KEY } from '../views/cell-alert';
|
||||
import { CellAlert } from '../views/cell-alert/CellAlertPopup';
|
||||
import { CELL_POPUP_COMPONENT_KEY } from '../views/cell-popup/config';
|
||||
import { CellPopup } from '../views/CellPopup';
|
||||
import { DEFINED_NAME_CONTAINER } from '../views/defined-name/component-name';
|
||||
import { DefinedNameContainer } from '../views/defined-name/DefinedNameContainer';
|
||||
import { dropdownMap } from '../views/dropdown';
|
||||
import { MENU_ITEM_FROZEN_COMPONENT, MenuItemFrozen } from '../views/menu-item-frozen/index';
|
||||
import { MENU_ITEM_INPUT_COMPONENT, MenuItemInput } from '../views/menu-item-input/index';
|
||||
import { SheetPermissionDialog, SheetPermissionPanel, SheetPermissionUserDialog } from '../views/permission';
|
||||
import { AlertDialog } from '../views/permission/AlertDialog';
|
||||
import { UNIVER_SHEET_PERMISSION_ALERT_DIALOG } from '../views/permission/error-msg-dialog/interface';
|
||||
import { SHEET_ZOOM_INPUT_COMPONENT, SheetZoomInput } from '../views/sheet-slider/SheetZoomInput';
|
||||
import { sheetsUIIcons } from './sheets-ui-icons';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager,
|
||||
@Inject(Injector) private readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerComponents();
|
||||
this._registerParts();
|
||||
this._registerIcons();
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(
|
||||
this._componentManager.register(CELL_ALERT_KEY, CellAlert)
|
||||
);
|
||||
|
||||
([
|
||||
[UNIVER_SHEET_PERMISSION_PANEL, SheetPermissionPanel],
|
||||
[UNIVER_SHEET_PERMISSION_USER_DIALOG, SheetPermissionUserDialog],
|
||||
[UNIVER_SHEET_PERMISSION_DIALOG, SheetPermissionDialog],
|
||||
[UNIVER_SHEET_PERMISSION_ALERT_DIALOG, AlertDialog],
|
||||
] as const).forEach(([key, comp]) => {
|
||||
this.disposeWithMe(this._componentManager.register(
|
||||
key,
|
||||
comp
|
||||
));
|
||||
});
|
||||
|
||||
const configService = this._injector.get(IConfigService);
|
||||
const config = configService.getConfig<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
if (config?.protectedRangeUserSelector) {
|
||||
const { component, framework } = config.protectedRangeUserSelector;
|
||||
this.disposeWithMe(
|
||||
this._componentManager.register(UNIVER_SHEET_PERMISSION_USER_PART, component, {
|
||||
framework,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _registerParts(): void {
|
||||
const componentManager = this._componentManager;
|
||||
|
||||
// init custom components
|
||||
this.disposeWithMe(componentManager.register(MENU_ITEM_INPUT_COMPONENT, MenuItemInput));
|
||||
this.disposeWithMe(componentManager.register(MENU_ITEM_FROZEN_COMPONENT, MenuItemFrozen));
|
||||
this.disposeWithMe(componentManager.register(SHEET_ZOOM_INPUT_COMPONENT, SheetZoomInput));
|
||||
this.disposeWithMe(componentManager.register(BORDER_PANEL_COMPONENT, BorderPanel));
|
||||
this.disposeWithMe(componentManager.register(DEFINED_NAME_CONTAINER, DefinedNameContainer));
|
||||
this.disposeWithMe(componentManager.register(CELL_POPUP_COMPONENT_KEY, CellPopup));
|
||||
Object.values(dropdownMap).forEach((component) => {
|
||||
this.disposeWithMe(componentManager.register(component.componentKey, component));
|
||||
});
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register(sheetsUIIcons));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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 { Workbook } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle, ISetWorksheetActivateCommandParams } from '@univerjs/sheets';
|
||||
import { Disposable, ICommandService, Inject, IUniverInstanceService, ThemeService, UniverInstanceType } from '@univerjs/core';
|
||||
import { deserializeRangeWithSheet, IDefinedNamesService, isReferenceStrings, operatorToken } from '@univerjs/engine-formula';
|
||||
import { getPrimaryForRange, ScrollToCellOperation, SetWorksheetActivateCommand, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { filter, merge } from 'rxjs';
|
||||
import { genNormalSelectionStyle } from '../../services/selection/const';
|
||||
|
||||
export class SheetsDefinedNameController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService,
|
||||
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
|
||||
@ICommandService private readonly _cmdSrv: ICommandService,
|
||||
@IDefinedNamesService private readonly _definedNamesService: IDefinedNamesService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this.disposeWithMe(merge(
|
||||
this._selectionManagerService.selectionMoveStart$,
|
||||
this._selectionManagerService.selectionMoving$,
|
||||
this._selectionManagerService.selectionMoveEnd$,
|
||||
this._selectionManagerService.selectionSet$
|
||||
)
|
||||
.pipe(filter((params) => !!params))
|
||||
.subscribe((params) => {
|
||||
this._syncDefinedNameRange(params as ISelectionWithStyle[]);
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._definedNamesService.focusRange$.subscribe(async (item) => {
|
||||
if (item == null) return;
|
||||
|
||||
const { unitId } = item;
|
||||
let { formulaOrRefString } = item;
|
||||
|
||||
if (formulaOrRefString.substring(0, 1) === operatorToken.EQUALS) {
|
||||
formulaOrRefString = formulaOrRefString.substring(1);
|
||||
}
|
||||
|
||||
const result = isReferenceStrings(formulaOrRefString);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = this._instanceSrv.getUnit<Workbook>(unitId)!;
|
||||
const selections = await this._getSelections(workbook, unitId, formulaOrRefString);
|
||||
|
||||
this._selectionManagerService.setSelections(selections);
|
||||
|
||||
this._cmdSrv.executeCommand(ScrollToCellOperation.id, {
|
||||
unitId,
|
||||
range: selections[0].range,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private _syncDefinedNameRange(params: ISelectionWithStyle[]) {
|
||||
if (params.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSelection = params[params.length - 1];
|
||||
const workbook = this._instanceSrv.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
if (!worksheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._definedNamesService.setCurrentRange({
|
||||
range: lastSelection.range,
|
||||
unitId: workbook!.getUnitId(),
|
||||
sheetId: worksheet.getSheetId(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _getSelections(workbook: Workbook, unitId: string, formulaOrRefString: string) {
|
||||
const valueArray = formulaOrRefString.split(',');
|
||||
|
||||
let worksheet = workbook.getActiveSheet();
|
||||
|
||||
if (!worksheet) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const selections = [];
|
||||
|
||||
for (let i = 0; i < valueArray.length; i++) {
|
||||
const refString = valueArray[i].trim();
|
||||
|
||||
const unitRange = deserializeRangeWithSheet(refString.trim());
|
||||
|
||||
// WTF@DR-Univer: side effects in get methods
|
||||
if (i === 0) {
|
||||
const worksheetCache = workbook.getSheetBySheetName(unitRange.sheetName);
|
||||
if (worksheetCache && worksheet.getSheetId() !== worksheetCache.getSheetId()) {
|
||||
worksheet = worksheetCache;
|
||||
await this._cmdSrv.executeCommand<ISetWorksheetActivateCommandParams>(SetWorksheetActivateCommand.id, {
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
unitId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (worksheet.getName() !== unitRange.sheetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let primary = null;
|
||||
if (i === valueArray.length - 1) {
|
||||
const range = unitRange.range;
|
||||
const { startRow, startColumn, endRow, endColumn } = range;
|
||||
primary = getPrimaryForRange({
|
||||
startRow,
|
||||
startColumn,
|
||||
endRow,
|
||||
endColumn,
|
||||
|
||||
}, worksheet);
|
||||
}
|
||||
|
||||
selections.push({
|
||||
range: unitRange.range,
|
||||
style: genNormalSelectionStyle(this._themeService),
|
||||
primary,
|
||||
});
|
||||
}
|
||||
|
||||
return selections;
|
||||
}
|
||||
}
|
||||
67
packages/sheets-ui/src/controllers/drag-render.controller.ts
Normal file
67
packages/sheets-ui/src/controllers/drag-render.controller.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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 { Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { ISheetSkeletonManagerParam } from '@univerjs/sheets';
|
||||
import { Disposable, DisposableCollection, Inject } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { DragManagerService } from '../services/drag-manager.service';
|
||||
import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service';
|
||||
|
||||
export class DragRenderController extends Disposable implements IRenderModule, IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@IRenderManagerService private _renderManagerService: IRenderManagerService,
|
||||
@Inject(DragManagerService) private _dragManagerService: DragManagerService,
|
||||
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initDragEvent();
|
||||
}
|
||||
|
||||
private _initDragEvent() {
|
||||
const disposeSet = new DisposableCollection();
|
||||
const handleSkeletonChange = (skeletonParam: Nullable<ISheetSkeletonManagerParam>) => {
|
||||
disposeSet.dispose();
|
||||
if (!skeletonParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scene } = this._context;
|
||||
const dragOverSub = scene.onDragOver$.subscribeEvent((evt) => {
|
||||
this._dragManagerService.onDragOver(evt);
|
||||
});
|
||||
|
||||
const dropSub = scene.onDrop$.subscribeEvent((evt) => {
|
||||
this._dragManagerService.onDrop(evt);
|
||||
});
|
||||
|
||||
disposeSet.add({
|
||||
dispose() {
|
||||
dragOverSub.unsubscribe();
|
||||
dropSub.unsubscribe();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
handleSkeletonChange(this._sheetSkeletonManagerService.getCurrentParam());
|
||||
this.disposeWithMe(this._sheetSkeletonManagerService.currentSkeleton$.subscribe((skeletonParam) => {
|
||||
handleSkeletonChange(skeletonParam);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import { ILogService, Inject, Injector, IUniverInstanceService, LocaleType, LogLevel, Plugin, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
function getTestWorkbookDataDemo(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: Workbook;
|
||||
}
|
||||
|
||||
export function createTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData ?? getTestWorkbookDataDemo());
|
||||
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
const logService = injector.get(ILogService);
|
||||
|
||||
logService.setLogLevel(LogLevel.SILENT); // change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 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 { ICommandInfo, IDocumentBody, IDocumentData, IDrawings, Nullable } from '@univerjs/core';
|
||||
import {
|
||||
BooleanNumber,
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
DocumentDataModel,
|
||||
FOCUSING_FX_BAR_EDITOR,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
ThemeService,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
MoveRangeMutation,
|
||||
RangeProtectionRuleModel,
|
||||
SetRangeValuesMutation,
|
||||
WorksheetProtectionRuleModel,
|
||||
} from '@univerjs/sheets';
|
||||
import { BehaviorSubject, EMPTY } from 'rxjs';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { IEditorBridgeService } from '../../../services/editor-bridge.service';
|
||||
import { IFormulaEditorManagerService } from '../../../services/editor/formula-editor-manager.service';
|
||||
import { EditorDataSyncController } from '../data-sync.controller';
|
||||
import { FormulaEditorController } from '../formula-editor.controller';
|
||||
|
||||
interface ITestableEditorDataSyncController {
|
||||
_checkAndSetRenderStyleConfig: (documentDataModel: Pick<DocumentDataModel, 'getSnapshot'>) => void;
|
||||
_syncContentAndRender: (
|
||||
unitId: string,
|
||||
body: IDocumentBody,
|
||||
drawings: Nullable<IDrawings>,
|
||||
drawingsOrder: Nullable<string[]>
|
||||
) => void;
|
||||
}
|
||||
|
||||
class TestCommandService {
|
||||
private readonly _listeners = new Set<(command: ICommandInfo) => void>();
|
||||
|
||||
onCommandExecuted(listener: (command: ICommandInfo) => void) {
|
||||
this._listeners.add(listener);
|
||||
|
||||
return {
|
||||
dispose: () => this._listeners.delete(listener),
|
||||
};
|
||||
}
|
||||
|
||||
emit(command: ICommandInfo) {
|
||||
this._listeners.forEach((listener) => listener(command));
|
||||
}
|
||||
}
|
||||
|
||||
describe('EditorDataSyncController', () => {
|
||||
const controllers: EditorDataSyncController[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
controllers.splice(0).forEach((controller) => controller.dispose());
|
||||
});
|
||||
|
||||
function createController(options: {
|
||||
isFocusFxBar?: boolean;
|
||||
themeTextColor?: string;
|
||||
formulaBarPosition?: { width: number; height: number } | null;
|
||||
documentDataModel?: DocumentDataModel;
|
||||
} = {}) {
|
||||
const commandService = new TestCommandService();
|
||||
const editorBridgeService = {
|
||||
currentEditCellState$: new BehaviorSubject(null),
|
||||
getEditLocation: vi.fn(() => ({
|
||||
row: 1,
|
||||
column: 2,
|
||||
})),
|
||||
refreshEditCellState: vi.fn(),
|
||||
isForceKeepVisible: vi.fn(() => false),
|
||||
};
|
||||
const contextService = {
|
||||
getContextValue: vi.fn((key: string) => {
|
||||
if (key === FOCUSING_FX_BAR_EDITOR) return options.isFocusFxBar ?? false;
|
||||
return false;
|
||||
}),
|
||||
subscribeContextValue$: vi.fn(() => EMPTY),
|
||||
};
|
||||
const currentTheme$ = new BehaviorSubject({});
|
||||
const skeleton = { calculate: vi.fn() };
|
||||
const documentViewModel = { reset: vi.fn() };
|
||||
const currentRender = {
|
||||
mainComponent: { makeDirty: vi.fn() },
|
||||
with: vi.fn(() => ({
|
||||
getSkeleton: () => skeleton,
|
||||
getViewModel: () => documentViewModel,
|
||||
})),
|
||||
};
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([IUniverInstanceService, {
|
||||
useValue: { getUnit: () => options.documentDataModel } as never,
|
||||
}]);
|
||||
injector.add([IRenderManagerService, {
|
||||
useValue: { getRenderUnitById: () => options.documentDataModel ? currentRender : undefined } as never,
|
||||
}]);
|
||||
injector.add([IEditorBridgeService, { useValue: editorBridgeService as never }]);
|
||||
injector.add([ICommandService, { useValue: commandService as never }]);
|
||||
injector.add([RangeProtectionRuleModel, { useValue: { getRangeRuleInitState: () => true } as never }]);
|
||||
injector.add([WorksheetProtectionRuleModel, { useValue: { getSheetRuleInitState: () => true } as never }]);
|
||||
injector.add([FormulaEditorController, { useValue: { autoScroll: vi.fn() } as never }]);
|
||||
injector.add([IFormulaEditorManagerService, {
|
||||
useValue: { getPosition: () => options.formulaBarPosition ?? null } as never,
|
||||
}]);
|
||||
injector.add([IContextService, { useValue: contextService as never }]);
|
||||
injector.add([ThemeService, {
|
||||
useValue: {
|
||||
currentTheme$,
|
||||
getColorFromTheme: vi.fn((token: string) => token === 'gray.900'
|
||||
? options.themeTextColor ?? '#1b1c1f'
|
||||
: '#000000'),
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([EditorDataSyncController]);
|
||||
|
||||
const controller = injector.get(EditorDataSyncController);
|
||||
|
||||
controllers.push(controller);
|
||||
|
||||
return {
|
||||
commandService,
|
||||
controller,
|
||||
editorBridgeService,
|
||||
};
|
||||
}
|
||||
|
||||
function checkAndSetRenderStyleConfig(controller: EditorDataSyncController, snapshot: IDocumentData) {
|
||||
(controller as unknown as ITestableEditorDataSyncController)._checkAndSetRenderStyleConfig({
|
||||
getSnapshot: vi.fn(() => snapshot),
|
||||
});
|
||||
}
|
||||
|
||||
function syncContentAndRender(
|
||||
controller: EditorDataSyncController,
|
||||
body: IDocumentBody
|
||||
) {
|
||||
const testableController = controller as unknown as ITestableEditorDataSyncController;
|
||||
testableController._syncContentAndRender(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, body, null, null);
|
||||
}
|
||||
|
||||
it('ignores a zero-width formula bar position', () => {
|
||||
const { controller } = createController({ formulaBarPosition: { width: 0, height: 28 } });
|
||||
const formulaBarSnapshot: IDocumentData = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
documentStyle: {},
|
||||
body: {
|
||||
dataStream: 'text\r\n',
|
||||
},
|
||||
};
|
||||
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
|
||||
expect(formulaBarSnapshot.documentStyle?.pageSize?.width).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
it('applies a measured formula bar width', () => {
|
||||
const { controller } = createController({ formulaBarPosition: { width: 320, height: 28 } });
|
||||
const formulaBarSnapshot: IDocumentData = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
documentStyle: {},
|
||||
body: {
|
||||
dataStream: 'text\r\n',
|
||||
},
|
||||
};
|
||||
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
|
||||
expect(formulaBarSnapshot.documentStyle?.pageSize?.width).toBe(320);
|
||||
});
|
||||
|
||||
it('keeps the previous formula bar width when the latest position has zero width', () => {
|
||||
const formulaBarPosition = { width: 320, height: 28 };
|
||||
const { controller } = createController({ formulaBarPosition });
|
||||
const formulaBarSnapshot: IDocumentData = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
documentStyle: {},
|
||||
body: {
|
||||
dataStream: 'text\r\n',
|
||||
},
|
||||
};
|
||||
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
formulaBarPosition.width = 0;
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
|
||||
expect(formulaBarSnapshot.documentStyle.pageSize?.width).toBe(320);
|
||||
});
|
||||
|
||||
it('notifies formula bar content observers when the selected cell changes', () => {
|
||||
const documentDataModel = new DocumentDataModel({
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
documentStyle: {},
|
||||
body: {
|
||||
dataStream: '=SUM(A1)\r\n',
|
||||
},
|
||||
});
|
||||
const { controller } = createController({ documentDataModel });
|
||||
let observedDataStream = '';
|
||||
const subscription = documentDataModel.change$.subscribe(() => {
|
||||
observedDataStream = documentDataModel.getBody()?.dataStream ?? '';
|
||||
});
|
||||
|
||||
syncContentAndRender(controller, { dataStream: '\r\n' });
|
||||
|
||||
expect(documentDataModel.getBody()?.dataStream).toBe('\r\n');
|
||||
expect(observedDataStream).toBe('\r\n');
|
||||
|
||||
subscription.unsubscribe();
|
||||
documentDataModel.dispose();
|
||||
});
|
||||
|
||||
it('refreshes the current edit cell when set-values updates the edited cell', () => {
|
||||
const { commandService, editorBridgeService } = createController();
|
||||
|
||||
commandService.emit({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
cellValue: {
|
||||
1: {
|
||||
2: { v: 'updated' },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ICommandInfo);
|
||||
|
||||
expect(editorBridgeService.refreshEditCellState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('refreshes the current edit cell when set-values clears the edited cell', () => {
|
||||
const { commandService, editorBridgeService } = createController();
|
||||
|
||||
commandService.emit({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
cellValue: {
|
||||
1: {
|
||||
2: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ICommandInfo);
|
||||
|
||||
expect(editorBridgeService.refreshEditCellState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('refreshes the current edit cell when move-range changes the edited cell', () => {
|
||||
const { commandService, editorBridgeService } = createController();
|
||||
|
||||
commandService.emit({
|
||||
id: MoveRangeMutation.id,
|
||||
params: {
|
||||
from: {
|
||||
subUnitId: 'sheet1',
|
||||
value: {
|
||||
1: {
|
||||
2: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
to: {
|
||||
subUnitId: 'sheet1',
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
} as ICommandInfo);
|
||||
|
||||
expect(editorBridgeService.refreshEditCellState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders formula reference styles in the cell editor when the cell editor is focused', () => {
|
||||
const { controller } = createController();
|
||||
const cellEditorSnapshot: IDocumentData = {
|
||||
id: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: '=G10+G12+G13\r\n',
|
||||
textRuns: [
|
||||
{ st: 1, ed: 4, ts: { cl: { rgb: '#8f7cf6' }, fs: 11 } },
|
||||
],
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
const formulaBarSnapshot: IDocumentData = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: '=G10+G12+G13\r\n',
|
||||
textRuns: [
|
||||
{ st: 1, ed: 4, ts: { cl: { rgb: '#8f7cf6' }, fs: 11 } },
|
||||
],
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
|
||||
checkAndSetRenderStyleConfig(controller, cellEditorSnapshot);
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
|
||||
expect(cellEditorSnapshot.documentStyle.renderConfig?.isRenderStyle).toBe(BooleanNumber.TRUE);
|
||||
expect(formulaBarSnapshot.documentStyle.renderConfig?.isRenderStyle).toBe(BooleanNumber.FALSE);
|
||||
});
|
||||
|
||||
it('uses the resolved theme color for the formula bar default text', () => {
|
||||
const { controller } = createController({ themeTextColor: '#f7f9fc' });
|
||||
const formulaBarSnapshot: IDocumentData = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: 'A Schedule of Items\r\n',
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
|
||||
expect(formulaBarSnapshot.documentStyle.textStyle?.cl?.rgb).toBe('#f7f9fc');
|
||||
});
|
||||
|
||||
it('renders formula reference styles in the formula bar when the formula bar is focused', () => {
|
||||
const { controller } = createController({ isFocusFxBar: true });
|
||||
const cellEditorSnapshot: IDocumentData = {
|
||||
id: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: '=G10+G12+G13\r\n',
|
||||
textRuns: [
|
||||
{ st: 1, ed: 4, ts: { cl: { rgb: '#8f7cf6' }, fs: 11 } },
|
||||
],
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
const formulaBarSnapshot: IDocumentData = {
|
||||
id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: '=G10+G12+G13\r\n',
|
||||
textRuns: [
|
||||
{ st: 1, ed: 4, ts: { cl: { rgb: '#8f7cf6' }, fs: 11 } },
|
||||
],
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
|
||||
checkAndSetRenderStyleConfig(controller, cellEditorSnapshot);
|
||||
checkAndSetRenderStyleConfig(controller, formulaBarSnapshot);
|
||||
|
||||
expect(cellEditorSnapshot.documentStyle.renderConfig?.isRenderStyle).toBe(BooleanNumber.FALSE);
|
||||
expect(formulaBarSnapshot.documentStyle.renderConfig?.isRenderStyle).toBe(BooleanNumber.TRUE);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,599 @@
|
||||
/**
|
||||
* 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 { IDocumentBody } from '@univerjs/core';
|
||||
import {
|
||||
Direction,
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
EDITOR_ACTIVATED,
|
||||
FOCUSING_EDITOR_INPUT_FORMULA,
|
||||
FOCUSING_FX_BAR_EDITOR,
|
||||
LocaleType,
|
||||
Styles,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { InsertTextCommand } from '@univerjs/docs';
|
||||
import { MoveCursorOperation, MoveSelectionOperation, SetDocInputStyleCommand, VIEWPORT_KEY } from '@univerjs/docs-ui';
|
||||
import { LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import { SetRangeValuesCommand } from '@univerjs/sheets';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
MoveSelectionCommand,
|
||||
MoveSelectionEnterAndTabCommand,
|
||||
} from '../../../commands/commands/set-selection.command';
|
||||
import { EditingRenderController, emptyBody } from '../editing.render-controller';
|
||||
|
||||
describe('emptyBody', () => {
|
||||
it('keeps an empty text-run collection initialized', () => {
|
||||
const body: IDocumentBody = { dataStream: 'value\r\n', textRuns: [] };
|
||||
|
||||
emptyBody(body);
|
||||
|
||||
expect(body.textRuns).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps one inherited style when resetting the editor', () => {
|
||||
const body: IDocumentBody = {
|
||||
dataStream: 'value\r\n',
|
||||
textRuns: [{ st: 0, ed: 5, ts: { fs: 11 } }],
|
||||
};
|
||||
|
||||
emptyBody(body);
|
||||
|
||||
expect(body.textRuns).toEqual([{ st: 0, ed: 1, ts: { fs: 11 } }]);
|
||||
});
|
||||
|
||||
it('removes an inherited style when style removal is requested', () => {
|
||||
const body: IDocumentBody = {
|
||||
dataStream: 'value\r\n',
|
||||
textRuns: [{ st: 0, ed: 5, ts: { fs: 11 } }],
|
||||
};
|
||||
|
||||
emptyBody(body, true);
|
||||
|
||||
expect(body.textRuns).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function createController(initialDataStream = 'new value\r\n', isPercentFormat = false, isInArrayFormulaRange = false) {
|
||||
const worksheet = {
|
||||
getSheetId: vi.fn(() => 'sheet-1'),
|
||||
getCellRaw: vi.fn(() => ({ v: 'old' })),
|
||||
getComposedCellStyleWithoutSelf: vi.fn(() => ({})),
|
||||
};
|
||||
const styles = new Styles();
|
||||
const workbook = {
|
||||
getUnitId: vi.fn(() => 'unit-1'),
|
||||
getActiveSheet: vi.fn(() => worksheet),
|
||||
getSheetBySheetId: vi.fn(() => worksheet),
|
||||
getStyles: vi.fn(() => styles),
|
||||
};
|
||||
let normalSnapshot = {
|
||||
body: { dataStream: initialDataStream, paragraphs: [{ startIndex: 9, paragraphId: 'normal-para' }] },
|
||||
documentStyle: {},
|
||||
};
|
||||
let formulaSnapshot = {
|
||||
body: { dataStream: initialDataStream, paragraphs: [{ startIndex: 9, paragraphId: 'formula-para' }] },
|
||||
documentStyle: {},
|
||||
};
|
||||
const docModel = {
|
||||
getUnitId: vi.fn(() => DOCS_NORMAL_EDITOR_UNIT_ID_KEY),
|
||||
getBody: vi.fn(() => normalSnapshot.body),
|
||||
getSnapshot: vi.fn(() => normalSnapshot),
|
||||
reset: vi.fn((snapshot) => {
|
||||
normalSnapshot = snapshot;
|
||||
}),
|
||||
};
|
||||
const formulaDocModel = {
|
||||
getUnitId: vi.fn(() => DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY),
|
||||
getSnapshot: vi.fn(() => formulaSnapshot),
|
||||
reset: vi.fn((snapshot) => {
|
||||
formulaSnapshot = snapshot;
|
||||
}),
|
||||
};
|
||||
const documentModel = {
|
||||
getBody: vi.fn((): IDocumentBody => ({ dataStream: 'new value\r\n' })),
|
||||
getSnapshot: vi.fn(() => ({
|
||||
body: { dataStream: 'new value\r\n' },
|
||||
documentStyle: {},
|
||||
})),
|
||||
};
|
||||
const controller = Object.create(EditingRenderController.prototype) as any;
|
||||
const formulaBarEditor = {
|
||||
setSelectionRanges: vi.fn(),
|
||||
blur: vi.fn(),
|
||||
};
|
||||
controller._editingUnit = 'unit-1';
|
||||
controller._lexerTreeBuilder = new LexerTreeBuilder();
|
||||
controller._localService = { getCurrentLocale: vi.fn(() => LocaleType.EN_US) };
|
||||
controller._functionService = { getDescriptions: vi.fn(() => ({})) };
|
||||
controller._undoRedoService = {
|
||||
rollback: vi.fn(),
|
||||
clearUndoRedo: vi.fn(),
|
||||
};
|
||||
controller._docStateChangeManagerService = {
|
||||
clearHistory: vi.fn(),
|
||||
};
|
||||
controller._contextService = {
|
||||
setContextValue: vi.fn(),
|
||||
getContextValue: vi.fn(() => false),
|
||||
};
|
||||
controller._cellEditorManagerService = { setState: vi.fn() };
|
||||
controller._sheetCellEditorResizeService = {
|
||||
fitTextSize: vi.fn((callback?: () => void) => callback?.()),
|
||||
};
|
||||
controller._editorService = {
|
||||
isSheetEditor: vi.fn(() => true),
|
||||
getEditor: vi.fn((editorId: string) => editorId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY ? formulaBarEditor : null),
|
||||
};
|
||||
controller._editorBridgeService = {
|
||||
getEditCellState: vi.fn(() => ({
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
row: 2,
|
||||
column: 3,
|
||||
documentLayoutObject: { documentModel },
|
||||
isInArrayFormulaRange,
|
||||
isPercentFormat,
|
||||
})),
|
||||
getEditLocation: vi.fn(() => ({
|
||||
unitId: 'unit-1',
|
||||
sheetId: 'sheet-1',
|
||||
row: 2,
|
||||
column: 3,
|
||||
documentLayoutObject: { documentModel },
|
||||
isInArrayFormulaRange,
|
||||
isPercentFormat,
|
||||
})),
|
||||
getCurrentEditorId: vi.fn(() => DOCS_NORMAL_EDITOR_UNIT_ID_KEY),
|
||||
isVisible: vi.fn(() => ({ visible: true, eventType: DeviceInputEventType.Keyboard, unitId: 'unit-1' })),
|
||||
getEditorDirty: vi.fn(() => true),
|
||||
isForceKeepVisible: vi.fn(() => false),
|
||||
disableForceKeepVisible: vi.fn(),
|
||||
refreshEditCellState: vi.fn(),
|
||||
refreshEditCellPosition: vi.fn(),
|
||||
changeEditorDirty: vi.fn(),
|
||||
};
|
||||
controller._sheetInterceptorService = {
|
||||
onWriteCell: vi.fn((_workbook, _worksheet, _row, _column, cellData) => cellData),
|
||||
onValidateCell: vi.fn(() => true),
|
||||
};
|
||||
controller._commandService = {
|
||||
syncExecuteCommand: vi.fn(() => true),
|
||||
executeCommand: vi.fn(),
|
||||
};
|
||||
controller._univerInstanceService = {
|
||||
getUnit: vi.fn((unitId: string) => {
|
||||
if (unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
return docModel;
|
||||
}
|
||||
if (unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
return formulaDocModel;
|
||||
}
|
||||
|
||||
return workbook;
|
||||
}),
|
||||
getCurrentUnitOfType: vi.fn(() => workbook),
|
||||
setCurrentUnitForType: vi.fn(),
|
||||
};
|
||||
controller._textSelectionManagerService = {
|
||||
replaceDocRanges: vi.fn(),
|
||||
refreshSelection: vi.fn(),
|
||||
};
|
||||
const workbookSelections = {
|
||||
getCurrentLastSelection: vi.fn(() => ({ range: { startRow: 1, startColumn: 1, endRow: 4, endColumn: 4 } })),
|
||||
getSelectionsOfWorksheet: vi.fn(() => [{ range: { startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 } }]),
|
||||
};
|
||||
controller._selectionManagerService = {
|
||||
getWorkbookSelections: vi.fn(() => workbookSelections),
|
||||
};
|
||||
controller._renderManagerService = {
|
||||
getRenderUnitById: vi.fn(() => ({
|
||||
scene: {
|
||||
getViewport: vi.fn((key) => key === VIEWPORT_KEY.VIEW_MAIN
|
||||
? { scrollToViewportPos: vi.fn() }
|
||||
: null),
|
||||
resetCursor: vi.fn(),
|
||||
},
|
||||
with: vi.fn(() => ({ resetInitialWidth: vi.fn() })),
|
||||
})),
|
||||
};
|
||||
controller._getEditorObject = vi.fn(() => ({
|
||||
document: {
|
||||
makeDirty: vi.fn(),
|
||||
},
|
||||
scene: {
|
||||
getViewport: vi.fn((key) => key === VIEWPORT_KEY.VIEW_MAIN
|
||||
? { scrollToViewportPos: vi.fn() }
|
||||
: null),
|
||||
resetCursor: vi.fn(),
|
||||
},
|
||||
}));
|
||||
controller._getEditorSkeleton = vi.fn(() => ({ calculate: vi.fn(), resetInitialWidth: vi.fn() }));
|
||||
controller._getEditorViewModel = vi.fn(() => ({ reset: vi.fn() }));
|
||||
|
||||
return {
|
||||
controller,
|
||||
docModel,
|
||||
formulaBarEditor,
|
||||
documentModel,
|
||||
getFormulaSnapshot: () => formulaSnapshot,
|
||||
getNormalSnapshot: () => normalSnapshot,
|
||||
workbook,
|
||||
worksheet,
|
||||
};
|
||||
}
|
||||
|
||||
describe('EditingRenderController business methods', () => {
|
||||
it('bypasses parsing, interceptors, and commands when the editor is clean', async () => {
|
||||
const { controller, worksheet } = createController();
|
||||
controller._editorBridgeService.getEditorDirty.mockReturnValue(false);
|
||||
|
||||
const result = await controller._submitEdit({
|
||||
body: { dataStream: 'canonical display value\r\n' },
|
||||
documentStyle: {},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(worksheet.getCellRaw).not.toHaveBeenCalled();
|
||||
expect(controller._sheetInterceptorService.onWriteCell).not.toHaveBeenCalled();
|
||||
expect(controller._commandService.syncExecuteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reuses an empty cell style for the next editor input', () => {
|
||||
const { controller, documentModel } = createController();
|
||||
documentModel.getBody.mockReturnValue({
|
||||
dataStream: '\r\n',
|
||||
textRuns: [{ st: 0, ed: 0, ts: { fs: 20, cl: { rgb: '#f05252' } } }],
|
||||
});
|
||||
|
||||
controller._cacheEmptyCellTextStyle();
|
||||
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(SetDocInputStyleCommand.id, {
|
||||
style: {
|
||||
fs: 20,
|
||||
cl: { rgb: '#f05252' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('submits edited cell content and rolls back when validation rejects the value', async () => {
|
||||
const { controller } = createController();
|
||||
controller._sheetInterceptorService.onValidateCell.mockResolvedValue(false);
|
||||
|
||||
const result = await controller._submitEdit({
|
||||
body: { dataStream: 'new value\r\n' },
|
||||
documentStyle: {},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(controller._sheetInterceptorService.onWriteCell).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 2, 3, {
|
||||
v: 'new value',
|
||||
f: null,
|
||||
si: null,
|
||||
p: null,
|
||||
});
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(SetRangeValuesCommand.id, expect.objectContaining({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 },
|
||||
value: { v: 'new value', f: null, si: null, p: null },
|
||||
}));
|
||||
expect(controller._undoRedoService.rollback).toHaveBeenCalledWith(expect.any(String), 'unit-1');
|
||||
});
|
||||
|
||||
it('captures rich-text cell data independently from the reusable editor snapshot', async () => {
|
||||
const { controller } = createController();
|
||||
const snapshot = {
|
||||
body: {
|
||||
dataStream: 'rich text\r\n',
|
||||
textRuns: [{ st: 5, ed: 9, ts: { bl: 1 } }],
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
|
||||
await controller._submitEdit(snapshot);
|
||||
const submitted = controller._commandService.syncExecuteCommand.mock.calls
|
||||
.find(([id]: [string]) => id === SetRangeValuesCommand.id)?.[1];
|
||||
snapshot.body.textRuns = [];
|
||||
|
||||
expect(submitted?.value.p).not.toBe(snapshot);
|
||||
expect(submitted?.value.p?.body?.textRuns).toEqual([{ st: 5, ed: 9, ts: { bl: 1 } }]);
|
||||
});
|
||||
|
||||
it('uses the whole current selection when committing an array edit', async () => {
|
||||
const { controller } = createController();
|
||||
|
||||
await controller._submitEdit({
|
||||
body: { dataStream: 'fill\r\n' },
|
||||
documentStyle: {},
|
||||
}, true);
|
||||
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(SetRangeValuesCommand.id, expect.objectContaining({
|
||||
range: { startRow: 1, startColumn: 1, endRow: 4, endColumn: 4 },
|
||||
}));
|
||||
expect(controller._undoRedoService.rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('moves sheet selection after finishing edit and switches back to the edited workbook when needed', () => {
|
||||
const { controller } = createController();
|
||||
controller._univerInstanceService.getCurrentUnitOfType.mockReturnValue({ getUnitId: () => 'other-unit' });
|
||||
|
||||
controller._moveSelection(KeyCode.ENTER, 'unit-1', 'sheet-1');
|
||||
controller._moveSelection(KeyCode.ARROW_LEFT, 'unit-1', 'sheet-1');
|
||||
controller._moveSelection(undefined, 'unit-1', 'sheet-1');
|
||||
|
||||
expect(controller._univerInstanceService.setCurrentUnitForType).toHaveBeenCalledWith('unit-1');
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(MoveSelectionEnterAndTabCommand.id, {
|
||||
keycode: KeyCode.ENTER,
|
||||
direction: 2,
|
||||
});
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(MoveSelectionCommand.id, {
|
||||
direction: 3,
|
||||
});
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||
subUnitId: 'sheet-1',
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Enter', KeyCode.ENTER, Direction.UP],
|
||||
['Tab', KeyCode.TAB, Direction.LEFT],
|
||||
])('moves the sheet selection in reverse after finishing edit with Shift+%s', async (_name, keycode, direction) => {
|
||||
const { controller } = createController();
|
||||
controller._editorBridgeService.getEditorDirty.mockReturnValue(false);
|
||||
|
||||
await controller._handleEditorInvisible({
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
unitId: 'unit-1',
|
||||
keycode,
|
||||
isShift: true,
|
||||
});
|
||||
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(MoveSelectionEnterAndTabCommand.id, {
|
||||
keycode,
|
||||
direction,
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes editor content when Esc cancels editing', async () => {
|
||||
const { controller } = createController();
|
||||
|
||||
await controller._handleEditorInvisible({
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
unitId: 'unit-1',
|
||||
keycode: KeyCode.ESC,
|
||||
});
|
||||
|
||||
expect(controller._editorBridgeService.refreshEditCellState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('moves the cursor inside the editor and resets editor state on exit', () => {
|
||||
const { controller, formulaBarEditor } = createController();
|
||||
|
||||
controller._moveInEditor(KeyCode.ARROW_RIGHT, false);
|
||||
controller._moveInEditor(KeyCode.ARROW_UP, true);
|
||||
controller._exitInput({ visible: false });
|
||||
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(MoveCursorOperation.id, { direction: 1 });
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(MoveSelectionOperation.id, { direction: 0 });
|
||||
expect(controller._contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_EDITOR_INPUT_FORMULA, false);
|
||||
expect(controller._contextService.setContextValue).toHaveBeenCalledWith(EDITOR_ACTIVATED, false);
|
||||
expect(controller._contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_FX_BAR_EDITOR, false);
|
||||
expect(controller._cellEditorManagerService.setState).toHaveBeenCalledWith({ show: false });
|
||||
expect(controller._undoRedoService.clearUndoRedo).toHaveBeenCalledWith(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
expect(controller._undoRedoService.clearUndoRedo).toHaveBeenCalledWith(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
expect(formulaBarEditor.setSelectionRanges).toHaveBeenCalledWith([], false);
|
||||
expect(formulaBarEditor.blur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('leaves initial keyboard input to the doc input pipeline when opening the cell editor', () => {
|
||||
const { controller, getFormulaSnapshot, getNormalSnapshot } = createController();
|
||||
|
||||
controller._handleEditorVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode: 187,
|
||||
initialValue: '=',
|
||||
unitId: 'unit-1',
|
||||
});
|
||||
|
||||
expect(getNormalSnapshot().body.dataStream).toBe('\r\n');
|
||||
expect(getFormulaSnapshot().body.dataStream).toBe('\r\n');
|
||||
expect(controller._textSelectionManagerService.replaceDocRanges).toHaveBeenCalledWith(
|
||||
[{ startOffset: 0, endOffset: 0 }],
|
||||
{
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
}
|
||||
);
|
||||
expect(controller._editorBridgeService.changeEditorDirty).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['3', '35', '0.35', '35%'])('preserves the percent suffix when input starts with a digit: %s', (initialValue) => {
|
||||
const { controller, getFormulaSnapshot, getNormalSnapshot } = createController('25%\r\n', true);
|
||||
|
||||
controller._handleEditorVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode: KeyCode.Digit3,
|
||||
initialValue,
|
||||
unitId: 'unit-1',
|
||||
});
|
||||
|
||||
expect(getNormalSnapshot().body.dataStream).toBe('25%\r\n');
|
||||
expect(getFormulaSnapshot().body.dataStream).toBe('25%\r\n');
|
||||
expect(controller._textSelectionManagerService.replaceDocRanges).toHaveBeenCalledWith(
|
||||
[{ startOffset: 0, endOffset: 2, collapsed: false }],
|
||||
{
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['$30', '=1', 'text', '+', '-', '.', ','])('replaces percent content when input does not start with a digit: %s', (initialValue) => {
|
||||
const { controller, getNormalSnapshot } = createController('25%\r\n', true);
|
||||
|
||||
controller._handleEditorVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode: KeyCode.Digit3,
|
||||
initialValue,
|
||||
unitId: 'unit-1',
|
||||
});
|
||||
|
||||
expect(getNormalSnapshot().body.dataStream).toBe('\r\n');
|
||||
});
|
||||
|
||||
it.each([KeyCode.BACKSPACE, KeyCode.DELETE])('keeps full-clear behavior for keycode %s', (keycode) => {
|
||||
const { controller, getNormalSnapshot } = createController('25%\r\n', true);
|
||||
|
||||
controller._handleEditorVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode,
|
||||
unitId: 'unit-1',
|
||||
});
|
||||
|
||||
expect(getNormalSnapshot().body.dataStream).toBe('\r\n');
|
||||
});
|
||||
|
||||
it('keeps full-clear behavior for array formula cells', () => {
|
||||
const { controller, getNormalSnapshot } = createController('25%\r\n', true, true);
|
||||
|
||||
controller._handleEditorVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode: KeyCode.Digit3,
|
||||
initialValue: '3',
|
||||
unitId: 'unit-1',
|
||||
});
|
||||
|
||||
expect(getNormalSnapshot().body.dataStream).toBe('\r\n');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['F2', { eventType: DeviceInputEventType.Keyboard, keycode: KeyCode.F2 }],
|
||||
['double-click', { eventType: DeviceInputEventType.Dblclick }],
|
||||
])('places the caret before the percent suffix for %s editing', (_name, input) => {
|
||||
const { controller } = createController('25%\r\n', true);
|
||||
const selection = { startOffset: 2, endOffset: 2, collapsed: true };
|
||||
|
||||
controller._handleEditorVisible({
|
||||
visible: true,
|
||||
unitId: 'unit-1',
|
||||
...input,
|
||||
});
|
||||
|
||||
expect(controller._textSelectionManagerService.replaceDocRanges).toHaveBeenLastCalledWith(
|
||||
[selection],
|
||||
{
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('overtypes a preserved percent suffix on subsequent percent input', () => {
|
||||
const { controller } = createController('0.35%\r\n', true);
|
||||
const beforeCommandListeners: Array<(command: { id: string; params: unknown }) => void> = [];
|
||||
controller._commandService.beforeCommandExecuted = vi.fn((listener) => {
|
||||
beforeCommandListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
});
|
||||
controller._commandService.onCommandExecuted = vi.fn(() => ({ dispose: vi.fn() }));
|
||||
controller._commandExecutedListener({ add: vi.fn() });
|
||||
const params = {
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
body: { dataStream: '%' },
|
||||
range: { startOffset: 4, endOffset: 4, collapsed: true },
|
||||
};
|
||||
|
||||
expect(beforeCommandListeners).toHaveLength(1);
|
||||
beforeCommandListeners[0]({ id: InsertTextCommand.id, params });
|
||||
|
||||
expect(params.range).toEqual({ startOffset: 4, endOffset: 5, collapsed: false });
|
||||
});
|
||||
|
||||
it('overtypes the original percent value on initial digit input', () => {
|
||||
const { controller } = createController('25%\r\n', true);
|
||||
controller._editorBridgeService.getEditorDirty.mockReturnValue(false);
|
||||
const beforeCommandListeners: Array<(command: { id: string; params: unknown }) => void> = [];
|
||||
controller._editorBridgeService.isVisible.mockReturnValue({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
initialValue: '3',
|
||||
unitId: 'unit-1',
|
||||
});
|
||||
controller._commandService.beforeCommandExecuted = vi.fn((listener) => {
|
||||
beforeCommandListeners.push(listener);
|
||||
return { dispose: vi.fn() };
|
||||
});
|
||||
controller._commandService.onCommandExecuted = vi.fn(() => ({ dispose: vi.fn() }));
|
||||
controller._commandExecutedListener({ add: vi.fn() });
|
||||
const params = {
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
body: { dataStream: '3' },
|
||||
range: { startOffset: 0, endOffset: 0, collapsed: true },
|
||||
};
|
||||
|
||||
beforeCommandListeners[0]({ id: InsertTextCommand.id, params });
|
||||
|
||||
expect(params.range).toEqual({ startOffset: 0, endOffset: 2, collapsed: false });
|
||||
});
|
||||
|
||||
it('syncs the active sheet editor selection instead of the host document selection on focus', () => {
|
||||
const { controller, workbook } = createController();
|
||||
const focus$ = new Subject<boolean>();
|
||||
const hostSelectionSync = vi.fn();
|
||||
const cellEditorSelectionSync = vi.fn();
|
||||
const disposableCollection = { add: vi.fn() };
|
||||
|
||||
controller._cellEditorManagerService.focus$ = focus$;
|
||||
controller._univerInstanceService.getCurrentUnitOfType.mockImplementation((type: UniverInstanceType) => {
|
||||
if (type === UniverInstanceType.UNIVER_DOC) {
|
||||
return { getUnitId: () => 'host-doc' };
|
||||
}
|
||||
|
||||
return workbook;
|
||||
});
|
||||
controller._renderManagerService.getRenderUnitById.mockImplementation((unitId: string) => ({
|
||||
with: vi.fn(() => {
|
||||
if (unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
return { sync: cellEditorSelectionSync };
|
||||
}
|
||||
if (unitId === 'host-doc') {
|
||||
return { sync: hostSelectionSync };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
}));
|
||||
|
||||
controller._initialCursorSync(disposableCollection);
|
||||
focus$.next(true);
|
||||
|
||||
expect(cellEditorSelectionSync).toHaveBeenCalledTimes(1);
|
||||
expect(hostSelectionSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { ICellData } from '@univerjs/core';
|
||||
import { CellValueType, Styles, Tools } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getComparableCellData } from '../editing.render-controller';
|
||||
|
||||
describe('cell editor commit comparison', () => {
|
||||
const unchanged: Array<{ name: string; previous: ICellData; incoming: ICellData }> = [
|
||||
{ name: 'ordinary text after draft Undo', previous: { v: 'Committed', t: CellValueType.STRING }, incoming: { v: 'Committed' } },
|
||||
{ name: 'number text', previous: { v: 42, t: CellValueType.NUMBER }, incoming: { v: '42' } },
|
||||
{ name: 'zero', previous: { v: 0, t: CellValueType.NUMBER }, incoming: { v: '0' } },
|
||||
{ name: 'true', previous: { v: 1, t: CellValueType.BOOLEAN }, incoming: { v: 'TRUE' } },
|
||||
{ name: 'false', previous: { v: 0, t: CellValueType.BOOLEAN }, incoming: { v: 'FALSE' } },
|
||||
{ name: 'forced text', previous: { v: '001', t: CellValueType.FORCE_STRING }, incoming: { v: '001', t: CellValueType.FORCE_STRING } },
|
||||
{ name: 'inherited text format', previous: { v: '001', t: CellValueType.STRING, s: 'text' }, incoming: { v: '001', s: 'text' } },
|
||||
{ name: 'resolved equivalent style', previous: { v: 'Styled', t: CellValueType.STRING, s: 'blue' }, incoming: { v: 'Styled', s: { cl: { rgb: '#0000ff' } } } },
|
||||
];
|
||||
|
||||
it.each(unchanged)('recognizes unchanged $name without mutating the input or styles', ({ previous, incoming }) => {
|
||||
const styles = new Styles({ text: { n: { pattern: '@' } }, blue: { cl: { rgb: '#0000ff' } } });
|
||||
const previousBefore = Tools.deepClone(previous);
|
||||
const incomingBefore = Tools.deepClone(incoming);
|
||||
expect(getComparableCellData(incoming, styles, previous)).toEqual(getComparableCellData(previous, styles, previous));
|
||||
expect(previous).toEqual(previousBefore);
|
||||
expect(incoming).toEqual(incomingBefore);
|
||||
expect(styles.get('text')).toEqual({ n: { pattern: '@' } });
|
||||
expect(styles.get('blue')).toEqual({ cl: { rgb: '#0000ff' } });
|
||||
});
|
||||
|
||||
const changed: Array<{ name: string; previous: ICellData; incoming: ICellData }> = [
|
||||
{ name: 'actual text edit', previous: { v: 'old', t: CellValueType.STRING }, incoming: { v: 'new' } },
|
||||
{ name: 'numeric conversion from text', previous: { v: '001', t: CellValueType.STRING }, incoming: { v: '001' } },
|
||||
{ name: 'removing forced text', previous: { v: '001', t: CellValueType.FORCE_STRING }, incoming: { v: '001' } },
|
||||
{ name: 'boolean conversion', previous: { v: 'TRUE', t: CellValueType.STRING }, incoming: { v: 'TRUE' } },
|
||||
{ name: 'format-only edit', previous: { v: 'same', t: CellValueType.STRING, s: { bl: 0 } }, incoming: { v: 'same', s: { bl: 1 } } },
|
||||
{ name: 'formula replacement', previous: { v: 2, t: CellValueType.NUMBER, f: '=1+1' }, incoming: { v: null, f: '=1+2' } },
|
||||
];
|
||||
|
||||
it.each(changed)('preserves a meaningful $name', ({ previous, incoming }) => {
|
||||
const styles = new Styles();
|
||||
expect(getComparableCellData(incoming, styles, previous)).not.toEqual(getComparableCellData(previous, styles, previous));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { describe, expect, it } from 'vitest';
|
||||
import { EMBEDDING_FORMULA_EDITOR, isEmbeddingFormulaEditor } from '../utils/is-embedding-formula-editor';
|
||||
import { isRangeSelector, RANGE_SELECTOR_SYMBOLS } from '../utils/is-range-selector';
|
||||
|
||||
describe('editor utils', () => {
|
||||
it('should detect embedding formula editor unit ids', () => {
|
||||
expect(isEmbeddingFormulaEditor(`abc_${EMBEDDING_FORMULA_EDITOR}_xyz`)).toBe(true);
|
||||
expect(isEmbeddingFormulaEditor('normal-doc-id')).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect range selector unit ids', () => {
|
||||
expect(isRangeSelector(`abc_${RANGE_SELECTOR_SYMBOLS}_xyz`)).toBe(true);
|
||||
expect(isRangeSelector('normal-doc-id')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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 {
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
EDITOR_ACTIVATED,
|
||||
FOCUSING_FX_BAR_EDITOR,
|
||||
FOCUSING_UNIVER_EDITOR,
|
||||
} from '@univerjs/core';
|
||||
import { CoverContentCommand, VIEWPORT_KEY } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SetCellEditVisibleOperation } from '../../../commands/operations/cell-edit.operation';
|
||||
import { FormulaEditorController } from '../formula-editor.controller';
|
||||
|
||||
function createController() {
|
||||
const fxBtnClick$ = new Subject<boolean>();
|
||||
const position = { width: 240, height: 40 };
|
||||
let focusEditorId = DOCS_NORMAL_EDITOR_UNIT_ID_KEY;
|
||||
const contextValues = new Map<string, unknown>([
|
||||
[EDITOR_ACTIVATED, false],
|
||||
[FOCUSING_FX_BAR_EDITOR, false],
|
||||
[FOCUSING_UNIVER_EDITOR, true],
|
||||
]);
|
||||
const scrollBar = { dispose: vi.fn() };
|
||||
const viewport = {
|
||||
getScrollBar: vi.fn(() => scrollBar),
|
||||
scrollToViewportPos: vi.fn(),
|
||||
resetCanvasSizeAndUpdateScroll: vi.fn(),
|
||||
};
|
||||
const scene = { transformByState: vi.fn(), getViewport: vi.fn((key) => key === VIEWPORT_KEY.VIEW_MAIN ? viewport : null) };
|
||||
const mainComponent = { resize: vi.fn() };
|
||||
const formulaDoc = {
|
||||
getBody: vi.fn(() => ({ dataStream: 'SUM(A1:A3)\r\n' })),
|
||||
getSnapshot: vi.fn(() => ({ documentStyle: { marginTop: 4, marginBottom: 6 } })),
|
||||
updateDocumentDataPageSize: vi.fn(),
|
||||
};
|
||||
const render = {
|
||||
scene,
|
||||
mainComponent,
|
||||
with: vi.fn(() => ({
|
||||
getSkeleton: vi.fn(() => ({
|
||||
getActualSize: vi.fn(() => ({ actualHeight: 24 })),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
const controller = Object.create(FormulaEditorController.prototype) as any;
|
||||
controller.dispose$ = new Subject<void>();
|
||||
controller._formulaEditorManagerService = {
|
||||
fxBtnClick$,
|
||||
getPosition: vi.fn(() => position),
|
||||
position$: new Subject(),
|
||||
};
|
||||
controller._renderManagerService = {
|
||||
getRenderUnitById: vi.fn((unitId: string) => unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY ? render : null),
|
||||
};
|
||||
controller._univerInstanceService = {
|
||||
setCurrentUnitForType: vi.fn(),
|
||||
getUnit: vi.fn(() => formulaDoc),
|
||||
};
|
||||
controller._editorBridgeService = { isVisible: vi.fn(() => ({ visible: false })) };
|
||||
controller._commandService = { syncExecuteCommand: vi.fn() };
|
||||
controller._contextService = {
|
||||
getContextValue: vi.fn((key: string) => contextValues.get(key)),
|
||||
setContextValue: vi.fn((key: string, value: unknown) => contextValues.set(key, value)),
|
||||
};
|
||||
controller._textSelectionManagerService = { replaceDocRanges: vi.fn() };
|
||||
controller._editorService = {
|
||||
getFocusEditor: vi.fn(() => ({ getEditorId: () => focusEditorId })),
|
||||
};
|
||||
|
||||
return {
|
||||
controller,
|
||||
contextValues,
|
||||
fxBtnClick$,
|
||||
formulaDoc,
|
||||
mainComponent,
|
||||
scene,
|
||||
setFocusEditorId: (editorId: string) => {
|
||||
focusEditorId = editorId;
|
||||
},
|
||||
scrollBar,
|
||||
viewport,
|
||||
};
|
||||
}
|
||||
|
||||
describe('FormulaEditorController business methods', () => {
|
||||
it('resizes the formula editor and resets scroll when content fits in the formula bar', () => {
|
||||
const { controller, mainComponent, scene, scrollBar, viewport } = createController();
|
||||
|
||||
controller.autoScroll();
|
||||
|
||||
expect(scene.transformByState).toHaveBeenCalledWith({ width: 240, height: 34 });
|
||||
expect(mainComponent.resize).toHaveBeenCalledWith(240, 34);
|
||||
expect(viewport.scrollToViewportPos).toHaveBeenCalledWith({ viewportScrollX: 0, viewportScrollY: 0 });
|
||||
expect(scrollBar.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('turns hidden formula-bar content into a formula when fx button is clicked', () => {
|
||||
const { controller, fxBtnClick$ } = createController();
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
||||
const raf = vi.fn((cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('requestAnimationFrame', raf);
|
||||
|
||||
controller._listenFxBtnClick();
|
||||
fxBtnClick$.next(false);
|
||||
|
||||
expect(controller._univerInstanceService.setCurrentUnitForType).toHaveBeenCalledWith(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(SetCellEditVisibleOperation.id, {
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.PointerDown,
|
||||
unitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(CoverContentCommand.id, {
|
||||
unitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: { dataStream: '=SUM(A1:A3)' },
|
||||
segmentId: '',
|
||||
textRanges: [{ startOffset: 11, endOffset: 11 }],
|
||||
});
|
||||
expect(controller._textSelectionManagerService.replaceDocRanges).toHaveBeenCalledWith([], {
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
expect(controller._contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_FX_BAR_EDITOR, true);
|
||||
|
||||
vi.stubGlobal('requestAnimationFrame', originalRequestAnimationFrame);
|
||||
});
|
||||
|
||||
it('enters formula mode from an active mobile formula bar', () => {
|
||||
const { contextValues, controller, fxBtnClick$ } = createController();
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
||||
contextValues.set(EDITOR_ACTIVATED, true);
|
||||
controller._editorBridgeService.isVisible.mockReturnValue({ visible: true });
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
||||
callback(0);
|
||||
return 1;
|
||||
});
|
||||
|
||||
controller._listenFxBtnClick();
|
||||
fxBtnClick$.next(true);
|
||||
|
||||
expect(controller._commandService.syncExecuteCommand).not.toHaveBeenCalledWith(
|
||||
SetCellEditVisibleOperation.id,
|
||||
expect.anything()
|
||||
);
|
||||
expect(controller._commandService.syncExecuteCommand).toHaveBeenCalledWith(CoverContentCommand.id, {
|
||||
unitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: { dataStream: '=SUM(A1:A3)' },
|
||||
segmentId: '',
|
||||
textRanges: [{ startOffset: 11, endOffset: 11 }],
|
||||
});
|
||||
expect(controller._contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_FX_BAR_EDITOR, true);
|
||||
|
||||
vi.stubGlobal('requestAnimationFrame', originalRequestAnimationFrame);
|
||||
});
|
||||
|
||||
it('keeps formula bar focus while formula range picking is active', () => {
|
||||
const { contextValues, controller, setFocusEditorId } = createController();
|
||||
contextValues.set(FOCUSING_FX_BAR_EDITOR, true);
|
||||
contextValues.set(EDITOR_ACTIVATED, true);
|
||||
setFocusEditorId(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
|
||||
controller._syncFxBarFocusContext();
|
||||
|
||||
expect(contextValues.get(FOCUSING_FX_BAR_EDITOR)).toBe(true);
|
||||
expect(controller._contextService.setContextValue).not.toHaveBeenCalledWith(FOCUSING_FX_BAR_EDITOR, false);
|
||||
});
|
||||
|
||||
it('clears formula bar focus when focus leaves outside formula range picking', () => {
|
||||
const { contextValues, controller, setFocusEditorId } = createController();
|
||||
contextValues.set(FOCUSING_FX_BAR_EDITOR, true);
|
||||
contextValues.set(EDITOR_ACTIVATED, false);
|
||||
setFocusEditorId(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
|
||||
controller._syncFxBarFocusContext();
|
||||
|
||||
expect(contextValues.get(FOCUSING_FX_BAR_EDITOR)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { IFunctionInfo, IFunctionNames } from '@univerjs/engine-formula';
|
||||
import { createIdentifier } from '@univerjs/core';
|
||||
import { FunctionType } from '@univerjs/engine-formula';
|
||||
|
||||
export class MockFunctionService {
|
||||
getDescriptions(): Map<IFunctionNames, IFunctionInfo> {
|
||||
const map = new Map<IFunctionNames, IFunctionInfo>();
|
||||
map.set('IF', { functionName: 'IF', functionType: FunctionType.Logical, description: 'IF function', abstract: 'IF function', functionParameter: [] });
|
||||
map.set('TAN', { functionName: 'TAN', functionType: FunctionType.Logical, description: 'TAN function', abstract: 'TAN function', functionParameter: [] });
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
export const IMockFunctionService = createIdentifier<MockFunctionService>('mock.univer.formula.function.service');
|
||||
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, ICommandInfo, IDocumentBody, IDocumentStyle, IDrawings, IParagraph, Nullable } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { DocumentViewModel } from '@univerjs/engine-render';
|
||||
import type { IMoveRangeMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import type { ICellEditorState } from '../../services/editor-bridge.service';
|
||||
import {
|
||||
BooleanNumber,
|
||||
createParagraphId,
|
||||
Disposable,
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
DocumentFlavor,
|
||||
FOCUSING_FX_BAR_EDITOR,
|
||||
HorizontalAlign,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
ThemeService,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
VerticalAlign,
|
||||
WrapStrategy,
|
||||
} from '@univerjs/core';
|
||||
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { ReplaceSnapshotCommand } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { MoveRangeMutation, RangeProtectionRuleModel, SetRangeValuesMutation, WorksheetProtectionRuleModel } from '@univerjs/sheets';
|
||||
import { skip } from 'rxjs';
|
||||
import { IEditorBridgeService } from '../../services/editor-bridge.service';
|
||||
import { IFormulaEditorManagerService } from '../../services/editor/formula-editor-manager.service';
|
||||
import { FormulaEditorController } from './formula-editor.controller';
|
||||
|
||||
const formulaEditorStyle: IDocumentStyle = {
|
||||
pageSize: {
|
||||
width: Number.POSITIVE_INFINITY,
|
||||
height: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
documentFlavor: DocumentFlavor.UNSPECIFIED,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
marginRight: 0,
|
||||
marginLeft: 0,
|
||||
paragraphLineGapDefault: 0,
|
||||
renderConfig: {
|
||||
horizontalAlign: HorizontalAlign.UNSPECIFIED,
|
||||
verticalAlign: VerticalAlign.TOP,
|
||||
centerAngle: 0,
|
||||
vertexAngle: 0,
|
||||
wrapStrategy: WrapStrategy.WRAP,
|
||||
isRenderStyle: BooleanNumber.FALSE,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* sync data between cell editor and formula editor
|
||||
*/
|
||||
export class EditorDataSyncController extends Disposable {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel,
|
||||
@Inject(WorksheetProtectionRuleModel) private readonly _worksheetProtectionRuleModel: WorksheetProtectionRuleModel,
|
||||
@Inject(FormulaEditorController) private readonly _formulaEditorController: FormulaEditorController,
|
||||
@IFormulaEditorManagerService private readonly _formulaEditorManagerService: IFormulaEditorManagerService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._syncFormulaEditorContent();
|
||||
this._commandExecutedListener();
|
||||
this._syncFormulaRefRenderStyleOnFocusChange();
|
||||
this._syncFormulaBarTextColorOnThemeChange();
|
||||
}
|
||||
|
||||
private _getEditorViewModel(unitId: string): Nullable<DocumentViewModel> {
|
||||
return this._renderManagerService.getRenderUnitById(unitId)?.with(DocSkeletonManagerService).getViewModel();
|
||||
}
|
||||
|
||||
// Sync cell content to formula editor bar when sheet selection changed.
|
||||
private _syncFormulaEditorContent() {
|
||||
this.disposeWithMe(this._editorBridgeService.currentEditCellState$.subscribe((editCellState) => {
|
||||
if (
|
||||
editCellState == null
|
||||
|| this._editorBridgeService.isForceKeepVisible()
|
||||
// If permissions are not initialized, data synchronization will not be performed.
|
||||
|| !this._rangeProtectionRuleModel.getRangeRuleInitState()
|
||||
|| !this._worksheetProtectionRuleModel.getSheetRuleInitState()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editorSyncHandler(editCellState);
|
||||
}));
|
||||
}
|
||||
|
||||
// Sync cell content to formula editor bar when sheet selection changed or visible changed.
|
||||
private _editorSyncHandler(param: ICellEditorState) {
|
||||
let body = Tools.deepClone(param.documentLayoutObject.documentModel?.getBody());
|
||||
const drawings = Tools.deepClone(param.documentLayoutObject.documentModel?.drawings);
|
||||
const drawingsOrder = Tools.deepClone(param.documentLayoutObject.documentModel?.getDrawingsOrder());
|
||||
|
||||
if (
|
||||
!body ||
|
||||
(param.isInArrayFormulaRange === true && this._editorBridgeService.isVisible().eventType === DeviceInputEventType.Dblclick)
|
||||
) {
|
||||
body = {
|
||||
dataStream: '\r\n',
|
||||
paragraphs: [
|
||||
{
|
||||
startIndex: 0,
|
||||
paragraphId: createParagraphId(new Set()),
|
||||
},
|
||||
],
|
||||
textRuns: [],
|
||||
};
|
||||
}
|
||||
|
||||
this._syncContentAndRender(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, body, drawings, drawingsOrder);
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
const INCLUDE_LIST = [DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY];
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === RichTextEditingMutation.id) {
|
||||
const params = command.params as IRichTextEditingMutationParams;
|
||||
const { unitId, trigger, isSync } = params;
|
||||
if (isSync || trigger === ReplaceSnapshotCommand.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (INCLUDE_LIST.includes(unitId)) {
|
||||
// sync cell content to formula editor bar when edit cell editor and vice verse.
|
||||
const editorDocDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
const syncId =
|
||||
unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY
|
||||
? DOCS_NORMAL_EDITOR_UNIT_ID_KEY
|
||||
: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY;
|
||||
|
||||
this._checkAndSetRenderStyleConfig(editorDocDataModel!);
|
||||
this._syncActionsAndRender(syncId, params);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Update formula bar content when you call SetRangeValuesMutation and MoveRangeMutation.
|
||||
const needUpdateFormulaEditorContentCommandList = [SetRangeValuesMutation.id, MoveRangeMutation.id];
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (!needUpdateFormulaEditorContentCommandList.includes(command.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editCellState = this._editorBridgeService.getEditLocation();
|
||||
|
||||
if (editCellState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { row, column } = editCellState;
|
||||
|
||||
if (this._shouldRefreshCurrentEditCell(command, row, column)) {
|
||||
/**
|
||||
* Refresh the current edit-cell snapshot so both the hidden cell editor and
|
||||
* the formula bar stay aligned with external cell mutations.
|
||||
*/
|
||||
this._editorBridgeService.refreshEditCellState();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _shouldRefreshCurrentEditCell(command: ICommandInfo, row: number, column: number): boolean {
|
||||
if (command.id === SetRangeValuesMutation.id && command.params) {
|
||||
const params = command.params as ISetRangeValuesMutationParams;
|
||||
return this._hasMatrixCell(params.cellValue, row, column);
|
||||
}
|
||||
|
||||
if (command.id === MoveRangeMutation.id && command.params) {
|
||||
const params = command.params as IMoveRangeMutationParams;
|
||||
return this._hasMatrixCell(params.from.value, row, column) || this._hasMatrixCell(params.to.value, row, column);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _hasMatrixCell(
|
||||
matrix: ISetRangeValuesMutationParams['cellValue'] | IMoveRangeMutationParams['from']['value'],
|
||||
row: number,
|
||||
column: number
|
||||
): boolean {
|
||||
const rowData = matrix?.[row];
|
||||
return rowData != null && Object.prototype.hasOwnProperty.call(rowData, column);
|
||||
}
|
||||
|
||||
// Sync actions between cell editor and formula editor, and make `dataStream` and `paragraph` is the same.
|
||||
private _syncActionsAndRender(
|
||||
unitId: string,
|
||||
parmas: IRichTextEditingMutationParams
|
||||
) {
|
||||
const INCLUDE_LIST = [DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY];
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (currentRender == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = currentRender.with(DocSkeletonManagerService).getSkeleton();
|
||||
const docDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const docViewModel = this._getEditorViewModel(unitId);
|
||||
|
||||
if (docDataModel == null || docViewModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandService.syncExecuteCommand(RichTextEditingMutation.id, {
|
||||
...parmas,
|
||||
textRanges: null,
|
||||
isSync: true,
|
||||
unitId,
|
||||
syncer: parmas.unitId,
|
||||
});
|
||||
|
||||
this._checkAndSetRenderStyleConfig(docDataModel);
|
||||
docViewModel.reset(docDataModel);
|
||||
|
||||
skeleton.calculate();
|
||||
|
||||
if (INCLUDE_LIST.includes(unitId)) {
|
||||
currentRender.mainComponent?.makeDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private _syncContentAndRender(
|
||||
unitId: string,
|
||||
body: IDocumentBody,
|
||||
drawings: Nullable<IDrawings>,
|
||||
drawingsOrder: Nullable<string[]>
|
||||
) {
|
||||
if (unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
if (body.paragraphs) {
|
||||
body.paragraphs = this._clearParagraph(body.paragraphs);
|
||||
}
|
||||
}
|
||||
const INCLUDE_LIST = [DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY];
|
||||
|
||||
const skeleton = this._renderManagerService.getRenderUnitById(unitId)?.with(DocSkeletonManagerService).getSkeleton();
|
||||
const docDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const docViewModel = this._getEditorViewModel(unitId);
|
||||
|
||||
if (docDataModel == null || docViewModel == null || skeleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
docDataModel.reset({
|
||||
...docDataModel.getSnapshot(),
|
||||
body,
|
||||
drawings: drawings ?? {},
|
||||
drawingsOrder: drawingsOrder ?? [],
|
||||
});
|
||||
|
||||
this._checkAndSetRenderStyleConfig(docDataModel);
|
||||
docViewModel.reset(docDataModel);
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (currentRender == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
skeleton.calculate();
|
||||
|
||||
if (INCLUDE_LIST.includes(unitId)) {
|
||||
currentRender.mainComponent?.makeDirty();
|
||||
}
|
||||
|
||||
if (unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
this._formulaEditorController.autoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
private _syncFormulaRefRenderStyleOnFocusChange() {
|
||||
this.disposeWithMe(this._contextService.subscribeContextValue$(FOCUSING_FX_BAR_EDITOR).subscribe(() => {
|
||||
this._refreshRenderStyleConfig(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
this._refreshRenderStyleConfig(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
}));
|
||||
}
|
||||
|
||||
private _syncFormulaBarTextColorOnThemeChange() {
|
||||
this.disposeWithMe(this._themeService.currentTheme$.pipe(skip(1)).subscribe(() => {
|
||||
this._refreshRenderStyleConfig(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
}));
|
||||
}
|
||||
|
||||
private _refreshRenderStyleConfig(unitId: string) {
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const skeleton = currentRender?.with(DocSkeletonManagerService).getSkeleton();
|
||||
const docDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const docViewModel = this._getEditorViewModel(unitId);
|
||||
|
||||
if (currentRender == null || skeleton == null || docDataModel == null || docViewModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkAndSetRenderStyleConfig(docDataModel);
|
||||
docViewModel.reset(docDataModel);
|
||||
skeleton.calculate();
|
||||
currentRender.mainComponent?.makeDirty();
|
||||
|
||||
if (unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
this._formulaEditorController.autoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
private _checkAndSetRenderStyleConfig(documentDataModel: DocumentDataModel) {
|
||||
const snapshot = documentDataModel.getSnapshot();
|
||||
|
||||
if (snapshot.id !== DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY && snapshot.id !== DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFormulaBar = snapshot.id === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY;
|
||||
const previousPageWidth = snapshot.documentStyle?.pageSize?.width;
|
||||
if (isFormulaBar) {
|
||||
snapshot.documentStyle = {
|
||||
...formulaEditorStyle,
|
||||
pageSize: { ...formulaEditorStyle.pageSize },
|
||||
renderConfig: { ...formulaEditorStyle.renderConfig },
|
||||
textStyle: {
|
||||
cl: {
|
||||
rgb: this._themeService.getColorFromTheme('gray.900'),
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
snapshot.documentStyle ??= {};
|
||||
}
|
||||
let renderConfig = snapshot.documentStyle.renderConfig;
|
||||
|
||||
if (renderConfig == null) {
|
||||
renderConfig = {};
|
||||
snapshot.documentStyle.renderConfig = renderConfig;
|
||||
}
|
||||
const position = isFormulaBar ? this._formulaEditorManagerService.getPosition() : null;
|
||||
if (isFormulaBar && position) {
|
||||
// A non-positive width means the formula bar is not laid out, so keep the last valid width.
|
||||
const width = position.width > 0 ? position.width : previousPageWidth;
|
||||
if (width != null && width > 0) {
|
||||
snapshot.documentStyle.pageSize = {
|
||||
width,
|
||||
height: Infinity,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const isFormula = (snapshot.body?.dataStream ?? '').startsWith('=');
|
||||
if (!isFormula) {
|
||||
renderConfig.isRenderStyle = this._getSourceStyleRenderValue(isFormulaBar);
|
||||
return;
|
||||
}
|
||||
|
||||
const isFocusFxBar = this._contextService.getContextValue(FOCUSING_FX_BAR_EDITOR);
|
||||
renderConfig.isRenderStyle = isFormulaBar === isFocusFxBar ? BooleanNumber.TRUE : BooleanNumber.FALSE;
|
||||
}
|
||||
|
||||
protected _getSourceStyleRenderValue(isFormulaBar: boolean): BooleanNumber {
|
||||
return isFormulaBar ? BooleanNumber.FALSE : BooleanNumber.TRUE;
|
||||
}
|
||||
|
||||
private _clearParagraph(paragraphs: IParagraph[]) {
|
||||
const newParagraphs = Tools.deepClone(paragraphs);
|
||||
for (const paragraph of newParagraphs) {
|
||||
if (paragraph.paragraphStyle) {
|
||||
paragraph.paragraphStyle.horizontalAlign = HorizontalAlign.UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
return newParagraphs;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 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 { DocumentDataModel, Nullable } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { RenderComponentType } from '@univerjs/engine-render';
|
||||
import type { IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service';
|
||||
import {
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
EDITOR_ACTIVATED,
|
||||
FOCUSING_FX_BAR_EDITOR,
|
||||
FOCUSING_UNIVER_EDITOR,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
RxDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DocSelectionManagerService,
|
||||
DocSkeletonManagerService,
|
||||
RichTextEditingMutation,
|
||||
} from '@univerjs/docs';
|
||||
import { CoverContentCommand, IEditorService, VIEWPORT_KEY } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType, IRenderManagerService, ScrollBar } from '@univerjs/engine-render';
|
||||
import { combineLatest, filter, takeUntil } from 'rxjs';
|
||||
import { getEditorObject } from '../../basics/editor/get-editor-object';
|
||||
import { SetCellEditVisibleOperation } from '../../commands/operations/cell-edit.operation';
|
||||
import { IEditorBridgeService } from '../../services/editor-bridge.service';
|
||||
import { IFormulaEditorManagerService } from '../../services/editor/formula-editor-manager.service';
|
||||
|
||||
export class FormulaEditorController extends RxDisposable {
|
||||
private _loadedMap = new WeakSet<RenderComponentType>();
|
||||
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IFormulaEditorManagerService private readonly _formulaEditorManagerService: IFormulaEditorManagerService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@IEditorService private readonly _editorService: IEditorService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._syncEditorSize();
|
||||
this._listenFxBtnClick();
|
||||
this._handleContentChange();
|
||||
|
||||
this._univerInstanceService.focused$.pipe(takeUntil(this.dispose$)).subscribe((unitId) => {
|
||||
this._create(unitId);
|
||||
});
|
||||
|
||||
this._create(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
|
||||
this.disposeWithMe(this._editorService.focus$.subscribe(() => {
|
||||
this._syncFxBarFocusContext();
|
||||
}));
|
||||
}
|
||||
|
||||
private _syncFxBarFocusContext(): void {
|
||||
const focusUnitId = this._editorService.getFocusEditor()?.getEditorId();
|
||||
if (focusUnitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
this._contextService.setContextValue(FOCUSING_FX_BAR_EDITOR, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._contextService.getContextValue(FOCUSING_FX_BAR_EDITOR) &&
|
||||
this._contextService.getContextValue(EDITOR_ACTIVATED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._contextService.setContextValue(FOCUSING_FX_BAR_EDITOR, false);
|
||||
}
|
||||
|
||||
private _handleContentChange() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === RichTextEditingMutation.id) {
|
||||
const params = commandInfo.params as IRichTextEditingMutationParams;
|
||||
const { unitId } = params;
|
||||
if (unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
this.autoScroll();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _create(unitId: Nullable<string>) {
|
||||
if (unitId !== DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formulaEditorDocObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (formulaEditorDocObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainComponent: documentComponent } = formulaEditorDocObject;
|
||||
|
||||
if (documentComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._loadedMap.has(documentComponent)) {
|
||||
this._loadedMap.add(documentComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private _listenFxBtnClick() {
|
||||
this._formulaEditorManagerService.fxBtnClick$.pipe(takeUntil(this.dispose$)).subscribe((forceFormulaMode) => {
|
||||
const isFocusButHidden =
|
||||
this._contextService.getContextValue(FOCUSING_UNIVER_EDITOR) &&
|
||||
!this._contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
|
||||
if (isFocusButHidden || forceFormulaMode) {
|
||||
this._univerInstanceService.setCurrentUnitForType(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
const formulaEditorDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
UniverInstanceType.UNIVER_DOC
|
||||
);
|
||||
|
||||
const visibleState = this._editorBridgeService.isVisible();
|
||||
if (visibleState.visible === false) {
|
||||
this._commandService.syncExecuteCommand(
|
||||
SetCellEditVisibleOperation.id,
|
||||
{
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.PointerDown,
|
||||
unitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
} as IEditorBridgeServiceVisibleParam
|
||||
);
|
||||
}
|
||||
|
||||
const content = formulaEditorDataModel?.getBody()?.dataStream;
|
||||
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newContent = content.startsWith('=') ? content : `=${content}`;
|
||||
|
||||
newContent = newContent.replace(/\r\n$/, '');
|
||||
|
||||
const textRanges = [
|
||||
{
|
||||
startOffset: newContent.length,
|
||||
endOffset: newContent.length,
|
||||
},
|
||||
];
|
||||
|
||||
const coverContentParams = {
|
||||
unitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: newContent,
|
||||
},
|
||||
segmentId: '',
|
||||
textRanges,
|
||||
};
|
||||
|
||||
this._commandService.syncExecuteCommand(CoverContentCommand.id, coverContentParams);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._textSelectionManagerService.replaceDocRanges([], {
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
this._textSelectionManagerService.replaceDocRanges(textRanges, {
|
||||
unitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
this._contextService.setContextValue(FOCUSING_FX_BAR_EDITOR, true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _syncEditorSize() {
|
||||
// this._univerInstanceService.
|
||||
const addFormulaBar$ = this._univerInstanceService.unitAdded$.pipe(filter((event) => event.unit.getUnitId() === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY));
|
||||
this.disposeWithMe(combineLatest([this._formulaEditorManagerService.position$, addFormulaBar$]).subscribe(([position]) => {
|
||||
if (!position) return this._clearScheduledCallback();
|
||||
const editorObject = getEditorObject(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, this._renderManagerService);
|
||||
const formulaEditorDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
UniverInstanceType.UNIVER_DOC
|
||||
);
|
||||
|
||||
if (editorObject == null || formulaEditorDataModel == null) return this._clearScheduledCallback();
|
||||
|
||||
const { width, height } = position;
|
||||
if (width === 0 || height === 0) return this._clearScheduledCallback();
|
||||
|
||||
const { engine } = editorObject;
|
||||
formulaEditorDataModel.updateDocumentDataPageSize(width);
|
||||
this.autoScroll();
|
||||
this._scheduledCallback = requestIdleCallback(() => engine.resizeBySize(width, height));
|
||||
}));
|
||||
}
|
||||
|
||||
private _scheduledCallback: number = -1;
|
||||
private _clearScheduledCallback(): void {
|
||||
if (this._scheduledCallback !== -1) cancelIdleCallback(this._scheduledCallback);
|
||||
this._scheduledCallback = -1;
|
||||
}
|
||||
|
||||
autoScroll() {
|
||||
const position = this._formulaEditorManagerService.getPosition();
|
||||
|
||||
const skeleton = this._renderManagerService.getRenderUnitById(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY)?.with(DocSkeletonManagerService).getSkeleton();
|
||||
const editorObject = this._renderManagerService.getRenderUnitById(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
|
||||
const formulaEditorDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
UniverInstanceType.UNIVER_DOC
|
||||
);
|
||||
|
||||
if (skeleton == null || position == null || editorObject == null || formulaEditorDataModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { marginTop = 0, marginBottom = 0 } = formulaEditorDataModel.getSnapshot().documentStyle;
|
||||
|
||||
const { scene, mainComponent } = editorObject;
|
||||
|
||||
let { actualHeight } = skeleton.getActualSize();
|
||||
// page actual height also need to include page margin top and margin bottom.
|
||||
actualHeight += marginTop + marginBottom;
|
||||
|
||||
const { width, height } = position;
|
||||
const viewportMain = scene.getViewport(VIEWPORT_KEY.VIEW_MAIN);
|
||||
let scrollBar = viewportMain?.getScrollBar() as Nullable<ScrollBar>;
|
||||
|
||||
scene.transformByState({
|
||||
width,
|
||||
height: actualHeight,
|
||||
});
|
||||
|
||||
mainComponent?.resize(width, actualHeight);
|
||||
|
||||
if (actualHeight > height) {
|
||||
if (scrollBar == null) {
|
||||
viewportMain && new ScrollBar(viewportMain, { enableHorizontal: false, barSize: 8 });
|
||||
} else {
|
||||
viewportMain?.resetCanvasSizeAndUpdateScroll();
|
||||
}
|
||||
} else {
|
||||
scrollBar = null;
|
||||
viewportMain?.scrollToViewportPos({ viewportScrollX: 0, viewportScrollY: 0 });
|
||||
viewportMain?.getScrollBar()?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const EMBEDDING_FORMULA_EDITOR = 'embedding_formula_editor';
|
||||
|
||||
export const isEmbeddingFormulaEditor = (unitId: string) => {
|
||||
return unitId.includes(EMBEDDING_FORMULA_EDITOR);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const RANGE_SELECTOR_SYMBOLS = 'range_selector';
|
||||
|
||||
export const isRangeSelector = (unitId: string) => {
|
||||
return unitId.includes(RANGE_SELECTOR_SYMBOLS);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 { Nullable, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import type { IUniverSheetsUIConfig } from '../config/config';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
CellValueType,
|
||||
Disposable,
|
||||
getNumfmtParseValueFilter,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
Inject,
|
||||
isRealNum,
|
||||
isTextFormat,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { SheetsSelectionsService, TextToNumberCommand } from '@univerjs/sheets';
|
||||
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { CellAlertManagerService, CellAlertType } from '../services/cell-alert-manager.service';
|
||||
|
||||
const ALERT_KEY = 'SHEET_FORCE_STRING_ALERT';
|
||||
|
||||
export class ForceStringAlertRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
|
||||
@Inject(CellAlertManagerService) private readonly _cellAlertManagerService: CellAlertManagerService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._initCellAlertPopup();
|
||||
}
|
||||
|
||||
private _initCellAlertPopup() {
|
||||
this.disposeWithMe(this._selectionManagerService.selectionChanged$.subscribe((selections) => this._updateAlert(selections)));
|
||||
this.disposeWithMe(this._context.unit.activeSheet$.subscribe(() => this._updateAlert(this._selectionManagerService.getCurrentSelections())));
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET).subscribe(() =>
|
||||
this._updateAlert(this._selectionManagerService.getCurrentSelections())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _isCurrentSheet(worksheet: Nullable<Worksheet>): worksheet is Worksheet {
|
||||
const selectionParam = this._selectionManagerService.currentSelectionParam;
|
||||
return Boolean(
|
||||
worksheet &&
|
||||
selectionParam?.unitId === this._context.unit.getUnitId() &&
|
||||
selectionParam.sheetId === worksheet.getSheetId()
|
||||
);
|
||||
}
|
||||
|
||||
private _updateAlert(selections: Nullable<Readonly<ISelectionWithStyle[]>>) {
|
||||
this._hideAlert();
|
||||
const workbook = this._context.unit;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!this._isCurrentSheet(worksheet)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selections?.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._configService.getConfig<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY)?.disableForceStringAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = selections[0];
|
||||
const primary = selection.primary;
|
||||
if (!primary) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = primary.actualRow;
|
||||
const col = primary.actualColumn;
|
||||
const cellData = worksheet.getCell(row, col);
|
||||
if (!cellData || cellData.v === null || cellData.v === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cellStyle = workbook.getStyles().get(cellData.s);
|
||||
if (isTextFormat(cellStyle?.n?.pattern)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(cellData.t !== CellValueType.FORCE_STRING && cellData.t !== CellValueType.STRING) ||
|
||||
(!isRealNum(cellData.v) && !(typeof cellData.v === 'string' && getNumfmtParseValueFilter(cellData.v)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
const range = { ...selection.range };
|
||||
this._cellAlertManagerService.showAlert({
|
||||
type: CellAlertType.WARNING,
|
||||
title: this._localeService.t<LocaleKey>('sheets-ui.info.error'),
|
||||
message: this._localeService.t<LocaleKey>('sheets-ui.info.forceStringInfo'),
|
||||
location: { unitId, subUnitId, row, col },
|
||||
width: 200,
|
||||
height: 74,
|
||||
key: this._alertKey,
|
||||
menu: [{
|
||||
label: this._localeService.t<LocaleKey>('sheets-ui.info.convertToNumber'),
|
||||
onSelect: () => {
|
||||
const result = this._commandService.syncExecuteCommand(TextToNumberCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: [range],
|
||||
});
|
||||
if (result) {
|
||||
this._hideAlert();
|
||||
}
|
||||
},
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
private get _alertKey() {
|
||||
return `${ALERT_KEY}:${this._context.unit.getUnitId()}`;
|
||||
}
|
||||
|
||||
private _hideAlert() {
|
||||
this._cellAlertManagerService.removeAlert(this._alertKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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 { Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { IUniverSheetsUIConfig } from '../config/config';
|
||||
import { CellValueType, getNumfmtParseValueFilter, IConfigService, Inject, InterceptorEffectEnum, isRealNum, isTextFormat, RxDisposable } from '@univerjs/core';
|
||||
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { SHEETS_UI_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { SheetSkeletonManagerService } from '../services/sheet-skeleton-manager.service';
|
||||
|
||||
export class ForceStringRenderController extends RxDisposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initViewModelIntercept();
|
||||
}
|
||||
|
||||
private _initViewModelIntercept() {
|
||||
const FORCE_STRING_MARK = {
|
||||
tl: {
|
||||
size: 6,
|
||||
color: '#409f11',
|
||||
},
|
||||
};
|
||||
|
||||
this.disposeWithMe(
|
||||
this._sheetInterceptorService.intercept(
|
||||
INTERCEPTOR_POINT.CELL_CONTENT,
|
||||
{
|
||||
priority: 10,
|
||||
effect: InterceptorEffectEnum.Style,
|
||||
handler: (cell, pos, next) => {
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentParam()?.skeleton;
|
||||
if (!skeleton) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
const cellRaw = pos.rawData;
|
||||
|
||||
if (!cellRaw || cellRaw.v === null || cellRaw.v === undefined) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the cell type is string or force string, and the value is a pure number or a string that can be converted to a number, show the force string mark.
|
||||
* '123 -> yes
|
||||
* '20% -> yes
|
||||
* '1,234.56 -> yes
|
||||
* 'abc -> no
|
||||
* '2025-09-17 -> no
|
||||
*/
|
||||
if (
|
||||
(cell?.t === CellValueType.FORCE_STRING || cell?.t === CellValueType.STRING) &&
|
||||
(isRealNum(cellRaw.v) || (typeof cellRaw.v === 'string' && getNumfmtParseValueFilter(cellRaw.v)))
|
||||
) {
|
||||
// If the cell is in text format, follow the logic of number format
|
||||
const cellStyle = pos.workbook.getStyles().get(cellRaw.s);
|
||||
if (isTextFormat(cellStyle?.n?.pattern)) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
// If the user has disabled the force string mark, do not show it
|
||||
if (this._configService.getConfig<IUniverSheetsUIConfig>(SHEETS_UI_PLUGIN_CONFIG_KEY)?.disableForceStringMark) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
if (cell === cellRaw) {
|
||||
cell = { ...cellRaw };
|
||||
}
|
||||
|
||||
cell.markers = { ...cell?.markers, ...FORCE_STRING_MARK };
|
||||
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
return next(cell);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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 { ICellData, IMutationInfo, IRange, IStyleData, Workbook } from '@univerjs/core';
|
||||
import type { IAddWorksheetMergeMutationParams, IRemoveWorksheetMergeMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import type { IFormatPainterHook, ISelectionFormatInfo } from '../../services/format-painter/format-painter.service';
|
||||
import {
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
RANGE_TYPE,
|
||||
Rectangle,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
AddMergeUndoMutationFactory,
|
||||
AddWorksheetMergeMutation,
|
||||
getAddMergeMutationRangeByType,
|
||||
getClearContentMutationParamsForRanges,
|
||||
RemoveMergeUndoMutationFactory,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SelectionMoveType,
|
||||
SetRangeValuesCommand,
|
||||
SetRangeValuesMutation,
|
||||
SetRangeValuesUndoMutationFactory,
|
||||
SetSelectionsOperation,
|
||||
SetStyleCommand,
|
||||
SheetInterceptorService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { checkCellContentInRanges } from '../../common/utils';
|
||||
import { FormatPainterStatus, IFormatPainterService } from '../../services/format-painter/format-painter.service';
|
||||
|
||||
export class FormatPainterController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IFormatPainterService private readonly _formatPainterService: IFormatPainterService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(Injector) private readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._addDefaultHook();
|
||||
}
|
||||
|
||||
private _addDefaultHook() {
|
||||
const defaultHook: IFormatPainterHook = {
|
||||
id: 'default-format-painter',
|
||||
priority: 0,
|
||||
isDefaultHook: true,
|
||||
onStatusChange: (status: FormatPainterStatus) => {
|
||||
if (status !== FormatPainterStatus.OFF) {
|
||||
const format = this._collectSelectionRangeFormat();
|
||||
if (format) {
|
||||
this._formatPainterService.setSelectionFormat(format);
|
||||
}
|
||||
}
|
||||
},
|
||||
onApply: (unitId, subUnitId, range, format) => {
|
||||
return this._getUndoRedoMutationInfo(unitId, subUnitId, range, format);
|
||||
},
|
||||
};
|
||||
this._formatPainterService.addHook(defaultHook);
|
||||
}
|
||||
|
||||
private _collectSelectionRangeFormat() {
|
||||
const selection = this._selectionManagerService.getCurrentLastSelection();
|
||||
const range = selection?.range;
|
||||
if (!range) return null;
|
||||
const { startRow, endRow, startColumn, endColumn } = range;
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
if (!worksheet) return null;
|
||||
const cellData = worksheet.getCellMatrix();
|
||||
|
||||
const styles = workbook.getStyles();
|
||||
const stylesMatrix = new ObjectMatrix<IStyleData>();
|
||||
const merges = [];
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
for (let c = startColumn; c <= endColumn; c++) {
|
||||
const cell = cellData.getValue(r, c) as ICellData;
|
||||
stylesMatrix.setValue(r, c, styles.getStyleByCell(cell) || {});
|
||||
const { isMergedMainCell, ...mergeInfo } = worksheet.getCellInfoInMergeData(r, c);
|
||||
if (isMergedMainCell) {
|
||||
merges.push({
|
||||
startRow: mergeInfo.startRow,
|
||||
startColumn: mergeInfo.startColumn,
|
||||
endRow: mergeInfo.endRow,
|
||||
endColumn: mergeInfo.endColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
styles: stylesMatrix,
|
||||
merges,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _getUndoRedoMutationInfo(unitId: string, subUnitId: string, originRange: IRange, format: ISelectionFormatInfo) {
|
||||
const sheetInterceptorService = this._sheetInterceptorService;
|
||||
const univerInstanceService = this._univerInstanceService;
|
||||
|
||||
const { merges, styles: stylesMatrix } = format;
|
||||
if (!stylesMatrix) return { undos: [], redos: [] };
|
||||
|
||||
const { startRow, startColumn, endRow, endColumn } = stylesMatrix.getDataRange();
|
||||
const styleRowsNum = endRow - startRow + 1;
|
||||
const styleColsNum = endColumn - startColumn + 1;
|
||||
const range = (originRange.startRow === originRange.endRow && originRange.startColumn === originRange.endColumn)
|
||||
? {
|
||||
startRow: originRange.startRow,
|
||||
startColumn: originRange.startColumn,
|
||||
endRow: originRange.startRow + styleRowsNum - 1,
|
||||
endColumn: originRange.startColumn + styleColsNum - 1,
|
||||
}
|
||||
: originRange;
|
||||
const styleValues: ICellData[][] = Array.from({ length: range.endRow - range.startRow + 1 }, () =>
|
||||
Array.from({ length: range.endColumn - range.startColumn + 1 }, () => ({})));
|
||||
const mergeRanges: IRange[] = [];
|
||||
|
||||
styleValues.forEach((row, rowIndex) => {
|
||||
row.forEach((col, colIndex) => {
|
||||
const mappedRowIndex = (rowIndex % styleRowsNum) + startRow;
|
||||
const mappedColIndex = (colIndex % styleColsNum) + startColumn;
|
||||
const style = stylesMatrix.getValue(mappedRowIndex, mappedColIndex);
|
||||
|
||||
if (style) {
|
||||
styleValues[rowIndex][colIndex].s = Object.keys(style).length > 0 ? style : null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// whether the primary cell is in a merge range
|
||||
let primaryCellMergeRows = 0;
|
||||
let primaryCellMergeCols = 0;
|
||||
|
||||
merges.forEach((merge) => {
|
||||
if (merge.startRow === startRow && merge.startColumn === startColumn) {
|
||||
primaryCellMergeRows = merge.endRow - merge.startRow;
|
||||
primaryCellMergeCols = merge.endColumn - merge.startColumn;
|
||||
}
|
||||
|
||||
const relatedRange: IRange = {
|
||||
startRow: merge.startRow - startRow,
|
||||
startColumn: merge.startColumn - startColumn,
|
||||
endRow: merge.endRow - startRow,
|
||||
endColumn: merge.endColumn - startColumn,
|
||||
};
|
||||
// merge will apply at least once
|
||||
const rowRepeats = Math.max(1, Math.floor((range.endRow - range.startRow + 1) / styleRowsNum));
|
||||
const colRepeats = Math.max(1, Math.floor((range.endColumn - range.startColumn + 1) / styleColsNum));
|
||||
for (let i = 0; i < rowRepeats; i++) {
|
||||
for (let j = 0; j < colRepeats; j++) {
|
||||
mergeRanges.push({
|
||||
startRow: relatedRange.startRow + i * styleRowsNum + range.startRow,
|
||||
startColumn: relatedRange.startColumn + j * styleColsNum + range.startColumn,
|
||||
endRow: relatedRange.endRow + i * styleRowsNum + range.startRow,
|
||||
endColumn: relatedRange.endColumn + j * styleColsNum + range.startColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
const currentSelections = [range];
|
||||
const clearCellValue = new ObjectMatrix<ICellData>();
|
||||
const cellValue = new ObjectMatrix<ICellData>();
|
||||
|
||||
if (Tools.isArray(styleValues)) {
|
||||
for (let i = 0; i < currentSelections.length; i++) {
|
||||
const { startRow, startColumn, endRow, endColumn } = currentSelections[i];
|
||||
for (let r = 0; r <= endRow - startRow; r++) {
|
||||
for (let c = 0; c <= endColumn - startColumn; c++) {
|
||||
clearCellValue.setValue(r + startRow, c + startColumn, { s: null });
|
||||
cellValue.setValue(r + startRow, c + startColumn, styleValues[r][c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearStyleMutationParams: ISetRangeValuesMutationParams = {
|
||||
subUnitId,
|
||||
unitId,
|
||||
trigger: SetStyleCommand.id,
|
||||
cellValue: clearCellValue.getMatrix(),
|
||||
};
|
||||
const setRangeValuesMutationParams: ISetRangeValuesMutationParams = {
|
||||
subUnitId,
|
||||
unitId,
|
||||
trigger: SetStyleCommand.id,
|
||||
cellValue: cellValue.getMatrix(),
|
||||
};
|
||||
const undoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = this._injector.invoke(
|
||||
SetRangeValuesUndoMutationFactory,
|
||||
clearStyleMutationParams
|
||||
);
|
||||
|
||||
const { undos: interceptorUndos, redos: interceptorRedos } = sheetInterceptorService.onCommandExecute({
|
||||
id: SetRangeValuesCommand.id,
|
||||
params: { ...setRangeValuesMutationParams, range: currentSelections },
|
||||
});
|
||||
|
||||
const mergeRedos: IMutationInfo[] = [];
|
||||
const mergeUndos: IMutationInfo[] = [];
|
||||
|
||||
// handle merge
|
||||
if (mergeRanges.length > 0) {
|
||||
const ranges = getAddMergeMutationRangeByType(mergeRanges);
|
||||
|
||||
// First we should check if there are values in the going-to-be-merged cells.
|
||||
const worksheet = (univerInstanceService.getUnit(unitId) as Workbook).getSheetBySheetId(subUnitId)!;
|
||||
const willRemoveSomeCell = checkCellContentInRanges(worksheet, ranges);
|
||||
|
||||
// prepare redo mutations
|
||||
const removeMergeMutationParams: IRemoveWorksheetMergeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges,
|
||||
};
|
||||
const addMergeMutationParams: IAddWorksheetMergeMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges,
|
||||
};
|
||||
mergeRedos.push({ id: RemoveWorksheetMergeMutation.id, params: removeMergeMutationParams });
|
||||
mergeRedos.push({ id: AddWorksheetMergeMutation.id, params: addMergeMutationParams });
|
||||
|
||||
// prepare undo mutations
|
||||
const undoRemoveMergeMutationParams = this._injector.invoke(
|
||||
RemoveMergeUndoMutationFactory,
|
||||
removeMergeMutationParams
|
||||
);
|
||||
const undoMutationParams = this._injector.invoke(
|
||||
AddMergeUndoMutationFactory,
|
||||
addMergeMutationParams
|
||||
);
|
||||
mergeUndos.push({ id: RemoveWorksheetMergeMutation.id, params: undoMutationParams });
|
||||
mergeUndos.push({ id: AddWorksheetMergeMutation.id, params: undoRemoveMergeMutationParams });
|
||||
|
||||
// add set range values mutations to undo redo mutations
|
||||
if (willRemoveSomeCell) {
|
||||
const data = this._injector.invoke((accessor) => getClearContentMutationParamsForRanges(accessor, unitId, worksheet, ranges));
|
||||
mergeRedos.unshift(...data.redos);
|
||||
mergeUndos.push(...data.undos);
|
||||
}
|
||||
}
|
||||
|
||||
// if the range changed, need to reset selections
|
||||
if (!Rectangle.equals(range, originRange)) {
|
||||
const primary = {
|
||||
actualRow: range.startRow,
|
||||
actualColumn: range.startColumn,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
isMerged: primaryCellMergeRows > 0 || primaryCellMergeCols > 0,
|
||||
isMergedMainCell: false,
|
||||
startRow: range.startRow,
|
||||
startColumn: range.startColumn,
|
||||
endRow: range.startRow + primaryCellMergeRows,
|
||||
endColumn: range.startColumn + primaryCellMergeCols,
|
||||
};
|
||||
mergeRedos.push({
|
||||
id: SetSelectionsOperation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
type: SelectionMoveType.ONLY_SET,
|
||||
selections: [{ range, primary, style: null }],
|
||||
},
|
||||
});
|
||||
mergeUndos.unshift({
|
||||
id: SetSelectionsOperation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
type: SelectionMoveType.ONLY_SET,
|
||||
selections: [{ range: originRange, primary, style: null }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
undos: [
|
||||
{ id: SetRangeValuesMutation.id, params: clearStyleMutationParams },
|
||||
{ id: SetRangeValuesMutation.id, params: undoSetRangeValuesMutationParams },
|
||||
...interceptorUndos,
|
||||
...mergeUndos,
|
||||
],
|
||||
redos: [
|
||||
{ id: SetRangeValuesMutation.id, params: clearStyleMutationParams },
|
||||
{ id: SetRangeValuesMutation.id, params: setRangeValuesMutationParams },
|
||||
...interceptorRedos,
|
||||
...mergeRedos,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user