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/find-replace/README.md
Normal file
41
packages/find-replace/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/find-replace
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/find-replace)
|
||||
[](https://npmjs.com/package/@univerjs/find-replace)
|
||||
[](https://npmjs.com/package/@univerjs/find-replace)
|
||||
|
||||
`@univerjs/find-replace` provides shared find and replace services and UI infrastructure that product-specific packages can extend.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/find-replace` | `UniverFindReplace` | Yes | Yes | No |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/find-replace
|
||||
# or
|
||||
npm install @univerjs/find-replace
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/find-replace/lib/index.css';
|
||||
import EnUS from '@univerjs/find-replace/locale/en-US';
|
||||
import { UniverFindReplacePlugin } from '@univerjs/find-replace';
|
||||
|
||||
univer.registerPlugin(UniverFindReplacePlugin);
|
||||
|
||||
// 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/find-replace)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
2086
packages/find-replace/docs/find-replace.excalidraw
Normal file
2086
packages/find-replace/docs/find-replace.excalidraw
Normal file
File diff suppressed because it is too large
Load Diff
92
packages/find-replace/package.json
Normal file
92
packages/find-replace/package.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"name": "@univerjs/find-replace",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Shared find and replace services and UI infrastructure for Univer.",
|
||||
"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",
|
||||
"find-replace",
|
||||
"search",
|
||||
"replace",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.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"
|
||||
},
|
||||
"./lib/*": "./lib/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/find-replace/postcss.config.mjs
Normal file
1
packages/find-replace/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
126
packages/find-replace/src/__tests__/create-test-bed.ts
Normal file
126
packages/find-replace/src/__tests__/create-test-bed.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { IMessageProps } from '@univerjs/design';
|
||||
import {
|
||||
Disposable,
|
||||
IConfirmService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
TestConfirmService,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import enUS from '../locale/en-US';
|
||||
|
||||
const TEST_WORKBOOK_DATA_DEMO: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
0: {
|
||||
v: 'A1',
|
||||
},
|
||||
1: {
|
||||
v: 'A2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
|
||||
class TestMessageService implements IMessageService {
|
||||
readonly messages: IMessageProps[] = [];
|
||||
|
||||
show(options: IMessageProps): Disposable {
|
||||
this.messages.push(options);
|
||||
return new Disposable();
|
||||
}
|
||||
|
||||
remove(_id: string): void {}
|
||||
|
||||
removeAll(): void {
|
||||
this.messages.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: Workbook;
|
||||
}
|
||||
|
||||
export function createTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'find-replace-test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([IConfirmService, { useClass: TestConfirmService }]);
|
||||
this._injector.add([IMessageService, { useClass: TestMessageService }]);
|
||||
dependencies?.forEach((d) => this._injector.add(d));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const snapshot = Tools.deepClone(workbookData || TEST_WORKBOOK_DATA_DEMO);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, snapshot);
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit(sheet.getUnitId());
|
||||
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const localeService = injector.get(LocaleService);
|
||||
localeService.load({ enUS });
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
|
||||
export { TestMessageService };
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Injector, Univer } from '@univerjs/core';
|
||||
import type { TestMessageService } from '../../../__tests__/create-test-bed';
|
||||
import { ICommandService, IConfirmService } from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createTestBed } from '../../../__tests__/create-test-bed';
|
||||
import { FindReplaceService, IFindReplaceService } from '../../../services/find-replace.service';
|
||||
import { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from '../replace.command';
|
||||
|
||||
describe('replace.command', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createTestBed(undefined, [[IFindReplaceService, { useClass: FindReplaceService }]]);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
commandService.registerCommand(ReplaceCurrentMatchCommand);
|
||||
commandService.registerCommand(ReplaceAllMatchesCommand);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('should proxy ReplaceCurrentMatchCommand to findReplaceService.replace', async () => {
|
||||
const commandService = get(ICommandService);
|
||||
const findReplaceService = get(IFindReplaceService);
|
||||
const replaceSpy = vi.spyOn(findReplaceService, 'replace').mockResolvedValue(true);
|
||||
|
||||
await expect(commandService.executeCommand(ReplaceCurrentMatchCommand.id)).resolves.toBe(true);
|
||||
expect(replaceSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return false when confirm is canceled for replace all', async () => {
|
||||
const commandService = get(ICommandService);
|
||||
const findReplaceService = get(IFindReplaceService);
|
||||
const confirmService = get(IConfirmService);
|
||||
const messageService = get(IMessageService) as TestMessageService;
|
||||
|
||||
vi.spyOn(confirmService, 'confirm').mockResolvedValue(false);
|
||||
const replaceAllSpy = vi.spyOn(findReplaceService, 'replaceAll');
|
||||
|
||||
await expect(commandService.executeCommand(ReplaceAllMatchesCommand.id)).resolves.toBe(false);
|
||||
expect(replaceAllSpy).not.toHaveBeenCalled();
|
||||
expect(messageService.messages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should show success message when all replacements succeed', async () => {
|
||||
const commandService = get(ICommandService);
|
||||
const findReplaceService = get(IFindReplaceService);
|
||||
const messageService = get(IMessageService) as TestMessageService;
|
||||
vi.spyOn(findReplaceService, 'replaceAll').mockResolvedValue({ success: 3, failure: 0 });
|
||||
|
||||
await expect(commandService.executeCommand(ReplaceAllMatchesCommand.id)).resolves.toBe(true);
|
||||
expect(messageService.messages[0]).toEqual(expect.objectContaining({ type: MessageType.Success }));
|
||||
});
|
||||
|
||||
it('should show warning when partially succeeds and return false', async () => {
|
||||
const commandService = get(ICommandService);
|
||||
const findReplaceService = get(IFindReplaceService);
|
||||
const messageService = get(IMessageService) as TestMessageService;
|
||||
vi.spyOn(findReplaceService, 'replaceAll').mockResolvedValue({ success: 2, failure: 1 });
|
||||
|
||||
await expect(commandService.executeCommand(ReplaceAllMatchesCommand.id)).resolves.toBe(false);
|
||||
expect(messageService.messages[0]).toEqual(expect.objectContaining({ type: MessageType.Warning }));
|
||||
});
|
||||
|
||||
it('should show error when all replacements fail and return false', async () => {
|
||||
const commandService = get(ICommandService);
|
||||
const findReplaceService = get(IFindReplaceService);
|
||||
const messageService = get(IMessageService) as TestMessageService;
|
||||
vi.spyOn(findReplaceService, 'replaceAll').mockResolvedValue({ success: 0, failure: 2 });
|
||||
|
||||
await expect(commandService.executeCommand(ReplaceAllMatchesCommand.id)).resolves.toBe(false);
|
||||
expect(messageService.messages[0]).toEqual(expect.objectContaining({ type: MessageType.Error }));
|
||||
});
|
||||
});
|
||||
@@ -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 { ICommand } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, IConfirmService, LocaleService } from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IMessageService } from '@univerjs/ui';
|
||||
import { IFindReplaceService } from '../../services/find-replace.service';
|
||||
|
||||
export const ReplaceCurrentMatchCommand: ICommand = {
|
||||
id: 'ui.command.replace-current-match',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
return findReplaceService.replace();
|
||||
},
|
||||
};
|
||||
|
||||
const CONFIRM_REPLACE_ALL_ID = 'CONFIRM_REPLACE_ALL';
|
||||
|
||||
export const ReplaceAllMatchesCommand: ICommand = {
|
||||
id: 'ui.command.replace-all-matches',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const confirmService = accessor.get(IConfirmService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const messageService = accessor.get(IMessageService);
|
||||
|
||||
if (!await confirmService.confirm({
|
||||
id: CONFIRM_REPLACE_ALL_ID,
|
||||
title: { title: localeService.t<LocaleKey>('find-replace.replace.confirm.title') },
|
||||
cancelText: localeService.t<LocaleKey>('find-replace.button.cancel'),
|
||||
confirmText: localeService.t<LocaleKey>('find-replace.button.confirm'),
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
const result = await findReplaceService.replaceAll();
|
||||
const { success, failure } = result;
|
||||
|
||||
if (failure > 0) {
|
||||
if (success === 0) {
|
||||
messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: localeService.t<LocaleKey>('find-replace.replace.all-failure'),
|
||||
});
|
||||
} else {
|
||||
messageService.show({
|
||||
type: MessageType.Warning,
|
||||
content: localeService.t<LocaleKey>('find-replace.replace.partial-success', `${success}`, `${failure}`),
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
type: MessageType.Success,
|
||||
content: localeService.t<LocaleKey>('find-replace.replace.all-success', `${success}`),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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 { IFindReplaceProvider } from '../../../services/find-replace.service';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createTestBed } from '../../../__tests__/create-test-bed';
|
||||
import { FindReplaceService, IFindReplaceService } from '../../../services/find-replace.service';
|
||||
import {
|
||||
CloseFindDialogOperation,
|
||||
FocusSelectionOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
} from '../find-replace.operation';
|
||||
|
||||
function createProvider(): IFindReplaceProvider {
|
||||
return {
|
||||
capabilities: {
|
||||
caseSensitive: true,
|
||||
matchesTheWholeWord: false,
|
||||
matchesTheWholeCell: true,
|
||||
findDirection: true,
|
||||
findScope: true,
|
||||
findBy: true,
|
||||
},
|
||||
isSupported: () => true,
|
||||
find: vi.fn(async () => []),
|
||||
terminate: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('find-replace.operation', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createTestBed(undefined, [[IFindReplaceService, { useClass: FindReplaceService }]]);
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
|
||||
const commandService = get(ICommandService);
|
||||
[
|
||||
CloseFindDialogOperation,
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
FocusSelectionOperation,
|
||||
].forEach((op) => commandService.registerCommand(op));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('should open find dialog or focus input based on reveal state', () => {
|
||||
const commandService = get(ICommandService);
|
||||
const findReplaceService = get(IFindReplaceService);
|
||||
findReplaceService.registerFindReplaceProvider(createProvider());
|
||||
|
||||
const startSpy = vi.spyOn(findReplaceService, 'start');
|
||||
const focusSpy = vi.spyOn(findReplaceService, 'focusFindInput');
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFindDialogOperation.id)).toBe(true);
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
expect(focusSpy).not.toHaveBeenCalled();
|
||||
|
||||
expect(commandService.syncExecuteCommand(OpenFindDialogOperation.id)).toBe(true);
|
||||
expect(focusSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should open replace dialog with proper branching', () => {
|
||||
const commandService = get(ICommandService);
|
||||
|
||||
const hiddenService = get(IFindReplaceService);
|
||||
hiddenService.registerFindReplaceProvider(createProvider());
|
||||
const hiddenStartSpy = vi.spyOn(hiddenService, 'start');
|
||||
expect(commandService.syncExecuteCommand(OpenReplaceDialogOperation.id)).toBe(true);
|
||||
expect(hiddenStartSpy).toHaveBeenCalledWith(true);
|
||||
hiddenService.terminate();
|
||||
|
||||
const revealService = get(IFindReplaceService);
|
||||
revealService.registerFindReplaceProvider(createProvider());
|
||||
revealService.start();
|
||||
const revealSpy = vi.spyOn(revealService, 'revealReplace');
|
||||
expect(commandService.syncExecuteCommand(OpenReplaceDialogOperation.id)).toBe(true);
|
||||
expect(revealSpy).toHaveBeenCalledTimes(1);
|
||||
revealService.terminate();
|
||||
|
||||
const focusService = get(IFindReplaceService);
|
||||
focusService.registerFindReplaceProvider(createProvider());
|
||||
focusService.start(true);
|
||||
const focusSpy = vi.spyOn(focusService, 'focusFindInput');
|
||||
expect(commandService.syncExecuteCommand(OpenReplaceDialogOperation.id)).toBe(true);
|
||||
expect(focusSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should route next/previous/focus operations to the service', () => {
|
||||
const commandService = get(ICommandService);
|
||||
const service = get(IFindReplaceService);
|
||||
|
||||
const nextSpy = vi.spyOn(service, 'moveToNextMatch');
|
||||
const previousSpy = vi.spyOn(service, 'moveToPreviousMatch');
|
||||
const focusSpy = vi.spyOn(service, 'focusSelection');
|
||||
|
||||
expect(commandService.syncExecuteCommand(GoToNextMatchOperation.id)).toBe(true);
|
||||
expect(commandService.syncExecuteCommand(GoToPreviousMatchOperation.id)).toBe(true);
|
||||
expect(commandService.syncExecuteCommand(FocusSelectionOperation.id)).toBe(true);
|
||||
|
||||
expect(nextSpy).toHaveBeenCalledTimes(1);
|
||||
expect(previousSpy).toHaveBeenCalledTimes(1);
|
||||
expect(focusSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should terminate the current session when closing the find UI', () => {
|
||||
const commandService = get(ICommandService);
|
||||
const service = get(IFindReplaceService);
|
||||
service.registerFindReplaceProvider(createProvider());
|
||||
service.start();
|
||||
const terminateSpy = vi.spyOn(service, 'terminate');
|
||||
|
||||
expect(commandService.syncExecuteCommand(CloseFindDialogOperation.id)).toBe(true);
|
||||
expect(terminateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(service.revealed).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IOperation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IFindReplaceService } from '../../services/find-replace.service';
|
||||
|
||||
export const OpenFindDialogOperation: IOperation = {
|
||||
id: 'ui.operation.open-find-dialog',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor) => {
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
if (!findReplaceService.revealed) {
|
||||
findReplaceService.start();
|
||||
} else {
|
||||
findReplaceService.focusFindInput();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenReplaceDialogOperation: IOperation = {
|
||||
id: 'ui.operation.open-replace-dialog',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor) => {
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
if (!findReplaceService.revealed) {
|
||||
findReplaceService.start(true);
|
||||
} else if (!findReplaceService.replaceRevealed) {
|
||||
findReplaceService.revealReplace();
|
||||
} else {
|
||||
findReplaceService.focusFindInput();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseFindDialogOperation: IOperation = {
|
||||
id: 'ui.operation.close-find-dialog',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor) => {
|
||||
accessor.get(IFindReplaceService).terminate();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const GoToNextMatchOperation: IOperation = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'ui.operation.go-to-next-match',
|
||||
handler: (accessor) => {
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
findReplaceService.moveToNextMatch();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const GoToPreviousMatchOperation: IOperation = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'ui.operation.go-to-previous-match',
|
||||
handler: (accessor) => {
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
findReplaceService.moveToPreviousMatch();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const FocusSelectionOperation: IOperation = {
|
||||
type: CommandType.OPERATION,
|
||||
id: 'ui.operation.focus-selection',
|
||||
handler: (accessor) => {
|
||||
const findReplaceService = accessor.get(IFindReplaceService);
|
||||
findReplaceService.focusSelection();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
27
packages/find-replace/src/config/config.ts
Normal file
27
packages/find-replace/src/config/config.ts
Normal file
@@ -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 { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const FIND_REPLACE_PLUGIN_CONFIG_KEY = 'find-replace.config';
|
||||
|
||||
export const configSymbol = Symbol(FIND_REPLACE_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverFindReplaceConfig {
|
||||
menu?: MenuConfig;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverFindReplaceConfig = {};
|
||||
18
packages/find-replace/src/const.ts
Normal file
18
packages/find-replace/src/const.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 MOBILE_FIND_REPLACE_SETTINGS_COMPONENT = 'MobileFindReplaceSettings';
|
||||
export const MOBILE_FIND_REPLACE_SETTINGS_DIALOG_ID = 'MOBILE_FIND_REPLACE_SETTINGS_DIALOG';
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { afterEach, describe, expect, it } from 'vitest';
|
||||
import { getFindReplaceDialogDefaultPosition } from '../find-replace.controller';
|
||||
|
||||
const ORIGINAL_INNER_WIDTH = window.innerWidth;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: ORIGINAL_INNER_WIDTH });
|
||||
});
|
||||
|
||||
describe('getFindReplaceDialogDefaultPosition', () => {
|
||||
it('anchors the dialog to the matching physical edge for each UI direction', () => {
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1000 });
|
||||
|
||||
expect(getFindReplaceDialogDefaultPosition('ltr')).toEqual({ x: 630, y: 64 });
|
||||
expect(getFindReplaceDialogDefaultPosition('rtl')).toEqual({ x: 20, y: 64 });
|
||||
});
|
||||
});
|
||||
@@ -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 { Injector, Univer } from '@univerjs/core';
|
||||
import { EDITOR_ACTIVATED, IContextService } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTestBed } from '../../__tests__/create-test-bed';
|
||||
import {
|
||||
FIND_REPLACE_AVAILABLE,
|
||||
FIND_REPLACE_DIALOG_FOCUS,
|
||||
FIND_REPLACE_INPUT_FOCUS,
|
||||
FIND_REPLACE_REPLACE_REVEALED,
|
||||
} from '../../services/context-keys';
|
||||
import {
|
||||
FocusSelectionShortcutItem,
|
||||
GoToNextFindMatchShortcutItem,
|
||||
GoToPreviousFindMatchShortcutItem,
|
||||
MacOpenFindDialogShortcutItem,
|
||||
OpenFindDialogShortcutItem,
|
||||
OpenReplaceDialogShortcutItem,
|
||||
} from '../find-replace.shortcut';
|
||||
|
||||
describe('find-replace.shortcut', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let contextService: IContextService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
contextService = get(IContextService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('should require provider availability and editor inactivity for opening find dialog', () => {
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, false);
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, true);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
|
||||
expect(OpenFindDialogShortcutItem.preconditions!(contextService)).toBe(true);
|
||||
expect(MacOpenFindDialogShortcutItem.preconditions!(contextService)).toBe(true);
|
||||
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, true);
|
||||
expect(OpenFindDialogShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
});
|
||||
|
||||
it('should open replace dialog only when dialog is hidden or replace is not revealed', () => {
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, false);
|
||||
contextService.setContextValue(FIND_REPLACE_REPLACE_REVEALED, false);
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, true);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
expect(OpenReplaceDialogShortcutItem.preconditions!(contextService)).toBe(true);
|
||||
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, true);
|
||||
contextService.setContextValue(FIND_REPLACE_REPLACE_REVEALED, true);
|
||||
expect(OpenReplaceDialogShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable open shortcuts when no provider is available', () => {
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, false);
|
||||
contextService.setContextValue(FIND_REPLACE_REPLACE_REVEALED, false);
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, false);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
|
||||
expect(OpenFindDialogShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
expect(MacOpenFindDialogShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
expect(OpenReplaceDialogShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
});
|
||||
|
||||
it('should require input and dialog focus for next/previous actions', () => {
|
||||
contextService.setContextValue(FIND_REPLACE_INPUT_FOCUS, true);
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, true);
|
||||
expect(GoToNextFindMatchShortcutItem.preconditions!(contextService)).toBe(true);
|
||||
expect(GoToPreviousFindMatchShortcutItem.preconditions!(contextService)).toBe(true);
|
||||
|
||||
contextService.setContextValue(FIND_REPLACE_INPUT_FOCUS, false);
|
||||
expect(GoToNextFindMatchShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
});
|
||||
|
||||
it('should focus selection when dialog is focused', () => {
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, true);
|
||||
expect(FocusSelectionShortcutItem.preconditions!(contextService)).toBe(true);
|
||||
contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, false);
|
||||
expect(FocusSelectionShortcutItem.preconditions!(contextService)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 { SearchIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { FindReplaceDialog } from '../views/dialog/FindReplaceDialog';
|
||||
|
||||
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({
|
||||
SearchIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
([
|
||||
['FindReplaceDialog', FindReplaceDialog],
|
||||
] as const).forEach(([key, comp]) => {
|
||||
this.disposeWithMe(
|
||||
this._componentManager.register(key, comp)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
173
packages/find-replace/src/controllers/find-replace.controller.ts
Normal file
173
packages/find-replace/src/controllers/find-replace.controller.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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, Nullable } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
RxDisposable,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { IDialogService, ILayoutService, IMenuManagerService, IShortcutService } from '@univerjs/ui';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from '../commands/commands/replace.command';
|
||||
import {
|
||||
CloseFindDialogOperation,
|
||||
FocusSelectionOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
} from '../commands/operations/find-replace.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import { IFindReplaceService } from '../services/find-replace.service';
|
||||
import {
|
||||
FocusSelectionShortcutItem,
|
||||
GoToNextFindMatchShortcutItem,
|
||||
GoToPreviousFindMatchShortcutItem,
|
||||
MacOpenFindDialogShortcutItem,
|
||||
OpenFindDialogShortcutItem,
|
||||
OpenReplaceDialogShortcutItem,
|
||||
} from './find-replace.shortcut';
|
||||
|
||||
const FIND_REPLACE_DIALOG_ID = 'DESKTOP_FIND_REPLACE_DIALOG';
|
||||
|
||||
const FIND_REPLACE_PANEL_WIDTH = 350;
|
||||
const FIND_REPLACE_PANEL_HORIZONTAL_PADDING = 20;
|
||||
const FIND_REPLACE_PANEL_TOP_PADDING = 64;
|
||||
|
||||
export class FindReplaceController extends RxDisposable {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IFindReplaceService private readonly _findReplaceService: IFindReplaceService,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
@ILayoutService private readonly _layoutService: ILayoutService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initUI();
|
||||
this._initShortcuts();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._closingListenerDisposable?.dispose();
|
||||
this._closingListenerDisposable = null;
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
CloseFindDialogOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
ReplaceAllMatchesCommand,
|
||||
ReplaceCurrentMatchCommand,
|
||||
FocusSelectionOperation,
|
||||
].forEach((c) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(c));
|
||||
});
|
||||
}
|
||||
|
||||
private _initShortcuts(): void {
|
||||
[
|
||||
OpenReplaceDialogShortcutItem,
|
||||
OpenFindDialogShortcutItem,
|
||||
MacOpenFindDialogShortcutItem,
|
||||
GoToPreviousFindMatchShortcutItem,
|
||||
GoToNextFindMatchShortcutItem,
|
||||
FocusSelectionShortcutItem,
|
||||
].forEach((s) => this.disposeWithMe(this._shortcutService.registerShortcut(s)));
|
||||
}
|
||||
|
||||
private _initUI(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
|
||||
// this controller is also responsible for toggling the FindReplaceDialog
|
||||
this._findReplaceService.stateUpdates$.pipe(takeUntil(this.dispose$)).subscribe((newState) => {
|
||||
if (newState.revealed === true) {
|
||||
this._openPanel();
|
||||
} else if (newState.revealed === false) {
|
||||
this._closePanel(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _openPanel(): void {
|
||||
const sessionUnitId = this._univerInstanceService.getFocusedUnit()?.getUnitId();
|
||||
this._dialogService.open({
|
||||
id: FIND_REPLACE_DIALOG_ID,
|
||||
draggable: true,
|
||||
width: FIND_REPLACE_PANEL_WIDTH,
|
||||
title: { title: this._localeService.t<LocaleKey>('find-replace.dialog.title') },
|
||||
children: { label: 'FindReplaceDialog' },
|
||||
mask: false,
|
||||
maskClosable: false,
|
||||
defaultPosition: getFindReplaceDialogDefaultPosition(this._localeService.getDirection()),
|
||||
preservePositionOnDestroy: true,
|
||||
onClose: () => this.closePanel(),
|
||||
});
|
||||
|
||||
this._closingListenerDisposable = toDisposable(
|
||||
this._univerInstanceService.focused$.pipe(takeUntil(this.dispose$)).subscribe((focused) => {
|
||||
if (!focused || focused !== sessionUnitId) {
|
||||
this.closePanel();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _closingListenerDisposable: Nullable<IDisposable>;
|
||||
closePanel(): void {
|
||||
this._closePanel(true);
|
||||
}
|
||||
|
||||
private _closePanel(terminateSession: boolean): void {
|
||||
if (!this._closingListenerDisposable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._closingListenerDisposable.dispose();
|
||||
this._closingListenerDisposable = null;
|
||||
|
||||
this._dialogService.close(FIND_REPLACE_DIALOG_ID);
|
||||
if (terminateSession) {
|
||||
this._findReplaceService.terminate();
|
||||
}
|
||||
|
||||
queueMicrotask(() => this._layoutService.focus());
|
||||
}
|
||||
}
|
||||
|
||||
export function getFindReplaceDialogDefaultPosition(direction: 'ltr' | 'rtl'): { x: number; y: number } {
|
||||
const { innerWidth } = window;
|
||||
const x = direction === 'rtl'
|
||||
? FIND_REPLACE_PANEL_HORIZONTAL_PADDING
|
||||
: (innerWidth - FIND_REPLACE_PANEL_WIDTH) - FIND_REPLACE_PANEL_HORIZONTAL_PADDING;
|
||||
const y = FIND_REPLACE_PANEL_TOP_PADDING;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
124
packages/find-replace/src/controllers/find-replace.shortcut.ts
Normal file
124
packages/find-replace/src/controllers/find-replace.shortcut.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 { IContextService } from '@univerjs/core';
|
||||
import type { IShortcutItem } from '@univerjs/ui';
|
||||
import { EDITOR_ACTIVATED } from '@univerjs/core';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
import {
|
||||
FocusSelectionOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
} from '../commands/operations/find-replace.operation';
|
||||
import {
|
||||
FIND_REPLACE_AVAILABLE,
|
||||
FIND_REPLACE_DIALOG_FOCUS,
|
||||
FIND_REPLACE_INPUT_FOCUS,
|
||||
FIND_REPLACE_REPLACE_REVEALED,
|
||||
} from '../services/context-keys';
|
||||
|
||||
function whenFindReplaceDialogFocused(contextService: IContextService): boolean {
|
||||
return contextService.getContextValue(FIND_REPLACE_DIALOG_FOCUS);
|
||||
}
|
||||
|
||||
function whenReplaceRevealed(contextService: IContextService): boolean {
|
||||
return contextService.getContextValue(FIND_REPLACE_REPLACE_REVEALED);
|
||||
}
|
||||
|
||||
function whenFindReplaceInputFocused(contextService: IContextService): boolean {
|
||||
return contextService.getContextValue(FIND_REPLACE_INPUT_FOCUS);
|
||||
}
|
||||
|
||||
const FIND_REPLACE_SHORTCUT_GROUP = '7_find-replace-shortcuts';
|
||||
|
||||
function whenFindReplaceAvailable(contextService: IContextService): boolean {
|
||||
return contextService.getContextValue(FIND_REPLACE_AVAILABLE);
|
||||
}
|
||||
|
||||
function whenEditorNotActivated(contextService: IContextService): boolean {
|
||||
return !contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
}
|
||||
|
||||
export const OpenFindDialogShortcutItem: IShortcutItem = {
|
||||
id: OpenFindDialogOperation.id,
|
||||
description: 'find-replace.shortcut.open-find-dialog',
|
||||
binding: KeyCode.F | MetaKeys.CTRL_COMMAND,
|
||||
group: FIND_REPLACE_SHORTCUT_GROUP,
|
||||
groupTitle: 'find-replace.shortcut.panel',
|
||||
preconditions(contextService) {
|
||||
return !whenFindReplaceDialogFocused(contextService) && whenFindReplaceAvailable(contextService) && whenEditorNotActivated(contextService);
|
||||
},
|
||||
};
|
||||
|
||||
export const MacOpenFindDialogShortcutItem: IShortcutItem = {
|
||||
id: OpenFindDialogOperation.id,
|
||||
description: 'find-replace.shortcut.open-find-dialog',
|
||||
binding: KeyCode.F | MetaKeys.CTRL_COMMAND,
|
||||
mac: KeyCode.F | MetaKeys.MAC_CTRL,
|
||||
preconditions(contextService) {
|
||||
return !whenFindReplaceDialogFocused(contextService) && whenFindReplaceAvailable(contextService) && whenEditorNotActivated(contextService);
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenReplaceDialogShortcutItem: IShortcutItem = {
|
||||
id: OpenReplaceDialogOperation.id,
|
||||
description: 'find-replace.shortcut.open-replace-dialog',
|
||||
binding: KeyCode.H | MetaKeys.CTRL_COMMAND,
|
||||
mac: KeyCode.H | MetaKeys.MAC_CTRL,
|
||||
group: FIND_REPLACE_SHORTCUT_GROUP,
|
||||
groupTitle: 'find-replace.shortcut.panel',
|
||||
preconditions(contextService) {
|
||||
return whenFindReplaceAvailable(contextService) && whenEditorNotActivated(contextService) && (!whenFindReplaceDialogFocused(contextService) || !whenReplaceRevealed(contextService));
|
||||
},
|
||||
};
|
||||
|
||||
export const GoToNextFindMatchShortcutItem: IShortcutItem = {
|
||||
id: GoToNextMatchOperation.id,
|
||||
description: 'find-replace.shortcut.go-to-next-match',
|
||||
binding: KeyCode.ENTER,
|
||||
group: FIND_REPLACE_SHORTCUT_GROUP,
|
||||
groupTitle: 'find-replace.shortcut.panel',
|
||||
priority: 1000,
|
||||
preconditions(contextService) {
|
||||
return whenFindReplaceInputFocused(contextService) && whenFindReplaceDialogFocused(contextService);
|
||||
},
|
||||
};
|
||||
|
||||
export const GoToPreviousFindMatchShortcutItem: IShortcutItem = {
|
||||
id: GoToPreviousMatchOperation.id,
|
||||
description: 'find-replace.shortcut.go-to-previous-match',
|
||||
binding: KeyCode.ENTER | MetaKeys.SHIFT,
|
||||
group: FIND_REPLACE_SHORTCUT_GROUP,
|
||||
groupTitle: 'find-replace.shortcut.panel',
|
||||
priority: 1000,
|
||||
preconditions(contextService) {
|
||||
return whenFindReplaceInputFocused(contextService) && whenFindReplaceDialogFocused(contextService);
|
||||
},
|
||||
};
|
||||
|
||||
export const FocusSelectionShortcutItem: IShortcutItem = {
|
||||
id: FocusSelectionOperation.id,
|
||||
description: 'find-replace.shortcut.focus-selection',
|
||||
binding: KeyCode.ESC,
|
||||
group: FIND_REPLACE_SHORTCUT_GROUP,
|
||||
groupTitle: 'find-replace.shortcut.panel',
|
||||
priority: 1000,
|
||||
preconditions(contextService) {
|
||||
return whenFindReplaceDialogFocused(contextService);
|
||||
},
|
||||
};
|
||||
@@ -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 { Disposable, Inject } from '@univerjs/core';
|
||||
import { SearchIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { MOBILE_FIND_REPLACE_SETTINGS_COMPONENT } from '../../const';
|
||||
import { MobileFindReplaceSettings } from '../../views/mobile/MobileFindReplaceSettings';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) componentManager: ComponentManager,
|
||||
@Inject(IconManager) iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(iconManager.register({ SearchIcon }));
|
||||
this.disposeWithMe(componentManager.register(
|
||||
MOBILE_FIND_REPLACE_SETTINGS_COMPONENT,
|
||||
MobileFindReplaceSettings
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ICommandService, Inject, Injector, IUniverInstanceService, RxDisposable } from '@univerjs/core';
|
||||
import { BuiltInUIPart, connectInjector, IDialogService, IMenuManagerService, IUIPartsService } from '@univerjs/ui';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from '../../commands/commands/replace.command';
|
||||
import {
|
||||
CloseFindDialogOperation,
|
||||
FocusSelectionOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
} from '../../commands/operations/find-replace.operation';
|
||||
import { MOBILE_FIND_REPLACE_SETTINGS_DIALOG_ID } from '../../const';
|
||||
import { mobileMenuSchema } from '../../menu/mobile-schema';
|
||||
import { IFindReplaceService } from '../../services/find-replace.service';
|
||||
import { MobileFindReplaceBar } from '../../views/mobile/MobileFindReplaceBar';
|
||||
|
||||
export class FindReplaceMobileController extends RxDisposable {
|
||||
constructor(
|
||||
@Inject(Injector) injector: Injector,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IDialogService dialogService: IDialogService,
|
||||
@IFindReplaceService findReplaceService: IFindReplaceService,
|
||||
@IMenuManagerService menuManagerService: IMenuManagerService,
|
||||
@IUniverInstanceService univerInstanceService: IUniverInstanceService,
|
||||
@IUIPartsService uiPartsService: IUIPartsService
|
||||
) {
|
||||
super();
|
||||
|
||||
[
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
CloseFindDialogOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
ReplaceAllMatchesCommand,
|
||||
ReplaceCurrentMatchCommand,
|
||||
FocusSelectionOperation,
|
||||
].forEach((command) => this.disposeWithMe(commandService.registerCommand(command)));
|
||||
|
||||
menuManagerService.mergeMenu(mobileMenuSchema);
|
||||
this.disposeWithMe(uiPartsService.registerComponent(
|
||||
BuiltInUIPart.FOOTER,
|
||||
() => connectInjector(MobileFindReplaceBar, injector)
|
||||
));
|
||||
|
||||
findReplaceService.stateUpdates$.pipe(takeUntil(this.dispose$)).subscribe((state) => {
|
||||
if (state.revealed === false) {
|
||||
dialogService.close(MOBILE_FIND_REPLACE_SETTINGS_DIALOG_ID);
|
||||
}
|
||||
});
|
||||
|
||||
let sessionUnitId: string | null = null;
|
||||
findReplaceService.stateUpdates$.pipe(takeUntil(this.dispose$)).subscribe((state) => {
|
||||
if (state.revealed === true) {
|
||||
sessionUnitId = univerInstanceService.getFocusedUnit()?.getUnitId() ?? null;
|
||||
} else if (state.revealed === false) {
|
||||
sessionUnitId = null;
|
||||
}
|
||||
});
|
||||
|
||||
univerInstanceService.focused$.pipe(takeUntil(this.dispose$)).subscribe((focused) => {
|
||||
if (sessionUnitId && focused !== sessionUnitId) {
|
||||
findReplaceService.terminate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
2
packages/find-replace/src/global.css
Normal file
2
packages/find-replace/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
51
packages/find-replace/src/index.ts
Normal file
51
packages/find-replace/src/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './global.css';
|
||||
|
||||
export { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from './commands/commands/replace.command';
|
||||
export {
|
||||
CloseFindDialogOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
} from './commands/operations/find-replace.operation';
|
||||
export type { IUniverFindReplaceConfig } from './config/config';
|
||||
export { FindReplaceController } from './controllers/find-replace.controller';
|
||||
export { menuSchema as FindReplaceMenuSchema } from './menu/schema';
|
||||
export { UniverFindReplaceMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverFindReplacePlugin } from './plugin';
|
||||
export type {
|
||||
IFindComplete,
|
||||
IFindMatch,
|
||||
IFindMoveParams,
|
||||
IFindQuery,
|
||||
IFindReplaceProvider,
|
||||
IFindReplaceState,
|
||||
IReplaceAllResult,
|
||||
} from './services/find-replace.service';
|
||||
export {
|
||||
createInitFindReplaceState,
|
||||
FindBy,
|
||||
FindDirection,
|
||||
FindModel,
|
||||
FindReplaceModel,
|
||||
FindReplaceService,
|
||||
FindReplaceState,
|
||||
FindScope,
|
||||
IFindReplaceService,
|
||||
} from './services/find-replace.service';
|
||||
81
packages/find-replace/src/locale/ar-SA.ts
Normal file
81
packages/find-replace/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'بحث واستبدال',
|
||||
mobile: {
|
||||
settings: 'إعدادات البحث والاستبدال',
|
||||
'settings-title': 'إعدادات البحث والاستبدال',
|
||||
'previous-match': 'التطابق السابق',
|
||||
'next-match': 'التطابق التالي',
|
||||
close: 'إغلاق البحث والاستبدال',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'فتح مربع حوار البحث',
|
||||
'open-replace-dialog': 'فتح مربع حوار الاستبدال',
|
||||
'go-to-next-match': 'الانتقال إلى التطابق التالي',
|
||||
'go-to-previous-match': 'الانتقال إلى التطابق السابق',
|
||||
'focus-selection': 'تركيز التحديد',
|
||||
panel: 'بحث واستبدال',
|
||||
},
|
||||
dialog: {
|
||||
title: 'بحث',
|
||||
find: 'بحث',
|
||||
replace: 'استبدال',
|
||||
'replace-all': 'استبدال الكل',
|
||||
'case-sensitive': 'حساس لحالة الأحرف',
|
||||
'find-placeholder': 'البحث',
|
||||
'advanced-finding': 'بحث واستبدال متقدم',
|
||||
'replace-placeholder': 'أدخل نص الاستبدال',
|
||||
'match-the-whole-cell': 'تطابق الخلية بأكملها',
|
||||
'match-the-whole-word': 'مطابقة الكلمة بالكامل',
|
||||
'find-direction': {
|
||||
title: 'اتجاه البحث',
|
||||
row: 'البحث حسب الصف',
|
||||
column: 'البحث حسب العمود',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'نطاق البحث',
|
||||
'current-sheet': 'الورقة الحالية',
|
||||
workbook: 'المصنف',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'البحث حسب',
|
||||
value: 'البحث حسب القيمة',
|
||||
formula: 'البحث في الصيغ',
|
||||
},
|
||||
'no-match': 'اكتمل البحث ولكن لم يتم العثور على أي تطابق.',
|
||||
'no-result': 'لا توجد نتائج',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'تم استبدال {0} تطابقات',
|
||||
'partial-success': 'تم استبدال {0} تطابقات، وتعذر استبدال {1}',
|
||||
'all-failure': 'فشل الاستبدال',
|
||||
confirm: {
|
||||
title: 'هل أنت متأكد من استبدال جميع التطابقات؟',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'موافق',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/ca-ES.ts
Normal file
81
packages/find-replace/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Cerca i reemplaça',
|
||||
mobile: {
|
||||
settings: 'Configuració de cerca i reemplaçament',
|
||||
'settings-title': 'Configuració de cerca i reemplaçament',
|
||||
'previous-match': 'Coincidència anterior',
|
||||
'next-match': 'Coincidència següent',
|
||||
close: 'Tanca la cerca i el reemplaçament',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Obrir el diàleg de cerca',
|
||||
'open-replace-dialog': 'Obrir el diàleg de reemplaçament',
|
||||
'go-to-next-match': 'Anar a la següent coincidència',
|
||||
'go-to-previous-match': 'Anar a la coincidència anterior',
|
||||
'focus-selection': 'Enfocar la selecció',
|
||||
panel: 'Cerca i reemplaça',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Cerca',
|
||||
find: 'Cerca',
|
||||
replace: 'Reemplaça',
|
||||
'replace-all': 'Reemplaça-ho tot',
|
||||
'case-sensitive': 'Distingeix majúscules de minúscules',
|
||||
'find-placeholder': 'Cerca',
|
||||
'advanced-finding': 'Cerca i reemplaçament avançats',
|
||||
'replace-placeholder': 'Introduïu la cadena de reemplaçament',
|
||||
'match-the-whole-cell': 'Coincidir amb la cel·la sencera',
|
||||
'match-the-whole-word': 'Coincideix amb la paraula sencera',
|
||||
'find-direction': {
|
||||
title: 'Direcció de la cerca',
|
||||
row: 'Cerca per fila',
|
||||
column: 'Cerca per columna',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Abast de la cerca',
|
||||
'current-sheet': 'Full actual',
|
||||
workbook: 'Llibre',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Cerca per',
|
||||
value: 'Cerca per valor',
|
||||
formula: 'Cerca fórmula',
|
||||
},
|
||||
'no-match': 'S\'ha completat la cerca però no s\'ha trobat cap coincidència.',
|
||||
'no-result': 'Sense resultats',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'S\'han reemplaçat totes les {0} coincidències',
|
||||
'partial-success': 'S\'han reemplaçat {0} coincidències, no s\'han pogut reemplaçar {1}',
|
||||
'all-failure': 'Error en el reemplaçament',
|
||||
confirm: {
|
||||
title: 'Esteu segur que voleu reemplaçar totes les coincidències?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'D\'acord',
|
||||
cancel: 'Cancel·lar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/de-DE.ts
Normal file
81
packages/find-replace/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Suchen & Ersetzen',
|
||||
mobile: {
|
||||
settings: 'Einstellungen für Suchen und Ersetzen',
|
||||
'settings-title': 'Suchen-und-Ersetzen-Einstellungen',
|
||||
'previous-match': 'Vorheriger Treffer',
|
||||
'next-match': 'Nächster Treffer',
|
||||
close: 'Suchen und Ersetzen schließen',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Suchdialog öffnen',
|
||||
'open-replace-dialog': 'Ersetzungsdialog öffnen',
|
||||
'go-to-next-match': 'Zum nächsten Treffer springen',
|
||||
'go-to-previous-match': 'Zum vorherigen Treffer springen',
|
||||
'focus-selection': 'Auswahl fokussieren',
|
||||
panel: 'Suchen & Ersetzen',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Suchen',
|
||||
find: 'Suchen',
|
||||
replace: 'Ersetzen',
|
||||
'replace-all': 'Alle ersetzen',
|
||||
'case-sensitive': 'Groß-/Kleinschreibung beachten',
|
||||
'find-placeholder': 'Suchen',
|
||||
'advanced-finding': 'Erweitertes Suchen & Ersetzen',
|
||||
'replace-placeholder': 'Ersetzungstext eingeben',
|
||||
'match-the-whole-cell': 'Ganze Zelle vergleichen',
|
||||
'match-the-whole-word': 'Nur ganzes Wort',
|
||||
'find-direction': {
|
||||
title: 'Suchrichtung',
|
||||
row: 'Nach Zeile suchen',
|
||||
column: 'Nach Spalte suchen',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Suchbereich',
|
||||
'current-sheet': 'Aktuelles Blatt',
|
||||
workbook: 'Arbeitsmappe',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Suchen nach',
|
||||
value: 'Nach Wert suchen',
|
||||
formula: 'Formel suchen',
|
||||
},
|
||||
'no-match': 'Suche abgeschlossen, aber kein Treffer gefunden.',
|
||||
'no-result': 'Kein Ergebnis',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Alle {0} Treffer ersetzt',
|
||||
'partial-success': '{0} Treffer ersetzt, {1} konnten nicht ersetzt werden',
|
||||
'all-failure': 'Ersetzen fehlgeschlagen',
|
||||
confirm: {
|
||||
title: 'Sollen alle Treffer ersetzt werden?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Abbrechen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
79
packages/find-replace/src/locale/en-US.ts
Normal file
79
packages/find-replace/src/locale/en-US.ts
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'find-replace': {
|
||||
toolbar: 'Find & Replace',
|
||||
mobile: {
|
||||
settings: 'Find and replace settings',
|
||||
'settings-title': 'Find and Replace Settings',
|
||||
'previous-match': 'Previous match',
|
||||
'next-match': 'Next match',
|
||||
close: 'Close find and replace',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Open Find Dialog',
|
||||
'open-replace-dialog': 'Open Replace Dialog',
|
||||
'go-to-next-match': 'Go to Next Match',
|
||||
'go-to-previous-match': 'Go to Previous Match',
|
||||
'focus-selection': 'Focus Selection',
|
||||
panel: 'Find & Replace',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Find',
|
||||
find: 'Find',
|
||||
replace: 'Replace',
|
||||
'replace-all': 'Replace All',
|
||||
'case-sensitive': 'Case Sensitive',
|
||||
'find-placeholder': 'Find',
|
||||
'advanced-finding': 'Advanced Searching & Replace',
|
||||
'replace-placeholder': 'Input Replace String',
|
||||
'match-the-whole-cell': 'Match the Whole Cell',
|
||||
'match-the-whole-word': 'Match Whole Word',
|
||||
'find-direction': {
|
||||
title: 'Find Direction',
|
||||
row: 'Search by Row',
|
||||
column: 'Search by Column',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Find Range',
|
||||
'current-sheet': 'Current Sheet',
|
||||
workbook: 'Workbook',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Find By',
|
||||
value: 'Find by Value',
|
||||
formula: 'Find Formula',
|
||||
},
|
||||
'no-match': 'Finding completed but no match found.',
|
||||
'no-result': 'No Result',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Replaced all {0} matches',
|
||||
'partial-success': 'Replaced {0} matches, failed to replace {1}',
|
||||
'all-failure': 'Replace failed',
|
||||
confirm: {
|
||||
title: 'Are you sure to replace all matches?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/es-ES.ts
Normal file
81
packages/find-replace/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Buscar y reemplazar',
|
||||
mobile: {
|
||||
settings: 'Configuración de buscar y reemplazar',
|
||||
'settings-title': 'Configuración de buscar y reemplazar',
|
||||
'previous-match': 'Coincidencia anterior',
|
||||
'next-match': 'Coincidencia siguiente',
|
||||
close: 'Cerrar buscar y reemplazar',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Abrir cuadro de diálogo de búsqueda',
|
||||
'open-replace-dialog': 'Abrir cuadro de diálogo de reemplazo',
|
||||
'go-to-next-match': 'Ir a la siguiente coincidencia',
|
||||
'go-to-previous-match': 'Ir a la coincidencia anterior',
|
||||
'focus-selection': 'Enfocar selección',
|
||||
panel: 'Buscar y reemplazar',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Buscar',
|
||||
find: 'Buscar',
|
||||
replace: 'Reemplazar',
|
||||
'replace-all': 'Reemplazar todo',
|
||||
'case-sensitive': 'Distinguir mayúsculas y minúsculas',
|
||||
'find-placeholder': 'Buscar',
|
||||
'advanced-finding': 'Búsqueda y reemplazo avanzados',
|
||||
'replace-placeholder': 'Introducir cadena de reemplazo',
|
||||
'match-the-whole-cell': 'Coincidir con toda la celda',
|
||||
'match-the-whole-word': 'Coincidir con la palabra completa',
|
||||
'find-direction': {
|
||||
title: 'Dirección de búsqueda',
|
||||
row: 'Buscar por fila',
|
||||
column: 'Buscar por columna',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Rango de búsqueda',
|
||||
'current-sheet': 'Hoja actual',
|
||||
workbook: 'Libro',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Buscar por',
|
||||
value: 'Buscar por valor',
|
||||
formula: 'Buscar fórmula',
|
||||
},
|
||||
'no-match': 'Búsqueda completada pero no se encontraron coincidencias.',
|
||||
'no-result': 'Sin resultados',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Se reemplazaron todas las {0} coincidencias',
|
||||
'partial-success': 'Se reemplazaron {0} coincidencias; no se pudieron reemplazar {1}',
|
||||
'all-failure': 'Error al reemplazar',
|
||||
confirm: {
|
||||
title: '¿Está seguro de que desea reemplazar todas las coincidencias?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'Aceptar',
|
||||
cancel: 'Cancelar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/fa-IR.ts
Normal file
81
packages/find-replace/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'یافتن و جایگزینی',
|
||||
mobile: {
|
||||
settings: 'تنظیمات یافتن و جایگزینی',
|
||||
'settings-title': 'تنظیمات یافتن و جایگزینی',
|
||||
'previous-match': 'مطابقت قبلی',
|
||||
'next-match': 'مطابقت بعدی',
|
||||
close: 'بستن یافتن و جایگزینی',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'باز کردن پنجره جستجو',
|
||||
'open-replace-dialog': 'باز کردن پنجره جایگزینی',
|
||||
'go-to-next-match': 'رفتن به مورد تطابق بعدی',
|
||||
'go-to-previous-match': 'رفتن به مورد تطابق قبلی',
|
||||
'focus-selection': 'تمرکز بر انتخاب',
|
||||
panel: 'یافتن و جایگزینی',
|
||||
},
|
||||
dialog: {
|
||||
title: 'جستجو',
|
||||
find: 'جستجو',
|
||||
replace: 'جایگزینی',
|
||||
'replace-all': 'جایگزینی همه',
|
||||
'case-sensitive': 'حساس به حروف بزرگ و کوچک',
|
||||
'find-placeholder': 'جستجو',
|
||||
'advanced-finding': 'جستجو و جایگزینی پیشرفته',
|
||||
'replace-placeholder': 'رشته جایگزینی را وارد کنید',
|
||||
'match-the-whole-cell': 'تطابق کل سلول',
|
||||
'match-the-whole-word': 'تطبیق کل کلمه',
|
||||
'find-direction': {
|
||||
title: 'جهت جستجو',
|
||||
row: 'جستجو بر اساس ردیف',
|
||||
column: 'جستجو بر اساس ستون',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'محدوده جستجو',
|
||||
'current-sheet': 'شیت فعلی',
|
||||
workbook: 'کتاب کار',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'جستجو بر اساس',
|
||||
value: 'جستجو بر اساس مقدار',
|
||||
formula: 'جستجوی فرمول',
|
||||
},
|
||||
'no-match': 'جستجو تکمیل شد اما هیچ تطابقی یافت نشد.',
|
||||
'no-result': 'بدون نتیجه',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'همه {0} مورد تطابق جایگزین شدند',
|
||||
'partial-success': '{0} مورد تطابق جایگزین شد، جایگزینی {1} مورد ناموفق بود',
|
||||
'all-failure': 'جایگزینی ناموفق بود',
|
||||
confirm: {
|
||||
title: 'آیا مطمئن هستید که میخواهید همه موارد تطابق را جایگزین کنید؟',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'تایید',
|
||||
cancel: 'انصراف',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/fr-FR.ts
Normal file
81
packages/find-replace/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Rechercher & Remplacer',
|
||||
mobile: {
|
||||
settings: 'Paramètres de recherche et de remplacement',
|
||||
'settings-title': 'Paramètres de recherche et de remplacement',
|
||||
'previous-match': 'Occurrence précédente',
|
||||
'next-match': 'Occurrence suivante',
|
||||
close: 'Fermer la recherche et le remplacement',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Ouvrir la boîte de dialogue de recherche',
|
||||
'open-replace-dialog': 'Ouvrir la boîte de dialogue de remplacement',
|
||||
'go-to-next-match': 'Aller à la correspondance suivante',
|
||||
'go-to-previous-match': 'Aller à la correspondance précédente',
|
||||
'focus-selection': 'Concentrer la sélection',
|
||||
panel: 'Rechercher & Remplacer',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Rechercher',
|
||||
find: 'Rechercher',
|
||||
replace: 'Remplacer',
|
||||
'replace-all': 'Remplacer tout',
|
||||
'case-sensitive': 'Sensible à la casse',
|
||||
'find-placeholder': 'Rechercher',
|
||||
'advanced-finding': 'Recherche et remplacement avancés',
|
||||
'replace-placeholder': 'Saisir la chaîne de remplacement',
|
||||
'match-the-whole-cell': 'Correspondance de la cellule entière',
|
||||
'match-the-whole-word': 'Mot entier',
|
||||
'find-direction': {
|
||||
title: 'Direction de la recherche',
|
||||
row: 'Rechercher par ligne',
|
||||
column: 'Rechercher par colonne',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Plage de recherche',
|
||||
'current-sheet': 'Feuille actuelle',
|
||||
workbook: 'Classeur',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Rechercher par',
|
||||
value: 'Rechercher par valeur',
|
||||
formula: 'Rechercher une formule',
|
||||
},
|
||||
'no-match': 'Recherche terminée mais aucune correspondance trouvée.',
|
||||
'no-result': 'Aucun résultat',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Toutes les {0} correspondances ont été remplacées',
|
||||
'partial-success': '{0} correspondances ont été remplacées, {1} ont échoué',
|
||||
'all-failure': 'Échec du remplacement',
|
||||
confirm: {
|
||||
title: 'Êtes-vous sûr de vouloir remplacer toutes les correspondances ?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Annuler',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/id-ID.ts
Normal file
81
packages/find-replace/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Cari & Ganti',
|
||||
mobile: {
|
||||
settings: 'Pengaturan cari dan ganti',
|
||||
'settings-title': 'Pengaturan Cari dan Ganti',
|
||||
'previous-match': 'Kecocokan sebelumnya',
|
||||
'next-match': 'Kecocokan berikutnya',
|
||||
close: 'Tutup cari dan ganti',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Buka Dialog Cari',
|
||||
'open-replace-dialog': 'Buka Dialog Ganti',
|
||||
'go-to-next-match': 'Ke Hasil Berikutnya',
|
||||
'go-to-previous-match': 'Ke Hasil Sebelumnya',
|
||||
'focus-selection': 'Fokus Pilihan',
|
||||
panel: 'Cari & Ganti',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Cari',
|
||||
find: 'Cari',
|
||||
replace: 'Ganti',
|
||||
'replace-all': 'Ganti Semua',
|
||||
'case-sensitive': 'Sensitif Huruf',
|
||||
'find-placeholder': 'Cari',
|
||||
'advanced-finding': 'Pencarian & Penggantian Lanjutan',
|
||||
'replace-placeholder': 'Masukkan String Pengganti',
|
||||
'match-the-whole-cell': 'Cocokkan Seluruh Sel',
|
||||
'match-the-whole-word': 'Cocokkan seluruh kata',
|
||||
'find-direction': {
|
||||
title: 'Arah Cari',
|
||||
row: 'Cari per Baris',
|
||||
column: 'Cari per Kolom',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Rentang Cari',
|
||||
'current-sheet': 'Lembar Saat Ini',
|
||||
workbook: 'Buku Kerja',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Cari Berdasarkan',
|
||||
value: 'Cari berdasarkan Nilai',
|
||||
formula: 'Cari Rumus',
|
||||
},
|
||||
'no-match': 'Pencarian selesai tetapi tidak ditemukan kecocokan.',
|
||||
'no-result': 'Tidak Ada Hasil',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Berhasil mengganti {0} kecocokan',
|
||||
'partial-success': 'Berhasil mengganti {0} kecocokan, gagal mengganti {1}',
|
||||
'all-failure': 'Penggantian gagal',
|
||||
confirm: {
|
||||
title: 'Apakah Anda yakin ingin mengganti semua kecocokan?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Batal',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/it-IT.ts
Normal file
81
packages/find-replace/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Trova e sostituisci',
|
||||
mobile: {
|
||||
settings: 'Impostazioni trova e sostituisci',
|
||||
'settings-title': 'Impostazioni Trova e sostituisci',
|
||||
'previous-match': 'Corrispondenza precedente',
|
||||
'next-match': 'Corrispondenza successiva',
|
||||
close: 'Chiudi trova e sostituisci',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Apri finestra Trova',
|
||||
'open-replace-dialog': 'Apri finestra Sostituisci',
|
||||
'go-to-next-match': 'Vai alla corrispondenza successiva',
|
||||
'go-to-previous-match': 'Vai alla corrispondenza precedente',
|
||||
'focus-selection': 'Focus selezione',
|
||||
panel: 'Trova e sostituisci',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Trova',
|
||||
find: 'Trova',
|
||||
replace: 'Sostituisci',
|
||||
'replace-all': 'Sostituisci tutto',
|
||||
'case-sensitive': 'Maiuscole/minuscole',
|
||||
'find-placeholder': 'Trova',
|
||||
'advanced-finding': 'Ricerca e sostituzione avanzate',
|
||||
'replace-placeholder': 'Inserisci stringa di sostituzione',
|
||||
'match-the-whole-cell': "Corrispondenza con l'intera cella",
|
||||
'match-the-whole-word': 'Trova parola intera',
|
||||
'find-direction': {
|
||||
title: 'Direzione ricerca',
|
||||
row: 'Cerca per riga',
|
||||
column: 'Cerca per colonna',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Intervallo ricerca',
|
||||
'current-sheet': 'Foglio corrente',
|
||||
workbook: 'Cartella di lavoro',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Criterio ricerca',
|
||||
value: 'Trova per valore',
|
||||
formula: 'Trova formula',
|
||||
},
|
||||
'no-match': 'Ricerca completata ma nessuna corrispondenza trovata.',
|
||||
'no-result': 'Nessun risultato',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Sostituite tutte le {0} corrispondenze',
|
||||
'partial-success': 'Sostituite {0} corrispondenze, {1} non sostituite',
|
||||
'all-failure': 'Sostituzione non riuscita',
|
||||
confirm: {
|
||||
title: 'Sei sicuro di voler sostituire tutte le corrispondenze?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Annulla',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/ja-JP.ts
Normal file
81
packages/find-replace/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: '検索と置換',
|
||||
mobile: {
|
||||
settings: '検索と置換の設定',
|
||||
'settings-title': '検索と置換の設定',
|
||||
'previous-match': '前の一致項目',
|
||||
'next-match': '次の一致項目',
|
||||
close: '検索と置換を閉じる',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': '検索ダイアログを開く',
|
||||
'open-replace-dialog': '置換ダイアログを開く',
|
||||
'go-to-next-match': '次の一致項目へ移動',
|
||||
'go-to-previous-match': '前の一致項目へ移動',
|
||||
'focus-selection': '選択範囲にフォーカス',
|
||||
panel: '検索と置換',
|
||||
},
|
||||
dialog: {
|
||||
title: '検索',
|
||||
find: '検索',
|
||||
replace: '置換',
|
||||
'replace-all': 'すべて置換',
|
||||
'case-sensitive': '大文字と小文字を区別',
|
||||
'find-placeholder': '検索',
|
||||
'advanced-finding': '詳細検索と置換',
|
||||
'replace-placeholder': '置換文字列を入力',
|
||||
'match-the-whole-cell': 'セル全体に一致',
|
||||
'match-the-whole-word': '単語単位で検索',
|
||||
'find-direction': {
|
||||
title: '検索方向',
|
||||
row: '行ごとに検索',
|
||||
column: '列ごとに検索',
|
||||
},
|
||||
'find-scope': {
|
||||
title: '検索範囲',
|
||||
'current-sheet': '現在のシート',
|
||||
workbook: 'ブック全体',
|
||||
},
|
||||
'find-by': {
|
||||
title: '検索条件',
|
||||
value: '値で検索',
|
||||
formula: '数式を検索',
|
||||
},
|
||||
'no-match': '検索が完了しましたが、一致する項目が見つかりませんでした。',
|
||||
'no-result': '結果なし',
|
||||
},
|
||||
replace: {
|
||||
'all-success': '一致項目 {0} 件すべてを置換しました',
|
||||
'partial-success': '{0} 件の一致項目を置換しました。{1} 件の置換に失敗しました',
|
||||
'all-failure': '置換に失敗しました',
|
||||
confirm: {
|
||||
title: 'すべての一致項目を置換してもよろしいですか?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: '確認',
|
||||
cancel: 'キャンセル',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/ko-KR.ts
Normal file
81
packages/find-replace/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: '찾기 및 바꾸기',
|
||||
mobile: {
|
||||
settings: '찾기 및 바꾸기 설정',
|
||||
'settings-title': '찾기 및 바꾸기 설정',
|
||||
'previous-match': '이전 일치 항목',
|
||||
'next-match': '다음 일치 항목',
|
||||
close: '찾기 및 바꾸기 닫기',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': '찾기 대화 상자 열기',
|
||||
'open-replace-dialog': '바꾸기 대화 상자 열기',
|
||||
'go-to-next-match': '다음 일치 항목으로 이동',
|
||||
'go-to-previous-match': '이전 일치 항목으로 이동',
|
||||
'focus-selection': '선택 영역에 포커스',
|
||||
panel: '찾기 및 바꾸기',
|
||||
},
|
||||
dialog: {
|
||||
title: '찾기',
|
||||
find: '찾기',
|
||||
replace: '바꾸기',
|
||||
'replace-all': '모두 바꾸기',
|
||||
'case-sensitive': '대/소문자 구분',
|
||||
'find-placeholder': '찾기',
|
||||
'advanced-finding': '고급 검색 및 바꾸기',
|
||||
'replace-placeholder': '바꿀 문자열 입력',
|
||||
'match-the-whole-cell': '전체 셀 내용 일치',
|
||||
'match-the-whole-word': '단어 단위로 일치',
|
||||
'find-direction': {
|
||||
title: '검색 방향',
|
||||
row: '행 기준 검색',
|
||||
column: '열 기준 검색',
|
||||
},
|
||||
'find-scope': {
|
||||
title: '검색 범위',
|
||||
'current-sheet': '현재 시트',
|
||||
workbook: '통합 문서',
|
||||
},
|
||||
'find-by': {
|
||||
title: '검색 기준',
|
||||
value: '값으로 찾기',
|
||||
formula: '수식 찾기',
|
||||
},
|
||||
'no-match': '검색이 완료되었으나 일치 항목을 찾을 수 없습니다.',
|
||||
'no-result': '결과 없음',
|
||||
},
|
||||
replace: {
|
||||
'all-success': '일치 항목 {0}개를 모두 바꿨습니다.',
|
||||
'partial-success': '일치 항목 {0}개를 바꾸고 {1}개는 바꾸지 못했습니다.',
|
||||
'all-failure': '바꾸기 실패',
|
||||
confirm: {
|
||||
title: '모든 일치 항목을 바꾸시겠습니까?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: '확인',
|
||||
cancel: '취소',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/pl-PL.ts
Normal file
81
packages/find-replace/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Znajdź i zamień',
|
||||
mobile: {
|
||||
settings: 'Ustawienia znajdowania i zamiany',
|
||||
'settings-title': 'Ustawienia znajdowania i zamiany',
|
||||
'previous-match': 'Poprzednie dopasowanie',
|
||||
'next-match': 'Następne dopasowanie',
|
||||
close: 'Zamknij znajdowanie i zamianę',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Otwórz okno Znajdź',
|
||||
'open-replace-dialog': 'Otwórz okno Zamień',
|
||||
'go-to-next-match': 'Przejdź do następnego dopasowania',
|
||||
'go-to-previous-match': 'Przejdź do poprzedniego dopasowania',
|
||||
'focus-selection': 'Skup na zaznaczeniu',
|
||||
panel: 'Znajdź i zamień',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Znajdź',
|
||||
find: 'Znajdź',
|
||||
replace: 'Zamień',
|
||||
'replace-all': 'Zamień wszystko',
|
||||
'case-sensitive': 'Rozróżniaj wielkość liter',
|
||||
'find-placeholder': 'Znajdź',
|
||||
'advanced-finding': 'Zaawansowane wyszukiwanie i zastępowanie',
|
||||
'replace-placeholder': 'Wprowadź ciąg zastępujący',
|
||||
'match-the-whole-cell': 'Dopasuj całą komórkę',
|
||||
'match-the-whole-word': 'Dopasuj całe słowo',
|
||||
'find-direction': {
|
||||
title: 'Kierunek wyszukiwania',
|
||||
row: 'Szukaj według wierszy',
|
||||
column: 'Szukaj według kolumn',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Zakres wyszukiwania',
|
||||
'current-sheet': 'Bieżący arkusz',
|
||||
workbook: 'Skoroszyt',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Szukaj według',
|
||||
value: 'Szukaj według wartości',
|
||||
formula: 'Szukaj formuły',
|
||||
},
|
||||
'no-match': 'Wyszukiwanie zakończone, ale nie znaleziono dopasowania.',
|
||||
'no-result': 'Brak wyników',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Zastąpiono wszystkie dopasowania: {0}',
|
||||
'partial-success': 'Zastąpiono dopasowania: {0}, nie udało się zastąpić: {1}',
|
||||
'all-failure': 'Zastępowanie nie powiodło się',
|
||||
confirm: {
|
||||
title: 'Czy na pewno chcesz zastąpić wszystkie dopasowania?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Anuluj',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/pt-BR.ts
Normal file
81
packages/find-replace/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Localizar e Substituir',
|
||||
mobile: {
|
||||
settings: 'Configurações de localizar e substituir',
|
||||
'settings-title': 'Configurações de Localizar e Substituir',
|
||||
'previous-match': 'Correspondência anterior',
|
||||
'next-match': 'Próxima correspondência',
|
||||
close: 'Fechar localizar e substituir',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Abrir Caixa de Localização',
|
||||
'open-replace-dialog': 'Abrir Caixa de Substituição',
|
||||
'go-to-next-match': 'Ir para Próxima Correspondência',
|
||||
'go-to-previous-match': 'Ir para Correspondência Anterior',
|
||||
'focus-selection': 'Focar Seleção',
|
||||
panel: 'Localizar e Substituir',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Localizar',
|
||||
find: 'Localizar',
|
||||
replace: 'Substituir',
|
||||
'replace-all': 'Substituir Tudo',
|
||||
'case-sensitive': 'Diferenciar Maiúsculas/Minúsculas',
|
||||
'find-placeholder': 'Localizar',
|
||||
'advanced-finding': 'Pesquisa e Substituição Avançadas',
|
||||
'replace-placeholder': 'Digite o Texto de Substituição',
|
||||
'match-the-whole-cell': 'Corresponder à Célula Inteira',
|
||||
'match-the-whole-word': 'Corresponder palavra inteira',
|
||||
'find-direction': {
|
||||
title: 'Direção da Pesquisa',
|
||||
row: 'Pesquisar por Linha',
|
||||
column: 'Pesquisar por Coluna',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Intervalo da Pesquisa',
|
||||
'current-sheet': 'Planilha Atual',
|
||||
workbook: 'Pasta de Trabalho',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Pesquisar Por',
|
||||
value: 'Pesquisar por Valor',
|
||||
formula: 'Localizar Fórmula',
|
||||
},
|
||||
'no-match': 'Pesquisa concluída, mas nenhuma correspondência foi encontrada.',
|
||||
'no-result': 'Nenhum Resultado',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Substituídas todas as {0} correspondências',
|
||||
'partial-success': '{0} correspondências substituídas, falha ao substituir {1}',
|
||||
'all-failure': 'Falha na substituição',
|
||||
confirm: {
|
||||
title: 'Tem certeza de que deseja substituir todas as correspondências?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Cancelar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/ru-RU.ts
Normal file
81
packages/find-replace/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Найти и заменить',
|
||||
mobile: {
|
||||
settings: 'Настройки поиска и замены',
|
||||
'settings-title': 'Настройки поиска и замены',
|
||||
'previous-match': 'Предыдущее совпадение',
|
||||
'next-match': 'Следующее совпадение',
|
||||
close: 'Закрыть поиск и замену',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Открыть диалоговое окно поиска',
|
||||
'open-replace-dialog': 'Открыть диалоговое окно замены',
|
||||
'go-to-next-match': 'Перейти к следующему совпадению',
|
||||
'go-to-previous-match': 'Перейти к предыдущему совпадению',
|
||||
'focus-selection': 'Фокус на выделении',
|
||||
panel: 'Найти и заменить',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Найти',
|
||||
find: 'Найти',
|
||||
replace: 'Заменить',
|
||||
'replace-all': 'Заменить все',
|
||||
'case-sensitive': 'Учитывать регистр',
|
||||
'find-placeholder': 'Найти',
|
||||
'advanced-finding': 'Расширенный поиск и замена',
|
||||
'replace-placeholder': 'Введите строку для замены',
|
||||
'match-the-whole-cell': 'Соответствие всей ячейке',
|
||||
'match-the-whole-word': 'Только слово целиком',
|
||||
'find-direction': {
|
||||
title: 'Направление поиска',
|
||||
row: 'Искать по строкам',
|
||||
column: 'Искать по столбцам',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Диапазон поиска',
|
||||
'current-sheet': 'Текущий лист',
|
||||
workbook: 'Книга',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Искать по',
|
||||
value: 'Искать по значению',
|
||||
formula: 'Искать формулу',
|
||||
},
|
||||
'no-match': 'Поиск завершен, но совпадений не найдено.',
|
||||
'no-result': 'Нет результатов',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Заменены все совпадения: {0}',
|
||||
'partial-success': 'Заменено совпадений: {0}, не удалось заменить: {1}',
|
||||
'all-failure': 'Ошибка замены',
|
||||
confirm: {
|
||||
title: 'Вы уверены, что хотите заменить все совпадения?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'ОК',
|
||||
cancel: 'Отмена',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/sk-SK.ts
Normal file
81
packages/find-replace/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Hľadať a nahradiť',
|
||||
mobile: {
|
||||
settings: 'Nastavenia hľadania a nahrádzania',
|
||||
'settings-title': 'Nastavenia hľadania a nahrádzania',
|
||||
'previous-match': 'Predchádzajúca zhoda',
|
||||
'next-match': 'Nasledujúca zhoda',
|
||||
close: 'Zavrieť hľadanie a nahrádzanie',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Otvoriť dialógové okno hľadania',
|
||||
'open-replace-dialog': 'Otvoriť dialógové okno nahradenia',
|
||||
'go-to-next-match': 'Prejsť na ďalšiu zhodu',
|
||||
'go-to-previous-match': 'Prejsť na predchádzajúcu zhodu',
|
||||
'focus-selection': 'Zamerať výber',
|
||||
panel: 'Hľadať a nahradiť',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Hľadať',
|
||||
find: 'Hľadať',
|
||||
replace: 'Nahradiť',
|
||||
'replace-all': 'Nahradiť všetko',
|
||||
'case-sensitive': 'Rozlišovať veľké a malé písmená',
|
||||
'find-placeholder': 'Hľadať',
|
||||
'advanced-finding': 'Pokročilé hľadanie a nahradenie',
|
||||
'replace-placeholder': 'Zadajte reťazec na nahradenie',
|
||||
'match-the-whole-cell': 'Zhoda s celou bunkou',
|
||||
'match-the-whole-word': 'Hľadať iba celé slová',
|
||||
'find-direction': {
|
||||
title: 'Smer hľadania',
|
||||
row: 'Hľadať podľa riadkov',
|
||||
column: 'Hľadať podľa stĺpcov',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Rozsah hľadania',
|
||||
'current-sheet': 'Aktuálny hárok',
|
||||
workbook: 'Zošit',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Hľadať podľa',
|
||||
value: 'Hľadať podľa hodnoty',
|
||||
formula: 'Hľadať vzorec',
|
||||
},
|
||||
'no-match': 'Hľadanie bolo dokončené, ale nenašla sa žiadna zhoda.',
|
||||
'no-result': 'Žiadne výsledky',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Všetkých {0} zhôd bolo nahradených',
|
||||
'partial-success': 'Nahradených {0} zhôd, {1} sa nepodarilo nahradiť',
|
||||
'all-failure': 'Nahradenie zlyhalo',
|
||||
confirm: {
|
||||
title: 'Ste si istí, že chcete nahradiť všetky zhody?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'OK',
|
||||
cancel: 'Zrušiť',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/find-replace/src/locale/types.ts
Normal file
20
packages/find-replace/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>;
|
||||
81
packages/find-replace/src/locale/vi-VN.ts
Normal file
81
packages/find-replace/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: 'Tìm kiếm và Thay thế',
|
||||
mobile: {
|
||||
settings: 'Cài đặt tìm kiếm và thay thế',
|
||||
'settings-title': 'Cài đặt Tìm kiếm và Thay thế',
|
||||
'previous-match': 'Kết quả trước',
|
||||
'next-match': 'Kết quả tiếp theo',
|
||||
close: 'Đóng tìm kiếm và thay thế',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': 'Mở hộp thoại Tìm kiếm',
|
||||
'open-replace-dialog': 'Mở hộp thoại Thay thế',
|
||||
'go-to-next-match': 'Chuyển đến kết quả tiếp theo',
|
||||
'go-to-previous-match': 'Chuyển đến kết quả trước đó',
|
||||
'focus-selection': 'Tập trung vào vùng chọn',
|
||||
panel: 'Tìm kiếm và Thay thế',
|
||||
},
|
||||
dialog: {
|
||||
title: 'Tìm kiếm',
|
||||
find: 'Tìm kiếm',
|
||||
replace: 'Thay thế',
|
||||
'replace-all': 'Thay thế Tất cả',
|
||||
'case-sensitive': 'Phân biệt chữ hoa chữ thường',
|
||||
'find-placeholder': 'Tìm',
|
||||
'advanced-finding': 'Tìm kiếm & Thay thế Nâng cao',
|
||||
'replace-placeholder': 'Nhập chuỗi thay thế',
|
||||
'match-the-whole-cell': 'Khớp toàn bộ ô',
|
||||
'match-the-whole-word': 'Khớp toàn bộ từ',
|
||||
'find-direction': {
|
||||
title: 'Hướng tìm kiếm',
|
||||
row: 'Tìm theo hàng',
|
||||
column: 'Tìm theo cột',
|
||||
},
|
||||
'find-scope': {
|
||||
title: 'Phạm vi tìm kiếm',
|
||||
'current-sheet': 'Trang tính hiện tại',
|
||||
workbook: 'Sổ làm việc',
|
||||
},
|
||||
'find-by': {
|
||||
title: 'Tìm theo',
|
||||
value: 'Tìm theo giá trị',
|
||||
formula: 'Tìm công thức',
|
||||
},
|
||||
'no-match': 'Đã hoàn thành tìm kiếm nhưng không tìm thấy kết quả phù hợp.',
|
||||
'no-result': 'Không có kết quả',
|
||||
},
|
||||
replace: {
|
||||
'all-success': 'Đã thay thế tất cả {0} kết quả phù hợp',
|
||||
'partial-success': 'Đã thay thế {0} kết quả phù hợp, không thể thay thế {1}',
|
||||
'all-failure': 'Thay thế thất bại',
|
||||
confirm: {
|
||||
title: 'Bạn có chắc chắn muốn thay thế tất cả các kết quả phù hợp không?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: 'Xác nhận',
|
||||
cancel: 'Hủy',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/zh-CN.ts
Normal file
81
packages/find-replace/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: '查找替换',
|
||||
mobile: {
|
||||
settings: '查找替换设置',
|
||||
'settings-title': '查找替换设置',
|
||||
'previous-match': '上一个匹配项',
|
||||
'next-match': '下一个匹配项',
|
||||
close: '关闭查找替换',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': '打开查找对话框',
|
||||
'open-replace-dialog': '打开替换对话框',
|
||||
'go-to-next-match': '下一个匹配项',
|
||||
'go-to-previous-match': '下一个匹配项',
|
||||
'focus-selection': '聚焦选区',
|
||||
panel: '查找替换',
|
||||
},
|
||||
dialog: {
|
||||
title: '查找',
|
||||
find: '查找',
|
||||
replace: '替换',
|
||||
'replace-all': '替换全部',
|
||||
'find-placeholder': '输入查找内容',
|
||||
'advanced-finding': '替换 / 高级查找',
|
||||
'replace-placeholder': '输入替换内容',
|
||||
'case-sensitive': '匹配大小写',
|
||||
'match-the-whole-cell': '匹配整个单元格',
|
||||
'match-the-whole-word': '全词匹配',
|
||||
'find-scope': {
|
||||
title: '查找范围',
|
||||
'current-sheet': '当前子表',
|
||||
workbook: '整个工作簿',
|
||||
},
|
||||
'find-direction': {
|
||||
title: '查找顺序',
|
||||
column: '按列查找',
|
||||
row: '按行查找',
|
||||
},
|
||||
'find-by': {
|
||||
title: '查找方式',
|
||||
formula: '查找公式',
|
||||
value: '查找值',
|
||||
},
|
||||
'no-match': '已完成搜索,但是未找到任何匹配项',
|
||||
'no-result': '无结果',
|
||||
},
|
||||
replace: {
|
||||
'all-success': '已全部替换 {0} 个匹配项',
|
||||
'partial-success': '已替换 {0} 个匹配项,{1} 个替换失败',
|
||||
'all-failure': '替换失败',
|
||||
confirm: {
|
||||
title: '确定要替换所有的匹配项吗?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/zh-HK.ts
Normal file
81
packages/find-replace/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: '尋找替換',
|
||||
mobile: {
|
||||
settings: '尋找替換設定',
|
||||
'settings-title': '尋找替換設定',
|
||||
'previous-match': '上一個符合項目',
|
||||
'next-match': '下一個符合項目',
|
||||
close: '關閉尋找替換',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': '打開尋找對話框',
|
||||
'open-replace-dialog': '打開取代對話框',
|
||||
'go-to-next-match': '前往下一個匹配項',
|
||||
'go-to-previous-match': '前往上一個匹配項',
|
||||
'focus-selection': '聚焦選取範圍',
|
||||
panel: '尋找替換',
|
||||
},
|
||||
dialog: {
|
||||
title: '尋找',
|
||||
find: '尋找',
|
||||
replace: '取代',
|
||||
'replace-all': '全部取代',
|
||||
'case-sensitive': '區分大小寫',
|
||||
'find-placeholder': '尋找',
|
||||
'advanced-finding': '進階搜尋與取代',
|
||||
'replace-placeholder': '輸入取代字串',
|
||||
'match-the-whole-cell': '完全符合儲存格內容',
|
||||
'match-the-whole-word': '全詞匹配',
|
||||
'find-direction': {
|
||||
title: '尋找方向',
|
||||
row: '按列搜尋',
|
||||
column: '按欄搜尋',
|
||||
},
|
||||
'find-scope': {
|
||||
title: '尋找範圍',
|
||||
'current-sheet': '目前工作表',
|
||||
workbook: '整個活頁簿',
|
||||
},
|
||||
'find-by': {
|
||||
title: '尋找方式',
|
||||
value: '按值尋找',
|
||||
formula: '尋找公式',
|
||||
},
|
||||
'no-match': '已完成尋找,但未找到任何相符項目。',
|
||||
'no-result': '無結果',
|
||||
},
|
||||
replace: {
|
||||
'all-success': '已取代全部 {0} 個相符項目',
|
||||
'partial-success': '已取代 {0} 個相符項目,{1} 個取代失敗',
|
||||
'all-failure': '取代失敗',
|
||||
confirm: {
|
||||
title: '確定要取代所有相符項目嗎?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: '確定',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
81
packages/find-replace/src/locale/zh-TW.ts
Normal file
81
packages/find-replace/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'find-replace': {
|
||||
toolbar: '尋找替換',
|
||||
mobile: {
|
||||
settings: '尋找替換設定',
|
||||
'settings-title': '尋找替換設定',
|
||||
'previous-match': '上一個符合項目',
|
||||
'next-match': '下一個符合項目',
|
||||
close: '關閉尋找替換',
|
||||
},
|
||||
shortcut: {
|
||||
'open-find-dialog': '打開尋找對話框',
|
||||
'open-replace-dialog': '打開取代對話框',
|
||||
'go-to-next-match': '前往下一個匹配項',
|
||||
'go-to-previous-match': '前往上一個匹配項',
|
||||
'focus-selection': '聚焦選取範圍',
|
||||
panel: '尋找替換',
|
||||
},
|
||||
dialog: {
|
||||
title: '尋找',
|
||||
find: '尋找',
|
||||
replace: '取代',
|
||||
'replace-all': '全部取代',
|
||||
'case-sensitive': '區分大小寫',
|
||||
'find-placeholder': '尋找',
|
||||
'advanced-finding': '進階搜尋與取代',
|
||||
'replace-placeholder': '輸入取代字串',
|
||||
'match-the-whole-cell': '完全符合儲存格內容',
|
||||
'match-the-whole-word': '全詞匹配',
|
||||
'find-direction': {
|
||||
title: '尋找方向',
|
||||
row: '按列搜尋',
|
||||
column: '按欄搜尋',
|
||||
},
|
||||
'find-scope': {
|
||||
title: '尋找範圍',
|
||||
'current-sheet': '目前工作表',
|
||||
workbook: '整個活頁簿',
|
||||
},
|
||||
'find-by': {
|
||||
title: '尋找方式',
|
||||
value: '按值尋找',
|
||||
formula: '尋找公式',
|
||||
},
|
||||
'no-match': '已完成尋找,但未找到任何相符項目。',
|
||||
'no-result': '無結果',
|
||||
},
|
||||
replace: {
|
||||
'all-success': '已取代全部 {0} 個相符項目',
|
||||
'partial-success': '已取代 {0} 個相符項目,{1} 個取代失敗',
|
||||
'all-failure': '取代失敗',
|
||||
confirm: {
|
||||
title: '確定要取代所有相符項目嗎?',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
confirm: '確定',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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 { EDITOR_ACTIVATED, FOCUSING_SHEET, IContextService, Univer } from '@univerjs/core';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { FIND_REPLACE_AVAILABLE } from '../../services/context-keys';
|
||||
import { FindReplaceMenuItemFactory } from '../find-replace.menu';
|
||||
|
||||
describe('FindReplaceMenuItemFactory', () => {
|
||||
let univer: Univer | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
univer?.dispose();
|
||||
univer = undefined;
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ availability: undefined, disabled: true, label: 'not initialized' },
|
||||
{ availability: false, disabled: true, label: 'unavailable' },
|
||||
{ availability: true, disabled: false, label: 'available' },
|
||||
])('reflects a $label provider before any context changes', ({ availability, disabled }) => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const contextService = injector.get(IContextService);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
contextService.setContextValue(FOCUSING_SHEET, true);
|
||||
if (availability !== undefined) {
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, availability);
|
||||
}
|
||||
|
||||
const values: boolean[] = [];
|
||||
const subscription = FindReplaceMenuItemFactory(injector).disabled$!.subscribe((value) => values.push(value));
|
||||
try {
|
||||
expect(values.length).toBeGreaterThan(0);
|
||||
expect(values[values.length - 1]).toBe(disabled);
|
||||
} finally {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
it('tracks provider readiness and removal while preserving editor and focus restrictions', () => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const contextService = injector.get(IContextService);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
contextService.setContextValue(FOCUSING_SHEET, true);
|
||||
const values: boolean[] = [];
|
||||
const subscription = FindReplaceMenuItemFactory(injector).disabled$!.subscribe((value) => values.push(value));
|
||||
try {
|
||||
expect(values[values.length - 1]).toBe(true);
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, true);
|
||||
expect(values[values.length - 1]).toBe(false);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, true);
|
||||
expect(values[values.length - 1]).toBe(true);
|
||||
contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
expect(values[values.length - 1]).toBe(false);
|
||||
contextService.setContextValue(FOCUSING_SHEET, false);
|
||||
expect(values[values.length - 1]).toBe(true);
|
||||
contextService.setContextValue(FOCUSING_SHEET, true);
|
||||
expect(values[values.length - 1]).toBe(false);
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, false);
|
||||
expect(values[values.length - 1]).toBe(true);
|
||||
} finally {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
const finalCount = values.length;
|
||||
contextService.setContextValue(FIND_REPLACE_AVAILABLE, true);
|
||||
expect(values).toHaveLength(finalCount);
|
||||
});
|
||||
});
|
||||
42
packages/find-replace/src/menu/find-replace.menu.ts
Normal file
42
packages/find-replace/src/menu/find-replace.menu.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 type { IAccessor } from '@univerjs/core';
|
||||
import type { IMenuButtonItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { EDITOR_ACTIVATED, FOCUSING_SHEET, IContextService, UniverInstanceType } from '@univerjs/core';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { combineLatest, map, startWith } from 'rxjs';
|
||||
import { OpenFindDialogOperation } from '../commands/operations/find-replace.operation';
|
||||
import { FIND_REPLACE_AVAILABLE } from '../services/context-keys';
|
||||
|
||||
export function FindReplaceMenuItemFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
const contextService = accessor.get(IContextService);
|
||||
|
||||
return {
|
||||
id: OpenFindDialogOperation.id,
|
||||
icon: 'SearchIcon',
|
||||
tooltip: 'find-replace.toolbar',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: combineLatest([
|
||||
contextService.subscribeContextValue$(EDITOR_ACTIVATED),
|
||||
contextService.subscribeContextValue$(FOCUSING_SHEET),
|
||||
contextService.subscribeContextValue$(FIND_REPLACE_AVAILABLE)
|
||||
.pipe(startWith(contextService.getContextValue(FIND_REPLACE_AVAILABLE))),
|
||||
]).pipe(map(([editorActivated, focusingSheet, available]) => editorActivated || !focusingSheet || !available)),
|
||||
};
|
||||
}
|
||||
29
packages/find-replace/src/menu/mobile-schema.ts
Normal file
29
packages/find-replace/src/menu/mobile-schema.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { RibbonDataGroup } from '@univerjs/ui';
|
||||
import { OpenFindDialogOperation } from '../commands/operations/find-replace.operation';
|
||||
import { FindReplaceMenuItemFactory } from './find-replace.menu';
|
||||
|
||||
export const mobileMenuSchema: MenuSchemaType = {
|
||||
[RibbonDataGroup.ORGANIZATION]: {
|
||||
[OpenFindDialogOperation.id]: {
|
||||
order: 2,
|
||||
menuItemFactory: FindReplaceMenuItemFactory,
|
||||
},
|
||||
},
|
||||
};
|
||||
30
packages/find-replace/src/menu/schema.ts
Normal file
30
packages/find-replace/src/menu/schema.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 { RibbonDataGroup } from '@univerjs/ui';
|
||||
import { OpenFindDialogOperation } from '../commands/operations/find-replace.operation';
|
||||
import { FindReplaceMenuItemFactory } from './find-replace.menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonDataGroup.ORGANIZATION]: {
|
||||
[OpenFindDialogOperation.id]: {
|
||||
order: 2,
|
||||
gridLayout: { row: 1, column: 3 },
|
||||
menuItemFactory: FindReplaceMenuItemFactory,
|
||||
},
|
||||
},
|
||||
};
|
||||
56
packages/find-replace/src/mobile-plugin.ts
Normal file
56
packages/find-replace/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 { IUniverFindReplaceConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin } from '@univerjs/core';
|
||||
import { UniverMobileUIPlugin } from '@univerjs/ui';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, FIND_REPLACE_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { FindReplaceMobileController } from './controllers/mobile/find-replace.controller';
|
||||
import { FindReplaceService, IFindReplaceService } from './services/find-replace.service';
|
||||
|
||||
@DependentOn(UniverMobileUIPlugin)
|
||||
export class UniverFindReplaceMobileUIPlugin extends Plugin {
|
||||
static override pluginName = 'UNIVER_FIND_REPLACE_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverFindReplaceConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { ...rest } = merge({}, defaultPluginConfig, this._config);
|
||||
this._configService.setConfig(FIND_REPLACE_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([MobileComponentsController]);
|
||||
this._injector.get(MobileComponentsController);
|
||||
([
|
||||
[FindReplaceMobileController],
|
||||
[IFindReplaceService, { useClass: FindReplaceService }],
|
||||
] as Dependency[]).forEach((dependency) => this._injector.add(dependency));
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(FindReplaceMobileController);
|
||||
}
|
||||
}
|
||||
59
packages/find-replace/src/plugin.ts
Normal file
59
packages/find-replace/src/plugin.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 { Dependency } from '@univerjs/core';
|
||||
import type { IUniverFindReplaceConfig } from './config/config';
|
||||
import { IConfigService, Inject, Injector, merge, Plugin } from '@univerjs/core';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, FIND_REPLACE_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { FindReplaceController } from './controllers/find-replace.controller';
|
||||
import { FindReplaceService, IFindReplaceService } from './services/find-replace.service';
|
||||
|
||||
export class UniverFindReplacePlugin extends Plugin {
|
||||
static override pluginName = 'UNIVER_FIND_REPLACE_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverFindReplaceConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(FIND_REPLACE_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
([
|
||||
[FindReplaceController],
|
||||
[IFindReplaceService, { useClass: FindReplaceService }],
|
||||
] as Dependency[]).forEach((d) => this._injector.add(d));
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(FindReplaceController);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FindModel, IFindMatch } from '../find-replace.service';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { FindReplaceModel, FindReplaceState } from '../find-replace.service';
|
||||
|
||||
describe('FindReplaceModel', () => {
|
||||
it('replaceAll should stop searching when all succeed', async () => {
|
||||
const state = new FindReplaceState();
|
||||
state.changeState({ findString: 'a', replaceString: 'b', revealed: true });
|
||||
|
||||
const univerInstanceService = { getFocusedUnit: vi.fn(() => ({ getUnitId: () => 'u1' })) };
|
||||
const commandService = { onCommandExecuted: vi.fn(() => ({ dispose: vi.fn() })) };
|
||||
|
||||
const matchesUpdate$ = new Subject<IFindMatch[]>();
|
||||
const activelyChangingMatch$ = new Subject<IFindMatch>();
|
||||
const match1: IFindMatch = { provider: 'p', unitId: 'u1', range: 1, replaceable: true } as any;
|
||||
|
||||
const model: FindModel = {
|
||||
unitId: 'u1',
|
||||
matchesUpdate$,
|
||||
activelyChangingMatch$,
|
||||
getMatches: () => [match1],
|
||||
moveToNextMatch: vi.fn(() => match1),
|
||||
moveToPreviousMatch: vi.fn(() => match1),
|
||||
replace: vi.fn(async () => true),
|
||||
replaceAll: vi.fn(async () => ({ success: 1, failure: 0 })),
|
||||
focusSelection: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const provider = {
|
||||
find: vi.fn(async () => [model]),
|
||||
terminate: vi.fn(),
|
||||
};
|
||||
|
||||
const providers = new Set([provider as any]);
|
||||
const findReplaceModel = new FindReplaceModel(state, providers, univerInstanceService as any, commandService as any);
|
||||
|
||||
await findReplaceModel.start();
|
||||
const res = await findReplaceModel.replaceAll();
|
||||
expect(res).toEqual({ success: 1, failure: 0 });
|
||||
expect(provider.terminate).toHaveBeenCalled();
|
||||
|
||||
findReplaceModel.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* 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 { UnitModel } from '@univerjs/core';
|
||||
import type {
|
||||
IFindMatch,
|
||||
IFindMoveParams,
|
||||
IFindQuery,
|
||||
IFindReplaceProviderCapabilities,
|
||||
IReplaceAllResult,
|
||||
} from '../find-replace.service';
|
||||
import {
|
||||
awaitTime,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { firstValueFrom, Subject } from 'rxjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FindBy, FindDirection, FindModel, FindReplaceService, FindScope } from '../find-replace.service';
|
||||
|
||||
class TestUniverInstanceService {
|
||||
readonly focused$ = new Subject<string | null>();
|
||||
focusedUnit = {
|
||||
getUnitId: () => 'u1',
|
||||
type: UniverInstanceType.UNIVER_DOC,
|
||||
} as UnitModel;
|
||||
|
||||
getFocusedUnit() {
|
||||
return this.focusedUnit;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandService {
|
||||
onCommandExecuted() {
|
||||
return { dispose() {} };
|
||||
}
|
||||
}
|
||||
|
||||
class TestContextService {
|
||||
readonly values: Array<{ key: unknown; value: unknown }> = [];
|
||||
|
||||
setContextValue(key: unknown, value: unknown): void {
|
||||
this.values.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
interface ITestModelConfig {
|
||||
unitId: string;
|
||||
matches: IFindMatch[];
|
||||
replaceAllResult?: IReplaceAllResult;
|
||||
}
|
||||
|
||||
class TestFindModel extends FindModel {
|
||||
readonly matchesUpdate$ = new Subject<IFindMatch[]>();
|
||||
readonly activelyChangingMatch$ = new Subject<IFindMatch>();
|
||||
readonly nextParams: Array<IFindMoveParams | undefined> = [];
|
||||
readonly previousParams: Array<IFindMoveParams | undefined> = [];
|
||||
readonly replaceCalls: string[] = [];
|
||||
readonly replaceAllCalls: string[] = [];
|
||||
nextQueue: Array<IFindMatch | null> = [];
|
||||
previousQueue: Array<IFindMatch | null> = [];
|
||||
focusCount = 0;
|
||||
|
||||
constructor(
|
||||
readonly unitId: string,
|
||||
private _matches: IFindMatch[],
|
||||
private readonly _replaceAllResult: IReplaceAllResult = { success: _matches.length, failure: 0 }
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getMatches(): IFindMatch[] {
|
||||
return this._matches;
|
||||
}
|
||||
|
||||
setMatches(matches: IFindMatch[]): void {
|
||||
this._matches = matches;
|
||||
this.matchesUpdate$.next(matches);
|
||||
}
|
||||
|
||||
moveToNextMatch(params?: IFindMoveParams): IFindMatch | null {
|
||||
this.nextParams.push(params);
|
||||
return this.nextQueue.length ? this.nextQueue.shift()! : this._matches[0] ?? null;
|
||||
}
|
||||
|
||||
moveToPreviousMatch(params?: IFindMoveParams): IFindMatch | null {
|
||||
this.previousParams.push(params);
|
||||
return this.previousQueue.length ? this.previousQueue.shift()! : this._matches.at(-1) ?? null;
|
||||
}
|
||||
|
||||
replace(replaceString: string): Promise<boolean> {
|
||||
this.replaceCalls.push(replaceString);
|
||||
return Promise.resolve(this._matches.length > 0);
|
||||
}
|
||||
|
||||
replaceAll(replaceString: string): Promise<IReplaceAllResult> {
|
||||
this.replaceAllCalls.push(replaceString);
|
||||
return Promise.resolve(this._replaceAllResult);
|
||||
}
|
||||
|
||||
focusSelection(): void {
|
||||
this.focusCount++;
|
||||
}
|
||||
}
|
||||
|
||||
class TestFindReplaceProvider {
|
||||
capabilities: IFindReplaceProviderCapabilities = {
|
||||
caseSensitive: true,
|
||||
matchesTheWholeWord: true,
|
||||
matchesTheWholeCell: false,
|
||||
findDirection: false,
|
||||
findScope: false,
|
||||
findBy: false,
|
||||
};
|
||||
|
||||
supportedType = UniverInstanceType.UNIVER_DOC;
|
||||
readonly queries: IFindQuery[] = [];
|
||||
readonly models: TestFindModel[] = [];
|
||||
modelConfigs: ITestModelConfig[] = [];
|
||||
terminateCount = 0;
|
||||
|
||||
constructor(@Inject(Injector) private readonly _injector: Injector) {}
|
||||
|
||||
isSupported(unit: UnitModel): boolean {
|
||||
return unit.type === this.supportedType;
|
||||
}
|
||||
|
||||
async find(query: IFindQuery): Promise<FindModel[]> {
|
||||
this.queries.push(query);
|
||||
this.models.splice(0, this.models.length, ...this.modelConfigs.map((config) =>
|
||||
this._injector.createInstance(TestFindModel, config.unitId, config.matches, config.replaceAllResult)
|
||||
));
|
||||
return this.models;
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.terminateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
function createService(registerProvider = true) {
|
||||
const injector = new Injector();
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
injector.add([IContextService, { useClass: TestContextService as never }]);
|
||||
injector.add([TestFindReplaceProvider]);
|
||||
injector.add([FindReplaceService]);
|
||||
const service = injector.get(FindReplaceService);
|
||||
const provider = injector.get(TestFindReplaceProvider);
|
||||
if (registerProvider) {
|
||||
service.registerFindReplaceProvider(provider);
|
||||
}
|
||||
return {
|
||||
injector,
|
||||
service,
|
||||
provider,
|
||||
contextService: injector.get(IContextService) as unknown as TestContextService,
|
||||
univerInstanceService: injector.get(IUniverInstanceService) as unknown as TestUniverInstanceService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('FindReplaceService', () => {
|
||||
it('selects the provider supporting the focused unit', async () => {
|
||||
const { service, provider, univerInstanceService } = createService();
|
||||
univerInstanceService.focusedUnit = {
|
||||
getUnitId: () => 'doc-1',
|
||||
type: UniverInstanceType.UNIVER_DOC,
|
||||
} as UnitModel;
|
||||
univerInstanceService.focused$.next('doc-1');
|
||||
|
||||
expect(service.start()).toBe(true);
|
||||
await expect(firstValueFrom(service.providerCapabilities$)).resolves.toEqual(provider.capabilities);
|
||||
expect(provider.queries).toHaveLength(0);
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('does not start when no provider supports the focused unit', () => {
|
||||
const { service, univerInstanceService } = createService();
|
||||
univerInstanceService.focusedUnit = {
|
||||
getUnitId: () => 'slide-1',
|
||||
type: UniverInstanceType.UNIVER_SLIDE,
|
||||
} as UnitModel;
|
||||
univerInstanceService.focused$.next('slide-1');
|
||||
|
||||
expect(service.start()).toBe(false);
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should start session and move to next match', async () => {
|
||||
const { service, provider } = createService();
|
||||
const match1: IFindMatch = { provider: 'p', unitId: 'u1', range: { i: 1 }, replaceable: true } as any;
|
||||
const match2: IFindMatch = { provider: 'p', unitId: 'u1', range: { i: 2 }, replaceable: false } as any;
|
||||
provider.modelConfigs = [{ unitId: 'u1', matches: [match1, match2], replaceAllResult: { success: 2, failure: 0 } }];
|
||||
|
||||
expect(service.start(false)).toBe(true);
|
||||
|
||||
service.changeFindString('hello');
|
||||
service.changeFindDirection(FindDirection.COLUMN);
|
||||
service.changeFindScope(FindScope.UNIT);
|
||||
service.changeFindBy(FindBy.FORMULA);
|
||||
|
||||
service.find();
|
||||
await awaitTime(0);
|
||||
|
||||
service.moveToNextMatch();
|
||||
expect(service.getCurrentMatch()).toEqual(match1);
|
||||
|
||||
const replaceables = await new Promise<IFindMatch[]>((resolve) => {
|
||||
service.replaceables$.subscribe((r) => {
|
||||
if (r.length) resolve(r);
|
||||
});
|
||||
});
|
||||
expect(replaceables).toEqual([match1]);
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should search with the typed replace query on the first navigation in replace mode', async () => {
|
||||
const { service, provider, contextService } = createService();
|
||||
const match: IFindMatch = { provider: 'p', unitId: 'u1', range: { row: 1 }, replaceable: true } as any;
|
||||
provider.modelConfigs = [{ unitId: 'u1', matches: [match] }];
|
||||
const focusSignals: number[] = [];
|
||||
service.focusSignal$.subscribe(() => focusSignals.push(focusSignals.length + 1));
|
||||
|
||||
expect(service.start(true)).toBe(true);
|
||||
service.changeInputtingFindString('invoice');
|
||||
service.moveToNextMatch();
|
||||
await awaitTime(0);
|
||||
|
||||
expect(provider.queries.at(-1)).toMatchObject({
|
||||
findString: 'invoice',
|
||||
replaceRevealed: true,
|
||||
matchesTheWholeWord: false,
|
||||
});
|
||||
expect(service.getCurrentMatch()).toEqual(match);
|
||||
expect(focusSignals).toEqual([1]);
|
||||
expect(contextService.values.map(({ value }) => value)).toContain(true);
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should hide replacement controls without terminating the find session', () => {
|
||||
const { service, contextService } = createService();
|
||||
|
||||
expect(service.start(true)).toBe(true);
|
||||
service.changeInputtingFindString('invoice');
|
||||
service.hideReplace();
|
||||
|
||||
expect(service.revealed).toBe(true);
|
||||
expect(service.replaceRevealed).toBe(false);
|
||||
expect(service.getFindString()).toBe('invoice');
|
||||
expect(contextService.values.at(-1)?.value).toBe(false);
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should navigate across find models when the current unit has no next or previous match', async () => {
|
||||
const { service, provider } = createService();
|
||||
const match1: IFindMatch = { provider: 'p', unitId: 'u1', range: { row: 1 }, replaceable: true } as any;
|
||||
const match2: IFindMatch = { provider: 'p', unitId: 'u2', range: { row: 2 }, replaceable: true } as any;
|
||||
provider.modelConfigs = [
|
||||
{ unitId: 'u1', matches: [match1] },
|
||||
{ unitId: 'u2', matches: [match2] },
|
||||
];
|
||||
|
||||
expect(service.start()).toBe(true);
|
||||
service.changeFindString('total');
|
||||
service.find();
|
||||
await awaitTime(0);
|
||||
|
||||
const [firstModel, secondModel] = provider.models;
|
||||
firstModel.nextQueue = [null];
|
||||
secondModel.nextQueue = [match2];
|
||||
service.moveToNextMatch();
|
||||
expect(service.getCurrentMatch()).toEqual(match2);
|
||||
expect(secondModel.nextParams).toContainEqual({ ignoreSelection: true });
|
||||
|
||||
secondModel.previousQueue = [null];
|
||||
firstModel.previousQueue = [match1];
|
||||
service.moveToPreviousMatch();
|
||||
expect(service.getCurrentMatch()).toEqual(match1);
|
||||
expect(firstModel.previousParams).toContainEqual({ ignoreSelection: true });
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should aggregate replace-all failures without clearing the current search session', async () => {
|
||||
const { service, provider } = createService();
|
||||
const match1: IFindMatch = { provider: 'p', unitId: 'u1', range: { row: 1 }, replaceable: true } as any;
|
||||
const match2: IFindMatch = { provider: 'p', unitId: 'u1', range: { row: 2 }, replaceable: true } as any;
|
||||
provider.modelConfigs = [
|
||||
{ unitId: 'u1', matches: [match1], replaceAllResult: { success: 2, failure: 0 } },
|
||||
{ unitId: 'u2', matches: [match2], replaceAllResult: { success: 0, failure: 1 } },
|
||||
];
|
||||
|
||||
expect(service.start()).toBe(true);
|
||||
service.changeFindString('draft');
|
||||
service.changeReplaceString('final');
|
||||
service.find();
|
||||
await awaitTime(0);
|
||||
|
||||
const terminateCountBeforeReplaceAll = provider.terminateCount;
|
||||
await expect(service.replaceAll()).resolves.toEqual({ success: 2, failure: 1 });
|
||||
expect(provider.models.map((model) => model.replaceAllCalls)).toEqual([['final'], ['final']]);
|
||||
expect(service.getCurrentMatch()).toEqual(match1);
|
||||
expect(provider.terminateCount).toBe(terminateCountBeforeReplaceAll);
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should report no matches and clear replaceable results when providers find nothing', async () => {
|
||||
const { service, provider } = createService();
|
||||
provider.modelConfigs = [{ unitId: 'u1', matches: [] }];
|
||||
const replaceableSnapshots: IFindMatch[][] = [];
|
||||
service.replaceables$.subscribe((replaceables) => replaceableSnapshots.push(replaceables));
|
||||
|
||||
expect(service.start()).toBe(true);
|
||||
service.changeFindString('missing');
|
||||
service.find();
|
||||
await awaitTime(0);
|
||||
|
||||
expect(service.getCurrentMatch()).toBeNull();
|
||||
expect(replaceableSnapshots.at(-1)).toEqual([]);
|
||||
|
||||
service.terminate();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('should fail commands that require an active session when no provider has started one', async () => {
|
||||
const { service } = createService(false);
|
||||
|
||||
expect(service.start()).toBe(false);
|
||||
expect(await service.replace()).toBe(false);
|
||||
await expect(service.replaceAll()).rejects.toThrow('model is not initialized');
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
30
packages/find-replace/src/services/context-keys.ts
Normal file
30
packages/find-replace/src/services/context-keys.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 FIND_REPLACE_INPUT_FOCUS = 'FIND_REPLACE_INPUT_FOCUS';
|
||||
|
||||
/**
|
||||
* If find replace panel is focused.
|
||||
*/
|
||||
export const FIND_REPLACE_DIALOG_FOCUS = 'FIND_REPLACE_DIALOG_FOCUS';
|
||||
|
||||
/**
|
||||
* If the find replace feature is activated and the replace is revealed.
|
||||
*/
|
||||
export const FIND_REPLACE_REPLACE_REVEALED = 'FIND_REPLACE_REPLACE_REVEALED';
|
||||
|
||||
/** If the focused unit has a find-replace provider. */
|
||||
export const FIND_REPLACE_AVAILABLE = 'FIND_REPLACE_AVAILABLE';
|
||||
957
packages/find-replace/src/services/find-replace.service.ts
Normal file
957
packages/find-replace/src/services/find-replace.service.ts
Normal file
@@ -0,0 +1,957 @@
|
||||
/**
|
||||
* 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, Nullable, UnitModel } from '@univerjs/core';
|
||||
import type { Observable } from 'rxjs';
|
||||
import {
|
||||
createIdentifier,
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { RENDER_RAW_FORMULA_KEY } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject, combineLatest, debounceTime, Subject, throttleTime } from 'rxjs';
|
||||
import { FIND_REPLACE_AVAILABLE, FIND_REPLACE_REPLACE_REVEALED } from './context-keys';
|
||||
|
||||
export type FindProgressFn = () => void;
|
||||
|
||||
export interface IFindComplete<T extends IFindMatch = IFindMatch> {
|
||||
results: T[];
|
||||
}
|
||||
|
||||
export interface IFindMatch<T = unknown> {
|
||||
provider: string;
|
||||
unitId: string;
|
||||
range: T;
|
||||
|
||||
/** Indicates if the match could be replaced. */
|
||||
replaceable?: boolean;
|
||||
}
|
||||
|
||||
export interface IFindMoveParams {
|
||||
/** Go to next (previous) matching in a loop. */
|
||||
loop?: boolean;
|
||||
|
||||
/** If the the selection is on the match and then should stay on the match. */
|
||||
stayIfOnMatch?: boolean;
|
||||
|
||||
/** Go the next selection ignoring the current selection's position. */
|
||||
ignoreSelection?: boolean;
|
||||
|
||||
/**
|
||||
* If this param is true, we should only change matching position without performing focusing.
|
||||
* This usually happens when "moving" is triggered when a document's content changes.
|
||||
*/
|
||||
noFocus?: boolean;
|
||||
}
|
||||
|
||||
export interface IReplaceAllResult {
|
||||
success: number;
|
||||
failure: number;
|
||||
}
|
||||
|
||||
export abstract class FindModel extends Disposable {
|
||||
abstract readonly unitId: string;
|
||||
|
||||
/**
|
||||
* Find model should emit new matches from this observable if they changed no matter due to incremental
|
||||
* or document's content changes.
|
||||
*/
|
||||
abstract readonly matchesUpdate$: Observable<IFindMatch[]>;
|
||||
abstract readonly activelyChangingMatch$: Observable<IFindMatch>;
|
||||
|
||||
abstract getMatches(): IFindMatch[];
|
||||
|
||||
abstract moveToNextMatch(params?: IFindMoveParams): IFindMatch | null;
|
||||
abstract moveToPreviousMatch(params?: IFindMoveParams): IFindMatch | null;
|
||||
|
||||
/** Replace the currently focused matching if there is one. */
|
||||
abstract replace(replaceString: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Replace all matches. This method would return how many
|
||||
*/
|
||||
abstract replaceAll(replaceString: string): Promise<IReplaceAllResult>;
|
||||
|
||||
abstract focusSelection(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider should be implemented by a business to provide the find results.
|
||||
*/
|
||||
export interface IFindReplaceProviderCapabilities {
|
||||
caseSensitive: boolean;
|
||||
matchesTheWholeWord: boolean;
|
||||
matchesTheWholeCell: boolean;
|
||||
findDirection: boolean;
|
||||
findScope: boolean;
|
||||
findBy: boolean;
|
||||
}
|
||||
|
||||
export interface IFindReplaceProvider {
|
||||
readonly capabilities: IFindReplaceProviderCapabilities;
|
||||
isSupported(unit: UnitModel): boolean;
|
||||
find(query: IFindQuery): Promise<FindModel[]>;
|
||||
terminate(): void;
|
||||
}
|
||||
|
||||
type IReplaceableMatch = IFindMatch & { replaceable: boolean };
|
||||
|
||||
/**
|
||||
* This service works as a core of the find & replace feature.
|
||||
*/
|
||||
export interface IFindReplaceService {
|
||||
readonly stateUpdates$: Observable<Partial<IFindReplaceState>>;
|
||||
readonly state$: Observable<IFindReplaceState>;
|
||||
|
||||
readonly currentMatch$: Observable<Nullable<IFindMatch>>;
|
||||
/** An observable value of all matches those could be replaced. */
|
||||
readonly replaceables$: Observable<IReplaceableMatch[]>;
|
||||
|
||||
readonly focusSignal$: Observable<void>;
|
||||
readonly providerCapabilities$: Observable<Nullable<IFindReplaceProviderCapabilities>>;
|
||||
|
||||
readonly revealed: boolean;
|
||||
readonly replaceRevealed: boolean;
|
||||
|
||||
/**
|
||||
* Register a find replace provider to the service. The provider is the actual bearer to
|
||||
* perform the find in different kinds of documents or different environments.
|
||||
* @param provider the find replace provider
|
||||
*/
|
||||
registerFindReplaceProvider(provider: IFindReplaceProvider): IDisposable;
|
||||
|
||||
/**
|
||||
* Get find string from the internal state.
|
||||
*/
|
||||
getFindString(): string;
|
||||
|
||||
/**
|
||||
* Start a find & replace session.
|
||||
* @returns execution result
|
||||
*/
|
||||
start(revealReplace?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* Terminate a find session and clear all caches.
|
||||
*/
|
||||
terminate(): void;
|
||||
|
||||
/**
|
||||
* Start searching with the current conditions.
|
||||
*/
|
||||
find(): void;
|
||||
|
||||
focusFindInput(): void;
|
||||
|
||||
revealReplace(): void;
|
||||
hideReplace(): void;
|
||||
changeFindString(value: string): void;
|
||||
changeInputtingFindString(value: string): void;
|
||||
changeReplaceString(value: string): void;
|
||||
changeCaseSensitive(sensitive: boolean): void;
|
||||
changeMatchesTheWholeCell(wholeCell: boolean): void;
|
||||
changeMatchesTheWholeWord(wholeWord: boolean): void;
|
||||
changeFindScope(scope: FindScope): void;
|
||||
changeFindDirection(direction: FindDirection): void;
|
||||
changeFindBy(findBy: FindBy): void;
|
||||
|
||||
moveToNextMatch(): void;
|
||||
moveToPreviousMatch(): void;
|
||||
|
||||
replace(): Promise<boolean>;
|
||||
replaceAll(): Promise<IReplaceAllResult>;
|
||||
|
||||
/**
|
||||
* Focus the selection of the current match.
|
||||
*/
|
||||
focusSelection(): void;
|
||||
|
||||
getProviders(): Set<IFindReplaceProvider>;
|
||||
}
|
||||
export const IFindReplaceService = createIdentifier<IFindReplaceService>('find-replace.service');
|
||||
|
||||
/**
|
||||
* The find query object with finding options.
|
||||
*/
|
||||
export interface IFindQuery extends Pick<
|
||||
IFindReplaceState,
|
||||
| 'replaceRevealed'
|
||||
| 'findString'
|
||||
| 'caseSensitive'
|
||||
| 'findBy'
|
||||
| 'findDirection'
|
||||
| 'findScope'
|
||||
| 'matchesTheWholeCell'
|
||||
| 'matchesTheWholeWord'
|
||||
> { }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param statusUpdate
|
||||
*/
|
||||
function shouldStateUpdateTriggerResearch(statusUpdate: Partial<IFindReplaceState>): boolean {
|
||||
if (typeof statusUpdate.findString !== 'undefined') return true;
|
||||
if (typeof statusUpdate.inputtingFindString !== 'undefined') return true;
|
||||
if (typeof statusUpdate.findDirection !== 'undefined') return true;
|
||||
if (typeof statusUpdate.matchesTheWholeCell !== 'undefined') return true;
|
||||
if (typeof statusUpdate.matchesTheWholeWord !== 'undefined') return true;
|
||||
if (typeof statusUpdate.caseSensitive !== 'undefined') return true;
|
||||
if (typeof statusUpdate.findScope !== 'undefined') return true;
|
||||
if (typeof statusUpdate.findBy !== 'undefined') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class stores find replace results and provides methods to perform replace or something.
|
||||
*
|
||||
* It **only** live through a find-replace session and would be disposed when the user
|
||||
* close the find replace dialog (considered as session being terminated).
|
||||
*/
|
||||
export class FindReplaceModel extends Disposable {
|
||||
readonly currentMatch$ = new BehaviorSubject<Nullable<IFindMatch>>(null);
|
||||
readonly replaceables$ = new BehaviorSubject<IReplaceableMatch[]>([]);
|
||||
|
||||
/** All find models returned by providers. */
|
||||
private _findModels: FindModel[] = [];
|
||||
|
||||
/** The find model that the current match is from. */
|
||||
private _matchingModel: FindModel | null = null;
|
||||
|
||||
private _matches: IFindMatch[] = [];
|
||||
|
||||
private _currentSearchingDisposables: Nullable<DisposableCollection> = null;
|
||||
|
||||
get searched(): boolean { return this._findModels.length > 0; }
|
||||
|
||||
constructor(
|
||||
private readonly _state: FindReplaceState,
|
||||
private readonly _providers: Set<IFindReplaceProvider>,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
// should restart finding when the following conditions changed
|
||||
this.disposeWithMe(
|
||||
this._state.stateUpdates$.pipe(throttleTime(200, undefined, { leading: true, trailing: true }))
|
||||
.subscribe(async (stateUpdate) => {
|
||||
const state = this._state.state;
|
||||
if (shouldStateUpdateTriggerResearch(stateUpdate)) {
|
||||
if (state.findString !== '' && !state.replaceRevealed) {
|
||||
await this._startSearching();
|
||||
this._state.changeState({ findCompleted: true });
|
||||
} else if (stateUpdate.replaceRevealed !== true) {
|
||||
this._stopSearching();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._stopSearching();
|
||||
|
||||
this.currentMatch$.complete();
|
||||
this.replaceables$.complete();
|
||||
|
||||
// reset all state, including
|
||||
this._state.changeState({ ...createInitFindReplaceState(), revealed: false });
|
||||
}
|
||||
|
||||
async start(): Promise<IFindComplete> {
|
||||
if (!this._state.findString) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
const complete = await this._startSearching();
|
||||
this._state.changeState({ findCompleted: true });
|
||||
return complete;
|
||||
}
|
||||
|
||||
focusSelection(): void {
|
||||
this._matchingModel?.focusSelection();
|
||||
}
|
||||
|
||||
/** Call this method to start a `searching`. */
|
||||
private async _startSearching(): Promise<IFindComplete> {
|
||||
if (!this._state.findString) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
const providers = Array.from(this._providers);
|
||||
const findModels = (this._findModels = (
|
||||
await Promise.all(providers.map((provider) => provider.find({
|
||||
findString: this._state.findString,
|
||||
findDirection: this._state.findDirection,
|
||||
findScope: this._state.findScope,
|
||||
findBy: this._state.findBy,
|
||||
replaceRevealed: this._state.replaceRevealed,
|
||||
caseSensitive: this._state.caseSensitive,
|
||||
matchesTheWholeCell: this._state.matchesTheWholeCell,
|
||||
matchesTheWholeWord: this._state.matchesTheWholeWord,
|
||||
})))
|
||||
).flat());
|
||||
|
||||
this._subscribeToModelsChanges(findModels);
|
||||
|
||||
const newMatches = this._matches = findModels.map((c) => c.getMatches()).flat();
|
||||
this.replaceables$.next(newMatches.filter((m) => m.replaceable) as IReplaceableMatch[]);
|
||||
|
||||
if (!newMatches.length) {
|
||||
this._state.changeState({ matchesCount: 0, matchesPosition: 0 });
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
this._moveToInitialMatch(findModels);
|
||||
this._state.changeState({ matchesCount: newMatches.length });
|
||||
return { results: newMatches };
|
||||
}
|
||||
|
||||
/** Terminate the current searching session, when searching string is empty. */
|
||||
private _stopSearching(): void {
|
||||
this._providers.forEach((provider) => provider.terminate());
|
||||
this._findModels = [];
|
||||
this._matches = [];
|
||||
this._matchingModel = null;
|
||||
|
||||
this._currentSearchingDisposables?.dispose();
|
||||
this._currentSearchingDisposables = null;
|
||||
|
||||
this.currentMatch$.next(null);
|
||||
this.replaceables$.next([]);
|
||||
|
||||
this._state.changeState({
|
||||
findCompleted: false,
|
||||
matchesCount: 0,
|
||||
matchesPosition: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// When a document's content changes, we should reset all matches and try to move to the next match.
|
||||
private _subscribeToModelsChanges(models: FindModel[]): void {
|
||||
const disposables = this._currentSearchingDisposables = new DisposableCollection();
|
||||
|
||||
const matchesUpdateSubscription = combineLatest(models.map((model) => model.matchesUpdate$))
|
||||
.pipe(debounceTime(220))
|
||||
.subscribe(([...allMatches]) => {
|
||||
const newMatches = this._matches = allMatches.flat();
|
||||
if (newMatches.length) {
|
||||
this._moveToInitialMatch(this._findModels, true);
|
||||
this._state.changeState({ matchesCount: newMatches.length });
|
||||
this.replaceables$.next(newMatches.filter((m) => m.replaceable) as IReplaceableMatch[]);
|
||||
} else {
|
||||
this._state.changeState({ matchesCount: 0, matchesPosition: 0 });
|
||||
this.replaceables$.next([]);
|
||||
}
|
||||
});
|
||||
|
||||
models.forEach((model) => disposables.add(toDisposable(model.activelyChangingMatch$.subscribe((match) => {
|
||||
const index = this._matches.findIndex((m) => m === match);
|
||||
this._state.changeState({ matchesPosition: index + 1 });
|
||||
}))));
|
||||
|
||||
disposables.add(toDisposable(matchesUpdateSubscription));
|
||||
}
|
||||
|
||||
async replace(): Promise<boolean> {
|
||||
if (!this._matchingModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._matchingModel.replace(this._state.replaceString);
|
||||
}
|
||||
|
||||
async replaceAll(): Promise<IReplaceAllResult> {
|
||||
const result = await Promise.all(this._findModels.map((m) => m.replaceAll(this._state.replaceString)))
|
||||
.then((results) => results.reduce((acc, cur) => {
|
||||
acc.success += cur.success;
|
||||
acc.failure += cur.failure;
|
||||
return acc;
|
||||
}, { success: 0, failure: 0 }));
|
||||
|
||||
if (result.failure === 0) {
|
||||
this._stopSearching();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getCurrentMatch(): Nullable<IFindMatch> {
|
||||
return this._state.matchesPosition > 0 ? this._matches[this._state.matchesPosition - 1] : null;
|
||||
}
|
||||
|
||||
private _markMatch(match: IFindMatch): void {
|
||||
const index = this._matches.findIndex((value) => value === match);
|
||||
this.currentMatch$.next(match);
|
||||
this._state.changeState({ matchesPosition: index + 1 });
|
||||
}
|
||||
|
||||
moveToNextMatch() {
|
||||
if (!this._matchingModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loopInCurrentUnit = this._findModels.length === 1;
|
||||
const nextMatch = this._matchingModel.moveToNextMatch({ loop: loopInCurrentUnit });
|
||||
if (nextMatch) {
|
||||
this._markMatch(nextMatch);
|
||||
return nextMatch;
|
||||
} else {
|
||||
const currentModelIndex = this._findModels.findIndex((m) => m === this._matchingModel);
|
||||
return this._moveToNextUnitMatch(currentModelIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private _moveToNextUnitMatch(startingIndex: number) {
|
||||
const l = this._findModels.length;
|
||||
for (let i = (startingIndex + 1) % l; i !== startingIndex;) {
|
||||
const nextPositionModel = this._findModels[i];
|
||||
const nextMatch = nextPositionModel.moveToNextMatch({ ignoreSelection: true });
|
||||
if (nextMatch) {
|
||||
this._matchingModel = nextPositionModel;
|
||||
this._markMatch(nextMatch);
|
||||
return nextMatch;
|
||||
}
|
||||
|
||||
i = (i + 1) % l;
|
||||
}
|
||||
|
||||
if (this._matchingModel) {
|
||||
const nextMatch = this._matchingModel.moveToNextMatch({ ignoreSelection: true });
|
||||
if (nextMatch) this._markMatch(nextMatch);
|
||||
return nextMatch;
|
||||
}
|
||||
}
|
||||
|
||||
moveToPreviousMatch() {
|
||||
if (!this._matchingModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loopInCurrentUnit = this._findModels.length === 1;
|
||||
const nextMatch = this._matchingModel.moveToPreviousMatch({ loop: loopInCurrentUnit });
|
||||
if (nextMatch) {
|
||||
const index = this._matches.findIndex((value) => value === nextMatch);
|
||||
this.currentMatch$.next(nextMatch);
|
||||
this._state.changeState({ matchesPosition: index + 1 });
|
||||
return nextMatch;
|
||||
} else {
|
||||
const l = this._findModels.length;
|
||||
const currentModelIndex = this._findModels.findIndex((m) => m === this._matchingModel);
|
||||
for (let i = (currentModelIndex - 1 + l) % l; i !== currentModelIndex;) {
|
||||
const nextPositionModel = this._findModels[i];
|
||||
const nextMatch = nextPositionModel.moveToPreviousMatch({ ignoreSelection: true });
|
||||
if (nextMatch) {
|
||||
this._matchingModel = nextPositionModel;
|
||||
this._markMatch(nextMatch);
|
||||
return nextMatch;
|
||||
}
|
||||
|
||||
i = (i - 1) % l;
|
||||
}
|
||||
|
||||
const nextMatch = this._matchingModel.moveToPreviousMatch({ ignoreSelection: true });
|
||||
if (nextMatch) this._markMatch(nextMatch);
|
||||
return nextMatch;
|
||||
}
|
||||
}
|
||||
|
||||
private _moveToInitialMatch(findModels: FindModel[], noFocus = false): number {
|
||||
const focusedUnitId = this._univerInstanceService.getFocusedUnit()?.getUnitId();
|
||||
if (!focusedUnitId) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const i = findModels.findIndex((model) => model.unitId === focusedUnitId);
|
||||
if (i !== -1) {
|
||||
this._matchingModel = findModels[i];
|
||||
const nextMatch = this._matchingModel.moveToNextMatch({ stayIfOnMatch: true, noFocus });
|
||||
if (nextMatch) {
|
||||
this._markMatch(nextMatch);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
this._moveToNextUnitMatch(i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export enum FindDirection {
|
||||
/** Default. */
|
||||
ROW = 'row',
|
||||
COLUMN = 'column',
|
||||
}
|
||||
|
||||
export enum FindBy {
|
||||
VALUE = 'value',
|
||||
FORMULA = 'formula',
|
||||
}
|
||||
|
||||
export enum FindScope {
|
||||
/** Default. */
|
||||
SUBUNIT = 'subunit',
|
||||
/** Find the scope in the current unit. */
|
||||
UNIT = 'unit',
|
||||
}
|
||||
|
||||
export interface IFindReplaceState {
|
||||
revealed: boolean;
|
||||
|
||||
/** The string user inputs in the input box. */
|
||||
findString: string;
|
||||
inputtingFindString: string;
|
||||
replaceString?: string;
|
||||
|
||||
/** Indicates if is in replacing mode. */
|
||||
replaceRevealed: boolean;
|
||||
|
||||
/** The currently focused match's index (1-based). */
|
||||
matchesPosition: number;
|
||||
/** The number of all matches. */
|
||||
matchesCount: number;
|
||||
|
||||
/** Indicates if an user triggered finding process is progressed. */
|
||||
findCompleted: boolean;
|
||||
|
||||
caseSensitive: boolean;
|
||||
matchesTheWholeCell: boolean;
|
||||
matchesTheWholeWord: boolean;
|
||||
findDirection: FindDirection;
|
||||
findScope: FindScope;
|
||||
findBy: FindBy;
|
||||
}
|
||||
|
||||
export function createInitFindReplaceState(): IFindReplaceState {
|
||||
return {
|
||||
caseSensitive: false,
|
||||
findBy: FindBy.VALUE,
|
||||
findCompleted: false,
|
||||
findDirection: FindDirection.ROW,
|
||||
findScope: FindScope.SUBUNIT,
|
||||
findString: '',
|
||||
inputtingFindString: '',
|
||||
matchesCount: 0,
|
||||
matchesPosition: 0,
|
||||
matchesTheWholeCell: false,
|
||||
matchesTheWholeWord: false,
|
||||
replaceRevealed: false,
|
||||
replaceString: '',
|
||||
revealed: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This class stores find replace options state. These state are stored
|
||||
* here instead of the React component so we can change state from
|
||||
* operations.
|
||||
*/
|
||||
export class FindReplaceState {
|
||||
private readonly _stateUpdates$ = new Subject<Partial<IFindReplaceState>>();
|
||||
readonly stateUpdates$: Observable<Partial<IFindReplaceState>> = this._stateUpdates$.asObservable();
|
||||
|
||||
private readonly _state$ = new BehaviorSubject<IFindReplaceState>(createInitFindReplaceState());
|
||||
readonly state$ = this._state$.asObservable();
|
||||
get state(): IFindReplaceState {
|
||||
return this._state$.getValue();
|
||||
}
|
||||
|
||||
private _findString = '';
|
||||
private _inputtingFindString = '';
|
||||
private _replaceString = '';
|
||||
private _revealed = false;
|
||||
private _replaceRevealed = false;
|
||||
private _matchesPosition = 0;
|
||||
private _matchesCount = 0;
|
||||
private _caseSensitive = true;
|
||||
private _matchesTheWholeCell = false;
|
||||
private _matchesTheWholeWord = false;
|
||||
private _findDirection = FindDirection.ROW;
|
||||
private _findScope = FindScope.SUBUNIT;
|
||||
private _findBy = FindBy.VALUE;
|
||||
private _findCompleted = false;
|
||||
|
||||
get inputtingFindString(): string { return this._inputtingFindString; }
|
||||
get findString(): string { return this._findString; }
|
||||
get revealed(): boolean { return this._revealed; }
|
||||
get replaceRevealed(): boolean { return this._replaceRevealed; }
|
||||
get matchesPosition(): number { return this._matchesPosition; }
|
||||
get matchesCount(): number { return this._matchesCount; }
|
||||
get replaceString(): string { return this._replaceString; }
|
||||
get caseSensitive(): boolean { return this._caseSensitive; }
|
||||
get matchesTheWholeCell(): boolean { return this._matchesTheWholeCell; }
|
||||
get matchesTheWholeWord(): boolean { return this._matchesTheWholeWord; }
|
||||
get findDirection(): FindDirection { return this._findDirection; }
|
||||
get findScope(): FindScope { return this._findScope; }
|
||||
get findBy(): FindBy { return this._findBy; }
|
||||
get findCompleted(): boolean { return this._findCompleted; }
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
changeState(changes: Partial<IFindReplaceState>): void {
|
||||
let changed = false;
|
||||
const changedState: Partial<IFindReplaceState> = {};
|
||||
|
||||
if (typeof changes.findString !== 'undefined' && changes.findString !== this._findString) {
|
||||
this._findString = changes.findString;
|
||||
changedState.findString = this._findString;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.revealed !== 'undefined' && changes.revealed !== this._revealed) {
|
||||
this._revealed = changes.revealed;
|
||||
changedState.revealed = changes.revealed;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.replaceRevealed !== 'undefined' && changes.replaceRevealed !== this._replaceRevealed) {
|
||||
this._replaceRevealed = changes.replaceRevealed;
|
||||
changedState.replaceRevealed = changes.replaceRevealed;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.replaceString !== 'undefined' && changes.replaceString !== this._replaceString) {
|
||||
this._replaceString = changes.replaceString;
|
||||
changedState.replaceString = changes.replaceString;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.matchesCount !== 'undefined' && changes.matchesCount !== this._matchesCount) {
|
||||
this._matchesCount = changes.matchesCount;
|
||||
changedState.matchesCount = changes.matchesCount;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.matchesPosition !== 'undefined' && changes.matchesPosition !== this._matchesPosition) {
|
||||
this._matchesPosition = changes.matchesPosition;
|
||||
changedState.matchesPosition = changes.matchesPosition;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.findBy !== 'undefined' && changes.findBy !== this._findBy) {
|
||||
this._findBy = changes.findBy;
|
||||
changedState.findBy = changes.findBy;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.findScope !== 'undefined' && changes.findScope !== this._findScope) {
|
||||
this._findScope = changes.findScope;
|
||||
changedState.findScope = changes.findScope;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.findDirection !== 'undefined' && changes.findDirection !== this._findDirection) {
|
||||
this._findDirection = changes.findDirection;
|
||||
changedState.findDirection = changes.findDirection;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.caseSensitive !== 'undefined' && changes.caseSensitive !== this._caseSensitive) {
|
||||
this._caseSensitive = changes.caseSensitive;
|
||||
changedState.caseSensitive = changes.caseSensitive;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.matchesTheWholeCell !== 'undefined' && changes.matchesTheWholeCell !== this._matchesTheWholeCell) {
|
||||
this._matchesTheWholeCell = changes.matchesTheWholeCell;
|
||||
changedState.matchesTheWholeCell = changes.matchesTheWholeCell;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.matchesTheWholeWord !== 'undefined' && changes.matchesTheWholeWord !== this._matchesTheWholeWord) {
|
||||
this._matchesTheWholeWord = changes.matchesTheWholeWord;
|
||||
changedState.matchesTheWholeWord = changes.matchesTheWholeWord;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.inputtingFindString !== 'undefined' && changes.inputtingFindString !== this._inputtingFindString) {
|
||||
this._inputtingFindString = changes.inputtingFindString;
|
||||
changedState.inputtingFindString = changes.inputtingFindString;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (typeof changes.findCompleted !== 'undefined' && changes.findCompleted !== this._findCompleted) {
|
||||
this._findCompleted = changes.findCompleted;
|
||||
changedState.findCompleted = changes.findCompleted;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this._state$.next({
|
||||
caseSensitive: this._caseSensitive,
|
||||
findBy: this._findBy,
|
||||
findCompleted: this._findCompleted,
|
||||
findDirection: this._findDirection,
|
||||
findScope: this._findScope,
|
||||
findString: this._findString,
|
||||
inputtingFindString: this._inputtingFindString,
|
||||
matchesCount: this._matchesCount,
|
||||
matchesPosition: this._matchesPosition,
|
||||
matchesTheWholeCell: this._matchesTheWholeCell,
|
||||
matchesTheWholeWord: this._matchesTheWholeWord,
|
||||
replaceRevealed: this._replaceRevealed,
|
||||
revealed: this._revealed,
|
||||
});
|
||||
|
||||
this._stateUpdates$.next(changedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since Univer' Find&Replace features works in a plugin manner,
|
||||
// each `IFind&Replace` provider should have their own implementations
|
||||
// of 'state' and 'model'.
|
||||
|
||||
export class FindReplaceService extends Disposable implements IFindReplaceService {
|
||||
private readonly _providers = new Set<IFindReplaceProvider>();
|
||||
private readonly _state = new FindReplaceState();
|
||||
private _model: Nullable<FindReplaceModel>;
|
||||
private _modelDisposables: Nullable<DisposableCollection> = null;
|
||||
|
||||
private readonly _currentMatch$ = new BehaviorSubject<Nullable<IFindMatch>>(null);
|
||||
readonly currentMatch$ = this._currentMatch$.asObservable();
|
||||
|
||||
private readonly _replaceables$ = new BehaviorSubject<IReplaceableMatch[]>([]);
|
||||
readonly replaceables$ = this._replaceables$.asObservable();
|
||||
|
||||
private readonly _focusSignal$ = new Subject<void>();
|
||||
readonly focusSignal$ = this._focusSignal$.asObservable();
|
||||
|
||||
private readonly _providerCapabilities$ = new BehaviorSubject<Nullable<IFindReplaceProviderCapabilities>>(null);
|
||||
readonly providerCapabilities$ = this._providerCapabilities$.asObservable();
|
||||
private _activeProvider: Nullable<IFindReplaceProvider> = null;
|
||||
|
||||
get stateUpdates$() { return this._state.stateUpdates$; }
|
||||
get state$() { return this._state.state$; }
|
||||
get revealed(): boolean { return this._state.revealed; }
|
||||
get replaceRevealed(): boolean { return this._state.replaceRevealed; }
|
||||
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(toDisposable(this._univerInstanceService.focused$.subscribe(() => this._syncActiveProvider())));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._currentMatch$.next(null);
|
||||
this._currentMatch$.complete();
|
||||
|
||||
this._replaceables$.next([]);
|
||||
this._replaceables$.complete();
|
||||
|
||||
this._focusSignal$.complete();
|
||||
|
||||
this._providerCapabilities$.complete();
|
||||
}
|
||||
|
||||
getProviders(): Set<IFindReplaceProvider> {
|
||||
return this._providers;
|
||||
}
|
||||
|
||||
getCurrentMatch(): Nullable<IFindMatch> {
|
||||
return this._model?.getCurrentMatch();
|
||||
}
|
||||
|
||||
getFindString(): string {
|
||||
return this._state.findString;
|
||||
}
|
||||
|
||||
changeFindString(findString: string): void {
|
||||
this._state.changeState({ findString });
|
||||
}
|
||||
|
||||
focusFindInput(): void {
|
||||
this._focusSignal$.next();
|
||||
}
|
||||
|
||||
changeInputtingFindString(value: string): void {
|
||||
if (value) {
|
||||
this._state.changeState({ inputtingFindString: value });
|
||||
} else {
|
||||
this._state.changeState({ inputtingFindString: '', findString: '' });
|
||||
}
|
||||
}
|
||||
|
||||
changeReplaceString(replaceString: string): void {
|
||||
this._state.changeState({ replaceString });
|
||||
}
|
||||
|
||||
changeMatchesTheWholeCell(matchesTheWholeCell: boolean): void {
|
||||
this._state.changeState({ matchesTheWholeCell });
|
||||
}
|
||||
|
||||
changeMatchesTheWholeWord(matchesTheWholeWord: boolean): void {
|
||||
this._state.changeState({ matchesTheWholeWord });
|
||||
}
|
||||
|
||||
changeCaseSensitive(caseSensitive: boolean): void {
|
||||
this._state.changeState({ caseSensitive });
|
||||
}
|
||||
|
||||
changeFindBy(findBy: FindBy): void {
|
||||
this._state.changeState({ findBy });
|
||||
this._toggleDisplayRawFormula(findBy === FindBy.FORMULA);
|
||||
}
|
||||
|
||||
changeFindScope(scope: FindScope): void {
|
||||
this._state.changeState({ findScope: scope });
|
||||
}
|
||||
|
||||
changeFindDirection(direction: FindDirection): void {
|
||||
this._state.changeState({ findDirection: direction });
|
||||
}
|
||||
|
||||
moveToNextMatch(): void {
|
||||
if (!this._model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state.replaceRevealed && !this._model.searched) {
|
||||
this._state.changeState({ findString: this._state.inputtingFindString });
|
||||
this._model.start();
|
||||
} else {
|
||||
this._model.moveToNextMatch();
|
||||
}
|
||||
|
||||
this._focusSignal$.next();
|
||||
}
|
||||
|
||||
moveToPreviousMatch(): void {
|
||||
if (!this._model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state.replaceRevealed && !this._model.searched) {
|
||||
this._state.changeState({ findString: this._state.inputtingFindString });
|
||||
this._model.start();
|
||||
} else {
|
||||
this._model.moveToPreviousMatch();
|
||||
}
|
||||
|
||||
this._focusSignal$.next();
|
||||
}
|
||||
|
||||
async replace(): Promise<boolean> {
|
||||
if (!this._model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._model.replace();
|
||||
}
|
||||
|
||||
async replaceAll(): Promise<IReplaceAllResult> {
|
||||
if (!this._model) {
|
||||
throw new Error('[FindReplaceService] replaceAll: model is not initialized!');
|
||||
}
|
||||
|
||||
return this._model.replaceAll();
|
||||
}
|
||||
|
||||
revealReplace(): void {
|
||||
this._state.changeState({ replaceRevealed: true, inputtingFindString: this._state.findString });
|
||||
this._toggleRevealReplace(true);
|
||||
}
|
||||
|
||||
hideReplace(): void {
|
||||
this._state.changeState({
|
||||
replaceRevealed: false,
|
||||
findString: this._state.inputtingFindString,
|
||||
});
|
||||
this._toggleRevealReplace(false);
|
||||
}
|
||||
|
||||
focusSelection(): void {
|
||||
this._model?.focusSelection();
|
||||
}
|
||||
|
||||
start(revealReplace = false): boolean {
|
||||
if (!this._activeProvider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._model = this._injector.createInstance(FindReplaceModel, this._state, new Set([this._activeProvider]));
|
||||
this._modelDisposables = new DisposableCollection();
|
||||
this._modelDisposables.add(toDisposable(this._model.currentMatch$.subscribe((match) => this._currentMatch$.next(match))));
|
||||
this._modelDisposables.add(toDisposable(this._model.replaceables$.subscribe((replaceables) => this._replaceables$.next(replaceables))));
|
||||
|
||||
const newState = createInitFindReplaceState();
|
||||
if (revealReplace) {
|
||||
newState.replaceRevealed = true;
|
||||
}
|
||||
|
||||
this._state.changeState(newState);
|
||||
this._toggleRevealReplace(revealReplace);
|
||||
return true;
|
||||
}
|
||||
|
||||
find(): void {
|
||||
this._model?.start();
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this._model?.dispose();
|
||||
this._model = null;
|
||||
|
||||
this._modelDisposables?.dispose();
|
||||
this._modelDisposables = null;
|
||||
|
||||
this._toggleDisplayRawFormula(false);
|
||||
this._toggleRevealReplace(false);
|
||||
}
|
||||
|
||||
registerFindReplaceProvider(provider: IFindReplaceProvider): IDisposable {
|
||||
this._providers.add(provider);
|
||||
this._syncActiveProvider();
|
||||
return toDisposable(() => {
|
||||
this._providers.delete(provider);
|
||||
this._syncActiveProvider();
|
||||
});
|
||||
}
|
||||
|
||||
private _syncActiveProvider(): void {
|
||||
const unit = this._univerInstanceService.getFocusedUnit();
|
||||
const provider = unit == null
|
||||
? null
|
||||
: Array.from(this._providers).find((candidate) => candidate.isSupported(unit)) ?? null;
|
||||
if (provider === this._activeProvider) return;
|
||||
|
||||
if (this.revealed) this.terminate();
|
||||
this._activeProvider = provider;
|
||||
this._providerCapabilities$.next(provider?.capabilities ?? null);
|
||||
this._contextService.setContextValue(FIND_REPLACE_AVAILABLE, provider != null);
|
||||
}
|
||||
|
||||
private _toggleRevealReplace(revealReplace: boolean): void {
|
||||
this._contextService.setContextValue(FIND_REPLACE_REPLACE_REVEALED, revealReplace);
|
||||
}
|
||||
|
||||
private _toggleDisplayRawFormula(force: boolean): void {
|
||||
this._contextService.setContextValue(RENDER_RAW_FORMULA_KEY, force);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 { Meta } from '@storybook/react';
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import { LocaleService, LocaleType } from '@univerjs/core';
|
||||
import { DesktopLayoutService, ILayoutService, RediContext } from '@univerjs/ui';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { FindReplaceController } from '../../controllers/find-replace.controller';
|
||||
import enUS from '../../locale/en-US';
|
||||
import ruRU from '../../locale/ru-RU';
|
||||
import zhCN from '../../locale/zh-CN';
|
||||
import { FindReplaceService, IFindReplaceService } from '../../services/find-replace.service';
|
||||
import { FindReplaceDialog } from './FindReplaceDialog';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Find Replace Dialog',
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
function FindDialogDemo() {
|
||||
const { injector } = useContext(RediContext);
|
||||
|
||||
const [inject] = useState(() => {
|
||||
const deps: Dependency[] = [
|
||||
[IFindReplaceService, { useClass: FindReplaceService }],
|
||||
[ILayoutService, { useClass: DesktopLayoutService }],
|
||||
[FindReplaceController],
|
||||
];
|
||||
|
||||
injector?.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: enUS,
|
||||
[LocaleType.ZH_CN]: zhCN,
|
||||
[LocaleType.RU_RU]: ruRU,
|
||||
});
|
||||
|
||||
deps.forEach((dependency) => injector?.add(dependency));
|
||||
|
||||
return injector;
|
||||
});
|
||||
|
||||
const memoizedValue = useMemo(() => ({ injector: inject }), [inject]);
|
||||
|
||||
return (
|
||||
<RediContext.Provider value={memoizedValue}>
|
||||
<FindReplaceDialog />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const FindDialog = {
|
||||
render() {
|
||||
return <FindDialogDemo />;
|
||||
},
|
||||
};
|
||||
|
||||
function ReplaceDialogDemo() {
|
||||
const { injector } = useContext(RediContext);
|
||||
|
||||
const [inject] = useState(() => {
|
||||
const deps: Dependency[] = [
|
||||
[IFindReplaceService, { useClass: FindReplaceService }],
|
||||
[ILayoutService, { useClass: DesktopLayoutService }],
|
||||
[FindReplaceController],
|
||||
];
|
||||
|
||||
injector?.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: enUS,
|
||||
[LocaleType.ZH_CN]: zhCN,
|
||||
[LocaleType.RU_RU]: ruRU,
|
||||
});
|
||||
|
||||
deps.forEach((dependency) => injector?.add(dependency));
|
||||
|
||||
return injector;
|
||||
});
|
||||
|
||||
const memoizedValue = useMemo(() => ({ injector: inject }), [inject]);
|
||||
|
||||
return (
|
||||
<RediContext.Provider value={memoizedValue}>
|
||||
<FindReplaceDialog />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const ReplaceDialog = {
|
||||
render() {
|
||||
return <ReplaceDialogDemo />;
|
||||
},
|
||||
};
|
||||
358
packages/find-replace/src/views/dialog/FindReplaceDialog.tsx
Normal file
358
packages/find-replace/src/views/dialog/FindReplaceDialog.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, IContextService, LocaleService } from '@univerjs/core';
|
||||
import { Button, Checkbox, FormDualColumnLayout, FormLayout, Input, MessageType, Select } from '@univerjs/design';
|
||||
import { ILayoutService, IMessageService, useDebounceFn, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from '../../commands/commands/replace.command';
|
||||
import { OpenReplaceDialogOperation } from '../../commands/operations/find-replace.operation';
|
||||
import { FIND_REPLACE_DIALOG_FOCUS, FIND_REPLACE_INPUT_FOCUS } from '../../services/context-keys';
|
||||
import { FindBy, FindDirection, FindScope, IFindReplaceService } from '../../services/find-replace.service';
|
||||
import { SearchInput } from './SearchInput';
|
||||
|
||||
interface ISubFormRef {
|
||||
focus(): void;
|
||||
selectHasFocus(): boolean;
|
||||
}
|
||||
|
||||
const FIND_SCOPE_OPTIONS: Array<{ label: LocaleKey; value: FindScope }> = [
|
||||
{ label: 'find-replace.dialog.find-scope.current-sheet', value: FindScope.SUBUNIT },
|
||||
{ label: 'find-replace.dialog.find-scope.workbook', value: FindScope.UNIT },
|
||||
];
|
||||
|
||||
const FIND_DIRECTION_OPTIONS: Array<{ label: LocaleKey; value: FindDirection }> = [
|
||||
{ label: 'find-replace.dialog.find-direction.row', value: FindDirection.ROW },
|
||||
{ label: 'find-replace.dialog.find-direction.column', value: FindDirection.COLUMN },
|
||||
];
|
||||
|
||||
const FIND_BY_OPTIONS: Array<{ label: LocaleKey; value: FindBy }> = [
|
||||
{ label: 'find-replace.dialog.find-by.value', value: FindBy.VALUE },
|
||||
{ label: 'find-replace.dialog.find-by.formula', value: FindBy.FORMULA },
|
||||
];
|
||||
|
||||
function useFindInputFocus(findReplaceService: IFindReplaceService, ref: Parameters<typeof useImperativeHandle>[0]) {
|
||||
const pendingFocusFrameRef = useRef<number | null>(null);
|
||||
const focus = useCallback(() => {
|
||||
(document.querySelector('.univer-find-input input') as HTMLInputElement | null)?.focus();
|
||||
}, []);
|
||||
const focusAfterDialogClose = useCallback(() => {
|
||||
focus();
|
||||
pendingFocusFrameRef.current = requestAnimationFrame(() => {
|
||||
pendingFocusFrameRef.current = null;
|
||||
focus();
|
||||
});
|
||||
}, [focus]);
|
||||
|
||||
const selectHasFocus = useCallback(() => {
|
||||
const allInputs = document.querySelectorAll('[data-u-comp=find-replace-dialog] [data-u-comp=search-input]');
|
||||
return Array.from(allInputs).some((input) => input === document.activeElement);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({ focus, selectHasFocus }));
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = findReplaceService.focusSignal$.subscribe(() => focus());
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
if (pendingFocusFrameRef.current !== null) {
|
||||
cancelAnimationFrame(pendingFocusFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [findReplaceService, focus]);
|
||||
|
||||
return { focus, focusAfterDialogClose, selectHasFocus };
|
||||
}
|
||||
|
||||
export const FindDialog = forwardRef(function FindDialogImpl(_props, ref) {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const findReplaceService = useDependency(IFindReplaceService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const state = useObservable(findReplaceService.state$, undefined, true);
|
||||
const { findCompleted, findString, matchesCount, matchesPosition } = state;
|
||||
|
||||
const revealReplace = useCallback(() => {
|
||||
commandService.executeCommand(OpenReplaceDialogOperation.id);
|
||||
}, [commandService]);
|
||||
|
||||
const onFindStringChange = useDebounceFn((findString: string) => {
|
||||
return findReplaceService.changeFindString(findString);
|
||||
}, 500);
|
||||
|
||||
useFindInputFocus(findReplaceService, ref);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchInput
|
||||
findCompleted={findCompleted}
|
||||
matchesCount={matchesCount}
|
||||
matchesPosition={matchesPosition}
|
||||
findReplaceService={findReplaceService}
|
||||
initialFindString={findString}
|
||||
onChange={onFindStringChange}
|
||||
/>
|
||||
<div className="univer-mt-4 univer-text-center">
|
||||
<a
|
||||
className={`
|
||||
hover:univer-text-primary-500/80
|
||||
univer-cursor-pointer univer-text-sm univer-text-primary-500 univer-transition-colors
|
||||
`}
|
||||
onClick={revealReplace}
|
||||
>
|
||||
{localeService.t<LocaleKey>('find-replace.dialog.advanced-finding')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReplaceDialog = forwardRef(function ReplaceDialogImpl(_props, ref) {
|
||||
const findReplaceService = useDependency(IFindReplaceService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const messageService = useDependency(IMessageService);
|
||||
|
||||
const currentMatch = useObservable(findReplaceService.currentMatch$, undefined, true);
|
||||
const replaceables = useObservable(findReplaceService.replaceables$, undefined, true);
|
||||
const capabilities = useObservable(findReplaceService.providerCapabilities$, null, true);
|
||||
const state = useObservable(findReplaceService.state$, undefined, true);
|
||||
const {
|
||||
matchesCount,
|
||||
matchesPosition,
|
||||
findString,
|
||||
inputtingFindString,
|
||||
replaceString,
|
||||
caseSensitive,
|
||||
matchesTheWholeCell,
|
||||
matchesTheWholeWord,
|
||||
findDirection,
|
||||
findScope,
|
||||
findBy,
|
||||
findCompleted,
|
||||
} = state;
|
||||
|
||||
const findDisabled = inputtingFindString.length === 0;
|
||||
const replaceDisabled = matchesCount === 0 || !currentMatch?.replaceable;
|
||||
const replaceAllDisabled = replaceables.length === 0;
|
||||
|
||||
const onFindStringChange = useCallback(
|
||||
(newValue: string) => findReplaceService.changeInputtingFindString(newValue),
|
||||
[findReplaceService]
|
||||
);
|
||||
const onReplaceStringChange = useCallback(
|
||||
(replaceString: string) => findReplaceService.changeReplaceString(replaceString),
|
||||
[findReplaceService]
|
||||
);
|
||||
|
||||
const { focusAfterDialogClose } = useFindInputFocus(findReplaceService, ref);
|
||||
|
||||
const onClickFindButton = useCallback(() => {
|
||||
if (findString === inputtingFindString) {
|
||||
findReplaceService.moveToNextMatch();
|
||||
} else {
|
||||
findReplaceService.changeFindString(inputtingFindString);
|
||||
findReplaceService.find();
|
||||
}
|
||||
}, [findString, inputtingFindString, findReplaceService]);
|
||||
const onClickReplaceButton = useCallback(() => commandService.executeCommand(ReplaceCurrentMatchCommand.id), [commandService]);
|
||||
const onClickReplaceAllButton = useCallback(async () => {
|
||||
await commandService.executeCommand(ReplaceAllMatchesCommand.id);
|
||||
focusAfterDialogClose();
|
||||
}, [commandService, focusAfterDialogClose]);
|
||||
|
||||
const onChangeFindDirection = useCallback((findDirection: string) => {
|
||||
findReplaceService.changeFindDirection(findDirection as FindDirection);
|
||||
}, [findReplaceService]);
|
||||
const onChangeFindScope = useCallback((findScope: string) => {
|
||||
findReplaceService.changeFindScope(findScope as FindScope);
|
||||
}, [findReplaceService]);
|
||||
const onChangeFindBy = useCallback((findBy: string) => {
|
||||
findReplaceService.changeFindBy(findBy as FindBy);
|
||||
}, [findReplaceService]);
|
||||
|
||||
const findScopeOptions = FIND_SCOPE_OPTIONS.map((option) => ({ ...option, label: localeService.t(option.label) }));
|
||||
const findDirectionOptions = FIND_DIRECTION_OPTIONS.map((option) => ({ ...option, label: localeService.t(option.label) }));
|
||||
const findByOptions = FIND_BY_OPTIONS.map((option) => ({ ...option, label: localeService.t(option.label) }));
|
||||
|
||||
useEffect(() => {
|
||||
const shouldDisplayNoMatchInfo = findCompleted && matchesCount === 0;
|
||||
|
||||
if (shouldDisplayNoMatchInfo) {
|
||||
messageService.show({
|
||||
content: localeService.t<LocaleKey>('find-replace.dialog.no-match'),
|
||||
type: MessageType.Warning,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}, [findCompleted, matchesCount, messageService, localeService]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormLayout label={localeService.t<LocaleKey>('find-replace.dialog.find')}>
|
||||
<SearchInput
|
||||
findCompleted={findCompleted}
|
||||
className="univer-find-input"
|
||||
matchesCount={matchesCount}
|
||||
matchesPosition={matchesPosition}
|
||||
findReplaceService={findReplaceService}
|
||||
initialFindString={inputtingFindString}
|
||||
onChange={onFindStringChange}
|
||||
/>
|
||||
</FormLayout>
|
||||
<FormLayout label={localeService.t<LocaleKey>('find-replace.dialog.replace')}>
|
||||
<Input
|
||||
placeholder={localeService.t<LocaleKey>('find-replace.dialog.replace-placeholder')}
|
||||
value={replaceString}
|
||||
onChange={(value) => onReplaceStringChange(value)}
|
||||
/>
|
||||
</FormLayout>
|
||||
{capabilities?.findDirection && (
|
||||
<FormLayout label={localeService.t<LocaleKey>('find-replace.dialog.find-direction.title')}>
|
||||
<Select value={findDirection} options={findDirectionOptions} onChange={onChangeFindDirection} />
|
||||
</FormLayout>
|
||||
)}
|
||||
{(capabilities?.findScope || capabilities?.findBy) && (
|
||||
<FormDualColumnLayout>
|
||||
<>
|
||||
{capabilities.findScope && (
|
||||
<FormLayout label={localeService.t<LocaleKey>('find-replace.dialog.find-scope.title')}>
|
||||
<Select value={findScope} options={findScopeOptions} onChange={onChangeFindScope} />
|
||||
</FormLayout>
|
||||
)}
|
||||
{capabilities.findBy && (
|
||||
<FormLayout label={localeService.t<LocaleKey>('find-replace.dialog.find-by.title')}>
|
||||
<Select value={findBy} options={findByOptions} onChange={onChangeFindBy} />
|
||||
</FormLayout>
|
||||
)}
|
||||
</>
|
||||
</FormDualColumnLayout>
|
||||
)}
|
||||
{(capabilities?.caseSensitive || capabilities?.matchesTheWholeCell || capabilities?.matchesTheWholeWord) && (
|
||||
<FormDualColumnLayout>
|
||||
<>
|
||||
{capabilities.caseSensitive && (
|
||||
<FormLayout>
|
||||
<Checkbox
|
||||
checked={caseSensitive}
|
||||
onChange={(checked) => {
|
||||
findReplaceService.changeCaseSensitive(Boolean(checked));
|
||||
if (findCompleted) {
|
||||
findReplaceService.find();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{localeService.t<LocaleKey>('find-replace.dialog.case-sensitive')}
|
||||
</Checkbox>
|
||||
</FormLayout>
|
||||
)}
|
||||
{capabilities.matchesTheWholeCell && (
|
||||
<FormLayout>
|
||||
<Checkbox checked={matchesTheWholeCell} onChange={(checked) => findReplaceService.changeMatchesTheWholeCell(Boolean(checked))}>
|
||||
{localeService.t<LocaleKey>('find-replace.dialog.match-the-whole-cell')}
|
||||
</Checkbox>
|
||||
</FormLayout>
|
||||
)}
|
||||
{capabilities.matchesTheWholeWord && (
|
||||
<FormLayout>
|
||||
<Checkbox
|
||||
checked={matchesTheWholeWord}
|
||||
onChange={(checked) => {
|
||||
findReplaceService.changeMatchesTheWholeWord(Boolean(checked));
|
||||
if (findCompleted) {
|
||||
findReplaceService.find();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{localeService.t<LocaleKey>('find-replace.dialog.match-the-whole-word')}
|
||||
</Checkbox>
|
||||
</FormLayout>
|
||||
)}
|
||||
</>
|
||||
</FormDualColumnLayout>
|
||||
)}
|
||||
<div className="univer-mt-6 univer-flex univer-justify-between">
|
||||
<Button variant="primary" onClick={onClickFindButton} disabled={findDisabled}>{localeService.t<LocaleKey>('find-replace.dialog.find')}</Button>
|
||||
<span className="univer-inline-flex univer-gap-2">
|
||||
<Button disabled={replaceDisabled} onClick={onClickReplaceButton}>{localeService.t<LocaleKey>('find-replace.dialog.replace')}</Button>
|
||||
<Button disabled={replaceAllDisabled} onClick={onClickReplaceAllButton}>{localeService.t<LocaleKey>('find-replace.dialog.replace-all')}</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export function FindReplaceDialog() {
|
||||
const findReplaceService = useDependency(IFindReplaceService);
|
||||
const layoutService = useDependency(ILayoutService);
|
||||
const contextService = useDependency(IContextService);
|
||||
|
||||
const state = useObservable(findReplaceService.state$, undefined, true);
|
||||
|
||||
const dialogContainerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const container = dialogContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const disposable = layoutService.registerContainerElement(container);
|
||||
return () => disposable.dispose();
|
||||
}, [layoutService]);
|
||||
|
||||
const focusRef = useRef<ISubFormRef>(null);
|
||||
const setDialogContainerFocus = useCallback(
|
||||
(focused: boolean) => contextService.setContextValue(FIND_REPLACE_DIALOG_FOCUS, focused),
|
||||
[contextService]
|
||||
);
|
||||
const setDialogInputFocus = useCallback(
|
||||
(focused: boolean) => contextService.setContextValue(FIND_REPLACE_INPUT_FOCUS, focused),
|
||||
[contextService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const focusSubscription = fromEvent(document, 'focusin').subscribe((event) => {
|
||||
if (event.target && dialogContainerRef.current?.contains(event.target as Node)) {
|
||||
setDialogContainerFocus(true);
|
||||
} else {
|
||||
setDialogContainerFocus(false);
|
||||
}
|
||||
|
||||
if (!focusRef.current || !focusRef.current.selectHasFocus()) {
|
||||
setDialogInputFocus(false);
|
||||
} else {
|
||||
setDialogInputFocus(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus the input element the first time we open the find replace dialog.
|
||||
focusRef.current?.focus();
|
||||
|
||||
setDialogContainerFocus(true);
|
||||
setDialogInputFocus(true);
|
||||
|
||||
return () => {
|
||||
focusSubscription.unsubscribe();
|
||||
setDialogContainerFocus(false);
|
||||
};
|
||||
}, [setDialogContainerFocus, setDialogInputFocus]);
|
||||
|
||||
return (
|
||||
<div ref={dialogContainerRef} data-u-comp="find-replace-dialog">
|
||||
{!state.replaceRevealed ? <FindDialog ref={focusRef} /> : <ReplaceDialog ref={focusRef} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
packages/find-replace/src/views/dialog/SearchInput.tsx
Normal file
90
packages/find-replace/src/views/dialog/SearchInput.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IInputProps } from '@univerjs/design';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { IFindReplaceService } from '../../services/find-replace.service';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { Input, Pager } from '@univerjs/design';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface ISearchInputProps extends Pick<IInputProps, 'onFocus' | 'onBlur' | 'className' | 'onChange'> {
|
||||
findCompleted: boolean;
|
||||
findReplaceService: IFindReplaceService;
|
||||
matchesPosition: number;
|
||||
matchesCount: number;
|
||||
initialFindString: string;
|
||||
}
|
||||
|
||||
export function SearchInput(props: ISearchInputProps) {
|
||||
const {
|
||||
findCompleted: findComplete,
|
||||
matchesCount,
|
||||
matchesPosition,
|
||||
initialFindString,
|
||||
findReplaceService,
|
||||
onChange,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
const [value, setValue] = useState(initialFindString);
|
||||
const noResult = findComplete && matchesCount === 0;
|
||||
const text = noResult
|
||||
? localeService.t<LocaleKey>('find-replace.dialog.no-result')
|
||||
: matchesCount === 0
|
||||
? ' '
|
||||
: undefined;
|
||||
|
||||
function handleChangePosition(newIndex: number) {
|
||||
if (matchesPosition === matchesCount && newIndex === 1) {
|
||||
findReplaceService.moveToNextMatch();
|
||||
} else if (matchesPosition === 1 && newIndex === matchesCount) {
|
||||
findReplaceService.moveToPreviousMatch();
|
||||
} else if (newIndex < matchesPosition) {
|
||||
findReplaceService.moveToPreviousMatch();
|
||||
} else {
|
||||
findReplaceService.moveToNextMatch();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="univer-relative univer-flex univer-items-center univer-gap-2" onDrag={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
data-u-comp="search-input"
|
||||
autoFocus
|
||||
placeholder={localeService.t<LocaleKey>('find-replace.dialog.find-placeholder')}
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
onChange?.(value);
|
||||
}}
|
||||
slot={(
|
||||
<Pager
|
||||
loop
|
||||
text={text}
|
||||
value={matchesPosition}
|
||||
total={matchesCount}
|
||||
onChange={handleChangePosition}
|
||||
/>
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
/**
|
||||
* 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 { awaitTime, ICommandService, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { ILayoutService, IMessageService, RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createTestBed } from '../../../__tests__/create-test-bed';
|
||||
import {
|
||||
ReplaceAllMatchesCommand,
|
||||
ReplaceCurrentMatchCommand,
|
||||
} from '../../../commands/commands/replace.command';
|
||||
import {
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
} from '../../../commands/operations/find-replace.operation';
|
||||
import {
|
||||
FindBy,
|
||||
FindDirection,
|
||||
FindModel,
|
||||
FindReplaceService,
|
||||
FindScope,
|
||||
IFindReplaceService,
|
||||
} from '../../../services/find-replace.service';
|
||||
import { FindReplaceDialog } from '../FindReplaceDialog';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
interface ICellText {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ICellRange {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IObservedFindState {
|
||||
matchesCount: number;
|
||||
matchesPosition: number;
|
||||
}
|
||||
|
||||
type FindProvider = Parameters<IFindReplaceService['registerFindReplaceProvider']>[0];
|
||||
type FindQuery = Parameters<FindProvider['find']>[0];
|
||||
type FindCapabilities = FindProvider['capabilities'];
|
||||
type FindMoveParams = Parameters<FindModel['moveToNextMatch']>[0];
|
||||
type FindMatch = NonNullable<ReturnType<FindModel['moveToNextMatch']>> & { range: ICellRange };
|
||||
type ReplaceAllResult = Awaited<ReturnType<FindModel['replaceAll']>>;
|
||||
|
||||
const INITIAL_ROWS: ICellText[] = [
|
||||
{ id: 'A1', text: 'apple' },
|
||||
{ id: 'A2', text: 'pineapple' },
|
||||
{ id: 'A3', text: 'Apple' },
|
||||
{ id: 'A4', text: 'banana' },
|
||||
];
|
||||
|
||||
const FIND_LABEL = 'find-replace.dialog.find';
|
||||
const REPLACE_LABEL = 'find-replace.dialog.replace';
|
||||
const REPLACE_ALL_LABEL = 'find-replace.dialog.replace-all';
|
||||
const ADVANCED_LABEL = 'find-replace.dialog.advanced-finding';
|
||||
|
||||
class TextFindModel extends FindModel {
|
||||
private readonly _matchesUpdate$ = new BehaviorSubject<FindMatch[]>([]);
|
||||
private readonly _activelyChangingMatch$ = new Subject<FindMatch>();
|
||||
private _matches: FindMatch[] = [];
|
||||
private _position = -1;
|
||||
|
||||
override readonly unitId = 'test';
|
||||
override readonly matchesUpdate$ = this._matchesUpdate$.asObservable();
|
||||
override readonly activelyChangingMatch$ = this._activelyChangingMatch$.asObservable();
|
||||
|
||||
constructor(
|
||||
private readonly _rows: ICellText[],
|
||||
private readonly _query: FindQuery
|
||||
) {
|
||||
super();
|
||||
this._refreshMatches();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._matchesUpdate$.complete();
|
||||
this._activelyChangingMatch$.complete();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
override getMatches(): FindMatch[] {
|
||||
return this._matches;
|
||||
}
|
||||
|
||||
override moveToNextMatch(params?: FindMoveParams): FindMatch | null {
|
||||
if (this._matches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params?.stayIfOnMatch && this._position >= 0) {
|
||||
return this._emitCurrent();
|
||||
}
|
||||
|
||||
if (params?.ignoreSelection) {
|
||||
this._position = 0;
|
||||
} else {
|
||||
this._position = this._position + 1;
|
||||
if (this._position >= this._matches.length) {
|
||||
this._position = params?.loop ? 0 : this._matches.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return this._emitCurrent();
|
||||
}
|
||||
|
||||
override moveToPreviousMatch(params?: FindMoveParams): FindMatch | null {
|
||||
if (this._matches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params?.ignoreSelection) {
|
||||
this._position = this._matches.length - 1;
|
||||
} else {
|
||||
this._position = this._position - 1;
|
||||
if (this._position < 0) {
|
||||
this._position = params?.loop ? this._matches.length - 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return this._emitCurrent();
|
||||
}
|
||||
|
||||
override async replace(replaceString: string): Promise<boolean> {
|
||||
const match = this._matches[this._position];
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const row = this._rows.find((item) => item.id === match.range.id);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
row.text = replaceString;
|
||||
this._refreshMatches();
|
||||
return true;
|
||||
}
|
||||
|
||||
override async replaceAll(replaceString: string): Promise<ReplaceAllResult> {
|
||||
const ids = new Set(this._matches.map((match) => match.range.id));
|
||||
this._rows.forEach((row) => {
|
||||
if (ids.has(row.id)) {
|
||||
row.text = replaceString;
|
||||
}
|
||||
});
|
||||
const success = ids.size;
|
||||
this._refreshMatches();
|
||||
return { success, failure: 0 };
|
||||
}
|
||||
|
||||
override focusSelection(): void {
|
||||
const match = this._matches[this._position];
|
||||
if (match) {
|
||||
this._activelyChangingMatch$.next(match);
|
||||
}
|
||||
}
|
||||
|
||||
private _emitCurrent(): FindMatch {
|
||||
const match = this._matches[this._position];
|
||||
this._activelyChangingMatch$.next(match);
|
||||
return match;
|
||||
}
|
||||
|
||||
private _refreshMatches(): void {
|
||||
this._matches = this._rows
|
||||
.filter((row) => this._doesRowMatch(row))
|
||||
.map((row) => ({
|
||||
provider: 'text-provider',
|
||||
unitId: this.unitId,
|
||||
range: { id: row.id },
|
||||
replaceable: true,
|
||||
}));
|
||||
this._position = this._matches.length > 0 ? Math.min(Math.max(this._position, 0), this._matches.length - 1) : -1;
|
||||
this._matchesUpdate$.next(this._matches);
|
||||
}
|
||||
|
||||
private _doesRowMatch(row: ICellText): boolean {
|
||||
const source = this._query.caseSensitive ? row.text : row.text.toLocaleLowerCase();
|
||||
const target = this._query.caseSensitive ? this._query.findString : this._query.findString.toLocaleLowerCase();
|
||||
return this._query.matchesTheWholeCell ? source === target : source.includes(target);
|
||||
}
|
||||
}
|
||||
|
||||
class TestFindReplaceProvider {
|
||||
capabilities: FindCapabilities = {
|
||||
caseSensitive: true,
|
||||
matchesTheWholeWord: false,
|
||||
matchesTheWholeCell: true,
|
||||
findDirection: true,
|
||||
findScope: true,
|
||||
findBy: true,
|
||||
};
|
||||
|
||||
readonly rows = INITIAL_ROWS.map((row) => ({ ...row }));
|
||||
readonly queries: FindQuery[] = [];
|
||||
|
||||
isSupported(unit: { type: UniverInstanceType }): boolean {
|
||||
return unit.type === UniverInstanceType.UNIVER_SHEET;
|
||||
}
|
||||
|
||||
async find(query: FindQuery): Promise<FindModel[]> {
|
||||
this.queries.push({ ...query });
|
||||
return [new TextFindModel(this.rows, query)];
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
}
|
||||
}
|
||||
|
||||
class TestLayoutService {
|
||||
private _focused = false;
|
||||
readonly rootContainerElement = document.body;
|
||||
|
||||
get isFocused() {
|
||||
return this._focused;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._focused = true;
|
||||
}
|
||||
|
||||
registerFocusHandler(): ReturnType<typeof toDisposable> {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
registerRootContainerElement(): ReturnType<typeof toDisposable> {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
registerContentElement(): ReturnType<typeof toDisposable> {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
registerContainerElement(): ReturnType<typeof toDisposable> {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
getContentElement() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
checkElementInCurrentContainers() {
|
||||
return true;
|
||||
}
|
||||
|
||||
checkContentIsFocused() {
|
||||
return this._focused;
|
||||
}
|
||||
}
|
||||
|
||||
function createDialogTestBed(capabilities?: FindCapabilities) {
|
||||
const testBed = createTestBed(undefined, [
|
||||
[IFindReplaceService, { useClass: FindReplaceService }],
|
||||
[ILayoutService, { useClass: TestLayoutService as never }],
|
||||
[TestFindReplaceProvider],
|
||||
]);
|
||||
|
||||
const commandService = testBed.get(ICommandService);
|
||||
[
|
||||
OpenFindDialogOperation,
|
||||
OpenReplaceDialogOperation,
|
||||
ReplaceCurrentMatchCommand,
|
||||
ReplaceAllMatchesCommand,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
const provider = testBed.get(TestFindReplaceProvider);
|
||||
if (capabilities) provider.capabilities = capabilities;
|
||||
const findReplaceService = testBed.get(IFindReplaceService);
|
||||
findReplaceService.registerFindReplaceProvider(provider);
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
commandService,
|
||||
provider,
|
||||
findReplaceService,
|
||||
};
|
||||
}
|
||||
|
||||
function renderDialog(injector: ReturnType<ReturnType<typeof createTestBed>['univer']['__getInjector']>) {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector }}>
|
||||
<FindReplaceDialog />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
return { container, root };
|
||||
}
|
||||
|
||||
function getInputs(container: HTMLElement): HTMLInputElement[] {
|
||||
return Array.from(container.querySelectorAll('input[type="text"]'));
|
||||
}
|
||||
|
||||
function getButton(container: HTMLElement, text: string): HTMLButtonElement {
|
||||
const button = Array.from(container.querySelectorAll('button')).find((item) => item.textContent?.trim() === text);
|
||||
if (!button) {
|
||||
throw new Error(`Button "${text}" was not found.`);
|
||||
}
|
||||
|
||||
return button as HTMLButtonElement;
|
||||
}
|
||||
|
||||
function getLink(container: HTMLElement, text: string): HTMLElement {
|
||||
const link = Array.from(container.querySelectorAll('a')).find((item) => item.textContent?.trim() === text);
|
||||
if (!link) {
|
||||
throw new Error(`Link "${text}" was not found.`);
|
||||
}
|
||||
|
||||
return link as HTMLElement;
|
||||
}
|
||||
|
||||
function setInputValue(input: HTMLInputElement, value: string): void {
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
setter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
async function flushSearch() {
|
||||
await act(async () => {
|
||||
await awaitTime(260);
|
||||
});
|
||||
}
|
||||
|
||||
describe('FindReplaceDialog', () => {
|
||||
let root: ReturnType<typeof createRoot> | undefined;
|
||||
let container: HTMLElement | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
act(() => root!.unmount());
|
||||
}
|
||||
container?.remove();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
});
|
||||
|
||||
it('runs a find session from the simple dialog input', async () => {
|
||||
const testBed = createDialogTestBed();
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenFindDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(getInputs(container!)[0], 'apple');
|
||||
});
|
||||
await act(async () => {
|
||||
await awaitTime(560);
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.queries.at(-1)).toMatchObject({
|
||||
findString: 'apple',
|
||||
replaceRevealed: false,
|
||||
findDirection: FindDirection.ROW,
|
||||
findScope: FindScope.SUBUNIT,
|
||||
findBy: FindBy.VALUE,
|
||||
caseSensitive: false,
|
||||
matchesTheWholeCell: false,
|
||||
});
|
||||
expect(readCurrentMatch(testBed.findReplaceService)?.range).toEqual({ id: 'A1' });
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 3, matchesPosition: 1 });
|
||||
});
|
||||
|
||||
it('reveals replace controls from the advanced link without restarting the session', async () => {
|
||||
const testBed = createDialogTestBed();
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenFindDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
await act(async () => {
|
||||
getLink(container!, ADVANCED_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(testBed.findReplaceService.replaceRevealed).toBe(true);
|
||||
expect(getButton(container!, FIND_LABEL).disabled).toBe(true);
|
||||
expect(getButton(container!, REPLACE_LABEL).disabled).toBe(true);
|
||||
expect(getButton(container!, REPLACE_ALL_LABEL).disabled).toBe(true);
|
||||
expect(getInputs(container!)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('shows only controls supported by the active provider', async () => {
|
||||
const testBed = createDialogTestBed({
|
||||
caseSensitive: true,
|
||||
matchesTheWholeWord: true,
|
||||
matchesTheWholeCell: false,
|
||||
findDirection: false,
|
||||
findScope: false,
|
||||
findBy: false,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenReplaceDialogOperation.id);
|
||||
});
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
expect(container.textContent).toContain('find-replace.dialog.case-sensitive');
|
||||
expect(container.textContent).toContain('find-replace.dialog.match-the-whole-word');
|
||||
expect(container.textContent).not.toContain('find-replace.dialog.find-direction.title');
|
||||
expect(container.textContent).not.toContain('find-replace.dialog.match-the-whole-cell');
|
||||
});
|
||||
|
||||
it('replaces the selected result and then replaces all remaining results', async () => {
|
||||
const testBed = createDialogTestBed();
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenReplaceDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
await act(async () => {
|
||||
const inputs = getInputs(container!);
|
||||
setInputValue(inputs[0], 'apple');
|
||||
setInputValue(inputs[1], 'orange');
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 3, matchesPosition: 1 });
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, REPLACE_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.rows.map((row) => row.text)).toEqual(['orange', 'pineapple', 'Apple', 'banana']);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, REPLACE_ALL_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.rows.map((row) => row.text)).toEqual(['orange', 'orange', 'orange', 'banana']);
|
||||
expect((testBed.get(IMessageService) as unknown as { messages: Array<{ content: string }> }).messages.at(-1)?.content).toBe('find-replace.replace.all-success');
|
||||
});
|
||||
|
||||
it('moves to the next result when finding the same replace dialog input again', async () => {
|
||||
const testBed = createDialogTestBed();
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenReplaceDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
await act(async () => {
|
||||
const inputs = getInputs(container!);
|
||||
setInputValue(inputs[0], 'apple');
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.queries).toHaveLength(1);
|
||||
expect(readCurrentMatch(testBed.findReplaceService)?.range).toEqual({ id: 'A1' });
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 3, matchesPosition: 1 });
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.queries).toHaveLength(1);
|
||||
expect(readCurrentMatch(testBed.findReplaceService)?.range).toEqual({ id: 'A2' });
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 3, matchesPosition: 2 });
|
||||
});
|
||||
|
||||
it('uses the replace dialog options when searching', async () => {
|
||||
const testBed = createDialogTestBed();
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenReplaceDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
await act(async () => {
|
||||
const inputs = getInputs(container!);
|
||||
setInputValue(inputs[0], 'Apple');
|
||||
const checkboxes = Array.from(container!.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'));
|
||||
checkboxes.forEach((checkbox) => checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true })));
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.queries.at(-1)).toMatchObject({
|
||||
findString: 'Apple',
|
||||
replaceRevealed: true,
|
||||
caseSensitive: true,
|
||||
matchesTheWholeCell: true,
|
||||
});
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 1, matchesPosition: 1 });
|
||||
expect(readCurrentMatch(testBed.findReplaceService)?.range).toEqual({ id: 'A3' });
|
||||
});
|
||||
|
||||
it('reruns completed searches when case or whole-word matching changes', async () => {
|
||||
const testBed = createDialogTestBed({
|
||||
caseSensitive: true,
|
||||
matchesTheWholeWord: true,
|
||||
matchesTheWholeCell: false,
|
||||
findDirection: false,
|
||||
findScope: false,
|
||||
findBy: false,
|
||||
});
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenReplaceDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
const checkboxes = Array.from(container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'));
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(getInputs(container!)[0], 'apple');
|
||||
checkboxes.forEach((checkbox) => checkbox.dispatchEvent(new MouseEvent('click', { bubbles: true })));
|
||||
});
|
||||
expect(testBed.provider.queries).toHaveLength(0);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
expect(testBed.provider.queries).toHaveLength(1);
|
||||
|
||||
await act(async () => {
|
||||
checkboxes[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
expect(testBed.provider.queries).toHaveLength(2);
|
||||
expect(testBed.provider.queries.at(-1)).toMatchObject({ caseSensitive: false, matchesTheWholeWord: true });
|
||||
|
||||
await act(async () => {
|
||||
checkboxes[1].dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
expect(testBed.provider.queries).toHaveLength(3);
|
||||
expect(testBed.provider.queries.at(-1)).toMatchObject({ caseSensitive: false, matchesTheWholeWord: false });
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(getInputs(container!)[0], 'kiwi');
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
expect(testBed.provider.queries).toHaveLength(4);
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 0, matchesPosition: 0 });
|
||||
|
||||
await act(async () => {
|
||||
checkboxes[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
expect(testBed.provider.queries).toHaveLength(5);
|
||||
expect(testBed.provider.queries.at(-1)).toMatchObject({ findString: 'kiwi', caseSensitive: true });
|
||||
});
|
||||
|
||||
it('warns and keeps replace actions disabled when a replace dialog search has no results', async () => {
|
||||
const testBed = createDialogTestBed();
|
||||
await act(async () => {
|
||||
testBed.commandService.syncExecuteCommand(OpenReplaceDialogOperation.id);
|
||||
});
|
||||
|
||||
const rendered = renderDialog(testBed.univer.__getInjector());
|
||||
root = rendered.root;
|
||||
container = rendered.container;
|
||||
|
||||
await act(async () => {
|
||||
const inputs = getInputs(container!);
|
||||
setInputValue(inputs[0], 'kiwi');
|
||||
setInputValue(inputs[1], 'orange');
|
||||
getButton(container!, FIND_LABEL).dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await flushSearch();
|
||||
|
||||
expect(testBed.provider.queries.at(-1)).toMatchObject({
|
||||
findString: 'kiwi',
|
||||
replaceRevealed: true,
|
||||
});
|
||||
expect(readState(testBed.findReplaceService)).toMatchObject({ matchesCount: 0, matchesPosition: 0 });
|
||||
expect(readCurrentMatch(testBed.findReplaceService)).toBeNull();
|
||||
expect((testBed.get(IMessageService) as unknown as { messages: Array<{ content: string }> }).messages.at(-1)?.content).toBe('find-replace.dialog.no-match');
|
||||
expect(getButton(container!, REPLACE_LABEL).disabled).toBe(true);
|
||||
expect(getButton(container!, REPLACE_ALL_LABEL).disabled).toBe(true);
|
||||
expect(testBed.provider.rows.map((row) => row.text)).toEqual(INITIAL_ROWS.map((row) => row.text));
|
||||
});
|
||||
});
|
||||
|
||||
function readState(findReplaceService: IFindReplaceService): IObservedFindState {
|
||||
let value!: IObservedFindState;
|
||||
const subscription = findReplaceService.state$.subscribe((state) => {
|
||||
value = state;
|
||||
});
|
||||
subscription.unsubscribe();
|
||||
return value;
|
||||
}
|
||||
|
||||
function readCurrentMatch(findReplaceService: IFindReplaceService): FindMatch | null {
|
||||
let value: FindMatch | null = null;
|
||||
const subscription = findReplaceService.currentMatch$.subscribe((match) => {
|
||||
value = match == null ? null : match as FindMatch;
|
||||
});
|
||||
subscription.unsubscribe();
|
||||
return value;
|
||||
}
|
||||
310
packages/find-replace/src/views/mobile/MobileFindReplaceBar.tsx
Normal file
310
packages/find-replace/src/views/mobile/MobileFindReplaceBar.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 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 { ReactNode } from 'react';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { clsx, Input, resetButtonClassName } from '@univerjs/design';
|
||||
import { ArrowDownIcon, ArrowUpIcon, CloseIcon, ConfigureTabIcon, SearchIcon } from '@univerjs/icons';
|
||||
import { IDialogService, MobileKeyboardInsetContext, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from '../../commands/commands/replace.command';
|
||||
import {
|
||||
CloseFindDialogOperation,
|
||||
GoToNextMatchOperation,
|
||||
GoToPreviousMatchOperation,
|
||||
} from '../../commands/operations/find-replace.operation';
|
||||
import {
|
||||
MOBILE_FIND_REPLACE_SETTINGS_COMPONENT,
|
||||
MOBILE_FIND_REPLACE_SETTINGS_DIALOG_ID,
|
||||
} from '../../const';
|
||||
import { IFindReplaceService } from '../../services/find-replace.service';
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 500;
|
||||
|
||||
export function MobileFindReplaceBar() {
|
||||
const findReplaceService = useDependency(IFindReplaceService);
|
||||
useObservable(findReplaceService.state$, undefined, true);
|
||||
|
||||
if (!findReplaceService.revealed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MobileFindReplaceBarContent />;
|
||||
}
|
||||
|
||||
function MobileFindReplaceBarContent() {
|
||||
const keyboardInset = useContext(MobileKeyboardInsetContext);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const dialogService = useDependency(IDialogService);
|
||||
const findReplaceService = useDependency(IFindReplaceService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const state = useObservable(findReplaceService.state$, undefined, true);
|
||||
const currentMatch = useObservable(findReplaceService.currentMatch$, null, true);
|
||||
const replaceables = useObservable(findReplaceService.replaceables$, [], true);
|
||||
const [findValue, setFindValue] = useState(() =>
|
||||
state.replaceRevealed ? state.inputtingFindString : state.findString);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const suppressNextFocusRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => inputRef.current?.focus());
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = findReplaceService.focusSignal$.subscribe(() => {
|
||||
if (suppressNextFocusRef.current) {
|
||||
suppressNextFocusRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [findReplaceService]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (searchTimerRef.current) {
|
||||
clearTimeout(searchTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function clearSearchTimer(): void {
|
||||
if (searchTimerRef.current) {
|
||||
clearTimeout(searchTimerRef.current);
|
||||
searchTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
function commitSearch(value: string): void {
|
||||
clearSearchTimer();
|
||||
if (!value) {
|
||||
findReplaceService.changeInputtingFindString('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (findReplaceService.replaceRevealed) {
|
||||
findReplaceService.changeInputtingFindString(value);
|
||||
findReplaceService.changeFindString(value);
|
||||
findReplaceService.find();
|
||||
return;
|
||||
}
|
||||
|
||||
findReplaceService.changeFindString(value);
|
||||
}
|
||||
|
||||
function scheduleSearch(value: string): void {
|
||||
clearSearchTimer();
|
||||
if (!value) {
|
||||
commitSearch(value);
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimerRef.current = setTimeout(() => commitSearch(value), SEARCH_DEBOUNCE_TIME);
|
||||
}
|
||||
|
||||
function blurActiveElement(): void {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function executeAfterBlur(commandId: string, suppressFocusSignal = false): void {
|
||||
blurActiveElement();
|
||||
if (findValue !== state.findString) {
|
||||
commitSearch(findValue);
|
||||
if (commandId === GoToNextMatchOperation.id || commandId === GoToPreviousMatchOperation.id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
suppressNextFocusRef.current = suppressFocusSignal;
|
||||
const execution = commandService.executeCommand(commandId);
|
||||
suppressNextFocusRef.current = false;
|
||||
execution.catch(() => undefined);
|
||||
}
|
||||
|
||||
function openSettings(): void {
|
||||
commitSearch(findValue);
|
||||
blurActiveElement();
|
||||
dialogService.open({
|
||||
id: MOBILE_FIND_REPLACE_SETTINGS_DIALOG_ID,
|
||||
title: { title: localeService.t<LocaleKey>('find-replace.mobile.settings-title') },
|
||||
children: { label: MOBILE_FIND_REPLACE_SETTINGS_COMPONENT },
|
||||
});
|
||||
}
|
||||
|
||||
const noResult = state.findCompleted && state.matchesCount === 0;
|
||||
const resultText = noResult
|
||||
? localeService.t<LocaleKey>('find-replace.dialog.no-result')
|
||||
: state.matchesCount > 0 ? `${state.matchesPosition}/${state.matchesCount}` : undefined;
|
||||
const navigationDisabled = state.matchesCount === 0;
|
||||
const replaceDisabled = state.matchesCount === 0 || !currentMatch?.replaceable;
|
||||
const replaceAllDisabled = replaceables.length === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="mobile-find-replace-bar"
|
||||
className="
|
||||
univer-fixed univer-inset-x-0 univer-z-40 univer-grid univer-gap-2 univer-bg-gray-0 univer-px-2
|
||||
univer-pt-1.5 univer-shadow-[0_-4px_16px_rgba(0,0,0,0.08)]
|
||||
dark:!univer-bg-gray-800
|
||||
"
|
||||
style={{
|
||||
bottom: keyboardInset,
|
||||
paddingBottom: 'calc(6px + env(safe-area-inset-bottom, 0px))',
|
||||
}}
|
||||
>
|
||||
<div className="univer-flex">
|
||||
<IconButton
|
||||
label={localeService.t<LocaleKey>('find-replace.mobile.settings')}
|
||||
onClick={openSettings}
|
||||
>
|
||||
<ConfigureTabIcon />
|
||||
</IconButton>
|
||||
<div className="univer-relative univer-mr-2 univer-min-w-0 univer-flex-1">
|
||||
<SearchIcon
|
||||
className="
|
||||
univer-pointer-events-none univer-absolute univer-left-3 univer-top-1/2 univer-z-10
|
||||
univer-size-5 -univer-translate-y-1/2 univer-text-gray-500
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size="middle"
|
||||
allowClear
|
||||
value={findValue}
|
||||
placeholder={localeService.t<LocaleKey>('find-replace.dialog.find-placeholder')}
|
||||
inputClass="!univer-pl-10"
|
||||
slot={resultText
|
||||
? <span className="univer-text-xs univer-text-gray-400">{resultText}</span>
|
||||
: undefined}
|
||||
onChange={(value) => {
|
||||
setFindValue(value);
|
||||
scheduleSearch(value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
executeAfterBlur(GoToNextMatchOperation.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
disabled={navigationDisabled}
|
||||
label={localeService.t<LocaleKey>('find-replace.mobile.previous-match')}
|
||||
onClick={() => executeAfterBlur(GoToPreviousMatchOperation.id, true)}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
disabled={navigationDisabled}
|
||||
label={localeService.t<LocaleKey>('find-replace.mobile.next-match')}
|
||||
onClick={() => executeAfterBlur(GoToNextMatchOperation.id, true)}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label={localeService.t<LocaleKey>('find-replace.mobile.close')}
|
||||
onClick={() => {
|
||||
blurActiveElement();
|
||||
commandService.executeCommand(CloseFindDialogOperation.id).catch(() => undefined);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
{state.replaceRevealed && (
|
||||
<div className="univer-flex univer-gap-1 univer-pl-9">
|
||||
<Input
|
||||
size="middle"
|
||||
className="univer-min-w-0 univer-flex-1"
|
||||
value={state.replaceString}
|
||||
placeholder={localeService.t<LocaleKey>('find-replace.dialog.replace-placeholder')}
|
||||
onChange={(value) => findReplaceService.changeReplaceString(value)}
|
||||
/>
|
||||
<ActionButton
|
||||
disabled={replaceDisabled}
|
||||
onClick={() => executeAfterBlur(ReplaceCurrentMatchCommand.id)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('find-replace.dialog.replace')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={replaceAllDisabled}
|
||||
onClick={() => executeAfterBlur(ReplaceAllMatchesCommand.id)}
|
||||
>
|
||||
{localeService.t<LocaleKey>('find-replace.dialog.replace-all')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface IIconButtonProps {
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function IconButton(props: IIconButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={props.label}
|
||||
disabled={props.disabled}
|
||||
className={clsx(resetButtonClassName, `
|
||||
univer-flex univer-size-9 univer-flex-none univer-items-center univer-justify-center univer-rounded-full
|
||||
univer-text-lg univer-text-gray-700
|
||||
active:univer-bg-gray-100
|
||||
disabled:univer-opacity-35
|
||||
dark:!univer-text-gray-200
|
||||
dark:active:!univer-bg-gray-700
|
||||
`)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface IActionButtonProps {
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function ActionButton(props: IActionButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={props.disabled}
|
||||
className={clsx(resetButtonClassName, `
|
||||
univer-h-10 univer-flex-none univer-whitespace-nowrap univer-rounded-lg univer-bg-primary-600 univer-px-3
|
||||
univer-text-sm univer-font-medium univer-text-gray-0
|
||||
active:univer-bg-primary-700
|
||||
disabled:univer-bg-gray-100 disabled:univer-text-gray-400
|
||||
dark:disabled:!univer-bg-gray-700 dark:disabled:!univer-text-gray-500
|
||||
`)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { Checkbox, MobileSelect } from '@univerjs/design';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { FindBy, FindDirection, FindScope, IFindReplaceService } from '../../services/find-replace.service';
|
||||
|
||||
const FIND_SCOPE_OPTIONS: Array<{ label: LocaleKey; value: FindScope }> = [
|
||||
{ label: 'find-replace.dialog.find-scope.current-sheet', value: FindScope.SUBUNIT },
|
||||
{ label: 'find-replace.dialog.find-scope.workbook', value: FindScope.UNIT },
|
||||
];
|
||||
|
||||
const FIND_DIRECTION_OPTIONS: Array<{ label: LocaleKey; value: FindDirection }> = [
|
||||
{ label: 'find-replace.dialog.find-direction.row', value: FindDirection.ROW },
|
||||
{ label: 'find-replace.dialog.find-direction.column', value: FindDirection.COLUMN },
|
||||
];
|
||||
|
||||
const FIND_BY_OPTIONS: Array<{ label: LocaleKey; value: FindBy }> = [
|
||||
{ label: 'find-replace.dialog.find-by.value', value: FindBy.VALUE },
|
||||
{ label: 'find-replace.dialog.find-by.formula', value: FindBy.FORMULA },
|
||||
];
|
||||
|
||||
export function MobileFindReplaceSettings() {
|
||||
const findReplaceService = useDependency(IFindReplaceService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const state = useObservable(findReplaceService.state$, undefined, true);
|
||||
const capabilities = useObservable(findReplaceService.providerCapabilities$, null, true);
|
||||
|
||||
function refreshSearch(force = false): void {
|
||||
if (state.findString && (force || state.replaceRevealed)) {
|
||||
findReplaceService.find();
|
||||
}
|
||||
}
|
||||
|
||||
const findScopeOptions = FIND_SCOPE_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
label: localeService.t<LocaleKey>(option.label),
|
||||
}));
|
||||
const findDirectionOptions = FIND_DIRECTION_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
label: localeService.t<LocaleKey>(option.label),
|
||||
}));
|
||||
const findByOptions = FIND_BY_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
label: localeService.t<LocaleKey>(option.label),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="univer-grid univer-gap-2" data-u-comp="mobile-find-replace-settings">
|
||||
<SettingCheckbox
|
||||
checked={state.replaceRevealed}
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.replace')}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
findReplaceService.revealReplace();
|
||||
} else {
|
||||
findReplaceService.hideReplace();
|
||||
}
|
||||
refreshSearch(true);
|
||||
}}
|
||||
/>
|
||||
{capabilities?.findDirection && (
|
||||
<SettingSelect
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.find-direction.title')}
|
||||
value={state.findDirection}
|
||||
options={findDirectionOptions}
|
||||
onChange={(value) => {
|
||||
findReplaceService.changeFindDirection(value as FindDirection);
|
||||
refreshSearch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{capabilities?.findScope && (
|
||||
<SettingSelect
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.find-scope.title')}
|
||||
value={state.findScope}
|
||||
options={findScopeOptions}
|
||||
onChange={(value) => {
|
||||
findReplaceService.changeFindScope(value as FindScope);
|
||||
refreshSearch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{capabilities?.findBy && (
|
||||
<SettingSelect
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.find-by.title')}
|
||||
value={state.findBy}
|
||||
options={findByOptions}
|
||||
onChange={(value) => {
|
||||
findReplaceService.changeFindBy(value as FindBy);
|
||||
refreshSearch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{capabilities?.caseSensitive && (
|
||||
<SettingCheckbox
|
||||
checked={state.caseSensitive}
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.case-sensitive')}
|
||||
onChange={(checked) => {
|
||||
findReplaceService.changeCaseSensitive(checked);
|
||||
refreshSearch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{capabilities?.matchesTheWholeCell && (
|
||||
<SettingCheckbox
|
||||
checked={state.matchesTheWholeCell}
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.match-the-whole-cell')}
|
||||
onChange={(checked) => {
|
||||
findReplaceService.changeMatchesTheWholeCell(checked);
|
||||
refreshSearch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{capabilities?.matchesTheWholeWord && (
|
||||
<SettingCheckbox
|
||||
checked={state.matchesTheWholeWord}
|
||||
label={localeService.t<LocaleKey>('find-replace.dialog.match-the-whole-word')}
|
||||
onChange={(checked) => {
|
||||
findReplaceService.changeMatchesTheWholeWord(checked);
|
||||
refreshSearch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ISettingSelectProps {
|
||||
label: string;
|
||||
value: string;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function SettingSelect(props: ISettingSelectProps) {
|
||||
return (
|
||||
<label
|
||||
className="
|
||||
univer-grid univer-gap-1 univer-text-sm univer-text-gray-700
|
||||
dark:!univer-text-gray-300
|
||||
"
|
||||
>
|
||||
<span>{props.label}</span>
|
||||
<MobileSelect value={props.value} options={props.options} onChange={props.onChange} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
interface ISettingCheckboxProps {
|
||||
checked: boolean;
|
||||
label: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
function SettingCheckbox(props: ISettingCheckboxProps) {
|
||||
return (
|
||||
<div className="univer-flex univer-min-h-9 univer-items-center">
|
||||
<Checkbox checked={props.checked} onChange={(checked) => props.onChange(Boolean(checked))}>
|
||||
{props.label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
packages/find-replace/tailwind.config.ts
Normal file
27
packages/find-replace/tailwind.config.ts
Normal file
@@ -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 { Config } from 'tailwindcss';
|
||||
import preset from '@univerjs-infra/shared/tailwind';
|
||||
|
||||
const config: Config = {
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
packages/find-replace/tsconfig.json
Normal file
8
packages/find-replace/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/find-replace/tsconfig.node.json
Normal file
18
packages/find-replace/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/find-replace/vitest.config.ts
Normal file
3
packages/find-replace/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user