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:
41
packages/sheets-formula-ui/README.md
Normal file
41
packages/sheets-formula-ui/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/sheets-formula-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-formula-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-formula-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-formula-ui)
|
||||
|
||||
`@univerjs/sheets-formula-ui` adds formula editing UI for Univer Sheets, including formula input, suggestions, highlighting, and range selection.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-formula-ui` | `UniverSheetsFormulaUi` | Yes | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-formula-ui
|
||||
# or
|
||||
npm install @univerjs/sheets-formula-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/sheets-formula-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/sheets-formula-ui/locale/en-US';
|
||||
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
|
||||
|
||||
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
||||
|
||||
// Merge EnUS into your Univer locale map when this package contributes UI text.
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-formula-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
111
packages/sheets-formula-ui/package.json
Normal file
111
packages/sheets-formula-ui/package.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-formula-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Formula editing UI 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",
|
||||
"formula",
|
||||
"formula-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",
|
||||
"react-dom": "^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/sheets": "workspace:*",
|
||||
"@univerjs/sheets-formula": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/sheets-formula-ui/postcss.config.mjs
Normal file
1
packages/sheets-formula-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -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 { IAccessor } from '@univerjs/core';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { ISheetClipboardService, PREDEFINED_HOOK_NAME_COPY, PREDEFINED_HOOK_NAME_PASTE, SheetPasteCommand } from '@univerjs/sheets-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetCopyFormulaOnlyCommand, SheetOnlyPasteFormulaCommand } from '../formula-clipboard.command';
|
||||
|
||||
describe('formula clipboard commands', () => {
|
||||
it('copies only formulas through the clipboard hook', async () => {
|
||||
const sheetClipboardService = { copy: vi.fn(() => true) };
|
||||
const accessor = {
|
||||
get: (token: unknown) => {
|
||||
if (token === ISheetClipboardService) return sheetClipboardService;
|
||||
throw new Error(`Unknown dependency: ${String(token)}`);
|
||||
},
|
||||
} as IAccessor;
|
||||
|
||||
await expect(SheetCopyFormulaOnlyCommand.handler(accessor)).resolves.toBe(true);
|
||||
expect(sheetClipboardService.copy).toHaveBeenCalledWith({
|
||||
copyHookType: PREDEFINED_HOOK_NAME_COPY.SPECIAL_COPY_FORMULA_ONLY,
|
||||
});
|
||||
});
|
||||
|
||||
it('pastes only formulas through the paste command', async () => {
|
||||
const commandService = { executeCommand: vi.fn(() => true) };
|
||||
const accessor = {
|
||||
get: (token: unknown) => {
|
||||
if (token === ICommandService) return commandService;
|
||||
throw new Error(`Unknown dependency: ${String(token)}`);
|
||||
},
|
||||
} as IAccessor;
|
||||
|
||||
await expect(SheetOnlyPasteFormulaCommand.handler(accessor)).resolves.toBe(true);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import { ISheetClipboardService, PREDEFINED_HOOK_NAME_COPY, PREDEFINED_HOOK_NAME_PASTE, SheetPasteCommand } from '@univerjs/sheets-ui';
|
||||
|
||||
export const SheetCopyFormulaOnlyCommand: ICommand = {
|
||||
id: 'sheet.command.copy-formula-only',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const sheetClipboardService = accessor.get(ISheetClipboardService);
|
||||
return sheetClipboardService.copy({
|
||||
copyHookType: PREDEFINED_HOOK_NAME_COPY.SPECIAL_COPY_FORMULA_ONLY,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SheetOnlyPasteFormulaCommand: ICommand = {
|
||||
id: 'sheet.command.paste-formula',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(SheetPasteCommand.id, {
|
||||
value: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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 { Dependency, IWorkbookData } from '@univerjs/core';
|
||||
import { AuthzIoLocalService, IAuthzIoService, ILogService, Inject, Injector, IUniverInstanceService, LocaleType, LogLevel, Plugin, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { EditorService, IEditorService } from '@univerjs/docs-ui';
|
||||
import { LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { IRefSelectionsService, RangeProtectionRuleModel, RefSelectionsService, SheetInterceptorService, SheetSkeletonService, SheetsSelectionsService, WorkbookPermissionService, WorksheetPermissionService, WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '@univerjs/sheets';
|
||||
import { EditorBridgeService, IEditorBridgeService, ISheetSelectionRenderService, SheetSelectionRenderService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { FormulaPromptService, IFormulaPromptService } from '../../../services/prompt.service';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO = (): IWorkbookData => ({
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
1: {
|
||||
1: {
|
||||
v: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
});
|
||||
|
||||
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
const overrideTokens = new Set(dependencies?.map((dependency) => Array.isArray(dependency) ? dependency[0] : dependency));
|
||||
|
||||
const addDependency = (dependency: Dependency) => {
|
||||
const token = Array.isArray(dependency) ? dependency[0] : dependency;
|
||||
|
||||
if (overrideTokens.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
injector.add(dependency);
|
||||
};
|
||||
|
||||
/**
|
||||
* This plugin hooks into Sheet's DI system to expose API to test scripts
|
||||
*/
|
||||
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;
|
||||
addDependency([ISheetSelectionRenderService, { useClass: SheetSelectionRenderService }]);
|
||||
addDependency([SheetsSelectionsService]);
|
||||
addDependency([SheetSkeletonService]);
|
||||
addDependency([LexerTreeBuilder]);
|
||||
addDependency([DocSelectionManagerService]);
|
||||
addDependency([IFormulaPromptService, { useClass: FormulaPromptService }]);
|
||||
addDependency([IEditorBridgeService, { useClass: EditorBridgeService }]);
|
||||
addDependency([IEditorService, { useClass: EditorService }]);
|
||||
addDependency([SheetSkeletonManagerService]);
|
||||
addDependency([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
addDependency([SheetInterceptorService]);
|
||||
addDependency([WorksheetPermissionService]);
|
||||
addDependency([WorksheetProtectionPointModel]);
|
||||
addDependency([WorkbookPermissionService]);
|
||||
addDependency([RangeProtectionRuleModel]);
|
||||
addDependency([IAuthzIoService, { useClass: AuthzIoLocalService }]);
|
||||
addDependency([WorksheetProtectionRuleModel]);
|
||||
addDependency([IRefSelectionsService, { useClass: RefSelectionsService }]);
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
this._injector.get(WorkbookPermissionService);
|
||||
this._injector.get(WorksheetPermissionService);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit(UniverInstanceType.UNIVER_SHEET, workbookData || TEST_WORKBOOK_DATA_DEMO());
|
||||
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
const logService = get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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, IDocumentData } from '@univerjs/core';
|
||||
import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IFormulaPromptService } from '../../../services/prompt.service';
|
||||
import { MORE_FUNCTIONS_COMPONENT } from '../../../views/more-functions/interface';
|
||||
import { HelpFunctionOperation } from '../help-function.operation';
|
||||
import { MoreFunctionsOperation } from '../more-functions.operation';
|
||||
import { ReferenceAbsoluteOperation } from '../reference-absolute.operation';
|
||||
import { SearchFunctionOperation } from '../search-function.operation';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
function createAccessor() {
|
||||
const promptService = { help: vi.fn(), search: vi.fn() };
|
||||
const sidebarService = { open: vi.fn() };
|
||||
const accessor = {
|
||||
get(token: unknown) {
|
||||
if (token === IFormulaPromptService) {
|
||||
return promptService;
|
||||
}
|
||||
|
||||
if (token === ISidebarService) {
|
||||
return sidebarService;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown dependency: ${String(token)}`);
|
||||
},
|
||||
} as IAccessor;
|
||||
|
||||
return { accessor, promptService, sidebarService };
|
||||
}
|
||||
|
||||
class TestFormulaEditor {
|
||||
readonly selectionRanges: Array<{ startOffset: number; endOffset: number; collapsed: boolean }> = [];
|
||||
private _editorId = DOCS_NORMAL_EDITOR_UNIT_ID_KEY;
|
||||
private _documentData: IDocumentData = {
|
||||
id: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
body: {
|
||||
dataStream: '=A1\r\n',
|
||||
},
|
||||
documentStyle: {},
|
||||
};
|
||||
|
||||
setEditorId(editorId: string) {
|
||||
this._editorId = editorId;
|
||||
}
|
||||
|
||||
setFormulaText(formulaText: string) {
|
||||
this._documentData = {
|
||||
...this._documentData,
|
||||
body: {
|
||||
dataStream: `${formulaText}\r\n`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getEditorId() {
|
||||
return this._editorId;
|
||||
}
|
||||
|
||||
getDocumentData() {
|
||||
return this._documentData;
|
||||
}
|
||||
|
||||
replaceText(text: string, ranges: Array<{ startOffset: number; endOffset: number; collapsed: boolean }>) {
|
||||
this.setFormulaText(text);
|
||||
this.selectionRanges.length = 0;
|
||||
this.selectionRanges.push(...ranges);
|
||||
}
|
||||
}
|
||||
|
||||
class TestEditorService {
|
||||
readonly editor = new TestFormulaEditor();
|
||||
private _hasFocusEditor = true;
|
||||
|
||||
setHasFocusEditor(hasFocusEditor: boolean) {
|
||||
this._hasFocusEditor = hasFocusEditor;
|
||||
}
|
||||
|
||||
getFocusEditor() {
|
||||
return this._hasFocusEditor ? this.editor : null;
|
||||
}
|
||||
|
||||
getEditor() {
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
register() {
|
||||
return { dispose: () => {} };
|
||||
}
|
||||
|
||||
getAllEditor() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
isEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getEditorRenderConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
isSheetEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
blur$ = { subscribe: () => ({ unsubscribe: () => {} }) };
|
||||
focus$ = { subscribe: () => ({ unsubscribe: () => {} }) };
|
||||
blur() {}
|
||||
focus() {}
|
||||
getFocusId() {
|
||||
return DOCS_NORMAL_EDITOR_UNIT_ID_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
describe('formula UI operations', () => {
|
||||
it('delegates help and search actions to the prompt service', async () => {
|
||||
const { accessor, promptService } = createAccessor();
|
||||
|
||||
await expect(HelpFunctionOperation.handler(accessor, { functionName: 'SUM' } as never)).resolves.toBe(true);
|
||||
await expect(SearchFunctionOperation.handler(accessor, { searchText: 'count' } as never)).resolves.toBe(true);
|
||||
|
||||
expect(promptService.help).toHaveBeenCalledWith({ functionName: 'SUM' });
|
||||
expect(promptService.search).toHaveBeenCalledWith({ searchText: 'count' });
|
||||
});
|
||||
|
||||
it('opens the more-functions sidebar with the expected component', async () => {
|
||||
const { accessor, sidebarService } = createAccessor();
|
||||
|
||||
await expect(MoreFunctionsOperation.handler(accessor)).resolves.toBe(true);
|
||||
expect(sidebarService.open).toHaveBeenCalledWith({
|
||||
header: { title: 'sheets-formula-ui.insert.tooltip' },
|
||||
children: { label: MORE_FUNCTIONS_COMPONENT },
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles the reference under the formula editor caret to the next absolute-reference state', async () => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[IEditorService, { useClass: TestEditorService as never }],
|
||||
]);
|
||||
const commandService = testBed.get(ICommandService);
|
||||
commandService.registerCommand(ReferenceAbsoluteOperation);
|
||||
const selectionManager = testBed.get(DocSelectionManagerService);
|
||||
selectionManager.__TEST_ONLY_setCurrentSelection({
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
selectionManager.__TEST_ONLY_add([{
|
||||
startOffset: '=A1'.length,
|
||||
endOffset: '=A1'.length,
|
||||
collapsed: true,
|
||||
isActive: true,
|
||||
segmentId: '',
|
||||
}]);
|
||||
const editorService = testBed.get(IEditorService) as unknown as TestEditorService;
|
||||
|
||||
expect(await commandService.executeCommand(ReferenceAbsoluteOperation.id)).toBe(true);
|
||||
|
||||
expect(editorService.editor.getDocumentData().body?.dataStream).toBe('=$A$1\r\n');
|
||||
expect(editorService.editor.selectionRanges).toEqual([{
|
||||
startOffset: '=$A$1'.length,
|
||||
endOffset: '=$A$1'.length,
|
||||
collapsed: true,
|
||||
}]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('does not change formula text when there is no focused formula editor or no reference at the caret', async () => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[IEditorService, { useClass: TestEditorService as never }],
|
||||
]);
|
||||
const commandService = testBed.get(ICommandService);
|
||||
commandService.registerCommand(ReferenceAbsoluteOperation);
|
||||
const selectionManager = testBed.get(DocSelectionManagerService);
|
||||
selectionManager.__TEST_ONLY_setCurrentSelection({
|
||||
unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
});
|
||||
selectionManager.__TEST_ONLY_add([{
|
||||
startOffset: '=SUM('.length,
|
||||
endOffset: '=SUM('.length,
|
||||
collapsed: true,
|
||||
isActive: true,
|
||||
segmentId: '',
|
||||
}]);
|
||||
const editorService = testBed.get(IEditorService) as unknown as TestEditorService;
|
||||
editorService.editor.setFormulaText('=SUM(1, 2)');
|
||||
|
||||
expect(await commandService.executeCommand(ReferenceAbsoluteOperation.id)).toBe(false);
|
||||
expect(editorService.editor.getDocumentData().body?.dataStream).toBe('=SUM(1, 2)\r\n');
|
||||
|
||||
editorService.setHasFocusEditor(false);
|
||||
expect(await commandService.executeCommand(ReferenceAbsoluteOperation.id)).toBe(false);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 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, Nullable, Univer, Workbook } from '@univerjs/core';
|
||||
import type { IInsertFunctionOperationParams } from '../insert-function.operation';
|
||||
import {
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import {
|
||||
SetRangeValuesCommand,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { InsertFunctionCommand } from '@univerjs/sheets-formula';
|
||||
import { IEditorBridgeService, SetCellEditVisibleOperation } from '@univerjs/sheets-ui';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
InsertFunctionOperation,
|
||||
isMultiRowsColumnsRange,
|
||||
isNumberCell,
|
||||
isSingleCell,
|
||||
} from '../insert-function.operation';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
describe('Test insert function operation', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let editorService: IEditorService;
|
||||
|
||||
class TestEditorService {
|
||||
getEditor = vi.fn(() => null);
|
||||
register = vi.fn();
|
||||
getAllEditor = vi.fn(() => new Map());
|
||||
isEditor = vi.fn(() => false);
|
||||
isSheetEditor = vi.fn(() => false);
|
||||
blur$ = { subscribe: vi.fn() };
|
||||
blur = vi.fn();
|
||||
focus$ = { subscribe: vi.fn() };
|
||||
focus = vi.fn();
|
||||
getFocusId = vi.fn(() => null);
|
||||
getFocusEditor = vi.fn(() => null);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(undefined, [[IEditorService, { useClass: TestEditorService as never }]]);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
editorService = get(IEditorService);
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(InsertFunctionOperation);
|
||||
commandService.registerCommand(InsertFunctionCommand);
|
||||
commandService.registerCommand(SetRangeValuesCommand);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(SetCellEditVisibleOperation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('insert function', () => {
|
||||
describe('correct situations', () => {
|
||||
it('insert function, match the data range above, directly set values', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// B3:B4
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 2, startColumn: 1, endRow: 3, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getValues() {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(2, 1, 3, 1)
|
||||
.getValues();
|
||||
}
|
||||
const params: IInsertFunctionOperationParams = {
|
||||
value: 'SUM',
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertFunctionOperation.id, params)).toBeTruthy();
|
||||
let values = getValues();
|
||||
expect(values?.[0]?.[0]?.f).toStrictEqual('=SUM(B2)');
|
||||
expect(values?.[0]?.[0]?.si).toStrictEqual(values?.[1]?.[0]?.si);
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getValues()).toStrictEqual([[null], [null]]);
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues();
|
||||
expect(values?.[0]?.[0]?.f).toStrictEqual('=SUM(B2)');
|
||||
expect(values?.[0]?.[0]?.si).toStrictEqual(values?.[1]?.[0]?.si);
|
||||
});
|
||||
|
||||
it('insert function, match the data range left, directly set values', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// C2:D2
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: 2, endRow: 1, endColumn: 3, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
function getValues() {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(1, 2, 1, 3)
|
||||
.getValues();
|
||||
}
|
||||
const params: IInsertFunctionOperationParams = {
|
||||
value: 'SUM',
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertFunctionOperation.id, params)).toBeTruthy();
|
||||
let values = getValues();
|
||||
expect(values?.[0]?.[0]?.f).toStrictEqual('=SUM(B2)');
|
||||
expect(values?.[0]?.[0]?.si).toStrictEqual(values?.[0]?.[1]?.si);
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getValues()).toStrictEqual([[null, null]]);
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues();
|
||||
expect(values?.[0]?.[0]?.f).toStrictEqual('=SUM(B2)');
|
||||
expect(values?.[0]?.[0]?.si).toStrictEqual(values?.[0]?.[1]?.si);
|
||||
});
|
||||
|
||||
it('insert function, the range cell has number value', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
const range = {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 4,
|
||||
endColumn: 3,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
};
|
||||
// A2:D5
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range,
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
const params: IInsertFunctionOperationParams = {
|
||||
value: 'SUM',
|
||||
};
|
||||
const cellMatrix = new ObjectMatrix<Nullable<ICellData>>({
|
||||
1: {
|
||||
0: { v: 1, t: 2 },
|
||||
1: { v: 2, t: 2 },
|
||||
2: { v: 3, t: 2 },
|
||||
},
|
||||
2: {
|
||||
0: { v: 2, t: 2 },
|
||||
1: { v: 3, t: 2 },
|
||||
3: { v: 5, t: 2 },
|
||||
},
|
||||
3: {
|
||||
0: { v: 3, t: 2 },
|
||||
2: { v: 5, t: 2 },
|
||||
3: { v: 6, t: 2 },
|
||||
},
|
||||
4: {
|
||||
1: { v: 5, t: 2 },
|
||||
2: { v: 6, t: 2 },
|
||||
3: { v: 7, t: 2 },
|
||||
},
|
||||
});
|
||||
await commandService.executeCommand(SetRangeValuesCommand.id, {
|
||||
value: cellMatrix.clone(),
|
||||
sheetId: 'sheet1',
|
||||
range,
|
||||
});
|
||||
|
||||
function getValues(range: { startRow: number; startColumn: number; endRow: number; endColumn: number }) {
|
||||
return get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId('sheet1')
|
||||
?.getRange(range.startRow, range.startColumn, range.endRow, range.endColumn)
|
||||
.getValues();
|
||||
}
|
||||
|
||||
let values = getValues({ startRow: 1, startColumn: 0, endRow: 4, endColumn: 3 });
|
||||
expect(values).toStrictEqual([
|
||||
[{ v: 1, t: 2 }, { v: 2, t: 2 }, { v: 3, t: 2 }, null],
|
||||
[{ v: 2, t: 2 }, { v: 3, t: 2 }, null, { v: 5, t: 2 }],
|
||||
[{ v: 3, t: 2 }, null, { v: 5, t: 2 }, { v: 6, t: 2 }],
|
||||
[null, { v: 5, t: 2 }, { v: 6, t: 2 }, { v: 7, t: 2 }],
|
||||
]);
|
||||
|
||||
values = getValues({ startRow: 5, startColumn: 0, endRow: 5, endColumn: 3 });
|
||||
expect(values).toStrictEqual([
|
||||
[null, null, null, null],
|
||||
]);
|
||||
|
||||
// insert function
|
||||
expect(await commandService.executeCommand(InsertFunctionOperation.id, params)).toBeTruthy();
|
||||
|
||||
values = getValues({ startRow: 5, startColumn: 0, endRow: 5, endColumn: 3 });
|
||||
expect(values).toStrictEqual([
|
||||
[{ f: '=SUM(A2:A5)' }, { f: '=SUM(B2:B5)' }, { f: '=SUM(C2:C5)' }, { f: '=SUM(D2:D5)' }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fault situations', () => {
|
||||
it('will not apply when there is no selected ranges', async () => {
|
||||
const result = await commandService.executeCommand(InsertFunctionOperation.id);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('enters edit mode for a single cell and injects the matched reference into both editors', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
const editorBridgeService = get(IEditorBridgeService);
|
||||
const normalEditorReplaceText = vi.fn();
|
||||
const formulaEditorReplaceText = vi.fn();
|
||||
|
||||
vi.spyOn(editorService, 'getEditor').mockImplementation((id?: string) => {
|
||||
if (id === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
return { replaceText: normalEditorReplaceText } as never;
|
||||
}
|
||||
|
||||
if (id === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
return { replaceText: formulaEditorReplaceText } as never;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 2, startColumn: 1, endRow: 2, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await commandService.executeCommand(InsertFunctionOperation.id, {
|
||||
value: 'SUM',
|
||||
});
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
expect(editorBridgeService.isVisible()).toMatchObject({
|
||||
visible: true,
|
||||
unitId: 'test',
|
||||
});
|
||||
expect(normalEditorReplaceText).toHaveBeenCalledWith('=SUM(B2');
|
||||
expect(formulaEditorReplaceText).toHaveBeenCalledWith('=SUM(B2', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('function isNumberCell', () => {
|
||||
it('should return true when cell type is number', () => {
|
||||
const cell = { t: 2, v: 1 };
|
||||
expect(isNumberCell(cell)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true when cell is number', () => {
|
||||
const cell = { v: 1 };
|
||||
expect(isNumberCell(cell)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when cell is string number', () => {
|
||||
const cell = { v: '1' };
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when cell is string', () => {
|
||||
const cell = { v: 'test' };
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when cell is null', () => {
|
||||
const cell = null;
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when cell is undefined', () => {
|
||||
const cell = undefined;
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when cell is empty object', () => {
|
||||
const cell = {};
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
it('should return true when cell is rich text number', () => {
|
||||
const cell = {
|
||||
p: {
|
||||
id: 'rich1',
|
||||
documentStyle: {},
|
||||
body: {
|
||||
dataStream: '111/r/n',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
it('should return true when cell is rich text', () => {
|
||||
const cell = {
|
||||
p: {
|
||||
id: 'rich1',
|
||||
documentStyle: {},
|
||||
body: {
|
||||
dataStream: 'rich/r/n',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isNumberCell(cell)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('function isSingleCell', () => {
|
||||
it('should return true when startRow === endRow and startColumn === endColumn', () => {
|
||||
const range = { startRow: 1, startColumn: 1, endRow: 1, endColumn: 1 };
|
||||
expect(isSingleCell(range)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when startRow !== endRow', () => {
|
||||
const range = { startRow: 1, startColumn: 1, endRow: 2, endColumn: 1 };
|
||||
expect(isSingleCell(range)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when startColumn !== endColumn', () => {
|
||||
const range = { startRow: 1, startColumn: 1, endRow: 1, endColumn: 2 };
|
||||
expect(isSingleCell(range)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('function isMultiRowsColumnsRange', () => {
|
||||
it('should return true when startRow !== endRow and startColumn !== endColumn', () => {
|
||||
const range = { startRow: 1, startColumn: 1, endRow: 2, endColumn: 2 };
|
||||
expect(isMultiRowsColumnsRange(range)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false when startRow === endRow', () => {
|
||||
const range = { startRow: 1, startColumn: 1, endRow: 1, endColumn: 2 };
|
||||
expect(isMultiRowsColumnsRange(range)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false when startColumn === endColumn', () => {
|
||||
const range = { startRow: 1, startColumn: 1, endRow: 2, endColumn: 1 };
|
||||
expect(isMultiRowsColumnsRange(range)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 type { IHelpFunctionOperationParams } from '../../services/prompt.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IFormulaPromptService } from '../../services/prompt.service';
|
||||
|
||||
export const HelpFunctionOperation: ICommand = {
|
||||
id: 'formula-ui.operation.help-function',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor, params: IHelpFunctionOperationParams) => {
|
||||
const promptService = accessor.get(IFormulaPromptService);
|
||||
promptService.help(params);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* 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, ICellData, ICommand, IRange, Nullable, ObjectMatrix } from '@univerjs/core';
|
||||
import type { IInsertFunction } from '@univerjs/sheets-formula';
|
||||
import {
|
||||
CellValueType,
|
||||
CommandType,
|
||||
DEFAULT_EMPTY_DOCUMENT_VALUE,
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
getCellValueType,
|
||||
ICommandService,
|
||||
isRealNum,
|
||||
IUniverInstanceService,
|
||||
Rectangle,
|
||||
} from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { serializeRange } from '@univerjs/engine-formula';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import {
|
||||
getCellAtRowCol,
|
||||
getSheetCommandTarget,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { InsertFunctionCommand } from '@univerjs/sheets-formula';
|
||||
import { IEditorBridgeService, SetCellEditVisibleOperation } from '@univerjs/sheets-ui';
|
||||
|
||||
export interface IInsertFunctionOperationParams {
|
||||
/**
|
||||
* function name
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const InsertFunctionOperation: ICommand = {
|
||||
id: 'formula-ui.operation.insert-function',
|
||||
type: CommandType.OPERATION,
|
||||
// eslint-disable-next-line
|
||||
handler: async (accessor: IAccessor, params: IInsertFunctionOperationParams) => {
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const currentSelections = selectionManagerService.getCurrentSelections();
|
||||
if (!currentSelections || !currentSelections.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService));
|
||||
if (!target) return false;
|
||||
|
||||
const { worksheet, unitId, subUnitId } = target;
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
|
||||
const { value } = params;
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const editorBridgeService = accessor.get(IEditorBridgeService);
|
||||
|
||||
// No match refRange situation, enter edit mode
|
||||
// In each range, first take the judgment result of the primary position (if there is no primary, take the upper left corner),
|
||||
// If the range cannot be found, enter the edit mode.
|
||||
// If there is a range and range cell has no number value, set the formula range directly, and then set the formula id of other positions. The first formula range is the nearest continuous numeric cell, and other formulas are relatively offset by ID.
|
||||
// If the range cell has number value, find the first blank cell in the row or column direction and set the formula range. The formula calculation range is the range.
|
||||
const list: IInsertFunction[] = [];
|
||||
const listOfRangeHasNumber: IInsertFunction[] = [];
|
||||
let editRange: IRange | null = null;
|
||||
let editRow = 0;
|
||||
let editColumn = 0;
|
||||
let editFormulaRangeString = '';
|
||||
|
||||
// Whether or not there is a matching refRange, a single cell, or a multi-row and multi-column range with no number value, enters the edit mode
|
||||
if (
|
||||
currentSelections.length === 1 &&
|
||||
(isSingleCell(currentSelections[0].range) || (isMultiRowsColumnsRange(currentSelections[0].range) && rangeHasNoNumber(cellMatrix, currentSelections[0].range)))
|
||||
) {
|
||||
const { range, primary } = currentSelections[0];
|
||||
const row = primary?.actualRow ?? range.startRow;
|
||||
const column = primary?.actualColumn ?? range.startColumn;
|
||||
|
||||
editRange = range;
|
||||
editRow = row;
|
||||
editColumn = column;
|
||||
|
||||
const refRange = findRefRange(cellMatrix, row, column);
|
||||
|
||||
if (refRange) {
|
||||
editFormulaRangeString = serializeRange(refRange);
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
currentSelections.some((selection) => {
|
||||
const { range, primary } = selection;
|
||||
|
||||
if (rangeHasNoNumber(cellMatrix, range)) {
|
||||
// single row or single column and range cell has no number value
|
||||
const row = primary?.actualRow ?? range.startRow;
|
||||
const column = primary?.actualColumn ?? range.startColumn;
|
||||
|
||||
const refRange = findRefRange(cellMatrix, row, column);
|
||||
|
||||
if (!refRange) {
|
||||
editRange = range;
|
||||
editRow = row;
|
||||
editColumn = column;
|
||||
return true;
|
||||
}
|
||||
|
||||
const rangeString = serializeRange(refRange);
|
||||
const formulaString = `=${value}(${rangeString})`;
|
||||
|
||||
list.push({
|
||||
range,
|
||||
primary: {
|
||||
row,
|
||||
column,
|
||||
},
|
||||
formula: formulaString,
|
||||
});
|
||||
} else {
|
||||
// range cell has number value
|
||||
const { startRow, startColumn, endRow, endColumn } = range;
|
||||
|
||||
if (startRow === endRow) {
|
||||
// single row, insert function in the blank cell of the row
|
||||
const blankCellColumn = findBlankCellOfRow(cellMatrix, startRow, endColumn, worksheet.getColumnCount() - 1);
|
||||
const newEndColumn = blankCellColumn === endColumn ? endColumn - 1 : endColumn;
|
||||
const rangeString = serializeRange({
|
||||
startRow,
|
||||
endRow,
|
||||
startColumn,
|
||||
endColumn: newEndColumn,
|
||||
});
|
||||
const formulaString = `=${value}(${rangeString})`;
|
||||
|
||||
listOfRangeHasNumber.push({
|
||||
range,
|
||||
primary: {
|
||||
row: startRow,
|
||||
column: blankCellColumn,
|
||||
},
|
||||
formula: formulaString,
|
||||
});
|
||||
} else {
|
||||
// single column or multi-column, insert function in the blank cell of the column
|
||||
let maxBlankCellRow = -1;
|
||||
|
||||
for (let c = startColumn; c <= endColumn; c++) {
|
||||
const blankCellRow = findBlankCellOfColumn(cellMatrix, c, endRow, worksheet.getRowCount() - 1);
|
||||
maxBlankCellRow = Math.max(maxBlankCellRow, blankCellRow);
|
||||
}
|
||||
|
||||
const newEndRow = maxBlankCellRow === endRow ? endRow - 1 : endRow;
|
||||
|
||||
for (let c = startColumn; c <= endColumn; c++) {
|
||||
const rangeString = serializeRange({
|
||||
startRow,
|
||||
endRow: newEndRow,
|
||||
startColumn: c,
|
||||
endColumn: c,
|
||||
});
|
||||
const formulaString = `=${value}(${rangeString})`;
|
||||
|
||||
listOfRangeHasNumber.push({
|
||||
range,
|
||||
primary: {
|
||||
row: maxBlankCellRow,
|
||||
column: c,
|
||||
},
|
||||
formula: formulaString,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (editRange) {
|
||||
// set current position
|
||||
const destRange = getCellAtRowCol(editRow, editColumn, worksheet);
|
||||
|
||||
const resultRange = {
|
||||
range: Rectangle.clone(editRange),
|
||||
primary: {
|
||||
startRow: destRange.startRow,
|
||||
startColumn: destRange.startColumn,
|
||||
endRow: destRange.endRow,
|
||||
endColumn: destRange.endColumn,
|
||||
actualRow: editRow,
|
||||
actualColumn: editColumn,
|
||||
isMerged: destRange.isMerged,
|
||||
isMergedMainCell: destRange.startRow === editRow && destRange.startColumn === editColumn,
|
||||
},
|
||||
};
|
||||
|
||||
const setSelectionParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
|
||||
selections: [resultRange],
|
||||
};
|
||||
await commandService.executeCommand(SetSelectionsOperation.id, setSelectionParams);
|
||||
const editor = editorService.getEditor(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
const formulaEditor = editorService.getEditor(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY);
|
||||
commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, {
|
||||
visible: true,
|
||||
unitId,
|
||||
eventType: DeviceInputEventType.Dblclick,
|
||||
});
|
||||
const formulaText = `=${value}(${editFormulaRangeString}`;
|
||||
editor?.replaceText(formulaText);
|
||||
formulaEditor?.replaceText(formulaText, false);
|
||||
}
|
||||
|
||||
if (list.length === 0 && listOfRangeHasNumber.length === 0) return false;
|
||||
|
||||
return commandService.executeCommand(InsertFunctionCommand.id, {
|
||||
list,
|
||||
listOfRangeHasNumber,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. Starting from the first position on the left or top and ending with a continuous number (the first non-blank cell is allowed to be text)
|
||||
* 2. Match the upper part first, then the left part. If not, insert a function with empty parameters.
|
||||
*/
|
||||
function findRefRange(cellMatrix: ObjectMatrix<Nullable<ICellData>>, row: number, column: number): Nullable<IRange> {
|
||||
const startRow = findStartRow(cellMatrix, row, column);
|
||||
if (startRow !== row) {
|
||||
return {
|
||||
startRow,
|
||||
endRow: row - 1,
|
||||
startColumn: column,
|
||||
endColumn: column,
|
||||
};
|
||||
}
|
||||
|
||||
const startColumn = findStartColumn(cellMatrix, row, column);
|
||||
if (startColumn !== column) {
|
||||
return {
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn,
|
||||
endColumn: column - 1,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findStartRow(cellMatrix: ObjectMatrix<Nullable<ICellData>>, row: number, column: number) {
|
||||
let isFirstNumber = false;
|
||||
|
||||
if (row === 0) return row;
|
||||
|
||||
for (let r = row - 1; r >= 0; r--) {
|
||||
const cell = cellMatrix.getValue(r, column);
|
||||
|
||||
if (isNumberCell(cell) && !isFirstNumber) {
|
||||
if (r === 0) return 0;
|
||||
isFirstNumber = true;
|
||||
} else if (isFirstNumber && !isNumberCell(cell)) {
|
||||
return r + 1;
|
||||
} else if (isFirstNumber && r === 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function findStartColumn(cellMatrix: ObjectMatrix<Nullable<ICellData>>, row: number, column: number) {
|
||||
let isFirstNumber = false;
|
||||
|
||||
if (column === 0) return column;
|
||||
|
||||
for (let c = column - 1; c >= 0; c--) {
|
||||
const cell = cellMatrix.getValue(row, c);
|
||||
|
||||
if (isNumberCell(cell) && !isFirstNumber) {
|
||||
if (c === 0) return 0;
|
||||
isFirstNumber = true;
|
||||
} else if (isFirstNumber && !isNumberCell(cell)) {
|
||||
return c + 1;
|
||||
} else if (isFirstNumber && c === 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
export function isNumberCell(cell: Nullable<ICellData>) {
|
||||
if (cell?.p) {
|
||||
const body = cell?.p.body;
|
||||
|
||||
if (body == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = body.dataStream;
|
||||
const lastString = data.substring(data.length - 2, data.length);
|
||||
const newDataStream = lastString === DEFAULT_EMPTY_DOCUMENT_VALUE ? data.substring(0, data.length - 2) : data;
|
||||
|
||||
return isRealNum(newDataStream);
|
||||
}
|
||||
return cell && (cell.t === CellValueType.NUMBER || getCellValueType(cell) === CellValueType.NUMBER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a single cell
|
||||
* @param range
|
||||
*/
|
||||
export function isSingleCell(range: IRange) {
|
||||
return range.startRow === range.endRow && range.startColumn === range.endColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a multi-row, multi-column range
|
||||
* @param range
|
||||
*/
|
||||
export function isMultiRowsColumnsRange(range: IRange) {
|
||||
return range.startRow !== range.endRow && range.startColumn !== range.endColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the range has no number
|
||||
* @param cellMatrix
|
||||
* @param range
|
||||
*/
|
||||
export function rangeHasNoNumber(cellMatrix: ObjectMatrix<Nullable<ICellData>>, range: IRange) {
|
||||
for (let i = range.startRow; i <= range.endRow; i++) {
|
||||
for (let j = range.startColumn; j <= range.endColumn; j++) {
|
||||
if (isNumberCell(cellMatrix.getValue(i, j))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function findBlankCellOfRow(cellMatrix: ObjectMatrix<Nullable<ICellData>>, row: number, startColumn: number, endColumn: number) {
|
||||
for (let c = startColumn; c <= endColumn; c++) {
|
||||
if (!cellMatrix.getValue(row, c)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return endColumn;
|
||||
}
|
||||
|
||||
function findBlankCellOfColumn(cellMatrix: ObjectMatrix<Nullable<ICellData>>, column: number, startRow: number, endRow: number) {
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
if (!cellMatrix.getValue(r, column)) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return endRow;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { ISidebarService } from '@univerjs/ui';
|
||||
import { MORE_FUNCTIONS_COMPONENT } from '../../views/more-functions/interface';
|
||||
|
||||
export const MoreFunctionsOperation: ICommand = {
|
||||
id: 'formula-ui.operation.more-functions',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
|
||||
sidebarService.open({
|
||||
header: { title: 'sheets-formula-ui.insert.tooltip' },
|
||||
children: { label: MORE_FUNCTIONS_COMPONENT },
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import { BuildTextUtils, CommandType, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { toggleReferenceAbsoluteAtCursor } from '../utils/reference-absolute';
|
||||
|
||||
export const ReferenceAbsoluteOperation: ICommand = {
|
||||
id: 'formula-ui.operation.change-ref-to-absolute',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const lexerTreeBuilder = accessor.get(LexerTreeBuilder);
|
||||
const focusEditor = editorService.getFocusEditor();
|
||||
|
||||
if (!focusEditor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const editorId = focusEditor.getEditorId();
|
||||
if (editorId !== DOCS_NORMAL_EDITOR_UNIT_ID_KEY && editorId !== DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTextRange = docSelectionManagerService.getActiveTextRange();
|
||||
if (!activeTextRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const formulaText = BuildTextUtils.transform.getPlainText(focusEditor.getDocumentData().body?.dataStream ?? '');
|
||||
const nextReferenceState = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, formulaText, activeTextRange.endOffset);
|
||||
|
||||
if (!nextReferenceState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
focusEditor.replaceText(nextReferenceState.formulaText, [{
|
||||
startOffset: nextReferenceState.cursorOffset,
|
||||
endOffset: nextReferenceState.cursorOffset,
|
||||
collapsed: true,
|
||||
}]);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -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 type { ISearchFunctionOperationParams } from '../../services/prompt.service';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IFormulaPromptService } from '../../services/prompt.service';
|
||||
|
||||
export const SearchFunctionOperation: ICommand = {
|
||||
id: 'formula-ui.operation.search-function',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor: IAccessor, params: ISearchFunctionOperationParams) => {
|
||||
const promptService = accessor.get(IFormulaPromptService);
|
||||
promptService.search(params);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { toggleReferenceAbsoluteAtCursor } from '../reference-absolute';
|
||||
|
||||
describe('toggleReferenceAbsoluteAtCursor', () => {
|
||||
const lexerTreeBuilder = new LexerTreeBuilder();
|
||||
|
||||
it('cycles a single reference through all Excel absolute states', () => {
|
||||
let formulaText = '=A1';
|
||||
let cursorOffset = formulaText.length;
|
||||
|
||||
const expectedCycle = ['=$A$1', '=A$1', '=$A1', '=A1'];
|
||||
|
||||
for (const expectedFormulaText of expectedCycle) {
|
||||
const nextState = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, formulaText, cursorOffset);
|
||||
|
||||
expect(nextState).not.toBeNull();
|
||||
expect(nextState!.formulaText).toBe(expectedFormulaText);
|
||||
|
||||
formulaText = nextState!.formulaText;
|
||||
cursorOffset = nextState!.cursorOffset;
|
||||
}
|
||||
});
|
||||
|
||||
it('cycles the cell part of a cross-sheet reference', () => {
|
||||
const nextState = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=Sheet2!B5', '=Sheet2!B'.length);
|
||||
|
||||
expect(nextState).toEqual({
|
||||
formulaText: '=Sheet2!$B$5',
|
||||
cursorOffset: '=Sheet2!B'.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('cycles only the referenced side of a range based on cursor position', () => {
|
||||
const startSide = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=A1:B2', '=A'.length);
|
||||
const endSide = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=A1:B2', '=A1:'.length + 1);
|
||||
|
||||
expect(startSide?.formulaText).toBe('=$A$1:B2');
|
||||
expect(endSide?.formulaText).toBe('=A1:$B$2');
|
||||
});
|
||||
|
||||
it('cycles both sides when the caret is adjacent to the range separator', () => {
|
||||
const beforeColon = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=A1:B2', '=A1'.length);
|
||||
const afterColon = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=A1:B2', '=A1:'.length);
|
||||
|
||||
expect(beforeColon?.formulaText).toBe('=$A$1:$B$2');
|
||||
expect(afterColon?.formulaText).toBe('=$A$1:$B$2');
|
||||
expect(beforeColon?.cursorOffset).toBe('=$A$1'.length);
|
||||
expect(afterColon?.cursorOffset).toBe('=$A$1:'.length);
|
||||
});
|
||||
|
||||
it('keeps cycling both sides on repeated F4 presses at the range separator', () => {
|
||||
let formulaText = '=A1:B2';
|
||||
let cursorOffset = '=A1'.length;
|
||||
|
||||
const expectedCycle = ['=$A$1:$B$2', '=A$1:B$2', '=$A1:$B2', '=A1:B2'];
|
||||
|
||||
for (const expectedFormulaText of expectedCycle) {
|
||||
const nextState = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, formulaText, cursorOffset);
|
||||
|
||||
expect(nextState?.formulaText).toBe(expectedFormulaText);
|
||||
|
||||
formulaText = nextState!.formulaText;
|
||||
cursorOffset = nextState!.cursorOffset;
|
||||
}
|
||||
});
|
||||
|
||||
it('the reference is row or column only', () => {
|
||||
const rowReference = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=1:1', '=1'.length);
|
||||
const columnReference = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=A:A', '=A'.length);
|
||||
|
||||
expect(rowReference?.formulaText).toBe('=$1:$1');
|
||||
expect(columnReference?.formulaText).toBe('=$A:$A');
|
||||
});
|
||||
|
||||
it('handles references at formula boundaries', () => {
|
||||
const startBoundary = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=A1+SUM(B2)', '=A1'.length);
|
||||
const endBoundary = toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=SUM(1)+B2', '=SUM(1)+B2'.length);
|
||||
|
||||
expect(startBoundary?.formulaText).toBe('=$A$1+SUM(B2)');
|
||||
expect(endBoundary?.formulaText).toBe('=SUM(1)+$B$2');
|
||||
});
|
||||
|
||||
it('returns null when the cursor is not on a cell reference', () => {
|
||||
expect(toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=SUM(1, 2)', '=SUM('.length)).toBeNull();
|
||||
expect(toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '="A1"', '="'.length + 1)).toBeNull();
|
||||
expect(toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=namedRange', '=named'.length)).toBeNull();
|
||||
expect(toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '', 0)).toBeNull();
|
||||
expect(toggleReferenceAbsoluteAtCursor(lexerTreeBuilder, '=1', 1)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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 { ISequenceNode, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { AbsoluteRefType, RANGE_TYPE } from '@univerjs/core';
|
||||
import {
|
||||
deserializeRangeWithSheetWithCache,
|
||||
generateStringWithSequence,
|
||||
getAbsoluteRefTypeWithSingleString,
|
||||
handleRefStringInfo,
|
||||
sequenceNodeType,
|
||||
serializeRangeToRefString,
|
||||
} from '@univerjs/engine-formula';
|
||||
|
||||
const CELL_REFERENCE_REGEX = /^\$?[A-Za-z]+\$?\d+$/;
|
||||
|
||||
export interface IReferenceAbsoluteResult {
|
||||
formulaText: string;
|
||||
cursorOffset: number;
|
||||
}
|
||||
|
||||
interface IToggleReferenceTokenResult {
|
||||
token: string;
|
||||
referenceTarget: 'start' | 'end' | 'both';
|
||||
previousSeparatorOffset: number | null;
|
||||
nextSeparatorOffset: number | null;
|
||||
}
|
||||
|
||||
export function toggleReferenceAbsoluteAtCursor(
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaText: string,
|
||||
cursorOffset: number
|
||||
): IReferenceAbsoluteResult | null {
|
||||
if (!formulaText.startsWith('=') || formulaText.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sequenceNodes = lexerTreeBuilder.sequenceNodesBuilder(formulaText);
|
||||
if (!sequenceNodes?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedCursorOffset = Math.min(Math.max(cursorOffset, 0), formulaText.length);
|
||||
const relativeCursorOffset = normalizedCursorOffset - 1;
|
||||
if (relativeCursorOffset < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchedReference = sequenceNodes.find((node) => {
|
||||
if (typeof node === 'string' || node.nodeType !== sequenceNodeType.REFERENCE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return relativeCursorOffset >= node.startIndex && relativeCursorOffset <= node.endIndex + 1;
|
||||
});
|
||||
|
||||
if (!matchedReference || typeof matchedReference === 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextReferenceState = _toggleReferenceToken(matchedReference, relativeCursorOffset - matchedReference.startIndex);
|
||||
if (nextReferenceState == null || nextReferenceState.token === matchedReference.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextSequenceNodes = sequenceNodes.map((node) => {
|
||||
if (node !== matchedReference) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
token: nextReferenceState.token,
|
||||
endIndex: node.startIndex + nextReferenceState.token.length - 1,
|
||||
};
|
||||
});
|
||||
|
||||
const nextFormulaText = `=${generateStringWithSequence(nextSequenceNodes)}`;
|
||||
const nextCursorOffset = _translateCursorOffset(
|
||||
normalizedCursorOffset,
|
||||
matchedReference.startIndex + 1,
|
||||
matchedReference.endIndex + 2,
|
||||
nextReferenceState.token.length,
|
||||
nextReferenceState
|
||||
);
|
||||
|
||||
return {
|
||||
formulaText: nextFormulaText,
|
||||
cursorOffset: nextCursorOffset,
|
||||
};
|
||||
}
|
||||
|
||||
function _toggleReferenceToken(referenceNode: ISequenceNode, offsetInToken: number): IToggleReferenceTokenResult | null {
|
||||
const sequenceGrid = deserializeRangeWithSheetWithCache(referenceNode.token);
|
||||
if (sequenceGrid == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { range } = sequenceGrid;
|
||||
|
||||
if (range.rangeType === RANGE_TYPE.ROW || range.rangeType === RANGE_TYPE.COLUMN) {
|
||||
const newToken = serializeRangeToRefString({
|
||||
...sequenceGrid,
|
||||
range: {
|
||||
...range,
|
||||
startAbsoluteRefType: range.startAbsoluteRefType === AbsoluteRefType.NONE ? AbsoluteRefType.ALL : AbsoluteRefType.NONE,
|
||||
endAbsoluteRefType: range.endAbsoluteRefType === AbsoluteRefType.NONE ? AbsoluteRefType.ALL : AbsoluteRefType.NONE,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
token: newToken,
|
||||
referenceTarget: 'both',
|
||||
previousSeparatorOffset: null,
|
||||
nextSeparatorOffset: null,
|
||||
};
|
||||
}
|
||||
|
||||
const { refBody } = handleRefStringInfo(referenceNode.token);
|
||||
const referenceParts = refBody.split(':');
|
||||
const previousSeparatorOffset = referenceParts.length === 2 ? referenceNode.token.indexOf(':') : null;
|
||||
|
||||
if (referenceParts.some((part) => !CELL_REFERENCE_REGEX.test(part))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRangeReference = referenceParts.length === 2;
|
||||
const referenceTarget = !isRangeReference
|
||||
? 'start'
|
||||
: _getRangeReferenceTarget(referenceNode.token, refBody, offsetInToken);
|
||||
|
||||
const nextRange = {
|
||||
...range,
|
||||
};
|
||||
|
||||
if (referenceTarget === 'start' || referenceTarget === 'both') {
|
||||
const nextAbsoluteRefType = _getNextAbsoluteRefType(getAbsoluteRefTypeWithSingleString(referenceParts[0]));
|
||||
nextRange.startAbsoluteRefType = nextAbsoluteRefType;
|
||||
if (!isRangeReference) {
|
||||
nextRange.endAbsoluteRefType = nextAbsoluteRefType;
|
||||
}
|
||||
}
|
||||
|
||||
if (referenceTarget === 'end' || referenceTarget === 'both') {
|
||||
nextRange.endAbsoluteRefType = _getNextAbsoluteRefType(getAbsoluteRefTypeWithSingleString(referenceParts[1]));
|
||||
}
|
||||
|
||||
const token = serializeRangeToRefString({
|
||||
...sequenceGrid,
|
||||
range: nextRange,
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
referenceTarget,
|
||||
previousSeparatorOffset,
|
||||
nextSeparatorOffset: referenceParts.length === 2 ? token.indexOf(':') : null,
|
||||
};
|
||||
}
|
||||
|
||||
function _getRangeReferenceTarget(
|
||||
token: string,
|
||||
refBody: string,
|
||||
offsetInToken: number
|
||||
): 'start' | 'end' | 'both' {
|
||||
const prefixLength = token.length - refBody.length;
|
||||
const separatorOffset = prefixLength + refBody.indexOf(':');
|
||||
|
||||
if (offsetInToken === separatorOffset || offsetInToken === separatorOffset + 1) {
|
||||
return 'both';
|
||||
}
|
||||
|
||||
return offsetInToken < separatorOffset ? 'start' : 'end';
|
||||
}
|
||||
|
||||
function _getNextAbsoluteRefType(absoluteRefType: AbsoluteRefType): AbsoluteRefType {
|
||||
switch (absoluteRefType) {
|
||||
case AbsoluteRefType.NONE:
|
||||
return AbsoluteRefType.ALL;
|
||||
case AbsoluteRefType.ALL:
|
||||
return AbsoluteRefType.ROW;
|
||||
case AbsoluteRefType.ROW:
|
||||
return AbsoluteRefType.COLUMN;
|
||||
case AbsoluteRefType.COLUMN:
|
||||
default:
|
||||
return AbsoluteRefType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
function _translateCursorOffset(
|
||||
cursorOffset: number,
|
||||
replaceStartOffset: number,
|
||||
replaceEndOffsetExclusive: number,
|
||||
nextLength: number,
|
||||
toggleResult: IToggleReferenceTokenResult
|
||||
): number {
|
||||
if (
|
||||
toggleResult.referenceTarget === 'both' &&
|
||||
toggleResult.previousSeparatorOffset != null &&
|
||||
toggleResult.nextSeparatorOffset != null
|
||||
) {
|
||||
const previousColonOffset = replaceStartOffset + toggleResult.previousSeparatorOffset;
|
||||
const nextColonOffset = replaceStartOffset + toggleResult.nextSeparatorOffset;
|
||||
|
||||
if (cursorOffset === previousColonOffset) {
|
||||
return nextColonOffset;
|
||||
}
|
||||
|
||||
if (cursorOffset === previousColonOffset + 1) {
|
||||
return nextColonOffset + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorOffset <= replaceStartOffset) {
|
||||
return cursorOffset;
|
||||
}
|
||||
|
||||
const previousLength = replaceEndOffsetExclusive - replaceStartOffset;
|
||||
const lengthDiff = nextLength - previousLength;
|
||||
|
||||
if (cursorOffset >= replaceEndOffsetExclusive) {
|
||||
return cursorOffset + lengthDiff;
|
||||
}
|
||||
|
||||
const offsetInsideReference = cursorOffset - replaceStartOffset;
|
||||
return replaceStartOffset + Math.min(offsetInsideReference, nextLength);
|
||||
}
|
||||
17
packages/sheets-formula-ui/src/common/plugin-name.ts
Normal file
17
packages/sheets-formula-ui/src/common/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 FORMULA_UI_PLUGIN_NAME = 'SHEET_FORMULA_UI_PLUGIN';
|
||||
17
packages/sheets-formula-ui/src/common/prompt.ts
Normal file
17
packages/sheets-formula-ui/src/common/prompt.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 META_KEY_CTRL_AND_SHIFT = 'meta_key_ctrl_And_Shift';
|
||||
36
packages/sheets-formula-ui/src/common/selection.ts
Normal file
36
packages/sheets-formula-ui/src/common/selection.ts
Normal file
@@ -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 { ThemeService } from '@univerjs/core';
|
||||
import type { ISelectionStyle } from '@univerjs/sheets';
|
||||
import { ColorKit } from '@univerjs/core';
|
||||
|
||||
export const FORMULA_REF_SELECTION_PLUGIN_NAME = 'formula_reference_selection_plugin_name';
|
||||
|
||||
export function genFormulaRefSelectionStyle(themeService: ThemeService, refColor: string, id: string): ISelectionStyle {
|
||||
const fill = new ColorKit(refColor).setAlpha(0.05).toRgbString();
|
||||
|
||||
return {
|
||||
id,
|
||||
strokeWidth: 1,
|
||||
stroke: refColor,
|
||||
fill,
|
||||
widgets: { tl: true, tc: true, tr: true, ml: true, mr: true, bl: true, bc: true, br: true },
|
||||
widgetSize: 6,
|
||||
widgetStrokeWidth: 1,
|
||||
widgetStroke: themeService.getColorFromTheme('gray.0'),
|
||||
};
|
||||
}
|
||||
40
packages/sheets-formula-ui/src/config/config.ts
Normal file
40
packages/sheets-formula-ui/src/config/config.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 type { Ctor } from '@univerjs/core';
|
||||
import type { BaseFunction, IFunctionInfo, IFunctionNames } from '@univerjs/engine-formula';
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
/**
|
||||
* Base configuration for the plugin.
|
||||
*/
|
||||
export const PLUGIN_CONFIG_KEY_BASE = 'sheets-formula-ui.base.config';
|
||||
|
||||
export const configSymbolBase = Symbol(PLUGIN_CONFIG_KEY_BASE);
|
||||
|
||||
export interface IUniverSheetsFormulaUIConfig {
|
||||
menu?: MenuConfig;
|
||||
notExecuteFormula?: boolean;
|
||||
description?: IFunctionInfo[];
|
||||
function?: Array<[Ctor<BaseFunction>, IFunctionNames]>;
|
||||
/**
|
||||
* The switch for the function screen tips display in the formula editor.
|
||||
* @default true
|
||||
*/
|
||||
functionScreenTips?: boolean;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsFormulaUIConfig = {};
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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 { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { EditorService, IEditorService } from '@univerjs/docs-ui';
|
||||
import {
|
||||
CalculateFormulaService,
|
||||
DefinedNamesService,
|
||||
FormulaCurrentConfigService,
|
||||
FormulaDataModel,
|
||||
FormulaRuntimeService,
|
||||
HyperlinkEngineFormulaService,
|
||||
ICalculateFormulaService,
|
||||
IDefinedNamesService,
|
||||
IFormulaCurrentConfigService,
|
||||
IFormulaRuntimeService,
|
||||
IHyperlinkEngineFormulaService,
|
||||
LexerTreeBuilder,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
DefinedNameDataController,
|
||||
IRefSelectionsService,
|
||||
RangeProtectionRuleModel,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
WorkbookPermissionService,
|
||||
WorksheetPermissionService,
|
||||
WorksheetProtectionPointModel,
|
||||
WorksheetProtectionRuleModel,
|
||||
} from '@univerjs/sheets';
|
||||
import { EditorBridgeService, IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 1,
|
||||
},
|
||||
1: {
|
||||
f: '=SUM(A1)',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
1: {
|
||||
v: 1,
|
||||
},
|
||||
},
|
||||
9: {
|
||||
0: {
|
||||
f: '=B10',
|
||||
},
|
||||
},
|
||||
19: {
|
||||
0: {
|
||||
v: 20,
|
||||
},
|
||||
},
|
||||
20: {
|
||||
0: {
|
||||
f: '=B21',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
has: Injector['has'];
|
||||
sheet: Workbook;
|
||||
}
|
||||
|
||||
export function createCommandTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
const has = injector.has.bind(injector);
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
private _formulaDataModel: FormulaDataModel | null = null;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const injector = this._injector;
|
||||
injector.add([WorksheetPermissionService]);
|
||||
injector.add([WorksheetProtectionPointModel]);
|
||||
injector.add([RangeProtectionRuleModel]);
|
||||
injector.add([WorkbookPermissionService]);
|
||||
injector.add([WorksheetProtectionRuleModel]);
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([ICalculateFormulaService, { useClass: CalculateFormulaService }]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([DefinedNameDataController]);
|
||||
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
|
||||
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
|
||||
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
|
||||
const hasSheetSkeletonManagerService = dependencies?.some((dependency) =>
|
||||
Array.isArray(dependency) && dependency[0] === SheetSkeletonManagerService
|
||||
);
|
||||
if (!hasSheetSkeletonManagerService) {
|
||||
injector.add([SheetSkeletonManagerService]);
|
||||
}
|
||||
injector.add([IEditorBridgeService, { useClass: EditorBridgeService }]);
|
||||
injector.add([IEditorService, { useClass: EditorService }]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([IRefSelectionsService, { useClass: SheetsSelectionsService }]);
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
this._injector.get(WorkbookPermissionService);
|
||||
this._injector.get(WorksheetPermissionService);
|
||||
this._injector.get(DefinedNameDataController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._formulaDataModel = get(FormulaDataModel);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData || TEST_WORKBOOK_DATA_DEMO);
|
||||
|
||||
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,
|
||||
has,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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 { ErrorType, extractFormulaError } from '@univerjs/engine-formula';
|
||||
import { Subject } from 'rxjs';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaAlertRenderController } from '../formula-alert-render.controller';
|
||||
|
||||
vi.mock('@univerjs/engine-formula', async (importActual) => {
|
||||
const actual = await importActual<typeof import('@univerjs/engine-formula')>();
|
||||
return {
|
||||
...actual,
|
||||
extractFormulaError: vi.fn(() => actual.ErrorType.DIV_BY_ZERO),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('FormulaAlertRenderController', () => {
|
||||
it('shows formula alerts for hovered cells and hides them for repeated hovers', async () => {
|
||||
vi.useFakeTimers();
|
||||
const currentCell$ = new Subject<{ location: { unitId: string; subUnitId: string; row: number; col: number } }>();
|
||||
const currentAlert = new Map<string, { alert: { location: { unitId: string; subUnitId: string; row: number; col: number } } }>();
|
||||
const showAlert = vi.fn((alert) => currentAlert.set('SHEET_FORMULA_ALERT', { alert }));
|
||||
const removeAlert = vi.fn((key: string) => currentAlert.delete(key));
|
||||
const worksheet = { getCell: vi.fn(() => ({ v: '#DIV/0!' })) };
|
||||
|
||||
const controller = new FormulaAlertRenderController(
|
||||
{ unit: { getActiveSheet: vi.fn(() => worksheet) } } as never,
|
||||
{ currentCell$ } as never,
|
||||
{ currentAlert, removeAlert, showAlert } as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getArrayFormulaCellData: vi.fn(() => null) } as never
|
||||
);
|
||||
|
||||
expect(controller).toBeTruthy();
|
||||
|
||||
const location = { unitId: 'book-1', subUnitId: 'sheet-1', row: 2, col: 1 };
|
||||
currentCell$.next({ location });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
|
||||
expect(showAlert).toHaveBeenCalledWith(expect.objectContaining({
|
||||
key: 'SHEET_FORMULA_ALERT',
|
||||
title: 'sheets-formula-ui.error.title',
|
||||
message: 'sheets-formula-ui.error.divByZero',
|
||||
location,
|
||||
}));
|
||||
|
||||
currentCell$.next({ location });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
expect(removeAlert).toHaveBeenCalledWith('SHEET_FORMULA_ALERT');
|
||||
});
|
||||
|
||||
it('hides alerts when there is no extractable formula error', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(extractFormulaError).mockReturnValueOnce(null as never);
|
||||
const currentCell$ = new Subject<{ location: { unitId: string; subUnitId: string; row: number; col: number } }>();
|
||||
const removeAlert = vi.fn();
|
||||
|
||||
const controller = new FormulaAlertRenderController(
|
||||
{ unit: { getActiveSheet: vi.fn(() => ({ getCell: vi.fn(() => ({ v: 'plain-text' })) })) } } as never,
|
||||
{ currentCell$ } as never,
|
||||
{ currentAlert: new Map(), removeAlert, showAlert: vi.fn() } as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getArrayFormulaCellData: vi.fn(() => null) } as never
|
||||
);
|
||||
|
||||
expect(controller).toBeTruthy();
|
||||
|
||||
currentCell$.next({ location: { unitId: 'book-1', subUnitId: 'sheet-1', row: 0, col: 0 } });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
|
||||
expect(removeAlert).toHaveBeenCalledWith('SHEET_FORMULA_ALERT');
|
||||
expect(vi.mocked(extractFormulaError)).toHaveBeenCalledWith({ v: 'plain-text' }, false);
|
||||
expect(ErrorType.DIV_BY_ZERO).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides alerts when the workbook has no active worksheet for the hovered cell', async () => {
|
||||
vi.useFakeTimers();
|
||||
const currentCell$ = new Subject<{ location: { unitId: string; subUnitId: string; row: number; col: number } }>();
|
||||
const removedAlerts: string[] = [];
|
||||
const shownAlerts: unknown[] = [];
|
||||
const controller = new FormulaAlertRenderController(
|
||||
{ unit: { getActiveSheet: () => null } } as never,
|
||||
{ currentCell$ } as never,
|
||||
{
|
||||
currentAlert: new Map(),
|
||||
removeAlert: (key: string) => {
|
||||
removedAlerts.push(key);
|
||||
},
|
||||
showAlert: (alert: unknown) => {
|
||||
shownAlerts.push(alert);
|
||||
},
|
||||
} as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getArrayFormulaCellData: () => null } as never
|
||||
);
|
||||
|
||||
currentCell$.next({ location: { unitId: 'book-1', subUnitId: 'sheet-1', row: 3, col: 4 } });
|
||||
await vi.advanceTimersByTimeAsync(120);
|
||||
|
||||
expect(removedAlerts).toEqual(['SHEET_FORMULA_ALERT']);
|
||||
expect(shownAlerts).toEqual([]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* 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, Nullable, Univer, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
set,
|
||||
ThemeService,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
AddWorksheetMergeMutation,
|
||||
AutoFillCommand,
|
||||
AutoFillController,
|
||||
AutoFillService,
|
||||
IAutoFillService,
|
||||
RangeProtectionRenderModel,
|
||||
RangeProtectionService,
|
||||
RemoveWorksheetMergeMutation,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { FormulaAutoFillController } from '@univerjs/sheets-formula';
|
||||
import {
|
||||
AutoFillUIController,
|
||||
ISheetSelectionRenderService,
|
||||
SheetSelectionRenderService,
|
||||
SheetsRenderService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import { IPlatformService, IShortcutService, PlatformService, ShortcutService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
class mockSheetsRenderService {
|
||||
registerSkeletonChangingMutations(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('Test auto fill with formula', () => {
|
||||
let univer: Univer;
|
||||
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;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[ISheetSelectionRenderService, { useClass: SheetSelectionRenderService }],
|
||||
[IAutoFillService, { useClass: AutoFillService }],
|
||||
[IShortcutService, { useClass: ShortcutService }],
|
||||
[IPlatformService, { useClass: PlatformService }],
|
||||
[SheetsRenderService, { useClass: mockSheetsRenderService }],
|
||||
[AutoFillController],
|
||||
[AutoFillUIController],
|
||||
[FormulaAutoFillController],
|
||||
[RangeProtectionService],
|
||||
[RangeProtectionRenderModel],
|
||||
]);
|
||||
|
||||
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(AutoFillController);
|
||||
get(AutoFillUIController);
|
||||
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(AddWorksheetMergeMutation);
|
||||
commandService.registerCommand(RemoveWorksheetMergeMutation);
|
||||
commandService.registerCommand(AutoFillCommand);
|
||||
|
||||
get(FormulaAutoFillController);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('correct situations', () => {
|
||||
it('one cell with formula', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 1, endRow: 0, endColumn: 1, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
const sourceRange = {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
};
|
||||
|
||||
const targetRange = {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
};
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, { sourceRange, targetRange });
|
||||
|
||||
// B1:B3 values will be in the following format
|
||||
// [
|
||||
// [ { f: '=SUM(A1)' } ],
|
||||
// [ { f: '=SUM(A2)', si: '1ZMZWH' } ],
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// ]
|
||||
let values = getValues(0, 1, 2, 1);
|
||||
let B2 = values && values[1][0];
|
||||
let B3 = values && values[2][0];
|
||||
|
||||
expect(B2?.f).toStrictEqual('=SUM(A2)');
|
||||
expect(B2?.si).toEqual(B3?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(0, 1, 2, 1);
|
||||
B2 = values && values[1][0];
|
||||
B3 = values && values[2][0];
|
||||
|
||||
expect(B2).toStrictEqual({
|
||||
t: 2,
|
||||
v: 1,
|
||||
});
|
||||
expect(B3).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(0, 1, 2, 1);
|
||||
B2 = values && values[1][0];
|
||||
B3 = values && values[2][0];
|
||||
|
||||
expect(B2?.f).toStrictEqual('=SUM(A2)');
|
||||
expect(B2?.si).toEqual(B3?.si);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 1,
|
||||
endColumn: 3,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// B1:D3 values will be in the following format
|
||||
// [
|
||||
// [
|
||||
// {
|
||||
// "f": "=SUM(A1)",
|
||||
// },
|
||||
// {
|
||||
// "f": "=SUM(B1)",
|
||||
// "si": "tF5X2Q"
|
||||
// },
|
||||
// {
|
||||
// "si": "tF5X2Q"
|
||||
// }
|
||||
// ],
|
||||
// [
|
||||
// {
|
||||
// "f": "=SUM(A2)",
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// }
|
||||
// ],
|
||||
// [
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// },
|
||||
// {
|
||||
// "si": "d4_488"
|
||||
// }
|
||||
// ]
|
||||
// ]
|
||||
values = getValues(0, 1, 2, 3);
|
||||
let C1 = values && values[0][1];
|
||||
let C2 = values && values[1][1];
|
||||
let C3 = values && values[2][1];
|
||||
let D1 = values && values[0][2];
|
||||
let D2 = values && values[1][2];
|
||||
let D3 = values && values[2][2];
|
||||
|
||||
expect(C1?.f).toStrictEqual('=SUM(B1)');
|
||||
expect(C2?.f).toBeUndefined();
|
||||
expect(C1?.si).toEqual(D1?.si);
|
||||
|
||||
expect(B2?.si).toEqual(C2?.si);
|
||||
expect(D2?.si).toEqual(C2?.si);
|
||||
|
||||
expect(B3?.si).toEqual(C3?.si);
|
||||
expect(D3?.si).toEqual(C3?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(0, 1, 2, 3);
|
||||
C1 = values && values[0][1];
|
||||
C2 = values && values[1][1];
|
||||
C3 = values && values[2][1];
|
||||
D1 = values && values[0][2];
|
||||
D2 = values && values[1][2];
|
||||
D3 = values && values[2][2];
|
||||
|
||||
expect(C1).toBeNull();
|
||||
expect(C2).toBeNull();
|
||||
expect(C3).toBeNull();
|
||||
expect(D1).toBeNull();
|
||||
expect(D2).toBeNull();
|
||||
expect(D3).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(0, 1, 2, 3);
|
||||
C1 = values && values[0][1];
|
||||
C2 = values && values[1][1];
|
||||
C3 = values && values[2][1];
|
||||
D1 = values && values[0][2];
|
||||
D2 = values && values[1][2];
|
||||
D3 = values && values[2][2];
|
||||
|
||||
expect(C1?.f).toStrictEqual('=SUM(B1)');
|
||||
expect(C2?.f).toBeUndefined();
|
||||
expect(C1?.si).toEqual(D1?.si);
|
||||
|
||||
expect(B2?.si).toEqual(C2?.si);
|
||||
expect(D2?.si).toEqual(C2?.si);
|
||||
|
||||
expect(B3?.si).toEqual(C3?.si);
|
||||
expect(D3?.si).toEqual(C3?.si);
|
||||
|
||||
// Restore the original data
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fill up with formula', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 9, startColumn: 0, endRow: 9, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 9,
|
||||
endRow: 9,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 6,
|
||||
endRow: 9,
|
||||
},
|
||||
});
|
||||
|
||||
// B1:B3 values will be in the following format
|
||||
// [
|
||||
// [ { f: '=B7', si: '1ZMZWH' } ],
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// [ { f: '=B10' } ],
|
||||
// ]
|
||||
let values = getValues(6, 0, 8, 0);
|
||||
let A7 = values && values[0][0];
|
||||
let A8 = values && values[1][0];
|
||||
let A9 = values && values[2][0];
|
||||
|
||||
expect(A7?.f).toStrictEqual('=B7');
|
||||
expect(A7?.si).toEqual(A8?.si);
|
||||
expect(A7?.si).toEqual(A9?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(6, 0, 8, 0);
|
||||
A7 = values && values[0][0];
|
||||
A8 = values && values[1][0];
|
||||
A9 = values && values[2][0];
|
||||
|
||||
expect(A7).toBeNull();
|
||||
expect(A8).toBeNull();
|
||||
expect(A9).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(6, 0, 8, 0);
|
||||
A7 = values && values[0][0];
|
||||
A8 = values && values[1][0];
|
||||
A9 = values && values[2][0];
|
||||
|
||||
expect(A7?.f).toStrictEqual('=B7');
|
||||
expect(A7?.si).toEqual(A8?.si);
|
||||
expect(A7?.si).toEqual(A9?.si);
|
||||
});
|
||||
|
||||
it('fill up with formula and number', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 19, startColumn: 0, endRow: 20, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
commandService.executeCommand(AutoFillCommand.id, {
|
||||
sourceRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 19,
|
||||
endRow: 20,
|
||||
},
|
||||
targetRange: {
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
startRow: 15,
|
||||
endRow: 20,
|
||||
},
|
||||
});
|
||||
|
||||
// B1:B3 values will be in the following format
|
||||
// [
|
||||
// [ { v: 18 } ]
|
||||
// [ { f: '=B17', si: '1ZMZWH' } ],
|
||||
// [ { v: 19 } ]
|
||||
// [ { si: '1ZMZWH' } ]
|
||||
// [ { v: 20 } ]
|
||||
// [ { f: '=B21' } ],
|
||||
// ]
|
||||
let values = getValues(15, 0, 18, 0);
|
||||
let A16 = values && values[0][0];
|
||||
let A17 = values && values[1][0];
|
||||
let A18 = values && values[2][0];
|
||||
let A19 = values && values[3][0];
|
||||
|
||||
expect(A16?.v).toStrictEqual(18);
|
||||
expect(A17?.f).toEqual('=B17');
|
||||
expect(A18?.v).toEqual(19);
|
||||
expect(A19?.si).toEqual(A17?.si);
|
||||
|
||||
// undo
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
|
||||
values = getValues(15, 0, 18, 0);
|
||||
A16 = values && values[0][0];
|
||||
A17 = values && values[1][0];
|
||||
A18 = values && values[2][0];
|
||||
A19 = values && values[3][0];
|
||||
|
||||
expect(A16).toBeNull();
|
||||
expect(A17).toBeNull();
|
||||
expect(A18).toBeNull();
|
||||
expect(A19).toBeNull();
|
||||
|
||||
// redo
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
values = getValues(15, 0, 18, 0);
|
||||
A16 = values && values[0][0];
|
||||
A17 = values && values[1][0];
|
||||
A18 = values && values[2][0];
|
||||
A19 = values && values[3][0];
|
||||
|
||||
expect(A16?.v).toStrictEqual(18);
|
||||
expect(A17?.f).toEqual('=B17');
|
||||
expect(A18?.v).toEqual(19);
|
||||
expect(A19?.si).toEqual(A17?.si);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import {
|
||||
BEFORE_CELL_EDIT,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { IEditorBridgeService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaEditorShowController } from '../formula-editor-show.controller';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
type EditLocation = NonNullable<ReturnType<IEditorBridgeService['getEditLocation']>>;
|
||||
|
||||
let activeUniver: Univer | null = null;
|
||||
|
||||
function createEditLocation(isInArrayFormulaRange = false): EditLocation {
|
||||
return {
|
||||
unitId: 'test',
|
||||
sheetId: 'sheet1',
|
||||
row: 1,
|
||||
column: 1,
|
||||
isInArrayFormulaRange,
|
||||
} as EditLocation;
|
||||
}
|
||||
|
||||
function createControllerTestBed(editLocation = createEditLocation()) {
|
||||
let injector: Injector;
|
||||
const renderContext = {
|
||||
unitId: 'test',
|
||||
scene: {
|
||||
onTransformChange$: {
|
||||
subscribeEvent: () => ({ dispose: vi.fn() }),
|
||||
},
|
||||
},
|
||||
};
|
||||
const createSheetSkeletonManagerService = () =>
|
||||
injector.createInstance(SheetSkeletonManagerService, renderContext as never);
|
||||
const createFormulaEditorShowController = () =>
|
||||
injector.createInstance(FormulaEditorShowController, renderContext as never);
|
||||
const testBed = createCommandTestBed(undefined, [
|
||||
[
|
||||
SheetSkeletonManagerService,
|
||||
{ useFactory: createSheetSkeletonManagerService },
|
||||
],
|
||||
[
|
||||
FormulaEditorShowController,
|
||||
{ useFactory: createFormulaEditorShowController },
|
||||
],
|
||||
]);
|
||||
activeUniver = testBed.univer;
|
||||
injector = testBed.univer.__getInjector();
|
||||
testBed.sheet.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'Sheet2', cellData: {} });
|
||||
|
||||
const commandService = testBed.get(ICommandService);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
|
||||
const editorBridgeService = testBed.get(IEditorBridgeService);
|
||||
vi.spyOn(editorBridgeService, 'getEditLocation').mockReturnValue(editLocation);
|
||||
testBed.get(FormulaEditorShowController);
|
||||
|
||||
return {
|
||||
commandService,
|
||||
editorBridgeService,
|
||||
formulaDataModel: testBed.get(FormulaDataModel),
|
||||
get: testBed.get,
|
||||
injector,
|
||||
refreshEditCellState: vi.spyOn(editorBridgeService, 'refreshEditCellState'),
|
||||
sheet: testBed.sheet,
|
||||
sheetInterceptorService: testBed.get(SheetInterceptorService),
|
||||
};
|
||||
}
|
||||
|
||||
function createSetRangeValuesParams(subUnitId = 'sheet1'): ISetRangeValuesMutationParams {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId,
|
||||
cellValue: {
|
||||
0: {
|
||||
1: { v: 2 },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setSpillCellData(formulaDataModel: FormulaDataModel): void {
|
||||
formulaDataModel.setArrayFormulaCellData({
|
||||
test: {
|
||||
sheet1: {
|
||||
1: {
|
||||
1: { v: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
activeUniver?.dispose();
|
||||
activeUniver = null;
|
||||
});
|
||||
|
||||
describe('FormulaEditorShowController', () => {
|
||||
it('preserves the formula string when starting to edit a calculated formula cell', () => {
|
||||
const testBed = createControllerTestBed();
|
||||
const worksheet = testBed.sheet.getSheetBySheetId('sheet1');
|
||||
if (worksheet == null) {
|
||||
throw new Error('Expected sheet1 to exist');
|
||||
}
|
||||
|
||||
const result = testBed.sheetInterceptorService.writeCellInterceptor
|
||||
.fetchThroughInterceptors(BEFORE_CELL_EDIT)(
|
||||
{ v: 3 },
|
||||
{
|
||||
row: 0,
|
||||
col: 1,
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
worksheet,
|
||||
workbook: testBed.sheet,
|
||||
origin: worksheet.getCellRaw(0, 1),
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toEqual({ v: 3, f: '=SUM(A1)' });
|
||||
});
|
||||
|
||||
it('passes an empty edit value through unchanged', () => {
|
||||
const testBed = createControllerTestBed();
|
||||
const worksheet = testBed.sheet.getSheetBySheetId('sheet1');
|
||||
if (worksheet == null) {
|
||||
throw new Error('Expected sheet1 to exist');
|
||||
}
|
||||
|
||||
const result = testBed.sheetInterceptorService.writeCellInterceptor
|
||||
.fetchThroughInterceptors(BEFORE_CELL_EDIT)(
|
||||
null,
|
||||
{
|
||||
row: 0,
|
||||
col: 1,
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
worksheet,
|
||||
workbook: testBed.sheet,
|
||||
origin: worksheet.getCellRaw(0, 1),
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('refreshes when the current cell becomes a spill cell', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('refreshes when the current cell was a spill cell before its array data was removed', async () => {
|
||||
const testBed = createControllerTestBed(createEditLocation(true));
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not refresh for a formula result on a non-spill cell', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not refresh for an ordinary range value mutation', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams()
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not refresh for a formula result on another sheet', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams('sheet2'),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not refresh while the editor is visible', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
testBed.editorBridgeService.changeVisible({
|
||||
visible: true,
|
||||
eventType: DeviceInputEventType.Dblclick,
|
||||
unitId: 'test',
|
||||
});
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes only for the controller associated with the result workbook', async () => {
|
||||
const testBed = createControllerTestBed();
|
||||
const otherWorkbookController = testBed.injector.createInstance(
|
||||
FormulaEditorShowController,
|
||||
{ unitId: 'other' } as never
|
||||
);
|
||||
setSpillCellData(testBed.formulaDataModel);
|
||||
|
||||
const result = await testBed.commandService.executeCommand(
|
||||
SetRangeValuesMutation.id,
|
||||
createSetRangeValuesParams(),
|
||||
{ applyFormulaCalculationResult: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(testBed.refreshEditCellState).toHaveBeenCalledOnce();
|
||||
otherWorkbookController.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 { ErrorType, extractFormulaError } from '@univerjs/engine-formula';
|
||||
import { INTERCEPTOR_POINT } from '@univerjs/sheets';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FormulaRenderManagerController } from '../formula-render.controller';
|
||||
|
||||
vi.mock('@univerjs/engine-formula', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/engine-formula')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
extractFormulaError: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('FormulaRenderManagerController', () => {
|
||||
it('marks formula error cells without mutating raw cell data', () => {
|
||||
let interceptor: any;
|
||||
const disposable = { dispose: vi.fn() };
|
||||
const sheetInterceptorService = {
|
||||
intercept: vi.fn((point, config) => {
|
||||
interceptor = config;
|
||||
return disposable;
|
||||
}),
|
||||
};
|
||||
const formulaDataModel = {
|
||||
getArrayFormulaCellData: vi.fn(() => ({ unit: { sheet: { 1: { 2: { f: '=A1' } } } } })),
|
||||
};
|
||||
const controller = new FormulaRenderManagerController(sheetInterceptorService as any, formulaDataModel as any);
|
||||
const rawCell = { v: '#DIV/0!', markers: { br: { size: 1 } } };
|
||||
vi.mocked(extractFormulaError).mockReturnValue(ErrorType.DIV_BY_ZERO);
|
||||
|
||||
const result = interceptor.handler(rawCell, {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
row: 1,
|
||||
col: 2,
|
||||
rawData: rawCell,
|
||||
}, (cell: unknown) => cell);
|
||||
|
||||
expect(sheetInterceptorService.intercept).toHaveBeenCalledWith(
|
||||
INTERCEPTOR_POINT.CELL_CONTENT,
|
||||
expect.objectContaining({ priority: 10 })
|
||||
);
|
||||
expect(result).not.toBe(rawCell);
|
||||
expect(result.markers).toEqual(expect.objectContaining({
|
||||
br: { size: 1 },
|
||||
tl: { size: 6, color: '#409f11' },
|
||||
}));
|
||||
expect(extractFormulaError).toHaveBeenCalledWith(rawCell, true);
|
||||
|
||||
controller.dispose();
|
||||
expect(disposable.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes through cells when there is no formula error or cell data', () => {
|
||||
let interceptor: any;
|
||||
const controller = new FormulaRenderManagerController({
|
||||
intercept: vi.fn((_point, config) => {
|
||||
interceptor = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as any, {
|
||||
getArrayFormulaCellData: vi.fn(() => null),
|
||||
} as any);
|
||||
|
||||
vi.mocked(extractFormulaError).mockReturnValueOnce(null as never);
|
||||
expect(interceptor.handler({ v: 'plain' }, {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
row: 0,
|
||||
col: 0,
|
||||
rawData: { v: 'plain' },
|
||||
}, (cell: unknown) => cell)).toEqual({ v: 'plain' });
|
||||
|
||||
vi.mocked(extractFormulaError).mockReturnValueOnce(ErrorType.VALUE);
|
||||
expect(interceptor.handler(null, {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
row: 0,
|
||||
col: 0,
|
||||
rawData: null,
|
||||
}, (cell: unknown) => cell)).toBeNull();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 { LocaleType } from '@univerjs/core';
|
||||
import { ReorderRangeCommand, SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { FormulaReorderController } from '../formula-reorder.controller';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'unit',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: 'Test workbook',
|
||||
styles: {},
|
||||
sheetOrder: ['sheet'],
|
||||
sheets: {
|
||||
sheet: {
|
||||
id: 'sheet',
|
||||
name: 'Sheet 1',
|
||||
cellData: {
|
||||
0: { 0: { v: 'old-row' } },
|
||||
1: { 0: { f: '=A2', si: 'shared' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('FormulaReorderController', () => {
|
||||
let univer: ReturnType<typeof createCommandTestBed>['univer'];
|
||||
let controller: FormulaReorderController;
|
||||
let sheetInterceptorService: SheetInterceptorService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(WORKBOOK_DATA, [[FormulaReorderController]]);
|
||||
univer = testBed.univer;
|
||||
controller = testBed.get(FormulaReorderController);
|
||||
sheetInterceptorService = testBed.get(SheetInterceptorService);
|
||||
});
|
||||
|
||||
afterEach(() => univer.dispose());
|
||||
|
||||
it('creates redo and undo mutations with formula references shifted to the reordered row', () => {
|
||||
const result = sheetInterceptorService.onCommandExecute({
|
||||
id: ReorderRangeCommand.id,
|
||||
params: {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 },
|
||||
order: { 0: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.redos).toEqual([{
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
cellValue: {
|
||||
0: {
|
||||
0: { f: '=A1', si: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
expect(result.undos).toEqual([{
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId: 'unit',
|
||||
subUnitId: 'sheet',
|
||||
cellValue: {
|
||||
0: {
|
||||
0: { v: 'old-row' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
|
||||
controller.dispose();
|
||||
expect(sheetInterceptorService.onCommandExecute({ id: ReorderRangeCommand.id, params: {} })).toEqual({
|
||||
preUndos: [],
|
||||
undos: [],
|
||||
preRedos: [],
|
||||
redos: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty mutations for non-reorder commands, missing worksheets and ranges without formulas', () => {
|
||||
expect(sheetInterceptorService.onCommandExecute({ id: 'other-command', params: {} })).toMatchObject({ redos: [], undos: [] });
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: ReorderRangeCommand.id,
|
||||
params: { unitId: 'unit', subUnitId: 'missing', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, order: {} },
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: ReorderRangeCommand.id,
|
||||
params: { unitId: 'unit', subUnitId: 'sheet', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 }, order: {} },
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ImageFormulaRenderController } from '../image-formula-render.controller';
|
||||
|
||||
describe('ImageFormulaRenderController', () => {
|
||||
it('refreshes sheet skeleton and marks the main component dirty for image formula changes', () => {
|
||||
let refresh: () => void = () => undefined;
|
||||
const skeletonManagerService = { reCalculate: vi.fn() };
|
||||
const mainComponent = { makeDirty: vi.fn() };
|
||||
const render = {
|
||||
with: vi.fn((token) => token === SheetSkeletonManagerService ? skeletonManagerService : null),
|
||||
mainComponent,
|
||||
};
|
||||
const workbook = { getUnitId: vi.fn(() => 'unit-1') };
|
||||
|
||||
new ImageFormulaRenderController(
|
||||
{ registerRefreshRenderFunction: vi.fn((fn) => { refresh = fn; }) } as any,
|
||||
{ getRenderUnitById: vi.fn(() => render) } as any,
|
||||
{ getCurrentUnitOfType: vi.fn((type) => type === UniverInstanceType.UNIVER_SHEET ? workbook : null) } as any
|
||||
);
|
||||
|
||||
refresh();
|
||||
|
||||
expect(render.with).toHaveBeenCalledWith(SheetSkeletonManagerService);
|
||||
expect(skeletonManagerService.reCalculate).toHaveBeenCalled();
|
||||
expect(mainComponent.makeDirty).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when there is no workbook, render, or main component', () => {
|
||||
let refresh: () => void = () => undefined;
|
||||
const imageFormulaController = { registerRefreshRenderFunction: vi.fn((fn) => {
|
||||
refresh = fn;
|
||||
}) };
|
||||
const renderManagerService = { getRenderUnitById: vi.fn<(...args: any[]) => any>(() => null) };
|
||||
const univerInstanceService = { getCurrentUnitOfType: vi.fn<(...args: any[]) => any>(() => null) };
|
||||
|
||||
new ImageFormulaRenderController(
|
||||
imageFormulaController as any,
|
||||
renderManagerService as any,
|
||||
univerInstanceService as any
|
||||
);
|
||||
|
||||
refresh();
|
||||
expect(renderManagerService.getRenderUnitById).not.toHaveBeenCalled();
|
||||
|
||||
univerInstanceService.getCurrentUnitOfType.mockReturnValue({ getUnitId: () => 'unit-1' });
|
||||
refresh();
|
||||
expect(renderManagerService.getRenderUnitById).toHaveBeenCalledWith('unit-1');
|
||||
|
||||
renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
with: vi.fn(() => ({ reCalculate: vi.fn() })),
|
||||
mainComponent: null,
|
||||
});
|
||||
expect(() => refresh()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,763 @@
|
||||
/**
|
||||
* 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, IWorkbookData, Nullable, Univer, Workbook } from '@univerjs/core';
|
||||
import type { IDeleteRangeMoveLeftCommandParams, IDeleteRangeMoveUpCommandParams, IInsertColCommandParams, IInsertRangeMoveDownCommandParams, IInsertRangeMoveRightCommandParams, IInsertRowCommandParams, IMoveColsCommandParams, IMoveRangeCommandParams, IMoveRowsCommandParams, IRemoveRowColCommandParams, IRemoveSheetCommandParams, ISetWorksheetNameCommandParams } from '@univerjs/sheets';
|
||||
import { Direction, ICommandService, IUniverInstanceService, LocaleType, RANGE_TYPE, RedoCommand, UndoCommand, UniverInstanceType } from '@univerjs/core';
|
||||
import { IDefinedNamesService, RemoveDefinedNameMutation, SetArrayFormulaDataMutation, SetDefinedNameMutation, SetFormulaDataMutation } from '@univerjs/engine-formula';
|
||||
import { DeleteRangeMoveLeftCommand, DeleteRangeMoveUpCommand, InsertColByRangeCommand, InsertColCommand, InsertColMutation, InsertRangeMoveDownCommand, InsertRangeMoveRightCommand, InsertRowByRangeCommand, InsertRowCommand, InsertRowMutation, MoveColsCommand, MoveColsMutation, MoveRangeCommand, MoveRangeMutation, MoveRowsCommand, MoveRowsMutation, RemoveColByRangeCommand, RemoveColCommand, RemoveColMutation, RemoveRowByRangeCommand, RemoveRowCommand, RemoveRowMutation, RemoveSheetCommand, RemoveSheetMutation, SetDefinedNameCommand, SetRangeValuesCommand, SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetNameCommand, SetWorksheetNameMutation, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { UpdateDefinedNameController } from '@univerjs/sheets-formula';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createCommandTestBed } from './create-command-test-bed';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO = (): IWorkbookData => ({
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
cellData: {},
|
||||
name: 'Sheet1',
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
cellData: {},
|
||||
name: 'Sheet2',
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {},
|
||||
resources: [
|
||||
{
|
||||
name: 'SHEET_DEFINED_NAME_PLUGIN',
|
||||
data: '{"soAI3OK4sq":{"id":"soAI3OK4sq","name":"DefinedName1","formulaOrRefString":"Sheet1!$A$1:$B$2","comment":"","localSheetId":"AllDefaultWorkbook"},"qwert12345":{"id":"qwert12345","name":"DefinedName2","formulaOrRefString":"Sheet1!$1:$3","comment":"","localSheetId":"AllDefaultWorkbook"},"asdfg67890":{"id":"asdfg67890","name":"DefinedName3","formulaOrRefString":"Sheet1!$A:$C","comment":"","localSheetId":"AllDefaultWorkbook"}}',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('Test update defined name', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let getValues: (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
sheetId?: string
|
||||
) => Array<Array<Nullable<ICellData>>> | undefined;
|
||||
let getDefinedNameRef: (name?: string) => string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed(TEST_WORKBOOK_DATA_DEMO(), [
|
||||
[UpdateDefinedNameController],
|
||||
]);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
get(UpdateDefinedNameController);
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(MoveRangeCommand);
|
||||
commandService.registerCommand(MoveRangeMutation);
|
||||
|
||||
commandService.registerCommand(MoveRowsCommand);
|
||||
commandService.registerCommand(MoveRowsMutation);
|
||||
|
||||
commandService.registerCommand(MoveColsCommand);
|
||||
commandService.registerCommand(MoveColsMutation);
|
||||
|
||||
commandService.registerCommand(InsertRowCommand);
|
||||
commandService.registerCommand(InsertRowMutation);
|
||||
|
||||
commandService.registerCommand(InsertColCommand);
|
||||
commandService.registerCommand(InsertColMutation);
|
||||
|
||||
commandService.registerCommand(RemoveRowCommand);
|
||||
commandService.registerCommand(RemoveRowMutation);
|
||||
|
||||
commandService.registerCommand(RemoveColCommand);
|
||||
commandService.registerCommand(RemoveColMutation);
|
||||
|
||||
commandService.registerCommand(DeleteRangeMoveLeftCommand);
|
||||
commandService.registerCommand(DeleteRangeMoveUpCommand);
|
||||
commandService.registerCommand(InsertRangeMoveDownCommand);
|
||||
commandService.registerCommand(InsertRangeMoveRightCommand);
|
||||
|
||||
commandService.registerCommand(SetWorksheetNameCommand);
|
||||
commandService.registerCommand(SetWorksheetNameMutation);
|
||||
commandService.registerCommand(RemoveSheetCommand);
|
||||
commandService.registerCommand(RemoveSheetMutation);
|
||||
|
||||
commandService.registerCommand(SetSelectionsOperation);
|
||||
commandService.registerCommand(SetRangeValuesCommand);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
commandService.registerCommand(SetFormulaDataMutation);
|
||||
commandService.registerCommand(SetArrayFormulaDataMutation);
|
||||
|
||||
// set defined name
|
||||
commandService.registerCommand(SetDefinedNameCommand);
|
||||
commandService.registerCommand(SetDefinedNameMutation);
|
||||
commandService.registerCommand(RemoveDefinedNameMutation);
|
||||
|
||||
[
|
||||
RemoveColByRangeCommand,
|
||||
RemoveRowByRangeCommand,
|
||||
InsertRowByRangeCommand,
|
||||
InsertColByRangeCommand,
|
||||
].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
|
||||
getValues = (
|
||||
startRow: number,
|
||||
startColumn: number,
|
||||
endRow: number,
|
||||
endColumn: number,
|
||||
sheetId: string = 'sheet1'
|
||||
): Array<Array<Nullable<ICellData>>> | undefined =>
|
||||
get(IUniverInstanceService)
|
||||
.getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)
|
||||
?.getSheetBySheetId(sheetId)
|
||||
?.getRange(startRow, startColumn, endRow, endColumn)
|
||||
.getValues();
|
||||
|
||||
getDefinedNameRef = (name: string = 'DefinedName1') => {
|
||||
const definedNameService = get(IDefinedNamesService);
|
||||
const definedName = definedNameService.getValueByName('test', name);
|
||||
if (definedName) {
|
||||
return definedName.formulaOrRefString;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
describe('update defined name', () => {
|
||||
it('Move range, update reference', async () => {
|
||||
const params: IMoveRangeCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 3,
|
||||
endRow: 1,
|
||||
endColumn: 4,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$D$1:$E$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$D$1:$E$2');
|
||||
});
|
||||
|
||||
it('Move rows, update reference', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 4,
|
||||
startColumn: 0,
|
||||
endRow: 4,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$4');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$4');
|
||||
});
|
||||
|
||||
it('Move rows to range start', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$2:$3');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$2:$3');
|
||||
});
|
||||
|
||||
it('Move rows to range end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 3,
|
||||
startColumn: 0,
|
||||
endRow: 3,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
});
|
||||
|
||||
it('Move first row beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 0,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 9,
|
||||
startColumn: 0,
|
||||
endRow: 9,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$2');
|
||||
});
|
||||
|
||||
it('Move last row beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 2, startColumn: 0, endRow: 2, endColumn: 0, rangeType: RANGE_TYPE.ROW },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveRowsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 2,
|
||||
startColumn: 0,
|
||||
endRow: 2,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 9,
|
||||
startColumn: 0,
|
||||
endRow: 9,
|
||||
endColumn: 19,
|
||||
rangeType: 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$9');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$3');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName2')).toBe('Sheet1!$1:$9');
|
||||
});
|
||||
|
||||
it('Move columns, update reference', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 4,
|
||||
endRow: 999,
|
||||
endColumn: 4,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
});
|
||||
|
||||
it('Move columns to range start', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 1, endRow: 0, endColumn: 1, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 1,
|
||||
endRow: 999,
|
||||
endColumn: 1,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$B:$C');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$B:$C');
|
||||
});
|
||||
|
||||
it('Move columns to range end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 3,
|
||||
endRow: 999,
|
||||
endColumn: 3,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
});
|
||||
|
||||
it('Move first column beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 999,
|
||||
endColumn: 0,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 9,
|
||||
endRow: 999,
|
||||
endColumn: 9,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$B');
|
||||
});
|
||||
|
||||
it('Move last column beyond the end', async () => {
|
||||
const selectionManager = get(SheetsSelectionsService);
|
||||
|
||||
// A1
|
||||
selectionManager.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 2, endRow: 0, endColumn: 2, rangeType: RANGE_TYPE.COLUMN },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const params: IMoveColsCommandParams = {
|
||||
fromRange: {
|
||||
startRow: 0,
|
||||
startColumn: 2,
|
||||
endRow: 999,
|
||||
endColumn: 2,
|
||||
rangeType: 2,
|
||||
},
|
||||
toRange: {
|
||||
startRow: 0,
|
||||
startColumn: 9,
|
||||
endRow: 999,
|
||||
endColumn: 9,
|
||||
rangeType: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$I');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$C');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef('DefinedName3')).toBe('Sheet1!$A:$I');
|
||||
});
|
||||
|
||||
it('Insert row, update reference', async () => {
|
||||
const params: IInsertRowCommandParams = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: {
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
},
|
||||
direction: Direction.UP,
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertRowCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
});
|
||||
|
||||
it('Insert column, update reference', async () => {
|
||||
const params: IInsertColCommandParams = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 14,
|
||||
},
|
||||
direction: Direction.LEFT,
|
||||
cellValue: {},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertColCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
});
|
||||
|
||||
it('Remove row, update reference', async () => {
|
||||
const params: IRemoveRowColCommandParams = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(RemoveRowCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
});
|
||||
|
||||
it('Remove column, update reference', async () => {
|
||||
const params: IRemoveRowColCommandParams = {
|
||||
range: {
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
startRow: 0,
|
||||
endRow: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(RemoveColCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
});
|
||||
|
||||
it('Delete move left, update reference', async () => {
|
||||
const params: IDeleteRangeMoveLeftCommandParams = {
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 1,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(DeleteRangeMoveLeftCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$A$2');
|
||||
});
|
||||
|
||||
it('Delete move up, update reference', async () => {
|
||||
const params: IDeleteRangeMoveUpCommandParams = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(DeleteRangeMoveUpCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$1');
|
||||
});
|
||||
|
||||
it('Insert move down, update reference', async () => {
|
||||
const params: IInsertRangeMoveDownCommandParams = {
|
||||
range: {
|
||||
startRow: 1,
|
||||
startColumn: 0,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$3');
|
||||
});
|
||||
|
||||
it('Insert move right, update reference', async () => {
|
||||
const params: IInsertRangeMoveRightCommandParams = {
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 1,
|
||||
endRow: 1,
|
||||
endColumn: 1,
|
||||
rangeType: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$B$2');
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1!$A$1:$C$2');
|
||||
});
|
||||
|
||||
it('set worksheet name', async () => {
|
||||
const params: ISetWorksheetNameCommandParams = {
|
||||
subUnitId: 'sheet1',
|
||||
name: 'Sheet1Rename',
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(SetWorksheetNameCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('Sheet1Rename!$A$1:$B$2');
|
||||
});
|
||||
|
||||
it('remove worksheet', async () => {
|
||||
const params: IRemoveSheetCommandParams = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
};
|
||||
|
||||
expect(await commandService.executeCommand(RemoveSheetCommand.id, params)).toBeTruthy();
|
||||
expect(getDefinedNameRef()).toBe('#REF!');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 { Disposable, Inject } from '@univerjs/core';
|
||||
import {
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
} from '@univerjs/icons';
|
||||
import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, RANGE_SELECTOR_COMPONENT_KEY } from '@univerjs/sheets-ui';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { FormulaEditor } from '../views/formula-editor/index';
|
||||
import { MORE_FUNCTIONS_COMPONENT } from '../views/more-functions/interface';
|
||||
import { MoreFunctions } from '../views/more-functions/MoreFunctions';
|
||||
import { RangeSelector } from '../views/range-selector';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(MORE_FUNCTIONS_COMPONENT, MoreFunctions));
|
||||
|
||||
this.disposeWithMe(this._componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, RangeSelector));
|
||||
|
||||
this.disposeWithMe(this._componentManager.register(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, FormulaEditor));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../locale/types';
|
||||
import { Disposable, Inject, isICellData, LocaleService } from '@univerjs/core';
|
||||
import { ErrorType, extractFormulaError, FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { CellAlertManagerService, CellAlertType, HoverManagerService } from '@univerjs/sheets-ui';
|
||||
import { debounceTime } from 'rxjs';
|
||||
|
||||
const ALERT_KEY = 'SHEET_FORMULA_ALERT';
|
||||
|
||||
const ErrorTypeToMessageMap = {
|
||||
[ErrorType.DIV_BY_ZERO]: 'divByZero',
|
||||
[ErrorType.NAME]: 'name',
|
||||
[ErrorType.VALUE]: 'value',
|
||||
[ErrorType.NUM]: 'num',
|
||||
[ErrorType.NA]: 'na',
|
||||
[ErrorType.CYCLE]: 'cycle',
|
||||
[ErrorType.REF]: 'ref',
|
||||
[ErrorType.SPILL]: 'spill',
|
||||
[ErrorType.CALC]: 'calc',
|
||||
[ErrorType.ERROR]: 'error',
|
||||
[ErrorType.CONNECT]: 'connect',
|
||||
[ErrorType.NULL]: 'null',
|
||||
};
|
||||
|
||||
export class FormulaAlertRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(HoverManagerService) private readonly _hoverManagerService: HoverManagerService,
|
||||
@Inject(CellAlertManagerService) private readonly _cellAlertManagerService: CellAlertManagerService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._initCellAlertPopup();
|
||||
}
|
||||
|
||||
private _initCellAlertPopup() {
|
||||
this.disposeWithMe(this._hoverManagerService.currentCell$.pipe(debounceTime(100)).subscribe((cellPos) => {
|
||||
if (cellPos) {
|
||||
const workbook = this._context.unit;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
|
||||
if (!worksheet) return this._hideAlert();
|
||||
|
||||
const cellData = worksheet.getCell(cellPos.location.row, cellPos.location.col);
|
||||
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData()?.
|
||||
[cellPos.location.unitId]?.
|
||||
[cellPos.location.subUnitId]?.
|
||||
[cellPos.location.row]?.
|
||||
[cellPos.location.col];
|
||||
|
||||
// Preventing blank object
|
||||
if (isICellData(cellData)) {
|
||||
const errorType = extractFormulaError(cellData, !!arrayFormulaCellData);
|
||||
|
||||
if (!errorType) {
|
||||
// fix #4002
|
||||
this._hideAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentAlert = this._cellAlertManagerService.currentAlert.get(ALERT_KEY);
|
||||
const currentLoc = currentAlert?.alert?.location;
|
||||
if (
|
||||
currentLoc &&
|
||||
currentLoc.row === cellPos.location.row &&
|
||||
currentLoc.col === cellPos.location.col &&
|
||||
currentLoc.subUnitId === cellPos.location.subUnitId &&
|
||||
currentLoc.unitId === cellPos.location.unitId
|
||||
) {
|
||||
this._hideAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
this._cellAlertManagerService.showAlert({
|
||||
type: CellAlertType.ERROR,
|
||||
title: this._localeService.t<LocaleKey>('sheets-formula-ui.error.title'),
|
||||
message: this._localeService.t(`sheets-formula-ui.error.${ErrorTypeToMessageMap[errorType]}`),
|
||||
location: cellPos.location,
|
||||
width: 200,
|
||||
height: 74,
|
||||
key: ALERT_KEY,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._hideAlert();
|
||||
}));
|
||||
}
|
||||
|
||||
private _hideAlert() {
|
||||
this._cellAlertManagerService.removeAlert(ALERT_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
/**
|
||||
* 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, ICellData, ICellDataWithSpanAndDisplay, IMutationInfo, IRange, Nullable, Workbook, Worksheet } from '@univerjs/core';
|
||||
import type { IDiscreteRange, ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import type { ICellDataWithSpanInfo, ICopyPastePayload, IPasteHookValueType, ISheetClipboardHook, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
|
||||
import {
|
||||
DEFAULT_EMPTY_DOCUMENT_VALUE,
|
||||
Disposable,
|
||||
generateRandomId,
|
||||
getEmptyCell,
|
||||
Inject,
|
||||
Injector,
|
||||
isFormulaId,
|
||||
isFormulaString,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '@univerjs/sheets';
|
||||
import { COPY_TYPE, ISheetClipboardService, PREDEFINED_HOOK_NAME_COPY, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
|
||||
|
||||
export const DEFAULT_PASTE_FORMULA = 'default-paste-formula';
|
||||
|
||||
export class FormulaClipboardController extends Disposable {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder,
|
||||
@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._registerClipboardHook();
|
||||
}
|
||||
|
||||
private _registerClipboardHook() {
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._copyFormulaOnlyHook()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._pasteFormulaHook()));
|
||||
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(this._pasteWithFormulaHook()));
|
||||
}
|
||||
|
||||
private _copyFormulaOnlyHook(): ISheetClipboardHook {
|
||||
const self = this;
|
||||
let currentSheet: Nullable<Worksheet> = null;
|
||||
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_COPY.SPECIAL_COPY_FORMULA_ONLY,
|
||||
priority: 10,
|
||||
onBeforeCopy(unitId, subUnitId) {
|
||||
currentSheet = self._getWorksheet(unitId, subUnitId);
|
||||
},
|
||||
onCopyCellContent(row: number, col: number): string {
|
||||
if (!currentSheet) return '';
|
||||
|
||||
const cell = currentSheet.getCellRaw(row, col);
|
||||
if (!cell) return '';
|
||||
|
||||
if (isFormulaString(cell.f)) return cell.f as string;
|
||||
if (isFormulaId(cell.si)) {
|
||||
const formulaDataModel = self._formulaDataModel;
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(row, col, currentSheet.getSheetId(), currentSheet.getUnitId());
|
||||
return formulaString || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
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;
|
||||
},
|
||||
handleMatrixOnCell(
|
||||
row: number,
|
||||
column: number,
|
||||
rowIndexInMatrix: number,
|
||||
columnIndexInMatrix: number,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanAndDisplay>,
|
||||
matrixFragment: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
plainMatrix: ObjectMatrix<ICellDataWithSpanAndDisplay>
|
||||
) {
|
||||
const cellData = matrix.getValue(row, column);
|
||||
if (currentSheet && cellData && (isFormulaString(cellData.f) || isFormulaId(cellData.si))) {
|
||||
const formulaString = isFormulaString(cellData.f)
|
||||
? (cellData.f as string)
|
||||
: self._formulaDataModel.getFormulaStringByCell(row, column, currentSheet.getSheetId(), currentSheet.getUnitId()) as string;
|
||||
matrixFragment.setValue(rowIndexInMatrix, columnIndexInMatrix, {
|
||||
...getEmptyCell(),
|
||||
f: formulaString,
|
||||
});
|
||||
plainMatrix.setValue(rowIndexInMatrix, columnIndexInMatrix, {
|
||||
...getEmptyCell(),
|
||||
f: formulaString,
|
||||
displayV: formulaString,
|
||||
});
|
||||
} else {
|
||||
matrix.setValue(row, column, getEmptyCell());
|
||||
matrixFragment.setValue(rowIndexInMatrix, columnIndexInMatrix, getEmptyCell());
|
||||
plainMatrix.setValue(rowIndexInMatrix, columnIndexInMatrix, getEmptyCell());
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _pasteFormulaHook(): ISheetClipboardHook {
|
||||
return {
|
||||
id: PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
priority: 10,
|
||||
specialPasteInfo: { label: 'specialPaste.formula' },
|
||||
onPasteCells: (pasteFrom, pasteTo, data, payload) => this._onPasteCells(pasteFrom, pasteTo, data, payload, true),
|
||||
};
|
||||
}
|
||||
|
||||
private _pasteWithFormulaHook(): ISheetClipboardHook {
|
||||
return {
|
||||
id: DEFAULT_PASTE_FORMULA,
|
||||
priority: 10,
|
||||
onPasteCells: (pasteFrom, pasteTo, data, payload) => this._onPasteCells(pasteFrom, pasteTo, data, payload, false),
|
||||
};
|
||||
}
|
||||
|
||||
private _getWorkbook(unitId?: string): Nullable<Workbook> {
|
||||
if (unitId) {
|
||||
return this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
}
|
||||
|
||||
return this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
}
|
||||
|
||||
private _getWorksheet(unitId?: string, subUnitId?: string): Nullable<Worksheet> {
|
||||
const workbook = this._getWorkbook(unitId);
|
||||
|
||||
if (subUnitId) {
|
||||
return workbook?.getSheetBySheetId(subUnitId);
|
||||
}
|
||||
|
||||
return workbook?.getActiveSheet();
|
||||
}
|
||||
|
||||
private _onPasteCells(
|
||||
pasteFrom: ISheetDiscreteRangeLocation | null,
|
||||
pasteTo: ISheetDiscreteRangeLocation,
|
||||
data: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
payload: ICopyPastePayload,
|
||||
isSpecialPaste: boolean
|
||||
) {
|
||||
// Intercept scenarios where formulas do not need to be processed, and only process default paste and paste formulas only
|
||||
const specialPastes: IPasteHookValueType[] = [
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
];
|
||||
if (specialPastes.includes(payload.pasteType)) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (payload.copyType === COPY_TYPE.CUT && !isSpecialPaste) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
const workbook = this._getWorkbook();
|
||||
const unitId = pasteTo.unitId || workbook?.getUnitId();
|
||||
const subUnitId = pasteTo.subUnitId || workbook?.getActiveSheet()?.getSheetId();
|
||||
|
||||
if (!unitId || !subUnitId) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
const pastedRange = pasteTo.range;
|
||||
const matrix = data;
|
||||
const copyInfo = {
|
||||
copyType: payload.copyType || COPY_TYPE.COPY,
|
||||
copyRange: pasteFrom?.range,
|
||||
pasteType: payload.pasteType,
|
||||
};
|
||||
|
||||
return this._injector.invoke((accessor) => getSetCellFormulaMutations(
|
||||
unitId,
|
||||
subUnitId,
|
||||
pastedRange,
|
||||
matrix,
|
||||
accessor,
|
||||
copyInfo,
|
||||
this._lexerTreeBuilder,
|
||||
this._formulaDataModel,
|
||||
isSpecialPaste,
|
||||
pasteFrom
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export function getSetCellFormulaMutations(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
accessor: IAccessor,
|
||||
copyInfo: {
|
||||
copyType: COPY_TYPE;
|
||||
copyRange?: IDiscreteRange;
|
||||
pasteType: string;
|
||||
},
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
_isSpecialPaste = false,
|
||||
pasteFrom: ISheetDiscreteRangeLocation | null
|
||||
) {
|
||||
const redoMutationsInfo: IMutationInfo[] = [];
|
||||
const undoMutationsInfo: IMutationInfo[] = [];
|
||||
|
||||
const valueMatrix = getValueMatrix(unitId, subUnitId, range, matrix, copyInfo, lexerTreeBuilder, formulaDataModel, pasteFrom);
|
||||
|
||||
if (!valueMatrix.hasValue()) {
|
||||
return {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
}
|
||||
|
||||
// set cell value and style
|
||||
const setValuesMutation: ISetRangeValuesMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: valueMatrix.clone(),
|
||||
};
|
||||
|
||||
redoMutationsInfo.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: setValuesMutation,
|
||||
});
|
||||
|
||||
// undo
|
||||
const undoSetValuesMutation: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory(
|
||||
accessor,
|
||||
setValuesMutation
|
||||
);
|
||||
|
||||
undoMutationsInfo.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: undoSetValuesMutation,
|
||||
});
|
||||
return {
|
||||
undos: undoMutationsInfo,
|
||||
redos: redoMutationsInfo,
|
||||
};
|
||||
}
|
||||
|
||||
function getValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
copyInfo: {
|
||||
copyType: COPY_TYPE;
|
||||
copyRange?: IDiscreteRange;
|
||||
pasteType: string;
|
||||
},
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation | null
|
||||
): ObjectMatrix<ICellData> {
|
||||
if (!pasteFrom) {
|
||||
return getValueMatrixOfPasteFromIsNull(unitId, subUnitId, range, matrix, formulaDataModel);
|
||||
}
|
||||
|
||||
if (copyInfo.pasteType === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE) {
|
||||
return getSpecialPasteValueValueMatrix(unitId, subUnitId, range, matrix, formulaDataModel, pasteFrom);
|
||||
}
|
||||
|
||||
if (copyInfo.pasteType === PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA) {
|
||||
return getSpecialPasteFormulaValueMatrix(unitId, subUnitId, range, matrix, lexerTreeBuilder, formulaDataModel, pasteFrom);
|
||||
}
|
||||
|
||||
return getDefaultPasteValueMatrix(unitId, subUnitId, range, matrix, copyInfo.copyType, lexerTreeBuilder, formulaDataModel, pasteFrom);
|
||||
}
|
||||
|
||||
function getValueMatrixOfPasteFromIsNull(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
formulaDataModel: FormulaDataModel
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const formulaData = formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaString(value.v)) {
|
||||
// If the copy value is a formula
|
||||
valueObject.v = null;
|
||||
valueObject.f = `${value.v}`;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (formulaData?.[toRow]?.[toCol]) {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
function getSpecialPasteValueValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const arrayFormulaCellData = formulaDataModel.getArrayFormulaCellData()?.[pasteFrom.unitId]?.[pasteFrom.subUnitId];
|
||||
const formulaData = formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
const fromRow = pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const fromCol = pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaString(value.f) || isFormulaId(value.si)) {
|
||||
// If the copy value is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (arrayFormulaCellData?.[fromRow]?.[fromCol]) {
|
||||
// If the copy value is an array formula
|
||||
const cell = arrayFormulaCellData[fromRow][fromCol];
|
||||
valueObject.v = cell.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (formulaData?.[toRow]?.[toCol]) {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
if (value.p) {
|
||||
const richText = getCellRichText(value);
|
||||
if (richText) {
|
||||
valueObject.v = richText;
|
||||
}
|
||||
}
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
function getSpecialPasteFormulaValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const formulaIdMap = new Map<string, string>();
|
||||
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaId(value.si)) {
|
||||
// If the copy value is a formula
|
||||
if (pasteFrom.unitId !== unitId || pasteFrom.subUnitId !== subUnitId) {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
pasteFrom.range.rows[row % pasteFrom.range.rows.length],
|
||||
pasteFrom.range.cols[col % pasteFrom.range.cols.length],
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(formulaString || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = null;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
valueObject.si = value.si;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (isFormulaString(value.f)) {
|
||||
// If the copy value is a formula
|
||||
const index = `${row % pasteFrom.range.rows.length}_${col % pasteFrom.range.cols.length}`;
|
||||
let formulaId = formulaIdMap.get(index);
|
||||
|
||||
if (!formulaId) {
|
||||
formulaId = generateRandomId(6);
|
||||
formulaIdMap.set(index, formulaId);
|
||||
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(value.f || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
// At the beginning of the second formula, set formulaId only
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = null;
|
||||
|
||||
if (value.p) {
|
||||
const richText = getCellRichText(value);
|
||||
if (richText) {
|
||||
valueObject.v = richText;
|
||||
}
|
||||
}
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
function getDefaultPasteValueMatrix(
|
||||
unitId: string,
|
||||
subUnitId: string,
|
||||
range: IDiscreteRange,
|
||||
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
|
||||
copyType: COPY_TYPE,
|
||||
lexerTreeBuilder: LexerTreeBuilder,
|
||||
formulaDataModel: FormulaDataModel,
|
||||
pasteFrom: ISheetDiscreteRangeLocation
|
||||
): ObjectMatrix<ICellData> {
|
||||
const valueMatrix = new ObjectMatrix<ICellData>();
|
||||
const formulaIdMap = new Map<string, string>();
|
||||
const formulaData = formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
const cutFormulaIds: string[] = [];
|
||||
|
||||
if (copyType === COPY_TYPE.CUT) {
|
||||
// If cut, the formula range should not be moved.
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaId(value.si)) {
|
||||
if (isFormulaString(value.f)) {
|
||||
cutFormulaIds.push(value.si as string);
|
||||
|
||||
valueObject.f = value.f;
|
||||
valueObject.si = value.si;
|
||||
} else if (cutFormulaIds.includes(value.si as string)) {
|
||||
valueObject.f = null;
|
||||
valueObject.si = value.si;
|
||||
} else {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
pasteFrom.range.rows[row % pasteFrom.range.rows.length],
|
||||
pasteFrom.range.cols[col % pasteFrom.range.cols.length],
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
|
||||
valueObject.f = formulaString;
|
||||
valueObject.si = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (isFormulaString(value.f)) {
|
||||
valueObject.f = value.f;
|
||||
valueObject.si = null;
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// copy, the formula range should be moved.
|
||||
matrix.forValue((row, col, value) => {
|
||||
const toRow = range.rows[row];
|
||||
const toCol = range.cols[col];
|
||||
const valueObject: ICellDataWithSpanInfo = {};
|
||||
|
||||
if (isFormulaId(value.si)) {
|
||||
// If the copy value is a formula
|
||||
if (pasteFrom.unitId !== unitId || pasteFrom.subUnitId !== subUnitId) {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
pasteFrom.range.rows[row % pasteFrom.range.rows.length],
|
||||
pasteFrom.range.cols[col % pasteFrom.range.cols.length],
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(formulaString || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = null;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
valueObject.si = value.si;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (isFormulaString(value.f)) {
|
||||
// If the copy value is a formula
|
||||
const index = `${row % pasteFrom.range.rows.length}_${col % pasteFrom.range.cols.length}`;
|
||||
let formulaId = formulaIdMap.get(index);
|
||||
|
||||
if (!formulaId) {
|
||||
formulaId = generateRandomId(6);
|
||||
formulaIdMap.set(index, formulaId);
|
||||
|
||||
const offsetX = range.cols[col] - pasteFrom.range.cols[col % pasteFrom.range.cols.length];
|
||||
const offsetY = range.rows[row] - pasteFrom.range.rows[row % pasteFrom.range.rows.length];
|
||||
const shiftedFormula = lexerTreeBuilder.moveFormulaRefOffset(value.f || '', offsetX, offsetY);
|
||||
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = shiftedFormula;
|
||||
} else {
|
||||
// At the beginning of the second formula, set formulaId only
|
||||
valueObject.si = formulaId;
|
||||
valueObject.f = null;
|
||||
}
|
||||
|
||||
valueObject.v = null;
|
||||
valueObject.p = null;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
} else if (formulaData?.[toRow]?.[toCol]) {
|
||||
// If the paste location is a formula
|
||||
valueObject.v = value.v;
|
||||
valueObject.f = null;
|
||||
valueObject.si = null;
|
||||
valueObject.p = value.p;
|
||||
|
||||
valueMatrix.setValue(toRow, toCol, valueObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If cut range has the first formula id, remove the related formula ids and convert it to formula string.
|
||||
if (cutFormulaIds.length > 0) {
|
||||
new ObjectMatrix(formulaData!).forValue((row, col, value) => {
|
||||
if (
|
||||
!(pasteFrom.range.rows.includes(row) && pasteFrom.range.cols.includes(col)) &&
|
||||
!(range.rows.includes(row) && range.cols.includes(col)) &&
|
||||
cutFormulaIds.includes(value?.si as string)
|
||||
) {
|
||||
const formulaString = formulaDataModel.getFormulaStringByCell(
|
||||
row,
|
||||
col,
|
||||
pasteFrom.subUnitId,
|
||||
pasteFrom.unitId
|
||||
);
|
||||
|
||||
valueMatrix.setValue(row, col, {
|
||||
f: formulaString,
|
||||
si: null,
|
||||
v: null,
|
||||
p: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return valueMatrix;
|
||||
}
|
||||
|
||||
function getCellRichText(cell: ICellData) {
|
||||
if (cell?.p) {
|
||||
const body = cell?.p.body;
|
||||
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = body.dataStream;
|
||||
const lastString = data.substring(data.length - 2, data.length);
|
||||
const newDataStream = lastString === DEFAULT_EMPTY_DOCUMENT_VALUE ? data.substring(0, data.length - 2) : data;
|
||||
|
||||
return newDataStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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 {
|
||||
ICellDataForSheetInterceptor,
|
||||
ICommandInfo,
|
||||
IObjectMatrixPrimitiveType,
|
||||
IRange,
|
||||
IRowAutoHeightInfo,
|
||||
Nullable,
|
||||
Workbook,
|
||||
Worksheet,
|
||||
} from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type {
|
||||
ISelectionWithStyle,
|
||||
ISetRangeValuesMutationParams,
|
||||
ISetWorksheetRowAutoHeightMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
ColorKit,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
ObjectMatrix,
|
||||
ThemeService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
ErrorType,
|
||||
FormulaDataModel,
|
||||
SetArrayFormulaDataMutation,
|
||||
SetFormulaCalculationResultMutation,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
attachSelectionWithCoord,
|
||||
BEFORE_CELL_EDIT,
|
||||
SetRangeValuesMutation,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
IEditorBridgeService,
|
||||
SELECTION_SHAPE_DEPTH,
|
||||
SelectionControl,
|
||||
SheetSkeletonManagerService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
|
||||
/**
|
||||
* For Array formula in cell editing
|
||||
*/
|
||||
export class FormulaEditorShowController extends Disposable implements IRenderModule {
|
||||
private _previousShape: Nullable<SelectionControl>;
|
||||
private _skeleton: SpreadsheetSkeleton;
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
|
||||
@Inject(ThemeService) private readonly _themeService: ThemeService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService
|
||||
) {
|
||||
super();
|
||||
this._initSkeletonChangeListener();
|
||||
this._initInterceptorEditorStart();
|
||||
this._commandExecutedListener();
|
||||
|
||||
// Do not intercept v:null and add t: CellValueType.NUMBER. When the cell =TODAY() is automatically filled, the number format will recognize the Number type and parse it as 1900-01-00 date format.
|
||||
}
|
||||
|
||||
private _initSkeletonChangeListener(): void {
|
||||
this.disposeWithMe(
|
||||
this._sheetSkeletonManagerService.currentSkeleton$.subscribe((param) => {
|
||||
if (param == null) {
|
||||
this._logService.debug('[FormulaEditorShowController]: should not receive currentSkeleton$ as null!');
|
||||
} else {
|
||||
const { skeleton } = param;
|
||||
const prevSheetId = this._skeleton?.worksheet?.getSheetId();
|
||||
this._changeRuntime(skeleton);
|
||||
|
||||
// change to another sheet
|
||||
if (prevSheetId !== skeleton.worksheet.getSheetId()) {
|
||||
this._removeArrayFormulaRangeShape();
|
||||
} else {
|
||||
const { unitId, sheetId } = param;
|
||||
this._updateArrayFormulaRangeShape(unitId, sheetId);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected _changeRuntime(skeleton: SpreadsheetSkeleton): void {
|
||||
this._skeleton = skeleton;
|
||||
}
|
||||
|
||||
private _initInterceptorEditorStart(): void {
|
||||
this.disposeWithMe(
|
||||
toDisposable(
|
||||
this._sheetInterceptorService.writeCellInterceptor.intercept(BEFORE_CELL_EDIT, {
|
||||
handler: (value, context, next) => {
|
||||
const { row, col, unitId, subUnitId, worksheet } = context;
|
||||
const arrayFormulaMatrixRange = this._formulaDataModel.getArrayFormulaRange();
|
||||
|
||||
const arrayFormulaMatrixCell = this._formulaDataModel.getArrayFormulaCellData();
|
||||
|
||||
this._removeArrayFormulaRangeShape();
|
||||
|
||||
if (value == null) {
|
||||
return next(value);
|
||||
}
|
||||
|
||||
let cellInfo: Nullable<ICellDataForSheetInterceptor> = null;
|
||||
|
||||
const formulaString = this._formulaDataModel.getFormulaStringByCell(row, col, subUnitId, unitId);
|
||||
|
||||
if (formulaString !== null) {
|
||||
cellInfo = { f: formulaString };
|
||||
}
|
||||
|
||||
/**
|
||||
* If the display conditions for the array formula are not met, return the range directly.
|
||||
*/
|
||||
if (
|
||||
value.v != null &&
|
||||
value.v !== '' &&
|
||||
arrayFormulaMatrixCell[unitId]?.[subUnitId]?.[row]?.[col] == null
|
||||
) {
|
||||
if (cellInfo) {
|
||||
return { ...value, ...cellInfo };
|
||||
}
|
||||
|
||||
return next(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the array formula for special display in subsequent processing
|
||||
*/
|
||||
const matrixRange = arrayFormulaMatrixRange?.[unitId]?.[subUnitId];
|
||||
if (matrixRange != null) {
|
||||
// For cells other than the upper left corner, the cellInfo information will be updated
|
||||
cellInfo = this._displayArrayFormulaRangeShape(matrixRange, row, col, unitId, subUnitId, worksheet, cellInfo);
|
||||
}
|
||||
|
||||
if (cellInfo) {
|
||||
return { ...value, ...cellInfo };
|
||||
}
|
||||
|
||||
return next(value);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _commandExecutedListener(): void {
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options) => {
|
||||
if (command.id === SetFormulaCalculationResultMutation.id || (command.id === SetArrayFormulaDataMutation.id && options && options.remove)) {
|
||||
this._removeArrayFormulaRangeShape();
|
||||
}
|
||||
|
||||
if (command.id !== SetRangeValuesMutation.id || options?.applyFormulaCalculationResult !== true || command.params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = command.params as ISetRangeValuesMutationParams;
|
||||
const editLocation = this._editorBridgeService.getEditLocation();
|
||||
if (
|
||||
editLocation == null
|
||||
|| params.unitId !== this._context.unitId
|
||||
|| params.unitId !== editLocation.unitId
|
||||
|| params.subUnitId !== editLocation.sheetId
|
||||
|| this._editorBridgeService.isVisible().visible
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { row, column, isInArrayFormulaRange } = editLocation;
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData();
|
||||
const isInArrayFormulaCell = arrayFormulaCellData?.[params.unitId]?.[params.subUnitId]?.[row]?.[column] != null;
|
||||
if (isInArrayFormulaRange !== true && !isInArrayFormulaCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editorBridgeService.refreshEditCellState();
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
if (SetWorksheetRowAutoHeightMutation.id === command.id) {
|
||||
requestIdleCallback(() => {
|
||||
const params = command.params as ISetWorksheetRowAutoHeightMutationParams;
|
||||
const { unitId, subUnitId, rowsAutoHeightInfo } = params;
|
||||
this._refreshArrayFormulaRangeShapeByRow(unitId, subUnitId, rowsAutoHeightInfo);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _displayArrayFormulaRangeShape(matrixRange: IObjectMatrixPrimitiveType<IRange>, row: number, col: number, unitId: string, subUnitId: string, worksheet: Worksheet, cellInfo: Nullable<ICellDataForSheetInterceptor>): Nullable<ICellDataForSheetInterceptor> {
|
||||
// const sheetFormulaData = this._formulaDataModel.getSheetFormulaData(unitId, subUnitId);
|
||||
let result = cellInfo;
|
||||
|
||||
new ObjectMatrix(matrixRange).forValue((rowIndex, columnIndex, range) => {
|
||||
if (range == null) {
|
||||
return true;
|
||||
}
|
||||
const { startRow, startColumn, endRow, endColumn } = range;
|
||||
if (rowIndex === row && columnIndex === col) {
|
||||
this._createArrayFormulaRangeShape(range, unitId, subUnitId);
|
||||
return false;
|
||||
}
|
||||
if (row >= startRow && row <= endRow && col >= startColumn && col <= endColumn) {
|
||||
const mainCellValue = worksheet.getCell(startRow, startColumn);
|
||||
|
||||
if (mainCellValue?.v === ErrorType.SPILL || mainCellValue?.f == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
result = {
|
||||
f: mainCellValue.f,
|
||||
isInArrayFormulaRange: true,
|
||||
};
|
||||
}
|
||||
|
||||
this._createArrayFormulaRangeShape(range, unitId, subUnitId);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _createArrayFormulaRangeShape(arrayRange: IRange, unitId: string, subUnitId: string): void {
|
||||
const renderUnit = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const skeleton = this._sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!renderUnit || !skeleton) return;
|
||||
|
||||
const { scene } = renderUnit;
|
||||
if (!scene) return;
|
||||
|
||||
const selectionWithStyle: ISelectionWithStyle = {
|
||||
range: arrayRange,
|
||||
primary: null,
|
||||
style: {
|
||||
strokeWidth: 1,
|
||||
stroke: this._themeService.getColorFromTheme('primary.600'),
|
||||
fill: new ColorKit(this._themeService.getColorFromTheme('gray.0')).setAlpha(0).toString(),
|
||||
widgets: {},
|
||||
},
|
||||
};
|
||||
const selectionWithCoord = attachSelectionWithCoord(selectionWithStyle, skeleton);
|
||||
const { rowHeaderWidth, columnHeaderHeight } = skeleton;
|
||||
const control = new SelectionControl(scene, SELECTION_SHAPE_DEPTH.FORMULA_EDITOR_SHOW, this._themeService, {
|
||||
highlightHeader: false,
|
||||
rowHeaderWidth,
|
||||
columnHeaderHeight,
|
||||
});
|
||||
control.updateRangeBySelectionWithCoord(selectionWithCoord);
|
||||
control.setEvent(false);
|
||||
this._previousShape = control;
|
||||
}
|
||||
|
||||
private _removeArrayFormulaRangeShape(): void {
|
||||
if (this._previousShape == null) {
|
||||
return;
|
||||
}
|
||||
this._previousShape.dispose();
|
||||
this._previousShape = null;
|
||||
}
|
||||
|
||||
private _refreshArrayFormulaRangeShape(unitId: string, subUnitId: string): void {
|
||||
if (this._previousShape) {
|
||||
const { startRow, endRow, startColumn, endColumn } = this._previousShape.getRange();
|
||||
const range = { startRow, endRow, startColumn, endColumn };
|
||||
this._removeArrayFormulaRangeShape();
|
||||
this._createArrayFormulaRangeShape(range, unitId, subUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkCurrentSheet(unitId: string, subUnitId: string): boolean {
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) return false;
|
||||
|
||||
const worksheet = skeleton.worksheet;
|
||||
if (!worksheet) return false;
|
||||
|
||||
if (worksheet.unitId === unitId && worksheet.getSheetId() === subUnitId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _updateArrayFormulaRangeShape(unitId: string, subUnitId: string): void {
|
||||
if (!this._checkCurrentSheet(unitId, subUnitId)) return;
|
||||
if (!this._previousShape) return;
|
||||
this._refreshArrayFormulaRangeShape(unitId, subUnitId);
|
||||
}
|
||||
|
||||
private _refreshArrayFormulaRangeShapeByRow(unitId: string, subUnitId: string, rowAutoHeightInfo: IRowAutoHeightInfo[]): void {
|
||||
if (!this._checkCurrentSheet(unitId, subUnitId)) return;
|
||||
|
||||
if (!this._previousShape) return;
|
||||
|
||||
const { startRow: shapeStartRow } = this._previousShape.getRange();
|
||||
|
||||
for (let i = 0; i < rowAutoHeightInfo.length; i++) {
|
||||
const { row } = rowAutoHeightInfo[i];
|
||||
if (shapeStartRow >= row) {
|
||||
this._refreshArrayFormulaRangeShape(unitId, subUnitId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { Inject, InterceptorEffectEnum, RxDisposable } from '@univerjs/core';
|
||||
import { extractFormulaError, FormulaDataModel } from '@univerjs/engine-formula';
|
||||
import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets';
|
||||
|
||||
const FORMULA_ERROR_MARK = {
|
||||
tl: {
|
||||
size: 6,
|
||||
color: '#409f11',
|
||||
},
|
||||
};
|
||||
|
||||
export class FormulaRenderManagerController extends RxDisposable {
|
||||
constructor(
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(this._sheetInterceptorService.intercept(
|
||||
INTERCEPTOR_POINT.CELL_CONTENT,
|
||||
{
|
||||
effect: InterceptorEffectEnum.Style,
|
||||
handler: (cell, pos, next) => {
|
||||
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData()?.
|
||||
[pos.unitId]?.
|
||||
[pos.subUnitId]?.
|
||||
[pos.row]?.
|
||||
[pos.col];
|
||||
|
||||
const errorType = extractFormulaError(cell, !!arrayFormulaCellData);
|
||||
if (!errorType) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
if (!cell) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
if (cell === pos.rawData) {
|
||||
cell = { ...pos.rawData };
|
||||
}
|
||||
|
||||
cell.markers = {
|
||||
...cell?.markers,
|
||||
...FORMULA_ERROR_MARK,
|
||||
};
|
||||
|
||||
return next(cell);
|
||||
},
|
||||
priority: 10,
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IReorderRangeMutationParams } from '@univerjs/sheets';
|
||||
import {
|
||||
Disposable,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
Range,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FormulaDataModel, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { ReorderRangeCommand, SetRangeValuesMutation, SheetInterceptorService } from '@univerjs/sheets';
|
||||
|
||||
export class FormulaReorderController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
|
||||
@Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
|
||||
getMutations: (command) => {
|
||||
if (command.id === ReorderRangeCommand.id) {
|
||||
return this._reorderFormula(command.params as IReorderRangeMutationParams);
|
||||
}
|
||||
|
||||
return {
|
||||
redos: [],
|
||||
undos: [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private _reorderFormula(params: IReorderRangeMutationParams) {
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
const { unitId, subUnitId, range, order } = params;
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
|
||||
if (!worksheet) {
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const redoFormulaMatrix = new ObjectMatrix<Nullable<ICellData>>();
|
||||
const undoFormulaMatrix = new ObjectMatrix<Nullable<ICellData>>();
|
||||
|
||||
let hasFormula = false;
|
||||
|
||||
Range.foreach(range, (row, col) => {
|
||||
let targetRow = row;
|
||||
|
||||
if (order.hasOwnProperty(row)) {
|
||||
targetRow = order[row];
|
||||
}
|
||||
|
||||
const targetCell = cellMatrix.getValue(targetRow, col);
|
||||
|
||||
if (targetCell?.f || targetCell?.si) {
|
||||
hasFormula = true;
|
||||
|
||||
const formulaString = this._formulaDataModel.getFormulaStringByCell(targetRow, col, subUnitId, unitId) as string;
|
||||
const shiftedFormula = this._lexerTreeBuilder.moveFormulaRefOffset(
|
||||
formulaString,
|
||||
0,
|
||||
row - targetRow
|
||||
);
|
||||
const newCell = Tools.deepClone(targetCell);
|
||||
newCell.f = shiftedFormula;
|
||||
newCell.si = null;
|
||||
|
||||
redoFormulaMatrix.setValue(row, col, newCell);
|
||||
} else {
|
||||
redoFormulaMatrix.setValue(row, col, targetCell);
|
||||
}
|
||||
|
||||
undoFormulaMatrix.setValue(row, col, cellMatrix.getValue(row, col));
|
||||
});
|
||||
|
||||
if (!hasFormula) {
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
|
||||
redos.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: redoFormulaMatrix.getMatrix(),
|
||||
},
|
||||
});
|
||||
|
||||
undos.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: undoFormulaMatrix.getMatrix(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
redos,
|
||||
undos,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 { Workbook } from '@univerjs/core';
|
||||
import { Disposable, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ImageFormulaCellInterceptorController } from '@univerjs/sheets-formula';
|
||||
import { SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
|
||||
export class ImageFormulaRenderController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ImageFormulaCellInterceptorController) private readonly _imageFormulaCellInterceptorController: ImageFormulaCellInterceptorController,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._imageFormulaCellInterceptorController.registerRefreshRenderFunction(() => {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return;
|
||||
|
||||
const render = this._renderManagerService.getRenderUnitById(workbook.getUnitId());
|
||||
if (!render) return;
|
||||
|
||||
render.with(SheetSkeletonManagerService).reCalculate();
|
||||
|
||||
const mainComponent = render.mainComponent;
|
||||
if (!mainComponent) return;
|
||||
|
||||
mainComponent.makeDirty();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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, Inject } from '@univerjs/core';
|
||||
import {
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
} from '@univerjs/icons';
|
||||
import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, RANGE_SELECTOR_COMPONENT_KEY } from '@univerjs/sheets-ui';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { MobileFormulaEditor } from '../../views/mobile/formula-editor/MobileFormulaEditor';
|
||||
import { MobileMoreFunctions } from '../../views/mobile/more-functions/MobileMoreFunctions';
|
||||
import { MORE_FUNCTIONS_COMPONENT } from '../../views/more-functions/interface';
|
||||
import { MobileRangeSelector } from '../../views/range-selector/MobileRangeSelector';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerIcons();
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
DatabaseFunctionIcon,
|
||||
DateFunctionIcon,
|
||||
EngineeringFunctionIcon,
|
||||
FinancialFunctionIcon,
|
||||
FunctionIcon,
|
||||
InformationFunctionIcon,
|
||||
LogicalFunctionIcon,
|
||||
LookupFunctionIcon,
|
||||
MathFunctionIcon,
|
||||
StatisticalFunctionIcon,
|
||||
TextFunctionIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(MORE_FUNCTIONS_COMPONENT, MobileMoreFunctions));
|
||||
this.disposeWithMe(this._componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, MobileRangeSelector));
|
||||
this.disposeWithMe(this._componentManager.register(
|
||||
EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY,
|
||||
MobileFormulaEditor
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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 { IInsertFunctionOperationParams } from '../../../commands/operations/insert-function.operation';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ReferenceAbsoluteOperation } from '../../../commands/operations/reference-absolute.operation';
|
||||
|
||||
const whenSheetEditorActivatedMock = vi.fn();
|
||||
const whenSheetEditorFocusedMock = vi.fn();
|
||||
|
||||
vi.mock('@univerjs/sheets-ui', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/sheets-ui')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
whenSheetEditorActivated: whenSheetEditorActivatedMock,
|
||||
whenSheetEditorFocused: whenSheetEditorFocusedMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('formula shortcuts', () => {
|
||||
it('binds F4 to reference absolute switching only while the sheet editor is active', async () => {
|
||||
const { ChangeRefToAbsoluteShortcut } = await import('../prompt.shortcut');
|
||||
const contextService = {};
|
||||
whenSheetEditorActivatedMock.mockReturnValueOnce(true);
|
||||
|
||||
expect(ChangeRefToAbsoluteShortcut.id).toBe(ReferenceAbsoluteOperation.id);
|
||||
expect(ChangeRefToAbsoluteShortcut.binding).toBe(KeyCode.F4);
|
||||
expect(ChangeRefToAbsoluteShortcut.preconditions?.(contextService as never)).toBe(true);
|
||||
expect(whenSheetEditorActivatedMock).toHaveBeenCalledWith(contextService);
|
||||
});
|
||||
|
||||
it('binds quick sum to SUM insertion while the sheet editor is focused', async () => {
|
||||
const { InsertFunctionOperation } = await import('../../../commands/operations/insert-function.operation');
|
||||
const { QuickSumShortcut } = await import('../quick-sum.shortcut');
|
||||
|
||||
expect(QuickSumShortcut.id).toBe(InsertFunctionOperation.id);
|
||||
expect(QuickSumShortcut.staticParameters).toEqual({ value: 'SUM' } satisfies IInsertFunctionOperationParams);
|
||||
expect(QuickSumShortcut.binding).toBe(MetaKeys.ALT | KeyCode.EQUAL);
|
||||
expect(QuickSumShortcut.mac).toBe(MetaKeys.CTRL_COMMAND | MetaKeys.ALT | KeyCode.EQUAL);
|
||||
expect(QuickSumShortcut.preconditions).toBe(whenSheetEditorFocusedMock);
|
||||
expect(QuickSumShortcut.description).toBe('sheets-formula-ui.shortcut.quick-sum');
|
||||
expect(QuickSumShortcut.group).toBe('4_sheet-edit');
|
||||
expect(QuickSumShortcut.groupTitle).toBe('sheets-ui.shortcut.sheet-edit');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 { IShortcutItem } from '@univerjs/ui';
|
||||
import { whenSheetEditorActivated } from '@univerjs/sheets-ui';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { ReferenceAbsoluteOperation } from '../../commands/operations/reference-absolute.operation';
|
||||
|
||||
export const ChangeRefToAbsoluteShortcut: IShortcutItem = {
|
||||
id: ReferenceAbsoluteOperation.id,
|
||||
binding: KeyCode.F4,
|
||||
preconditions: (contextService) => whenSheetEditorActivated(contextService),
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 { IShortcutItem } from '@univerjs/ui';
|
||||
import type { IInsertFunctionOperationParams } from '../../commands/operations/insert-function.operation';
|
||||
import { whenSheetEditorFocused } from '@univerjs/sheets-ui';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
import { InsertFunctionOperation } from '../../commands/operations/insert-function.operation';
|
||||
|
||||
export const QuickSumShortcut: IShortcutItem<IInsertFunctionOperationParams> = {
|
||||
id: InsertFunctionOperation.id,
|
||||
binding: MetaKeys.ALT | KeyCode.EQUAL,
|
||||
preconditions: whenSheetEditorFocused,
|
||||
mac: MetaKeys.CTRL_COMMAND | MetaKeys.ALT | KeyCode.EQUAL,
|
||||
staticParameters: { value: 'SUM' },
|
||||
description: 'sheets-formula-ui.shortcut.quick-sum',
|
||||
group: '4_sheet-edit',
|
||||
groupTitle: 'sheets-ui.shortcut.sheet-edit',
|
||||
};
|
||||
88
packages/sheets-formula-ui/src/controllers/ui.controller.ts
Normal file
88
packages/sheets-formula-ui/src/controllers/ui.controller.ts
Normal file
@@ -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 { Dependency } from '@univerjs/core';
|
||||
import { Disposable, ICommandService, Inject, Injector, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetsUIPart } from '@univerjs/sheets-ui';
|
||||
import { connectInjector, IMenuManagerService, IShortcutService, IUIPartsService } from '@univerjs/ui';
|
||||
import { SheetCopyFormulaOnlyCommand, SheetOnlyPasteFormulaCommand } from '../commands/commands/formula-clipboard.command';
|
||||
import { HelpFunctionOperation } from '../commands/operations/help-function.operation';
|
||||
import { InsertFunctionOperation } from '../commands/operations/insert-function.operation';
|
||||
import { MoreFunctionsOperation } from '../commands/operations/more-functions.operation';
|
||||
import { ReferenceAbsoluteOperation } from '../commands/operations/reference-absolute.operation';
|
||||
import { SearchFunctionOperation } from '../commands/operations/search-function.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import { FormulaProgressBar } from '../views/formula-progress/FormulaProgress';
|
||||
import { FormulaEditorShowController } from './formula-editor-show.controller';
|
||||
import { ChangeRefToAbsoluteShortcut } from './shortcuts/prompt.shortcut';
|
||||
import { QuickSumShortcut } from './shortcuts/quick-sum.shortcut';
|
||||
|
||||
export class FormulaUIController extends Disposable {
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService,
|
||||
@IUIPartsService private readonly _uiPartsService: IUIPartsService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._registerCommands();
|
||||
this._registerMenus();
|
||||
this._registerShortcuts();
|
||||
this._registerComponents();
|
||||
this._registerRenderModules();
|
||||
}
|
||||
|
||||
private _registerMenus(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
[
|
||||
SheetCopyFormulaOnlyCommand,
|
||||
SheetOnlyPasteFormulaCommand,
|
||||
InsertFunctionOperation,
|
||||
MoreFunctionsOperation,
|
||||
SearchFunctionOperation,
|
||||
HelpFunctionOperation,
|
||||
ReferenceAbsoluteOperation,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
private _registerShortcuts(): void {
|
||||
[
|
||||
QuickSumShortcut,
|
||||
ChangeRefToAbsoluteShortcut,
|
||||
].forEach((item) => {
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut(item));
|
||||
});
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._uiPartsService.registerComponent(SheetsUIPart.FORMULA_AUX, () => connectInjector(FormulaProgressBar, this._injector)));
|
||||
}
|
||||
|
||||
private _registerRenderModules(): void {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [FormulaEditorShowController] as Dependency));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 { describe, expect, it } from 'vitest';
|
||||
import { extractFormulaNumber } from '../utils';
|
||||
|
||||
describe('Test utils', () => {
|
||||
it('Function extractFormulaNumber', () => {
|
||||
const v = 0.07 / 0.1;
|
||||
expect(extractFormulaNumber({ v })).toBeNull();
|
||||
expect(extractFormulaNumber({ v, f: '=SUM(A1)' })).toBeNull();
|
||||
expect(extractFormulaNumber({ v, si: 'id1', f: '=SUM(A1)', t: CellValueType.NUMBER })).toBe(0.7);
|
||||
});
|
||||
});
|
||||
40
packages/sheets-formula-ui/src/controllers/utils/utils.ts
Normal file
40
packages/sheets-formula-ui/src/controllers/utils/utils.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 type { ICellData, IContextService, Nullable } from '@univerjs/core';
|
||||
import { CellValueType, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR, isFormulaId, isFormulaString } from '@univerjs/core';
|
||||
import { stripErrorMargin } from '@univerjs/engine-formula';
|
||||
|
||||
export function whenEditorStandalone(contextService: IContextService) {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_DOC) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR)
|
||||
// &&
|
||||
// contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the formula number from the cell, handle the precision issue
|
||||
* @param cell
|
||||
* @returns
|
||||
*/
|
||||
export function extractFormulaNumber(cell: Nullable<ICellData>) {
|
||||
if ((isFormulaString(cell?.f) || isFormulaId(cell?.si)) && cell?.t === CellValueType.NUMBER && typeof cell?.v === 'number') {
|
||||
return stripErrorMargin(cell.v);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import {
|
||||
ActiveDirtyManagerService,
|
||||
AstRootNodeFactory,
|
||||
AstTreeBuilder,
|
||||
CalculateController,
|
||||
CalculateFormulaService,
|
||||
DefinedNamesService,
|
||||
DependencyManagerService,
|
||||
DescriptionService,
|
||||
FeatureCalculationManagerService,
|
||||
FormulaCurrentConfigService,
|
||||
FormulaDataModel,
|
||||
FormulaDependencyGenerator,
|
||||
FormulaRuntimeService,
|
||||
FunctionNodeFactory,
|
||||
FunctionService,
|
||||
HyperlinkEngineFormulaService,
|
||||
IActiveDirtyManagerService,
|
||||
ICalculateFormulaService,
|
||||
IDefinedNamesService,
|
||||
IDependencyManagerService,
|
||||
IDescriptionService,
|
||||
IFeatureCalculationManagerService,
|
||||
IFormulaCurrentConfigService,
|
||||
IFormulaExternalReferenceDataLoader,
|
||||
IFormulaRuntimeService,
|
||||
IFunctionService,
|
||||
IHyperlinkEngineFormulaService,
|
||||
Interpreter,
|
||||
IOtherFormulaManagerService,
|
||||
IRegisterFunctionService,
|
||||
ISheetRowFilteredService,
|
||||
ISuperTableService,
|
||||
LambdaNodeFactory,
|
||||
LambdaParameterNodeFactory,
|
||||
Lexer,
|
||||
LexerTreeBuilder,
|
||||
NoopFormulaExternalReferenceDataLoader,
|
||||
OperatorNodeFactory,
|
||||
OtherFormulaManagerService,
|
||||
PrefixNodeFactory,
|
||||
ReferenceNodeFactory,
|
||||
RegisterFunctionService,
|
||||
SheetRowFilteredService,
|
||||
SuffixNodeFactory,
|
||||
SuperTableService,
|
||||
UnionNodeFactory,
|
||||
ValueNodeFactory,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import enUS from '@univerjs/sheets-formula-ui/locale/en-US';
|
||||
import zhCN from '@univerjs/sheets-formula-ui/locale/zh-CN';
|
||||
import '@univerjs/engine-formula/facade';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-formula/facade';
|
||||
|
||||
function getTestWorkbookDataDemo(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
3: {
|
||||
f: '=SUM(A1)',
|
||||
si: '3e4r5t',
|
||||
},
|
||||
},
|
||||
1: {
|
||||
3: {
|
||||
f: '=SUM(A2)',
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: Workbook;
|
||||
univerAPI: FUniver;
|
||||
}
|
||||
|
||||
export function createFormulaTestBed(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]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
|
||||
registerFormulaDependencies(injector);
|
||||
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
injector.get(LocaleService).load({ zhCN, enUS });
|
||||
|
||||
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
|
||||
|
||||
const univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
univerAPI,
|
||||
};
|
||||
}
|
||||
|
||||
function registerFormulaDependencies(injector: Injector) {
|
||||
injector.add([IRegisterFunctionService, { useClass: RegisterFunctionService }]);
|
||||
injector.add([
|
||||
IDescriptionService,
|
||||
{
|
||||
useFactory: () => injector.createInstance(DescriptionService),
|
||||
},
|
||||
]);
|
||||
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([IActiveDirtyManagerService, { useClass: ActiveDirtyManagerService }]);
|
||||
injector.add([ISheetRowFilteredService, { useClass: SheetRowFilteredService }]);
|
||||
|
||||
injector.add([ICalculateFormulaService, { useClass: CalculateFormulaService }]);
|
||||
injector.add([Lexer]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
|
||||
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
|
||||
injector.add([IFormulaExternalReferenceDataLoader, { useClass: NoopFormulaExternalReferenceDataLoader }]);
|
||||
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
|
||||
injector.add([IDependencyManagerService, { useClass: DependencyManagerService }]);
|
||||
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
|
||||
injector.add([IFunctionService, { useClass: FunctionService }]);
|
||||
injector.add([IOtherFormulaManagerService, { useClass: OtherFormulaManagerService }]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([ISuperTableService, { useClass: SuperTableService }]);
|
||||
injector.add([CalculateController]);
|
||||
|
||||
injector.add([IFeatureCalculationManagerService, { useClass: FeatureCalculationManagerService }]);
|
||||
injector.add([FormulaDependencyGenerator]);
|
||||
injector.add([Interpreter]);
|
||||
injector.add([AstTreeBuilder]);
|
||||
|
||||
injector.add([AstRootNodeFactory]);
|
||||
injector.add([FunctionNodeFactory]);
|
||||
injector.add([LambdaNodeFactory]);
|
||||
injector.add([LambdaParameterNodeFactory]);
|
||||
injector.add([OperatorNodeFactory]);
|
||||
injector.add([PrefixNodeFactory]);
|
||||
injector.add([ReferenceNodeFactory]);
|
||||
injector.add([SuffixNodeFactory]);
|
||||
injector.add([UnionNodeFactory]);
|
||||
injector.add([ValueNodeFactory]);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { FUniver } from '@univerjs/core/facade';
|
||||
import type { IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula';
|
||||
import { ICommandService, IConfigService } from '@univerjs/core';
|
||||
import { SetArrayFormulaDataMutation, SetFormulaCalculationNotificationMutation, SetFormulaCalculationResultMutation, SetFormulaCalculationStartMutation, SetFormulaCalculationStopMutation, SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula';
|
||||
import { CalculationMode, PLUGIN_CONFIG_KEY_BASE } from '@univerjs/sheets-formula';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createFormulaTestBed } from './create-formula-test-bed';
|
||||
|
||||
describe('Test FFormula', () => {
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
let univerAPI: FUniver;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createFormulaTestBed();
|
||||
get = testBed.get;
|
||||
univerAPI = testBed.univerAPI;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
commandService.registerCommand(SetFormulaCalculationStartMutation);
|
||||
commandService.registerCommand(SetTriggerFormulaCalculationStartMutation);
|
||||
commandService.registerCommand(SetFormulaCalculationStopMutation);
|
||||
commandService.registerCommand(SetFormulaCalculationNotificationMutation);
|
||||
commandService.registerCommand(SetFormulaCalculationResultMutation);
|
||||
commandService.registerCommand(SetArrayFormulaDataMutation);
|
||||
});
|
||||
|
||||
it('FFormula executeCalculation', () => {
|
||||
const formula = univerAPI.getFormula();
|
||||
|
||||
formula.calculationStart((forceCalculate) => {
|
||||
expect(forceCalculate).toBe(true);
|
||||
});
|
||||
|
||||
formula.calculationProcessing((stageInfo) => {
|
||||
expect(stageInfo).toBeDefined();
|
||||
});
|
||||
|
||||
formula.calculationEnd((functionsExecutedState) => {
|
||||
expect(functionsExecutedState).toBeDefined();
|
||||
});
|
||||
|
||||
formula.executeCalculation();
|
||||
formula.stopCalculation();
|
||||
});
|
||||
|
||||
it('FFormula setInitialFormulaComputing', () => {
|
||||
const formula = univerAPI.getFormula();
|
||||
|
||||
const configService = get(IConfigService);
|
||||
|
||||
configService.setConfig(PLUGIN_CONFIG_KEY_BASE, {});
|
||||
|
||||
const config = configService.getConfig<Partial<IUniverSheetsFormulaBaseConfig>>(PLUGIN_CONFIG_KEY_BASE);
|
||||
|
||||
expect(config?.initialFormulaComputing).toBeUndefined();
|
||||
|
||||
formula.setInitialFormulaComputing(CalculationMode.FORCED);
|
||||
|
||||
expect(config?.initialFormulaComputing).toBe(CalculationMode.FORCED);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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 { IUnitRangeName } from '@univerjs/core';
|
||||
import { firstValueFrom, skip } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GlobalRangeSelectorService } from '../../services/range-selector.service';
|
||||
import { createFormulaTestBed } from './create-formula-test-bed';
|
||||
import '../../facade';
|
||||
|
||||
describe('Test FUniver with sheets-formula-ui facade', () => {
|
||||
let disposeUniver: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
disposeUniver = () => {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
disposeUniver();
|
||||
});
|
||||
|
||||
it('showRangeSelectorDialog forwards options to the selector service and resolves with selected ranges', async () => {
|
||||
const testBed = createFormulaTestBed(undefined, [[GlobalRangeSelectorService]]);
|
||||
const selectorService = testBed.get(GlobalRangeSelectorService);
|
||||
const callback = vi.fn();
|
||||
const selectedRanges: IUnitRangeName[] = [{
|
||||
unitId: 'test',
|
||||
sheetName: 'sheet1',
|
||||
range: {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
},
|
||||
}];
|
||||
|
||||
disposeUniver = () => testBed.univer.dispose();
|
||||
|
||||
const selectorPromise = firstValueFrom(selectorService.currentSelector$.pipe(skip(1)));
|
||||
const resultPromise = testBed.univerAPI.showRangeSelectorDialog({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
initialValue: selectedRanges,
|
||||
maxRangeCount: 2,
|
||||
supportAcrossSheet: true,
|
||||
callback,
|
||||
});
|
||||
|
||||
const currentSelector = await selectorPromise;
|
||||
|
||||
expect(currentSelector).toMatchObject({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
initialValue: selectedRanges,
|
||||
maxRangeCount: 2,
|
||||
supportAcrossSheet: true,
|
||||
});
|
||||
|
||||
currentSelector!.callback(selectedRanges);
|
||||
|
||||
await expect(resultPromise).resolves.toEqual(selectedRanges);
|
||||
expect(callback).toHaveBeenCalledWith(selectedRanges);
|
||||
});
|
||||
});
|
||||
67
packages/sheets-formula-ui/src/facade/f-univer.ts
Normal file
67
packages/sheets-formula-ui/src/facade/f-univer.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 { IUnitRangeName } from '@univerjs/core';
|
||||
import type { IShowRangeSelectorDialogOptions } from '@univerjs/sheets-formula-ui';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { GlobalRangeSelectorService } from '@univerjs/sheets-formula-ui';
|
||||
|
||||
export interface IFUniverSheetsFormulaUIMixin {
|
||||
/**
|
||||
* Shows the range selector dialog.
|
||||
*
|
||||
* @param {IShowRangeSelectorDialogOptions} opts The options of the range selector dialog.
|
||||
* @returns {Promise<IUnitRangeName[]>} The selected ranges.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const unitId = fWorkbook.getId();
|
||||
*
|
||||
* await univerAPI.showRangeSelectorDialog({
|
||||
* unitId,
|
||||
* subUnitId: fWorksheet.getSheetId(),
|
||||
* initialValue: [{
|
||||
* unitId,
|
||||
* sheetName: fWorksheet.getSheetName(),
|
||||
* range: fWorksheet.getRange('A1:B2').getRange()
|
||||
* }],
|
||||
* maxRangeCount: 2,
|
||||
* supportAcrossSheet: true,
|
||||
* callback: (ranges, isCancel) => {
|
||||
* // Handle the selected ranges
|
||||
* console.log(ranges, isCancel);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
showRangeSelectorDialog(opts: IShowRangeSelectorDialogOptions): Promise<IUnitRangeName[]>;
|
||||
}
|
||||
|
||||
export class FUniverSheetsFormulaUIMixin extends FUniver implements IFUniverSheetsFormulaUIMixin {
|
||||
override showRangeSelectorDialog(opts: IShowRangeSelectorDialogOptions): Promise<IUnitRangeName[]> {
|
||||
const globalRangeSelectorService = this._injector.get(GlobalRangeSelectorService);
|
||||
return globalRangeSelectorService.showRangeSelectorDialog(opts);
|
||||
}
|
||||
}
|
||||
|
||||
FUniver.extend(FUniverSheetsFormulaUIMixin);
|
||||
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FUniver extends IFUniverSheetsFormulaUIMixin { }
|
||||
}
|
||||
19
packages/sheets-formula-ui/src/facade/index.ts
Normal file
19
packages/sheets-formula-ui/src/facade/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 './f-univer';
|
||||
|
||||
export * from './f-univer';
|
||||
2
packages/sheets-formula-ui/src/global.css
Normal file
2
packages/sheets-formula-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
42
packages/sheets-formula-ui/src/index.ts
Normal file
42
packages/sheets-formula-ui/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 './global.css';
|
||||
|
||||
export {
|
||||
SheetCopyFormulaOnlyCommand,
|
||||
SheetOnlyPasteFormulaCommand,
|
||||
} from './commands/commands/formula-clipboard.command';
|
||||
export { HelpFunctionOperation } from './commands/operations/help-function.operation';
|
||||
export { InsertFunctionOperation } from './commands/operations/insert-function.operation';
|
||||
export { MoreFunctionsOperation } from './commands/operations/more-functions.operation';
|
||||
export { ReferenceAbsoluteOperation } from './commands/operations/reference-absolute.operation';
|
||||
export { SearchFunctionOperation } from './commands/operations/search-function.operation';
|
||||
export { genFormulaRefSelectionStyle } from './common/selection';
|
||||
export type { IUniverSheetsFormulaUIConfig } from './config/config';
|
||||
export { FormulaReorderController } from './controllers/formula-reorder.controller';
|
||||
export { menuSchema as SheetsFormulaUIMenuSchema } from './menu/schema';
|
||||
export { UniverSheetsFormulaMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverSheetsFormulaUIPlugin } from './plugin';
|
||||
export { FORMULA_PROMPT_ACTIVATED } from './services/prompt.service';
|
||||
export { GlobalRangeSelectorService } from './services/range-selector.service';
|
||||
export type { IShowRangeSelectorDialogOptions } from './services/range-selector.service';
|
||||
export { RefSelectionsRenderService } from './services/render-services/ref-selections.render.service';
|
||||
export { FormulaEditor } from './views/formula-editor/index';
|
||||
export type { IFormulaEditorRef } from './views/formula-editor/index';
|
||||
export { RangeSelector } from './views/range-selector/index';
|
||||
export type { IRangeSelectorInstance, IRangeSelectorProps } from './views/range-selector/index';
|
||||
export { MobileRangeSelector } from './views/range-selector/MobileRangeSelector';
|
||||
103
packages/sheets-formula-ui/src/locale/ar-SA.ts
Normal file
103
packages/sheets-formula-ui/src/locale/ar-SA.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'مجموع سريع',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'دوال',
|
||||
common: 'دوال شائعة',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'مثال',
|
||||
helpAbstract: 'نبذة',
|
||||
required: 'مطلوب.',
|
||||
optional: 'اختياري.',
|
||||
},
|
||||
error: {
|
||||
title: 'خطأ',
|
||||
divByZero: 'خطأ القسمة على صفر',
|
||||
name: 'خطأ اسم غير صالح',
|
||||
value: 'خطأ في القيمة',
|
||||
num: 'خطأ رقمي',
|
||||
na: 'خطأ القيمة غير متوفرة',
|
||||
cycle: 'خطأ المرجع الدائري',
|
||||
ref: 'خطأ مرجع خلية غير صالح',
|
||||
spill: 'نطاق التدفق ليس فارغًا',
|
||||
calc: 'خطأ حسابي',
|
||||
error: 'خطأ',
|
||||
connect: 'جارٍ الحصول على البيانات',
|
||||
null: 'خطأ فارغ',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'مالي',
|
||||
date: 'التاريخ والوقت',
|
||||
math: 'الرياضيات والمثلثات',
|
||||
statistical: 'إحصائي',
|
||||
lookup: 'البحث والمرجع',
|
||||
database: 'قاعدة بيانات',
|
||||
text: 'نص',
|
||||
logical: 'منطقي',
|
||||
information: 'معلومات',
|
||||
engineering: 'هندسي',
|
||||
cube: 'مكعب',
|
||||
compatibility: 'التوافق',
|
||||
web: 'ويب',
|
||||
array: 'مصفوفة',
|
||||
univer: 'Univer',
|
||||
user: 'معرّف من قبل المستخدم',
|
||||
definedname: 'اسم معرّف',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'تأكيد',
|
||||
prev: 'السابق',
|
||||
next: 'التالي',
|
||||
searchFunctionPlaceholder: 'البحث عن دالة',
|
||||
allFunctions: 'كل الدوال',
|
||||
syntax: 'الصيغة',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'إدراج دالة',
|
||||
recommended: 'موصى بها',
|
||||
recent: 'الأخيرة',
|
||||
details: 'التفاصيل',
|
||||
insert: 'إدراج الدالة',
|
||||
empty: 'لا توجد دوال',
|
||||
close: 'إغلاق',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'نسخ الصيغة فقط',
|
||||
pasteFormula: 'لصق الصيغة',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'تحديد نطاق بيانات',
|
||||
addAnotherRange: 'إضافة نطاق',
|
||||
buttonTooltip: 'تحديد نطاق بيانات',
|
||||
placeHolder: 'تحديد نطاق أو إدخاله.',
|
||||
confirm: 'تأكيد',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/ca-ES.ts
Normal file
103
packages/sheets-formula-ui/src/locale/ca-ES.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Suma ràpida',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funcions',
|
||||
common: 'Funcions habituals',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'EXEMPLE',
|
||||
helpAbstract: 'SOBRE',
|
||||
required: 'Obligatori.',
|
||||
optional: 'Opcional.',
|
||||
},
|
||||
error: {
|
||||
title: 'Error',
|
||||
divByZero: 'Error de divisió per zero',
|
||||
name: 'Error de nom no vàlid',
|
||||
value: 'Error en el valor',
|
||||
num: 'Error numèric',
|
||||
na: 'Error de valor no disponible',
|
||||
cycle: 'Error de referència circular',
|
||||
ref: 'Error de referència de cel·la no vàlida',
|
||||
spill: 'L\'interval de vessament no està buit',
|
||||
calc: 'Error de càlcul',
|
||||
error: 'Error',
|
||||
connect: 'Obtenint dades',
|
||||
null: 'Error nul',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Financera',
|
||||
date: 'Data i hora',
|
||||
math: 'Matemàtiques i trigonometria',
|
||||
statistical: 'Estadística',
|
||||
lookup: 'Cerca i referència',
|
||||
database: 'Base de dades',
|
||||
text: 'Text',
|
||||
logical: 'Lògica',
|
||||
information: 'Informació',
|
||||
engineering: 'Enginyeria',
|
||||
cube: 'Cub',
|
||||
compatibility: 'Compatibilitat',
|
||||
web: 'Web',
|
||||
array: 'Matriu',
|
||||
univer: 'Univer',
|
||||
user: 'Definida per l\'usuari',
|
||||
definedname: 'Nom definit',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Confirma',
|
||||
prev: 'Anterior',
|
||||
next: 'Següent',
|
||||
searchFunctionPlaceholder: 'Cerca funció',
|
||||
allFunctions: 'Totes les funcions',
|
||||
syntax: 'SINTAXI',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Insereix una funció',
|
||||
recommended: 'Recomanades',
|
||||
recent: 'Recents',
|
||||
details: 'Detalls',
|
||||
insert: 'Insereix la funció',
|
||||
empty: 'No hi ha funcions',
|
||||
close: 'Tanca',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Copia només la fórmula',
|
||||
pasteFormula: 'Enganxa la fórmula',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Selecciona un interval de dades',
|
||||
addAnotherRange: 'Afegeix interval',
|
||||
buttonTooltip: 'Selecciona interval de dades',
|
||||
placeHolder: 'Selecciona un interval o escriu.',
|
||||
confirm: 'Confirma',
|
||||
cancel: 'Cancel·la',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/de-DE.ts
Normal file
103
packages/sheets-formula-ui/src/locale/de-DE.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Schnelle Summe',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funktionen',
|
||||
common: 'Häufige Funktionen',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'BEISPIEL',
|
||||
helpAbstract: 'INFO',
|
||||
required: 'Erforderlich.',
|
||||
optional: 'Optional.',
|
||||
},
|
||||
error: {
|
||||
title: 'Fehler',
|
||||
divByZero: 'Divisionsfehler',
|
||||
name: 'Ungültiger Name',
|
||||
value: 'Wertfehler',
|
||||
num: 'Zahlenfehler',
|
||||
na: 'Wert nicht verfügbar',
|
||||
cycle: 'Zirkelbezug',
|
||||
ref: 'Ungültiger Zellbezug',
|
||||
spill: 'Bereich ist nicht leer',
|
||||
calc: 'Berechnungsfehler',
|
||||
error: 'Fehler',
|
||||
connect: 'Daten werden abgerufen',
|
||||
null: 'Null-Fehler',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Finanzmathematik',
|
||||
date: 'Datum und Uhrzeit',
|
||||
math: 'Mathematik und Trigonometrie',
|
||||
statistical: 'Statistik',
|
||||
lookup: 'Suchen und Verweisen',
|
||||
database: 'Datenbank',
|
||||
text: 'Text',
|
||||
logical: 'Logik',
|
||||
information: 'Information',
|
||||
engineering: 'Technik',
|
||||
cube: 'Cube',
|
||||
compatibility: 'Kompatibilität',
|
||||
web: 'Web',
|
||||
array: 'Array',
|
||||
univer: 'Univer',
|
||||
user: 'Benutzerdefiniert',
|
||||
definedname: 'Definierter Name',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Bestätigen',
|
||||
prev: 'Zurück',
|
||||
next: 'Weiter',
|
||||
searchFunctionPlaceholder: 'Funktion suchen',
|
||||
allFunctions: 'Alle Funktionen',
|
||||
syntax: 'SYNTAX',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Funktion einfügen',
|
||||
recommended: 'Empfohlen',
|
||||
recent: 'Zuletzt verwendet',
|
||||
details: 'Details',
|
||||
insert: 'Funktion einfügen',
|
||||
empty: 'Keine Funktionen',
|
||||
close: 'Schließen',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Nur Formel kopieren',
|
||||
pasteFormula: 'Formel einfügen',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Datenbereich auswählen',
|
||||
addAnotherRange: 'Bereich hinzufügen',
|
||||
buttonTooltip: 'Datenbereich auswählen',
|
||||
placeHolder: 'Bereich auswählen oder eingeben.',
|
||||
confirm: 'Bestätigen',
|
||||
cancel: 'Abbrechen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
101
packages/sheets-formula-ui/src/locale/en-US.ts
Normal file
101
packages/sheets-formula-ui/src/locale/en-US.ts
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Quick Sum',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Functions',
|
||||
common: 'Common Functions',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'EXAMPLE',
|
||||
helpAbstract: 'ABOUT',
|
||||
required: 'Required.',
|
||||
optional: 'Optional.',
|
||||
},
|
||||
error: {
|
||||
title: 'Error',
|
||||
divByZero: 'Divide by zero error',
|
||||
name: 'Invalid name error',
|
||||
value: 'Error in value',
|
||||
num: 'Number error',
|
||||
na: 'Value not available error',
|
||||
cycle: 'Circular reference error',
|
||||
ref: 'Invalid cell reference error',
|
||||
spill: "Spill range isn't blank",
|
||||
calc: 'Calculation error',
|
||||
error: 'Error',
|
||||
connect: 'Getting data',
|
||||
null: 'Null Error',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Financial',
|
||||
date: 'Date & Time',
|
||||
math: 'Math & Trig',
|
||||
statistical: 'Statistical',
|
||||
lookup: 'Lookup & Reference',
|
||||
database: 'Database',
|
||||
text: 'Text',
|
||||
logical: 'Logical',
|
||||
information: 'Information',
|
||||
engineering: 'Engineering',
|
||||
cube: 'Cube',
|
||||
compatibility: 'Compatibility',
|
||||
web: 'Web',
|
||||
array: 'Array',
|
||||
univer: 'Univer',
|
||||
user: 'User Defined',
|
||||
definedname: 'Defined Name',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Confirm',
|
||||
prev: 'Previous',
|
||||
next: 'Next',
|
||||
searchFunctionPlaceholder: 'Search function',
|
||||
allFunctions: 'All Functions',
|
||||
syntax: 'SYNTAX',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Insert function',
|
||||
recommended: 'Recommended',
|
||||
recent: 'Recent',
|
||||
details: 'Details',
|
||||
insert: 'Insert function',
|
||||
empty: 'No functions',
|
||||
close: 'Close',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Copy Formula Only',
|
||||
pasteFormula: 'Paste Formula',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Select a data range',
|
||||
addAnotherRange: 'Add range',
|
||||
buttonTooltip: 'Select data range',
|
||||
placeHolder: 'Select range or enter.',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/es-ES.ts
Normal file
103
packages/sheets-formula-ui/src/locale/es-ES.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Suma rápida',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funciones',
|
||||
common: 'Funciones comunes',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'EJEMPLO',
|
||||
helpAbstract: 'ACERCA DE',
|
||||
required: 'Obligatorio.',
|
||||
optional: 'Opcional.',
|
||||
},
|
||||
error: {
|
||||
title: 'Error',
|
||||
divByZero: 'Error de división por cero',
|
||||
name: 'Error de nombre no válido',
|
||||
value: 'Error en el valor',
|
||||
num: 'Error numérico',
|
||||
na: 'Error de valor no disponible',
|
||||
cycle: 'Error de referencia circular',
|
||||
ref: 'Error de referencia de celda no válida',
|
||||
spill: 'El rango de desbordamiento no está vacío',
|
||||
calc: 'Error de cálculo',
|
||||
error: 'Error',
|
||||
connect: 'Obteniendo datos',
|
||||
null: 'Error nulo',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Financiera',
|
||||
date: 'Fecha y hora',
|
||||
math: 'Matemáticas y trigonometría',
|
||||
statistical: 'Estadística',
|
||||
lookup: 'Búsqueda y referencia',
|
||||
database: 'Base de datos',
|
||||
text: 'Texto',
|
||||
logical: 'Lógica',
|
||||
information: 'Información',
|
||||
engineering: 'Ingeniería',
|
||||
cube: 'Cubo',
|
||||
compatibility: 'Compatibilidad',
|
||||
web: 'Web',
|
||||
array: 'Matriz',
|
||||
univer: 'Univer',
|
||||
user: 'Definida por el usuario',
|
||||
definedname: 'Nombre definido',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Confirmar',
|
||||
prev: 'Anterior',
|
||||
next: 'Siguiente',
|
||||
searchFunctionPlaceholder: 'Buscar función',
|
||||
allFunctions: 'Todas las funciones',
|
||||
syntax: 'SINTAXIS',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Insertar función',
|
||||
recommended: 'Recomendadas',
|
||||
recent: 'Recientes',
|
||||
details: 'Detalles',
|
||||
insert: 'Insertar función',
|
||||
empty: 'No hay funciones',
|
||||
close: 'Cerrar',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Copiar solo fórmula',
|
||||
pasteFormula: 'Pegar fórmula',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Selecciona un rango de datos',
|
||||
addAnotherRange: 'Agregar rango',
|
||||
buttonTooltip: 'Seleccionar rango de datos',
|
||||
placeHolder: 'Selecciona un rango o escribe.',
|
||||
confirm: 'Confirmar',
|
||||
cancel: 'Cancelar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/fa-IR.ts
Normal file
103
packages/sheets-formula-ui/src/locale/fa-IR.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'جمع سریع',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'توابع',
|
||||
common: 'توابع پرکاربرد',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'مثال',
|
||||
helpAbstract: 'درباره',
|
||||
required: 'ضروری.',
|
||||
optional: 'اختیاری.',
|
||||
},
|
||||
error: {
|
||||
title: 'خطا',
|
||||
divByZero: 'خطای تقسیم بر صفر',
|
||||
name: 'خطای نام نامعتبر',
|
||||
value: 'خطا در مقدار',
|
||||
num: 'خطای عدد',
|
||||
na: 'خطای مقدار موجود نیست',
|
||||
cycle: 'خطای مرجع دورهای',
|
||||
ref: 'خطای مرجع سلول نامعتبر',
|
||||
spill: 'دامنه ریزش خالی نیست',
|
||||
calc: 'خطای محاسبه',
|
||||
error: 'خطا',
|
||||
connect: 'در حال دریافت دادهها',
|
||||
null: 'خطای تهی',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'مالی',
|
||||
date: 'تاریخ و زمان',
|
||||
math: 'ریاضی و مثلثاتی',
|
||||
statistical: 'آماری',
|
||||
lookup: 'جستجو و مرجع',
|
||||
database: 'پایگاه داده',
|
||||
text: 'متن',
|
||||
logical: 'منطقی',
|
||||
information: 'اطلاعاتی',
|
||||
engineering: 'مهندسی',
|
||||
cube: 'مکعب',
|
||||
compatibility: 'سازگاری',
|
||||
web: 'وب',
|
||||
array: 'آرایه',
|
||||
univer: 'Univer',
|
||||
user: 'تعریف شده توسط کاربر',
|
||||
definedname: 'نام تعریف شده',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'تایید',
|
||||
prev: 'قبلی',
|
||||
next: 'بعدی',
|
||||
searchFunctionPlaceholder: 'جستجوی تابع',
|
||||
allFunctions: 'همه توابع',
|
||||
syntax: 'سینتکس',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'درج تابع',
|
||||
recommended: 'پیشنهادی',
|
||||
recent: 'اخیر',
|
||||
details: 'جزئیات',
|
||||
insert: 'درج تابع',
|
||||
empty: 'تابعی وجود ندارد',
|
||||
close: 'بستن',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'کپی فقط فرمول',
|
||||
pasteFormula: 'چسباندن فرمول',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'انتخاب محدوده داده',
|
||||
addAnotherRange: 'افزودن محدوده',
|
||||
buttonTooltip: 'انتخاب محدوده داده',
|
||||
placeHolder: 'انتخاب محدوده یا وارد کردن.',
|
||||
confirm: 'تایید',
|
||||
cancel: 'انصراف',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/fr-FR.ts
Normal file
103
packages/sheets-formula-ui/src/locale/fr-FR.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Somme rapide',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Fonctions',
|
||||
common: 'Fonctions courantes',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'EXEMPLE',
|
||||
helpAbstract: 'À PROPOS',
|
||||
required: 'Requis.',
|
||||
optional: 'Optionnel.',
|
||||
},
|
||||
error: {
|
||||
title: 'Erreur',
|
||||
divByZero: 'Erreur de division par zéro',
|
||||
name: 'Erreur de nom invalide',
|
||||
value: 'Erreur de valeur',
|
||||
num: 'Erreur de nombre',
|
||||
na: 'Erreur de valeur non disponible',
|
||||
cycle: 'Erreur de référence circulaire',
|
||||
ref: 'Erreur de référence de cellule invalide',
|
||||
spill: "La plage de débordement n'est pas vide",
|
||||
calc: 'Erreur de calcul',
|
||||
error: 'Erreur',
|
||||
connect: 'Récupération des données',
|
||||
null: 'Erreur nulle',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Financier',
|
||||
date: 'Date & Heure',
|
||||
math: 'Math & Trigo',
|
||||
statistical: 'Statistique',
|
||||
lookup: 'Recherche & Référence',
|
||||
database: 'Base de données',
|
||||
text: 'Texte',
|
||||
logical: 'Logique',
|
||||
information: 'Information',
|
||||
engineering: 'Ingénierie',
|
||||
cube: 'Cube',
|
||||
compatibility: 'Compatibilité',
|
||||
web: 'Web',
|
||||
array: 'Tableau',
|
||||
univer: 'Univer',
|
||||
user: 'Définies par l\'utilisateur',
|
||||
definedname: 'Nom défini',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Confirmer',
|
||||
prev: 'Précédent',
|
||||
next: 'Suivant',
|
||||
searchFunctionPlaceholder: 'Rechercher une fonction',
|
||||
allFunctions: 'Toutes les fonctions',
|
||||
syntax: 'SYNTAXE',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Insérer une fonction',
|
||||
recommended: 'Recommandées',
|
||||
recent: 'Récentes',
|
||||
details: 'Détails',
|
||||
insert: 'Insérer la fonction',
|
||||
empty: 'Aucune fonction',
|
||||
close: 'Fermer',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Copier uniquement la formule',
|
||||
pasteFormula: 'Coller la formule',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Sélectionner une plage de données',
|
||||
addAnotherRange: 'Ajouter une plage',
|
||||
buttonTooltip: 'Sélectionner une plage de données',
|
||||
placeHolder: 'Sélectionner une plage ou entrer.',
|
||||
confirm: 'Confirmer',
|
||||
cancel: 'Annuler',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/id-ID.ts
Normal file
103
packages/sheets-formula-ui/src/locale/id-ID.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Jumlah Cepat',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Fungsi',
|
||||
common: 'Fungsi Umum',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'CONTOH',
|
||||
helpAbstract: 'TENTANG',
|
||||
required: 'Wajib.',
|
||||
optional: 'Opsional.',
|
||||
},
|
||||
error: {
|
||||
title: 'Kesalahan',
|
||||
divByZero: 'Kesalahan pembagian dengan nol',
|
||||
name: 'Kesalahan nama tidak valid',
|
||||
value: 'Kesalahan pada nilai',
|
||||
num: 'Kesalahan angka',
|
||||
na: 'Kesalahan nilai tidak tersedia',
|
||||
cycle: 'Kesalahan referensi sirkular',
|
||||
ref: 'Kesalahan referensi sel tidak valid',
|
||||
spill: 'Rentang tumpahan tidak kosong',
|
||||
calc: 'Kesalahan perhitungan',
|
||||
error: 'Kesalahan',
|
||||
connect: 'Mendapatkan data',
|
||||
null: 'Kesalahan Nol',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Keuangan',
|
||||
date: 'Tanggal & Waktu',
|
||||
math: 'Matematika & Trigonometri',
|
||||
statistical: 'Statistik',
|
||||
lookup: 'Pencarian & Referensi',
|
||||
database: 'Database',
|
||||
text: 'Teks',
|
||||
logical: 'Logika',
|
||||
information: 'Informasi',
|
||||
engineering: 'Teknik',
|
||||
cube: 'Kubus',
|
||||
compatibility: 'Kompatibilitas',
|
||||
web: 'Web',
|
||||
array: 'Array',
|
||||
univer: 'Univer',
|
||||
user: 'Ditetapkan Pengguna',
|
||||
definedname: 'Nama Ditetapkan',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Konfirmasi',
|
||||
prev: 'Sebelumnya',
|
||||
next: 'Berikutnya',
|
||||
searchFunctionPlaceholder: 'Cari fungsi',
|
||||
allFunctions: 'Semua Fungsi',
|
||||
syntax: 'SINTAKS',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Sisipkan fungsi',
|
||||
recommended: 'Direkomendasikan',
|
||||
recent: 'Terbaru',
|
||||
details: 'Detail',
|
||||
insert: 'Sisipkan fungsi',
|
||||
empty: 'Tidak ada fungsi',
|
||||
close: 'Tutup',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Salin Hanya Rumus',
|
||||
pasteFormula: 'Tempel Rumus',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Pilih rentang data',
|
||||
addAnotherRange: 'Tambah rentang',
|
||||
buttonTooltip: 'Pilih rentang data',
|
||||
placeHolder: 'Pilih rentang atau ketik.',
|
||||
confirm: 'Konfirmasi',
|
||||
cancel: 'Batal',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/it-IT.ts
Normal file
103
packages/sheets-formula-ui/src/locale/it-IT.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Somma rapida',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funzioni',
|
||||
common: 'Funzioni comuni',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'ESEMPIO',
|
||||
helpAbstract: 'DESCRIZIONE',
|
||||
required: 'Obbligatorio.',
|
||||
optional: 'Facoltativo.',
|
||||
},
|
||||
error: {
|
||||
title: 'Errore',
|
||||
divByZero: 'Errore di divisione per zero',
|
||||
name: 'Errore di nome non valido',
|
||||
value: 'Errore nel valore',
|
||||
num: 'Errore numerico',
|
||||
na: 'Errore valore non disponibile',
|
||||
cycle: 'Errore di riferimento circolare',
|
||||
ref: 'Errore di riferimento cella non valido',
|
||||
spill: 'L\'intervallo di spill non è vuoto',
|
||||
calc: 'Errore di calcolo',
|
||||
error: 'Errore',
|
||||
connect: 'Recupero dati',
|
||||
null: 'Errore nullo',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Finanziarie',
|
||||
date: 'Data e ora',
|
||||
math: 'Matematiche e trigonometriche',
|
||||
statistical: 'Statistiche',
|
||||
lookup: 'Ricerca e riferimento',
|
||||
database: 'Database',
|
||||
text: 'Testo',
|
||||
logical: 'Logiche',
|
||||
information: 'Informative',
|
||||
engineering: 'Ingegneristiche',
|
||||
cube: 'Cubi',
|
||||
compatibility: 'Compatibilità',
|
||||
web: 'Web',
|
||||
array: 'Matrici',
|
||||
univer: 'Univer',
|
||||
user: 'Definite dall\'utente',
|
||||
definedname: 'Nomi definiti',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Conferma',
|
||||
prev: 'Precedente',
|
||||
next: 'Successivo',
|
||||
searchFunctionPlaceholder: 'Cerca funzione',
|
||||
allFunctions: 'Tutte le funzioni',
|
||||
syntax: 'SINTASSI',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Inserisci funzione',
|
||||
recommended: 'Consigliate',
|
||||
recent: 'Recenti',
|
||||
details: 'Dettagli',
|
||||
insert: 'Inserisci funzione',
|
||||
empty: 'Nessuna funzione',
|
||||
close: 'Chiudi',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Copia solo formula',
|
||||
pasteFormula: 'Incolla formula',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Seleziona un intervallo di dati',
|
||||
addAnotherRange: 'Aggiungi intervallo',
|
||||
buttonTooltip: 'Seleziona intervallo di dati',
|
||||
placeHolder: 'Seleziona intervallo o digita.',
|
||||
confirm: 'Conferma',
|
||||
cancel: 'Annulla',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/ja-JP.ts
Normal file
103
packages/sheets-formula-ui/src/locale/ja-JP.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'クイック合計',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: '関数',
|
||||
common: 'よく使う関数',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: '例',
|
||||
helpAbstract: '説明',
|
||||
required: '必須.',
|
||||
optional: '省略.',
|
||||
},
|
||||
error: {
|
||||
title: 'エラー',
|
||||
divByZero: '0での除算エラー',
|
||||
name: '名前エラー',
|
||||
value: '値エラー',
|
||||
num: '数値エラー',
|
||||
na: '利用できない値エラー',
|
||||
cycle: '循環参照エラー',
|
||||
ref: '不正なセル参照エラー',
|
||||
spill: 'スピル範囲のエラー',
|
||||
calc: '計算エラー',
|
||||
error: 'エラー',
|
||||
connect: 'データ取得中',
|
||||
null: 'Null エラー',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: '財務',
|
||||
date: '日付と時刻',
|
||||
math: '数学および三角関数',
|
||||
statistical: '統計',
|
||||
lookup: '検索と参照',
|
||||
database: 'データベース',
|
||||
text: 'テキスト',
|
||||
logical: '論理',
|
||||
information: '情報',
|
||||
engineering: '工学',
|
||||
cube: 'キューブ',
|
||||
compatibility: '互換性',
|
||||
web: 'Web',
|
||||
array: '配列',
|
||||
univer: 'Univer',
|
||||
user: 'ユーザー定義',
|
||||
definedname: '定義済みの名前',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'OK',
|
||||
prev: '前へ',
|
||||
next: '次へ',
|
||||
searchFunctionPlaceholder: '関数を検索',
|
||||
allFunctions: 'すべての関数',
|
||||
syntax: '構文',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: '関数を挿入',
|
||||
recommended: 'おすすめ',
|
||||
recent: '最近使用した関数',
|
||||
details: '詳細',
|
||||
insert: '関数を挿入',
|
||||
empty: '関数がありません',
|
||||
close: '閉じる',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: '数式のみをコピー',
|
||||
pasteFormula: '数式を貼り付け',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'データ範囲の選択',
|
||||
addAnotherRange: '範囲を追加',
|
||||
buttonTooltip: 'データ範囲を選択',
|
||||
placeHolder: '範囲を選択または入力してください。',
|
||||
confirm: '確認',
|
||||
cancel: 'キャンセル',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/ko-KR.ts
Normal file
103
packages/sheets-formula-ui/src/locale/ko-KR.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': '빠른 합계',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: '함수',
|
||||
common: '자주 쓰는 함수',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: '예제',
|
||||
helpAbstract: '정보',
|
||||
required: '필수.',
|
||||
optional: '선택.',
|
||||
},
|
||||
error: {
|
||||
title: '오류',
|
||||
divByZero: '0으로 나누기 오류',
|
||||
name: '잘못된 이름 오류',
|
||||
value: '값 오류',
|
||||
num: '숫자 오류',
|
||||
na: '사용할 수 없는 값 오류',
|
||||
cycle: '순환 참조 오류',
|
||||
ref: '잘못된 셀 참조 오류',
|
||||
spill: '분산 범위가 비어 있지 않음',
|
||||
calc: '계산 오류',
|
||||
error: '오류',
|
||||
connect: '데이터 가져오는 중',
|
||||
null: 'Null 오류',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: '재무',
|
||||
date: '날짜 및 시간',
|
||||
math: '수학 및 삼각 함수',
|
||||
statistical: '통계',
|
||||
lookup: '찾기 및 참조',
|
||||
database: '데이터베이스',
|
||||
text: '텍스트',
|
||||
logical: '논리',
|
||||
information: '정보',
|
||||
engineering: '공학',
|
||||
cube: '큐브',
|
||||
compatibility: '호환성',
|
||||
web: '웹',
|
||||
array: '배열',
|
||||
univer: 'Univer',
|
||||
user: '사용자 정의',
|
||||
definedname: '정의된 이름',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: '확인',
|
||||
prev: '이전',
|
||||
next: '다음',
|
||||
searchFunctionPlaceholder: '함수 검색',
|
||||
allFunctions: '모든 함수',
|
||||
syntax: '구문',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: '함수 삽입',
|
||||
recommended: '추천',
|
||||
recent: '최근',
|
||||
details: '세부 정보',
|
||||
insert: '함수 삽입',
|
||||
empty: '함수가 없습니다',
|
||||
close: '닫기',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: '수식만 복사',
|
||||
pasteFormula: '수식 붙여넣기',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: '데이터 범위 선택',
|
||||
addAnotherRange: '범위 추가',
|
||||
buttonTooltip: '데이터 범위 선택',
|
||||
placeHolder: '범위를 선택하거나 입력하세요.',
|
||||
confirm: '확인',
|
||||
cancel: '취소',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/pl-PL.ts
Normal file
103
packages/sheets-formula-ui/src/locale/pl-PL.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Szybka suma',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funkcje',
|
||||
common: 'Często używane funkcje',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'PRZYKŁAD',
|
||||
helpAbstract: 'OPIS',
|
||||
required: 'Wymagane.',
|
||||
optional: 'Opcjonalne.',
|
||||
},
|
||||
error: {
|
||||
title: 'Błąd',
|
||||
divByZero: 'Błąd dzielenia przez zero',
|
||||
name: 'Błąd nieprawidłowej nazwy',
|
||||
value: 'Błąd wartości',
|
||||
num: 'Błąd liczby',
|
||||
na: 'Wartość niedostępna',
|
||||
cycle: 'Błąd odwołania cyklicznego',
|
||||
ref: 'Błąd nieprawidłowego odwołania do komórki',
|
||||
spill: 'Zakres rozlewania nie jest pusty',
|
||||
calc: 'Błąd obliczenia',
|
||||
error: 'Błąd',
|
||||
connect: 'Pobieranie danych',
|
||||
null: 'Błąd pusty',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Finansowe',
|
||||
date: 'Data i czas',
|
||||
math: 'Matematyczne i trygonometryczne',
|
||||
statistical: 'Statystyczne',
|
||||
lookup: 'Wyszukiwanie i odwołania',
|
||||
database: 'Bazy danych',
|
||||
text: 'Tekstowe',
|
||||
logical: 'Logiczne',
|
||||
information: 'Informacyjne',
|
||||
engineering: 'Inżynierskie',
|
||||
cube: 'Wielowymiarowe',
|
||||
compatibility: 'Zgodność',
|
||||
web: 'Sieć Web',
|
||||
array: 'Tablicowe',
|
||||
univer: 'Univer',
|
||||
user: 'Zdefiniowane przez użytkownika',
|
||||
definedname: 'Nazwa zdefiniowana',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Potwierdź',
|
||||
prev: 'Poprzednie',
|
||||
next: 'Następne',
|
||||
searchFunctionPlaceholder: 'Wyszukaj funkcję',
|
||||
allFunctions: 'Wszystkie funkcje',
|
||||
syntax: 'SKŁADNIA',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Wstaw funkcję',
|
||||
recommended: 'Polecane',
|
||||
recent: 'Ostatnie',
|
||||
details: 'Szczegóły',
|
||||
insert: 'Wstaw funkcję',
|
||||
empty: 'Brak funkcji',
|
||||
close: 'Zamknij',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Kopiuj tylko formułę',
|
||||
pasteFormula: 'Wklej formułę',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Wybierz zakres danych',
|
||||
addAnotherRange: 'Dodaj zakres',
|
||||
buttonTooltip: 'Wybierz zakres danych',
|
||||
placeHolder: 'Wybierz zakres lub wpisz.',
|
||||
confirm: 'Potwierdź',
|
||||
cancel: 'Anuluj',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/pt-BR.ts
Normal file
103
packages/sheets-formula-ui/src/locale/pt-BR.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Soma Rápida',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funções',
|
||||
common: 'Funções Comuns',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'EXEMPLO',
|
||||
helpAbstract: 'SOBRE',
|
||||
required: 'Obrigatório.',
|
||||
optional: 'Opcional.',
|
||||
},
|
||||
error: {
|
||||
title: 'Erro',
|
||||
divByZero: 'Erro de divisão por zero',
|
||||
name: 'Erro de nome inválido',
|
||||
value: 'Erro no valor',
|
||||
num: 'Erro numérico',
|
||||
na: 'Erro de valor não disponível',
|
||||
cycle: 'Erro de referência circular',
|
||||
ref: 'Erro de referência de célula inválida',
|
||||
spill: 'O intervalo de derramamento não está em branco',
|
||||
calc: 'Erro de cálculo',
|
||||
error: 'Erro',
|
||||
connect: 'Obtendo dados',
|
||||
null: 'Erro Nulo',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Financeira',
|
||||
date: 'Data e Hora',
|
||||
math: 'Matemática e Trigonométrica',
|
||||
statistical: 'Estatística',
|
||||
lookup: 'Procura e Referência',
|
||||
database: 'Banco de Dados',
|
||||
text: 'Texto',
|
||||
logical: 'Lógica',
|
||||
information: 'Informação',
|
||||
engineering: 'Engenharia',
|
||||
cube: 'Cubo',
|
||||
compatibility: 'Compatibilidade',
|
||||
web: 'Web',
|
||||
array: 'Matriz',
|
||||
univer: 'Univer',
|
||||
user: 'Definida pelo Usuário',
|
||||
definedname: 'Nome Definido',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Confirmar',
|
||||
prev: 'Anterior',
|
||||
next: 'Próxima',
|
||||
searchFunctionPlaceholder: 'Pesquisar função',
|
||||
allFunctions: 'Todas as Funções',
|
||||
syntax: 'SINTAXE',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Inserir função',
|
||||
recommended: 'Recomendadas',
|
||||
recent: 'Recentes',
|
||||
details: 'Detalhes',
|
||||
insert: 'Inserir função',
|
||||
empty: 'Nenhuma função',
|
||||
close: 'Fechar',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Copiar Apenas Fórmula',
|
||||
pasteFormula: 'Colar Fórmula',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Selecionar um intervalo de dados',
|
||||
addAnotherRange: 'Adicionar intervalo',
|
||||
buttonTooltip: 'Selecionar intervalo de dados',
|
||||
placeHolder: 'Selecione o intervalo ou digite.',
|
||||
confirm: 'Confirmar',
|
||||
cancel: 'Cancelar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/ru-RU.ts
Normal file
103
packages/sheets-formula-ui/src/locale/ru-RU.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Быстрая сумма',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Функции',
|
||||
common: 'Часто используемые функции',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'ПРИМЕР',
|
||||
helpAbstract: 'ОПИСАНИЕ',
|
||||
required: 'Обязательно.',
|
||||
optional: 'Необязательно.',
|
||||
},
|
||||
error: {
|
||||
title: 'Ошибка',
|
||||
divByZero: 'Ошибка деления на ноль',
|
||||
name: 'Ошибка неверного имени',
|
||||
value: 'Ошибка в значении',
|
||||
num: 'Ошибка числа',
|
||||
na: 'Ошибка недоступного значения',
|
||||
cycle: 'Ошибка циклической ссылки',
|
||||
ref: 'Ошибка неверной ссылки на ячейку',
|
||||
spill: 'Диапазон разлива не пуст',
|
||||
calc: 'Ошибка вычисления',
|
||||
error: 'Ошибка',
|
||||
connect: 'Получение данных',
|
||||
null: 'Ошибка нулевого значения',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Финансовые',
|
||||
date: 'Дата и время',
|
||||
math: 'Математика и тригонометрия',
|
||||
statistical: 'Статистические',
|
||||
lookup: 'Поиск и ссылки',
|
||||
database: 'База данных',
|
||||
text: 'Текстовые',
|
||||
logical: 'Логические',
|
||||
information: 'Информационные',
|
||||
engineering: 'Инженерные',
|
||||
cube: 'Куб',
|
||||
compatibility: 'Совместимость',
|
||||
web: 'Веб',
|
||||
array: 'Массив',
|
||||
univer: 'Универсальные',
|
||||
user: 'Пользовательские',
|
||||
definedname: 'Определенное имя',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Подтвердить',
|
||||
prev: 'Предыдущий',
|
||||
next: 'Следующий',
|
||||
searchFunctionPlaceholder: 'Поиск функции',
|
||||
allFunctions: 'Все функции',
|
||||
syntax: 'СИНТАКСИС',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Вставить функцию',
|
||||
recommended: 'Рекомендуемые',
|
||||
recent: 'Недавние',
|
||||
details: 'Подробнее',
|
||||
insert: 'Вставить функцию',
|
||||
empty: 'Нет функций',
|
||||
close: 'Закрыть',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Копировать только формулу',
|
||||
pasteFormula: 'Вставить Формулу',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Выберите диапазон данных',
|
||||
addAnotherRange: 'Добавить диапазон',
|
||||
buttonTooltip: 'Выбрать диапазон данных',
|
||||
placeHolder: 'Выберите диапазон или введите.',
|
||||
confirm: 'Подтвердить',
|
||||
cancel: 'Отменить',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
103
packages/sheets-formula-ui/src/locale/sk-SK.ts
Normal file
103
packages/sheets-formula-ui/src/locale/sk-SK.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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Rýchly súčet',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Funkcie',
|
||||
common: 'Bežné funkcie',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'PRÍKLAD',
|
||||
helpAbstract: 'O FUNKCII',
|
||||
required: 'Povinné.',
|
||||
optional: 'Voliteľné.',
|
||||
},
|
||||
error: {
|
||||
title: 'Chyba',
|
||||
divByZero: 'Chyba delenia nulou',
|
||||
name: 'Neplatný názov',
|
||||
value: 'Chyba v hodnote',
|
||||
num: 'Číselná chyba',
|
||||
na: 'Hodnota nie je k dispozícii',
|
||||
cycle: 'Chyba kruhovej referencie',
|
||||
ref: 'Neplatný odkaz na bunku',
|
||||
spill: 'Rozliaty rozsah nie je prázdny',
|
||||
calc: 'Chyba výpočtu',
|
||||
error: 'Chyba',
|
||||
connect: 'Načítavajú sa údaje',
|
||||
null: 'Nulová chyba',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Finančné',
|
||||
date: 'Dátum a čas',
|
||||
math: 'Matematika a trigonometria',
|
||||
statistical: 'Štatistické',
|
||||
lookup: 'Vyhľadávanie a odkazy',
|
||||
database: 'Databáza',
|
||||
text: 'Text',
|
||||
logical: 'Logické',
|
||||
information: 'Informácie',
|
||||
engineering: 'Inžinierske',
|
||||
cube: 'Kocka',
|
||||
compatibility: 'Kompatibilita',
|
||||
web: 'Web',
|
||||
array: 'Pole',
|
||||
univer: 'Univer',
|
||||
user: 'Definované používateľom',
|
||||
definedname: 'Definovaný názov',
|
||||
},
|
||||
moreFunctions: {
|
||||
confirm: 'Potvrdiť',
|
||||
prev: 'Predchádzajúce',
|
||||
next: 'Nasledujúce',
|
||||
searchFunctionPlaceholder: 'Hľadať funkciu',
|
||||
allFunctions: 'Všetky funkcie',
|
||||
syntax: 'SYNTAX',
|
||||
},
|
||||
mobileFunction: {
|
||||
title: 'Vložiť funkciu',
|
||||
recommended: 'Odporúčané',
|
||||
recent: 'Nedávne',
|
||||
details: 'Podrobnosti',
|
||||
insert: 'Vložiť funkciu',
|
||||
empty: 'Žiadne funkcie',
|
||||
close: 'Zavrieť',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Kopírovať iba vzorec',
|
||||
pasteFormula: 'Prilepiť vzorec',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: 'Vyberte rozsah údajov',
|
||||
addAnotherRange: 'Pridať rozsah',
|
||||
buttonTooltip: 'Vybrať rozsah údajov',
|
||||
placeHolder: 'Vyberte rozsah alebo zadajte.',
|
||||
confirm: 'Potvrdiť',
|
||||
cancel: 'Zrušiť',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-formula-ui/src/locale/types.ts
Normal file
20
packages/sheets-formula-ui/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
106
packages/sheets-formula-ui/src/locale/vi-VN.ts
Normal file
106
packages/sheets-formula-ui/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': 'Tổng nhanh',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: 'Hàm',
|
||||
common: 'Hàm thường dùng',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: 'Ví dụ',
|
||||
helpAbstract: 'Giới thiệu',
|
||||
required: 'Bắt buộc.',
|
||||
optional: 'Tùy chọn.',
|
||||
},
|
||||
|
||||
error: {
|
||||
title: 'Lỗi',
|
||||
divByZero: 'Mẫu số bằng không',
|
||||
name: 'Tên không hợp lệ',
|
||||
value: 'Lỗi trong giá trị',
|
||||
num: 'Lỗi số',
|
||||
na: 'Giá trị không khả dụng',
|
||||
cycle: 'Tham chiếu vòng lặp',
|
||||
ref: 'Tham chiếu ô không hợp lệ',
|
||||
spill: 'Vùng tràn không phải vùng trống',
|
||||
calc: 'Lỗi tính toán',
|
||||
error: 'Lỗi',
|
||||
connect: 'Đang kết nối',
|
||||
null: 'Lỗi giá trị rỗng',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: 'Tài chính',
|
||||
date: 'Ngày và giờ',
|
||||
math: 'Toán học và lượng giác',
|
||||
statistical: 'Thống kê',
|
||||
lookup: 'Tra cứu và tham chiếu',
|
||||
database: 'Cơ sở dữ liệu',
|
||||
text: 'Văn bản',
|
||||
logical: 'Lôgic',
|
||||
information: 'Thông tin',
|
||||
engineering: 'Kỹ thuật',
|
||||
cube: 'Khối dữ liệu',
|
||||
compatibility: 'Tương thích',
|
||||
web: 'Web',
|
||||
array: 'Mảng',
|
||||
univer: 'Univer',
|
||||
user: 'Tùy chỉnh của người dùng',
|
||||
definedname: 'Tên đã xác định',
|
||||
},
|
||||
|
||||
moreFunctions: {
|
||||
confirm: 'Áp dụng',
|
||||
prev: 'Bước trước',
|
||||
next: 'Bước tiếp theo',
|
||||
searchFunctionPlaceholder: 'Tìm kiếm hàm',
|
||||
allFunctions: 'Tất cả các hàm',
|
||||
syntax: 'Cú pháp',
|
||||
},
|
||||
|
||||
mobileFunction: {
|
||||
title: 'Chèn hàm',
|
||||
recommended: 'Đề xuất',
|
||||
recent: 'Gần đây',
|
||||
details: 'Chi tiết',
|
||||
insert: 'Chèn hàm',
|
||||
empty: 'Không có hàm',
|
||||
close: 'Đóng',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: 'Chỉ sao chép công thức',
|
||||
pasteFormula: 'Chỉ dán công thức',
|
||||
},
|
||||
rangeSelector: {
|
||||
title: 'Chọn một phạm vi dữ liệu',
|
||||
addAnotherRange: 'Thêm phạm vi',
|
||||
buttonTooltip: 'Chọn phạm vi dữ liệu',
|
||||
placeHolder: 'Chọn phạm vi hoặc nhập',
|
||||
confirm: 'Xác nhận',
|
||||
cancel: 'Hủy',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-formula-ui/src/locale/zh-CN.ts
Normal file
106
packages/sheets-formula-ui/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': '快速求和',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: '函数',
|
||||
common: '常用函数',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: '示例',
|
||||
helpAbstract: '简介',
|
||||
required: '必需。',
|
||||
optional: '可选。',
|
||||
},
|
||||
|
||||
error: {
|
||||
title: '错误',
|
||||
divByZero: '除数为零',
|
||||
name: '无效名称',
|
||||
value: '值中的错误',
|
||||
num: '数值错误',
|
||||
na: '值不可用',
|
||||
cycle: '循环引用',
|
||||
ref: '无效的单元格引用',
|
||||
spill: '溢出区域不是空白区域',
|
||||
calc: '计算错误',
|
||||
error: '错误',
|
||||
connect: '正在连接中',
|
||||
null: '空值错误',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: '财务',
|
||||
date: '日期与时间',
|
||||
math: '数学与三角函数',
|
||||
statistical: '统计',
|
||||
lookup: '查找与引用',
|
||||
database: '数据库',
|
||||
text: '文本',
|
||||
logical: '逻辑',
|
||||
information: '信息',
|
||||
engineering: '工程',
|
||||
cube: '多维数据集',
|
||||
compatibility: '兼容性',
|
||||
web: 'Web',
|
||||
array: '数组',
|
||||
univer: 'Univer',
|
||||
user: '用户自定义',
|
||||
definedname: '定义名称',
|
||||
},
|
||||
|
||||
moreFunctions: {
|
||||
confirm: '应用',
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
searchFunctionPlaceholder: '搜索函数',
|
||||
allFunctions: '全部函数',
|
||||
syntax: '语法',
|
||||
},
|
||||
|
||||
mobileFunction: {
|
||||
title: '插入函数',
|
||||
recommended: '推荐',
|
||||
recent: '最近',
|
||||
details: '详情',
|
||||
insert: '插入函数',
|
||||
empty: '暂无函数',
|
||||
close: '关闭',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: '仅复制公式',
|
||||
pasteFormula: '仅粘贴公式',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: '选择一个数据范围',
|
||||
addAnotherRange: '添加范围',
|
||||
buttonTooltip: '选择数据范围',
|
||||
placeHolder: '框选范围或输入',
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-formula-ui/src/locale/zh-HK.ts
Normal file
106
packages/sheets-formula-ui/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': '快速求和',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: '函數',
|
||||
common: '常用函數',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: '範例',
|
||||
helpAbstract: '簡介',
|
||||
required: '必要。 ',
|
||||
optional: '可選。 ',
|
||||
},
|
||||
|
||||
error: {
|
||||
title: '錯誤',
|
||||
divByZero: '除數為零',
|
||||
name: '無效名稱',
|
||||
value: '值中的錯誤',
|
||||
num: '數值錯誤',
|
||||
na: '值不可用',
|
||||
cycle: '循環引用',
|
||||
ref: '無效的儲存格引用',
|
||||
spill: '溢出區域不是空白區域',
|
||||
calc: '計算錯誤',
|
||||
error: '錯誤',
|
||||
connect: '正在連線中',
|
||||
null: '空值錯誤',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: '財務',
|
||||
date: '日期與時間',
|
||||
math: '數學與三角函數',
|
||||
statistical: '統計',
|
||||
lookup: '尋找與引用',
|
||||
database: '資料庫',
|
||||
text: '文字',
|
||||
logical: '邏輯',
|
||||
information: '訊息',
|
||||
engineering: '工程',
|
||||
cube: '多維資料集',
|
||||
compatibility: '兼容性',
|
||||
web: 'Web',
|
||||
array: '陣列',
|
||||
univer: 'Univer',
|
||||
user: '使用者自訂',
|
||||
definedname: '定義名稱',
|
||||
},
|
||||
|
||||
moreFunctions: {
|
||||
confirm: '應用',
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
searchFunctionPlaceholder: '搜尋函數',
|
||||
allFunctions: '全部函數',
|
||||
syntax: '語法',
|
||||
},
|
||||
|
||||
mobileFunction: {
|
||||
title: '插入函數',
|
||||
recommended: '推薦',
|
||||
recent: '最近',
|
||||
details: '詳細資料',
|
||||
insert: '插入函數',
|
||||
empty: '暫無函數',
|
||||
close: '關閉',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: '僅複製公式',
|
||||
pasteFormula: '僅貼上公式',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: '選擇一個資料範圍',
|
||||
addAnotherRange: '新增範圍',
|
||||
buttonTooltip: '選擇資料範圍',
|
||||
placeHolder: '框選範圍或輸入',
|
||||
confirm: '確認',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
106
packages/sheets-formula-ui/src/locale/zh-TW.ts
Normal file
106
packages/sheets-formula-ui/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-formula-ui': {
|
||||
shortcut: {
|
||||
'quick-sum': '快速求和',
|
||||
},
|
||||
|
||||
insert: {
|
||||
tooltip: '函數',
|
||||
common: '常用函數',
|
||||
},
|
||||
prompt: {
|
||||
helpExample: '範例',
|
||||
helpAbstract: '簡介',
|
||||
required: '必要。 ',
|
||||
optional: '可選。 ',
|
||||
},
|
||||
|
||||
error: {
|
||||
title: '錯誤',
|
||||
divByZero: '除數為零',
|
||||
name: '無效名稱',
|
||||
value: '值中的錯誤',
|
||||
num: '數值錯誤',
|
||||
na: '值不可用',
|
||||
cycle: '循環引用',
|
||||
ref: '無效的儲存格引用',
|
||||
spill: '溢出區域不是空白區域',
|
||||
calc: '計算錯誤',
|
||||
error: '錯誤',
|
||||
connect: '正在連線中',
|
||||
null: '空值錯誤',
|
||||
},
|
||||
|
||||
functionType: {
|
||||
financial: '財務',
|
||||
date: '日期與時間',
|
||||
math: '數學與三角函數',
|
||||
statistical: '統計',
|
||||
lookup: '尋找與引用',
|
||||
database: '資料庫',
|
||||
text: '文字',
|
||||
logical: '邏輯',
|
||||
information: '訊息',
|
||||
engineering: '工程',
|
||||
cube: '多維資料集',
|
||||
compatibility: '相容性',
|
||||
web: 'Web',
|
||||
array: '陣列',
|
||||
univer: 'Univer',
|
||||
user: '使用者自訂',
|
||||
definedname: '定義名稱',
|
||||
},
|
||||
|
||||
moreFunctions: {
|
||||
confirm: '應用',
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
searchFunctionPlaceholder: '搜尋函數',
|
||||
allFunctions: '全部函數',
|
||||
syntax: '語法',
|
||||
},
|
||||
|
||||
mobileFunction: {
|
||||
title: '插入函數',
|
||||
recommended: '建議',
|
||||
recent: '最近',
|
||||
details: '詳細資料',
|
||||
insert: '插入函數',
|
||||
empty: '沒有函數',
|
||||
close: '關閉',
|
||||
},
|
||||
operation: {
|
||||
copyFormulaOnly: '僅複製公式',
|
||||
pasteFormula: '僅貼上公式',
|
||||
},
|
||||
|
||||
rangeSelector: {
|
||||
title: '選擇一個資料範圍',
|
||||
addAnotherRange: '新增範圍',
|
||||
buttonTooltip: '選擇資料範圍',
|
||||
placeHolder: '框選範圍或輸入',
|
||||
confirm: '確認',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
188
packages/sheets-formula-ui/src/menu/menu.ts
Normal file
188
packages/sheets-formula-ui/src/menu/menu.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IMenuItem, IValueOption } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { UniverInstanceType } from '@univerjs/core';
|
||||
import { FunctionType, IDescriptionService } from '@univerjs/engine-formula';
|
||||
import {
|
||||
RangeProtectionPermissionEditPoint,
|
||||
RangeProtectionPermissionViewPoint,
|
||||
WorkbookCopyPermission,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetCopyPermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetSetCellValuePermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { getCurrentRangeDisable$, menuClipboardDisabledObservable } from '@univerjs/sheets-ui';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { combineLatestWith, map } from 'rxjs';
|
||||
import {
|
||||
SheetCopyFormulaOnlyCommand,
|
||||
SheetOnlyPasteFormulaCommand,
|
||||
} from '../commands/commands/formula-clipboard.command';
|
||||
import { InsertFunctionOperation } from '../commands/operations/insert-function.operation';
|
||||
import { MoreFunctionsOperation } from '../commands/operations/more-functions.operation';
|
||||
|
||||
type FunctionCategoryKey =
|
||||
| 'financial'
|
||||
| 'logical'
|
||||
| 'text'
|
||||
| 'date'
|
||||
| 'lookup'
|
||||
| 'math'
|
||||
| 'statistical'
|
||||
| 'engineering'
|
||||
| 'information'
|
||||
| 'database';
|
||||
|
||||
type EnsureLocaleKey<T extends LocaleKey> = T;
|
||||
type FunctionCategoryLocaleKey = EnsureLocaleKey<`sheets-formula-ui.functionType.${FunctionCategoryKey}`>;
|
||||
|
||||
export function InsertCommonFunctionMenuItemFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
const commonFunctions = ['SUMIF', 'SUM', 'AVERAGE', 'IF', 'COUNT', 'SIN', 'MAX'];
|
||||
let selections: IValueOption<LocaleKey>[] = commonFunctions.map((name) => ({
|
||||
label: {
|
||||
name,
|
||||
selectable: false,
|
||||
},
|
||||
value: name,
|
||||
}));
|
||||
|
||||
try {
|
||||
const descriptionService = accessor.get(IDescriptionService);
|
||||
const filtered = commonFunctions.filter((name) => Boolean(descriptionService.getFunctionInfo(name)));
|
||||
if (filtered.length > 0) {
|
||||
selections = filtered.map((name) => ({
|
||||
label: {
|
||||
name,
|
||||
selectable: false,
|
||||
},
|
||||
value: name,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// Fallback to static common list.
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${InsertFunctionOperation.id}.common`,
|
||||
commandId: InsertFunctionOperation.id,
|
||||
title: 'sheets-formula-ui.insert.common',
|
||||
tooltip: 'sheets-formula-ui.insert.tooltip',
|
||||
icon: 'FunctionIcon',
|
||||
type: MenuItemType.SELECTOR,
|
||||
selections,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
function createInsertFunctionCategoryMenuItemFactory(
|
||||
functionType: FunctionType,
|
||||
categoryKey: FunctionCategoryKey,
|
||||
icon?: string
|
||||
) {
|
||||
return function insertFunctionCategoryMenuItemFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
let selections: IValueOption<LocaleKey>[] = [];
|
||||
|
||||
try {
|
||||
const descriptionService = accessor.get(IDescriptionService);
|
||||
selections = descriptionService.getSearchListByType(functionType).map(({ name }) => ({
|
||||
label: {
|
||||
name,
|
||||
selectable: false,
|
||||
},
|
||||
value: name,
|
||||
}));
|
||||
} catch {
|
||||
selections = [];
|
||||
}
|
||||
|
||||
const localeKey = `sheets-formula-ui.functionType.${categoryKey}` as FunctionCategoryLocaleKey;
|
||||
|
||||
return {
|
||||
id: `${InsertFunctionOperation.id}.${categoryKey}`,
|
||||
commandId: InsertFunctionOperation.id,
|
||||
title: localeKey,
|
||||
tooltip: localeKey,
|
||||
icon,
|
||||
type: MenuItemType.SELECTOR,
|
||||
selections,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const InsertFinancialFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Financial, 'financial', 'FinancialFunctionIcon');
|
||||
export const InsertLogicalFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Logical, 'logical', 'LogicalFunctionIcon');
|
||||
export const InsertTextFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Text, 'text', 'TextFunctionIcon');
|
||||
export const InsertDateFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Date, 'date', 'DateFunctionIcon');
|
||||
export const InsertLookupFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Lookup, 'lookup', 'LookupFunctionIcon');
|
||||
export const InsertMathFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Math, 'math', 'MathFunctionIcon');
|
||||
export const InsertStatisticalFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Statistical, 'statistical', 'StatisticalFunctionIcon');
|
||||
export const InsertEngineeringFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Engineering, 'engineering', 'EngineeringFunctionIcon');
|
||||
export const InsertInformationFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Information, 'information', 'InformationFunctionIcon');
|
||||
export const InsertDatabaseFunctionMenuItemFactory = createInsertFunctionCategoryMenuItemFactory(FunctionType.Database, 'database', 'DatabaseFunctionIcon');
|
||||
|
||||
// All Functions entry displayed at the bottom of category dropdowns.
|
||||
export function AllFunctionsMenuItemFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: MoreFunctionsOperation.id,
|
||||
title: 'sheets-formula-ui.moreFunctions.allFunctions',
|
||||
tooltip: 'sheets-formula-ui.insert.tooltip',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: getCurrentRangeDisable$(accessor, {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission, WorksheetSetCellValuePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Right click menu - Copy Formula Only
|
||||
export function CopyFormulaOnlyMenuItemFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: SheetCopyFormulaOnlyCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-formula-ui.operation.copyFormulaOnly',
|
||||
disabled$: getCurrentRangeDisable$(accessor, {
|
||||
workbookTypes: [WorkbookCopyPermission],
|
||||
worksheetTypes: [WorksheetCopyPermission],
|
||||
rangeTypes: [RangeProtectionPermissionViewPoint],
|
||||
}),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
// Right click menu - Paste Formula
|
||||
export function PasteFormulaMenuItemFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: SheetOnlyPasteFormulaCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-formula-ui.operation.pasteFormula',
|
||||
disabled$: menuClipboardDisabledObservable(accessor).pipe(
|
||||
combineLatestWith(getCurrentRangeDisable$(accessor, {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
worksheetTypes: [WorksheetSetCellValuePermission, WorksheetEditPermission],
|
||||
})),
|
||||
map(([d1, d2]) => d1 || d2)
|
||||
),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
104
packages/sheets-formula-ui/src/menu/schema.ts
Normal file
104
packages/sheets-formula-ui/src/menu/schema.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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 { MenuSchemaType } from '@univerjs/ui';
|
||||
import { COPY_SPECIAL_MENU_ID, PASTE_SPECIAL_MENU_ID } from '@univerjs/sheets-ui';
|
||||
import { RibbonFormulasGroup } from '@univerjs/ui';
|
||||
import { SheetCopyFormulaOnlyCommand, SheetOnlyPasteFormulaCommand } from '../commands/commands/formula-clipboard.command';
|
||||
import { InsertFunctionOperation } from '../commands/operations/insert-function.operation';
|
||||
import { MoreFunctionsOperation } from '../commands/operations/more-functions.operation';
|
||||
import {
|
||||
AllFunctionsMenuItemFactory,
|
||||
CopyFormulaOnlyMenuItemFactory,
|
||||
InsertCommonFunctionMenuItemFactory,
|
||||
InsertDatabaseFunctionMenuItemFactory,
|
||||
InsertDateFunctionMenuItemFactory,
|
||||
InsertEngineeringFunctionMenuItemFactory,
|
||||
InsertFinancialFunctionMenuItemFactory,
|
||||
InsertInformationFunctionMenuItemFactory,
|
||||
InsertLogicalFunctionMenuItemFactory,
|
||||
InsertLookupFunctionMenuItemFactory,
|
||||
InsertMathFunctionMenuItemFactory,
|
||||
InsertStatisticalFunctionMenuItemFactory,
|
||||
InsertTextFunctionMenuItemFactory,
|
||||
PasteFormulaMenuItemFactory,
|
||||
} from './menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonFormulasGroup.BASIC]: {
|
||||
[`${InsertFunctionOperation.id}.common`]: {
|
||||
order: 0,
|
||||
gridLayout: { row: 1, column: 1, rowSpan: 2, showLabel: false },
|
||||
menuItemFactory: InsertCommonFunctionMenuItemFactory,
|
||||
[MoreFunctionsOperation.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: AllFunctionsMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.financial`]: {
|
||||
order: 1,
|
||||
menuItemFactory: InsertFinancialFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.logical`]: {
|
||||
order: 2,
|
||||
menuItemFactory: InsertLogicalFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.text`]: {
|
||||
order: 3,
|
||||
menuItemFactory: InsertTextFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.date`]: {
|
||||
order: 4,
|
||||
menuItemFactory: InsertDateFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.lookup`]: {
|
||||
order: 5,
|
||||
menuItemFactory: InsertLookupFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.math`]: {
|
||||
order: 6,
|
||||
menuItemFactory: InsertMathFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.statistical`]: {
|
||||
order: 7,
|
||||
menuItemFactory: InsertStatisticalFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.engineering`]: {
|
||||
order: 8,
|
||||
menuItemFactory: InsertEngineeringFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.information`]: {
|
||||
order: 9,
|
||||
menuItemFactory: InsertInformationFunctionMenuItemFactory,
|
||||
},
|
||||
[`${InsertFunctionOperation.id}.database`]: {
|
||||
order: 10,
|
||||
menuItemFactory: InsertDatabaseFunctionMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[COPY_SPECIAL_MENU_ID]: {
|
||||
[SheetCopyFormulaOnlyCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: CopyFormulaOnlyMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[PASTE_SPECIAL_MENU_ID]: {
|
||||
[SheetOnlyPasteFormulaCommand.id]: {
|
||||
order: 4,
|
||||
menuItemFactory: PasteFormulaMenuItemFactory,
|
||||
},
|
||||
},
|
||||
};
|
||||
144
packages/sheets-formula-ui/src/mobile-plugin.ts
Normal file
144
packages/sheets-formula-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IUniverSheetsFormulaUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Plugin,
|
||||
registerDependencies,
|
||||
touchDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
||||
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { BuiltInUIPart, connectInjector, IUIPartsService, UniverMobileUIPlugin } from '@univerjs/ui';
|
||||
import pkg from '../package.json';
|
||||
import { FORMULA_UI_PLUGIN_NAME } from './common/plugin-name';
|
||||
import {
|
||||
defaultPluginConfig,
|
||||
PLUGIN_CONFIG_KEY_BASE,
|
||||
} from './config/config';
|
||||
import { FormulaAlertRenderController } from './controllers/formula-alert-render.controller';
|
||||
import { FormulaClipboardController } from './controllers/formula-clipboard.controller';
|
||||
import { FormulaEditorShowController } from './controllers/formula-editor-show.controller';
|
||||
import { FormulaRenderManagerController } from './controllers/formula-render.controller';
|
||||
import { FormulaReorderController } from './controllers/formula-reorder.controller';
|
||||
import { ImageFormulaRenderController } from './controllers/image-formula-render.controller';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { FormulaUIController } from './controllers/ui.controller';
|
||||
import { FormulaPromptService, IFormulaPromptService } from './services/prompt.service';
|
||||
import { GlobalRangeSelectorService } from './services/range-selector.service';
|
||||
import { MobileRefSelectionsRenderService } from './services/render-services/mobile-ref-selections.render.service';
|
||||
import { RefSelectionsRenderService } from './services/render-services/ref-selections.render.service';
|
||||
import { MobileGlobalRangeSelector } from './views/range-selector/MobileGlobal';
|
||||
|
||||
/**
|
||||
* The configuration of the formula UI plugin.
|
||||
*/
|
||||
@DependentOn(
|
||||
UniverFormulaEnginePlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverSheetsFormulaPlugin,
|
||||
UniverSheetsMobileUIPlugin,
|
||||
UniverMobileUIPlugin
|
||||
)
|
||||
export class UniverSheetsFormulaMobileUIPlugin extends Plugin {
|
||||
static override pluginName = FORMULA_UI_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsFormulaUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@IUIPartsService private readonly _uiPartsService: IUIPartsService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(PLUGIN_CONFIG_KEY_BASE, rest, { merge: true });
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([MobileComponentsController]);
|
||||
this._injector.get(MobileComponentsController);
|
||||
registerDependencies(this._injector, [
|
||||
[IFormulaPromptService, { useClass: FormulaPromptService }],
|
||||
[GlobalRangeSelectorService],
|
||||
[FormulaUIController],
|
||||
[FormulaClipboardController],
|
||||
[FormulaEditorShowController],
|
||||
[FormulaRenderManagerController],
|
||||
[FormulaReorderController],
|
||||
[ImageFormulaRenderController],
|
||||
]);
|
||||
|
||||
this._initUIPart();
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
// render basics
|
||||
([
|
||||
[RefSelectionsRenderService, { useClass: MobileRefSelectionsRenderService }],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, dep));
|
||||
});
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
([
|
||||
[FormulaAlertRenderController],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, dep));
|
||||
});
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[FormulaUIController], // FormulaProgressBar relies on TriggerCalculationController, but it is necessary to ensure that the formula calculation is done after rendered.
|
||||
[FormulaClipboardController],
|
||||
[FormulaRenderManagerController],
|
||||
[ImageFormulaRenderController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onSteady(): void {
|
||||
this._injector.get(FormulaReorderController);
|
||||
}
|
||||
|
||||
private _initUIPart(): void {
|
||||
this.disposeWithMe(this._uiPartsService.registerComponent(
|
||||
BuiltInUIPart.GLOBAL,
|
||||
() => connectInjector(MobileGlobalRangeSelector, this._injector)
|
||||
));
|
||||
}
|
||||
}
|
||||
139
packages/sheets-formula-ui/src/plugin.ts
Normal file
139
packages/sheets-formula-ui/src/plugin.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import type { IUniverSheetsFormulaUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Plugin,
|
||||
registerDependencies,
|
||||
touchDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { BuiltInUIPart, connectInjector, IUIPartsService } from '@univerjs/ui';
|
||||
import pkg from '../package.json';
|
||||
import { FORMULA_UI_PLUGIN_NAME } from './common/plugin-name';
|
||||
import {
|
||||
defaultPluginConfig,
|
||||
PLUGIN_CONFIG_KEY_BASE,
|
||||
} from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { FormulaAlertRenderController } from './controllers/formula-alert-render.controller';
|
||||
import { FormulaClipboardController } from './controllers/formula-clipboard.controller';
|
||||
import { FormulaEditorShowController } from './controllers/formula-editor-show.controller';
|
||||
import { FormulaRenderManagerController } from './controllers/formula-render.controller';
|
||||
import { FormulaReorderController } from './controllers/formula-reorder.controller';
|
||||
import { ImageFormulaRenderController } from './controllers/image-formula-render.controller';
|
||||
import { FormulaUIController } from './controllers/ui.controller';
|
||||
import { FormulaPromptService, IFormulaPromptService } from './services/prompt.service';
|
||||
import { GlobalRangeSelectorService } from './services/range-selector.service';
|
||||
import { RefSelectionsRenderService } from './services/render-services/ref-selections.render.service';
|
||||
import { GlobalRangeSelector } from './views/range-selector/Global';
|
||||
|
||||
/**
|
||||
* The configuration of the formula UI plugin.
|
||||
*/
|
||||
@DependentOn(
|
||||
UniverFormulaEnginePlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverSheetsFormulaPlugin,
|
||||
UniverSheetsUIPlugin
|
||||
)
|
||||
export class UniverSheetsFormulaUIPlugin extends Plugin {
|
||||
static override pluginName = FORMULA_UI_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsFormulaUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@IUIPartsService private readonly _uiPartsService: IUIPartsService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(PLUGIN_CONFIG_KEY_BASE, rest, { merge: true });
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
registerDependencies(this._injector, [
|
||||
[IFormulaPromptService, { useClass: FormulaPromptService }],
|
||||
[GlobalRangeSelectorService],
|
||||
[FormulaUIController],
|
||||
[FormulaClipboardController],
|
||||
[FormulaEditorShowController],
|
||||
[FormulaRenderManagerController],
|
||||
[FormulaReorderController],
|
||||
[ImageFormulaRenderController],
|
||||
]);
|
||||
|
||||
this._initUIPart();
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
// render basics
|
||||
([
|
||||
[RefSelectionsRenderService],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, dep));
|
||||
});
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
([
|
||||
[FormulaAlertRenderController],
|
||||
] as Dependency[]).forEach((dep) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, dep));
|
||||
});
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[FormulaUIController], // FormulaProgressBar relies on TriggerCalculationController, but it is necessary to ensure that the formula calculation is done after rendered.
|
||||
[FormulaClipboardController],
|
||||
[FormulaRenderManagerController],
|
||||
[ImageFormulaRenderController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onSteady(): void {
|
||||
this._injector.get(FormulaReorderController);
|
||||
}
|
||||
|
||||
private _initUIPart(): void {
|
||||
this.disposeWithMe(this._uiPartsService.registerComponent(BuiltInUIPart.GLOBAL, () => connectInjector(GlobalRangeSelector, this._injector)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 { ISequenceNode } from '@univerjs/engine-formula';
|
||||
import { Direction, IContextService, Injector } from '@univerjs/core';
|
||||
import { sequenceNodeType } from '@univerjs/engine-formula';
|
||||
import { map, Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FORMULA_PROMPT_ACTIVATED, FormulaPromptService } from '../prompt.service';
|
||||
|
||||
function createReferenceNode(token: string, startIndex: number, endIndex: number): ISequenceNode {
|
||||
return {
|
||||
token,
|
||||
startIndex,
|
||||
endIndex,
|
||||
nodeType: sequenceNodeType.REFERENCE,
|
||||
} as ISequenceNode;
|
||||
}
|
||||
|
||||
function createService() {
|
||||
const contextChanged$ = new Subject<{ [key: string]: boolean }>();
|
||||
const contextMap = new Map<string, boolean>();
|
||||
const contextService = {
|
||||
getContextValue: vi.fn((key: string) => contextMap.get(key) ?? false),
|
||||
setContextValue: vi.fn((key: string, value: boolean) => {
|
||||
contextMap.set(key, value);
|
||||
contextChanged$.next({ [key]: value });
|
||||
}),
|
||||
subscribeContextValue$: vi.fn((key: string) => contextChanged$.pipe(map((event) => event[key] ?? false))),
|
||||
};
|
||||
|
||||
class TestContextService {
|
||||
contextChanged$ = contextChanged$.asObservable();
|
||||
getContextValue = contextService.getContextValue;
|
||||
setContextValue = contextService.setContextValue;
|
||||
subscribeContextValue$ = contextService.subscribeContextValue$;
|
||||
}
|
||||
|
||||
const injector = new Injector();
|
||||
injector.add([IContextService, { useClass: TestContextService }]);
|
||||
injector.add([FormulaPromptService]);
|
||||
return {
|
||||
contextService,
|
||||
service: injector.get(FormulaPromptService),
|
||||
};
|
||||
}
|
||||
|
||||
describe('FormulaPromptService', () => {
|
||||
it('tracks prompt visibility and forwards prompt events', () => {
|
||||
const { contextService, service } = createService();
|
||||
const searchEvents: unknown[] = [];
|
||||
const helpEvents: unknown[] = [];
|
||||
const navigateEvents: unknown[] = [];
|
||||
const acceptEvents: boolean[] = [];
|
||||
const acceptedNames: string[] = [];
|
||||
|
||||
service.search$.subscribe((event) => searchEvents.push(event));
|
||||
service.help$.subscribe((event) => helpEvents.push(event));
|
||||
service.navigate$.subscribe((event) => navigateEvents.push(event));
|
||||
service.accept$.subscribe((event) => acceptEvents.push(event));
|
||||
service.acceptFormulaName$.subscribe((event) => acceptedNames.push(event));
|
||||
|
||||
service.search({ visible: true, searchText: 'su', searchList: [] });
|
||||
service.help({ visible: true, paramIndex: 1, functionInfo: { functionName: 'SUM' } as never });
|
||||
service.navigate({ direction: Direction.DOWN });
|
||||
service.accept(true);
|
||||
service.acceptFormulaName('SUM');
|
||||
|
||||
expect(contextService.setContextValue).toHaveBeenCalledWith(FORMULA_PROMPT_ACTIVATED, true);
|
||||
expect(service.isSearching()).toBe(true);
|
||||
expect(service.isHelping()).toBe(true);
|
||||
expect(searchEvents).toEqual([{ visible: true, searchText: 'su', searchList: [] }]);
|
||||
expect(helpEvents).toEqual([{ visible: true, paramIndex: 1, functionInfo: { functionName: 'SUM' } }]);
|
||||
expect(navigateEvents).toEqual([{ direction: Direction.DOWN }]);
|
||||
expect(acceptEvents).toEqual([true]);
|
||||
expect(acceptedNames).toEqual(['SUM']);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('updates and inserts sequence nodes while keeping later reference indexes aligned', () => {
|
||||
const { service } = createService();
|
||||
|
||||
service.setSequenceNodes([
|
||||
'=',
|
||||
createReferenceNode('A1', 1, 2),
|
||||
'+',
|
||||
createReferenceNode('B2', 4, 5),
|
||||
]);
|
||||
|
||||
expect(service.getCurrentSequenceNodeIndex(1)).toBe(1);
|
||||
expect(service.getCurrentSequenceNodeByIndex(1)).toMatchObject({ token: 'A1' });
|
||||
|
||||
service.updateSequenceRef(1, 'Sheet1!A1');
|
||||
|
||||
expect(service.getSequenceNodes()).toEqual([
|
||||
'=',
|
||||
createReferenceNode('Sheet1!A1', 1, 9),
|
||||
'+',
|
||||
createReferenceNode('B2', 11, 12),
|
||||
]);
|
||||
|
||||
service.insertSequenceString(1, '(');
|
||||
service.insertSequenceRef(2, 'C3');
|
||||
|
||||
expect(service.getSequenceNodes()).toEqual([
|
||||
'=',
|
||||
'(',
|
||||
createReferenceNode('C3', 2, 3),
|
||||
createReferenceNode('Sheet1!A1', 4, 12),
|
||||
'+',
|
||||
createReferenceNode('B2', 14, 15),
|
||||
]);
|
||||
|
||||
service.enableLockedSelectionChange();
|
||||
service.enableLockedSelectionInsert();
|
||||
expect(service.isLockedSelectionChange()).toBe(true);
|
||||
expect(service.isLockedSelectionInsert()).toBe(true);
|
||||
|
||||
service.disableLockedSelectionChange();
|
||||
service.disableLockedSelectionInsert();
|
||||
service.clearSequenceNodes();
|
||||
|
||||
expect(service.isLockedSelectionChange()).toBe(false);
|
||||
expect(service.isLockedSelectionInsert()).toBe(false);
|
||||
expect(service.getSequenceNodes()).toEqual([]);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { IUnitRangeName } from '@univerjs/core';
|
||||
import { Injector } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { GlobalRangeSelectorService } from '../range-selector.service';
|
||||
|
||||
function createService(): GlobalRangeSelectorService {
|
||||
const injector = new Injector();
|
||||
injector.add([GlobalRangeSelectorService]);
|
||||
return injector.get(GlobalRangeSelectorService);
|
||||
}
|
||||
|
||||
describe('GlobalRangeSelectorService', () => {
|
||||
it('opens a range selector request and resolves with the ranges picked by the user', async () => {
|
||||
const service = createService();
|
||||
const shown: unknown[] = [];
|
||||
const selectedRanges: IUnitRangeName[] = [{
|
||||
unitId: 'unit-1',
|
||||
sheetName: 'Sheet1',
|
||||
range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
}];
|
||||
const callbackRanges: IUnitRangeName[][] = [];
|
||||
const sub = service.currentSelector$.subscribe((value) => shown.push(value));
|
||||
|
||||
const promise = service.showRangeSelectorDialog({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
callback: (ranges) => callbackRanges.push(ranges),
|
||||
});
|
||||
const selector = shown.at(-1) as { callback: (ranges: IUnitRangeName[]) => void };
|
||||
selector.callback(selectedRanges);
|
||||
|
||||
await expect(promise).resolves.toEqual(selectedRanges);
|
||||
expect(callbackRanges).toEqual([selectedRanges]);
|
||||
expect(shown[0]).toBeNull();
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
});
|
||||
@@ -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 { LocaleService } from '@univerjs/core';
|
||||
import type { IFunctionInfo } from '@univerjs/engine-formula';
|
||||
import { FunctionType } from '@univerjs/engine-formula';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateParam, getFunctionName, getFunctionTypeValues } from '../utils';
|
||||
|
||||
describe('formula service utils', () => {
|
||||
const localeService = {
|
||||
t: (key: string) => {
|
||||
const dictionary: Record<string, string> = {
|
||||
'sheets-formula-ui.functionType.math': 'Math',
|
||||
'sheets-formula-ui.functionType.logical': 'Logical',
|
||||
'sheets-formula-ui.alias.sum': '求和',
|
||||
};
|
||||
|
||||
return dictionary[key] ?? key;
|
||||
},
|
||||
} as LocaleService;
|
||||
|
||||
it('builds function type options and excludes unsupported groups for built-in formulas', () => {
|
||||
const values = getFunctionTypeValues(localeService, false);
|
||||
const labels = new Set(values.map((item) => item.label));
|
||||
const builtInValues = new Set(values.map((item) => item.value));
|
||||
|
||||
expect(labels.has('Math')).toBe(true);
|
||||
expect(labels.has('Logical')).toBe(true);
|
||||
expect(builtInValues.has(`${FunctionType.User}`)).toBe(false);
|
||||
expect(builtInValues.has(`${FunctionType.DefinedName}`)).toBe(false);
|
||||
expect(builtInValues.has(`${FunctionType.Table}`)).toBe(false);
|
||||
expect(getFunctionTypeValues(localeService, true).some((item) => item.value === `${FunctionType.User}`)).toBe(true);
|
||||
});
|
||||
|
||||
it('prefers localized aliases but falls back to the raw function name', () => {
|
||||
expect(getFunctionName({ functionName: 'SUM', aliasFunctionName: 'sheets-formula-ui.alias.sum' } as IFunctionInfo, localeService)).toBe('求和');
|
||||
expect(getFunctionName({ functionName: 'COUNTIF', aliasFunctionName: 'sheets-formula-ui.alias.unknown' } as IFunctionInfo, localeService)).toBe('COUNTIF');
|
||||
expect(getFunctionName({ functionName: 'MAX' } as IFunctionInfo, localeService)).toBe('MAX');
|
||||
});
|
||||
|
||||
it('formats required, optional, and repeated parameters for the function helper UI', () => {
|
||||
expect(generateParam({ name: 'number1', require: false, repeat: false } as never)).toBe('[number1]');
|
||||
expect(generateParam({ name: 'number1', require: true, repeat: false } as never)).toBe('number1');
|
||||
expect(generateParam({ name: 'criteria', require: false, repeat: true } as never)).toBe('[criteria,...]');
|
||||
expect(generateParam({ name: 'value', require: true, repeat: true } as never)).toBe('value,...');
|
||||
});
|
||||
});
|
||||
406
packages/sheets-formula-ui/src/services/prompt.service.ts
Normal file
406
packages/sheets-formula-ui/src/services/prompt.service.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* 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 { Direction, IDisposable } from '@univerjs/core';
|
||||
import type { IFunctionInfo, ISearchItem, ISequenceNode } from '@univerjs/engine-formula';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { createIdentifier, IContextService } from '@univerjs/core';
|
||||
import { sequenceNodeType } from '@univerjs/engine-formula';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
/** If the formula prompt is visible. */
|
||||
export const FORMULA_PROMPT_ACTIVATED = 'FORMULA_PROMPT_ACTIVATED';
|
||||
|
||||
export interface ISearchFunctionOperationParams {
|
||||
/**
|
||||
* show SearchFunction Component or not
|
||||
*/
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* function search text
|
||||
*/
|
||||
searchText: string;
|
||||
|
||||
/**
|
||||
* function list
|
||||
*/
|
||||
searchList: ISearchItem[];
|
||||
}
|
||||
|
||||
export interface IHelpFunctionOperationParams {
|
||||
/**
|
||||
* show HelpFunction Component or not
|
||||
*/
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* function param index
|
||||
*/
|
||||
paramIndex: number;
|
||||
|
||||
/**
|
||||
* function info
|
||||
*/
|
||||
functionInfo: IFunctionInfo;
|
||||
}
|
||||
|
||||
export interface INavigateParam {
|
||||
direction: Direction.UP | Direction.DOWN;
|
||||
}
|
||||
|
||||
export interface IFormulaPromptService {
|
||||
/**
|
||||
* listen search function open
|
||||
*/
|
||||
search$: Observable<ISearchFunctionOperationParams>;
|
||||
|
||||
/**
|
||||
* open search function
|
||||
*/
|
||||
search(param: ISearchFunctionOperationParams): void;
|
||||
|
||||
/**
|
||||
* listen help function open
|
||||
*/
|
||||
help$: Observable<IHelpFunctionOperationParams>;
|
||||
|
||||
/**
|
||||
* open help function
|
||||
*/
|
||||
|
||||
help(param: IHelpFunctionOperationParams): void;
|
||||
|
||||
/**
|
||||
* listen navigate shortcut, UP and DOWN
|
||||
*/
|
||||
navigate$: Observable<INavigateParam>;
|
||||
|
||||
/**
|
||||
* set navigate shortcut
|
||||
*/
|
||||
navigate(param: INavigateParam): void;
|
||||
|
||||
/**
|
||||
* listen accept shortcut, TAB/ENTER
|
||||
*/
|
||||
accept$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* set accept shortcut
|
||||
*/
|
||||
accept(param: boolean): void;
|
||||
|
||||
/**
|
||||
* accept formula name
|
||||
*/
|
||||
acceptFormulaName$: Observable<string>;
|
||||
|
||||
/**
|
||||
* set accept formula name
|
||||
*/
|
||||
acceptFormulaName(param: string): void;
|
||||
|
||||
isSearching(): boolean;
|
||||
|
||||
isHelping(): boolean;
|
||||
|
||||
dispose(): void;
|
||||
|
||||
getSequenceNodes(): Array<string | ISequenceNode>;
|
||||
|
||||
setSequenceNodes(nodes: Array<string | ISequenceNode>): void;
|
||||
|
||||
clearSequenceNodes(): void;
|
||||
|
||||
getCurrentSequenceNodeIndex(strIndex: number): number;
|
||||
|
||||
getCurrentSequenceNodeByIndex(nodeIndex: number): string | ISequenceNode;
|
||||
|
||||
getCurrentSequenceNode(strIndex: number): string | ISequenceNode;
|
||||
|
||||
updateSequenceRef(nodeIndex: number, refString: string): void;
|
||||
|
||||
insertSequenceRef(index: number, refString: string): void;
|
||||
|
||||
insertSequenceString(index: number, content: string): void;
|
||||
|
||||
enableLockedSelectionChange(): void;
|
||||
|
||||
disableLockedSelectionChange(): void;
|
||||
|
||||
isLockedSelectionChange(): boolean;
|
||||
|
||||
enableLockedSelectionInsert(): void;
|
||||
|
||||
disableLockedSelectionInsert(): void;
|
||||
|
||||
isLockedSelectionInsert(): boolean;
|
||||
}
|
||||
|
||||
export const IFormulaPromptService = createIdentifier<IFormulaPromptService>('formula-ui.prompt-service');
|
||||
|
||||
export class FormulaPromptService implements IFormulaPromptService, IDisposable {
|
||||
private readonly _search$ = new Subject<ISearchFunctionOperationParams>();
|
||||
|
||||
private readonly _help$ = new Subject<IHelpFunctionOperationParams>();
|
||||
|
||||
private readonly _navigate$ = new Subject<INavigateParam>();
|
||||
|
||||
private readonly _accept$ = new Subject<boolean>();
|
||||
|
||||
private readonly _acceptFormulaName$ = new Subject<string>();
|
||||
|
||||
readonly search$ = this._search$.asObservable();
|
||||
|
||||
readonly help$ = this._help$.asObservable();
|
||||
|
||||
readonly navigate$ = this._navigate$.asObservable();
|
||||
|
||||
readonly accept$ = this._accept$.asObservable();
|
||||
|
||||
readonly acceptFormulaName$ = this._acceptFormulaName$.asObservable();
|
||||
|
||||
private _searching: boolean = false;
|
||||
|
||||
private _helping: boolean = false;
|
||||
|
||||
private _sequenceNodes: Array<string | ISequenceNode> = [];
|
||||
|
||||
private _isLockedOnSelectionChangeRefString: boolean = false;
|
||||
|
||||
private _isLockedOnSelectionInsertRefString: boolean = false;
|
||||
|
||||
constructor(
|
||||
@IContextService private readonly _contextService: IContextService
|
||||
) {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._search$.complete();
|
||||
this._help$.complete();
|
||||
this._navigate$.complete();
|
||||
this._accept$.complete();
|
||||
this._acceptFormulaName$.complete();
|
||||
this._sequenceNodes = [];
|
||||
}
|
||||
|
||||
search(param: ISearchFunctionOperationParams) {
|
||||
this._contextService.setContextValue(FORMULA_PROMPT_ACTIVATED, param.visible);
|
||||
this._searching = param.visible;
|
||||
this._search$.next(param);
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this._searching;
|
||||
}
|
||||
|
||||
help(param: IHelpFunctionOperationParams) {
|
||||
this._helping = param.visible;
|
||||
this._help$.next(param);
|
||||
}
|
||||
|
||||
isHelping() {
|
||||
return this._helping;
|
||||
}
|
||||
|
||||
navigate(param: INavigateParam) {
|
||||
this._navigate$.next(param);
|
||||
}
|
||||
|
||||
accept(param: boolean) {
|
||||
this._accept$.next(param);
|
||||
}
|
||||
|
||||
acceptFormulaName(param: string) {
|
||||
this._acceptFormulaName$.next(param);
|
||||
}
|
||||
|
||||
getSequenceNodes() {
|
||||
return [...this._sequenceNodes];
|
||||
}
|
||||
|
||||
setSequenceNodes(nodes: Array<string | ISequenceNode>) {
|
||||
this._sequenceNodes = nodes;
|
||||
}
|
||||
|
||||
clearSequenceNodes() {
|
||||
this._sequenceNodes = [];
|
||||
}
|
||||
|
||||
getCurrentSequenceNode(strIndex: number) {
|
||||
return this._sequenceNodes[this.getCurrentSequenceNodeIndex(strIndex)];
|
||||
}
|
||||
|
||||
getCurrentSequenceNodeByIndex(nodeIndex: number) {
|
||||
return this._sequenceNodes[nodeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the text coordinates in the sequenceNodes and determine the actual insertion index.
|
||||
* @param strIndex
|
||||
*/
|
||||
getCurrentSequenceNodeIndex(strIndex: number) {
|
||||
let nodeIndex = 0;
|
||||
const firstNode = this._sequenceNodes[0];
|
||||
|
||||
for (let i = 0, len = this._sequenceNodes.length; i < len; i++) {
|
||||
const node = this._sequenceNodes[i];
|
||||
|
||||
if (typeof node === 'string') {
|
||||
nodeIndex++;
|
||||
} else {
|
||||
const { endIndex } = node;
|
||||
|
||||
nodeIndex = endIndex;
|
||||
}
|
||||
|
||||
if (strIndex <= nodeIndex) {
|
||||
/**
|
||||
* =((|A1 and =|**,fix https://github.com/dream-num/univer/issues/1387
|
||||
*/
|
||||
if (typeof firstNode === 'string' && strIndex !== 0) {
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return this._sequenceNodes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the reference text based on the changes of the selection.
|
||||
* @param nodeIndex
|
||||
* @param refString
|
||||
*/
|
||||
updateSequenceRef(nodeIndex: number, refString: string) {
|
||||
const node = this._sequenceNodes[nodeIndex];
|
||||
|
||||
if (typeof node === 'string' || node.nodeType !== sequenceNodeType.REFERENCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const difference = refString.length - node.token.length;
|
||||
|
||||
const newNode = { ...node };
|
||||
|
||||
newNode.token = refString;
|
||||
|
||||
newNode.endIndex += difference;
|
||||
|
||||
this._sequenceNodes[nodeIndex] = newNode;
|
||||
|
||||
for (let i = nodeIndex + 1, len = this._sequenceNodes.length; i < len; i++) {
|
||||
const node = this._sequenceNodes[i];
|
||||
if (typeof node === 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newNode = { ...node };
|
||||
|
||||
newNode.startIndex += difference;
|
||||
newNode.endIndex += difference;
|
||||
|
||||
this._sequenceNodes[i] = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the cursor is on the right side of a formula token,
|
||||
* you can add reference text to the formula by drawing a selection.
|
||||
* @param index
|
||||
* @param refString
|
||||
*/
|
||||
insertSequenceRef(index: number, refString: string) {
|
||||
const refStringCount = refString.length;
|
||||
|
||||
const nodeIndex = this.getCurrentSequenceNodeIndex(index);
|
||||
|
||||
this._sequenceNodes.splice(nodeIndex, 0, {
|
||||
token: refString,
|
||||
startIndex: index,
|
||||
endIndex: index + refStringCount - 1,
|
||||
nodeType: sequenceNodeType.REFERENCE,
|
||||
});
|
||||
|
||||
for (let i = nodeIndex + 1, len = this._sequenceNodes.length; i < len; i++) {
|
||||
const node = this._sequenceNodes[i];
|
||||
if (typeof node === 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newNode = { ...node };
|
||||
|
||||
newNode.startIndex += refStringCount;
|
||||
newNode.endIndex += refStringCount;
|
||||
|
||||
this._sequenceNodes[i] = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a string at the cursor position in the text corresponding to the sequenceNodes.
|
||||
* @param index
|
||||
* @param content
|
||||
*/
|
||||
insertSequenceString(index: number, content: string) {
|
||||
const nodeIndex = this.getCurrentSequenceNodeIndex(index);
|
||||
const str = content.split('');
|
||||
this._sequenceNodes.splice(nodeIndex, 0, ...str);
|
||||
|
||||
const contentCount = str.length;
|
||||
|
||||
for (let i = nodeIndex + contentCount, len = this._sequenceNodes.length; i < len; i++) {
|
||||
const node = this._sequenceNodes[i];
|
||||
if (typeof node === 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newNode = { ...node };
|
||||
|
||||
newNode.startIndex += contentCount;
|
||||
newNode.endIndex += contentCount;
|
||||
|
||||
this._sequenceNodes[i] = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
enableLockedSelectionChange() {
|
||||
this._isLockedOnSelectionChangeRefString = true;
|
||||
}
|
||||
|
||||
disableLockedSelectionChange() {
|
||||
this._isLockedOnSelectionChangeRefString = false;
|
||||
}
|
||||
|
||||
isLockedSelectionChange() {
|
||||
return this._isLockedOnSelectionChangeRefString;
|
||||
}
|
||||
|
||||
enableLockedSelectionInsert() {
|
||||
this._isLockedOnSelectionInsertRefString = true;
|
||||
}
|
||||
|
||||
disableLockedSelectionInsert() {
|
||||
this._isLockedOnSelectionInsertRefString = false;
|
||||
}
|
||||
|
||||
isLockedSelectionInsert() {
|
||||
return this._isLockedOnSelectionInsertRefString;
|
||||
}
|
||||
}
|
||||
@@ -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 { IUnitRangeName } from '@univerjs/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface IShowRangeSelectorDialogOptions {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
initialValue?: IUnitRangeName[];
|
||||
maxRangeCount?: number;
|
||||
supportAcrossSheet?: boolean;
|
||||
callback: (ranges: IUnitRangeName[]) => void;
|
||||
}
|
||||
|
||||
export class GlobalRangeSelectorService {
|
||||
private _currentSelector$ = new BehaviorSubject<IShowRangeSelectorDialogOptions | null>(null);
|
||||
|
||||
currentSelector$ = this._currentSelector$.asObservable();
|
||||
|
||||
showRangeSelectorDialog(opts: IShowRangeSelectorDialogOptions) {
|
||||
const callback = opts.callback;
|
||||
const promise = new Promise<IUnitRangeName[]>((resolve) => {
|
||||
opts.callback = (ranges) => {
|
||||
resolve(ranges);
|
||||
callback(ranges);
|
||||
};
|
||||
});
|
||||
|
||||
this._currentSelector$.next(opts);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,964 @@
|
||||
/**
|
||||
* 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 { IDisposable, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { IPointerEvent, IRenderContext } from '@univerjs/engine-render';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import {
|
||||
IContextService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
RANGE_TYPE,
|
||||
toDisposable,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
CanvasColorService,
|
||||
ICanvasColorService,
|
||||
IRenderManagerService,
|
||||
RenderManagerService,
|
||||
SHEET_VIEWPORT_KEY,
|
||||
Spreadsheet,
|
||||
Viewport,
|
||||
} from '@univerjs/engine-render';
|
||||
import {
|
||||
IRefSelectionsService,
|
||||
REF_SELECTIONS_ENABLED,
|
||||
RefSelectionsService,
|
||||
SelectionMoveType,
|
||||
SELECTIONS_ENABLED,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
MOBILE_EXPANDING_SELECTION,
|
||||
MobileSelectionControl,
|
||||
SHEET_VIEW_KEY,
|
||||
SheetSkeletonManagerService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import {
|
||||
IPlatformService,
|
||||
IShortcutService,
|
||||
IUIRuntimeScopeService,
|
||||
PlatformService,
|
||||
ShortcutService,
|
||||
UIRuntimeScopeService,
|
||||
} from '@univerjs/ui';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MobileRefSelectionsRenderService } from '../mobile-ref-selections.render.service';
|
||||
import { RefSelectionsRenderService } from '../ref-selections.render.service';
|
||||
|
||||
describe('RefSelectionsRenderService additive modifiers with real render providers', () => {
|
||||
let univer: Univer;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
// Only the unavailable canvas backend is substituted.
|
||||
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.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([SheetSkeletonService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([IRefSelectionsService, { useClass: RefSelectionsService }]);
|
||||
injector.add([IPlatformService, { useClass: PlatformService }]);
|
||||
injector.add([IUIRuntimeScopeService, { useClass: UIRuntimeScopeService }]);
|
||||
injector.add([IShortcutService, { useClass: ShortcutService }]);
|
||||
injector.get(SheetSkeletonService);
|
||||
injector.get(SheetInterceptorService);
|
||||
injector.get(IRenderManagerService).registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetSkeletonManagerService]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
univer.dispose();
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
for (const remainLast of [false, true]) {
|
||||
for (const modifier of ['none', 'ctrlKey', 'metaKey'] as const) {
|
||||
it(`keeps reference ownership with ${modifier} and remain-last ${remainLast}`, () => {
|
||||
const injector = univer.__getInjector();
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, {
|
||||
id: 'ref-modifiers',
|
||||
name: 'Reference modifiers',
|
||||
sheetOrder: ['sheet1'],
|
||||
sheets: { sheet1: { id: 'sheet1', name: 'Sheet 1', rowCount: 20, columnCount: 10, cellData: {} } },
|
||||
});
|
||||
const renderManager = injector.get(IRenderManagerService);
|
||||
const render = renderManager.createRender(workbook.getUnitId());
|
||||
render.engine.resizeBySize(800, 600);
|
||||
new Viewport(SHEET_VIEWPORT_KEY.VIEW_MAIN, render.scene, { left: 0, top: 0, right: 0, bottom: 0 });
|
||||
const spreadsheet = new Spreadsheet(SHEET_VIEW_KEY.MAIN);
|
||||
render.mainComponent = spreadsheet;
|
||||
render.components.set(SHEET_VIEW_KEY.MAIN, spreadsheet);
|
||||
render.scene.addObject(spreadsheet);
|
||||
const skeletonManager = render.with(SheetSkeletonManagerService);
|
||||
skeletonManager.setCurrent({ sheetId: 'sheet1' });
|
||||
renderManager.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [RefSelectionsRenderService]);
|
||||
const service = render.with(RefSelectionsRenderService);
|
||||
service.setRemainLastEnabled(remainLast);
|
||||
injector.get(IContextService).setContextValue(REF_SELECTIONS_ENABLED, true);
|
||||
service.enableSelectionChanging();
|
||||
const skeleton = skeletonManager.getCurrentSkeleton()!;
|
||||
|
||||
for (const index of [0, 2]) {
|
||||
const cell = skeleton.getNoMergeCellWithCoordByIndex(index, index);
|
||||
const event = {
|
||||
offsetX: (cell.startX + cell.endX) / 2,
|
||||
offsetY: (cell.startY + cell.endY) / 2,
|
||||
button: 0,
|
||||
...(index === 2 && modifier !== 'none' ? { [modifier]: true } : {}),
|
||||
} as IPointerEvent;
|
||||
spreadsheet.onPointerDown$.emitEvent(event);
|
||||
render.scene.onPointerUp$.emitEvent(event);
|
||||
}
|
||||
|
||||
const selections = injector.get(IRefSelectionsService)
|
||||
.getWorkbookSelections(workbook.getUnitId())
|
||||
.getCurrentSelections();
|
||||
const ranges = selections.map(({ range }) => ({
|
||||
startRow: range.startRow,
|
||||
startColumn: range.startColumn,
|
||||
endRow: range.endRow,
|
||||
endColumn: range.endColumn,
|
||||
}));
|
||||
if (modifier === 'none') {
|
||||
expect(ranges).toEqual([{ startRow: 2, startColumn: 2, endRow: 2, endColumn: 2 }]);
|
||||
} else {
|
||||
expect(ranges).toEqual([
|
||||
{ startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 },
|
||||
{ startRow: 2, startColumn: 2, endRow: 2, endColumn: 2 },
|
||||
]);
|
||||
}
|
||||
expect(service.getSelectionControls()).toHaveLength(ranges.length);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function createTestEvent<TEvent, TState = { stopPropagation: () => void }>() {
|
||||
const handlers = new Set<(evt: TEvent, state: TState) => void>();
|
||||
return {
|
||||
subscribeEvent(handler: (evt: TEvent, state: TState) => void): IDisposable {
|
||||
handlers.add(handler);
|
||||
const dispose = () => handlers.delete(handler);
|
||||
return { dispose, unsubscribe: dispose } as unknown as IDisposable;
|
||||
},
|
||||
emit(evt: TEvent, state: TState): void {
|
||||
for (const handler of handlers) {
|
||||
handler(evt, state);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeEngine() {
|
||||
return {
|
||||
width: 800,
|
||||
height: 600,
|
||||
beginFrame$: new Subject<void>(),
|
||||
endFrame$: new Subject<unknown>(),
|
||||
renderFrameTimeMetric$: new Subject<[string, number]>(),
|
||||
renderFrameTags$: new Subject<[string, unknown]>(),
|
||||
runRenderLoop: (cb: () => void) => cb(),
|
||||
stopRenderLoop: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeViewport(viewportKey: string, options: { canvasWidth: number; canvasHeight: number }) {
|
||||
return {
|
||||
viewportKey,
|
||||
viewportScrollX: 0,
|
||||
viewportScrollY: 0,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
isActive: true,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: options.canvasWidth,
|
||||
height: options.canvasHeight,
|
||||
marginLeft: 0,
|
||||
marginTop: 0,
|
||||
onScrollAfter$: createTestEvent<unknown>(),
|
||||
onScrollByBar$: createTestEvent<unknown>(),
|
||||
limitedScroll: () => ({ isLimitedX: false, isLimitedY: false }),
|
||||
scrollToViewportPos: ({ viewportScrollX, viewportScrollY }: { viewportScrollX: number; viewportScrollY: number }) => {
|
||||
return { viewportScrollX, viewportScrollY };
|
||||
},
|
||||
updateScrollVal: () => undefined,
|
||||
scrollByViewportDeltaVal: () => true,
|
||||
transViewportScroll2ScrollValue: (x: number, y: number) => ({ x, y }),
|
||||
transScroll2ViewportScrollValue: (x: number, y: number) => ({ x, y }),
|
||||
enable: () => undefined,
|
||||
disable: () => undefined,
|
||||
setMargin: () => undefined,
|
||||
setViewportSize: () => undefined,
|
||||
setPadding: () => undefined,
|
||||
resetPadding: () => undefined,
|
||||
resizeWhenFreezeChange: () => undefined,
|
||||
calcViewportInfo: () => ({ viewBound: null }),
|
||||
getScrollBar: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeScene(viewportMap: Map<unknown, ReturnType<typeof createFakeViewport>>, engine: ReturnType<typeof createFakeEngine>) {
|
||||
const layers = new Map<number, unknown>();
|
||||
const scene = {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
onMouseWheel$: createTestEvent<unknown>(),
|
||||
onTransformChange$: createTestEvent<unknown>(),
|
||||
onPointerMove$: createTestEvent<unknown>(),
|
||||
onPointerUp$: createTestEvent<unknown>(),
|
||||
addObjects: (objects: Array<{ parent?: unknown }>) => {
|
||||
for (const object of objects) {
|
||||
object.parent = scene;
|
||||
}
|
||||
},
|
||||
addObject: (object: { parent?: unknown }) => {
|
||||
object.parent = scene;
|
||||
},
|
||||
removeObject: () => undefined,
|
||||
addLayer: (layer: { zIndex?: number }) => {
|
||||
if (typeof layer.zIndex === 'number') {
|
||||
layers.set(layer.zIndex, layer);
|
||||
}
|
||||
},
|
||||
findLayerByZIndex: (zIndex: number) => layers.get(zIndex) ?? null,
|
||||
disableObjectsEvent: () => undefined,
|
||||
enableObjectsEvent: () => undefined,
|
||||
enableLayerCache: () => undefined,
|
||||
makeDirty: () => undefined,
|
||||
getViewport: (key: unknown) => viewportMap.get(key) ?? null,
|
||||
getMainViewport: () => viewportMap.get(SHEET_VIEWPORT_KEY.VIEW_MAIN)!,
|
||||
getViewports: () => Array.from(viewportMap.values()),
|
||||
getActiveViewportByCoord: () => viewportMap.get(SHEET_VIEWPORT_KEY.VIEW_MAIN) ?? null,
|
||||
findViewportByPosToScene: () => viewportMap.get(SHEET_VIEWPORT_KEY.VIEW_MAIN) ?? null,
|
||||
getViewportScrollXY: (viewport: { viewportScrollX: number; viewportScrollY: number }) => ({ x: viewport.viewportScrollX, y: viewport.viewportScrollY }),
|
||||
getParent: () => ({ classType: 'SCENE' }),
|
||||
getTransformer: () => ({
|
||||
clearSelectedObjects: () => undefined,
|
||||
}),
|
||||
getCoordRelativeToViewport: (vec: { x?: number; y?: number } | number[]) => ({
|
||||
x: Array.isArray(vec) ? vec[0] : vec.x ?? 0,
|
||||
y: Array.isArray(vec) ? vec[1] : vec.y ?? 0,
|
||||
}),
|
||||
getScrollXYInfoByViewport: (_coords: unknown, viewport?: { viewportScrollX: number; viewportScrollY: number } | null) => ({ x: viewport?.viewportScrollX ?? 0, y: viewport?.viewportScrollY ?? 0 }),
|
||||
getAncestorScale: () => ({ scaleX: scene.scaleX, scaleY: scene.scaleY }),
|
||||
setCursor: () => undefined,
|
||||
resetCursor: () => undefined,
|
||||
getEngine: () => engine,
|
||||
transformByState: () => undefined,
|
||||
scale: (x: number, y: number) => {
|
||||
scene.scaleX = x;
|
||||
scene.scaleY = y;
|
||||
},
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
|
||||
function createFakeSkeleton(options?: {
|
||||
getLocation?: () => [string, string];
|
||||
worksheet?: {
|
||||
getFreeze: () => { startRow: number; startColumn: number; xSplit: number; ySplit: number };
|
||||
getRowCount: () => number;
|
||||
getColumnCount: () => number;
|
||||
getSheetId: () => string;
|
||||
getCellInfoInMergeData: (row: number, col: number) => {
|
||||
startRow: number;
|
||||
startColumn: number;
|
||||
endRow: number;
|
||||
endColumn: number;
|
||||
isMerged: boolean;
|
||||
isMergedMainCell: boolean;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const rowHeight = 20;
|
||||
const colWidth = 100;
|
||||
return {
|
||||
rowHeaderWidth: 46,
|
||||
columnHeaderHeight: 20,
|
||||
rowHeaderWidthAndMarginLeft: 46,
|
||||
columnHeaderHeightAndMarginTop: 20,
|
||||
getCellWithCoordByIndex: (row: number, col: number) => ({
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
actualRow: row,
|
||||
actualColumn: col,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
startX: col * colWidth,
|
||||
startY: row * rowHeight,
|
||||
endX: (col + 1) * colWidth,
|
||||
endY: (row + 1) * rowHeight,
|
||||
mergeInfo: {
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
startX: col * colWidth,
|
||||
startY: row * rowHeight,
|
||||
endX: (col + 1) * colWidth,
|
||||
endY: (row + 1) * rowHeight,
|
||||
},
|
||||
}),
|
||||
getNoMergeCellWithCoordByIndex: (row: number, col: number) => ({
|
||||
startX: col * colWidth,
|
||||
startY: row * rowHeight,
|
||||
endX: (col + 1) * colWidth,
|
||||
endY: (row + 1) * rowHeight,
|
||||
}),
|
||||
getCellIndexByOffset: (x: number, y: number) => ({
|
||||
row: Math.max(0, Math.floor(y / rowHeight)),
|
||||
column: Math.max(0, Math.floor(x / colWidth)),
|
||||
}),
|
||||
getCellByOffset: (x: number, y: number) => {
|
||||
const row = Math.max(0, Math.floor(y / rowHeight));
|
||||
const column = Math.max(0, Math.floor(x / colWidth));
|
||||
return {
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: column,
|
||||
endColumn: column,
|
||||
actualRow: row,
|
||||
actualColumn: column,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
};
|
||||
},
|
||||
getColumnCount: () => 20,
|
||||
getRowCount: () => 20,
|
||||
expandRangeByMerge: <TRange>(range: TRange) => range,
|
||||
getLocation: options?.getLocation ?? (() => ['test', 'sheet1'] as [string, string]),
|
||||
worksheet: options?.worksheet ?? {
|
||||
getFreeze: () => ({ startRow: 0, startColumn: 0, xSplit: 0, ySplit: 0 }),
|
||||
getRowCount: () => 20,
|
||||
getColumnCount: () => 20,
|
||||
getSheetId: () => 'sheet1',
|
||||
getCellInfoInMergeData: (row: number, col: number) => ({
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endRow: row,
|
||||
endColumn: col,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {},
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Sheet2',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class TestShortcutService {
|
||||
forceEscape(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
class TestSheetSkeletonManagerService {
|
||||
private readonly _currentSkeleton$ = new BehaviorSubject<unknown>(null);
|
||||
readonly currentSkeleton$ = this._currentSkeleton$.asObservable();
|
||||
private _currentParam: unknown;
|
||||
|
||||
emitCurrentSkeleton(param: unknown): void {
|
||||
this._currentParam = param;
|
||||
this._currentSkeleton$.next(param);
|
||||
}
|
||||
|
||||
getCurrentParam(): unknown {
|
||||
return this._currentParam;
|
||||
}
|
||||
|
||||
getCurrentSkeleton() {
|
||||
return (this._currentParam as { skeleton?: unknown } | null)?.skeleton ?? null;
|
||||
}
|
||||
|
||||
getSkeletonParam() {
|
||||
return this._currentParam;
|
||||
}
|
||||
}
|
||||
|
||||
function createSelection(startRow: number, startColumn: number): ISelectionWithStyle {
|
||||
return {
|
||||
range: {
|
||||
startRow,
|
||||
endRow: startRow,
|
||||
startColumn,
|
||||
endColumn: startColumn,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
},
|
||||
primary: {
|
||||
startRow,
|
||||
endRow: startRow,
|
||||
startColumn,
|
||||
endColumn: startColumn,
|
||||
actualRow: startRow,
|
||||
actualColumn: startColumn,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
style: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createRefSelectionTestBed(mobile = false) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'ref-selection-render-test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([SheetsSelectionsService]);
|
||||
this._injector.add([IRefSelectionsService, { useClass: RefSelectionsService }]);
|
||||
this._injector.add([IShortcutService, { useClass: TestShortcutService as never }]);
|
||||
this._injector.add([SheetSkeletonManagerService, { useClass: TestSheetSkeletonManagerService as never }]);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
Tools.deepClone(WORKBOOK_DATA)
|
||||
);
|
||||
|
||||
injector.get(IUniverInstanceService).focusUnit(workbook.getUnitId());
|
||||
const localeService = injector.get(LocaleService);
|
||||
localeService.setLocale(LocaleType.EN_US);
|
||||
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
const contextService = injector.get(IContextService);
|
||||
contextService.setContextValue(SELECTIONS_ENABLED, true);
|
||||
contextService.setContextValue(REF_SELECTIONS_ENABLED, true);
|
||||
|
||||
const engine = createFakeEngine();
|
||||
const viewportMap = new Map<unknown, ReturnType<typeof createFakeViewport>>();
|
||||
const viewportKeys = [
|
||||
SHEET_VIEWPORT_KEY.VIEW_MAIN,
|
||||
SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP,
|
||||
SHEET_VIEWPORT_KEY.VIEW_MAIN_LEFT,
|
||||
SHEET_VIEWPORT_KEY.VIEW_MAIN_TOP,
|
||||
];
|
||||
for (const viewportKey of viewportKeys) {
|
||||
viewportMap.set(viewportKey, createFakeViewport(viewportKey, { canvasWidth: engine.width, canvasHeight: engine.height }));
|
||||
}
|
||||
|
||||
const scene = createFakeScene(viewportMap, engine);
|
||||
const components = new Map<unknown, unknown>();
|
||||
components.set(SHEET_VIEW_KEY.ROW, { zIndex: 2, onPointerDown$: createTestEvent<unknown>(), dispose: () => undefined });
|
||||
components.set(SHEET_VIEW_KEY.COLUMN, { zIndex: 2, onPointerDown$: createTestEvent<unknown>(), dispose: () => undefined });
|
||||
components.set(SHEET_VIEW_KEY.LEFT_TOP, { onPointerDown$: createTestEvent<unknown>(), dispose: () => undefined });
|
||||
const mainComponent = {
|
||||
zIndex: 1,
|
||||
onPointerDown$: createTestEvent<unknown>(),
|
||||
onPointerUp$: createTestEvent<unknown>(),
|
||||
makeForceDirty: () => undefined,
|
||||
};
|
||||
const context = {
|
||||
unitId: workbook.getUnitId(),
|
||||
unit: workbook,
|
||||
type: UniverInstanceType.UNIVER_SHEET,
|
||||
engine,
|
||||
scene,
|
||||
mainComponent,
|
||||
components,
|
||||
isMainScene: true,
|
||||
activated$: new BehaviorSubject(true),
|
||||
activate: () => undefined,
|
||||
deactivate: () => undefined,
|
||||
} as unknown as IRenderContext<Workbook>;
|
||||
const skeleton = createFakeSkeleton();
|
||||
const skeletonManager = injector.get(SheetSkeletonManagerService) as unknown as TestSheetSkeletonManagerService;
|
||||
const service = mobile
|
||||
? injector.createInstance(MobileRefSelectionsRenderService, context)
|
||||
: injector.createInstance(RefSelectionsRenderService, context);
|
||||
|
||||
skeletonManager.emitCurrentSkeleton({
|
||||
unitId: workbook.getUnitId(),
|
||||
sheetId: 'sheet1',
|
||||
skeleton,
|
||||
});
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
workbook,
|
||||
service,
|
||||
scene,
|
||||
mainComponent,
|
||||
rowHeader: components.get(SHEET_VIEW_KEY.ROW) as { onPointerDown$: ReturnType<typeof createTestEvent<unknown>> },
|
||||
columnHeader: components.get(SHEET_VIEW_KEY.COLUMN) as { onPointerDown$: ReturnType<typeof createTestEvent<unknown>> },
|
||||
leftTop: components.get(SHEET_VIEW_KEY.LEFT_TOP) as { onPointerDown$: ReturnType<typeof createTestEvent<unknown>> },
|
||||
skeleton,
|
||||
skeletonManager,
|
||||
contextService,
|
||||
refSelectionsService: injector.get(IRefSelectionsService),
|
||||
};
|
||||
}
|
||||
|
||||
describe('RefSelectionsRenderService', () => {
|
||||
const disposables: Array<ReturnType<typeof createRefSelectionTestBed>> = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (disposables.length > 0) {
|
||||
disposables.pop()!.univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders formula reference selections from the ref selection model', () => {
|
||||
const testBed = createRefSelectionTestBed();
|
||||
disposables.push(testBed);
|
||||
const { service, refSelectionsService, workbook } = testBed;
|
||||
|
||||
refSelectionsService.setSelections(workbook.getUnitId(), 'sheet1', [
|
||||
createSelection(1, 1),
|
||||
createSelection(2, 2),
|
||||
], SelectionMoveType.ONLY_SET);
|
||||
|
||||
expect(service.getSelectionControls()).toHaveLength(2);
|
||||
expect(service.getSelectionDataWithStyle()).toEqual([
|
||||
expect.objectContaining({
|
||||
rangeWithCoord: expect.objectContaining({
|
||||
unitId: workbook.getUnitId(),
|
||||
sheetId: 'sheet1',
|
||||
startRow: 1,
|
||||
startColumn: 1,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
rangeWithCoord: expect.objectContaining({
|
||||
unitId: workbook.getUnitId(),
|
||||
sheetId: 'sheet1',
|
||||
startRow: 2,
|
||||
startColumn: 2,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
service.clearLastSelection();
|
||||
expect(service.getSelectionControls()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('syncs user-driven formula selections back to the ref selection model', () => {
|
||||
const testBed = createRefSelectionTestBed();
|
||||
disposables.push(testBed);
|
||||
const { service, refSelectionsService, workbook } = testBed;
|
||||
|
||||
service.resetSelectionsByModelData([createSelection(4, 3)]);
|
||||
service.refreshSelectionMoveStart();
|
||||
service.refreshSelectionMoveEnd();
|
||||
|
||||
const workbookSelections = refSelectionsService.getWorkbookSelections(workbook.getUnitId());
|
||||
expect(workbookSelections.getCurrentSelections()).toEqual([
|
||||
expect.objectContaining({
|
||||
range: expect.objectContaining({
|
||||
startRow: 4,
|
||||
startColumn: 3,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('clears old controls when the active sheet skeleton changes', () => {
|
||||
const testBed = createRefSelectionTestBed();
|
||||
disposables.push(testBed);
|
||||
const { service, skeletonManager, workbook } = testBed;
|
||||
|
||||
service.resetSelectionsByModelData([createSelection(1, 1)]);
|
||||
expect(service.getSelectionControls()).toHaveLength(1);
|
||||
|
||||
const sheet2Skeleton = createFakeSkeleton({
|
||||
getLocation: () => [workbook.getUnitId(), 'sheet2'],
|
||||
worksheet: {
|
||||
getFreeze: () => ({ startRow: 0, startColumn: 0, xSplit: 0, ySplit: 0 }),
|
||||
getRowCount: () => 20,
|
||||
getColumnCount: () => 20,
|
||||
getSheetId: () => 'sheet2',
|
||||
getCellInfoInMergeData: (row: number, col: number) => ({
|
||||
startRow: row,
|
||||
startColumn: col,
|
||||
endRow: row,
|
||||
endColumn: col,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
skeletonManager.emitCurrentSkeleton({
|
||||
unitId: workbook.getUnitId(),
|
||||
sheetId: 'sheet2',
|
||||
skeleton: sheet2Skeleton,
|
||||
});
|
||||
|
||||
expect(service.getLocation()).toEqual([workbook.getUnitId(), 'sheet2']);
|
||||
expect(service.getSelectionControls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('enables and disables formula ref-selection interaction mode idempotently', () => {
|
||||
const testBed = createRefSelectionTestBed();
|
||||
disposables.push(testBed);
|
||||
const { service } = testBed;
|
||||
|
||||
service.setRemainLastEnabled(false);
|
||||
service.setSkipLastEnabled(true);
|
||||
const disposable = service.enableSelectionChanging();
|
||||
service.disableSelectionChanging();
|
||||
disposable.dispose();
|
||||
|
||||
expect(service.getSelectionControls()).toEqual([]);
|
||||
});
|
||||
|
||||
it('updates formula reference selections from spreadsheet pointer selection', () => {
|
||||
const testBed = createRefSelectionTestBed();
|
||||
disposables.push(testBed);
|
||||
const { service, mainComponent, scene, refSelectionsService, workbook } = testBed;
|
||||
const stoppedEvents: string[] = [];
|
||||
|
||||
const selectionChanging = service.enableSelectionChanging();
|
||||
mainComponent.onPointerDown$.emit({
|
||||
offsetX: 150,
|
||||
offsetY: 45,
|
||||
button: 0,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
}, {
|
||||
stopPropagation: () => stoppedEvents.push('spreadsheet'),
|
||||
});
|
||||
|
||||
expect(stoppedEvents).toEqual(['spreadsheet']);
|
||||
expect(service.getSelectionControls()).toHaveLength(1);
|
||||
expect(service.getSelectionDataWithStyle()[0].rangeWithCoord).toEqual(expect.objectContaining({
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
rangeType: RANGE_TYPE.NORMAL,
|
||||
}));
|
||||
|
||||
scene.onPointerUp$.emit({}, { stopPropagation: () => undefined });
|
||||
const workbookSelections = refSelectionsService.getWorkbookSelections(workbook.getUnitId());
|
||||
expect(workbookSelections.getCurrentSelections()).toEqual([
|
||||
expect.objectContaining({
|
||||
range: expect.objectContaining({
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
selectionChanging.dispose();
|
||||
});
|
||||
|
||||
it('updates formula reference selections from row, column and sheet header gestures', () => {
|
||||
const testBed = createRefSelectionTestBed();
|
||||
disposables.push(testBed);
|
||||
const { service, rowHeader, columnHeader, leftTop, scene } = testBed;
|
||||
const stoppedEvents: string[] = [];
|
||||
const selectionChanging = service.enableSelectionChanging();
|
||||
|
||||
rowHeader.onPointerDown$.emit({
|
||||
offsetX: 10,
|
||||
offsetY: 65,
|
||||
button: 0,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
}, {
|
||||
stopPropagation: () => stoppedEvents.push('row'),
|
||||
});
|
||||
expect(service.getSelectionDataWithStyle()[0].rangeWithCoord).toEqual(expect.objectContaining({
|
||||
startRow: 3,
|
||||
endRow: 3,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
rangeType: RANGE_TYPE.ROW,
|
||||
}));
|
||||
scene.onPointerUp$.emit({}, { stopPropagation: () => undefined });
|
||||
service.clearLastSelection();
|
||||
|
||||
columnHeader.onPointerDown$.emit({
|
||||
offsetX: 250,
|
||||
offsetY: 10,
|
||||
button: 0,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
}, {
|
||||
stopPropagation: () => stoppedEvents.push('column'),
|
||||
});
|
||||
expect(service.getSelectionDataWithStyle()[0].rangeWithCoord).toEqual(expect.objectContaining({
|
||||
startRow: 0,
|
||||
endRow: 19,
|
||||
startColumn: 2,
|
||||
endColumn: 2,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
}));
|
||||
scene.onPointerUp$.emit({}, { stopPropagation: () => undefined });
|
||||
|
||||
leftTop.onPointerDown$.emit({
|
||||
offsetX: 5,
|
||||
offsetY: 5,
|
||||
button: 0,
|
||||
}, {
|
||||
stopPropagation: () => stoppedEvents.push('all'),
|
||||
});
|
||||
expect(service.getSelectionDataWithStyle()[0].rangeWithCoord).toEqual(expect.objectContaining({
|
||||
startRow: 0,
|
||||
endRow: 19,
|
||||
startColumn: 0,
|
||||
endColumn: 19,
|
||||
}));
|
||||
scene.onPointerUp$.emit({}, { stopPropagation: () => undefined });
|
||||
|
||||
expect(stoppedEvents).toEqual(['row', 'column', 'all']);
|
||||
selectionChanging.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileRefSelectionsRenderService', () => {
|
||||
const disposables: Array<ReturnType<typeof createRefSelectionTestBed>> = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (disposables.length > 0) {
|
||||
disposables.pop()!.univer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders only the two mobile corner handles and disables dragging the selection border', () => {
|
||||
const testBed = createRefSelectionTestBed(true);
|
||||
disposables.push(testBed);
|
||||
const { service } = testBed;
|
||||
const selection = createSelection(1, 1);
|
||||
selection.style = {
|
||||
stroke: '#00aa00',
|
||||
fill: 'rgba(0, 170, 0, 0.05)',
|
||||
widgets: { tl: true, tc: true, tr: true, ml: true, mr: true, bl: true, bc: true, br: true },
|
||||
widgetSize: 6,
|
||||
};
|
||||
|
||||
service.resetSelectionsByModelData([selection]);
|
||||
const control = service.getActiveSelectionControl<MobileSelectionControl>()!;
|
||||
|
||||
expect(control).toBeInstanceOf(MobileSelectionControl);
|
||||
expect(control.currentStyle.stroke).toBe(selection.style.stroke);
|
||||
expect(control.currentStyle.fill).toBe(selection.style.fill);
|
||||
expect(control.currentStyle.widgets).toEqual({});
|
||||
expect(control.currentStyle.expandCornerSize).toBe(48);
|
||||
expect(control.fillControl.visible).toBe(false);
|
||||
expect(control.expandControlTopLeft?.visible).toBe(true);
|
||||
expect(control.expandControlBottomRight?.visible).toBe(true);
|
||||
expect(control.expandControlTopLeft?.width).toBe(48);
|
||||
expect(control.expandControlTopLeft?.visualWidth).toBe(12);
|
||||
expect(control.expandControlTopLeft?.fill).toBe(selection.style.stroke);
|
||||
expect(control.expandControlBottomRight?.width).toBe(48);
|
||||
expect(control.expandControlBottomRight?.visualWidth).toBe(12);
|
||||
expect(control.expandControlBottomRight?.fill).toBe(selection.style.stroke);
|
||||
expect(control.topLeftWidget.visible).toBe(false);
|
||||
expect(control.bottomRightWidget.visible).toBe(false);
|
||||
expect(control.topCenterWidget.visible).toBe(false);
|
||||
expect(control.topRightWidget.visible).toBe(false);
|
||||
expect(control.middleLeftWidget.visible).toBe(false);
|
||||
expect(control.middleRightWidget.visible).toBe(false);
|
||||
expect(control.bottomLeftWidget.visible).toBe(false);
|
||||
expect(control.bottomCenterWidget.visible).toBe(false);
|
||||
expect(control.leftControl.evented).toBe(false);
|
||||
expect(control.rightControl.evented).toBe(false);
|
||||
expect(control.topControl.evented).toBe(false);
|
||||
expect(control.bottomControl.evented).toBe(false);
|
||||
});
|
||||
|
||||
it('expands a formula reference selection only from a mobile corner handle', () => {
|
||||
const testBed = createRefSelectionTestBed(true);
|
||||
disposables.push(testBed);
|
||||
const { service, scene, contextService } = testBed;
|
||||
|
||||
service.resetSelectionsByModelData([createSelection(1, 1)]);
|
||||
const control = service.getActiveSelectionControl<MobileSelectionControl>()!;
|
||||
|
||||
control.expandControlBottomRight!.onPointerDown$.emitEvent({ offsetX: 150, offsetY: 30 } as never);
|
||||
expect(contextService.getContextValue(MOBILE_EXPANDING_SELECTION)).toBe(true);
|
||||
|
||||
scene.onPointerMove$.emit({ offsetX: 350, offsetY: 90 }, { stopPropagation: () => undefined });
|
||||
expect(service.getActiveRange()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 4,
|
||||
startColumn: 1,
|
||||
endColumn: 3,
|
||||
});
|
||||
|
||||
scene.onPointerUp$.emit({ offsetX: 350, offsetY: 90 }, { stopPropagation: () => undefined });
|
||||
expect(contextService.getContextValue(MOBILE_EXPANDING_SELECTION)).toBe(false);
|
||||
});
|
||||
|
||||
it('expands a formula reference selection from the top-left mobile corner handle', () => {
|
||||
const testBed = createRefSelectionTestBed(true);
|
||||
disposables.push(testBed);
|
||||
const { service, scene } = testBed;
|
||||
const selection = createSelection(2, 1);
|
||||
selection.range.endRow = 4;
|
||||
selection.range.endColumn = 3;
|
||||
|
||||
service.resetSelectionsByModelData([selection]);
|
||||
const control = service.getActiveSelectionControl<MobileSelectionControl>()!;
|
||||
|
||||
control.expandControlTopLeft!.onPointerDown$.emitEvent({ offsetX: 150, offsetY: 50 } as never);
|
||||
scene.onPointerMove$.emit({ offsetX: 50, offsetY: 10 }, { stopPropagation: () => undefined });
|
||||
|
||||
expect(service.getActiveRange()).toEqual({
|
||||
startRow: 0,
|
||||
endRow: 4,
|
||||
startColumn: 0,
|
||||
endColumn: 3,
|
||||
});
|
||||
|
||||
scene.onPointerUp$.emit({ offsetX: 50, offsetY: 10 }, { stopPropagation: () => undefined });
|
||||
});
|
||||
|
||||
it('expands the formula reference whose mobile corner handle is dragged', () => {
|
||||
const testBed = createRefSelectionTestBed(true);
|
||||
disposables.push(testBed);
|
||||
const { service, scene } = testBed;
|
||||
|
||||
service.resetSelectionsByModelData([createSelection(1, 1), createSelection(5, 5)]);
|
||||
const [firstControl] = service.getSelectionControls() as MobileSelectionControl[];
|
||||
|
||||
firstControl.expandControlBottomRight!.onPointerDown$.emitEvent({ offsetX: 150, offsetY: 30 } as never);
|
||||
scene.onPointerMove$.emit({ offsetX: 350, offsetY: 90 }, { stopPropagation: () => undefined });
|
||||
|
||||
expect(service.getSelectionDataWithStyle().map(({ rangeWithCoord }) => ({
|
||||
startRow: rangeWithCoord.startRow,
|
||||
endRow: rangeWithCoord.endRow,
|
||||
startColumn: rangeWithCoord.startColumn,
|
||||
endColumn: rangeWithCoord.endColumn,
|
||||
}))).toEqual([
|
||||
{ startRow: 1, endRow: 4, startColumn: 1, endColumn: 3 },
|
||||
{ startRow: 5, endRow: 5, startColumn: 5, endColumn: 5 },
|
||||
]);
|
||||
|
||||
scene.onPointerUp$.emit({ offsetX: 350, offsetY: 90 }, { stopPropagation: () => undefined });
|
||||
});
|
||||
|
||||
it('keeps the formula reference selection unchanged when dragging the spreadsheet', () => {
|
||||
const testBed = createRefSelectionTestBed(true);
|
||||
disposables.push(testBed);
|
||||
const { service, mainComponent, scene } = testBed;
|
||||
const state = { stopPropagation: () => undefined };
|
||||
|
||||
service.resetSelectionsByModelData([createSelection(1, 1)]);
|
||||
const selectionChanging = service.enableSelectionChanging();
|
||||
mainComponent.onPointerDown$.emit({
|
||||
offsetX: 150,
|
||||
offsetY: 30,
|
||||
button: 0,
|
||||
}, state);
|
||||
scene.onPointerMove$.emit({ offsetX: 350, offsetY: 90 }, state);
|
||||
mainComponent.onPointerUp$.emit({
|
||||
offsetX: 350,
|
||||
offsetY: 90,
|
||||
button: 0,
|
||||
}, state);
|
||||
|
||||
expect(service.getActiveRange()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
});
|
||||
|
||||
selectionChanging.dispose();
|
||||
});
|
||||
|
||||
it('updates the formula reference selection when tapping the spreadsheet', () => {
|
||||
const testBed = createRefSelectionTestBed(true);
|
||||
disposables.push(testBed);
|
||||
const { service, mainComponent } = testBed;
|
||||
const state = { stopPropagation: () => undefined };
|
||||
|
||||
service.resetSelectionsByModelData([createSelection(1, 1)]);
|
||||
const selectionChanging = service.enableSelectionChanging();
|
||||
mainComponent.onPointerDown$.emit({
|
||||
offsetX: 350,
|
||||
offsetY: 90,
|
||||
button: 0,
|
||||
}, state);
|
||||
mainComponent.onPointerUp$.emit({
|
||||
offsetX: 350,
|
||||
offsetY: 90,
|
||||
button: 0,
|
||||
}, state);
|
||||
|
||||
expect(service.getActiveRange()).toEqual({
|
||||
startRow: 4,
|
||||
endRow: 4,
|
||||
startColumn: 3,
|
||||
endColumn: 3,
|
||||
});
|
||||
|
||||
selectionChanging.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* 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 { ICellWithCoord, IDisposable, Nullable, Workbook } from '@univerjs/core';
|
||||
import type {
|
||||
IMouseEvent,
|
||||
IPointerEvent,
|
||||
IRenderContext,
|
||||
Scene,
|
||||
Spreadsheet,
|
||||
SpreadsheetColumnHeader,
|
||||
SpreadsheetHeader,
|
||||
SpreadsheetSkeleton,
|
||||
Viewport,
|
||||
} from '@univerjs/engine-render';
|
||||
import type { ISelectionStyle, ISelectionWithStyle, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { DisposableCollection, IContextService, Inject, Injector, RANGE_TYPE, ThemeService } from '@univerjs/core';
|
||||
import { ScrollTimer, ScrollTimerType, SHEET_VIEWPORT_KEY, Vector2 } from '@univerjs/engine-render';
|
||||
import { attachSelectionWithCoord, IRefSelectionsService } from '@univerjs/sheets';
|
||||
import {
|
||||
checkInHeaderRanges,
|
||||
getAllSelection,
|
||||
getCoordByOffset,
|
||||
MOBILE_EXPANDING_SELECTION,
|
||||
MOBILE_PINCH_ZOOMING,
|
||||
MobileSelectionControl,
|
||||
SheetSkeletonManagerService,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import { IShortcutService } from '@univerjs/ui';
|
||||
import { RefSelectionsRenderService } from './ref-selections.render.service';
|
||||
|
||||
enum ExpandingControl {
|
||||
BOTTOM_RIGHT = 'bottom-right',
|
||||
TOP_LEFT = 'top-left',
|
||||
LEFT = 'left',
|
||||
RIGHT = 'right',
|
||||
TOP = 'top',
|
||||
BOTTOM = 'bottom',
|
||||
}
|
||||
|
||||
class MobileRefSelectionControl extends MobileSelectionControl {
|
||||
protected override _updateLayoutOfSelectionControl(style: ISelectionStyle): void {
|
||||
super._updateLayoutOfSelectionControl(style);
|
||||
|
||||
const expandCornerSize = this.currentStyle.expandCornerSize ?? 0;
|
||||
const visibleSize = expandCornerSize / 4;
|
||||
const props = {
|
||||
radius: expandCornerSize / 2,
|
||||
visualWidth: visibleSize,
|
||||
visualHeight: visibleSize,
|
||||
fill: this.currentStyle.stroke,
|
||||
stroke: this.currentStyle.widgetStroke ?? this.currentStyle.autofillStroke,
|
||||
strokeWidth: this.currentStyle.widgetStrokeWidth ?? this.currentStyle.autofillStrokeWidth,
|
||||
};
|
||||
|
||||
this.expandControlTopLeft?.resize(expandCornerSize, expandCornerSize);
|
||||
this.expandControlTopLeft?.setProps(props);
|
||||
this.expandControlBottomRight?.resize(expandCornerSize, expandCornerSize);
|
||||
this.expandControlBottomRight?.setProps(props);
|
||||
this.fillControl.evented = false;
|
||||
this.fillControl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
export class MobileRefSelectionsRenderService extends RefSelectionsRenderService {
|
||||
private _expandingControlMode = ExpandingControl.BOTTOM_RIGHT;
|
||||
|
||||
constructor(
|
||||
context: IRenderContext<Workbook>,
|
||||
@Inject(Injector) injector: Injector,
|
||||
@Inject(ThemeService) themeService: ThemeService,
|
||||
@IShortcutService shortcutService: IShortcutService,
|
||||
@Inject(SheetSkeletonManagerService) sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@IContextService contextService: IContextService,
|
||||
@IRefSelectionsService refSelectionsService: SheetsSelectionsService
|
||||
) {
|
||||
super(
|
||||
context,
|
||||
injector,
|
||||
themeService,
|
||||
shortcutService,
|
||||
sheetSkeletonManagerService,
|
||||
contextService,
|
||||
refSelectionsService
|
||||
);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._contextService.setContextValue(MOBILE_EXPANDING_SELECTION, false);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
protected override _initCanvasEventListeners(): IDisposable {
|
||||
const sheetObject = this._getSheetObject();
|
||||
const { spreadsheetRowHeader, spreadsheetColumnHeader, spreadsheet, spreadsheetLeftTopPlaceholder } = sheetObject;
|
||||
const { scene } = sheetObject;
|
||||
const listenerDisposables = new DisposableCollection();
|
||||
|
||||
this._initSpreadsheetEvents(listenerDisposables, spreadsheet);
|
||||
this._initHeaderEvent(listenerDisposables, spreadsheetRowHeader, spreadsheet, scene, RANGE_TYPE.ROW);
|
||||
this._initHeaderEvent(listenerDisposables, spreadsheetColumnHeader, spreadsheet, scene, RANGE_TYPE.COLUMN);
|
||||
|
||||
listenerDisposables.add(spreadsheetLeftTopPlaceholder.onPointerDown$.subscribeEvent((_evt, state) => {
|
||||
if (!this.inRefSelectionMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._reset();
|
||||
this._addSelectionControlByModelData(getAllSelection(skeleton));
|
||||
this._selectionMoveStart$.next(this.getSelectionDataWithStyle());
|
||||
this._selectionMoveEnd$.next(this.getSelectionDataWithStyle());
|
||||
state.stopPropagation();
|
||||
}));
|
||||
|
||||
return listenerDisposables;
|
||||
}
|
||||
|
||||
private _initSpreadsheetEvents(listenerDisposables: DisposableCollection, spreadsheet: Spreadsheet): void {
|
||||
const pointerDownPosition = { x: 0, y: 0 };
|
||||
|
||||
listenerDisposables.add(spreadsheet.onPointerDown$.subscribeEvent((evt, state) => {
|
||||
if (!this.inRefSelectionMode() || this._contextService.getContextValue(MOBILE_PINCH_ZOOMING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pointerDownPosition.x = evt.offsetX;
|
||||
pointerDownPosition.y = evt.offsetY;
|
||||
state.stopPropagation();
|
||||
}));
|
||||
|
||||
listenerDisposables.add(spreadsheet.onPointerUp$.subscribeEvent((evt, state) => {
|
||||
if (!this.inRefSelectionMode() || this._contextService.getContextValue(MOBILE_PINCH_ZOOMING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tapMovementThreshold = 10;
|
||||
if (
|
||||
Math.abs(evt.offsetX - pointerDownPosition.x) > tapMovementThreshold ||
|
||||
Math.abs(evt.offsetY - pointerDownPosition.y) > tapMovementThreshold
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._onPointerDown(evt, spreadsheet.zIndex + 1, RANGE_TYPE.NORMAL, this._getActiveViewport(evt));
|
||||
this.endSelection();
|
||||
state.stopPropagation();
|
||||
}));
|
||||
}
|
||||
|
||||
private _initHeaderEvent(
|
||||
listenerDisposables: DisposableCollection,
|
||||
header: SpreadsheetHeader | SpreadsheetColumnHeader,
|
||||
spreadsheet: Spreadsheet,
|
||||
scene: Scene,
|
||||
rangeType: RANGE_TYPE.ROW | RANGE_TYPE.COLUMN
|
||||
): void {
|
||||
listenerDisposables.add(header.onPointerDown$.subscribeEvent((evt, state) => {
|
||||
if (!this.inRefSelectionMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinate = getCoordByOffset(evt.offsetX, evt.offsetY, scene, skeleton);
|
||||
const index = rangeType === RANGE_TYPE.ROW ? coordinate.row : coordinate.column;
|
||||
const matchSelectionData = checkInHeaderRanges(
|
||||
this._workbookSelections.getCurrentSelections(),
|
||||
index,
|
||||
rangeType
|
||||
);
|
||||
if (matchSelectionData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTimerType = rangeType === RANGE_TYPE.ROW ? ScrollTimerType.Y : ScrollTimerType.X;
|
||||
this._onPointerDown(
|
||||
evt,
|
||||
(spreadsheet.zIndex || 1) + 1,
|
||||
rangeType,
|
||||
this._getActiveViewport(evt),
|
||||
scrollTimerType
|
||||
);
|
||||
this.endSelection();
|
||||
state.stopPropagation();
|
||||
}));
|
||||
}
|
||||
|
||||
override resetSelectionsByModelData(selectionsWithStyleList: readonly ISelectionWithStyle[]): void {
|
||||
super.resetSelectionsByModelData(
|
||||
selectionsWithStyleList.map((selection) => this._withMobileSelectionStyle(selection))
|
||||
);
|
||||
}
|
||||
|
||||
override newSelectionControl(
|
||||
scene: Scene,
|
||||
skeleton: SpreadsheetSkeleton,
|
||||
selection: ISelectionWithStyle
|
||||
): MobileSelectionControl {
|
||||
const {
|
||||
rowHeaderWidth,
|
||||
rowHeaderWidthAndMarginLeft,
|
||||
columnHeaderHeight,
|
||||
columnHeaderHeightAndMarginTop,
|
||||
} = skeleton;
|
||||
const rangeType = selection.range.rangeType ?? RANGE_TYPE.NORMAL;
|
||||
const control = new MobileRefSelectionControl(scene, this._selectionControls.length, this._themeService, {
|
||||
highlightHeader: this._highlightHeader,
|
||||
rowHeaderWidth,
|
||||
columnHeaderHeight,
|
||||
rowHeaderOffsetX: Math.max(0, rowHeaderWidthAndMarginLeft - rowHeaderWidth),
|
||||
columnHeaderOffsetY: Math.max(0, columnHeaderHeightAndMarginTop - columnHeaderHeight),
|
||||
rangeType,
|
||||
});
|
||||
const selectionWithMobileStyle = this._withMobileSelectionStyle(selection);
|
||||
const selectionWithCoord = attachSelectionWithCoord(selectionWithMobileStyle, skeleton);
|
||||
control.updateRangeBySelectionWithCoord(selectionWithCoord, skeleton);
|
||||
control.setEvent(false);
|
||||
[
|
||||
control.topLeftWidget,
|
||||
control.topCenterWidget,
|
||||
control.topRightWidget,
|
||||
control.middleLeftWidget,
|
||||
control.middleRightWidget,
|
||||
control.bottomLeftWidget,
|
||||
control.bottomCenterWidget,
|
||||
control.bottomRightWidget,
|
||||
].forEach((widget) => {
|
||||
widget.evented = false;
|
||||
});
|
||||
this._selectionControls.push(control);
|
||||
|
||||
const expandingModes = this._getExpandingModes(rangeType);
|
||||
control.expandControlTopLeft?.onPointerDown$.subscribeEvent((evt: IPointerEvent | IMouseEvent) => {
|
||||
this._startExpandingSelection(evt, control, rangeType, expandingModes.topLeft);
|
||||
});
|
||||
control.expandControlBottomRight?.onPointerDown$.subscribeEvent((evt: IPointerEvent | IMouseEvent) => {
|
||||
this._startExpandingSelection(evt, control, rangeType, expandingModes.bottomRight);
|
||||
});
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
private _withMobileSelectionStyle(selection: ISelectionWithStyle): ISelectionWithStyle {
|
||||
return {
|
||||
...selection,
|
||||
style: {
|
||||
...selection.style,
|
||||
widgets: {},
|
||||
expandCornerSize: 48,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _getExpandingModes(rangeType: RANGE_TYPE): { topLeft: ExpandingControl; bottomRight: ExpandingControl } {
|
||||
if (rangeType === RANGE_TYPE.ROW) {
|
||||
return { topLeft: ExpandingControl.TOP, bottomRight: ExpandingControl.BOTTOM };
|
||||
}
|
||||
if (rangeType === RANGE_TYPE.COLUMN) {
|
||||
return { topLeft: ExpandingControl.LEFT, bottomRight: ExpandingControl.RIGHT };
|
||||
}
|
||||
return { topLeft: ExpandingControl.TOP_LEFT, bottomRight: ExpandingControl.BOTTOM_RIGHT };
|
||||
}
|
||||
|
||||
private _startExpandingSelection(
|
||||
evt: IPointerEvent | IMouseEvent,
|
||||
activeSelectionControl: MobileSelectionControl,
|
||||
rangeType: RANGE_TYPE,
|
||||
expandingControlMode: ExpandingControl
|
||||
): void {
|
||||
const scene = this._scene;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setActiveSelectionIndex(this._selectionControls.indexOf(activeSelectionControl));
|
||||
this._expandingControlMode = expandingControlMode;
|
||||
this._contextService.setContextValue(MOBILE_EXPANDING_SELECTION, true);
|
||||
const anchorCell = this._changeCurrentCellWhenControlPointerDown(activeSelectionControl);
|
||||
this._startRangeWhenPointerDown = {
|
||||
...anchorCell.mergeInfo,
|
||||
startRow: anchorCell.mergeInfo?.startRow ?? anchorCell.actualRow,
|
||||
endRow: anchorCell.mergeInfo?.endRow ?? anchorCell.actualRow,
|
||||
startColumn: anchorCell.mergeInfo?.startColumn ?? anchorCell.actualColumn,
|
||||
endColumn: anchorCell.mergeInfo?.endColumn ?? anchorCell.actualColumn,
|
||||
};
|
||||
this._selectionMoveStart$.next(this.getSelectionDataWithStyle());
|
||||
this._clearUpdatingListeners();
|
||||
this._addEndingListeners();
|
||||
scene.getTransformer()?.clearSelectedObjects();
|
||||
|
||||
const relativeCoords = scene.getCoordRelativeToViewport(Vector2.FromArray([evt.offsetX, evt.offsetY]));
|
||||
this._startViewportPosX = relativeCoords.x;
|
||||
this._startViewportPosY = relativeCoords.y;
|
||||
const viewportMain = scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
this._setupPointerMoveListener(
|
||||
viewportMain,
|
||||
activeSelectionControl,
|
||||
rangeType,
|
||||
ScrollTimerType.ALL,
|
||||
relativeCoords.x,
|
||||
relativeCoords.y
|
||||
);
|
||||
|
||||
this._escapeShortcutDisposable = this._shortcutService.forceEscape();
|
||||
this._scenePointerUpSub = scene.onPointerUp$.subscribeEvent(() => {
|
||||
this.endSelection();
|
||||
this._contextService.setContextValue(MOBILE_EXPANDING_SELECTION, false);
|
||||
this._expandingControlMode = ExpandingControl.BOTTOM_RIGHT;
|
||||
});
|
||||
}
|
||||
|
||||
private _changeCurrentCellWhenControlPointerDown(activeSelectionControl: MobileSelectionControl): ICellWithCoord {
|
||||
const { startRow, startColumn, endRow, endColumn } = activeSelectionControl.model;
|
||||
let currentCell: ICellWithCoord;
|
||||
|
||||
if (this._expandingControlMode === ExpandingControl.TOP_LEFT) {
|
||||
currentCell = this._skeleton.getCellWithCoordByIndex(endRow, endColumn);
|
||||
} else if (this._expandingControlMode === ExpandingControl.LEFT) {
|
||||
currentCell = this._skeleton.getCellWithCoordByIndex(startRow, endColumn);
|
||||
} else if (this._expandingControlMode === ExpandingControl.TOP) {
|
||||
currentCell = this._skeleton.getCellWithCoordByIndex(endRow, startColumn);
|
||||
} else {
|
||||
currentCell = this._skeleton.getCellWithCoordByIndex(startRow, startColumn);
|
||||
}
|
||||
|
||||
if (
|
||||
this._expandingControlMode === ExpandingControl.LEFT ||
|
||||
this._expandingControlMode === ExpandingControl.RIGHT ||
|
||||
this._expandingControlMode === ExpandingControl.TOP ||
|
||||
this._expandingControlMode === ExpandingControl.BOTTOM
|
||||
) {
|
||||
currentCell.isMerged = false;
|
||||
currentCell.isMergedMainCell = false;
|
||||
}
|
||||
|
||||
activeSelectionControl.updateCurrCell(currentCell);
|
||||
return currentCell;
|
||||
}
|
||||
|
||||
protected override _setupPointerMoveListener(
|
||||
viewportMain: Nullable<Viewport>,
|
||||
activeSelectionControl: MobileSelectionControl,
|
||||
rangeType: RANGE_TYPE,
|
||||
scrollTimerType: ScrollTimerType = ScrollTimerType.ALL,
|
||||
_moveStartPosX: number,
|
||||
_moveStartPosY: number
|
||||
): void {
|
||||
this._scrollTimer = ScrollTimer.create(this._scene, scrollTimerType);
|
||||
this._scrollTimer.startScroll(viewportMain?.left ?? 0, viewportMain?.top ?? 0, viewportMain);
|
||||
|
||||
const scene = this._scene;
|
||||
this._scenePointerMoveSub = scene.onPointerMove$.subscribeEvent((moveEvt: IPointerEvent | IMouseEvent) => {
|
||||
if (this._contextService.getContextValue(MOBILE_PINCH_ZOOMING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativeCoords = scene.getCoordRelativeToViewport(
|
||||
Vector2.FromArray([moveEvt.offsetX, moveEvt.offsetY])
|
||||
);
|
||||
this._movingHandler(relativeCoords.x, relativeCoords.y, activeSelectionControl, rangeType);
|
||||
this._scrollTimer.scrolling(relativeCoords.x, relativeCoords.y, () => {
|
||||
this._movingHandler(relativeCoords.x, relativeCoords.y, activeSelectionControl, rangeType);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange, IRangeWithCoord, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IMouseEvent, IPointerEvent, IRenderContext, IRenderModule, Scene, SpreadsheetSkeleton, Viewport } from '@univerjs/engine-render';
|
||||
import type { ISelectionStyle, ISelectionWithCoord, ISelectionWithStyle, SheetsSelectionsService, WorkbookSelectionModel } from '@univerjs/sheets';
|
||||
import { DisposableCollection, IContextService, Inject, Injector, RANGE_TYPE, Rectangle, ThemeService, toDisposable } from '@univerjs/core';
|
||||
import { ScrollTimerType, SHEET_VIEWPORT_KEY, Vector2 } from '@univerjs/engine-render';
|
||||
import { attachSelectionWithCoord, convertSelectionDataToRange, IRefSelectionsService, SelectionMoveType } from '@univerjs/sheets';
|
||||
import { BaseSelectionRenderService, checkInHeaderRanges, genNormalSelectionStyle, getAllSelection, getCoordByOffset, getSheetObject, SelectionControl, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { IShortcutService } from '@univerjs/ui';
|
||||
|
||||
export function getActiveSelectionControlIndex(
|
||||
selectionControls: SelectionControl[],
|
||||
activeSelectionControl: Nullable<SelectionControl>
|
||||
): number {
|
||||
const activeControlIndex = activeSelectionControl ? selectionControls.indexOf(activeSelectionControl) : -1;
|
||||
return activeControlIndex === -1 ? selectionControls.length - 1 : activeControlIndex;
|
||||
}
|
||||
|
||||
export function isSelectionRangeChanged(previous: Nullable<IRange>, current: Nullable<IRange>): boolean {
|
||||
if (!previous || !current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return previous.startRow !== current.startRow ||
|
||||
previous.startColumn !== current.startColumn ||
|
||||
previous.endRow !== current.endRow ||
|
||||
previous.endColumn !== current.endColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service extends the existing `SelectionRenderService` to provide the rendering of prompt selections
|
||||
* when user is editing ref ranges in formulas.
|
||||
*
|
||||
* Not that this service works with Uni-mode, which means it should be able to deal with multi render unit
|
||||
* and handle selections on them, though each at a time.
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
export class RefSelectionsRenderService extends BaseSelectionRenderService implements IRenderModule {
|
||||
protected readonly _workbookSelections: WorkbookSelectionModel;
|
||||
|
||||
private _eventDisposables: Nullable<IDisposable>;
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@Inject(Injector) injector: Injector,
|
||||
@Inject(ThemeService) themeService: ThemeService,
|
||||
@IShortcutService shortcutService: IShortcutService,
|
||||
@Inject(SheetSkeletonManagerService) sheetSkeletonManagerService: SheetSkeletonManagerService,
|
||||
@IContextService protected readonly _contextService: IContextService,
|
||||
@IRefSelectionsService private readonly _refSelectionsService: SheetsSelectionsService
|
||||
) {
|
||||
super(
|
||||
injector,
|
||||
themeService,
|
||||
shortcutService,
|
||||
sheetSkeletonManagerService,
|
||||
_contextService
|
||||
);
|
||||
|
||||
this._workbookSelections = this._refSelectionsService.getWorkbookSelections(this._context.unitId);
|
||||
|
||||
this._initSelectionChangeListener();
|
||||
this._initSkeletonChangeListener();
|
||||
this._initUserActionSyncListener();
|
||||
|
||||
this._setSelectionStyle(getDefaultRefSelectionStyle(this._themeService));
|
||||
this._remainLastEnabled = true; // For ref range selections, we should always remain others.
|
||||
this._highlightHeader = false;
|
||||
}
|
||||
|
||||
getLocation(): [string, string] {
|
||||
return this._skeleton.getLocation();
|
||||
}
|
||||
|
||||
setRemainLastEnabled(enabled: boolean): void {
|
||||
this._remainLastEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is set to true when you need to add a new selection.
|
||||
* @param {boolean} enabled
|
||||
* @memberof RefSelectionsRenderService
|
||||
*/
|
||||
setSkipLastEnabled(enabled: boolean): void {
|
||||
this._skipLastEnabled = enabled;
|
||||
}
|
||||
|
||||
clearLastSelection(): void {
|
||||
const last = this._selectionControls[this._selectionControls.length - 1];
|
||||
if (last) {
|
||||
last.dispose();
|
||||
this._selectionControls.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method and user will be able to select on the canvas to update selections.
|
||||
*/
|
||||
enableSelectionChanging(): IDisposable {
|
||||
this._disableSelectionChanging();
|
||||
this._eventDisposables = this._initCanvasEventListeners();
|
||||
return toDisposable(() => this._disableSelectionChanging());
|
||||
}
|
||||
|
||||
private _disableSelectionChanging(): void {
|
||||
this._eventDisposables?.dispose();
|
||||
this._eventDisposables = null;
|
||||
}
|
||||
|
||||
disableSelectionChanging(): void {
|
||||
this._disableSelectionChanging();
|
||||
}
|
||||
|
||||
protected _initCanvasEventListeners(): IDisposable {
|
||||
const sheetObject = this._getSheetObject();
|
||||
const { spreadsheetRowHeader, spreadsheetColumnHeader, spreadsheet, spreadsheetLeftTopPlaceholder } = sheetObject;
|
||||
const { scene } = this._context;
|
||||
|
||||
const listenerDisposables = new DisposableCollection();
|
||||
listenerDisposables.add(spreadsheet?.onPointerDown$.subscribeEvent((evt: IPointerEvent | IMouseEvent, state) => {
|
||||
if (!this.inRefSelectionMode()) return;
|
||||
|
||||
this._onPointerDown(evt, spreadsheet.zIndex + 1, RANGE_TYPE.NORMAL, this._getActiveViewport(evt));
|
||||
if (evt.button !== 2) {
|
||||
state.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
listenerDisposables.add(
|
||||
spreadsheetRowHeader?.onPointerDown$.subscribeEvent((evt: IPointerEvent | IMouseEvent, state) => {
|
||||
if (!this.inRefSelectionMode()) return;
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) return;
|
||||
const { row } = getCoordByOffset(evt.offsetX, evt.offsetY, scene, skeleton);
|
||||
const matchSelectionData = checkInHeaderRanges(this._workbookSelections.getCurrentSelections(), row, RANGE_TYPE.ROW);
|
||||
if (matchSelectionData) return;
|
||||
|
||||
this._onPointerDown(evt, (spreadsheet.zIndex || 1) + 1, RANGE_TYPE.ROW, this._getActiveViewport(evt), ScrollTimerType.Y);
|
||||
if (evt.button !== 2) {
|
||||
state.stopPropagation();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
listenerDisposables.add(spreadsheetColumnHeader?.onPointerDown$.subscribeEvent((evt: IPointerEvent | IMouseEvent, state) => {
|
||||
if (!this.inRefSelectionMode()) return;
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) return;
|
||||
const { column } = getCoordByOffset(evt.offsetX, evt.offsetY, scene, skeleton);
|
||||
const matchSelectionData = checkInHeaderRanges(this._workbookSelections.getCurrentSelections(), column, RANGE_TYPE.COLUMN);
|
||||
if (matchSelectionData) return;
|
||||
|
||||
this._onPointerDown(evt, (spreadsheet.zIndex || 1) + 1, RANGE_TYPE.COLUMN, this._getActiveViewport(evt), ScrollTimerType.X);
|
||||
|
||||
if (evt.button !== 2) {
|
||||
state.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
listenerDisposables.add(spreadsheetLeftTopPlaceholder?.onPointerDown$.subscribeEvent((evt: IPointerEvent | IMouseEvent, state) => {
|
||||
// remove all other selections
|
||||
this._reset();
|
||||
if (!this.inRefSelectionMode()) return;
|
||||
const skeleton = this._sheetSkeletonManagerService.getCurrentSkeleton();
|
||||
if (!skeleton) return;
|
||||
const selectionWithStyle = getAllSelection(skeleton);
|
||||
this._addSelectionControlByModelData(selectionWithStyle);
|
||||
this._selectionMoveStart$.next(this.getSelectionDataWithStyle());
|
||||
const dispose = scene.onPointerUp$.subscribeEvent(() => {
|
||||
dispose.unsubscribe();
|
||||
this._selectionMoveEnd$.next(this.getSelectionDataWithStyle());
|
||||
});
|
||||
if (evt.button !== 2) {
|
||||
state.stopPropagation();
|
||||
}
|
||||
}));
|
||||
|
||||
return listenerDisposables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a selection in spreadsheet, create a new SelectionControl and then update this control by range derives from selection.
|
||||
* For ref selection, create selectionShapeExtension to handle user action.
|
||||
* @param {ISelectionWithCoord} selectionWithStyle
|
||||
*/
|
||||
protected override _addSelectionControlByModelData(selectionWithStyle: ISelectionWithStyle): SelectionControl {
|
||||
const skeleton = this._skeleton;
|
||||
const style = selectionWithStyle.style ?? genNormalSelectionStyle(this._themeService);
|
||||
const scene = this._scene;
|
||||
|
||||
selectionWithStyle.style = style;
|
||||
const control = this.newSelectionControl(scene, skeleton, selectionWithStyle);
|
||||
return control;
|
||||
}
|
||||
|
||||
private _initSelectionChangeListener(): void {
|
||||
// used for refresh selection when focus in formula editor.
|
||||
// TODO @lumixraku The response of the formula selection to keyboard events is different from that of a regular selection; I believe they should be consistent.
|
||||
// unlike normal selection, there is no need to listen selectionService.selectionMoveEnd$ for keyboard moving event. prompt@highlightFormula would refresh selection.
|
||||
this.disposeWithMe(this._refSelectionsService.selectionSet$.subscribe((selectionsWithStyles) => {
|
||||
this._reset();
|
||||
const skeleton = this._skeleton;
|
||||
if (!skeleton) return;
|
||||
// The selections' style would be colorful here. PromptController would change the color of selections later.
|
||||
this.resetSelectionsByModelData(selectionsWithStyles || []);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selectionModel in this._workbookSelections by user action in spreadsheet area.
|
||||
*/
|
||||
private _initUserActionSyncListener(): void {
|
||||
this.disposeWithMe(this.selectionMoveStart$.subscribe((selectionDataWithStyle) => {
|
||||
this._updateSelections(selectionDataWithStyle, SelectionMoveType.MOVE_START);
|
||||
}));
|
||||
this.disposeWithMe(this.selectionMoving$.subscribe((selectionDataWithStyle) => {
|
||||
this._updateSelections(selectionDataWithStyle, SelectionMoveType.MOVING);
|
||||
}));
|
||||
this.disposeWithMe(this.selectionMoveEnd$.subscribe((selectionDataWithStyle) => {
|
||||
this._updateSelections(selectionDataWithStyle, SelectionMoveType.MOVE_END);
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateSelections(selectionDataWithStyleList: ISelectionWithCoord[], type: SelectionMoveType): void {
|
||||
const workbook = this._context.unit;
|
||||
const sheetId = workbook.getActiveSheet()!.getSheetId();
|
||||
|
||||
if (selectionDataWithStyleList.length === 0) return;
|
||||
this._workbookSelections.setSelections(
|
||||
sheetId,
|
||||
selectionDataWithStyleList.map((selectionDataWithStyle) => convertSelectionDataToRange(selectionDataWithStyle)),
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
private _initSkeletonChangeListener(): void {
|
||||
// changing sheet is not the only way cause currentSkeleton$ emit, a lot of cmds will emit currentSkeleton$
|
||||
// COMMAND_LISTENER_SKELETON_CHANGE ---> currentSkeleton$.next
|
||||
// 'sheet.mutation.set-worksheet-row-auto-height' is one of COMMAND_LISTENER_SKELETON_CHANGE
|
||||
// dv render controller would cause row auto height, this._autoHeightController.getUndoRedoParamsOfAutoHeight
|
||||
this.disposeWithMe(this._sheetSkeletonManagerService.currentSkeleton$.subscribe((param) => {
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { skeleton } = param;
|
||||
const { scene } = this._context;
|
||||
const viewportMain = scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
|
||||
// changing sheet
|
||||
if (this._skeleton && this._skeleton.worksheet?.getSheetId() !== skeleton.worksheet.getSheetId()) {
|
||||
this._reset();
|
||||
}
|
||||
this._changeRuntime(skeleton, scene, viewportMain);
|
||||
|
||||
// for col width & row height resize
|
||||
const currentSelections = this._workbookSelections.getCurrentSelections();
|
||||
this.resetSelectionsByModelData(currentSelections);
|
||||
}));
|
||||
}
|
||||
|
||||
protected _getActiveViewport(evt: IPointerEvent | IMouseEvent): Nullable<Viewport> {
|
||||
const sheetObject = this._getSheetObject();
|
||||
return sheetObject?.scene.getActiveViewportByCoord(Vector2.FromArray([evt.offsetX, evt.offsetY]));
|
||||
}
|
||||
|
||||
protected _getSheetObject() {
|
||||
return getSheetObject(this._context.unit, this._context)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pointer down event, bind pointermove & pointerup handler.
|
||||
* then trigger selectionMoveStart$.
|
||||
*
|
||||
* @param evt
|
||||
* @param _zIndex
|
||||
* @param rangeType
|
||||
* @param viewport
|
||||
* @param scrollTimerType
|
||||
*/
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
protected _onPointerDown(
|
||||
evt: IPointerEvent | IMouseEvent,
|
||||
_zIndex = 0,
|
||||
rangeType: RANGE_TYPE = RANGE_TYPE.NORMAL,
|
||||
viewport: Nullable<Viewport>,
|
||||
scrollTimerType: ScrollTimerType = ScrollTimerType.ALL
|
||||
): void {
|
||||
this._rangeType = rangeType;
|
||||
|
||||
const skeleton = this._skeleton;
|
||||
const scene = this._scene;
|
||||
if (!scene || !skeleton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewport) {
|
||||
this._activeViewport = viewport;
|
||||
}
|
||||
|
||||
const { offsetX: evtOffsetX, offsetY: evtOffsetY } = evt;
|
||||
const viewportMain = scene.getViewport(SHEET_VIEWPORT_KEY.VIEW_MAIN);
|
||||
if (!viewportMain) return;
|
||||
const relativeCoords = scene.getCoordRelativeToViewport(Vector2.FromArray([evtOffsetX, evtOffsetY]));
|
||||
|
||||
const { x: offsetX, y: offsetY } = relativeCoords;
|
||||
this._startViewportPosX = offsetX;
|
||||
this._startViewportPosY = offsetY;
|
||||
|
||||
const scrollXY = scene.getScrollXYInfoByViewport(relativeCoords);
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
const selectCell = this._skeleton.getCellByOffset(offsetX, offsetY, scaleX, scaleY, scrollXY);
|
||||
if (!selectCell) return;
|
||||
switch (rangeType) {
|
||||
case RANGE_TYPE.NORMAL:
|
||||
break;
|
||||
case RANGE_TYPE.ROW:
|
||||
selectCell.startColumn = 0;
|
||||
selectCell.endColumn = this._skeleton.getColumnCount() - 1;
|
||||
break;
|
||||
case RANGE_TYPE.COLUMN:
|
||||
selectCell.startRow = 0;
|
||||
selectCell.endRow = this._skeleton.getRowCount() - 1;
|
||||
break;
|
||||
case RANGE_TYPE.ALL:
|
||||
selectCell.startRow = 0;
|
||||
selectCell.startColumn = 0;
|
||||
selectCell.endRow = this._skeleton.getRowCount() - 1;
|
||||
selectCell.endColumn = this._skeleton.getColumnCount() - 1;
|
||||
}
|
||||
|
||||
let selectionWithStyle: ISelectionWithStyle = { range: selectCell, primary: selectCell, style: null };
|
||||
// If the selected cell is merged cell, we need to adjust the selection range to the merged main cell in the formula reference selection scenario.
|
||||
if (selectCell.isMerged || selectCell.isMergedMainCell) {
|
||||
selectionWithStyle = {
|
||||
range: {
|
||||
...selectCell,
|
||||
startRow: selectCell.startRow,
|
||||
endRow: selectCell.startRow,
|
||||
startColumn: selectCell.startColumn,
|
||||
endColumn: selectCell.startColumn,
|
||||
},
|
||||
primary: {
|
||||
...selectCell,
|
||||
actualRow: selectCell.startRow,
|
||||
actualColumn: selectCell.startColumn,
|
||||
startRow: selectCell.startRow,
|
||||
endRow: selectCell.startRow,
|
||||
startColumn: selectCell.startColumn,
|
||||
endColumn: selectCell.startColumn,
|
||||
},
|
||||
style: null,
|
||||
};
|
||||
}
|
||||
selectionWithStyle.range.rangeType = rangeType;
|
||||
|
||||
const selectionCellWithCoord = attachSelectionWithCoord(selectionWithStyle, this._skeleton);
|
||||
this._startRangeWhenPointerDown = { ...selectionCellWithCoord.rangeWithCoord };
|
||||
|
||||
const cursorCellRangeWithRangeType: IRangeWithCoord = { ...selectionCellWithCoord.rangeWithCoord, rangeType };
|
||||
|
||||
let activeSelectionControl: Nullable<SelectionControl> = this.getActiveSelectionControl();
|
||||
const curControls = this.getSelectionControls();
|
||||
for (const control of curControls) {
|
||||
// If right click on a selection, we should not create a new selection control.
|
||||
// Instead, the context menu will popup.
|
||||
if (evt.button === 2 && Rectangle.contains(control.model, cursorCellRangeWithRangeType)) {
|
||||
activeSelectionControl = control;
|
||||
return;
|
||||
}
|
||||
// Click to an existing selection,
|
||||
if (control.model.isEqual(cursorCellRangeWithRangeType)) {
|
||||
activeSelectionControl = control;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._checkClearPreviousControls(evt);
|
||||
|
||||
const currentCell = activeSelectionControl?.model.currentCell;
|
||||
const expandByShiftKey = evt.shiftKey && currentCell;
|
||||
const rangeBeforePointerDown = expandByShiftKey && activeSelectionControl
|
||||
? {
|
||||
startRow: activeSelectionControl.model.startRow,
|
||||
startColumn: activeSelectionControl.model.startColumn,
|
||||
endRow: activeSelectionControl.model.endRow,
|
||||
endColumn: activeSelectionControl.model.endColumn,
|
||||
}
|
||||
: null;
|
||||
const remainLastEnable = this._remainLastEnabled &&
|
||||
!evt.ctrlKey &&
|
||||
!evt.metaKey &&
|
||||
!evt.shiftKey &&
|
||||
!this._skipLastEnabled &&
|
||||
!this._singleSelectionEnabled;
|
||||
|
||||
//#region update selection control
|
||||
if (expandByShiftKey && currentCell) {
|
||||
// Perform pointer down selection.
|
||||
this._makeSelectionByTwoCells(
|
||||
currentCell,
|
||||
cursorCellRangeWithRangeType,
|
||||
skeleton,
|
||||
rangeType,
|
||||
activeSelectionControl! // Get updated in this method
|
||||
);
|
||||
} else if (remainLastEnable && activeSelectionControl) {
|
||||
// Supports the formula ref text selection feature,
|
||||
// under the condition of preserving all previous selections, it modifies the position of the latest selection.
|
||||
|
||||
activeSelectionControl.updateRangeBySelectionWithCoord(selectionCellWithCoord);
|
||||
} else {
|
||||
// In normal situation, pointerdown ---> Create new SelectionControl,
|
||||
activeSelectionControl = this.newSelectionControl(scene, skeleton, selectionWithStyle);
|
||||
}
|
||||
// Clear highlight except the active selection. The active ref may not be the last one.
|
||||
const selectionControls = this.getSelectionControls();
|
||||
const activeControlIndex = getActiveSelectionControlIndex(selectionControls, activeSelectionControl);
|
||||
for (let i = 0; i < selectionControls.length; i++) {
|
||||
if (i !== activeControlIndex && selectionControls[i].model.currentCell) {
|
||||
selectionControls[i].clearHighlight();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this._selectionMoveStart$.next(this.getSelectionDataWithStyle());
|
||||
if (expandByShiftKey && activeSelectionControl && isSelectionRangeChanged(rangeBeforePointerDown, activeSelectionControl.model)) {
|
||||
this._selectionMoving$.next(this.getSelectionDataWithStyle());
|
||||
}
|
||||
|
||||
scene.disableObjectsEvent();
|
||||
this._clearUpdatingListeners();
|
||||
this._addEndingListeners();
|
||||
|
||||
scene.getTransformer()?.clearSelectedObjects();
|
||||
|
||||
// if (rangeType === RANGE_TYPE.ROW || rangeType === RANGE_TYPE.COLUMN) {
|
||||
// if (rangeType === RANGE_TYPE.ROW) {
|
||||
// offsetX = 0;
|
||||
// } else if (rangeType === RANGE_TYPE.COLUMN) {
|
||||
// offsetY = 0;
|
||||
// }
|
||||
// // TODO @lumixraku. This is so bad! There should be a explicit way to update col&row range. But now depends on the side effect of _movingHandler.
|
||||
// // call _movingHandler to update range, col selection, endRow should be last row of current sheet.
|
||||
// this._movingHandler(offsetX, offsetY, activeSelectionControl, rangeType);
|
||||
// }
|
||||
|
||||
this._setupPointerMoveListener(viewportMain, activeSelectionControl!, rangeType, scrollTimerType, offsetX, offsetY);
|
||||
|
||||
this._escapeShortcutDisposable = this._shortcutService.forceEscape();
|
||||
this._scenePointerUpSub = scene.onPointerUp$.subscribeEvent(() => {
|
||||
this._clearUpdatingListeners();
|
||||
|
||||
// selection control would be disposed in _selectionMoveEnd$.
|
||||
// SelectionRenderService@selectionMoveEnd$ exec SetSelectionsOperation
|
||||
// SheetsSelectionsService@setSelections
|
||||
// SelectionRenderService._workbookSelections.selectionMoveEnd$ call _reset() to clear selectionControl.
|
||||
this._selectionMoveEnd$.next(this.getSelectionDataWithStyle());
|
||||
this._escapeShortcutDisposable?.dispose();
|
||||
this._escapeShortcutDisposable = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff between normal selection, no highlightHeader for ref selections.
|
||||
* @param scene
|
||||
* @param skeleton
|
||||
* @param selection
|
||||
* @returns {SelectionControl} selectionControl just created
|
||||
*/
|
||||
override newSelectionControl(scene: Scene, skeleton: SpreadsheetSkeleton, selection: ISelectionWithStyle): SelectionControl {
|
||||
const zIndex = this.getSelectionControls().length;
|
||||
const { rowHeaderWidth, columnHeaderHeight } = skeleton;
|
||||
const control = new SelectionControl(scene, zIndex, this._themeService, {
|
||||
highlightHeader: this._highlightHeader,
|
||||
enableAutoFill: false,
|
||||
rowHeaderWidth,
|
||||
columnHeaderHeight,
|
||||
});
|
||||
const selectionWithCoord = attachSelectionWithCoord(selection, skeleton);
|
||||
control.updateRangeBySelectionWithCoord(selectionWithCoord);
|
||||
this._selectionControls.push(control);
|
||||
|
||||
control.setControlExtension({
|
||||
skeleton,
|
||||
scene,
|
||||
themeService: this._themeService,
|
||||
injector: this._injector,
|
||||
selectionHooks: {
|
||||
selectionMoveEnd: (): void => {
|
||||
this._selectionMoveEnd$.next(this.getSelectionDataWithStyle());
|
||||
},
|
||||
},
|
||||
});
|
||||
return control;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the selections style while adding a range into the formula string (blue dashed).
|
||||
* @param themeService
|
||||
* @returns The selection's style.
|
||||
*/
|
||||
function getDefaultRefSelectionStyle(themeService: ThemeService): ISelectionStyle {
|
||||
const style = genNormalSelectionStyle(themeService);
|
||||
style.widgets = { tl: true, tc: true, tr: true, ml: true, mr: true, bl: true, bc: true, br: true };
|
||||
return style;
|
||||
}
|
||||
59
packages/sheets-formula-ui/src/services/utils.ts
Normal file
59
packages/sheets-formula-ui/src/services/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 { LocaleService } from '@univerjs/core';
|
||||
import type { IFunctionInfo, IFunctionParam } from '@univerjs/engine-formula';
|
||||
import { FunctionType } from '@univerjs/engine-formula';
|
||||
|
||||
export function getFunctionTypeValues(
|
||||
localeService: LocaleService,
|
||||
customFormula: boolean
|
||||
): Array<{ label: string; value: string }> {
|
||||
// Exclude the DefinedName key
|
||||
return Object.keys(FunctionType)
|
||||
.filter((key) => isNaN(Number(key)) && key !== 'DefinedName' && key !== 'Table' && (customFormula || key !== 'User'))
|
||||
.map((key) => ({
|
||||
label: localeService.t(`sheets-formula-ui.functionType.${key.toLocaleLowerCase()}`),
|
||||
value: `${FunctionType[key as keyof typeof FunctionType]}`,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getFunctionName(item: IFunctionInfo, localeService: LocaleService) {
|
||||
let functionName = '';
|
||||
if (item.aliasFunctionName) {
|
||||
functionName = localeService.t(item.aliasFunctionName);
|
||||
|
||||
if (functionName === item.aliasFunctionName) {
|
||||
functionName = item.functionName;
|
||||
}
|
||||
} else {
|
||||
functionName = item.functionName;
|
||||
}
|
||||
|
||||
return functionName;
|
||||
}
|
||||
|
||||
export function generateParam(param: IFunctionParam) {
|
||||
if (!param.require && !param.repeat) {
|
||||
return `[${param.name}]`;
|
||||
} else if (param.require && !param.repeat) {
|
||||
return param.name;
|
||||
} else if (!param.require && param.repeat) {
|
||||
return `[${param.name},...]`;
|
||||
} else if (param.require && param.repeat) {
|
||||
return `${param.name},...`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 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 { IDisposable } from '@univerjs/core';
|
||||
import { createIdentifier, DisposableCollection, toDisposable } from '@univerjs/core';
|
||||
|
||||
export const FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE = 'data-embed-interaction-boundary-owner';
|
||||
export const FORMULA_EMBED_RUNTIME_FOCUS_ROLE_ATTRIBUTE = 'data-embed-runtime-focus-role';
|
||||
export const FORMULA_EMBED_ID_ATTRIBUTE = 'data-embed-id';
|
||||
export const FORMULA_EMBED_HOST_UNIT_ID_ATTRIBUTE = 'data-embed-host-unit-id';
|
||||
export const FORMULA_EMBED_CHILD_UNIT_ID_ATTRIBUTE = 'data-embed-child-unit-id';
|
||||
|
||||
export interface IFormulaEmbedRuntimeDomScope {
|
||||
embedId: string;
|
||||
hostUnitId?: string;
|
||||
childUnitId?: string;
|
||||
}
|
||||
|
||||
export interface IFormulaEmbedRuntimeFocusCoordinator {
|
||||
resolveRuntimeScopeByChildUnitId(childUnitId: string): IFormulaEmbedRuntimeDomScope | undefined;
|
||||
acquireLease(options: {
|
||||
embedId: string;
|
||||
role: string;
|
||||
owner: string;
|
||||
hostUnitId?: string;
|
||||
childUnitId?: string;
|
||||
associatedChildUnitIds?: string[];
|
||||
}): IDisposable;
|
||||
registerElement(options: {
|
||||
embedId: string;
|
||||
role: string;
|
||||
element: HTMLElement;
|
||||
}): IDisposable;
|
||||
}
|
||||
|
||||
export const IFormulaEmbedRuntimeFocusCoordinator = createIdentifier<IFormulaEmbedRuntimeFocusCoordinator>('sheets-formula-ui.embed-runtime-focus-coordinator');
|
||||
|
||||
export interface IFormulaEmbedInteractionBoundaryService {
|
||||
registerOwnedElement(embedId: string, element: Element): IDisposable;
|
||||
}
|
||||
|
||||
export const IFormulaEmbedInteractionBoundaryService = createIdentifier<IFormulaEmbedInteractionBoundaryService>('sheets-formula-ui.embed-interaction-boundary.service');
|
||||
|
||||
interface IRegisterFormulaEditorRuntimePortalOptions {
|
||||
embedId: string;
|
||||
editorId: string;
|
||||
ownerDocument?: Document;
|
||||
interactionBoundaryService?: IFormulaEmbedInteractionBoundaryService;
|
||||
focusCoordinator?: IFormulaEmbedRuntimeFocusCoordinator;
|
||||
}
|
||||
|
||||
export function registerFormulaEditorRuntimePortal(options: IRegisterFormulaEditorRuntimePortalOptions): IDisposable {
|
||||
const ownerDocument = options.ownerDocument ?? (typeof document === 'undefined' ? undefined : document);
|
||||
if (!ownerDocument) {
|
||||
return toDisposable(() => {});
|
||||
}
|
||||
|
||||
const collection = new DisposableCollection();
|
||||
const view = ownerDocument.defaultView;
|
||||
const frameHandles: number[] = [];
|
||||
let observer: MutationObserver | undefined;
|
||||
let portalRegistration: IDisposable | undefined;
|
||||
let registeredPortalRoot: HTMLElement | null = null;
|
||||
let disposed = false;
|
||||
|
||||
const tryRegister = () => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const portalRoot = resolveFormulaEditorPortalRoot(options.editorId, ownerDocument);
|
||||
if (portalRoot === registeredPortalRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
portalRegistration?.dispose();
|
||||
portalRegistration = undefined;
|
||||
registeredPortalRoot = null;
|
||||
if (!portalRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
registeredPortalRoot = portalRoot;
|
||||
portalRegistration = registerFormulaEditorPortalRoot(options, ownerDocument, portalRoot);
|
||||
};
|
||||
|
||||
const scheduleRetry = (remaining: number) => {
|
||||
if (remaining <= 0 || !view?.requestAnimationFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = view.requestAnimationFrame(() => {
|
||||
const index = frameHandles.indexOf(handle);
|
||||
if (index >= 0) {
|
||||
frameHandles.splice(index, 1);
|
||||
}
|
||||
tryRegister();
|
||||
if (!registeredPortalRoot) {
|
||||
scheduleRetry(remaining - 1);
|
||||
}
|
||||
});
|
||||
frameHandles.push(handle);
|
||||
};
|
||||
|
||||
tryRegister();
|
||||
if (!registeredPortalRoot) {
|
||||
scheduleRetry(2);
|
||||
}
|
||||
if (view?.MutationObserver && ownerDocument.body) {
|
||||
observer = new view.MutationObserver(() => tryRegister());
|
||||
observer.observe(ownerDocument.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
collection.add(toDisposable(() => {
|
||||
disposed = true;
|
||||
frameHandles.forEach((handle) => view?.cancelAnimationFrame?.(handle));
|
||||
frameHandles.length = 0;
|
||||
observer?.disconnect();
|
||||
observer = undefined;
|
||||
portalRegistration?.dispose();
|
||||
portalRegistration = undefined;
|
||||
registeredPortalRoot = null;
|
||||
}));
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
function resolveFormulaEditorPortalRoot(editorId: string, ownerDocument: Document): HTMLElement | null {
|
||||
return (ownerDocument.getElementById(`univer-doc-selection-container-${editorId}`) as HTMLElement | null)
|
||||
?? (ownerDocument.getElementById(`__editor_${editorId}`) as HTMLElement | null);
|
||||
}
|
||||
|
||||
function registerFormulaEditorPortalRoot(
|
||||
options: IRegisterFormulaEditorRuntimePortalOptions,
|
||||
ownerDocument: Document,
|
||||
portalRoot: HTMLElement
|
||||
): IDisposable {
|
||||
const collection = new DisposableCollection();
|
||||
const editorElement = ownerDocument.getElementById(`__editor_${options.editorId}`) as HTMLElement | null;
|
||||
const elements = editorElement && editorElement !== portalRoot ? [portalRoot, editorElement] : [portalRoot];
|
||||
|
||||
elements.forEach((element) => {
|
||||
if (options.interactionBoundaryService) {
|
||||
collection.add(options.interactionBoundaryService.registerOwnedElement(options.embedId, element));
|
||||
}
|
||||
if (options.focusCoordinator) {
|
||||
collection.add(options.focusCoordinator.registerElement({
|
||||
embedId: options.embedId,
|
||||
role: 'child-editor',
|
||||
element,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
export function resolveFormulaEmbedRuntimeDomScope(root: HTMLElement | null | undefined): IFormulaEmbedRuntimeDomScope | undefined {
|
||||
const scopeElement = root?.closest<HTMLElement>(`[${FORMULA_EMBED_ID_ATTRIBUTE}]`);
|
||||
const embedId = scopeElement?.getAttribute(FORMULA_EMBED_ID_ATTRIBUTE);
|
||||
if (!scopeElement || !embedId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
embedId,
|
||||
hostUnitId: scopeElement.getAttribute(FORMULA_EMBED_HOST_UNIT_ID_ATTRIBUTE) ?? undefined,
|
||||
childUnitId: scopeElement.getAttribute(FORMULA_EMBED_CHILD_UNIT_ID_ATTRIBUTE) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveActiveFormulaEmbedRuntimeDomScope(ownerDocument: Document | undefined): IFormulaEmbedRuntimeDomScope | undefined {
|
||||
const activeElement = ownerDocument?.activeElement;
|
||||
return activeElement instanceof HTMLElement ? resolveFormulaEmbedRuntimeDomScope(activeElement) : undefined;
|
||||
}
|
||||
|
||||
export function isEventTargetInSameFormulaEmbedInteractionBoundary(left: EventTarget | null | undefined, right: EventTarget | null | undefined): boolean {
|
||||
const leftOwner = resolveFormulaEmbedInteractionOwnerId(left);
|
||||
return Boolean(leftOwner && leftOwner === resolveFormulaEmbedInteractionOwnerId(right));
|
||||
}
|
||||
|
||||
function resolveFormulaEmbedInteractionOwnerId(target: EventTarget | null | undefined): string | undefined {
|
||||
if (!(target instanceof Element)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return target.closest(`[${FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE}]`)
|
||||
?.getAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE) ?? undefined;
|
||||
}
|
||||
@@ -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 type { Editor } from '@univerjs/docs-ui';
|
||||
import type { IFunctionParam } from '@univerjs/engine-formula';
|
||||
import type { LocaleKey } from '../../../locale/types';
|
||||
import { LocaleService, noop } from '@univerjs/core';
|
||||
import { borderClassName, borderTopClassName, clsx, scrollbarClassName } from '@univerjs/design';
|
||||
import { CloseIcon, MoreRightIcon } from '@univerjs/icons';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { RectPopup, useDependency, useEvent, useObservable } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
import { generateParam } from '../../../services/utils';
|
||||
import { useEditorPosition } from '../hooks/use-editor-position';
|
||||
import { useFormulaDescribe } from '../hooks/use-formula-describe';
|
||||
import { HelpHiddenTip } from './HelpHiddenTip';
|
||||
|
||||
function Params({ className, title, value }: {
|
||||
className?: string;
|
||||
title: string;
|
||||
value?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="univer-my-2">
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-mb-2 univer-text-sm univer-font-medium univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`, className)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className="univer-whitespace-pre-wrap univer-break-words univer-text-xs univer-text-gray-500"
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Help(props: {
|
||||
prefix?: string;
|
||||
value?: IFunctionParam[];
|
||||
active: number;
|
||||
onClick: (paramIndex: number) => void;
|
||||
}) {
|
||||
const { prefix, value, active, onClick } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
{prefix}
|
||||
(
|
||||
</span>
|
||||
{value &&
|
||||
value.map((item: IFunctionParam, i: number) => (
|
||||
<span key={item.name}>
|
||||
<span
|
||||
className={active === i ? 'univer-text-primary-500' : ''}
|
||||
onClick={() => onClick(i)}
|
||||
>
|
||||
{generateParam(item)}
|
||||
</span>
|
||||
{i === value.length - 1 ? '' : ','}
|
||||
</span>
|
||||
))}
|
||||
)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HelpFunction(props: {
|
||||
onParamsSwitch?: (index: number) => void;
|
||||
onClose?: () => void;
|
||||
editor: Editor;
|
||||
isFocus: boolean;
|
||||
formulaText: string;
|
||||
}) {
|
||||
const { onParamsSwitch = noop, onClose: propColose = noop, isFocus, editor, formulaText } = props;
|
||||
const { functionInfo, paramIndex, reset } = useFormulaDescribe(isFocus, formulaText, editor);
|
||||
const editorBridgeService = useDependency(IEditorBridgeService);
|
||||
const hidden = !useObservable(editorBridgeService.helpFunctionVisible$);
|
||||
const [contentVisible, setContentVisible] = useState(false);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const required = localeService.t<LocaleKey>('sheets-formula-ui.prompt.required');
|
||||
const optional = localeService.t<LocaleKey>('sheets-formula-ui.prompt.optional');
|
||||
const editorId = editor.getEditorId();
|
||||
const [position$] = useEditorPosition(editorId, !!functionInfo, [functionInfo, paramIndex]);
|
||||
function handleSwitchActive(paramIndex: number) {
|
||||
onParamsSwitch && onParamsSwitch(paramIndex);
|
||||
}
|
||||
|
||||
const setHidden = useEvent((v: boolean) => {
|
||||
editorBridgeService.helpFunctionVisible$.next(!v);
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
setHidden(true);
|
||||
propColose();
|
||||
};
|
||||
|
||||
return functionInfo
|
||||
? hidden
|
||||
? (
|
||||
<RectPopup key="hidden" portal anchorRect$={position$} direction="left-center">
|
||||
<HelpHiddenTip onClick={() => setHidden(false)} />
|
||||
</RectPopup>
|
||||
)
|
||||
: (
|
||||
<RectPopup key="show" portal onClickOutside={() => reset()} anchorRect$={position$} direction="vertical">
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-m-0 univer-box-border univer-w-[250px] univer-select-none univer-list-none
|
||||
univer-rounded-lg univer-bg-gray-0 univer-leading-5 univer-shadow-md univer-outline-none
|
||||
dark:!univer-bg-gray-900
|
||||
`, borderClassName)}
|
||||
>
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-box-border univer-flex univer-items-center univer-justify-between univer-px-4
|
||||
univer-py-3 univer-text-xs univer-font-medium univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`, borderTopClassName)}
|
||||
style={{
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
<Help
|
||||
prefix={functionInfo.functionName}
|
||||
value={functionInfo.functionParameter}
|
||||
active={paramIndex}
|
||||
onClick={handleSwitchActive}
|
||||
/>
|
||||
<div className="univer-flex">
|
||||
<div
|
||||
className={`
|
||||
univer-ml-2 univer-flex univer-size-6 univer-cursor-pointer univer-items-center
|
||||
univer-justify-center univer-rounded univer-bg-transparent univer-p-0
|
||||
univer-text-xs univer-text-gray-500 univer-outline-none univer-transition-colors
|
||||
hover:univer-bg-gray-200
|
||||
dark:hover:!univer-bg-gray-600
|
||||
`}
|
||||
style={{ transform: contentVisible ? 'rotateZ(-90deg)' : 'rotateZ(90deg)' }}
|
||||
onClick={() => setContentVisible(!contentVisible)}
|
||||
>
|
||||
<MoreRightIcon />
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
univer-ml-2 univer-flex univer-size-6 univer-cursor-pointer univer-items-center
|
||||
univer-justify-center univer-rounded univer-bg-transparent univer-p-0
|
||||
univer-text-xs univer-text-gray-600 univer-outline-none univer-transition-colors
|
||||
hover:univer-bg-gray-300
|
||||
dark:!univer-text-gray-200
|
||||
dark:hover:!univer-bg-gray-600
|
||||
`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-box-border univer-max-h-[350px] univer-overflow-y-auto univer-px-4 univer-pb-3
|
||||
univer-pt-0
|
||||
`, scrollbarClassName)}
|
||||
style={{
|
||||
height: contentVisible ? 'unset' : 0,
|
||||
padding: contentVisible ? 'revert-layer' : 0,
|
||||
}}
|
||||
>
|
||||
<div className="univer-mt-3">
|
||||
<Params
|
||||
title={localeService.t<LocaleKey>('sheets-formula-ui.prompt.helpExample')}
|
||||
value={`${functionInfo.functionName}(${functionInfo.functionParameter
|
||||
.map((item) => item.example)
|
||||
.join(',')})`}
|
||||
/>
|
||||
<Params
|
||||
title={localeService.t<LocaleKey>('sheets-formula-ui.prompt.helpAbstract')}
|
||||
value={functionInfo.description}
|
||||
/>
|
||||
{functionInfo &&
|
||||
functionInfo.functionParameter &&
|
||||
functionInfo.functionParameter.map((item: IFunctionParam, i: number) => (
|
||||
<Params
|
||||
key={i}
|
||||
className={paramIndex === i ? 'univer-text-primary-500' : ''}
|
||||
title={item.name}
|
||||
value={`${item.require ? required : optional} ${item.detail}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RectPopup>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export const HelpHiddenTip = ({ onClick }: { onClick: () => void }) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-z-[15] univer-box-border univer-h-[18px] univer-cursor-pointer univer-overflow-visible
|
||||
univer-whitespace-nowrap univer-rounded-l univer-border univer-border-r-0 univer-border-gray-600
|
||||
univer-bg-primary-600 univer-p-0.5 univer-text-xs univer-font-bold univer-leading-[13px]
|
||||
univer-text-gray-0
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
?
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 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 { Editor } from '@univerjs/docs-ui';
|
||||
import type { IFunctionInfo } from '@univerjs/engine-formula';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
ConfigService,
|
||||
ContextService,
|
||||
IConfigService,
|
||||
IContextService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
} from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { FunctionType, IDescriptionService, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { ISidebarService, RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { FormulaPromptService, IFormulaPromptService } from '../../../../services/prompt.service';
|
||||
import { HelpFunction } from '../HelpFunction';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const sumInfo: IFunctionInfo = {
|
||||
functionName: 'SUM',
|
||||
functionType: FunctionType.Math,
|
||||
description: 'Adds values together.',
|
||||
abstract: 'Adds values.',
|
||||
functionParameter: [
|
||||
{
|
||||
name: 'number1',
|
||||
detail: 'First value.',
|
||||
example: 'A1',
|
||||
require: 1,
|
||||
repeat: 0,
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
detail: 'Additional values.',
|
||||
example: 'B1',
|
||||
require: 0,
|
||||
repeat: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
class HelpDescriptionService {
|
||||
getFunctionInfo(searchText: string) {
|
||||
return searchText.toUpperCase() === sumInfo.functionName ? sumInfo : undefined;
|
||||
}
|
||||
|
||||
hasDefinedNameDescription() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class HelpEditorBridgeService {
|
||||
readonly helpFunctionVisible$ = new BehaviorSubject(true);
|
||||
}
|
||||
|
||||
class HelpEditorService {
|
||||
getEditor(): unknown {
|
||||
return HelpState.editor;
|
||||
}
|
||||
}
|
||||
|
||||
class HelpSidebarService {
|
||||
readonly scrollEvent$ = new Subject<Event>();
|
||||
}
|
||||
|
||||
class HelpUniverInstanceService {
|
||||
readonly unitAdded$ = new Subject<void>();
|
||||
}
|
||||
|
||||
class HelpResizeObserver {
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
unobserve(): void {}
|
||||
}
|
||||
|
||||
class HelpState {
|
||||
static editor: Editor;
|
||||
static switched: number[] = [];
|
||||
static closed = 0;
|
||||
|
||||
static reset(): void {
|
||||
this.switched = [];
|
||||
this.closed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function createEditor(): Editor {
|
||||
const selectionChange$ = new Subject<{ textRanges: Array<{ startOffset: number; endOffset: number; collapsed: boolean }> }>();
|
||||
|
||||
return {
|
||||
selectionChange$,
|
||||
getEditorId: () => 'help-function-editor',
|
||||
getBoundingClientRect: () => ({ left: 10, top: 20, right: 180, bottom: 44 }),
|
||||
getDocumentData: () => ({ documentStyle: { marginTop: 0, marginBottom: 0 } }),
|
||||
getSkeleton: () => ({
|
||||
getSkeletonData: () => ({
|
||||
pages: [{ height: 24 }],
|
||||
}),
|
||||
}),
|
||||
} as unknown as Editor;
|
||||
}
|
||||
|
||||
function createHelpFunctionTestBed() {
|
||||
const injector = new Injector();
|
||||
|
||||
injector.add([IConfigService, { useClass: ConfigService }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([LocaleService, { useClass: LocaleService }]);
|
||||
injector.add([LexerTreeBuilder, { useClass: LexerTreeBuilder }]);
|
||||
injector.add([IFormulaPromptService, { useClass: FormulaPromptService }]);
|
||||
injector.add([IDescriptionService, { useClass: HelpDescriptionService as never }]);
|
||||
injector.add([IEditorBridgeService, { useClass: HelpEditorBridgeService as never }]);
|
||||
injector.add([IEditorService, { useClass: HelpEditorService as never }]);
|
||||
injector.add([ISidebarService, { useClass: HelpSidebarService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: HelpUniverInstanceService as never }]);
|
||||
|
||||
injector.get(LocaleService).load({
|
||||
[LocaleType.ZH_CN]: {
|
||||
'sheets-formula-ui': {
|
||||
prompt: {
|
||||
required: 'Required.',
|
||||
optional: 'Optional.',
|
||||
helpExample: 'Example',
|
||||
helpAbstract: 'Summary',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const editor = createEditor();
|
||||
HelpState.editor = editor;
|
||||
|
||||
return {
|
||||
injector,
|
||||
editor,
|
||||
editorBridgeService: injector.get(IEditorBridgeService) as unknown as HelpEditorBridgeService,
|
||||
};
|
||||
}
|
||||
|
||||
function renderWithInjector(root: Root, injector: Injector, element: React.ReactNode) {
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
{element}
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function showHelpForSum(editor: Editor) {
|
||||
await act(async () => {
|
||||
(editor.selectionChange$ as unknown as Subject<{ textRanges: Array<{ startOffset: number; endOffset: number; collapsed: boolean }> }>).next({
|
||||
textRanges: [{ startOffset: 4, endOffset: 4, collapsed: true }],
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
});
|
||||
}
|
||||
|
||||
function getHelpControls(): HTMLElement[] {
|
||||
return Array.from(document.body.querySelectorAll('div'))
|
||||
.filter((node) => node.className.includes('univer-size-6'));
|
||||
}
|
||||
|
||||
function getHelpControl(index: number): HTMLElement {
|
||||
const control = getHelpControls()[index];
|
||||
if (!control) {
|
||||
throw new TypeError(`Help control ${index} was not rendered`);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
function getTextElement(text: string, classNamePart?: string): HTMLElement {
|
||||
const element = Array.from(document.body.querySelectorAll('div,span'))
|
||||
.filter((node) => node.textContent === text)
|
||||
.filter((node) => !classNamePart || node.className.includes(classNamePart))
|
||||
.at(-1);
|
||||
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
throw new TypeError(`Element with text ${text} was not rendered`);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
describe('HelpFunction', () => {
|
||||
let container: HTMLDivElement;
|
||||
let popupRoot: HTMLDivElement;
|
||||
let root: Root;
|
||||
let resizeObserver: typeof ResizeObserver | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
HelpState.reset();
|
||||
resizeObserver = globalThis.ResizeObserver;
|
||||
globalThis.ResizeObserver = HelpResizeObserver as typeof ResizeObserver;
|
||||
container = document.createElement('div');
|
||||
popupRoot = document.createElement('div');
|
||||
popupRoot.id = 'univer-popup-portal';
|
||||
document.body.appendChild(container);
|
||||
document.body.appendChild(popupRoot);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
popupRoot.remove();
|
||||
if (resizeObserver) {
|
||||
globalThis.ResizeObserver = resizeObserver;
|
||||
} else {
|
||||
delete (globalThis as { ResizeObserver?: unknown }).ResizeObserver;
|
||||
}
|
||||
});
|
||||
|
||||
it('expands the active function help and reports parameter switches', async () => {
|
||||
const { injector, editor } = createHelpFunctionTestBed();
|
||||
|
||||
renderWithInjector(
|
||||
root,
|
||||
injector, (
|
||||
<HelpFunction
|
||||
isFocus
|
||||
editor={editor}
|
||||
formulaText="=SUM("
|
||||
onParamsSwitch={(index) => HelpState.switched.push(index)}
|
||||
/>
|
||||
));
|
||||
|
||||
await showHelpForSum(editor);
|
||||
|
||||
expect(document.body.textContent).toContain('SUM(number1,[number2,...])');
|
||||
|
||||
await act(async () => {
|
||||
getHelpControl(0).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain('SUM(A1,B1)');
|
||||
expect(document.body.textContent).toContain('Adds values together.');
|
||||
expect(document.body.textContent).toContain('Required. First value.');
|
||||
expect(document.body.textContent).toContain('Optional. Additional values.');
|
||||
|
||||
await act(async () => {
|
||||
getTextElement('[number2,...]').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(HelpState.switched).toEqual([1]);
|
||||
});
|
||||
|
||||
it('hides the help card after close and restores it from the compact tip', async () => {
|
||||
const { editor, editorBridgeService, injector } = createHelpFunctionTestBed();
|
||||
|
||||
renderWithInjector(
|
||||
root,
|
||||
injector, (
|
||||
<HelpFunction
|
||||
isFocus
|
||||
editor={editor}
|
||||
formulaText="=SUM("
|
||||
onClose={() => {
|
||||
HelpState.closed += 1;
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
await showHelpForSum(editor);
|
||||
|
||||
expect(document.body.textContent).toContain('SUM(number1,[number2,...])');
|
||||
|
||||
await act(async () => {
|
||||
getHelpControl(1).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(HelpState.closed).toBe(1);
|
||||
expect(editorBridgeService.helpFunctionVisible$.getValue()).toBe(false);
|
||||
expect(document.body.textContent).not.toContain('SUM(number1,[number2,...])');
|
||||
expect(document.body.textContent).toContain('?');
|
||||
|
||||
await act(async () => {
|
||||
getTextElement('?', 'univer-cursor-pointer').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(editorBridgeService.helpFunctionVisible$.getValue()).toBe(true);
|
||||
expect(document.body.textContent).toContain('SUM(number1,[number2,...])');
|
||||
});
|
||||
|
||||
it('keeps the prior hidden preference until the compact tip restores help', async () => {
|
||||
const { editor, editorBridgeService, injector } = createHelpFunctionTestBed();
|
||||
|
||||
editorBridgeService.helpFunctionVisible$.next(false);
|
||||
|
||||
renderWithInjector(
|
||||
root,
|
||||
injector, (
|
||||
<HelpFunction
|
||||
isFocus
|
||||
editor={editor}
|
||||
formulaText="=SUM("
|
||||
/>
|
||||
));
|
||||
|
||||
await showHelpForSum(editor);
|
||||
|
||||
expect(editorBridgeService.helpFunctionVisible$.getValue()).toBe(false);
|
||||
expect(document.body.textContent).not.toContain('SUM(number1,[number2,...])');
|
||||
expect(document.body.textContent).toContain('?');
|
||||
|
||||
await act(async () => {
|
||||
getTextElement('?', 'univer-cursor-pointer').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(editorBridgeService.helpFunctionVisible$.getValue()).toBe(true);
|
||||
expect(document.body.textContent).toContain('SUM(number1,[number2,...])');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,867 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, toDisposable } from '@univerjs/core';
|
||||
import {
|
||||
buildFormulaTextRuns,
|
||||
getFormulaHighlightDataStream,
|
||||
getFormulaSequenceCharacterAtOffset,
|
||||
isFormulaReferenceAddingContext,
|
||||
isFormulaReferenceAddingTextContext,
|
||||
sequenceNodeType,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE,
|
||||
FORMULA_EMBED_RUNTIME_FOCUS_ROLE_ATTRIBUTE,
|
||||
isEventTargetInSameFormulaEmbedInteractionBoundary,
|
||||
registerFormulaEditorRuntimePortal,
|
||||
} from '../../formula-embed-integration.service';
|
||||
import {
|
||||
focusFormulaEditor,
|
||||
hasActiveFormulaEmbedInteraction,
|
||||
shouldRefocusFormulaEditorOnMouseUp,
|
||||
shouldSkipFormulaEditorMouseUpFocus,
|
||||
} from '../use-focus';
|
||||
import {
|
||||
FormulaSelectingType,
|
||||
resolveFormulaSelectingIntent,
|
||||
resolveFormulaSelectionCursorIndex,
|
||||
resolveFormulaSelectionDataStream,
|
||||
resolveFormulaSelectionWorkbook,
|
||||
shouldAddFormulaReference,
|
||||
shouldSkipReferenceEditingByPointer,
|
||||
} from '../use-formula-selection';
|
||||
import { calcHighlightRanges, createFormulaHighlightBody, createHighlightTextRuns } from '../use-highlight';
|
||||
import {
|
||||
isFormulaEditorInteractionOwner,
|
||||
shouldMoveFormulaSelectionFromCurrentSelection,
|
||||
} from '../use-left-and-right-arrow';
|
||||
import {
|
||||
createSelectionChangeDuplicateEndGuard,
|
||||
createSelectionChangeHandler,
|
||||
getInitialFormulaReferenceSelectionCount,
|
||||
getLastFormulaSelection,
|
||||
getSelectionsForFormulaRefUpdate,
|
||||
getSharedSelectionChangeDuplicateEndGuard,
|
||||
insertFormulaReferenceText,
|
||||
isSameFormulaSelection,
|
||||
prepareSelectionChangeContext,
|
||||
replaceFormulaControlSelection,
|
||||
shouldSkipFormulaReferenceUpdate,
|
||||
} from '../use-sheet-selection-change';
|
||||
|
||||
function range(row: number, col: number, sheetId = 'sheet1', unitId = 'unit1') {
|
||||
return {
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startColumn: col,
|
||||
endColumn: col,
|
||||
sheetId,
|
||||
unitId,
|
||||
};
|
||||
}
|
||||
|
||||
describe('formula selection update helpers', () => {
|
||||
it('does not rewrite editor selections while doc pointer selection is in progress', () => {
|
||||
const editorService = {
|
||||
focus: vi.fn(),
|
||||
};
|
||||
const editor = {
|
||||
getEditorId: vi.fn(() => 'editor-1'),
|
||||
getSelectionRanges: vi.fn(() => [{ startOffset: 1, endOffset: 1, collapsed: true }]),
|
||||
setSelectionRanges: vi.fn(),
|
||||
getDocumentData: vi.fn(() => ({ body: { dataStream: 'abc\r\n' } })),
|
||||
docSelectionRenderService: {
|
||||
isOnPointerEvent: true,
|
||||
},
|
||||
};
|
||||
|
||||
focusFormulaEditor(editorService as never, editor as never);
|
||||
|
||||
expect(editorService.focus).toHaveBeenCalledWith('editor-1');
|
||||
expect(editor.setSelectionRanges).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps formula editor mouse-up refocus available when the editor canvas handled the pointer interaction', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.dataset.uComp = 'render-canvas';
|
||||
const button = document.createElement('button');
|
||||
|
||||
expect(shouldSkipFormulaEditorMouseUpFocus(canvas)).toBe(false);
|
||||
expect(shouldSkipFormulaEditorMouseUpFocus(button)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not refocus the formula editor on mouse-up when the editor is already focused', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.dataset.uComp = 'render-canvas';
|
||||
|
||||
expect(shouldRefocusFormulaEditorOnMouseUp({
|
||||
target: canvas,
|
||||
isFocusing: true,
|
||||
isPointerSelecting: false,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('does not refocus the formula editor while pointer text selection is active', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.dataset.uComp = 'render-canvas';
|
||||
|
||||
expect(shouldRefocusFormulaEditorOnMouseUp({
|
||||
target: canvas,
|
||||
isFocusing: false,
|
||||
isPointerSelecting: true,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('refocuses the formula editor on mouse-up only when focus was lost and no pointer selection is active', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.dataset.uComp = 'render-canvas';
|
||||
|
||||
expect(shouldRefocusFormulaEditorOnMouseUp({
|
||||
target: canvas,
|
||||
isFocusing: false,
|
||||
isPointerSelecting: false,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('treats sheet range selection inside the same embed owner as an active formula interaction', () => {
|
||||
const block = document.createElement('div');
|
||||
const editorHost = document.createElement('div');
|
||||
const sheetCanvas = document.createElement('canvas');
|
||||
block.setAttribute('data-embed-interaction-boundary-owner', 'embed-1');
|
||||
block.append(editorHost, sheetCanvas);
|
||||
document.body.appendChild(block);
|
||||
|
||||
sheetCanvas.tabIndex = -1;
|
||||
sheetCanvas.focus();
|
||||
|
||||
expect(isEventTargetInSameFormulaEmbedInteractionBoundary(editorHost, sheetCanvas)).toBe(true);
|
||||
expect(hasActiveFormulaEmbedInteraction(editorHost)).toBe(true);
|
||||
|
||||
block.remove();
|
||||
});
|
||||
|
||||
it('registers formula editor portal elements as embedded child editors', () => {
|
||||
const editorId = DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY;
|
||||
const portalRoot = document.createElement('div');
|
||||
const editorElement = document.createElement('div');
|
||||
portalRoot.id = `univer-doc-selection-container-${editorId}`;
|
||||
editorElement.id = `__editor_${editorId}`;
|
||||
portalRoot.appendChild(editorElement);
|
||||
document.body.appendChild(portalRoot);
|
||||
|
||||
const registeredFocusElements = new Set<Element>();
|
||||
const registeredBoundaryElements = new Set<Element>();
|
||||
const focusCoordinator = {
|
||||
resolveRuntimeScopeByChildUnitId: () => undefined,
|
||||
acquireLease: () => toDisposable(() => {}),
|
||||
registerElement: ({ element, role }: { element: HTMLElement; role: string }) => {
|
||||
registeredFocusElements.add(element);
|
||||
element.setAttribute(FORMULA_EMBED_RUNTIME_FOCUS_ROLE_ATTRIBUTE, role);
|
||||
return toDisposable(() => {
|
||||
registeredFocusElements.delete(element);
|
||||
element.removeAttribute(FORMULA_EMBED_RUNTIME_FOCUS_ROLE_ATTRIBUTE);
|
||||
});
|
||||
},
|
||||
};
|
||||
const interactionBoundaryService = {
|
||||
registerOwnedElement: (embedId: string, element: Element) => {
|
||||
registeredBoundaryElements.add(element);
|
||||
element.setAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE, embedId);
|
||||
return toDisposable(() => {
|
||||
registeredBoundaryElements.delete(element);
|
||||
element.removeAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE);
|
||||
});
|
||||
},
|
||||
};
|
||||
const disposable = registerFormulaEditorRuntimePortal({
|
||||
embedId: 'embed-1',
|
||||
editorId,
|
||||
interactionBoundaryService,
|
||||
focusCoordinator,
|
||||
});
|
||||
|
||||
expect(portalRoot.getAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE)).toBe('embed-1');
|
||||
expect(editorElement.getAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE)).toBe('embed-1');
|
||||
expect(portalRoot.getAttribute(FORMULA_EMBED_RUNTIME_FOCUS_ROLE_ATTRIBUTE)).toBe('child-editor');
|
||||
expect(editorElement.getAttribute(FORMULA_EMBED_RUNTIME_FOCUS_ROLE_ATTRIBUTE)).toBe('child-editor');
|
||||
expect(registeredBoundaryElements.has(editorElement)).toBe(true);
|
||||
expect(registeredFocusElements.has(editorElement)).toBe(true);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(portalRoot.hasAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE)).toBe(false);
|
||||
expect(editorElement.hasAttribute(FORMULA_EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE)).toBe(false);
|
||||
portalRoot.remove();
|
||||
});
|
||||
|
||||
it('only skips reference editing when pointer-origin editing is still disabled and click editing is allowed', () => {
|
||||
expect(shouldSkipReferenceEditingByPointer(true)).toBe(true);
|
||||
expect(shouldSkipReferenceEditingByPointer(true, true)).toBe(false);
|
||||
expect(shouldSkipReferenceEditingByPointer(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('falls back to the formula editor workbook when the focused current workbook is unavailable', () => {
|
||||
const fallbackWorkbook = { unitId: 'embedded-sheet' };
|
||||
|
||||
expect(resolveFormulaSelectionWorkbook(undefined, fallbackWorkbook)).toBe(fallbackWorkbook);
|
||||
expect(resolveFormulaSelectionWorkbook(null, fallbackWorkbook)).toBe(fallbackWorkbook);
|
||||
expect(resolveFormulaSelectionWorkbook({ unitId: 'current-sheet' }, fallbackWorkbook)).toEqual({ unitId: 'current-sheet' });
|
||||
});
|
||||
|
||||
it('treats the hidden normal editor as the fx bar owner while the fx bar owns formula selection', () => {
|
||||
expect(isFormulaEditorInteractionOwner(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, {
|
||||
fxBarFocused: true,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('lets the fx bar own formula selection when the canvas leaves no focused editor', () => {
|
||||
expect(isFormulaEditorInteractionOwner(null, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, {
|
||||
fxBarFocused: true,
|
||||
allowMissingFocus: true,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('does not let the hidden normal editor own the fx bar outside an fx formula selection session', () => {
|
||||
expect(isFormulaEditorInteractionOwner(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, {
|
||||
fxBarFocused: false,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('reads formula selection text from the formula editor instead of the current host document', () => {
|
||||
const accessor = {
|
||||
get: vi.fn(() => ({
|
||||
getCurrentUniverDocInstance: vi.fn(() => ({
|
||||
getBody: () => ({ dataStream: 'host document text\r\n' }),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
const editor = {
|
||||
getDocumentDataModel: vi.fn(() => ({
|
||||
getBody: () => ({ dataStream: '=SUM(\r\n' }),
|
||||
})),
|
||||
};
|
||||
|
||||
expect(resolveFormulaSelectionDataStream(accessor as never, editor as never)).toEqual({
|
||||
dataStream: '=SUM(\r\n',
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the editor unit when the formula editor document model has been disposed', () => {
|
||||
const accessor = {
|
||||
get: vi.fn(() => ({
|
||||
getUnit: vi.fn((unitId: string) => unitId === 'formula-editor'
|
||||
? { getBody: () => ({ dataStream: '=A1\r\n' }) }
|
||||
: undefined),
|
||||
getCurrentUnitOfType: vi.fn(() => ({
|
||||
getBody: () => ({ dataStream: 'host document text\r\n' }),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
const editor = {
|
||||
getDocumentDataModel: vi.fn(() => null),
|
||||
};
|
||||
|
||||
expect(resolveFormulaSelectionDataStream(accessor as never, editor as never, 'formula-editor')).toEqual({
|
||||
dataStream: '=A1\r\n',
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('reads formula selection text from the editor unit before falling back to the current host document', () => {
|
||||
const accessor = {
|
||||
get: vi.fn(() => ({
|
||||
getUnit: vi.fn((unitId: string) => unitId === 'formula-editor'
|
||||
? { getBody: () => ({ dataStream: '=A1\r\n' }) }
|
||||
: undefined),
|
||||
getCurrentUniverDocInstance: vi.fn(() => ({
|
||||
getBody: () => ({ dataStream: 'host document text\r\n' }),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
expect(resolveFormulaSelectionDataStream(accessor as never, undefined, 'formula-editor')).toEqual({
|
||||
dataStream: '=A1\r\n',
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the end of a fresh formula when the editor selection offset is still stale', () => {
|
||||
expect(resolveFormulaSelectionCursorIndex({ collapsed: true, startOffset: 0 }, '=')).toBe(1);
|
||||
expect(resolveFormulaSelectionCursorIndex({ collapsed: true, startOffset: 0 }, '=SUM(')).toBe(5);
|
||||
expect(resolveFormulaSelectionCursorIndex({ collapsed: true, startOffset: 0 }, 'plain')).toBe(0);
|
||||
expect(resolveFormulaSelectionCursorIndex({ collapsed: true, startOffset: 2 }, '=A1')).toBe(2);
|
||||
});
|
||||
|
||||
it('prefers adding a new formula reference when the cursor is after a delimiter', () => {
|
||||
expect(resolveFormulaSelectingIntent(true, true)).toBe(FormulaSelectingType.NEED_ADD);
|
||||
expect(resolveFormulaSelectingIntent(true, false)).toBe(FormulaSelectingType.NEED_ADD);
|
||||
expect(resolveFormulaSelectingIntent(false, true)).toBe(FormulaSelectingType.CAN_EDIT);
|
||||
expect(resolveFormulaSelectingIntent(false, false)).toBe(FormulaSelectingType.NOT_SELECT);
|
||||
});
|
||||
|
||||
it('only arms reference picking after formula delimiters and operators', () => {
|
||||
expect(shouldAddFormulaReference('=', 1)).toBe(true);
|
||||
expect(shouldAddFormulaReference('=SUM(', 5)).toBe(true);
|
||||
expect(shouldAddFormulaReference('=SUM(A1,', 8)).toBe(true);
|
||||
expect(shouldAddFormulaReference('=A1+', 4)).toBe(true);
|
||||
expect(shouldAddFormulaReference('=Sheet1!', 8)).toBe(true);
|
||||
expect(shouldAddFormulaReference('=SUM', 4)).toBe(false);
|
||||
expect(shouldAddFormulaReference('=A1', 3)).toBe(false);
|
||||
});
|
||||
|
||||
it('reorders the active selection into the formula reference being edited and keeps ctrl-added ranges separate', () => {
|
||||
const selections = [range(0, 0), range(1, 1), range(2, 2)];
|
||||
|
||||
expect(getSelectionsForFormulaRefUpdate(selections, 0)).toEqual({
|
||||
orderedSelections: [range(2, 2), range(0, 0), range(1, 1)],
|
||||
});
|
||||
expect(getSelectionsForFormulaRefUpdate(selections, 1, true)).toEqual({
|
||||
orderedSelections: [range(0, 0), range(1, 1)],
|
||||
insertedSelection: range(2, 2),
|
||||
});
|
||||
expect(getSelectionsForFormulaRefUpdate(selections, -1)).toEqual({
|
||||
orderedSelections: selections,
|
||||
});
|
||||
});
|
||||
|
||||
it('previews ctrl-add selection updates before move end and ignores initial selection events', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 1,
|
||||
onSelectionsChange,
|
||||
});
|
||||
|
||||
handler([range(0, 0)], true, { initial: true });
|
||||
handler([range(0, 0), range(1, 1)], false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([range(0, 0), range(1, 1)], false, true);
|
||||
|
||||
handler([range(0, 0), range(1, 1)], true);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
handler([range(3, 3)], false);
|
||||
expect(onSelectionsChange).toHaveBeenLastCalledWith([range(3, 3)], false, false);
|
||||
});
|
||||
|
||||
it('ignores replayed initial formula references when no initial reference selection exists', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
onSelectionsChange,
|
||||
});
|
||||
|
||||
handler([range(7, 5)], true, { initial: true });
|
||||
|
||||
expect(onSelectionsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the formula text end as the insertion context when the embedded editor selection is temporarily empty', () => {
|
||||
const lexerTreeBuilder = {
|
||||
sequenceNodesBuilder: vi.fn(() => []),
|
||||
};
|
||||
const editor = {
|
||||
getSelectionRanges: vi.fn(() => []),
|
||||
getDocumentData: vi.fn(() => ({ body: { dataStream: '=\r\n' } })),
|
||||
};
|
||||
|
||||
expect(prepareSelectionChangeContext({
|
||||
editor: editor as never,
|
||||
lexerTreeBuilder: lexerTreeBuilder as never,
|
||||
})).toMatchObject({
|
||||
offset: 0,
|
||||
nodeIndex: -1,
|
||||
updatingRefIndex: -1,
|
||||
sequenceNodes: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not create a fallback formula context for non-formula editor text', () => {
|
||||
const lexerTreeBuilder = {
|
||||
sequenceNodesBuilder: vi.fn(() => []),
|
||||
};
|
||||
const editor = {
|
||||
getSelectionRanges: vi.fn(() => []),
|
||||
getDocumentData: vi.fn(() => ({ body: { dataStream: 'plain\r\n' } })),
|
||||
};
|
||||
|
||||
expect(prepareSelectionChangeContext({
|
||||
editor: editor as never,
|
||||
lexerTreeBuilder: lexerTreeBuilder as never,
|
||||
})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('starts the first keyboard-added formula reference from the edited cell selection', () => {
|
||||
expect(shouldMoveFormulaSelectionFromCurrentSelection(FormulaSelectingType.NEED_ADD, 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('routes formula editor interactions only to the focused formula editor', () => {
|
||||
expect(isFormulaEditorInteractionOwner('__INTERNAL_EDITOR__DOCS_NORMAL', '__INTERNAL_EDITOR__DOCS_NORMAL')).toBe(true);
|
||||
expect(isFormulaEditorInteractionOwner('__INTERNAL_EDITOR__DOCS_FORMULA_BAR', '__INTERNAL_EDITOR__DOCS_NORMAL')).toBe(false);
|
||||
expect(isFormulaEditorInteractionOwner(null, '__INTERNAL_EDITOR__DOCS_NORMAL')).toBe(false);
|
||||
});
|
||||
|
||||
it('continues keyboard-added formula references from the last reference selection after a delimiter', () => {
|
||||
expect(shouldMoveFormulaSelectionFromCurrentSelection(FormulaSelectingType.NEED_ADD, 1)).toBe(false);
|
||||
expect(shouldMoveFormulaSelectionFromCurrentSelection(FormulaSelectingType.NEED_ADD, 2)).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps cross-sheet reference editing anchored to the current sheet selection', () => {
|
||||
expect(shouldMoveFormulaSelectionFromCurrentSelection(FormulaSelectingType.EDIT_OTHER_SHEET_REFERENCE, 1)).toBe(true);
|
||||
expect(shouldMoveFormulaSelectionFromCurrentSelection(FormulaSelectingType.CAN_EDIT, 1)).toBe(false);
|
||||
});
|
||||
|
||||
it('recognizes delimiter-adjacent formula context as a new reference insertion point', () => {
|
||||
const nodes = [
|
||||
{ token: 'M28', nodeType: sequenceNodeType.REFERENCE },
|
||||
',',
|
||||
];
|
||||
|
||||
expect(getFormulaSequenceCharacterAtOffset(nodes, 3)).toBe('8');
|
||||
expect(getFormulaSequenceCharacterAtOffset(nodes, 4)).toBe(',');
|
||||
expect(isFormulaReferenceAddingContext(nodes, 3)).toBe(false);
|
||||
expect(isFormulaReferenceAddingContext(nodes, 4)).toBe(true);
|
||||
expect(isFormulaReferenceAddingTextContext('M28,', 4)).toBe(true);
|
||||
expect(insertFormulaReferenceText('M28,', 'M27', 4)).toBe('M28,M27');
|
||||
});
|
||||
|
||||
it('skips stale non-add formula selection updates when no rendered reference exists', () => {
|
||||
expect(shouldSkipFormulaReferenceUpdate(false, 0)).toBe(true);
|
||||
expect(shouldSkipFormulaReferenceUpdate(false, 1)).toBe(false);
|
||||
expect(shouldSkipFormulaReferenceUpdate(true, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it('applies a click-created formula reference from selection start before pointer-up controls are reset', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
onSelectionsChange,
|
||||
});
|
||||
|
||||
handler([range(7, 5)], false);
|
||||
handler([], true);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([range(7, 5)], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not reapply the same click-created formula reference on selection end', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
onSelectionsChange,
|
||||
});
|
||||
|
||||
handler([range(7, 5)], false);
|
||||
handler([range(7, 5)], true);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([range(7, 5)], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('commits duplicate selection end without reapplying formula text changes', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const onDuplicateEnd = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
onSelectionsChange,
|
||||
onDuplicateEnd,
|
||||
});
|
||||
|
||||
handler([range(7, 5)], false);
|
||||
handler([range(7, 5)], true);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([range(7, 5)], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
expect(onDuplicateEnd).toHaveBeenCalledWith([range(7, 5)]);
|
||||
expect(onDuplicateEnd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not reapply the same click-created formula reference while selection is still moving', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
onSelectionsChange,
|
||||
});
|
||||
|
||||
handler([range(7, 5)], false);
|
||||
handler([range(7, 5)], false);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([range(7, 5)], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('previews a ctrl-added formula reference before selection end', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 1,
|
||||
onSelectionsChange,
|
||||
});
|
||||
const existingRange = range(7, 5);
|
||||
const addedRange = range(8, 6);
|
||||
|
||||
handler([existingRange, addedRange], false);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([existingRange, addedRange], false, true);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('updates a pending ctrl-added reference as the active reference while dragging', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 1,
|
||||
onSelectionsChange,
|
||||
});
|
||||
const existingRange = range(7, 5);
|
||||
const firstAddedRange = range(8, 6);
|
||||
const movedAddedRange = { ...range(8, 6), endRow: 10, endColumn: 8 };
|
||||
|
||||
handler([existingRange, firstAddedRange], false);
|
||||
handler([existingRange, movedAddedRange], false);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenNthCalledWith(1, [existingRange, firstAddedRange], false, true);
|
||||
expect(onSelectionsChange).toHaveBeenNthCalledWith(2, [existingRange, movedAddedRange], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not append duplicate ctrl-added previews for the same range', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 1,
|
||||
onSelectionsChange,
|
||||
});
|
||||
const existingRange = range(7, 5);
|
||||
const addedRange = range(8, 6);
|
||||
|
||||
handler([existingRange, addedRange], false);
|
||||
handler([existingRange, addedRange], false);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([existingRange, addedRange], false, true);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps ordinary same-count dragging in replace mode', () => {
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 2,
|
||||
onSelectionsChange,
|
||||
});
|
||||
const existingRange = range(7, 5);
|
||||
const movedRange = { ...range(8, 6), endRow: 10, endColumn: 8 };
|
||||
|
||||
handler([existingRange, movedRange], false);
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([existingRange, movedRange], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('dedupes the same formula reference when selection end is reported by another source', () => {
|
||||
const duplicateEndGuard = createSelectionChangeDuplicateEndGuard();
|
||||
const onSelectionsChange = vi.fn();
|
||||
const handler = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
duplicateEndGuard,
|
||||
onSelectionsChange,
|
||||
});
|
||||
const selectedRange = range(7, 5);
|
||||
const renderedRange = { ...selectedRange, startX: 10, endX: 20, startY: 30, endY: 40 };
|
||||
|
||||
handler([renderedRange], false);
|
||||
if (!duplicateEndGuard.shouldSkip([selectedRange], true)) {
|
||||
onSelectionsChange([selectedRange], true);
|
||||
}
|
||||
|
||||
expect(onSelectionsChange).toHaveBeenCalledWith([renderedRange], false, false);
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('matches formula selections by range identity instead of render coordinates', () => {
|
||||
expect(isSameFormulaSelection(
|
||||
{ ...range(7, 5), startX: 10, endX: 20 },
|
||||
range(7, 5, 'other-sheet', 'other-unit')
|
||||
)).toBe(true);
|
||||
expect(isSameFormulaSelection(
|
||||
{ ...range(7, 5), startX: 10, endX: 20 },
|
||||
range(7, 6)
|
||||
)).toBe(false);
|
||||
});
|
||||
|
||||
it('shares duplicate formula selection guards across paired formula editors', () => {
|
||||
const guardFromCellEditor = getSharedSelectionChangeDuplicateEndGuard('unit1:sheet1:test');
|
||||
const guardFromFormulaBar = getSharedSelectionChangeDuplicateEndGuard('unit1:sheet1:test');
|
||||
|
||||
expect(guardFromCellEditor.shouldSkip([range(7, 5)], false)).toBe(false);
|
||||
expect(guardFromFormulaBar.shouldSkip([range(7, 5)], false)).toBe(true);
|
||||
|
||||
guardFromCellEditor.reset();
|
||||
});
|
||||
|
||||
it('counts only rendered formula reference controls and parsed formula references as initial references', () => {
|
||||
expect(getInitialFormulaReferenceSelectionCount(0, 0)).toBe(0);
|
||||
expect(getInitialFormulaReferenceSelectionCount(0, 2)).toBe(2);
|
||||
expect(getInitialFormulaReferenceSelectionCount(1, 0)).toBe(1);
|
||||
expect(getInitialFormulaReferenceSelectionCount(1, 0, FormulaSelectingType.NEED_ADD)).toBe(1);
|
||||
expect(getInitialFormulaReferenceSelectionCount(1, 1, FormulaSelectingType.NEED_ADD)).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps formula control ranges scoped to the current selection unit and sheet', () => {
|
||||
const selections = [range(0, 0, 'sheet1', 'unit1')];
|
||||
|
||||
expect(replaceFormulaControlSelection(selections, 0, range(2, 2, 'other-sheet', 'other-unit'))).toEqual([
|
||||
range(2, 2, 'sheet1', 'unit1'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores stale formula control events when the matching selection data is unavailable', () => {
|
||||
expect(replaceFormulaControlSelection([], 0, range(2, 2))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns no formula selection for empty selection events', () => {
|
||||
expect(getLastFormulaSelection([])).toBeUndefined();
|
||||
expect(getLastFormulaSelection([range(1, 1), range(2, 2)])).toEqual(range(2, 2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('formula highlight helpers', () => {
|
||||
it('does not cover the first highlighted character when there is no leading marker', () => {
|
||||
const textRuns = [{ st: 0, ed: 12, ts: { cl: { rgb: '#ff0000' } } }];
|
||||
|
||||
expect(createHighlightTextRuns(textRuns, 0)).toEqual(textRuns);
|
||||
});
|
||||
|
||||
it('does not copy stale paragraph metadata into a formula text replacement', () => {
|
||||
const body = createFormulaHighlightBody('=F40', [
|
||||
{ st: 0, ed: 1, ts: { fs: 11 } },
|
||||
{ st: 1, ed: 4, ts: { fs: 11 } },
|
||||
]);
|
||||
|
||||
expect(body).toEqual({
|
||||
dataStream: '=F40',
|
||||
textRuns: [
|
||||
{ st: 0, ed: 1, ts: { fs: 11 } },
|
||||
{ st: 1, ed: 4, ts: { fs: 11 } },
|
||||
],
|
||||
});
|
||||
expect(body.paragraphs).toBeUndefined();
|
||||
expect(body.sectionBreaks).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves incomplete formula editor text while applying token highlights', () => {
|
||||
expect(getFormulaHighlightDataStream('=', [
|
||||
'SUM(',
|
||||
{ token: 'D37', nodeType: sequenceNodeType.REFERENCE, startIndex: 4, endIndex: 6 },
|
||||
',',
|
||||
{ token: 'F40', nodeType: sequenceNodeType.REFERENCE, startIndex: 8, endIndex: 10 },
|
||||
], 'SUM(D37,F40,J36,J42')).toBe('=SUM(D37,F40,J36,J42\r\n');
|
||||
});
|
||||
|
||||
it('builds colored text runs for references, numbers, strings, arrays, defined names, and plain text', () => {
|
||||
const result = buildFormulaTextRuns(
|
||||
{ hasDefinedNameDescription: vi.fn((token: string) => token === 'SalesTotal') } as any,
|
||||
{
|
||||
formulaRefColors: ['#ff0000', '#00ff00'],
|
||||
numberColor: '#0000ff',
|
||||
stringColor: '#ff00ff',
|
||||
plainTextColor: '#111111',
|
||||
},
|
||||
[
|
||||
'SUM(',
|
||||
{ token: 'A1', nodeType: sequenceNodeType.REFERENCE, startIndex: 4, endIndex: 5 },
|
||||
',',
|
||||
{ token: '42', nodeType: sequenceNodeType.NUMBER, startIndex: 7, endIndex: 8 },
|
||||
{ token: '"ok"', nodeType: sequenceNodeType.STRING, startIndex: 9, endIndex: 12 },
|
||||
{ token: '{1,2}', nodeType: sequenceNodeType.ARRAY, startIndex: 13, endIndex: 17 },
|
||||
{ token: 'SalesTotal', nodeType: sequenceNodeType.DEFINED_NAME, startIndex: 18, endIndex: 27 },
|
||||
{ token: '+', nodeType: sequenceNodeType.NORMAL, startIndex: 28, endIndex: 28 },
|
||||
{ token: 'A1', nodeType: sequenceNodeType.REFERENCE, startIndex: 29, endIndex: 30 },
|
||||
]
|
||||
);
|
||||
|
||||
expect(result.refSelections).toEqual([
|
||||
expect.objectContaining({ token: 'A1', themeColor: '#ff0000', refIndex: 1, index: 0 }),
|
||||
expect.objectContaining({ token: 'A1', themeColor: '#ff0000', refIndex: 8, index: 1 }),
|
||||
]);
|
||||
expect(result.textRuns.map((run) => run.ts?.cl?.rgb)).toEqual([
|
||||
'#111111',
|
||||
'#ff0000',
|
||||
'#111111',
|
||||
'#0000ff',
|
||||
'#ff00ff',
|
||||
'#ff00ff',
|
||||
'#111111',
|
||||
'#111111',
|
||||
'#ff0000',
|
||||
]);
|
||||
});
|
||||
|
||||
it('calculates visible formula reference selections and activates the reference under the editor cursor', () => {
|
||||
const currentSelections = [{
|
||||
range: range(0, 0),
|
||||
primary: { actualRow: 9, actualColumn: 9 },
|
||||
}];
|
||||
const refSelectionsService = {
|
||||
getCurrentSelections: vi.fn(() => currentSelections),
|
||||
setSelections: vi.fn(),
|
||||
};
|
||||
const refSelectionsRenderService = {
|
||||
setActiveSelectionIndex: vi.fn(),
|
||||
resetActiveSelectionIndex: vi.fn(),
|
||||
};
|
||||
const workbook = {
|
||||
getUnitId: vi.fn(() => 'unit1'),
|
||||
getActiveSheet: vi.fn(() => ({
|
||||
getSheetId: () => 'sheet1',
|
||||
getName: () => 'Sheet1',
|
||||
getRowCount: () => 100,
|
||||
getColumnCount: () => 50,
|
||||
})),
|
||||
getSheetBySheetName: vi.fn((name: string) => name === 'Sheet2'
|
||||
? { getSheetId: () => 'sheet2' }
|
||||
: { getSheetId: () => 'sheet1' }),
|
||||
};
|
||||
const univerInstanceService = {
|
||||
getUnit: vi.fn(() => workbook),
|
||||
};
|
||||
const result = calcHighlightRanges({
|
||||
unitId: 'unit1',
|
||||
subUnitId: 'sheet1',
|
||||
currentWorkbook: workbook as any,
|
||||
refSelections: [
|
||||
{ token: 'A1', themeColor: '#ff0000', refIndex: 0, startIndex: 0, endIndex: 1, index: 0 },
|
||||
{ token: 'Sheet2!A1', themeColor: '#00ff00', refIndex: 1, startIndex: 3, endIndex: 11, index: 1 },
|
||||
{ token: 'Book2#Sheet1!A1', themeColor: '#0000ff', refIndex: 2, startIndex: 12, endIndex: 26, index: 2 },
|
||||
],
|
||||
editor: {
|
||||
getSelectionRanges: vi.fn(() => [{ startOffset: 3 }]),
|
||||
} as any,
|
||||
refSelectionsService: refSelectionsService as any,
|
||||
refSelectionsRenderService: refSelectionsRenderService as any,
|
||||
sheetSkeletonManagerService: {
|
||||
getSkeleton: vi.fn(() => ({})),
|
||||
} as any,
|
||||
themeService: { getColorFromTheme: vi.fn((key: string) => key === 'gray.0' ? '#fff' : key) } as any,
|
||||
univerInstanceService: univerInstanceService as any,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result?.[0].range).toMatchObject({ startRow: 0, endRow: 0, startColumn: 0, endColumn: 0, unitId: 'unit1', sheetId: 'sheet1' });
|
||||
expect(result?.[0].primary).toBe(currentSelections[0].primary);
|
||||
expect(result?.[0].style).toMatchObject({ stroke: '#ff0000', widgetStroke: '#fff' });
|
||||
expect(refSelectionsRenderService.setActiveSelectionIndex).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('does not assign the active keyboard selection primary to an earlier formula reference', () => {
|
||||
const activeKeyboardSelection = {
|
||||
range: range(7, 2),
|
||||
primary: { actualRow: 7, actualColumn: 2 },
|
||||
};
|
||||
const workbook = {
|
||||
getUnitId: vi.fn(() => 'unit1'),
|
||||
getActiveSheet: vi.fn(() => ({
|
||||
getSheetId: () => 'sheet1',
|
||||
getName: () => 'Sheet1',
|
||||
getRowCount: () => 100,
|
||||
getColumnCount: () => 50,
|
||||
})),
|
||||
getSheetBySheetName: vi.fn(() => ({ getSheetId: () => 'sheet1' })),
|
||||
};
|
||||
|
||||
const result = calcHighlightRanges({
|
||||
unitId: 'unit1',
|
||||
subUnitId: 'sheet1',
|
||||
currentWorkbook: workbook as any,
|
||||
refSelections: [
|
||||
{ token: 'C10', themeColor: '#ff0000', refIndex: 0, startIndex: 0, endIndex: 2, index: 0 },
|
||||
{ token: 'C8', themeColor: '#00ff00', refIndex: 1, startIndex: 4, endIndex: 5, index: 1 },
|
||||
],
|
||||
editor: undefined,
|
||||
refSelectionsService: {
|
||||
getCurrentSelections: vi.fn(() => [activeKeyboardSelection]),
|
||||
setSelections: vi.fn(),
|
||||
} as any,
|
||||
refSelectionsRenderService: undefined,
|
||||
sheetSkeletonManagerService: {
|
||||
getSkeleton: vi.fn(() => ({})),
|
||||
} as any,
|
||||
themeService: { getColorFromTheme: vi.fn((key: string) => key === 'gray.0' ? '#fff' : key) } as any,
|
||||
univerInstanceService: { getUnit: vi.fn(() => workbook) } as any,
|
||||
});
|
||||
|
||||
expect(result?.[0].range).toMatchObject(range(9, 2));
|
||||
expect(result?.[0].primary).toBeUndefined();
|
||||
expect(result?.[1].range).toMatchObject(range(7, 2));
|
||||
expect(result?.[1].primary).toBe(activeKeyboardSelection.primary);
|
||||
});
|
||||
|
||||
it('ignores primary-only selections when calculating formula reference highlight primary cells', () => {
|
||||
const workbook = {
|
||||
getUnitId: vi.fn(() => 'unit1'),
|
||||
getActiveSheet: vi.fn(() => ({
|
||||
getSheetId: () => 'sheet1',
|
||||
getName: () => 'Sheet1',
|
||||
getRowCount: () => 100,
|
||||
getColumnCount: () => 50,
|
||||
})),
|
||||
getSheetBySheetName: vi.fn(() => ({ getSheetId: () => 'sheet1' })),
|
||||
};
|
||||
|
||||
const result = calcHighlightRanges({
|
||||
unitId: 'unit1',
|
||||
subUnitId: 'sheet1',
|
||||
currentWorkbook: workbook as any,
|
||||
refSelections: [
|
||||
{ token: 'A1', themeColor: '#ff0000', refIndex: 0, startIndex: 0, endIndex: 1, index: 0 },
|
||||
],
|
||||
editor: undefined,
|
||||
refSelectionsService: {
|
||||
getCurrentSelections: vi.fn(() => [{ primary: { actualRow: 9, actualColumn: 9 } }]),
|
||||
setSelections: vi.fn(),
|
||||
} as any,
|
||||
refSelectionsRenderService: undefined,
|
||||
sheetSkeletonManagerService: {
|
||||
getSkeleton: vi.fn(() => ({})),
|
||||
} as any,
|
||||
themeService: { getColorFromTheme: vi.fn((key: string) => key === 'gray.0' ? '#fff' : key) } as any,
|
||||
univerInstanceService: { getUnit: vi.fn(() => workbook) } as any,
|
||||
});
|
||||
|
||||
expect(result?.[0].primary).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns empty highlight selections when the workbook or active sheet is unavailable', () => {
|
||||
const refSelectionsService = {
|
||||
getCurrentSelections: vi.fn(() => []),
|
||||
setSelections: vi.fn(),
|
||||
};
|
||||
|
||||
expect(calcHighlightRanges({
|
||||
unitId: 'missing',
|
||||
subUnitId: 'sheet1',
|
||||
currentWorkbook: { getUnitId: () => 'unit1' } as any,
|
||||
refSelections: [{ token: 'A1', themeColor: '#ff0000', refIndex: 0, startIndex: 0, endIndex: 1, index: 0 }],
|
||||
editor: undefined,
|
||||
refSelectionsService: refSelectionsService as any,
|
||||
refSelectionsRenderService: undefined,
|
||||
sheetSkeletonManagerService: undefined,
|
||||
themeService: { getColorFromTheme: vi.fn(() => '#fff') } as any,
|
||||
univerInstanceService: { getUnit: vi.fn(() => null) } as any,
|
||||
})).toEqual([]);
|
||||
expect(refSelectionsService.setSelections).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 { FunctionType, getFormulaReplaceResult, sequenceNodeType } from '@univerjs/engine-formula';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('getFormulaReplaceResult', () => {
|
||||
it('adds the call bracket when replacing a typed function prefix and keeps the rest of the formula', () => {
|
||||
expect(getFormulaReplaceResult(
|
||||
[
|
||||
{ nodeType: sequenceNodeType.FUNCTION, token: 'SU', startIndex: 0, endIndex: 1 },
|
||||
'A1:A3)',
|
||||
'+B1',
|
||||
],
|
||||
0,
|
||||
'SUM',
|
||||
FunctionType.Math
|
||||
)).toEqual({
|
||||
text: 'SUM(A1:A3)+B1',
|
||||
offset: -2,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate the call bracket when the typed function already has one', () => {
|
||||
expect(getFormulaReplaceResult(
|
||||
[
|
||||
{ nodeType: sequenceNodeType.FUNCTION, token: 'SU', startIndex: 0, endIndex: 1 },
|
||||
'(',
|
||||
'A1:A3)',
|
||||
],
|
||||
0,
|
||||
'SUM',
|
||||
FunctionType.Math
|
||||
)).toEqual({
|
||||
text: 'SUM(A1:A3)',
|
||||
offset: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not append an open bracket when replacing table names', () => {
|
||||
expect(getFormulaReplaceResult(
|
||||
[{ nodeType: sequenceNodeType.FUNCTION, token: 'Ord', startIndex: 0, endIndex: 2 }],
|
||||
0,
|
||||
'Orders',
|
||||
FunctionType.Table
|
||||
)).toEqual({
|
||||
text: 'Orders',
|
||||
offset: -3,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not append an open bracket when replacing defined names', () => {
|
||||
expect(getFormulaReplaceResult(
|
||||
[{ nodeType: sequenceNodeType.FUNCTION, token: 'Tax', startIndex: 0, endIndex: 2 }],
|
||||
0,
|
||||
'TaxRate',
|
||||
FunctionType.DefinedName
|
||||
)).toEqual({
|
||||
text: 'TaxRate',
|
||||
offset: -4,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createSelectionChangeDuplicateEndGuard, createSelectionChangeHandler } from '../use-sheet-selection-change';
|
||||
|
||||
describe('formula reference selection gesture boundaries', () => {
|
||||
it.each([false, true])('accepts the same reference in a new editor after the previous gesture ends: %s', (completed) => {
|
||||
const duplicateEndGuard = createSelectionChangeDuplicateEndGuard<IRange>();
|
||||
const selectedRange = { startRow: 1, endRow: 1, startColumn: 1, endColumn: 1 };
|
||||
const firstChange = vi.fn();
|
||||
const first = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
duplicateEndGuard,
|
||||
onSelectionsChange: firstChange,
|
||||
});
|
||||
first([selectedRange], false, { start: true });
|
||||
first(completed ? [selectedRange] : [], true);
|
||||
expect(firstChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
const nextChange = vi.fn();
|
||||
const commit = vi.fn();
|
||||
const next = createSelectionChangeHandler({
|
||||
initialSelectionsCount: 0,
|
||||
duplicateEndGuard,
|
||||
onSelectionsChange: nextChange,
|
||||
onDuplicateEnd: commit,
|
||||
});
|
||||
next([selectedRange], false, { start: true });
|
||||
next([selectedRange], false);
|
||||
next([selectedRange], true);
|
||||
|
||||
expect(nextChange).toHaveBeenCalledExactlyOnceWith([selectedRange], false, false);
|
||||
expect(commit).toHaveBeenCalledExactlyOnceWith([selectedRange]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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 { IEditorService } from '@univerjs/docs-ui';
|
||||
import { ISidebarService, useDependency, useEvent } from '@univerjs/ui';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { BehaviorSubject, throttleTime } from 'rxjs';
|
||||
import useResizeScrollObserver from './use-resize-scroll-observer';
|
||||
|
||||
export function useEditorPosition(editorId: string, ready: boolean, deps?: any[]) {
|
||||
const editorService = useDependency(IEditorService);
|
||||
const position$ = useMemo(() => new BehaviorSubject({ left: -999, top: -999, right: -999, bottom: -999 }), []);
|
||||
const sidebarService = useDependency(ISidebarService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const updatePosition = useEvent(() => {
|
||||
const doc = editorService.getEditor(editorId);
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
const position = doc.getBoundingClientRect();
|
||||
const { marginTop = 0, marginBottom = 0 } = doc.getDocumentData().documentStyle;
|
||||
const skeleton = doc.getSkeleton();
|
||||
if (!skeleton) return;
|
||||
const height = skeleton.getSkeletonData()?.pages[0].height;
|
||||
let { left, top, right, bottom } = position;
|
||||
top = top + marginTop;
|
||||
bottom = height ? top + height : bottom - marginBottom;
|
||||
|
||||
const current = position$.getValue();
|
||||
if (current.left === left && current.top === top && current.right === right && current.bottom === bottom) {
|
||||
return;
|
||||
}
|
||||
position$.next({ left: left - 1, right: right + 1, top: top - 1, bottom: bottom + 1 });
|
||||
return position;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
updatePosition();
|
||||
}, [editorId, editorService, univerInstanceService.unitAdded$, updatePosition, ready, ...(deps ?? [])]);
|
||||
|
||||
useResizeScrollObserver(updatePosition);
|
||||
|
||||
useEffect(() => {
|
||||
const sidebarSubscription = sidebarService.scrollEvent$.pipe(throttleTime(100)).subscribe(updatePosition);
|
||||
|
||||
return () => {
|
||||
sidebarSubscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [position$, updatePosition] as const;
|
||||
}
|
||||
@@ -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 { Editor } from '@univerjs/docs-ui';
|
||||
import { Tools } from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { useDependency, useEvent } from '@univerjs/ui';
|
||||
import { isEventTargetInSameFormulaEmbedInteractionBoundary } from '../formula-embed-integration.service';
|
||||
|
||||
export function focusFormulaEditor(
|
||||
editorService: Pick<IEditorService, 'focus'>,
|
||||
editor?: Pick<Editor, 'getEditorId' | 'getSelectionRanges' | 'setSelectionRanges' | 'getDocumentData' | 'docSelectionRenderService'> & { editorDOM?: HTMLElement },
|
||||
offset?: number
|
||||
) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorService.focus(editor.getEditorId());
|
||||
focusFormulaEditorElement(editor);
|
||||
if (editor.docSelectionRenderService.isOnPointerEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selections = [...editor.getSelectionRanges()];
|
||||
if (Tools.isDefine(offset)) {
|
||||
editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]);
|
||||
} else if (!selections.length) {
|
||||
const body = editor.getDocumentData().body?.dataStream ?? '\r\n';
|
||||
const offset = Math.max(body.length - 2, 0);
|
||||
editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]);
|
||||
} else {
|
||||
editor.setSelectionRanges(selections);
|
||||
}
|
||||
}
|
||||
|
||||
function focusFormulaEditorElement(editor: Pick<Editor, 'getEditorId'> & { editorDOM?: HTMLElement }): void {
|
||||
const ownerDocument = editor.editorDOM?.ownerDocument ?? document;
|
||||
const editorElement = ownerDocument.getElementById(`__editor_${editor.getEditorId()}`);
|
||||
editorElement?.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
export function shouldSkipFormulaEditorMouseUpFocus(_target: EventTarget | null): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldRefocusFormulaEditorOnMouseUp(options: {
|
||||
target: EventTarget | null;
|
||||
isFocusing: boolean | undefined;
|
||||
isPointerSelecting: boolean | undefined;
|
||||
}): boolean {
|
||||
if (shouldSkipFormulaEditorMouseUpFocus(options.target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.isPointerSelecting || options.isFocusing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function hasActiveFormulaEmbedInteraction(scopeElement: HTMLElement | null | undefined): boolean {
|
||||
const ownerDocument = scopeElement?.ownerDocument;
|
||||
return isEventTargetInSameFormulaEmbedInteractionBoundary(scopeElement, ownerDocument?.activeElement);
|
||||
}
|
||||
|
||||
export const useFocus = (editor?: Editor) => {
|
||||
const editorService = useDependency(IEditorService);
|
||||
const focus = useEvent((offset?: number) => {
|
||||
focusFormulaEditor(editorService, editor, offset);
|
||||
});
|
||||
|
||||
return focus;
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 { Editor } from '@univerjs/docs-ui';
|
||||
import type { IFunctionInfo } from '@univerjs/engine-formula';
|
||||
import { IDescriptionService, LexerTreeBuilder, matchToken } from '@univerjs/engine-formula';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs';
|
||||
import { IFormulaPromptService } from '../../../services/prompt.service';
|
||||
import { useStateRef } from './use-state-ref';
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const useFormulaDescribe = (isNeed: boolean, formulaText: string, editor?: Editor) => {
|
||||
const formulaPromptService = useDependency(IFormulaPromptService);
|
||||
const descriptionService = useDependency(IDescriptionService);
|
||||
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
|
||||
|
||||
const [functionInfo, setFunctionInfo] = useState<IFunctionInfo | undefined>();
|
||||
const [paramIndex, setParamIndex] = useState<number>(-1);
|
||||
const [isShow, setIsShow] = useState(true);
|
||||
const isShowRef = useStateRef(isShow);
|
||||
|
||||
const formulaTextRef = useRef(formulaText);
|
||||
formulaTextRef.current = formulaText;
|
||||
const reset = () => {
|
||||
setFunctionInfo(undefined);
|
||||
setParamIndex(-1);
|
||||
setIsShow(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const nodes = lexerTreeBuilder.sequenceNodesBuilder(formulaText.slice(1));
|
||||
formulaPromptService.setSequenceNodes(nodes ?? []);
|
||||
}, [formulaText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && isNeed) {
|
||||
const d = editor.selectionChange$.pipe(debounceTime(50)).subscribe((e) => {
|
||||
if (e.textRanges.length === 1) {
|
||||
const [range] = e.textRanges;
|
||||
if (range.collapsed && isShowRef.current) {
|
||||
const { startOffset } = range;
|
||||
const nodeIndex = formulaPromptService.getCurrentSequenceNodeIndex(startOffset - 2);
|
||||
const currentSequenceNode = formulaPromptService.getCurrentSequenceNodeByIndex(nodeIndex);
|
||||
const nextSequenceNode = formulaPromptService.getCurrentSequenceNodeByIndex(nodeIndex + 1);
|
||||
|
||||
if (currentSequenceNode) {
|
||||
if (
|
||||
typeof currentSequenceNode !== 'string' &&
|
||||
currentSequenceNode.nodeType === 3 &&
|
||||
!descriptionService.hasDefinedNameDescription(currentSequenceNode.token.trim()) &&
|
||||
nextSequenceNode === matchToken.OPEN_BRACKET
|
||||
) {
|
||||
// If the current node is a function name and the next node is an open bracket '(', should show the function description.
|
||||
const info = descriptionService.getFunctionInfo(currentSequenceNode.token);
|
||||
setFunctionInfo(info);
|
||||
setParamIndex(-1);
|
||||
return;
|
||||
} else {
|
||||
// Why subtract 1? Because the nodes do not include the initial '=' character, but the selection does include '='.
|
||||
const res = lexerTreeBuilder.getFunctionAndParameter(`${formulaTextRef.current}A`, startOffset - 1);
|
||||
if (res) {
|
||||
const { functionName, paramIndex } = res;
|
||||
const info = descriptionService.getFunctionInfo(functionName);
|
||||
setFunctionInfo(info);
|
||||
setParamIndex(paramIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setFunctionInfo(undefined);
|
||||
setParamIndex(-1);
|
||||
});
|
||||
const d2 = editor.selectionChange$.pipe(
|
||||
filter((e) => e.textRanges.length === 1),
|
||||
map((e) => e.textRanges[0].startOffset),
|
||||
distinctUntilChanged()
|
||||
).subscribe(() => {
|
||||
setIsShow(true);
|
||||
});
|
||||
return () => {
|
||||
d.unsubscribe();
|
||||
d2.unsubscribe();
|
||||
};
|
||||
}
|
||||
}, [editor, isNeed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNeed) {
|
||||
reset();
|
||||
}
|
||||
}, [isNeed]);
|
||||
|
||||
return {
|
||||
functionInfo,
|
||||
paramIndex,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
@@ -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 { Editor } from '@univerjs/docs-ui';
|
||||
import type { FunctionType, ISearchItemWithType } from '@univerjs/engine-formula';
|
||||
import type { INode } from './use-formula-token';
|
||||
import {
|
||||
getFormulaReplaceResult as getSharedFormulaReplaceResult,
|
||||
IDescriptionService,
|
||||
searchFormulaFunctions,
|
||||
sequenceNodeType,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { findIndexFromSequenceNodes } from '../../range-selector/utils/find-index-from-sequence-nodes';
|
||||
import { useStateRef } from './use-state-ref';
|
||||
|
||||
function getFormulaReplaceResult(nodes: INode[], index: number, formulaName: string, functionType: FunctionType) {
|
||||
return getSharedFormulaReplaceResult(nodes, index, formulaName, functionType);
|
||||
}
|
||||
|
||||
export const useFormulaSearch = (isNeed: boolean, nodes: INode[] = [], editor?: Editor) => {
|
||||
const descriptionService = useDependency(IDescriptionService);
|
||||
|
||||
const [searchList, setSearchList] = useState<ISearchItemWithType[]>([]);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const indexRef = useRef(-1);
|
||||
const stateRef = useStateRef({ nodes });
|
||||
|
||||
const reset = () => {
|
||||
setSearchList([]);
|
||||
setSearchText('');
|
||||
indexRef.current = -1;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && isNeed) {
|
||||
const d = editor.input$.pipe(debounceTime(300)).subscribe(() => {
|
||||
const selections = editor.getSelectionRanges();
|
||||
if (selections.length === 1) {
|
||||
const nodes = stateRef.current.nodes;
|
||||
const range = selections[0];
|
||||
if (range.collapsed) {
|
||||
// Why minus 1: because nodes do not include the initial ‘=’ character, but selection does
|
||||
const currentNodeIndex = findIndexFromSequenceNodes(nodes, range.startOffset - 1, false);
|
||||
indexRef.current = currentNodeIndex;
|
||||
const currentNode = nodes[currentNodeIndex];
|
||||
if (currentNode && typeof currentNode !== 'string' && currentNode.nodeType === sequenceNodeType.FUNCTION) {
|
||||
indexRef.current = currentNodeIndex;
|
||||
const token = currentNode.token;
|
||||
const list = searchFormulaFunctions(descriptionService, token);
|
||||
// Here we limit the maximum number of search results to 10 to prevent performance issues caused by rendering too many items in the dropdown.
|
||||
setSearchList(list);
|
||||
setSearchText(token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
indexRef.current = -1;
|
||||
setSearchText('');
|
||||
setSearchList((pre) => {
|
||||
if (!pre?.length) {
|
||||
return pre;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
d.unsubscribe();
|
||||
};
|
||||
};
|
||||
}, [editor, isNeed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNeed) {
|
||||
reset();
|
||||
}
|
||||
}, [isNeed]);
|
||||
|
||||
const handlerFormulaReplace = (formulaName: string, functionType: FunctionType) => {
|
||||
return getFormulaReplaceResult(stateRef.current.nodes, indexRef.current, formulaName, functionType);
|
||||
};
|
||||
return {
|
||||
searchList,
|
||||
searchText,
|
||||
handlerFormulaReplace,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* 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, IAccessor, IUnitRangeName, Workbook } from '@univerjs/core';
|
||||
import type { Editor } from '@univerjs/docs-ui';
|
||||
import type { ISequenceNode } from '@univerjs/engine-formula';
|
||||
import { Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { deserializeRangeWithSheetWithCache, isFormulaLexerToken, LexerTreeBuilder, matchRefDrawToken, matchToken, sequenceNodeType } from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { useDependency, useEvent } from '@univerjs/ui';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { filter } from 'rxjs';
|
||||
import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render.service';
|
||||
import { useStateRef } from './use-state-ref';
|
||||
|
||||
export function resolveFormulaSelectionDataStream(accssor: IAccessor, editor?: Pick<Editor, 'getDocumentDataModel'>, editorId?: string) {
|
||||
const editorDataStream = editor?.getDocumentDataModel()?.getBody()?.dataStream;
|
||||
if (editorDataStream != null) {
|
||||
return { dataStream: editorDataStream, offset: 0 };
|
||||
}
|
||||
|
||||
const univerInstanceService = accssor.get(IUniverInstanceService);
|
||||
const editorDocumentModel = editorId
|
||||
? univerInstanceService.getUnit<DocumentDataModel>(editorId, UniverInstanceType.UNIVER_DOC)
|
||||
: undefined;
|
||||
const documentModel = editorDocumentModel ?? univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
if (!documentModel?.getBody()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataStream = documentModel.getBody()?.dataStream ?? '';
|
||||
return { dataStream, offset: 0 };
|
||||
}
|
||||
|
||||
export enum FormulaSelectingType {
|
||||
NOT_SELECT = 0,
|
||||
NEED_ADD = 1,
|
||||
CAN_EDIT = 2,
|
||||
// editing cross sheet reference
|
||||
EDIT_OTHER_SHEET_REFERENCE = 3,
|
||||
EDIT_OTHER_WORKBOOK_REFERENCE = 4,
|
||||
}
|
||||
|
||||
export function shouldSkipReferenceEditingByPointer(isDisabledByPointer: boolean, disableOnClick?: boolean): boolean {
|
||||
return isDisabledByPointer && !disableOnClick;
|
||||
}
|
||||
|
||||
export function resolveFormulaSelectionWorkbook<TWorkbook>(currentWorkbook: TWorkbook | null | undefined, fallbackWorkbook: TWorkbook | null | undefined): TWorkbook | undefined {
|
||||
return currentWorkbook ?? fallbackWorkbook ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveFormulaSelectionCursorIndex(activeRange: { collapsed?: boolean; startOffset?: number } | undefined, dataStream: string): number {
|
||||
const index = activeRange?.collapsed ? activeRange.startOffset! : -1;
|
||||
if (index <= 0 && dataStream.startsWith('=') && dataStream.length > 0) {
|
||||
return dataStream.length;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
export function getSelectionAfterLaggingFormulaInput(
|
||||
dataStream: string,
|
||||
selection: { collapsed?: boolean; startOffset?: number; endOffset?: number } | undefined,
|
||||
content: string
|
||||
): { startOffset: number; endOffset: number; collapsed: true } | undefined {
|
||||
if (
|
||||
!content ||
|
||||
content.includes('\r') ||
|
||||
content.includes('\n') ||
|
||||
!dataStream.startsWith('=') ||
|
||||
!selection?.collapsed
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startOffset = selection.startOffset;
|
||||
if (startOffset == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (selection.endOffset !== startOffset) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (dataStream.slice(startOffset, startOffset + content.length) !== content) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nextOffset = startOffset + content.length;
|
||||
return {
|
||||
startOffset: nextOffset,
|
||||
endOffset: nextOffset,
|
||||
collapsed: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveFormulaSelectingIntent(adding: boolean, editing: boolean): FormulaSelectingType {
|
||||
if (adding) {
|
||||
return FormulaSelectingType.NEED_ADD;
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return FormulaSelectingType.CAN_EDIT;
|
||||
}
|
||||
|
||||
return FormulaSelectingType.NOT_SELECT;
|
||||
}
|
||||
|
||||
export function shouldAddFormulaReference(dataStream: string, index: number): boolean {
|
||||
const char = dataStream[index - 1];
|
||||
const nextChar = dataStream[index];
|
||||
|
||||
return Boolean(
|
||||
char &&
|
||||
(matchRefDrawToken(char) || char === '!') &&
|
||||
(!nextChar || (isFormulaLexerToken(nextChar) && nextChar !== matchToken.OPEN_BRACKET))
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export function useFormulaSelecting(opts: { editor?: Editor; editorId: string; isFocus: boolean; disableOnClick?: boolean; resetSignal?: number; unitId: string; subUnitId: string }) {
|
||||
const { editor, editorId, isFocus, disableOnClick, resetSignal, unitId, subUnitId } = opts;
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const sheetRenderer = renderManagerService.getRenderUnitById(unitId);
|
||||
const renderer = renderManagerService.getRenderUnitById(editorId);
|
||||
const docSelectionRenderService = renderer?.with(DocSelectionRenderService);
|
||||
const docSelectionManagerService = useDependency(DocSelectionManagerService);
|
||||
const injector = useDependency(Injector);
|
||||
const [isSelecting, innerSetIsSelecting] = useState<FormulaSelectingType>(FormulaSelectingType.NOT_SELECT);
|
||||
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
|
||||
const isDisabledByPointer = useRef(true);
|
||||
const lastInputContentRef = useRef('');
|
||||
const refSelectionsRenderService = sheetRenderer?.with(RefSelectionsRenderService);
|
||||
const isSelectingRef = useStateRef(isSelecting);
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const sourceSheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
|
||||
const setIsSelecting = useEvent((v: FormulaSelectingType) => {
|
||||
if (refSelectionsRenderService) {
|
||||
refSelectionsRenderService.setSkipLastEnabled(v === FormulaSelectingType.NEED_ADD || v === FormulaSelectingType.EDIT_OTHER_SHEET_REFERENCE || v === FormulaSelectingType.EDIT_OTHER_WORKBOOK_REFERENCE);
|
||||
}
|
||||
isSelectingRef.current = v;
|
||||
innerSetIsSelecting(v);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
const calculateSelectingType = useEvent(() => {
|
||||
const currentWorkbook = resolveFormulaSelectionWorkbook(
|
||||
univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET),
|
||||
workbook
|
||||
);
|
||||
if (!currentWorkbook) return;
|
||||
const currentSheet = currentWorkbook.getActiveSheet();
|
||||
const activeRange = editor?.getSelectionRanges()?.[0] ?? docSelectionRenderService?.getActiveTextRange();
|
||||
const config = resolveFormulaSelectionDataStream(injector, editor, editorId);
|
||||
if (!config) return;
|
||||
const dataStream = config?.dataStream?.slice(0, -2);
|
||||
const normalizedActiveRange = getSelectionAfterLaggingFormulaInput(dataStream, activeRange, lastInputContentRef.current) ?? activeRange;
|
||||
const index = resolveFormulaSelectionCursorIndex(normalizedActiveRange, dataStream);
|
||||
const nodes = (lexerTreeBuilder.sequenceNodesBuilder(dataStream) ?? []).map((node) => {
|
||||
if (typeof node === 'object') {
|
||||
if (node.nodeType === sequenceNodeType.REFERENCE) {
|
||||
return {
|
||||
...node,
|
||||
range: deserializeRangeWithSheetWithCache(node.token),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
range: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
const focusingNode = nodes.find((node) => typeof node === 'object' && node.nodeType === sequenceNodeType.REFERENCE && index === node.endIndex + 2) as unknown as (ISequenceNode & { range: IUnitRangeName });
|
||||
const adding = shouldAddFormulaReference(dataStream, index);
|
||||
const editing = Boolean(focusingNode);
|
||||
const selectingIntent = resolveFormulaSelectingIntent(Boolean(adding), editing);
|
||||
|
||||
if (dataStream?.substring(0, 1) === '=' && selectingIntent !== FormulaSelectingType.NOT_SELECT) {
|
||||
if (selectingIntent === FormulaSelectingType.NEED_ADD) {
|
||||
isDisabledByPointer.current = false;
|
||||
setIsSelecting(FormulaSelectingType.NEED_ADD);
|
||||
} else if (focusingNode) {
|
||||
if (shouldSkipReferenceEditingByPointer(isDisabledByPointer.current, disableOnClick)) {
|
||||
return;
|
||||
}
|
||||
isDisabledByPointer.current = false;
|
||||
|
||||
const { sheetName, unitId } = focusingNode.range;
|
||||
const currentUnitId = resolveFormulaSelectionWorkbook(
|
||||
univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET),
|
||||
workbook
|
||||
)?.getUnitId();
|
||||
if (unitId && unitId !== currentUnitId) {
|
||||
setIsSelecting(FormulaSelectingType.EDIT_OTHER_WORKBOOK_REFERENCE);
|
||||
} else if (
|
||||
(!sheetName && currentSheet.getSheetId() === sourceSheet?.getSheetId()) ||
|
||||
sheetName === currentSheet.getName()
|
||||
) {
|
||||
setIsSelecting(FormulaSelectingType.CAN_EDIT);
|
||||
} else {
|
||||
setIsSelecting(FormulaSelectingType.EDIT_OTHER_SHEET_REFERENCE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsSelecting(FormulaSelectingType.NOT_SELECT);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const sub = docSelectionManagerService.textSelection$
|
||||
.pipe(filter((param) => param.unitId === editorId))
|
||||
.subscribe(() => {
|
||||
calculateSelectingType();
|
||||
});
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, [calculateSelectingType, docSelectionManagerService.textSelection$, editorId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocus || !editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const sub = editor.input$.subscribe(({ content, isComposing }) => {
|
||||
if (!isComposing) {
|
||||
lastInputContentRef.current = content;
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
calculateSelectingType();
|
||||
});
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
calculateSelectingType();
|
||||
lastInputContentRef.current = '';
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, [calculateSelectingType, editor, isFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editorDocumentModel = univerInstanceService.getUnit<DocumentDataModel>(editorId, UniverInstanceType.UNIVER_DOC);
|
||||
const sub = editorDocumentModel?.change$?.subscribe(() => {
|
||||
queueMicrotask(calculateSelectingType);
|
||||
});
|
||||
|
||||
return () => sub?.unsubscribe();
|
||||
}, [calculateSelectingType, editorId, isFocus, univerInstanceService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocus) {
|
||||
setIsSelecting(FormulaSelectingType.NOT_SELECT);
|
||||
isDisabledByPointer.current = true;
|
||||
}
|
||||
}, [isFocus, setIsSelecting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetSignal === undefined) return;
|
||||
setIsSelecting(FormulaSelectingType.NOT_SELECT);
|
||||
isDisabledByPointer.current = true;
|
||||
}, [resetSignal, setIsSelecting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disableOnClick) return;
|
||||
const sub = renderer?.mainComponent?.onPointerDown$.subscribeEvent(() => {
|
||||
setIsSelecting(FormulaSelectingType.NOT_SELECT);
|
||||
isDisabledByPointer.current = true;
|
||||
});
|
||||
|
||||
return () => sub?.unsubscribe();
|
||||
}, [disableOnClick, renderer?.mainComponent?.onPointerDown$, setIsSelecting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocus) return;
|
||||
const sub = workbook?.activeSheet$.subscribe(() => {
|
||||
calculateSelectingType();
|
||||
});
|
||||
const sub2 = univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET).subscribe(() => {
|
||||
calculateSelectingType();
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
sub2?.unsubscribe();
|
||||
};
|
||||
}, [calculateSelectingType, isFocus, workbook?.activeSheet$, univerInstanceService.getCurrentTypeOfUnit$]);
|
||||
|
||||
return { isSelecting, isSelectingRef };
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 { ISequenceNode } from '@univerjs/engine-formula';
|
||||
import { LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export type INode = (string | ISequenceNode);
|
||||
export const useFormulaToken = () => {
|
||||
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
|
||||
const getFormulaToken = useCallback((text: string) => lexerTreeBuilder.sequenceNodesBuilder(text) || [], [lexerTreeBuilder]);
|
||||
return getFormulaToken;
|
||||
};
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* 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, IRange, ITextRange, ITextRun, Workbook } from '@univerjs/core';
|
||||
import type { Editor } from '@univerjs/docs-ui';
|
||||
import type { ISequenceNode } from '@univerjs/engine-formula';
|
||||
import type { ISelectionWithStyle, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import type { INode } from './use-formula-token';
|
||||
import { ICommandService, IUniverInstanceService, ThemeService, UniverInstanceType } from '@univerjs/core';
|
||||
import { ReplaceTextRunsCommand } from '@univerjs/docs-ui';
|
||||
import {
|
||||
buildFormulaTextRuns,
|
||||
deserializeRangeWithSheet,
|
||||
getFormulaHighlightDataStream,
|
||||
IDescriptionService,
|
||||
} from '@univerjs/engine-formula';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { IRefSelectionsService, setEndForRange } from '@univerjs/sheets';
|
||||
import { ISheetSelectionRenderService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { useDependency, useEvent } from '@univerjs/ui';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { genFormulaRefSelectionStyle } from '../../../common/selection';
|
||||
import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render.service';
|
||||
|
||||
export interface IRefSelection {
|
||||
refIndex: number;
|
||||
themeColor: string;
|
||||
token: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function isSameSelectionRange(left: IRange | undefined, right: IRange): boolean {
|
||||
return !!left &&
|
||||
left.startRow === right.startRow &&
|
||||
left.startColumn === right.startColumn &&
|
||||
left.endRow === right.endRow &&
|
||||
left.endColumn === right.endColumn &&
|
||||
(!left.sheetId || !right.sheetId || left.sheetId === right.sheetId) &&
|
||||
(!left.unitId || !right.unitId || left.unitId === right.unitId);
|
||||
}
|
||||
|
||||
export function calcHighlightRanges(opts: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
currentWorkbook: Workbook;
|
||||
refSelections: IRefSelection[];
|
||||
editor: Editor | undefined;
|
||||
refSelectionsService: SheetsSelectionsService;
|
||||
refSelectionsRenderService: RefSelectionsRenderService | undefined;
|
||||
sheetSkeletonManagerService: SheetSkeletonManagerService | undefined;
|
||||
themeService: ThemeService;
|
||||
univerInstanceService: IUniverInstanceService;
|
||||
}) {
|
||||
const {
|
||||
unitId,
|
||||
subUnitId,
|
||||
currentWorkbook,
|
||||
refSelections,
|
||||
editor,
|
||||
refSelectionsService,
|
||||
refSelectionsRenderService,
|
||||
sheetSkeletonManagerService,
|
||||
themeService,
|
||||
univerInstanceService,
|
||||
} = opts;
|
||||
const currentUnitId = currentWorkbook.getUnitId();
|
||||
const workbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
const selectionWithStyle: ISelectionWithStyle[] = [];
|
||||
if (!workbook || !worksheet) {
|
||||
return selectionWithStyle;
|
||||
}
|
||||
const currentSheetId = worksheet.getSheetId();
|
||||
const getSheetIdByName = (name: string) => workbook?.getSheetBySheetName(name)?.getSheetId();
|
||||
|
||||
const skeleton = sheetSkeletonManagerService?.getSkeleton(currentSheetId);
|
||||
if (!skeleton) return;
|
||||
const endIndexes: number[] = [];
|
||||
const currentSelections = refSelectionsService.getCurrentSelections();
|
||||
const matchedCurrentSelectionIndexes = new Set<number>();
|
||||
const getPrimaryForRange = (range: IRange, fallbackIndex: number) => {
|
||||
const currentSelection = currentSelections[fallbackIndex];
|
||||
if (isSameSelectionRange(currentSelection?.range, range)) {
|
||||
matchedCurrentSelectionIndexes.add(fallbackIndex);
|
||||
return currentSelection.primary;
|
||||
}
|
||||
|
||||
const matchedIndex = currentSelections.findIndex((selection, index) =>
|
||||
!matchedCurrentSelectionIndexes.has(index) && isSameSelectionRange(selection.range, range)
|
||||
);
|
||||
if (matchedIndex !== -1) {
|
||||
matchedCurrentSelectionIndexes.add(matchedIndex);
|
||||
return currentSelections[matchedIndex].primary;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
for (let i = 0, len = refSelections.length; i < len; i++) {
|
||||
const refSelection = refSelections[i];
|
||||
const { themeColor, token, refIndex, endIndex } = refSelection;
|
||||
|
||||
const unitRangeName = deserializeRangeWithSheet(token);
|
||||
const { unitId: refUnitId, sheetName, range: rawRange } = unitRangeName;
|
||||
const refSheetId = getSheetIdByName(sheetName);
|
||||
|
||||
if (!refSheetId && sheetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentUnitId !== unitId && refUnitId !== currentUnitId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (refUnitId && refUnitId !== currentUnitId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((refSheetId && refSheetId !== currentSheetId) || (!refSheetId && currentSheetId !== subUnitId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const range = setEndForRange(rawRange, worksheet.getRowCount(), worksheet.getColumnCount());
|
||||
range.unitId = unitId;
|
||||
range.sheetId = currentSheetId;
|
||||
selectionWithStyle.push({
|
||||
range,
|
||||
primary: getPrimaryForRange(range, selectionWithStyle.length),
|
||||
style: genFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()),
|
||||
});
|
||||
endIndexes.push(endIndex);
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
const cursor = editor.getSelectionRanges()?.[0]?.startOffset;
|
||||
const activeIndex = endIndexes.findIndex((end) => end + 2 === cursor);
|
||||
if (activeIndex !== -1) {
|
||||
refSelectionsRenderService?.setActiveSelectionIndex(activeIndex);
|
||||
} else if (selectionWithStyle.length) {
|
||||
refSelectionsRenderService?.setActiveSelectionIndex(selectionWithStyle.length - 1);
|
||||
} else {
|
||||
refSelectionsRenderService?.resetActiveSelectionIndex();
|
||||
}
|
||||
}
|
||||
|
||||
return selectionWithStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} unitId
|
||||
* @param {string} subUnitId 打开面板的时候传入的 sheetId
|
||||
* @param {IRefSelection[]} refSelections
|
||||
*/
|
||||
|
||||
export function useSheetHighlight(unitId: string, subUnitId: string) {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const themeService = useDependency(ThemeService);
|
||||
const refSelectionsService = useDependency(IRefSelectionsService);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const ownerRender = renderManagerService.getRenderUnitById(unitId);
|
||||
const ownerRefSelectionsRenderService = ownerRender?.with(RefSelectionsRenderService);
|
||||
|
||||
const getHighlightWorkbook = useEvent((refSelections: IRefSelection[]) => {
|
||||
const ownerWorkbook = univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const currentWorkbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentUnitId = currentWorkbook?.getUnitId();
|
||||
const hasExplicitCurrentWorkbookRef = Boolean(currentUnitId) && refSelections.some((refSelection) =>
|
||||
deserializeRangeWithSheet(refSelection.token).unitId === currentUnitId
|
||||
);
|
||||
|
||||
return hasExplicitCurrentWorkbookRef ? currentWorkbook : ownerWorkbook ?? currentWorkbook;
|
||||
});
|
||||
|
||||
const highlightSheet = useEvent((refSelections: IRefSelection[], editor?: Editor, isEnd = false) => {
|
||||
const currentWorkbook = getHighlightWorkbook(refSelections);
|
||||
if (!currentWorkbook) return;
|
||||
const currentRender = renderManagerService.getRenderUnitById(currentWorkbook.getUnitId());
|
||||
const refSelectionsRenderService = currentRender?.with(RefSelectionsRenderService);
|
||||
const sheetSelectionRenderService = currentRender?.with(ISheetSelectionRenderService);
|
||||
const sheetSkeletonManagerService = currentRender?.with(SheetSkeletonManagerService);
|
||||
if (!isEnd && refSelectionsRenderService?.selectionMoving) return;
|
||||
const currentSheetId = currentWorkbook.getActiveSheet()?.getSheetId();
|
||||
if (!currentSheetId) return;
|
||||
const selectionWithStyle = calcHighlightRanges({
|
||||
unitId,
|
||||
subUnitId,
|
||||
currentWorkbook,
|
||||
refSelections,
|
||||
editor,
|
||||
refSelectionsService,
|
||||
refSelectionsRenderService,
|
||||
sheetSkeletonManagerService,
|
||||
themeService,
|
||||
univerInstanceService,
|
||||
});
|
||||
|
||||
if (!selectionWithStyle) return;
|
||||
const allControls = refSelectionsRenderService?.getSelectionControls() || [];
|
||||
// If the number are same, try to reuse the current controls
|
||||
if (allControls.length === selectionWithStyle.length) {
|
||||
refSelectionsRenderService?.resetSelectionsByModelData(selectionWithStyle);
|
||||
} else {
|
||||
refSelectionsService.setSelections(currentWorkbook.getUnitId(), currentSheetId, selectionWithStyle);
|
||||
}
|
||||
if (isEnd && selectionWithStyle.length) {
|
||||
sheetSelectionRenderService?.resetSelectionsByModelData([]);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
ownerRefSelectionsRenderService?.resetActiveSelectionIndex();
|
||||
};
|
||||
}, [ownerRefSelectionsRenderService]);
|
||||
|
||||
return highlightSheet;
|
||||
}
|
||||
|
||||
export function useDocHight(_leadingCharacter: string = '') {
|
||||
const descriptionService = useDependency(IDescriptionService);
|
||||
const colorMap = useColor();
|
||||
const commandService = useDependency(ICommandService);
|
||||
const leadingCharacterLength = useMemo(() => _leadingCharacter.length, [_leadingCharacter]);
|
||||
|
||||
const highlightDoc = useEvent((
|
||||
editor: Editor,
|
||||
sequenceNodes: INode[],
|
||||
isNeedResetSelection = true,
|
||||
newSelections?: ITextRange[],
|
||||
sourceText?: string
|
||||
) => {
|
||||
const data = editor.getDocumentData();
|
||||
const editorId = editor.getEditorId();
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const body = data.body;
|
||||
if (!body) {
|
||||
return [];
|
||||
}
|
||||
const str = body.dataStream.slice(0, body.dataStream.length - 2);
|
||||
if (!str.startsWith(_leadingCharacter)) return [];
|
||||
if (sequenceNodes == null || sequenceNodes.length === 0) {
|
||||
commandService.syncExecuteCommand(ReplaceTextRunsCommand.id, {
|
||||
unitId: editorId,
|
||||
body: createFormulaHighlightBody(str, []),
|
||||
});
|
||||
return [];
|
||||
} else {
|
||||
const { textRuns, refSelections } = buildTextRuns(descriptionService, colorMap, sequenceNodes);
|
||||
if (leadingCharacterLength) {
|
||||
textRuns.forEach((e) => {
|
||||
e.ed = e.ed + leadingCharacterLength;
|
||||
e.st = e.st + leadingCharacterLength;
|
||||
});
|
||||
}
|
||||
|
||||
const highlightDataStream = getFormulaHighlightDataStream(_leadingCharacter, sequenceNodes, sourceText);
|
||||
const highlightTextRuns = createHighlightTextRuns(textRuns, leadingCharacterLength);
|
||||
let selections;
|
||||
if (isNeedResetSelection) {
|
||||
// Switching between uppercase and lowercase will trigger a reflow, causing the cursor to be misplaced. Let's refresh the cursor position here.
|
||||
selections = editor.getSelectionRanges();
|
||||
// After 'buildTextRuns' , the content changes, most of it is deleted, and the cursor position needs to be corrected
|
||||
const maxOffset = highlightDataStream.length - 2 + leadingCharacterLength;
|
||||
selections.forEach((selection) => {
|
||||
selection.startOffset = Math.max(0, Math.min(selection.startOffset, maxOffset));
|
||||
selection.endOffset = Math.max(0, Math.min(selection.endOffset, maxOffset));
|
||||
});
|
||||
}
|
||||
commandService.syncExecuteCommand(ReplaceTextRunsCommand.id, {
|
||||
unitId: editorId,
|
||||
body: createFormulaHighlightBody(highlightDataStream.slice(0, -2), highlightTextRuns),
|
||||
textRanges: newSelections ?? selections,
|
||||
});
|
||||
return refSelections;
|
||||
}
|
||||
});
|
||||
return highlightDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReplaceTextRunsCommand shifts the editor's existing structural metadata when text changes.
|
||||
* Its replacement body must therefore contain only inline formula data; carrying paragraphs
|
||||
* or section breaks copied from the old snapshot would leave their indexes stale.
|
||||
*/
|
||||
export function createFormulaHighlightBody(dataStream: string, textRuns: ITextRun[]): IDocumentBody {
|
||||
return { dataStream, textRuns };
|
||||
}
|
||||
|
||||
export function createHighlightTextRuns(textRuns: ITextRun[], leadingCharacterLength: number): ITextRun[] {
|
||||
return leadingCharacterLength
|
||||
? [{ st: 0, ed: leadingCharacterLength, ts: { fs: 11 } }, ...textRuns]
|
||||
: textRuns;
|
||||
}
|
||||
|
||||
interface IColorMap {
|
||||
formulaRefColors: string[];
|
||||
numberColor: string;
|
||||
stringColor: string;
|
||||
plainTextColor: string;
|
||||
}
|
||||
|
||||
export function useColor(): IColorMap {
|
||||
const themeService = useDependency(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const result = useMemo(() => {
|
||||
const formulaRefColors = [
|
||||
themeService.getColorFromTheme('loop-color.1'),
|
||||
themeService.getColorFromTheme('loop-color.2'),
|
||||
themeService.getColorFromTheme('loop-color.3'),
|
||||
themeService.getColorFromTheme('loop-color.4'),
|
||||
themeService.getColorFromTheme('loop-color.5'),
|
||||
themeService.getColorFromTheme('loop-color.6'),
|
||||
themeService.getColorFromTheme('loop-color.7'),
|
||||
themeService.getColorFromTheme('loop-color.8'),
|
||||
themeService.getColorFromTheme('loop-color.9'),
|
||||
themeService.getColorFromTheme('loop-color.10'),
|
||||
themeService.getColorFromTheme('loop-color.11'),
|
||||
themeService.getColorFromTheme('loop-color.12'),
|
||||
].map((color) => themeService.isValidThemeColor(color) ? themeService.getColorFromTheme(color) : color);
|
||||
const numberColor = themeService.getColorFromTheme('blue.700');
|
||||
const stringColor = themeService.getColorFromTheme('jiqing.800');
|
||||
const plainTextColor = themeService.getColorFromTheme('gray.1000');
|
||||
return { formulaRefColors, numberColor, stringColor, plainTextColor };
|
||||
}, [theme]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildTextRuns(descriptionService: IDescriptionService, colorMap: IColorMap, sequenceNodes: Array<ISequenceNode | string>) {
|
||||
return buildFormulaTextRuns(descriptionService, colorMap, sequenceNodes);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
export const useIsFirstRender = () => {
|
||||
const isFirstRef = useRef(true);
|
||||
useLayoutEffect(() => {
|
||||
isFirstRef.current = false;
|
||||
}, []);
|
||||
return isFirstRef;
|
||||
};
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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 { Editor } from '@univerjs/docs-ui';
|
||||
import { CommandType, Direction, DisposableCollection, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, FOCUSING_FX_BAR_EDITOR, generateRandomId, ICommandService, IContextService } from '@univerjs/core';
|
||||
import { IEditorService, MoveCursorOperation, MoveSelectionOperation } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import { ExpandSelectionCommand, JumpOver, MoveSelectionCommand } from '@univerjs/sheets-ui';
|
||||
import { IShortcutService, KeyCode, MetaKeys, useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { FormulaSelectingType } from './use-formula-selection';
|
||||
|
||||
export function shouldMoveFormulaSelectionFromCurrentSelection(selectingType: FormulaSelectingType, refSelectionCount: number): boolean {
|
||||
if (selectingType === FormulaSelectingType.NEED_ADD) {
|
||||
return refSelectionCount === 0;
|
||||
}
|
||||
|
||||
return selectingType === FormulaSelectingType.EDIT_OTHER_SHEET_REFERENCE;
|
||||
}
|
||||
|
||||
export interface IFormulaEditorInteractionOwnerOptions {
|
||||
fxBarFocused?: boolean;
|
||||
formulaBarEditorId?: string;
|
||||
normalEditorId?: string;
|
||||
allowMissingFocus?: boolean;
|
||||
}
|
||||
|
||||
export function isFormulaEditorInteractionOwner(
|
||||
focusEditorId: string | null | undefined | void,
|
||||
editorId: string,
|
||||
options?: IFormulaEditorInteractionOwnerOptions
|
||||
): boolean {
|
||||
if (focusEditorId === editorId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!options?.fxBarFocused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editorId === (options.formulaBarEditorId ?? DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) &&
|
||||
(
|
||||
focusEditorId === (options.normalEditorId ?? DOCS_NORMAL_EDITOR_UNIT_ID_KEY) ||
|
||||
Boolean(options.allowMissingFocus && !focusEditorId)
|
||||
);
|
||||
}
|
||||
|
||||
export const useLeftAndRightArrow = (
|
||||
isNeed: boolean,
|
||||
shouldMoveSelection: FormulaSelectingType,
|
||||
editor?: Editor,
|
||||
onMoveInEditor?: (keyCode: KeyCode, metaKey?: MetaKeys) => void,
|
||||
getRefSelectionCount?: () => number
|
||||
) => {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const shortcutService = useDependency(IShortcutService);
|
||||
const editorService = useDependency(IEditorService);
|
||||
const contextService = useDependency(IContextService);
|
||||
const operationNamespace = useMemo(() => generateRandomId(4), []);
|
||||
const shouldMoveSelectionRef = useRef(shouldMoveSelection);
|
||||
shouldMoveSelectionRef.current = shouldMoveSelection;
|
||||
const onMoveInEditorRef = useRef(onMoveInEditor);
|
||||
onMoveInEditorRef.current = onMoveInEditor;
|
||||
const getRefSelectionCountRef = useRef(getRefSelectionCount);
|
||||
getRefSelectionCountRef.current = getRefSelectionCount;
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
useEffect(() => {
|
||||
if (!editor || !isNeed) {
|
||||
return;
|
||||
}
|
||||
const editorId = editor.getEditorId();
|
||||
const operationId = `sheet.formula-embedding-editor.${editorId}.${operationNamespace}`;
|
||||
const d = new DisposableCollection();
|
||||
const shouldHandleShortcut = () => {
|
||||
try {
|
||||
const fxBarFocused = contextService.getContextValue(FOCUSING_FX_BAR_EDITOR);
|
||||
return isFormulaEditorInteractionOwner(editorService.getFocusId(), editorId, { fxBarFocused }) &&
|
||||
(editor.docSelectionRenderService.isFocusing || fxBarFocused);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const handleMoveInEditor = (keycode: KeyCode, metaKey?: MetaKeys) => {
|
||||
if (onMoveInEditorRef.current) {
|
||||
onMoveInEditorRef.current(keycode, metaKey);
|
||||
return;
|
||||
}
|
||||
|
||||
let direction = Direction.LEFT;
|
||||
if (keycode === KeyCode.ARROW_DOWN) {
|
||||
direction = Direction.DOWN;
|
||||
} else if (keycode === KeyCode.ARROW_UP) {
|
||||
direction = Direction.UP;
|
||||
} else if (keycode === KeyCode.ARROW_RIGHT) {
|
||||
direction = Direction.RIGHT;
|
||||
}
|
||||
|
||||
if (metaKey === MetaKeys.SHIFT) {
|
||||
commandService.executeCommand(MoveSelectionOperation.id, {
|
||||
direction,
|
||||
});
|
||||
} else {
|
||||
commandService.executeCommand(MoveCursorOperation.id, {
|
||||
direction,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeycode = (keycode: KeyCode, metaKey?: MetaKeys) => {
|
||||
let direction = Direction.DOWN;
|
||||
if (keycode === KeyCode.ARROW_DOWN) {
|
||||
direction = Direction.DOWN;
|
||||
} else if (keycode === KeyCode.ARROW_UP) {
|
||||
direction = Direction.UP;
|
||||
} else if (keycode === KeyCode.ARROW_LEFT) {
|
||||
direction = Direction.LEFT;
|
||||
} else if (keycode === KeyCode.ARROW_RIGHT) {
|
||||
direction = Direction.RIGHT;
|
||||
}
|
||||
if (shouldMoveSelectionRef.current) {
|
||||
const fromCurrentSelection = shouldMoveFormulaSelectionFromCurrentSelection(
|
||||
shouldMoveSelectionRef.current,
|
||||
getRefSelectionCountRef.current?.() ?? 0
|
||||
);
|
||||
if (metaKey === MetaKeys.CTRL_COMMAND) {
|
||||
commandService.executeCommand(MoveSelectionCommand.id, {
|
||||
direction,
|
||||
jumpOver: JumpOver.moveGap,
|
||||
extra: 'formula-editor',
|
||||
fromCurrentSelection,
|
||||
});
|
||||
} else if (metaKey === MetaKeys.SHIFT) {
|
||||
commandService.executeCommand(ExpandSelectionCommand.id, {
|
||||
direction,
|
||||
extra: 'formula-editor',
|
||||
});
|
||||
} else if (metaKey === (MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT)) {
|
||||
commandService.executeCommand(ExpandSelectionCommand.id, {
|
||||
direction,
|
||||
jumpOver: JumpOver.moveGap,
|
||||
extra: 'formula-editor',
|
||||
});
|
||||
} else {
|
||||
commandService.executeCommand(MoveSelectionCommand.id, {
|
||||
direction,
|
||||
extra: 'formula-editor',
|
||||
fromCurrentSelection,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
handleMoveInEditor(keycode, metaKey);
|
||||
}
|
||||
};
|
||||
|
||||
d.add(commandService.registerCommand({
|
||||
id: operationId,
|
||||
type: CommandType.OPERATION,
|
||||
handler(_event, params) {
|
||||
const { keyCode, metaKey } = params as { eventType: DeviceInputEventType; keyCode: KeyCode; metaKey?: MetaKeys };
|
||||
handleKeycode(keyCode, metaKey);
|
||||
},
|
||||
}));
|
||||
|
||||
const keyCodes = [
|
||||
{ keyCode: KeyCode.ARROW_DOWN },
|
||||
{ keyCode: KeyCode.ARROW_LEFT },
|
||||
{ keyCode: KeyCode.ARROW_RIGHT },
|
||||
{ keyCode: KeyCode.ARROW_UP },
|
||||
{ keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.CTRL_COMMAND },
|
||||
{ keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.CTRL_COMMAND },
|
||||
{ keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.CTRL_COMMAND },
|
||||
{ keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.CTRL_COMMAND },
|
||||
{ keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT },
|
||||
{ keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT },
|
||||
];
|
||||
|
||||
keyCodes.map(({ keyCode, metaKey }) => {
|
||||
return {
|
||||
id: operationId,
|
||||
binding: metaKey ? keyCode | metaKey : keyCode,
|
||||
preconditions: shouldHandleShortcut,
|
||||
priority: 900,
|
||||
staticParameters: {
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keyCode,
|
||||
metaKey,
|
||||
},
|
||||
};
|
||||
}).forEach((item) => {
|
||||
d.add(shortcutService.registerShortcut(item));
|
||||
});
|
||||
|
||||
return () => {
|
||||
d.dispose();
|
||||
};
|
||||
}, [commandService, contextService, editor, editorService, isNeed, operationNamespace, shortcutService]);
|
||||
};
|
||||
@@ -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 { Workbook } from '@univerjs/core';
|
||||
import { EDITOR_ACTIVATED, IContextService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { IRefSelectionsService, REF_SELECTIONS_ENABLED } from '@univerjs/sheets';
|
||||
import { IContextMenuService, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useEffect, useLayoutEffect, useMemo } from 'react';
|
||||
import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render.service';
|
||||
|
||||
export const useRefactorEffect = (isNeed: boolean, selecting: boolean | number, unitId: string, editorId: string, disableContextMenu = true, keepRefSelectionsEnabled = false) => {
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const contextService = useDependency(IContextService);
|
||||
const contextMenuService = useDependency(IContextMenuService);
|
||||
const refSelectionsService = useDependency(IRefSelectionsService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const currentUnit = useObservable(useMemo(() => univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET), [univerInstanceService]));
|
||||
const render = renderManagerService.getRenderUnitById(currentUnit?.getUnitId() ?? '');
|
||||
const refSelectionsRenderService = render?.with(RefSelectionsRenderService);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isNeed) {
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, true);
|
||||
disableContextMenu && contextMenuService.disable();
|
||||
|
||||
return () => {
|
||||
const currentDoc = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)!;
|
||||
if (currentDoc?.getUnitId() === editorId) {
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
}
|
||||
|
||||
disableContextMenu && contextMenuService.enable();
|
||||
refSelectionsService.clear();
|
||||
};
|
||||
}
|
||||
}, [contextService, isNeed, refSelectionsService, disableContextMenu, editorId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isNeed && Boolean(selecting)) {
|
||||
let active = true;
|
||||
const d1 = refSelectionsRenderService?.enableSelectionChanging();
|
||||
contextService.setContextValue(REF_SELECTIONS_ENABLED, true);
|
||||
const subscription = keepRefSelectionsEnabled
|
||||
? contextService.subscribeContextValue$(REF_SELECTIONS_ENABLED).subscribe((enabled) => {
|
||||
if (!enabled) {
|
||||
queueMicrotask(() => {
|
||||
if (active) contextService.setContextValue(REF_SELECTIONS_ENABLED, true);
|
||||
});
|
||||
}
|
||||
})
|
||||
: null;
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
subscription?.unsubscribe();
|
||||
contextService.setContextValue(REF_SELECTIONS_ENABLED, false);
|
||||
d1?.dispose();
|
||||
};
|
||||
}
|
||||
}, [contextService, isNeed, keepRefSelectionsEnabled, refSelectionsRenderService, selecting]);
|
||||
|
||||
// reset setSkipLastEnabled
|
||||
useEffect(() => {
|
||||
if (isNeed) {
|
||||
refSelectionsRenderService?.setSkipLastEnabled(false);
|
||||
}
|
||||
}, [isNeed, refSelectionsRenderService]);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user