diff --git a/packages/docs-quick-insert-ui/src/controllers/doc-quick-insert-trigger.controller.ts b/packages/docs-quick-insert-ui/src/controllers/doc-quick-insert-trigger.controller.ts index 40b25202c9..bfd999bcae 100644 --- a/packages/docs-quick-insert-ui/src/controllers/doc-quick-insert-trigger.controller.ts +++ b/packages/docs-quick-insert-ui/src/controllers/doc-quick-insert-trigger.controller.ts @@ -15,10 +15,10 @@ */ import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import type { IDeleteCommandParams, IInsertCommandParams, IMoveCursorOperationParams } from '@univerjs/docs-ui'; +import type { IDeleteCommandParams, IIMEInputCommandParams, IInsertCommandParams, IMoveCursorOperationParams } from '@univerjs/docs-ui'; import { DeleteDirection, Direction, Disposable, ICommandService, Inject } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; -import { DeleteCommand, DeleteLeftCommand, InsertCommand, MoveCursorOperation } from '@univerjs/docs-ui'; +import { DeleteCommand, DeleteLeftCommand, IMEInputCommand, InsertCommand, MoveCursorOperation } from '@univerjs/docs-ui'; import { IShortcutService, KeyCode } from '@univerjs/ui'; import { CloseQuickInsertPopupOperation, ShowQuickInsertPopupOperation } from '../commands/operations/quick-insert-popup.operation'; import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service'; @@ -44,9 +44,10 @@ export class DocQuickInsertTriggerController extends Disposable { this._initMenuHandler(); } + // eslint-disable-next-line max-lines-per-function private _initTrigger() { this.disposeWithMe( - // eslint-disable-next-line complexity + // eslint-disable-next-line complexity, max-lines-per-function this._commandService.onCommandExecuted((commandInfo) => { const { _docQuickInsertPopupService, _textSelectionManagerService, _commandService } = this; if (commandInfo.id === InsertCommand.id) { @@ -85,6 +86,17 @@ export class DocQuickInsertTriggerController extends Disposable { }, 100); } + if (commandInfo.id === IMEInputCommand.id) { + const params = commandInfo.params as IIMEInputCommandParams; + if (!_docQuickInsertPopupService.isComposing && params.isCompositionStart) { + _docQuickInsertPopupService.setIsComposing(true); + } + + if (_docQuickInsertPopupService.isComposing && params.isCompositionEnd) { + _docQuickInsertPopupService.setIsComposing(false); + } + } + if (commandInfo.id === RichTextEditingMutation.id) { const params = commandInfo.params as IRichTextEditingMutationParams; if (params.isCompositionEnd) { diff --git a/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts b/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts index 95e483201b..d24dca6208 100644 --- a/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts +++ b/packages/docs-quick-insert-ui/src/services/doc-quick-insert-popup.service.ts @@ -18,7 +18,9 @@ import type { DocumentDataModel, IDisposable, Nullable } from '@univerjs/core'; import type { IInsertCommandParams } from '@univerjs/docs-ui'; import type { Observable } from 'rxjs'; import { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; +import { DocSelectionManagerService, DocSkeletonManagerService } from '@univerjs/docs'; import { DocCanvasPopManagerService } from '@univerjs/docs-ui'; +import { IRenderManagerService } from '@univerjs/engine-render'; import { BehaviorSubject, combineLatest, distinctUntilChanged, map, tap } from 'rxjs'; import { DeleteSearchKeyCommand } from '../commands/commands/doc-quick-insert.command'; import { KeywordInputPlaceholder } from '../views/KeywordInputPlaceholder'; @@ -47,6 +49,9 @@ export interface IDocPopup { preconditions?: (params: IInsertCommandParams) => boolean; } +const noopDisposable = { + dispose: () => {}, +}; export class DocQuickInsertPopupService extends Disposable { private readonly _popups: Set = new Set(); @@ -62,6 +67,16 @@ export class DocQuickInsertPopupService extends Disposable { return this._editPopup$.value; } + private readonly _isComposing$ = new BehaviorSubject(false); + readonly isComposing$ = this._isComposing$.asObservable(); + get isComposing() { + return this._isComposing$.value; + } + + setIsComposing(isComposing: boolean) { + this._isComposing$.next(isComposing); + } + private readonly _inputOffset$ = new BehaviorSubject<{ start: number; end: number }>({ start: 0, end: 0 }); readonly inputOffset$ = this._inputOffset$.asObservable(); get inputOffset() { @@ -84,7 +99,9 @@ export class DocQuickInsertPopupService extends Disposable { constructor( @Inject(DocCanvasPopManagerService) private readonly _docCanvasPopupManagerService: DocCanvasPopManagerService, @Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService, - @Inject(ICommandService) private readonly _commandService: ICommandService + @Inject(ICommandService) private readonly _commandService: ICommandService, + @Inject(IRenderManagerService) private readonly _renderManagerService: IRenderManagerService, + @Inject(DocSelectionManagerService) private readonly _docSelectionManagerService: DocSelectionManagerService ) { super(); @@ -92,13 +109,17 @@ export class DocQuickInsertPopupService extends Disposable { const getBodySlice = (start: number, end: number) => this._univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)?.getBody()?.dataStream.slice(start, end); + let lastFilterKeyword = ''; this.filterKeyword$ = this._inputOffset$.pipe( map((offset) => { const slice = getBodySlice(offset.start, offset.end); return slice?.slice(1) ?? ''; }), - distinctUntilChanged() + distinctUntilChanged(), + tap((filterKeyword) => { + lastFilterKeyword = filterKeyword; + }) ); this.disposeWithMe(combineLatest([ @@ -109,9 +130,18 @@ export class DocQuickInsertPopupService extends Disposable { this._inputPlaceholderRenderRoot?.mount(); } })), + this.isComposing$.pipe(tap((isComposing) => { + if (isComposing) { + this._inputPlaceholderRenderRoot?.unmount?.dispose(); + } else { + // If the last filter keyword is empty, after the composition end, mount the input placeholder render root + lastFilterKeyword.length <= 0 && this._inputPlaceholderRenderRoot?.mount(); + } + })), this.editPopup$.pipe(tap((popup) => { if (!popup) { this._inputPlaceholderRenderRoot?.unmount?.dispose(); + this._inputPlaceholderRenderRoot = null; } })), ]).subscribe()); @@ -131,11 +161,27 @@ export class DocQuickInsertPopupService extends Disposable { private _createInputPlaceholderRenderRoot(mount: () => IDisposable) { const renderRoot: { + isMounted: boolean; unmount?: IDisposable; mount: () => void; } = { + isMounted: false, mount() { - this.unmount = mount(); + // Prevent duplicate mounting + if (this.isMounted) { + return; + } + + this.isMounted = true; + + const unmount = mount(); + + this.unmount = { + dispose: () => { + unmount.dispose(); + this.isMounted = false; + }, + }; }, }; @@ -160,6 +206,20 @@ export class DocQuickInsertPopupService extends Disposable { ); this._inputPlaceholderRenderRoot = this._createInputPlaceholderRenderRoot(() => { + const docSkeletonManagerService = this._renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService); + const activeRange = this._docSelectionManagerService.getActiveTextRange(); + if (!docSkeletonManagerService || !activeRange) { + return noopDisposable; + } + + const skeleton = docSkeletonManagerService.getSkeleton(); + const curGlyph = skeleton.findNodeByCharIndex(activeRange.startOffset, activeRange.segmentId, activeRange.segmentPage); + const isEmptyLine = curGlyph?.content === '\r'; + // Only show filter keyword placeholder on empty line + if (!isEmptyLine) { + return noopDisposable; + } + const disposable = this._docCanvasPopupManagerService.attachPopupToRange( { startOffset: index + 1, endOffset: index + 1, collapsed: false }, { diff --git a/packages/docs-ui/src/index.ts b/packages/docs-ui/src/index.ts index 72d2fccf7b..db19a2189e 100644 --- a/packages/docs-ui/src/index.ts +++ b/packages/docs-ui/src/index.ts @@ -80,7 +80,7 @@ export { export { DeleteCustomBlockCommand, DeleteLeftCommand, DeleteRightCommand, type IDeleteCustomBlockParams, MergeTwoParagraphCommand } from './commands/commands/doc-delete.command'; export { getCursorWhenDelete } from './commands/commands/doc-delete.command'; -export { IMEInputCommand } from './commands/commands/ime-input.command'; +export { type IIMEInputCommandParams, IMEInputCommand } from './commands/commands/ime-input.command'; export { ResetInlineFormatTextBackgroundColorCommand, SetInlineFormatBoldCommand,