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 fac5faa17626..048898254a21 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 @@ -62,6 +62,7 @@ export const useFormulaDescribe = (isNeed: boolean, formulaText: string, editor? paramIndexSet(-1); }); const d2 = editor.selectionChange$.pipe( + filter((e) => e.isEditing), filter((e) => e.textRanges.length === 1), map((e) => e.textRanges[0].startOffset), distinctUntilChanged() diff --git a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useHighlight.ts b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useHighlight.ts deleted file mode 100644 index bbfd194a8132..000000000000 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useHighlight.ts +++ /dev/null @@ -1,191 +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 type { ISequenceNode } from '@univerjs/engine-formula'; -import type { ISelectionStyle, ISelectionWithStyle } from '@univerjs/sheets'; -import { ColorKit, IUniverInstanceService, ThemeService, useDependency, useObservable } from '@univerjs/core'; -import { IEditorService } from '@univerjs/docs-ui'; -import { deserializeRangeWithSheet } from '@univerjs/engine-formula'; -import { IRenderManagerService } from '@univerjs/engine-render'; -import { IRefSelectionsService, setEndForRange } from '@univerjs/sheets'; -import { IDescriptionService } from '@univerjs/sheets-formula'; -import { attachRangeWithCoord, SheetSkeletonManagerService } from '@univerjs/sheets-ui'; -import { useEffect, useState } from 'react'; -import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; -import { buildTextRuns, useColor } from '../../range-selector/hooks/useHighlight'; -import { useStateRef } from './useStateRef'; - -interface IRefSelection { - refIndex: number; - themeColor: string; - token: string; -} - -/** - * @param {string} unitId - * @param {string} subUnitId 打开面板的时候传入的 sheetId - * @param {IRefSelection[]} refSelections - */ - -export function useSheetHighlight(isNeed: boolean, unitId: string, subUnitId: string, refSelections: IRefSelection[]) { - const univerInstanceService = useDependency(IUniverInstanceService); - const themeService = useDependency(ThemeService); - const refSelectionsService = useDependency(IRefSelectionsService); - const renderManagerService = useDependency(IRenderManagerService); - const render = renderManagerService.getRenderById(unitId); - const refSelectionsRenderService = render?.with(RefSelectionsRenderService); - const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - - const [ranges, rangesSet] = useState([]); - - const workbook = univerInstanceService.getUnit(unitId); - const worksheet = workbook?.getSheetBySheetId(subUnitId); - - const activeSheet = useObservable(workbook?.activeSheet$); - const contextRef = useStateRef({ activeSheet, sheetName: worksheet?.getName() }); - - useEffect(() => { - const workbook = univerInstanceService.getUnit(unitId); - const selectionWithStyle: ISelectionWithStyle[] = []; - const { activeSheet, sheetName } = contextRef.current; - if (!workbook || !activeSheet || !isNeed) { - rangesSet(selectionWithStyle); - return; - } - - for (let i = 0, len = refSelections.length; i < len; i++) { - const refSelection = refSelections[i]; - const { themeColor, token, refIndex } = refSelection; - - const unitRangeName = deserializeRangeWithSheet(token); - if (!unitRangeName.unitId) { - unitRangeName.unitId = unitId; - } - if (!unitRangeName.sheetName) { - unitRangeName.sheetName = sheetName || ''; - } - const { unitId: refUnitId, sheetName: rangeSheetName, range: rawRange } = unitRangeName; - if (unitId !== refUnitId) { - continue; - } - - if (rangeSheetName !== activeSheet.getName()) { - continue; - } - - const range = setEndForRange(rawRange, activeSheet.getRowCount(), activeSheet.getColumnCount()); - - selectionWithStyle.push({ - range, - primary: null, - style: getFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()), - }); - } - rangesSet(selectionWithStyle); - }, [unitId, subUnitId, refSelections, isNeed]); - - useEffect(() => { - const skeleton = sheetSkeletonManagerService?.getCurrentSkeleton(); - if (skeleton && isNeed) { - const allControls = refSelectionsRenderService?.getSelectionControls() || []; - if (allControls.length === ranges.length) { - allControls.forEach((control, index) => { - const selection = ranges[index]; - control.updateRange(attachRangeWithCoord(skeleton, selection.range), null); - control.updateStyle(selection.style!); - }); - } else { - refSelectionsService.setSelections(ranges); - } - } - }, [ranges, isNeed]); -} - -export function useDocHight(editorId: string, sequenceNodes: (string | ISequenceNode)[]) { - const editorService = useDependency(IEditorService); - const descriptionService = useDependency(IDescriptionService); - const colorMap = useColor(); - const [ranges, rangesSet] = useState([]); - - useEffect(() => { - const editor = editorService.getEditor(editorId); - if (!editor) { - return; - } - const data = editor.getDocumentData(); - if (!data) { - return; - } - const body = data.body; - if (!body) { - return; - } - const cloneBody = { dataStream: '', ...data.body }; - - if (sequenceNodes == null || sequenceNodes.length === 0) { - cloneBody.textRuns = []; - cloneBody.dataStream = data.body?.dataStream || '\r\n'; - rangesSet([]); - } else { - const { textRuns, refSelections } = buildTextRuns(descriptionService, colorMap, sequenceNodes); - // 公式前面需要加1 - textRuns.forEach((e) => { - e.ed++; - e.st++; - }); - cloneBody.textRuns = textRuns; - const text = sequenceNodes.reduce((pre, cur) => { - if (typeof cur === 'string') { - return `${pre}${cur}`; - } - return `${pre}${cur.token}`; - }, ''); - cloneBody.dataStream = `=${text}\r\n`; - rangesSet(refSelections); - } - // Switching between uppercase and lowercase will trigger a reflow, causing the cursor to be misplaced. Let's refresh the cursor position here. - const selections = editor.getSelectionRanges(); - // After 'buildTextRuns' , the content changes, most of it is deleted, and the cursor position needs to be corrected - const maxOffset = cloneBody.dataStream.length - 2; - selections.forEach((selection) => { - selection.startOffset = Math.max(0, Math.min(selection.startOffset, maxOffset)); - selection.endOffset = Math.max(0, Math.min(selection.endOffset, maxOffset)); - }); - const cloneData = { ...data, body: cloneBody }; - editor.setDocumentData(cloneData, selections); - }, [editorId, sequenceNodes, colorMap]); - - return ranges; -} - -function getFormulaRefSelectionStyle(themeService: ThemeService, refColor: string, id: string): ISelectionStyle { - const style = themeService.getCurrentTheme(); - const fill = new ColorKit(refColor).setAlpha(0.05).toRgbString(); - return { - id, - strokeWidth: 1, - stroke: refColor, - fill, - widgets: { tl: true, tc: true, tr: true, ml: true, mr: true, bl: true, bc: true, br: true }, - widgetSize: 6, - widgetStrokeWidth: 1, - widgetStroke: style.colorWhite, - hasAutoFill: false, - hasRowHeader: false, - hasColumnHeader: false, - }; -} 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 1ad7b7bcf2b3..d94576fc6940 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 @@ -154,7 +154,7 @@ export const useSelectionAdd = (unitId: string, sequenceNodes: INode[], editor?: setIsAddSelection(false); return; } - const nextNode = sequenceNodes[index + 1]; + const nextNode = sequenceNodes[index + 1] || ''; const nextContent = getContent(nextNode) as any; const content = getContent(currentNode) as any; if (currentNodeAddToken.includes(content) && (!nextNode || nextNodeAddToken.includes(nextContent))) { 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 1d97906f5a79..ef145f66d39e 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSheetSelectionChange.ts +++ b/packages/sheets-formula-ui/src/views/formula-editor/hooks/useSheetSelectionChange.ts @@ -26,7 +26,7 @@ import { deserializeRangeWithSheet, sequenceNodeType, serializeRange, serializeR import { IRenderManagerService } from '@univerjs/engine-render'; import { useEffect, useMemo, useRef } from 'react'; import { merge } from 'rxjs'; -import { debounceTime, distinctUntilChanged, map, throttleTime } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; import { findIndexFromSequenceNodes } from '../../range-selector/utils/findIndexFromSequenceNodes'; import { getOffsetFromSequenceNodes } from '../../range-selector/utils/getOffsetFromSequenceNodes'; @@ -43,7 +43,7 @@ export const useSheetSelectionChange = ( sequenceNodes: INode[], isSupportAcrossSheet: boolean, editor?: Editor, - handleRangeChange: ((refString: string, offset: number) => void) = noop) => { + handleRangeChange: ((refString: string, offset: number, isEnd: boolean) => void) = noop) => { const renderManagerService = useDependency(IRenderManagerService); const univerInstanceService = useDependency(IUniverInstanceService); @@ -62,6 +62,8 @@ export const useSheetSelectionChange = ( const isScalingRef = useRef(false); + const scalingOptionRef = useRef<{ result: string; offset: number }>(); + useEffect(() => { if (refSelectionsRenderService && isNeed) { let isFirst = true; @@ -97,7 +99,7 @@ export const useSheetSelectionChange = ( sequenceNodes.push({ token: refRanges[0], nodeType: sequenceNodeType.REFERENCE } as any); const newSequenceNodes = [...sequenceNodes, ...lastNodes]; const result = sequenceNodeToText(newSequenceNodes); - handleRangeChange(result, getOffsetFromSequenceNodes(sequenceNodes)); + handleRangeChange(result, getOffsetFromSequenceNodes(sequenceNodes), true); } else { const range = selections[selections.length - 1]; const rangeSheetId = range.rangeWithCoord.sheetId ?? subUnitId; @@ -110,7 +112,7 @@ export const useSheetSelectionChange = ( const refRanges = unitRangesToText([unitRangeName], isSupportAcrossSheet && isAcrossSheet); sequenceNodes.unshift({ token: refRanges[0], nodeType: sequenceNodeType.REFERENCE } as any); const result = sequenceNodeToText(sequenceNodes); - handleRangeChange(result, refRanges[0].length); + handleRangeChange(result, refRanges[0].length, true); } } else { // 更新全部的 ref Selection @@ -163,12 +165,17 @@ export const useSheetSelectionChange = ( const preNode = sequenceNodes[sequenceNodes.length - 1]; const isPreNodeRef = preNode && (typeof preNode === 'string' ? false : preNode.nodeType === sequenceNodeType.REFERENCE); const result = `${currentText}${theLastList.length && isPreNodeRef ? ',' : ''}${theLastList.join(',')}`; - handleRangeChange(result, result.length); + handleRangeChange(result, result.length, true); } }; 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) => { @@ -242,14 +249,14 @@ export const useSheetSelectionChange = ( return node; }); const result = sequenceNodeToText(newSequenceNodes); - handleRangeChange(result, offset || -1); + handleRangeChange(result, -1, false); + scalingOptionRef.current = { result, offset }; }; const reListen = () => { disposableCollection.dispose(); const controls = refSelectionsRenderService.getSelectionControls(); controls.forEach((control, index) => { disposableCollection.add(merge(control.selectionMoving$, control.selectionScaling$).pipe( - throttleTime(30), map((e) => { return serializeRange(e); }), 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 fa77c6d9fd0c..803f5a9ba75c 100644 --- a/packages/sheets-formula-ui/src/views/formula-editor/index.tsx +++ b/packages/sheets-formula-ui/src/views/formula-editor/index.tsx @@ -18,14 +18,16 @@ import type { IDisposable } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { ReactNode } from 'react'; import { createInternalEditorID, generateRandomId, useDependency } from '@univerjs/core'; -import { IEditorService } from '@univerjs/docs-ui'; +import { DocBackScrollRenderController, IEditorService } from '@univerjs/docs-ui'; import { operatorToken } from '@univerjs/engine-formula'; import { EMBEDDING_FORMULA_EDITOR } from '@univerjs/sheets-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 { useLeftAndRightArrow } from '../range-selector/hooks/useLeftAndRightArrow'; import { useRefactorEffect } from '../range-selector/hooks/useRefactorEffect'; import { useRefocus } from '../range-selector/hooks/useRefocus'; @@ -35,7 +37,6 @@ import { useSwitchSheet } from '../range-selector/hooks/useSwitchSheet'; import { HelpFunction } from './help-function/HelpFunction'; import { useFormulaDescribe } from './hooks/useFormulaDescribe'; import { useFormulaSearch } from './hooks/useFormulaSearch'; -import { useDocHight, useSheetHighlight } from './hooks/useHighlight'; import { useSheetSelectionChange } from './hooks/useSheetSelectionChange'; import { useVerify } from './hooks/useVerify'; import styles from './index.module.less'; @@ -98,13 +99,25 @@ export function FormulaEditor(props: IFormulaEditorProps) { const editorId = useMemo(() => createInternalEditorID(`${EMBEDDING_FORMULA_EDITOR}-${generateRandomId(4)}`), []); const isError = useMemo(() => errorText !== undefined, [errorText]); - const { sequenceNodes, sequenceNodesSet } = useFormulaToken(formulaWithoutEqualSymbol); + const getFormulaToken = useFormulaToken(); + const sequenceNodes = useMemo(() => getFormulaToken(formulaWithoutEqualSymbol), [formulaWithoutEqualSymbol]); const needEmit = useEmitChange(sequenceNodes, (text: string) => { onChange(`=${text}`); }, editor); - const refSelections = useDocHight(editorId, sequenceNodes); + const highlightDoc = useDocHight('='); + const highlightSheet = useSheetHighlight(unitId); + const highligh = (text: string, isNeedResetSelection: boolean = true) => { + if (!editor) { + 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); @@ -128,24 +141,33 @@ export function FormulaEditor(props: IFormulaEditorProps) { } }, [_isFocus, focus]); - const handleSelectionChange = (refString: string, offset: number) => { + const { checkScrollBar } = useResize(editor); + useRefactorEffect(isFocus, unitId); + useLeftAndRightArrow(isFocus, editor); + + const handleSelectionChange = (refString: string, offset: number, isEnd: boolean) => { const result = `=${refString}`; needEmit(); formulaTextSet(result); - if (offset > -1) { - setTimeout(() => { - editor?.setSelectionRanges([{ startOffset: offset + 1, endOffset: offset + 1 }]); - }, 30); + 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(); } }; - - useSheetHighlight(isFocus, unitId, subUnitId, refSelections); - useResize(editor); - useRefactorEffect(isFocus, unitId); - useLeftAndRightArrow(isFocus, editor); useSheetSelectionChange(isFocus, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, editor, handleSelectionChange); + useRefocus(); - useSwitchSheet(isFocus, unitId, isSupportAcrossSheet, isFocusSet, onBlur, () => sequenceNodesSet((pre) => [...pre])); + useSwitchSheet(isFocus, unitId, isSupportAcrossSheet, isFocusSet, onBlur, noop); const { searchList, searchText, handlerFormulaReplace, reset: resetFormulaSearch } = useFormulaSearch(isFocus, sequenceNodes, editor); const { functionInfo, paramIndex, reset } = useFormulaDescribe(isFocus, formulaText, editor); @@ -156,6 +178,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { const text = (e.data.body?.dataStream ?? '').replaceAll(/\n|\r/g, ''); needEmit(); formulaTextSet(text); + highligh(getFormulaText(text), false); }); return () => { d.unsubscribe(); @@ -163,6 +186,8 @@ export function FormulaEditor(props: IFormulaEditorProps) { } }, [editor]); + useFirstHighlightDoc(formulaWithoutEqualSymbol, '=', isFocus, highlightDoc, highlightSheet, editor); + useLayoutEffect(() => { let dispose: IDisposable; if (formulaEditorContainerRef.current) { @@ -201,6 +226,7 @@ export function FormulaEditor(props: IFormulaEditorProps) { } resetFormulaSearch(); focus(); + highligh(res.text); } }; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useFirstHighlightDoc.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFirstHighlightDoc.ts new file mode 100644 index 000000000000..6b6e9982f164 --- /dev/null +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useFirstHighlightDoc.ts @@ -0,0 +1,57 @@ +/** + * 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 { useDocHight, useSheetHighlight } from './useHighlight'; +import { useEffect, useRef } from 'react'; +import { useFormulaToken } from './useFormulaToken'; + +export const useFirstHighlightDoc = ( + text: string, + leadingCharacter: string, + isNeed: boolean, + highlightDoc: ReturnType, + highlightSheet: ReturnType, + editor?: Editor +) => { + const getFormulaToken = useFormulaToken(); + const isInit = useRef(true); + useEffect(() => { + if (editor) { + if (isInit.current) { + const sequenceNodes = getFormulaToken(text); + if (sequenceNodes.length) { + const ranges = highlightDoc(editor, sequenceNodes); + if (isNeed) { + highlightSheet(ranges); + } + } else { + const data = editor.getDocumentData(); + const dataStream = data.body?.dataStream ?? `${leadingCharacter}\r\n`; + const cloneBody = { dataStream, ...data.body }; + editor.setDocumentData({ ...data, body: cloneBody }); + } + isInit.current = false; + } else { + if (isNeed) { + const sequenceNodes = getFormulaToken(text); + const ranges = highlightDoc(editor, sequenceNodes); + highlightSheet(ranges); + } + } + } + }, [editor, isNeed]); +}; 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 a2a18ad80976..6d90ce9e0b87 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,20 +17,10 @@ import type { ISequenceNode } from '@univerjs/engine-formula'; import { useDependency } from '@univerjs/core'; import { LexerTreeBuilder } from '@univerjs/engine-formula'; -import { useEffect, useState } from 'react'; export type INode = (string | ISequenceNode); -export const useFormulaToken = (text: string) => { +export const useFormulaToken = () => { const lexerTreeBuilder = useDependency(LexerTreeBuilder); - - const [sequenceNodes, sequenceNodesSet] = useState([]); - - useEffect(() => { - sequenceNodesSet(lexerTreeBuilder.sequenceNodesBuilder(text) ?? []); - }, [text]); - - return { - sequenceNodes, - sequenceNodesSet, - }; + const getFormulaToken = (text: string) => lexerTreeBuilder.sequenceNodesBuilder(text) || []; + 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 fc6390cf38c4..5381866ce262 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 @@ -15,16 +15,17 @@ */ import type { ITextRun, Workbook } from '@univerjs/core'; +import type { Editor } from '@univerjs/docs-ui'; import type { ISequenceNode } from '@univerjs/engine-formula'; import type { ISelectionStyle, ISelectionWithStyle } from '@univerjs/sheets'; +import type { INode } from './useFormulaToken'; import { ColorKit, IUniverInstanceService, ThemeService, useDependency } from '@univerjs/core'; -import { IEditorService } 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 { attachRangeWithCoord, SheetSkeletonManagerService } from '@univerjs/sheets-ui'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service'; interface IRefSelection { @@ -39,7 +40,7 @@ interface IRefSelection { * @param {IRefSelection[]} refSelections */ -export function useSheetHighlight(isNeed: boolean, unitId: string, subUnitId: string, refSelections: IRefSelection[]) { +export function useSheetHighlight(unitId: string) { const univerInstanceService = useDependency(IUniverInstanceService); const themeService = useDependency(ThemeService); const refSelectionsService = useDependency(IRefSelectionsService); @@ -48,14 +49,12 @@ export function useSheetHighlight(isNeed: boolean, unitId: string, subUnitId: st const refSelectionsRenderService = render?.with(RefSelectionsRenderService); const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService); - const [ranges, rangesSet] = useState([]); - - useEffect(() => { + const highlightSheet = (refSelections: IRefSelection[]) => { const workbook = univerInstanceService.getUnit(unitId); const worksheet = workbook?.getActiveSheet(); const selectionWithStyle: ISelectionWithStyle[] = []; - if (!workbook || !worksheet || !isNeed) { - rangesSet(selectionWithStyle); + if (!workbook || !worksheet) { + refSelectionsService.setSelections(selectionWithStyle); return; } const currentSheetId = worksheet?.getSheetId(); @@ -85,52 +84,52 @@ export function useSheetHighlight(isNeed: boolean, unitId: string, subUnitId: st style: getFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()), }); } - rangesSet(selectionWithStyle); - }, [unitId, subUnitId, refSelections, isNeed]); - - useEffect(() => { const skeleton = sheetSkeletonManagerService?.getCurrentSkeleton(); - if (skeleton && isNeed) { + + if (skeleton) { const allControls = refSelectionsRenderService?.getSelectionControls() || []; - if (allControls.length === ranges.length) { + if (allControls.length === selectionWithStyle.length) { allControls.forEach((control, index) => { - const selection = ranges[index]; + const selection = selectionWithStyle[index]; control.updateRange(attachRangeWithCoord(skeleton, selection.range), null); control.updateStyle(selection.style!); }); } else { - refSelectionsService.setSelections(ranges); + refSelectionsService.setSelections(selectionWithStyle); } } - }, [ranges, isNeed]); + }; + return highlightSheet; } -export function useDocHight(editorId: string, sequenceNodes: (string | ISequenceNode)[]) { - const editorService = useDependency(IEditorService); +export function useDocHight(_leadingCharacter: string = '') { const descriptionService = useDependency(IDescriptionService); const colorMap = useColor(); - const [ranges, rangesSet] = useState([]); + const leadingCharacterLength = useMemo(() => _leadingCharacter.length, [_leadingCharacter]); - useEffect(() => { - const editor = editorService.getEditor(editorId); - if (!editor) { - return; - } + const highlightDoc = (editor: Editor, sequenceNodes: INode[], isNeedResetSelection = true) => { const data = editor.getDocumentData(); if (!data) { - return; + return []; } const body = data.body; if (!body) { - return; + return []; } const cloneBody = { dataStream: '', ...data.body }; if (sequenceNodes == null || sequenceNodes.length === 0) { cloneBody.textRuns = []; - cloneBody.dataStream = '\r\n'; - rangesSet([]); + const cloneData = { ...data, body: cloneBody }; + editor.setDocumentData(cloneData); + return []; } else { const { textRuns, refSelections } = buildTextRuns(descriptionService, colorMap, sequenceNodes); + if (leadingCharacterLength) { + textRuns.forEach((e) => { + e.ed = e.ed + leadingCharacterLength; + e.st = e.st + leadingCharacterLength; + }); + } cloneBody.textRuns = textRuns; const text = sequenceNodes.reduce((pre, cur) => { if (typeof cur === 'string') { @@ -138,23 +137,25 @@ export function useDocHight(editorId: string, sequenceNodes: (string | ISequence } return `${pre}${cur.token}`; }, ''); - cloneBody.dataStream = `${text}\r\n`; - rangesSet(refSelections); - } - // Switching between uppercase and lowercase will trigger a reflow, causing the cursor to be misplaced. Let's refresh the cursor position here. - const selections = editor.getSelectionRanges(); - // After 'buildTextRuns' , the content changes, most of it is deleted, and the cursor position needs to be corrected - const maxOffset = cloneBody.dataStream.length - 2; - selections.forEach((selection) => { - selection.startOffset = Math.max(0, Math.min(selection.startOffset, maxOffset)); - selection.endOffset = Math.max(0, Math.min(selection.endOffset, maxOffset)); - }); - const cloneData = { ...data, body: cloneBody }; - - editor.setDocumentData(cloneData, selections); - }, [editorId, sequenceNodes, colorMap]); + cloneBody.dataStream = `${_leadingCharacter}${text}\r\n`; + let selections; + if (isNeedResetSelection) { + // Switching between uppercase and lowercase will trigger a reflow, causing the cursor to be misplaced. Let's refresh the cursor position here. + selections = editor.getSelectionRanges(); + // After 'buildTextRuns' , the content changes, most of it is deleted, and the cursor position needs to be corrected + const maxOffset = cloneBody.dataStream.length - 2 + leadingCharacterLength; + selections.forEach((selection) => { + selection.startOffset = Math.max(0, Math.min(selection.startOffset, maxOffset)); + selection.endOffset = Math.max(0, Math.min(selection.endOffset, maxOffset)); + }); + } - return ranges; + const cloneData = { ...data, body: cloneBody }; + editor.setDocumentData(cloneData, selections); + return refSelections; + } + }; + return highlightDoc; } export function useColor() { 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 d8f108037c1c..f198231d4c9b 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 @@ -38,7 +38,7 @@ export const useSheetSelectionChange = (isNeed: boolean, sequenceNodes: INode[], isSupportAcrossSheet: boolean, isOnlyOneRange: boolean, - handleRangeChange: (refString: string, offset: number) => void) => { + handleRangeChange: (refString: string, offset: number, isEnd: boolean) => void) => { const renderManagerService = useDependency(IRenderManagerService); const univerInstanceService = useDependency(IUniverInstanceService); const isScalingRef = useRef(false); @@ -58,10 +58,12 @@ export const useSheetSelectionChange = (isNeed: boolean, }, [sequenceNodes]); oldFilterReferenceNodes.current = filterReferenceNodes; + const scalingOptionRef = useRef<{ result: string; offset: number }>(); + useEffect(() => { if (isNeed && refSelectionsRenderService) { let isFirst = true; - const handleSelectionsChange = (selections: ISelectionWithCoordAndStyle[]) => { + const handleSelectionsChange = (selections: ISelectionWithCoordAndStyle[], isEnd: boolean) => { if (isFirst || isScalingRef.current) { isFirst = false; return; @@ -125,15 +127,20 @@ export const useSheetSelectionChange = (isNeed: boolean, const thePre = sequenceNodeToText(newSequenceNodes); const result = `${thePre}${(thePre && theLast) ? matchToken.COMMA : ''}${theLast}`; const isScaling = isScalingRef.current; - handleRangeChange(result, isScaling ? -1 : result.length); + handleRangeChange(result, isScaling ? -1 : result.length, isEnd); }; const d1 = refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => { - handleSelectionsChange(selections); + handleSelectionsChange(selections, true); isScalingRef.current = false; + if (scalingOptionRef.current) { + const { result, offset } = scalingOptionRef.current; + handleRangeChange(result, offset, true); + scalingOptionRef.current = undefined; + } }); const d2 = refSelectionsRenderService.selectionMoving$.pipe(throttleTime(50)).subscribe((selections) => { - handleSelectionsChange(selections); + handleSelectionsChange(selections, false); }); return () => { @@ -141,7 +148,7 @@ export const useSheetSelectionChange = (isNeed: boolean, d2.unsubscribe(); }; } - }, [isNeed, filterReferenceNodes, refSelectionsRenderService, isSupportAcrossSheet, isOnlyOneRange]); + }, [isNeed, filterReferenceNodes, refSelectionsRenderService, isSupportAcrossSheet, isOnlyOneRange, handleRangeChange]); useEffect(() => { if (isNeed && refSelectionsRenderService) { @@ -159,9 +166,6 @@ export const useSheetSelectionChange = (isNeed: boolean, } return node; } else if (node.nodeType === sequenceNodeType.REFERENCE) { - if (!isFinish) { - offset += node.token.length; - } const unitRange = deserializeRangeWithSheet(token); unitRange.unitId = unitRange.unitId === '' ? unitId : unitRange.unitId; unitRange.sheetName = unitRange.sheetName === '' ? currentSheetName : unitRange.sheetName; @@ -174,15 +178,20 @@ export const useSheetSelectionChange = (isNeed: boolean, cloneNode.token = serializeRange(unitRange.range); } currentIndex++; + offset += cloneNode.token.length; return cloneNode; } currentIndex++; + if (!isFinish) { + offset += node.token.length; + } return node; } return node; }); const result = sequenceNodeToText(newSequenceNodes); - handleRangeChange(result, offset || -1); + scalingOptionRef.current = { result, offset }; + handleRangeChange(result, -1, false); }; let time = 0 as any; const dispose = refSelectionsRenderService.selectionMoveEnd$.subscribe(() => { @@ -191,7 +200,6 @@ export const useSheetSelectionChange = (isNeed: boolean, const controls = refSelectionsRenderService.getSelectionControls(); controls.forEach((control, index) => { disposableCollection.add(merge(control.selectionMoving$, control.selectionScaling$).pipe( - throttleTime(30), map((e) => { return serializeRange(e); }), @@ -210,5 +218,5 @@ export const useSheetSelectionChange = (isNeed: boolean, clearTimeout(time); }; } - }, [isNeed, refSelectionsRenderService, filterReferenceNodes]); + }, [isNeed, refSelectionsRenderService, filterReferenceNodes, handleRangeChange]); }; diff --git a/packages/sheets-formula-ui/src/views/range-selector/hooks/useVerify.ts b/packages/sheets-formula-ui/src/views/range-selector/hooks/useVerify.ts index 94728d1db062..e1b5575aeddf 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/hooks/useVerify.ts +++ b/packages/sheets-formula-ui/src/views/range-selector/hooks/useVerify.ts @@ -15,12 +15,12 @@ */ import type { IRangeSelectorProps } from '../'; -import type { useFormulaToken } from './useFormulaToken'; +import type { INode } from './useFormulaToken'; import { useEffect, useRef } from 'react'; import { sequenceNodeToText } from '../utils/sequenceNodeToText'; import { verifyRange } from '../utils/verifyRange'; -export const useVerify = (isNeed: boolean, onVerify: IRangeSelectorProps['onVerify'], sequenceNodes: ReturnType['sequenceNodes']) => { +export const useVerify = (isNeed: boolean, onVerify: IRangeSelectorProps['onVerify'], sequenceNodes: INode[]) => { const isInitRender = useRef(true); // No validation is performed during the initialization phase. 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 3c3f61612772..8d5e4feb989e 100644 --- a/packages/sheets-formula-ui/src/views/range-selector/index.tsx +++ b/packages/sheets-formula-ui/src/views/range-selector/index.tsx @@ -17,7 +17,7 @@ import type { IDisposable, IUnitRangeName } from '@univerjs/core'; import type { Editor } from '@univerjs/docs-ui'; import type { ReactNode } from 'react'; -import { createInternalEditorID, debounce, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, generateRandomId, ICommandService, LocaleService, useDependency } from '@univerjs/core'; +import { createInternalEditorID, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, generateRandomId, ICommandService, LocaleService, useDependency } from '@univerjs/core'; import { Button, Dialog, Input, Tooltip } from '@univerjs/design'; import { DocBackScrollRenderController, IEditorService } from '@univerjs/docs-ui'; import { deserializeRangeWithSheet, LexerTreeBuilder, matchToken, sequenceNodeType } from '@univerjs/engine-formula'; @@ -28,11 +28,12 @@ import { RANGE_SELECTOR_SYMBOLS, SetCellEditVisibleOperation } from '@univerjs/s import cl from 'clsx'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { filter, noop } from 'rxjs'; +import { filter, 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'; import { buildTextRuns, useColor, useDocHight, useSheetHighlight } from './hooks/useHighlight'; @@ -108,7 +109,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const editorId = useMemo(() => createInternalEditorID(`${RANGE_SELECTOR_SYMBOLS}-${generateRandomId(4)}`), []); const [editor, editorSet] = useState(); const containerRef = useRef(null); - + const isNeed = useMemo(() => !rangeDialogVisible && isFocus, [rangeDialogVisible, isFocus]); const [rangeString, rangeStringSet] = useState(() => { if (typeof initValue === 'string') { return initValue; @@ -135,7 +136,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const resetSelection = useResetSelection(!rangeDialogVisible && isFocus); - const handleInputDebounce = useMemo(() => debounce((text: string) => { + const handleInput = useMemo(() => (text: string) => { const nodes = lexerTreeBuilder.sequenceNodesBuilder(text); if (nodes) { const verify = verifyRange(nodes); @@ -144,13 +145,17 @@ export function RangeSelector(props: IRangeSelectorProps) { 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 = ''; } - node.token = unitRangesToText([unitRange], isSupportAcrossSheet)[0]; + cloneNode.token = unitRangesToText([unitRange], isSupportAcrossSheet)[0]; + return cloneNode; } return node; }); @@ -160,7 +165,7 @@ export function RangeSelector(props: IRangeSelectorProps) { } else { rangeStringSet(''); } - }, 30), [isSupportAcrossSheet]); + }, [isSupportAcrossSheet]); const focus = useFocus(editor); @@ -180,58 +185,75 @@ export function RangeSelector(props: IRangeSelectorProps) { } else { resetSelection(); isFocusSet(_isFocus); + editor?.blur(); } }, [_isFocus, focus]); const { checkScrollBar } = useResize(editor); + 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) { + return; + } + const sequenceNodes = getFormulaToken(text); + const ranges = highlightDoc(editor, sequenceNodes, isNeedResetSelection); + highlightSheet(ranges); + }; - const { sequenceNodes, sequenceNodesSet } = useFormulaToken(rangeString); - const sheetHighlightRanges = useDocHight(editorId, sequenceNodes); - - const needEmit = useEmitChange(sequenceNodes, handleInputDebounce, editor); + const needEmit = useEmitChange(sequenceNodes, handleInput, editor); const handleSheetSelectionChange = useMemo(() => { - return (text: string, offset: number) => { + return (text: string, offset: number, isEnd: boolean) => { + highligh(text); rangeStringSet(text); needEmit(); - 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); + 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(); } - checkScrollBar(); }; }, [editor]); - useSheetHighlight(!rangeDialogVisible && isFocus, unitId, subUnitId, sheetHighlightRanges); - - useSheetSelectionChange(!rangeDialogVisible && isFocus, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); + useSheetSelectionChange(isNeed, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); - useRefactorEffect(!rangeDialogVisible && isFocus, unitId); + useRefactorEffect(isNeed, unitId); useOnlyOneRange(unitId, isOnlyOneRange); useEditorInput(unitId, rangeString, editor); - useVerify(!rangeDialogVisible && isFocus, onVerify, sequenceNodes); + useVerify(isNeed, onVerify, sequenceNodes); - useLeftAndRightArrow(!rangeDialogVisible && isFocus, editor); + useLeftAndRightArrow(isNeed, editor); useRefocus(); - useSwitchSheet(!rangeDialogVisible && isFocus, unitId, isSupportAcrossSheet, isFocusSet, onBlur, () => sequenceNodesSet((pre) => [...pre])); + useSwitchSheet(isNeed, unitId, isSupportAcrossSheet, isFocusSet, onBlur, () => { + if (isNeed) { + highligh(rangeString); + } + }); useEffect(() => { if (editor) { - const dispose = editor.input$.subscribe((e) => { + const dispose = editor.input$.pipe(throttleTime(100)).subscribe((e) => { const text = (e.data.body?.dataStream ?? '').replaceAll(/\n|\r/g, '').replaceAll(/,{2,}/g, ',').replaceAll(/(^,)/g, ''); - needEmit(); + highligh(text, false); rangeStringSet(text); + needEmit(); }); return () => { dispose.unsubscribe(); @@ -274,6 +296,8 @@ export function RangeSelector(props: IRangeSelectorProps) { }; }, []); + useFirstHighlightDoc(rangeString, '', isFocus, highlightDoc, highlightSheet, editor); + const handleClick = () => { // 在进行多个 input 切换的时候,失焦必须快于获得焦点. // 即使失焦是 mousedown 事件, @@ -288,6 +312,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const handleConfirm = (ranges: IUnitRangeName[]) => { const text = unitRangesToText(ranges, isSupportAcrossSheet).join(matchToken.COMMA); + highligh(text); needEmit(); rangeStringSet(text); rangeDialogVisibleSet(false); @@ -300,6 +325,7 @@ export function RangeSelector(props: IRangeSelectorProps) { const handleClose = () => { rangeDialogVisibleSet(false); onRangeSelectorDialogVisibleChange(false); + setTimeout(focus, 30); }; const handleOpenModal = () => { @@ -386,13 +412,13 @@ function RangeSelectorDialog(props: { const colorMap = useColor(); const rangeText = useMemo(() => ranges.join(matchToken.COMMA), [ranges]); - const { sequenceNodes, sequenceNodesSet } = useFormulaToken(rangeText); + const getFormulaToken = useFormulaToken(); + const sequenceNodes = useMemo(() => getFormulaToken(rangeText), [rangeText]); const refSelections = useMemo(() => buildTextRuns(descriptionService, colorMap, sequenceNodes).refSelections, [sequenceNodes]); const handleClose = () => { - // remove - sequenceNodesSet([]); + rangesSet([]); setTimeout(() => { _handleClose(); }, 30); @@ -445,11 +471,15 @@ function RangeSelectorDialog(props: { } }, [focusIndex, isOnlyOneRange]); - useSheetHighlight(visible, unitId, subUnitId, refSelections); + const highlightSheet = useSheetHighlight(unitId); useSheetSelectionChange(focusIndex >= 0, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange); useRefactorEffect(focusIndex >= 0, unitId); useOnlyOneRange(unitId, isOnlyOneRange); - useSwitchSheet(focusIndex >= 0, unitId, isSupportAcrossSheet, noop, noop, () => sequenceNodesSet((pre) => [...pre])); + useSwitchSheet(focusIndex >= 0, unitId, isSupportAcrossSheet, noop, noop, () => highlightSheet(refSelections)); + + useEffect(() => { + highlightSheet(refSelections); + }, [refSelections]); // 如果只有一个空 range,那么默认自动添加 range useEffect(() => {