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 da4ca2852678..2c609b4c81ce 100644 --- a/packages-experimental/debugger/src/controllers/debugger.controller.ts +++ b/packages-experimental/debugger/src/controllers/debugger.controller.ts @@ -36,8 +36,8 @@ import { ThemeOperation } from '../commands/operations/theme.operation'; 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'; @@ -83,7 +83,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 fc707859310b..59252d2e0300 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, retainSelectionTextX } from './text-x-utils'; +import { addCustomRangeTextX, deleteCustomRangeTextX, deleteSelectionTextX, replaceSelectionTextRuns, replaceSelectionTextX, retainSelectionTextX } from './text-x-utils'; export class BuildTextUtils { static customRange = { @@ -41,6 +41,7 @@ export class BuildTextUtils { makeSelection, normalizeSelection, delete: deleteSelectionTextX, + replaceTextRuns: replaceSelectionTextRuns, retain: retainSelectionTextX, }; 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 93a9d5988f02..fead6dcf28ca 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 @@ -15,7 +15,7 @@ */ 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'; @@ -23,7 +23,7 @@ import { type Nullable, Tools, UpdateDocsAttributeType } from '../../../../share 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 { @@ -328,3 +328,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 3ad1a3bc704d..1633e4335a53 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -72,8 +72,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/design/package.json b/packages/design/package.json index 2cc8bd17c6a2..c45d53b083e4 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.12", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -87,7 +86,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.6.0" }, 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/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/index.ts b/packages/design/src/index.ts index da74f9574464..4599253eaf17 100644 --- a/packages/design/src/index.ts +++ b/packages/design/src/index.ts @@ -48,6 +48,5 @@ export { Switch } from './components/switch'; export { type ILocale } from './locale/interface'; export { defaultTheme, greenTheme, themeInstance } from './themes'; export { DraggableList, type IDraggableListProps } from './components/draggable-list'; -export { type IMentionsProps, Mention, type MentionProps, Mentions } from './components/mentions'; export { clsx } from './helper/clsx'; export { resizeObserverCtor } from './helper/resize-observer'; 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-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-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..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 @@ -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(); @@ -69,16 +57,11 @@ export class DocEditorBridgeController extends Disposable implements IRenderModu this._commandExecutedListener(); - this._initialSetValue(); - this._initialBlur(); this._initialFocus(); - - this._initialValueChange(); } - // eslint-disable-next-line complexity private _resize(unitId: Nullable) { if (unitId == null) { return; @@ -114,10 +97,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 +107,11 @@ 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, - }); - }) - ); } private _initialBlur() { this.disposeWithMe( this._editorService.blur$.subscribe(() => { - // this._docSelectionRenderService.removeAllRanges(); - this._docSelectionRenderService.blur(); }) ); @@ -204,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', @@ -228,20 +152,11 @@ 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); }) ); - // 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(); @@ -251,39 +166,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 +192,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..113ece19307c 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,30 @@ 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) { this._focusEditorUnitId = id; } - /** @deprecated */ getFocusId() { return this._focusEditorUnitId; } - /** @deprecated */ getFocusEditor() { if (this._focusEditorUnitId) { return this.getEditor(this._focusEditorUnitId); @@ -289,119 +130,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 +159,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 +175,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 +184,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 +225,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 +241,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..dc356cf1b9ac 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'; @@ -50,13 +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. - 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; @@ -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 { @@ -161,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(); @@ -173,12 +131,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 +139,6 @@ export class Editor extends Disposable implements IEditor { private _undoRedoService: IUndoRedoService ) { super(); - this._openForSheetUnitId = this._param.openForSheetUnitId; - this._openForSheetSubUnitId = this._param.openForSheetSubUnitId; this._listenSelection(); } @@ -268,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(); @@ -281,26 +236,23 @@ 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, + // }); - this._focus = true; + // if (lastSelectionInfo) { + // this._docSelectionManagerService.replaceDocRanges(lastSelectionInfo, { + // unitId: editorUnitId, + // subUnitId: editorUnitId, + // }, false); + // } } blur(): void { const docSelectionRenderService = this._param.render.with(DocSelectionRenderService); docSelectionRenderService.blur(); - this._focus = false; } // Selects the entire content of the editor. @@ -352,13 +304,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,89 +373,24 @@ 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; - } - - /** @deprecated */ - setFocus(state = false) { - 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 +420,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/design/src/components/mentions/index.ts b/packages/docs-ui/src/views/rich-text-editor/hooks/index.ts similarity index 80% rename from packages/design/src/components/mentions/index.ts rename to packages/docs-ui/src/views/rich-text-editor/hooks/index.ts index fd9b5794dba7..5e0a9a86e6b6 100644 --- a/packages/design/src/components/mentions/index.ts +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export { Mentions } from './Mentions'; -export type { IMentionsProps } from './Mentions'; -export { Mention, type MentionProps } from 'react-mentions'; +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/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 new file mode 100644 index 000000000000..6d8924137035 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/hooks/useResize.ts @@ -0,0 +1,135 @@ +/** + * 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, autoScroll?: 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(() => { + // eslint-disable-next-line complexity + return debounce(() => { + if (!autoScrollbar) return; + 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(); + } + autoScroll && viewportMain?.scrollToBarPos({ x: 0, y: Infinity }); + } 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(); + } + autoScroll && viewportMain?.scrollToBarPos({ x: Infinity, y: 0 }); + } else { + scrollBar = null; + viewportMain?.scrollToBarPos({ x: 0, y: 0 }); + viewportMain?.getScrollBar()?.dispose(); + } + } + }, 30); + }, [editor, autoScrollbar, isSingle, autoScroll]); + + 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..30ea8da0a6d8 --- /dev/null +++ b/packages/docs-ui/src/views/rich-text-editor/index.tsx @@ -0,0 +1,165 @@ +/** + * 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 { DocSkeletonManagerService } from '@univerjs/docs'; +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 { useOnChange } from './hooks/useOnChange'; +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; + onHeightChange?: (height: number) => void; + onChange?: (data: IDocumentData) => void; + maxHeight?: number; + defaultHeight?: number; +} + +export const RichTextEditor = forwardRef((props, ref) => { + const { + className, + autoFocus, + onFocusChange: _onFocusChange, + initialValue, + onClickOutside: _onClickOutside, + keyboardEventConfig, + moveCursor = true, + 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({ + 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 ?? '')); + 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 ?? '')); + + const sub = editor?.selectionChange$.subscribe(() => { + setShowPlaceholder(!BuildTextUtils.transform.getPlainText(editor?.getDocumentData().body?.dataStream ?? '')); + }); + + return () => sub?.unsubscribe(); + }, [editor]); + + useObservable(editor?.blur$); + useObservable(editor?.focus$); + + useEffect(() => { + onFocusChange?.(isFocusing); + }, [isFocusing, onFocusChange]); + + 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?.(); + } + }; + + 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]); + useOnChange(editor, onChange); + + 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-conditional-formatting/src/facade/f-range.ts b/packages/sheets-conditional-formatting/src/facade/f-range.ts index 2dac517da1a3..9fad516431dc 100644 --- a/packages/sheets-conditional-formatting/src/facade/f-range.ts +++ b/packages/sheets-conditional-formatting/src/facade/f-range.ts @@ -38,7 +38,7 @@ export interface IFRangeConditionalFormattingMixin { /** * Gets all the conditional formatting for the current range - * @return {*} {IConditionFormattingRule[]} + * @returns {*} {IConditionFormattingRule[]} * @memberof IFWorksheetConditionalFormattingMixin * @example * ```ts @@ -51,7 +51,7 @@ export interface IFRangeConditionalFormattingMixin { getConditionalFormattingRules(): IConditionFormattingRule[]; /** * Creates a constructor for conditional formatting - * @return {*} {ConditionalFormatRuleBuilder} + * @returns {*} {ConditionalFormatRuleBuilder} * @memberof IFWorksheetConditionalFormattingMixin * @example * ```ts @@ -81,7 +81,6 @@ export interface IFRangeConditionalFormattingMixin { /** * Delete conditional format according to `cfId` - * * @param {string} cfId * @returns {FRange} Returns the current range instance for method chaining * @memberof IFRangeConditionalFormattingMixin 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 ( { + 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-data-validation-ui/src/views/components/list-dropdown/index.tsx b/packages/sheets-data-validation-ui/src/views/components/list-dropdown/index.tsx index e56b1a6728f2..006d4e36d3eb 100644 --- a/packages/sheets-data-validation-ui/src/views/components/list-dropdown/index.tsx +++ b/packages/sheets-data-validation-ui/src/views/components/list-dropdown/index.tsx @@ -156,9 +156,7 @@ export function ListDropDown(props: IDropdownComponentProps) { const params = command.params as IRichTextEditingMutationParams; const { unitId } = params; const unit = instanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); - if (!unit) { - return; - } + if (!unit || !editorBridgeService.isVisible().visible) return; const text = BuildTextUtils.transform.getPlainText(unit.getSnapshot().body?.dataStream ?? ''); setEditingText(text); } @@ -167,7 +165,7 @@ export function ListDropDown(props: IDropdownComponentProps) { return () => { dispose.dispose(); }; - }, [commandService, instanceService]); + }, [commandService, editorBridgeService, instanceService]); if (!worksheet) { return null; diff --git a/packages/sheets-data-validation/src/facade/f-data-validation-builder.ts b/packages/sheets-data-validation/src/facade/f-data-validation-builder.ts index bf430139ca91..bcd9565561ce 100644 --- a/packages/sheets-data-validation/src/facade/f-data-validation-builder.ts +++ b/packages/sheets-data-validation/src/facade/f-data-validation-builder.ts @@ -40,7 +40,6 @@ export class FDataValidationBuilder { /** * Builds an FDataValidation instance based on the _rule property of the current class - * * @returns {FDataValidation} A new instance of the FDataValidation class */ build(): FDataValidation { @@ -49,8 +48,7 @@ export class FDataValidationBuilder { /** * Creates a duplicate of the current DataValidationBuilder object - * - * @return {FDataValidationBuilder} A new instance of the DataValidationBuilder class + * @returns {FDataValidationBuilder} A new instance of the DataValidationBuilder class */ copy(): FDataValidationBuilder { return new FDataValidationBuilder({ @@ -61,7 +59,6 @@ export class FDataValidationBuilder { /** * Determines whether invalid data is allowed - * * @returns {boolean} True if invalid data is allowed, False otherwise */ getAllowInvalid(): boolean { @@ -70,7 +67,6 @@ export class FDataValidationBuilder { /** * Gets the data validation type of the rule - * * @returns {DataValidationType} The data validation type */ getCriteriaType(): DataValidationType | string { @@ -79,7 +75,6 @@ export class FDataValidationBuilder { /** * Gets the values used for criteria evaluation - * * @returns {[string, string, string]} An array containing the operator, formula1, and formula2 values */ getCriteriaValues(): [string | undefined, string | undefined, string | undefined] { @@ -88,7 +83,6 @@ export class FDataValidationBuilder { /** * Gets the help text information, which is used to provide users with guidance and support - * * @returns {string | undefined} Returns the help text information. If there is no error message, it returns an undefined value. */ getHelpText(): string | undefined { @@ -97,7 +91,6 @@ export class FDataValidationBuilder { /** * Sets the data validation type to CHECKBOX and sets the checked and unchecked values - * * @param checkedValue The value when the checkbox is checked (Optional) * @param uncheckedValue The value when the checkbox is unchecked (Optional) * @returns The current instance of the FDataValidationBuilder class to allow for method chaining @@ -112,9 +105,8 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be after a specific date - * * @param date The date to compare against. The formatted date string will be set as formula1 - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateAfter(date: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -126,9 +118,8 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be before a specific date - * * @param date The date to compare against. The formatted date string will be set as formula1 - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateBefore(date: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -141,10 +132,9 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be within a specific date range - * * @param start The starting date of the range. The formatted date string will be set as formula1 * @param end The ending date of the range. The formatted date string will be set as formula2 - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateBetween(start: Date, end: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -157,9 +147,8 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be equal to a specific date - * * @param date The date to compare against. The formatted date string will be set as formula1 - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateEqualTo(date: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -172,10 +161,9 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be not within a specific date range - * * @param start The starting date of the date range * @param end The ending date of the date range - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateNotBetween(start: Date, end: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -188,9 +176,8 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be on or after a specific date - * * @param date The date to compare against. The formatted date string will be set as formula1 - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateOnOrAfter(date: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -203,9 +190,8 @@ export class FDataValidationBuilder { /** * Set the data validation type to DATE and configure the validation rules to be on or before a specific date - * * @param date The date to compare against. The formatted date string will be set as formula1 - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireDateOnOrBefore(date: Date): FDataValidationBuilder { this._rule.type = DataValidationType.DATE; @@ -219,9 +205,8 @@ export class FDataValidationBuilder { /** * Requires that a custom formula be satisfied. * Sets the data validation type to CUSTOM and configures the validation rule based on the provided formula string. - * * @param formula The formula string that needs to be satisfied. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireFormulaSatisfied(formula: string): FDataValidationBuilder { this._rule.type = DataValidationType.CUSTOM; @@ -233,11 +218,10 @@ export class FDataValidationBuilder { /** * Requires the user to enter a number within a specific range, which can be integer or decimal. * Sets the data validation type based on the isInteger parameter and configures the validation rules for the specified number range. - * * @param start The starting value of the number range. * @param end The ending value of the number range. * @param isInteger Indicates whether the required number is an integer. Default is undefined, meaning it can be an integer or decimal. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireNumberBetween(start: number, end: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${start}`; @@ -251,10 +235,9 @@ export class FDataValidationBuilder { /** * Requires the user to enter a number that is equal to a specific value, which can be an integer or a decimal. * Sets the data validation type based on the isInteger parameter and configures the validation rules for the specified number. - * * @param num The number to which the entered number should be equal. * @param isInteger Indicates whether the required number is an integer. Default is undefined, meaning it can be an integer or a decimal. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireNumberEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${num}`; @@ -267,10 +250,9 @@ export class FDataValidationBuilder { /** * Requires the user to enter a number that is greater than a specific value, which can be an integer or a decimal. * Sets the data validation type based on the isInteger parameter and configures the validation rules for the specified number. - * * @param num The number to which the entered number should be greater. * @param isInteger Indicates whether the required number is an integer. Default is undefined, meaning it can be an integer or a decimal. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireNumberGreaterThan(num: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${num}`; @@ -283,10 +265,9 @@ export class FDataValidationBuilder { /** * Requires the user to enter a number that is greater than or equal to a specific value, which can be an integer or a decimal. * Sets the data validation type based on the isInteger parameter and configures the validation rules for the specified number. - * * @param num The number to which the entered number should be greater than or equal. * @param isInteger Indicates whether the required number is an integer. Default is undefined, meaning it can be an integer or a decimal. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireNumberGreaterThanOrEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${num}`; @@ -299,10 +280,9 @@ export class FDataValidationBuilder { /** * Requires the user to enter a number that is less than a specific value, which can be an integer or a decimal. * Sets the data validation type based on the isInteger parameter and configures the validation rules for the specified number. - * * @param num The number to which the entered number should be less. * @param isInteger Indicates whether the required number is an integer. Default is undefined, meaning it can be an integer or a decimal. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireNumberLessThan(num: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${num}`; @@ -315,10 +295,9 @@ export class FDataValidationBuilder { /** * Sets the data validation rule to require a number less than or equal to a specified value * The specified value can be an integer or a decimal - * * @param num The number to which the entered number should be less than or equal * @param isInteger Indicates whether the required number is an integer - * @return The current instance of the DataValidationBuilder class, allowing for method chaining + * @returns The current instance of the DataValidationBuilder class, allowing for method chaining */ requireNumberLessThanOrEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${num}`; @@ -331,11 +310,10 @@ export class FDataValidationBuilder { /** * Sets a data validation rule that requires the user to enter a number outside a specified range * The specified range includes all integers and decimals - * * @param start The starting point of the specified range * @param end The end point of the specified range * @param isInteger Optional parameter, indicating whether the number to be verified is an integer. Default value is false - * @return An instance of the FDataValidationBuilder class, allowing for method chaining + * @returns An instance of the FDataValidationBuilder class, allowing for method chaining */ requireNumberNotBetween(start: number, end: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${start}`; @@ -349,10 +327,9 @@ export class FDataValidationBuilder { /** * Creates a data validation rule that requires the user to enter a number that is not equal to a specific value * The specific value can be an integer or a decimal - * * @param num The number to which the entered number should not be equal * @param isInteger Indicates whether the required number is an integer. Default is undefined, meaning it can be an integer or a decimal - * @return The current instance of the FDataValidationBuilder class to allow for method chaining + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining */ requireNumberNotEqualTo(num: number, isInteger?: boolean): FDataValidationBuilder { this._rule.formula1 = `${num}`; @@ -365,11 +342,10 @@ export class FDataValidationBuilder { /** * Sets a data validation rule that requires the user to enter a value from a list of specific values. * The list can be displayed in a dropdown, and the user can choose multiple values according to the settings. - * * @param values An array containing the specific values that the user can enter. * @param multiple Optional parameter indicating whether the user can select multiple values. Default is false, meaning only one value can be selected. * @param showDropdown Optional parameter indicating whether to display the list in a dropdown. Default is true, meaning the list will be displayed as a dropdown. - * @return An instance of the FDataValidationBuilder class, allowing for method chaining. + * @returns An instance of the FDataValidationBuilder class, allowing for method chaining. */ requireValueInList(values: string[], multiple?: boolean, showDropdown?: boolean): FDataValidationBuilder { this._rule.type = multiple ? DataValidationType.LIST_MULTIPLE : DataValidationType.LIST; @@ -383,11 +359,10 @@ export class FDataValidationBuilder { /** * Sets a data validation rule that requires the user to enter a value within a specific range. * The range is defined by an FRange object, which contains the unit ID, sheet name, and cell range. - * * @param range An FRange object representing the range of values that the user can enter. * @param multiple Optional parameter indicating whether the user can select multiple values. Default is false, meaning only one value can be selected. * @param showDropdown Optional parameter indicating whether to display the list in a dropdown. Default is true, meaning the list will be displayed as a dropdown. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ requireValueInRange(range: FRange, multiple?: boolean, showDropdown?: boolean): FDataValidationBuilder { this._rule.type = multiple ? DataValidationType.LIST_MULTIPLE : DataValidationType.LIST; @@ -406,9 +381,8 @@ export class FDataValidationBuilder { * Sets whether to allow invalid data and configures the error style for data validation. * If invalid data is not allowed, the error style will be set to STOP, indicating that data entry must stop upon encountering an error. * If invalid data is allowed, the error style will be set to WARNING, indicating that a warning will be displayed when invalid data is entered, but data entry can continue. - * * @param allowInvalidData A boolean value indicating whether to allow invalid data. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ setAllowInvalid(allowInvalidData: boolean): FDataValidationBuilder { this._rule.errorStyle = !allowInvalidData ? DataValidationErrorStyle.STOP : DataValidationErrorStyle.WARNING; @@ -418,9 +392,8 @@ export class FDataValidationBuilder { /** * Sets the help text and enables the display of error messages for data validation. * This method allows you to set a custom help text that will be displayed when the user enters invalid data. - * * @param helpText The text to display as help information. - * @return The current instance of the FDataValidationBuilder class to allow for method chaining. + * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ setHelpText(helpText: string): FDataValidationBuilder { this._rule.error = helpText; @@ -431,12 +404,11 @@ export class FDataValidationBuilder { /** * Sets the criteria values for data validation. * This method is used to configure the validation rules based on specific criteria values. - * * @param type The type of data validation. * @param values An array containing the criteria values. * The array should have three elements: [operator, formula1, formula2]. * operator is a DataValidationOperator enum value, formula1 is the first formula, and formula2 is the second formula. - * @return The current instance of the FDataValidationBuilder class, allowing for method chaining. + * @returns The current instance of the FDataValidationBuilder class, allowing for method chaining. */ withCriteriaValues(type: DataValidationType | string, values: [DataValidationOperator, string, string]): this { this._rule.type = type; @@ -454,7 +426,6 @@ export class FDataValidationBuilder { /** * Sets the options for the data validation rule. * For details of options, please refer to https://univer.ai/typedoc/@univerjs/core/interfaces/IDataValidationRuleOptions - * * @param options The options to set for the data validation rule. * @returns The current instance of the FDataValidationBuilder class to allow for method chaining. */ diff --git a/packages/sheets-data-validation/src/facade/f-data-validation.ts b/packages/sheets-data-validation/src/facade/f-data-validation.ts index 242b1a66b0de..7ff8a55c04d1 100644 --- a/packages/sheets-data-validation/src/facade/f-data-validation.ts +++ b/packages/sheets-data-validation/src/facade/f-data-validation.ts @@ -35,8 +35,7 @@ export class FDataValidation { /** * Gets whether invalid data is allowed based on the error style value. - * - * @return true if invalid data is allowed, false otherwise. + * @returns true if invalid data is allowed, false otherwise. */ getAllowInvalid(): boolean { return this.rule.errorStyle !== DataValidationErrorStyle.STOP; @@ -44,7 +43,6 @@ export class FDataValidation { /** * Gets the data validation type of the rule - * * @returns The data validation type */ getCriteriaType(): DataValidationType | string { @@ -53,7 +51,6 @@ export class FDataValidation { /** * Gets the values used for criteria evaluation - * * @returns An array containing the operator, formula1, and formula2 values */ getCriteriaValues(): [string | undefined, string | undefined, string | undefined] { @@ -62,7 +59,6 @@ export class FDataValidation { /** * Gets the help text information, which is used to provide users with guidance and support - * * @returns Returns the help text information. If there is no error message, it returns an undefined value. */ getHelpText(): string | undefined { @@ -72,8 +68,7 @@ export class FDataValidation { /** * Creates a new instance of FDataValidationBuilder using the current rule object. * This method is useful for copying an existing data validation rule configuration. - * - * @return A new FDataValidationBuilder instance with the same rule configuration. + * @returns A new FDataValidationBuilder instance with the same rule configuration. */ copy(): FDataValidationBuilder { return new FDataValidationBuilder(this.rule); @@ -81,7 +76,6 @@ export class FDataValidation { /** * Gets whether the data validation rule is applied to the worksheet. - * * @returns true if the rule is applied, false otherwise. */ getApplied(): boolean { @@ -100,7 +94,6 @@ export class FDataValidation { /** * Gets the ranges to which the data validation rule is applied. - * * @returns An array of IRange objects representing the ranges to which the data validation rule is applied. */ getRanges(): FRange[] { @@ -114,7 +107,6 @@ export class FDataValidation { /** * Gets the title of the error message dialog box. - * * @returns The title of the error message dialog box. */ getUnitId(): string | undefined { @@ -123,7 +115,6 @@ export class FDataValidation { /** * Gets the sheetId of the worksheet. - * * @returns The sheetId of the worksheet. */ getSheetId(): string | undefined { @@ -134,6 +125,7 @@ export class FDataValidation { * Set Criteria for the data validation rule. * @param type The type of data validation criteria. * @param values An array containing the operator, formula1, and formula2 values. + * @param allowBlank * @returns true if the criteria is set successfully, false otherwise. */ setCriteria(type: DataValidationType, values: [DataValidationOperator, string, string], allowBlank = true): FDataValidation { diff --git a/packages/sheets-filter/src/facade/f-event.ts b/packages/sheets-filter/src/facade/f-event.ts index 5b7712892885..811b1cb53e5b 100644 --- a/packages/sheets-filter/src/facade/f-event.ts +++ b/packages/sheets-filter/src/facade/f-event.ts @@ -25,7 +25,6 @@ export interface IFSheetFilterEventMixin { /** * This event will be emitted when the filter criteria on a column is changed. * Type of the event is {@link ISheetRangeFilteredParams}. - * * @example * ```typescript * const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeFiltered, (params) => { @@ -39,7 +38,6 @@ export interface IFSheetFilterEventMixin { /** * This event will be emitted before the filter criteria on a column is changed. * Type of the event is {@link ISheetRangeFilteredParams}. - * * @example * ```typescript * const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeFilter, (params) => { @@ -53,7 +51,6 @@ export interface IFSheetFilterEventMixin { /** * This event will be emitted when the filter on a worksheet is cleared. * Type of the event is {@link ISheetRangeFilterClearedEventParams}. - * * @example * ```typescript * const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeFilterCleared, (params) => { @@ -67,7 +64,6 @@ export interface IFSheetFilterEventMixin { /** * This event will be emitted after the filter on a worksheet is cleared. * Type of the event is {@link ISheetRangeFilterClearedEventParams}. - * * @example * ```typescript * const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeFilterClear, (params) => { diff --git a/packages/sheets-filter/src/facade/f-range.ts b/packages/sheets-filter/src/facade/f-range.ts index f66330ed3d09..906e3a239820 100644 --- a/packages/sheets-filter/src/facade/f-range.ts +++ b/packages/sheets-filter/src/facade/f-range.ts @@ -25,10 +25,8 @@ import { FFilter } from './f-filter'; export interface IFRangeFilter { /** * Create a filter for the current range. If the worksheet already has a filter, this method would return `null`. - * - * @return The interface class to handle the filter. If the worksheet already has a filter, + * @returns The interface class to handle the filter. If the worksheet already has a filter, * this method would return `null`. - * * @example * ```typescript * const workbook = univerAPI.getActiveWorkbook(); @@ -39,10 +37,8 @@ export interface IFRangeFilter { createFilter(this: FRange): FFilter | null; /** * Get the filter for the current range's worksheet. Normally, you can directly call `getFilter` on {@link FWorksheet}. - * - * @return {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter, + * @returns {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter, * this method would return `null`. - * * @example * ```typescript * const workbook = univerAPI.getActiveWorkbook(); @@ -70,8 +66,7 @@ export class FRangeFilter extends FRange implements IFRangeFilter { /** * Get the filter for the current range's worksheet. - * - * @return {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter, + * @returns {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter, * this method would return `null`. */ override getFilter(): FFilter | null { diff --git a/packages/sheets-filter/src/facade/f-worksheet.ts b/packages/sheets-filter/src/facade/f-worksheet.ts index 6064fcc182f6..769428dbfa3b 100644 --- a/packages/sheets-filter/src/facade/f-worksheet.ts +++ b/packages/sheets-filter/src/facade/f-worksheet.ts @@ -23,10 +23,8 @@ import { FFilter } from './f-filter'; export interface IFWorksheetFilter { /** * Get the filter for the current worksheet. - * - * @return {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter, + * @returns {FFilter | null} The interface class to handle the filter. If the worksheet does not have a filter, * this method would return `null`. - * * @example * ```typescript * const workbook = univerAPI.getActiveWorkbook(); 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..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; @@ -269,7 +273,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..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 @@ -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..d087cdaa1901 --- /dev/null +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useFormulaSelection.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 { IAccessor } from '@univerjs/core'; +import { Injector, IUniverInstanceService, useDependency } from '@univerjs/core'; +import { DocSelectionManagerService } from '@univerjs/docs'; +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 { 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, disableOnClick?: 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 lexerTreeBuilder = useDependency(LexerTreeBuilder); + 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; + }) + ) + .subscribe((index) => { + const config = getCurrentBodyDataStreamAndOffset(injector); + if (!config) return; + const dataStream = config?.dataStream?.slice(0, -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; + + 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, lexerTreeBuilder]); + + useEffect(() => { + if (!isFocus) { + setIsSelecting(FormulaSelectingType.NOT_SELECT); + isDisabledByPointer.current = true; + } + }, [isFocus]); + + useEffect(() => { + if (!disableOnClick) return; + const sub = renderer?.mainComponent?.onPointerDown$.subscribeEvent(() => { + setIsSelecting(FormulaSelectingType.NOT_SELECT); + isDisabledByPointer.current = true; + }); + + return () => sub?.unsubscribe(); + }, [disableOnClick, renderer?.mainComponent?.onPointerDown$]); + + return { isSelecting }; +} 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 1ae2f2a23c6f..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 @@ -18,16 +18,20 @@ 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 { INode } from '../../range-selector/utils/filterReferenceNode'; -import { DisposableCollection, 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 { useEvent } from '@univerjs/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'; @@ -38,38 +42,45 @@ import { useSelectionAdd } from './useSelectionAdd'; const noop = (() => { }) as any; export const useSheetSelectionChange = ( isNeed: boolean, + isFocus: boolean, 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 { getIsNeedAddSelection } = useSelectionAdd(unitId, sequenceNodes, editor); + const docSelectionManagerService = useDependency(DocSelectionManagerService); + const themeService = useDependency(ThemeService); + const { getIsNeedAddSelection } = useSelectionAdd(unitId, 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); const refSelectionsRenderService = render?.with(RefSelectionsRenderService); - + const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); + const refSelectionsService = useDependency(IRefSelectionsService); const isScalingRef = useRef(false); const scalingOptionRef = useRef<{ result: string; offset: number }>(); + useEffect(() => {}, []); + useEffect(() => { if (refSelectionsRenderService && isNeed) { let isFirst = true; // eslint-disable-next-line complexity - const handleSelectionsChange = (selections: ISelectionWithCoord[]) => { - if (isFirst || isScalingRef.current) { + const handleSelectionsChange = (selections: ISelectionWithCoord[], isEnd: boolean) => { + if (isFirst) { isFirst = false; return; } @@ -80,14 +91,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,11 +107,11 @@ 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); - handleRangeChange(result, getOffsetFromSequenceNodes(sequenceNodes), true); + handleRangeChange(result, getOffsetFromSequenceNodes(sequenceNodes), isEnd); } else { const range = selections[selections.length - 1]; const rangeSheetId = range.rangeWithCoord.sheetId ?? subUnitId; @@ -112,12 +124,12 @@ 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 let currentRefIndex = 0; - const currentText = sequenceNodes.map((item) => { + const newTokens = sequenceNodes.map((item) => { if (typeof item === 'string') { return item; } @@ -144,11 +156,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,38 +179,33 @@ 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, !theLastList.length && newOffset ? newOffset : result.length, isEnd); } }; - const d1 = refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { - handleSelectionsChange(selections); - isScalingRef.current = false; - if (scalingOptionRef.current) { - const { result, offset } = scalingOptionRef.current; - handleRangeChange(result, offset || -1, true); - scalingOptionRef.current = undefined; - } - }); - - // const d2 = refSelectionsRenderService.selectionMoving$.subscribe((selections) => { - // handleSelectionsChange(selections); - // }); + 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, true); + })); return () => { - d1.unsubscribe(); - // d2.unsubscribe(); + disposableCollection.dispose(); }; } - }, [refSelectionsRenderService, editor, isSupportAcrossSheet, isNeed]); + }, [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; @@ -249,9 +264,10 @@ export const useSheetSelectionChange = ( return node; }); const result = sequenceNodeToText(newSequenceNodes); - handleRangeChange(result, -1, false); + handleRangeChange(result, -1, true); scalingOptionRef.current = { result, offset }; }; + const reListen = () => { disposableCollection.dispose(); const controls = refSelectionsRenderService.getSelectionControls(); @@ -260,14 +276,29 @@ export const useSheetSelectionChange = ( map((e) => { return serializeRange(e); }), - distinctUntilChanged() + distinctUntilChanged(), + debounceTime(100) ).subscribe((rangeText) => { isScalingRef.current = true; 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$, refSelectionsRenderService.selectionMoveEnd$).pipe(debounceTime(50)).subscribe(() => { + const dispose = merge( + editor.input$, + refSelectionsService.selectionSet$, + refSelectionsRenderService.selectionMoveEnd$ + ).pipe(debounceTime(50) + ).subscribe(() => { reListen(); }); @@ -276,5 +307,85 @@ export const useSheetSelectionChange = ( disposableCollection.dispose(); }; } - }, [isNeed, refSelectionsRenderService, editor]); + }, [isFocus, refSelectionsRenderService, editor, refSelectionsService.selectionSet$, contextRef, sequenceNodesRef, handleRangeChange, isSupportAcrossSheet, unitId]); + + 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, sheetName, subUnitId, unitId]); + + useEffect(() => { + if (!editor) { + return; + } + const sub = docSelectionManagerService.textSelection$.subscribe((e) => { + if (e.unitId !== editor.getEditorId()) { + return; + } + + calcHighlightRanges({ + unitId, + subUnitId, + refSelections: refSelectionRef.current, + editor, + refSelectionsService, + refSelectionsRenderService, + sheetSkeletonManagerService, + themeService, + univerInstanceService, + }); + }); + + return () => sub.unsubscribe(); + }, [docSelectionManagerService.textSelection$, editor, refSelectionRef, refSelectionsRenderService, refSelectionsService, sequenceNodesRef, sheetSkeletonManagerService, subUnitId, themeService, unitId, univerInstanceService]); }; 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/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 b36b7a2cd0f7..95026a69df6f 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -14,32 +14,31 @@ * limitations under the License. */ -import type { 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'; -import { createInternalEditorID, generateRandomId, useDependency } from '@univerjs/core'; -import { DocBackScrollRenderController, IEditorService } from '@univerjs/docs-ui'; -import { deserializeRangeWithSheet, LexerTreeBuilder, operatorToken, sequenceNodeType } 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, 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 { 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 { rangePreProcess } from '../range-selector/utils/rangePreProcess'; -import { sequenceNodeToText } from '../range-selector/utils/sequenceNodeToText'; -import { unitRangesToText } from '../range-selector/utils/unitRangesToText'; 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 styles from './index.module.less'; @@ -60,28 +59,50 @@ 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; + /** + * Disable selection when click formula editor + */ + disableSelectionOnClick?: 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: propOnChange, + onVerify, + actions, + className, + editorId: propEditorId, + moveCursor = true, + onFormulaSelectingChange: propOnFormulaSelectingChange, + keyboradEventConfig, + onMoveInEditor, + resetSelectionOnBlur = true, + autoScrollbar = true, + isSingle = true, + disableSelectionOnClick = false, } = props; const editorService = useDependency(IEditorService); - const lexerTreeBuilder = useDependency(LexerTreeBuilder); - const sheetEmbeddingRef = useRef(null); - const [formulaText, formulaTextSet] = useState(() => { - if (initValue.startsWith(operatorToken.EQUALS)) { - return initValue; - } - return ''; - }); - + const onChange = useEvent(propOnChange); // init actions if (actions) { actions.handleOutClick = (e: MouseEvent, cb: () => void) => { @@ -92,127 +113,86 @@ 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, disableSelectionOnClick); + 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) => { - const nodes = lexerTreeBuilder.sequenceNodesBuilder(text); - if (nodes) { - const preNodes = nodes.map((node) => { - if (typeof node === 'string') { - return node; - } else if (node.nodeType === sequenceNodeType.REFERENCE) { - // The 'sequenceNodesBuilder' will cache the results. - // You Can't modify the reference here. This will cause a cache error - const cloneNode = { ...node }; - const unitRange = deserializeRangeWithSheet(node.token); - unitRange.range = rangePreProcess(unitRange.range); - if (!isSupportAcrossSheet) { - unitRange.sheetName = ''; - unitRange.unitId = ''; - } - cloneNode.token = unitRangesToText([unitRange], isSupportAcrossSheet)[0]; - return cloneNode; - } - return node; - }); - const result = sequenceNodeToText(preNodes); - onChange(`=${result}`); - } - }, editor); + useUpdateEffect(() => { + onChange(formulaText); + }, [formulaText, onChange]); const highlightDoc = useDocHight('='); - const highlightSheet = useSheetHighlight(unitId); - const highligh = (text: string, isNeedResetSelection: boolean = true) => { - if (!editor) { + const highlightSheet = useSheetHighlight(unitId, subUnitId); + const highlight = useEvent((text: string, isNeedResetSelection: boolean = true, isEnd?: boolean, newSelections?: ITextRange[]) => { + 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] === '=', + newSelections + ); + refSelections.current = ranges; - const resetSelection = useResetSelection(isFocus); + if (isEnd) { + highlightSheet(isFocus ? ranges : [], editorRef.current); + } + }); - 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]); + }, [formulaText, isFocus, highlight]); - 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; @@ -220,15 +200,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 () => { @@ -236,10 +222,57 @@ 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; + } + 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 }; + const docBackScrollRenderController = editor?.render.with(DocBackScrollRenderController); + docBackScrollRenderController?.scrollToRange({ ...range, collapsed: true }); + }, 50); + } + checkScrollBar(); + } + }); + + useSheetSelectionChange( + isFocus && Boolean(isSelecting && docFocusing), + isFocus, + 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]; @@ -250,26 +283,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..e1afc9d84c54 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/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/hooks/useFocus.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFocus.ts index 60a25bb957b3..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 @@ -15,26 +15,24 @@ */ 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) { + 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..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 @@ -14,25 +14,103 @@ * 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 } from '@univerjs/sheets'; +import type { ISelectionWithStyle, SheetsSelectionsService } from '@univerjs/sheets'; import type { INode } from './useFormulaToken'; -import { 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'; 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; +} + +export function calcHighlightRanges(opts: { + unitId: string; + subUnitId: string; + refSelections: IRefSelection[]; + editor: Editor | undefined; + 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); + } + + 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; } /** @@ -41,7 +119,7 @@ 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); @@ -50,61 +128,52 @@ export function useSheetHighlight(unitId: string) { const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - const highlightSheet = (refSelections: IRefSelection[]) => { - 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; - - for (let i = 0, len = refSelections.length; i < len; i++) { - const refSelection = refSelections[i]; - const { themeColor, token, refIndex } = 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) { - continue; - } - - const range = setEndForRange(rawRange, worksheet.getRowCount(), worksheet.getColumnCount()); - selectionWithStyle.push({ - range, - primary: null, - style: genFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()), - }); - } + const highlightSheet = useCallback((refSelections: IRefSelection[], editor?: Editor) => { + 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); } - }; + }, [refSelectionsRenderService, refSelectionsService, sheetSkeletonManagerService, themeService, unitId, subUnitId, 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, + newSelections?: ITextRange[] + ) => { const data = editor.getDocumentData(); + const editorId = editor.getEditorId(); if (!data) { return []; } @@ -114,9 +183,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 +219,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: newSelections ?? 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 +257,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 +280,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 +316,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 +338,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..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 @@ -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); } }; @@ -55,20 +111,40 @@ export const useLeftAndRightArrow = (isNeed: boolean, editor?: Editor) => { 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); }, })); - [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: { eventType: DeviceInputEventType.Keyboard, keyCode, + metaKey, }, }; }).forEach((item) => { @@ -78,5 +154,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..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 @@ -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,42 +30,46 @@ 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]); + }, [contextService, isNeed, refSelectionsService]); 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]); + }, [contextService, isNeed, refSelectionsRenderService, selecting]); - //right context controller + // right context controller useEffect(() => { if (isNeed) { + contextService.setContextValue(EDITOR_ACTIVATED, true); contextMenuService.disable(); return () => { + contextService.setContextValue(EDITOR_ACTIVATED, false); contextMenuService.enable(); }; } - }, [isNeed]); + }, [contextMenuService, contextService, isNeed]); // reset setSkipLastEnabled useEffect(() => { if (isNeed) { refSelectionsRenderService?.setSkipLastEnabled(false); } - }, [isNeed]); + }, [isNeed, refSelectionsRenderService]); }; 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/hooks/useSheetSelectionChange.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useSheetSelectionChange.ts index b594d944b555..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[], @@ -148,7 +149,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 +219,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 5d987fb11d7c..5397d483e170 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -15,24 +15,27 @@ */ 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 { createInternalEditorID, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, generateRandomId, ICommandService, LocaleService, useDependency } from '@univerjs/core'; +import type { IRefSelection } from './hooks/useHighlight'; +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 { 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 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'; -import { useEmitChange } from './hooks/useEmitChange'; import { useFirstHighlightDoc } from './hooks/useFirstHighlightDoc'; import { useFocus } from './hooks/useFocus'; import { useFormulaToken } from './hooks/useFormulaToken'; @@ -88,27 +91,35 @@ 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: propOnChange = noopFunction, + onVerify = noopFunction, + onRangeSelectorDialogVisibleChange = noopFunction, + onBlur = noopFunction, + onFocus = noopFunction, + isFocus: _isFocus = true, + isOnlyOneRange = false, + isSupportAcrossSheet = false, + } = props; + const onChange = useEvent(propOnChange); 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 +128,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 +151,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,67 +186,58 @@ 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 highlightSheet = useSheetHighlight(unitId, subUnitId); + 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, editorRef.current); + } + }); - 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) { + 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); - useRefactorEffect(isNeed, unitId); + useRefactorEffect(isNeed, isNeed && docFocusing, unitId); useOnlyOneRange(unitId, isOnlyOneRange); @@ -237,7 +245,7 @@ export function RangeSelector(props: IRangeSelectorProps) { useVerify(isNeed, onVerify, sequenceNodes); - useLeftAndRightArrow(isNeed, editor); + useLeftAndRightArrow(isNeed, 0, editor); useRefocus(); @@ -253,7 +261,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(); @@ -281,33 +288,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[]) => { @@ -316,7 +328,6 @@ export function RangeSelector(props: IRangeSelectorProps) { editor?.setDocumentData({ ...editor.getDocumentData(), body: { dataStream: '\r\n', textRuns: [] } }); } highligh(text); - needEmit(); rangeStringSet(text); rangeDialogVisibleSet(false); onRangeSelectorDialogVisibleChange(false); @@ -324,6 +335,7 @@ export function RangeSelector(props: IRangeSelectorProps) { isFocusSet(true); editor?.setSelectionRanges([{ startOffset: text.length, endOffset: text.length }]); focus(); + checkScrollBar(); }, 30); }; @@ -393,14 +405,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); @@ -470,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) { @@ -478,11 +487,11 @@ function RangeSelectorDialog(props: { } else { rangesSet(ranges); } - }, [focusIndex, isOnlyOneRange]); + }); - const highlightSheet = useSheetHighlight(unitId); + const highlightSheet = useSheetHighlight(unitId, subUnitId); 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)); @@ -497,21 +506,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-hyper-link/src/facade/f-range.ts b/packages/sheets-hyper-link/src/facade/f-range.ts index cf5df3728c88..7ff0d87c4630 100644 --- a/packages/sheets-hyper-link/src/facade/f-range.ts +++ b/packages/sheets-hyper-link/src/facade/f-range.ts @@ -55,6 +55,8 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi // #region hyperlink /** + * @param url + * @param label * @deprecated */ override setHyperLink(url: string, label?: string): Promise { @@ -94,6 +96,9 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi } /** + * @param id + * @param url + * @param label * @deprecated */ override updateHyperLink(id: string, url: string, label?: string): Promise { @@ -113,6 +118,7 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi } /** + * @param id * @deprecated */ override cancelHyperLink(id: string): boolean { diff --git a/packages/sheets-sort/src/facade/f-event.ts b/packages/sheets-sort/src/facade/f-event.ts index e66c61eb4385..9ded2e8427a6 100644 --- a/packages/sheets-sort/src/facade/f-event.ts +++ b/packages/sheets-sort/src/facade/f-event.ts @@ -24,7 +24,6 @@ export interface IFSheetSortEventMixin { /** * This event will be emitted when a range on a worksheet is sorted. * Type of the event is {@link ISheetRangeSortedParams}. - * * @example * ```typescript * const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetRangeSorted, (params) => { @@ -39,7 +38,6 @@ export interface IFSheetSortEventMixin { /** * This event will be emitted before sorting a range on a worksheet. * Type of the event is {@link ISheetRangeSortParams}. - * * @example * ```typescript * const callbackDisposable = univerAPI.addEvent(univerAPI.Event.SheetBeforeRangeSort, (params) => { diff --git a/packages/sheets-sort/src/facade/f-range.ts b/packages/sheets-sort/src/facade/f-range.ts index d3905d067751..58911bad2a2f 100644 --- a/packages/sheets-sort/src/facade/f-range.ts +++ b/packages/sheets-sort/src/facade/f-range.ts @@ -23,11 +23,8 @@ export type SortColumnSpec = { column: number; ascending: boolean } | number; export interface IFRangeSort { /** * Sorts the cells in the given range, by column(s) and order specified. - * * @param {SortColumnSpec | SortColumnSpec[]} column The column index with order or an array of column indexes with order. The column index starts from 1. - * * @returns The range itself for chaining. - * * @example * ```typescript * const activeSpreadsheet = univerAPI.getActiveWorkbook(); diff --git a/packages/sheets-sort/src/facade/f-worksheet.ts b/packages/sheets-sort/src/facade/f-worksheet.ts index d8bba9d6a6ba..43c6b8c17106 100644 --- a/packages/sheets-sort/src/facade/f-worksheet.ts +++ b/packages/sheets-sort/src/facade/f-worksheet.ts @@ -23,11 +23,9 @@ import { FWorksheet } from '@univerjs/sheets/facade'; export interface IFWorksheetSort { /** * Sort the worksheet by the specified column. - * * @param {number} colIndex The column index to sort by. which starts from 1. * @param {boolean} [asc=true] The sort order. `true` for ascending, `false` for descending. * @returns The worksheet itself for chaining. - * * @example * ```typescript * const activeSpreadsheet = univerAPI.getActiveWorkbook(); diff --git a/packages/sheets-thread-comment/src/facade/f-univer.ts b/packages/sheets-thread-comment/src/facade/f-univer.ts index be6b08c19c48..ad2d4df7500d 100644 --- a/packages/sheets-thread-comment/src/facade/f-univer.ts +++ b/packages/sheets-thread-comment/src/facade/f-univer.ts @@ -44,7 +44,7 @@ export interface IFUniverCommentMixin { /** * create a new thread comment - * @return {FTheadCommentBuilder} thead comment builder + * @returns {FTheadCommentBuilder} thead comment builder * @example * ```ts * const comment = univerAPI.newTheadComment().setContent(univerAPI.newRichText().insertText('hello zhangsan')); diff --git a/packages/sheets-thread-comment/src/facade/f-workbook.ts b/packages/sheets-thread-comment/src/facade/f-workbook.ts index 582a85d2b080..570c81871616 100644 --- a/packages/sheets-thread-comment/src/facade/f-workbook.ts +++ b/packages/sheets-thread-comment/src/facade/f-workbook.ts @@ -90,6 +90,7 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook } /** + * @param callback * @deprecated */ override onThreadCommentChange(callback: (commentUpdate: CommentUpdate) => void | false): IDisposable { @@ -99,6 +100,7 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook } /** + * @param callback * @deprecated */ override onBeforeAddThreadComment(callback: (params: IAddCommentCommandParams, options: IExecutionOptions | undefined) => void | false): IDisposable { @@ -116,6 +118,7 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook } /** + * @param callback * @deprecated */ override onBeforeUpdateThreadComment(callback: (params: IUpdateCommandParams, options: IExecutionOptions | undefined) => void | false): IDisposable { @@ -133,6 +136,7 @@ export class FWorkbookThreadCommentMixin extends FWorkbook implements IFWorkbook } /** + * @param callback * @deprecated */ override onBeforeDeleteThreadComment(callback: (params: IDeleteCommentCommandParams, options: IExecutionOptions | undefined) => void | false): IDisposable { 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/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; } 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/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..5a78b782dde0 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) { @@ -184,6 +209,7 @@ export class EditorDataSyncController extends Disposable { this._commandService.syncExecuteCommand(RichTextEditingMutation.id, { ...parmas, + textRanges: null, isSync: true, unitId, syncer: parmas.unitId, @@ -212,7 +238,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 +250,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 +275,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..9d0b26b7187a 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'; @@ -56,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'; @@ -98,7 +97,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 +116,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 +158,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); })); @@ -199,7 +197,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 +244,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 +251,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._getDocumentDataModel(); + 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?.canFocusing || formulaSelectionRenderManager?.canFocusing) { + this._univerInstanceService.setCurrentUnitForType(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); + cellSelectionRenderManager?.activate( + HIDDEN_EDITOR_POSITION, + HIDDEN_EDITOR_POSITION, + true + ); } })); } @@ -293,19 +299,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; } @@ -351,9 +351,9 @@ 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; - // Change `CursorChange` to changed status, when formula bar clicked. this._cursorChange = (eventType === DeviceInputEventType.PointerDown || eventType === DeviceInputEventType.Dblclick) @@ -375,29 +375,19 @@ 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._getDocumentDataModel(); + const skeleton = this._getEditorSkeleton(DOCS_NORMAL_EDITOR_UNIT_ID_KEY); if (!skeleton || !documentDataModel) { return; } @@ -409,12 +399,33 @@ 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) ) { - this._emptyDocumentDataModel(!!isInArrayFormulaRange); + this._emptyDocumentDataModel(documentDataModel.getSnapshot().documentStyle, !!isInArrayFormulaRange); document.makeDirty(); // @JOCS, Why calculate here? @@ -423,19 +434,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, @@ -449,10 +463,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 +486,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()) { @@ -480,9 +505,10 @@ 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: worksheetId, + subUnitId: sheetId, selections, }); } @@ -490,50 +516,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 +554,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 +646,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(() => { @@ -681,10 +679,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) { @@ -695,62 +697,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._getDocumentDataModel(); + 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 +878,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..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 @@ -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 && cellValue[row] && Object.hasOwn(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 3cbda7bb85fa..4b89ddf4882b 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 @@ -448,9 +448,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/mobile-plugin.ts b/packages/sheets-ui/src/mobile-plugin.ts index 3a180b3268a6..e9d0b2262ba4 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'; @@ -111,6 +111,7 @@ export class UniverSheetsMobileUIPlugin extends Plugin { [SheetsRenderService], [SheetUIMobileController], [StatusBarController], + [IRefSelectionsService, { useClass: RefSelectionsService }], // permission [SheetPermissionPanelModel], 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..86f9e6defb74 100644 --- a/packages/sheets-ui/src/services/canvas-pop-manager.service.ts +++ b/packages/sheets-ui/src/services/canvas-pop-manager.service.ts @@ -20,7 +20,7 @@ import type { ISetWorksheetRowAutoHeightMutationParams, ISheetLocationBase } fro import type { IPopup } from '@univerjs/ui'; import { Disposable, DisposableCollection, ICommandService, Inject, 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, IRefSelectionsService, RefRangeService, SetFrozenMutation, SetWorksheetRowAutoHeightMutation, SheetsSelectionsService } 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,39 @@ 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, + @IRefSelectionsService private readonly _refSelectionsService: ISheetSelectionRenderService, + @Inject(SheetsSelectionsService) private readonly _selectionManagerService: SheetsSelectionsService ) { super(); + + this._initMoving(); + } + + private _isSelectionMoving = false; + + private _initMoving() { + this.disposeWithMe( + this._refSelectionsService.selectionMoving$.subscribe(() => { + this._isSelectionMoving = true; + }) + ); + this.disposeWithMe( + 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; + }) + ); } /** @@ -214,7 +245,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 +314,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 +380,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 +414,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 +438,7 @@ export class SheetCanvasPopManagerService extends Disposable { return null; } - if (sheetSelectionRenderService.selectionMoving && !showOnSelectionMoving) { + if (this._isSelectionMoving && (!popup.showOnSelectionMoving)) { return; } 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..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 @@ -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( + const info = this._predictingSize( position, canvasOffset, documentSkeleton, @@ -71,45 +73,62 @@ export class SheetCellEditorResizeService extends Disposable implements IRenderM scaleX, scaleY ); - const { verticalAlign, paddingData, fill } = documentLayoutObject; + 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); 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 + ); } /** @@ -127,15 +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 = documentLayoutObject.documentModel; + const documentDataModel = this._univerInstanceService.getUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY, UniverInstanceType.UNIVER_DOC); const { vertexAngle: angle } = convertTextRotation(textRotation); - const clientWidth = document.body.clientWidth; - 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 { @@ -144,7 +164,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(); @@ -167,12 +189,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 +211,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 +221,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 +252,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,14 +264,13 @@ 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, horizontalAlign)!; - const info = this._getEditorMaxSize(actualRangeWithCoord, canvasOffset); - if (!info) return; const { height: clientHeight, width: clientWidth, scaleAdjust } = info; let physicHeight = editorHeight; @@ -248,13 +278,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 +299,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 +331,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 +396,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 c26923befab9..4d9689f77825 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..5fd96fc51c02 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(); + } + }} + disableSelectionOnClick + /> + )}
); }; 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..9a27c81a8d06 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, 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'; 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,20 @@ 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); + useObservable(useMemo(() => contextService.subscribeContextValue$(FOCUSING_FX_BAR_EDITOR), [contextService])); + const isFocusFxBar = contextService.getContextValue(FOCUSING_FX_BAR_EDITOR); + const ref = useRef(null); function getPermissionIds(unitId: string, subUnitId: string): string[] { return [ @@ -107,44 +115,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 +135,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 +224,35 @@ 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..54dce75c3e68 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,94 @@ 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} + maxHeight={64} + 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 +129,7 @@ export const ThreadCommentEditor = forwardRef { onCancel?.(); setEditing(false); - setLocalComment({ text: undefined }); + editor.current?.replaceText('', true); commandService.executeCommand(SetActiveCommentOperation.id); }} > @@ -149,7 +137,7 @@ export const ThreadCommentEditor = forwardRef
@@ -200,6 +205,7 @@ export const ThreadCommentTree = (props: IThreadCommentTreeProps) => { onAddComment, onDeleteComment, onResolve, + type, } = props; const threadCommentModel = useDependency(ThreadCommentModel); const [isHover, setIsHover] = useState(false); @@ -338,6 +344,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 +378,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..abc23835d122 --- /dev/null +++ b/packages/ui/src/components/hooks/index.ts @@ -0,0 +1,21 @@ +/** + * 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 { useUpdateEffect } from './update-effect'; +export { useClickOutSide } from './useClickOutSide'; +export { useVirtualList } from './virtual-list'; diff --git a/packages/design/src/components/mentions/Mentions.tsx b/packages/ui/src/components/hooks/update-effect.ts similarity index 54% rename from packages/design/src/components/mentions/Mentions.tsx rename to packages/ui/src/components/hooks/update-effect.ts index a3ae171f08ab..b3bf45b54abb 100644 --- a/packages/design/src/components/mentions/Mentions.tsx +++ b/packages/ui/src/components/hooks/update-effect.ts @@ -14,20 +14,16 @@ * 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'; +import React, { useEffect } from 'react'; -export interface IMentionsProps extends MentionsInputProps {} - -export const Mentions = forwardRef, IMentionsProps>((props, ref) => { - return ( - - ); -}); +export const useUpdateEffect: typeof React.useEffect = (effect, deps) => { + const hasMount = React.useRef(false); + useEffect(() => { + if (hasMount.current) { + return effect(); + } else { + hasMount.current = true; + } + }, deps); +}; 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 c8cf18894080..395463311964 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..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 { @@ -70,7 +72,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; @@ -106,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 */ })); @@ -193,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 && } +
); } diff --git a/packages/watermark/src/facade/f-univer.ts b/packages/watermark/src/facade/f-univer.ts index aeea273b5448..4dc0586bf89c 100644 --- a/packages/watermark/src/facade/f-univer.ts +++ b/packages/watermark/src/facade/f-univer.ts @@ -21,7 +21,6 @@ import { IWatermarkTypeEnum, WatermarkImageBaseConfig, WatermarkService, Waterma export interface IFUniverWatermarkMixin { /** * Adds a watermark to the unit. Supports both text and image watermarks based on the specified type. - * * @param {IWatermarkTypeEnum.Text | IWatermarkTypeEnum.Image} type - The type of watermark to add. Can be either 'Text' or 'Image'. * @param {ITextWatermarkConfig | IImageWatermarkConfig} config - The configuration object for the watermark. * - If the type is 'Text', the config should follow the ITextWatermarkConfig interface. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3a71e2bad24..8533d4f60c4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1170,9 +1170,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.12 version: 0.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1221,9 +1218,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) @@ -3710,9 +3704,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'} @@ -5333,9 +5324,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: @@ -6794,6 +6782,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'} @@ -6824,6 +6816,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==} @@ -7566,6 +7562,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'} @@ -7886,9 +7890,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==} @@ -9628,12 +9629,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.1: resolution: {integrity: sha512-Ivuj6AxRDlo/H8OiEDU1mdgivxuKbwGOa5Ub6Yf+bHcu0JWioT7ttlpCWF63/gKrJBlRMB6fW9/eNOXINg9+Gg==} peerDependencies: @@ -9707,17 +9702,14 @@ packages: reflect-metadata@0.1.13: resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - reflect.getprototypeof@1.0.6: - resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - reflect.getprototypeof@1.0.9: - resolution: {integrity: sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==} + reflect.getprototypeof@1.0.6: + 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==} @@ -9966,6 +9958,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'} @@ -10232,11 +10228,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'} @@ -11284,10 +11275,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 @@ -12910,10 +12897,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 @@ -14628,6 +14611,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 @@ -14669,6 +14706,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 @@ -15665,6 +15709,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: {} @@ -16002,10 +16064,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: @@ -16324,7 +16382,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: @@ -17987,15 +18045,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.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 @@ -18107,28 +18156,26 @@ snapshots: reflect-metadata@0.1.13: {} - reflect.getprototypeof@1.0.6: + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.8 + es-abstract: 1.23.9 es-errors: 1.3.0 - get-intrinsic: 1.2.6 - globalthis: 1.0.4 - which-builtin-type: 1.1.4 + 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.9: + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - dunder-proto: 1.0.1 es-abstract: 1.23.8 es-errors: 1.3.0 get-intrinsic: 1.2.6 - gopd: 1.2.0 - which-builtin-type: 1.2.1 - - regenerator-runtime@0.13.11: {} + globalthis: 1.0.4 + which-builtin-type: 1.1.4 regenerator-runtime@0.14.1: {} @@ -18468,6 +18515,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 @@ -18751,12 +18804,6 @@ snapshots: dependencies: webpack: 5.97.1(@swc/core@1.7.5)(esbuild@0.24.2) - 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.8 @@ -19062,7 +19109,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: @@ -19572,14 +19619,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