From b663b8944ec78b96dc14ae0e7a8a8ce5aefcaecf Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 00:14:23 +0800 Subject: [PATCH 01/41] refactor(sheets-ui): cell editor 1125 (#4383) Co-authored-by: lumixraku Co-authored-by: GitHub Actions Co-authored-by: jocs --- .../commands/operations/sidebar.operation.ts | 10 +- .../src/controllers/debugger.controller.ts | 6 +- .../src/views/test-editor/TestTextEditor.tsx | 117 - .../src/views/components/DocFormulaPopup.tsx | 6 +- .../src/views/uni-toolbar/UniFormulaBar.tsx | 6 +- .../docs/data-model/document-data-model.ts | 17 +- .../data-model/text-x/build-utils/index.ts | 3 +- .../text-x/build-utils/text-x-utils.ts | 74 +- .../core/src/docs/data-model/text-x/utils.ts | 113 +- packages/core/src/index.ts | 15 +- packages/core/src/services/context/context.ts | 1 + ...c-drawing-transformer-update.controller.ts | 1 - .../commands/add-doc-comment.command.ts | 1 + .../show-comment-panel.operation.ts | 4 +- ...doc-thread-comment-selection.controller.ts | 3 +- .../views/doc-thread-comment-panel/index.tsx | 6 +- .../src/basics/custom-decoration-factory.ts | 7 +- packages/docs-ui/src/basics/paragraph.ts | 10 + .../commands/replace-content.command.ts | 65 +- .../src/components/editor/TextEditor.tsx | 331 --- .../range-selector/RangeSelector.tsx | 379 --- .../range-selector/index.module.less | 158 -- .../doc-editor-bridge.controller.ts | 126 +- .../doc-selection-render.controller.ts | 13 +- .../doc.render-controller.ts | 10 + packages/docs-ui/src/docs-ui-plugin.ts | 3 +- packages/docs-ui/src/index.ts | 9 +- .../services/editor/editor-manager.service.ts | 458 +--- .../docs-ui/src/services/editor/editor.ts | 204 +- .../selection/doc-selection-render.service.ts | 43 +- packages/docs-ui/src/shortcuts/utils.ts | 4 +- .../src/views/rich-text-editor/hooks/index.ts | 18 + .../views/rich-text-editor/hooks/useEditor.ts | 85 + .../hooks/useKeyboardEvent.ts | 71 + .../hooks/useLeftAndRightArrow.ts | 113 + .../views/rich-text-editor/hooks/useResize.ts | 131 ++ .../views/rich-text-editor/index.module.less | 41 + .../src/views/rich-text-editor/index.tsx | 136 ++ .../mutations/core-editing.mutation.ts | 2 +- .../src/components/docs/document.ts | 1 + .../src/shape/base-scroll-bar.ts | 6 + .../engine-render/src/shape/scroll-bar.ts | 28 +- .../src/views/components/detail/index.tsx | 1 - .../formula-input/custom-formula-input.tsx | 3 +- .../views/components/list-dropdown/index.tsx | 6 +- .../__tests__/create-command-test-bed.ts | 3 +- .../operations/insert-function.operation.ts | 22 +- .../src/controllers/prompt.controller.ts | 2033 ----------------- .../src/controllers/utils/utils.ts | 7 +- .../ref-selections.render-service.ts | 2 +- .../src/sheets-formula-ui.plugin.ts | 12 +- .../help-function/HelpFunction.tsx | 239 +- .../formula-editor/hooks/useEditorPostion.ts | 63 + .../hooks/useFormulaDescribe.ts | 7 +- .../hooks/useFormulaSelection.ts | 114 + .../hooks/useSheetSelectionChange.ts | 146 +- .../views/formula-editor/hooks/useStateRef.ts | 4 +- .../src/views/formula-editor/hooks/util.ts | 25 + .../views/formula-editor/index.module.less | 14 +- .../src/views/formula-editor/index.tsx | 321 +-- .../search-function/SearchFunction.tsx | 61 +- .../views/more-functions/MoreFunctions.tsx | 23 +- .../views/range-selector/hooks/useFocus.ts | 30 +- .../range-selector/hooks/useFormulaToken.ts | 3 +- .../range-selector/hooks/useHighlight.ts | 100 +- .../range-selector/hooks/useKeyboardEvent.ts | 17 + .../hooks/useLeftAndRightArrow.ts | 123 +- .../range-selector/hooks/useRefactorEffect.ts | 20 +- .../range-selector/hooks/useResetSelection.ts | 25 +- .../views/range-selector/hooks/useResize.ts | 87 +- .../src/views/range-selector/index.tsx | 155 +- .../range-selector/utils/unitRangesToText.ts | 4 +- .../commands/inline-format.command.ts | 5 +- .../commands/set-selection.command.ts | 23 +- .../sidebar-defined-name.operation.ts | 2 - packages/sheets-ui/src/common/keys.ts | 1 + .../__tests__/end-edit.controller.spec.ts | 3 +- .../editor/data-sync.controller.ts | 51 +- .../editor/editing.render-controller.ts | 203 +- .../editor/formula-editor.controller.ts | 29 +- .../editor-bridge.render-controller.ts | 26 +- .../scroll.render-controller.ts | 6 +- packages/sheets-ui/src/plugin.ts | 2 +- .../src/services/editor-bridge.service.ts | 33 +- .../editor/cell-editor-resize.service.ts | 135 +- .../base-selection-render.service.ts | 16 +- .../selection/selection-shape-extension.ts | 10 +- .../editor-container/EditorContainer.tsx | 105 +- .../src/views/editor-container/hooks.ts | 63 + .../views/editor-container/index.module.less | 7 + .../src/views/formula-bar/FormulaBar.tsx | 119 +- .../src/views/formula-bar/index.module.less | 12 + .../src/views/zen-editor/ZenEditor.tsx | 5 +- packages/sheets/api-extractor.json | 454 ++++ packages/sheets/build.js | 20 + .../operations/selection.operation.ts | 2 +- .../commands/utils/selection-command-util.ts | 5 +- .../selections/selection-data-model.ts | 2 +- packages/sheets/src/tsdoc-metadata.json | 11 + .../slide-editing.render-controller.ts | 57 +- .../services/slide-editor-bridge.service.ts | 8 +- .../editor-container/EditorContainer.tsx | 12 +- .../src/views/thread-comment-editor/index.tsx | 147 +- .../src/views/thread-comment-editor/util.ts | 19 +- .../src/views/thread-comment-tree/index.tsx | 39 +- packages/ui/src/components/hooks/index.ts | 20 + .../src/components/hooks/useClickOutSide.ts | 40 + packages/ui/src/controllers/menus/menus.ts | 22 +- .../controllers/shared-shortcut.controller.ts | 13 +- packages/ui/src/index.ts | 4 +- .../src/views/components/popup/RectPopup.tsx | 2 + 111 files changed, 3245 insertions(+), 4993 deletions(-) delete mode 100644 packages-experimental/debugger/src/views/test-editor/TestTextEditor.tsx delete mode 100644 packages/docs-ui/src/components/editor/TextEditor.tsx delete mode 100644 packages/docs-ui/src/components/range-selector/RangeSelector.tsx delete mode 100644 packages/docs-ui/src/components/range-selector/index.module.less create mode 100644 packages/docs-ui/src/views/rich-text-editor/hooks/index.ts create mode 100644 packages/docs-ui/src/views/rich-text-editor/hooks/useEditor.ts create mode 100644 packages/docs-ui/src/views/rich-text-editor/hooks/useKeyboardEvent.ts create mode 100644 packages/docs-ui/src/views/rich-text-editor/hooks/useLeftAndRightArrow.ts create mode 100644 packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts create mode 100644 packages/docs-ui/src/views/rich-text-editor/index.module.less create mode 100644 packages/docs-ui/src/views/rich-text-editor/index.tsx delete mode 100644 packages/sheets-formula-ui/src/controllers/prompt.controller.ts create mode 100644 packages/sheets-formula-ui/src/views/formula-editor/hooks/useEditorPostion.ts create mode 100644 packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts create mode 100644 packages/sheets-formula-ui/src/views/formula-editor/hooks/util.ts create mode 100644 packages/sheets-formula-ui/src/views/range-selector/hooks/useKeyboardEvent.ts create mode 100644 packages/sheets-ui/src/views/editor-container/hooks.ts create mode 100644 packages/sheets/api-extractor.json create mode 100644 packages/sheets/build.js create mode 100644 packages/sheets/src/tsdoc-metadata.json create mode 100644 packages/ui/src/components/hooks/index.ts create mode 100644 packages/ui/src/components/hooks/useClickOutSide.ts diff --git a/packages-experimental/debugger/src/commands/operations/sidebar.operation.ts b/packages-experimental/debugger/src/commands/operations/sidebar.operation.ts index 57ce3d6e6129..a64529ca4542 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 646c6de34776..0e3ecd050409 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 e37b2fd1cf31..000000000000 --- 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 1b0b3b7e1b48..67e77dc14ac1 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 9ff2f59075aa..62b78b92eefc 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 f28ce073289a..b21584bde493 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 9b083e5a5914..32022eb0e4d2 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 9d2897f2a9b0..80125135d4ee 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 94d2ebd2ff31..50aaa28a5d1c 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 9d1809fd4268..9605cf72ef46 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 8a0d90e5f08c..ad36a09bb3d1 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 3d8cbe311334..77633acfee30 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 496f1fc7b8cb..9efb7f866493 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 3f02112b827a..cd00042ede01 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 428c99d9fbff..b7190d903bb5 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 1d924057c1d4..c5a2f6453d26 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 fd124e3f7229..d88e8bda45a6 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 b44b5bd1f212..000000000000 --- 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 d0c6aac9696f..000000000000 --- 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 0004b6aeec85..000000000000 --- 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 f6aad1762664..10ff840086fa 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 4cbe54480330..7af3a5bcf498 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 d5a51dee644c..62026d5b8b2b 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 73baea2857ad..cebc5235674f 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 049f39573bb4..f6bee8c5f11c 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 ec65a3bb0d8c..47c106967321 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 5694bd35d68c..5bb9f8d1b9e1 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 a3351bf1f55f..c2dcabac57ad 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 d69fd6499d17..2e3e52cc49f3 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 000000000000..5e0a9a86e6b6 --- /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 000000000000..c5fdf75a3e3c --- /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 000000000000..7f31211a9dff --- /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 000000000000..b8e8a3803406 --- /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 000000000000..b6e45b794e8b --- /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 000000000000..063fb869af53 --- /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 000000000000..dd3d9259c6fa --- /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 afd42631d570..fec9393e426d 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 0d0484bda804..6f4e756956e3 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 284dcf19f4ce..932f058a5676 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 7fb39c780322..f4e330766e0c 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 098218941603..b775a97e66bf 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 3f50e0e7279c..35475afca8da 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 ed7865135199..90a3d0e3c3e6 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 482727cfd6b6..7c0b9a5347c5 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 d7cfbdb67ca8..000000000000 --- 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 b092d20df47b..33345832e0f1 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 09122ad026b7..cec5f75055a7 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 9f75df91cf3d..565d8e7cd152 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 18395e8eeca1..e93dc5735802 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 000000000000..943fe08dc324 --- /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 048898254a21..b8563228a7fb 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 000000000000..1c74133e4a02 --- /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 1ae2f2a23c6f..580ba5ee7c69 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 37f2bff40ae6..a9cdf3afc040 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 000000000000..cd05f4dffdf2 --- /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 878daf0feb0d..f213596b76bb 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 803f5a9ba75c..5f1308011329 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 b93734b74980..7221ef37b1d8 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 886c42df9af5..f34c5bba8549 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 60a25bb957b3..c9e582e404ad 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 6d90ce9e0b87..253fdad80733 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 0111c0778dbf..d695bf9dce8a 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 000000000000..5a21a0682c7a --- /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 a3fcd0135c65..9d79391c670b 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 509065046f53..385373cd3597 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 fb06435fa6fa..6450936db853 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 25afa03b2749..aaf61e3869c3 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 904f2a7bb5ab..bb850527c5b5 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 55a84c03e858..2c0e06816b2b 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 96063e78a32d..424713f558cb 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 ca93651353e2..e943f69c637a 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 4b5d70f40515..26929e83947a 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 dcf5059631ea..7c29fd73a27e 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 d51dd0631e99..cc97e1ab7152 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 d4f8459e6614..aa2a003fd343 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 e97c9bc47753..1caf4eee0026 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 6ffe1e4d6c52..f7a25ea61131 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 a17a7fb40383..7655b053170c 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 fd0bf34b6285..7a749d93d895 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 ce7b4161a3ed..93978ec65128 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 967b9d7687b7..7c39a00cb9fb 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 dd0194027363..6cee235ba3b3 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 e5246d168cd6..ae6e2d3d8d74 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 31810a29cac0..590da6bbb852 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 000000000000..37ecbb871e00 --- /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 19c13f673aa5..f5d57ed5c13e 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 804be38a9e2a..8368d77113b3 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 ee2fc16823a9..7ccaf5fdfab8 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 7f7bada4fa58..12b856a844ce 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 000000000000..1f20c772b0cb --- /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 000000000000..97500f1edf79 --- /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 603067d4a567..c6ffa7e70cc4 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 f8f6b4a37462..46e9fc8b54a0 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 fc25862df135..6681226f2754 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 000000000000..4c59070de4d9 --- /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 c4034ba6e9da..a02401ba2f67 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 097236eade59..ed9dd80ba3f9 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 a04b58dcbdd0..899620b44c48 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 85ac407ffb15..6953fb5cf7cc 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 000000000000..9252a49ae323 --- /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 000000000000..3a4e3e50586c --- /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 615ee556e8c5..eef9b76740f6 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 fe4b737794bd..7c43de28758c 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 47d938da8677..f5dd1c9faeca 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 55003f9cea84..5921d9fc70fe 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; From 9cf6447e122c924ee8642e2055730f344a5341d9 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 00:42:41 +0800 Subject: [PATCH 02/41] fix: start edit with f2 --- .../operations/cell-edit.operation.ts | 15 ++++- .../editor/data-sync.controller.ts | 1 + .../editor/editing.render-controller.ts | 65 ++++++++++++------- .../editor/formula-editor.controller.ts | 2 +- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/packages/sheets-ui/src/commands/operations/cell-edit.operation.ts b/packages/sheets-ui/src/commands/operations/cell-edit.operation.ts index 28fe99ee578c..72ba5e60f697 100644 --- a/packages/sheets-ui/src/commands/operations/cell-edit.operation.ts +++ b/packages/sheets-ui/src/commands/operations/cell-edit.operation.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import type { IOperation } from '@univerjs/core'; +import type { IOperation, Workbook } from '@univerjs/core'; import type { IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service'; -import { CommandType, ICommandService } from '@univerjs/core'; +import { CommandType, ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core'; import { IEditorBridgeService } from '../../services/editor-bridge.service'; export const SetCellEditVisibleOperation: IOperation = { @@ -40,7 +40,16 @@ export const SetCellEditVisibleWithF2Operation: IOperation { const commandService = accessor.get(ICommandService); - commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, params); + const univerInstanceService = accessor.get(IUniverInstanceService); + const workbook = univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); + if (!workbook) { + return false; + } + commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, { + ...params, + unitId: workbook.getUnitId(), + }); + return true; }, }; 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 d51dd0631e99..df87a3fd5823 100644 --- a/packages/sheets-ui/src/controllers/editor/data-sync.controller.ts +++ b/packages/sheets-ui/src/controllers/editor/data-sync.controller.ts @@ -184,6 +184,7 @@ export class EditorDataSyncController extends Disposable { this._commandService.syncExecuteCommand(RichTextEditingMutation.id, { ...parmas, + textRanges: null, isSync: true, unitId, syncer: parmas.unitId, 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 d4f8459e6614..7aef54b6500a 100644 --- a/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts +++ b/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts @@ -98,7 +98,6 @@ export class EditingRenderController extends Disposable implements IRenderModule @Inject(SheetsSelectionsService) selectionManagerService: SheetsSelectionsService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IContextService private readonly _contextService: IContextService, - @IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @IEditorBridgeService private readonly _editorBridgeService: IEditorBridgeService, @ICellEditorManagerService private readonly _cellEditorManagerService: ICellEditorManagerService, @@ -118,7 +117,7 @@ export class EditingRenderController extends Disposable implements IRenderModule // EditingRenderController is per unit. It should only handle keyboard events when the unit is // the current of its type. - this.disposeWithMe(this._instanceSrv.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { + this.disposeWithMe(this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => { if (workbook?.getUnitId() === this._context.unitId) { this._d = this._init(); } else { @@ -160,7 +159,7 @@ export class EditingRenderController extends Disposable implements IRenderModule this._commandExecutedListener(d); this._initSkeletonListener(d); - this.disposeWithMe(this._instanceSrv.unitDisposed$.subscribe((_unit: UnitModel) => { + this.disposeWithMe(this._univerInstanceService.unitDisposed$.subscribe((_unit: UnitModel) => { clearTimeout(this._cursorTimeout); })); @@ -261,7 +260,7 @@ export class EditingRenderController extends Disposable implements IRenderModule return; } - if (this._instanceSrv.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY) === documentLayoutObject.documentModel) { + if (this._getDocumentDataModel() === documentLayoutObject.documentModel) { return; } @@ -274,7 +273,7 @@ export class EditingRenderController extends Disposable implements IRenderModule documentModel!.updateDocumentDataPageSize((endX - startX) / scaleX); } - this._instanceSrv.changeDoc(editorUnitId, documentModel!); + this._univerInstanceService.changeDoc(editorUnitId, documentModel!); this._contextService.setContextValue(FOCUSING_EDITOR_BUT_HIDDEN, true); this._textSelectionManagerService.replaceTextRanges([{ startOffset: 0, @@ -351,6 +350,7 @@ 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. + // eslint-disable-next-line complexity private _handleEditorVisible(param: IEditorBridgeServiceVisibleParam) { const { eventType, keycode } = param; @@ -377,7 +377,6 @@ export class EditingRenderController extends Disposable implements IRenderModule this._editorBridgeService.refreshEditCellPosition(false); const { - documentLayoutObject, editorUnitId, unitId, sheetId, @@ -396,7 +395,7 @@ export class EditingRenderController extends Disposable implements IRenderModule this._contextService.setContextValue(EDITOR_ACTIVATED, true); - const { documentModel: documentDataModel } = documentLayoutObject; + const documentDataModel = this._getDocumentDataModel(); const skeleton = this._getEditorSkeleton(editorUnitId); if (!skeleton || !documentDataModel) { return; @@ -409,8 +408,29 @@ export class EditingRenderController extends Disposable implements IRenderModule viewportScrollY: Number.POSITIVE_INFINITY, }); }); - // move selection - if ( + + // f2, continue to edit + if (eventType === DeviceInputEventType.Keyboard && keycode === KeyCode.F2) { + document.makeDirty(); + this._textSelectionManagerService.replaceDocRanges([ + { + startOffset: 0, + endOffset: 0, + }, + ]); + const endOffset = (documentDataModel.getBody()?.dataStream.length ?? 2) - 2; + this._textSelectionManagerService.replaceDocRanges( + [{ + startOffset: endOffset, + endOffset, + }], + { + unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + } + ); + } else if ( + // clear and edit eventType === DeviceInputEventType.Keyboard || (eventType === DeviceInputEventType.Dblclick && isInArrayFormulaRange) ) { @@ -423,19 +443,22 @@ export class EditingRenderController extends Disposable implements IRenderModule this._editorBridgeService.changeEditorDirty(true); } - this._textSelectionManagerService.replaceDocRanges([ - { + this._textSelectionManagerService.replaceDocRanges( + [{ startOffset: 0, endOffset: 0, - }, - ]); + }], + { + unitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + subUnitId: DOCS_NORMAL_EDITOR_UNIT_ID_KEY, + } + ); } else if (eventType === DeviceInputEventType.Dblclick) { if (this._contextService.getContextValue(FOCUSING_EDITOR_INPUT_FORMULA)) { return; } const cursor = documentDataModel.getBody()!.dataStream.length - 2 || 0; - this._textSelectionManagerService.replaceDocRanges([ { startOffset: cursor, @@ -681,10 +704,14 @@ export class EditingRenderController extends Disposable implements IRenderModule } } + private _getDocumentDataModel() { + return this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); + } + // WTF: this is should not exist at all. It is because all editor instances reuse the singleton // "DocSelectionManagerService" and other modules. Which will be refactored soon in August, 2024. private _isCurrentSheetFocused(): boolean { - return this._instanceSrv.getFocusedUnit()?.getUnitId() === this._context.unitId; + return this._univerInstanceService.getFocusedUnit()?.getUnitId() === this._context.unitId; } private _getEditorSkeleton(editorId: string) { @@ -696,13 +723,7 @@ export class EditingRenderController extends Disposable implements IRenderModule } private _emptyDocumentDataModel(removeStyle: boolean) { - const editCellState = this._editorBridgeService.getEditCellState(); - if (editCellState == null) { - return; - } - - const { documentLayoutObject } = editCellState; - const documentDataModel = documentLayoutObject.documentModel; + const documentDataModel = this._getDocumentDataModel(); if (documentDataModel == null) { return; } 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 e97c9bc47753..10fe40220cc1 100644 --- a/packages/sheets-ui/src/controllers/editor/formula-editor.controller.ts +++ b/packages/sheets-ui/src/controllers/editor/formula-editor.controller.ts @@ -40,8 +40,8 @@ import { CoverContentCommand, VIEWPORT_KEY as DOC_VIEWPORT_KEY } from '@univerjs import { DeviceInputEventType, IRenderManagerService, ScrollBar } from '@univerjs/engine-render'; import { 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(); From 8c60cb6a5ce46e9287ced97667325988b5170b72 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 01:17:55 +0800 Subject: [PATCH 03/41] feat: update --- .../src/views/mention-edit-popup/index.tsx | 9 ++- .../src/views/mention-list/index.module.less | 2 + .../src/views/mention-list/index.tsx | 10 ++- .../src/views/rich-text-editor/index.tsx | 4 ++ .../src/views/thread-comment-editor/util.ts | 70 ++----------------- .../src/views/thread-comment-tree/index.tsx | 1 - 6 files changed, 20 insertions(+), 76 deletions(-) diff --git a/packages/docs-mention-ui/src/views/mention-edit-popup/index.tsx b/packages/docs-mention-ui/src/views/mention-edit-popup/index.tsx index 91be5187e288..80da58d5a89b 100644 --- a/packages/docs-mention-ui/src/views/mention-edit-popup/index.tsx +++ b/packages/docs-mention-ui/src/views/mention-edit-popup/index.tsx @@ -17,6 +17,7 @@ import type { DocumentDataModel, ITypeMentionList } from '@univerjs/core'; import { ICommandService, IMentionIOService, IUniverInstanceService, Tools, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; import { DocSelectionManagerService } from '@univerjs/docs'; +import { IEditorService } from '@univerjs/docs-ui'; import React, { useEffect, useMemo, useState } from 'react'; import { filter } from 'rxjs'; import { AddDocMentionCommand } from '../../commands/commands/doc-mention.command'; @@ -29,6 +30,7 @@ export const MentionEditPopup = () => { const univerInstanceService = useDependency(IUniverInstanceService); const editPopup = useObservable(popupService.editPopup$); const mentionIOService = useDependency(IMentionIOService); + const editorService = useDependency(IEditorService); const documentDataModel = editPopup ? univerInstanceService.getUnit(editPopup.unitId) : null; const textSelectionService = useDependency(DocSelectionManagerService); const [mentions, setMentions] = useState([]); @@ -48,17 +50,17 @@ export const MentionEditPopup = () => { } })(); }, [mentionIOService, editPopup, search]); - if (!editPopup) { return null; } return ( popupService.closeEditPopup()} mentions={mentions} - onSelect={(mention) => { - commandService.executeCommand(AddDocMentionCommand.id, { + onSelect={async (mention) => { + await commandService.executeCommand(AddDocMentionCommand.id, { unitId: univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC)!.getUnitId(), mention: { ...mention, @@ -66,6 +68,7 @@ export const MentionEditPopup = () => { }, startIndex: editPopup.anchor, }); + editorService.focus(editPopup.unitId); }} /> ); diff --git a/packages/docs-mention-ui/src/views/mention-list/index.module.less b/packages/docs-mention-ui/src/views/mention-list/index.module.less index 63690c41360c..d15c31ca501b 100644 --- a/packages/docs-mention-ui/src/views/mention-list/index.module.less +++ b/packages/docs-mention-ui/src/views/mention-list/index.module.less @@ -25,6 +25,7 @@ margin-right: 6px; flex: 0 0 auto; border-radius: 6px; + pointer-events: none; } &-active { @@ -40,5 +41,6 @@ flex: 1 1 0; white-space: nowrap; text-overflow: ellipsis; + pointer-events: none; } } diff --git a/packages/docs-mention-ui/src/views/mention-list/index.tsx b/packages/docs-mention-ui/src/views/mention-list/index.tsx index 2912f771023c..5550c2c180c4 100644 --- a/packages/docs-mention-ui/src/views/mention-list/index.tsx +++ b/packages/docs-mention-ui/src/views/mention-list/index.tsx @@ -24,27 +24,25 @@ export interface IMentionListProps { active?: string; onSelect?: (item: IMention) => void; onClick?: () => void; + editorId: string; } export const MentionList = (props: IMentionListProps) => { - const { mentions, active, onSelect, onClick } = props; + const { mentions, active, onSelect, onClick, editorId } = props; const ref = useRef(null); const [activeId, setActiveId] = useState(active ?? mentions[0]?.mentions[0]?.objectId); const handleSelect = (item: IMention) => { onSelect?.(item); }; - // useEffect(() => { - // ref.current?.focus(); - // }, []); - return ( -
+
{mentions.map((typeMentions) => (
{typeMentions.title}
{typeMentions.mentions.map((mention) => (
handleSelect(mention)} diff --git a/packages/docs-ui/src/views/rich-text-editor/index.tsx b/packages/docs-ui/src/views/rich-text-editor/index.tsx index dd3d9259c6fa..70186fdf28f3 100644 --- a/packages/docs-ui/src/views/rich-text-editor/index.tsx +++ b/packages/docs-ui/src/views/rich-text-editor/index.tsx @@ -93,6 +93,9 @@ export const RichTextEditor = forwardRef((props, r useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (editorService.getFocusId() !== editorId) return; + + const id = (event.target as HTMLDivElement)?.dataset?.editorid; + if (id === editorId) return; if (sheetEmbeddingRef.current && !sheetEmbeddingRef.current.contains(event.target as any)) { onClickOutside?.(); } @@ -101,6 +104,7 @@ export const RichTextEditor = forwardRef((props, r setTimeout(() => { document.addEventListener('click', handleClickOutside); }, 100); + return () => { document.removeEventListener('click', handleClickOutside); }; diff --git a/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts b/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts index c3c7ca6f8d4b..9a5783fe2a5a 100644 --- a/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts +++ b/packages/thread-comment-ui/src/views/thread-comment-editor/util.ts @@ -25,62 +25,6 @@ export type TextNode = { content: IThreadCommentMention; }; -export const parseMentions = (text: string): TextNode[] => { - const regex = /@\[(.*?)\]\((.*?)\)|(\w+)/g; - let match; - let lastIndex = 0; - const result: TextNode[] = []; - - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - // Add the text between two user mentions or before the first user mention - result.push({ - type: 'text', - content: text.substring(lastIndex, match.index), - }); - } - - if (match[1] && match[2]) { - // User mention found - result.push({ - type: 'mention', - content: { - label: match[1], - id: match[2], - }, - }); - } else if (match[3]) { - // Text (numbers) found - result.push({ - type: 'text', - content: match[3], - }); - } - lastIndex = regex.lastIndex; - } - - // Add any remaining text after the last mention (if any) - if (lastIndex < text.length) { - result.push({ - type: 'text', - content: text.substring(lastIndex), - }); - } - - return result; -}; - -export const transformTextNode2Text = (nodes: TextNode[]) => { - return nodes.map((item) => { - switch (item.type) { - case 'mention': - return `@[${item.content.label}](${item.content.id})`; - default: - return item.content; - } - }).join(''); -}; - const transformDocument2TextNodesInParagraph = (doc: IDocumentBody) => { const { dataStream, customRanges } = doc; const end = dataStream.endsWith('\r\n') ? dataStream.length - 2 : dataStream.length; @@ -98,11 +42,11 @@ const transformDocument2TextNodesInParagraph = (doc: IDocumentBody) => { textNodes.push({ type: 'mention', content: { - label: dataStream.slice(range.startIndex, range.endIndex).slice(1, -1), + label: dataStream.slice(range.startIndex, range.endIndex + 1), id: range.rangeId, }, }); - lastIndex = range.endIndex; + lastIndex = range.endIndex + 1; }); textNodes.push({ @@ -134,8 +78,8 @@ export const transformTextNodes2Document = (nodes: TextNode[]): IDocumentBody => break; case 'mention': { const start = str.length; - str += `\x1F${node.content.label}\x1E`; - const end = str.length; + str += node.content.label; + const end = str.length - 1; customRanges.push({ rangeId: node.content.id, rangeType: CustomRangeType.MENTION, @@ -170,9 +114,3 @@ export const transformTextNodes2Document = (nodes: TextNode[]): IDocumentBody => customRanges, }; }; - -export const transformMention = (mention: IThreadCommentMention) => ({ - display: mention.label, - id: `${mention.id}`, - raw: mention, -}); diff --git a/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx b/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx index 0dea5d26c331..34aa3d6ce822 100644 --- a/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx +++ b/packages/thread-comment-ui/src/views/thread-comment-tree/index.tsx @@ -171,7 +171,6 @@ const ThreadCommentItem = (props: IThreadCommentItemProps) => { case 'mention': return ( - @ {item.content.label} {' '} From f3bed4014209f3de0fa2b793e366ece040096a69 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 01:38:58 +0800 Subject: [PATCH 04/41] feat: formula-editor emit change --- .../formula-input/list-formula-input.tsx | 50 +++++++++---------- .../src/views/formula-editor/index.tsx | 15 +++--- packages/ui/src/components/hooks/index.ts | 1 + .../ui/src/components/hooks/update-effect.ts | 29 +++++++++++ 4 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 packages/ui/src/components/hooks/update-effect.ts diff --git a/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx b/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx index 9447de612171..2d87c1196a94 100644 --- a/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx +++ b/packages/sheets-data-validation-ui/src/views/components/formula-input/list-formula-input.tsx @@ -284,33 +284,29 @@ export function ListFormulaInput(props: IFormulaInputProps) { }); }, [strList, onChange, isFormulaStr, formulaStrCopy, refColors]); - const updateFormula = useMemo( - () => - async (str: string) => { - if (!isFormulaString(str)) { - onChange?.({ - formula1: '', - formula2, - }); - return; - } - if (dataValidationFormulaController.getFormulaRefCheck(str)) { - onChange?.({ - formula1: isFormulaString(str) ? str : '', - formula2, - }); - setLocalError(''); - } else { - onChange?.({ - formula1: '', - formula2, - }); - setFormulaStr('='); - setLocalError(localeService.t('dataValidation.validFail.formulaError')); - } - }, - [formula2, onChange] - ); + const updateFormula = useEvent(async (str: string) => { + if (!isFormulaString(str)) { + onChange?.({ + formula1: '', + formula2, + }); + return; + } + if (dataValidationFormulaController.getFormulaRefCheck(str)) { + onChange?.({ + formula1: isFormulaString(str) ? str : '', + formula2, + }); + setLocalError(''); + } else { + onChange?.({ + formula1: '', + formula2, + }); + setFormulaStr('='); + setLocalError(localeService.t('dataValidation.validFail.formulaError')); + } + }); const formulaEditorActionsRef = useRef[0]['actions']>({}); const [isFocusFormulaEditor, isFocusFormulaEditorSet] = useState(false); 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 5f1308011329..6eaf1732f1ae 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -25,10 +25,9 @@ import { BuildTextUtils, createInternalEditorID, generateRandomId, IUniverInstan 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 { useEvent, useUpdateEffect } from '@univerjs/ui'; import clsx from 'clsx'; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useEmitChange } from '../range-selector/hooks/useEmitChange'; import { useFocus } from '../range-selector/hooks/useFocus'; import { useFormulaToken } from '../range-selector/hooks/useFormulaToken'; import { useDocHight, useSheetHighlight } from '../range-selector/hooks/useHighlight'; @@ -83,7 +82,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { isSupportAcrossSheet = false, onFocus = noop, onBlur = noop, - onChange, + onChange: propOnChange, onVerify, actions, className, @@ -99,7 +98,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { const editorService = useDependency(IEditorService); const sheetEmbeddingRef = useRef(null); - + const onChange = useEvent(propOnChange); // init actions if (actions) { actions.handleOutClick = (e: MouseEvent, cb: () => void) => { @@ -136,9 +135,10 @@ export function FormulaEditor(props: IFormulaEditorProps) { const docFocusing = currentDoc?.getUnitId() === editorId; const refSelections = useRef([] as IRefSelection[]); const selectingMode = isSelecting; - const needEmit = useEmitChange(sequenceNodes, (text: string) => { - onChange(`=${text}`); - }, editor); + + useUpdateEffect(() => { + onChange(formulaText); + }, [formulaText, onChange]); const highlightDoc = useDocHight('='); const highlightSheet = useSheetHighlight(unitId); @@ -238,7 +238,6 @@ export function FormulaEditor(props: IFormulaEditorProps) { if (!isFocusing) { return; } - needEmit(); highlight(`=${refString}`, true, isEnd); if (isEnd) { focus(); diff --git a/packages/ui/src/components/hooks/index.ts b/packages/ui/src/components/hooks/index.ts index 9252a49ae323..abc23835d122 100644 --- a/packages/ui/src/components/hooks/index.ts +++ b/packages/ui/src/components/hooks/index.ts @@ -16,5 +16,6 @@ export { useEvent } from './event'; export { useObservable, useObservableRef } from './observable'; +export { useUpdateEffect } from './update-effect'; export { useClickOutSide } from './useClickOutSide'; export { useVirtualList } from './virtual-list'; diff --git a/packages/ui/src/components/hooks/update-effect.ts b/packages/ui/src/components/hooks/update-effect.ts new file mode 100644 index 000000000000..b3bf45b54abb --- /dev/null +++ b/packages/ui/src/components/hooks/update-effect.ts @@ -0,0 +1,29 @@ +/** + * 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 React, { useEffect } from 'react'; + +export const useUpdateEffect: typeof React.useEffect = (effect, deps) => { + const hasMount = React.useRef(false); + + useEffect(() => { + if (hasMount.current) { + return effect(); + } else { + hasMount.current = true; + } + }, deps); +}; From ee73d97335e3dc3009ae6e538dc329441fd0f810 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 01:45:33 +0800 Subject: [PATCH 05/41] feat: range-selector onChange --- .../range-selector/hooks/useEmitChange.ts | 47 ------------------- .../src/views/range-selector/index.tsx | 27 +++++++---- 2 files changed, 19 insertions(+), 55 deletions(-) delete mode 100644 packages/sheets-formula-ui/src/views/range-selector/hooks/useEmitChange.ts diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useEmitChange.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useEmitChange.ts deleted file mode 100644 index 491fb63c7f90..000000000000 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useEmitChange.ts +++ /dev/null @@ -1,47 +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 { Editor } from '@univerjs/docs-ui'; -import type { INode } from './useFormulaToken'; -import { useEffect, useRef } from 'react'; -import { sequenceNodeToText } from '../utils/sequenceNodeToText'; - -export const useEmitChange = (sequenceNodes: INode[], onChange: (v: string) => void, editor?: Editor) => { - const isNeedEmit = useRef(false); - - useEffect(() => { - if (editor) { - const d = editor.input$.subscribe(() => { - isNeedEmit.current = true; - }); - return () => { - d.unsubscribe(); - }; - } - }, [editor]); - - useEffect(() => { - if (isNeedEmit.current && sequenceNodes) { - isNeedEmit.current = false; - const result = sequenceNodeToText(sequenceNodes); - onChange(result); - } - }, [sequenceNodes]); - - const needEmit = () => isNeedEmit.current = true; - - return needEmit; -}; 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 bb850527c5b5..f1900689e1e1 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -15,27 +15,28 @@ */ import type { IDisposable, IUnitRangeName } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; import type { Editor } from '@univerjs/docs-ui'; import type { ReactNode } from 'react'; import type { IRefSelection } from './hooks/useHighlight'; -import { createInternalEditorID, generateRandomId, ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; +import { BuildTextUtils, createInternalEditorID, generateRandomId, ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType, useDependency, useObservable } from '@univerjs/core'; import { Button, Dialog, Input, Tooltip } from '@univerjs/design'; +import { RichTextEditingMutation } from '@univerjs/docs'; 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 { IDescriptionService } from '@univerjs/sheets-formula'; 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 React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; 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'; import { useFocus } from './hooks/useFocus'; import { useFormulaToken } from './hooks/useFormulaToken'; @@ -98,7 +99,7 @@ export function RangeSelector(props: IRangeSelectorProps) { errorText, placeholder, actions, - onChange = noopFunction, + onChange: propOnChange = noopFunction, onVerify = noopFunction, onRangeSelectorDialogVisibleChange = noopFunction, onBlur = noopFunction, @@ -107,6 +108,7 @@ export function RangeSelector(props: IRangeSelectorProps) { isOnlyOneRange = false, isSupportAcrossSheet = false, } = props; + const onChange = useEvent(propOnChange); const editorService = useDependency(IEditorService); const localeService = useDependency(LocaleService); const commandService = useDependency(ICommandService); @@ -203,13 +205,23 @@ export function RangeSelector(props: IRangeSelectorProps) { } }); - const needEmit = useEmitChange(sequenceNodes, handleInput, editor); + useEffect(() => { + const sub = commandService.onCommandExecuted((info) => { + if (info.id === RichTextEditingMutation.id) { + const params = info.params as IRichTextEditingMutationParams; + const { unitId } = params; + if (unitId === editorId) { + onChange(BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); + } + } + }); + return () => sub.dispose(); + }, [commandService, editor, editorId, onChange]); const handleSheetSelectionChange = useMemo(() => { return (text: string, offset: number, isEnd: boolean) => { highligh(text); rangeStringSet(text); - needEmit(); if (isEnd) { focus(); if (offset !== -1) { @@ -317,7 +329,6 @@ export function RangeSelector(props: IRangeSelectorProps) { const handleConfirm = (ranges: IUnitRangeName[]) => { const text = unitRangesToText(ranges, isSupportAcrossSheet).join(matchToken.COMMA); highligh(text); - needEmit(); rangeStringSet(text); rangeDialogVisibleSet(false); onRangeSelectorDialogVisibleChange(false); From 15067ab618c1ae2df339fd17a1d1af8876f822b6 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 02:12:14 +0800 Subject: [PATCH 06/41] fix: hide popup on moving --- .../services/canvas-pop-manager.service.ts | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts index 14096787981c..14afd4b1f1f5 100644 --- a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts +++ b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts @@ -18,9 +18,9 @@ import type { DrawingTypeEnum, ICommandInfo, INeedCheckDisposable, IRange, Nulla import type { BaseObject, IBoundRectNoAngle, IRender, SpreadsheetSkeleton, Viewport } from '@univerjs/engine-render'; import type { ISetWorksheetRowAutoHeightMutationParams, ISheetLocationBase } from '@univerjs/sheets'; import type { IPopup } from '@univerjs/ui'; -import { Disposable, DisposableCollection, ICommandService, Inject, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; +import { Disposable, DisposableCollection, ICommandService, Inject, Injector, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; -import { COMMAND_LISTENER_SKELETON_CHANGE, RefRangeService, SetFrozenMutation, SetWorksheetRowAutoHeightMutation } from '@univerjs/sheets'; +import { COMMAND_LISTENER_SKELETON_CHANGE, getSelectionsService, RefRangeService, SetFrozenMutation, SetWorksheetRowAutoHeightMutation } from '@univerjs/sheets'; import { ICanvasPopupService } from '@univerjs/ui'; import { BehaviorSubject } from 'rxjs'; import { SetScrollOperation } from '../commands/operations/scroll.operation'; @@ -32,6 +32,7 @@ import { SheetSkeletonManagerService } from './sheet-skeleton-manager.service'; export interface ICanvasPopup extends Omit { mask?: boolean; extraProps?: Record; + showOnSelectionMoving?: boolean; } interface IPopupMenuItem { @@ -51,9 +52,28 @@ export class SheetCanvasPopManagerService extends Disposable { @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, @Inject(RefRangeService) private readonly _refRangeService: RefRangeService, - @ICommandService private readonly _commandService: ICommandService + @ICommandService private readonly _commandService: ICommandService, + @Inject(Injector) private readonly _injector: Injector ) { super(); + + this._initMoving(); + } + + private _isSelectionMoving = false; + + private _initMoving() { + const slectionService = getSelectionsService(this._injector); + this.disposeWithMe( + slectionService.selectionMoving$.subscribe(() => { + this._isSelectionMoving = true; + }) + ); + this.disposeWithMe( + slectionService.selectionMoveEnd$.subscribe(() => { + this._isSelectionMoving = false; + }) + ); } /** @@ -214,7 +234,7 @@ export class SheetCanvasPopManagerService extends Disposable { attachPopupToObject(targetObject: BaseObject, popup: ICanvasPopup): INeedCheckDisposable { const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; const worksheet = workbook.getActiveSheet(); - if (!worksheet) { + if (!worksheet || (this._isSelectionMoving && !popup.showOnSelectionMoving)) { return { dispose: () => { // empty @@ -283,6 +303,10 @@ export class SheetCanvasPopManagerService extends Disposable { return null; } + if (this._isSelectionMoving && !popup.showOnSelectionMoving) { + return; + } + const skeleton = this._renderManagerService.getRenderById(unitId)?.with(SheetSkeletonManagerService).getOrCreateSkeleton({ sheetId: subUnitId, }); @@ -345,6 +369,10 @@ export class SheetCanvasPopManagerService extends Disposable { return null; } + if (this._isSelectionMoving && !popup.showOnSelectionMoving) { + return; + } + const position$ = new BehaviorSubject(bound); const id = this._globalPopupManagerService.addPopup({ ...popup, @@ -375,10 +403,9 @@ export class SheetCanvasPopManagerService extends Disposable { * @param _unitId * @param _subUnitId * @param viewport - * @param showOnSelectionMoving * @returns */ - attachPopupToCell(row: number, col: number, popup: ICanvasPopup, _unitId?: string, _subUnitId?: string, viewport?: Viewport, showOnSelectionMoving = false): Nullable { + attachPopupToCell(row: number, col: number, popup: ICanvasPopup, _unitId?: string, _subUnitId?: string, viewport?: Viewport): Nullable { const workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET)!; const worksheet = workbook.getActiveSheet(); if (!worksheet) { @@ -400,7 +427,7 @@ export class SheetCanvasPopManagerService extends Disposable { return null; } - if (sheetSelectionRenderService.selectionMoving && !showOnSelectionMoving) { + if (this._isSelectionMoving && (!popup.showOnSelectionMoving)) { return; } From b93a1fde90e395b20720302d6e6e6b9ea2db33c1 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 02:19:31 +0800 Subject: [PATCH 07/41] fix: hide popup on selection move --- .../services/canvas-pop-manager.service.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts index 14afd4b1f1f5..86f9e6defb74 100644 --- a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts +++ b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts @@ -18,9 +18,9 @@ import type { DrawingTypeEnum, ICommandInfo, INeedCheckDisposable, IRange, Nulla import type { BaseObject, IBoundRectNoAngle, IRender, SpreadsheetSkeleton, Viewport } from '@univerjs/engine-render'; import type { ISetWorksheetRowAutoHeightMutationParams, ISheetLocationBase } from '@univerjs/sheets'; import type { IPopup } from '@univerjs/ui'; -import { Disposable, DisposableCollection, ICommandService, Inject, Injector, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; +import { Disposable, DisposableCollection, ICommandService, Inject, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; -import { COMMAND_LISTENER_SKELETON_CHANGE, getSelectionsService, RefRangeService, SetFrozenMutation, SetWorksheetRowAutoHeightMutation } from '@univerjs/sheets'; +import { COMMAND_LISTENER_SKELETON_CHANGE, IRefSelectionsService, RefRangeService, SetFrozenMutation, SetWorksheetRowAutoHeightMutation, SheetsSelectionsService } from '@univerjs/sheets'; import { ICanvasPopupService } from '@univerjs/ui'; import { BehaviorSubject } from 'rxjs'; import { SetScrollOperation } from '../commands/operations/scroll.operation'; @@ -53,7 +53,8 @@ export class SheetCanvasPopManagerService extends Disposable { @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, @Inject(RefRangeService) private readonly _refRangeService: RefRangeService, @ICommandService private readonly _commandService: ICommandService, - @Inject(Injector) private readonly _injector: Injector + @IRefSelectionsService private readonly _refSelectionsService: ISheetSelectionRenderService, + @Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService ) { super(); @@ -63,14 +64,24 @@ export class SheetCanvasPopManagerService extends Disposable { private _isSelectionMoving = false; private _initMoving() { - const slectionService = getSelectionsService(this._injector); this.disposeWithMe( - slectionService.selectionMoving$.subscribe(() => { + this._refSelectionsService.selectionMoving$.subscribe(() => { this._isSelectionMoving = true; }) ); this.disposeWithMe( - slectionService.selectionMoveEnd$.subscribe(() => { + this._refSelectionsService.selectionMoveEnd$.subscribe(() => { + this._isSelectionMoving = false; + }) + ); + + this.disposeWithMe( + this._selectionManagerService.selectionMoving$.subscribe(() => { + this._isSelectionMoving = true; + }) + ); + this.disposeWithMe( + this._selectionManagerService.selectionMoveEnd$.subscribe(() => { this._isSelectionMoving = false; }) ); From fb79580a43dcf4912748ae47e2da3fe546a5f132 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 02:20:16 +0800 Subject: [PATCH 08/41] fix: ts --- packages/sheets-formula-ui/src/views/range-selector/index.tsx | 1 - 1 file changed, 1 deletion(-) 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 f1900689e1e1..c3413ec6c13b 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -264,7 +264,6 @@ export function RangeSelector(props: IRangeSelectorProps) { const text = (e.data.body?.dataStream ?? '').replaceAll(/\n|\r/g, '').replaceAll(/,{2,}/g, ',').replaceAll(/(^,)/g, ''); highligh(text, false); rangeStringSet(text); - needEmit(); }); return () => { dispose.unsubscribe(); From 9df20d23c839bb5c2e9d6c6314d00d81da7fd978 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 02:29:12 +0800 Subject: [PATCH 09/41] feat: remove useless code --- .../doc-editor-bridge.controller.ts | 35 ------------------- 1 file changed, 35 deletions(-) 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 10ff840086fa..8f00a4d3d34f 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 @@ -57,8 +57,6 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu this._commandExecutedListener(); - this._initialSetValue(); - this._initialBlur(); this._initialFocus(); @@ -111,22 +109,6 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu mainComponent?.resize(contentWidth, contentHeight); } - 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, - // }); - // }) - // ); - } - private _initialBlur() { this.disposeWithMe( this._editorService.blur$.subscribe(() => { @@ -157,17 +139,6 @@ 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._docSelectionRenderService.removeAllRanges(); - // this._docSelectionRenderService.addDocRanges([textRange]); - // }) - // ); - const focusExcepts = [ 'univer-formula-search', 'univer-formula-help', @@ -186,12 +157,6 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu }) ); - // this.disposeWithMe( - // fromEvent(window, 'mousedown').subscribe(() => { - // this._editorService.changeSpreadsheetFocusState(false); - // }) - // ); - const currentUniverSheet = this._univerInstanceService.getAllUnitsForType(UniverInstanceType.UNIVER_SHEET); currentUniverSheet.forEach((unit) => { const unitId = unit.getUnitId(); From ff1bab7c2f9698fa943f0a87808402d7f944e568 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 02:37:50 +0800 Subject: [PATCH 10/41] feat: remove react-mentions --- packages/design/package.json | 2 - .../components/mentions/Mentions.stories.tsx | 68 ------------------ .../src/components/mentions/Mentions.tsx | 33 --------- .../src/components/mentions/index.module.less | 71 ------------------- .../design/src/components/mentions/index.ts | 19 ----- packages/design/src/index.ts | 1 - .../docs-ui/src/services/editor/editor.ts | 4 ++ pnpm-lock.yaml | 58 --------------- 8 files changed, 4 insertions(+), 252 deletions(-) delete mode 100644 packages/design/src/components/mentions/Mentions.stories.tsx delete mode 100644 packages/design/src/components/mentions/Mentions.tsx delete mode 100644 packages/design/src/components/mentions/index.module.less delete mode 100644 packages/design/src/components/mentions/index.ts diff --git a/packages/design/package.json b/packages/design/package.json index e063af61080b..7b2a103f28c2 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -70,7 +70,6 @@ "dependencies": { "@rc-component/color-picker": "^2.0.1", "@rc-component/trigger": "^2.2.5", - "@types/react-mentions": "^4.4.0", "@univerjs/icons": "^0.2.8", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -88,7 +87,6 @@ "rc-virtual-list": "^3.15.0", "react-draggable": "^4.4.6", "react-grid-layout": "^1.5.0", - "react-mentions": "^4.4.10", "react-transition-group": "^4.4.5", "tailwind-merge": "^2.5.5" }, diff --git a/packages/design/src/components/mentions/Mentions.stories.tsx b/packages/design/src/components/mentions/Mentions.stories.tsx deleted file mode 100644 index cb18c89f34bf..000000000000 --- a/packages/design/src/components/mentions/Mentions.stories.tsx +++ /dev/null @@ -1,68 +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 { Meta } from '@storybook/react'; -import React, { useState } from 'react'; - -import { Mention } from 'react-mentions'; -import { Mentions } from './Mentions'; - -const meta: Meta = { - title: 'Components / Mentions', - component: Mentions, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], -}; - -export default meta; - -export const InputBasic = { - - render() { - const [value, onChange] = useState(''); - - return ( -
- onChange(e.target.value)} - > - - -
- - ); - }, -}; - diff --git a/packages/design/src/components/mentions/Mentions.tsx b/packages/design/src/components/mentions/Mentions.tsx deleted file mode 100644 index a3ae171f08ab..000000000000 --- a/packages/design/src/components/mentions/Mentions.tsx +++ /dev/null @@ -1,33 +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 React, { forwardRef } from 'react'; -import type { MentionsInputProps } from 'react-mentions'; -import { MentionsInput } from 'react-mentions'; -import styles from './index.module.less'; - -export interface IMentionsProps extends MentionsInputProps {} - -export const Mentions = forwardRef, IMentionsProps>((props, ref) => { - return ( - - ); -}); - diff --git a/packages/design/src/components/mentions/index.module.less b/packages/design/src/components/mentions/index.module.less deleted file mode 100644 index 9a85dbadb3ef..000000000000 --- a/packages/design/src/components/mentions/index.module.less +++ /dev/null @@ -1,71 +0,0 @@ -.mentions { - width: 100%; -} - -.mentions__control { - min-height: 32px; -} - -.mentions__highlighter { - border-radius: 6px; - background: rgba(var(--color-white)); - padding: 6px 10px; - font-size: 13px !important; - line-height: 20px !important; - max-height: 114px; - - strong { - color: rgb(var(--blue-500)); - } -} - -.mentions__highlighter__substring { - visibility: inherit !important; - color: rgb(var(--color-black)); -} - -.mentions__input { - width: 100%; - caret-color: red; - background-color: transparent; - color: transparent; - padding: 6px 10px; - border: 1px solid rgb(var(--border-color)); - border-radius: 6px; - font-size: 13px !important; - line-height: 20px !important; - max-height: 114px; -} - -.mentions__input:focus { - border: 1px solid rgb(var(--blue-500)); - outline: none !important; -} - -.mentions__suggestions { - border-radius: 8px; - overflow: hidden; - background: rgb(var(--color-white)) !important; - border: 1px solid rgb(var(--grey-200)) !important; - box-shadow: var(--box-shadow-base) !important; - width: 100%; - box-sizing: border-box; - margin-top: 20px !important; -} - -.mentions__suggestions__list { - display: flex; - flex-direction: column; - padding: 8px !important; - width: 100%; - box-sizing: border-box; -} - -.mentions__suggestions__item { - padding: 4px 8px; - border-radius: 6px; -} - -.mentions__suggestions__item--focused { - background-color: rgb(var(--grey-50)); -} diff --git a/packages/design/src/components/mentions/index.ts b/packages/design/src/components/mentions/index.ts deleted file mode 100644 index fd9b5794dba7..000000000000 --- a/packages/design/src/components/mentions/index.ts +++ /dev/null @@ -1,19 +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. - */ - -export { Mentions } from './Mentions'; -export type { IMentionsProps } from './Mentions'; -export { Mention, type MentionProps } from 'react-mentions'; diff --git a/packages/design/src/index.ts b/packages/design/src/index.ts index 05dcf36ca574..112fd18d6f34 100644 --- a/packages/design/src/index.ts +++ b/packages/design/src/index.ts @@ -49,4 +49,3 @@ export { type ILocale } from './locale/interface'; export { defaultTheme, greenTheme, themeInstance } from './themes'; export { DraggableList, type IDraggableListProps } from './components/draggable-list'; export { type ITextareaProps, Textarea } from './components/textarea'; -export { type IMentionsProps, Mention, type MentionProps, Mentions } from './components/mentions'; diff --git a/packages/docs-ui/src/services/editor/editor.ts b/packages/docs-ui/src/services/editor/editor.ts index 5bb9f8d1b9e1..520a86812a71 100644 --- a/packages/docs-ui/src/services/editor/editor.ts +++ b/packages/docs-ui/src/services/editor/editor.ts @@ -56,6 +56,9 @@ interface IEditor { // The Editor.blur() method removes keyboard focus from the current editor. blur(): void; // has focus. + /** + * @deprecated use `IEditorService` instead + */ isFocus(): boolean; // Selects the entire content of the editor. // Calling editor.select() will not necessarily focus the editor, so it is often used with Editor.focus @@ -372,6 +375,7 @@ export class Editor extends Disposable implements IEditor { return this._param.render; } + /** @deprecated */ isFocus() { return this._focus; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5b72040e734..d48e6cfe231a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1158,9 +1158,6 @@ importers: '@rc-component/trigger': specifier: ^2.2.5 version: 2.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/react-mentions': - specifier: ^4.4.0 - version: 4.4.0 '@univerjs/icons': specifier: ^0.2.8 version: 0.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1212,9 +1209,6 @@ importers: react-grid-layout: specifier: ^1.5.0 version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-mentions: - specifier: ^4.4.10 - version: 4.4.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3684,9 +3678,6 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.4.5': - resolution: {integrity: sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==} - '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -5205,9 +5196,6 @@ packages: '@types/react-grid-layout@1.3.5': resolution: {integrity: sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==} - '@types/react-mentions@4.4.0': - resolution: {integrity: sha512-dKnY1h42GPUO/QAyei6HxEsFUbEcqK/t1k60ZbLJstB9RAs8OCT69mj9AnUbeNdbzYVISE88OC2IYkkthAAn2g==} - '@types/react-transition-group@4.4.12': resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} peerDependencies: @@ -7652,9 +7640,6 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} - invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - inversify@6.0.1: resolution: {integrity: sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ==} @@ -9310,12 +9295,6 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-mentions@4.4.10: - resolution: {integrity: sha512-JHiQlgF1oSZR7VYPjq32wy97z1w1oE4x10EuhKjPr4WUKhVzG1uFQhQjKqjQkbVqJrmahf+ldgBTv36NrkpKpA==} - peerDependencies: - react: '>=16.8.3' - react-dom: '>=16.8.3' - react-mosaic-component@6.1.0: resolution: {integrity: sha512-iWrNUSdW6HK9SB6kaj7/auvIGZWlyEFR8ulQKC9lskY047uluo5ur4fiuZTNroUTZvGqL02AiLzBBj1+et8RZA==} peerDependencies: @@ -9393,9 +9372,6 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -9868,11 +9844,6 @@ packages: peerDependencies: webpack: ^5.27.0 - substyle@9.4.1: - resolution: {integrity: sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==} - peerDependencies: - react: '>=16.8.3' - sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -10867,10 +10838,6 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.4.5': - dependencies: - regenerator-runtime: 0.13.11 - '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.0 @@ -12420,10 +12387,6 @@ snapshots: dependencies: '@types/react': 18.3.12 - '@types/react-mentions@4.4.0': - dependencies: - '@types/react': 18.3.12 - '@types/react-transition-group@4.4.12(@types/react@18.3.12)': dependencies: '@types/react': 18.3.12 @@ -15265,10 +15228,6 @@ snapshots: interpret@1.4.0: {} - invariant@2.2.4: - dependencies: - loose-envify: 1.4.0 - inversify@6.0.1: {} ip-address@9.0.5: @@ -17162,15 +17121,6 @@ snapshots: react-is@18.3.1: {} - react-mentions@4.4.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@babel/runtime': 7.4.5 - invariant: 2.2.4 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - substyle: 9.4.1(react@18.3.1) - react-mosaic-component@6.1.0(@types/node@22.10.1)(@types/react@18.3.12)(dnd-core@16.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: classnames: 2.5.1 @@ -17292,8 +17242,6 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 - regenerator-runtime@0.13.11: {} - regenerator-runtime@0.14.1: {} regexp-ast-analysis@0.7.1: @@ -17825,12 +17773,6 @@ snapshots: dependencies: webpack: 5.94.0(@swc/core@1.7.5)(esbuild@0.24.0) - substyle@9.4.1(react@18.3.1): - dependencies: - '@babel/runtime': 7.4.5 - invariant: 2.2.4 - react: 18.3.1 - sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.5 From 9923e61279e2339d18b5c9e1ed03def10c1a5302 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 02:51:49 +0800 Subject: [PATCH 11/41] fix: mobile --- packages/sheets-ui/src/mobile-plugin.ts | 39 +++++++++++++------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/sheets-ui/src/mobile-plugin.ts b/packages/sheets-ui/src/mobile-plugin.ts index 18ba371ca650..afcb15d99f0e 100644 --- a/packages/sheets-ui/src/mobile-plugin.ts +++ b/packages/sheets-ui/src/mobile-plugin.ts @@ -19,7 +19,7 @@ import type { IUniverSheetsUIConfig } from './controllers/config.schema'; import { DependentOn, Inject, Injector, IUniverInstanceService, Plugin, UniverInstanceType } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; -import { UniverSheetsPlugin } from '@univerjs/sheets'; +import { IRefSelectionsService, RefSelectionsService, UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverMobileUIPlugin } from '@univerjs/ui'; import { filter } from 'rxjs/operators'; @@ -29,47 +29,47 @@ import { CellAlertRenderController } from './controllers/cell-alert.controller'; import { CellCustomRenderController } from './controllers/cell-custom-render.controller'; import { SheetClipboardController } from './controllers/clipboard/clipboard.controller'; import { defaultPluginConfig } from './controllers/config.schema'; +// import { SheetContextMenuRenderController } from './controllers/render-controllers/contextmenu.render-controller'; +import { DragRenderController } from './controllers/drag-render.controller'; import { ForceStringAlertRenderController } from './controllers/force-string-alert-render.controller'; import { ForceStringRenderController } from './controllers/force-string-render.controller'; import { HoverRenderController } from './controllers/hover-render.controller'; import { MarkSelectionRenderController } from './controllers/mark-selection.controller'; +import { SheetUIMobileController } from './controllers/mobile/mobile-sheet-ui.controller'; +import { SheetPermissionInitController } from './controllers/permission/sheet-permission-init.controller'; +import { SheetPermissionInterceptorBaseController } from './controllers/permission/sheet-permission-interceptor-base.controller'; +import { SheetPermissionInterceptorCanvasRenderController } from './controllers/permission/sheet-permission-interceptor-canvas-render.controller'; +import { SheetPermissionInterceptorClipboardController } from './controllers/permission/sheet-permission-interceptor-clipboard.controller'; +import { SheetPermissionInterceptorFormulaRenderController } from './controllers/permission/sheet-permission-interceptor-formula-render.controller'; +import { SheetPermissionRenderController, SheetPermissionRenderManagerController, WorksheetProtectionRenderController } from './controllers/permission/sheet-permission-render.controller'; import { FormatPainterRenderController } from './controllers/render-controllers/format-painter.render-controller'; import { HeaderFreezeRenderController } from './controllers/render-controllers/freeze.render-controller'; import { HeaderMoveRenderController } from './controllers/render-controllers/header-move.render-controller'; +import { SheetContextMenuMobileRenderController } from './controllers/render-controllers/mobile/mobile-contextmenu.render-controller'; import { MobileSheetsScrollRenderController } from './controllers/render-controllers/mobile/mobile-scroll.render-controller'; +import { SheetRenderController } from './controllers/render-controllers/sheet.render-controller'; import { SheetsZoomRenderController } from './controllers/render-controllers/zoom.render-controller'; import { StatusBarController } from './controllers/status-bar.controller'; import { AutoFillService, IAutoFillService } from './services/auto-fill/auto-fill.service'; import { SheetCanvasPopManagerService } from './services/canvas-pop-manager.service'; import { CellAlertManagerService } from './services/cell-alert-manager.service'; import { ISheetClipboardService, SheetClipboardService } from './services/clipboard/clipboard.service'; +import { DragManagerService } from './services/drag-manager.service'; import { FormatPainterService, IFormatPainterService } from './services/format-painter/format-painter.service'; import { HoverManagerService } from './services/hover-manager.service'; import { IMarkSelectionService, MarkSelectionService } from './services/mark-selection/mark-selection.service'; -import { SheetScrollManagerService } from './services/scroll-manager.service'; -import { ISheetBarService, SheetBarService } from './services/sheet-bar/sheet-bar.service'; -import { SheetSkeletonManagerService } from './services/sheet-skeleton-manager.service'; -import { SheetsRenderService } from './services/sheets-render.service'; -import { ShortcutExperienceService } from './services/shortcut-experience.service'; -import { IStatusBarService, StatusBarService } from './services/status-bar.service'; -// import { SheetContextMenuRenderController } from './controllers/render-controllers/contextmenu.render-controller'; -import { DragRenderController } from './controllers/drag-render.controller'; -import { SheetUIMobileController } from './controllers/mobile/mobile-sheet-ui.controller'; -import { SheetPermissionInitController } from './controllers/permission/sheet-permission-init.controller'; -import { SheetPermissionInterceptorBaseController } from './controllers/permission/sheet-permission-interceptor-base.controller'; -import { SheetPermissionInterceptorCanvasRenderController } from './controllers/permission/sheet-permission-interceptor-canvas-render.controller'; -import { SheetPermissionInterceptorClipboardController } from './controllers/permission/sheet-permission-interceptor-clipboard.controller'; -import { SheetPermissionInterceptorFormulaRenderController } from './controllers/permission/sheet-permission-interceptor-formula-render.controller'; -import { SheetPermissionRenderController, SheetPermissionRenderManagerController, WorksheetProtectionRenderController } from './controllers/permission/sheet-permission-render.controller'; -import { SheetContextMenuMobileRenderController } from './controllers/render-controllers/mobile/mobile-contextmenu.render-controller'; -import { SheetRenderController } from './controllers/render-controllers/sheet.render-controller'; -import { DragManagerService } from './services/drag-manager.service'; import { SheetPermissionPanelModel } from './services/permission/sheet-permission-panel.model'; import { SheetPermissionUserManagerService } from './services/permission/sheet-permission-user-list.service'; import { SheetPrintInterceptorService } from './services/print-interceptor.service'; +import { SheetScrollManagerService } from './services/scroll-manager.service'; import { SelectAllService } from './services/select-all/select-all.service'; import { ISheetSelectionRenderService } from './services/selection/base-selection-render.service'; import { MobileSheetsSelectionRenderService } from './services/selection/mobile-selection-render.service'; +import { ISheetBarService, SheetBarService } from './services/sheet-bar/sheet-bar.service'; +import { SheetSkeletonManagerService } from './services/sheet-skeleton-manager.service'; +import { SheetsRenderService } from './services/sheets-render.service'; +import { ShortcutExperienceService } from './services/shortcut-experience.service'; +import { IStatusBarService, StatusBarService } from './services/status-bar.service'; /** * @ignore @@ -112,6 +112,7 @@ export class UniverSheetsMobileUIPlugin extends Plugin { [SheetsRenderService], [SheetUIMobileController], [StatusBarController], + [IRefSelectionsService, { useClass: RefSelectionsService }], // permission [SheetPermissionPanelModel], From cf9a491831205a43a8d2c3a1f065f92b5badc0f6 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 29 Dec 2024 03:04:51 +0800 Subject: [PATCH 12/41] feat: optimize code --- .../services/editor/editor-manager.service.ts | 3 --- .../docs-ui/src/services/editor/editor.ts | 24 +++++-------------- 2 files changed, 6 insertions(+), 21 deletions(-) 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 47c106967321..113ece19307c 100644 --- a/packages/docs-ui/src/services/editor/editor-manager.service.ts +++ b/packages/docs-ui/src/services/editor/editor-manager.service.ts @@ -113,9 +113,6 @@ export class EditorService extends Disposable implements IEditorService, IDispos } private _setFocusId(id: Nullable) { - if (id) { - this.getEditor(id)?.setFocus(true); - } this._focusEditorUnitId = id; } diff --git a/packages/docs-ui/src/services/editor/editor.ts b/packages/docs-ui/src/services/editor/editor.ts index 520a86812a71..dc356cf1b9ac 100644 --- a/packages/docs-ui/src/services/editor/editor.ts +++ b/packages/docs-ui/src/services/editor/editor.ts @@ -50,16 +50,13 @@ interface IEditor { // Emit when doc selection changed. selectionChange$: Observable; + isFocus(): boolean; // Methods // The focused editor is the editor that will receive keyboard and similar events by default. focus(): void; // The Editor.blur() method removes keyboard focus from the current editor. blur(): void; // has focus. - /** - * @deprecated use `IEditorService` instead - */ - isFocus(): boolean; // Selects the entire content of the editor. // Calling editor.select() will not necessarily focus the editor, so it is often used with Editor.focus select(): void; @@ -123,7 +120,6 @@ export class Editor extends Disposable implements IEditor { paste$: Observable = this._paste$.asObservable(); // Editor get focus. - private _focus = false; private readonly _focus$ = new Subject(); focus$: Observable = this._focus$.asObservable(); @@ -222,6 +218,11 @@ export class Editor extends Disposable implements IEditor { ); } + isFocus() { + const docSelectionRenderService = this._param.render.with(DocSelectionRenderService); + return docSelectionRenderService.isFocusing && Boolean(docSelectionRenderService.getActiveTextRange()); + } + focus() { const curDoc = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); const editorUnitId = this.getEditorId(); @@ -246,15 +247,12 @@ export class Editor extends Disposable implements IEditor { // subUnitId: editorUnitId, // }, false); // } - - this._focus = true; } blur(): void { const docSelectionRenderService = this._param.render.with(DocSelectionRenderService); docSelectionRenderService.blur(); - this._focus = false; } // Selects the entire content of the editor. @@ -375,16 +373,6 @@ export class Editor extends Disposable implements IEditor { return this._param.render; } - /** @deprecated */ - isFocus() { - return this._focus; - } - - /** @deprecated */ - setFocus(state = false) { - this._focus = state; - } - isReadOnly() { return this._param.readonly === true; } From 4d007b04dc9590b6b0b932fe3f5ce899a1b3a9f8 Mon Sep 17 00:00:00 2001 From: zhangw Date: Wed, 8 Jan 2025 14:50:06 +0800 Subject: [PATCH 13/41] feat: lock --- pnpm-lock.yaml | 134 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 470bf38fe04e..ffd2ea39ad29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6603,6 +6603,10 @@ packages: resolution: {integrity: sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==} engines: {node: '>= 0.4'} + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} @@ -6633,6 +6637,10 @@ packages: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} @@ -7371,6 +7379,14 @@ packages: resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==} engines: {node: '>= 0.4'} + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -9436,8 +9452,8 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-mosaic-component@6.1.0: - resolution: {integrity: sha512-iWrNUSdW6HK9SB6kaj7/auvIGZWlyEFR8ulQKC9lskY047uluo5ur4fiuZTNroUTZvGqL02AiLzBBj1+et8RZA==} + react-mosaic-component@6.1.1: + resolution: {integrity: sha512-Ivuj6AxRDlo/H8OiEDU1mdgivxuKbwGOa5Ub6Yf+bHcu0JWioT7ttlpCWF63/gKrJBlRMB6fW9/eNOXINg9+Gg==} peerDependencies: react: '>=16' @@ -9509,6 +9525,10 @@ packages: reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -9756,6 +9776,10 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -14343,6 +14367,60 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.18 + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.0 + math-intrinsics: 1.1.0 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.18 + es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 @@ -14384,6 +14462,13 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-shim-unscopables@1.0.2: dependencies: hasown: 2.0.2 @@ -15372,6 +15457,24 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.0.0 + get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -16027,7 +16130,7 @@ snapshots: es-object-atoms: 1.0.0 get-intrinsic: 1.2.6 has-symbols: 1.1.0 - reflect.getprototypeof: 1.0.9 + reflect.getprototypeof: 1.0.10 set-function-name: 2.0.2 jackspeak@3.4.0: @@ -17695,7 +17798,7 @@ snapshots: react-is@18.3.1: {} - react-mosaic-component@6.1.0(@types/node@22.10.1)(@types/react@18.3.12)(dnd-core@16.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-mosaic-component@6.1.1(@types/node@22.10.5)(@types/react@18.3.12)(dnd-core@16.0.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: classnames: 2.5.1 immutability-helper: 3.1.1 @@ -17806,6 +17909,17 @@ snapshots: reflect-metadata@0.1.13: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.8 @@ -18129,6 +18243,12 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -18717,7 +18837,7 @@ snapshots: gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.9 + reflect.getprototypeof: 1.0.10 typed-array-length@1.0.6: dependencies: @@ -19227,14 +19347,14 @@ snapshots: which-builtin-type@1.2.1: dependencies: call-bound: 1.0.3 - function.prototype.name: 1.1.6 + function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 is-async-function: 2.0.0 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 is-generator-function: 1.0.10 is-regex: 1.2.1 - is-weakref: 1.0.2 + is-weakref: 1.1.0 isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 From e78c899f6b1671913f488f27794de99b9b506934 Mon Sep 17 00:00:00 2001 From: zhangw Date: Wed, 8 Jan 2025 15:42:48 +0800 Subject: [PATCH 14/41] fix: https://github.com/dream-num/univer-pro/issues/3873 --- .../render-controllers/editor-bridge.render-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f7a25ea61131..a178d5f77962 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 @@ -211,7 +211,7 @@ export class EditorBridgeRenderController extends RxDisposable implements IRende const editCell = this._editorBridgeService.getEditLocation(); if (editCell) { const { unitId: editingUnitId, sheetId: editingSheetId, row, column } = editCell; - if (unitId === editingUnitId && subUnitId === editingSheetId && cellValue?.[row]?.[column]) { + if (unitId === editingUnitId && subUnitId === editingSheetId && cellValue && cellValue[row] && Object.hasOwn(cellValue[row], column)) { this._editorBridgeService.refreshEditCellState(); } } From 7d2eb5ec87528ff900ea9e0a65ce9008f695c155 Mon Sep 17 00:00:00 2001 From: zhangw Date: Wed, 8 Jan 2025 19:52:41 +0800 Subject: [PATCH 15/41] fix: https://github.com/dream-num/univer-pro/issues/3878 --- .../src/views/formula-editor/hooks/util.ts | 25 ------------------- .../src/views/formula-editor/index.tsx | 8 ++---- .../range-selector/hooks/useHighlight.ts | 21 ++++++++++------ .../src/views/range-selector/index.tsx | 5 ++-- 4 files changed, 17 insertions(+), 42 deletions(-) delete mode 100644 packages/sheets-formula-ui/src/views/formula-editor/hooks/util.ts 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 deleted file mode 100644 index cd05f4dffdf2..000000000000 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/util.ts +++ /dev/null @@ -1,25 +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 { 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.tsx b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx index db773b05ed2e..5a87027433a6 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -23,7 +23,6 @@ import type { IKeyboardEventConfig } from '../range-selector/hooks/useKeyboardEv 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 { LexerTreeBuilder } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; import { EMBEDDING_FORMULA_EDITOR } from '@univerjs/sheets-ui'; import { useEvent, useUpdateEffect } from '@univerjs/ui'; @@ -42,7 +41,6 @@ import { HelpFunction } from './help-function/HelpFunction'; 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'; @@ -98,8 +96,6 @@ export function FormulaEditor(props: IFormulaEditorProps) { } = props; const editorService = useDependency(IEditorService); - const lexerTreeBuilder = useDependency(LexerTreeBuilder); - const sheetEmbeddingRef = useRef(null); const onChange = useEvent(propOnChange); // init actions @@ -144,7 +140,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { }, [formulaText, onChange]); const highlightDoc = useDocHight('='); - const highlightSheet = useSheetHighlight(unitId); + const highlightSheet = useSheetHighlight(unitId, subUnitId); const highlight = useEvent((text: string, isNeedResetSelection: boolean = true, isEnd?: boolean) => { if (!editorRef.current) { return; @@ -162,7 +158,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { refSelections.current = ranges; if (isEnd) { - highlightSheet(isFocus ? ranges : [], getFocusingReference(editorRef.current, ranges)); + highlightSheet(isFocus ? ranges : [], editorRef.current); } }); 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 d695bf9dce8a..8aa559be7191 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,7 +14,7 @@ * limitations under the License. */ -import type { ITextRun, Nullable, Workbook } from '@univerjs/core'; +import type { ITextRun, Workbook } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { ISequenceNode } from '@univerjs/engine-formula'; import type { ISelectionWithStyle } from '@univerjs/sheets'; @@ -45,7 +45,7 @@ export interface IRefSelection { * @param {IRefSelection[]} refSelections */ -export function useSheetHighlight(unitId: string) { +export function useSheetHighlight(unitId: string, subUnitId?: string) { const univerInstanceService = useDependency(IUniverInstanceService); const themeService = useDependency(ThemeService); const refSelectionsService = useDependency(IRefSelectionsService); @@ -54,7 +54,8 @@ export function useSheetHighlight(unitId: string) { const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - const highlightSheet = useCallback((refSelections: IRefSelection[], focusingRef: Nullable) => { + // eslint-disable-next-line complexity + const highlightSheet = useCallback((refSelections: IRefSelection[], editor: Editor) => { const workbook = univerInstanceService.getUnit(unitId); const worksheet = workbook?.getActiveSheet(); const selectionWithStyle: ISelectionWithStyle[] = []; @@ -68,9 +69,10 @@ export function useSheetHighlight(unitId: string) { const skeleton = sheetSkeletonManagerService?.getWorksheetSkeleton(currentSheetId)?.skeleton; if (!skeleton) return; + const endIndexes: number[] = []; for (let i = 0, len = refSelections.length; i < len; i++) { const refSelection = refSelections[i]; - const { themeColor, token, refIndex } = refSelection; + const { themeColor, token, refIndex, endIndex } = refSelection; const unitRangeName = deserializeRangeWithSheet(token); const { unitId: refUnitId, sheetName, range: rawRange } = unitRangeName; @@ -80,7 +82,7 @@ export function useSheetHighlight(unitId: string) { const refSheetId = getSheetIdByName(sheetName); - if (refSheetId && refSheetId !== currentSheetId) { + if ((refSheetId && refSheetId !== currentSheetId) || (!refSheetId && currentSheetId !== subUnitId)) { continue; } @@ -90,6 +92,7 @@ export function useSheetHighlight(unitId: string) { primary: null, style: genFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()), }); + endIndexes.push(endIndex); } const allControls = refSelectionsRenderService?.getSelectionControls() || []; @@ -99,12 +102,14 @@ export function useSheetHighlight(unitId: string) { refSelectionsService.setSelections(selectionWithStyle); } - if (focusingRef) { - refSelectionsRenderService?.setActiveSelectionIndex(focusingRef.index); + const cursor = editor.getSelectionRanges()?.[0]?.startOffset; + const activeIndex = endIndexes.findIndex((end) => end + 2 === cursor); + if (activeIndex !== -1) { + refSelectionsRenderService?.setActiveSelectionIndex(activeIndex); } else { refSelectionsRenderService?.resetActiveSelectionIndex(); } - }, [refSelectionsRenderService, refSelectionsService, sheetSkeletonManagerService, themeService, unitId, univerInstanceService]); + }, [refSelectionsRenderService, refSelectionsService, sheetSkeletonManagerService, themeService, unitId, subUnitId, univerInstanceService]); useEffect(() => { return () => { 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 15c0d8763cc5..0e03fe000c4f 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -35,7 +35,6 @@ import cl from 'clsx'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; 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 { useFirstHighlightDoc } from './hooks/useFirstHighlightDoc'; import { useFocus } from './hooks/useFocus'; @@ -192,7 +191,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const sequenceNodes = useMemo(() => getFormulaToken(rangeString), [rangeString]); const highlightDoc = useDocHight(); - const highlightSheet = useSheetHighlight(unitId); + const highlightSheet = useSheetHighlight(unitId, subUnitId); const highligh = useEvent((text: string, isNeedResetSelection: boolean = true, showSelection = true) => { if (!editorRef.current) { return; @@ -201,7 +200,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const ranges = highlightDoc(editorRef.current, sequenceNodes, isNeedResetSelection); refSelections.current = ranges; if (showSelection) { - highlightSheet(ranges, getFocusingReference(editorRef.current, ranges)); + highlightSheet(ranges, editorRef.current); } }); From 7f6c9f324eb16e092806e71155bf5c085b287cd6 Mon Sep 17 00:00:00 2001 From: zhangw Date: Wed, 8 Jan 2025 20:09:14 +0800 Subject: [PATCH 16/41] feat: update --- .../hooks/useSheetSelectionChange.ts | 29 ++-- .../range-selector/hooks/useHighlight.ts | 135 +++++++++++------- 2 files changed, 103 insertions(+), 61 deletions(-) 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 580ba5ee7c69..71124418c543 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 @@ -19,24 +19,24 @@ import type { Workbook } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; 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, ICommandService, IUniverInstanceService, useDependency, useObservable } from '@univerjs/core'; +import { DisposableCollection, ICommandService, IUniverInstanceService, ThemeService, 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 { SheetSkeletonManagerService } from '@univerjs/sheets-ui'; import { useEffect, useMemo, useRef } from 'react'; import { merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; +import { calcHighlightRanges, type IRefSelection } from '../../range-selector/hooks/useHighlight'; import { findIndexFromSequenceNodes } from '../../range-selector/utils/findIndexFromSequenceNodes'; import { getOffsetFromSequenceNodes } from '../../range-selector/utils/getOffsetFromSequenceNodes'; import { sequenceNodeToText } from '../../range-selector/utils/sequenceNodeToText'; 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 = ( @@ -55,7 +55,7 @@ export const useSheetSelectionChange = ( const commandService = useDependency(ICommandService); const sequenceNodesRef = useStateRef(sequenceNodes); const docSelectionManagerService = useDependency(DocSelectionManagerService); - + const themeService = useDependency(ThemeService); const { getIsNeedAddSelection } = useSelectionAdd(unitId, sequenceNodes, editor); const workbook = univerInstanceService.getUnit(unitId); @@ -65,6 +65,7 @@ export const useSheetSelectionChange = ( const contextRef = useStateRef({ activeSheet, sheetName }); const render = renderManagerService.getRenderById(unitId); const refSelectionsRenderService = render?.with(RefSelectionsRenderService); + const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); const refSelectionsService = useDependency(IRefSelectionsService); const isScalingRef = useRef(false); @@ -356,17 +357,21 @@ export const useSheetSelectionChange = ( return; } const sub = docSelectionManagerService.textSelection$.subscribe((e) => { - const { unitId } = e; - if (unitId !== editor.getEditorId()) { + if (e.unitId !== editor.getEditorId()) { return; } - const focusingRef = getFocusingReference(editor, refSelectionRef.current); - if (focusingRef) { - refSelectionsRenderService?.setActiveSelectionIndex(focusingRef.index); - } else { - refSelectionsRenderService?.resetActiveSelectionIndex(); - } + calcHighlightRanges({ + unitId, + subUnitId, + refSelections: refSelectionRef.current, + editor, + refSelectionsService, + refSelectionsRenderService, + sheetSkeletonManagerService, + themeService, + univerInstanceService, + }); }); return () => sub.unsubscribe(); 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 8aa559be7191..1d42dfe0bf23 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 @@ -17,9 +17,9 @@ import type { ITextRun, 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 { ISelectionWithStyle, SheetsSelectionsService } from '@univerjs/sheets'; import type { INode } from './useFormulaToken'; -import { getBodySlice, ICommandService, IUniverInstanceService, ThemeService, useDependency } from '@univerjs/core'; +import { getBodySlice, ICommandService, IUniverInstanceService, ThemeService, UniverInstanceType, useDependency } from '@univerjs/core'; import { ReplaceTextRunsCommand } from '@univerjs/docs-ui'; import { deserializeRangeWithSheet, sequenceNodeType } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; @@ -39,6 +39,78 @@ export interface IRefSelection { index: number; } +export function calcHighlightRanges(opts: { + unitId: string; + subUnitId?: string; + refSelections: IRefSelection[]; + editor: Editor; + refSelectionsService: SheetsSelectionsService; + refSelectionsRenderService: RefSelectionsRenderService | undefined; + sheetSkeletonManagerService: SheetSkeletonManagerService | undefined; + themeService: ThemeService; + univerInstanceService: IUniverInstanceService; +}) { + const { + unitId, + subUnitId, + refSelections, + editor, + refSelectionsService, + refSelectionsRenderService, + sheetSkeletonManagerService, + themeService, + univerInstanceService, + } = opts; + const workbook = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_SHEET); + const worksheet = workbook?.getActiveSheet(); + const selectionWithStyle: ISelectionWithStyle[] = []; + if (!workbook || !worksheet) { + refSelectionsService.setSelections(selectionWithStyle); + return; + } + const currentSheetId = worksheet.getSheetId(); + const getSheetIdByName = (name: string) => workbook?.getSheetBySheetName(name)?.getSheetId(); + + const skeleton = sheetSkeletonManagerService?.getWorksheetSkeleton(currentSheetId)?.skeleton; + if (!skeleton) return; + + const endIndexes: number[] = []; + for (let i = 0, len = refSelections.length; i < len; i++) { + const refSelection = refSelections[i]; + const { themeColor, token, refIndex, endIndex } = refSelection; + + const unitRangeName = deserializeRangeWithSheet(token); + const { unitId: refUnitId, sheetName, range: rawRange } = unitRangeName; + if (refUnitId && unitId !== refUnitId) { + continue; + } + + const refSheetId = getSheetIdByName(sheetName); + + if ((refSheetId && refSheetId !== currentSheetId) || (!refSheetId && currentSheetId !== subUnitId)) { + continue; + } + + const range = setEndForRange(rawRange, worksheet.getRowCount(), worksheet.getColumnCount()); + selectionWithStyle.push({ + range, + primary: null, + style: genFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()), + }); + endIndexes.push(endIndex); + } + + const cursor = editor.getSelectionRanges()?.[0]?.startOffset; + const activeIndex = endIndexes.findIndex((end) => end + 2 === cursor); + if (activeIndex !== -1) { + refSelectionsRenderService?.setActiveSelectionIndex(activeIndex); + } else { + refSelectionsRenderService?.resetActiveSelectionIndex(); + } + + return selectionWithStyle; +} + /** * @param {string} unitId * @param {string} subUnitId 打开面板的时候传入的 sheetId @@ -54,61 +126,26 @@ export function useSheetHighlight(unitId: string, subUnitId?: string) { const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - // eslint-disable-next-line complexity const highlightSheet = useCallback((refSelections: IRefSelection[], editor: Editor) => { - const workbook = univerInstanceService.getUnit(unitId); - const worksheet = workbook?.getActiveSheet(); - const selectionWithStyle: ISelectionWithStyle[] = []; - if (!workbook || !worksheet) { - refSelectionsService.setSelections(selectionWithStyle); - return; - } - const currentSheetId = worksheet.getSheetId(); - const getSheetIdByName = (name: string) => workbook?.getSheetBySheetName(name)?.getSheetId(); - - const skeleton = sheetSkeletonManagerService?.getWorksheetSkeleton(currentSheetId)?.skeleton; - if (!skeleton) return; - - const endIndexes: number[] = []; - for (let i = 0, len = refSelections.length; i < len; i++) { - const refSelection = refSelections[i]; - const { themeColor, token, refIndex, endIndex } = refSelection; - - const unitRangeName = deserializeRangeWithSheet(token); - const { unitId: refUnitId, sheetName, range: rawRange } = unitRangeName; - if (refUnitId && unitId !== refUnitId) { - continue; - } - - const refSheetId = getSheetIdByName(sheetName); - - if ((refSheetId && refSheetId !== currentSheetId) || (!refSheetId && currentSheetId !== subUnitId)) { - continue; - } - - const range = setEndForRange(rawRange, worksheet.getRowCount(), worksheet.getColumnCount()); - selectionWithStyle.push({ - range, - primary: null, - style: genFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()), - }); - endIndexes.push(endIndex); - } + const selectionWithStyle = calcHighlightRanges({ + unitId, + subUnitId, + refSelections, + editor, + refSelectionsService, + refSelectionsRenderService, + sheetSkeletonManagerService, + themeService, + univerInstanceService, + }); + if (!selectionWithStyle) return; const allControls = refSelectionsRenderService?.getSelectionControls() || []; if (allControls.length === selectionWithStyle.length) { refSelectionsRenderService?.resetSelectionsByModelData(selectionWithStyle); } else { refSelectionsService.setSelections(selectionWithStyle); } - - const cursor = editor.getSelectionRanges()?.[0]?.startOffset; - const activeIndex = endIndexes.findIndex((end) => end + 2 === cursor); - if (activeIndex !== -1) { - refSelectionsRenderService?.setActiveSelectionIndex(activeIndex); - } else { - refSelectionsRenderService?.resetActiveSelectionIndex(); - } }, [refSelectionsRenderService, refSelectionsService, sheetSkeletonManagerService, themeService, unitId, subUnitId, univerInstanceService]); useEffect(() => { From 1e742c3257cee8160810f861c924bc29266a8aea Mon Sep 17 00:00:00 2001 From: zhangw Date: Wed, 8 Jan 2025 21:46:19 +0800 Subject: [PATCH 17/41] feat: update --- .../views/range-selector/hooks/useHighlight.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 1d42dfe0bf23..342788eeab82 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 @@ -43,7 +43,7 @@ export function calcHighlightRanges(opts: { unitId: string; subUnitId?: string; refSelections: IRefSelection[]; - editor: Editor; + editor: Editor | undefined; refSelectionsService: SheetsSelectionsService; refSelectionsRenderService: RefSelectionsRenderService | undefined; sheetSkeletonManagerService: SheetSkeletonManagerService | undefined; @@ -100,12 +100,14 @@ export function calcHighlightRanges(opts: { endIndexes.push(endIndex); } - const cursor = editor.getSelectionRanges()?.[0]?.startOffset; - const activeIndex = endIndexes.findIndex((end) => end + 2 === cursor); - if (activeIndex !== -1) { - refSelectionsRenderService?.setActiveSelectionIndex(activeIndex); - } else { - refSelectionsRenderService?.resetActiveSelectionIndex(); + if (editor) { + const cursor = editor.getSelectionRanges()?.[0]?.startOffset; + const activeIndex = endIndexes.findIndex((end) => end + 2 === cursor); + if (activeIndex !== -1) { + refSelectionsRenderService?.setActiveSelectionIndex(activeIndex); + } else { + refSelectionsRenderService?.resetActiveSelectionIndex(); + } } return selectionWithStyle; @@ -126,7 +128,7 @@ export function useSheetHighlight(unitId: string, subUnitId?: string) { const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - const highlightSheet = useCallback((refSelections: IRefSelection[], editor: Editor) => { + const highlightSheet = useCallback((refSelections: IRefSelection[], editor?: Editor) => { const selectionWithStyle = calcHighlightRanges({ unitId, subUnitId, From 362521ce6c289bf2ef78227be2036dffbc349f92 Mon Sep 17 00:00:00 2001 From: zhangw Date: Wed, 8 Jan 2025 22:47:15 +0800 Subject: [PATCH 18/41] feat: update --- .../formula-editor/hooks/useFormulaSelection.ts | 17 ++++++++--------- .../src/views/formula-editor/index.tsx | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) 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 index 1c74133e4a02..6cb1fc391089 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts @@ -15,11 +15,10 @@ */ 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 { isFormulaLexerToken, LexerTreeBuilder, 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'; @@ -42,15 +41,14 @@ export enum FormulaSelectingType { CAN_EDIT = 2, } -export function useFormulaSelecting(editorId: string, isFocus: boolean, nodes: (string | ISequenceNode)[]) { +export function useFormulaSelecting(editorId: string, isFocus: boolean) { 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 lexerTreeBuilder = useDependency(LexerTreeBuilder); const isDisabledByPointer = useRef(true); const isSelectingRef = useRef(isSelecting); isSelectingRef.current = isSelecting; @@ -70,9 +68,10 @@ export function useFormulaSelecting(editorId: string, isFocus: boolean, nodes: ( 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 nodes = lexerTreeBuilder.sequenceNodesBuilder(dataStream) ?? []; + const char = dataStream[index - 1]; + const nextChar = dataStream[index]; + const focusingIndex = nodes.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; @@ -92,7 +91,7 @@ export function useFormulaSelecting(editorId: string, isFocus: boolean, nodes: ( }); return () => sub.unsubscribe(); - }, [docSelectionManagerService.textSelection$, docSelectionRenderService, editorId, injector]); + }, [docSelectionManagerService.textSelection$, docSelectionRenderService, editorId, injector, lexerTreeBuilder]); useEffect(() => { if (!isFocus) { 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 5a87027433a6..6c49005cf27b 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -123,7 +123,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { 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 { isSelecting } = useFormulaSelecting(editorId, isFocus); const highTextRef = useRef(''); const renderManagerService = useDependency(IRenderManagerService); const renderer = renderManagerService.getRenderById(editorId); From 943b1a7c411d49772e689f67558c3f1714533b56 Mon Sep 17 00:00:00 2001 From: zhangw Date: Thu, 9 Jan 2025 01:20:27 +0800 Subject: [PATCH 19/41] feat: update --- .../hooks/useSheetSelectionChange.ts | 13 ++++--- .../hooks/useLeftAndRightArrow.ts | 5 ++- .../hooks/useSheetSelectionChange.ts | 4 +- .../src/views/range-selector/index.tsx | 38 +++++++++---------- 4 files changed, 30 insertions(+), 30 deletions(-) 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 71124418c543..822ae18d68f9 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 @@ -26,6 +26,7 @@ import { deserializeRangeWithSheet, sequenceNodeType, serializeRange, serializeR import { IRenderManagerService } from '@univerjs/engine-render'; import { IRefSelectionsService, SetSelectionsOperation } from '@univerjs/sheets'; import { SheetSkeletonManagerService } from '@univerjs/sheets-ui'; +import { useEvent } from '@univerjs/ui'; import { useEffect, useMemo, useRef } from 'react'; import { merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; @@ -59,8 +60,8 @@ export const useSheetSelectionChange = ( const { getIsNeedAddSelection } = useSelectionAdd(unitId, sequenceNodes, editor); const workbook = univerInstanceService.getUnit(unitId); - const getSheetNameById = (sheetId: string) => workbook?.getSheetBySheetId(sheetId)?.getName() ?? ''; - const sheetName = useMemo(() => getSheetNameById(subUnitId), [subUnitId]); + const getSheetNameById = useEvent((sheetId: string) => workbook?.getSheetBySheetId(sheetId)?.getName() ?? ''); + const sheetName = useMemo(() => getSheetNameById(subUnitId), [getSheetNameById, subUnitId]); const activeSheet = useObservable(workbook?.activeSheet$); const contextRef = useStateRef({ activeSheet, sheetName }); const render = renderManagerService.getRenderById(unitId); @@ -201,7 +202,7 @@ export const useSheetSelectionChange = ( disposableCollection.dispose(); }; } - }, [refSelectionsRenderService, editor, isSupportAcrossSheet, isNeed]); + }, [refSelectionsRenderService, editor, isSupportAcrossSheet, isNeed, sequenceNodesRef, getIsNeedAddSelection, subUnitId, unitId, getSheetNameById, sheetName, handleRangeChange, contextRef]); useEffect(() => { if (isNeed && refSelectionsRenderService && editor) { @@ -295,7 +296,7 @@ export const useSheetSelectionChange = ( disposableCollection.dispose(); }; } - }, [isNeed, refSelectionsRenderService, editor]); + }, [isNeed, refSelectionsRenderService, editor, refSelectionsService.selectionSet$, contextRef, sequenceNodesRef, handleRangeChange, isSupportAcrossSheet, unitId]); useEffect(() => { if (listenSelectionSet) { @@ -350,7 +351,7 @@ export const useSheetSelectionChange = ( d.dispose(); }; } - }, [commandService, getSheetNameById, handleRangeChange, isSupportAcrossSheet, listenSelectionSet, sequenceNodesRef]); + }, [commandService, getSheetNameById, handleRangeChange, isSupportAcrossSheet, listenSelectionSet, sequenceNodesRef, sheetName, subUnitId, unitId]); useEffect(() => { if (!editor) { @@ -375,5 +376,5 @@ export const useSheetSelectionChange = ( }); return () => sub.unsubscribe(); - }, [docSelectionManagerService.textSelection$, editor, refSelectionRef, refSelectionsRenderService, sequenceNodesRef]); + }, [docSelectionManagerService.textSelection$, editor, refSelectionRef, refSelectionsRenderService, refSelectionsService, sequenceNodesRef, sheetSkeletonManagerService, subUnitId, themeService, unitId, univerInstanceService]); }; 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 9d79391c670b..0db03e1ebef2 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 @@ -111,8 +111,8 @@ export const useLeftAndRightArrow = (isNeed: boolean, shouldMoveSelection: Formu id: operationId, type: CommandType.OPERATION, handler(_event, params) { - const { keyCode } = params as { eventType: DeviceInputEventType; keyCode: KeyCode }; - handleKeycode(keyCode); + const { keyCode, metaKey } = params as { eventType: DeviceInputEventType; keyCode: KeyCode; metaKey?: MetaKeys }; + handleKeycode(keyCode, metaKey); }, })); @@ -144,6 +144,7 @@ export const useLeftAndRightArrow = (isNeed: boolean, shouldMoveSelection: Formu staticParameters: { eventType: DeviceInputEventType.Keyboard, keyCode, + metaKey, }, }; }).forEach((item) => { diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts index b594d944b555..af724ee58bac 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts @@ -148,7 +148,7 @@ export const useSheetSelectionChange = (isNeed: boolean, d2.unsubscribe(); }; } - }, [isNeed, filterReferenceNodes, refSelectionsRenderService, isSupportAcrossSheet, isOnlyOneRange, handleRangeChange]); + }, [isNeed, filterReferenceNodes, refSelectionsRenderService, isSupportAcrossSheet, isOnlyOneRange, handleRangeChange, univerInstanceService, unitId]); useEffect(() => { if (isNeed && refSelectionsRenderService) { @@ -218,5 +218,5 @@ export const useSheetSelectionChange = (isNeed: boolean, clearTimeout(time); }; } - }, [isNeed, refSelectionsRenderService, filterReferenceNodes, handleRangeChange]); + }, [isNeed, refSelectionsRenderService, filterReferenceNodes, handleRangeChange, univerInstanceService, unitId, isSupportAcrossSheet]); }; 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 0e03fe000c4f..aa6b3c60e28d 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -32,7 +32,7 @@ import { RANGE_SELECTOR_SYMBOLS, SetCellEditVisibleOperation } from '@univerjs/s import { useEvent } from '@univerjs/ui'; import cl from 'clsx'; -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { noop, throttleTime } from 'rxjs'; import { RefSelectionsRenderService } from '../../services/render-services/ref-selections.render-service'; import { useEditorInput } from './hooks/useEditorInput'; @@ -217,25 +217,23 @@ export function RangeSelector(props: IRangeSelectorProps) { return () => sub.dispose(); }, [commandService, editor, editorId, onChange]); - const handleSheetSelectionChange = useMemo(() => { - return (text: string, offset: number, isEnd: boolean) => { - highligh(text); - rangeStringSet(text); - if (isEnd) { - focus(); - if (offset !== -1) { + const handleSheetSelectionChange = useEvent((text: string, offset: number, isEnd: boolean) => { + highligh(text); + rangeStringSet(text); + if (isEnd) { + focus(); + if (offset !== -1) { // 在渲染结束之后再设置选区 - setTimeout(() => { - const range = { startOffset: offset, endOffset: offset }; - editor?.setSelectionRanges([range]); - const docBackScrollRenderController = editor?.render.with(DocBackScrollRenderController); - docBackScrollRenderController?.scrollToRange({ ...range, collapsed: true }); - }, 50); - } - checkScrollBar(); + setTimeout(() => { + const range = { startOffset: offset, endOffset: offset }; + editor?.setSelectionRanges([range]); + const docBackScrollRenderController = editor?.render.with(DocBackScrollRenderController); + docBackScrollRenderController?.scrollToRange({ ...range, collapsed: true }); + }, 50); } - }; - }, [editor]); + checkScrollBar(); + } + }); useSheetSelectionChange(isNeed, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); @@ -481,7 +479,7 @@ function RangeSelectorDialog(props: { }); }; - const handleSheetSelectionChange = useCallback((rangeText: string) => { + const handleSheetSelectionChange = useEvent((rangeText: string) => { refSelectionsRenderService?.setSkipLastEnabled(false); const ranges = rangeText.split(matchToken.COMMA).filter((e) => !!e); if (isOnlyOneRange) { @@ -489,7 +487,7 @@ function RangeSelectorDialog(props: { } else { rangesSet(ranges); } - }, [focusIndex, isOnlyOneRange]); + }); const highlightSheet = useSheetHighlight(unitId); useSheetSelectionChange(focusIndex >= 0, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); From 97e7f7199820f6e12400ac54b9c7e684501da283 Mon Sep 17 00:00:00 2001 From: zhangw Date: Thu, 9 Jan 2025 02:02:45 +0800 Subject: [PATCH 20/41] feat: update --- .../src/services/editor/cell-editor-resize.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7c39a00cb9fb..0167df5583eb 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 @@ -151,8 +151,6 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const { vertexAngle: angle } = convertTextRotation(textRotation); - const clientWidth = document.body.clientWidth; - if (wrapStrategy === WrapStrategy.WRAP && angle === 0) { const { actualWidth, actualHeight } = documentSkeleton.getActualSize(); // The skeleton obtains the original volume, which needs to be multiplied by the magnification factor. @@ -162,7 +160,9 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM }; } - documentDataModel?.updateDocumentDataPageSize((clientWidth - startX - canvasOffset.left) / scaleX); + const maxSize = this._getEditorMaxSize(actualRangeWithCoord, canvasOffset, documentLayoutObject.horizontalAlign); + if (!maxSize) return; + documentDataModel?.updateDocumentDataPageSize(maxSize.width / scaleX); documentSkeleton.calculate(); const size = documentSkeleton.getActualSize(); From 8ce57519790c03879775ac22f4b2ad24ee1c7823 Mon Sep 17 00:00:00 2001 From: zhangw Date: Thu, 9 Jan 2025 19:43:13 +0800 Subject: [PATCH 21/41] feat: update --- .../src/views/formula-editor/hooks/useSheetSelectionChange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 822ae18d68f9..e6e1e70956ef 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 @@ -184,7 +184,7 @@ export const useSheetSelectionChange = ( 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, newOffset ?? result.length, true); + handleRangeChange(result, !theLastList.length && newOffset ? newOffset : result.length, true); } }; const disposableCollection = new DisposableCollection(); From 7797bc3e61ae5e9e8bf4f565bcd18b65361f1b50 Mon Sep 17 00:00:00 2001 From: zhangw Date: Thu, 9 Jan 2025 21:00:55 +0800 Subject: [PATCH 22/41] feat: update --- .../hooks/useSheetSelectionChange.ts | 27 ++++++++++++------- .../src/views/formula-editor/index.tsx | 1 + .../hooks/useSheetSelectionChange.ts | 3 ++- 3 files changed, 20 insertions(+), 11 deletions(-) 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 e6e1e70956ef..0f5cd96fd3b2 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 @@ -42,6 +42,7 @@ import { useSelectionAdd } from './useSelectionAdd'; const noop = (() => { }) as any; export const useSheetSelectionChange = ( isNeed: boolean, + isFocus: boolean, unitId: string, subUnitId: string, sequenceNodes: INode[], @@ -79,7 +80,7 @@ export const useSheetSelectionChange = ( let isFirst = true; // eslint-disable-next-line complexity const handleSelectionsChange = (selections: ISelectionWithCoord[]) => { - if (isFirst || isScalingRef.current) { + if (isFirst) { isFirst = false; return; } @@ -189,13 +190,8 @@ export const useSheetSelectionChange = ( }; const disposableCollection = new DisposableCollection(); disposableCollection.add(refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { + if (isScalingRef.current) return; handleSelectionsChange(selections); - isScalingRef.current = false; - if (scalingOptionRef.current) { - const { result, offset } = scalingOptionRef.current; - handleRangeChange(result, offset || -1, true); - scalingOptionRef.current = undefined; - } })); return () => { @@ -205,7 +201,7 @@ export const useSheetSelectionChange = ( }, [refSelectionsRenderService, editor, isSupportAcrossSheet, isNeed, sequenceNodesRef, getIsNeedAddSelection, subUnitId, unitId, getSheetNameById, sheetName, handleRangeChange, contextRef]); useEffect(() => { - if (isNeed && refSelectionsRenderService && editor) { + if (isFocus && refSelectionsRenderService && editor) { const disposableCollection = new DisposableCollection(); const handleSequenceNodeReplace = (token: string, index: number) => { let currentIndex = 0; @@ -267,6 +263,7 @@ export const useSheetSelectionChange = ( handleRangeChange(result, -1, false); scalingOptionRef.current = { result, offset }; }; + const reListen = () => { disposableCollection.dispose(); const controls = refSelectionsRenderService.getSelectionControls(); @@ -282,11 +279,21 @@ export const useSheetSelectionChange = ( handleSequenceNodeReplace(rangeText, index); })); }); + + disposableCollection.add(refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { + isScalingRef.current = false; + if (scalingOptionRef.current) { + const { result, offset } = scalingOptionRef.current; + handleRangeChange(result, offset || -1, true); + scalingOptionRef.current = undefined; + } + })); }; const dispose = merge( editor.input$, refSelectionsService.selectionSet$, - refSelectionsRenderService.selectionMoveEnd$).pipe(debounceTime(50) + refSelectionsRenderService.selectionMoveEnd$ + ).pipe(debounceTime(50) ).subscribe(() => { reListen(); }); @@ -296,7 +303,7 @@ export const useSheetSelectionChange = ( disposableCollection.dispose(); }; } - }, [isNeed, refSelectionsRenderService, editor, refSelectionsService.selectionSet$, contextRef, sequenceNodesRef, handleRangeChange, isSupportAcrossSheet, unitId]); + }, [isFocus, refSelectionsRenderService, editor, refSelectionsService.selectionSet$, contextRef, sequenceNodesRef, handleRangeChange, isSupportAcrossSheet, unitId]); useEffect(() => { if (listenSelectionSet) { 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 6c49005cf27b..938b786aeb19 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -255,6 +255,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { useSheetSelectionChange( isFocus && Boolean(isSelecting && docFocusing), + isFocus, unitId, subUnitId, sequenceNodes, diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts index af724ee58bac..a9fc9a99cacf 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts @@ -32,7 +32,8 @@ import { rangePreProcess } from '../utils/rangePreProcess'; import { sequenceNodeToText } from '../utils/sequenceNodeToText'; import { getSheetNameById, unitRangesToText } from '../utils/unitRangesToText'; -export const useSheetSelectionChange = (isNeed: boolean, +export const useSheetSelectionChange = ( + isNeed: boolean, unitId: string, _subUnitId: string, sequenceNodes: INode[], From 66585faf4cc25258a1b817bbc381949ee1e9889d Mon Sep 17 00:00:00 2001 From: zhangw Date: Thu, 9 Jan 2025 21:04:26 +0800 Subject: [PATCH 23/41] feat: update --- .../src/views/formula-editor/hooks/useSheetSelectionChange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0f5cd96fd3b2..7e46f5d450d8 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 @@ -260,7 +260,7 @@ export const useSheetSelectionChange = ( return node; }); const result = sequenceNodeToText(newSequenceNodes); - handleRangeChange(result, -1, false); + handleRangeChange(result, -1, true); scalingOptionRef.current = { result, offset }; }; From 8aca66a300cd1f5b588b637c25bd941cc5bda228 Mon Sep 17 00:00:00 2001 From: zhangw Date: Thu, 9 Jan 2025 23:55:53 +0800 Subject: [PATCH 24/41] feat: update --- .../src/views/formula-editor/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 938b786aeb19..8e65ba363dbb 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -166,16 +166,16 @@ export function FormulaEditor(props: IFormulaEditorProps) { if (isFocus) { highlight(formulaText, false, true); } - }, [isFocus]); + }, [formulaText, isFocus, highlight]); - useEffect(() => { - const sub = docSelectionRenderService?.onChangeByEvent$.subscribe((e) => { - const formulaText = BuildTextUtils.transform.getPlainText(document?.getBody()?.dataStream ?? ''); - highlight(formulaText, false, true); - }); + // useEffect(() => { + // const sub = docSelectionRenderService?.onChangeByEvent$.subscribe((e) => { + // const formulaText = BuildTextUtils.transform.getPlainText(document?.getBody()?.dataStream ?? ''); + // highlight(formulaText, false, true); + // }); - return () => sub?.unsubscribe(); - }, [docSelectionRenderService?.onChangeByEvent$, document, highlight]); + // return () => sub?.unsubscribe(); + // }, [docSelectionRenderService?.onChangeByEvent$, document, highlight]); useVerify(isFocus, onVerify, formulaText); const focus = useFocus(editor); From f86a8ef92d7892fb274490ff1750f6c9181c3e0d Mon Sep 17 00:00:00 2001 From: zhangw Date: Fri, 10 Jan 2025 00:15:18 +0800 Subject: [PATCH 25/41] fix: popup --- .../views/formula-editor/help-function/HelpFunction.tsx | 2 +- .../formula-editor/search-function/SearchFunction.tsx | 2 +- packages/ui/src/views/components/popup/RectPopup.tsx | 8 ++++++-- packages/ui/src/views/components/popup/index.module.less | 2 +- packages/ui/src/views/workbench/Workbench.tsx | 1 + 5 files changed, 10 insertions(+), 5 deletions(-) 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 e93dc5735802..058c93a402a9 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 @@ -107,7 +107,7 @@ export function HelpFunction(props: IHelpFunctionProps) { return visible && functionInfo ? ( - reset()} anchorRect$={position$} direction="vertical"> + reset()} anchorRect$={position$} direction="vertical">
0 && visible && ( - +
    { diff --git a/packages/ui/src/views/components/popup/RectPopup.tsx b/packages/ui/src/views/components/popup/RectPopup.tsx index 5921d9fc70fe..5a19be7eb679 100644 --- a/packages/ui/src/views/components/popup/RectPopup.tsx +++ b/packages/ui/src/views/components/popup/RectPopup.tsx @@ -19,6 +19,7 @@ import type { RefObject } from 'react'; import type { Observable } from 'rxjs'; import { useEvent } from 'rc-util'; import React, { createContext, useContext, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; import styles from './index.module.less'; interface IAbsolutePosition { @@ -50,6 +51,7 @@ export interface IRectPopupProps { onPointerLeave?: (e: React.PointerEvent) => void; onClick?: (e: React.MouseEvent) => void; // #endregion + portal?: boolean; } export interface IPopupLayoutInfo extends Pick { @@ -108,7 +110,7 @@ function calcPopupPosition(layout: IPopupLayoutInfo): { top: number; left: numbe }; function RectPopup(props: IRectPopupProps) { - const { children, anchorRect$, direction = 'vertical', onClickOutside, excludeOutside, excludeRects, onPointerEnter, onPointerLeave, onClick, hidden, onContextMenu } = props; + const { portal, children, anchorRect$, direction = 'vertical', onClickOutside, excludeOutside, excludeRects, onPointerEnter, onPointerLeave, onClick, hidden, onContextMenu } = props; const nodeRef = useRef(null); const clickOtherFn = useEvent(onClickOutside ?? (() => { /* empty */ })); const contextMenuFn = useEvent(onContextMenu ?? (() => { /* empty */ })); @@ -195,7 +197,7 @@ function RectPopup(props: IRectPopupProps) { }; }, [contextMenuFn]); - return ( + const ele = (
    ); + + return !portal ? ele : createPortal(ele, document.getElementById('univer-popup-portal')!); } RectPopup.calcPopupPosition = calcPopupPosition; diff --git a/packages/ui/src/views/components/popup/index.module.less b/packages/ui/src/views/components/popup/index.module.less index d1ad720237ca..a78187f55754 100644 --- a/packages/ui/src/views/components/popup/index.module.less +++ b/packages/ui/src/views/components/popup/index.module.less @@ -1,6 +1,6 @@ .popup-fixed { position: fixed; - z-index: 1000; + z-index: 1020; top: -9999px; left: -9999px; } diff --git a/packages/ui/src/views/workbench/Workbench.tsx b/packages/ui/src/views/workbench/Workbench.tsx index 165dc429d257..85fd6206dbdc 100644 --- a/packages/ui/src/views/workbench/Workbench.tsx +++ b/packages/ui/src/views/workbench/Workbench.tsx @@ -193,6 +193,7 @@ export function DesktopWorkbench(props: IUniverWorkbenchProps) { {contextMenu && } +
    ); } From 2a950d9ccde75b024bc29136fc96663eb89bf4e5 Mon Sep 17 00:00:00 2001 From: zhangw Date: Fri, 10 Jan 2025 11:26:35 +0800 Subject: [PATCH 26/41] feat: update --- .../src/services/editor/cell-editor-resize.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0167df5583eb..677e8ba4159c 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 @@ -265,8 +265,8 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const { document: documentComponent, scene: editorScene, engine: docEngine } = editorObject; const viewportMain = editorScene.getViewport(VIEWPORT_KEY.VIEW_MAIN); - const info = this._getEditorMaxSize(actualRangeWithCoord, canvasOffset, horizontalAlign); - if (!info) return; + const info = this._getEditorMaxSize(actualRangeWithCoord, canvasOffset, horizontalAlign)!; + const { height: clientHeight, width: clientWidth, scaleAdjust } = info; let physicHeight = editorHeight; From 83e3854de9e0d2fbd3ffacbe00b8c5a3f83697bf Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 14:53:15 +0800 Subject: [PATCH 27/41] feat: update --- .../src/commands/commands/utils/selection-utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sheets-ui/src/commands/commands/utils/selection-utils.ts b/packages/sheets-ui/src/commands/commands/utils/selection-utils.ts index c278fbf1465e..5da5b294cbc4 100644 --- a/packages/sheets-ui/src/commands/commands/utils/selection-utils.ts +++ b/packages/sheets-ui/src/commands/commands/utils/selection-utils.ts @@ -517,13 +517,13 @@ export function checkIfShrink(selection: ISelection, direction: Direction, works switch (direction) { case Direction.UP: case Direction.DOWN: - startRange.startRow = primary!.startRow; - startRange.endRow = primary!.endRow; + startRange.startRow = primary?.startRow ?? range.startRow; + startRange.endRow = primary?.endRow ?? range.startRow; break; case Direction.LEFT: case Direction.RIGHT: - startRange.startColumn = primary!.startColumn; - startRange.endColumn = primary!.endColumn; + startRange.startColumn = primary?.startColumn ?? range.startColumn; + startRange.endColumn = primary?.endColumn ?? range.startColumn; break; } From 6371e67ad688d7243ef662e6ae4abefc1931a840 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 15:50:07 +0800 Subject: [PATCH 28/41] feat: update --- .../render-services/ref-selections.render-service.ts | 4 ++++ .../src/controllers/editor/editing.render-controller.ts | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) 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 cec5f75055a7..9a752023066e 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 @@ -105,6 +105,10 @@ export class RefSelectionsRenderService extends BaseSelectionRenderService imple this._eventDisposables = null; } + disableSelectionChanging(): void { + this._disableSelectionChanging(); + } + private _initCanvasEventListeners(): IDisposable { const sheetObject = this._getSheetObject(); const { spreadsheetRowHeader, spreadsheetColumnHeader, spreadsheet, spreadsheetLeftTopPlaceholder } = sheetObject; 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 f214f41d072f..9d0b26b7187a 100644 --- a/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts +++ b/packages/sheets-ui/src/controllers/editor/editing.render-controller.ts @@ -55,7 +55,7 @@ import { IRenderManagerService, } from '@univerjs/engine-render'; -import { COMMAND_LISTENER_SKELETON_CHANGE, SetRangeValuesCommand, SetSelectionsOperation, SetWorksheetActivateCommand, SetWorksheetActiveOperation, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets'; +import { COMMAND_LISTENER_SKELETON_CHANGE, REF_SELECTIONS_ENABLED, SetRangeValuesCommand, SetSelectionsOperation, SetWorksheetActivateCommand, SetWorksheetActiveOperation, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets'; import { KeyCode } from '@univerjs/ui'; import { distinctUntilChanged, filter } from 'rxjs'; import { getEditorObject } from '../../basics/editor/get-editor-object'; @@ -284,7 +284,7 @@ export class EditingRenderController extends Disposable implements IRenderModule 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) { + if (cellSelectionRenderManager?.canFocusing || formulaSelectionRenderManager?.canFocusing) { this._univerInstanceService.setCurrentUnitForType(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); cellSelectionRenderManager?.activate( HIDDEN_EDITOR_POSITION, @@ -505,6 +505,7 @@ export class EditingRenderController extends Disposable implements IRenderModule } const selections = this._workbookSelections.getCurrentSelections(); if (selections) { + this._contextService.setContextValue(REF_SELECTIONS_ENABLED, false); this._commandService.syncExecuteCommand(SetSelectionsOperation.id, { unitId: this._context.unit.getUnitId(), subUnitId: sheetId, From 5423e3cf2108f6dc7f137cf7f21344b27a9160bb Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 17:24:03 +0800 Subject: [PATCH 29/41] feat: update --- .../formula-editor/hooks/useSelectionAdd.ts | 25 ++++++++++--------- .../hooks/useSheetSelectionChange.ts | 16 +++++++----- .../src/views/formula-editor/index.tsx | 11 ++++---- .../range-selector/hooks/useHighlight.ts | 12 ++++++--- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSelectionAdd.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSelectionAdd.ts index d94576fc6940..de7b4a51e29e 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSelectionAdd.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSelectionAdd.ts @@ -17,13 +17,12 @@ import type { Editor } from '@univerjs/docs-ui'; import type { INode } from '../../range-selector/utils/filterReferenceNode'; import { useDependency } from '@univerjs/core'; -import { compareToken, matchToken, operatorToken } from '@univerjs/engine-formula'; +import { compareToken, LexerTreeBuilder, matchToken, operatorToken } from '@univerjs/engine-formula'; import { IRenderManagerService } from '@univerjs/engine-render'; +import { useEvent } from '@univerjs/ui'; import { useEffect, useMemo, useRef } from 'react'; -import { debounceTime } from 'rxjs'; import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; import { findIndexFromSequenceNodes } from '../../range-selector/utils/findIndexFromSequenceNodes'; -import { useStateRef } from './useStateRef'; const createLock = (initValue: boolean, step = 300) => { let isEnableCancel = initValue; @@ -82,23 +81,24 @@ const getContent = (node: INode) => typeof node === 'string' ? node : node.token * @return {*} */ // eslint-disable-next-line max-lines-per-function -export const useSelectionAdd = (unitId: string, sequenceNodes: INode[], editor?: Editor) => { +export const useSelectionAdd = (unitId: string, editor?: Editor) => { const renderManagerService = useDependency(IRenderManagerService); const render = renderManagerService.getRenderById(unitId); const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const isNeedAddSelection = useRef(false); - const sequenceNodesRef = useStateRef(sequenceNodes); + const lexerTreeBuilder = useDependency(LexerTreeBuilder); // 非用户行为导致的选区改变,应该屏蔽选区变更事件 const isLockSelectionEvent = useMemo(() => createLock(false, 300), []); - const setIsAddSelection = (v: boolean) => { + const setIsAddSelection = useEvent((v: boolean) => { if (refSelectionsRenderService) { refSelectionsRenderService.setSkipLastEnabled(v); } isNeedAddSelection.current = v; - }; - const getIsNeedAddSelection = () => isNeedAddSelection.current; + }); + + const getIsNeedAddSelection = useEvent(() => isNeedAddSelection.current); useEffect(() => { if (editor && refSelectionsRenderService) { @@ -114,10 +114,12 @@ export const useSelectionAdd = (unitId: string, sequenceNodes: INode[], editor?: } }); // sequenceNodes 的创建会在 input 事件之后,为了拿到最新的 sequenceNodes , 这里延后 100ms - const d2 = editor.selectionChange$.pipe(debounceTime(100)).subscribe((e) => { + const d2 = editor.selectionChange$.subscribe((e) => { if (isLockSelectionEvent.getValue()) { return; } + const dataStream = editor.getDocumentData().body?.dataStream.slice(0, -2) ?? ''; + const sequenceNodes = lexerTreeBuilder.sequenceNodesBuilder(dataStream); const selections = e.textRanges; if (!selections.length) { return; @@ -131,8 +133,7 @@ export const useSelectionAdd = (unitId: string, sequenceNodes: INode[], editor?: setIsAddSelection(false); return; } - const sequenceNodes = sequenceNodesRef.current; - if (!sequenceNodes.length) { + if (!sequenceNodes?.length) { setIsAddSelection(true); return; } @@ -175,7 +176,7 @@ export const useSelectionAdd = (unitId: string, sequenceNodes: INode[], editor?: d2.unsubscribe(); }; } - }, [editor, refSelectionsRenderService]); + }, [editor, isLockSelectionEvent, lexerTreeBuilder, refSelectionsRenderService, setIsAddSelection]); return { setIsAddSelection, 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 7e46f5d450d8..0fb09e503b48 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 @@ -58,7 +58,7 @@ export const useSheetSelectionChange = ( const sequenceNodesRef = useStateRef(sequenceNodes); const docSelectionManagerService = useDependency(DocSelectionManagerService); const themeService = useDependency(ThemeService); - const { getIsNeedAddSelection } = useSelectionAdd(unitId, sequenceNodes, editor); + const { getIsNeedAddSelection } = useSelectionAdd(unitId, editor); const workbook = univerInstanceService.getUnit(unitId); const getSheetNameById = useEvent((sheetId: string) => workbook?.getSheetBySheetId(sheetId)?.getName() ?? ''); @@ -79,7 +79,7 @@ export const useSheetSelectionChange = ( if (refSelectionsRenderService && isNeed) { let isFirst = true; // eslint-disable-next-line complexity - const handleSelectionsChange = (selections: ISelectionWithCoord[]) => { + const handleSelectionsChange = (selections: ISelectionWithCoord[], isEnd: boolean) => { if (isFirst) { isFirst = false; return; @@ -111,7 +111,7 @@ export const useSheetSelectionChange = ( sequenceNodes.push({ token: refRanges[0], nodeType: sequenceNodeType.REFERENCE } as any); const newSequenceNodes = [...sequenceNodes, ...lastNodes]; const result = sequenceNodeToText(newSequenceNodes); - handleRangeChange(result, getOffsetFromSequenceNodes(sequenceNodes), true); + handleRangeChange(result, getOffsetFromSequenceNodes(sequenceNodes), isEnd); } else { const range = selections[selections.length - 1]; const rangeSheetId = range.rangeWithCoord.sheetId ?? subUnitId; @@ -124,7 +124,7 @@ export const useSheetSelectionChange = ( const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet && isAcrossSheet); sequenceNodes.unshift({ token: refRanges[0], nodeType: sequenceNodeType.REFERENCE } as any); const result = sequenceNodeToText(sequenceNodes); - handleRangeChange(result, refRanges[0].length, true); + handleRangeChange(result, refRanges[0].length, isEnd); } } else { // 更新全部的 ref Selection @@ -185,13 +185,17 @@ export const useSheetSelectionChange = ( 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, !theLastList.length && newOffset ? newOffset : result.length, true); + handleRangeChange(result, !theLastList.length && newOffset ? newOffset : result.length, isEnd); } }; const disposableCollection = new DisposableCollection(); + disposableCollection.add(refSelectionsRenderService.selectionMoving$.subscribe((selections) => { + if (isScalingRef.current) return; + handleSelectionsChange(selections, false); + })); disposableCollection.add(refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { if (isScalingRef.current) return; - handleSelectionsChange(selections); + handleSelectionsChange(selections, true); })); return () => { 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 8e65ba363dbb..c50952b3c623 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { DocumentDataModel, IDisposable } from '@univerjs/core'; +import type { DocumentDataModel, IDisposable, ITextRange } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { KeyCode, MetaKeys } from '@univerjs/ui'; import type { ReactNode } from 'react'; @@ -141,7 +141,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { const highlightDoc = useDocHight('='); const highlightSheet = useSheetHighlight(unitId, subUnitId); - const highlight = useEvent((text: string, isNeedResetSelection: boolean = true, isEnd?: boolean) => { + const highlight = useEvent((text: string, isNeedResetSelection: boolean = true, isEnd?: boolean, newSelections?: ITextRange[]) => { if (!editorRef.current) { return; } @@ -153,7 +153,8 @@ export function FormulaEditor(props: IFormulaEditorProps) { sequenceNodes, isNeedResetSelection, // remove equals need to remove highlight style - preText.slice(1) === text && preText[0] === '=' + preText.slice(1) === text && preText[0] === '=', + newSelections ); refSelections.current = ranges; @@ -237,14 +238,12 @@ export function FormulaEditor(props: IFormulaEditorProps) { if (!isFocusing) { return; } - highlight(`=${refString}`, true, isEnd); + highlight(`=${refString}`, true, isEnd, [{ startOffset: offset + 1, endOffset: offset + 1, collapsed: true }]); 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); 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 342788eeab82..bd86b91b0c94 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,7 +14,7 @@ * limitations under the License. */ -import type { ITextRun, Workbook } from '@univerjs/core'; +import type { ITextRange, ITextRun, Workbook } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { ISequenceNode } from '@univerjs/engine-formula'; import type { ISelectionWithStyle, SheetsSelectionsService } from '@univerjs/sheets'; @@ -165,7 +165,13 @@ export function useDocHight(_leadingCharacter: string = '') { const commandService = useDependency(ICommandService); const leadingCharacterLength = useMemo(() => _leadingCharacter.length, [_leadingCharacter]); - const highlightDoc = useCallback((editor: Editor, sequenceNodes: INode[], isNeedResetSelection = true, clearTextRun = true) => { + const highlightDoc = useCallback(( + editor: Editor, + sequenceNodes: INode[], + isNeedResetSelection = true, + clearTextRun = true, + newSelections?: ITextRange[] + ) => { const data = editor.getDocumentData(); const editorId = editor.getEditorId(); if (!data) { @@ -216,7 +222,7 @@ export function useDocHight(_leadingCharacter: string = '') { commandService.syncExecuteCommand(ReplaceTextRunsCommand.id, { unitId: editorId, body: getBodySlice(cloneBody, 0, cloneBody.dataStream.length - 2), - textRanges: selections, + textRanges: newSelections ?? selections, }); return refSelections; } From 1524f7a579f19d997303c44fc118c03e2ceb9681 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 17:45:11 +0800 Subject: [PATCH 30/41] feat: lint --- .../src/services/editor/cell-editor-resize.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 677e8ba4159c..9b7187e638ec 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 @@ -65,7 +65,7 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM const documentSkeleton = this._getEditorSkeleton(); if (!documentSkeleton) return; - let { actualWidth, actualHeight } = this._predictingSize( + const info = this._predictingSize( position, canvasOffset, documentSkeleton, @@ -73,7 +73,8 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM scaleX, scaleY ); - + if (!info) return; + let { actualWidth, actualHeight } = info; const { verticalAlign, horizontalAlign, paddingData, fill } = documentLayoutObject; actualWidth = actualWidth + (paddingData.l ?? 0) + (paddingData.r ?? 0); actualHeight = actualHeight + (paddingData.t ?? 0) + (paddingData.b ?? 0); From 9179356c49e971632fd921905e9e2cc84714ce2c Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 19:18:34 +0800 Subject: [PATCH 31/41] feat: update --- .../views/formula-editor/hooks/useFormulaSelection.ts | 10 +++++----- .../src/views/formula-editor/index.tsx | 7 ++++++- .../src/views/editor-container/EditorContainer.tsx | 1 + .../sheets-ui/src/views/formula-bar/FormulaBar.tsx | 1 + 4 files changed, 13 insertions(+), 6 deletions(-) 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 index 6cb1fc391089..d087cdaa1901 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.ts @@ -21,7 +21,7 @@ import { DocSelectionRenderService } from '@univerjs/docs-ui'; import { isFormulaLexerToken, LexerTreeBuilder, 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'; +import { filter, map } from 'rxjs'; function getCurrentBodyDataStreamAndOffset(accssor: IAccessor) { const univerInstanceService = accssor.get(IUniverInstanceService); @@ -41,7 +41,7 @@ export enum FormulaSelectingType { CAN_EDIT = 2, } -export function useFormulaSelecting(editorId: string, isFocus: boolean) { +export function useFormulaSelecting(editorId: string, isFocus: boolean, disableOnClick?: boolean) { const renderManagerService = useDependency(IRenderManagerService); const renderer = renderManagerService.getRenderById(editorId); const docSelectionRenderService = renderer?.with(DocSelectionRenderService); @@ -61,8 +61,7 @@ export function useFormulaSelecting(editorId: string, isFocus: boolean) { const activeRange = docSelectionRenderService?.getActiveTextRange(); const index = activeRange?.collapsed ? activeRange.startOffset! : -1; return index; - }), - distinctUntilChanged() + }) ) .subscribe((index) => { const config = getCurrentBodyDataStreamAndOffset(injector); @@ -101,13 +100,14 @@ export function useFormulaSelecting(editorId: string, isFocus: boolean) { }, [isFocus]); useEffect(() => { + if (!disableOnClick) return; const sub = renderer?.mainComponent?.onPointerDown$.subscribeEvent(() => { setIsSelecting(FormulaSelectingType.NOT_SELECT); isDisabledByPointer.current = true; }); return () => sub?.unsubscribe(); - }, [renderer?.mainComponent?.onPointerDown$]); + }, [disableOnClick, renderer?.mainComponent?.onPointerDown$]); return { isSelecting }; } 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 c50952b3c623..95026a69df6f 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -68,6 +68,10 @@ export interface IFormulaEditorProps { resetSelectionOnBlur?: boolean; isSingle?: boolean; autoScrollbar?: boolean; + /** + * Disable selection when click formula editor + */ + disableSelectionOnClick?: boolean; } const noop = () => { }; @@ -93,6 +97,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { resetSelectionOnBlur = true, autoScrollbar = true, isSingle = true, + disableSelectionOnClick = false, } = props; const editorService = useDependency(IEditorService); @@ -123,7 +128,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { 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); + const { isSelecting } = useFormulaSelecting(editorId, isFocus, disableSelectionOnClick); const highTextRef = useRef(''); const renderManagerService = useDependency(IRenderManagerService); const renderer = renderManagerService.getRenderById(editorId); diff --git a/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx b/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx index 590da6bbb852..5fd96fc51c02 100644 --- a/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx +++ b/packages/sheets-ui/src/views/editor-container/EditorContainer.tsx @@ -171,6 +171,7 @@ export const EditorContainer: React.FC = () => { editorBridgeService.disableForceKeepVisible(); } }} + disableSelectionOnClick /> )}
diff --git a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx index 8368d77113b3..6303ffff11b3 100644 --- a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx +++ b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx @@ -227,6 +227,7 @@ export function FormulaBar() {
{FormulaEditor && ( {}} From 4829293f9766ce524d559ef281dff13cfbc48a51 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 20:27:22 +0800 Subject: [PATCH 32/41] fix: across selection --- .../src/views/range-selector/hooks/useFocus.ts | 4 +--- .../views/range-selector/hooks/useRefactorEffect.ts | 10 +++++----- .../sheets-ui/src/views/formula-bar/FormulaBar.tsx | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) 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 c9e582e404ad..927477a70e87 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 @@ -25,9 +25,7 @@ export const useFocus = (editor?: Editor) => { const selections = [...editor.getSelectionRanges()]; if (Tools.isDefine(offset)) { editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); - } else if (selections.length) { - editor.setSelectionRanges(selections); - } else { + } else if (!selections.length) { const body = editor.getDocumentData().body?.dataStream ?? '\r\n'; const offset = Math.max(body.length - 2, 0); editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); 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 385373cd3597..f8fc9c53b41a 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 @@ -40,7 +40,7 @@ export const useRefactorEffect = (isNeed: boolean, selecting: boolean, unitId: s refSelectionsService.clear(); }; } - }, [isNeed]); + }, [contextService, isNeed, refSelectionsService]); useLayoutEffect(() => { if (isNeed && selecting) { @@ -52,9 +52,9 @@ export const useRefactorEffect = (isNeed: boolean, selecting: boolean, unitId: s d1?.dispose(); }; } - }, [isNeed, selecting]); + }, [contextService, isNeed, refSelectionsRenderService, selecting]); - //right context controller + // right context controller useEffect(() => { if (isNeed) { contextService.setContextValue(EDITOR_ACTIVATED, true); @@ -64,12 +64,12 @@ export const useRefactorEffect = (isNeed: boolean, selecting: boolean, unitId: s contextMenuService.enable(); }; } - }, [isNeed]); + }, [contextMenuService, contextService, isNeed]); // reset setSkipLastEnabled useEffect(() => { if (isNeed) { refSelectionsRenderService?.setSkipLastEnabled(false); } - }, [isNeed]); + }, [isNeed, refSelectionsRenderService]); }; diff --git a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx index 6303ffff11b3..9a27c81a8d06 100644 --- a/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx +++ b/packages/sheets-ui/src/views/formula-bar/FormulaBar.tsx @@ -21,7 +21,7 @@ import { CheckMarkSingle, CloseSingle, DropdownSingle, FxSingle } from '@univerj import { RangeProtectionPermissionEditPoint, RangeProtectionRuleModel, SheetsSelectionsService, WorkbookEditablePermission, WorksheetEditPermission, WorksheetProtectionRuleModel, WorksheetSetCellValuePermission } from '@univerjs/sheets'; import { ComponentContainer, ComponentManager, KeyCode, useComponentsOfPart } from '@univerjs/ui'; import clsx from 'clsx'; -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { EMPTY, merge, switchMap } from 'rxjs'; import { EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY } from '../../common/keys'; import { useActiveWorkbook } from '../../components/hook'; @@ -59,6 +59,7 @@ export function FormulaBar() { const FormulaEditor = componentManager.get(EMBEDDING_FORMULA_EDITOR_COMPONENT_KEY); const formulaAuxUIParts = useComponentsOfPart(SheetsUIPart.FORMULA_AUX); const contextService = useDependency(IContextService); + useObservable(useMemo(() => contextService.subscribeContextValue$(FOCUSING_FX_BAR_EDITOR), [contextService])); const isFocusFxBar = contextService.getContextValue(FOCUSING_FX_BAR_EDITOR); const ref = useRef(null); From 8c97614f5112e50035bfe0e2e2e40271545cbd80 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sat, 11 Jan 2025 20:57:31 +0800 Subject: [PATCH 33/41] feat: max-height on comment --- .../rich-text-editor/hooks/useOnChange.ts | 41 +++++++++++++++++++ .../views/rich-text-editor/hooks/useResize.ts | 8 +++- .../src/views/rich-text-editor/index.tsx | 27 +++++++++++- .../src/views/thread-comment-editor/index.tsx | 1 + 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 packages/docs-ui/src/views/rich-text-editor/hooks/useOnChange.ts diff --git a/packages/docs-ui/src/views/rich-text-editor/hooks/useOnChange.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/useOnChange.ts new file mode 100644 index 000000000000..1649ccc95d08 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useOnChange.ts @@ -0,0 +1,41 @@ +/** + * 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 } from '@univerjs/core'; +import type { IRichTextEditingMutationParams } from '@univerjs/docs'; +import type { Editor } from '../../../services/editor/editor'; +import { ICommandService, useDependency } from '@univerjs/core'; +import { RichTextEditingMutation } from '@univerjs/docs'; +import { useEffect } from 'react'; + +export function useOnChange(editor: Editor | undefined, onChange: (data: IDocumentData) => void) { + const commandService = useDependency(ICommandService); + + useEffect(() => { + if (!editor) return; + const dispose = commandService.onCommandExecuted((command) => { + if (command.id === RichTextEditingMutation.id) { + const params = command.params as IRichTextEditingMutationParams; + if (params.unitId !== editor.getEditorId()) return; + onChange(editor.getDocumentData()); + } + }); + + return () => { + dispose.dispose(); + }; + }, [editor, onChange, commandService]); +} 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 index b6e45b794e8b..6d8924137035 100644 --- a/packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts @@ -23,7 +23,7 @@ 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) => { +export const useResize = (editor?: Editor, isSingle = true, autoScrollbar?: boolean, autoScroll?: boolean) => { const resize = useCallback(() => { if (editor) { const { scene, mainComponent } = editor.render; @@ -41,7 +41,9 @@ export const useResize = (editor?: Editor, isSingle = true, autoScrollbar?: bool }, [editor, isSingle]); const checkScrollBar = useMemo(() => { + // eslint-disable-next-line complexity return debounce(() => { + if (!autoScrollbar) return; if (!editor || !autoScrollbar) { return; } @@ -76,6 +78,7 @@ export const useResize = (editor?: Editor, isSingle = true, autoScrollbar?: bool } else { viewportMain?.resetCanvasSizeAndUpdateScroll(); } + autoScroll && viewportMain?.scrollToBarPos({ x: 0, y: Infinity }); } else { scrollBar = null; viewportMain?.scrollToBarPos({ x: 0, y: 0 }); @@ -93,6 +96,7 @@ export const useResize = (editor?: Editor, isSingle = true, autoScrollbar?: bool } else { viewportMain?.resetCanvasSizeAndUpdateScroll(); } + autoScroll && viewportMain?.scrollToBarPos({ x: Infinity, y: 0 }); } else { scrollBar = null; viewportMain?.scrollToBarPos({ x: 0, y: 0 }); @@ -100,7 +104,7 @@ export const useResize = (editor?: Editor, isSingle = true, autoScrollbar?: bool } } }, 30); - }, [editor, autoScrollbar, isSingle]); + }, [editor, autoScrollbar, isSingle, autoScroll]); useEffect(() => { if (!autoScrollbar) return; diff --git a/packages/docs-ui/src/views/rich-text-editor/index.tsx b/packages/docs-ui/src/views/rich-text-editor/index.tsx index 70186fdf28f3..30ea8da0a6d8 100644 --- a/packages/docs-ui/src/views/rich-text-editor/index.tsx +++ b/packages/docs-ui/src/views/rich-text-editor/index.tsx @@ -17,6 +17,7 @@ import type { Editor } from '../../services/editor/editor'; import type { IKeyboardEventConfig } from './hooks'; import { BuildTextUtils, createInternalEditorID, generateRandomId, type IDocumentData, useDependency, useObservable } from '@univerjs/core'; +import { DocSkeletonManagerService } from '@univerjs/docs'; import { IRenderManagerService } from '@univerjs/engine-render'; import { useEvent } from '@univerjs/ui'; import clsx from 'clsx'; @@ -26,6 +27,7 @@ import { DocSelectionRenderService } from '../../services/selection/doc-selectio import { useKeyboardEvent, useResize } from './hooks'; import { useEditor } from './hooks/useEditor'; import { useLeftAndRightArrow } from './hooks/useLeftAndRightArrow'; +import { useOnChange } from './hooks/useOnChange'; import styles from './index.module.less'; export interface IRichTextEditorProps { @@ -40,6 +42,10 @@ export interface IRichTextEditorProps { isSingle?: boolean; placeholder?: string; editorId?: string; + onHeightChange?: (height: number) => void; + onChange?: (data: IDocumentData) => void; + maxHeight?: number; + defaultHeight?: number; } export const RichTextEditor = forwardRef((props, ref) => { @@ -54,10 +60,15 @@ export const RichTextEditor = forwardRef((props, r style, isSingle, editorId: propsEditorId, + onHeightChange, + onChange: _onChange, + defaultHeight = 32, + maxHeight = 32, } = props; const editorService = useDependency(IEditorService); const onFocusChange = useEvent(_onFocusChange); const onClickOutside = useEvent(_onClickOutside); + const [height, setHeight] = useState(defaultHeight); const formulaEditorContainerRef = React.useRef(null); const editorId = useMemo(() => propsEditorId ?? createInternalEditorID(`RICH_TEXT_EDITOR-${generateRandomId(4)}`), [propsEditorId]); const editor = useEditor({ @@ -73,6 +84,17 @@ export const RichTextEditor = forwardRef((props, r const isFocusing = docSelectionRenderService?.isFocusing ?? false; const sheetEmbeddingRef = React.useRef(null); const [showPlaceholder, setShowPlaceholder] = useState(() => !BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); + const { checkScrollBar } = useResize(editor, isSingle, true, true); + const onChange = useEvent((data: IDocumentData) => { + const docSkeleton = renderer?.with(DocSkeletonManagerService); + const size = docSkeleton?.getSkeleton().getActualSize(); + if (size) { + onHeightChange?.(size.actualHeight); + setHeight(Math.max(defaultHeight, Math.min(size.actualHeight, maxHeight))); + } + _onChange?.(data); + checkScrollBar(); + }); useEffect(() => { setShowPlaceholder(!BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); @@ -83,9 +105,10 @@ export const RichTextEditor = forwardRef((props, r return () => sub?.unsubscribe(); }, [editor]); + useObservable(editor?.blur$); useObservable(editor?.focus$); - useResize(editor, isSingle, true); + useEffect(() => { onFocusChange?.(isFocusing); }, [isFocusing, onFocusChange]); @@ -113,6 +136,7 @@ export const RichTextEditor = forwardRef((props, r useLeftAndRightArrow(isFocusing && moveCursor, false, editor); useKeyboardEvent(isFocusing, keyboardEventConfig, editor); useImperativeHandle(ref, () => editor!, [editor]); + useOnChange(editor, onChange); return (
@@ -120,6 +144,7 @@ export const RichTextEditor = forwardRef((props, r className={clsx(styles.richTextEditorWrap, { [styles.richTextEditorActive]: isFocusing, })} + style={{ height }} ref={sheetEmbeddingRef} >
isFocus && setEditing(isFocus)} isSingle={false} + maxHeight={64} onClickOutside={() => { setTimeout(() => { editorService.focus(rootEditorId); From d278c5df348e0f48acb2e47ea39fce9d27d7b541 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 12 Jan 2025 02:21:48 +0800 Subject: [PATCH 34/41] feat: update --- .../src/services/editor/cell-editor-resize.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 9b7187e638ec..1b6acd90e255 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 @@ -146,13 +146,16 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM // startX and startY are the width and height after scaling. const { startX, endX } = actualRangeWithCoord; - const { textRotation, wrapStrategy } = documentLayoutObject; + const { textRotation, wrapStrategy, paddingData } = documentLayoutObject; const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); const { vertexAngle: angle } = convertTextRotation(textRotation); if (wrapStrategy === WrapStrategy.WRAP && angle === 0) { + documentDataModel?.updateDocumentDataPageSize(endX - startX); + documentDataModel?.updateDocumentDataMargin({ l: paddingData.l, t: paddingData.t }); + documentSkeleton.calculate(); const { actualWidth, actualHeight } = documentSkeleton.getActualSize(); // The skeleton obtains the original volume, which needs to be multiplied by the magnification factor. return { From 28eaf8279728c214ec5f30ecfbac1d186c7429a8 Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 12 Jan 2025 02:46:37 +0800 Subject: [PATCH 35/41] feat: update --- packages/sheets-formula-ui/src/views/range-selector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aa6b3c60e28d..5397d483e170 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -489,7 +489,7 @@ function RangeSelectorDialog(props: { } }); - const highlightSheet = useSheetHighlight(unitId); + const highlightSheet = useSheetHighlight(unitId, subUnitId); useSheetSelectionChange(focusIndex >= 0, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); useRefactorEffect(focusIndex >= 0, focusIndex >= 0, unitId); useOnlyOneRange(unitId, isOnlyOneRange); From f05940fe24581bb64308dc69ec019466cc19e88a Mon Sep 17 00:00:00 2001 From: zhangw Date: Sun, 12 Jan 2025 02:48:53 +0800 Subject: [PATCH 36/41] feat: update --- .../src/views/range-selector/hooks/useHighlight.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 bd86b91b0c94..fda5b9eb2a4a 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 @@ -41,7 +41,7 @@ export interface IRefSelection { export function calcHighlightRanges(opts: { unitId: string; - subUnitId?: string; + subUnitId: string; refSelections: IRefSelection[]; editor: Editor | undefined; refSelectionsService: SheetsSelectionsService; @@ -119,7 +119,7 @@ export function calcHighlightRanges(opts: { * @param {IRefSelection[]} refSelections */ -export function useSheetHighlight(unitId: string, subUnitId?: string) { +export function useSheetHighlight(unitId: string, subUnitId: string) { const univerInstanceService = useDependency(IUniverInstanceService); const themeService = useDependency(ThemeService); const refSelectionsService = useDependency(IRefSelectionsService); From b322cd666c1b841cbdb0319602d0e09f1a8abdba Mon Sep 17 00:00:00 2001 From: zhangw Date: Mon, 13 Jan 2025 13:10:26 +0800 Subject: [PATCH 37/41] feat: update --- .../docs-ui/src/services/editor/editor.ts | 4 +++ .../selection/doc-selection-render.service.ts | 31 +++++++++---------- .../views/range-selector/hooks/useFocus.ts | 8 ++--- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/docs-ui/src/services/editor/editor.ts b/packages/docs-ui/src/services/editor/editor.ts index dc356cf1b9ac..e3d6e5b4f553 100644 --- a/packages/docs-ui/src/services/editor/editor.ts +++ b/packages/docs-ui/src/services/editor/editor.ts @@ -143,6 +143,10 @@ export class Editor extends Disposable implements IEditor { this._listenSelection(); } + get docSelectionRenderService() { + return this._param.render.with(DocSelectionRenderService); + } + private _listenSelection() { const docSelectionRenderService = this._param.render.with(DocSelectionRenderService); 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 c2dcabac57ad..ea42567989a7 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 @@ -93,6 +93,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo private _currentSegmentId: string = ''; private _currentSegmentPage: number = -1; private _selectionStyle: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE; + private _onPointerEvent = false; private _viewPortObserverMap = new Map< string, @@ -108,6 +109,10 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo // When the user switches editors, whether to clear the doc ranges. private _reserveRanges = false; + get isOnPointerEvent() { + return this._onPointerEvent; + } + get isFocusing() { return this._input === document.activeElement; } @@ -181,7 +186,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo _currentSegmentPage: segmentPage, _selectionStyle: style, } = this; - const { scene, mainComponent } = this._context; const document = mainComponent as Documents; const docSkeleton = this._docSkeletonManagerService.getSkeleton(); @@ -509,7 +513,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo let preMoveOffsetX = evtOffsetX; let preMoveOffsetY = evtOffsetY; - + this._onPointerEvent = true; this._scenePointerMoveSubs.push(scene.onPointerMove$.subscribeEvent((moveEvt: IPointerEvent | IMouseEvent) => { const { offsetX: moveOffsetX, offsetY: moveOffsetY } = moveEvt; scene.setCursor(CURSOR_TYPE.TEXT); @@ -532,6 +536,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo [...this._scenePointerMoveSubs, ...this._scenePointerUpSubs].forEach((e) => { e.unsubscribe(); }); + this._onPointerEvent = false; scene.enableObjectsEvent(); // Add cursor. @@ -622,6 +627,14 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo return this._rectRangeList.map(serializeRectRange); } + getAllTextRanges() { + return this._getAllTextRanges(); + } + + getAllRectRanges() { + return this._getAllRectRanges(); + } + private _getActiveRange(): Nullable { const activeRange = this._rangeList.find((range) => range.isActive()); @@ -764,20 +777,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo this._rectRangeList = newRanges; } - private _removeCollapsedTextRange() { - const oldTextRanges = this._rangeList; - - this._rangeList = []; - - for (const textRange of oldTextRanges) { - if (textRange.collapsed) { - textRange.dispose(); - } else { - this._rangeList.push(textRange); - } - } - } - private _removeAllRanges() { this._removeAllTextRanges(); this._removeAllRectRanges(); 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 927477a70e87..f8fa3f300104 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 @@ -16,22 +16,22 @@ import type { Editor } from '@univerjs/docs-ui'; import { Tools } from '@univerjs/core'; -import { useCallback } from 'react'; +import { useEvent } from '@univerjs/ui'; export const useFocus = (editor?: Editor) => { - const focus = useCallback((offset?: number) => { + const focus = useEvent((offset?: number) => { if (editor) { editor.focus(); const selections = [...editor.getSelectionRanges()]; if (Tools.isDefine(offset)) { editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]); - } else if (!selections.length) { + } else if (!selections.length && !editor.docSelectionRenderService.isOnPointerEvent) { 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; }; From 41f2d897da489db659b4280e7362e4bda1db89df Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 13 Jan 2025 06:01:37 +0000 Subject: [PATCH 38/41] chore(snapshots): update snapshots --- ...fault-sheet-fullpage-ci-chromium-linux.png | Bin 77629 -> 78036 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/e2e/visual-comparison/sheets/sheets-visual-comparison.spec.ts-snapshots/default-sheet-fullpage-ci-chromium-linux.png b/e2e/visual-comparison/sheets/sheets-visual-comparison.spec.ts-snapshots/default-sheet-fullpage-ci-chromium-linux.png index 08825fe9791066ef0f367ae2bf9bce125afe872d..800eeb6e88f65a1d58140b24ae7513da39e83367 100644 GIT binary patch literal 78036 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 Jl{J3%{{Sy9UmXAd From cecc4e112fb43a15cf9ed70d9934e695b321faaf Mon Sep 17 00:00:00 2001 From: zhangw Date: Mon, 13 Jan 2025 19:49:05 +0800 Subject: [PATCH 39/41] feat: update --- packages/docs-ui/src/services/editor/editor.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/docs-ui/src/services/editor/editor.ts b/packages/docs-ui/src/services/editor/editor.ts index e3d6e5b4f553..8455f65dc3da 100644 --- a/packages/docs-ui/src/services/editor/editor.ts +++ b/packages/docs-ui/src/services/editor/editor.ts @@ -292,6 +292,12 @@ export class Editor extends Disposable implements IEditor { return this._docSelectionManagerService.getDocRanges(params); } + getCursorPosition(): number { + const selectionRanges = this.getSelectionRanges(); + + return selectionRanges.find((range) => range.collapsed)?.startOffset ?? -1; + } + // get editor id. getEditorId(): string { return this._getEditorId(); @@ -304,6 +310,10 @@ export class Editor extends Disposable implements IEditor { return docDataModel.getSnapshot(); } + getDocumentDataModel() { + return this._getDocDataModel(); + } + // Set the new document data. setDocumentData(data: IDocumentData, textRanges: Nullable) { const { id } = data; From a69443ef68fd8d7bc31ae07f1940d06456fef059 Mon Sep 17 00:00:00 2001 From: zhangw Date: Mon, 13 Jan 2025 21:14:26 +0800 Subject: [PATCH 40/41] feat: update --- packages/docs-ui/src/services/editor/editor.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/docs-ui/src/services/editor/editor.ts b/packages/docs-ui/src/services/editor/editor.ts index 8455f65dc3da..c3a7ac79af95 100644 --- a/packages/docs-ui/src/services/editor/editor.ts +++ b/packages/docs-ui/src/services/editor/editor.ts @@ -227,6 +227,9 @@ export class Editor extends Disposable implements IEditor { return docSelectionRenderService.isFocusing && Boolean(docSelectionRenderService.getActiveTextRange()); } + /** + * @deprecated use `IEditorService.focus` as instead. this is for internal usage. + */ focus() { const curDoc = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC); const editorUnitId = this.getEditorId(); @@ -253,6 +256,9 @@ export class Editor extends Disposable implements IEditor { // } } + /** + * @deprecated use `IEditorService.blur` as instead. this is for internal usage. + */ blur(): void { const docSelectionRenderService = this._param.render.with(DocSelectionRenderService); From 9144500f1bb6e0c586e9e6f6d824f2385b70fcdd Mon Sep 17 00:00:00 2001 From: zhangw Date: Mon, 13 Jan 2025 22:16:38 +0800 Subject: [PATCH 41/41] feat: update --- .../render-controllers/editor-bridge.render-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a178d5f77962..014d296e2e81 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 @@ -211,7 +211,7 @@ export class EditorBridgeRenderController extends RxDisposable implements IRende const editCell = this._editorBridgeService.getEditLocation(); if (editCell) { const { unitId: editingUnitId, sheetId: editingSheetId, row, column } = editCell; - if (unitId === editingUnitId && subUnitId === editingSheetId && cellValue && cellValue[row] && Object.hasOwn(cellValue[row], column)) { + if (unitId === editingUnitId && subUnitId === editingSheetId && cellValue && cellValue[row] && Object.prototype.hasOwnProperty.call(cellValue[row], column)) { this._editorBridgeService.refreshEditCellState(); } }