feat(find-replace): use esc to focus on selection (#4746)

This commit is contained in:
Wenzhao Hu
2025-02-28 18:16:39 +08:00
committed by GitHub
parent c3fa72f1f9
commit f4b30ae3c0
21 changed files with 135 additions and 23 deletions
@@ -112,6 +112,12 @@ export interface IDialogProps {
* whether click mask to close, default is true
*/
maskClosable?: boolean;
/**
* whether support press esc to close
* @default true
*/
keyboard?: boolean;
}
export function Dialog(props: IDialogProps) {
@@ -130,6 +136,7 @@ export function Dialog(props: IDialogProps) {
footer,
onClose,
mask,
keyboard = true,
dialogStyles,
closable,
maskClosable,
@@ -234,6 +241,7 @@ export function Dialog(props: IDialogProps) {
styles={dialogStyles}
closable={closable}
maskClosable={maskClosable}
keyboard={keyboard}
>
{children}
</RcDialog>
@@ -70,3 +70,13 @@ export const GoToPreviousMatchOperation: IOperation = {
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;
},
};
@@ -37,6 +37,7 @@ import { takeUntil } from 'rxjs';
import { ReplaceAllMatchesCommand, ReplaceCurrentMatchCommand } from '../commands/commands/replace.command';
import {
FocusSelectionOperation,
GoToNextMatchOperation,
GoToPreviousMatchOperation,
OpenFindDialogOperation,
@@ -45,6 +46,7 @@ import {
import { IFindReplaceService } from '../services/find-replace.service';
import { FindReplaceDialog } from '../views/dialog/FindReplaceDialog';
import {
FocusSelectionShortcutItem,
GoToNextFindMatchShortcutItem,
GoToPreviousFindMatchShortcutItem,
MacOpenFindDialogShortcutItem,
@@ -94,6 +96,7 @@ export class FindReplaceController extends RxDisposable {
GoToPreviousMatchOperation,
ReplaceAllMatchesCommand,
ReplaceCurrentMatchCommand,
FocusSelectionOperation,
].forEach((c) => {
this.disposeWithMe(this._commandService.registerCommand(c));
});
@@ -106,6 +109,7 @@ export class FindReplaceController extends RxDisposable {
MacOpenFindDialogShortcutItem,
GoToPreviousFindMatchShortcutItem,
GoToNextFindMatchShortcutItem,
FocusSelectionShortcutItem,
].forEach((s) => this.disposeWithMe(this._shortcutService.registerShortcut(s)));
}
@@ -14,10 +14,13 @@
* limitations under the License.
*/
import { type IShortcutItem, KeyCode, MetaKeys } from '@univerjs/ui';
import type { IContextService } from '@univerjs/core';
import type { IShortcutItem } from '@univerjs/ui';
import { EDITOR_ACTIVATED, FOCUSING_SHEET, type IContextService } from '@univerjs/core';
import { EDITOR_ACTIVATED, FOCUSING_SHEET } from '@univerjs/core';
import { KeyCode, MetaKeys } from '@univerjs/ui';
import {
FocusSelectionOperation,
GoToNextMatchOperation,
GoToPreviousMatchOperation,
OpenFindDialogOperation,
@@ -100,3 +103,14 @@ export const GoToPreviousFindMatchShortcutItem: IShortcutItem = {
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,
priority: 1000,
preconditions(contextService) {
return whenFindReplaceDialogFocused(contextService);
},
};
@@ -25,6 +25,7 @@ const locale: typeof zhCN = {
'close-dialog': 'Close Find & Replace Dialog',
'go-to-next-match': 'Go to Next Match',
'go-to-previous-match': 'Go to Previous Match',
'focus-selection': 'Focus Selection',
},
dialog: {
title: 'Find',
@@ -25,6 +25,7 @@ const locale: typeof zhCN = {
'close-dialog': 'بستن پنجره گفتگوی یافتن و جایگزینی',
'go-to-next-match': 'رفتن به تطابق بعدی',
'go-to-previous-match': 'رفتن به تطابق قبلی',
'focus-selection': 'تمرکز بر روی انتخاب',
},
dialog: {
title: 'یافتن',
@@ -25,6 +25,7 @@ const locale: typeof enUS = {
'close-dialog': 'Fermer la boîte de dialogue Rechercher & Remplacer',
'go-to-next-match': 'Aller à la correspondance suivante',
'go-to-previous-match': 'Aller à la correspondance précédente',
'focus-selection': 'Focus sur la sélection',
},
dialog: {
title: 'Rechercher',
@@ -25,6 +25,7 @@ const locale: typeof zhCN = {
'close-dialog': 'Закрыть диалог поиска и замены',
'go-to-next-match': 'Перейти к следующему совпадению',
'go-to-previous-match': 'Перейти к предыдущему совпадению',
'focus-selection': 'Фокус на выделении',
},
dialog: {
title: 'Найти',
@@ -25,6 +25,7 @@ const locale: typeof zhCN = {
'close-dialog': 'Đóng hộp thoại tìm kiếm và thay thế',
'go-to-next-match': 'Đến mục khớp tiếp theo',
'go-to-previous-match': 'Đến mục khớp trước đó',
'focus-selection': 'Tập trung vào lựa chọn',
},
dialog: {
title: 'Tìm kiếm',
@@ -23,6 +23,7 @@ const locale = {
'close-dialog': '关闭查找替换对话框',
'go-to-next-match': '下一个匹配项',
'go-to-previous-match': '下一个匹配项',
'focus-selection': '聚焦选区',
},
dialog: {
title: '查找',
@@ -25,6 +25,7 @@ const locale: typeof zhCN = {
'close-dialog': '關閉查找替換對話框',
'go-to-next-match': '下一個匹配項',
'go-to-previous-match': '下一個匹配項',
'focus-selection': '聚焦選區',
},
dialog: {
title: '找',
@@ -16,7 +16,7 @@
import type { IDisposable, Nullable } from '@univerjs/core';
import type { Observable } from 'rxjs';
import { createIdentifier, Disposable, DisposableCollection, IContextService, Inject, Injector, IUniverInstanceService, toDisposable } from '@univerjs/core';
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_REPLACE_REVEALED } from './context-keys';
@@ -80,6 +80,8 @@ export abstract class FindModel extends Disposable {
* Replace all matches. This method would return how many
*/
abstract replaceAll(replaceString: string): Promise<IReplaceAllResult>;
abstract focusSelection(): void;
}
/**
@@ -154,6 +156,11 @@ export interface IFindReplaceService {
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');
@@ -212,7 +219,8 @@ export class FindReplaceModel extends Disposable {
constructor(
private readonly _state: FindReplaceState,
private readonly _providers: Set<IFindReplaceProvider>,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
@@ -254,6 +262,10 @@ export class FindReplaceModel extends Disposable {
return complete;
}
focusSelection(): void {
this._matchingModel?.focusSelection();
}
/** Call this method to start a `searching`. */
private async _startSearching(): Promise<IFindComplete> {
if (!this._state.findString) {
@@ -502,9 +514,6 @@ export interface IFindReplaceState {
findBy: FindBy;
}
/**
*
*/
export function createInitFindReplaceState(): IFindReplaceState {
return {
caseSensitive: false,
@@ -813,6 +822,10 @@ export class FindReplaceService extends Disposable implements IFindReplaceServic
this._toggleRevealReplace(true);
}
focusSelection(): void {
this._model?.focusSelection();
}
start(revealReplace = false): boolean {
if (this._providers.size === 0) {
return false;
@@ -305,7 +305,6 @@ function useFindScopeOptions(localeService: LocaleService): Array<{ label: strin
{ label: localeService.t('find-replace.dialog.find-scope.current-sheet'), value: FindScope.SUBUNIT },
{ label: localeService.t('find-replace.dialog.find-scope.workbook'), value: FindScope.UNIT },
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [locale]);
return options;
@@ -318,7 +317,6 @@ function useFindDirectionOptions(localeService: LocaleService): Array<{ label: s
{ label: localeService.t('find-replace.dialog.find-direction.row'), value: FindDirection.ROW },
{ label: localeService.t('find-replace.dialog.find-direction.column'), value: FindDirection.COLUMN },
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [locale]);
return options;
@@ -331,7 +329,6 @@ function useFindByOptions(localeService: LocaleService): Array<{ label: string;
{ label: localeService.t('find-replace.dialog.find-by.value'), value: FindBy.VALUE },
{ label: localeService.t('find-replace.dialog.find-by.formula'), value: FindBy.FORMULA },
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [locale]);
return options;
@@ -16,9 +16,8 @@
import type { LocaleService } from '@univerjs/core';
import type { IInputWithSlotProps } from '@univerjs/design';
import { InputWithSlot, Pager } from '@univerjs/design';
import React from 'react';
import type { IFindReplaceService } from '../../services/find-replace.service';
import { InputWithSlot, Pager } from '@univerjs/design';
export interface ISearchInputProps extends Pick<IInputWithSlotProps, 'onFocus' | 'onBlur' | 'className' | 'onChange'> {
findCompleted: boolean;
@@ -40,11 +39,11 @@ export function SearchInput(props: ISearchInputProps) {
return (
<InputWithSlot
autoFocus={true}
autoFocus
placeholder={localeService.t('find-replace.dialog.find-placeholder')}
slot={(
<Pager
loop={true}
loop
text={text}
value={matchesPosition}
total={matchesCount}
@@ -31,7 +31,9 @@ export class SheetsDrawingLoadController extends Disposable {
@IResourceManagerService private _resourceManagerService: IResourceManagerService
) {
super();
this._initSnapshot();
this.disposeWithMe(this._commandService.registerCommand(SetDrawingApplyMutation));
}
@@ -41,18 +43,22 @@ export class SheetsDrawingLoadController extends Disposable {
if (map) {
return JSON.stringify(map);
}
return '';
};
const parseJson = (json: string): IDrawingSubunitMap<ISheetDrawing> => {
if (!json) {
return {};
}
try {
return JSON.parse(json);
} catch (err) {
} catch {
return {};
}
};
this.disposeWithMe(
this._resourceManagerService.registerPluginResource<IDrawingSubunitMap<ISheetDrawing>>({
pluginName: SHEET_DRAWING_PLUGIN,
@@ -56,6 +56,7 @@ export class SheetsFilterController extends Disposable {
this.disposeWithMe(this._sheetInterceptorService.interceptCommand({
getMutations: (command) => this._getUpdateFilter(command),
}));
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id === SetWorksheetActiveOperation.id) {
const params = commandInfo.params as ISetWorksheetActiveOperationParams;
@@ -150,9 +151,11 @@ export class SheetsFilterController extends Disposable {
if (!unitId || !subUnitId || !targetSubUnitId) {
return this._handleNull();
}
return this._handleCopySheetCommand(unitId, subUnitId, targetSubUnitId);
}
}
return {
redos: [],
undos: [],
@@ -16,14 +16,14 @@
import type { ICellData, IDisposable, IObjectMatrixPrimitiveType, IRange, Nullable, Workbook, Worksheet } from '@univerjs/core';
import type { IFindComplete, IFindMatch, IFindMoveParams, IFindQuery, IFindReplaceProvider, IReplaceAllResult } from '@univerjs/find-replace';
import type { ISelectionWithStyle, ISetRangeValuesCommandParams, ISetSelectionsOperationParams, ISetWorksheetActivateCommandParams, ISheetCommandSharedParams, WorkbookSelectionModel } from '@univerjs/sheets';
import type { ISelectionWithStyle, ISelectRangeCommandParams, ISetRangeValuesCommandParams, ISetSelectionsOperationParams, ISetWorksheetActivateCommandParams, ISheetCommandSharedParams, WorkbookSelectionModel } from '@univerjs/sheets';
import type { IScrollToCellCommandParams } from '@univerjs/sheets-ui';
import type { ISheetReplaceCommandParams, ISheetReplacement } from '../commands/commands/sheet-replace.command';
import type { ISheetFindReplaceHighlightShapeProps } from '../views/shapes/find-replace-highlight.shape';
import { ColorKit, CommandType, Disposable, EDITOR_ACTIVATED, fromCallback, groupBy, ICommandService, IContextService, Inject, Injector, IUniverInstanceService, ObjectMatrix, replaceInDocumentBody, rotate, ThemeService, Tools, UniverInstanceType } from '@univerjs/core';
import { IRenderManagerService, RENDER_RAW_FORMULA_KEY } from '@univerjs/engine-render';
import { FindBy, FindDirection, FindModel, FindReplaceController, FindScope, IFindReplaceService } from '@univerjs/find-replace';
import { SetRangeValuesCommand, SetSelectionsOperation, SetWorksheetActivateCommand, SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
import { SelectRangeCommand, SetRangeValuesCommand, SetSelectionsOperation, SetWorksheetActivateCommand, SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
import { getCoordByCell, getSheetObject, ScrollToCellCommand, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { debounceTime, filter, merge, skip, Subject, throttleTime } from 'rxjs';
@@ -164,6 +164,18 @@ export class SheetFindModel extends FindModel {
}
}
override focusSelection(): void {
const currentMatch = this.currentMatch;
if (!currentMatch) return;
this._commandService.executeCommand(SelectRangeCommand.id, {
unitId: currentMatch.unitId,
subUnit: currentMatch.range.subUnitId,
range: currentMatch.range.range,
} as ISelectRangeCommandParams);
}
private _toggleDisplayRawFormula(force: boolean): void {
this._contextService.setContextValue(RENDER_RAW_FORMULA_KEY, force);
}
@@ -14,10 +14,12 @@
* limitations under the License.
*/
import type { IOperation } from '@univerjs/core';
import type { IAccessor, ICommand, IOperation, IRange } from '@univerjs/core';
import type { ISelectionWithStyle } from '../../basics/selection';
import type { SelectionMoveType } from '../../services/selections/type';
import { CommandType } from '@univerjs/core';
import { CommandType, ICommandService, IUniverInstanceService } from '@univerjs/core';
import { getPrimaryForRange } from '../commands/utils/selection-utils';
import { getSheetCommandTarget } from '../commands/utils/target-util';
import { getSelectionsService } from '../utils/selection-command-util';
export interface ISetSelectionsOperationParams {
@@ -39,6 +41,7 @@ export const SetSelectionsOperation: IOperation<ISetSelectionsOperationParams> =
type: CommandType.OPERATION,
handler: (accessor, params) => {
if (!params) return false;
const { selections, type, unitId, subUnitId } = params;
const selectionManagerService = getSelectionsService(accessor);
@@ -48,3 +51,38 @@ export const SetSelectionsOperation: IOperation<ISetSelectionsOperationParams> =
return true;
},
};
export interface ISelectRangeCommandParams {
unitId: string;
subUnit: string;
range: IRange;
/** If should scroll to the selected range. */
reveal?: boolean;
extra?: string;
}
export const SelectRangeCommand: ICommand<ISelectRangeCommandParams> = {
id: 'sheet.command.select-range',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params: ISelectRangeCommandParams) => {
if (!params) return false;
const { unitId, subUnit, range } = params;
const commandService = accessor.get(ICommandService);
const target = getSheetCommandTarget(accessor.get(IUniverInstanceService), params);
if (!target) return false;
const selections: ISelectionWithStyle[] = [{
range,
primary: getPrimaryForRange(range, target.worksheet),
style: null,
}];
return commandService.syncExecuteCommand(SetSelectionsOperation.id, {
unitId,
subUnitId: subUnit,
selections,
} as ISetSelectionsOperationParams);
},
};
@@ -153,7 +153,7 @@ import {
import { ToggleGridlinesMutation } from '../commands/mutations/toggle-gridlines.mutation';
import { UnregisterWorksheetRangeThemeStyleMutation } from '../commands/mutations/unregister-range-theme-style.mutation';
import { ScrollToCellOperation } from '../commands/operations/scroll-to-cell.operation';
import { SetSelectionsOperation } from '../commands/operations/selection.operation';
import { SelectRangeCommand, SetSelectionsOperation } from '../commands/operations/selection.operation';
import { SetWorksheetActiveOperation } from '../commands/operations/set-worksheet-active.operation';
import { ONLY_REGISTER_FORMULA_RELATED_MUTATIONS_KEY } from './config';
import { MAX_CELL_PER_SHEET_DEFAULT, MAX_CELL_PER_SHEET_KEY } from './config/config';
@@ -286,6 +286,7 @@ export class BasicWorksheetController extends Disposable implements IDisposable
SetWorksheetColWidthMutation,
// SetWorksheetColIsAutoWidthCommand,
SelectRangeCommand,
SetSelectionsOperation,
ScrollToCellOperation,
InsertDefinedNameCommand,
+1 -1
View File
@@ -442,7 +442,7 @@ export {
export { type IScrollToCellOperationParams, ScrollToCellOperation } from './commands/operations/scroll-to-cell.operation';
export { type ISetSelectionsOperationParams, SetSelectionsOperation } from './commands/operations/selection.operation';
export { type ISelectRangeCommandParams, type ISetSelectionsOperationParams, SelectRangeCommand, SetSelectionsOperation } from './commands/operations/selection.operation';
export { type ISetWorksheetActiveOperationParams, SetWorksheetActiveOperation } from './commands/operations/set-worksheet-active.operation';
export { type IToggleCellCheckboxCommandParams, ToggleCellCheckboxCommand } from './commands/commands/toggle-checkbox.command';
export { SCOPE_WORKBOOK_VALUE_DEFINED_NAME } from './controllers/defined-name-data.controller';
@@ -15,11 +15,11 @@
*/
import type { IDisposable } from '@univerjs/core';
import { createIdentifier } from '@univerjs/core';
import type { Observable } from 'rxjs';
import type { IDialogPartMethodOptions } from '../../views/components/dialog-part/interface';
import { createIdentifier } from '@univerjs/core';
export const IDialogService = createIdentifier<IDialogService>('univer.ui.dialog-service');
export interface IDialogService {
open(params: IDialogPartMethodOptions): IDisposable;