mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
refactor(sheets-ui): cell editor & formula editor (#4384)
Co-authored-by: lumixraku <lumixraku@gmail.com> Co-authored-by: GitHub Actions <actions@github.com> Co-authored-by: jocs <ransixi@gmail.com>
This commit is contained in:
co-authored by
lumixraku
GitHub Actions
jocs
parent
e53eb2c75f
commit
52b2300a1d
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
@@ -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<Workbook>(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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}));
|
||||
|
||||
@@ -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<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (workbook == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
|
||||
const sheetId = workbook.getActiveSheet()?.getSheetId();
|
||||
|
||||
const [readonly, setReadonly] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextEditor
|
||||
id={createInternalEditorID('test-editor-1')}
|
||||
placeholder="please input value"
|
||||
openForSheetUnitId={unitId}
|
||||
openForSheetSubUnitId={sheetId}
|
||||
isReadonly={readonly}
|
||||
style={editorStyle}
|
||||
canvasStyle={{ fontSize: 10 }}
|
||||
value="I found one cent on the roadside."
|
||||
/>
|
||||
<br />
|
||||
<TextEditor
|
||||
id={createInternalEditorID('test-editor-2')}
|
||||
placeholder="please input value"
|
||||
openForSheetUnitId={unitId}
|
||||
openForSheetSubUnitId={sheetId}
|
||||
isReadonly={readonly}
|
||||
onlyInputFormula
|
||||
style={editorStyle}
|
||||
canvasStyle={{ fontSize: 10 }}
|
||||
/>
|
||||
<br />
|
||||
<TextEditor
|
||||
id={createInternalEditorID('test-editor-3')}
|
||||
placeholder="please input value"
|
||||
openForSheetUnitId={unitId}
|
||||
openForSheetSubUnitId={sheetId}
|
||||
isReadonly={readonly}
|
||||
onlyInputRange
|
||||
style={editorStyle}
|
||||
canvasStyle={{ fontSize: 10 }}
|
||||
/>
|
||||
<br />
|
||||
<TextEditor
|
||||
id={createInternalEditorID('test-editor-4')}
|
||||
placeholder="please input value"
|
||||
openForSheetUnitId={unitId}
|
||||
openForSheetSubUnitId={sheetId}
|
||||
isReadonly={readonly}
|
||||
isSingle={false}
|
||||
onlyInputContent
|
||||
style={{ ...editorStyle, height: '140px' }}
|
||||
canvasStyle={{ fontSize: 14 }}
|
||||
/>
|
||||
<br />
|
||||
<DocRangeSelector
|
||||
placeholder="please input value"
|
||||
id={createInternalEditorID('test-rangeSelector-1')}
|
||||
width={280}
|
||||
openForSheetUnitId={unitId}
|
||||
isReadonly={readonly}
|
||||
openForSheetSubUnitId={sheetId}
|
||||
/>
|
||||
<br />
|
||||
<DocRangeSelector
|
||||
placeholder="please input value"
|
||||
value="I am a wolf man"
|
||||
id={createInternalEditorID('test-rangeSelector-2')}
|
||||
isSingleChoice
|
||||
isReadonly={readonly}
|
||||
openForSheetUnitId={unitId}
|
||||
openForSheetSubUnitId={sheetId}
|
||||
/>
|
||||
<br />
|
||||
<Input placeholder="please input value" allowClear />
|
||||
<br />
|
||||
<button type="button" onClick={() => setReadonly(!readonly)}>
|
||||
{readonly === true ? 'enable' : 'disable'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className={styles.docUiFormulaPopup} onMouseEnter={() => onHovered(true)} onMouseLeave={() => onHovered(false)}>
|
||||
<TextEditor
|
||||
{/* <TextEditor
|
||||
id={UNI_FORMULA_EDITOR_ID}
|
||||
className={clsx(styles.docUiFormulaPopupEditor, focused && styles.docUiFormulaPopupEditorActivated)}
|
||||
placeholder={localeService.t('uni-formula.popup.placeholder')}
|
||||
@@ -138,7 +138,7 @@ function DocFormula(props: { popupInfo: IUniFormulaPopupInfo }) {
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlur={() => setFocused(false)}
|
||||
/>
|
||||
/> */}
|
||||
<div className={clsx(styles.formulaIcon, { [styles.formulaIconDisable]: !formulaString })}>
|
||||
<span
|
||||
className={clsx(styles.iconContainer, styles.iconContainerError)}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import type { Nullable, Workbook } from '@univerjs/core';
|
||||
import { BooleanNumber, DEFAULT_EMPTY_DOCUMENT_VALUE, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DocumentFlavor, HorizontalAlign, IPermissionService, IUniverInstanceService, Rectangle, ThemeService, UniverInstanceType, useDependency, useObservable, VerticalAlign, WrapStrategy } from '@univerjs/core';
|
||||
import { TextEditor } from '@univerjs/docs-ui';
|
||||
// import { TextEditor } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import { CheckMarkSingle, CloseSingle, FxSingle } from '@univerjs/icons';
|
||||
import { RangeProtectionPermissionEditPoint, RangeProtectionRuleModel, SheetsSelectionsService, WorkbookEditablePermission, WorksheetEditPermission, WorksheetProtectionRuleModel, WorksheetSetCellValuePermission } from '@univerjs/sheets';
|
||||
@@ -206,7 +206,7 @@ export function FormulaBar() {
|
||||
|
||||
return (
|
||||
<div className={styles.uniFormulaBox}>
|
||||
<TextEditor
|
||||
{/* <TextEditor
|
||||
id={DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY}
|
||||
isSheetEditor
|
||||
resizeCallBack={resizeCallBack}
|
||||
@@ -215,7 +215,7 @@ export function FormulaBar() {
|
||||
className={clsx(styles.uniFormulaInput, styles.formulaContent)}
|
||||
snapshot={INITIAL_SNAPSHOT}
|
||||
isSingle
|
||||
/>
|
||||
/> */}
|
||||
<div className={clsx(styles.formulaIcon, { [styles.formulaIconDisable]: disable })}>
|
||||
<span
|
||||
className={clsx(styles.iconContainer, styles.iconContainerError)}
|
||||
|
||||
@@ -14,13 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { UnitModel, UniverInstanceType } from '../../common/unit';
|
||||
import { Tools } from '../../shared/tools';
|
||||
import { getEmptySnapshot } from './empty-snapshot';
|
||||
import { JSONX } from './json-x/json-x';
|
||||
import { PRESET_LIST_TYPE } from './preset-list-type';
|
||||
import { getBodySlice, SliceBodyType } from './text-x/utils';
|
||||
import type { Nullable } from '../../shared';
|
||||
import type {
|
||||
IDocumentBody,
|
||||
@@ -32,6 +25,13 @@ import type {
|
||||
} from '../../types/interfaces/i-document-data';
|
||||
import type { IPaddingData } from '../../types/interfaces/i-style-data';
|
||||
import type { JSONXActions } from './json-x/json-x';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { UnitModel, UniverInstanceType } from '../../common/unit';
|
||||
import { Tools } from '../../shared/tools';
|
||||
import { getEmptySnapshot } from './empty-snapshot';
|
||||
import { JSONX } from './json-x/json-x';
|
||||
import { PRESET_LIST_TYPE } from './preset-list-type';
|
||||
import { getBodySlice, SliceBodyType } from './text-x/utils';
|
||||
|
||||
export const DEFAULT_DOC = {
|
||||
id: 'default_doc',
|
||||
@@ -228,6 +228,7 @@ export class DocumentDataModel extends DocumentDataModelSimple {
|
||||
headerModelMap: Map<string, DocumentDataModel> = new Map();
|
||||
|
||||
footerModelMap: Map<string, DocumentDataModel> = new Map();
|
||||
change$ = new BehaviorSubject<number>(0);
|
||||
|
||||
constructor(snapshot: Partial<IDocumentData>) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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<typeof Mentions> = {
|
||||
title: 'Components / Mentions',
|
||||
component: Mentions,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const InputBasic = {
|
||||
|
||||
render() {
|
||||
const [value, onChange] = useState('');
|
||||
|
||||
return (
|
||||
<div style={{ width: 400 }}>
|
||||
<Mentions
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<Mention
|
||||
trigger="@"
|
||||
data={[
|
||||
{
|
||||
id: '1',
|
||||
display: 'zhangwei ',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
display: 'zhangwei2 ',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
display: 'zhangwei3 ',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Mentions>
|
||||
</div>
|
||||
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<DocumentDataModel>(editPopup.unitId) : null;
|
||||
const textSelectionService = useDependency(DocSelectionManagerService);
|
||||
const [mentions, setMentions] = useState<ITypeMentionList[]>([]);
|
||||
@@ -48,17 +50,17 @@ export const MentionEditPopup = () => {
|
||||
}
|
||||
})();
|
||||
}, [mentionIOService, editPopup, search]);
|
||||
|
||||
if (!editPopup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MentionList
|
||||
editorId={editPopup.unitId}
|
||||
onClick={() => 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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const [activeId, setActiveId] = useState(active ?? mentions[0]?.mentions[0]?.objectId);
|
||||
const handleSelect = (item: IMention) => {
|
||||
onSelect?.(item);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// ref.current?.focus();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<div ref={ref} tabIndex={0} className={styles.docMentionPanel} onClick={onClick}>
|
||||
<div ref={ref} data-editorid={editorId} tabIndex={0} className={styles.docMentionPanel} onClick={onClick}>
|
||||
{mentions.map((typeMentions) => (
|
||||
<div key={typeMentions.type}>
|
||||
<div className={styles.docMentionType}>{typeMentions.title}</div>
|
||||
{typeMentions.mentions.map((mention) => (
|
||||
<div
|
||||
data-editorid={editorId}
|
||||
key={mention.objectId}
|
||||
className={cs(styles.docMention, { [styles.docMentionActive]: activeId === mention.objectId })}
|
||||
onClick={() => handleSelect(mention)}
|
||||
|
||||
@@ -45,6 +45,7 @@ export const AddDocCommentComment: ICommand<IAddDocCommentComment> = {
|
||||
{
|
||||
id: comment.threadId,
|
||||
type: CustomDecorationType.COMMENT,
|
||||
unitId,
|
||||
}
|
||||
);
|
||||
if (doMutation) {
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-1
@@ -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<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const primary = ranges[0] as ITextRangeWithStyle | undefined;
|
||||
if (lastSelection?.startOffset === primary?.startOffset && lastSelection?.endOffset === primary?.endOffset) {
|
||||
|
||||
@@ -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$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC), [univerInstanceService]);
|
||||
const doc$ = useMemo(() => univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(filter((doc) => !!doc && !isInternalEditorID(doc.getUnitId()))), [univerInstanceService]);
|
||||
const doc = useObservable(doc$);
|
||||
const subUnitId$ = useMemo(() => new Observable<string>((sub) => sub.next(DEFAULT_DOC_SUBUNIT_ID)), []);
|
||||
const docSelectionManagerService = useDependency(DocSelectionManagerService);
|
||||
|
||||
@@ -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<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const documentDataModel = propUnitId ?
|
||||
univerInstanceService.getUnit<DocumentDataModel>(propUnitId, UniverInstanceType.UNIVER_DOC)
|
||||
: univerInstanceService.getCurrentUnitForType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!documentDataModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ReplaceSnapshotCommand: ICommand<IReplaceSnapshotCommandParams> = {
|
||||
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<IReplaceSnapshotCommandParams> = {
|
||||
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<IReplaceSnapshotCommandParams> = {
|
||||
lists: prevLists,
|
||||
drawings: prevDrawings,
|
||||
drawingsOrder: prevDrawingsOrder,
|
||||
documentStyle: prevDocumentStyle,
|
||||
} = prevSnapshot;
|
||||
|
||||
if (body == null || prevBody == null) {
|
||||
@@ -88,6 +89,13 @@ export const ReplaceSnapshotCommand: ICommand<IReplaceSnapshotCommandParams> = {
|
||||
|
||||
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<IReplaceSelectionCommandParams> =
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ReplaceTextRunsCommand: ICommand<IReplaceContentCommandParams> = {
|
||||
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<DocumentDataModel>(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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<React.HTMLAttributes<HTMLDivElement>, 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<HTMLDivElement>) => 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<string>; // Configuring which workbook the selector defaults to opening in determines whether the ref includes a [unitId] prefix.
|
||||
openForSheetSubUnitId?: Nullable<string>; // Configuring the default worksheet where the selector opens determines whether the ref includes a [unitId]sheet1 prefix.
|
||||
|
||||
onChange?: (value: Nullable<string>) => 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<MyComponentProps, 'onChange' | 'onActive'>): 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<string>('');
|
||||
|
||||
const [validationVisible, setValidationVisible] = useState(isValueValid);
|
||||
|
||||
const [validationOffset, setValidationOffset] = useState<[number, number]>([0, 0]);
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(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<Editor>) => {
|
||||
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<string>) => {
|
||||
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 (
|
||||
<>
|
||||
<div {...propsNew} style={{ ...propsNew.style, ...(disabled ? { pointerEvents: 'none', opacity: 0 } : {}) }} className={className + borderStyle} ref={editorRef}>
|
||||
<div style={{ display: validationVisible ? 'none' : 'block' }} className={styles.textEditorValidationError}>{validationContent}</div>
|
||||
<div style={{ display: hasValue() ? 'none' : 'unset' }} className={styles.textEditorContainerPlaceholder}>{placeholderValue}</div>
|
||||
</div>
|
||||
{/* Don't delete it yet, test the stability without popup */}
|
||||
{/* <Popup visible={!validationVisible} offset={validationOffset}>
|
||||
<div className={styles.textEditorValidationError}>{validationContent}</div>
|
||||
</Popup> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<string>; // Configuring which workbook the selector defaults to opening in determines whether the ref includes a [unitId] prefix.
|
||||
openForSheetSubUnitId?: Nullable<string>; // 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<string[]>(['']);
|
||||
|
||||
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<number>(-1);
|
||||
|
||||
const selectorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentInputIndexRef = useRef<number>(-1);
|
||||
|
||||
const openForSheetUnitIdRef = useRef<Nullable<string>>(openForSheetUnitId);
|
||||
|
||||
const openForSheetSubUnitIdRef = useRef<Nullable<string>>(openForSheetSubUnitId);
|
||||
|
||||
const isSingleChoiceRef = useRef<Nullable<boolean>>(isSingleChoice);
|
||||
|
||||
const isReadonlyRef = useRef<Nullable<boolean>>(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<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetName(name)?.getSheetId() || '';
|
||||
}
|
||||
|
||||
function handleTextValueChange(value: Nullable<string>) {
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
className={clsx(sClassName, className)}
|
||||
ref={selectorRef}
|
||||
style={{ width, height, cursor: dialogOnly ? 'pointer' : undefined }}
|
||||
onClickCapture={(event) => {
|
||||
if (dialogOnly) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleOpenModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextEditor style={dialogOnly ? dialogOnlyInputStyle : undefined} placeholder={placeholder} value={value} isReadonly={isReadonly} isSingleChoice={isSingleChoice} openForSheetUnitId={openForSheetUnitId} openForSheetSubUnitId={openForSheetSubUnitId} onValid={onEditorValid} onActive={onEditorActive} onChange={handleTextValueChange} id={id} onlyInputRange canvasStyle={{ fontSize: 10 }} className={styles.rangeSelectorEditor} />
|
||||
<Tooltip title={localeService.t('rangeSelector.buttonTooltip')} placement="bottom">
|
||||
<button type="button" className={styles.rangeSelectorIcon} onClick={handleOpenModal}>
|
||||
<SelectRangeSingle />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
width="328px"
|
||||
visible={selectorVisible}
|
||||
title={localeService.t('rangeSelector.title')}
|
||||
draggable
|
||||
closeIcon={<CloseSingle />}
|
||||
footer={(
|
||||
<footer>
|
||||
<Button onClick={handleCloseModal}>{localeService.t('rangeSelector.cancel')}</Button>
|
||||
<Button style={{ marginLeft: 10 }} onClick={handleConform} type="primary">{localeService.t('rangeSelector.confirm')}</Button>
|
||||
</footer>
|
||||
)}
|
||||
onClose={handleCloseModal}
|
||||
>
|
||||
<div className={clsx(styles.rangeSelectorModal, className)}>
|
||||
{rangeDataList.map((item, index) => (
|
||||
<div key={index} className={styles.rangeSelectorModalContainer}>
|
||||
<div style={{ width: rangeDataList.length === 1 ? '280px' : '252px' }} className={styles.rangeSelectorModalContainerInput}>
|
||||
<Input
|
||||
className={currentInputIndex === index ? styles.rangeSelectorModalContainerInputActive : ((rangeDataList.length - 1 === index && currentInputIndex === -1) ? styles.rangeSelectorModalContainerInputActive : '')}
|
||||
placeholder={localeService.t('rangeSelector.placeHolder')}
|
||||
affixWrapperStyle={{ width: '100%' }}
|
||||
key={`input${index}`}
|
||||
onClick={() => setCurrentInputIndex(index)}
|
||||
value={item}
|
||||
onChange={(value) => changeItem(index, value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: rangeDataList.length === 1 ? 'none' : 'inline-block' }} className={styles.rangeSelectorModalContainerButton}>
|
||||
<DeleteSingle className={styles.rangeSelectorModalContainerDeleteButton} onClick={() => removeItem(index)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div style={{ display: isSingleChoice ? 'none' : 'unset' }} className={styles.rangeSelectorModalAdd}>
|
||||
<Button type="link" size="small" onClick={handleAddRange} className={styles.rangeSelectorModalAddButton}>
|
||||
<IncreaseSingle />
|
||||
<span>{localeService.t('rangeSelector.addAnotherRange')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-116
@@ -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<string>) {
|
||||
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<ScrollBar>;
|
||||
|
||||
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<Workbook>(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();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+1
-12
@@ -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<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
// if (sheetInstances.length > 0) {
|
||||
// const workbook = this._univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
// this._editorService.setOperationSheetUnitId(workbook.getUnitId());
|
||||
// // this._editorService.setOperationSheetSubUnitId(workbook.getActiveSheet().getSheetId());
|
||||
// }
|
||||
|
||||
this._editorService.focusStyle(unitId);
|
||||
this._editorService.focus(unitId);
|
||||
}
|
||||
|
||||
private _commandExecutedListener() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<Nullable<Editor>>;
|
||||
|
||||
register(config: IEditorConfigParams, container: HTMLDivElement): IDisposable;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
isVisible(id: string): Nullable<boolean>;
|
||||
|
||||
inputFormula$: Observable<IEditorInputFormulaParam>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
setFormula(formulaString: string): void;
|
||||
|
||||
resize$: Observable<string>;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
resize(id: string): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
getAllEditor(): Map<string, Editor>;
|
||||
|
||||
/**
|
||||
* The sheet currently being operated on will determine
|
||||
* whether to include unitId information in the ref.
|
||||
*/
|
||||
setOperationSheetUnitId(unitId: Nullable<string>): void;
|
||||
getOperationSheetUnitId(): Nullable<string>;
|
||||
/**
|
||||
* The sub-table within the sheet currently being operated on
|
||||
* will determine whether to include subUnitId information in the ref.
|
||||
*/
|
||||
setOperationSheetSubUnitId(sheetId: Nullable<string>): void;
|
||||
getOperationSheetSubUnitId(): Nullable<string>;
|
||||
|
||||
isEditor(editorUnitId: string): boolean;
|
||||
|
||||
isSheetEditor(editorUnitId: string): boolean;
|
||||
|
||||
closeRangePrompt$: Observable<unknown>;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
closeRangePrompt(): void;
|
||||
|
||||
blur$: Observable<unknown>;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
blur(): void;
|
||||
|
||||
focus$: Observable<ISuccinctDocRangeParam>;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
focus(editorUnitId?: string): void;
|
||||
focus(editorUnitId: string): void;
|
||||
|
||||
setValue$: Observable<IEditorSetValueParam>;
|
||||
valueChange$: Observable<Readonly<Editor>>;
|
||||
|
||||
/**
|
||||
* @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<Nullable<string>>;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
focusStyle(editorUnitId: Nullable<string>): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
refreshValueChange(editorId: string): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
checkValueLegality(editorId: string): boolean;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
getValue(id: string): Nullable<string>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
getRichValue(id: string): Nullable<IDocumentBody>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
changeSpreadsheetFocusState(state: boolean): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
getSpreadsheetFocusState(): boolean;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
selectionChangingState(): boolean;
|
||||
|
||||
singleSelection$: Observable<boolean>;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
singleSelection(state: boolean): void;
|
||||
|
||||
setFocusId(id: Nullable<string>): void;
|
||||
getFocusId(): Nullable<string>;
|
||||
|
||||
getFocusEditor(): Readonly<Nullable<Editor>>;
|
||||
}
|
||||
|
||||
@@ -193,46 +71,15 @@ export class EditorService extends Disposable implements IEditorService, IDispos
|
||||
|
||||
private _focusEditorUnitId: Nullable<string>;
|
||||
|
||||
private readonly _state$ = new Subject<Nullable<IEditorStateParams>>();
|
||||
readonly state$ = this._state$.asObservable();
|
||||
|
||||
private _currentSheetUnitId: Nullable<string>;
|
||||
|
||||
private _currentSheetSubUnitId: Nullable<string>;
|
||||
|
||||
private readonly _inputFormula$ = new Subject<IEditorInputFormulaParam>();
|
||||
readonly inputFormula$ = this._inputFormula$.asObservable();
|
||||
|
||||
private readonly _resize$ = new Subject<string>();
|
||||
readonly resize$ = this._resize$.asObservable();
|
||||
|
||||
private readonly _closeRangePrompt$ = new Subject<unknown>();
|
||||
readonly closeRangePrompt$ = this._closeRangePrompt$.asObservable();
|
||||
|
||||
private readonly _blur$ = new Subject();
|
||||
readonly blur$ = this._blur$.asObservable();
|
||||
|
||||
private readonly _focus$ = new Subject<ISuccinctDocRangeParam>();
|
||||
readonly focus$ = this._focus$.asObservable();
|
||||
|
||||
private readonly _setValue$ = new Subject<IEditorSetValueParam>();
|
||||
readonly setValue$ = this._setValue$.asObservable();
|
||||
|
||||
private readonly _valueChange$ = new Subject<Readonly<Editor>>();
|
||||
readonly valueChange$ = this._valueChange$.asObservable();
|
||||
|
||||
private readonly _focusStyle$ = new Subject<Nullable<string>>();
|
||||
readonly focusStyle$ = this._focusStyle$.asObservable();
|
||||
|
||||
private readonly _singleSelection$ = new Subject<boolean>();
|
||||
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<string>) {
|
||||
private _setFocusId(id: Nullable<string>) {
|
||||
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);
|
||||
}
|
||||
|
||||
/** @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) {
|
||||
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();
|
||||
|
||||
const focusingEditor = this.getFocusEditor();
|
||||
focusingEditor?.blur();
|
||||
this._blur$.next(null);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
focus(editorUnitId: string | undefined = this._univerInstanceService.getCurrentUniverDocInstance()?.getUnitId()) {
|
||||
focus(editorUnitId: string) {
|
||||
if (this._focusEditorUnitId) {
|
||||
this.blur();
|
||||
}
|
||||
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);
|
||||
|
||||
this.focusStyle(editorUnitId);
|
||||
if (!isInternalEditorID(editorUnitId)) {
|
||||
this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, true);
|
||||
}
|
||||
|
||||
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<string>) {
|
||||
this._currentSheetUnitId = unitId;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getOperationSheetUnitId() {
|
||||
return this._currentSheetUnitId;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
setOperationSheetSubUnitId(sheetId: Nullable<string>) {
|
||||
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<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (sheets.length > 0) {
|
||||
const current = this._univerInstanceService.getCurrentUnitForType<Workbook>(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() {
|
||||
|
||||
@@ -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<IDocSelectionInnerParam>;
|
||||
|
||||
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<string>;
|
||||
/**
|
||||
* @deprecated The implementer makes its own judgment.
|
||||
*/
|
||||
openForSheetSubUnitId?: Nullable<string>;
|
||||
}
|
||||
|
||||
export interface IEditorOptions extends IEditorConfigParams, IEditorStateParams {
|
||||
@@ -161,7 +120,6 @@ export class Editor extends Disposable implements IEditor {
|
||||
paste$: Observable<IEditorInputConfig> = this._paste$.asObservable();
|
||||
|
||||
// Editor get focus.
|
||||
private _focus = false;
|
||||
private readonly _focus$ = new Subject<IEditorInputConfig>();
|
||||
focus$: Observable<IEditorInputConfig> = this._focus$.asObservable();
|
||||
|
||||
@@ -173,12 +131,6 @@ export class Editor extends Disposable implements IEditor {
|
||||
private readonly _selectionChange$ = new Subject<IDocSelectionInnerParam>();
|
||||
selectionChange$: Observable<IDocSelectionInnerParam> = this._selectionChange$.asObservable();
|
||||
|
||||
private _valueLegality = true;
|
||||
|
||||
private _openForSheetUnitId: Nullable<string>;
|
||||
|
||||
private _openForSheetSubUnitId: Nullable<string>;
|
||||
|
||||
constructor(
|
||||
private _param: IEditorOptions,
|
||||
private _univerInstanceService: IUniverInstanceService,
|
||||
@@ -187,12 +139,14 @@ export class Editor extends Disposable implements IEditor {
|
||||
private _undoRedoService: IUndoRedoService
|
||||
) {
|
||||
super();
|
||||
this._openForSheetUnitId = this._param.openForSheetUnitId;
|
||||
this._openForSheetSubUnitId = this._param.openForSheetSubUnitId;
|
||||
|
||||
this._listenSelection();
|
||||
}
|
||||
|
||||
get docSelectionRenderService() {
|
||||
return this._param.render.with(DocSelectionRenderService);
|
||||
}
|
||||
|
||||
private _listenSelection() {
|
||||
const docSelectionRenderService = this._param.render.with(DocSelectionRenderService);
|
||||
|
||||
@@ -268,6 +222,14 @@ export class Editor extends Disposable implements IEditor {
|
||||
);
|
||||
}
|
||||
|
||||
isFocus() {
|
||||
const docSelectionRenderService = this._param.render.with(DocSelectionRenderService);
|
||||
return docSelectionRenderService.isFocusing && Boolean(docSelectionRenderService.getActiveTextRange());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `IEditorService.focus` as instead. this is for internal usage.
|
||||
*/
|
||||
focus() {
|
||||
const curDoc = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_DOC);
|
||||
const editorUnitId = this.getEditorId();
|
||||
@@ -281,26 +243,26 @@ 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,
|
||||
});
|
||||
// const lastSelectionInfo = this._docSelectionManagerService.getDocRanges({
|
||||
// unitId: editorUnitId,
|
||||
// subUnitId: editorUnitId,
|
||||
// });
|
||||
|
||||
if (lastSelectionInfo) {
|
||||
this._docSelectionManagerService.replaceDocRanges(lastSelectionInfo, {
|
||||
unitId: editorUnitId,
|
||||
subUnitId: editorUnitId,
|
||||
}, false);
|
||||
}
|
||||
|
||||
this._focus = true;
|
||||
// if (lastSelectionInfo) {
|
||||
// this._docSelectionManagerService.replaceDocRanges(lastSelectionInfo, {
|
||||
// unitId: editorUnitId,
|
||||
// subUnitId: editorUnitId,
|
||||
// }, false);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `IEditorService.blur` as instead. this is for internal usage.
|
||||
*/
|
||||
blur(): void {
|
||||
const docSelectionRenderService = this._param.render.with(DocSelectionRenderService);
|
||||
|
||||
docSelectionRenderService.blur();
|
||||
this._focus = false;
|
||||
}
|
||||
|
||||
// Selects the entire content of the editor.
|
||||
@@ -336,6 +298,12 @@ export class Editor extends Disposable implements IEditor {
|
||||
return this._docSelectionManagerService.getDocRanges(params);
|
||||
}
|
||||
|
||||
getCursorPosition(): number {
|
||||
const selectionRanges = this.getSelectionRanges();
|
||||
|
||||
return selectionRanges.find((range) => range.collapsed)?.startOffset ?? -1;
|
||||
}
|
||||
|
||||
// get editor id.
|
||||
getEditorId(): string {
|
||||
return this._getEditorId();
|
||||
@@ -348,17 +316,48 @@ export class Editor extends Disposable implements IEditor {
|
||||
return docDataModel.getSnapshot();
|
||||
}
|
||||
|
||||
getDocumentDataModel() {
|
||||
return this._getDocDataModel();
|
||||
}
|
||||
|
||||
// Set the new document data.
|
||||
setDocumentData(data: IDocumentData, textRanges: Nullable<ITextRangeWithStyle[]>) {
|
||||
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 +393,24 @@ export class Editor extends Disposable implements IEditor {
|
||||
return this._param.render;
|
||||
}
|
||||
|
||||
isSingleChoice() {
|
||||
return this._param.isSingleChoice ?? false;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
setOpenForSheetUnitId(unitId: Nullable<string>) {
|
||||
this._openForSheetUnitId = unitId;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getOpenForSheetUnitId() {
|
||||
return this._openForSheetUnitId;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
setOpenForSheetSubUnitId(subUnitId: Nullable<string>) {
|
||||
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 +440,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.
|
||||
*/
|
||||
|
||||
@@ -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<Nullable<INodePosition>>(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<IEditorInputConfig>();
|
||||
readonly onPaste$ = this._onPaste$.asObservable();
|
||||
|
||||
@@ -87,6 +93,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
private _currentSegmentId: string = '';
|
||||
private _currentSegmentPage: number = -1;
|
||||
private _selectionStyle: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE;
|
||||
private _onPointerEvent = false;
|
||||
|
||||
private _viewPortObserverMap = new Map<
|
||||
string,
|
||||
@@ -99,10 +106,21 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
private _isIMEInputApply = false;
|
||||
private _scenePointerMoveSubs: Array<Subscription> = [];
|
||||
private _scenePointerUpSubs: Array<Subscription> = [];
|
||||
private _editorFocusing = true;
|
||||
// When the user switches editors, whether to clear the doc ranges.
|
||||
private _reserveRanges = false;
|
||||
|
||||
get isOnPointerEvent() {
|
||||
return this._onPointerEvent;
|
||||
}
|
||||
|
||||
get isFocusing() {
|
||||
return this._input === document.activeElement;
|
||||
}
|
||||
|
||||
get canFocusing() {
|
||||
return this.isFocusing || document.activeElement === document.body || document.activeElement === null;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<DocumentDataModel>,
|
||||
@ILayoutService private readonly _layoutService: ILayoutService,
|
||||
@@ -168,7 +186,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
_currentSegmentPage: segmentPage,
|
||||
_selectionStyle: style,
|
||||
} = this;
|
||||
|
||||
const { scene, mainComponent } = this._context;
|
||||
const document = mainComponent as Documents;
|
||||
const docSkeleton = this._docSkeletonManagerService.getSkeleton();
|
||||
@@ -305,12 +322,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 +336,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (!this._editorFocusing) {
|
||||
return;
|
||||
}
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
@@ -330,22 +343,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 +445,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();
|
||||
|
||||
@@ -517,7 +513,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
let preMoveOffsetX = evtOffsetX;
|
||||
|
||||
let preMoveOffsetY = evtOffsetY;
|
||||
|
||||
this._onPointerEvent = true;
|
||||
this._scenePointerMoveSubs.push(scene.onPointerMove$.subscribeEvent((moveEvt: IPointerEvent | IMouseEvent) => {
|
||||
const { offsetX: moveOffsetX, offsetY: moveOffsetY } = moveEvt;
|
||||
scene.setCursor(CURSOR_TYPE.TEXT);
|
||||
@@ -540,6 +536,7 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
[...this._scenePointerMoveSubs, ...this._scenePointerUpSubs].forEach((e) => {
|
||||
e.unsubscribe();
|
||||
});
|
||||
this._onPointerEvent = false;
|
||||
scene.enableObjectsEvent();
|
||||
|
||||
// Add cursor.
|
||||
@@ -630,6 +627,14 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
return this._rectRangeList.map(serializeRectRange);
|
||||
}
|
||||
|
||||
getAllTextRanges() {
|
||||
return this._getAllTextRanges();
|
||||
}
|
||||
|
||||
getAllRectRanges() {
|
||||
return this._getAllRectRanges();
|
||||
}
|
||||
|
||||
private _getActiveRange(): Nullable<ITextRangeWithStyle> {
|
||||
const activeRange = this._rangeList.find((range) => range.isActive());
|
||||
|
||||
@@ -700,6 +705,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;
|
||||
@@ -771,20 +777,6 @@ export class DocSelectionRenderService extends RxDisposable implements IRenderMo
|
||||
this._rectRangeList = newRanges;
|
||||
}
|
||||
|
||||
private _removeCollapsedTextRange() {
|
||||
const oldTextRanges = this._rangeList;
|
||||
|
||||
this._rangeList = [];
|
||||
|
||||
for (const textRange of oldTextRanges) {
|
||||
if (textRange.collapsed) {
|
||||
textRange.dispose();
|
||||
} else {
|
||||
this._rangeList.push(textRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _removeAllRanges() {
|
||||
this._removeAllTextRanges();
|
||||
this._removeAllRectRanges();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+2
-3
@@ -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';
|
||||
@@ -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<IDocumentData>;
|
||||
container: RefObject<HTMLDivElement>;
|
||||
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<Editor>();
|
||||
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;
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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<ScrollBar>;
|
||||
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 };
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Editor, IRichTextEditorProps>((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<HTMLDivElement>(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<HTMLDivElement>(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 (
|
||||
<div className={clsx(styles.richTextEditor, className)} style={style}>
|
||||
<div
|
||||
className={clsx(styles.richTextEditorWrap, {
|
||||
[styles.richTextEditorActive]: isFocusing,
|
||||
})}
|
||||
style={{ height }}
|
||||
ref={sheetEmbeddingRef}
|
||||
>
|
||||
<div
|
||||
className={styles.richTextEditorText}
|
||||
ref={formulaEditorContainerRef}
|
||||
onMouseUp={() => editor?.focus()}
|
||||
/>
|
||||
{!showPlaceholder
|
||||
? null
|
||||
: (
|
||||
<div className={styles.richTextEditorPlaceholder}>
|
||||
{props.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -101,7 +101,7 @@ export const RichTextEditingMutation: IMutation<IRichTextEditingMutationParams,
|
||||
documentViewModel.reset(documentDataModel);
|
||||
// Step 3: Update cursor & selection.
|
||||
// Make sure update cursor & selection after doc skeleton is calculated.
|
||||
if (!noNeedSetTextRange && textRanges && trigger != null) {
|
||||
if (!noNeedSetTextRange && textRanges && trigger != null && !isSync) {
|
||||
queueMicrotask(() => {
|
||||
docSelectionManagerService.replaceDocRanges(textRanges, { unitId, subUnitId: unitId }, isEditing, params.options);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Subscription>;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -127,7 +127,6 @@ export function DataValidationDetail() {
|
||||
...unitRange, sheetId: '',
|
||||
};
|
||||
});
|
||||
|
||||
if (isUnitRangesEqual(unitRanges, localRanges)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-1
@@ -29,9 +29,10 @@ export function CustomFormulaInput(props: IFormulaInputProps) {
|
||||
const handleOutClick = formulaEditorActionsRef.current?.handleOutClick;
|
||||
handleOutClick && handleOutClick(e, () => isFocusFormulaEditorSet(false));
|
||||
});
|
||||
|
||||
return (
|
||||
<FormulaEditor
|
||||
initValue={value?.formula1 ?? '' as any}
|
||||
initValue={value?.formula1 ?? '=' as any}
|
||||
unitId={unitId}
|
||||
subUnitId={subUnitId}
|
||||
isFocus={isFocusFormulaEditor}
|
||||
|
||||
+23
-27
@@ -284,33 +284,29 @@ export function ListFormulaInput(props: IFormulaInputProps) {
|
||||
});
|
||||
}, [strList, onChange, isFormulaStr, formulaStrCopy, refColors]);
|
||||
|
||||
const updateFormula = useMemo(
|
||||
() =>
|
||||
async (str: string) => {
|
||||
if (!isFormulaString(str)) {
|
||||
onChange?.({
|
||||
formula1: '',
|
||||
formula2,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (dataValidationFormulaController.getFormulaRefCheck(str)) {
|
||||
onChange?.({
|
||||
formula1: isFormulaString(str) ? str : '',
|
||||
formula2,
|
||||
});
|
||||
setLocalError('');
|
||||
} else {
|
||||
onChange?.({
|
||||
formula1: '',
|
||||
formula2,
|
||||
});
|
||||
setFormulaStr('=');
|
||||
setLocalError(localeService.t('dataValidation.validFail.formulaError'));
|
||||
}
|
||||
},
|
||||
[formula2, onChange]
|
||||
);
|
||||
const updateFormula = useEvent(async (str: string) => {
|
||||
if (!isFormulaString(str)) {
|
||||
onChange?.({
|
||||
formula1: '',
|
||||
formula2,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (dataValidationFormulaController.getFormulaRefCheck(str)) {
|
||||
onChange?.({
|
||||
formula1: isFormulaString(str) ? str : '',
|
||||
formula2,
|
||||
});
|
||||
setLocalError('');
|
||||
} else {
|
||||
onChange?.({
|
||||
formula1: '',
|
||||
formula2,
|
||||
});
|
||||
setFormulaStr('=');
|
||||
setLocalError(localeService.t('dataValidation.validFail.formulaError'));
|
||||
}
|
||||
});
|
||||
|
||||
const formulaEditorActionsRef = useRef<Parameters<typeof FormulaEditor>[0]['actions']>({});
|
||||
const [isFocusFormulaEditor, isFocusFormulaEditorSet] = useState(false);
|
||||
|
||||
@@ -156,9 +156,7 @@ export function ListDropDown(props: IDropdownComponentProps) {
|
||||
const params = command.params as IRichTextEditingMutationParams;
|
||||
const { unitId } = params;
|
||||
const unit = instanceService.getUnit<DocumentDataModel>(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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+2
-1
@@ -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));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+102
-137
@@ -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 (
|
||||
<Popup visible={visible} offset={offset}>
|
||||
{functionInfo
|
||||
? (
|
||||
<div className={styles.formulaHelpFunction}>
|
||||
<div className={styles.formulaHelpFunctionTitle}>
|
||||
<Help
|
||||
prefix={functionInfo.functionName}
|
||||
value={functionInfo.functionParameter}
|
||||
active={paramIndex}
|
||||
onClick={handleSwitchActive}
|
||||
/>
|
||||
<div className={styles.formulaHelpFunctionTitleIcons}>
|
||||
<div
|
||||
className={styles.formulaHelpFunctionTitleIcon}
|
||||
style={{ transform: contentVisible ? 'rotateZ(-90deg)' : 'rotateZ(90deg)' }}
|
||||
onClick={() => setContentVisible(!contentVisible)}
|
||||
>
|
||||
<MoreSingle />
|
||||
</div>
|
||||
<div
|
||||
className={styles.formulaHelpFunctionTitleIcon}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseSingle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.formulaHelpFunctionContent}
|
||||
style={{
|
||||
height: contentVisible ? 'unset' : 0,
|
||||
padding: contentVisible ? 'revert-layer' : 0,
|
||||
}}
|
||||
>
|
||||
<div className={styles.formulaHelpFunctionContentInner}>
|
||||
<Params
|
||||
title={localeService.t('formula.prompt.helpExample')}
|
||||
value={`${functionInfo.functionName}(${functionInfo.functionParameter
|
||||
.map((item) => item.example)
|
||||
.join(',')})`}
|
||||
/>
|
||||
<Params
|
||||
title={localeService.t('formula.prompt.helpAbstract')}
|
||||
value={functionInfo.description}
|
||||
/>
|
||||
{functionInfo &&
|
||||
functionInfo.functionParameter &&
|
||||
functionInfo.functionParameter.map((item: IFunctionParam, i: number) => (
|
||||
<Params
|
||||
key={i}
|
||||
className={paramIndex === i ? styles.formulaHelpFunctionActive : ''}
|
||||
title={item.name}
|
||||
value={`${item.require ? required : optional} ${item.detail}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<></>
|
||||
)}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
interface IParamsProps {
|
||||
className?: string;
|
||||
title?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const Params = (props: IParamsProps) => (
|
||||
const Params = ({ className, title, value }: IParamsProps) => (
|
||||
<div className={styles.formulaHelpFunctionContentParams}>
|
||||
<div className={`
|
||||
${styles.formulaHelpFunctionContentParamsTitle}
|
||||
${props.className}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{props.title}
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.formulaHelpFunctionContentParamsDetail}>{props.value}</div>
|
||||
<div className={styles.formulaHelpFunctionContentParamsDetail}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -187,8 +62,7 @@ const Help = (props: IHelpProps) => {
|
||||
</span>
|
||||
{value &&
|
||||
value.map((item: IFunctionParam, i: number) => (
|
||||
// TODO@Dushusir: more params needs to be active
|
||||
<span key={i} className={styles.formulaHelpParamItem}>
|
||||
<span key={item.name} className={styles.formulaHelpParamItem}>
|
||||
<span
|
||||
className={active === i ? styles.formulaHelpFunctionActive : styles.formulaHelpParamActive}
|
||||
onClick={() => onClick(i)}
|
||||
@@ -202,3 +76,94 @@ const Help = (props: IHelpProps) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
? (
|
||||
<RectPopup portal onClickOutside={() => reset()} anchorRect$={position$} direction="vertical">
|
||||
<div className={styles.formulaHelpFunction}>
|
||||
<div className={styles.formulaHelpFunctionTitle}>
|
||||
<Help
|
||||
prefix={functionInfo.functionName}
|
||||
value={functionInfo.functionParameter}
|
||||
active={paramIndex}
|
||||
onClick={handleSwitchActive}
|
||||
/>
|
||||
<div className={styles.formulaHelpFunctionTitleIcons}>
|
||||
<div
|
||||
className={styles.formulaHelpFunctionTitleIcon}
|
||||
style={{ transform: contentVisible ? 'rotateZ(-90deg)' : 'rotateZ(90deg)' }}
|
||||
onClick={() => setContentVisible(!contentVisible)}
|
||||
>
|
||||
<MoreSingle />
|
||||
</div>
|
||||
<div
|
||||
className={styles.formulaHelpFunctionTitleIcon}
|
||||
onClick={onClose}
|
||||
>
|
||||
<CloseSingle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.formulaHelpFunctionContent}
|
||||
style={{
|
||||
height: contentVisible ? 'unset' : 0,
|
||||
padding: contentVisible ? 'revert-layer' : 0,
|
||||
}}
|
||||
>
|
||||
<div className={styles.formulaHelpFunctionContentInner}>
|
||||
<Params
|
||||
title={localeService.t('formula.prompt.helpExample')}
|
||||
value={`${functionInfo.functionName}(${functionInfo.functionParameter
|
||||
.map((item) => item.example)
|
||||
.join(',')})`}
|
||||
/>
|
||||
<Params
|
||||
title={localeService.t('formula.prompt.helpAbstract')}
|
||||
value={functionInfo.description}
|
||||
/>
|
||||
{functionInfo &&
|
||||
functionInfo.functionParameter &&
|
||||
functionInfo.functionParameter.map((item: IFunctionParam, i: number) => (
|
||||
<Params
|
||||
key={i}
|
||||
className={paramIndex === i ? styles.formulaHelpFunctionActive : ''}
|
||||
title={item.name}
|
||||
value={`${item.require ? required : optional} ${item.detail}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RectPopup>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>(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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+156
-45
@@ -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<IRefSelection[]>,
|
||||
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<Workbook>(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]);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export const useStateRef = <T = any>(value: T) => {
|
||||
const cache = useRef<T>();
|
||||
const cache = useRef<T>(value);
|
||||
cache.current = value;
|
||||
return cache as { current: T };
|
||||
return cache;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement>(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<HTMLElement>(null);
|
||||
const [editor, editorSet] = useState<Editor>();
|
||||
const editorRef = useRef<Editor>();
|
||||
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<DocumentDataModel>(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 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;
|
||||
|
||||
if (isEnd) {
|
||||
highlightSheet(isFocus ? ranges : [], editorRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocus) {
|
||||
highlight(formulaText, false, true);
|
||||
}
|
||||
}, [formulaText, isFocus, highlight]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const sub = docSelectionRenderService?.onChangeByEvent$.subscribe((e) => {
|
||||
// const formulaText = BuildTextUtils.transform.getPlainText(document?.getBody()?.dataStream ?? '');
|
||||
// highlight(formulaText, false, true);
|
||||
// });
|
||||
|
||||
// return () => sub?.unsubscribe();
|
||||
// }, [docSelectionRenderService?.onChangeByEvent$, document, highlight]);
|
||||
|
||||
// const refSelections = useDocHight(editorId, sequenceNodes);
|
||||
useVerify(isFocus, onVerify, formulaText);
|
||||
const focus = useFocus(editor);
|
||||
|
||||
const resetSelection = useResetSelection(isFocus);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// 在进行多个 input 切换的时候,失焦必须快于获得焦点.
|
||||
if (_isFocus) {
|
||||
const time = setTimeout(() => {
|
||||
isFocusSet(_isFocus);
|
||||
if (_isFocus) {
|
||||
focus();
|
||||
}
|
||||
}, 30);
|
||||
return () => {
|
||||
clearTimeout(time);
|
||||
};
|
||||
} else {
|
||||
resetSelection();
|
||||
isFocusSet(_isFocus);
|
||||
}
|
||||
}, [_isFocus, focus]);
|
||||
|
||||
const { checkScrollBar } = useResize(editor);
|
||||
useRefactorEffect(isFocus, unitId);
|
||||
useLeftAndRightArrow(isFocus, editor);
|
||||
|
||||
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);
|
||||
|
||||
useRefocus();
|
||||
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);
|
||||
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 (
|
||||
<div className={styles.sheetEmbeddingFormulaEditor}>
|
||||
<div className={clsx(styles.sheetEmbeddingFormulaEditor, className)}>
|
||||
<div
|
||||
className={clsx(styles.sheetEmbeddingFormulaEditorWrap, {
|
||||
[styles.sheetEmbeddingFormulaEditorActive]: isFocus,
|
||||
@@ -283,26 +308,29 @@ export function FormulaEditor(props: IFormulaEditorProps) {
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
</div>
|
||||
{errorText !== undefined ? <div className={styles.sheetEmbeddingFormulaEditorErrorWrap}>{errorText}</div> : null}
|
||||
</div>
|
||||
<HelpFunction
|
||||
editorId={editorId}
|
||||
paramIndex={paramIndex}
|
||||
functionInfo={functionInfo}
|
||||
onClose={() => {
|
||||
reset();
|
||||
focus();
|
||||
}}
|
||||
>
|
||||
</HelpFunction>
|
||||
<SearchFunction
|
||||
searchText={searchText}
|
||||
editorId={editorId}
|
||||
searchList={searchList}
|
||||
onSelect={handleFunctionSelect}
|
||||
ref={searchFunctionRef}
|
||||
>
|
||||
</SearchFunction>
|
||||
{errorText !== undefined ? <div className={styles.sheetEmbeddingFormulaEditorErrorWrap}>{errorText}</div> : null}
|
||||
{editor
|
||||
? (
|
||||
<HelpFunction
|
||||
editor={editor}
|
||||
isFocus={isFocus}
|
||||
formulaText={formulaText}
|
||||
onClose={() => focus()}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{editor
|
||||
? (
|
||||
<SearchFunction
|
||||
isFocus={isFocus}
|
||||
sequenceNodes={sequenceNodes}
|
||||
onSelect={handleFunctionSelect}
|
||||
ref={searchFunctionRef}
|
||||
editor={editor}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
;
|
||||
|
||||
+30
-31
@@ -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<HTMLElement, ISearchFunctionProps>(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<HTMLUListElement>();
|
||||
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 && (
|
||||
<Popup visible={visible} offset={offset}>
|
||||
return searchList.length > 0 && visible && (
|
||||
<RectPopup portal anchorRect$={position$} direction="vertical">
|
||||
<ul
|
||||
className={styles.formulaSearchFunction}
|
||||
ref={(v) => {
|
||||
@@ -206,7 +205,7 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
|
||||
>
|
||||
{searchList.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
key={item.name}
|
||||
className={active === index
|
||||
? `
|
||||
${styles.formulaSearchFunctionItem}
|
||||
@@ -217,7 +216,7 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
|
||||
onMouseLeave={handleLiMouseLeave}
|
||||
onMouseMove={debounceResetMouseState}
|
||||
onClick={() => {
|
||||
onSelect(item.name);
|
||||
handleFunctionSelect(item.name);
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
@@ -231,6 +230,6 @@ function SearchFunctionFactory(props: ISearchFunctionProps, ref: any) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Popup>
|
||||
</RectPopup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
// const [params, setParams] = useState<string[]>([]); // TODO@Dushusir: bind setParams to InputParams's onChange
|
||||
const [functionInfo, setFunctionInfo] = useState<IFunctionInfo | null>(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 (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -15,26 +15,24 @@
|
||||
*/
|
||||
|
||||
import type { Editor } from '@univerjs/docs-ui';
|
||||
import { useMemo } from 'react';
|
||||
import { Tools } from '@univerjs/core';
|
||||
import { useEvent } from '@univerjs/ui';
|
||||
|
||||
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 = useEvent((offset?: number) => {
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
const selections = [...editor.getSelectionRanges()];
|
||||
if (Tools.isDefine(offset)) {
|
||||
editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]);
|
||||
} else if (!selections.length && !editor.docSelectionRenderService.isOnPointerEvent) {
|
||||
const body = editor.getDocumentData().body?.dataStream ?? '\r\n';
|
||||
const offset = Math.max(body.length - 2, 0);
|
||||
editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]);
|
||||
}
|
||||
};
|
||||
}, [editor]);
|
||||
});
|
||||
|
||||
return focus;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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<Workbook>(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<Workbook>(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<ISequenceNode | string>) {
|
||||
const { formulaRefColors, numberColor, stringColor } = colorMap;
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export function buildTextRuns(descriptionService: IDescriptionService, colorMap: IColorMap, sequenceNodes: Array<ISequenceNode | string>) {
|
||||
const { formulaRefColors, numberColor, stringColor, plainTextColor } = colorMap;
|
||||
const textRuns: ITextRun[] = [];
|
||||
const refSelections: IRefSelection[] = [];
|
||||
const themeColorMap = new Map<string, string>();
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
+102
-26
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
refSelectionsService.clear();
|
||||
};
|
||||
}
|
||||
}, [contextService, isNeed, refSelectionsService]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isNeed && selecting) {
|
||||
const d1 = refSelectionsRenderService?.enableSelectionChanging();
|
||||
contextService.setContextValue(REF_SELECTIONS_ENABLED, true);
|
||||
|
||||
return () => {
|
||||
contextService.setContextValue(REF_SELECTIONS_ENABLED, false);
|
||||
d1?.dispose();
|
||||
};
|
||||
}
|
||||
}, [isNeed]);
|
||||
}, [contextService, isNeed, refSelectionsRenderService, selecting]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isNeed) {
|
||||
return () => {
|
||||
refSelectionsService.clear();
|
||||
};
|
||||
}
|
||||
}, [isNeed]);
|
||||
|
||||
//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]);
|
||||
};
|
||||
|
||||
@@ -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<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const sheet = workbook?.getActiveSheet();
|
||||
const selections = [...sheetsSelectionsService.getCurrentSelections()];
|
||||
return () => {
|
||||
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(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;
|
||||
};
|
||||
|
||||
@@ -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<ScrollBar>;
|
||||
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';
|
||||
|
||||
+4
-3
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const [rangeDialogVisible, rangeDialogVisibleSet] = useState(false);
|
||||
const [isFocus, isFocusSet] = useState(_isFocus);
|
||||
const editorId = useMemo(() => createInternalEditorID(`${RANGE_SELECTOR_SYMBOLS}-${generateRandomId(4)}`), []);
|
||||
const [editor, editorSet] = useState<Editor>();
|
||||
const editorRef = useRef<Editor>();
|
||||
const editor = editorRef.current;
|
||||
const containerRef = useRef<HTMLDivElement>(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<IRefSelection[]>([]);
|
||||
|
||||
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);
|
||||
|
||||
const handleSheetSelectionChange = useMemo(() => {
|
||||
return (text: string, offset: number, isEnd: boolean) => {
|
||||
highligh(text);
|
||||
rangeStringSet(text);
|
||||
needEmit();
|
||||
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);
|
||||
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 ?? ''));
|
||||
}
|
||||
checkScrollBar();
|
||||
}
|
||||
};
|
||||
}, [editor]);
|
||||
});
|
||||
return () => sub.dispose();
|
||||
}, [commandService, editor, editorId, onChange]);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
width="328px"
|
||||
@@ -538,7 +532,7 @@ function RangeSelectorDialog(props: {
|
||||
>
|
||||
<div className={styles.sheetRangeSelectorDialog}>
|
||||
{ranges.map((text, index) => (
|
||||
<div key={index} className={styles.sheetRangeSelectorDialogItem}>
|
||||
<div key={`${text}_${index}`} className={styles.sheetRangeSelectorDialogItem}>
|
||||
<Input
|
||||
affixWrapperStyle={{ width: '100%' }}
|
||||
placeholder={localeService.t('rangeSelector.placeHolder')}
|
||||
|
||||
@@ -24,12 +24,12 @@ export function getSheetNameById(univerInstanceService: IUniverInstanceService,
|
||||
return univerInstanceService.getUnit<Workbook>(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);
|
||||
|
||||
@@ -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<boolean> {
|
||||
@@ -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<boolean> {
|
||||
@@ -113,6 +118,7 @@ export class FRangeHyperlinkMixin extends FRange implements IFRangeHyperlinkMixi
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @deprecated
|
||||
*/
|
||||
override cancelHyperLink(id: string): boolean {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<IMoveSelectionCommandParams> = {
|
||||
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<IMoveSelectionCommandParams> = {
|
||||
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<IMoveSelectionCommandParams> = {
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
selections,
|
||||
type: SelectionMoveType.MOVE_END,
|
||||
extra,
|
||||
} as ISetSelectionsOperationParams);
|
||||
return rs;
|
||||
},
|
||||
@@ -140,9 +145,8 @@ export const MoveSelectionCommand: ICommand<IMoveSelectionCommandParams> = {
|
||||
export const MoveSelectionEnterAndTabCommand: ICommand<IMoveSelectionEnterAndTabCommandParams> = {
|
||||
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<IMoveSelectionEnterAndTab
|
||||
subUnitId: sheetId,
|
||||
type: SelectionMoveType.MOVE_END,
|
||||
selections,
|
||||
extra: params.extra,
|
||||
});
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const selectionService = renderManagerService.getRenderById(unitId)?.with(ISheetSelectionRenderService);
|
||||
@@ -312,6 +317,7 @@ export interface IExpandSelectionCommandParams {
|
||||
direction: Direction;
|
||||
jumpOver?: JumpOver;
|
||||
nextStep?: number;
|
||||
extra?: string;
|
||||
}
|
||||
|
||||
// Though the command's name is "expand-selection", it actually does not expand but shrink the selection.
|
||||
@@ -319,7 +325,7 @@ export interface IExpandSelectionCommandParams {
|
||||
export const ExpandSelectionCommand: ICommand<IExpandSelectionCommandParams> = {
|
||||
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<IExpandSelectionCommandParams> = {
|
||||
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<IExpandSelectionCommandParams> = {
|
||||
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<IExpandSelectionCommandParams> = {
|
||||
primary, // this remains unchanged
|
||||
},
|
||||
],
|
||||
extra,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IEditorBridgeServiceVisibleParam> = {
|
||||
@@ -40,7 +40,16 @@ export const SetCellEditVisibleWithF2Operation: IOperation<IEditorBridgeServiceV
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, params);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return false;
|
||||
}
|
||||
commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, {
|
||||
...params,
|
||||
unitId: workbook.getUnitId(),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user