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:
zhangw
2025-01-13 22:41:04 +08:00
committed by GitHub
co-authored by lumixraku GitHub Actions jocs
parent e53eb2c75f
commit 52b2300a1d
143 changed files with 3838 additions and 5692 deletions
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;
}
+13 -2
View File
@@ -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';
-2
View File
@@ -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));
}
-1
View File
@@ -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) {
@@ -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,
@@ -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;
}
+10
View File
@@ -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);
}
}
}
}
@@ -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();
}
}
})
@@ -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);
}
+2 -1
View File
@@ -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);
});
+5 -4
View File
@@ -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() {
+70 -172
View File
@@ -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();
+2 -2
View File
@@ -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);
}
@@ -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 {
+21 -7
View File
@@ -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;
}
@@ -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}
@@ -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) => {
+3 -8
View File
@@ -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();
@@ -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)
);
}
@@ -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);
}
}
@@ -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,
@@ -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>
)
;
@@ -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';
@@ -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';
@@ -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