diff --git a/packages-experimental/debugger/src/commands/operations/sidebar.operation.ts b/packages-experimental/debugger/src/commands/operations/sidebar.operation.ts index 57ce3d6e612..a64529ca454 100644 --- a/packages-experimental/debugger/src/commands/operations/sidebar.operation.ts +++ b/packages-experimental/debugger/src/commands/operations/sidebar.operation.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import type { IAccessor, ICommand, Workbook } from '@univerjs/core'; import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { IEditorService } from '@univerjs/docs-ui'; import { ISidebarService } from '@univerjs/ui'; -import type { IAccessor, ICommand, Workbook } from '@univerjs/core'; import { TEST_EDITOR_CONTAINER_COMPONENT } from '../../views/test-editor/component-name'; export interface IUIComponentCommandParams { @@ -34,25 +34,17 @@ export const SidebarOperation: ICommand = { const unit = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; switch (params.value) { case 'open': - editorService.setOperationSheetUnitId(unit.getUnitId()); - editorService.setOperationSheetSubUnitId(unit.getActiveSheet()?.getSheetId()); sidebarService.open({ header: { title: 'Sidebar title' }, children: { label: TEST_EDITOR_CONTAINER_COMPONENT }, footer: { title: 'Sidebar Footer' }, onClose: () => { - editorService.setOperationSheetUnitId(null); - editorService.setOperationSheetSubUnitId(null); - editorService.closeRangePrompt(); }, }); break; case 'close': default: - editorService.setOperationSheetUnitId(null); - editorService.setOperationSheetSubUnitId(null); - editorService.closeRangePrompt(); sidebarService.close(); break; } diff --git a/packages-experimental/debugger/src/controllers/debugger.controller.ts b/packages-experimental/debugger/src/controllers/debugger.controller.ts index 646c6de3477..0e3ecd05040 100644 --- a/packages-experimental/debugger/src/controllers/debugger.controller.ts +++ b/packages-experimental/debugger/src/controllers/debugger.controller.ts @@ -35,8 +35,8 @@ import { ImageDemo } from '../components/Image'; // @ts-ignore import VueI18nIcon from '../components/VueI18nIcon.vue'; -import { TEST_EDITOR_CONTAINER_COMPONENT } from '../views/test-editor/component-name'; -import { TestEditorContainer } from '../views/test-editor/TestTextEditor'; +// import { TEST_EDITOR_CONTAINER_COMPONENT } from '../views/test-editor/component-name'; +// import { TestEditorContainer } from '../views/test-editor/TestTextEditor'; import { RecordController } from './local-save/record.controller'; import { menuSchema } from './menu.schema'; @@ -81,7 +81,7 @@ export class DebuggerController extends Disposable { private _initCustomComponents(): void { const componentManager = this._componentManager; - this.disposeWithMe(componentManager.register(TEST_EDITOR_CONTAINER_COMPONENT, TestEditorContainer)); + // this.disposeWithMe(componentManager.register(TEST_EDITOR_CONTAINER_COMPONENT, TestEditorContainer)); this.disposeWithMe(componentManager.register('VueI18nIcon', VueI18nIcon, { framework: 'vue3', })); diff --git a/packages-experimental/debugger/src/views/test-editor/TestTextEditor.tsx b/packages-experimental/debugger/src/views/test-editor/TestTextEditor.tsx deleted file mode 100644 index e37b2fd1cf3..00000000000 --- a/packages-experimental/debugger/src/views/test-editor/TestTextEditor.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Workbook } from '@univerjs/core'; - -import { createInternalEditorID, IUniverInstanceService, UniverInstanceType, useDependency } from '@univerjs/core'; -import { Input } from '@univerjs/design'; -import { DocRangeSelector, TextEditor } from '@univerjs/docs-ui'; -import React, { useState } from 'react'; - -const editorStyle: React.CSSProperties = { - width: '100%', -}; - -/** - * Floating editor's container. - */ -export const TestEditorContainer = () => { - const univerInstanceService = useDependency(IUniverInstanceService); - const workbook = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; - if (workbook == null) { - return; - } - - const unitId = workbook.getUnitId(); - - const sheetId = workbook.getActiveSheet()?.getSheetId(); - - const [readonly, setReadonly] = useState(false); - - return ( -
- -
- -
- -
- -
- -
- -
- -
- -
- ); -}; diff --git a/packages-experimental/uni-formula-ui/src/views/components/DocFormulaPopup.tsx b/packages-experimental/uni-formula-ui/src/views/components/DocFormulaPopup.tsx index 1b0b3b7e1b4..67e77dc14ac 100644 --- a/packages-experimental/uni-formula-ui/src/views/components/DocFormulaPopup.tsx +++ b/packages-experimental/uni-formula-ui/src/views/components/DocFormulaPopup.tsx @@ -17,7 +17,7 @@ import type { IDocumentData, Nullable } from '@univerjs/core'; import type { IUniFormulaPopupInfo } from '../../services/formula-popup.service'; import { BooleanNumber, createInternalEditorID, DEFAULT_EMPTY_DOCUMENT_VALUE, DocumentFlavor, HorizontalAlign, ICommandService, LocaleService, useDependency, VerticalAlign, WrapStrategy } from '@univerjs/core'; -import { TextEditor } from '@univerjs/docs-ui'; +// import { TextEditor } from '@univerjs/docs-ui'; import { CheckMarkSingle, CloseSingle } from '@univerjs/icons'; import { useObservable } from '@univerjs/ui'; import clsx from 'clsx'; @@ -123,7 +123,7 @@ function DocFormula(props: { popupInfo: IUniFormulaPopupInfo }) { return (
onHovered(true)} onMouseLeave={() => onHovered(false)}> - setFocused(false)} - /> + /> */}
- + /> */}
= new Map(); footerModelMap: Map = new Map(); + change$ = new BehaviorSubject(0); constructor(snapshot: Partial) { super(Tools.isEmptyObject(snapshot) ? getEmptySnapshot() : snapshot); @@ -281,6 +282,7 @@ export class DocumentDataModel extends DocumentDataModelSimple { this.snapshot = { ...DEFAULT_DOC, ...snapshot }; this._initializeHeaderFooterModel(); + this.change$.next(this.change$.value + 1); } getSelfOrHeaderFooterModel(segmentId?: string) { @@ -315,6 +317,7 @@ export class DocumentDataModel extends DocumentDataModelSimple { this._initializeHeaderFooterModel(); } + this.change$.next(this.change$.value + 1); return this.snapshot; } diff --git a/packages/core/src/docs/data-model/text-x/build-utils/index.ts b/packages/core/src/docs/data-model/text-x/build-utils/index.ts index 9ff2f59075a..62b78b92eef 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/index.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/index.ts @@ -20,7 +20,7 @@ import { addDrawing } from './drawings'; import { changeParagraphBulletNestLevel, setParagraphBullet, switchParagraphBullet, toggleChecklistParagraph } from './paragraph'; import { fromPlainText, getPlainText, isEmptyDocument } from './parse'; import { isSegmentIntersects, makeSelection, normalizeSelection } from './selection'; -import { addCustomRangeTextX, deleteCustomRangeTextX, deleteSelectionTextX, replaceSelectionTextX } from './text-x-utils'; +import { addCustomRangeTextX, deleteCustomRangeTextX, deleteSelectionTextX, replaceSelectionTextRuns, replaceSelectionTextX } from './text-x-utils'; export class BuildTextUtils { static customRange = { @@ -41,6 +41,7 @@ export class BuildTextUtils { makeSelection, normalizeSelection, delete: deleteSelectionTextX, + replaceTextRuns: replaceSelectionTextRuns, }; static range = { diff --git a/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts b/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts index f28ce073289..b21584bde49 100644 --- a/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts +++ b/packages/core/src/docs/data-model/text-x/build-utils/text-x-utils.ts @@ -16,7 +16,7 @@ import type { IAccessor } from '@wendellhu/redi'; import type { ITextRange, ITextRangeParam } from '../../../../sheets/typedef'; -import type { CustomRangeType, IDocumentBody } from '../../../../types/interfaces'; +import type { CustomRangeType, IDocumentBody, ITextRun } from '../../../../types/interfaces'; import type { DocumentDataModel } from '../../document-data-model'; import type { TextXAction } from '../action-types'; import type { TextXSelection } from '../text-x'; @@ -24,7 +24,7 @@ import { type Nullable, UpdateDocsAttributeType } from '../../../../shared'; import { textDiff } from '../../../../shared/text-diff'; import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; -import { getBodySlice } from '../utils'; +import { getBodySlice, getTextRunSlice } from '../utils'; import { excludePointsFromRange, getIntersectingCustomRanges, getSelectionForAddCustomRange } from './custom-range'; export interface IDeleteCustomRangeParam { @@ -301,3 +301,73 @@ export const replaceSelectionTextX = (params: IReplaceSelectionTextXParams) => { textX.push(...actions); return textX; }; + +function isTextRunsEqual(textRuns: ITextRun[] | undefined, oldTextRuns: ITextRun[] | undefined) { + if (textRuns?.length === oldTextRuns?.length && textRuns?.every((textRun, index) => JSON.stringify(textRun) === JSON.stringify(oldTextRuns?.[index]))) { + return true; + } + + return false; +} + +export const replaceSelectionTextRuns = (params: IReplaceSelectionTextXParams) => { + const { selection, body: insertBody, doc } = params; + const segmentId = selection.segmentId; + const body = doc.getSelfOrHeaderFooterModel(segmentId)?.getBody(); + if (!body) return false; + + const oldBody = selection.collapsed ? null : getBodySlice(body, selection.startOffset, selection.endOffset); + const diffs = textDiff(oldBody ? oldBody.dataStream : '', insertBody.dataStream); + let cursor = 0; + const actions = diffs.map(([type, text]) => { + switch (type) { + // retain + case 0: { + const textRunsSlice = getTextRunSlice(insertBody, cursor, cursor + text.length, false); + const oldTextRunsSlice = getTextRunSlice(oldBody!, cursor, cursor + text.length, false); + const action: TextXAction = { + t: TextXActionType.RETAIN, + body: isTextRunsEqual(textRunsSlice, oldTextRunsSlice) + ? undefined + : { + textRuns: textRunsSlice, + dataStream: '', + }, + len: text.length, + }; + cursor += text.length; + return action; + } + // insert + case 1: { + const action: TextXAction = { + t: TextXActionType.INSERT, + body: getBodySlice(insertBody, cursor, cursor + text.length), + len: text.length, + }; + cursor += text.length; + return action; + } + // delete + default: { + const action: TextXAction = { + t: TextXActionType.DELETE, + len: text.length, + }; + return action; + } + } + }); + + if (actions.every((action) => action.t === TextXActionType.RETAIN && !action.body)) { + return false; + } + + const textX = new TextX(); + textX.push({ + t: TextXActionType.RETAIN, + len: selection.startOffset, + }); + textX.push(...actions); + return textX; +}; diff --git a/packages/core/src/docs/data-model/text-x/utils.ts b/packages/core/src/docs/data-model/text-x/utils.ts index 9b083e5a591..32022eb0e4d 100644 --- a/packages/core/src/docs/data-model/text-x/utils.ts +++ b/packages/core/src/docs/data-model/text-x/utils.ts @@ -26,19 +26,13 @@ export enum SliceBodyType { cut, } -// eslint-disable-next-line max-lines-per-function, complexity -export function getBodySlice( +export function getTextRunSlice( body: IDocumentBody, startOffset: number, endOffset: number, - returnEmptyArray = true, - type = SliceBodyType.cut -): IDocumentBody { - const { dataStream, textRuns, paragraphs = [], customBlocks = [], tables = [], sectionBreaks = [] } = body; - - const docBody: IDocumentBody = { - dataStream: dataStream.slice(startOffset, endOffset), - }; + returnEmptyTextRuns = true +) { + const { textRuns } = body; if (textRuns) { const newTextRuns: ITextRun[] = []; @@ -66,7 +60,7 @@ export function getBodySlice( } } - docBody.textRuns = normalizeTextRuns( + return normalizeTextRuns( newTextRuns.map((tr) => { const { st, ed } = tr; return { @@ -76,18 +70,24 @@ export function getBodySlice( }; }) ); - } else if (returnEmptyArray) { + } else if (returnEmptyTextRuns) { // In the case of no style before, add the style, removeTextRuns will be empty, // in this case, you need to add an empty textRun for undo. - docBody.textRuns = [{ + return [{ st: 0, ed: endOffset - startOffset, ts: {}, }]; } +} +export function getTableSlice( + body: IDocumentBody, + startOffset: number, + endOffset: number +) { + const { tables = [] } = body; const newTables = []; - for (const table of tables) { const clonedTable = Tools.deepClone(table); const { startIndex, endIndex } = clonedTable; @@ -100,11 +100,15 @@ export function getBodySlice( }); } } + return newTables; +} - if (newTables.length) { - docBody.tables = newTables; - } - +export function getParagraphsSlice( + body: IDocumentBody, + startOffset: number, + endOffset: number +) { + const { paragraphs = [] } = body; const newParagraphs: IParagraph[] = []; for (const paragraph of paragraphs) { @@ -115,12 +119,19 @@ export function getBodySlice( } if (newParagraphs.length) { - docBody.paragraphs = newParagraphs.map((p) => ({ + return newParagraphs.map((p) => ({ ...p, startIndex: p.startIndex - startOffset, })); } +} +export function getSectionBreakSlice( + body: IDocumentBody, + startOffset: number, + endOffset: number +) { + const { sectionBreaks = [] } = body; const newSectionBreaks: ISectionBreak[] = []; for (const sectionBreak of sectionBreaks) { @@ -131,11 +142,57 @@ export function getBodySlice( } if (newSectionBreaks.length) { - docBody.sectionBreaks = newSectionBreaks.map((sb) => ({ + return newSectionBreaks.map((sb) => ({ ...sb, startIndex: sb.startIndex - startOffset, })); } +} + +export function getCustomBlockSlice( + body: IDocumentBody, + startOffset: number, + endOffset: number +) { + const { customBlocks = [] } = body; + const newCustomBlocks: ICustomBlock[] = []; + + for (const block of customBlocks) { + const { startIndex } = block; + if (startIndex >= startOffset && startIndex <= endOffset) { + newCustomBlocks.push(Tools.deepClone(block)); + } + } + + if (newCustomBlocks.length) { + return newCustomBlocks.map((b) => ({ + ...b, + startIndex: b.startIndex - startOffset, + })); + } +} + +export function getBodySlice( + body: IDocumentBody, + startOffset: number, + endOffset: number, + returnEmptyArray = true, + type = SliceBodyType.cut +): IDocumentBody { + const { dataStream } = body; + + const docBody: IDocumentBody = { + dataStream: dataStream.slice(startOffset, endOffset), + }; + + docBody.textRuns = getTextRunSlice(body, startOffset, endOffset, returnEmptyArray); + + const newTables = getTableSlice(body, startOffset, endOffset); + if (newTables.length) { + docBody.tables = newTables; + } + + docBody.paragraphs = getParagraphsSlice(body, startOffset, endOffset); if (type === SliceBodyType.cut) { const customDecorations = getCustomDecorationSlice(body, startOffset, endOffset); @@ -152,21 +209,7 @@ export function getBodySlice( docBody.customRanges = []; } - const newCustomBlocks: ICustomBlock[] = []; - - for (const block of customBlocks) { - const { startIndex } = block; - if (startIndex >= startOffset && startIndex <= endOffset) { - newCustomBlocks.push(Tools.deepClone(block)); - } - } - - if (newCustomBlocks.length) { - docBody.customBlocks = newCustomBlocks.map((b) => ({ - ...b, - startIndex: b.startIndex - startOffset, - })); - } + docBody.customBlocks = getCustomBlockSlice(body, startOffset, endOffset); return docBody; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9d2897f2a9b..80125135d4e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -68,8 +68,19 @@ export { updateAttributeByDelete } from './docs/data-model/text-x/apply-utils/de export { updateAttributeByInsert } from './docs/data-model/text-x/apply-utils/insert-apply'; export { TextX } from './docs/data-model/text-x/text-x'; export type { TPriority } from './docs/data-model/text-x/text-x'; -export { composeBody, getBodySlice, SliceBodyType } from './docs/data-model/text-x/utils'; -export { getCustomDecorationSlice, getCustomRangeSlice, normalizeBody } from './docs/data-model/text-x/utils'; +export { + composeBody, + getBodySlice, + getCustomBlockSlice, + getCustomDecorationSlice, + getCustomRangeSlice, + getParagraphsSlice, + getSectionBreakSlice, + getTableSlice, + getTextRunSlice, + normalizeBody, + SliceBodyType, +} from './docs/data-model/text-x/utils'; export { EventState, EventSubject, fromEventSubject, type IEventObserver } from './observer/observable'; export { AuthzIoLocalService } from './services/authz-io/authz-io-local.service'; export { IAuthzIoService } from './services/authz-io/type'; diff --git a/packages/core/src/services/context/context.ts b/packages/core/src/services/context/context.ts index 94d2ebd2ff3..50aaa28a5d1 100644 --- a/packages/core/src/services/context/context.ts +++ b/packages/core/src/services/context/context.ts @@ -37,6 +37,7 @@ export const FOCUSING_EDITOR_INPUT_FORMULA = 'FOCUSING_EDITOR_INPUT_FORMULA'; /** The focusing state of the formula editor (Fx bar). */ export const FOCUSING_FX_BAR_EDITOR = 'FOCUSING_FX_BAR_EDITOR'; +/** The focusing state of the cell editor. */ export const FOCUSING_UNIVER_EDITOR = 'FOCUSING_UNIVER_EDITOR'; export const FOCUSING_EDITOR_STANDALONE = 'FOCUSING_EDITOR_INPUT_FORMULA'; diff --git a/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts b/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts index 9d1809fd426..9605cf72ef4 100644 --- a/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts +++ b/packages/docs-drawing-ui/src/controllers/doc-drawing-transformer-update.controller.ts @@ -80,7 +80,6 @@ export class DocDrawingTransformerController extends Disposable { private _listenDrawingFocus(): void { this.disposeWithMe( this._drawingManagerService.add$.subscribe((drawingParams) => { - // console.log('===add$', drawingParams); if (drawingParams.length === 0) { return; } diff --git a/packages/docs-thread-comment-ui/src/commands/commands/add-doc-comment.command.ts b/packages/docs-thread-comment-ui/src/commands/commands/add-doc-comment.command.ts index 8a0d90e5f08..ad36a09bb3d 100644 --- a/packages/docs-thread-comment-ui/src/commands/commands/add-doc-comment.command.ts +++ b/packages/docs-thread-comment-ui/src/commands/commands/add-doc-comment.command.ts @@ -45,6 +45,7 @@ export const AddDocCommentComment: ICommand = { { id: comment.threadId, type: CustomDecorationType.COMMENT, + unitId, } ); if (doMutation) { diff --git a/packages/docs-thread-comment-ui/src/commands/operations/show-comment-panel.operation.ts b/packages/docs-thread-comment-ui/src/commands/operations/show-comment-panel.operation.ts index 3d8cbe31133..77633acfee3 100644 --- a/packages/docs-thread-comment-ui/src/commands/operations/show-comment-panel.operation.ts +++ b/packages/docs-thread-comment-ui/src/commands/operations/show-comment-panel.operation.ts @@ -99,7 +99,7 @@ export const StartAddCommentOperation: ICommand = { } const docSelectionRenderManager = renderManagerService.getRenderById(doc.getUnitId())?.with(DocSelectionRenderService); - + docSelectionRenderManager?.setReserveRangesStatus(true); if (textRange.collapsed) { if (panelService.panelVisible) { panelService.setPanelVisible(false); @@ -132,7 +132,7 @@ export const StartAddCommentOperation: ICommand = { threadId: commentId, }; - docSelectionRenderManager?.blurEditor(); + docSelectionRenderManager?.blur(); docCommentService.startAdd(comment); panelService.setActiveComment({ unitId, diff --git a/packages/docs-thread-comment-ui/src/controllers/doc-thread-comment-selection.controller.ts b/packages/docs-thread-comment-ui/src/controllers/doc-thread-comment-selection.controller.ts index 496f1fc7b8c..9efb7f86649 100644 --- a/packages/docs-thread-comment-ui/src/controllers/doc-thread-comment-selection.controller.ts +++ b/packages/docs-thread-comment-ui/src/controllers/doc-thread-comment-selection.controller.ts @@ -17,7 +17,7 @@ import type { DocumentDataModel, ITextRange } from '@univerjs/core'; import type { ISetTextSelectionsOperationParams } from '@univerjs/docs'; import type { ITextRangeWithStyle } from '@univerjs/engine-render'; -import { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; +import { Disposable, ICommandService, Inject, isInternalEditorID, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { SetTextSelectionsOperation } from '@univerjs/docs'; import { DocBackScrollRenderController } from '@univerjs/docs-ui'; import { IRenderManagerService } from '@univerjs/engine-render'; @@ -49,6 +49,7 @@ export class DocThreadCommentSelectionController extends Disposable { if (commandInfo.id === SetTextSelectionsOperation.id) { const params = commandInfo.params as ISetTextSelectionsOperationParams; const { unitId, ranges } = params; + if (isInternalEditorID(unitId)) return; const doc = this._univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); const primary = ranges[0] as ITextRangeWithStyle | undefined; if (lastSelection?.startOffset === primary?.startOffset && lastSelection?.endOffset === primary?.endOffset) { diff --git a/packages/docs-thread-comment-ui/src/views/doc-thread-comment-panel/index.tsx b/packages/docs-thread-comment-ui/src/views/doc-thread-comment-panel/index.tsx index 3f02112b827..cd00042ede0 100644 --- a/packages/docs-thread-comment-ui/src/views/doc-thread-comment-panel/index.tsx +++ b/packages/docs-thread-comment-ui/src/views/doc-thread-comment-panel/index.tsx @@ -16,11 +16,11 @@ import type { DocumentDataModel } from '@univerjs/core'; import type { IAddDocCommentComment } from '../../commands/commands/add-doc-comment.command'; -import { ICommandService, Injector, IUniverInstanceService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; +import { ICommandService, Injector, isInternalEditorID, IUniverInstanceService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs'; import { ThreadCommentPanel } from '@univerjs/thread-comment-ui'; import React, { useEffect, useMemo, useState } from 'react'; -import { debounceTime, Observable } from 'rxjs'; +import { debounceTime, filter, Observable } from 'rxjs'; import { AddDocCommentComment } from '../../commands/commands/add-doc-comment.command'; import { DeleteDocCommentComment, type IDeleteDocCommentComment } from '../../commands/commands/delete-doc-comment.command'; import { StartAddCommentOperation } from '../../commands/operations/show-comment-panel.operation'; @@ -31,7 +31,7 @@ import { DocThreadCommentService } from '../../services/doc-thread-comment.servi export const DocThreadCommentPanel = () => { const univerInstanceService = useDependency(IUniverInstanceService); const injector = useDependency(Injector); - const doc$ = useMemo(() => univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC), [univerInstanceService]); + const doc$ = useMemo(() => univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC).pipe(filter((doc) => !!doc && !isInternalEditorID(doc.getUnitId()))), [univerInstanceService]); const doc = useObservable(doc$); const subUnitId$ = useMemo(() => new Observable((sub) => sub.next(DEFAULT_DOC_SUBUNIT_ID)), []); const docSelectionManagerService = useDependency(DocSelectionManagerService); diff --git a/packages/docs-ui/src/basics/custom-decoration-factory.ts b/packages/docs-ui/src/basics/custom-decoration-factory.ts index 428c99d9fbf..b7190d903bb 100644 --- a/packages/docs-ui/src/basics/custom-decoration-factory.ts +++ b/packages/docs-ui/src/basics/custom-decoration-factory.ts @@ -53,14 +53,17 @@ interface IAddCustomDecorationFactoryParam { segmentId?: string; id: string; type: CustomDecorationType; + unitId?: string; } export function addCustomDecorationBySelectionFactory(accessor: IAccessor, param: IAddCustomDecorationFactoryParam) { - const { segmentId, id, type } = param; + const { segmentId, id, type, unitId: propUnitId } = param; const docSelectionManagerService = accessor.get(DocSelectionManagerService); const univerInstanceService = accessor.get(IUniverInstanceService); - const documentDataModel = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); + const documentDataModel = propUnitId ? + univerInstanceService.getUnit(propUnitId, UniverInstanceType.UNIVER_DOC) + : univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); if (!documentDataModel) { return false; } diff --git a/packages/docs-ui/src/basics/paragraph.ts b/packages/docs-ui/src/basics/paragraph.ts index 1d924057c1d..c5a2f6453d2 100644 --- a/packages/docs-ui/src/basics/paragraph.ts +++ b/packages/docs-ui/src/basics/paragraph.ts @@ -47,6 +47,16 @@ export function getTextRunAtPosition( } } + if (position === 0) { + const textRun = textRuns?.[0]; + if (textRun && textRun.st === 0) { + retTextRun.ts = { + ...retTextRun.ts, + ...textRun.ts, + }; + } + } + if (cacheStyle) { retTextRun.ts = { ...retTextRun.ts, diff --git a/packages/docs-ui/src/commands/commands/replace-content.command.ts b/packages/docs-ui/src/commands/commands/replace-content.command.ts index fd124e3f722..d88e8bda45a 100644 --- a/packages/docs-ui/src/commands/commands/replace-content.command.ts +++ b/packages/docs-ui/src/commands/commands/replace-content.command.ts @@ -33,7 +33,7 @@ export const ReplaceSnapshotCommand: ICommand = { id: 'doc.command-replace-snapshot', type: CommandType.COMMAND, // eslint-disable-next-line max-lines-per-function, complexity - handler: async (accessor, params: IReplaceSnapshotCommandParams) => { + handler: (accessor, params: IReplaceSnapshotCommandParams) => { const { unitId, snapshot, textRanges, segmentId = '', options } = params; const univerInstanceService = accessor.get(IUniverInstanceService); const commandService = accessor.get(ICommandService); @@ -46,7 +46,7 @@ export const ReplaceSnapshotCommand: ICommand = { return false; } - const { body, tableSource, footers, headers, lists, drawings, drawingsOrder } = snapshot; + const { body, tableSource, footers, headers, lists, drawings, drawingsOrder, documentStyle } = Tools.deepClone(snapshot); const { body: prevBody, tableSource: prevTableSource, @@ -55,6 +55,7 @@ export const ReplaceSnapshotCommand: ICommand = { lists: prevLists, drawings: prevDrawings, drawingsOrder: prevDrawingsOrder, + documentStyle: prevDocumentStyle, } = prevSnapshot; if (body == null || prevBody == null) { @@ -88,6 +89,13 @@ export const ReplaceSnapshotCommand: ICommand = { const jsonX = JSONX.getInstance(); + if (!Tools.diffValue(prevDocumentStyle, documentStyle)) { + const actions = jsonX.replaceOp(['documentStyle'], prevDocumentStyle, documentStyle); + if (actions != null) { + rawActions.push(actions); + } + } + if (!Tools.diffValue(body, prevBody)) { const actions = jsonX.replaceOp(['body'], prevBody, body); if (actions != null) { @@ -340,3 +348,56 @@ export const ReplaceSelectionCommand: ICommand = return true; }, }; + +export const ReplaceTextRunsCommand: ICommand = { + id: 'doc.command.replace-text-runs', + type: CommandType.COMMAND, + + handler: (accessor, params: IReplaceContentCommandParams) => { + const { unitId, body, textRanges, segmentId = '', options } = params; + const univerInstanceService = accessor.get(IUniverInstanceService); + const commandService = accessor.get(ICommandService); + // const docSelectionManagerService = accessor.get(DocSelectionManagerService); + + const docDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); + const prevBody = docDataModel?.getSelfOrHeaderFooterModel(segmentId).getSnapshot().body; + + if (docDataModel == null || prevBody == null) { + return false; + } + + const textX = BuildTextUtils.selection.replaceTextRuns({ + doc: docDataModel, + body, + selection: { + startOffset: 0, + endOffset: prevBody.dataStream.length - 2, + collapsed: false, + }, + }); + + if (!textX) { + return false; + } + + const doMutation = { + id: RichTextEditingMutation.id, + params: { + unitId, + actions: [], + textRanges, + noHistory: true, + } as IRichTextEditingMutationParams, + }; + const jsonX = JSONX.getInstance(); + const path = getRichTextEditPath(docDataModel, segmentId); + doMutation.params.actions = jsonX.editOp(textX.serialize(), path); + doMutation.params.textRanges = textRanges; + if (options) { + doMutation.params.options = options; + } + + const result = commandService.syncExecuteCommand(doMutation.id, doMutation.params); + return Boolean(result); + }, +}; diff --git a/packages/docs-ui/src/components/editor/TextEditor.tsx b/packages/docs-ui/src/components/editor/TextEditor.tsx deleted file mode 100644 index b44b5bd1f21..00000000000 --- a/packages/docs-ui/src/components/editor/TextEditor.tsx +++ /dev/null @@ -1,331 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * 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 { IDocumentData, Nullable } from '@univerjs/core'; -import type { Editor, IEditorCanvasStyle } from '../../services/editor/editor'; -import { debounce, isInternalEditorID, LocaleService, useDependency } from '@univerjs/core'; -import React, { useEffect, useRef, useState } from 'react'; -import { isElementVisible } from '../../basics/editor'; -import { IEditorService } from '../../services/editor/editor-manager.service'; -import styles from './index.module.less'; -import { genSnapShotByValue } from './utils'; - -type MyComponentProps = React.DetailedHTMLProps, HTMLDivElement>; - -const excludeProps = new Set([ - 'snapshot', - 'resizeCallBack', - 'cancelDefaultResizeListener', - 'isSheetEditor', - 'canvasStyle', - 'isFormulaEditor', - 'isSingle', - 'isReadonly', - 'onlyInputFormula', - 'onlyInputRange', - 'value', - 'onlyInputContent', - 'isSingleChoice', - 'openForSheetUnitId', - 'openForSheetSubUnitId', - 'onChange', - 'onActive', - 'onValid', - 'placeholder', -]); - -export interface ITextEditorProps { - id: string; // unitId - className?: string; // Parent class name. - - snapshot?: IDocumentData; // The default initialization snapshot for the editor can be simply replaced with the value attribute, for cellEditor and formulaBar - - value?: string; // default values. - - // WTF: snapshot and value both exists? And use have to set value and snapshot.textStream separately> - - resizeCallBack?: (editor: Nullable) => void; // Container scale callback. - - cancelDefaultResizeListener?: boolean; // Disable the default container scaling listener, for cellEditor and formulaBar - - canvasStyle?: IEditorCanvasStyle; // Setting the style of the editor is similar to setting the drawing style of a canvas, and therefore, it should be distinguished from the CSS 'style'. At present, it only supports the 'fontsize' attribute. - - isSheetEditor?: boolean; // Specify whether the editor is bound to a sheet. Currently, there are cellEditor and formulaBar. - isFormulaEditor?: boolean; - isSingle?: boolean; // Set whether the editor allows multiline input, default is true, equivalent to input; false is equivalent to textarea. - isReadonly?: boolean; // Set the editor to read-only state. - - onlyInputFormula?: boolean; // Only input formula string - onlyInputRange?: boolean; // Only input ref range - onlyInputContent?: boolean; // Only plain content can be entered, turning off formula and range input highlighting. - - isSingleChoice?: boolean; // Whether to restrict to only selecting a single region/area/district. - - openForSheetUnitId?: Nullable; // Configuring which workbook the selector defaults to opening in determines whether the ref includes a [unitId] prefix. - openForSheetSubUnitId?: Nullable; // Configuring the default worksheet where the selector opens determines whether the ref includes a [unitId]sheet1 prefix. - - onChange?: (value: Nullable) => void; // Callback for changes in the selector value. - onActive?: (state: boolean) => void; // Callback for editor active. - onValid?: (state: boolean) => void; // Editor input value validation, currently effective only under onlyRange and onlyFormula conditions. - - placeholder?: string; // Placeholder text. - isValueValid?: boolean; // Whether the value is valid. - disabled?: boolean; -} - -/** - * The component to render toolbar item label and menu item label. - * @param props - * @deprecated The business side encapsulates its own Editor component. - */ -export function TextEditor(props: ITextEditorProps & Omit): JSX.Element | null { - const { - id, - snapshot, - resizeCallBack, - cancelDefaultResizeListener, - isSheetEditor = false, - canvasStyle = {}, - value, - isSingle = true, - isReadonly = false, - isFormulaEditor = false, - onlyInputFormula = false, - onlyInputRange = false, - onlyInputContent = false, - isSingleChoice = false, - openForSheetUnitId, - openForSheetSubUnitId, - onChange, - onActive, - onValid, - isValueValid = true, - placeholder, - disabled, - } = props; - - const editorService = useDependency(IEditorService); - - const localeService = useDependency(LocaleService); - - const [placeholderValue, placeholderSet] = useState(''); - - const [validationContent, setValidationContent] = useState(''); - - const [validationVisible, setValidationVisible] = useState(isValueValid); - - const [validationOffset, setValidationOffset] = useState<[number, number]>([0, 0]); - - const editorRef = useRef(null); - - const [active, setActive] = useState(false); - - if (!isInternalEditorID(id)) { - throw new Error('Invalid editor ID'); - } - - useEffect(() => { - const editorDom = editorRef.current; - - if (!editorDom) { - return; - } - - const resizeObserver = new ResizeObserver(() => { - if (cancelDefaultResizeListener !== true) { - editorService.resize(id); - } - resizeCallBack && resizeCallBack(editorDom); - }); - - resizeObserver.observe(editorDom); - - const initialSnapshot = snapshot ?? genSnapShotByValue(id, value); - - if (initialSnapshot.id !== id) { - initialSnapshot.id = id; - } - - const registerSubscription = editorService.register({ - editorUnitId: id, - initialSnapshot, - cancelDefaultResizeListener, - isSheetEditor, - canvasStyle, - isSingle, - readonly: isReadonly, - isSingleChoice, - onlyInputFormula, - onlyInputRange, - onlyInputContent, - openForSheetUnitId, - openForSheetSubUnitId, - isFormulaEditor, - }, - editorDom); - - editorService.setValueNoRefresh(value || '', id); - placeholderSet(placeholder || ''); - - const activeChange = debounce((state: boolean) => { - setActive(state); - onActive && onActive(state); - }, 30); - - // !IMPORTANT: Set a delay of 160ms to ensure that the position is corrected after the sidebar animation ends @jikkai - const ANIMATION_DELAY = 160; - const valueChange = debounce((editor: Readonly) => { - const unitId = editor.getEditorId(); - const isLegality = editorService.checkValueLegality(unitId); - - setTimeout(() => { - const rect = editor.getBoundingClientRect(); - setValidationOffset([rect.left, rect.top - 16]); - if (rect.left + rect.top > 0) { - setValidationVisible(isLegality); - } - - if (editor.onlyInputFormula()) { - setValidationContent(localeService.t('textEditor.formulaError')); - } else { - setValidationContent(localeService.t('textEditor.rangeError')); - } - }, ANIMATION_DELAY); - - const currentValue = editorService.getValue(unitId); - - if (currentValue !== value) { - onValid && onValid(isLegality); - // WTF: why emit value on focus? - onChange && onChange(editorService.getValue(id)); - } - }, 30); - - const focusStyleSubscription = editorService.focusStyle$.subscribe((unitId: Nullable) => { - let state = false; - if (unitId === id) { - state = true; - } - activeChange(state); - - setTimeout(() => { - if (!isElementVisible(editorDom)) { - setValidationVisible(true); - } else { - const editor = editorService.getEditor(id); - editor && valueChange(editor); - } - }, ANIMATION_DELAY); - }); - - const valueChangeSubscription = editorService.valueChange$.subscribe((editor) => { - if (editor.isSheetEditor()) { - return; - } - - // WTF: should not use editorService to sync values. All editors instance would be notified! - if (editor.getEditorId() !== id) { - return; - } - - const focusEditor = editorService.getFocusEditor(); - - if (focusEditor && focusEditor.getEditorId() !== id) { - return; - } - - valueChange(editor); - }); - - return () => { - resizeObserver.unobserve(editorDom); - resizeObserver.disconnect(); - registerSubscription.dispose(); - focusStyleSubscription?.unsubscribe(); - valueChangeSubscription?.unsubscribe(); - }; - }, []); - - useEffect(() => { - const editor = editorService.getEditor(id); - if (editor == null) { - return; - } - - editor.update({ - readonly: isReadonly, isSingle, isSingleChoice, onlyInputContent, onlyInputFormula, onlyInputRange, openForSheetSubUnitId, openForSheetUnitId, - }); - }, [isReadonly, isSingle, isSingleChoice, onlyInputContent, onlyInputFormula, onlyInputRange, openForSheetSubUnitId, openForSheetUnitId]); - - useEffect(() => { - if (value == null) { - return; - } - - editorService.setValueNoRefresh(value, id); - }, [value]); - - useEffect(() => { - setValidationVisible(isValueValid); - }, [isValueValid]); - - function hasValue() { - const value = editorService.getValue(id); - if (value == null) { - return false; - } - - if (value === '') { - return false; - } - - return true; - } - - const propsNew = Object.fromEntries( - Object.entries(props).filter(([key]) => !excludeProps.has(key)) - ); - - let className = styles.textEditorContainer; - if (props.className != null) { - className = props.className; - } - - let borderStyle = ''; - - if (props.className == null) { - if (isReadonly) { - borderStyle = ` ${styles.textEditorContainerDisabled}`; - } else if (!validationVisible) { - borderStyle = ` ${styles.textEditorContainerError}`; - } else if (active) { - borderStyle = ` ${styles.textEditorContainerActive}`; - } - } - - return ( - <> -
-
{validationContent}
-
{placeholderValue}
-
- {/* Don't delete it yet, test the stability without popup */} - {/* -
{validationContent}
-
*/} - - ); -} diff --git a/packages/docs-ui/src/components/range-selector/RangeSelector.tsx b/packages/docs-ui/src/components/range-selector/RangeSelector.tsx deleted file mode 100644 index d0c6aac9696..00000000000 --- a/packages/docs-ui/src/components/range-selector/RangeSelector.tsx +++ /dev/null @@ -1,379 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * 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 { IUnitRangeWithName, Nullable, Workbook } from '@univerjs/core'; -import { IUniverInstanceService, LocaleService, UniverInstanceType, useDependency } from '@univerjs/core'; -import { Button, Dialog, Input, Tooltip } from '@univerjs/design'; -import { getRangeWithRefsString, isReferenceStringWithEffectiveColumn, serializeRange, serializeRangeWithSheet, serializeRangeWithSpreadsheet } from '@univerjs/engine-formula'; -import { CloseSingle, DeleteSingle, IncreaseSingle, SelectRangeSingle } from '@univerjs/icons'; - -import { useEvent } from '@univerjs/ui'; -import clsx from 'clsx'; -import React, { useEffect, useRef, useState } from 'react'; -import { IEditorService } from '../../services/editor/editor-manager.service'; -import { IRangeSelectorService } from '../../services/range-selector/range-selector.service'; -import { TextEditor } from '../editor/TextEditor'; -import styles from './index.module.less'; - -export interface IRangeSelectorProps { - id: string; - value?: string; // default values. - onChange?: (ranges: IUnitRangeWithName[]) => void; // Callback for changes in the selector value. - onActive?: (state: boolean) => void; // Callback for editor active. - onValid?: (state: boolean) => void; // input value validation - isSingleChoice?: boolean; // Whether to restrict to only selecting a single region/area/district. - isReadonly?: boolean; // Set the selector to read-only state. - openForSheetUnitId?: Nullable; // Configuring which workbook the selector defaults to opening in determines whether the ref includes a [unitId] prefix. - openForSheetSubUnitId?: Nullable; // Configuring the default worksheet where the selector opens determines whether the ref includes a [unitId]sheet1 prefix. - width?: number | string; // The width of the selector. - size?: 'mini' | 'small' | 'middle' | 'large'; // The size of the selector. - placeholder?: string; // Placeholder text. - className?: string; - textEditorClassName?: string; - onSelectorVisibleChange?: (visible: boolean) => void; - dialogOnly?: boolean; -} - -const dialogOnlyInputStyle: React.CSSProperties = { - pointerEvents: 'none', -}; - -/** - * @deprecated - */ -export function RangeSelector(props: IRangeSelectorProps) { - const { dialogOnly, onChange, id, value = '', width = 220, placeholder = '', size = 'middle', onActive, onValid, isSingleChoice = false, openForSheetUnitId, openForSheetSubUnitId, isReadonly = false, className, textEditorClassName, onSelectorVisibleChange: _onSelectorVisibleChange } = props; - const onSelectorVisibleChange = useEvent(_onSelectorVisibleChange); - const [rangeDataList, setRangeDataList] = useState(['']); - - const addNewItem = (newValue: string) => { - setRangeDataList((prevRangeDataList) => [...prevRangeDataList, newValue]); - }; - - const removeItem = (indexToRemove: number) => { - setRangeDataList((prevRangeDataList) => - prevRangeDataList.filter((_, index) => index !== indexToRemove) - ); - }; - - const changeItem = (indexToChange: number, newValue: string) => { - setRangeDataList((prevRangeDataList) => - prevRangeDataList.map((item, index) => - index === indexToChange ? newValue : item - ) - ); - }; - - const changeLastItem = (newValue: string) => { - setRangeDataList((prevRangeDataList) => { - const newList = [...prevRangeDataList]; - if (newList.length > 0) { - newList[newList.length - 1] = newValue; - } - return newList; - }); - }; - - const editorService = useDependency(IEditorService); - - const rangeSelectorService = useDependency(IRangeSelectorService); - - const univerInstanceService = useDependency(IUniverInstanceService); - - const [selectorVisible, setSelectorVisible] = useState(false); - - const localeService = useDependency(LocaleService); - - const [active, setActive] = useState(false); - - const [valid, setValid] = useState(true); - - const [rangeValue, setRangeValue] = useState(value); - - const [currentInputIndex, setCurrentInputIndex] = useState(-1); - - const selectorRef = useRef(null); - - const currentInputIndexRef = useRef(-1); - - const openForSheetUnitIdRef = useRef>(openForSheetUnitId); - - const openForSheetSubUnitIdRef = useRef>(openForSheetSubUnitId); - - const isSingleChoiceRef = useRef>(isSingleChoice); - - const isReadonlyRef = useRef>(isReadonly); - - useEffect(() => { - const selector = selectorRef.current; - - if (!selector) { - return; - } - - const resizeObserver = new ResizeObserver(() => { - editorService.resize(id); - }); - resizeObserver.observe(selector); - - let prevRangesCount = 1; - const valueChangeSubscription = rangeSelectorService.selectionChange$.subscribe((ranges) => { - if (rangeSelectorService.getCurrentSelectorId() !== id) { - return; - } - - if (ranges.length === 0) { - prevRangesCount = 0; - return; - } - - const addItemCount = ranges.length - prevRangesCount; - - prevRangesCount = ranges.length; - - if (addItemCount < 0) { - return; - } - - const lastRange = ranges[ranges.length - 1]; - - let rangeRef: string = ''; - - if (lastRange.unitId === openForSheetUnitIdRef.current && lastRange.sheetId === openForSheetSubUnitIdRef.current) { - rangeRef = serializeRange(lastRange.range); - } else if (lastRange.unitId === openForSheetUnitIdRef.current) { - rangeRef = serializeRangeWithSheet(lastRange.sheetName, lastRange.range); - } else { - rangeRef = serializeRangeWithSpreadsheet(lastRange.unitId, lastRange.sheetName, lastRange.range); - } - - if (addItemCount >= 1 && !isSingleChoiceRef.current) { - addNewItem(rangeRef); - setCurrentInputIndex(-1); - } else { - if (currentInputIndexRef.current === -1) { - changeLastItem(rangeRef); - } else { - changeItem(currentInputIndexRef.current, rangeRef); - } - } - }); - - // Clean up on unmount - return () => { - valueChangeSubscription.unsubscribe(); - resizeObserver.unobserve(selector); - }; - }, []); - - useEffect(() => { - rangeSelectorService.triggerModalVisibleChange(selectorVisible); - }, [onSelectorVisibleChange, rangeSelectorService, selectorVisible]); - - useEffect(() => { - return () => { - rangeSelectorService.triggerModalVisibleChange(false); - }; - }, [rangeSelectorService]); - - useEffect(() => { - openForSheetUnitIdRef.current = openForSheetUnitId; - openForSheetSubUnitIdRef.current = openForSheetSubUnitId; - isSingleChoiceRef.current = isSingleChoice; - isReadonlyRef.current = isReadonly; - }, [openForSheetUnitId, openForSheetSubUnitId, isSingleChoice, isReadonly]); - - useEffect(() => { - currentInputIndexRef.current = currentInputIndex; - }, [currentInputIndex]); - - function handleCloseModal() { - setSelectorVisible(false); - onSelectorVisibleChange(false); - rangeSelectorService.setCurrentSelectorId(null); - } - - function handleOpenModal() { - if (isReadonlyRef.current === true) { - return; - } - - editorService.closeRangePrompt(); - - rangeSelectorService.setCurrentSelectorId(id); - - setSelectorVisible(true); - onSelectorVisibleChange(true); - - if (rangeValue.length > 0) { - if (valid) { - setRangeDataList(rangeValue.split(',')); - } else { - setRangeDataList(['']); - } - } else { - rangeSelectorService.openSelector(); - } - } - - function onEditorActive(state: boolean) { - setActive(state); - onActive && onActive(state); - } - - function onEditorValid(state: boolean) { - setValid(state); - onValid && onValid(state); - } - - function handleConform() { - if (isReadonlyRef.current === true) { - handleCloseModal(); - return; - } - - let result = ''; - const list = rangeDataList.filter((rangeRef) => { - return isReferenceStringWithEffectiveColumn(rangeRef.trim()); - }); - if (list.length === 1) { - const rangeRef = list[0]; - if (isReferenceStringWithEffectiveColumn(rangeRef.trim())) { - result = rangeRef.trim(); - } - } else { - result = list.join(','); - } - - editorService.setValue(result, id); - - handleTextValueChange(result); - - handleCloseModal(); - } - - function handleAddRange() { - addNewItem(''); - setCurrentInputIndex(-1); - } - - function getSheetIdByName(name: string) { - return univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetName(name)?.getSheetId() || ''; - } - - function handleTextValueChange(value: Nullable) { - setRangeValue(value || ''); - - if (value == null) { - onChange && onChange([]); - return; - } - - const ranges = getRangeWithRefsString(value, getSheetIdByName); - - onChange && onChange(ranges || []); - } - - let sClassName = styles.rangeSelector; - - if (isReadonly) { - sClassName = `${styles.rangeSelector} ${styles.rangeSelectorDisabled}`; - } else if (!valid) { - sClassName = `${styles.rangeSelector} ${styles.rangeSelectorError}`; - } else if (active) { - sClassName = `${styles.rangeSelector} ${styles.rangeSelectorActive}`; - } - - if (textEditorClassName) { - sClassName = `${sClassName} ${textEditorClassName}`; - } - - let height = 28; - if (size === 'mini') { - height = 20; - } else if (size === 'small') { - height = 24; - } else if (size === 'large') { - height = 32; - } - return ( - <> -
{ - if (dialogOnly) { - event.stopPropagation(); - event.preventDefault(); - handleOpenModal(); - } - }} - > - - - - -
- - } - footer={( -
- - -
- )} - onClose={handleCloseModal} - > -
- {rangeDataList.map((item, index) => ( -
-
- setCurrentInputIndex(index)} - value={item} - onChange={(value) => changeItem(index, value)} - /> -
-
- removeItem(index)} /> -
-
- ))} - -
- -
-
- -
- - ); -} diff --git a/packages/docs-ui/src/components/range-selector/index.module.less b/packages/docs-ui/src/components/range-selector/index.module.less deleted file mode 100644 index 0004b6aeec8..00000000000 --- a/packages/docs-ui/src/components/range-selector/index.module.less +++ /dev/null @@ -1,158 +0,0 @@ -@padding-top-bottom: 6px; -@height: 28px; -@width: 220px; -@icon-size: 24px; - -.range-selector { - overflow: hidden; - display: flex; - align-items: center; - justify-content: space-between; - - color: rgb(var(--grey-600)); - - // padding: 0 var(--padding-sm) 0 var(--padding-base); - - border: 1px solid rgb(var(--border-color)); - border-radius: var(--border-radius-base); - - width: @width; - height: @height; - - &-editor { - position: relative; - user-select: none; - width: 100%; - height: 100%; - border: 0; - outline: 0; - } - - &-icon { - cursor: pointer; - - display: flex; - align-items: center; - justify-content: center; - - width: @icon-size; - height: @icon-size; - padding: 0; - - margin-right: 4px; - - font-size: var(--font-size-lg); - color: rgb(var(--text-color)); - - background-color: transparent; - border: none; - border-radius: var(--border-radius-base); - outline: none; - - &:not([disabled]):hover { - background-color: rgb(var(--grey-100)); - } - - &[disabled] { - cursor: not-allowed; - color: rgb(var(--grey-200)); - } - } - - &:hover { - border-color: rgb(var(--hyacinth-500)); - } - - &-active { - border-color: rgb(var(--hyacinth-500)); - - .range-selector-icon { - color: rgb(var(--hyacinth-500)); - } - } - - &-error { - border-color: rgb(var(--red-400)); - - .range-selector-icon { - color: rgb(var(--red-400)); - } - - &:hover { - border-color: rgb(var(--red-400)); - } - } - - &-disabled { - border-color: rgb(var(--grey-100)); - - .range-selector-icon { - color: rgb(var(--grey-100)); - } - - &:hover { - border-color: rgb(var(--grey-100)); - } - } -} - -.range-selector-modal { - position: relative; - - max-height: 500px; - - overflow: hidden; - - overflow-y: auto; - - &-container { - display: flex; - flex-direction: row; - // justify-content: center; - align-items: center; - - margin-bottom: 10px; - - &-input { - display: inline-block; - width: 280px; - - &-active { - border-color: rgb(var(--hyacinth-500)); - } - } - - &-button { - display: inline-block; - text-align: center; - width: 28px; - - &:hover { - cursor: pointer; - color: rgb(var(--hyacinth-500)); - } - } - &-delete-button { - margin: auto; - } - } - - &-add { - position: relative; - width: 300px; - margin-top: 5px; - text-align: left; - color: rgb(var(--hyacinth-500)); - font-size: var(--font-size-xs); - & &-button { - display: flex; - align-items: center; - justify-content: center; - - &:hover { - cursor: pointer; - background-color: rgb(var(--hyacinth-500), 0.05); - } - } - } -} diff --git a/packages/docs-ui/src/controllers/render-controllers/doc-editor-bridge.controller.ts b/packages/docs-ui/src/controllers/render-controllers/doc-editor-bridge.controller.ts index f6aad176266..10ff840086f 100644 --- a/packages/docs-ui/src/controllers/render-controllers/doc-editor-bridge.controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/doc-editor-bridge.controller.ts @@ -19,10 +19,8 @@ import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { IRenderContext, IRenderModule } from '@univerjs/engine-render'; import { checkForSubstrings, Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; -import { IRenderManagerService, ScrollBar } from '@univerjs/engine-render'; +import { IRenderManagerService } from '@univerjs/engine-render'; import { fromEvent } from 'rxjs'; -import { VIEWPORT_KEY } from '../../basics/docs-view-key'; -import { CoverContentCommand } from '../../commands/commands/replace-content.command'; import { IEditorService } from '../../services/editor/editor-manager.service'; import { DocSelectionRenderService } from '../../services/selection/doc-selection-render.service'; @@ -43,16 +41,6 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu } private _initialize() { - this.disposeWithMe( - this._editorService.resize$.subscribe((unitId: string) => { - if (unitId !== this._context.unitId) { - return; - } - - this._resize(unitId); - }) - ); - this._editorService.getAllEditor().forEach((editor) => { const unitId = editor.getEditorId(); @@ -74,11 +62,8 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu this._initialBlur(); this._initialFocus(); - - this._initialValueChange(); } - // eslint-disable-next-line complexity private _resize(unitId: Nullable) { if (unitId == null) { return; @@ -114,10 +99,6 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu const { width, height } = editor.getBoundingClientRect(); - const viewportMain = scene.getViewport(VIEWPORT_KEY.VIEW_MAIN); - - let scrollBar = viewportMain?.getScrollBar() as Nullable; - const contentWidth = Math.max(actualWidth, width); const contentHeight = Math.max(actualHeight, height); @@ -128,55 +109,27 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu }); mainComponent?.resize(contentWidth, contentHeight); - - if (!editor.isSingle()) { - if (actualHeight > height) { - if (scrollBar == null) { - viewportMain && new ScrollBar(viewportMain, { enableHorizontal: false, barSize: 8 }); - } else { - viewportMain?.resetCanvasSizeAndUpdateScroll(); - } - } else { - scrollBar = null; - viewportMain?.scrollToBarPos({ x: 0, y: 0 }); - viewportMain?.getScrollBar()?.dispose(); - } - } else { - if (actualWidth > width) { - if (scrollBar == null) { - viewportMain && new ScrollBar(viewportMain, { barSize: 8, enableVertical: false }); - } else { - viewportMain?.resetCanvasSizeAndUpdateScroll(); - } - } else { - scrollBar = null; - viewportMain?.scrollToBarPos({ x: 0, y: 0 }); - viewportMain?.getScrollBar()?.dispose(); - } - } } private _initialSetValue() { - this.disposeWithMe( - this._editorService.setValue$.subscribe((param) => { - if (param.editorUnitId !== this._context.unitId) { - return; - } - - this._commandService.executeCommand(CoverContentCommand.id, { - unitId: param.editorUnitId, - body: param.body, - segmentId: null, - }); - }) - ); + // this.disposeWithMe( + // this._editorService.setValue$.subscribe((param) => { + // if (param.editorUnitId !== this._context.unitId) { + // return; + // } + + // this._commandService.executeCommand(CoverContentCommand.id, { + // unitId: param.editorUnitId, + // body: param.body, + // segmentId: null, + // }); + // }) + // ); } private _initialBlur() { this.disposeWithMe( this._editorService.blur$.subscribe(() => { - // this._docSelectionRenderService.removeAllRanges(); - this._docSelectionRenderService.blur(); }) ); @@ -204,16 +157,16 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu } private _initialFocus() { - this.disposeWithMe( - this._editorService.focus$.subscribe((textRange) => { - if (this._editorService.getFocusEditor()?.getEditorId() !== this._context.unitId) { - return; - } + // this.disposeWithMe( + // this._editorService.focus$.subscribe((textRange) => { + // if (this._editorService.getFocusEditor()?.getEditorId() !== this._context.unitId) { + // return; + // } - this._docSelectionRenderService.removeAllRanges(); - this._docSelectionRenderService.addDocRanges([textRange]); - }) - ); + // this._docSelectionRenderService.removeAllRanges(); + // this._docSelectionRenderService.addDocRanges([textRange]); + // }) + // ); const focusExcepts = [ 'univer-formula-search', @@ -228,11 +181,8 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu const hasSearch = target.classList[0] || ''; if (checkForSubstrings(hasSearch, focusExcepts)) { - this._editorService.changeSpreadsheetFocusState(true); event.stopPropagation(); - return; } - this._editorService.changeSpreadsheetFocusState(false); }) ); @@ -251,39 +201,11 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu return; } fromEvent(canvasEle, 'mousedown').subscribe((evt) => { - this._editorService.changeSpreadsheetFocusState(true); evt.stopPropagation(); }); }); } - private _initialValueChange() { - this.disposeWithMe( - this._docSelectionRenderService.onCompositionupdate$.subscribe(this._valueChange.bind(this)) - ); - this.disposeWithMe( - this._docSelectionRenderService.onInput$.subscribe(this._valueChange.bind(this)) - ); - this.disposeWithMe( - this._docSelectionRenderService.onKeydown$.subscribe(this._valueChange.bind(this)) - ); - this.disposeWithMe( - this._docSelectionRenderService.onPaste$.subscribe(this._valueChange.bind(this)) - ); - } - - private _valueChange() { - const { unitId } = this._context; - - const editor = this._editorService.getEditor(unitId); - - if (editor == null || editor.isSheetEditor()) { - return; - } - - this._editorService.refreshValueChange(unitId); - } - /** * Listen to document edits to refresh the size of the formula editor. */ @@ -305,8 +227,6 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu // Only for Text editor? if (editor && !editor.params.scrollBar) { this._resize(unitId); - - this._valueChange(); } } }) diff --git a/packages/docs-ui/src/controllers/render-controllers/doc-selection-render.controller.ts b/packages/docs-ui/src/controllers/render-controllers/doc-selection-render.controller.ts index 4cbe5448033..7af3a5bcf49 100644 --- a/packages/docs-ui/src/controllers/render-controllers/doc-selection-render.controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/doc-selection-render.controller.ts @@ -221,18 +221,7 @@ export class DocSelectionRenderController extends Disposable implements IRenderM } private _setEditorFocus(unitId: string) { - // TODO@wzhudev: fix - /** - * The object for selecting data in the editor is set to the current sheet. - */ - // const sheetInstances = this._univerInstanceService.getAllUnitsForType(UniverInstanceType.UNIVER_SHEET); - // if (sheetInstances.length > 0) { - // const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; - // this._editorService.setOperationSheetUnitId(workbook.getUnitId()); - // // this._editorService.setOperationSheetSubUnitId(workbook.getActiveSheet().getSheetId()); - // } - - this._editorService.focusStyle(unitId); + this._editorService.focus(unitId); } private _commandExecutedListener() { diff --git a/packages/docs-ui/src/controllers/render-controllers/doc.render-controller.ts b/packages/docs-ui/src/controllers/render-controllers/doc.render-controller.ts index d5a51dee644..62026d5b8b2 100644 --- a/packages/docs-ui/src/controllers/render-controllers/doc.render-controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/doc.render-controller.ts @@ -178,6 +178,16 @@ export class DocRenderController extends RxDisposable implements IRenderModule { docsComponent.changeSkeleton(skeleton); docBackground.changeSkeleton(skeleton); + const { unitId } = this._context; + + // REFACTOR: @Jocs, should not use scroll bar to indicate a Zen Editor. refactor after support modern doc. + const editor = this._editorService.getEditor(unitId); + if (this._editorService.isEditor(unitId) && !editor?.params.scrollBar) { + this._context.mainComponent?.makeDirty(); + + return; + } + this._recalculateSizeBySkeleton(skeleton); } diff --git a/packages/docs-ui/src/docs-ui-plugin.ts b/packages/docs-ui/src/docs-ui-plugin.ts index 73baea2857a..cebc5235674 100644 --- a/packages/docs-ui/src/docs-ui-plugin.ts +++ b/packages/docs-ui/src/docs-ui-plugin.ts @@ -46,7 +46,7 @@ import { IMEInputCommand } from './commands/commands/ime-input.command'; import { ResetInlineFormatTextBackgroundColorCommand, SetInlineFormatBoldCommand, SetInlineFormatCommand, SetInlineFormatFontFamilyCommand, SetInlineFormatFontSizeCommand, SetInlineFormatItalicCommand, SetInlineFormatStrikethroughCommand, SetInlineFormatSubscriptCommand, SetInlineFormatSuperscriptCommand, SetInlineFormatTextBackgroundColorCommand, SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand } from './commands/commands/inline-format.command'; import { BulletListCommand, ChangeListNestingLevelCommand, ChangeListTypeCommand, CheckListCommand, ListOperationCommand, OrderListCommand, QuickListCommand, ToggleCheckListCommand } from './commands/commands/list.command'; import { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignOperationCommand, AlignRightCommand } from './commands/commands/paragraph-align.command'; -import { CoverContentCommand, ReplaceContentCommand, ReplaceSnapshotCommand } from './commands/commands/replace-content.command'; +import { CoverContentCommand, ReplaceContentCommand, ReplaceSnapshotCommand, ReplaceTextRunsCommand } from './commands/commands/replace-content.command'; import { SetDocZoomRatioCommand } from './commands/commands/set-doc-zoom-ratio.command'; import { SwitchDocModeCommand } from './commands/commands/switch-doc-mode.command'; import { CreateDocTableCommand } from './commands/commands/table/doc-table-create.command'; @@ -222,6 +222,7 @@ export class UniverDocsUIPlugin extends Plugin { DocParagraphSettingPanelOperation, MoveCursorOperation, MoveSelectionOperation, + ReplaceTextRunsCommand, ].forEach((e) => { this._commandService.registerCommand(e); }); diff --git a/packages/docs-ui/src/index.ts b/packages/docs-ui/src/index.ts index 049f39573bb..f6bee8c5f11 100644 --- a/packages/docs-ui/src/index.ts +++ b/packages/docs-ui/src/index.ts @@ -24,14 +24,14 @@ export { addCustomDecorationBySelectionFactory, addCustomDecorationFactory, dele export * from './basics/docs-view-key'; export { hasParagraphInTable } from './basics/paragraph'; export { docDrawingPositionToTransform, transformToDocDrawingPosition } from './basics/transform-position'; - +export { type IKeyboardEventConfig, useKeyboardEvent, useResize } from './views/rich-text-editor/hooks'; +export { RichTextEditor } from './views/rich-text-editor'; export { getCommandSkeleton, getRichTextEditPath } from './commands/util'; -export { TextEditor } from './components/editor/TextEditor'; -export { RangeSelector as DocRangeSelector } from './components/range-selector/RangeSelector'; +// export { TextEditor } from './components/editor/TextEditor'; +// export { RangeSelector as DocRangeSelector } from './components/range-selector/RangeSelector'; export { DocUIController } from './controllers/doc-ui.controller'; export { menuSchema as DocsUIMenuSchema } from './controllers/menu.schema'; export { DocBackScrollRenderController } from './controllers/render-controllers/back-scroll.render-controller'; - export { DocRenderController } from './controllers/render-controllers/doc.render-controller'; export * from './docs-ui-plugin'; export * from './services'; @@ -114,6 +114,7 @@ export { AlignOperationCommand, AlignRightCommand, } from './commands/commands/paragraph-align.command'; +export { ReplaceTextRunsCommand } from './commands/commands/replace-content.command'; export { CoverContentCommand, type IReplaceSelectionCommandParams, type IReplaceSnapshotCommandParams, ReplaceContentCommand, ReplaceSnapshotCommand } from './commands/commands/replace-content.command'; export { SetDocZoomRatioCommand } from './commands/commands/set-doc-zoom-ratio.command'; export { CreateDocTableCommand, type ICreateDocTableCommandParams } from './commands/commands/table/doc-table-create.command'; diff --git a/packages/docs-ui/src/services/editor/editor-manager.service.ts b/packages/docs-ui/src/services/editor/editor-manager.service.ts index ec65a3bb0d8..47c10696732 100644 --- a/packages/docs-ui/src/services/editor/editor-manager.service.ts +++ b/packages/docs-ui/src/services/editor/editor-manager.service.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import type { DocumentDataModel, IDisposable, IDocumentBody, IDocumentData, Nullable, Workbook } from '@univerjs/core'; +import type { DocumentDataModel, IDisposable, IDocumentBody, IDocumentData, Nullable } from '@univerjs/core'; import type { ISuccinctDocRangeParam, Scene } from '@univerjs/engine-render'; import type { Observable } from 'rxjs'; -import type { IEditorConfigParams, IEditorStateParams } from './editor'; -import { createIdentifier, DEFAULT_EMPTY_DOCUMENT_VALUE, Disposable, EDITOR_ACTIVATED, FOCUSING_EDITOR_INPUT_FORMULA, FOCUSING_EDITOR_STANDALONE, FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, HorizontalAlign, ICommandService, IContextService, Inject, isInternalEditorID, IUndoRedoService, IUniverInstanceService, toDisposable, UniverInstanceType, VerticalAlign } from '@univerjs/core'; +import type { IEditorConfigParams } from './editor'; +import { createIdentifier, DEFAULT_EMPTY_DOCUMENT_VALUE, Disposable, EDITOR_ACTIVATED, FOCUSING_EDITOR_STANDALONE, HorizontalAlign, ICommandService, IContextService, Inject, isInternalEditorID, IUndoRedoService, IUniverInstanceService, toDisposable, UniverInstanceType, VerticalAlign } from '@univerjs/core'; import { DocSelectionManagerService } from '@univerjs/docs'; -import { isReferenceStrings, LexerTreeBuilder, operatorToken } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; import { fromEvent, Subject } from 'rxjs'; import { Editor } from './editor'; @@ -46,145 +45,24 @@ export interface IEditorInputFormulaParam { formulaString: string; } -/** - * @deprecated - */ export interface IEditorService { getEditor(id?: string): Readonly>; register(config: IEditorConfigParams, container: HTMLDivElement): IDisposable; - /** - * @deprecated - */ - isVisible(id: string): Nullable; - - inputFormula$: Observable; - - /** - * @deprecated - */ - setFormula(formulaString: string): void; - - resize$: Observable; - /** - * @deprecated - */ - resize(id: string): void; - - /** - * @deprecated - */ getAllEditor(): Map; - /** - * The sheet currently being operated on will determine - * whether to include unitId information in the ref. - */ - setOperationSheetUnitId(unitId: Nullable): void; - getOperationSheetUnitId(): Nullable; - /** - * The sub-table within the sheet currently being operated on - * will determine whether to include subUnitId information in the ref. - */ - setOperationSheetSubUnitId(sheetId: Nullable): void; - getOperationSheetSubUnitId(): Nullable; - isEditor(editorUnitId: string): boolean; isSheetEditor(editorUnitId: string): boolean; - closeRangePrompt$: Observable; - /** - * @deprecated - */ - closeRangePrompt(): void; - blur$: Observable; - /** - * @deprecated - */ blur(): void; focus$: Observable; - /** - * @deprecated - */ - focus(editorUnitId?: string): void; - - setValue$: Observable; - valueChange$: Observable>; - - /** - * @deprecated - */ - setValue(val: string, editorUnitId?: string): void; - - /** - * @deprecated - */ - setValueNoRefresh(val: string, editorUnitId?: string): void; - - /** - * @deprecated - */ - setRichValue(body: IDocumentBody, editorUnitId?: string): void; - - /** - * @deprecated - */ - getFirstEditor(): Editor; - - focusStyle$: Observable>; - /** - * @deprecated - */ - focusStyle(editorUnitId: Nullable): void; - - /** - * @deprecated - */ - refreshValueChange(editorId: string): void; - - /** - * @deprecated - */ - checkValueLegality(editorId: string): boolean; - - /** - * @deprecated - */ - getValue(id: string): Nullable; - - /** - * @deprecated - */ - getRichValue(id: string): Nullable; - - /** - * @deprecated - */ - changeSpreadsheetFocusState(state: boolean): void; - - /** - * @deprecated - */ - getSpreadsheetFocusState(): boolean; - - /** - * @deprecated - */ - selectionChangingState(): boolean; - - singleSelection$: Observable; - /** - * @deprecated - */ - singleSelection(state: boolean): void; - - setFocusId(id: Nullable): void; - getFocusId(): Nullable; + focus(editorUnitId: string): void; + getFocusId(): Nullable; getFocusEditor(): Readonly>; } @@ -193,46 +71,15 @@ export class EditorService extends Disposable implements IEditorService, IDispos private _focusEditorUnitId: Nullable; - private readonly _state$ = new Subject>(); - readonly state$ = this._state$.asObservable(); - - private _currentSheetUnitId: Nullable; - - private _currentSheetSubUnitId: Nullable; - - private readonly _inputFormula$ = new Subject(); - readonly inputFormula$ = this._inputFormula$.asObservable(); - - private readonly _resize$ = new Subject(); - readonly resize$ = this._resize$.asObservable(); - - private readonly _closeRangePrompt$ = new Subject(); - readonly closeRangePrompt$ = this._closeRangePrompt$.asObservable(); - private readonly _blur$ = new Subject(); readonly blur$ = this._blur$.asObservable(); private readonly _focus$ = new Subject(); readonly focus$ = this._focus$.asObservable(); - private readonly _setValue$ = new Subject(); - readonly setValue$ = this._setValue$.asObservable(); - - private readonly _valueChange$ = new Subject>(); - readonly valueChange$ = this._valueChange$.asObservable(); - - private readonly _focusStyle$ = new Subject>(); - readonly focusStyle$ = this._focusStyle$.asObservable(); - - private readonly _singleSelection$ = new Subject(); - readonly singleSelection$ = this._singleSelection$.asObservable(); - - private _spreadsheetFocusState: boolean = false; - constructor( @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, - @Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder, @Inject(DocSelectionManagerService) private readonly _docSelectionManagerService: DocSelectionManagerService, @IContextService private readonly _contextService: IContextService, @ICommandService private readonly _commandService: ICommandService, @@ -249,36 +96,33 @@ export class EditorService extends Disposable implements IEditorService, IDispos this.disposeWithMe( fromEvent(window, 'focusin').subscribe((event) => { const target = event.target as HTMLElement; - this._blurSheetEditor(target); }) ); } - /** @deprecated */ private _blurSheetEditor(target: HTMLElement) { if (editorFocusInElements.some((item) => target.classList.contains(item))) { return; } - // NOTE: Note that the focus editor will not be docs' editor but calling `this._editorService.blur()` will blur doc's editor. const focusEditor = this.getFocusEditor(); if (focusEditor && focusEditor.isSheetEditor() !== true) { this.blur(); } } - /** @deprecated */ - setFocusId(id: Nullable) { + private _setFocusId(id: Nullable) { + if (id) { + this.getEditor(id)?.setFocus(true); + } this._focusEditorUnitId = id; } - /** @deprecated */ getFocusId() { return this._focusEditorUnitId; } - /** @deprecated */ getFocusEditor() { if (this._focusEditorUnitId) { return this.getEditor(this._focusEditorUnitId); @@ -289,119 +133,25 @@ export class EditorService extends Disposable implements IEditorService, IDispos return this._editors.has(editorUnitId); } - /** @deprecated */ isSheetEditor(editorUnitId: string) { const editor = this._editors.get(editorUnitId); return !!(editor && editor.isSheetEditor()); } - /** @deprecated */ - closeRangePrompt() { - const documentDataModel = this._univerInstanceService.getCurrentUniverDocInstance(); - if (!documentDataModel) { - return; - } - - const editorUnitId = documentDataModel.getUnitId(); - + blur() { + this._setFocusId(null); this._contextService.setContextValue(EDITOR_ACTIVATED, false); this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, false); - if (!this.isEditor(editorUnitId) || this.isSheetEditor(editorUnitId)) { - return; - } - - this.changeSpreadsheetFocusState(false); - - this.blur(); - } - - /** @deprecated */ - changeSpreadsheetFocusState(state: boolean) { - this._spreadsheetFocusState = state; - } - - /** @deprecated */ - getSpreadsheetFocusState() { - return this._spreadsheetFocusState; - } - - /** @deprecated */ - focusStyle(editorUnitId: string) { - const editor = this.getEditor(editorUnitId); - if (!editor) { - return false; - } - - editor.setFocus(true); - - this._contextService.setContextValue(EDITOR_ACTIVATED, true); - - if (!isInternalEditorID(editorUnitId)) { - this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, true); - this._contextService.setContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, editor.isSingle()); - } - - if (!this._spreadsheetFocusState) { - this.singleSelection(!!editor.isSingleChoice()); - } - - this._focusStyle$.next(editorUnitId); - this.setFocusId(editorUnitId); - } - - /** @deprecated */ - singleSelection(state: boolean) { - this._singleSelection$.next(state); + const focusingEditor = this.getFocusEditor(); + focusingEditor?.blur(); + this._blur$.next(null); } - /** @deprecated */ - selectionChangingState() { - // const documentDataModel = this._univerInstanceService.getCurrentUniverDocInstance(); - const editorUnitId = this.getFocusId(); - if (editorUnitId == null) { - return true; - } - const editor = this.getEditor(editorUnitId); - - if (!editor || editor.isSheetEditor() || editor.isFormulaEditor()) { - return true; - } - - if (editor.onlyInputRange() !== true && editor.onlyInputFormula() !== true) { - this.blur(); - return true; - } - - if (editor.onlyInputFormula() === true && this._contextService.getContextValue(FOCUSING_EDITOR_INPUT_FORMULA) !== true) { + focus(editorUnitId: string) { + if (this._focusEditorUnitId) { this.blur(); - return true; } - - return !this.getSpreadsheetFocusState(); - } - - /** @deprecated */ - blur() { - if (!this._spreadsheetFocusState) { - this._closeRangePrompt$.next(null); - this.singleSelection(false); - this.setFocusId(null); - this._contextService.setContextValue(EDITOR_ACTIVATED, false); - this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, false); - } - - this.getAllEditor().forEach((editor) => { - editor.setFocus(false); - }); - - this._focusStyle$.next(); - - this._blur$.next(null); - } - - /** @deprecated */ - focus(editorUnitId: string | undefined = this._univerInstanceService.getCurrentUniverDocInstance()?.getUnitId()) { if (editorUnitId == null) { return; } @@ -412,10 +162,15 @@ export class EditorService extends Disposable implements IEditorService, IDispos } this._univerInstanceService.setCurrentUnitForType(editorUnitId); - const valueCount = editor.getValue().length; + this._contextService.setContextValue(EDITOR_ACTIVATED, true); + + if (!isInternalEditorID(editorUnitId)) { + this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, true); + } - this.focusStyle(editorUnitId); + editor.focus(); + this._setFocusId(editorUnitId); this._focus$.next({ startOffset: valueCount, @@ -423,55 +178,7 @@ export class EditorService extends Disposable implements IEditorService, IDispos }); } - /** @deprecated */ - setFormula(formulaString: string, editorUnitId = this._getCurrentEditorUnitId()) { - this._inputFormula$.next({ formulaString, editorUnitId }); - } - - /** @deprecated */ - setValue(val: string, editorUnitId: string = this._getCurrentEditorUnitId()) { - this.setValueNoRefresh(val, editorUnitId); - this._refreshValueChange(editorUnitId); - } - - /** @deprecated */ - setValueNoRefresh(val: string, editorUnitId: string) { - this._setValue$.next({ - body: { - dataStream: val, - }, - editorUnitId, - }); - - this.resize(editorUnitId); - } - - /** @deprecated */ - getValue(id: string) { - const editor = this.getEditor(id); - if (editor == null) { - return; - } - return editor.getValue(); - } - - /** @deprecated */ - setRichValue(body: IDocumentBody, editorUnitId: string = this._getCurrentEditorUnitId()) { - this._setValue$.next({ body, editorUnitId }); - this._refreshValueChange(editorUnitId); - } - - /** @deprecated */ - getRichValue(id: string) { - const editor = this.getEditor(id); - if (editor == null) { - return; - } - return editor.getBody(); - } - override dispose(): void { - this._state$.complete(); this._editors.clear(); super.dispose(); } @@ -480,53 +187,10 @@ export class EditorService extends Disposable implements IEditorService, IDispos return this._editors.get(id); } - /** @deprecated */ getAllEditor() { return this._editors; } - /** @deprecated */ - getFirstEditor() { - return [...this.getAllEditor().values()][0]; - } - - /** @deprecated */ - resize(unitId: string) { - const editor = this.getEditor(unitId); - if (editor == null) { - return; - } - - editor.verticalAlign(); - - this._resize$.next(unitId); - } - - /** @deprecated */ - isVisible(id: string) { - return this.getEditor(id)?.isVisible(); - } - - /** @deprecated */ - setOperationSheetUnitId(unitId: Nullable) { - this._currentSheetUnitId = unitId; - } - - /** @deprecated */ - getOperationSheetUnitId() { - return this._currentSheetUnitId; - } - - /** @deprecated */ - setOperationSheetSubUnitId(sheetId: Nullable) { - this._currentSheetSubUnitId = sheetId; - } - - /** @deprecated */ - getOperationSheetSubUnitId() { - return this._currentSheetSubUnitId; - } - register(config: IEditorConfigParams, container: HTMLDivElement): IDisposable { const { initialSnapshot, canvasStyle = {} } = config; const editorUnitId = initialSnapshot.id; @@ -564,12 +228,6 @@ export class EditorService extends Disposable implements IEditorService, IDispos if (!config.scrollBar) { (render.mainComponent?.getScene() as Scene)?.getViewports()?.[0].getScrollBar()?.dispose(); } - - // @ggg, Move this to Text Editor? - if (!editor.isSheetEditor() && !config.noNeedVerticalAlign) { - editor.verticalAlign(); - editor.updateCanvasStyle(); - } } return toDisposable(() => { this._unRegister(editorUnitId); @@ -586,76 +244,6 @@ export class EditorService extends Disposable implements IEditorService, IDispos editor.dispose(); this._editors.delete(editorUnitId); this._univerInstanceService.disposeUnit(editorUnitId); - this._contextService.setContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, false); - - // DEBT: no necessary when we refactor editor module - if (!this.isSheetEditor(editorUnitId)) return; - - /** - * Compatible with the editor in the sheet scenario, - * it is necessary to refocus back to the current sheet when unloading. - */ - // REFACTOR: @zw, move to sheet cell editor. - const sheets = this._univerInstanceService.getAllUnitsForType(UniverInstanceType.UNIVER_SHEET); - if (sheets.length > 0) { - const current = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); - - if (current) { - this._univerInstanceService.focusUnit(current.getUnitId()); - } - } - } - - /** @deprecated */ - refreshValueChange(editorUnitId: string) { - this._refreshValueChange(editorUnitId); - } - - /** @deprecated */ - checkValueLegality(editorUnitId: string) { - const editor = this._editors.get(editorUnitId); - - if (editor == null) { - return true; - } - - let value = editor.getValue(); - - editor.setValueLegality(); - - value = value.replace(/\r\n/g, '').replace(/\n/g, '').replace(/\n/g, ''); - - if (value.length === 0) { - return true; - } - - if (editor.onlyInputFormula()) { - if (value.substring(0, 1) !== operatorToken.EQUALS) { - editor.setValueLegality(false); - return false; - } - const bracketCount = this._lexerTreeBuilder.checkIfAddBracket(value); - editor.setValueLegality(bracketCount === 0); - } else if (editor.onlyInputRange()) { - const valueArray = value.split(','); - if (editor.isSingleChoice() && valueArray.length > 1) { - editor.setValueLegality(false); - return false; - } - - editor.setValueLegality(isReferenceStrings(value)); - } - - return editor.isValueLegality(); - } - - private _refreshValueChange(editorId: string) { - const editor = this.getEditor(editorId); - if (editor == null) { - return; - } - - this._valueChange$.next(editor); } private _getCurrentEditorUnitId() { diff --git a/packages/docs-ui/src/services/editor/editor.ts b/packages/docs-ui/src/services/editor/editor.ts index 5694bd35d68..5bb9f8d1b9e 100644 --- a/packages/docs-ui/src/services/editor/editor.ts +++ b/packages/docs-ui/src/services/editor/editor.ts @@ -17,7 +17,7 @@ import type { DocumentDataModel, ICommandService, IDocumentData, IDocumentStyle, IPosition, IUndoRedoService, IUniverInstanceService, Nullable } from '@univerjs/core'; import type { DocSelectionManagerService } from '@univerjs/docs'; import type { IDocSelectionInnerParam, IRender, ISuccinctDocRangeParam, ITextRangeWithStyle } from '@univerjs/engine-render'; -import { DEFAULT_STYLES, Disposable, UniverInstanceType } from '@univerjs/core'; +import { Disposable, isInternalEditorID, UniverInstanceType } from '@univerjs/core'; import { KeyCode } from '@univerjs/ui'; import { merge, type Observable, Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -99,47 +99,6 @@ export interface IEditorConfigParams { // show scrollBar scrollBar?: boolean; - - // need vertical align and update canvas style. TODO: remove this latter. - /** @deprecated */ - noNeedVerticalAlign?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - isSheetEditor?: boolean; - /** - * If the editor is for formula editing. - * @deprecated this is a temp fix before refactoring editor. - */ - isFormulaEditor?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - isSingle?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - onlyInputFormula?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - onlyInputRange?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - onlyInputContent?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - isSingleChoice?: boolean; - /** - * @deprecated The implementer makes its own judgment. - */ - openForSheetUnitId?: Nullable; - /** - * @deprecated The implementer makes its own judgment. - */ - openForSheetSubUnitId?: Nullable; } export interface IEditorOptions extends IEditorConfigParams, IEditorStateParams { @@ -173,12 +132,6 @@ export class Editor extends Disposable implements IEditor { private readonly _selectionChange$ = new Subject(); selectionChange$: Observable = this._selectionChange$.asObservable(); - private _valueLegality = true; - - private _openForSheetUnitId: Nullable; - - private _openForSheetSubUnitId: Nullable; - constructor( private _param: IEditorOptions, private _univerInstanceService: IUniverInstanceService, @@ -187,8 +140,6 @@ export class Editor extends Disposable implements IEditor { private _undoRedoService: IUndoRedoService ) { super(); - this._openForSheetUnitId = this._param.openForSheetUnitId; - this._openForSheetSubUnitId = this._param.openForSheetSubUnitId; this._listenSelection(); } @@ -281,17 +232,17 @@ export class Editor extends Disposable implements IEditor { docSelectionRenderService.focus(); // Step 3: Sets the selection of the last selection, and if not, to the beginning of the document. - const lastSelectionInfo = this._docSelectionManagerService.getDocRanges({ - unitId: editorUnitId, - subUnitId: editorUnitId, - }); - - if (lastSelectionInfo) { - this._docSelectionManagerService.replaceDocRanges(lastSelectionInfo, { - unitId: editorUnitId, - subUnitId: editorUnitId, - }, false); - } + // const lastSelectionInfo = this._docSelectionManagerService.getDocRanges({ + // unitId: editorUnitId, + // subUnitId: editorUnitId, + // }); + + // if (lastSelectionInfo) { + // this._docSelectionManagerService.replaceDocRanges(lastSelectionInfo, { + // unitId: editorUnitId, + // subUnitId: editorUnitId, + // }, false); + // } this._focus = true; } @@ -352,13 +303,40 @@ export class Editor extends Disposable implements IEditor { setDocumentData(data: IDocumentData, textRanges: Nullable) { const { id } = data; - this._commandService.executeCommand(ReplaceSnapshotCommand.id, { + this._commandService.syncExecuteCommand(ReplaceSnapshotCommand.id, { unitId: id, snapshot: data, textRanges, }); } + replaceText(text: string, resetCursor = true) { + const data = this.getDocumentData(); + + this.setDocumentData( + { + ...data, + body: { + dataStream: `${text}\r\n`, + paragraphs: [{ + startIndex: 0, + }], + customRanges: [], + sectionBreaks: [], + tables: [], + textRuns: [], + }, + }, + resetCursor + ? [{ + startOffset: text.length, + endOffset: text.length, + collapsed: true, + }] + : null + ); + } + // Clear the undo redo history of this editor. clearUndoRedoHistory(): void { const editorUnitId = this.getEditorId(); @@ -394,40 +372,6 @@ export class Editor extends Disposable implements IEditor { return this._param.render; } - isSingleChoice() { - return this._param.isSingleChoice ?? false; - } - - /** @deprecated */ - setOpenForSheetUnitId(unitId: Nullable) { - this._openForSheetUnitId = unitId; - } - - /** @deprecated */ - getOpenForSheetUnitId() { - return this._openForSheetUnitId; - } - - /** @deprecated */ - setOpenForSheetSubUnitId(subUnitId: Nullable) { - this._openForSheetSubUnitId = subUnitId; - } - - /** @deprecated */ - getOpenForSheetSubUnitId() { - return this._openForSheetSubUnitId; - } - - /** @deprecated */ - isValueLegality() { - return this._valueLegality === true; - } - - /** @deprecated */ - setValueLegality(state = true) { - this._valueLegality = state; - } - isFocus() { return this._focus; } @@ -437,46 +381,24 @@ export class Editor extends Disposable implements IEditor { this._focus = state; } - /** @deprecated */ - isSingle() { - return this._param.isSingle === true || this.onlyInputRange(); - } - isReadOnly() { return this._param.readonly === true; } - /** @deprecated */ - onlyInputContent() { - return this._param.onlyInputContent === true; - } - - /** @deprecated */ - onlyInputFormula() { - return this._param.onlyInputFormula === true; - } - - /** @deprecated */ - onlyInputRange() { - return this._param.onlyInputRange === true; - } - getBoundingClientRect() { return this._param.editorDom.getBoundingClientRect(); } + get editorDOM() { + return this._param.editorDom; + } + isVisible() { return this._param.visible; } - /** @deprecated */ isSheetEditor() { - return this._param.isSheetEditor === true; - } - - /** @deprecated */ - isFormulaEditor() { - return this._param.isFormulaEditor === true; + return isInternalEditorID(this._getEditorId()); } /** @@ -506,42 +428,6 @@ export class Editor extends Disposable implements IEditor { }; } - /** - * @deprecated. - */ - verticalAlign() { - const docDataModel = this._getDocDataModel(); - - if (docDataModel == null) { - return; - } - - const { width, height } = this._param.editorDom.getBoundingClientRect(); - - if (height === 0 || width === 0) { - return; - } - - if (!this.isSingle()) { - docDataModel.updateDocumentDataPageSize(width, undefined); - return; - } - - let fontSize = DEFAULT_STYLES.fs; - - if (this._param.canvasStyle?.fontSize) { - fontSize = this._param.canvasStyle.fontSize; - } - - const top = (height - (fontSize * 4 / 3)) / 2 - 2; - - docDataModel.updateDocumentDataMargin({ - t: top < 0 ? 0 : top, - }); - - docDataModel.updateDocumentDataPageSize(undefined, undefined); - } - /** * @deprecated. */ diff --git a/packages/docs-ui/src/services/selection/doc-selection-render.service.ts b/packages/docs-ui/src/services/selection/doc-selection-render.service.ts index a3351bf1f55..c2dcabac57a 100644 --- a/packages/docs-ui/src/services/selection/doc-selection-render.service.ts +++ b/packages/docs-ui/src/services/selection/doc-selection-render.service.ts @@ -21,8 +21,8 @@ import type { RectRange } from './rect-range'; import { DataStreamTreeTokenType, DOC_RANGE_TYPE, ILogService, Inject, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core'; import { DocSkeletonManagerService } from '@univerjs/docs'; import { CURSOR_TYPE, getSystemHighlightColor, GlyphType, NORMAL_TEXT_SELECTION_PLUGIN_STYLE, PageLayoutType, ScrollTimer, Vector2 } from '@univerjs/engine-render'; -import { ILayoutService } from '@univerjs/ui'; -import { BehaviorSubject, fromEvent, Subject, takeUntil } from 'rxjs'; +import { ILayoutService, KeyCode } from '@univerjs/ui'; +import { BehaviorSubject, filter, fromEvent, merge, Subject, takeUntil } from 'rxjs'; import { getCanvasOffsetByEngine, getParagraphInfoByGlyph, getRangeListFromCharIndex, getRangeListFromSelection, getRectRangeFromCharIndex, getTextRangeFromCharIndex, serializeRectRange, serializeTextRange } from './selection-utils'; import { TextRange } from './text-range'; @@ -55,6 +55,12 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo private readonly _onSelectionStart$ = new BehaviorSubject>(null); readonly onSelectionStart$ = this._onSelectionStart$.asObservable(); + readonly onChangeByEvent$ = merge( + this._onInput$, + this._onKeydown$.pipe(filter((e) => (e.event as KeyboardEvent).keyCode === KeyCode.BACKSPACE)), + this._onCompositionend$ + ); + private readonly _onPaste$ = new Subject(); readonly onPaste$ = this._onPaste$.asObservable(); @@ -99,10 +105,17 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo private _isIMEInputApply = false; private _scenePointerMoveSubs: Array = []; private _scenePointerUpSubs: Array = []; - private _editorFocusing = true; // When the user switches editors, whether to clear the doc ranges. private _reserveRanges = false; + get isFocusing() { + return this._input === document.activeElement; + } + + get canFocusing() { + return this.isFocusing || document.activeElement === document.body || document.activeElement === null; + } + constructor( private readonly _context: IRenderContext, @ILayoutService private readonly _layoutService: ILayoutService, @@ -305,12 +318,11 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo * @deprecated */ activate(x: number, y: number, force = false) { - const isFocusing = this._input === document.activeElement || document.activeElement === document.body || document.activeElement === null; this._container.style.left = `${x}px`; this._container.style.top = `${y}px`; this._container.style.zIndex = '1000'; - if (isFocusing || force) { + if (this.canFocusing || force) { this.focus(); } } @@ -320,9 +332,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo } focus(): void { - if (!this._editorFocusing) { - return; - } this._input.focus(); } @@ -330,22 +339,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo this._input.blur(); } - /** - * @deprecated - */ - focusEditor(): void { - this._editorFocusing = true; - this.focus(); - } - - /** - * @deprecated - */ - blurEditor(): void { - this._editorFocusing = false; - this.blur(); - } - // FIXME: for editor cell editor we don't need to blur the input element /** * @deprecated @@ -448,7 +441,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo // Handle pointer down. // eslint-disable-next-line max-lines-per-function, complexity __onPointDown(evt: IPointerEvent | IMouseEvent) { - this._editorFocusing = true; const { scene, mainComponent } = this._context; const skeleton = this._docSkeletonManagerService.getSkeleton(); @@ -700,6 +692,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo this._input.contentEditable = 'true'; this._input.classList.add('univer-editor'); + this._input.id = `__editor_${this._context.unitId}`; this._input.style.cssText = ` position: absolute; overflow: hidden; diff --git a/packages/docs-ui/src/shortcuts/utils.ts b/packages/docs-ui/src/shortcuts/utils.ts index d69fd6499d1..2e3e52cc49f 100644 --- a/packages/docs-ui/src/shortcuts/utils.ts +++ b/packages/docs-ui/src/shortcuts/utils.ts @@ -15,7 +15,7 @@ */ import type { IContextService } from '@univerjs/core'; -import { FOCUSING_COMMON_DRAWINGS, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR, FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE } from '@univerjs/core'; +import { FOCUSING_COMMON_DRAWINGS, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR } from '@univerjs/core'; export function whenDocAndEditorFocused(contextService: IContextService): boolean { return contextService.getContextValue(FOCUSING_DOC) @@ -26,6 +26,6 @@ export function whenDocAndEditorFocused(contextService: IContextService): boolea export function whenDocAndEditorFocusedWithBreakLine(contextService: IContextService): boolean { return contextService.getContextValue(FOCUSING_DOC) && contextService.getContextValue(FOCUSING_UNIVER_EDITOR) - && !contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE) + // && !contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE) && !contextService.getContextValue(FOCUSING_COMMON_DRAWINGS); } diff --git a/packages/docs-ui/src/views/rich-text-editor/hooks/index.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/index.ts new file mode 100644 index 00000000000..5e0a9a86e6b --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { type IKeyboardEventConfig, useKeyboardEvent } from './useKeyboardEvent'; +export { useResize } from './useResize'; diff --git a/packages/docs-ui/src/views/rich-text-editor/hooks/useEditor.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/useEditor.ts new file mode 100644 index 00000000000..c5fdf75a3e3 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useEditor.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { IDocumentData, Nullable } from '@univerjs/core'; +import type { RefObject } from 'react'; +import type { Editor } from '../../../services/editor/editor'; +import { useDependency } from '@univerjs/core'; +import { useLayoutEffect, useMemo, useState } from 'react'; +import { IEditorService } from '../../../services/editor/editor-manager.service'; + +export interface IUseEditorProps { + editorId: string; + initialValue: Nullable; + container: RefObject; + autoFocus?: boolean; + isSingle?: boolean; +} + +export function useEditor(opts: IUseEditorProps) { + const { editorId, initialValue, container, autoFocus: _autoFocus, isSingle } = opts; + const autoFocus = useMemo(() => _autoFocus ?? false, []); + const [editor, setEditor] = useState(); + const editorService = useDependency(IEditorService); + + useLayoutEffect(() => { + if (container.current) { + const snapshot: IDocumentData = { + body: { + dataStream: '\r\n', + textRuns: [], + customBlocks: [], + customDecorations: [], + customRanges: [], + paragraphs: [{ + startIndex: 0, + }], + }, + ...initialValue, + documentStyle: { + ...initialValue?.documentStyle, + pageSize: { + width: !isSingle ? container.current.clientWidth : Infinity, + height: Infinity, + }, + }, + id: editorId, + }; + const dispose = editorService.register( + { + autofocus: true, + editorUnitId: editorId, + initialSnapshot: snapshot, + }, + container.current + ); + const editor = editorService.getEditor(editorId)! as Editor; + setEditor(editor); + + if (autoFocus) { + editor.focus(); + const end = (snapshot.body?.dataStream.length ?? 2) - 2; + editor.setSelectionRanges([{ startOffset: end, endOffset: end }]); + } + + return () => { + dispose?.dispose(); + }; + } + }, []); + + return editor; +} diff --git a/packages/docs-ui/src/views/rich-text-editor/hooks/useKeyboardEvent.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/useKeyboardEvent.ts new file mode 100644 index 00000000000..7f31211a9df --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useKeyboardEvent.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { KeyCode, MetaKeys } from '@univerjs/ui'; +import type { Editor } from '../../../services/editor/editor'; +import { CommandType, DisposableCollection, generateRandomId, ICommandService, useDependency } from '@univerjs/core'; +import { DeviceInputEventType } from '@univerjs/engine-render'; +import { IShortcutService } from '@univerjs/ui'; +import { useEffect, useMemo } from 'react'; + +export interface IKeyboardEventConfig { + keyCodes: { keyCode: KeyCode; metaKey?: MetaKeys }[]; + handler: (keyCode: KeyCode, metaKey?: MetaKeys) => void; +} + +export function useKeyboardEvent(isNeed: boolean, config?: IKeyboardEventConfig, editor?: Editor) { + const commandService = useDependency(ICommandService); + const shortcutService = useDependency(IShortcutService); + const key = useMemo(() => generateRandomId(4), []); + + useEffect(() => { + if (!editor || !isNeed || !config) { + return; + } + const editorId = editor.getEditorId(); + const operationId = `sheet.operation.editor-${editorId}-keyboard-${key}`; + const d = new DisposableCollection(); + + d.add(commandService.registerCommand({ + id: operationId, + type: CommandType.OPERATION, + handler(_event, params) { + const { keyCode, metaKey } = params as { eventType: DeviceInputEventType; keyCode: KeyCode; metaKey?: MetaKeys }; + config.handler(keyCode, metaKey); + }, + })); + + config.keyCodes.map((keyCode) => { + return { + id: operationId, + binding: keyCode.metaKey ? keyCode.keyCode | keyCode.metaKey : keyCode.keyCode, + preconditions: () => true, + priority: 901, + staticParameters: { + eventType: DeviceInputEventType.Keyboard, + keyCode: keyCode.keyCode, + metaKey: keyCode.metaKey, + }, + }; + }).forEach((item) => { + d.add(shortcutService.registerShortcut(item)); + }); + + return () => { + d.dispose(); + }; + }, [commandService, config, editor, isNeed, key, shortcutService]); +} diff --git a/packages/docs-ui/src/views/rich-text-editor/hooks/useLeftAndRightArrow.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/useLeftAndRightArrow.ts new file mode 100644 index 00000000000..b8e8a380340 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useLeftAndRightArrow.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Editor } from '../../../services/editor/editor'; +import { CommandType, Direction, DisposableCollection, ICommandService, useDependency } from '@univerjs/core'; +import { DeviceInputEventType } from '@univerjs/engine-render'; +import { IShortcutService, KeyCode, MetaKeys } from '@univerjs/ui'; +import { useEffect, useRef } from 'react'; +import { MoveCursorOperation, MoveSelectionOperation } from '../../../commands/operations/doc-cursor.operation'; + +// eslint-disable-next-line max-lines-per-function +export const useLeftAndRightArrow = (isNeed: boolean, selectingMode: boolean, editor?: Editor, onMoveInEditor?: (keyCode: KeyCode, metaKey?: MetaKeys) => void) => { + const commandService = useDependency(ICommandService); + const shortcutService = useDependency(IShortcutService); + const selectingModeRef = useRef(selectingMode); + selectingModeRef.current = selectingMode; + const onMoveInEditorRef = useRef(onMoveInEditor); + onMoveInEditorRef.current = onMoveInEditor; + + useEffect(() => { + if (!editor || !isNeed) { + return; + } + const editorId = editor.getEditorId(); + const operationId = `sheet.formula-embedding-editor.${editorId}`; + const d = new DisposableCollection(); + const handleMoveInEditor = (keycode: KeyCode, metaKey?: MetaKeys) => { + if (onMoveInEditorRef.current) { + onMoveInEditorRef.current(keycode, metaKey); + return; + } + + let direction = Direction.LEFT; + if (keycode === KeyCode.ARROW_DOWN) { + direction = Direction.DOWN; + } else if (keycode === KeyCode.ARROW_UP) { + direction = Direction.UP; + } else if (keycode === KeyCode.ARROW_RIGHT) { + direction = Direction.RIGHT; + } + + if (metaKey === MetaKeys.SHIFT) { + commandService.executeCommand(MoveSelectionOperation.id, { + direction, + }); + } else { + commandService.executeCommand(MoveCursorOperation.id, { + direction, + }); + } + }; + + d.add(commandService.registerCommand({ + id: operationId, + type: CommandType.OPERATION, + handler(_event, params) { + const { keyCode } = params as { eventType: DeviceInputEventType; keyCode: KeyCode }; + handleMoveInEditor(keyCode); + }, + })); + + const keyCodes = [ + { keyCode: KeyCode.ARROW_DOWN }, + { keyCode: KeyCode.ARROW_LEFT }, + { keyCode: KeyCode.ARROW_RIGHT }, + { keyCode: KeyCode.ARROW_UP }, + { keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + ]; + + keyCodes.map(({ keyCode, metaKey }) => { + return { + id: operationId, + binding: metaKey ? keyCode | metaKey : keyCode, + preconditions: () => true, + priority: 900, + staticParameters: { + eventType: DeviceInputEventType.Keyboard, + keyCode, + }, + }; + }).forEach((item) => { + d.add(shortcutService.registerShortcut(item)); + }); + + return () => { + d.dispose(); + }; + }, [commandService, editor, isNeed, shortcutService]); +}; diff --git a/packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts new file mode 100644 index 00000000000..b6e45b794e8 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts @@ -0,0 +1,131 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Nullable } from '@univerjs/core'; +import type { Editor } from '../../../services/editor/editor'; +import { debounce } from '@univerjs/core'; +import { DocSkeletonManagerService } from '@univerjs/docs'; +import { ScrollBar } from '@univerjs/engine-render'; +import { useCallback, useEffect, useMemo } from 'react'; +import { VIEWPORT_KEY } from '../../../basics/docs-view-key'; + +// eslint-disable-next-line max-lines-per-function +export const useResize = (editor?: Editor, isSingle = true, autoScrollbar?: boolean) => { + const resize = useCallback(() => { + if (editor) { + const { scene, mainComponent } = editor.render; + const docSkeletonManagerService = editor.render.with(DocSkeletonManagerService); + const { width, height } = editor.getBoundingClientRect(); + + docSkeletonManagerService.getViewModel().getDataModel().updateDocumentDataPageSize(isSingle ? Infinity : width, Infinity); + scene.transformByState({ + width, + height, + }); + + mainComponent?.resize(width, height); + } + }, [editor, isSingle]); + + const checkScrollBar = useMemo(() => { + return debounce(() => { + if (!editor || !autoScrollbar) { + return; + } + + const docSkeletonManagerService = editor.render.with(DocSkeletonManagerService); + const skeleton = docSkeletonManagerService.getSkeleton(); + const { scene, mainComponent } = editor.render; + const viewportMain = scene.getViewport(VIEWPORT_KEY.VIEW_MAIN); + const { actualWidth, actualHeight } = skeleton.getActualSize(); + const { width, height } = editor.getBoundingClientRect(); + let scrollBar = viewportMain?.getScrollBar() as Nullable; + const contentWidth = Math.max(actualWidth, width); + const contentHeight = Math.max(actualHeight, height); + + scene.transformByState({ + width: contentWidth, + height: contentHeight, + }); + + mainComponent?.resize(contentWidth, contentHeight); + if (!isSingle) { + if (actualHeight > height) { + if (scrollBar == null) { + if (viewportMain) { + scrollBar = new ScrollBar(viewportMain, { + enableHorizontal: false, + enableVertical: true, + barSize: 8, + minThumbSizeV: 8, + }); + } + } else { + viewportMain?.resetCanvasSizeAndUpdateScroll(); + } + } else { + scrollBar = null; + viewportMain?.scrollToBarPos({ x: 0, y: 0 }); + viewportMain?.getScrollBar()?.dispose(); + } + } else { + if (actualWidth > width) { + if (scrollBar == null) { + viewportMain && new ScrollBar(viewportMain, { + barSize: 8, + enableVertical: false, + enableHorizontal: true, + minThumbSizeV: 8, + }); + } else { + viewportMain?.resetCanvasSizeAndUpdateScroll(); + } + } else { + scrollBar = null; + viewportMain?.scrollToBarPos({ x: 0, y: 0 }); + viewportMain?.getScrollBar()?.dispose(); + } + } + }, 30); + }, [editor, autoScrollbar, isSingle]); + + useEffect(() => { + if (!autoScrollbar) return; + if (editor) { + const time = setTimeout(() => { + resize(); + checkScrollBar(); + }, 500); + return () => { + clearTimeout(time); + }; + } + }, [editor, autoScrollbar, resize, checkScrollBar]); + + useEffect(() => { + if (!autoScrollbar) return; + if (editor) { + const d = editor.input$.subscribe(() => { + checkScrollBar(); + }); + return () => { + d.unsubscribe(); + }; + } + }, [editor, autoScrollbar, checkScrollBar]); + + return { resize, checkScrollBar }; +}; diff --git a/packages/docs-ui/src/views/rich-text-editor/index.module.less b/packages/docs-ui/src/views/rich-text-editor/index.module.less new file mode 100644 index 00000000000..063fb869af5 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/index.module.less @@ -0,0 +1,41 @@ +.rich-text-editor { + &-active { + border-color: rgb(var(--hyacinth-500)) !important; + } + + &-wrap { + height: 32px; + padding: 6px 8px 2px 6px; + width: 100%; + display: flex; + justify-content: space-around; + align-items: center; + gap: 8px; + border: 1px solid rgb(var(--border-color)); + border-radius: var(--border-radius-base); + box-sizing: border-box; + position: relative; + + .rich-text-editor-text { + width: 100%; + height: 100%; + position: relative; + } + + .rich-text-editor-error-wrap { + font-size: 12px; + color: rgb(var(--red-500)); + position: absolute; + bottom: -18px; + left: 0px; + } + } + + &-placeholder { + font-size: 14px; + color: rgb(var(--grey-500)); + position: absolute; + left: 5px; + top: 5px; + } +} diff --git a/packages/docs-ui/src/views/rich-text-editor/index.tsx b/packages/docs-ui/src/views/rich-text-editor/index.tsx new file mode 100644 index 00000000000..dd3d9259c6f --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/index.tsx @@ -0,0 +1,136 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Editor } from '../../services/editor/editor'; +import type { IKeyboardEventConfig } from './hooks'; +import { BuildTextUtils, createInternalEditorID, generateRandomId, type IDocumentData, useDependency, useObservable } from '@univerjs/core'; +import { IRenderManagerService } from '@univerjs/engine-render'; +import { useEvent } from '@univerjs/ui'; +import clsx from 'clsx'; +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import { IEditorService } from '../../services/editor/editor-manager.service'; +import { DocSelectionRenderService } from '../../services/selection/doc-selection-render.service'; +import { useKeyboardEvent, useResize } from './hooks'; +import { useEditor } from './hooks/useEditor'; +import { useLeftAndRightArrow } from './hooks/useLeftAndRightArrow'; +import styles from './index.module.less'; + +export interface IRichTextEditorProps { + className?: string; + autoFocus?: boolean; + onFocusChange?: (isFocus: boolean) => void; + initialValue?: IDocumentData; + onClickOutside?: () => void; + keyboardEventConfig?: IKeyboardEventConfig; + moveCursor?: boolean; + style?: React.CSSProperties; + isSingle?: boolean; + placeholder?: string; + editorId?: string; +} + +export const RichTextEditor = forwardRef((props, ref) => { + const { + className, + autoFocus, + onFocusChange: _onFocusChange, + initialValue, + onClickOutside: _onClickOutside, + keyboardEventConfig, + moveCursor = true, + style, + isSingle, + editorId: propsEditorId, + } = props; + const editorService = useDependency(IEditorService); + const onFocusChange = useEvent(_onFocusChange); + const onClickOutside = useEvent(_onClickOutside); + const formulaEditorContainerRef = React.useRef(null); + const editorId = useMemo(() => propsEditorId ?? createInternalEditorID(`RICH_TEXT_EDITOR-${generateRandomId(4)}`), [propsEditorId]); + const editor = useEditor({ + editorId, + initialValue, + container: formulaEditorContainerRef, + autoFocus, + isSingle, + }); + const renderManagerService = useDependency(IRenderManagerService); + const renderer = renderManagerService.getRenderById(editorId); + const docSelectionRenderService = renderer?.with(DocSelectionRenderService); + const isFocusing = docSelectionRenderService?.isFocusing ?? false; + const sheetEmbeddingRef = React.useRef(null); + const [showPlaceholder, setShowPlaceholder] = useState(() => !BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); + + useEffect(() => { + setShowPlaceholder(!BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); + + const sub = editor?.selectionChange$.subscribe(() => { + setShowPlaceholder(!BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); + }); + + return () => sub?.unsubscribe(); + }, [editor]); + useObservable(editor?.blur$); + useObservable(editor?.focus$); + useResize(editor, isSingle, true); + useEffect(() => { + onFocusChange?.(isFocusing); + }, [isFocusing, onFocusChange]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (editorService.getFocusId() !== editorId) return; + if (sheetEmbeddingRef.current && !sheetEmbeddingRef.current.contains(event.target as any)) { + onClickOutside?.(); + } + }; + + setTimeout(() => { + document.addEventListener('click', handleClickOutside); + }, 100); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [editor, editorId, editorService, onClickOutside]); + + useLeftAndRightArrow(isFocusing && moveCursor, false, editor); + useKeyboardEvent(isFocusing, keyboardEventConfig, editor); + useImperativeHandle(ref, () => editor!, [editor]); + + return ( +
+
+
editor?.focus()} + /> + {!showPlaceholder + ? null + : ( +
+ {props.placeholder} +
+ )} +
+
+ ); +}); diff --git a/packages/docs/src/commands/mutations/core-editing.mutation.ts b/packages/docs/src/commands/mutations/core-editing.mutation.ts index afd42631d57..fec9393e426 100644 --- a/packages/docs/src/commands/mutations/core-editing.mutation.ts +++ b/packages/docs/src/commands/mutations/core-editing.mutation.ts @@ -101,7 +101,7 @@ export const RichTextEditingMutation: IMutation { docSelectionManagerService.replaceDocRanges(textRanges, { unitId, subUnitId: unitId }, isEditing, params.options); }); diff --git a/packages/engine-render/src/components/docs/document.ts b/packages/engine-render/src/components/docs/document.ts index 0d0484bda80..6f4e756956e 100644 --- a/packages/engine-render/src/components/docs/document.ts +++ b/packages/engine-render/src/components/docs/document.ts @@ -177,6 +177,7 @@ export class Documents extends DocComponent { pagePaddingBottom, verticalAlign ); + const alignOffsetNoAngle = Vector2.create(horizontalOffsetNoAngle, verticalOffsetNoAngle); const centerAngle = degToRad(centerAngleDeg); const vertexAngle = degToRad(vertexAngleDeg); diff --git a/packages/engine-render/src/shape/base-scroll-bar.ts b/packages/engine-render/src/shape/base-scroll-bar.ts index 284dcf19f4c..932f058a567 100644 --- a/packages/engine-render/src/shape/base-scroll-bar.ts +++ b/packages/engine-render/src/shape/base-scroll-bar.ts @@ -27,6 +27,9 @@ export interface IScrollBarProps { thumbBackgroundColor?: string; thumbHoverBackgroundColor?: string; thumbActiveBackgroundColor?: string; + /** + * The thickness of a scrolling bar. + */ barSize?: number; barBackgroundColor?: string; barBorder?: number; @@ -36,6 +39,9 @@ export interface IScrollBarProps { enableVertical?: boolean; mainScene?: Scene; + + minThumbSizeH?: number; + minThumbSizeV?: number; } export abstract class BaseScrollBar extends Disposable { diff --git a/packages/engine-render/src/shape/scroll-bar.ts b/packages/engine-render/src/shape/scroll-bar.ts index 7fb39c78032..f4e330766e0 100644 --- a/packages/engine-render/src/shape/scroll-bar.ts +++ b/packages/engine-render/src/shape/scroll-bar.ts @@ -27,7 +27,7 @@ import { Transform } from '../basics/transform'; import { BaseScrollBar } from './base-scroll-bar'; import { Rect } from './rect'; -const MINI_THUMB_SIZE = 17; +const MIN_THUMB_SIZE = 17; export class ScrollBar extends BaseScrollBar { protected _viewport!: Viewport; @@ -50,6 +50,9 @@ export class ScrollBar extends BaseScrollBar { private _verticalPointerUpSub: Nullable; + /** + * The thickness of a scrolling bar. + */ barSize = 14; barBorder = 1; @@ -71,6 +74,15 @@ export class ScrollBar extends BaseScrollBar { barBorderColor = 'rgba(255,255,255,0.7)'; + /** + * The min width of horizon thumb. + */ + minThumbSizeH = MIN_THUMB_SIZE; + /** + * The min height of vertical thumb. + */ + minThumbSizeV = MIN_THUMB_SIZE; + private _eventSub = new Subscription(); constructor(view: Viewport, props?: IScrollBarProps) { @@ -213,9 +225,9 @@ export class ScrollBar extends BaseScrollBar { this.thumbLengthRatio; // this._horizontalThumbWidth = this._horizontalThumbWidth < MINI_THUMB_SIZE ? MINI_THUMB_SIZE : this._horizontalThumbWidth; - if (this.horizontalThumbWidth < MINI_THUMB_SIZE) { - this.horizontalMinusMiniThumb = MINI_THUMB_SIZE - this.horizontalThumbWidth; - this.horizontalThumbWidth = MINI_THUMB_SIZE; + if (this.horizontalThumbWidth < this.minThumbSizeH) { + this.horizontalMinusMiniThumb = this.minThumbSizeH - this.horizontalThumbWidth; + this.horizontalThumbWidth = this.minThumbSizeH; } this.horizonScrollTrack?.transformByState({ @@ -226,6 +238,7 @@ export class ScrollBar extends BaseScrollBar { }); if (this.horizontalThumbWidth >= parentWidth - this.barSize) { + // why hide the thumb rect ? this.horizonThumbRect?.setProps({ visible: false, }); @@ -255,9 +268,9 @@ export class ScrollBar extends BaseScrollBar { this.verticalThumbHeight = ((this.verticalBarHeight * this.verticalBarHeight) / contentHeight) * this.thumbLengthRatio; // this._verticalThumbHeight = this._verticalThumbHeight < MINI_THUMB_SIZE ? MINI_THUMB_SIZE : this._verticalThumbHeight; - if (this.verticalThumbHeight < MINI_THUMB_SIZE) { - this.verticalMinusMiniThumb = MINI_THUMB_SIZE - this.verticalThumbHeight; - this.verticalThumbHeight = MINI_THUMB_SIZE; + if (this.verticalThumbHeight < this.minThumbSizeV) { + this.verticalMinusMiniThumb = this.minThumbSizeV - this.verticalThumbHeight; + this.verticalThumbHeight = this.minThumbSizeV; } this.verticalScrollTrack?.transformByState({ @@ -268,6 +281,7 @@ export class ScrollBar extends BaseScrollBar { }); if (this.verticalThumbHeight >= parentHeight - this.barSize) { + // why hide the thumb rect ? this.verticalThumbRect?.setProps({ visible: false, }); diff --git a/packages/sheets-data-validation-ui/src/views/components/detail/index.tsx b/packages/sheets-data-validation-ui/src/views/components/detail/index.tsx index 09821894160..b775a97e66b 100644 --- a/packages/sheets-data-validation-ui/src/views/components/detail/index.tsx +++ b/packages/sheets-data-validation-ui/src/views/components/detail/index.tsx @@ -127,7 +127,6 @@ export function DataValidationDetail() { ...unitRange, sheetId: '', }; }); - if (isUnitRangesEqual(unitRanges, localRanges)) { return; } diff --git a/packages/sheets-data-validation-ui/src/views/components/formula-input/custom-formula-input.tsx b/packages/sheets-data-validation-ui/src/views/components/formula-input/custom-formula-input.tsx index 3f50e0e7279..35475afca8d 100644 --- a/packages/sheets-data-validation-ui/src/views/components/formula-input/custom-formula-input.tsx +++ b/packages/sheets-data-validation-ui/src/views/components/formula-input/custom-formula-input.tsx @@ -29,9 +29,10 @@ export function CustomFormulaInput(props: IFormulaInputProps) { const handleOutClick = formulaEditorActionsRef.current?.handleOutClick; handleOutClick && handleOutClick(e, () => isFocusFormulaEditorSet(false)); }); + return ( { dispose.dispose(); }; - }, [commandService, instanceService]); + }, [commandService, editorBridgeService, instanceService]); if (!worksheet) { return null; diff --git a/packages/sheets-formula-ui/src/commands/operations/__tests__/create-command-test-bed.ts b/packages/sheets-formula-ui/src/commands/operations/__tests__/create-command-test-bed.ts index ed786513519..90a3d0e3c3e 100644 --- a/packages/sheets-formula-ui/src/commands/operations/__tests__/create-command-test-bed.ts +++ b/packages/sheets-formula-ui/src/commands/operations/__tests__/create-command-test-bed.ts @@ -20,7 +20,7 @@ import { DocSelectionManagerService } from '@univerjs/docs'; import { EditorService, IEditorService } from '@univerjs/docs-ui'; import { LexerTreeBuilder } from '@univerjs/engine-formula'; import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render'; -import { RangeProtectionRuleModel, SheetInterceptorService, SheetsSelectionsService, WorkbookPermissionService, WorksheetPermissionService, WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '@univerjs/sheets'; +import { IRefSelectionsService, RangeProtectionRuleModel, RefSelectionsService, SheetInterceptorService, SheetsSelectionsService, WorkbookPermissionService, WorksheetPermissionService, WorksheetProtectionPointModel, WorksheetProtectionRuleModel } from '@univerjs/sheets'; import { EditorBridgeService, IEditorBridgeService, ISheetSelectionRenderService, SheetSelectionRenderService, SheetSkeletonManagerService } from '@univerjs/sheets-ui'; import { FormulaPromptService, IFormulaPromptService } from '../../../services/prompt.service'; @@ -82,6 +82,7 @@ export function createCommandTestBed(workbookData?: IWorkbookData, dependencies? injector.add([RangeProtectionRuleModel]); injector.add([IAuthzIoService, { useClass: AuthzIoLocalService }]); injector.add([WorksheetProtectionRuleModel]); + injector.add([IRefSelectionsService, { useClass: RefSelectionsService }]); dependencies?.forEach((d) => injector.add(d)); diff --git a/packages/sheets-formula-ui/src/commands/operations/insert-function.operation.ts b/packages/sheets-formula-ui/src/commands/operations/insert-function.operation.ts index 482727cfd6b..7c0b9a5347c 100644 --- a/packages/sheets-formula-ui/src/commands/operations/insert-function.operation.ts +++ b/packages/sheets-formula-ui/src/commands/operations/insert-function.operation.ts @@ -19,6 +19,8 @@ import { CellValueType, CommandType, DEFAULT_EMPTY_DOCUMENT_VALUE, + DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, + DOCS_NORMAL_EDITOR_UNIT_ID_KEY, getCellValueType, ICommandService, isRealNum, @@ -27,6 +29,7 @@ import { } from '@univerjs/core'; import { IEditorService } from '@univerjs/docs-ui'; import { serializeRange } from '@univerjs/engine-formula'; +import { DeviceInputEventType } from '@univerjs/engine-render'; import { getCellAtRowCol, @@ -35,6 +38,7 @@ import { SheetsSelectionsService, } from '@univerjs/sheets'; import { type IInsertFunction, InsertFunctionCommand } from '@univerjs/sheets-formula'; +import { IEditorBridgeService } from '@univerjs/sheets-ui'; export interface IInsertFunctionOperationParams { /** @@ -46,6 +50,7 @@ export interface IInsertFunctionOperationParams { export const InsertFunctionOperation: ICommand = { id: 'formula-ui.operation.insert-function', type: CommandType.OPERATION, + // eslint-disable-next-line max-lines-per-function handler: async (accessor: IAccessor, params: IInsertFunctionOperationParams) => { const selectionManagerService = accessor.get(SheetsSelectionsService); const editorService = accessor.get(IEditorService); @@ -62,6 +67,7 @@ export const InsertFunctionOperation: ICommand = { const { value } = params; const commandService = accessor.get(ICommandService); + const editorBridgeService = accessor.get(IEditorBridgeService); // No match refRange situation, enter edit mode // In each range, first take the judgment result of the primary position (if there is no primary, take the upper left corner), @@ -148,12 +154,16 @@ export const InsertFunctionOperation: ICommand = { selections: [resultRange], }; await commandService.executeCommand(SetSelectionsOperation.id, setSelectionParams); - - // TODO@DR-Univer: Maybe setTimeout can be removed - setTimeout(() => { - // edit cell - editorService.setFormula(`=${value}(${editFormulaRangeString}`); - }, 0); + const editor = editorService.getEditor(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); + const formulaEditor = editorService.getEditor(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY); + editorBridgeService.changeVisible({ + visible: true, + unitId, + eventType: DeviceInputEventType.Dblclick, + }); + const formulaText = `=${value}(${editFormulaRangeString}`; + editor?.replaceText(formulaText); + formulaEditor?.replaceText(formulaText, false); } if (list.length === 0) return false; diff --git a/packages/sheets-formula-ui/src/controllers/prompt.controller.ts b/packages/sheets-formula-ui/src/controllers/prompt.controller.ts deleted file mode 100644 index d7cfbdb67ca..00000000000 --- a/packages/sheets-formula-ui/src/controllers/prompt.controller.ts +++ /dev/null @@ -1,2033 +0,0 @@ -/** - * Copyright 2023-present DreamNum Inc. - * - * 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. - */ - -// FIXME: why so many calling to close the editor here? - -import type { - DocumentDataModel, - ICommandInfo, - IDisposable, - IRange, - IRangeWithCoord, - ITextRun, - Nullable, - Workbook, -} from '@univerjs/core'; -import type { Editor } from '@univerjs/docs-ui'; -import type { IAbsoluteRefTypeForRange, ISequenceNode } from '@univerjs/engine-formula'; -import type { - ISelectionWithStyle, -} from '@univerjs/sheets'; -import type { EditorBridgeService, SelectionControl } from '@univerjs/sheets-ui'; -import type { ISelectEditorFormulaOperationParam } from '../commands/operations/editor-formula.operation'; -import { - AbsoluteRefType, - Direction, - Disposable, - DisposableCollection, - DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, - DOCS_NORMAL_EDITOR_UNIT_ID_KEY, - DOCS_ZEN_EDITOR_UNIT_ID_KEY, - FOCUSING_EDITOR_INPUT_FORMULA, - FORMULA_EDITOR_ACTIVATED, - ICommandService, - IContextService, - Inject, - isFormulaString, - IUniverInstanceService, - RANGE_TYPE, - Rectangle, - ThemeService, - Tools, - UniverInstanceType, -} from '@univerjs/core'; -import { - DocSelectionManagerService, - DocSkeletonManagerService, -} from '@univerjs/docs'; -import { DocSelectionRenderService, IEditorService, MoveCursorOperation, ReplaceContentCommand } from '@univerjs/docs-ui'; -import { - compareToken, - deserializeRangeWithSheet, - generateStringWithSequence, - getAbsoluteRefTypeWitString, - LexerTreeBuilder, - matchRefDrawToken, - matchToken, - normalizeSheetName, - sequenceNodeType, - serializeRange, - serializeRangeToRefString, -} from '@univerjs/engine-formula'; -import { - DeviceInputEventType, - IRenderManagerService, -} from '@univerjs/engine-render'; -import { - convertSelectionDataToRange, - getPrimaryForRange, - IRefSelectionsService, - REF_SELECTIONS_ENABLED, - SelectionMoveType, - setEndForRange, SheetsSelectionsService } from '@univerjs/sheets'; -import { IDescriptionService } from '@univerjs/sheets-formula'; - -import { - ExpandSelectionCommand, - getEditorObject, - IEditorBridgeService, - isEmbeddingFormulaEditor, - isRangeSelector, - JumpOver, - MoveSelectionCommand, - SheetCellEditorResizeService, - SheetSkeletonManagerService, -} from '@univerjs/sheets-ui'; -import { IContextMenuService, ILayoutService, KeyCode, MetaKeys, UNI_DISABLE_CHANGING_FOCUS_KEY } from '@univerjs/ui'; -import { distinctUntilChanged, distinctUntilKeyChanged, filter, merge } from 'rxjs'; -import { SelectEditorFormulaOperation } from '../commands/operations/editor-formula.operation'; -import { HelpFunctionOperation } from '../commands/operations/help-function.operation'; -import { ReferenceAbsoluteOperation } from '../commands/operations/reference-absolute.operation'; -import { SearchFunctionOperation } from '../commands/operations/search-function.operation'; -import { META_KEY_CTRL_AND_SHIFT } from '../common/prompt'; -import { genFormulaRefSelectionStyle } from '../common/selection'; -import { IFormulaPromptService } from '../services/prompt.service'; -import { RefSelectionsRenderService } from '../services/render-services/ref-selections.render-service'; - -interface IRefSelection { - refIndex: number; - themeColor: string; - token: string; -} - -enum ArrowMoveAction { - InitialState, - moveCursor, - moveRefReady, - movingRef, - exitInput, -} - -enum InputPanelState { - InitialState, - keyNormal, - keyArrow, - mouse, -} - -const sheetEditorUnitIds = [DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY]; - -export class PromptController extends Disposable { - private _listenInputCache: Set = new Set(); - private _formulaRefColors: string[] = []; - - private _previousSequenceNodes: Nullable>; - - private _previousRangesCount: number = 0; - - private _previousInsertRefStringIndex: Nullable; - private _currentInsertRefStringIndex: number = -1; - - private _arrowMoveActionState: ArrowMoveAction = ArrowMoveAction.InitialState; - - private _isSelectionMovingRefSelections: IRefSelection[] = []; - - private _stringColor = ''; - - private _numberColor = ''; - - private _insertSelections: ISelectionWithStyle[] = []; - - private _inputPanelState: InputPanelState = InputPanelState.InitialState; - - private _userCursorMove: boolean = false; - - private _previousEditorUnitId: Nullable; - - private _existsSequenceNode = false; - - // TODO@wzhudev: selection render service would be a render unit, we we cannot - // easily access it here. - private get _selectionRenderService(): RefSelectionsRenderService { - return this._renderManagerService.getRenderById( - this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!.getUnitId() - )!.with(RefSelectionsRenderService); - } - - /** - * For multiple sheet instances. - */ - private get _allSelectionRenderServices(): RefSelectionsRenderService[] { - return this._renderManagerService.getAllRenderersOfType(UniverInstanceType.UNIVER_SHEET) - .map((renderer) => renderer.with(RefSelectionsRenderService)); - } - - constructor( - @ICommandService private readonly _commandService: ICommandService, - @IContextService private readonly _contextService: IContextService, - @Inject(IEditorBridgeService) private readonly _editorBridgeService: EditorBridgeService, - @Inject(IFormulaPromptService) private readonly _formulaPromptService: IFormulaPromptService, - @Inject(LexerTreeBuilder) private readonly _lexerTreeBuilder: LexerTreeBuilder, - @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, - @Inject(ThemeService) private readonly _themeService: ThemeService, - @Inject(SheetsSelectionsService) private readonly _sheetsSelectionsService: SheetsSelectionsService, - @IRefSelectionsService private readonly _refSelectionsService: SheetsSelectionsService, - @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, - @Inject(IDescriptionService) private readonly _descriptionService: IDescriptionService, - @Inject(DocSelectionManagerService) private readonly _docSelectionManagerService: DocSelectionManagerService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @IEditorService private readonly _editorService: IEditorService, - @ILayoutService private readonly _layoutService: ILayoutService - - ) { - super(); - - this._initialize(); - } - - override dispose(): void { - this._formulaRefColors = []; - this._resetTemp(); - } - - private _resetTemp() { - this._previousSequenceNodes = null; - - this._previousInsertRefStringIndex = null; - - this._isSelectionMovingRefSelections = []; - - this._previousRangesCount = 0; - - this._currentInsertRefStringIndex = -1; - } - - private _initialize(): void { - this._initialCursorSync(); - this._initAcceptFormula(); - this._initialFormulaTheme(); - this._initSelectionsEndListener(); - this._closeRangePromptWhenEditorInvisible(); - this._initialEditorInputChange(); - this._commandExecutedListener(); - this._cursorStateListener(); - this._inputFormulaListener(); - this._userMouseListener(); - this._initialChangeEditor(); - } - - private _initialFormulaTheme() { - const style = this._themeService.getCurrentTheme(); - - this._formulaRefColors = [ - style.loopColor1, - style.loopColor2, - style.loopColor3, - style.loopColor4, - style.loopColor5, - style.loopColor6, - style.loopColor7, - style.loopColor8, - style.loopColor9, - style.loopColor10, - style.loopColor11, - style.loopColor12, - ]; - - this._numberColor = style.hyacinth700; - - this._stringColor = style.verdancy800; - } - - private _initialCursorSync() { - this.disposeWithMe( - this._docSelectionManagerService.textSelection$ - .pipe( - filter((item) => { - return !isRangeSelector(item.unitId) && !isEmbeddingFormulaEditor(item.unitId); - }) - ) - .subscribe((params) => { - if (params?.unitId == null) { - return; - } - const editor = this._editorService.getEditor(params.unitId); - if (!editor - || editor.onlyInputContent() - || (editor.isSheetEditor() && !this._isFormulaEditorActivated()) - // Remove this latter. - || editor.params.scrollBar - ) { - return; - } - - const onlyInputRange = editor.onlyInputRange(); - - // @ts-ignore - if (params?.options?.fromSelection) { - return; - } else { - this._quitSelectingMode(); - } - - this._contextSwitch(); - this._checkShouldEnterSelectingMode(onlyInputRange); - - if (this._formulaPromptService.isLockedSelectionChange()) { - return; - } - - this._highlightFormula(); - - if (onlyInputRange) { - return; - } - - // TODO@Dushusir: use real text info - this._changeFunctionPanelState(); - }) - ); - } - - private _initialEditorInputChange() { - const arrows = [KeyCode.ARROW_DOWN, KeyCode.ARROW_UP, KeyCode.ARROW_LEFT, KeyCode.ARROW_RIGHT, KeyCode.CTRL, KeyCode.SHIFT]; - // TODO: @runzhe Should there be a registration mechanism, rather than a unified process here? - this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC) - .pipe(filter((documentDataModel) => { - const unitId = documentDataModel?.getUnitId() || ''; - return !isRangeSelector(unitId) && !isEmbeddingFormulaEditor(unitId); - })) - .subscribe((documentDataModel) => { - const unitId = documentDataModel?.getUnitId(); - - if (unitId == null) { - return; - } - - if (this._listenInputCache.has(unitId)) { - return; - } - - const editor = this._editorService.getEditor(unitId); - - if (editor == null) { - return; - } - - const docSelectionRenderService = this._renderManagerService.getRenderById(unitId)?.with(DocSelectionRenderService); - - if (docSelectionRenderService) { - this.disposeWithMe( - docSelectionRenderService.onInputBefore$.subscribe((param) => { - this._previousSequenceNodes = null; - this._previousInsertRefStringIndex = null; - - this._selectionRenderService.setSkipLastEnabled(true); - - const event = param?.event as KeyboardEvent; - if (!event) return; - - if (!arrows.includes(event.which)) { - if (this._arrowMoveActionState !== ArrowMoveAction.moveCursor) { - this._arrowMoveActionState = ArrowMoveAction.moveRefReady; - } - - this._inputPanelState = InputPanelState.keyNormal; - } else { - this._inputPanelState = InputPanelState.keyArrow; - } - - if (event.which !== KeyCode.F4) { - this._userCursorMove = false; - } - }) - ); - } - - this._listenInputCache.add(unitId); - }); - } - - private _closeRangePromptWhenEditorInvisible() { - // NOTE: to be refactored - - this.disposeWithMe(this._editorBridgeService.afterVisible$ - .pipe(distinctUntilKeyChanged('visible')) - .subscribe((visibleParam) => { - if (!visibleParam.visible) this._closeRangePrompt(); - }) - - ); - - this.disposeWithMe(this._contextService.subscribeContextValue$(FORMULA_EDITOR_ACTIVATED) - .pipe(distinctUntilChanged()) - .subscribe((activated) => { - if (!activated) this._closeRangePrompt(); - })); - } - - private _initialChangeEditor() { - this.disposeWithMe( - this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC) - .pipe(filter((documentDataModel) => { - const editorId = documentDataModel?.getUnitId() || ''; - return !isRangeSelector(editorId) && !isEmbeddingFormulaEditor(editorId); - })) - .subscribe((documentDataModel) => { - if (documentDataModel == null) { - return; - } - - const editorId = documentDataModel.getUnitId(); - - if (!this._editorService.isEditor(editorId) || this._previousEditorUnitId === editorId) { - return; - } - - if (!this._editorService.isSheetEditor(editorId)) { - this._closeRangePrompt(editorId); - this._previousEditorUnitId = editorId; - } - }) - ); - - this.disposeWithMe( - this._editorService.closeRangePrompt$.subscribe(() => { - if (!this._editorService.getSpreadsheetFocusState() || !this._formulaPromptService.isLockedSelectionInsert()) { - this._closeRangePrompt(); - } - }) - ); - } - - private _closeRangePrompt(editorId: Nullable) { - const docId = editorId || this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC)?.getUnitId() || ''; - if (isRangeSelector(docId) || isEmbeddingFormulaEditor(docId) || docId === DOCS_ZEN_EDITOR_UNIT_ID_KEY) { - return; - } - this._insertSelections = []; - this._refSelectionsService.clear(); - - if (editorId && this._editorService.isSheetEditor(editorId)) { - this._updateEditorModel('\r\n', []); - } - - this._contextService.setContextValue(FOCUSING_EDITOR_INPUT_FORMULA, false); - this._contextService.setContextValue(REF_SELECTIONS_ENABLED, false); - this._contextService.setContextValue(UNI_DISABLE_CHANGING_FOCUS_KEY, false); - - this._quitSelectingMode(); - - this._resetTemp(); - - this._hideFunctionPanel(); - } - - private _initSelectionsEndListener() { - const d = new DisposableCollection(); - - // response events from selection control, when selection control is created - // this is so weird !!! why didn't selection control handle move event itself ??? - - this.disposeWithMe(merge(this._refSelectionsService.selectionSet$, this._refSelectionsService.selectionMoveEnd$).subscribe((selections) => { - d.dispose(); - - if (!selections || selections.length === 0) return; - // Theme color should be set when SelectionControl is created, it's too late to set theme color at selection End(pointerup). - // The logic below has been moved to syncToEditor. - // this._allSelectionRenderServices.forEach((r) => this._updateRefSelectionStyle(r, this._isSelectionMovingRefSelections)); - const docID = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC)?.getUnitId() || ''; - if (isRangeSelector(docID) || isEmbeddingFormulaEditor(docID)) { - return; - } - const selectionControls = this._allSelectionRenderServices.map((s) => s.getSelectionControls()).flat(); - selectionControls.forEach((c) => { - c.disableHelperSelection(); - d.add(merge(c.selectionMoving$, c.selectionScaling$).subscribe((toRange) => { - const docID = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC)?.getUnitId() || ''; - if (isRangeSelector(docID) || isEmbeddingFormulaEditor(docID)) { - d.dispose(); - this._formulaPromptService.disableLockedSelectionChange(); - return; - } - this._onSelectionControlChange(toRange, c); - })); - d.add(merge(c.selectionMoveEnd$, c.selectionScaled$).subscribe(() => { - this._formulaPromptService.disableLockedSelectionChange(); - })); - }); - })); - } - - /** - * For interaction with mouse & keyboard shortcuts on spreadsheet. Not in formula editor. - */ - private _updateSelecting(selectionsWithStyles: ISelectionWithStyle[], performInsertion: boolean = false) { - if (selectionsWithStyles.length === 0) return; - if (this._editorService.selectionChangingState() && !this._formulaPromptService.isLockedSelectionInsert()) return; - - this._insertControlSelections(selectionsWithStyles); - - if (performInsertion) { - const currentSelection = selectionsWithStyles[selectionsWithStyles.length - 1]; - this._insertControlSelectionReplace(currentSelection); - } - } - - private _currentlyWorkingRefRenderer: Nullable = null; - private _selectionsChangeDisposables: Nullable; - private _enableRefSelectionsRenderService() { - const d = this._selectionsChangeDisposables = new DisposableCollection(); - this._allSelectionRenderServices.forEach((renderer) => { - d.add(renderer.enableSelectionChanging()); - - // When the current selections change, the ref string is updated without touch `IRefSelectionsService`. - d.add(renderer.selectionMoving$.subscribe((selections) => { - this._updateSelecting(selections.map((s) => convertSelectionDataToRange(s))); - })); - - // When the selection change begins, if other render service has last selection, - // it should be removed. - d.add(renderer.selectionMoveStart$.subscribe((selections) => { - const performInsertion = this._checkClearingLastSelection(renderer); - this._currentlyWorkingRefRenderer = renderer; - this._updateSelecting(selections.map((s) => convertSelectionDataToRange(s)), performInsertion); - })); - }); - } - - private _checkClearingLastSelection(renderer: RefSelectionsRenderService): boolean { - if (this._currentlyWorkingRefRenderer && this._currentlyWorkingRefRenderer !== renderer) { - this._currentlyWorkingRefRenderer.clearLastSelection(); - return false; - } - - return true; - } - - private _disposeSelectionsChangeListeners(): void { - this._selectionsChangeDisposables?.dispose(); - this._selectionsChangeDisposables = null; - } - - private _insertControlSelections(selections: ISelectionWithStyle[]) { - const currentSelection = selections[selections.length - 1]; - - this._resetSequenceNodes(selections.length); - - if ( - (selections.length === this._previousRangesCount || this._previousRangesCount === 0) && - this._previousSequenceNodes != null - ) { - this._insertControlSelectionReplace(currentSelection); - } else { - // Holding down ctrl causes an addition, requiring the ref string to be increased. - let insertNodes = this._formulaPromptService.getSequenceNodes()!; - const char = this._getCurrentChar()!; - - // To reset the cursor position when resetting the editor's content. - if (insertNodes.length === 0 && this._currentInsertRefStringIndex > 0) { - this._currentInsertRefStringIndex = -1; - } - - this._previousInsertRefStringIndex = this._currentInsertRefStringIndex; - - if (!matchRefDrawToken(char) && this._focusIsOnlyRange(selections.length)) { - this._formulaPromptService.insertSequenceString(this._currentInsertRefStringIndex, matchToken.COMMA); - insertNodes = this._formulaPromptService.getSequenceNodes(); - this._previousInsertRefStringIndex += 1; - } - - this._previousSequenceNodes = Tools.deepClone(insertNodes); - this._formulaPromptService.setSequenceNodes(insertNodes); - - const refString = this._generateRefString(currentSelection); - this._formulaPromptService.insertSequenceRef(this._previousInsertRefStringIndex, refString); - - this._selectionRenderService.setSkipLastEnabled(false); - } - - this._arrowMoveActionState = ArrowMoveAction.moveRefReady; - this._previousRangesCount = selections.length; - } - - private _initAcceptFormula() { - this.disposeWithMe( - this._formulaPromptService.acceptFormulaName$.subscribe((formulaString: string) => { - const activeRange = this._docSelectionManagerService.getActiveTextRange(); - - if (activeRange == null) { - this._hideFunctionPanel(); - return; - } - - const { startOffset } = activeRange; - - const lastSequenceNodes = this._formulaPromptService.getSequenceNodes(); - - const nodeIndex = this._formulaPromptService.getCurrentSequenceNodeIndex(startOffset - 2); - - const node = lastSequenceNodes[nodeIndex]; - - if (node == null || typeof node === 'string') { - this._hideFunctionPanel(); - return; - } - - const difference = formulaString.length - node.token.length; - const newNode = { ...node }; - - newNode.token = formulaString; - - newNode.endIndex += difference; - - lastSequenceNodes[nodeIndex] = newNode; - - const isDefinedName = this._descriptionService.hasDefinedNameDescription(formulaString); - - const isFormulaDefinedName = this._descriptionService.isFormulaDefinedName(formulaString); - - const formulaStringCount = formulaString.length + 1; - - const mustAddBracket = !isDefinedName || isFormulaDefinedName; - - if (mustAddBracket) { - lastSequenceNodes.splice(nodeIndex + 1, 0, matchToken.OPEN_BRACKET); - } - - for (let i = nodeIndex + 2, len = lastSequenceNodes.length; i < len; i++) { - const node = lastSequenceNodes[i]; - if (typeof node === 'string') { - continue; - } - - const newNode = { ...node }; - - newNode.startIndex += formulaStringCount; - newNode.endIndex += formulaStringCount; - - lastSequenceNodes[i] = newNode; - } - - let selectionIndex = newNode.endIndex + 1; - if (mustAddBracket) { - selectionIndex += 1; - } - - this._syncToEditor(lastSequenceNodes, selectionIndex, undefined, true, false); - }) - ); - } - - private _changeFunctionPanelState() { - const activeRange = this._docSelectionManagerService.getActiveTextRange(); - - if (activeRange == null) { - this._hideFunctionPanel(); - return; - } - - const { startOffset } = activeRange; - - const currentSequenceNode = this._formulaPromptService.getCurrentSequenceNode(startOffset - 2); - - if (currentSequenceNode == null) { - this._hideFunctionPanel(); - return; - } - - if (typeof currentSequenceNode !== 'string' && currentSequenceNode.nodeType === sequenceNodeType.FUNCTION && !this._descriptionService.hasDefinedNameDescription(currentSequenceNode.token.trim())) { - const token = currentSequenceNode.token.toUpperCase(); - - if (this._inputPanelState === InputPanelState.keyNormal) { - // show search function panel - const searchList = this._descriptionService.getSearchListByNameFirstLetter(token); - this._hideFunctionPanel(); - if (searchList == null || searchList.length === 0) { - return; - } - this._commandService.executeCommand(SearchFunctionOperation.id, { - visible: true, - searchText: token, - searchList, - }); - } else { - // show help function panel - this._changeHelpFunctionPanelState(token, -1); - } - - return; - } - - const config = this._getCurrentBodyDataStreamAndOffset(); - - const functionAndParameter = this._lexerTreeBuilder.getFunctionAndParameter(config?.dataStream || '', startOffset - 1 + (config?.offset || 0)); - - if (!functionAndParameter) { - this._hideFunctionPanel(); - return; - } - - const { functionName, paramIndex } = functionAndParameter; - - this._changeHelpFunctionPanelState(functionName.toUpperCase(), paramIndex); - } - - private _changeHelpFunctionPanelState(token: string, paramIndex: number) { - const functionInfo = this._descriptionService.getFunctionInfo(token); - this._hideFunctionPanel(); - if (functionInfo == null) { - return; - } - - // show help function panel - this._commandService.executeCommand(HelpFunctionOperation.id, { - visible: true, - paramIndex, - functionInfo, - }); - } - - private _hideFunctionPanel() { - this._commandService.executeCommand(SearchFunctionOperation.id, { - visible: false, - searchText: '', - }); - this._commandService.executeCommand(HelpFunctionOperation.id, { - visible: false, - paramIndex: -1, - }); - } - - private _checkShouldEnterSelectingMode(isOnlyInputRangeEditor = false): void { - if (isOnlyInputRangeEditor) { - this._enterSelectingMode(); - return; - } - - const char = this._getCurrentChar(); - const dataStream = this._getCurrentDataStream(); - if (dataStream?.substring(0, 1) === '=' && char && matchRefDrawToken(char)) { - this._enterSelectingMode(); - } else { - this._quitSelectingMode(); - } - } - - /** - * - * @returns Return the character under the current cursor in the editor. - */ - private _getCurrentChar() { - const activeRange = this._docSelectionManagerService.getActiveTextRange(); - - if (activeRange == null) { - return; - } - - const { startOffset } = activeRange; - - const config = this._getCurrentBodyDataStreamAndOffset(); - - if (config == null || startOffset == null) { - return; - } - - const dataStream = config.dataStream; - - return dataStream[startOffset - 1 + config.offset]; - } - - private _getCurrentDataStream() { - const config = this._getCurrentBodyDataStreamAndOffset(); - return config?.dataStream; - } - - private _isSelectingMode = false; - private _enterSelectingMode() { - if (this._isSelectingMode) { - return; - } - - this._editorBridgeService.enableForceKeepVisible(); - this._contextMenuService.disable(); - this._formulaPromptService.enableLockedSelectionInsert(); - this._selectionRenderService.setRemainLastEnabled(true); - - // Maybe `enterSelectingMode` should be merged with `_enableRefSelectionsRenderService`. - this._enableRefSelectionsRenderService(); - this._currentlyWorkingRefRenderer = null; - - // TODO: remain last - if (this._arrowMoveActionState !== ArrowMoveAction.moveCursor) { - this._arrowMoveActionState = ArrowMoveAction.moveRefReady; - } - - this._isSelectingMode = true; - } - - /** - * Disable the ref string generation mode. In the ref string generation mode, - * users can select a certain area using the mouse and arrow keys, and convert the area into a ref string. - */ - private _quitSelectingMode() { - if (!this._isSelectingMode) { - return; - } - - this._editorBridgeService.disableForceKeepVisible(); - this._contextMenuService.enable(); - this._formulaPromptService.disableLockedSelectionInsert(); - this._currentInsertRefStringIndex = -1; - - this._disposeSelectionsChangeListeners(); - - if (this._arrowMoveActionState === ArrowMoveAction.moveRefReady) { - this._arrowMoveActionState = ArrowMoveAction.exitInput; - } - - this._isSelectingMode = false; - } - - private _getCurrentBodyDataStreamAndOffset() { - const documentModel = this._univerInstanceService.getCurrentUniverDocInstance(); - - if (!documentModel?.getBody()) { - return; - } - - const unitId = documentModel.getUnitId(); - - const editor = this._editorService.getEditor(unitId); - - const dataStream = documentModel.getBody()?.dataStream ?? ''; - - if (!editor || !editor.onlyInputRange()) { - return { dataStream, offset: 0 }; - } - - return { dataStream: compareToken.EQUALS + dataStream, offset: 1 }; - } - - private _getFormulaAndCellEditorBody(unitIds: string[]) { - return unitIds.map((unitId) => { - const dataModel = this._univerInstanceService.getUniverDocInstance(unitId); - - return dataModel?.getBody(); - }); - } - - private _editorModelUnitIds() { - const currentDocumentDataModel = this._univerInstanceService.getCurrentUniverDocInstance()!; - const unitId = currentDocumentDataModel.getUnitId(); - - if (this._editorService.isEditor(unitId) && !this._editorService.isSheetEditor(unitId)) { - return [unitId]; - } - - return sheetEditorUnitIds; - } - - /** - * Detect whether the user's input content is a formula. If it is a formula, - * serialize the current input content into a sequenceNode; - * otherwise, close the formula panel. - * @param currentInputValue The text content entered by the user in the editor. - */ - private _contextSwitch() { - const config = this._getCurrentBodyDataStreamAndOffset(); - if (config && isFormulaString(config.dataStream)) { - this._contextService.setContextValue(FOCUSING_EDITOR_INPUT_FORMULA, true); - this._contextService.setContextValue(REF_SELECTIONS_ENABLED, true); - this._contextService.setContextValue(UNI_DISABLE_CHANGING_FOCUS_KEY, true); - - const lastSequenceNodes = - this._lexerTreeBuilder.sequenceNodesBuilder(config.dataStream) || - []; - - this._formulaPromptService.setSequenceNodes(lastSequenceNodes); - - const activeRange = this._docSelectionManagerService.getActiveTextRange(); - - if (activeRange == null) { - return; - } - - const { startOffset } = activeRange; - - this._currentInsertRefStringIndex = startOffset - 1 + config.offset; - - return; - } - this._contextService.setContextValue(FOCUSING_EDITOR_INPUT_FORMULA, false); - this._contextService.setContextValue(REF_SELECTIONS_ENABLED, false); - this._contextService.setContextValue(UNI_DISABLE_CHANGING_FOCUS_KEY, false); - - this._formulaPromptService.disableLockedSelectionChange(); - - this._formulaPromptService.disableLockedSelectionInsert(); - - // this._lastSequenceNodes = []; - - this._formulaPromptService.clearSequenceNodes(); - - this._hideFunctionPanel(); - } - - private _getContextState() { - return this._contextService.getContextValue(FOCUSING_EDITOR_INPUT_FORMULA); - } - - /** - * Highlight cell editor and formula bar editor. - */ - private _highlightFormula() { - if (this._getContextState() === false) { - return; - } - - const sequenceNodes = this._formulaPromptService.getSequenceNodes(); - - const unitIds = this._editorModelUnitIds(); - - const bodyList = this._getFormulaAndCellEditorBody(unitIds).filter((b) => !!b); - - // this._refSelectionsService.clear(); - - if (sequenceNodes == null || sequenceNodes.length === 0) { - this._existsSequenceNode = false; - bodyList.forEach((body) => (body!.textRuns = [])); - } else { - // this._lastSequenceNodes = sequenceNodes; - this._existsSequenceNode = true; - const { textRuns, refSelections } = this._buildTextRuns(sequenceNodes); - bodyList.forEach((body) => (body!.textRuns = textRuns)); - this._allSelectionRenderServices.forEach((r) => this._refreshSelectionForReference(r, refSelections)); - - // No need set refSelection styles here. this._syncToEditor has same effect. - // this._allSelectionRenderServices.forEach((r) => this._updateRefSelectionStyle(r, this._isSelectionMovingRefSelections)); - } - - this._refreshFormulaAndCellEditor(unitIds); - } - - /** - * : - * # - * Generate styles for formula text, highlighting references, text, numbers, and arrays. - */ - private _buildTextRuns(sequenceNodes: Array) { - const textRuns: ITextRun[] = []; - const refSelections: IRefSelection[] = []; - const themeColorMap = new Map(); - let refColorIndex = 0; - const offset = this._getCurrentBodyDataStreamAndOffset()?.offset || 0; - for (let i = 0, len = sequenceNodes.length; i < len; i++) { - const node = sequenceNodes[i]; - if (typeof node === 'string' || this._descriptionService.hasDefinedNameDescription(node.token.trim())) { - continue; - } - - const { startIndex, endIndex, nodeType, token } = node; - let themeColor = ''; - if (nodeType === sequenceNodeType.REFERENCE) { - if (themeColorMap.has(token)) { - themeColor = themeColorMap.get(token)!; - } else { - const colorIndex = refColorIndex % this._formulaRefColors.length; - themeColor = this._formulaRefColors[colorIndex]; - themeColorMap.set(token, themeColor); - refColorIndex++; - } - - refSelections.push({ - refIndex: i, - themeColor, - token, - }); - } else if (nodeType === sequenceNodeType.NUMBER) { - themeColor = this._numberColor; - } else if (nodeType === sequenceNodeType.STRING) { - themeColor = this._stringColor; - } else if (nodeType === sequenceNodeType.ARRAY) { - themeColor = this._stringColor; - } - - if (themeColor && themeColor.length > 0) { - textRuns.push({ - st: startIndex + 1 - offset, - ed: endIndex + 2 - offset, - ts: { - cl: { - rgb: themeColor, - }, - }, - }); - } - } - - return { textRuns, refSelections }; - } - - private _exceedCurrentRange(range: IRange, rowCount: number, columnCount: number) { - const { startRow, startColumn } = range; - if (startRow > rowCount - 1) { - return true; - } - - if (startColumn > columnCount - 1) { - return true; - } - - return false; - } - - /** - * Draw the referenced selection text based on the style and token. - * @param refSelections - */ - - private _refreshSelectionForReference(refSelectionRenderService: RefSelectionsRenderService, refSelections: IRefSelection[]) { - // const [unitId, sheetId] = refSelectionRenderService.getLocation(); - const { unitId, sheetId } = this._editorBridgeService.getEditCellState()!; - const { unitId: selfUnitId, sheetId: currSheetId } = this._getCurrentUnitIdAndSheetId(); - - const isSelfSheet = sheetId === currSheetId; - - const workbook = this._univerInstanceService.getUniverSheetInstance(unitId)!; - const worksheet = workbook.getSheetBySheetId(sheetId)!; - - let lastRange: Nullable = null; - - const selectionWithStyle: ISelectionWithStyle[] = []; - for (let i = 0, len = refSelections.length; i < len; i++) { - const refSelection = refSelections[i]; - const { themeColor, token, refIndex } = refSelection; - - const gridRange = deserializeRangeWithSheet(token); - const { unitId: refUnitId, sheetName, range: rawRange } = gridRange; - - /** - * pro/issues/436 - * When the range is an entire row or column, NaN values need to be corrected. - */ - const range = setEndForRange(rawRange, worksheet.getRowCount(), worksheet.getColumnCount()); - - if (refUnitId != null && refUnitId.length > 0 && unitId !== refUnitId) continue; - - // sheet name is designed to be unique. - const refSheetId = this._getSheetIdByName(unitId, sheetName.trim()); - - // Cross sheet operation - if (!isSelfSheet && refSheetId !== currSheetId) continue; - - // Current sheet operation - if (isSelfSheet && sheetName.length !== 0 && refSheetId !== sheetId) continue; - - if (this._exceedCurrentRange(range, worksheet.getRowCount(), worksheet.getColumnCount())) continue; - - const lastRangeCopy = this._getPrimary(range, themeColor, refIndex); - if (lastRangeCopy) { - lastRange = lastRangeCopy; - selectionWithStyle.push(lastRange); - continue; - } - - const primary = getPrimaryForRange(range, worksheet); - - if ( - !Rectangle.equals(primary, range) && - range.startRow === range.endRow && - range.startColumn === range.endColumn - ) { - range.startRow = primary.startRow; - range.endRow = primary.endRow; - range.startColumn = primary.startColumn; - range.endColumn = primary.endColumn; - } - - selectionWithStyle.push({ - range, - primary, - style: genFormulaRefSelectionStyle(this._themeService, themeColor, refIndex.toString()), - }); - } - - // why add lastRange after all? that would changes selection sequence !!! why ??? - // if (lastRange) { - // selectionWithStyle.push(lastRange); - // } - - // why use sheetId not currSheetId ??? - if (selectionWithStyle.length) { - this._refSelectionsService.setSelections(unitId, sheetId, selectionWithStyle, SelectionMoveType.ONLY_SET); - } - } - - private _getPrimary(range: IRange, themeColor: string, refIndex: number) { - const matchedInsertSelection = this._insertSelections.find((selection) => { - const { startRow, startColumn, endRow, endColumn } = selection.range; - if ( - startRow === range.startRow && - startColumn === range.startColumn && - endRow === range.endRow && - endColumn === range.endColumn - ) { - return true; - } - if ( - startRow === range.startRow && - startColumn === range.startColumn && - range.startRow === range.endRow && - range.startColumn === range.endColumn - ) { - return true; - } - - return false; - }); - - if (matchedInsertSelection?.primary == null) { - return; - } - - const { - isMerged, - isMergedMainCell, - startRow: mergeStartRow, - endRow: mergeEndRow, - startColumn: mergeStartColumn, - endColumn: mergeEndColumn, - } = matchedInsertSelection.primary; - - if ( - (isMerged || isMergedMainCell) && - mergeStartRow === range.startRow && - mergeStartColumn === range.startColumn && - range.startRow === range.endRow && - range.startColumn === range.endColumn - ) { - range.endRow = mergeEndRow; - range.endColumn = mergeEndColumn; - } - - return { - range, - primary: matchedInsertSelection.primary, - style: genFormulaRefSelectionStyle(this._themeService, themeColor, refIndex.toString()), - }; - } - - private _getSheetIdByName(unitId: string, sheetName: string) { - const workbook = this._univerInstanceService.getUniverSheetInstance(unitId); - return workbook?.getSheetBySheetName(normalizeSheetName(sheetName))?.getSheetId(); - } - - private _getSheetNameById(unitId: string, sheetId: string) { - const workbook = this._univerInstanceService.getUniverSheetInstance(unitId); - const sheetName = workbook?.getSheetBySheetId(sheetId)?.getName() || ''; - return sheetName; - } - - private _getCurrentUnitIdAndSheetId() { - const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; - const worksheet = workbook.getActiveSheet(); - const skeleton = this._renderManagerService.getRenderById(workbook.getUnitId())?.with(SheetSkeletonManagerService)?.getCurrentSkeleton(); - - return { - unitId: workbook.getUnitId(), - sheetId: worksheet?.getSheetId() || '', - skeleton, - }; - } - - // FIXME@Jocs: this internal implementations should be exposed to callers. - // This method should be moved to EditorService. - private _getEditorOpenedForSheet() { - const documentDataModel = this._univerInstanceService.getCurrentUniverDocInstance()!; - const editorUnitId = documentDataModel.getUnitId(); - const editor = this._editorService.getEditor(editorUnitId); - if (!editor) { - return { - openUnitId: null, - openSheetId: null, - }; - } - - return { - openUnitId: editor.getOpenForSheetUnitId(), - openSheetId: editor.getOpenForSheetSubUnitId(), - }; - } - - /** - * Convert the selection range to a ref string for the formula engine, such as A1:B1 - * @param currentSelection - */ - private _generateRefString(currentSelection: ISelectionWithStyle) { - let refUnitId = ''; - let refSheetName = ''; - - const { unitId, sheetId } = currentSelection.range; - const { openUnitId, openSheetId } = this._getEditorOpenedForSheet(); - - if (unitId !== openUnitId && unitId) { - refUnitId = unitId; - } - - if (sheetId !== openSheetId && unitId && sheetId) { - refSheetName = this._getSheetNameById(unitId, sheetId); - } - - const { range, primary } = currentSelection; - let { startRow, endRow, startColumn, endColumn } = range; - const { startAbsoluteRefType, endAbsoluteRefType, rangeType } = range; - - if (primary) { - const { - isMerged, - isMergedMainCell, - startRow: mergeStartRow, - endRow: mergeEndRow, - startColumn: mergeStartColumn, - endColumn: mergeEndColumn, - } = primary; - - if ( - (isMerged || isMergedMainCell) && - mergeStartRow === startRow && - mergeStartColumn === startColumn && - mergeEndRow === endRow && - mergeEndColumn === endColumn - ) { - startRow = mergeStartRow; - startColumn = mergeStartColumn; - endRow = mergeStartRow; - endColumn = mergeStartColumn; - } - } - - return serializeRangeToRefString({ - sheetName: refSheetName, - unitId: refUnitId, - range: { - startRow, - endRow, - startColumn, - endColumn, - rangeType, - startAbsoluteRefType, - endAbsoluteRefType, - }, - }); - } - - /** - * Update Editor formula text in prompt editor by current selection in spreadsheet. - * Restore the sequenceNode generated by the lexer to the text in the editor, and set the cursor position. - * - * @param sequenceNodes - * @param textSelectionOffset - */ - - private _syncToEditor( - sequenceNodes: Array, - textSelectionOffset: number, - editorUnitId?: string, - canUndo: boolean = true, - fromSelection = true - ) { - let dataStream = generateStringWithSequence(sequenceNodes); - const { textRuns, refSelections } = this._buildTextRuns(sequenceNodes); - this._isSelectionMovingRefSelections = refSelections; - - // Get theme color from prompt formula editor when creating a new selection. - this._allSelectionRenderServices.forEach((r) => this._updateRefSelectionStyle(r, this._isSelectionMovingRefSelections)); - - const activeRange = this._docSelectionManagerService.getActiveTextRange(); - if (activeRange == null) { - return; - } - - this._currentInsertRefStringIndex = textSelectionOffset; - - if (editorUnitId == null) { - editorUnitId = this._univerInstanceService.getCurrentUniverDocInstance()!.getUnitId(); - } - - this._fitEditorSize(); - - const editor = this._editorService.getEditor(editorUnitId); - - // You need to set a mode for single selection area or multiple selection areas, adapting to a rangeSelector that only has a single selection area. - if (editor?.isSingleChoice()) { - dataStream = dataStream.split(',')[0]; - this._selectionRenderService.setSingleSelectionEnabled(true); - } else { - this._selectionRenderService.setSingleSelectionEnabled(false); - } - - let formulaString = dataStream; - let offset = 1; - - if (!editor || !editor.onlyInputRange()) { - formulaString = `${compareToken.EQUALS}${dataStream}`; - offset = 0; - } - - const { collapsed, style } = activeRange; - if (canUndo) { - this._commandService.executeCommand(ReplaceContentCommand.id, { - unitId: editorUnitId, - body: { - dataStream: formulaString, - textRuns, - }, - textRanges: [ - { - startOffset: textSelectionOffset + 1 - offset, - endOffset: textSelectionOffset + 1 - offset, - collapsed, - style, - }, - ], - segmentId: null, - options: { fromSelection }, - }); - // The ReplaceContentCommand has canceled the selection operation, so it needs to be triggered externally once. - this._docSelectionManagerService.replaceTextRanges([ - { - startOffset: textSelectionOffset + 1 - offset, - endOffset: textSelectionOffset + 1 - offset, - style, - }, - ], true, { fromSelection }); - } else { - this._updateEditorModel(`${formulaString}\r\n`, textRuns); - this._docSelectionManagerService.replaceTextRanges([ - { - startOffset: textSelectionOffset + 1 - offset, - endOffset: textSelectionOffset + 1 - offset, - style, - }, - ], true, { fromSelection }); - } - - /** - * After selecting the formula, allow the editor to continue entering content. - */ - this._layoutService.focus(); - } - - private _fitEditorSize() { - const currentDocumentDataModel = this._univerInstanceService.getCurrentUniverDocInstance(); - const editorUnitId = currentDocumentDataModel!.getUnitId(); - - if (this._editorService.isEditor(editorUnitId) && !this._editorService.isSheetEditor(editorUnitId)) { - return; - } - this._editorBridgeService.changeEditorDirty(true); - if (!this._editorBridgeService.isVisible().visible) { - return; - } - - if (editorUnitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) { - const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); - const workbookUnitId = workbook?.getUnitId() ?? ''; - const render = this._renderManagerService.getRenderById(workbookUnitId); - if (!render) { - return; - } - const sheetCellEditorResizeService = render.with(SheetCellEditorResizeService); - sheetCellEditorResizeService.fitTextSize(); - } - } - - /** - * Update the editor's model value to facilitate formula updates. - * @param dataStream - * @param textRuns - */ - private _updateEditorModel(dataStream: string, textRuns: ITextRun[]) { - const documentDataModel = this._univerInstanceService.getCurrentUniverDocInstance(); - - const editorUnitId = documentDataModel!.getUnitId(); - if (!this._editorService.isEditor(editorUnitId)) { - return; - } - - const docViewModel = this._renderManagerService.getRenderById(editorUnitId)?.with(DocSkeletonManagerService).getViewModel(); - if (docViewModel == null || documentDataModel == null) { - return; - } - - const snapshot = documentDataModel?.getSnapshot(); - - if (snapshot == null) { - return; - } - - const newBody = { - dataStream, - textRuns, - }; - - snapshot.body = newBody; - - docViewModel.reset(documentDataModel); - } - - private _insertControlSelectionReplace(currentSelection: ISelectionWithStyle) { - if (this._previousSequenceNodes == null) { - this._previousSequenceNodes = this._formulaPromptService.getSequenceNodes(); - } - - if (this._previousInsertRefStringIndex == null) { - this._previousInsertRefStringIndex = this._currentInsertRefStringIndex; - } - - // No new control is added, the current ref string is still modified. - const insertNodes = Tools.deepClone(this._previousSequenceNodes); - if (insertNodes == null) { - return; - } - - const { skeleton } = this._getCurrentUnitIdAndSheetId(); - const unitId = skeleton?.worksheet.getUnitId(); - const sheetId = skeleton?.worksheet.getSheetId(); - currentSelection.range.sheetId = sheetId; - currentSelection.range.unitId = unitId; - - const refString = this._generateRefString(currentSelection); - this._formulaPromptService.setSequenceNodes(insertNodes); - this._formulaPromptService.insertSequenceRef(this._previousInsertRefStringIndex, refString); - this._syncToEditor(insertNodes, this._previousInsertRefStringIndex + refString.length); - const selectionsWithStyle = this._selectionRenderService.getSelectionDataWithStyle() || []; - this._insertSelections = []; - - // selectionsWithStyle.forEach((currentSelection) => { - // const range = convertSelectionDataToRange(currentSelection); - // this._insertSelections.push(range); - // }); - const lastSelectionWithStyle = selectionsWithStyle[selectionsWithStyle.length - 1]; - if (lastSelectionWithStyle) { - const range = convertSelectionDataToRange(lastSelectionWithStyle); - this._insertSelections.push(range); - } - } - - /** - * pro/issues/450 - * In range selection mode, certain measures are implemented to ensure that the selection behavior is processed correctly. - */ - private _focusIsOnlyRange(selectionCount: number) { - const currentEditor = this._editorService.getFocusEditor(); - if (!currentEditor) { - return true; - } - - if (!currentEditor.onlyInputRange()) { - return true; - } - - if (this._existsSequenceNode) { - return true; - } - - if (selectionCount > 1 || (this._previousSequenceNodes != null && this._previousSequenceNodes.length > 0)) { - return true; - } - - if (this._previousInsertRefStringIndex != null) { - this._previousInsertRefStringIndex += 1; - } - - return false; - } - - /** - * pro/issues/450 - * In range selection mode, certain measures are implemented to ensure that the selection behavior is processed correctly. - */ - private _resetSequenceNodes(selectionCount: number) { - const currentEditor = this._editorService.getFocusEditor(); - if (!currentEditor) { - return; - } - - if (!currentEditor.onlyInputRange()) { - return; - } - - if (selectionCount > 1) { - return; - } - - if (this._existsSequenceNode) { - this._formulaPromptService.clearSequenceNodes(); - this._previousRangesCount = 0; - this._existsSequenceNode = false; - } - } - - // FIXME: @wzhudev: this method could be merged with `_refreshSelectionForReference`. - - private _updateRefSelectionStyle(refSelectionRenderService: RefSelectionsRenderService, refSelections: IRefSelection[]) { - const controls = refSelectionRenderService.getSelectionControls(); - const [unitId, sheetId] = refSelectionRenderService.getLocation(); - - const matchedControls = new Set(); - for (let i = 0, len = refSelections.length; i < len; i++) { - const refSelection = refSelections[i]; - const { refIndex, themeColor, token } = refSelection; - const rangeWithSheet = deserializeRangeWithSheet(token); - const { unitId: refUnitId, sheetName, range } = rangeWithSheet; - - if (!refUnitId && refUnitId.length > 0 && unitId !== refUnitId) { - continue; - } - - const refSheetId = this._getSheetIdByName(unitId, sheetName.trim()); - if (refSheetId && refSheetId !== sheetId) { - continue; - } - - const control = controls.find((c) => { - // 范围相等方法又写一遍! - const { startRow, startColumn, endRow, endColumn, rangeType } = c.getRange(); - if ( - rangeType === RANGE_TYPE.COLUMN && - startColumn === range.startColumn && - endColumn === range.endColumn - ) { - return true; - } - - if (rangeType === RANGE_TYPE.ROW && startRow === range.startRow && endRow === range.endRow) { - return true; - } - - if ( - startRow === range.startRow && - startColumn === range.startColumn && - endRow === range.endRow && - endColumn === range.endColumn - ) { - return true; - } - if ( - startRow === range.startRow && - startColumn === range.startColumn && - range.startRow === range.endRow && - range.startColumn === range.endColumn - ) { - return true; - } - - return false; - }); - - if (control) { - const style = genFormulaRefSelectionStyle(this._themeService, themeColor, refIndex.toString()); - control.updateStyle(style); - matchedControls.add(control); - } - } - } - - private _onSelectionControlChange(toRange: IRangeWithCoord, selectionControl: SelectionControl) { - // FIXME: change here - const { skeleton } = this._getCurrentUnitIdAndSheetId(); - if (!skeleton) return; - // const { unitId, sheetId } = toRange; - this._formulaPromptService.enableLockedSelectionChange(); - - const id = selectionControl.currentStyle?.id; - if (!id || !Tools.isStringNumber(id)) { - return; - } - - let { startRow, endRow, startColumn, endColumn } = toRange; - const primary = skeleton - ? skeleton.worksheet.getCellInfoInMergeData(startRow, startColumn) - : { - actualRow: startRow, - actualColumn: startColumn, - isMergedMainCell: false, - isMerged: false, - endRow: startRow, - endColumn: startColumn, - startRow, - startColumn, - }; - - if (primary) { - const { - isMerged, - isMergedMainCell, - startRow: mergeStartRow, - endRow: mergeEndRow, - startColumn: mergeStartColumn, - endColumn: mergeEndColumn, - } = primary; - - if ( - (isMerged || isMergedMainCell) && mergeStartRow === startRow && mergeStartColumn === startColumn && - mergeEndRow === endRow && mergeEndColumn === endColumn - ) { - startRow = mergeStartRow; - startColumn = mergeStartColumn; - endRow = mergeStartRow; - endColumn = mergeStartColumn; - } - } - - const nodeIndex = Number(id); - - const currentNode = this._formulaPromptService.getCurrentSequenceNodeByIndex(nodeIndex); - if (!currentNode) { - return; - } - let refType: IAbsoluteRefTypeForRange = { startAbsoluteRefType: AbsoluteRefType.NONE }; - if (typeof currentNode !== 'string') { - const token = (currentNode as ISequenceNode).token; - - refType = getAbsoluteRefTypeWitString(token) as IAbsoluteRefTypeForRange; - - if (refType.endAbsoluteRefType == null) { - refType.endAbsoluteRefType = refType.startAbsoluteRefType; - } - } - - const unitId = skeleton?.worksheet.getUnitId(); - const sheetId = skeleton?.worksheet.getSheetId(); - const refString = this._generateRefString({ - range: { - startRow: Math.min(startRow, endRow), - endRow: Math.max(startRow, endRow), - startColumn: Math.min(startColumn, endColumn), - endColumn: Math.max(startColumn, endColumn), - ...refType, - sheetId, - unitId, - }, - primary, - style: null, - }); - - this._formulaPromptService.updateSequenceRef(nodeIndex, refString); - const sequenceNodes = this._formulaPromptService.getSequenceNodes(); - const node = sequenceNodes[nodeIndex]; - - if (typeof node === 'string') { - return; - } - - this._syncToEditor(sequenceNodes, node.endIndex + 1); - selectionControl.updateRange(toRange, this._selectionRenderService.attachPrimaryWithCoord(primary)); - } - - private _refreshFormulaAndCellEditor(unitIds: string[]) { - for (const unitId of unitIds) { - const editorObject = getEditorObject(unitId, this._renderManagerService); - - const documentComponent = editorObject?.document; - - if (documentComponent == null) { - continue; - } - - documentComponent.getSkeleton()?.calculate(); - documentComponent.makeDirty(); - } - } - - private _cursorStateListener() { - /** - * The user's operations follow the sequence of opening the editor and then moving the cursor. - * The logic here predicts the user's first cursor movement behavior based on this rule - */ - - const editorObject = this._getEditorObject(); - - if (editorObject == null) { - return; - } - - const { mainComponent: documentComponent } = editorObject; - if (documentComponent) { - this.disposeWithMe(documentComponent.onPointerDown$.subscribeEvent(() => { - this._arrowMoveActionState = ArrowMoveAction.moveCursor; - this._inputPanelState = InputPanelState.mouse; - })); - } - } - - private _pressEnter(params: ISelectEditorFormulaOperationParam) { - const { keycode, isSingleEditor = false } = params; - - if (this._formulaPromptService.isSearching()) { - this._formulaPromptService.accept(true); - return; - } - - if (isSingleEditor === true) { - return; - } - - // FIXME: @Jocs: lots of code duplications here - - this._editorBridgeService.changeVisible({ - visible: false, - eventType: DeviceInputEventType.Keyboard, - keycode, - unitId: '', - }); - } - - private _pressTab(params: ISelectEditorFormulaOperationParam) { - const { keycode, isSingleEditor = false } = params; - if (this._formulaPromptService.isSearching()) { - this._formulaPromptService.accept(true); - return; - } - - if (isSingleEditor === true) { - return; - } - - this._editorBridgeService.changeVisible({ - visible: false, - eventType: DeviceInputEventType.Keyboard, - keycode, - unitId: '', - }); - } - - private _pressEsc(params: ISelectEditorFormulaOperationParam) { - const { keycode } = params; - const focusEditor = this._editorService.getFocusEditor(); - if (!focusEditor || focusEditor?.isSheetEditor() === true) { - this._editorBridgeService.changeVisible({ - visible: false, - eventType: DeviceInputEventType.Keyboard, - keycode, - unitId: '', - }); - } - } - - private _pressArrowKey(params: ISelectEditorFormulaOperationParam) { - const { keycode, metaKey } = params; - let direction = Direction.DOWN; - if (keycode === KeyCode.ARROW_DOWN) { - direction = Direction.DOWN; - } else if (keycode === KeyCode.ARROW_UP) { - direction = Direction.UP; - } else if (keycode === KeyCode.ARROW_LEFT) { - direction = Direction.LEFT; - } else if (keycode === KeyCode.ARROW_RIGHT) { - direction = Direction.RIGHT; - } - - if (metaKey === MetaKeys.CTRL_COMMAND) { - this._commandService.executeCommand(MoveSelectionCommand.id, { - direction, - jumpOver: JumpOver.moveGap, - }); - } else if (metaKey === MetaKeys.SHIFT) { - this._commandService.executeCommand(ExpandSelectionCommand.id, { - direction, - }); - } else if (metaKey === META_KEY_CTRL_AND_SHIFT) { - this._commandService.executeCommand(ExpandSelectionCommand.id, { - direction, - jumpOver: JumpOver.moveGap, - }); - } else { - this._commandService.executeCommand(MoveSelectionCommand.id, { - direction, - }); - } - } - - private _commandExecutedListener() { - // Listen to document edits to refresh the size of the editor. - const updateCommandList = [SelectEditorFormulaOperation.id]; - - this.disposeWithMe( - this._commandService.onCommandExecuted((command: ICommandInfo) => { - const instance = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); - const unitId = instance?.getUnitId() || ''; - if (isRangeSelector(unitId) || isEmbeddingFormulaEditor(unitId)) { - return; - } - if (command.id === ReferenceAbsoluteOperation.id) { - this._changeRefString(); - } else if (updateCommandList.includes(command.id)) { - const params = command.params as ISelectEditorFormulaOperationParam; - const { keycode, isSingleEditor = false } = params; - - if (keycode === KeyCode.ENTER) { - this._pressEnter(params); - return; - } - - if (keycode === KeyCode.TAB) { - this._pressTab(params); - return; - } - - if (keycode === KeyCode.ESC) { - this._pressEsc(params); - return; - } - - if (this._formulaPromptService.isSearching()) { - if (keycode === KeyCode.ARROW_DOWN) { - this._formulaPromptService.navigate({ direction: Direction.DOWN }); - return; - } - if (keycode === KeyCode.ARROW_UP) { - this._formulaPromptService.navigate({ direction: Direction.UP }); - return; - } - } - - if (isSingleEditor === true) { - return; - } - - if (this._arrowMoveActionState === ArrowMoveAction.moveCursor) { - this._moveInEditor(keycode); - return; - } - if (this._arrowMoveActionState === ArrowMoveAction.exitInput) { - this._editorBridgeService.changeVisible({ - visible: false, - eventType: DeviceInputEventType.Keyboard, - keycode, - unitId: '', - }); - return; - } - - if (this._arrowMoveActionState === ArrowMoveAction.moveRefReady) { - this._arrowMoveActionState = ArrowMoveAction.movingRef; - } - - // If there's no current selections in the ref selections service, we should copy for - // normal selection. - const previousRanges = this._refSelectionsService.getCurrentSelections(); - if (previousRanges.length === 0) { - const selectionData = this._sheetsSelectionsService.getCurrentLastSelection(); - if (selectionData != null) { - const selectionDataNew = Tools.deepClone(selectionData); - this._refSelectionsService.setSelections([selectionDataNew]); - } - } - - this._pressArrowKey(params); - - const selectionWithStyles = this._refSelectionsService.getCurrentSelections(); - const currentSelection = selectionWithStyles[selectionWithStyles.length - 1]; - - this._insertControlSelectionReplace(currentSelection); - this._highlightFormula(); - } - }) - ); - } - - private _moveInEditor(keycode: Nullable) { - if (keycode == null) { - return; - } - let direction = Direction.LEFT; - if (keycode === KeyCode.ARROW_DOWN) { - direction = Direction.DOWN; - } else if (keycode === KeyCode.ARROW_UP) { - direction = Direction.UP; - } else if (keycode === KeyCode.ARROW_RIGHT) { - direction = Direction.RIGHT; - } - - this._commandService.executeCommand(MoveCursorOperation.id, { - direction, - }); - } - - private _userMouseListener() { - const editorObject = this._getEditorObject(); - - if (editorObject == null) { - return; - } - - const { mainComponent: documentComponent } = editorObject; - if (documentComponent) { - this.disposeWithMe(documentComponent?.onPointerDown$.subscribeEvent(() => { - this._userCursorMove = true; - })); - } - } - - private _inputFormulaListener() { - this.disposeWithMe( - this._editorService.inputFormula$.subscribe((param) => { - const { formulaString, editorUnitId } = param; - - if (formulaString.substring(0, 1) !== compareToken.EQUALS) { - return; - } - const { unitId } = this._getCurrentUnitIdAndSheetId(); - const visibleState = this._editorBridgeService.isVisible(); - if (visibleState.visible === false) { - this._editorBridgeService.changeVisible({ - visible: true, - eventType: DeviceInputEventType.Dblclick, - unitId, - }); - } - - const lastSequenceNodes = this._lexerTreeBuilder.sequenceNodesBuilder(formulaString) || []; - - this._formulaPromptService.setSequenceNodes(lastSequenceNodes); - - this._syncToEditor(lastSequenceNodes, formulaString.length - 1, editorUnitId, true, false); - }) - ); - } - - /** - * Absolute range, triggered by F4 - */ - private _changeRefString() { - const activeRange = this._docSelectionManagerService.getActiveTextRange(); - - if (activeRange == null) { - return; - } - - const { startOffset } = activeRange; - - const strIndex = startOffset - 2; - - const nodeIndex = this._formulaPromptService.getCurrentSequenceNodeIndex(strIndex); - - const node = this._formulaPromptService.getCurrentSequenceNodeByIndex(nodeIndex); - - if (node == null || typeof node === 'string' || node.nodeType !== sequenceNodeType.REFERENCE) { - return; - } - - const tokenArray = node.token.split('!'); - - let token = node.token; - - if (tokenArray.length > 1) { - token = tokenArray[tokenArray.length - 1]; - } - - let unitIDAndSheetName = ''; - - for (let i = 0, len = tokenArray.length; i < len - 1; i++) { - unitIDAndSheetName += tokenArray[i]; - } - - let finalToken = token; - if (token.indexOf(matchToken.COLON) > -1) { - if (!this._userCursorMove) { - finalToken = this._changeRangeRef(token); - } else { - const refStringSplit = token.split(matchToken.COLON); - const prefix = refStringSplit[0]; - const suffix = refStringSplit[1]; - const relativeIndex = strIndex - node.startIndex; - - if (relativeIndex <= prefix.length) { - finalToken = this._changeSingleRef(prefix) + matchToken.COLON + suffix; - } else { - finalToken = prefix + matchToken.COLON + this._changeSingleRef(suffix); - } - } - } else { - finalToken = this._changeSingleRef(token); - } - - finalToken = unitIDAndSheetName + finalToken; - - const difference = finalToken.length - node.token.length; - - this._formulaPromptService.updateSequenceRef(nodeIndex, finalToken); - - this._syncToEditor(this._formulaPromptService.getSequenceNodes(), strIndex + difference + 1); - } - - private _changeRangeRef(token: string) { - const range = deserializeRangeWithSheet(token).range; - let resultToken = ''; - if (range.startAbsoluteRefType === AbsoluteRefType.NONE || range.startAbsoluteRefType == null) { - range.startAbsoluteRefType = AbsoluteRefType.ALL; - range.endAbsoluteRefType = AbsoluteRefType.ALL; - } else { - range.startAbsoluteRefType = AbsoluteRefType.NONE; - range.endAbsoluteRefType = AbsoluteRefType.NONE; - } - resultToken = serializeRange(range); - return resultToken; - } - - private _changeSingleRef(token: string) { - const range = deserializeRangeWithSheet(token).range; - const type = range.startAbsoluteRefType; - let resultToken = ''; - if (type === AbsoluteRefType.NONE || type == null) { - range.startAbsoluteRefType = AbsoluteRefType.ALL; - range.endAbsoluteRefType = AbsoluteRefType.ALL; - } else if (type === AbsoluteRefType.ALL) { - range.startAbsoluteRefType = AbsoluteRefType.ROW; - range.endAbsoluteRefType = AbsoluteRefType.ROW; - } else if (type === AbsoluteRefType.ROW) { - range.startAbsoluteRefType = AbsoluteRefType.COLUMN; - range.endAbsoluteRefType = AbsoluteRefType.COLUMN; - } else { - range.startAbsoluteRefType = AbsoluteRefType.NONE; - range.endAbsoluteRefType = AbsoluteRefType.NONE; - } - - resultToken = serializeRange(range); - return resultToken; - } - - private _getEditorObject() { - const docInstance = this._univerInstanceService.getCurrentUniverDocInstance(); - if (!docInstance) return; - const editorUnitId = docInstance.getUnitId(); - const editor = this._editorService.getEditor(editorUnitId); - return editor?.render; - } - - private _isFormulaEditorActivated(): boolean { - // TODO: Finally we will remove 'this._editorBridgeService.isVisible().visible === true' to - // just the the context value. - return this._editorBridgeService.isVisible().visible === true || this._contextService.getContextValue(FORMULA_EDITOR_ACTIVATED); - } - - private _isSheetOrFormulaEditor(editor: Editor): boolean { - return editor.isSheetEditor() || editor.isFormulaEditor(); - } -} diff --git a/packages/sheets-formula-ui/src/controllers/utils/utils.ts b/packages/sheets-formula-ui/src/controllers/utils/utils.ts index b092d20df47..33345832e0f 100644 --- a/packages/sheets-formula-ui/src/controllers/utils/utils.ts +++ b/packages/sheets-formula-ui/src/controllers/utils/utils.ts @@ -16,14 +16,15 @@ import type { ICellData, IContextService, Nullable } from '@univerjs/core'; import type { ErrorType } from '@univerjs/engine-formula'; -import { CellValueType, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR, FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, isFormulaId, isFormulaString } from '@univerjs/core'; +import { CellValueType, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR, isFormulaId, isFormulaString } from '@univerjs/core'; import { ERROR_TYPE_SET, stripErrorMargin } from '@univerjs/engine-formula'; export function whenEditorStandalone(contextService: IContextService) { return ( contextService.getContextValue(FOCUSING_DOC) && - contextService.getContextValue(FOCUSING_UNIVER_EDITOR) && - contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE) + contextService.getContextValue(FOCUSING_UNIVER_EDITOR) + // && + // contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE) ); } diff --git a/packages/sheets-formula-ui/src/services/render-services/ref-selections.render-service.ts b/packages/sheets-formula-ui/src/services/render-services/ref-selections.render-service.ts index 09122ad026b..cec5f75055a 100644 --- a/packages/sheets-formula-ui/src/services/render-services/ref-selections.render-service.ts +++ b/packages/sheets-formula-ui/src/services/render-services/ref-selections.render-service.ts @@ -269,7 +269,7 @@ export class RefSelectionsRenderService extends BaseSelectionRenderService imple * @param viewport * @param scrollTimerType */ - // eslint-disable-next-line complexity + // eslint-disable-next-line complexity, max-lines-per-function protected _onPointerDown( evt: IPointerEvent | IMouseEvent, _zIndex = 0, diff --git a/packages/sheets-formula-ui/src/sheets-formula-ui.plugin.ts b/packages/sheets-formula-ui/src/sheets-formula-ui.plugin.ts index 9f75df91cf3..565d8e7cd15 100644 --- a/packages/sheets-formula-ui/src/sheets-formula-ui.plugin.ts +++ b/packages/sheets-formula-ui/src/sheets-formula-ui.plugin.ts @@ -33,7 +33,6 @@ import { FormulaClipboardController } from './controllers/formula-clipboard.cont import { FormulaEditorShowController } from './controllers/formula-editor-show.controller'; import { FormulaRenderManagerController } from './controllers/formula-render.controller'; import { FormulaUIController } from './controllers/formula-ui.controller'; -import { PromptController } from './controllers/prompt.controller'; import { FormulaPromptService, IFormulaPromptService } from './services/prompt.service'; import { RefSelectionsRenderService } from './services/render-services/ref-selections.render-service'; import { FormulaEditor } from './views/formula-editor/index'; @@ -75,10 +74,13 @@ export class UniverSheetsFormulaUIPlugin extends Plugin { [FormulaClipboardController], [FormulaEditorShowController], [FormulaRenderManagerController], - [PromptController], ]; dependencies.forEach((dependency) => j.add(dependency)); + + const componentManager = this._injector.get(ComponentManager); + componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, RangeSelector); + componentManager.register(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, FormulaEditor); } override onRendered(): void { @@ -94,15 +96,9 @@ export class UniverSheetsFormulaUIPlugin extends Plugin { [FormulaClipboardController], [FormulaRenderManagerController], ]); - - const componentManager = this._injector.get(ComponentManager); - - componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, RangeSelector); - componentManager.register(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY, FormulaEditor); } override onSteady(): void { this._injector.get(FormulaAutoFillController); - this._injector.get(PromptController); } } diff --git a/packages/sheets-formula-ui/src/views/formula-editor/help-function/HelpFunction.tsx b/packages/sheets-formula-ui/src/views/formula-editor/help-function/HelpFunction.tsx index 18395e8eeca..e93dc573580 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/help-function/HelpFunction.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/help-function/HelpFunction.tsx @@ -14,159 +14,34 @@ * limitations under the License. */ -import type { IFunctionInfo, IFunctionParam } from '@univerjs/engine-formula'; +import type { Editor } from '@univerjs/docs-ui'; +import type { IFunctionParam } from '@univerjs/engine-formula'; import { LocaleService, useDependency } from '@univerjs/core'; -import { Popup } from '@univerjs/design'; -import { IEditorService } from '@univerjs/docs-ui'; import { CloseSingle, MoreSingle } from '@univerjs/icons'; -import { ISidebarService } from '@univerjs/ui'; -import React, { useEffect, useMemo, useState } from 'react'; -import { throttleTime } from 'rxjs'; +import { RectPopup } from '@univerjs/ui'; +import React, { useMemo, useState } from 'react'; import { generateParam } from '../../../services/utils'; -import { useResizeScrollObserver } from '../hooks/useResizeScrollObserver'; +import { useEditorPostion } from '../hooks/useEditorPostion'; +import { useFormulaDescribe } from '../hooks/useFormulaDescribe'; import styles from './index.module.less'; -interface IHelpFunctionProps { - functionInfo?: IFunctionInfo; - paramIndex: number; - editorId: string; - onParamsSwitch?: (index: number) => void; - onClose?: () => void; -}; -const noop = () => { }; -export function HelpFunction(props: IHelpFunctionProps) { - const { functionInfo, paramIndex, editorId, onParamsSwitch = noop, onClose = noop } = props; - - const editorService = useDependency(IEditorService); - const sidebarService = useDependency(ISidebarService); - - const visible = useMemo(() => !!functionInfo && paramIndex >= 0, [functionInfo, paramIndex]); - - const [contentVisible, setContentVisible] = useState(true); - const [offset, setOffset] = useState<[number, number]>([0, 0]); - const localeService = useDependency(LocaleService); - const required = localeService.t('formula.prompt.required'); - const optional = localeService.t('formula.prompt.optional'); - - useResizeScrollObserver(updatePosition); - - useEffect(() => { - const sidebarSubscription = sidebarService.scrollEvent$.pipe(throttleTime(100)).subscribe(updatePosition); - - return () => { - sidebarSubscription.unsubscribe(); - }; - }, []); - useEffect(() => { - const doc = editorService.getEditor(editorId); - if (!doc) { - return; - } - const position = doc.getBoundingClientRect(); - const { left, top, height } = position; - setOffset([left, top + height]); - }, [functionInfo, paramIndex, editorId]); - - function updatePosition() { - const doc = editorService.getEditor(editorId); - if (!doc) { - return; - } - const position = doc.getBoundingClientRect(); - const { left, top, height } = position; - setOffset([left, top + height]); - return position; - } - - function handleSwitchActive(paramIndex: number) { - onParamsSwitch && onParamsSwitch(paramIndex); - } - - return ( - - {functionInfo - ? ( -
-
- -
-
setContentVisible(!contentVisible)} - > - -
-
- -
-
-
- -
-
- item.example) - .join(',')})`} - /> - - {functionInfo && - functionInfo.functionParameter && - functionInfo.functionParameter.map((item: IFunctionParam, i: number) => ( - - ))} -
-
-
- ) - : ( - <> - )} -
- ); -} - interface IParamsProps { className?: string; title?: string; value?: string; } -const Params = (props: IParamsProps) => ( +const Params = ({ className, title, value }: IParamsProps) => (
- {props.title} + {title}
-
{props.value}
+
{value}
); @@ -187,8 +62,7 @@ const Help = (props: IHelpProps) => { {value && value.map((item: IFunctionParam, i: number) => ( - // TODO@Dushusir: more params needs to be active - + onClick(i)} @@ -202,3 +76,94 @@ const Help = (props: IHelpProps) => {
); }; + +interface IHelpFunctionProps { + onParamsSwitch?: (index: number) => void; + onClose?: () => void; + editor: Editor; + isFocus: boolean; + formulaText: string; +}; + +const noop = () => { }; +export function HelpFunction(props: IHelpFunctionProps) { + const { onParamsSwitch = noop, onClose: propColose = noop, isFocus, editor, formulaText } = props; + const { functionInfo, paramIndex, reset } = useFormulaDescribe(isFocus, formulaText, editor); + const visible = useMemo(() => !!functionInfo && paramIndex >= 0, [functionInfo, paramIndex]); + const [contentVisible, setContentVisible] = useState(true); + const localeService = useDependency(LocaleService); + const required = localeService.t('formula.prompt.required'); + const optional = localeService.t('formula.prompt.optional'); + const editorId = editor.getEditorId(); + const [position$] = useEditorPostion(editorId, visible, [functionInfo, paramIndex]); + function handleSwitchActive(paramIndex: number) { + onParamsSwitch && onParamsSwitch(paramIndex); + } + + const onClose = () => { + reset(); + propColose(); + }; + + return visible && functionInfo + ? ( + reset()} anchorRect$={position$} direction="vertical"> +
+
+ +
+
setContentVisible(!contentVisible)} + > + +
+
+ +
+
+
+
+
+ item.example) + .join(',')})`} + /> + + {functionInfo && + functionInfo.functionParameter && + functionInfo.functionParameter.map((item: IFunctionParam, i: number) => ( + + ))} +
+
+
+
+ ) + : null; +} diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useEditorPostion.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useEditorPostion.ts new file mode 100644 index 00000000000..943fe08dc32 --- /dev/null +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useEditorPostion.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IUniverInstanceService, useDependency } from '@univerjs/core'; +import { IEditorService } from '@univerjs/docs-ui'; +import { ISidebarService, useEvent } from '@univerjs/ui'; +import { useEffect, useMemo } from 'react'; +import { BehaviorSubject, throttleTime } from 'rxjs'; +import useResizeScrollObserver from './useResizeScrollObserver'; + +export function useEditorPostion(editorId: string, ready: boolean, deps?: any[]) { + const editorService = useDependency(IEditorService); + const position$ = useMemo(() => new BehaviorSubject({ left: -999, top: -999, right: -999, bottom: -999 }), []); + const sidebarService = useDependency(ISidebarService); + const univerInstanceService = useDependency(IUniverInstanceService); + const updatePosition = useEvent(() => { + const doc = editorService.getEditor(editorId); + if (!doc) { + return; + } + const position = doc.getBoundingClientRect(); + const { left, top, right, bottom } = position; + const current = position$.getValue(); + if (current.left === left && current.top === top && current.right === right && current.bottom === bottom) { + return; + } + position$.next({ left: left - 1, right: right + 1, top: top - 1, bottom: bottom + 1 }); + return position; + }); + + useEffect(() => { + if (!ready) { + return; + } + updatePosition(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editorId, editorService, univerInstanceService.unitAdded$, updatePosition, ready, ...(deps ?? [])]); + + useResizeScrollObserver(updatePosition); + + useEffect(() => { + const sidebarSubscription = sidebarService.scrollEvent$.pipe(throttleTime(100)).subscribe(updatePosition); + + return () => { + sidebarSubscription.unsubscribe(); + }; + }, []); + + return [position$, updatePosition] as const; +} diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaDescribe.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaDescribe.ts index 048898254a2..b8563228a7f 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaDescribe.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaDescribe.ts @@ -34,7 +34,6 @@ export const useFormulaDescribe = (isNeed: boolean, formulaText: string, editor? const formulaTextRef = useRef(formulaText); formulaTextRef.current = formulaText; - const reset = () => { functionInfoSet(undefined); paramIndexSet(-1); @@ -48,7 +47,7 @@ export const useFormulaDescribe = (isNeed: boolean, formulaText: string, editor? const [range] = e.textRanges; if (range.collapsed && isShowRef.current) { // 为什么减1,因为nodes是不包含初始 ‘=’ 字符的,但是 selection 会包含 '=' - const res = lexerTreeBuilder.getFunctionAndParameter(formulaTextRef.current, range.startOffset - 1); + const res = lexerTreeBuilder.getFunctionAndParameter(`${formulaTextRef.current}A`, range.startOffset - 1); if (res) { const { functionName, paramIndex } = res; const info = descriptionService.getFunctionInfo(functionName); @@ -83,6 +82,8 @@ export const useFormulaDescribe = (isNeed: boolean, formulaText: string, editor? }, [isNeed]); return { - functionInfo, paramIndex, reset, + functionInfo, + paramIndex, + reset, }; }; diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts new file mode 100644 index 00000000000..1c74133e4a0 --- /dev/null +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { ISequenceNode } from '@univerjs/engine-formula'; +import { Injector, IUniverInstanceService, useDependency } from '@univerjs/core'; +import { DocSelectionManagerService } from '@univerjs/docs'; +import { DocSelectionRenderService } from '@univerjs/docs-ui'; +import { isFormulaLexerToken, matchRefDrawToken, matchToken, sequenceNodeType } from '@univerjs/engine-formula'; +import { IRenderManagerService } from '@univerjs/engine-render'; +import { useEffect, useRef, useState } from 'react'; +import { distinctUntilChanged, filter, map } from 'rxjs'; + +function getCurrentBodyDataStreamAndOffset(accssor: IAccessor) { + const univerInstanceService = accssor.get(IUniverInstanceService); + const documentModel = univerInstanceService.getCurrentUniverDocInstance(); + + if (!documentModel?.getBody()) { + return; + } + + const dataStream = documentModel.getBody()?.dataStream ?? ''; + return { dataStream, offset: 0 }; +} + +export enum FormulaSelectingType { + NOT_SELECT = 0, + NEED_ADD = 1, + CAN_EDIT = 2, +} + +export function useFormulaSelecting(editorId: string, isFocus: boolean, nodes: (string | ISequenceNode)[]) { + const renderManagerService = useDependency(IRenderManagerService); + const renderer = renderManagerService.getRenderById(editorId); + const docSelectionRenderService = renderer?.with(DocSelectionRenderService); + const docSelectionManagerService = useDependency(DocSelectionManagerService); + const injector = useDependency(Injector); + const [isSelecting, setIsSelecting] = useState(FormulaSelectingType.NOT_SELECT); + const nodesRef = useRef(nodes); + nodesRef.current = nodes; + const isDisabledByPointer = useRef(true); + const isSelectingRef = useRef(isSelecting); + isSelectingRef.current = isSelecting; + + useEffect(() => { + const sub = docSelectionManagerService.textSelection$ + .pipe( + filter((param) => param.unitId === editorId), + map(() => { + const activeRange = docSelectionRenderService?.getActiveTextRange(); + const index = activeRange?.collapsed ? activeRange.startOffset! : -1; + return index; + }), + distinctUntilChanged() + ) + .subscribe((index) => { + const config = getCurrentBodyDataStreamAndOffset(injector); + if (!config) return; + const dataStream = config?.dataStream?.slice(0, -2); + const char = dataStream[index - 1 + config.offset]; + const nextChar = dataStream[index + config.offset]; + const focusingIndex = nodesRef.current.findIndex((node) => typeof node === 'object' && node.nodeType === sequenceNodeType.REFERENCE && index === node.endIndex + 2); + const adding = (char && matchRefDrawToken(char)) && (!nextChar || (isFormulaLexerToken(nextChar) && nextChar !== matchToken.OPEN_BRACKET)); + const editing = focusingIndex > -1; + + if (dataStream?.substring(0, 1) === '=' && (adding || editing)) { + if (editing) { + if (isDisabledByPointer.current) { + return; + } + setIsSelecting(FormulaSelectingType.CAN_EDIT); + } else { + isDisabledByPointer.current = false; + setIsSelecting(FormulaSelectingType.NEED_ADD); + } + } else { + setIsSelecting(FormulaSelectingType.NOT_SELECT); + } + }); + + return () => sub.unsubscribe(); + }, [docSelectionManagerService.textSelection$, docSelectionRenderService, editorId, injector]); + + useEffect(() => { + if (!isFocus) { + setIsSelecting(FormulaSelectingType.NOT_SELECT); + isDisabledByPointer.current = true; + } + }, [isFocus]); + + useEffect(() => { + const sub = renderer?.mainComponent?.onPointerDown$.subscribeEvent(() => { + setIsSelecting(FormulaSelectingType.NOT_SELECT); + isDisabledByPointer.current = true; + }); + + return () => sub?.unsubscribe(); + }, [renderer?.mainComponent?.onPointerDown$]); + + return { isSelecting }; +} diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSheetSelectionChange.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSheetSelectionChange.ts index 1ae2f2a23c6..580ba5ee7c6 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSheetSelectionChange.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSheetSelectionChange.ts @@ -18,12 +18,14 @@ import type { Workbook } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; - -import type { ISelectionWithCoord } from '@univerjs/sheets'; +import type { ISelectionWithCoord, ISetSelectionsOperationParams } from '@univerjs/sheets'; +import type { IRefSelection } from '../../range-selector/hooks/useHighlight'; import type { INode } from '../../range-selector/utils/filterReferenceNode'; -import { DisposableCollection, IUniverInstanceService, useDependency, useObservable } from '@univerjs/core'; +import { DisposableCollection, ICommandService, IUniverInstanceService, useDependency, useObservable } from '@univerjs/core'; +import { DocSelectionManagerService } from '@univerjs/docs'; import { deserializeRangeWithSheet, sequenceNodeType, serializeRange, serializeRangeWithSheet } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; +import { IRefSelectionsService, SetSelectionsOperation } from '@univerjs/sheets'; import { useEffect, useMemo, useRef } from 'react'; import { merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; @@ -34,6 +36,7 @@ import { sequenceNodeToText } from '../../range-selector/utils/sequenceNodeToTex import { unitRangesToText } from '../../range-selector/utils/unitRangesToText'; import { useStateRef } from '../hooks/useStateRef'; import { useSelectionAdd } from './useSelectionAdd'; +import { getFocusingReference } from './util'; const noop = (() => { }) as any; export const useSheetSelectionChange = ( @@ -41,13 +44,17 @@ export const useSheetSelectionChange = ( unitId: string, subUnitId: string, sequenceNodes: INode[], + refSelectionRef: React.MutableRefObject, isSupportAcrossSheet: boolean, + listenSelectionSet: boolean, editor?: Editor, - handleRangeChange: ((refString: string, offset: number, isEnd: boolean) => void) = noop) => { + handleRangeChange: ((refString: string, offset: number, isEnd: boolean, isModify?: boolean) => void) = noop +) => { const renderManagerService = useDependency(IRenderManagerService); const univerInstanceService = useDependency(IUniverInstanceService); - + const commandService = useDependency(ICommandService); const sequenceNodesRef = useStateRef(sequenceNodes); + const docSelectionManagerService = useDependency(DocSelectionManagerService); const { getIsNeedAddSelection } = useSelectionAdd(unitId, sequenceNodes, editor); @@ -56,14 +63,15 @@ export const useSheetSelectionChange = ( const sheetName = useMemo(() => getSheetNameById(subUnitId), [subUnitId]); const activeSheet = useObservable(workbook?.activeSheet$); const contextRef = useStateRef({ activeSheet, sheetName }); - const render = renderManagerService.getRenderById(unitId); const refSelectionsRenderService = render?.with(RefSelectionsRenderService); - + const refSelectionsService = useDependency(IRefSelectionsService); const isScalingRef = useRef(false); const scalingOptionRef = useRef<{ result: string; offset: number }>(); + useEffect(() => {}, []); + useEffect(() => { if (refSelectionsRenderService && isNeed) { let isFirst = true; @@ -80,14 +88,15 @@ export const useSheetSelectionChange = ( const docRange = currentDocSelections[0]; const offset = docRange.startOffset - 1; const sequenceNodes = [...sequenceNodesRef.current]; + const nodeIndex = findIndexFromSequenceNodes(sequenceNodes, offset, false); + if (getIsNeedAddSelection()) { if (offset !== 0) { - const index = findIndexFromSequenceNodes(sequenceNodes, offset, false); - if (index === -1 && sequenceNodes.length) { + if (nodeIndex === -1 && sequenceNodes.length) { return; } const range = selections[selections.length - 1]; - const lastNodes = sequenceNodes.splice(index + 1); + const lastNodes = sequenceNodes.splice(nodeIndex + 1); const rangeSheetId = range.rangeWithCoord.sheetId ?? subUnitId; const unitRangeName = { range: range.rangeWithCoord, @@ -95,7 +104,7 @@ export const useSheetSelectionChange = ( sheetName: getSheetNameById(rangeSheetId), }; const isAcrossSheet = rangeSheetId !== subUnitId; - const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet && isAcrossSheet); + const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet && isAcrossSheet, sheetName); sequenceNodes.push({ token: refRanges[0], nodeType: sequenceNodeType.REFERENCE } as any); const newSequenceNodes = [...sequenceNodes, ...lastNodes]; const result = sequenceNodeToText(newSequenceNodes); @@ -117,7 +126,7 @@ export const useSheetSelectionChange = ( } else { // 更新全部的 ref Selection let currentRefIndex = 0; - const currentText = sequenceNodes.map((item) => { + const newTokens = sequenceNodes.map((item) => { if (typeof item === 'string') { return item; } @@ -144,11 +153,19 @@ export const useSheetSelectionChange = ( unitId: selection.rangeWithCoord.unitId ?? unitId, sheetName: getSheetNameById(rangeSheetId), }; - const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet); + const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet, sheetName); return refRanges[0]; } return item.token; - }).join(''); + }); + let currentText = ''; + let newOffset; + newTokens.forEach((item, index) => { + currentText += item; + if (index === nodeIndex) { + newOffset = currentText.length; + } + }); const theLastList: string[] = []; for (let index = currentRefIndex; index <= selections.length - 1; index++) { const selection = selections[index]; @@ -159,16 +176,17 @@ export const useSheetSelectionChange = ( sheetName: getSheetNameById(rangeSheetId), }; const isAcrossSheet = rangeSheetId !== subUnitId; - const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet && isAcrossSheet); + const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet && isAcrossSheet, sheetName); theLastList.push(refRanges[0]); } const preNode = sequenceNodes[sequenceNodes.length - 1]; const isPreNodeRef = preNode && (typeof preNode === 'string' ? false : preNode.nodeType === sequenceNodeType.REFERENCE); const result = `${currentText}${theLastList.length && isPreNodeRef ? ',' : ''}${theLastList.join(',')}`; - handleRangeChange(result, result.length, true); + handleRangeChange(result, newOffset ?? result.length, true); } }; - const d1 = refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { + const disposableCollection = new DisposableCollection(); + disposableCollection.add(refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { handleSelectionsChange(selections); isScalingRef.current = false; if (scalingOptionRef.current) { @@ -176,15 +194,10 @@ export const useSheetSelectionChange = ( handleRangeChange(result, offset || -1, true); scalingOptionRef.current = undefined; } - }); - - // const d2 = refSelectionsRenderService.selectionMoving$.subscribe((selections) => { - // handleSelectionsChange(selections); - // }); + })); return () => { - d1.unsubscribe(); - // d2.unsubscribe(); + disposableCollection.dispose(); }; } }, [refSelectionsRenderService, editor, isSupportAcrossSheet, isNeed]); @@ -260,14 +273,19 @@ export const useSheetSelectionChange = ( map((e) => { return serializeRange(e); }), - distinctUntilChanged() + distinctUntilChanged(), + debounceTime(100) ).subscribe((rangeText) => { isScalingRef.current = true; handleSequenceNodeReplace(rangeText, index); })); }); }; - const dispose = merge(editor.input$, refSelectionsRenderService.selectionMoveEnd$).pipe(debounceTime(50)).subscribe(() => { + const dispose = merge( + editor.input$, + refSelectionsService.selectionSet$, + refSelectionsRenderService.selectionMoveEnd$).pipe(debounceTime(50) + ).subscribe(() => { reListen(); }); @@ -277,4 +295,80 @@ export const useSheetSelectionChange = ( }; } }, [isNeed, refSelectionsRenderService, editor]); + + useEffect(() => { + if (listenSelectionSet) { + const d = commandService.onCommandExecuted((commandInfo) => { + if (commandInfo.id !== SetSelectionsOperation.id) { + return; + } + + const params = commandInfo.params as ISetSelectionsOperationParams; + if (params.extra !== 'formula-editor') { + return; + } + const { selections } = params; + if (selections.length) { + const last = selections[selections.length - 1]; + if (last) { + const range = last.range; + const sheetId = subUnitId; + const unitRangeName = { + range, + unitId: params.unitId === unitId ? '' : params.unitId, + sheetName: params.subUnitId === sheetId ? '' : getSheetNameById(sheetId), + }; + const sequenceNodes = [...sequenceNodesRef.current]; + const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet, sheetName); + const result = refRanges[0]; + let lastNode = sequenceNodes[sequenceNodes.length - 1]; + if (typeof lastNode === 'object' && lastNode.nodeType === sequenceNodeType.REFERENCE) { + lastNode = { ...lastNode }; + lastNode.token = result; + lastNode.endIndex = lastNode.startIndex + result.length; + sequenceNodes[sequenceNodes.length - 1] = lastNode; + const refStr = sequenceNodeToText(sequenceNodes); + handleRangeChange(refStr, getOffsetFromSequenceNodes(sequenceNodes), true); + } else { + const start = getOffsetFromSequenceNodes(sequenceNodes); + sequenceNodes.push({ + nodeType: sequenceNodeType.REFERENCE, + token: result, + startIndex: start, + endIndex: start + result.length, + }); + + const refStr = sequenceNodeToText(sequenceNodes); + handleRangeChange(refStr, getOffsetFromSequenceNodes(sequenceNodes), true); + } + } + } + }); + + return () => { + d.dispose(); + }; + } + }, [commandService, getSheetNameById, handleRangeChange, isSupportAcrossSheet, listenSelectionSet, sequenceNodesRef]); + + useEffect(() => { + if (!editor) { + return; + } + const sub = docSelectionManagerService.textSelection$.subscribe((e) => { + const { unitId } = e; + if (unitId !== editor.getEditorId()) { + return; + } + + const focusingRef = getFocusingReference(editor, refSelectionRef.current); + if (focusingRef) { + refSelectionsRenderService?.setActiveSelectionIndex(focusingRef.index); + } else { + refSelectionsRenderService?.resetActiveSelectionIndex(); + } + }); + + return () => sub.unsubscribe(); + }, [docSelectionManagerService.textSelection$, editor, refSelectionRef, refSelectionsRenderService, sequenceNodesRef]); }; diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useStateRef.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useStateRef.ts index 37f2bff40ae..a9cdf3afc04 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useStateRef.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useStateRef.ts @@ -17,7 +17,7 @@ import { useRef } from 'react'; export const useStateRef = (value: T) => { - const cache = useRef(); + const cache = useRef(value); cache.current = value; - return cache as { current: T }; + return cache; }; diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/util.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/util.ts new file mode 100644 index 00000000000..cd05f4dffdf --- /dev/null +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/util.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Editor } from '@univerjs/docs-ui'; +import type { IRefSelection } from '../../range-selector/hooks/useHighlight'; + +export function getFocusingReference(editor: Editor, refSelections: IRefSelection[]) { + const cursor = editor.getSelectionRanges()?.[0]?.startOffset; + if (cursor) { + return refSelections.find((node) => node.endIndex + 2 === cursor); + } +} diff --git a/packages/sheets-formula-ui/src/views/formula-editor/index.module.less b/packages/sheets-formula-ui/src/views/formula-editor/index.module.less index 878daf0feb0..f213596b76b 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.module.less +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.module.less @@ -21,17 +21,15 @@ height: 100%; position: relative; } - - .sheet-embedding-formula-editor-error-wrap { - font-size: 12px; - color: rgb(var(--red-500)); - position: absolute; - bottom: -18px; - left: 0px; - } } &-error { border: 1px solid rgb(var(--red-500)) !important; } + + .sheet-embedding-formula-editor-error-wrap { + font-size: 12px; + color: rgb(var(--red-500)); + margin: var(--margin-xxs) 0; + } } diff --git a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx index 803f5a9ba75..5f130801132 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -14,31 +14,35 @@ * limitations under the License. */ -import type { IDisposable } from '@univerjs/core'; +import type { DocumentDataModel, IDisposable } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; +import type { KeyCode, MetaKeys } from '@univerjs/ui'; import type { ReactNode } from 'react'; -import { createInternalEditorID, generateRandomId, useDependency } from '@univerjs/core'; -import { DocBackScrollRenderController, IEditorService } from '@univerjs/docs-ui'; -import { operatorToken } from '@univerjs/engine-formula'; +import type { IRefSelection } from '../range-selector/hooks/useHighlight'; +import type { IKeyboardEventConfig } from '../range-selector/hooks/useKeyboardEvent'; +import type { FormulaSelectingType } from './hooks/useFormulaSelection'; +import { BuildTextUtils, createInternalEditorID, generateRandomId, IUniverInstanceService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; +import { DocBackScrollRenderController, DocSelectionRenderService, IEditorService } from '@univerjs/docs-ui'; +import { IRenderManagerService } from '@univerjs/engine-render'; import { EMBEDDING_FORMULA_EDITOR } from '@univerjs/sheets-ui'; +import { useEvent } from '@univerjs/ui'; import clsx from 'clsx'; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useEmitChange } from '../range-selector/hooks/useEmitChange'; -import { useFirstHighlightDoc } from '../range-selector/hooks/useFirstHighlightDoc'; import { useFocus } from '../range-selector/hooks/useFocus'; import { useFormulaToken } from '../range-selector/hooks/useFormulaToken'; import { useDocHight, useSheetHighlight } from '../range-selector/hooks/useHighlight'; +import { useKeyboardEvent } from '../range-selector/hooks/useKeyboardEvent'; import { useLeftAndRightArrow } from '../range-selector/hooks/useLeftAndRightArrow'; import { useRefactorEffect } from '../range-selector/hooks/useRefactorEffect'; -import { useRefocus } from '../range-selector/hooks/useRefocus'; import { useResetSelection } from '../range-selector/hooks/useResetSelection'; import { useResize } from '../range-selector/hooks/useResize'; import { useSwitchSheet } from '../range-selector/hooks/useSwitchSheet'; import { HelpFunction } from './help-function/HelpFunction'; -import { useFormulaDescribe } from './hooks/useFormulaDescribe'; -import { useFormulaSearch } from './hooks/useFormulaSearch'; +import { useFormulaSelecting } from './hooks/useFormulaSelection'; import { useSheetSelectionChange } from './hooks/useSheetSelectionChange'; import { useVerify } from './hooks/useVerify'; +import { getFocusingReference } from './hooks/util'; import styles from './index.module.less'; import { SearchFunction } from './search-function/SearchFunction'; import { getFormulaText } from './utils/getFormulaText'; @@ -57,26 +61,44 @@ export interface IFormulaEditorProps { actions?: { handleOutClick?: (e: MouseEvent, cb: () => void) => void; }; + className?: string; + editorId?: string; + moveCursor?: boolean; + onFormulaSelectingChange?: (isSelecting: FormulaSelectingType) => void; + keyboradEventConfig?: IKeyboardEventConfig; + onMoveInEditor?: (keyCode: KeyCode, metaKey?: MetaKeys) => void; + resetSelectionOnBlur?: boolean; + isSingle?: boolean; + autoScrollbar?: boolean; } + const noop = () => { }; export function FormulaEditor(props: IFormulaEditorProps) { - const { errorText, initValue, unitId, subUnitId, isFocus: _isFocus = true, isSupportAcrossSheet = false, - onFocus = noop, - onBlur = noop, - onChange, - onVerify, - actions, + const { + errorText, + initValue, + unitId, + subUnitId, + isFocus: _isFocus = true, + isSupportAcrossSheet = false, + onFocus = noop, + onBlur = noop, + onChange, + onVerify, + actions, + className, + editorId: propEditorId, + moveCursor = true, + onFormulaSelectingChange: propOnFormulaSelectingChange, + keyboradEventConfig, + onMoveInEditor, + resetSelectionOnBlur = true, + autoScrollbar = true, + isSingle = true, } = props; const editorService = useDependency(IEditorService); - const sheetEmbeddingRef = useRef(null); - const [formulaText, formulaTextSet] = useState(() => { - if (initValue.startsWith(operatorToken.EQUALS)) { - return initValue; - } - return ''; - }); // init actions if (actions) { @@ -88,105 +110,84 @@ export function FormulaEditor(props: IFormulaEditorProps) { }; } - const formulaWithoutEqualSymbol = useMemo(() => { - return getFormulaText(formulaText); - }, [formulaText]); - + const onFormulaSelectingChange = useEvent(propOnFormulaSelectingChange); const searchFunctionRef = useRef(null); - const [editor, editorSet] = useState(); + const editorRef = useRef(); + const editor = editorRef.current; const [isFocus, isFocusSet] = useState(_isFocus); const formulaEditorContainerRef = useRef(null); - const editorId = useMemo(() => createInternalEditorID(`${EMBEDDING_FORMULA_EDITOR}-${generateRandomId(4)}`), []); + const editorId = useMemo(() => propEditorId ?? createInternalEditorID(`${EMBEDDING_FORMULA_EDITOR}-${generateRandomId(4)}`), []); const isError = useMemo(() => errorText !== undefined, [errorText]); - + const univerInstanceService = useDependency(IUniverInstanceService); + const document = univerInstanceService.getUnit(editorId); + useObservable(document?.change$); const getFormulaToken = useFormulaToken(); - const sequenceNodes = useMemo(() => getFormulaToken(formulaWithoutEqualSymbol), [formulaWithoutEqualSymbol]); - + const formulaText = BuildTextUtils.transform.getPlainText(document?.getBody()?.dataStream ?? ''); + const formulaWithoutEqualSymbol = useMemo(() => getFormulaText(formulaText), [formulaText]); + const sequenceNodes = useMemo(() => getFormulaToken(formulaWithoutEqualSymbol), [formulaWithoutEqualSymbol, getFormulaToken]); + const { isSelecting } = useFormulaSelecting(editorId, isFocus, sequenceNodes); + const highTextRef = useRef(''); + const renderManagerService = useDependency(IRenderManagerService); + const renderer = renderManagerService.getRenderById(editorId); + const docSelectionRenderService = renderer?.with(DocSelectionRenderService); + const isFocusing = docSelectionRenderService?.isFocusing; + const currentDoc$ = useMemo(() => univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC), [univerInstanceService]); + const currentDoc = useObservable(currentDoc$); + const docFocusing = currentDoc?.getUnitId() === editorId; + const refSelections = useRef([] as IRefSelection[]); + const selectingMode = isSelecting; const needEmit = useEmitChange(sequenceNodes, (text: string) => { onChange(`=${text}`); }, editor); const highlightDoc = useDocHight('='); const highlightSheet = useSheetHighlight(unitId); - const highligh = (text: string, isNeedResetSelection: boolean = true) => { - if (!editor) { + const highlight = useEvent((text: string, isNeedResetSelection: boolean = true, isEnd?: boolean) => { + if (!editorRef.current) { return; } - const sequenceNodes = getFormulaToken(text); - const ranges = highlightDoc(editor, sequenceNodes, isNeedResetSelection); - highlightSheet(ranges); - }; - - // const refSelections = useDocHight(editorId, sequenceNodes); - useVerify(isFocus, onVerify, formulaText); - const focus = useFocus(editor); + const preText = highTextRef.current; + highTextRef.current = text; + const sequenceNodes = getFormulaToken(text[0] === '=' ? text.slice(1) : ''); + const ranges = highlightDoc( + editorRef.current, + sequenceNodes, + isNeedResetSelection, + // remove equals need to remove highlight style + preText.slice(1) === text && preText[0] === '=' + ); + refSelections.current = ranges; - const resetSelection = useResetSelection(isFocus); + if (isEnd) { + highlightSheet(isFocus ? ranges : [], getFocusingReference(editorRef.current, ranges)); + } + }); - useLayoutEffect(() => { - // 在进行多个 input 切换的时候,失焦必须快于获得焦点. - if (_isFocus) { - const time = setTimeout(() => { - isFocusSet(_isFocus); - if (_isFocus) { - focus(); - } - }, 30); - return () => { - clearTimeout(time); - }; - } else { - resetSelection(); - isFocusSet(_isFocus); + useEffect(() => { + if (isFocus) { + highlight(formulaText, false, true); } - }, [_isFocus, focus]); + }, [isFocus]); - const { checkScrollBar } = useResize(editor); - useRefactorEffect(isFocus, unitId); - useLeftAndRightArrow(isFocus, editor); + useEffect(() => { + const sub = docSelectionRenderService?.onChangeByEvent$.subscribe((e) => { + const formulaText = BuildTextUtils.transform.getPlainText(document?.getBody()?.dataStream ?? ''); + highlight(formulaText, false, true); + }); - const handleSelectionChange = (refString: string, offset: number, isEnd: boolean) => { - const result = `=${refString}`; - needEmit(); - formulaTextSet(result); - highligh(refString); - if (isEnd) { - focus(); - if (offset !== -1) { - // 在渲染结束之后再设置选区 - setTimeout(() => { - const range = { startOffset: offset + 1, endOffset: offset + 1 }; - editor?.setSelectionRanges([range]); - const docBackScrollRenderController = editor?.render.with(DocBackScrollRenderController); - docBackScrollRenderController?.scrollToRange({ ...range, collapsed: true }); - }, 50); - } - checkScrollBar(); - } - }; - useSheetSelectionChange(isFocus, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, editor, handleSelectionChange); + return () => sub?.unsubscribe(); + }, [docSelectionRenderService?.onChangeByEvent$, document, highlight]); - useRefocus(); - useSwitchSheet(isFocus, unitId, isSupportAcrossSheet, isFocusSet, onBlur, noop); + useVerify(isFocus, onVerify, formulaText); + const focus = useFocus(editor); - const { searchList, searchText, handlerFormulaReplace, reset: resetFormulaSearch } = useFormulaSearch(isFocus, sequenceNodes, editor); - const { functionInfo, paramIndex, reset } = useFormulaDescribe(isFocus, formulaText, editor); + const resetSelection = useResetSelection(isFocus, unitId, subUnitId); useEffect(() => { - if (editor) { - const d = editor.input$.subscribe((e) => { - const text = (e.data.body?.dataStream ?? '').replaceAll(/\n|\r/g, ''); - needEmit(); - formulaTextSet(text); - highligh(getFormulaText(text), false); - }); - return () => { - d.unsubscribe(); - }; - } - }, [editor]); + onFormulaSelectingChange(isSelecting); + }, [onFormulaSelectingChange, isSelecting]); - useFirstHighlightDoc(formulaWithoutEqualSymbol, '=', isFocus, highlightDoc, highlightSheet, editor); + useKeyboardEvent(isFocus, keyboradEventConfig, editor); useLayoutEffect(() => { let dispose: IDisposable; @@ -194,15 +195,21 @@ export function FormulaEditor(props: IFormulaEditorProps) { dispose = editorService.register({ autofocus: true, editorUnitId: editorId, - isSingle: true, initialSnapshot: { id: editorId, - body: { dataStream: `${initValue}\r\n` }, + body: { + dataStream: `${initValue}\r\n`, + textRuns: [], + customBlocks: [], + customDecorations: [], + customRanges: [], + }, documentStyle: {}, }, }, formulaEditorContainerRef.current); const editor = editorService.getEditor(editorId)! as Editor; - editorSet(editor); + editorRef.current = editor; + highlight(initValue, false, true); } return () => { @@ -210,10 +217,59 @@ export function FormulaEditor(props: IFormulaEditorProps) { }; }, []); - const handleFunctionSelect = (v: string) => { - const res = handlerFormulaReplace(v); + useLayoutEffect(() => { + if (_isFocus) { + isFocusSet(_isFocus); + focus(); + } else { + if (resetSelectionOnBlur) { + editor?.blur(); + resetSelection(); + } + isFocusSet(_isFocus); + } + }, [_isFocus, editor, focus, resetSelection, resetSelectionOnBlur]); + + const { checkScrollBar } = useResize(editor, isSingle, autoScrollbar); + useRefactorEffect(isFocus, Boolean(isSelecting && docFocusing), unitId); + useLeftAndRightArrow(isFocus && moveCursor, selectingMode, editor, onMoveInEditor); + + const handleSelectionChange = useEvent((refString: string, offset: number, isEnd: boolean) => { + if (!isFocusing) { + return; + } + needEmit(); + highlight(`=${refString}`, true, isEnd); + if (isEnd) { + focus(); + if (offset !== -1) { + // 在渲染结束之后再设置选区 + setTimeout(() => { + const range = { startOffset: offset + 1, endOffset: offset + 1 }; + editor?.setSelectionRanges([range]); + const docBackScrollRenderController = editor?.render.with(DocBackScrollRenderController); + docBackScrollRenderController?.scrollToRange({ ...range, collapsed: true }); + }, 50); + } + checkScrollBar(); + } + }); + + useSheetSelectionChange( + isFocus && Boolean(isSelecting && docFocusing), + unitId, + subUnitId, + sequenceNodes, + refSelections, + isSupportAcrossSheet, + Boolean(selectingMode), + editor, + handleSelectionChange + ); + useSwitchSheet(isFocus && Boolean(isSelecting && docFocusing), unitId, isSupportAcrossSheet, isFocusSet, onBlur, noop); + + const handleFunctionSelect = (res: { text: string; offset: number }) => { if (res) { - formulaTextSet(`=${res.text}`); const selections = editor?.getSelectionRanges(); if (selections && selections.length === 1) { const range = selections[0]; @@ -224,26 +280,18 @@ export function FormulaEditor(props: IFormulaEditorProps) { }, 30); } } - resetFormulaSearch(); focus(); - highligh(res.text); + highlight(`=${res.text}`); } }; const handleMouseUp = () => { - // 在进行多个 input 切换的时候,失焦必须快于获得焦点. - // 即使失焦是 mousedown 事件, - // 聚焦是 mouseup 事件, - // 但是 react 的 useEffect 无法保证顺序,无法确保失焦在聚焦之前. - - setTimeout(() => { - isFocusSet(true); - onFocus(); - focus(); - }, 30); + isFocusSet(true); + onFocus(); + focus(); }; return ( -
+
- {errorText !== undefined ?
{errorText}
: null}
- { - reset(); - focus(); - }} - > - - - + {errorText !== undefined ?
{errorText}
: null} + {editor + ? ( + focus()} + /> + ) + : null} + {editor + ? ( + + ) + : null}
) ; diff --git a/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx b/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx index b93734b7498..7221ef37b1d 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/search-function/SearchFunction.tsx @@ -14,52 +14,50 @@ * limitations under the License. */ -import type { ISearchItem } from '@univerjs/sheets-formula'; +import type { Editor } from '@univerjs/docs-ui'; +import type { ISequenceNode } from '@univerjs/engine-formula'; import { CommandType, DisposableCollection, ICommandService, useDependency } from '@univerjs/core'; -import { Popup } from '@univerjs/design'; -import { IEditorService } from '@univerjs/docs-ui'; import { DeviceInputEventType } from '@univerjs/engine-render'; -import { IShortcutService, KeyCode } from '@univerjs/ui'; +import { IShortcutService, KeyCode, RectPopup } from '@univerjs/ui'; import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; +import { useEditorPostion } from '../hooks/useEditorPostion'; +import { useFormulaSearch } from '../hooks/useFormulaSearch'; import { useStateRef } from '../hooks/useStateRef'; import styles from './index.module.less'; interface ISearchFunctionProps { - searchList: ISearchItem[]; - searchText: string; - onSelect: (functionName: string) => void; + isFocus: boolean; + sequenceNodes: (string | ISequenceNode)[]; + onSelect: (data: { + text: string; + offset: number; + }) => void; onChange?: (functionName: string) => void; - editorId: string; + editor: Editor; onClose?: () => void; }; const noop = () => { }; export const SearchFunction = forwardRef(SearchFunctionFactory); function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { - const { searchText, searchList, onSelect, editorId, onClose = noop } = props; - const editorService = useDependency(IEditorService); + const { isFocus, sequenceNodes, onSelect, editor, onClose = noop } = props; + const editorId = editor.getEditorId(); const shortcutService = useDependency(IShortcutService); const commandService = useDependency(ICommandService); - + const { searchList, searchText, handlerFormulaReplace, reset: resetFormulaSearch } = useFormulaSearch(isFocus, sequenceNodes, editor); const visible = useMemo(() => !!searchList.length, [searchList]); const ulRef = useRef(); const [active, activeSet] = useState(0); - const [offset, setOffset] = useState<[number, number]>([0, 0]); const isEnableMouseEnterOrOut = useRef(false); - + const [position$] = useEditorPostion(editorId, visible, [searchText, searchList]); const stateRef = useStateRef({ searchList, active }); - const editor = editorService.getEditor(editorId); - useEffect(() => { - const editor = editorService.getEditor(editorId); - const position = editor?.getBoundingClientRect(); - if (position == null) { - return; + const handleFunctionSelect = (v: string) => { + const res = handlerFormulaReplace(v); + if (res) { + resetFormulaSearch(); + onSelect(res); } - const { left, top, height } = position; - - setOffset([left, top + height]); - activeSet(0); // Reset active state - }, [searchText, searchList]); + }; function handleLiMouseEnter(index: number) { if (!isEnableMouseEnterOrOut.current) { @@ -106,11 +104,12 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { case KeyCode.TAB: case KeyCode.ENTER: { const item = searchList[active]; - onSelect(item.name); + handleFunctionSelect(item.name); break; } case KeyCode.ESC: { - onSelect(''); + resetFormulaSearch(); + onClose(); break; } } @@ -193,8 +192,8 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { }; }, []); - return searchList.length > 0 && ( - + return searchList.length > 0 && visible && ( +
    { @@ -206,7 +205,7 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) { > {searchList.map((item, index) => (
  • { - onSelect(item.name); + handleFunctionSelect(item.name); if (editor) { editor.focus(); } @@ -231,6 +230,6 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
  • ))}
-
+ ); } diff --git a/packages/sheets-formula-ui/src/views/more-functions/MoreFunctions.tsx b/packages/sheets-formula-ui/src/views/more-functions/MoreFunctions.tsx index 886c42df9af..f34c5bba854 100644 --- a/packages/sheets-formula-ui/src/views/more-functions/MoreFunctions.tsx +++ b/packages/sheets-formula-ui/src/views/more-functions/MoreFunctions.tsx @@ -15,10 +15,12 @@ */ import type { IFunctionInfo } from '@univerjs/engine-formula'; -import { LocaleService, useDependency } from '@univerjs/core'; +import { DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, IUniverInstanceService, LocaleService, useDependency } from '@univerjs/core'; import { Button } from '@univerjs/design'; import { IEditorService } from '@univerjs/docs-ui'; -import { useActiveWorkbook } from '@univerjs/sheets-ui'; +import { DeviceInputEventType } from '@univerjs/engine-render'; +import { getSheetCommandTarget } from '@univerjs/sheets'; +import { IEditorBridgeService, useActiveWorkbook } from '@univerjs/sheets-ui'; import React, { useState } from 'react'; import styles from './index.module.less'; import { InputParams } from './input-params/InputParams'; @@ -30,9 +32,10 @@ export function MoreFunctions() { const [inputParams, setInputParams] = useState(false); // const [params, setParams] = useState([]); // TODO@Dushusir: bind setParams to InputParams's onChange const [functionInfo, setFunctionInfo] = useState(null); - + const editorBridgeService = useDependency(IEditorBridgeService); const localeService = useDependency(LocaleService); const editorService = useDependency(IEditorService); + const univerInstanceService = useDependency(IUniverInstanceService); function handleClickNextPrev() { if (selectFunction) { @@ -44,8 +47,18 @@ export function MoreFunctions() { } function handleConfirm() { - // TODO@Dushusir: save function `=${functionInfo?.functionName}(${params.join(',')})` - editorService.setFormula(`=${functionInfo?.functionName}(`); + const sheetTarget = getSheetCommandTarget(univerInstanceService); + if (!sheetTarget) return; + editorBridgeService.changeVisible({ + visible: true, + unitId: sheetTarget.unitId, + eventType: DeviceInputEventType.Dblclick, + }); + const editor = editorService.getEditor(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); + const formulaEditor = editorService.getEditor(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY); + const formulaText = `=${functionInfo?.functionName}(`; + editor?.replaceText(formulaText); + formulaEditor?.replaceText(formulaText, false); } return ( diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useFocus.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFocus.ts index 60a25bb957b..c9e582e404a 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useFocus.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFocus.ts @@ -15,26 +15,26 @@ */ import type { Editor } from '@univerjs/docs-ui'; -import { useMemo } from 'react'; +import { Tools } from '@univerjs/core'; +import { useCallback } from 'react'; export const useFocus = (editor?: Editor) => { - const focus = useMemo(() => { - return () => { - if (editor) { - editor.focus(); - const selections = [...editor.getSelectionRanges()]; - if (selections.length) { - editor.setSelectionRanges(selections); - } - // end - if (!selections.length) { - const body = editor.getDocumentData().body?.dataStream ?? '\r\n'; - const offset = Math.max(body.length - 2, 0); - editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); - } + const focus = useCallback((offset?: number) => { + if (editor) { + editor.focus(); + const selections = [...editor.getSelectionRanges()]; + if (Tools.isDefine(offset)) { + editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); + } else if (selections.length) { + editor.setSelectionRanges(selections); + } else { + const body = editor.getDocumentData().body?.dataStream ?? '\r\n'; + const offset = Math.max(body.length - 2, 0); + editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); } }; }, [editor]); + return focus; }; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useFormulaToken.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFormulaToken.ts index 6d90ce9e0b8..253fdad8073 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useFormulaToken.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFormulaToken.ts @@ -17,10 +17,11 @@ import type { ISequenceNode } from '@univerjs/engine-formula'; import { useDependency } from '@univerjs/core'; import { LexerTreeBuilder } from '@univerjs/engine-formula'; +import { useCallback } from 'react'; export type INode = (string | ISequenceNode); export const useFormulaToken = () => { const lexerTreeBuilder = useDependency(LexerTreeBuilder); - const getFormulaToken = (text: string) => lexerTreeBuilder.sequenceNodesBuilder(text) || []; + const getFormulaToken = useCallback((text: string) => lexerTreeBuilder.sequenceNodesBuilder(text) || [], [lexerTreeBuilder]); return getFormulaToken; }; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useHighlight.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useHighlight.ts index 0111c0778db..d695bf9dce8 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useHighlight.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useHighlight.ts @@ -14,25 +14,29 @@ * limitations under the License. */ -import type { ITextRun, Workbook } from '@univerjs/core'; +import type { ITextRun, Nullable, Workbook } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { ISequenceNode } from '@univerjs/engine-formula'; import type { ISelectionWithStyle } from '@univerjs/sheets'; import type { INode } from './useFormulaToken'; -import { IUniverInstanceService, ThemeService, useDependency } from '@univerjs/core'; +import { getBodySlice, ICommandService, IUniverInstanceService, ThemeService, useDependency } from '@univerjs/core'; +import { ReplaceTextRunsCommand } from '@univerjs/docs-ui'; import { deserializeRangeWithSheet, sequenceNodeType } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; import { IRefSelectionsService, setEndForRange } from '@univerjs/sheets'; import { IDescriptionService } from '@univerjs/sheets-formula'; import { SheetSkeletonManagerService } from '@univerjs/sheets-ui'; -import { useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { genFormulaRefSelectionStyle } from '../../../common/selection'; import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; -interface IRefSelection { +export interface IRefSelection { refIndex: number; themeColor: string; token: string; + startIndex: number; + endIndex: number; + index: number; } /** @@ -50,7 +54,7 @@ export function useSheetHighlight(unitId: string) { const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - const highlightSheet = (refSelections: IRefSelection[]) => { + const highlightSheet = useCallback((refSelections: IRefSelection[], focusingRef: Nullable) => { const workbook = univerInstanceService.getUnit(unitId); const worksheet = workbook?.getActiveSheet(); const selectionWithStyle: ISelectionWithStyle[] = []; @@ -94,17 +98,32 @@ export function useSheetHighlight(unitId: string) { } else { refSelectionsService.setSelections(selectionWithStyle); } - }; + + if (focusingRef) { + refSelectionsRenderService?.setActiveSelectionIndex(focusingRef.index); + } else { + refSelectionsRenderService?.resetActiveSelectionIndex(); + } + }, [refSelectionsRenderService, refSelectionsService, sheetSkeletonManagerService, themeService, unitId, univerInstanceService]); + + useEffect(() => { + return () => { + refSelectionsRenderService?.resetActiveSelectionIndex(); + }; + }, [refSelectionsRenderService]); + return highlightSheet; } export function useDocHight(_leadingCharacter: string = '') { const descriptionService = useDependency(IDescriptionService); const colorMap = useColor(); + const commandService = useDependency(ICommandService); const leadingCharacterLength = useMemo(() => _leadingCharacter.length, [_leadingCharacter]); - const highlightDoc = (editor: Editor, sequenceNodes: INode[], isNeedResetSelection = true) => { + const highlightDoc = useCallback((editor: Editor, sequenceNodes: INode[], isNeedResetSelection = true, clearTextRun = true) => { const data = editor.getDocumentData(); + const editorId = editor.getEditorId(); if (!data) { return []; } @@ -114,9 +133,13 @@ export function useDocHight(_leadingCharacter: string = '') { } const cloneBody = { dataStream: '', ...data.body }; if (sequenceNodes == null || sequenceNodes.length === 0) { - cloneBody.textRuns = []; - const cloneData = { ...data, body: cloneBody }; - editor.setDocumentData(cloneData); + if (clearTextRun) { + cloneBody.textRuns = []; + commandService.syncExecuteCommand(ReplaceTextRunsCommand.id, { + unitId: editorId, + body: getBodySlice(cloneBody, 0, cloneBody.dataStream.length - 2), + }); + } return []; } else { const { textRuns, refSelections } = buildTextRuns(descriptionService, colorMap, sequenceNodes); @@ -146,15 +169,25 @@ export function useDocHight(_leadingCharacter: string = '') { }); } - const cloneData = { ...data, body: cloneBody }; - editor.setDocumentData(cloneData, selections); + commandService.syncExecuteCommand(ReplaceTextRunsCommand.id, { + unitId: editorId, + body: getBodySlice(cloneBody, 0, cloneBody.dataStream.length - 2), + textRanges: selections, + }); return refSelections; } - }; + }, [commandService, descriptionService, colorMap, leadingCharacterLength, _leadingCharacter]); return highlightDoc; } -export function useColor() { +interface IColorMap { + formulaRefColors: string[]; + numberColor: string; + stringColor: string; + plainTextColor: string; +} + +export function useColor(): IColorMap { const themeService = useDependency(ThemeService); const style = themeService.getCurrentTheme(); const result = useMemo(() => { @@ -174,17 +207,15 @@ export function useColor() { ]; const numberColor = style.hyacinth700; const stringColor = style.verdancy800; - return { formulaRefColors, numberColor, stringColor }; + const plainTextColor = style.colorBlack; + return { formulaRefColors, numberColor, stringColor, plainTextColor }; }, [style]); return result; } -export function buildTextRuns(descriptionService: IDescriptionService, colorMap: { - formulaRefColors: string[]; - numberColor: string; - stringColor: string; -}, sequenceNodes: Array) { - const { formulaRefColors, numberColor, stringColor } = colorMap; +// eslint-disable-next-line max-lines-per-function +export function buildTextRuns(descriptionService: IDescriptionService, colorMap: IColorMap, sequenceNodes: Array) { + const { formulaRefColors, numberColor, stringColor, plainTextColor } = colorMap; const textRuns: ITextRun[] = []; const refSelections: IRefSelection[] = []; const themeColorMap = new Map(); @@ -199,10 +230,24 @@ export function buildTextRuns(descriptionService: IDescriptionService, colorMap: textRuns.push({ st: start, ed: end, + ts: { + cl: { + rgb: plainTextColor, + }, + }, }); continue; } if (descriptionService.hasDefinedNameDescription(node.token.trim())) { + textRuns.push({ + st: node.startIndex, + ed: node.endIndex + 1, + ts: { + cl: { + rgb: plainTextColor, + }, + }, + }); continue; } const { startIndex, endIndex, nodeType, token } = node; @@ -221,6 +266,9 @@ export function buildTextRuns(descriptionService: IDescriptionService, colorMap: refIndex: i, themeColor, token, + startIndex: node.startIndex, + endIndex: node.endIndex, + index: refSelections.length, }); } else if (nodeType === sequenceNodeType.NUMBER) { themeColor = numberColor; @@ -240,6 +288,16 @@ export function buildTextRuns(descriptionService: IDescriptionService, colorMap: }, }, }); + } else { + textRuns.push({ + st: startIndex, + ed: endIndex + 1, + ts: { + cl: { + rgb: plainTextColor, + }, + }, + }); } } diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useKeyboardEvent.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useKeyboardEvent.ts new file mode 100644 index 00000000000..5a21a0682c7 --- /dev/null +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useKeyboardEvent.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { type IKeyboardEventConfig, useKeyboardEvent } from '@univerjs/docs-ui'; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useLeftAndRightArrow.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useLeftAndRightArrow.ts index a3fcd0135c6..9d79391c670 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useLeftAndRightArrow.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useLeftAndRightArrow.ts @@ -14,16 +14,24 @@ * limitations under the License. */ -import type { Editor } from '@univerjs/docs-ui'; -import { CommandType, DisposableCollection, ICommandService, useDependency } from '@univerjs/core'; +import { CommandType, Direction, DisposableCollection, ICommandService, useDependency } from '@univerjs/core'; +import { type Editor, MoveCursorOperation, MoveSelectionOperation } from '@univerjs/docs-ui'; import { DeviceInputEventType } from '@univerjs/engine-render'; -import { IShortcutService, KeyCode } from '@univerjs/ui'; -import { useEffect } from 'react'; +import { ExpandSelectionCommand, JumpOver, MoveSelectionCommand } from '@univerjs/sheets-ui'; +import { IShortcutService, KeyCode, MetaKeys } from '@univerjs/ui'; +import { useEffect, useRef } from 'react'; +import { FormulaSelectingType } from '../../formula-editor/hooks/useFormulaSelection'; -export const useLeftAndRightArrow = (isNeed: boolean, editor?: Editor) => { +// eslint-disable-next-line max-lines-per-function +export const useLeftAndRightArrow = (isNeed: boolean, shouldMoveSelection: FormulaSelectingType, editor?: Editor, onMoveInEditor?: (keyCode: KeyCode, metaKey?: MetaKeys) => void) => { const commandService = useDependency(ICommandService); const shortcutService = useDependency(IShortcutService); + const shouldMoveSelectionRef = useRef(shouldMoveSelection); + shouldMoveSelectionRef.current = shouldMoveSelection; + const onMoveInEditorRef = useRef(onMoveInEditor); + onMoveInEditorRef.current = onMoveInEditor; + // eslint-disable-next-line max-lines-per-function useEffect(() => { if (!editor || !isNeed) { return; @@ -31,23 +39,71 @@ export const useLeftAndRightArrow = (isNeed: boolean, editor?: Editor) => { const editorId = editor.getEditorId(); const operationId = `sheet.formula-embedding-editor.${editorId}`; const d = new DisposableCollection(); - const handleKeycode = (keycode: KeyCode) => { - const selections = editor.getSelectionRanges(); - if (selections.length === 1) { - const range = selections[0]; - switch (keycode) { - case KeyCode.ARROW_LEFT: { - const offset = Math.max(range.startOffset - 1, 0); - editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); - break; - } - case KeyCode.ARROW_RIGHT: { - const content = (editor.getDocumentData().body?.dataStream || ',,').length - 2; - const offset = Math.min(range.endOffset + 1, content); - editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); - break; - } + const handleMoveInEditor = (keycode: KeyCode, metaKey?: MetaKeys) => { + if (onMoveInEditorRef.current) { + onMoveInEditorRef.current(keycode, metaKey); + return; + } + + let direction = Direction.LEFT; + if (keycode === KeyCode.ARROW_DOWN) { + direction = Direction.DOWN; + } else if (keycode === KeyCode.ARROW_UP) { + direction = Direction.UP; + } else if (keycode === KeyCode.ARROW_RIGHT) { + direction = Direction.RIGHT; + } + + if (metaKey === MetaKeys.SHIFT) { + commandService.executeCommand(MoveSelectionOperation.id, { + direction, + }); + } else { + commandService.executeCommand(MoveCursorOperation.id, { + direction, + }); + } + }; + + const handleKeycode = (keycode: KeyCode, metaKey?: MetaKeys) => { + let direction = Direction.DOWN; + if (keycode === KeyCode.ARROW_DOWN) { + direction = Direction.DOWN; + } else if (keycode === KeyCode.ARROW_UP) { + direction = Direction.UP; + } else if (keycode === KeyCode.ARROW_LEFT) { + direction = Direction.LEFT; + } else if (keycode === KeyCode.ARROW_RIGHT) { + direction = Direction.RIGHT; + } + if (shouldMoveSelectionRef.current) { + if (metaKey === MetaKeys.CTRL_COMMAND) { + commandService.executeCommand(MoveSelectionCommand.id, { + direction, + jumpOver: JumpOver.moveGap, + extra: 'formula-editor', + fromCurrentSelection: shouldMoveSelectionRef.current === FormulaSelectingType.NEED_ADD, + }); + } else if (metaKey === MetaKeys.SHIFT) { + commandService.executeCommand(ExpandSelectionCommand.id, { + direction, + extra: 'formula-editor', + }); + } else if (metaKey === (MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT)) { + commandService.executeCommand(ExpandSelectionCommand.id, { + direction, + jumpOver: JumpOver.moveGap, + extra: 'formula-editor', + }); + } else { + commandService.executeCommand(MoveSelectionCommand.id, { + direction, + extra: 'formula-editor', + fromCurrentSelection: shouldMoveSelectionRef.current === FormulaSelectingType.NEED_ADD, + }); } + } else { + handleMoveInEditor(keycode, metaKey); } }; @@ -60,10 +116,29 @@ export const useLeftAndRightArrow = (isNeed: boolean, editor?: Editor) => { }, })); - [KeyCode.ARROW_LEFT, KeyCode.ARROW_RIGHT, KeyCode.ARROW_DOWN, KeyCode.ARROW_UP].map((keyCode) => { + const keyCodes = [ + { keyCode: KeyCode.ARROW_DOWN }, + { keyCode: KeyCode.ARROW_LEFT }, + { keyCode: KeyCode.ARROW_RIGHT }, + { keyCode: KeyCode.ARROW_UP }, + { keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.CTRL_COMMAND }, + { keyCode: KeyCode.ARROW_DOWN, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_LEFT, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_RIGHT, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + { keyCode: KeyCode.ARROW_UP, metaKey: MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT }, + ]; + + keyCodes.map(({ keyCode, metaKey }) => { return { id: operationId, - binding: keyCode, + binding: metaKey ? keyCode | metaKey : keyCode, preconditions: () => true, priority: 900, staticParameters: { @@ -78,5 +153,5 @@ export const useLeftAndRightArrow = (isNeed: boolean, editor?: Editor) => { return () => { d.dispose(); }; - }, [editor, isNeed]); + }, [commandService, editor, isNeed, shortcutService]); }; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useRefactorEffect.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useRefactorEffect.ts index 509065046f5..385373cd359 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useRefactorEffect.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useRefactorEffect.ts @@ -22,7 +22,7 @@ import { IContextMenuService } from '@univerjs/ui'; import { useEffect, useLayoutEffect } from 'react'; import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; -export const useRefactorEffect = (isNeed: boolean, unitId: string) => { +export const useRefactorEffect = (isNeed: boolean, selecting: boolean, unitId: string) => { const renderManagerService = useDependency(IRenderManagerService); const contextService = useDependency(IContextService); const contextMenuService = useDependency(IContextMenuService); @@ -30,33 +30,37 @@ export const useRefactorEffect = (isNeed: boolean, unitId: string) => { const render = renderManagerService.getRenderById(unitId); const refSelectionsRenderService = render?.with(RefSelectionsRenderService); + useLayoutEffect(() => { if (isNeed) { - const d1 = refSelectionsRenderService?.enableSelectionChanging(); - contextService.setContextValue(REF_SELECTIONS_ENABLED, true); contextService.setContextValue(EDITOR_ACTIVATED, true); return () => { contextService.setContextValue(EDITOR_ACTIVATED, false); - contextService.setContextValue(REF_SELECTIONS_ENABLED, false); - d1?.dispose(); + refSelectionsService.clear(); }; } }, [isNeed]); useLayoutEffect(() => { - if (isNeed) { + if (isNeed && selecting) { + const d1 = refSelectionsRenderService?.enableSelectionChanging(); + contextService.setContextValue(REF_SELECTIONS_ENABLED, true); + return () => { - refSelectionsService.clear(); + contextService.setContextValue(REF_SELECTIONS_ENABLED, false); + d1?.dispose(); }; } - }, [isNeed]); + }, [isNeed, selecting]); //right context controller useEffect(() => { if (isNeed) { + contextService.setContextValue(EDITOR_ACTIVATED, true); contextMenuService.disable(); return () => { + contextService.setContextValue(EDITOR_ACTIVATED, false); contextMenuService.enable(); }; } diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useResetSelection.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useResetSelection.ts index fb06435fa6f..6450936db85 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useResetSelection.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useResetSelection.ts @@ -17,27 +17,22 @@ import type { Workbook } from '@univerjs/core'; import { IUniverInstanceService, UniverInstanceType, useDependency } from '@univerjs/core'; import { SheetsSelectionsService } from '@univerjs/sheets'; -import { useMemo } from 'react'; +import { useCallback } from 'react'; -export const useResetSelection = (isNeed: boolean) => { +export const useResetSelection = (isNeed: boolean, unitId: string, subUnitId: string) => { const univerInstanceService = useDependency(IUniverInstanceService); const sheetsSelectionsService = useDependency(SheetsSelectionsService); - const resetSelection = useMemo(() => { + const resetSelection = useCallback(() => { if (isNeed) { + const selections = [...sheetsSelectionsService.getWorkbookSelections(unitId).getSelectionsOfWorksheet(subUnitId)]; const workbook = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); - const sheet = workbook?.getActiveSheet(); - const selections = [...sheetsSelectionsService.getCurrentSelections()]; - return () => { - const workbook = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); - const currentSheet = workbook?.getActiveSheet(); - if (currentSheet && currentSheet === sheet) { - sheetsSelectionsService.setSelections(selections); - } - }; - } - return () => { }; - }, [isNeed]); + const currentSheet = workbook?.getActiveSheet(); + if (currentSheet && currentSheet.getSheetId() === subUnitId) { + sheetsSelectionsService.setSelections(selections); + } + }; + }, [isNeed, sheetsSelectionsService, subUnitId, unitId, univerInstanceService]); return resetSelection; }; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useResize.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useResize.ts index 25afa03b274..aaf61e3869c 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useResize.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useResize.ts @@ -14,89 +14,4 @@ * limitations under the License. */ -import type { Nullable } from '@univerjs/core'; -import { debounce } from '@univerjs/core'; -import { DocSkeletonManagerService } from '@univerjs/docs'; -import { type Editor, VIEWPORT_KEY } from '@univerjs/docs-ui'; -import { ScrollBar } from '@univerjs/engine-render'; -import { useEffect, useMemo } from 'react'; - -export const useResize = (editor?: Editor) => { - const resize = () => { - if (editor) { - const { scene, mainComponent } = editor.render; - const docSkeletonManagerService = editor.render.with(DocSkeletonManagerService); - const { width, height } = editor.getBoundingClientRect(); - - docSkeletonManagerService.getViewModel().getDataModel().updateDocumentDataPageSize(Infinity); - scene.transformByState({ - width, - height, - }); - - mainComponent?.resize(width, height); - } - }; - - const checkScrollBar = useMemo(() => { - return debounce(() => { - if (!editor) { - return; - } - const docSkeletonManagerService = editor.render.with(DocSkeletonManagerService); - const skeleton = docSkeletonManagerService.getSkeleton(); - const { scene, mainComponent } = editor.render; - const viewportMain = scene.getViewport(VIEWPORT_KEY.VIEW_MAIN); - const { actualWidth } = skeleton.getActualSize(); - const { width, height } = editor.getBoundingClientRect(); - let scrollBar = viewportMain?.getScrollBar() as Nullable; - const contentWidth = Math.max(actualWidth, width); - - const contentHeight = height; - - scene.transformByState({ - width: contentWidth, - height: contentHeight, - }); - - mainComponent?.resize(contentWidth, contentHeight); - - if (actualWidth > width) { - if (scrollBar == null) { - viewportMain && new ScrollBar(viewportMain, { barSize: 8, enableVertical: false }); - } else { - viewportMain?.resetCanvasSizeAndUpdateScroll(); - } - } else { - scrollBar = null; - viewportMain?.scrollToBarPos({ x: 0, y: 0 }); - viewportMain?.getScrollBar()?.dispose(); - } - }, 30); - }, [editor]); - - useEffect(() => { - if (editor) { - const time = setTimeout(() => { - resize(); - checkScrollBar(); - }, 500); - return () => { - clearTimeout(time); - }; - } - }, [editor]); - - useEffect(() => { - if (editor) { - const d = editor.input$.subscribe(() => { - checkScrollBar(); - }); - return () => { - d.unsubscribe(); - }; - } - }, [editor]); - - return { resize, checkScrollBar }; -}; +export { useResize } from '@univerjs/docs-ui'; diff --git a/packages/sheets-formula-ui/src/views/range-selector/index.tsx b/packages/sheets-formula-ui/src/views/range-selector/index.tsx index 904f2a7bb5a..bb850527c5b 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -17,20 +17,23 @@ import type { IDisposable, IUnitRangeName } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { ReactNode } from 'react'; -import { createInternalEditorID, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, generateRandomId, ICommandService, LocaleService, useDependency } from '@univerjs/core'; +import type { IRefSelection } from './hooks/useHighlight'; +import { createInternalEditorID, generateRandomId, ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; import { Button, Dialog, Input, Tooltip } from '@univerjs/design'; import { DocBackScrollRenderController, IEditorService } from '@univerjs/docs-ui'; import { deserializeRangeWithSheet, LexerTreeBuilder, matchToken, sequenceNodeType } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; import { CloseSingle, DeleteSingle, IncreaseSingle, SelectRangeSingle } from '@univerjs/icons'; import { IDescriptionService } from '@univerjs/sheets-formula'; -import { RANGE_SELECTOR_SYMBOLS, SetCellEditVisibleOperation } from '@univerjs/sheets-ui'; +import { RANGE_SELECTOR_SYMBOLS, SetCellEditVisibleOperation } from '@univerjs/sheets-ui'; +import { useEvent } from '@univerjs/ui'; import cl from 'clsx'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { filter, noop, throttleTime } from 'rxjs'; -import { RefSelectionsRenderService } from '../../services/render-services/ref-selections.render-service'; +import { noop, throttleTime } from 'rxjs'; +import { RefSelectionsRenderService } from '../../services/render-services/ref-selections.render-service'; +import { getFocusingReference } from '../formula-editor/hooks/util'; import { useEditorInput } from './hooks/useEditorInput'; import { useEmitChange } from './hooks/useEmitChange'; import { useFirstHighlightDoc } from './hooks/useFirstHighlightDoc'; @@ -88,27 +91,34 @@ export interface IRangeSelectorProps { const noopFunction = () => { }; export function RangeSelector(props: IRangeSelectorProps) { - const { initValue, unitId, subUnitId, errorText, placeholder, actions, - onChange = noopFunction, - onVerify = noopFunction, - onRangeSelectorDialogVisibleChange = noopFunction, - onBlur = noopFunction, - onFocus = noopFunction, - isFocus: _isFocus = true, - isOnlyOneRange = false, - isSupportAcrossSheet = false } = props; - + const { + initValue, + unitId, + subUnitId, + errorText, + placeholder, + actions, + onChange = noopFunction, + onVerify = noopFunction, + onRangeSelectorDialogVisibleChange = noopFunction, + onBlur = noopFunction, + onFocus = noopFunction, + isFocus: _isFocus = true, + isOnlyOneRange = false, + isSupportAcrossSheet = false, + } = props; const editorService = useDependency(IEditorService); const localeService = useDependency(LocaleService); const commandService = useDependency(ICommandService); const lexerTreeBuilder = useDependency(LexerTreeBuilder); - const rangeSelectorWrapRef = useRef(null); const [rangeDialogVisible, rangeDialogVisibleSet] = useState(false); const [isFocus, isFocusSet] = useState(_isFocus); const editorId = useMemo(() => createInternalEditorID(`${RANGE_SELECTOR_SYMBOLS}-${generateRandomId(4)}`), []); - const [editor, editorSet] = useState(); + const editorRef = useRef(); + const editor = editorRef.current; const containerRef = useRef(null); + const univerInstanceService = useDependency(IUniverInstanceService); const isNeed = useMemo(() => !rangeDialogVisible && isFocus, [rangeDialogVisible, isFocus]); const [rangeString, rangeStringSet] = useState(() => { if (typeof initValue === 'string') { @@ -117,15 +127,21 @@ export function RangeSelector(props: IRangeSelectorProps) { return unitRangesToText(initValue, isSupportAcrossSheet).join(matchToken.COMMA); } }); + const currentDoc$ = useMemo(() => univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_DOC), [univerInstanceService]); + const currentDoc = useObservable(currentDoc$); + const docFocusing = currentDoc?.getUnitId() === editorId; + const refSelections = useRef([]); + + const clickOutside = useEvent((e: MouseEvent, cb: () => void) => { + if (rangeSelectorWrapRef.current && !rangeDialogVisible) { + const isContain = rangeSelectorWrapRef.current.contains(e.target as Node); + !isContain && cb(); + } + }); // init actions if (actions) { - actions.handleOutClick = (e: MouseEvent, cb: () => void) => { - if (rangeSelectorWrapRef.current && !rangeDialogVisible) { - const isContain = rangeSelectorWrapRef.current.contains(e.target as Node); - !isContain && cb(); - } - }; + actions.handleOutClick = clickOutside; } const ranges = useMemo(() => { @@ -134,7 +150,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const isError = useMemo(() => errorText !== undefined, [errorText]); - const resetSelection = useResetSelection(!rangeDialogVisible && isFocus); + const resetSelection = useResetSelection(!rangeDialogVisible && isFocus, unitId, subUnitId); const handleInput = useMemo(() => (text: string) => { const nodes = lexerTreeBuilder.sequenceNodesBuilder(text); @@ -169,40 +185,23 @@ export function RangeSelector(props: IRangeSelectorProps) { const focus = useFocus(editor); - useLayoutEffect(() => { - // 如果是失去焦点的话,需要立刻执行 - // 在进行多个 input 切换的时候,失焦必须立刻执行. - if (_isFocus) { - const time = setTimeout(() => { - isFocusSet(_isFocus); - if (_isFocus) { - focus(); - } - }, 30); - return () => { - clearTimeout(time); - }; - } else { - resetSelection(); - isFocusSet(_isFocus); - editor?.blur(); - } - }, [_isFocus, focus]); - - const { checkScrollBar } = useResize(editor); + const { checkScrollBar } = useResize(editor, true, true); const getFormulaToken = useFormulaToken(); const sequenceNodes = useMemo(() => getFormulaToken(rangeString), [rangeString]); const highlightDoc = useDocHight(); const highlightSheet = useSheetHighlight(unitId); - const highligh = (text: string, isNeedResetSelection: boolean = true) => { - if (!editor) { + const highligh = useEvent((text: string, isNeedResetSelection: boolean = true, showSelection = true) => { + if (!editorRef.current) { return; } const sequenceNodes = getFormulaToken(text); - const ranges = highlightDoc(editor, sequenceNodes, isNeedResetSelection); - highlightSheet(ranges); - }; + const ranges = highlightDoc(editorRef.current, sequenceNodes, isNeedResetSelection); + refSelections.current = ranges; + if (showSelection) { + highlightSheet(ranges, getFocusingReference(editorRef.current, ranges)); + } + }); const needEmit = useEmitChange(sequenceNodes, handleInput, editor); @@ -229,7 +228,7 @@ export function RangeSelector(props: IRangeSelectorProps) { useSheetSelectionChange(isNeed, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); - useRefactorEffect(isNeed, unitId); + useRefactorEffect(isNeed, isNeed && docFocusing, unitId); useOnlyOneRange(unitId, isOnlyOneRange); @@ -237,7 +236,7 @@ export function RangeSelector(props: IRangeSelectorProps) { useVerify(isNeed, onVerify, sequenceNodes); - useLeftAndRightArrow(isNeed, editor); + useLeftAndRightArrow(isNeed, 0, editor); useRefocus(); @@ -281,33 +280,38 @@ export function RangeSelector(props: IRangeSelectorProps) { dispose = editorService.register({ autofocus: true, editorUnitId: editorId, - isSingle: true, initialSnapshot: { id: editorId, - body: { dataStream: '\r\n' }, + body: { dataStream: `${rangeString}\r\n`, textRuns: [] }, documentStyle: {}, }, }, containerRef.current); const editor = editorService.getEditor(editorId)! as Editor; - editorSet(editor); + editorRef.current = editor; + highligh(rangeString, false, false); } return () => { dispose?.dispose(); }; }, []); + useLayoutEffect(() => { + if (_isFocus) { + isFocusSet(_isFocus); + focus(); + } else { + editor?.blur(); + resetSelection(); + isFocusSet(_isFocus); + } + }, [_isFocus, focus]); + useFirstHighlightDoc(rangeString, '', isFocus, highlightDoc, highlightSheet, editor); const handleClick = () => { - // 在进行多个 input 切换的时候,失焦必须快于获得焦点. - // 即使失焦是 mousedown 事件, - // 聚焦是 mouseup 事件, - // 但是 react 的 useEffect 无法保证顺序,无法确保失焦在聚焦之前. - setTimeout(() => { - onFocus(); - focus(); - isFocusSet(true); - }, 30); + onFocus(); + focus(); + isFocusSet(true); }; const handleConfirm = (ranges: IUnitRangeName[]) => { @@ -321,6 +325,7 @@ export function RangeSelector(props: IRangeSelectorProps) { isFocusSet(true); editor?.setSelectionRanges([{ startOffset: text.length, endOffset: text.length }]); focus(); + checkScrollBar(); }, 30); }; @@ -390,14 +395,11 @@ function RangeSelectorDialog(props: { isOnlyOneRange: boolean; isSupportAcrossSheet: boolean; }) { - const { editorId, handleConfirm, handleClose: _handleClose, visible, initValue, unitId, subUnitId, isOnlyOneRange, isSupportAcrossSheet } = props; - + const { handleConfirm, handleClose: _handleClose, visible, initValue, unitId, subUnitId, isOnlyOneRange, isSupportAcrossSheet } = props; const localeService = useDependency(LocaleService); - const editorService = useDependency(IEditorService); const descriptionService = useDependency(IDescriptionService); const lexerTreeBuilder = useDependency(LexerTreeBuilder); const renderManagerService = useDependency(IRenderManagerService); - const render = renderManagerService.getRenderById(unitId); const refSelectionsRenderService = render?.with(RefSelectionsRenderService); @@ -479,7 +481,7 @@ function RangeSelectorDialog(props: { const highlightSheet = useSheetHighlight(unitId); useSheetSelectionChange(focusIndex >= 0, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); - useRefactorEffect(focusIndex >= 0, unitId); + useRefactorEffect(focusIndex >= 0, focusIndex >= 0, unitId); useOnlyOneRange(unitId, isOnlyOneRange); useSwitchSheet(focusIndex >= 0, unitId, isSupportAcrossSheet, noop, noop, () => highlightSheet(refSelections)); @@ -494,21 +496,6 @@ function RangeSelectorDialog(props: { } }, [ranges]); - useEffect(() => { - const d = editorService.focusStyle$ - .pipe( - filter((e) => !!e && DOCS_NORMAL_EDITOR_UNIT_ID_KEY !== e) - ) - .subscribe((e) => { - if (e !== editorId) { - handleClose(); - } - }); - return () => { - d.unsubscribe(); - }; - }, [editorService, editorId]); - return (
{ranges.map((text, index) => ( -
+
(unitId)?.getSheetBySheetId(sheetId)?.getName() || ''; } -export const unitRangesToText = (ranges: IUnitRangeName[], isNeedSheetName: boolean = false) => { +export const unitRangesToText = (ranges: IUnitRangeName[], isNeedSheetName: boolean = false, originSheetName = '') => { if (!isNeedSheetName) { return ranges.map((item) => serializeRange(item.range)); } else { return ranges.map((item) => { - if (item.sheetName !== '') { + if (item.sheetName !== '' && item.sheetName !== originSheetName) { return serializeRangeWithSheet(item.sheetName, item.range); } return serializeRange(item.range); diff --git a/packages/sheets-ui/src/commands/commands/inline-format.command.ts b/packages/sheets-ui/src/commands/commands/inline-format.command.ts index 55a84c03e85..2c0e06816b2 100644 --- a/packages/sheets-ui/src/commands/commands/inline-format.command.ts +++ b/packages/sheets-ui/src/commands/commands/inline-format.command.ts @@ -15,7 +15,7 @@ */ import type { ICommand } from '@univerjs/core'; -import { CommandType, EDITOR_ACTIVATED, ICommandService, IContextService } from '@univerjs/core'; +import { CommandType, EDITOR_ACTIVATED, ICommandService, IContextService, ThemeService } from '@univerjs/core'; import { SetInlineFormatBoldCommand, SetInlineFormatFontFamilyCommand, SetInlineFormatFontSizeCommand, SetInlineFormatItalicCommand, SetInlineFormatStrikethroughCommand, SetInlineFormatSubscriptCommand, SetInlineFormatSuperscriptCommand, SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand } from '@univerjs/docs-ui'; import { SetBoldCommand, @@ -184,11 +184,12 @@ export const ResetRangeTextColorCommand: ICommand = { const commandService = accessor.get(ICommandService); const contextService = accessor.get(IContextService); const isCellEditorFocus = contextService.getContextValue(EDITOR_ACTIVATED); + const themeService = accessor.get(ThemeService); if (isCellEditorFocus) { return commandService.executeCommand(SetInlineFormatTextColorCommand.id, { value: null }); } - return commandService.executeCommand(SetTextColorCommand.id, { value: null }); + return commandService.executeCommand(SetTextColorCommand.id, { value: themeService.getCurrentTheme().textColor }); }, }; diff --git a/packages/sheets-ui/src/commands/commands/set-selection.command.ts b/packages/sheets-ui/src/commands/commands/set-selection.command.ts index 96063e78a32..424713f558c 100644 --- a/packages/sheets-ui/src/commands/commands/set-selection.command.ts +++ b/packages/sheets-ui/src/commands/commands/set-selection.command.ts @@ -58,11 +58,15 @@ export interface IMoveSelectionCommandParams { direction: Direction; jumpOver?: JumpOver; nextStep?: number; + extra?: string; + fromCurrentSelection?: boolean; } export interface IMoveSelectionEnterAndTabCommandParams { direction: Direction; keycode: KeyCode; + extra?: string; + fromCurrentSelection?: boolean; } /** @@ -71,7 +75,7 @@ export interface IMoveSelectionEnterAndTabCommandParams { export const MoveSelectionCommand: ICommand = { id: 'sheet.command.move-selection', type: CommandType.COMMAND, - handler: async (accessor, params) => { + handler: (accessor, params) => { if (!params) { return false; } @@ -80,12 +84,12 @@ export const MoveSelectionCommand: ICommand = { if (!target) return false; const { workbook, worksheet } = target; - const selection = getSelectionsService(accessor).getCurrentLastSelection(); + const selection = getSelectionsService(accessor, params.fromCurrentSelection).getCurrentLastSelection(); if (!selection) { return false; } - const { direction, jumpOver } = params; + const { direction, jumpOver, extra } = params; const { range, primary } = selection; const startRange = getStartRange(range, primary, direction); @@ -129,6 +133,7 @@ export const MoveSelectionCommand: ICommand = { subUnitId: worksheet.getSheetId(), selections, type: SelectionMoveType.MOVE_END, + extra, } as ISetSelectionsOperationParams); return rs; }, @@ -140,9 +145,8 @@ export const MoveSelectionCommand: ICommand = { export const MoveSelectionEnterAndTabCommand: ICommand = { id: 'sheet.command.move-selection-enter-tab', type: CommandType.COMMAND, - // eslint-disable-next-line max-lines-per-function, complexity - handler: async (accessor, params) => { + handler: (accessor, params) => { if (!params) { return false; } @@ -300,6 +304,7 @@ export const MoveSelectionEnterAndTabCommand: ICommand = { id: 'sheet.command.expand-selection', type: CommandType.COMMAND, - handler: async (accessor, params) => { + handler: (accessor, params) => { if (!params) return false; const target = getSheetCommandTarget(accessor.get(IUniverInstanceService)); @@ -331,7 +337,7 @@ export const ExpandSelectionCommand: ICommand = { if (!selection) return false; const { range: startRange, primary } = selection; - const { jumpOver, direction } = params; + const { jumpOver, direction, extra } = params; const isShrink = checkIfShrink(selection, direction, worksheet); const destRange = !isShrink @@ -352,7 +358,7 @@ export const ExpandSelectionCommand: ICommand = { return false; } - return accessor.get(ICommandService).executeCommand(SetSelectionsOperation.id, { + return accessor.get(ICommandService).syncExecuteCommand(SetSelectionsOperation.id, { unitId, subUnitId, type: SelectionMoveType.ONLY_SET, @@ -362,6 +368,7 @@ export const ExpandSelectionCommand: ICommand = { primary, // this remains unchanged }, ], + extra, }); }, }; diff --git a/packages/sheets-ui/src/commands/operations/sidebar-defined-name.operation.ts b/packages/sheets-ui/src/commands/operations/sidebar-defined-name.operation.ts index ca93651353e..e943f69c637 100644 --- a/packages/sheets-ui/src/commands/operations/sidebar-defined-name.operation.ts +++ b/packages/sheets-ui/src/commands/operations/sidebar-defined-name.operation.ts @@ -40,13 +40,11 @@ export const SidebarDefinedNameOperation: ICommand = { const { unitId } = target; switch (params.value) { case 'open': - editorService.setOperationSheetUnitId(unitId); sidebarService.open({ id: DEFINED_NAME_CONTAINER, header: { title: localeService.t('definedName.featureTitle') }, children: { label: DEFINED_NAME_CONTAINER }, onClose: () => { - editorService.closeRangePrompt(); }, width: 333, }); diff --git a/packages/sheets-ui/src/common/keys.ts b/packages/sheets-ui/src/common/keys.ts index 4b5d70f4051..26929e83947 100644 --- a/packages/sheets-ui/src/common/keys.ts +++ b/packages/sheets-ui/src/common/keys.ts @@ -21,6 +21,7 @@ export const SHEET_ZOOM_RANGE = [10, 400]; */ export const RANGE_SELECTOR_COMPONENT_KEY = 'RANGE_SELECTOR_COMPONENT_KEY'; export const EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY = 'EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY'; +export const EMBEDDING_CELL_EDITOR_COMPONENT_KEY = 'EMBEDDING_CELL_EDITOR_COMPONENT_KEY'; // end export enum SHEET_VIEW_KEY { diff --git a/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts b/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts index dcf5059631e..7c29fd73a27 100644 --- a/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts +++ b/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts @@ -129,9 +129,8 @@ describe('Test EndEditController', () => { return getCellDataByInput( cell, - documentLayoutObject.documentModel, + documentLayoutObject.documentModel?.getSnapshot(), lexerTreeBuilder, - (model) => model.getSnapshot(), localeService, get(IMockFunctionService) as IFunctionService, workbook.getStyles() diff --git a/packages/sheets-ui/src/controllers/editor/data-sync.controller.ts b/packages/sheets-ui/src/controllers/editor/data-sync.controller.ts index d51dd0631e9..cc97e1ab715 100644 --- a/packages/sheets-ui/src/controllers/editor/data-sync.controller.ts +++ b/packages/sheets-ui/src/controllers/editor/data-sync.controller.ts @@ -14,18 +14,41 @@ * limitations under the License. */ -import type { DocumentDataModel, ICommandInfo, IDocumentBody, IDrawings, IParagraph, Nullable } from '@univerjs/core'; +import type { DocumentDataModel, ICommandInfo, IDocumentBody, IDocumentStyle, IDrawings, IParagraph, Nullable } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { DocumentViewModel } from '@univerjs/engine-render'; import type { IMoveRangeMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets'; import type { ICellEditorState } from '../../services/editor-bridge.service'; -import { BooleanNumber, Disposable, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, HorizontalAlign, ICommandService, Inject, IUniverInstanceService, Tools, UniverInstanceType } from '@univerjs/core'; +import { BooleanNumber, Disposable, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DocumentFlavor, HorizontalAlign, ICommandService, Inject, IUniverInstanceService, Tools, UniverInstanceType, VerticalAlign, WrapStrategy } from '@univerjs/core'; import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs'; +import { ReplaceSnapshotCommand } from '@univerjs/docs-ui'; import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render'; import { MoveRangeMutation, RangeProtectionRuleModel, SetRangeValuesMutation, WorksheetProtectionRuleModel } from '@univerjs/sheets'; import { IEditorBridgeService } from '../../services/editor-bridge.service'; +import { IFormulaEditorManagerService } from '../../services/editor/formula-editor-manager.service'; import { FormulaEditorController } from './formula-editor.controller'; +const formulaEditorStyle: IDocumentStyle = { + pageSize: { + width: Number.POSITIVE_INFINITY, + height: Number.POSITIVE_INFINITY, + }, + documentFlavor: DocumentFlavor.UNSPECIFIED, + marginTop: 5, + marginBottom: 5, + marginRight: 0, + marginLeft: 0, + paragraphLineGapDefault: 0, + renderConfig: { + horizontalAlign: HorizontalAlign.UNSPECIFIED, + verticalAlign: VerticalAlign.TOP, + centerAngle: 0, + vertexAngle: 0, + wrapStrategy: WrapStrategy.WRAP, + isRenderStyle: BooleanNumber.FALSE, + }, +}; + /** * sync data between cell editor and formula editor */ @@ -37,7 +60,8 @@ export class EditorDataSyncController extends Disposable { @ICommandService private readonly _commandService: ICommandService, @Inject(RangeProtectionRuleModel) private readonly _rangeProtectionRuleModel: RangeProtectionRuleModel, @Inject(WorksheetProtectionRuleModel) private readonly _worksheetProtectionRuleModel: WorksheetProtectionRuleModel, - @Inject(FormulaEditorController) private readonly _formulaEditorController: FormulaEditorController + @Inject(FormulaEditorController) private readonly _formulaEditorController: FormulaEditorController, + @IFormulaEditorManagerService private readonly _formulaEditorManagerService: IFormulaEditorManagerService ) { super(); @@ -101,10 +125,11 @@ export class EditorDataSyncController extends Disposable { this._commandService.onCommandExecuted((command: ICommandInfo) => { if (command.id === RichTextEditingMutation.id) { const params = command.params as IRichTextEditingMutationParams; - const { unitId } = params; - if (params.isSync) { + const { unitId, trigger, isSync } = params; + if (isSync || trigger === ReplaceSnapshotCommand.id) { return; } + if (INCLUDE_LIST.includes(unitId)) { // sync cell content to formula editor bar when edit cell editor and vice verse. const editorDocDataModel = this._univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); @@ -175,7 +200,7 @@ export class EditorDataSyncController extends Disposable { } const skeleton = currentRender.with(DocSkeletonManagerService).getSkeleton(); - const docDataModel = this._univerInstanceService.getUniverDocInstance(unitId); + const docDataModel = this._univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); const docViewModel = this._getEditorViewModel(unitId); if (docDataModel == null || docViewModel == null) { @@ -212,7 +237,7 @@ export class EditorDataSyncController extends Disposable { const INCLUDE_LIST = [DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY]; const skeleton = this._renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService).getSkeleton(); - const docDataModel = this._univerInstanceService.getUniverDocInstance(unitId); + const docDataModel = this._univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); const docViewModel = this._getEditorViewModel(unitId); if (docDataModel == null || docViewModel == null || skeleton == null) { @@ -224,10 +249,8 @@ export class EditorDataSyncController extends Disposable { docDataModel.getSnapshot().drawingsOrder = drawingsOrder ?? []; this._checkAndSetRenderStyleConfig(docDataModel); - docViewModel.reset(docDataModel); const currentRender = this._renderManagerService.getRenderById(unitId); - if (currentRender == null) { return; } @@ -251,13 +274,21 @@ export class EditorDataSyncController extends Disposable { return; } + snapshot.documentStyle = formulaEditorStyle; let renderConfig = snapshot.documentStyle.renderConfig; if (renderConfig == null) { renderConfig = {}; snapshot.documentStyle.renderConfig = renderConfig; } - + const position = this._formulaEditorManagerService.getPosition(); + if (position) { + const width = position.width; + snapshot.documentStyle.pageSize = { + width, + height: Infinity, + }; + } if ((body?.dataStream ?? '').startsWith('=')) { renderConfig.isRenderStyle = BooleanNumber.TRUE; } else { diff --git a/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts b/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts index d4f8459e661..aa2a003fd34 100644 --- a/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts +++ b/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts @@ -16,7 +16,7 @@ /* eslint-disable max-lines-per-function */ -import type { DocumentDataModel, ICellData, ICommandInfo, IDisposable, IDocumentBody, IDocumentData, IStyleData, Nullable, Styles, UnitModel, Workbook } from '@univerjs/core'; +import type { DocumentDataModel, ICellData, ICommandInfo, IDisposable, IDocumentBody, IDocumentData, IDocumentStyle, IStyleData, Nullable, Styles, UnitModel, Workbook } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { IRenderContext, IRenderModule } from '@univerjs/engine-render'; import type { WorkbookSelectionModel } from '@univerjs/sheets'; @@ -28,7 +28,6 @@ import { FOCUSING_EDITOR_INPUT_FORMULA, FOCUSING_EDITOR_STANDALONE, FOCUSING_FX_BAR_EDITOR, - FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, ICommandService, IContextService, Inject, @@ -46,7 +45,7 @@ import { DocSkeletonManagerService, RichTextEditingMutation, } from '@univerjs/docs'; -import { VIEWPORT_KEY as DOC_VIEWPORT_KEY, DocSelectionRenderService, IEditorService, MoveCursorOperation, MoveSelectionOperation } from '@univerjs/docs-ui'; +import { VIEWPORT_KEY as DOC_VIEWPORT_KEY, DocSelectionRenderService, IEditorService, MoveCursorOperation, MoveSelectionOperation, ReplaceSnapshotCommand } from '@univerjs/docs-ui'; import { IFunctionService, LexerTreeBuilder, matchToken } from '@univerjs/engine-formula'; import { DEFAULT_TEXT_FORMAT } from '@univerjs/engine-numfmt'; @@ -199,7 +198,7 @@ export class EditingRenderController extends Disposable implements IRenderModule const param = this._editorBridgeService.getEditCellState(); const editorId = this._editorBridgeService.getCurrentEditorId(); - if (!param || !editorId || !this._editorService.isSheetEditor(editorId)) { + if (!param || !editorId) { return; } @@ -246,7 +245,6 @@ export class EditingRenderController extends Disposable implements IRenderModule if (editCellState == null || this._editorBridgeService.isForceKeepVisible()) { return; } - const state = this._editorBridgeService.getEditCellState(); if (state == null) { return; @@ -254,37 +252,46 @@ export class EditingRenderController extends Disposable implements IRenderModule const { position, documentLayoutObject, scaleX, editorUnitId } = state; - if ( - this._contextService.getContextValue(FOCUSING_EDITOR_STANDALONE) || - this._contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE) - ) { - return; - } - - if (this._instanceSrv.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY) === documentLayoutObject.documentModel) { + if (this._contextService.getContextValue(FOCUSING_EDITOR_STANDALONE)) { return; } + const cellDocument = this._instanceSrv.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); + if (cellDocument == null) return; const { startX, endX } = position; const { textRotation, wrapStrategy, documentModel } = documentLayoutObject; const { vertexAngle: angle } = convertTextRotation(textRotation); - documentModel!.updateDocumentId(editorUnitId); if (wrapStrategy === WrapStrategy.WRAP && angle === 0) { - documentModel!.updateDocumentDataPageSize((endX - startX) / scaleX); + cellDocument.updateDocumentDataPageSize((endX - startX) / scaleX); } - this._instanceSrv.changeDoc(editorUnitId, documentModel!); - this._contextService.setContextValue(FOCUSING_EDITOR_BUT_HIDDEN, true); - this._textSelectionManagerService.replaceTextRanges([{ - startOffset: 0, - endOffset: 0, - }]); - - const docSelectionRenderManager = this._renderManagerService.getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_DOC)?.with(DocSelectionRenderService); + this._commandService.syncExecuteCommand(ReplaceSnapshotCommand.id, { + unitId: editorUnitId, + snapshot: (documentModel!.getSnapshot()), + }); - if (docSelectionRenderManager) { - docSelectionRenderManager.activate(HIDDEN_EDITOR_POSITION, HIDDEN_EDITOR_POSITION, !document.activeElement || document.activeElement.classList.contains('univer-editor')); + this._contextService.setContextValue(FOCUSING_EDITOR_BUT_HIDDEN, true); + this._textSelectionManagerService.replaceDocRanges( + [{ + startOffset: 0, + endOffset: 0, + }], + { + unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + } + ); + + const cellSelectionRenderManager = this._renderManagerService.getRenderById(DOCS_NORMAL_EDITOR_UNIT_ID_KEY)?.with(DocSelectionRenderService); + const formulaSelectionRenderManager = this._renderManagerService.getRenderById(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY)?.with(DocSelectionRenderService); + if (cellSelectionRenderManager?.isFocusing || formulaSelectionRenderManager?.isFocusing) { + this._univerInstanceService.setCurrentUnitForType(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); + cellSelectionRenderManager?.activate( + HIDDEN_EDITOR_POSITION, + HIDDEN_EDITOR_POSITION, + true + ); } })); } @@ -293,19 +300,13 @@ export class EditingRenderController extends Disposable implements IRenderModule * Listen to document edits to refresh the size of the sheet editor, not for normal editor. */ private _commandExecutedListener(d: DisposableCollection) { - const updateCommandList = [RichTextEditingMutation.id]; - d.add(this._commandService.onCommandExecuted((command: ICommandInfo) => { - if (updateCommandList.includes(command.id)) { + if (command.id === RichTextEditingMutation.id) { const params = command.params as IRichTextEditingMutationParams; const { unitId: commandUnitId } = params; // Only when the sheet it attached to is focused. Maybe we should change it to the render unit sys. - if ( - !this._isCurrentSheetFocused() || - !this._editorService.isSheetEditor(commandUnitId) || - isRangeSelector(commandUnitId) - ) { + if (!this._isCurrentSheetFocused() || isRangeSelector(commandUnitId)) { return; } @@ -353,7 +354,6 @@ export class EditingRenderController extends Disposable implements IRenderModule // You can double-click on the cell or input content by keyboard to put the cell into the edit state. private _handleEditorVisible(param: IEditorBridgeServiceVisibleParam) { const { eventType, keycode } = param; - // Change `CursorChange` to changed status, when formula bar clicked. this._cursorChange = (eventType === DeviceInputEventType.PointerDown || eventType === DeviceInputEventType.Dblclick) @@ -375,29 +375,18 @@ export class EditingRenderController extends Disposable implements IRenderModule }); this._editorBridgeService.refreshEditCellPosition(false); - - const { - documentLayoutObject, - editorUnitId, - unitId, - sheetId, - isInArrayFormulaRange = false, - } = editCellState; - + const { unitId, isInArrayFormulaRange = false } = editCellState; const editorObject = this._getEditorObject(); if (editorObject == null) { return; } - this._setOpenForCurrent(unitId, sheetId); - const { document, scene } = editorObject; this._contextService.setContextValue(EDITOR_ACTIVATED, true); - - const { documentModel: documentDataModel } = documentLayoutObject; - const skeleton = this._getEditorSkeleton(editorUnitId); + const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); + const skeleton = this._getEditorSkeleton(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); if (!skeleton || !documentDataModel) { return; } @@ -414,7 +403,7 @@ export class EditingRenderController extends Disposable implements IRenderModule eventType === DeviceInputEventType.Keyboard || (eventType === DeviceInputEventType.Dblclick && isInArrayFormulaRange) ) { - this._emptyDocumentDataModel(!!isInArrayFormulaRange); + this._emptyDocumentDataModel(documentDataModel.getSnapshot().documentStyle, !!isInArrayFormulaRange); document.makeDirty(); // @JOCS, Why calculate here? @@ -449,10 +438,9 @@ export class EditingRenderController extends Disposable implements IRenderModule private async _handleEditorInvisible(param: IEditorBridgeServiceVisibleParam) { const editCellState = this._editorBridgeService.getEditCellState(); - + const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); + const snapshot = Tools.deepClone(documentDataModel?.getSnapshot()); let { keycode } = param; - this._setOpenForCurrent(null, null); - this._cursorChange = CursorChange.InitialState; this._exitInput(param); @@ -473,6 +461,18 @@ export class EditingRenderController extends Disposable implements IRenderModule const workbookId = this._context.unitId; const worksheetId = worksheet.getSheetId(); + const { unitId, sheetId } = editCellState; + /** + * When closing the editor, switch to the current tab of the editor. + */ + if (workbookId === unitId && sheetId !== worksheetId) { + // SetWorksheetActivateCommand handler uses Promise + await this._commandService.executeCommand(SetWorksheetActivateCommand.id, { + subUnitId: sheetId, + unitId, + }); + } + // Reselect the current selections, when exist cell editor by press ESC.I if (keycode === KeyCode.ESC) { if (this._editorBridgeService.isForceKeepVisible()) { @@ -482,7 +482,7 @@ export class EditingRenderController extends Disposable implements IRenderModule if (selections) { this._commandService.syncExecuteCommand(SetSelectionsOperation.id, { unitId: this._context.unit.getUnitId(), - subUnitId: worksheetId, + subUnitId: sheetId, selections, }); } @@ -490,50 +490,23 @@ export class EditingRenderController extends Disposable implements IRenderModule return; } - const { unitId, sheetId } = editCellState; - - /** - * When closing the editor, switch to the current tab of the editor. - */ - if (workbookId === unitId && sheetId !== worksheetId && this._editorBridgeService.isForceKeepVisible()) { - // SetWorksheetActivateCommand handler uses Promise - await this._commandService.executeCommand(SetWorksheetActivateCommand.id, { - subUnitId: sheetId, - unitId, - }); - } - - const documentDataModel = editCellState.documentLayoutObject.documentModel; - - if (documentDataModel) { - await this._submitCellData(documentDataModel); + if (snapshot) { + await this._submitCellData(snapshot); } // moveCursor need to put behind of SetRangeValuesCommand, fix https://github.com/dream-num/univer/issues/1155 this._moveCursor(keycode); } - private _setOpenForCurrent(unitId: Nullable, sheetId: Nullable) { - const sheetEditors = this._editorService.getAllEditor(); - for (const [_, sheetEditor] of sheetEditors) { - if (!sheetEditor.isSheetEditor()) { - continue; - } - - sheetEditor.setOpenForSheetUnitId(unitId); - sheetEditor.setOpenForSheetSubUnitId(sheetId); - } - } - private _getEditorObject() { return getEditorObject(this._editorBridgeService.getCurrentEditorId(), this._renderManagerService); } submitCellData(documentDataModel: DocumentDataModel) { - return this._submitCellData(documentDataModel); + return this._submitCellData(documentDataModel.getSnapshot()); } - private async _submitCellData(documentDataModel: DocumentDataModel) { + private async _submitCellData(snapshot: IDocumentData) { const editCellState = this._editorBridgeService.getEditCellState(); if (editCellState == null) { return; @@ -555,10 +528,9 @@ export class EditingRenderController extends Disposable implements IRenderModule // If cross-sheet operation, switch current sheet first, then const cellData // This should moved to after cell editor const cellData: Nullable = getCellDataByInput( - worksheet.getCellRaw(row, column) || {}, - documentDataModel, + { ...(worksheet.getCellRaw(row, column) || {}) }, + snapshot, this._lexerTreeBuilder, - (model) => model.getSnapshot(), this._localService, this._functionService, workbook.getStyles() @@ -648,8 +620,8 @@ export class EditingRenderController extends Disposable implements IRenderModule * The logic here predicts the user's first cursor movement behavior based on this rule */ private _cursorStateListener(d: DisposableCollection) { - const editorObject = this._getEditorObject()!; - if (!editorObject.document) return; + const editorObject = this._getEditorObject(); + if (!editorObject?.document) return; const { document: documentComponent } = editorObject; d.add(toDisposable(documentComponent.onPointerDown$.subscribeEvent(() => { @@ -695,62 +667,45 @@ export class EditingRenderController extends Disposable implements IRenderModule return this._renderManagerService.getRenderById(editorId)?.with(DocSkeletonManagerService).getViewModel(); } - private _emptyDocumentDataModel(removeStyle: boolean) { - const editCellState = this._editorBridgeService.getEditCellState(); - if (editCellState == null) { - return; - } - - const { documentLayoutObject } = editCellState; - const documentDataModel = documentLayoutObject.documentModel; - if (documentDataModel == null) { - return; - } - - const empty = (documentDataModel: DocumentDataModel) => { + private _emptyDocumentDataModel(documentStyle: IDocumentStyle, removeStyle: boolean) { + const empty = (documentDataModel: DocumentDataModel, resetDocumentStyle?: boolean) => { const snapshot = Tools.deepClone(documentDataModel.getSnapshot()); const documentViewModel = this._getEditorViewModel(documentDataModel.getUnitId()); - if (documentViewModel == null) { return; } emptyBody(snapshot.body!, removeStyle); + if (resetDocumentStyle) { + snapshot.documentStyle = documentStyle; + } snapshot.drawings = {}; snapshot.drawingsOrder = []; documentDataModel.reset(snapshot); documentViewModel.reset(documentDataModel); }; - empty(documentDataModel); + const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); + documentDataModel && empty(documentDataModel, true); + const formulaDocument = this._univerInstanceService.getUnit(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); formulaDocument && empty(formulaDocument); } } -// eslint-disable-next-line +// eslint-disable-next-line complexity export function getCellDataByInput( cellData: ICellData, - documentDataModel: Nullable, + snapshot: Nullable, lexerTreeBuilder: LexerTreeBuilder, - getSnapshot: (data: DocumentDataModel) => IDocumentData, localeService: LocaleService, functionService: IFunctionService, styles: Styles ) { - cellData = Tools.deepClone(cellData); - - if (documentDataModel == null) { + if (snapshot?.body == null) { return null; } - - const snapshot = getSnapshot(documentDataModel); - const { body } = snapshot; - if (body == null) { - return null; - } - cellData.t = undefined; const data = body.dataStream; @@ -893,15 +848,11 @@ function emptyBody(body: IDocumentBody, removeStyle = false) { } if (body.paragraphs != null) { - if (body.paragraphs.length === 1) { - body.paragraphs[0].startIndex = 0; - } else { - body.paragraphs = [ - { - startIndex: 0, - }, - ]; - } + body.paragraphs = [ + { + startIndex: 0, + }, + ]; } if (body.sectionBreaks != null) { diff --git a/packages/sheets-ui/src/controllers/editor/formula-editor.controller.ts b/packages/sheets-ui/src/controllers/editor/formula-editor.controller.ts index e97c9bc4775..1caf4eee002 100644 --- a/packages/sheets-ui/src/controllers/editor/formula-editor.controller.ts +++ b/packages/sheets-ui/src/controllers/editor/formula-editor.controller.ts @@ -36,12 +36,12 @@ import { DocSkeletonManagerService, RichTextEditingMutation, } from '@univerjs/docs'; -import { CoverContentCommand, VIEWPORT_KEY as DOC_VIEWPORT_KEY } from '@univerjs/docs-ui'; +import { CoverContentCommand, VIEWPORT_KEY as DOC_VIEWPORT_KEY, IEditorService } from '@univerjs/docs-ui'; import { DeviceInputEventType, IRenderManagerService, ScrollBar } from '@univerjs/engine-render'; -import { takeUntil } from 'rxjs'; +import { combineLatest, filter, takeUntil } from 'rxjs'; import { getEditorObject } from '../../basics/editor/get-editor-object'; -import { IFormulaEditorManagerService } from '../../services/editor/formula-editor-manager.service'; import { IEditorBridgeService } from '../../services/editor-bridge.service'; +import { IFormulaEditorManagerService } from '../../services/editor/formula-editor-manager.service'; export class FormulaEditorController extends RxDisposable { private _loadedMap = new WeakSet(); @@ -54,7 +54,8 @@ export class FormulaEditorController extends RxDisposable { @IContextService private readonly _contextService: IContextService, @IFormulaEditorManagerService private readonly _formulaEditorManagerService: IFormulaEditorManagerService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, - @Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService + @Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService, + @IEditorService private readonly _editorService: IEditorService ) { super(); @@ -72,17 +73,12 @@ export class FormulaEditorController extends RxDisposable { this._create(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY); - this._textSelectionManagerService.textSelection$.pipe(takeUntil(this.dispose$)).subscribe((param) => { - if (param == null) { - return; - } - const { unitId } = param; - // Mark formula editor as non-focused, when current selection is not in formula editor. - if (unitId !== DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) { + this.disposeWithMe(this._editorService.focus$.subscribe(() => { + const focusUnitId = this._editorService.getFocusEditor()?.getEditorId(); + if (focusUnitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY) { this._contextService.setContextValue(FOCUSING_FX_BAR_EDITOR, false); - this._undoRedoService.clearUndoRedo(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY); } - }); + })); } private _handleContentChange() { @@ -210,9 +206,10 @@ export class FormulaEditorController extends RxDisposable { // Listen to changes in the size of the formula editor container to set the size of the editor. private _syncEditorSize() { - this._formulaEditorManagerService.position$.pipe(takeUntil(this.dispose$)).subscribe((position) => { + // this._univerInstanceService. + const addFOrmulaBar$ = this._univerInstanceService.unitAdded$.pipe(filter((unit) => unit.getUnitId() === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY)); + this.disposeWithMe(combineLatest([this._formulaEditorManagerService.position$, addFOrmulaBar$]).subscribe(([position]) => { if (!position) return this._clearScheduledCallback(); - const editorObject = getEditorObject(DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, this._renderManagerService); const formulaEditorDataModel = this._univerInstanceService.getUniverDocInstance( DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY @@ -227,7 +224,7 @@ export class FormulaEditorController extends RxDisposable { formulaEditorDataModel.updateDocumentDataPageSize(width); this.autoScroll(); this._scheduledCallback = requestIdleCallback(() => engine.resizeBySize(width, height)); - }); + })); } private _scheduledCallback: number = -1; diff --git a/packages/sheets-ui/src/controllers/render-controllers/editor-bridge.render-controller.ts b/packages/sheets-ui/src/controllers/render-controllers/editor-bridge.render-controller.ts index 6ffe1e4d6c5..f7a25ea6113 100644 --- a/packages/sheets-ui/src/controllers/render-controllers/editor-bridge.render-controller.ts +++ b/packages/sheets-ui/src/controllers/render-controllers/editor-bridge.render-controller.ts @@ -17,7 +17,7 @@ import type { ICommandInfo, IDisposable, IExecutionOptions, ISelectionCell, Nullable, Workbook } from '@univerjs/core'; import type { IEditorInputConfig } from '@univerjs/docs-ui'; import type { IRender, IRenderContext, IRenderModule } from '@univerjs/engine-render'; -import type { ISelectionWithStyle } from '@univerjs/sheets'; +import type { ISelectionWithStyle, ISetRangeValuesMutationParams } from '@univerjs/sheets'; import type { ICurrentEditCellParam, IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service'; import { DisposableCollection, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, FOCUSING_FX_BAR_EDITOR, FOCUSING_SHEET, ICommandService, IContextService, Inject, IUniverInstanceService, RxDisposable, toDisposable, UniverInstanceType } from '@univerjs/core'; import { DocSelectionRenderService, IEditorService, IRangeSelectorService } from '@univerjs/docs-ui'; @@ -172,13 +172,11 @@ export class EditorBridgeRenderController extends RxDisposable implements IRende if (!this._isCurrentSheetFocused()) { return; } - const isFocusFormulaEditor = this._contextService.getContextValue(FOCUSING_FX_BAR_EDITOR); const isFocusSheets = this._contextService.getContextValue(FOCUSING_SHEET); const unitId = render.unitId; if (this._editorBridgeService.isVisible().visible) return; - - if (unitId && isFocusSheets && !isFocusFormulaEditor && this._editorService.isSheetEditor(unitId)) { + if (unitId && isFocusSheets && !isFocusFormulaEditor) { this._showEditorByKeyboard(config); } })); @@ -199,12 +197,25 @@ export class EditorBridgeRenderController extends RxDisposable implements IRende } private _commandExecutedListener(d: DisposableCollection) { - const refreshCommandSet = new Set([ClearSelectionFormatCommand.id, SetRangeValuesMutation.id, SetZoomRatioCommand.id]); + const refreshCommandSet = new Set([ClearSelectionFormatCommand.id, SetZoomRatioCommand.id]); d.add(this._commandService.onCommandExecuted((command: ICommandInfo) => { if (refreshCommandSet.has(command.id)) { if (this._editorBridgeService.isVisible().visible) return; this._editorBridgeService.refreshEditCellState(); } + + if (command.id === SetRangeValuesMutation.id) { + const params = command.params as ISetRangeValuesMutationParams; + const { cellValue, unitId, subUnitId } = params; + if (!cellValue) return; + const editCell = this._editorBridgeService.getEditLocation(); + if (editCell) { + const { unitId: editingUnitId, sheetId: editingSheetId, row, column } = editCell; + if (unitId === editingUnitId && subUnitId === editingSheetId && cellValue?.[row]?.[column]) { + this._editorBridgeService.refreshEditCellState(); + } + } + } })); d.add(this._commandService.beforeCommandExecuted((command: ICommandInfo, options?: IExecutionOptions) => { @@ -216,12 +227,11 @@ export class EditorBridgeRenderController extends RxDisposable implements IRende } private _showEditorByKeyboard(config: Nullable) { - if (config == null) { + const event = config?.event as InputEvent; + if (config == null || (!event.data && event.inputType !== 'InsertParagraph')) { return; } - const event = config.event as KeyboardEvent; - this._commandService.executeCommand(SetCellEditVisibleOperation.id, { visible: true, eventType: DeviceInputEventType.Keyboard, diff --git a/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts b/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts index a17a7fb4038..7655b053170 100644 --- a/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts +++ b/packages/sheets-ui/src/controllers/render-controllers/scroll.render-controller.ts @@ -440,9 +440,9 @@ export class SheetsScrollRenderController extends Disposable implements IRenderM const selection = this._getSelectionsService().getCurrentLastSelection(); if (!selection) return; - const { startRow, startColumn, actualRow, actualColumn } = selection.primary; - const selectionStartRow = targetIsActualRowAndColumn ? actualRow : startRow; - const selectionStartColumn = targetIsActualRowAndColumn ? actualColumn : startColumn; + const { startRow, startColumn, actualRow, actualColumn } = selection.primary ?? selection.range; + const selectionStartRow = targetIsActualRowAndColumn ? actualRow ?? startRow : startRow; + const selectionStartColumn = targetIsActualRowAndColumn ? actualColumn ?? startColumn : startColumn; this._scrollToCell(selectionStartRow, selectionStartColumn); } diff --git a/packages/sheets-ui/src/plugin.ts b/packages/sheets-ui/src/plugin.ts index fd0bf34b628..7a749d93d89 100644 --- a/packages/sheets-ui/src/plugin.ts +++ b/packages/sheets-ui/src/plugin.ts @@ -184,6 +184,7 @@ export class UniverSheetsUIPlugin extends Plugin { [SheetsRenderService], [ActiveWorksheetController], [SheetPermissionInterceptorBaseController], + [SheetPermissionInitController], ]); } @@ -191,7 +192,6 @@ export class UniverSheetsUIPlugin extends Plugin { this._registerRenderModules(); touchDependencies(this._injector, [ - [SheetPermissionInitController], [SheetPermissionRenderManagerController], [SheetClipboardController], [FormulaEditorController], diff --git a/packages/sheets-ui/src/services/editor-bridge.service.ts b/packages/sheets-ui/src/services/editor-bridge.service.ts index ce7b4161a3e..93978ec6512 100644 --- a/packages/sheets-ui/src/services/editor-bridge.service.ts +++ b/packages/sheets-ui/src/services/editor-bridge.service.ts @@ -27,7 +27,6 @@ import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY, EDITOR_ACTIVATED, FOCUSING_EDITOR_STANDALONE, - FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, IContextService, Inject, IUniverInstanceService, @@ -83,8 +82,8 @@ export interface IEditorBridgeService { currentEditCellState$: Observable>; currentEditCellLayout$: Observable>; currentEditCell$: Observable>; - visible$: Observable; + forceKeepVisible$: Observable; dispose(): void; refreshEditCellState(): void; @@ -107,9 +106,6 @@ export interface IEditorBridgeService { export class EditorBridgeService extends Disposable implements IEditorBridgeService, IDisposable { private _editorUnitId: string = DOCS_NORMAL_EDITOR_UNIT_ID_KEY; - - private _isForceKeepVisible: boolean = false; - private _editorIsDirty: boolean = false; private _isDisabled: boolean = false; @@ -140,6 +136,9 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ private readonly _afterVisible$ = new BehaviorSubject(this._visible); readonly afterVisible$ = this._afterVisible$.asObservable(); + private readonly _forceKeepVisible$ = new BehaviorSubject(false); + readonly forceKeepVisible$ = this._forceKeepVisible$.asObservable(); + constructor( @Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @@ -219,10 +218,6 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ startY = this._currentEditCellLayout.position.startY; } - this._editorService.setOperationSheetUnitId(unitId); - - this._editorService.setOperationSheetSubUnitId(sheetId); - this._currentEditCellLayout = { position: { startX, @@ -251,7 +246,6 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ */ this._contextService.setContextValue(EDITOR_ACTIVATED, false); this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, false); - this._contextService.setContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, false); } const editCellState = this.getLatestEditCellState(); @@ -389,10 +383,6 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ } } - this._editorService.setOperationSheetUnitId(unitId); - - this._editorService.setOperationSheetSubUnitId(sheetId); - return { position: { startX, @@ -418,15 +408,6 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ } changeVisible(param: IEditorBridgeServiceVisibleParam) { - /** - * Non-sheetEditor and formula selection mode, - * double-clicking cannot activate the sheet editor. - */ - const editor = this._editorService.getFocusEditor(); - if (this._refSelectionsService.getCurrentSelections().length > 0 && editor && !editor.isSheetEditor()) { - return; - } - this._visible = param; // Reset the dirty status when the editor is visible. @@ -443,15 +424,15 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ } enableForceKeepVisible(): void { - this._isForceKeepVisible = true; + this._forceKeepVisible$.next(true); } disableForceKeepVisible(): void { - this._isForceKeepVisible = false; + this._forceKeepVisible$.next(false); } isForceKeepVisible(): boolean { - return this._isForceKeepVisible; + return this._forceKeepVisible$.getValue(); } changeEditorDirty(dirtyStatus: boolean) { diff --git a/packages/sheets-ui/src/services/editor/cell-editor-resize.service.ts b/packages/sheets-ui/src/services/editor/cell-editor-resize.service.ts index 967b9d7687b..7c39a00cb9f 100644 --- a/packages/sheets-ui/src/services/editor/cell-editor-resize.service.ts +++ b/packages/sheets-ui/src/services/editor/cell-editor-resize.service.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import type { IPosition, Nullable, Workbook } from '@univerjs/core'; +import type { DocumentDataModel, IPosition, Nullable, Workbook } from '@univerjs/core'; import type { DocumentSkeleton, IDocumentLayoutObject, IRenderContext, IRenderModule, Scene } from '@univerjs/engine-render'; -import { Disposable, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, HorizontalAlign, Inject, VerticalAlign, WrapStrategy } from '@univerjs/core'; +import { Disposable, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, HorizontalAlign, Inject, IUniverInstanceService, UniverInstanceType, VerticalAlign, WrapStrategy } from '@univerjs/core'; import { DocSkeletonManagerService } from '@univerjs/docs'; -import { VIEWPORT_KEY as DOC_VIEWPORT_KEY, DOCS_COMPONENT_MAIN_LAYER_INDEX } from '@univerjs/docs-ui'; +import { DOCS_COMPONENT_MAIN_LAYER_INDEX, VIEWPORT_KEY } from '@univerjs/docs-ui'; import { convertTextRotation, fixLineWidthByScale, IRenderManagerService, Rect, ScrollBar } from '@univerjs/engine-render'; import { ILayoutService } from '@univerjs/ui'; import { getEditorObject } from '../../basics/editor/get-editor-object'; @@ -43,18 +43,20 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM @ICellEditorManagerService private readonly _cellEditorManagerService: ICellEditorManagerService, @IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, - @Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService + @Inject(SheetSkeletonManagerService) private readonly _sheetSkeletonManagerService: SheetSkeletonManagerService, + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService ) { super(); } + // eslint-disable-next-line complexity fitTextSize(callback?: () => void) { const param = this._editorBridgeService.getEditCellState(); if (!param) return; const { position, documentLayoutObject, canvasOffset, scaleX, scaleY } = param; const { startX, startY, endX, endY } = position; - const documentDataModel = documentLayoutObject.documentModel; + const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); if (documentDataModel == null) { return; @@ -63,7 +65,7 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const documentSkeleton = this._getEditorSkeleton(); if (!documentSkeleton) return; - const { actualWidth, actualHeight } = this._predictingSize( + let { actualWidth, actualHeight } = this._predictingSize( position, canvasOffset, documentSkeleton, @@ -71,45 +73,61 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM scaleX, scaleY ); - const { verticalAlign, paddingData, fill } = documentLayoutObject; + + const { verticalAlign, horizontalAlign, paddingData, fill } = documentLayoutObject; + actualWidth = actualWidth + (paddingData.l ?? 0) + (paddingData.r ?? 0); + actualHeight = actualHeight + (paddingData.t ?? 0) + (paddingData.b ?? 0); let editorWidth = endX - startX; let editorHeight = endY - startY; - if (editorWidth < actualWidth) { - editorWidth = actualWidth; + editorWidth = Math.ceil(actualWidth); } if (editorHeight < actualHeight) { - editorHeight = actualHeight; - // To restore the page margins for the skeleton. - documentDataModel.updateDocumentDataMargin(paddingData); - } else { - // Set the top margin under vertical alignment. - let offsetTop = 0; - - if (verticalAlign === VerticalAlign.MIDDLE) { - offsetTop = (editorHeight - actualHeight) / 2 / scaleY; - } else if (verticalAlign === VerticalAlign.TOP) { - offsetTop = paddingData.t || 0; - } else { // VerticalAlign.UNSPECIFIED follow the same rule as HorizontalAlign.BOTTOM. - offsetTop = (editorHeight - actualHeight) / scaleY - (paddingData.b || 0); - } + editorHeight = Math.ceil(actualHeight); + } - // offsetTop /= scaleY; - offsetTop = offsetTop < (paddingData.t || 0) ? paddingData.t || 0 : offsetTop; + // Set the top margin under vertical alignment. + let offsetTop = 0; - documentDataModel.updateDocumentDataMargin({ - t: offsetTop, - }); + if (verticalAlign === VerticalAlign.MIDDLE) { + offsetTop = (editorHeight - actualHeight) / 2 / scaleY; + } else if (verticalAlign === VerticalAlign.TOP) { + offsetTop = paddingData.t || 0; + } else { + // VerticalAlign.UNSPECIFIED follow the same rule as HorizontalAlign.BOTTOM. + offsetTop = (editorHeight - actualHeight) / scaleY; } - // re-calculate skeleton(viewModel for component) - documentSkeleton.calculate(); + let offsetLeft = 0; + if (horizontalAlign === HorizontalAlign.CENTER) { + offsetLeft = (editorWidth - actualWidth) / 2 / scaleX; + } else if (horizontalAlign === HorizontalAlign.RIGHT) { + offsetLeft = (editorWidth - actualWidth) / scaleX; + } else { + offsetLeft = paddingData.l || 0; + } + + offsetTop = offsetTop < (paddingData.t || 0) ? paddingData.t || 0 : offsetTop; + offsetLeft = offsetLeft < (paddingData.l || 0) ? paddingData.l || 0 : offsetLeft; + documentDataModel.updateDocumentDataMargin({ + t: offsetTop, + l: offsetLeft, + }); - editorWidth -= 1; - editorHeight -= 1; - this._editAreaProcessing(editorWidth, editorHeight, position, canvasOffset, fill, scaleX, scaleY, callback); + documentSkeleton.calculate(); + this._editAreaProcessing( + editorWidth, + editorHeight, + position, + canvasOffset, + fill, + scaleX, + scaleY, + horizontalAlign, + callback + ); } /** @@ -129,7 +147,7 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const { textRotation, wrapStrategy } = documentLayoutObject; - const documentDataModel = documentLayoutObject.documentModel; + const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); const { vertexAngle: angle } = convertTextRotation(textRotation); @@ -167,12 +185,12 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM }); return { - actualWidth: editorWidth, + actualWidth: size.actualWidth * scaleX, actualHeight: size.actualHeight * scaleY, }; } - private _getEditorMaxSize(position: IPosition, canvasOffset: ICanvasOffset) { + private _getEditorMaxSize(position: IPosition, canvasOffset: ICanvasOffset, horizontalAlign: HorizontalAlign) { const editorObject = this._getEditorObject(); if (editorObject == null) { return; @@ -189,8 +207,8 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const widthOfCanvas = pxToNum(canvasElement.style.width); // declared width const { width } = canvasClientRect; // real width affected by scale const scaleAdjust = width / widthOfCanvas; - - const { startX, startY } = position; + const { startX, startY, endX } = position; + const enginWidth = this._context.engine.width; const clientHeight = document.body.clientHeight - @@ -199,11 +217,19 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM canvasOffset.top - EDITOR_BORDER_SIZE * 2; - const clientWidth = document.body.clientWidth - startX - canvasOffset.left; + let clientWidth = width - startX; + + if (horizontalAlign === HorizontalAlign.CENTER) { + const rightGap = enginWidth - endX; + const leftGap = startX; + clientWidth = (endX - startX) + Math.min(leftGap, rightGap) * 2; + } else if (horizontalAlign === HorizontalAlign.RIGHT) { + clientWidth = endX; + } return { height: clientHeight, - width: clientWidth, + width: clientWidth - EDITOR_BORDER_SIZE, scaleAdjust, }; } @@ -222,6 +248,7 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM fill: Nullable, scaleX: number = 1, scaleY: number = 1, + horizontalAlign: HorizontalAlign, callback?: () => void ) { const editorObject = this._getEditorObject(); @@ -233,13 +260,12 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const canvasElement = engine.getCanvasElement(); // We should take the scale into account when canvas is scaled by CSS. - let { startX, startY } = actualRangeWithCoord; const { document: documentComponent, scene: editorScene, engine: docEngine } = editorObject; - const viewportMain = editorScene.getViewport(DOC_VIEWPORT_KEY.VIEW_MAIN); + const viewportMain = editorScene.getViewport(VIEWPORT_KEY.VIEW_MAIN); - const info = this._getEditorMaxSize(actualRangeWithCoord, canvasOffset); + const info = this._getEditorMaxSize(actualRangeWithCoord, canvasOffset, horizontalAlign); if (!info) return; const { height: clientHeight, width: clientWidth, scaleAdjust } = info; @@ -248,13 +274,16 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM let scrollBar = viewportMain?.getScrollBar() as Nullable; if (physicHeight > clientHeight) { - physicHeight = clientHeight; - if (scrollBar == null) { viewportMain && new ScrollBar(viewportMain, { enableHorizontal: false, barSize: 8 }); } else { viewportMain?.resetCanvasSizeAndUpdateScroll(); } + viewportMain?.scrollToViewportPos({ + viewportScrollY: physicHeight - clientHeight, + }); + + physicHeight = clientHeight; } else { scrollBar = null; viewportMain?.getScrollBar()?.dispose(); @@ -266,10 +295,6 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM editorWidth = clientWidth; } - // move to fitTextSize - // startX -= FIX_ONE_PIXEL_BLUR_OFFSET; - // startY -= FIX_ONE_PIXEL_BLUR_OFFSET; - this._addBackground(editorScene, editorWidth / scaleX, editorHeight / scaleY, fill); const { scaleX: precisionScaleX, scaleY: precisionScaleY } = editorScene.getPrecisionScale(); @@ -302,6 +327,13 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM startX = startX * scaleAdjust + (canvasBoundingRect.left - contentBoundingRect.left); startY = startY * scaleAdjust + (canvasBoundingRect.top - contentBoundingRect.top); + const cellWidth = actualRangeWithCoord.endX - actualRangeWithCoord.startX; + if (horizontalAlign === HorizontalAlign.RIGHT) { + startX += (cellWidth - editorWidth) * scaleAdjust; + } else if (horizontalAlign === HorizontalAlign.CENTER) { + startX += (cellWidth - editorWidth * scaleAdjust) / 2; + } + // Update cell editor container position and size. this._cellEditorManagerService.setState({ startX, @@ -360,8 +392,9 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const skeleton = this._sheetSkeletonManagerService.getWorksheetSkeleton(editCellState.sheetId)?.skeleton; if (!skeleton) return; - const { row, column, scaleX, scaleY, position, canvasOffset } = editCellState; - const maxSize = this._getEditorMaxSize(position, canvasOffset); + const { row, column, scaleX, scaleY, position, canvasOffset, documentLayoutObject } = editCellState; + const { horizontalAlign } = documentLayoutObject; + const maxSize = this._getEditorMaxSize(position, canvasOffset, horizontalAlign); if (!maxSize) return; const { height: clientHeight, width: clientWidth, scaleAdjust } = maxSize; diff --git a/packages/sheets-ui/src/services/selection/base-selection-render.service.ts b/packages/sheets-ui/src/services/selection/base-selection-render.service.ts index dd019402736..6cee235ba3b 100644 --- a/packages/sheets-ui/src/services/selection/base-selection-render.service.ts +++ b/packages/sheets-ui/src/services/selection/base-selection-render.service.ts @@ -115,6 +115,8 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele endColumn: -1, }; + protected _activeControlIndex = -1; + /** * the posX of viewport when the pointer down */ @@ -419,6 +421,14 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele ); } + setActiveSelectionIndex(index: number) { + this._activeControlIndex = index; + } + + resetActiveSelectionIndex(): void { + this._activeControlIndex = -1; + } + /** * get active(actually last) selection control * @returns T extends SelectionControl @@ -426,7 +436,11 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele getActiveSelectionControl(): Nullable { const controls = this.getSelectionControls(); if (controls) { - return controls[controls.length - 1] as T; + if (this._activeControlIndex < 0) { + return controls[controls.length - 1] as T; + } + + return controls[this._activeControlIndex] as T; } } diff --git a/packages/sheets-ui/src/services/selection/selection-shape-extension.ts b/packages/sheets-ui/src/services/selection/selection-shape-extension.ts index e5246d168cd..ae6e2d3d8d7 100644 --- a/packages/sheets-ui/src/services/selection/selection-shape-extension.ts +++ b/packages/sheets-ui/src/services/selection/selection-shape-extension.ts @@ -27,7 +27,7 @@ import { SELECTION_CONTROL_BORDER_BUFFER_WIDTH } from '@univerjs/sheets'; import { SheetSkeletonManagerService } from '../sheet-skeleton-manager.service'; import { ISheetSelectionRenderService } from './base-selection-render.service'; import { genNormalSelectionStyle, RANGE_FILL_PERMISSION_CHECK, RANGE_MOVE_PERMISSION_CHECK } from './const'; -import { attachPrimaryWithCoord, attachSelectionWithCoord } from './util'; +import { attachSelectionWithCoord } from './util'; const HELPER_SELECTION_TEMP_NAME = '__SpreadsheetHelperSelectionTempRect'; @@ -258,8 +258,12 @@ export class SelectionShapeExtension { }); this._targetSelection = { ...selectionWithCoord.rangeWithCoord }; - const primaryWithCoordAndMergeInfo = attachPrimaryWithCoord(this._skeleton, primaryCell); - this._control.updateCurrCell(primaryWithCoordAndMergeInfo); + // DO NOT UPDATE CURR CELL while dragging whole selection. + // Updating the primary cell during the middle of a drag operation may result in the primary cell being out of range in certain scenarios. + // ex: dragging normal selection to a merged area. there is a check to see if this move is valid, if not, the selection process would revert back to original state. + + // normal selection should keep the original state when dragging whole selection. + // Now ref selection needs _control.selectionMoving$ update selection when dragging. this._control.selectionMoving$.next(selectionWithCoord.rangeWithCoord); } diff --git a/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx b/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx index 31810a29cac..590da6bbb85 100644 --- a/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx +++ b/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx @@ -14,13 +14,18 @@ * limitations under the License. */ -import type { IDocumentData } from '@univerjs/core'; -import { DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DocumentFlavor, IContextService, useDependency } from '@univerjs/core'; -import { IEditorService, TextEditor } from '@univerjs/docs-ui'; - -import { DISABLE_AUTO_FOCUS_KEY, useObservable } from '@univerjs/ui'; -import React, { useEffect, useState } from 'react'; +import type { KeyCode } from '@univerjs/ui'; +import { DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService, IContextService, useDependency } from '@univerjs/core'; +import { IEditorService } from '@univerjs/docs-ui'; +import { DeviceInputEventType } from '@univerjs/engine-render'; +import { ComponentManager, DISABLE_AUTO_FOCUS_KEY, MetaKeys, useEvent, useObservable, useSidebarClick } from '@univerjs/ui'; +import React, { useEffect, useRef, useState } from 'react'; +import { SetCellEditVisibleArrowOperation } from '../../commands/operations/cell-edit.operation'; + +import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY } from '../../common/keys'; +import { IEditorBridgeService } from '../../services/editor-bridge.service'; import { ICellEditorManagerService } from '../../services/editor/cell-editor-manager.service'; +import { useKeyEventConfig } from './hooks'; import styles from './index.module.less'; interface ICellIEditorProps { } @@ -45,36 +50,19 @@ export const EditorContainer: React.FC = () => { const cellEditorManagerService = useDependency(ICellEditorManagerService); const editorService = useDependency(IEditorService); const contextService = useDependency(IContextService); - + const componentManager = useDependency(ComponentManager); + const editorBridgeService = useDependency(IEditorBridgeService); + const visible = useObservable(editorBridgeService.visible$); + const commandService = useDependency(ICommandService); + const isRefSelecting = useRef<0 | 1 | 2>(0); const disableAutoFocus = useObservable( () => contextService.subscribeContextValue$(DISABLE_AUTO_FOCUS_KEY), false, undefined, [contextService, DISABLE_AUTO_FOCUS_KEY] ); - - const snapshot: IDocumentData = { - id: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, - body: { - dataStream: `${DEFAULT_EMPTY_DOCUMENT_VALUE}`, - tables: [], - textRuns: [], - paragraphs: [ - { - startIndex: 0, - }, - ], - sectionBreaks: [ - { - startIndex: 1, - }, - ], - }, - tableSource: {}, - documentStyle: { - documentFlavor: DocumentFlavor.UNSPECIFIED, - }, - }; + const FormulaEditor = componentManager.get(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY); + const editState = editorBridgeService.getEditLocation(); useEffect(() => { const sub = cellEditorManagerService.state$.subscribe((param) => { @@ -126,6 +114,30 @@ export const EditorContainer: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [disableAutoFocus, state]); + const handleClickSideBar = useEvent(() => { + if (editorBridgeService.isVisible().visible) { + editorBridgeService.changeVisible({ + visible: false, + eventType: DeviceInputEventType.PointerUp, + unitId: editState!.unitId, + }); + } + }); + + useSidebarClick(handleClickSideBar); + + const keyCodeConfig = useKeyEventConfig(isRefSelecting, editState?.unitId!); + + const onMoveInEditor = useEvent((keycode: KeyCode, metaKey: MetaKeys) => { + commandService.executeCommand(SetCellEditVisibleArrowOperation.id, { + keycode, + visible: false, + eventType: DeviceInputEventType.Keyboard, + isShift: metaKey === MetaKeys.SHIFT || metaKey === (MetaKeys.CTRL_COMMAND | MetaKeys.SHIFT), + unitId: editState?.unitId, + }); + }); + return (
= () => { height: state.height, }} > - + {FormulaEditor && ( + {}} + isFocus={visible?.visible} + unitId={editState?.unitId} + subUnitId={editState?.sheetId} + keyboradEventConfig={keyCodeConfig} + onMoveInEditor={onMoveInEditor} + isSupportAcrossSheet + resetSelectionOnBlur={false} + isSingle={false} + autoScrollbar={false} + onFormulaSelectingChange={(isSelecting: 0 | 1 | 2) => { + isRefSelecting.current = isSelecting; + if (isSelecting) { + editorBridgeService.enableForceKeepVisible(); + } else { + editorBridgeService.disableForceKeepVisible(); + } + }} + /> + )}
); }; diff --git a/packages/sheets-ui/src/views/editor-container/hooks.ts b/packages/sheets-ui/src/views/editor-container/hooks.ts new file mode 100644 index 00000000000..37ecbb871e0 --- /dev/null +++ b/packages/sheets-ui/src/views/editor-container/hooks.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IUniverInstanceService, useDependency, useObservable } from '@univerjs/core'; +import { DocSelectionRenderService } from '@univerjs/docs-ui'; +import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render'; +import { KeyCode } from '@univerjs/ui'; +import { useMemo } from 'react'; +import { IEditorBridgeService } from '../../services/editor-bridge.service'; + +export function useKeyEventConfig(isRefSelecting: React.MutableRefObject<0 | 1 | 2>, unitId: string) { + const editorBridgeService = useDependency(IEditorBridgeService); + + const keyCodeConfig = useMemo(() => ({ + keyCodes: [ + { keyCode: KeyCode.ENTER }, + { keyCode: KeyCode.ESC }, + { keyCode: KeyCode.TAB }, + ], + handler: (keycode: KeyCode) => { + if (keycode === KeyCode.ENTER || keycode === KeyCode.ESC || keycode === KeyCode.TAB) { + editorBridgeService.disableForceKeepVisible(); + editorBridgeService.changeVisible({ + visible: false, + eventType: DeviceInputEventType.Keyboard, + keycode, + unitId: unitId!, + }); + } + }, + }), [editorBridgeService, unitId]); + + return keyCodeConfig; +} + +export function useIsFocusing(editorId: string) { + const univerInstanceService = useDependency(IUniverInstanceService); + const renderManagerService = useDependency(IRenderManagerService); + const docSelectionRenderService = renderManagerService.getRenderById(editorId)?.with(DocSelectionRenderService); + useObservable(docSelectionRenderService?.onBlur$); + useObservable(docSelectionRenderService?.onFocus$); + + // useEffect(() => { + // if (docSelectionRenderService?.isFocusing) { + // univerInstanceService.focusUnit(editorId); + // } + // }, [docSelectionRenderService?.isFocusing, editorId, univerInstanceService]); + + return docSelectionRenderService?.isFocusing; +} diff --git a/packages/sheets-ui/src/views/editor-container/index.module.less b/packages/sheets-ui/src/views/editor-container/index.module.less index 19c13f673aa..f5d57ed5c13 100644 --- a/packages/sheets-ui/src/views/editor-container/index.module.less +++ b/packages/sheets-ui/src/views/editor-container/index.module.less @@ -24,5 +24,12 @@ canvas { position: absolute; } + + .sheet-embedding-formula-editor-wrap { + height: auto; + border: none; + padding: 0; + border-radius: 0; + } } } diff --git a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx index 804be38a9e2..8368d77113b 100644 --- a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx +++ b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx @@ -14,21 +14,22 @@ * limitations under the License. */ -import type { IDocumentData, Nullable, Workbook } from '@univerjs/core'; -import { BooleanNumber, DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DocumentFlavor, HorizontalAlign, IPermissionService, IUniverInstanceService, Rectangle, UniverInstanceType, useDependency, useObservable, VerticalAlign, WrapStrategy } from '@univerjs/core'; -import { TextEditor } from '@univerjs/docs-ui'; +import type { Workbook } from '@univerjs/core'; +import { DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, FOCUSING_FX_BAR_EDITOR, IContextService, IPermissionService, IUniverInstanceService, Rectangle, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; import { DeviceInputEventType } from '@univerjs/engine-render'; import { CheckMarkSingle, CloseSingle, DropdownSingle, FxSingle } from '@univerjs/icons'; import { RangeProtectionPermissionEditPoint, RangeProtectionRuleModel, SheetsSelectionsService, WorkbookEditablePermission, WorksheetEditPermission, WorksheetProtectionRuleModel, WorksheetSetCellValuePermission } from '@univerjs/sheets'; -import { ComponentContainer, KeyCode, useComponentsOfPart } from '@univerjs/ui'; +import { ComponentContainer, ComponentManager, KeyCode, useComponentsOfPart } from '@univerjs/ui'; import clsx from 'clsx'; -import React, { useEffect, useLayoutEffect, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { EMPTY, merge, switchMap } from 'rxjs'; +import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY } from '../../common/keys'; import { useActiveWorkbook } from '../../components/hook'; import { SheetsUIPart } from '../../consts/ui-name'; import { IEditorBridgeService } from '../../services/editor-bridge.service'; import { IFormulaEditorManagerService } from '../../services/editor/formula-editor-manager.service'; import { DefinedName } from '../defined-name/DefinedName'; +import { useKeyEventConfig } from '../editor-container/hooks'; import styles from './index.module.less'; enum ArrowDirection { @@ -47,13 +48,19 @@ export function FormulaBar() { const univerInstanceService = useDependency(IUniverInstanceService); const selectionManager = useDependency(SheetsSelectionsService); const permissionService = useDependency(IPermissionService); - const [disable, setDisable] = useState(false); const [imageDisable, setImageDisable] = useState(false); const currentWorkbook = useActiveWorkbook(); + const componentManager = useDependency(ComponentManager); const workbook = useObservable(() => univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET), undefined, undefined, [])!; - + const isRefSelecting = useRef<0 | 1 | 2>(0); + const editState = editorBridgeService.getEditLocation(); + const keyCodeConfig = useKeyEventConfig(isRefSelecting, editState?.unitId ?? ''); + const FormulaEditor = componentManager.get(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY); const formulaAuxUIParts = useComponentsOfPart(SheetsUIPart.FORMULA_AUX); + const contextService = useDependency(IContextService); + const isFocusFxBar = contextService.getContextValue(FOCUSING_FX_BAR_EDITOR); + const ref = useRef(null); function getPermissionIds(unitId: string, subUnitId: string): string[] { return [ @@ -107,44 +114,6 @@ export function FormulaBar() { }; }, [workbook]); - const INITIAL_SNAPSHOT: IDocumentData = { - id: DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, - body: { - dataStream: `${DEFAULT_EMPTY_DOCUMENT_VALUE}`, - textRuns: [], - tables: [], - paragraphs: [ - { - startIndex: 0, - }, - ], - sectionBreaks: [{ - startIndex: 1, - }], - }, - tableSource: {}, - documentStyle: { - pageSize: { - width: Number.POSITIVE_INFINITY, - height: Number.POSITIVE_INFINITY, - }, - documentFlavor: DocumentFlavor.UNSPECIFIED, - marginTop: 5, - marginBottom: 5, - marginRight: 0, - marginLeft: 0, - paragraphLineGapDefault: 0, - renderConfig: { - horizontalAlign: HorizontalAlign.UNSPECIFIED, - verticalAlign: VerticalAlign.TOP, - centerAngle: 0, - vertexAngle: 0, - wrapStrategy: WrapStrategy.WRAP, - isRenderStyle: BooleanNumber.FALSE, - }, - }, - }; - useEffect(() => { const subscription = editorBridgeService.visible$.subscribe((visibleInfo) => { setIconStyle(visibleInfo.visible ? styles.formulaActive : styles.formulaGrey); @@ -165,15 +134,20 @@ export function FormulaBar() { return () => subscription.unsubscribe(); }, [editorBridgeService.currentEditCellState$]); - function resizeCallBack(editor: Nullable) { - if (editor == null) { - return; - } + useEffect(() => { + if (ref.current) { + const handleResize = () => { + const editorRect = ref.current!.getBoundingClientRect(); + formulaEditorManagerService.setPosition(editorRect); + }; - const editorRect = editor.getBoundingClientRect(); + handleResize(); + const a = new ResizeObserver(handleResize); - formulaEditorManagerService.setPosition(editorRect); - } + a.observe(ref.current); + return () => a.disconnect(); + } + }, [formulaEditorManagerService]); function handleArrowClick() { setArrowDirection(arrowDirection === ArrowDirection.Down ? ArrowDirection.Up : ArrowDirection.Down); @@ -249,19 +223,34 @@ export function FormulaBar() {
-
- e.preventDefault()} - className={styles.formulaContent} - snapshot={INITIAL_SNAPSHOT} - isSingle={false} - disabled={disabled} - /> -
+
+
+ {FormulaEditor && ( + {}} + isFocus={isFocusFxBar} + className={styles.formulaContent} + unitId={editState?.unitId} + subUnitId={editState?.sheetId} + isSupportAcrossSheet + resetSelectionOnBlur={false} + isSingle={false} + keyboradEventConfig={keyCodeConfig} + onFormulaSelectingChange={(isSelecting: 0 | 1 | 2) => { + isRefSelecting.current = isSelecting; + if (isSelecting) { + editorBridgeService.enableForceKeepVisible(); + } else { + editorBridgeService.disableForceKeepVisible(); + } + }} + autoScrollbar={false} + /> + )} +
+
{arrowDirection === ArrowDirection.Down ? ( diff --git a/packages/sheets-ui/src/views/formula-bar/index.module.less b/packages/sheets-ui/src/views/formula-bar/index.module.less index ee2fc16823a..7ccaf5fdfab 100644 --- a/packages/sheets-ui/src/views/formula-bar/index.module.less +++ b/packages/sheets-ui/src/views/formula-bar/index.module.less @@ -87,6 +87,10 @@ } .formula-input { + flex: 1; + } + + .formula-container { overflow: hidden; display: flex; flex: 1; @@ -94,6 +98,14 @@ width: 100%; padding: 0 0 0 10px; + .sheet-embedding-formula-editor-wrap { + height: auto; + border: none; + padding: 0; + border-radius: 0; + height: 100%; + } + .formula-content { position: relative; diff --git a/packages/sheets-zen-editor/src/views/zen-editor/ZenEditor.tsx b/packages/sheets-zen-editor/src/views/zen-editor/ZenEditor.tsx index 7f7bada4fa5..12b856a844c 100644 --- a/packages/sheets-zen-editor/src/views/zen-editor/ZenEditor.tsx +++ b/packages/sheets-zen-editor/src/views/zen-editor/ZenEditor.tsx @@ -84,7 +84,6 @@ export function ZenEditor() { editorUnitId: DOCS_ZEN_EDITOR_UNIT_ID_KEY, initialSnapshot: INITIAL_SNAPSHOT, scrollBar: true, - noNeedVerticalAlign: true, backScrollOffset: 100, }, editorDom); @@ -104,10 +103,14 @@ export function ZenEditor() { }, []); // Empty dependency array means this effect runs once on mount and clean up on unmount function handleCloseBtnClick() { + const editor = editorService.getEditor(DOCS_ZEN_EDITOR_UNIT_ID_KEY); + editor?.blur(); commandService.executeCommand(CancelZenEditCommand.id); } function handleConfirmBtnClick() { + const editor = editorService.getEditor(DOCS_ZEN_EDITOR_UNIT_ID_KEY); + editor?.blur(); commandService.executeCommand(ConfirmZenEditCommand.id); } diff --git a/packages/sheets/api-extractor.json b/packages/sheets/api-extractor.json new file mode 100644 index 00000000000..1f20c772b0c --- /dev/null +++ b/packages/sheets/api-extractor.json @@ -0,0 +1,454 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "./lib/types/facade/f-selection.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we might specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + * + * The "bundledPackages" elements may specify glob patterns using minimatch syntax. To ensure deterministic + * output, globs are expanded by matching explicitly declared top-level dependencies only. For example, + * the pattern below will NOT match "@my-company/example" unless it appears in a field such as "dependencies" + * or "devDependencies" of the project's package.json file: + * + * "bundledPackages": [ "@my-company/*" ], + */ + "bundledPackages": [], + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". + * + * DEFAULT VALUE: "by-name" + */ + // "enumMemberOrder": "by-name", + + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": false + + /** + * The base filename for the API report files, to be combined with "reportFolder" or "reportTempFolder" + * to produce the full file path. The "reportFileName" should not include any path separators such as + * "\" or "/". The "reportFileName" should not include a file extension, since API Extractor will automatically + * append an appropriate file extension such as ".api.md". If the "reportVariants" setting is used, then the + * file extension includes the variant name, for example "my-report.public.api.md" or "my-report.beta.api.md". + * The "complete" variant always uses the simple extension "my-report.api.md". + * + * Previous versions of API Extractor required "reportFileName" to include the ".api.md" extension explicitly; + * for backwards compatibility, that is still accepted but will be discarded before applying the above rules. + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: "" + */ + // "reportFileName": "", + + /** + * To support different approval requirements for different API levels, multiple "variants" of the API report can + * be generated. The "reportVariants" setting specifies a list of variants to be generated. If omitted, + * by default only the "complete" variant will be generated, which includes all @internal, @alpha, @beta, + * and @public items. Other possible variants are "alpha" (@alpha + @beta + @public), "beta" (@beta + @public), + * and "public" (@public only). + * + * DEFAULT VALUE: [ "complete" ] + */ + // "reportVariants": ["public", "beta"], + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + // "reportFolder": "/etc/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/", + + /** + * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json", + + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false, + + /** + * The base URL where the project's source code can be viewed on a website such as GitHub or + * Azure DevOps. This URL path corresponds to the `` path on disk. + * + * This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items. + * For example, if the `projectFolderUrl` is "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor" and an API + * item's file path is "api/ExtractorConfig.ts", the full URL file path would be + * "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js". + * + * This setting can be omitted if you don't need source code links in your API documentation reference. + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "projectFolderUrl": "http://github.com/path/to/your/projectFolder" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "/dist/-alpha.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/packages/sheets/build.js b/packages/sheets/build.js new file mode 100644 index 00000000000..97500f1edf7 --- /dev/null +++ b/packages/sheets/build.js @@ -0,0 +1,20 @@ +const tsc = require('tsc-prog'); + +tsc.build({ + basePath: __dirname, // always required, used for relative paths + configFilePath: 'tsconfig.json', // config to inherit from (optional) + compilerOptions: { + rootDir: 'src', + outDir: 'dist', + declaration: true, + skipLibCheck: true, + }, + bundleDeclaration: { + entryPoint: './facade/index.d.ts', // relative to the OUTPUT directory ('dist' here) + fallbackOnError: false, // default: true + globals: false, // default: true + augmentations: false, // default: true + }, + include: ['src/**/*'], + exclude: ['**/*.test.ts', '**/*.spec.ts'], +}); diff --git a/packages/sheets/src/commands/operations/selection.operation.ts b/packages/sheets/src/commands/operations/selection.operation.ts index 603067d4a56..c6ffa7e70cc 100644 --- a/packages/sheets/src/commands/operations/selection.operation.ts +++ b/packages/sheets/src/commands/operations/selection.operation.ts @@ -28,6 +28,7 @@ export interface ISetSelectionsOperationParams { /** If should scroll to the selected range. */ reveal?: boolean; + extra?: string; } /** @@ -38,7 +39,6 @@ export const SetSelectionsOperation: IOperation = type: CommandType.OPERATION, handler: (accessor, params) => { if (!params) return false; - const { selections, type, unitId, subUnitId } = params; const selectionManagerService = getSelectionsService(accessor); diff --git a/packages/sheets/src/commands/utils/selection-command-util.ts b/packages/sheets/src/commands/utils/selection-command-util.ts index f8f6b4a3746..46e9fc8b54a 100644 --- a/packages/sheets/src/commands/utils/selection-command-util.ts +++ b/packages/sheets/src/commands/utils/selection-command-util.ts @@ -20,10 +20,11 @@ import { IRefSelectionsService } from '../../services/selections/ref-selections. import { REF_SELECTIONS_ENABLED, SheetsSelectionsService } from '../../services/selections/selection.service'; export function getSelectionsService( - accessor: IAccessor + accessor: IAccessor, + fromCurrentSelection?: boolean ): SheetsSelectionsService { const contextService = accessor.get(IContextService); const isInRefSelectionMode = contextService.getContextValue(REF_SELECTIONS_ENABLED); - return accessor.get(isInRefSelectionMode ? IRefSelectionsService : SheetsSelectionsService); + return accessor.get(isInRefSelectionMode && !fromCurrentSelection ? IRefSelectionsService : SheetsSelectionsService); } diff --git a/packages/sheets/src/services/selections/selection-data-model.ts b/packages/sheets/src/services/selections/selection-data-model.ts index fc25862df13..6681226f275 100644 --- a/packages/sheets/src/services/selections/selection-data-model.ts +++ b/packages/sheets/src/services/selections/selection-data-model.ts @@ -93,7 +93,7 @@ export class WorkbookSelectionModel extends Disposable { this._selectionMoveEnd$.next(selectionDatas); break; case SelectionMoveType.ONLY_SET: { - this._selectionSet$.next(selectionDatas); + this._eventAfterSetSelections(selectionDatas); break; } default: diff --git a/packages/sheets/src/tsdoc-metadata.json b/packages/sheets/src/tsdoc-metadata.json new file mode 100644 index 00000000000..4c59070de4d --- /dev/null +++ b/packages/sheets/src/tsdoc-metadata.json @@ -0,0 +1,11 @@ +// This file is read by tools that parse documentation comments conforming to the TSDoc standard. +// It should be published with your NPM package. It should not be tracked by Git. +{ + "tsdocVersion": "0.12", + "toolPackages": [ + { + "packageName": "@microsoft/api-extractor", + "packageVersion": "7.48.0" + } + ] +} diff --git a/packages/slides-ui/src/controllers/slide-editing.render-controller.ts b/packages/slides-ui/src/controllers/slide-editing.render-controller.ts index c4034ba6e9d..a02401ba2f6 100644 --- a/packages/slides-ui/src/controllers/slide-editing.render-controller.ts +++ b/packages/slides-ui/src/controllers/slide-editing.render-controller.ts @@ -14,6 +14,25 @@ * limitations under the License. */ +import type { + ICommandInfo, + IDisposable, + IDocumentBody, + IPosition, + Nullable, + SlideDataModel, + UnitModel } from '@univerjs/core'; +import type { IDocObjectParam, IEditorInputConfig } from '@univerjs/docs-ui'; +import type { + DocBackground, + Documents, + DocumentSkeleton, + IDocumentLayoutObject, + IRenderContext, + IRenderModule, + Scene, +} from '@univerjs/engine-render'; +import type { IEditorBridgeServiceVisibleParam } from '../services/slide-editor-bridge.service'; import { DEFAULT_EMPTY_DOCUMENT_VALUE, Direction, @@ -52,30 +71,11 @@ import { } from '@univerjs/engine-render'; import { ILayoutService, KeyCode } from '@univerjs/ui'; import { filter } from 'rxjs'; -import type { - ICommandInfo, - IDisposable, - IDocumentBody, - IPosition, - Nullable, - SlideDataModel, - UnitModel } from '@univerjs/core'; -import type { IDocObjectParam, IEditorInputConfig } from '@univerjs/docs-ui'; -import type { - DocBackground, - Documents, - DocumentSkeleton, - IDocumentLayoutObject, - IRenderContext, - IRenderModule, - Scene, -} from '@univerjs/engine-render'; import { SetTextEditArrowOperation } from '../commands/operations/text-edit.operation'; import { SLIDE_EDITOR_ID } from '../const'; import { ISlideEditorBridgeService } from '../services/slide-editor-bridge.service'; import { ISlideEditorManagerService } from '../services/slide-editor-manager.service'; import { CursorChange } from '../type'; -import type { IEditorBridgeServiceVisibleParam } from '../services/slide-editor-bridge.service'; const HIDDEN_EDITOR_POSITION = -1000; @@ -745,18 +745,6 @@ export class SlideEditingRenderController extends Disposable implements IRenderM this._handleEditorVisible({ visible: true, eventType: 3, unitId }); } - private _setOpenForCurrent(unitId: Nullable, subUnitId: Nullable) { - const editors = this._editorService.getAllEditor(); - for (const [_, ed] of editors) { - // if (!ed.isSheetEditor()) { - // continue; - // } - - ed.setOpenForSheetUnitId(unitId); - ed.setOpenForSheetSubUnitId(subUnitId); - } - } - private _getEditorObject() { return getEditorObject(this._editorBridgeService.getCurrentEditorId(), this._renderManagerService); } @@ -764,8 +752,6 @@ export class SlideEditingRenderController extends Disposable implements IRenderM private async _handleEditorInvisible(param: IEditorBridgeServiceVisibleParam) { const { keycode } = param; - this._setOpenForCurrent(null, null); - this._cursorChange = CursorChange.InitialState; this._exitInput(param); @@ -836,7 +822,10 @@ export class SlideEditingRenderController extends Disposable implements IRenderM * The logic here predicts the user's first cursor movement behavior based on this rule */ private _cursorStateListener(d: DisposableCollection) { - const editorObject = this._getEditorObject()!; + const editorObject = this._getEditorObject(); + if (!editorObject) { + return; + } const { document: documentComponent } = editorObject; d.add(toDisposable(documentComponent.onPointerDown$.subscribeEvent(() => { diff --git a/packages/slides-ui/src/services/slide-editor-bridge.service.ts b/packages/slides-ui/src/services/slide-editor-bridge.service.ts index 097236eade5..ed9dd80ba3f 100644 --- a/packages/slides-ui/src/services/slide-editor-bridge.service.ts +++ b/packages/slides-ui/src/services/slide-editor-bridge.service.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import type { IDisposable, IDocumentBody, IDocumentData, IDocumentSettings, IDocumentStyle, IParagraph, IParagraphStyle, IPosition, Nullable } from '@univerjs/core'; +import type { Engine, IDocumentLayoutObject, RichText, Scene } from '@univerjs/engine-render'; +import type { KeyCode } from '@univerjs/ui'; +import type { Observable } from 'rxjs'; import { createIdentifier, Disposable, @@ -29,10 +33,6 @@ import { IEditorService } from '@univerjs/docs-ui'; import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render'; import { SLIDE_KEY } from '@univerjs/slides'; import { BehaviorSubject, Subject } from 'rxjs'; -import type { IDisposable, IDocumentBody, IDocumentData, IDocumentSettings, IDocumentStyle, IParagraph, IParagraphStyle, IPosition, Nullable } from '@univerjs/core'; -import type { Engine, IDocumentLayoutObject, RichText, Scene } from '@univerjs/engine-render'; -import type { KeyCode } from '@univerjs/ui'; -import type { Observable } from 'rxjs'; import { SLIDE_EDITOR_ID } from '../const'; // TODO same as @univerjs/slides/views/render/adaptors/index.js diff --git a/packages/slides-ui/src/views/editor-container/EditorContainer.tsx b/packages/slides-ui/src/views/editor-container/EditorContainer.tsx index a04b58dcbdd..899620b44c4 100644 --- a/packages/slides-ui/src/views/editor-container/EditorContainer.tsx +++ b/packages/slides-ui/src/views/editor-container/EditorContainer.tsx @@ -16,7 +16,7 @@ import type { IDocumentData } from '@univerjs/core'; import { DEFAULT_EMPTY_DOCUMENT_VALUE, DocumentFlavor, IContextService, useDependency } from '@univerjs/core'; -import { IEditorService, TextEditor } from '@univerjs/docs-ui'; +import { IEditorService } from '@univerjs/docs-ui'; import { FIX_ONE_PIXEL_BLUR_OFFSET } from '@univerjs/engine-render'; import { DISABLE_AUTO_FOCUS_KEY, useObservable } from '@univerjs/ui'; @@ -131,14 +131,12 @@ export const SlideEditorContainer: React.FC = () => { height: state.height, }} > - + /> */}
); }; diff --git a/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx b/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx index 85ac407ffb1..6953fb5cf7c 100644 --- a/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx +++ b/packages/thread-comment-ui/src/views/thread-comment-editor/index.tsx @@ -14,18 +14,16 @@ * limitations under the License. */ -import type { IDocumentBody } from '@univerjs/core'; -import type { MentionProps } from '@univerjs/design'; +import type { IDocumentBody, IDocumentData } from '@univerjs/core'; +import type { Editor, IKeyboardEventConfig } from '@univerjs/docs-ui'; import type { IThreadComment } from '@univerjs/thread-comment'; -import { ICommandService, IMentionIOService, LocaleService, UniverInstanceType, useDependency } from '@univerjs/core'; -import { Button, Mention, Mentions } from '@univerjs/design'; -import { DocSelectionManagerService } from '@univerjs/docs'; -import { DocSelectionRenderService } from '@univerjs/docs-ui'; -import { IRenderManagerService } from '@univerjs/engine-render'; -import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; +import { BuildTextUtils, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, ICommandService, LocaleService, Tools, UniverInstanceType, useDependency } from '@univerjs/core'; +import { Button } from '@univerjs/design'; +import { BreakLineCommand, IEditorService, RichTextEditor } from '@univerjs/docs-ui'; +import { KeyCode } from '@univerjs/ui'; +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { SetActiveCommentOperation } from '../../commands/operations/comment.operations'; import styles from './index.module.less'; -import { parseMentions, transformDocument2TextNodes, transformTextNode2Text, transformTextNodes2Document } from './util'; export interface IThreadCommentEditorProps { id?: string; @@ -35,104 +33,93 @@ export interface IThreadCommentEditorProps { autoFocus?: boolean; unitId: string; subUnitId: string; + type: UniverInstanceType; } export interface IThreadCommentEditorInstance { reply: (text: IDocumentBody) => void; } -const defaultRenderSuggestion: MentionProps['renderSuggestion'] = (mention, search, highlightedDisplay, index, focused) => { - const icon = (mention as any).raw?.icon; - return ( -
- {icon ? : null} -
- {mention.display ?? mention.id} -
-
- ); -}; +function getSnapshot(body: IDocumentBody): IDocumentData { + return { + id: 'd', + body, + documentStyle: {}, + }; +} export const ThreadCommentEditor = forwardRef((props, ref) => { - const { comment, onSave, id, onCancel, autoFocus, unitId } = props; - const mentionIOService = useDependency(IMentionIOService); + const { comment, onSave, id, onCancel, autoFocus, unitId, type } = props; const commandService = useDependency(ICommandService); const localeService = useDependency(LocaleService); - const [localComment, setLocalComment] = useState({ ...comment }); const [editing, setEditing] = useState(false); - const inputRef = useRef(null); - const docSelectionManagerService = useDependency(DocSelectionManagerService); - const renderManagerService = useDependency(IRenderManagerService); - const docSelectionRenderService = renderManagerService.getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_DOC)?.with(DocSelectionRenderService); + const editorService = useDependency(IEditorService); + const editor = useRef(null); + const rootEditorId = type === UniverInstanceType.UNIVER_SHEET ? DOCS_NORMAL_EDITOR_UNIT_ID_KEY : unitId; + const [canSubmit, setCanSubmit] = useState(() => BuildTextUtils.transform.getPlainText(editor.current?.getDocumentData().body?.dataStream ?? '')); + useEffect(() => { + setCanSubmit(BuildTextUtils.transform.getPlainText(editor.current?.getDocumentData().body?.dataStream ?? '')); + + const sub = editor.current?.selectionChange$.subscribe(() => { + setCanSubmit(BuildTextUtils.transform.getPlainText(editor.current?.getDocumentData().body?.dataStream ?? '')); + }); + + return () => sub?.unsubscribe(); + }, [editor.current?.selectionChange$]); + + const keyboardEventConfig: IKeyboardEventConfig = useMemo(() => ( + { + keyCodes: [{ keyCode: KeyCode.ENTER }], + handler: (keyCode) => { + if (keyCode === KeyCode.ENTER) { + commandService.executeCommand( + BreakLineCommand.id + ); + } + }, + } + ), [commandService]); useImperativeHandle(ref, () => ({ reply(text) { - setLocalComment({ - ...comment, - text, - attachments: [], - }); - (inputRef.current as any)?.inputElement.focus(); + editor.current?.focus(); + editor.current?.setDocumentData(getSnapshot(text)); }, })); const handleSave = () => { - if (localComment.text) { + if (editor.current) { + const newText = Tools.deepClone(editor.current.getDocumentData().body); + setEditing(false); onSave?.({ - ...localComment, - text: localComment.text, + ...comment, + text: newText!, }); - setEditing(false); - setLocalComment({ text: undefined }); - (inputRef.current as any)?.inputElement.blur(); + editor.current.replaceText(''); + setTimeout(() => { + editor.current?.setSelectionRanges([]); + editor.current?.blur(); + }, 10); } }; return (
e.preventDefault()}> - { - const text = e.target.value; - if (!text) { - setLocalComment({ ...comment, text: undefined }); - } - setLocalComment?.({ ...comment, text: transformTextNodes2Document(parseMentions(e.target.value)) }); + initialValue={comment?.text && getSnapshot(comment.text)} + onFocusChange={(isFocus) => isFocus && setEditing(isFocus)} + isSingle={false} + onClickOutside={() => { + setTimeout(() => { + editorService.focus(rootEditorId); + }, 30); }} - onFocus={() => { - const activeRange = docSelectionManagerService.getActiveTextRange(); - if (activeRange && activeRange.collapsed) { - docSelectionRenderService?.removeAllRanges(); - } - docSelectionRenderService?.blur(); - setEditing(true); - }} - > - mentionIOService.list({ search: query, unitId }) - .then((res) => res.list.map( - (typeMentions) => ( - typeMentions.mentions.map( - (mention) => ({ - id: mention.objectId, - display: mention.label, - raw: mention, - }) - ) - ) - ).flat()) - .then(callback) as any} - displayTransform={(id, label) => `@${label} `} - renderSuggestion={defaultRenderSuggestion} - - /> - + /> {editing ? (
@@ -141,7 +128,7 @@ export const ThreadCommentEditor = forwardRef { onCancel?.(); setEditing(false); - setLocalComment({ text: undefined }); + editor.current?.replaceText('', true); commandService.executeCommand(SetActiveCommentOperation.id); }} > @@ -149,7 +136,7 @@ export const ThreadCommentEditor = forwardRef
@@ -200,6 +206,7 @@ export const ThreadCommentTree = (props: IThreadCommentTreeProps) => { onAddComment, onDeleteComment, onResolve, + type, } = props; const threadCommentModel = useDependency(ThreadCommentModel); const [isHover, setIsHover] = useState(false); @@ -338,6 +345,7 @@ export const ThreadCommentTree = (props: IThreadCommentTreeProps) => { isRoot={item.id === comments?.root.id} editing={editingId === item.id} resolved={comments?.root.resolved} + type={type} onEditingChange={(editing) => { if (editing) { setEditingId(item.id); @@ -371,6 +379,7 @@ export const ThreadCommentTree = (props: IThreadCommentTreeProps) => { { diff --git a/packages/ui/src/components/hooks/index.ts b/packages/ui/src/components/hooks/index.ts new file mode 100644 index 00000000000..9252a49ae32 --- /dev/null +++ b/packages/ui/src/components/hooks/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { useEvent } from './event'; +export { useObservable, useObservableRef } from './observable'; +export { useClickOutSide } from './useClickOutSide'; +export { useVirtualList } from './virtual-list'; diff --git a/packages/ui/src/components/hooks/useClickOutSide.ts b/packages/ui/src/components/hooks/useClickOutSide.ts new file mode 100644 index 00000000000..3a4e3e50586 --- /dev/null +++ b/packages/ui/src/components/hooks/useClickOutSide.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { RefObject } from 'react'; +import { useEffect } from 'react'; +import { useEvent } from './event'; + +export interface IUseClickOutSideOptions { + handler: () => void; +} + +export function useClickOutSide(ref: RefObject, opts: IUseClickOutSideOptions) { + const handler = useEvent(opts.handler); + + useEffect(() => { + const listener = (event: MouseEvent) => { + if (ref.current && event.target && !ref.current.contains(event.target as Node)) { + handler(); + } + }; + + document.addEventListener('mousedown', listener); + return () => { + document.removeEventListener('mousedown', listener); + }; + }, [handler, ref]); +} diff --git a/packages/ui/src/controllers/menus/menus.ts b/packages/ui/src/controllers/menus/menus.ts index 615ee556e8c..eef9b76740f 100644 --- a/packages/ui/src/controllers/menus/menus.ts +++ b/packages/ui/src/controllers/menus/menus.ts @@ -16,32 +16,42 @@ import type { IAccessor } from '@univerjs/core'; import type { IMenuButtonItem } from '../../services/menu/menu'; -import { IUndoRedoService, RedoCommand, UndoCommand } from '@univerjs/core'; +import { EDITOR_ACTIVATED, FOCUSING_FX_BAR_EDITOR, IContextService, IUndoRedoService, RedoCommand, UndoCommand } from '@univerjs/core'; + +import { combineLatest, merge, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { MenuItemType } from '../../services/menu/menu'; -export function UndoMenuItemFactory(accessor: IAccessor): IMenuButtonItem { +const undoRedoDisableFactory$ = (accessor: IAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); + const contextService = accessor.get(IContextService); + + return combineLatest([ + undoRedoService.undoRedoStatus$.pipe(map((v) => v.undos <= 0)), + merge([of({}), contextService.contextChanged$]), + ]).pipe(map(([undoDisable]) => { + return undoDisable || contextService.getContextValue(EDITOR_ACTIVATED) || contextService.getContextValue(FOCUSING_FX_BAR_EDITOR); + })); +}; +export function UndoMenuItemFactory(accessor: IAccessor): IMenuButtonItem { return { id: UndoCommand.id, type: MenuItemType.BUTTON, icon: 'UndoSingle', title: 'Undo', tooltip: 'toolbar.undo', - disabled$: undoRedoService.undoRedoStatus$.pipe(map((v) => v.undos <= 0)), + disabled$: undoRedoDisableFactory$(accessor), }; } export function RedoMenuItemFactory(accessor: IAccessor): IMenuButtonItem { - const undoRedoService = accessor.get(IUndoRedoService); - return { id: RedoCommand.id, type: MenuItemType.BUTTON, icon: 'RedoSingle', title: 'Redo', tooltip: 'toolbar.redo', - disabled$: undoRedoService.undoRedoStatus$.pipe(map((v) => v.redos <= 0)), + disabled$: undoRedoDisableFactory$(accessor), }; } diff --git a/packages/ui/src/controllers/shared-shortcut.controller.ts b/packages/ui/src/controllers/shared-shortcut.controller.ts index fe4b737794b..7c43de28758 100644 --- a/packages/ui/src/controllers/shared-shortcut.controller.ts +++ b/packages/ui/src/controllers/shared-shortcut.controller.ts @@ -17,7 +17,7 @@ import type { IContextService } from '@univerjs/core'; import type { IShortcutItem } from '../services/shortcut/shortcut.service'; -import { Disposable, FOCUSING_UNIVER_EDITOR, ICommandService, RedoCommand, UndoCommand } from '@univerjs/core'; +import { Disposable, EDITOR_ACTIVATED, FOCUSING_FX_BAR_EDITOR, FOCUSING_UNIVER_EDITOR, ICommandService, RedoCommand, UndoCommand } from '@univerjs/core'; import { CopyCommand, CutCommand, PasteCommand } from '../services/clipboard/clipboard.command'; import { KeyCode, MetaKeys } from '../services/shortcut/keycode'; import { IShortcutService } from '../services/shortcut/shortcut.service'; @@ -30,6 +30,13 @@ function whenEditorFocused(contextService: IContextService): boolean { return contextService.getContextValue(FOCUSING_UNIVER_EDITOR); } +function whenEditorFocusedButNotCellEditor(contextService: IContextService): boolean { + return ( + contextService.getContextValue(FOCUSING_UNIVER_EDITOR) && + !(contextService.getContextValue(EDITOR_ACTIVATED) || contextService.getContextValue(FOCUSING_FX_BAR_EDITOR)) + ); +} + export const CopyShortcutItem: IShortcutItem = { id: CopyCommand.id, description: 'shortcut.copy', @@ -72,7 +79,7 @@ export const UndoShortcutItem: IShortcutItem = { description: 'shortcut.undo', group: '1_common-edit', binding: KeyCode.Z | MetaKeys.CTRL_COMMAND, - preconditions: whenEditorFocused, + preconditions: whenEditorFocusedButNotCellEditor, }; export const RedoShortcutItem: IShortcutItem = { @@ -80,7 +87,7 @@ export const RedoShortcutItem: IShortcutItem = { description: 'shortcut.redo', group: '1_common-edit', binding: KeyCode.Y | MetaKeys.CTRL_COMMAND, - preconditions: whenEditorFocused, + preconditions: whenEditorFocusedButNotCellEditor, }; /** diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 47d938da867..f5dd1c9faec 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -20,11 +20,9 @@ export * from './common'; export { getHeaderFooterMenuHiddenObservable, getMenuHiddenObservable } from './common/menu-hidden-observable'; export { mergeMenuConfigs } from './common/menu-merge-configs'; export * from './components'; -export { useEvent } from './components/hooks/event'; export { t } from './components/hooks/locale'; -export { useObservable, useObservableRef } from './components/hooks/observable'; +export * from './components/hooks'; export { RectPopup } from './views/components/popup/RectPopup'; -export { useVirtualList } from './components/hooks/virtual-list'; export { Menu as UIMenu } from './components/menu/desktop/Menu'; export { type INotificationOptions, type NotificationType } from './components/notification/Notification'; export { ProgressBar } from './components/progress-bar/ProgressBar'; diff --git a/packages/ui/src/views/components/popup/RectPopup.tsx b/packages/ui/src/views/components/popup/RectPopup.tsx index 55003f9cea8..5921d9fc70f 100644 --- a/packages/ui/src/views/components/popup/RectPopup.tsx +++ b/packages/ui/src/views/components/popup/RectPopup.tsx @@ -70,7 +70,9 @@ function calcPopupPosition(layout: IPopupLayoutInfo): { top: number; left: numbe if (direction === 'vertical' || direction.includes('top') || direction.includes('bottom')) { const { left: startX, top: startY, right: endX, bottom: endY } = position; const verticalStyle = (direction === 'vertical' && endY > containerHeight - height - PUSHING_MINIMUM_GAP) || direction.indexOf('top') > -1 + // top ? { top: Math.max(startY - height, PUSHING_MINIMUM_GAP) } + // bottom : { top: Math.min(endY, containerHeight - height - PUSHING_MINIMUM_GAP) }; let horizontalStyle;