refactor(range-selector): refactor the selection component (#3580)

Co-authored-by: jocs <ransixi@gmail.com>
Co-authored-by: zhangw <zhang9748@foxmail.com>
This commit is contained in:
Gpound.liu
2024-10-14 22:46:32 +08:00
committed by GitHub
co-authored by jocs zhangw
parent f9f497302d
commit e16ead3257
57 changed files with 2677 additions and 935 deletions
@@ -174,9 +174,15 @@ function RectPopup(props: IRectPopupProps) {
}, [anchorRect, anchorRect.bottom, anchorRect.left, anchorRect.right, anchorRect.top, clickOtherFn, excludeOutside]);
useEffect(() => {
window.addEventListener('contextmenu', contextMenuFn);
const handleContextMenu = (e: MouseEvent) => {
if (e.ctrlKey && e.button === 0) {
return;
}
contextMenuFn();
};
window.addEventListener('contextmenu', handleContextMenu);
return () => {
window.removeEventListener('contextmenu', contextMenuFn);
window.removeEventListener('contextmenu', handleContextMenu);
};
}, [contextMenuFn]);
@@ -51,6 +51,9 @@ 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);
@@ -66,7 +66,7 @@ export class DocBackScrollRenderController extends RxDisposable implements IRend
const { startOffset } = range;
const anchorNodePosition = skeleton.findNodePositionByCharIndex(startOffset);
this.scrollToNode(anchorNodePosition);
anchorNodePosition && this.scrollToNode(anchorNodePosition);
}
scrollToNode(startNodePosition: Nullable<INodePosition>) {
@@ -278,7 +278,7 @@ export class EditorService extends Disposable implements IEditorService, IDispos
this._contextService.setContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, editor.isSingle());
if (!this._spreadsheetFocusState) {
this.singleSelection(editor.isSingleChoice());
this.singleSelection(!!editor.isSingleChoice());
}
this._focusStyle$.next(editorUnitId);
@@ -18,7 +18,9 @@ import type { DocumentDataModel, ICommandService, IDocumentData, IDocumentStyle,
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 { KeyCode } from '@univerjs/ui';
import { merge, type Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { ReplaceSnapshotCommand } from '../../commands/commands/replace-content.command';
import { DocSelectionRenderService, type IEditorInputConfig } from '../selection/doc-selection-render.service';
@@ -222,7 +224,13 @@ export class Editor extends Disposable implements IEditor {
this.disposeWithMe(
merge(
docSelectionRenderService.onInput$,
docSelectionRenderService.onKeydown$,
docSelectionRenderService.onKeydown$.pipe(filter((e) => {
const event = e.event as KeyboardEvent;
if (event.ctrlKey || event.metaKey) {
return [KeyCode.X, KeyCode.V].includes(event.keyCode);
}
return [KeyCode.BACKSPACE].includes(event.keyCode);
})),
docSelectionRenderService.onCompositionupdate$,
docSelectionRenderService.onCompositionend$,
docSelectionRenderService.onPaste$
@@ -14,6 +14,9 @@
* limitations under the License.
*/
import { ICommandService, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
import { NORMAL_TEXT_SELECTION_PLUGIN_STYLE } from '@univerjs/engine-render';
import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
import type { DocumentDataModel, Nullable } from '@univerjs/core';
import type {
IDocSelectionInnerParam,
@@ -21,9 +24,6 @@ import type {
ISuccinctDocRangeParam,
ITextRangeWithStyle,
} from '@univerjs/engine-render';
import { ICommandService, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
import { NORMAL_TEXT_SELECTION_PLUGIN_STYLE } from '@univerjs/engine-render';
import { BehaviorSubject, takeUntil } from 'rxjs';
import { SetTextSelectionsOperation } from '../commands/operations/text-selection.operation';
interface IDocSelectionManagerSearchParam {
@@ -51,7 +51,7 @@ export class DocSelectionManagerService extends RxDisposable {
private readonly _textSelectionInfo: ITextSelectionInfo = new Map();
private readonly _textSelection$ = new BehaviorSubject<Nullable<ITextSelectionManagerInsertParam>>(null);
private readonly _textSelection$ = new Subject<ITextSelectionManagerInsertParam>();
readonly textSelection$ = this._textSelection$.asObservable();
private readonly _refreshSelection$ = new BehaviorSubject<Nullable<IRefreshSelectionParam>>(null);
@@ -75,12 +75,14 @@
"@univerjs/protocol": "0.1.39-alpha.15",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-conditional-formatting": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"clsx": "^2.1.1",
"react-grid-layout": "^1.4.4",
"react-resizable": "^3.0.5"
},
"devDependencies": {
"@types/react-grid-layout": "^1.3.5",
"@univerjs-infra/shared": "workspace:*",
@@ -14,22 +14,25 @@
* limitations under the License.
*/
import { createInternalEditorID, ICommandService, InterceptorManager, IUniverInstanceService, LocaleService, UniverInstanceType, useDependency } from '@univerjs/core';
import { Button, Select } from '@univerjs/design';
import { RangeSelector } from '@univerjs/docs-ui';
import { serializeRange } from '@univerjs/engine-formula';
import { RemoveSheetMutation, setEndForRange, SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
import { CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel, SHEET_CONDITIONAL_FORMATTING_PLUGIN } from '@univerjs/sheets-conditional-formatting';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import type { IRange, IUnitRange, Workbook } from '@univerjs/core';
import type { IRange, Workbook } from '@univerjs/core';
import type { IRemoveSheetMutationParams } from '@univerjs/sheets';
import type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import type { IAddCfCommandParams } from '../../../commands/commands/add-cf.command';
import type { ISetCfCommandParams } from '../../../commands/commands/set-cf.command';
import type { IStyleEditorProps } from './type';
import { ICommandService, InterceptorManager, IUniverInstanceService, LocaleService, UniverInstanceType, useDependency } from '@univerjs/core';
import { Button, Select } from '@univerjs/design';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import { RemoveSheetMutation, setEndForRange, SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
import { CFRuleType, CFSubRuleType, ConditionalFormattingRuleModel } from '@univerjs/sheets-conditional-formatting';
import { RangeSelector } from '@univerjs/sheets-formula-ui';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { AddCfCommand } from '../../../commands/commands/add-cf.command';
import { SetCfCommand } from '../../../commands/commands/set-cf.command';
import styleBase from '../index.module.less';
import { ColorScaleStyleEditor } from './colorScale';
import { DataBarStyleEditor } from './dataBar';
import { FormulaStyleEditor } from './formula';
import { HighlightCellStyleEditor } from './highlightCell';
@@ -37,9 +40,6 @@ import { IconSet } from './iconSet';
import styles from './index.module.less';
import { RankStyleEditor } from './rank';
import { beforeSubmit, submit } from './type';
import type { IAddCfCommandParams } from '../../../commands/commands/add-cf.command';
import type { ISetCfCommandParams } from '../../../commands/commands/set-cf.command';
import type { IStyleEditorProps } from './type';
interface IRuleEditProps {
rule?: IConditionFormattingRule;
@@ -58,6 +58,16 @@ export const RuleEdit = (props: IRuleEditProps) => {
const unitId = getUnitId(univerInstanceService);
const subUnitId = getSubUnitId(univerInstanceService);
const [isFocusRangeSelector, isFocusRangeSelectorSet] = useState(() => {
// 如果是自定义公式的话,则不自动聚焦在选区.
const { rule } = props;
if (rule?.rule.type === CFRuleType.highlightCell) {
return rule.rule.subType !== CFSubRuleType.formula;
}
return true;
});
const rangeSelectorActionsRef = useRef<Parameters<typeof RangeSelector>[0]['actions']>({});
const [errorText, errorTextSet] = useState<string | undefined>(undefined);
const rangeResult = useRef<IRange[]>(props.rule?.ranges ?? []);
const rangeString = useMemo(() => {
@@ -174,11 +184,15 @@ export const RuleEdit = (props: IRuleEditProps) => {
result.current = config as Parameters<IStyleEditorProps['onChange']>;
};
const onRangeSelectorChange = (ranges: IUnitRange[]) => {
rangeResult.current = ranges.map((r) => r.range);
const onRangeSelectorChange = (rangeString: string) => {
const result = rangeString.split(',').map(deserializeRangeWithSheet).map((item) => item.range);
rangeResult.current = result;
};
const handleSubmit = () => {
if (errorText) {
return;
}
const getRanges = () => {
const worksheet = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet();
if (!worksheet) {
@@ -213,21 +227,41 @@ export const RuleEdit = (props: IRuleEditProps) => {
}
}
};
const handleCancel = () => {
props.onCancel();
};
const handleVerify = (v: boolean, rangeText: string) => {
if (v) {
if (rangeText.length < 1) {
errorTextSet(localeService.t('sheet.cf.errorMessage.rangeError'));
} else {
errorTextSet(undefined);
}
} else {
errorTextSet(localeService.t('sheet.cf.errorMessage.rangeError'));
}
};
const handlePanelClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleOutClick = rangeSelectorActionsRef.current?.handleOutClick;
handleOutClick && handleOutClick(e, isFocusRangeSelectorSet);
};
return (
<div className={styles.cfRuleStyleEditor}>
<div className={styles.cfRuleStyleEditor} onClick={handlePanelClick}>
<div className={styleBase.title}>{localeService.t('sheet.cf.panel.range')}</div>
<div className={`${styleBase.mTBase}`}>
<RangeSelector
placeholder={localeService.t('sheet.cf.form.rangeSelector')}
width={'100%' as unknown as number}
openForSheetSubUnitId={subUnitId}
openForSheetUnitId={unitId}
value={rangeString}
id={createInternalEditorID(`${SHEET_CONDITIONAL_FORMATTING_PLUGIN}_rangeSelector`)}
unitId={unitId}
errorText={errorText}
subUnitId={subUnitId}
initValue={rangeString}
onChange={onRangeSelectorChange}
onVerify={handleVerify}
isFocus={isFocusRangeSelector}
actions={rangeSelectorActionsRef.current}
/>
</div>
<div className={styleBase.title}>{localeService.t('sheet.cf.panel.styleType')}</div>
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import type { IDisposable } from '@univerjs/core';
import type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { Disposable, generateRandomId, Inject, Injector, IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
import { ComponentManager, ISidebarService } from '@univerjs/ui';
import type { IDisposable } from '@univerjs/core';
import type { IConditionFormattingRule } from '@univerjs/sheets-conditional-formatting';
import { ConditionFormattingPanel } from '../components/panel';
const CF_PANEL_KEY = 'sheet.conditional.formatting.panel';
@@ -41,10 +41,20 @@ export class ConditionalFormattingPanelController extends Disposable {
if (!sheet) this._sidebarDisposable?.dispose();
})
);
this.disposeWithMe(this._sidebarService.sidebarOptions$.subscribe((info) => {
if (info.id === CF_PANEL_KEY) {
if (!info.visible) {
setTimeout(() => {
this._sidebarService.sidebarOptions$.next({ visible: false });
});
}
}
}));
}
openPanel(rule?: IConditionFormattingRule) {
const props = {
id: CF_PANEL_KEY,
header: { title: this._localeService.t('sheet.cf.title') },
children: {
label: CF_PANEL_KEY,
@@ -171,6 +171,7 @@ const locale: typeof zhCN = {
errorMessage: {
notBlank: 'Condition can not be empty',
formulaError: 'Wrong formula',
rangeError: 'Bad selection',
},
},
},
@@ -170,6 +170,7 @@ const locale: typeof zhCN = {
errorMessage: {
notBlank: 'شرط نمی‌تواند خالی باشد',
formulaError: 'فرمول اشتباه',
rangeError: 'Bad selection',
},
},
},
@@ -170,7 +170,7 @@ const locale: typeof zhCN = {
errorMessage: {
notBlank: 'Условие не может быть пустым',
formulaError: 'Ошибка формулы',
rangeError: 'Bad selection',
},
},
},
@@ -170,6 +170,7 @@ const locale: typeof zhCN = {
errorMessage: {
notBlank: 'Điều kiện không được để trống',
formulaError: 'Lỗi công thức',
rangeError: 'Bad selection',
},
},
},
@@ -169,6 +169,7 @@ const locale = {
},
errorMessage: {
notBlank: '条件不能为空',
rangeError: '选区错误',
formulaError: '公式错误',
},
},
@@ -171,6 +171,7 @@ const locale: typeof zhCN = {
},
errorMessage: {
notBlank: '條件不能為空',
rangeError: '選區錯誤',
formulaError: '公式錯誤',
},
},
@@ -78,8 +78,10 @@
"@univerjs/protocol": "0.1.39-alpha.15",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-data-validation": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
"clsx": "^2.1.1",
"dayjs": "^1.11.13"
@@ -18,6 +18,7 @@ import type { IDataValidationRule, IDisposable, Nullable } from '@univerjs/core'
import { Disposable, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core';
import { ISidebarService } from '@univerjs/ui';
import { BehaviorSubject, distinctUntilChanged, filter } from 'rxjs';
import { DATA_VALIDATION_PANEL } from '../commands/operations/data-validation.operation';
export class DataValidationPanelService extends Disposable {
private _open$ = new BehaviorSubject<boolean>(false);
@@ -49,6 +50,16 @@ export class DataValidationPanelService extends Disposable {
this.close();
})
);
this.disposeWithMe(this._sidebarService.sidebarOptions$.subscribe((info) => {
if (info.id === DATA_VALIDATION_PANEL) {
if (!info.visible) {
setTimeout(() => {
this._sidebarService.sidebarOptions$.next({ visible: false });
});
}
}
}));
}
override dispose(): void {
@@ -14,19 +14,20 @@
* limitations under the License.
*/
import type { DataValidationOperator, DataValidationType, IDataValidationRuleBase, IDataValidationRuleOptions, IExecutionOptions, ISheetDataValidationRule, IUnitRange } from '@univerjs/core';
import type { DataValidationOperator, DataValidationType, IDataValidationRuleBase, IDataValidationRuleOptions, IExecutionOptions, ISheetDataValidationRule, IUnitRange, Workbook } from '@univerjs/core';
import type { IUpdateSheetDataValidationRangeCommandParams } from '@univerjs/sheets-data-validation';
import { createInternalEditorID, debounce, ICommandService, isUnitRangesEqual, isValidRange, LocaleService, RedoCommand, shallowEqual, UndoCommand, useDependency } from '@univerjs/core';
import { debounce, ICommandService, isUnitRangesEqual, IUniverInstanceService, LocaleService, RedoCommand, shallowEqual, UndoCommand, UniverInstanceType, useDependency } from '@univerjs/core';
import { DataValidationModel, DataValidatorRegistryScope, DataValidatorRegistryService, getRuleOptions, getRuleSetting, TWO_FORMULA_OPERATOR_COUNT } from '@univerjs/data-validation';
import { Button, FormLayout, Select } from '@univerjs/design';
import { RangeSelector } from '@univerjs/docs-ui';
import { serializeRange } from '@univerjs/engine-formula';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import { SheetsSelectionsService } from '@univerjs/sheets';
import { RemoveSheetDataValidationCommand, UpdateSheetDataValidationOptionsCommand, UpdateSheetDataValidationRangeCommand, UpdateSheetDataValidationSettingCommand } from '@univerjs/sheets-data-validation';
import { RangeSelector } from '@univerjs/sheets-formula-ui';
import { ComponentManager, useEvent, useObservable } from '@univerjs/ui';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { DataValidationPanelService } from '../../../services/data-validation-panel.service';
import { DataValidationOptions } from '../options';
import styles from './index.module.less';
// debounce execute commands, for better redo-undo experience
@@ -39,7 +40,12 @@ const debounceExecuteFactory = (commandService: ICommandService) => debounce(
},
1000
);
function getSheetIdByName(univerInstanceService: IUniverInstanceService, unitId: string, name: string) {
if (unitId) {
return univerInstanceService.getUnit<Workbook>(unitId)?.getSheetBySheetName(name)?.getSheetId() || '';
}
return univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)?.getSheetBySheetName(name)?.getSheetId() || '';
}
export function DataValidationDetail() {
const [key, setKey] = useState(0);
const dataValidationPanelService = useDependency(DataValidationPanelService);
@@ -47,6 +53,8 @@ export function DataValidationDetail() {
const { unitId, subUnitId, rule } = activeRuleInfo || {};
const ruleId = rule.uid;
const validatorService = useDependency(DataValidatorRegistryService);
const univerInstanceService = useDependency(IUniverInstanceService);
const componentManager = useDependency(ComponentManager);
const commandService = useDependency(ICommandService);
const dataValidationModel = useDependency(DataValidationModel);
@@ -58,6 +66,9 @@ export function DataValidationDetail() {
const [localRanges, setLocalRanges] = useState<IUnitRange[]>(() => localRule.ranges.map((i) => ({ unitId: '', sheetId: '', range: i })));
const debounceExecute = useMemo(() => debounceExecuteFactory(commandService), [commandService]);
const rangeSelectorActionsRef = useRef<Parameters<typeof RangeSelector>[0]['actions']>({});
const [isFocusRangeSelector, isFocusRangeSelectorSet] = useState(false);
const sheetSelectionService = useDependency(SheetsSelectionsService);
useEffect(() => {
@@ -103,7 +114,18 @@ export function DataValidationDetail() {
}
};
const handleUpdateRuleRanges = useEvent((unitRanges: IUnitRange[]) => {
const handleUpdateRuleRanges = useEvent((rangeText: string) => {
const unitRanges = rangeText.split(',').map(deserializeRangeWithSheet).map((unitRange) => {
const sheetName = unitRange.sheetName;
if (sheetName) {
const sheetId = getSheetIdByName(univerInstanceService, unitRange.unitId, sheetName);
return { ...unitRange, sheetId };
}
return {
...unitRange, sheetId: '',
};
});
if (isUnitRangesEqual(unitRanges, localRanges)) {
return;
}
@@ -222,13 +244,27 @@ export function DataValidationDetail() {
}
);
};
const handlePanelClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleOutClick = rangeSelectorActionsRef.current?.handleOutClick;
handleOutClick && handleOutClick(e, isFocusRangeSelectorSet);
};
return (
<div className={styles.dataValidationDetail}>
<div className={styles.dataValidationDetail} onClick={handlePanelClick}>
<FormLayout
label={localeService.t('dataValidation.panel.range')}
error={!localRule.ranges.length ? localeService.t('dataValidation.panel.rangeError') : ''}
>
<RangeSelector
unitId={unitId}
subUnitId={subUnitId}
initValue={rangeStr}
onChange={handleUpdateRuleRanges}
isFocus={isFocusRangeSelector}
actions={rangeSelectorActionsRef.current}
/>
{/* <RangeSelector
key={key}
className={styles.dataValidationDetailFormItem}
value={rangeStr}
@@ -243,7 +279,7 @@ export function DataValidationDetail() {
handleUpdateRuleRanges(newRange);
}}
/>
/> */}
</FormLayout>
<FormLayout label={localeService.t('dataValidation.panel.type')}>
<Select
+2 -1
View File
@@ -79,7 +79,8 @@
"@univerjs/sheets-formula": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
"@univerjs/ui": "workspace:*",
"clsx": "^2.1.1"
},
"devDependencies": {
"@types/react": "^18.3.11",
@@ -90,13 +90,13 @@ import {
ExpandSelectionCommand,
getEditorObject,
IEditorBridgeService,
isRangeSelector,
JumpOver,
MoveSelectionCommand,
SheetSkeletonManagerService,
} from '@univerjs/sheets-ui';
import { IContextMenuService, ILayoutService, KeyCode, MetaKeys, SetEditorResizeOperation, UNI_DISABLE_CHANGING_FOCUS_KEY } from '@univerjs/ui';
import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs';
import { distinctUntilChanged, distinctUntilKeyChanged, filter } from 'rxjs';
import { SelectEditorFormulaOperation } from '../commands/operations/editor-formula.operation';
import { HelpFunctionOperation } from '../commands/operations/help-function.operation';
import { ReferenceAbsoluteOperation } from '../commands/operations/reference-absolute.operation';
@@ -254,31 +254,27 @@ export class PromptController extends Disposable {
this.disposeWithMe(
this._docSelectionManagerService.textSelection$
.pipe(
distinctUntilChanged(
(prev, curr) =>
prev?.unitId === curr?.unitId &&
prev?.segmentId === curr?.segmentId &&
Boolean(prev?.textRanges.every((t, i) => t.startOffset === curr?.textRanges[i].startOffset && t.endOffset === curr?.textRanges[i].endOffset))
)
filter((item) => {
return !isRangeSelector(item.unitId);
})
)
.subscribe((params) => {
if (params?.unitId == null) {
return;
}
const editor = this._editorService.getEditor(params.unitId);
if (!editor
|| editor.onlyInputContent()
|| (editor.isSheetEditor() && !this._isFormulaEditorActivated())
// Remove this latter.
|| editor.params.scrollBar
|| editor.onlyInputContent()
|| (editor.isSheetEditor() && !this._isFormulaEditorActivated())
// Remove this latter.
|| editor.params.scrollBar
) {
return;
}
const onlyInputRange = editor.onlyInputRange();
// @ts-ignore
// @ts-ignore
if (params?.options?.fromSelection) {
return;
} else {
@@ -298,7 +294,7 @@ export class PromptController extends Disposable {
return;
}
// TODO@Dushusir: use real text info
// TODO@Dushusir: use real text info
this._changeFunctionPanelState();
})
);
@@ -314,6 +310,10 @@ export class PromptController extends Disposable {
return;
}
if (isRangeSelector(unitId)) {
return;
}
if (this._listenInputCache.has(unitId)) {
return;
}
@@ -385,6 +385,9 @@ export class PromptController extends Disposable {
const editorId = documentDataModel.getUnitId();
if (isRangeSelector(editorId)) {
return;
}
if (!this._editorService.isEditor(editorId) || this._previousEditorUnitId === editorId) {
return;
}
@@ -1560,7 +1563,9 @@ export class PromptController extends Disposable {
const nodeIndex = Number(id);
const currentNode = this._formulaPromptService.getCurrentSequenceNodeByIndex(nodeIndex);
if (!currentNode) {
return;
}
let refType: IAbsoluteRefTypeForRange = { startAbsoluteRefType: AbsoluteRefType.NONE };
if (typeof currentNode !== 'string') {
const token = (currentNode as ISequenceNode).token;
+2
View File
@@ -21,6 +21,8 @@ export { InsertFunctionOperation } from './commands/operations/insert-function.o
export { MoreFunctionsOperation } from './commands/operations/more-functions.operation';
export { ReferenceAbsoluteOperation } from './commands/operations/reference-absolute.operation';
export { SearchFunctionOperation } from './commands/operations/search-function.operation';
export { RangeSelector } from './views/range-selector/index';
export { RefSelectionsRenderService } from './services/render-services/ref-selections.render-service';
// #region - all commands
@@ -22,6 +22,7 @@ import { ScrollTimerType, SHEET_VIEWPORT_KEY, Vector2 } from '@univerjs/engine-r
import { convertSelectionDataToRange, getNormalSelectionStyle, IRefSelectionsService, SelectionMoveType } from '@univerjs/sheets';
import { attachSelectionWithCoord, BaseSelectionRenderService, checkInHeaderRanges, getAllSelection, getCoordByOffset, getSheetObject, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { IShortcutService } from '@univerjs/ui';
import { merge } from 'rxjs';
/**
* This service extends the existing `SelectionRenderService` to provide the rendering of prompt selections
@@ -71,6 +72,11 @@ export class RefSelectionsRenderService extends BaseSelectionRenderService imple
this._remainLastEnabled = enabled;
}
/**
* This is set to true when you need to add a new selection.
* @param {boolean} enabled
* @memberof RefSelectionsRenderService
*/
setSkipLastEnabled(enabled: boolean): void {
this._skipLastEnabled = enabled;
}
@@ -146,7 +152,10 @@ export class RefSelectionsRenderService extends BaseSelectionRenderService imple
const selectionData = this.attachSelectionWithCoord(selectionWithStyle);
this._addSelectionControlBySelectionData(selectionData);
this._selectionMoveStart$.next(this.getSelectionDataWithStyle());
const dispose = scene.onPointerUp$.subscribeEvent(() => {
dispose.unsubscribe();
this._selectionMoveEnd$.next(this.getSelectionDataWithStyle());
});
if (evt.button !== 2) {
state.stopPropagation();
}
@@ -187,7 +196,7 @@ export class RefSelectionsRenderService extends BaseSelectionRenderService imple
// Changing the selection area through the 8 control points of the ref selection will not trigger this subscriber.
// beforeSelectionMoveEnd$ & selectionMoveEnd$ would triggered when change skeleton(change sheet).
this.disposeWithMe(this._workbookSelections.selectionMoveEnd$.subscribe((selectionsWithStyles) => {
this.disposeWithMe(merge(this._workbookSelections.selectionMoveEnd$, this._workbookSelections.selectionSet$).subscribe((selectionsWithStyles) => {
this._reset();
// The selections' style would be colorful here. PromptController would change the color of selections later.
@@ -20,6 +20,8 @@ import { DependentOn, IConfigService, Inject, Injector, Plugin, UniverInstanceTy
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
import { RANGE_SELECTOR_COMPONENT_KEY } from '@univerjs/sheets-ui';
import { ComponentManager } from '@univerjs/ui';
import { FORMULA_UI_PLUGIN_NAME } from './common/plugin-name';
import {
defaultPluginBaseConfig,
@@ -34,6 +36,7 @@ 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 { RangeSelector } from './views/range-selector';
/**
* The configuration of the formula UI plugin.
@@ -88,6 +91,10 @@ export class UniverSheetsFormulaUIPlugin extends Plugin {
this._injector.get(FormulaClipboardController);
this._injector.get(FormulaRenderManagerController);
const componentManager = this._injector.get(ComponentManager);
componentManager.register(RANGE_SELECTOR_COMPONENT_KEY, RangeSelector);
}
override onSteady(): void {
@@ -0,0 +1,59 @@
/**
* 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 { useDependency } from '@univerjs/core';
import { matchToken } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { useEffect } from 'react';
import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service';
/**
* add selections by inputting a `,`.
* @param {string} unitId
* @param {string} rangeString
* @param {Editor} [editor]
*/
export const useEditorInput = (unitId: string, rangeString: string, editor?: Editor) => {
const renderManagerService = useDependency(IRenderManagerService);
const render = renderManagerService.getRenderById(unitId);
const refSelectionsRenderService = render?.with(RefSelectionsRenderService);
useEffect(() => {
if (editor && refSelectionsRenderService) {
const d = editor.input$.subscribe((e) => {
const content = e.content;
if (content === matchToken.COMMA) {
refSelectionsRenderService.setSkipLastEnabled(true);
} else {
refSelectionsRenderService.setSkipLastEnabled(false);
}
});
return () => {
d.unsubscribe();
};
}
}, [editor, refSelectionsRenderService]);
useEffect(() => {
if (!refSelectionsRenderService) {
return;
}
if (!rangeString.endsWith(matchToken.COMMA)) {
refSelectionsRenderService.setSkipLastEnabled(false);
}
}, [rangeString, refSelectionsRenderService]);
};
@@ -0,0 +1,35 @@
/**
* 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 { useDependency } from '@univerjs/core';
import { LexerTreeBuilder } from '@univerjs/engine-formula';
import { useEffect, useState } from 'react';
import type { ISequenceNode } from '@univerjs/engine-formula';
export const useFormulaToken = (text: string) => {
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
const [sequenceNodes, sequenceNodesSet] = useState<(string | ISequenceNode)[]>([]);
useEffect(() => {
sequenceNodesSet(lexerTreeBuilder.sequenceNodesBuilder(text) ?? []);
}, [text]);
return {
sequenceNodes,
sequenceNodesSet,
};
};
@@ -0,0 +1,265 @@
/**
* 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 { ITextRun, Workbook } from '@univerjs/core';
import type { ISequenceNode } from '@univerjs/engine-formula';
import type { ISelectionStyle, ISelectionWithStyle } from '@univerjs/sheets';
import { ColorKit, IUniverInstanceService, ThemeService, useDependency } from '@univerjs/core';
import { IEditorService } from '@univerjs/docs-ui';
import { deserializeRangeWithSheet, sequenceNodeType } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { IRefSelectionsService, setEndForRange } from '@univerjs/sheets';
import { IDescriptionService } from '@univerjs/sheets-formula';
import { attachRangeWithCoord, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
import { useEffect, useMemo, useState } from 'react';
import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service';
interface IRefSelection {
refIndex: number;
themeColor: string;
token: string;
}
/**
* @param {string} unitId
* @param {string} subUnitId 打开面板的时候传入的 sheetId
* @param {IRefSelection[]} refSelections
*/
export function useSheetHighlight(isNeed: boolean, unitId: string, subUnitId: string, refSelections: IRefSelection[]) {
const univerInstanceService = useDependency(IUniverInstanceService);
const themeService = useDependency(ThemeService);
const refSelectionsService = useDependency(IRefSelectionsService);
const renderManagerService = useDependency(IRenderManagerService);
const render = renderManagerService.getRenderById(unitId);
const refSelectionsRenderService = render?.with(RefSelectionsRenderService);
const sheetSkeletonManagerService = render?.with(SheetSkeletonManagerService);
const [ranges, rangesSet] = useState<ISelectionWithStyle[]>([]);
useEffect(() => {
const workbook = univerInstanceService.getUnit<Workbook>(unitId);
const worksheet = workbook?.getActiveSheet();
const selectionWithStyle: ISelectionWithStyle[] = [];
if (!workbook || !worksheet || !isNeed) {
rangesSet(selectionWithStyle);
return;
}
const currentSheetId = worksheet?.getSheetId();
const getSheetIdByName = (name: string) => workbook?.getSheetBySheetName(name)?.getSheetId();
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: undefined,
style: getFormulaRefSelectionStyle(themeService, themeColor, refIndex.toString()),
});
}
rangesSet(selectionWithStyle);
}, [unitId, subUnitId, refSelections, isNeed]);
useEffect(() => {
const skeleton = sheetSkeletonManagerService?.getCurrentSkeleton();
if (skeleton) {
const allControls = refSelectionsRenderService?.getSelectionControls() || [];
if (allControls.length === ranges.length) {
allControls.forEach((control, index) => {
const selection = ranges[index];
const primaryCell = skeleton.getCellByIndex(selection.range.startRow, selection.range.startColumn);
control.updateRange(attachRangeWithCoord(skeleton, selection.range), primaryCell);
control.updateStyle(selection.style!);
});
} else {
refSelectionsService.setSelections(ranges);
}
}
}, [ranges, sheetSkeletonManagerService]);
useEffect(() => () => {
refSelectionsService.setSelections([]);
}, []);
}
export function useDocHight(editorId: string, sequenceNodes: (string | ISequenceNode)[]) {
const editorService = useDependency(IEditorService);
const descriptionService = useDependency(IDescriptionService);
const colorMap = useColor();
const [ranges, rangesSet] = useState<IRefSelection[]>([]);
useEffect(() => {
const editor = editorService.getEditor(editorId);
if (!editor) {
return;
}
const data = editor.getDocumentData();
if (!data) {
return;
}
const body = data.body;
if (!body) {
return;
}
const cloneBody = { dataStream: '', ...data.body };
if (sequenceNodes == null || sequenceNodes.length === 0) {
cloneBody.textRuns = [];
cloneBody.dataStream = '\r\n';
rangesSet([]);
} else {
const { textRuns, refSelections } = buildTextRuns(descriptionService, colorMap, sequenceNodes);
cloneBody.textRuns = textRuns;
const text = sequenceNodes.reduce((pre, cur) => {
if (typeof cur === 'string') {
return `${pre}${cur}`;
}
return `${pre}${cur.token}`;
}, '');
cloneBody.dataStream = `${text}\r\n`;
rangesSet(refSelections);
}
// Switching between uppercase and lowercase will trigger a reflow, causing the cursor to be misplaced. Let's refresh the cursor position here.
const selection = editor.getSelectionRanges();
const cloneData = { ...data, body: cloneBody };
editor.setDocumentData(cloneData, selection);
}, [editorId, sequenceNodes, colorMap]);
return ranges;
}
export function useColor() {
const themeService = useDependency(ThemeService);
const style = themeService.getCurrentTheme();
const result = useMemo(() => {
const formulaRefColors = [
style.loopColor1,
style.loopColor2,
style.loopColor3,
style.loopColor4,
style.loopColor5,
style.loopColor6,
style.loopColor7,
style.loopColor8,
style.loopColor9,
style.loopColor10,
style.loopColor11,
style.loopColor12,
];
const numberColor = style.hyacinth700;
const stringColor = style.verdancy800;
return { formulaRefColors, numberColor, stringColor };
}, [style]);
return result;
}
export function buildTextRuns(descriptionService: IDescriptionService, colorMap: {
formulaRefColors: string[];
numberColor: string;
stringColor: string;
}, sequenceNodes: Array<ISequenceNode | string>) {
const { formulaRefColors, numberColor, stringColor } = colorMap;
const textRuns: ITextRun[] = [];
const refSelections: IRefSelection[] = [];
const themeColorMap = new Map<string, string>();
let refColorIndex = 0;
for (let i = 0, len = sequenceNodes.length; i < len; i++) {
const node = sequenceNodes[i];
if (typeof node === 'string') {
const theLastItem = textRuns[textRuns.length - 1];
const start = theLastItem ? (theLastItem.ed) : 0;
const end = start + node.length;
textRuns.push({
st: start,
ed: end,
});
continue;
}
if (descriptionService.hasDefinedNameDescription(node.token.trim())) {
continue;
}
const { startIndex, endIndex, nodeType, token } = node;
let themeColor = '';
if (nodeType === sequenceNodeType.REFERENCE) {
if (themeColorMap.has(token)) {
themeColor = themeColorMap.get(token)!;
} else {
const colorIndex = refColorIndex % formulaRefColors.length;
themeColor = formulaRefColors[colorIndex];
themeColorMap.set(token, themeColor);
refColorIndex++;
}
refSelections.push({
refIndex: i,
themeColor,
token,
});
} else if (nodeType === sequenceNodeType.NUMBER) {
themeColor = numberColor;
} else if (nodeType === sequenceNodeType.STRING) {
themeColor = stringColor;
} else if (nodeType === sequenceNodeType.ARRAY) {
themeColor = stringColor;
}
if (themeColor && themeColor.length > 0) {
textRuns.push({
st: startIndex,
ed: endIndex + 1,
ts: {
cl: {
rgb: themeColor,
},
},
});
}
}
return { textRuns, refSelections };
};
function getFormulaRefSelectionStyle(themeService: ThemeService, refColor: string, id: string): ISelectionStyle {
const style = themeService.getCurrentTheme();
const fill = new ColorKit(refColor).setAlpha(0.05).toRgbString();
return {
id,
strokeWidth: 1,
stroke: refColor,
fill,
widgets: { tl: true, tc: true, tr: true, ml: true, mr: true, bl: true, bc: true, br: true },
widgetSize: 6,
widgetStrokeWidth: 1,
widgetStroke: style.colorWhite,
hasAutoFill: false,
hasRowHeader: false,
hasColumnHeader: false,
};
}
@@ -0,0 +1,25 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useLayoutEffect, useRef } from 'react';
export const useIsFirstRender = () => {
const isFirstRef = useRef(true);
useLayoutEffect(() => {
isFirstRef.current = false;
}, []);
return isFirstRef;
};
@@ -0,0 +1,92 @@
/**
* 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 { IContextService, IUniverInstanceService, UniverInstanceType, useDependency } from '@univerjs/core';
import { IRenderManagerService } from '@univerjs/engine-render';
import { DISABLE_NORMAL_SELECTIONS, SheetsSelectionsService } from '@univerjs/sheets';
import { IEditorBridgeService } from '@univerjs/sheets-ui';
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, isOnlyOneRange: boolean) => {
const renderManagerService = useDependency(IRenderManagerService);
const univerInstanceService = useDependency(IUniverInstanceService);
const contextService = useDependency(IContextService);
const editorBridgeService = useDependency(IEditorBridgeService);
const sheetsSelectionsService = useDependency(SheetsSelectionsService);
const contextMenuService = useDependency(IContextMenuService);
const render = renderManagerService.getRenderById(unitId);
const refSelectionsRenderService = render?.with(RefSelectionsRenderService);
useEffect(() => {
if (isNeed) {
const d1 = refSelectionsRenderService?.enableSelectionChanging();
contextService.setContextValue(DISABLE_NORMAL_SELECTIONS, true);
editorBridgeService.enableForceKeepVisible();
return () => {
d1?.dispose();
contextService.setContextValue(DISABLE_NORMAL_SELECTIONS, false);
editorBridgeService.disableForceKeepVisible();
};
}
}, [isNeed]);
// reset selection
useLayoutEffect(() => {
if (isNeed) {
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);
}
};
}
}, [isNeed]);
//right context controller
useEffect(() => {
if (isNeed) {
contextMenuService.disable();
return () => {
contextMenuService.enable();
};
}
}, [isNeed]);
// reset setSkipLastEnabled
useEffect(() => {
if (isNeed) {
refSelectionsRenderService?.setSkipLastEnabled(false);
}
}, [isNeed]);
useEffect(() => {
if (isOnlyOneRange) {
refSelectionsRenderService?.setRemainLastEnabled(false);
} else {
refSelectionsRenderService?.setRemainLastEnabled(true);
}
}, [isOnlyOneRange]);
};
@@ -0,0 +1,102 @@
/**
* 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 { 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 };
};
@@ -0,0 +1,222 @@
/**
* 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.
*/
/* eslint-disable max-lines-per-function */
import type { Workbook } from '@univerjs/core';
import type { ISelectionWithCoordAndStyle } from '@univerjs/sheets';
import type { INode } from '../utils/filterReferenceNode';
import { debounce, DisposableCollection, IUniverInstanceService, useDependency } from '@univerjs/core';
import { deserializeRangeWithSheet, matchToken, sequenceNodeType, serializeRange, serializeRangeWithSheet } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { useEffect, useMemo, useRef } from 'react';
import { distinctUntilChanged, map } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { RefSelectionsRenderService } from '../../../services/render-services/ref-selections.render-service';
import { filterReferenceNode, isComma } from '../utils/filterReferenceNode';
import { rangePreProcess } from '../utils/rangePreProcess';
import { sequenceNodeToText } from '../utils/sequenceNodeToText';
import { getSheetNameById, unitRangesToText } from '../utils/unitRangesToText';
export const useSheetSelectionChange = (isNeed: boolean,
unitId: string,
_subUnitId: string,
sequenceNodes: INode[],
isSupportAcrossSheet: boolean,
isOnlyOneRange: boolean,
handleRangeChange: (refString: string, offset: number) => void) => {
const renderManagerService = useDependency(IRenderManagerService);
const univerInstanceService = useDependency(IUniverInstanceService);
const isScalingRef = useRef(false);
const debounceReset = useMemo(() => debounce(() => {
isScalingRef.current = false;
}, 300), []);
const setIsScaling = () => {
isScalingRef.current = true;
debounceReset();
};
const render = renderManagerService.getRenderById(unitId);
const refSelectionsRenderService = render?.with(RefSelectionsRenderService);
const oldFilterReferenceNodes = useRef<INode[]>([]);
const filterReferenceNodes = useMemo(() => {
const newFilterReferenceNodes = filterReferenceNode(sequenceNodes);
const old = oldFilterReferenceNodes.current;
if (newFilterReferenceNodes.length === old.length) {
old.splice(0);
old.push(...newFilterReferenceNodes);
return old;
}
return newFilterReferenceNodes;
}, [sequenceNodes]);
oldFilterReferenceNodes.current = filterReferenceNodes;
useEffect(() => {
if (isNeed && refSelectionsRenderService) {
let isFirst = true;
const handleSelectionsChange = (selections: ISelectionWithCoordAndStyle[]) => {
if (isFirst) {
isFirst = false;
return;
}
const cloneSelectionList = [...selections];
const workbook = univerInstanceService.getUnit<Workbook>(unitId);
const currentSheetName = workbook?.getActiveSheet()?.getName() || '';
const newSequenceNodes = filterReferenceNodes.map((node, index) => {
if (typeof node === 'string') {
const preItem = filterReferenceNodes[index - 1];
if (!preItem) {
return null;
}
const nextItem = filterReferenceNodes[index + 1];
if (isComma(node)) {
if (isComma(nextItem)) {
return null;
}
if (index === (filterReferenceNodes.length - 1)) {
return null;
}
}
return node;
} else if (node.nodeType === sequenceNodeType.REFERENCE) {
const unitRange = deserializeRangeWithSheet(node.token);
unitRange.unitId = unitRange.unitId === '' ? unitId : unitRange.unitId;
unitRange.sheetName = unitRange.sheetName === '' ? currentSheetName : unitRange.sheetName;
const { unitId: rangeUnitId, sheetName: rangeSubName } = unitRange;
if (isOnlyOneRange) {
if (rangeUnitId !== unitId || currentSheetName !== rangeSubName) {
return null;
}
}
if (rangeUnitId === unitId && currentSheetName === rangeSubName) {
const currentSelection = cloneSelectionList.shift();
if (currentSelection && getSheetNameById(univerInstanceService, unitId, currentSelection.rangeWithCoord.sheetId || '') === rangeSubName) {
const cloneNode = { ...node };
rangePreProcess(currentSelection.rangeWithCoord);
if (isSupportAcrossSheet) {
cloneNode.token = serializeRangeWithSheet(currentSheetName, currentSelection.rangeWithCoord);
} else {
cloneNode.token = serializeRange(currentSelection.rangeWithCoord);
}
return cloneNode;
}
}
return node;
}
return null;
}).filter((e) => !!e) as INode[];
const theLast = unitRangesToText(
cloneSelectionList.map((e) => ({
range: e.rangeWithCoord,
unitId: e.rangeWithCoord.unitId ?? '',
sheetName: getSheetNameById(univerInstanceService, e.rangeWithCoord.unitId ?? '', e.rangeWithCoord.sheetId ?? ''),
})),
isSupportAcrossSheet
)
.join(matchToken.COMMA);
const thePre = sequenceNodeToText(newSequenceNodes);
const result = `${thePre}${(thePre && theLast) ? matchToken.COMMA : ''}${theLast}`;
const isScaling = isScalingRef.current;
handleRangeChange(result, isScaling ? -1 : result.length);
};
const d1 = refSelectionsRenderService.selectionMoveEnd$.subscribe((selections) => {
handleSelectionsChange(selections);
});
const d2 = refSelectionsRenderService.selectionMoving$.pipe(throttleTime(50)).subscribe((selections) => {
handleSelectionsChange(selections);
});
return () => {
d1.unsubscribe();
d2.unsubscribe();
};
}
}, [isNeed, filterReferenceNodes, refSelectionsRenderService, isSupportAcrossSheet, isOnlyOneRange]);
useEffect(() => {
if (isNeed && refSelectionsRenderService) {
const disposableCollection = new DisposableCollection();
const handleSequenceNodeReplace = (token: string, index: number) => {
let currentIndex = 0;
let offset = 0;
let isFinish = false;
const workbook = univerInstanceService.getUnit<Workbook>(unitId);
const currentSheetName = workbook?.getActiveSheet()?.getName() || '';
const newSequenceNodes = filterReferenceNodes.map((node) => {
if (typeof node === 'string') {
if (!isFinish) {
offset += node.length;
}
return node;
} else if (node.nodeType === sequenceNodeType.REFERENCE) {
if (!isFinish) {
offset += node.token.length;
}
const unitRange = deserializeRangeWithSheet(token);
unitRange.unitId = unitRange.unitId === '' ? unitId : unitRange.unitId;
unitRange.sheetName = unitRange.sheetName === '' ? currentSheetName : unitRange.sheetName;
if (currentIndex === index) {
isFinish = true;
const cloneNode = { ...node, token };
if (isSupportAcrossSheet) {
cloneNode.token = serializeRangeWithSheet(unitRange.sheetName, unitRange.range);
} else {
cloneNode.token = serializeRange(unitRange.range);
}
currentIndex++;
return cloneNode;
}
currentIndex++;
return node;
}
return node;
});
const result = sequenceNodeToText(newSequenceNodes);
handleRangeChange(result, offset || -1);
};
let time = 0 as any;
const dispose = refSelectionsRenderService.selectionMoveEnd$.subscribe(() => {
time = setTimeout(() => {
const controls = refSelectionsRenderService.getSelectionControls();
controls.forEach((control, index) => {
disposableCollection.add(control.selectionScaling$.subscribe((e) => {
const rangeText = serializeRange(e);
handleSequenceNodeReplace(rangeText, index);
setIsScaling();
}));
disposableCollection.add(control.selectionMoving$.pipe(map((e) => {
return serializeRange(e);
}), distinctUntilChanged()).subscribe((rangeText) => {
handleSequenceNodeReplace(rangeText, index);
setIsScaling();
}));
});
}, 30);
});
return () => {
dispose.unsubscribe();
disposableCollection.dispose();
clearTimeout(time);
};
}
}, [isNeed, refSelectionsRenderService, filterReferenceNodes]);
};
@@ -0,0 +1,44 @@
/**
* 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 { useEffect, useRef } from 'react';
import { sequenceNodeToText } from '../utils/sequenceNodeToText';
import { verifyRange } from '../utils/verifyRange';
import type { IRangeSelectorProps } from '../';
import type { useFormulaToken } from './useFormulaToken';
export const useVerify = (onVerify: IRangeSelectorProps['onVerify'], sequenceNodes: ReturnType<typeof useFormulaToken>['sequenceNodes']) => {
const isInitRender = useRef(true);
// No validation is performed during the initialization phase.
useEffect(() => {
const time: any = setTimeout(() => {
isInitRender.current = false;
}, 500);
return () => {
clearTimeout(time);
};
});
useEffect(() => {
if (!isInitRender.current) {
if (onVerify) {
const result = verifyRange(sequenceNodes);
onVerify(result, sequenceNodeToText(sequenceNodes));
}
}
}, [sequenceNodes, onVerify]);
};
@@ -0,0 +1,63 @@
.sheet-range-selector {
&-text-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;
}
&-placeholder {
font-size: 14px;
color: rgb(var(--grey-500));
position: absolute;
left: 5px;
bottom: 8px;
}
&-error {
border: 1px solid rgb(var(--red-500));
}
&-error-wrap {
font-size: 12px;
color: rgb(var(--red-500));
position: absolute;
bottom: -18px;
left: 0px;
}
&-text {
position: relative;
height: 22px;
width: 100%;
}
&-active {
border-color: rgb(var(--hyacinth-500));
}
&-icon {
cursor: pointer;
}
&-dialog {
&-item {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
margin-bottom: 8px;
&-delete {
cursor: pointer;
}
}
}
}
@@ -0,0 +1,485 @@
/**
* 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 { IDisposable, IUnitRangeName } from '@univerjs/core';
import type { Editor } from '@univerjs/docs-ui';
import type { ReactNode } from 'react';
import { createInternalEditorID, debounce, generateRandomId, ICommandService, LocaleService, useDependency } from '@univerjs/core';
import { Button, Dialog, Input, Tooltip } from '@univerjs/design';
import { DocBackScrollRenderController, IEditorService } from '@univerjs/docs-ui';
import { deserializeRangeWithSheet, LexerTreeBuilder, matchToken, sequenceNodeType } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { CloseSingle, DeleteSingle, IncreaseSingle, SelectRangeSingle } from '@univerjs/icons';
import { SetWorksheetActiveOperation } from '@univerjs/sheets';
import { IDescriptionService } from '@univerjs/sheets-formula';
import { RANGE_SELECTOR_SYMBOLS, SetCellEditVisibleOperation } from '@univerjs/sheets-ui';
import cl from 'clsx';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { RefSelectionsRenderService } from '../../services/render-services/ref-selections.render-service';
import { useEditorInput } from './hooks/useEditorInput';
import { useFormulaToken } from './hooks/useFormulaToken';
import { buildTextRuns, useColor, useDocHight, useSheetHighlight } from './hooks/useHighlight';
import { useRefactorEffect } from './hooks/useRefactorEffect';
import { useResize } from './hooks/useResize';
import { useSheetSelectionChange } from './hooks/useSheetSelectionChange';
import { useVerify } from './hooks/useVerify';
import styles from './index.module.less';
import { rangePreProcess } from './utils/rangePreProcess';
import { sequenceNodeToText } from './utils/sequenceNodeToText';
import { unitRangesToText } from './utils/unitRangesToText';
import { verifyRange } from './utils/verifyRange';
export interface IRangeSelectorProps {
initValue: string | IUnitRangeName[];
onChange: (result: string) => void;
unitId: string;
subUnitId: string;
errorText?: string | ReactNode;
onVerify?: (res: boolean, result: string) => void;
placeholder?: string;
isFocus?: boolean;
onBlur?: () => void;
/**
* 暂无效果
* @memberof IRangeSelectorProps
*/
onFocus?: () => void;
actions?: {
handleOutClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, cb: (v: boolean) => void) => void;
};
/**
* 是否只允许最多一个 range 区域,默认为 false。
* @type {boolean}
*/
isOnlyOneRange?: boolean;
/**
* 是否支持跨表选择选区.
* 如果不支持,那么会舍弃所有的 sheetName 信息
* @type {boolean}
* @memberof IRangeSelectorProps
*/
isSupportAcrossSheet?: boolean;
onRangeSelectorDialogVisibleChange?: (visible: boolean) => void;
};
const noopFunction = () => { };
export function RangeSelector(props: IRangeSelectorProps) {
const { initValue, unitId, subUnitId, errorText, placeholder, actions,
onChange = noopFunction,
onVerify = noopFunction,
onRangeSelectorDialogVisibleChange = noopFunction,
onBlur = noopFunction,
isFocus: _isFocus = true,
isOnlyOneRange = false,
isSupportAcrossSheet = false } = props;
const editorService = useDependency(IEditorService);
const localeService = useDependency(LocaleService);
const commandService = useDependency(ICommandService);
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
const rangeSelectorWrapRef = useRef<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 containerRef = useRef<HTMLDivElement>(null);
const [rangeString, rangeStringSet] = useState(() => {
if (typeof initValue === 'string') {
return initValue;
} else {
return unitRangesToText(initValue, isSupportAcrossSheet).join(matchToken.COMMA);
}
});
// init actions
if (actions) {
actions.handleOutClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, cb: (v: boolean) => void) => {
if (rangeSelectorWrapRef.current) {
const isContain = rangeSelectorWrapRef.current.contains(e.target as Node);
cb(isContain);
}
};
}
const ranges = useMemo(() => {
return rangeString.split(matchToken.COMMA).filter((e) => !!e).map((text) => deserializeRangeWithSheet(text));
}, [rangeString]);
const isError = useMemo(() => errorText !== undefined, [errorText]);
const handleInputDebounce = useMemo(() => debounce((text: string) => {
const nodes = lexerTreeBuilder.sequenceNodesBuilder(text);
if (nodes) {
const verify = verifyRange(nodes);
if (verify) {
const preNodes = nodes.map((node) => {
if (typeof node === 'string') {
return node;
} else if (node.nodeType === sequenceNodeType.REFERENCE) {
const unitRange = deserializeRangeWithSheet(node.token);
unitRange.range = rangePreProcess(unitRange.range);
if (!isSupportAcrossSheet) {
unitRange.sheetName = '';
unitRange.unitId = '';
}
node.token = unitRangesToText([unitRange], isSupportAcrossSheet)[0];
}
return node;
});
const result = sequenceNodeToText(preNodes);
onChange(result);
}
} else {
rangeStringSet('');
}
}, 30), [isSupportAcrossSheet]);
const handleConfirm = (ranges: IUnitRangeName[]) => {
const text = unitRangesToText(ranges, isSupportAcrossSheet).join(matchToken.COMMA);
rangeStringSet(text);
onChange(text);
rangeDialogVisibleSet(false);
onRangeSelectorDialogVisibleChange(false);
};
const handleClose = () => {
rangeDialogVisibleSet(false);
onRangeSelectorDialogVisibleChange(false);
};
const handleOpenModal = () => {
if (!isError) {
rangeDialogVisibleSet(true);
onRangeSelectorDialogVisibleChange(true);
}
};
const focus = useMemo(() => {
let time: NodeJS.Timeout = 0 as any;
return () => {
clearTimeout(time);
if (editor) {
time = setTimeout(() => {
editor.focus();
}, 30);
}
};
}, [editor]);
const { checkScrollBar } = useResize(editor);
const handleSheetSelectionChange = useMemo(() => {
return (text: string, offset: number) => {
rangeStringSet(text);
onChange(text);
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();
};
}, [editor]);
const { sequenceNodes, sequenceNodesSet } = useFormulaToken(rangeString);
const sheetHighlightRanges = useDocHight(editorId, sequenceNodes);
useSheetHighlight(!rangeDialogVisible && isFocus, unitId, subUnitId, sheetHighlightRanges);
useSheetSelectionChange(!rangeDialogVisible && isFocus, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange);
useRefactorEffect(!rangeDialogVisible && isFocus, unitId, isOnlyOneRange);
useEditorInput(unitId, rangeString, editor);
useVerify(onVerify, sequenceNodes);
useEffect(() => {
if (editor) {
const dispose = editor.input$.subscribe((e) => {
const text = (e.data.body?.dataStream ?? '').replaceAll(/\n|\r/g, '').replaceAll(/,{2,}/g, ',').replaceAll(/(^,)/g, '');
rangeStringSet(text);
handleInputDebounce(text);
});
return () => {
dispose.unsubscribe();
};
}
}, [editor]);
useEffect(() => {
const d1 = commandService.beforeCommandExecuted((info) => {
if (info.id === SetWorksheetActiveOperation.id) {
isFocusSet(false);
onBlur();
// Refresh the component
sequenceNodesSet((pre) => [...pre]);
}
});
const d2 = commandService.onCommandExecuted((info) => {
if (info.id === SetCellEditVisibleOperation.id) {
rangeDialogVisibleSet(false);
onRangeSelectorDialogVisibleChange(false);
isFocusSet(false);
onBlur();
}
});
return () => {
d1.dispose();
d2.dispose();
};
}, []);
useEffect(() => {
isFocusSet(_isFocus);
}, [_isFocus]);
useEffect(() => {
if (editor && rangeDialogVisible) {
editor.blur();
const d = editor.focus$.subscribe(() => {
editor.blur();
});
return () => {
d.unsubscribe();
};
}
}, [editor, rangeDialogVisible]);
useLayoutEffect(() => {
let dispose: IDisposable;
if (containerRef.current) {
dispose = editorService.register({
autofocus: true,
editorUnitId: editorId,
isSingle: true,
initialSnapshot: {
id: editorId,
body: { dataStream: '\r\n' },
documentStyle: {},
},
}, containerRef.current);
const editor = editorService.getEditor(editorId)! as Editor;
editorSet(editor);
}
return () => {
dispose?.dispose();
};
}, [containerRef.current]);
return (
<div className={styles.sheetRangeSelector} ref={rangeSelectorWrapRef}>
<div className={cl(styles.sheetRangeSelectorTextWrap, {
[styles.sheetRangeSelectorActive]: isFocus && !isError,
[styles.sheetRangeSelectorError]: isError,
})}
>
<div className={styles.sheetRangeSelectorText} ref={containerRef}>
</div>
<Tooltip title={localeService.t('rangeSelector.buttonTooltip')} placement="bottom">
<SelectRangeSingle className={styles.sheetRangeSelectorIcon} onClick={handleOpenModal} />
</Tooltip>
{errorText !== undefined ? <div className={styles.sheetRangeSelectorErrorWrap}>{errorText}</div> : null}
{(placeholder !== undefined && !rangeString) ? <div className={styles.sheetRangeSelectorPlaceholder}>{placeholder}</div> : null}
</div>
{rangeDialogVisible && (
<RangeSelectorDialog
handleConfirm={handleConfirm}
handleClose={handleClose}
unitId={unitId}
subUnitId={subUnitId}
initValue={ranges}
visible={rangeDialogVisible}
isOnlyOneRange={isOnlyOneRange}
isSupportAcrossSheet={isSupportAcrossSheet}
>
</RangeSelectorDialog>
)}
</div>
);
}
function RangeSelectorDialog(props: {
handleConfirm: (ranges: IUnitRangeName[]) => void;
handleClose: () => void;
visible: boolean;
initValue: IUnitRangeName[];
unitId: string;
subUnitId: string;
isOnlyOneRange: boolean;
isSupportAcrossSheet: boolean;
}) {
const { handleConfirm, handleClose: _handleClose, visible, initValue, unitId, subUnitId, isOnlyOneRange, isSupportAcrossSheet } = props;
const localeService = useDependency(LocaleService);
const descriptionService = useDependency(IDescriptionService);
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
const renderManagerService = useDependency(IRenderManagerService);
const render = renderManagerService.getRenderById(unitId);
const refSelectionsRenderService = render?.with(RefSelectionsRenderService);
const [ranges, rangesSet] = useState(() => {
if (isOnlyOneRange) {
const firstRange = initValue[0];
if (firstRange) {
return unitRangesToText([firstRange], isSupportAcrossSheet);
} else {
return [''];
}
}
return unitRangesToText(initValue, isSupportAcrossSheet);
});
const [focusIndex, focusIndexSet] = useState(() => ranges.length - 1);
const colorMap = useColor();
const rangeText = useMemo(() => ranges.join(matchToken.COMMA), [ranges]);
const { sequenceNodes, sequenceNodesSet } = useFormulaToken(rangeText);
const refSelections = useMemo(() => buildTextRuns(descriptionService, colorMap, sequenceNodes).refSelections, [sequenceNodes]);
const handleClose = () => {
// remove
sequenceNodesSet([]);
setTimeout(() => {
_handleClose();
}, 30);
};
const handleRangeInput = (index: number, value: string) => {
if (!value) {
refSelectionsRenderService?.setSkipLastEnabled(true);
} else {
refSelectionsRenderService?.setSkipLastEnabled(false);
}
rangesSet((v) => {
const result = [...v];
result[index] = value;
return result;
});
};
const handleRangeRemove = (index: number) => {
refSelectionsRenderService?.setSkipLastEnabled(false);
rangesSet((v) => {
if (v.length === 1) {
return v;
}
const result: string[] = [];
v.forEach((r, i) => {
if (index !== i) {
result.push(r);
}
});
return result;
});
};
const handleRangeAdd = () => {
refSelectionsRenderService?.setSkipLastEnabled(true);
rangesSet((v) => {
v.push('');
focusIndexSet(v.length - 1);
return [...v];
});
};
const handleSheetSelectionChange = useCallback((rangeText: string) => {
refSelectionsRenderService?.setSkipLastEnabled(false);
const ranges = rangeText.split(matchToken.COMMA).filter((e) => !!e);
if (isOnlyOneRange) {
rangesSet([ranges[0] ?? '']);
} else {
rangesSet(ranges);
}
}, [focusIndex, isOnlyOneRange]);
useSheetHighlight(visible, unitId, subUnitId, refSelections);
useSheetSelectionChange(focusIndex >= 0, unitId, subUnitId, sequenceNodes, isSupportAcrossSheet, isOnlyOneRange, handleSheetSelectionChange);
useRefactorEffect(focusIndex >= 0, unitId, isOnlyOneRange);
// 如果只有一个空 range,那么默认自动添加 range
useEffect(() => {
if (ranges.length === 0 || (ranges.length === 1 && !ranges[0])) {
refSelectionsRenderService?.setSkipLastEnabled(true);
}
}, [ranges]);
return (
<Dialog
width="328px"
visible={visible}
title={localeService.t('rangeSelector.title')}
draggable
closeIcon={<CloseSingle />}
footer={(
<footer>
<Button onClick={handleClose}>{localeService.t('rangeSelector.cancel')}</Button>
<Button
style={{ marginLeft: 10 }}
onClick={() => handleConfirm(ranges.filter((text) => {
const nodes = lexerTreeBuilder.sequenceNodesBuilder(text);
return nodes && nodes.length === 1 && typeof nodes[0] !== 'string' && nodes[0].nodeType === sequenceNodeType.REFERENCE;
}).map((text) => deserializeRangeWithSheet(text)).map((unitRange) => ({ ...unitRange, range: rangePreProcess(unitRange.range) })))}
type="primary"
>
{localeService.t('rangeSelector.confirm')}
</Button>
</footer>
)}
onClose={handleClose}
>
<div className={styles.sheetRangeSelectorDialog}>
{ranges.map((text, index) => (
<div key={index} className={styles.sheetRangeSelectorDialogItem}>
<Input
affixWrapperStyle={{ width: '100%' }}
placeholder={localeService.t('rangeSelector.placeHolder')}
key={`input_${index}`}
onFocus={() => focusIndexSet(index)}
value={text}
onChange={(value) => handleRangeInput(index, value)}
/>
{ranges.length > 1 && !isOnlyOneRange && <DeleteSingle className={styles.sheetRangeSelectorDialogItemDelete} onClick={() => handleRangeRemove(index)} />}
</div>
))}
{!isOnlyOneRange && (
<div>
<Button type="link" size="small" onClick={handleRangeAdd}>
<IncreaseSingle />
<span>{localeService.t('rangeSelector.addAnotherRange')}</span>
</Button>
</div>
)}
</div>
</Dialog>
);
}
@@ -0,0 +1,33 @@
/**
* 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 ISequenceNode, matchToken, sequenceNodeType } from '@univerjs/engine-formula';
export type INode = (string | ISequenceNode);
export const isComma = (e: INode) => {
if (typeof e === 'string') {
return e === matchToken.COMMA;
}
return false;
};
export const isReference = (e: INode) => {
if (typeof e === 'object') {
return e.nodeType === sequenceNodeType.REFERENCE;
}
};
export const filterReferenceNode = (nodes: INode[]) => {
return nodes.filter((node) => isComma(node) || isReference(node));
};
@@ -0,0 +1,40 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ISequenceNode } from '@univerjs/engine-formula';
export const findIndexFromSequenceNodes = (sequenceNode: (string | ISequenceNode)[], targetIndex: number, isEqual = true) => {
let result = -1;
sequenceNode.reduce((pre, cur, index) => {
if (pre.isFinish) {
return pre;
}
const oldIndex = pre.currentIndex;
if (typeof cur !== 'string') {
pre.currentIndex += cur.endIndex - cur.startIndex + 1;
} else {
const length = cur.length;
pre.currentIndex += length;
}
if (isEqual ? (pre.currentIndex === targetIndex) : (targetIndex > oldIndex && targetIndex <= pre.currentIndex)) {
result = index;
pre.isFinish = true;
}
return pre;
}, { currentIndex: 0, isFinish: false });
return result;
};
@@ -0,0 +1,26 @@
/**
* 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 { ISequenceNode } from '@univerjs/engine-formula';
export const getOffsetFromSequenceNodes = (sequenceNode: (string | ISequenceNode)[]) => {
return sequenceNode.reduce((pre, cur) => {
if (typeof cur === 'string') {
return pre + cur.length;
}
return pre + cur.endIndex - cur.startIndex + 1;
}, 0);
};
@@ -0,0 +1,31 @@
/**
* 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 { IRange } from '@univerjs/core';
export const rangePreProcess = (range: IRange) => {
if (range.endColumn < range.startColumn) {
const end = range.endColumn;
range.endColumn = range.startColumn;
range.startColumn = end;
}
if (range.endRow < range.startRow) {
const end = range.endRow;
range.endRow = range.startRow;
range.startRow = end;
}
return range;
};
@@ -0,0 +1,20 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ISequenceNode } from '@univerjs/engine-formula';
export const sequenceNodeToText = (sequenceNode: (string | ISequenceNode)[]) => sequenceNode.map((item) => typeof item === 'string' ? item : item.token).join('');
@@ -0,0 +1,38 @@
/**
* 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 { IUnitRangeName, IUniverInstanceService, Workbook } from '@univerjs/core';
import { serializeRange, serializeRangeWithSheet } from '@univerjs/engine-formula';
export function getSheetIdByName(univerInstanceService: IUniverInstanceService, unitId: string, name: string) {
return univerInstanceService.getUnit<Workbook>(unitId)?.getSheetBySheetName(name)?.getSheetId() || '';
}
export function getSheetNameById(univerInstanceService: IUniverInstanceService, unitId: string, sheetId: string) {
return univerInstanceService.getUnit<Workbook>(unitId)?.getSheetBySheetId(sheetId)?.getName() || '';
}
export const unitRangesToText = (ranges: IUnitRangeName[], isSupportAcrossSheet: boolean = false) => {
if (!isSupportAcrossSheet) {
return ranges.map((item) => serializeRange(item.range));
} else {
return ranges.map((item) => {
if (item.sheetName !== '') {
return serializeRangeWithSheet(item.sheetName, item.range);
}
return serializeRange(item.range);
});
}
};
@@ -0,0 +1,37 @@
/**
* 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 ISequenceNode, matchToken, sequenceNodeType } from '@univerjs/engine-formula';
/**
* @param {((string | ISequenceNode)[])} sequenceNodes
* @return {*}
*/
export const verifyRange = (sequenceNodes: (string | ISequenceNode)[]) => {
const isFail = sequenceNodes.some((item) => {
if (typeof item === 'string') {
if (item !== matchToken.COMMA) {
return true;
}
} else {
if (item.nodeType !== sequenceNodeType.REFERENCE) {
return true;
}
}
return false;
});
return !isFail;
};
+1 -3
View File
@@ -68,9 +68,7 @@
"@univerjs/icons": "^0.1.79",
"@univerjs/rpc": "workspace:*",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-numfmt": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*"
"@univerjs/sheets-numfmt": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
@@ -73,6 +73,7 @@
"@univerjs/engine-render": "workspace:*",
"@univerjs/icons": "^0.1.79",
"@univerjs/sheets": "workspace:*",
"@univerjs/sheets-formula-ui": "workspace:*",
"@univerjs/sheets-hyper-link": "workspace:*",
"@univerjs/sheets-ui": "workspace:*",
"@univerjs/ui": "workspace:*",
@@ -18,7 +18,7 @@ import type { DocumentDataModel, ICustomRange, IDisposable, INeedCheckDisposable
import type { IBoundRectNoAngle } from '@univerjs/engine-render';
import type { ISheetLocationBase } from '@univerjs/sheets';
import type { ICanvasPopup } from '@univerjs/sheets-ui';
import { BuildTextUtils, createInternalEditorID, CustomRangeType, Disposable, DOCS_ZEN_EDITOR_UNIT_ID_KEY, Inject, Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { BuildTextUtils, CustomRangeType, Disposable, DOCS_ZEN_EDITOR_UNIT_ID_KEY, Inject, Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DocSelectionManagerService } from '@univerjs/docs';
import { DocCanvasPopManagerService, IEditorService, IRangeSelectorService } from '@univerjs/docs-ui';
import { getCustomRangePosition, getEditingCustomRangePosition, IEditorBridgeService, SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
@@ -77,6 +77,8 @@ export class SheetsHyperLinkPopupService extends Disposable {
private _currentEditing$ = new BehaviorSubject<(IHyperLinkEditing & { customRange?: ICustomRange; label?: string }) | null>(null);
currentEditing$ = this._currentEditing$.asObservable();
private _isKeepVisible: boolean = false;
get currentPopup() {
return this._currentPopup;
}
@@ -107,6 +109,14 @@ export class SheetsHyperLinkPopupService extends Disposable {
});
}
public setIsKeepVisible(v: boolean) {
this._isKeepVisible = v;
}
public getIsKeepVisible() {
return this._isKeepVisible;
}
showPopup(location: IHyperLinkPopupOptions) {
if (this._currentPopup && isEqualLink(location, this._currentPopup)) {
return;
@@ -226,17 +236,14 @@ export class SheetsHyperLinkPopupService extends Disposable {
componentKey: CellLinkEdit.componentKey,
direction: 'bottom',
onClickOutside: () => {
const hyperLinkRangeSelectorId = createInternalEditorID('hyper-link-edit');
if (this._editorService.getFocusId() === hyperLinkRangeSelectorId) {
return;
if (!this.getIsKeepVisible()) {
this.endEditing();
}
if (this._rangeSelectorService.getCurrentSelectorId() === hyperLinkRangeSelectorId && this._rangeSelectorService.selectorModalVisible) {
return;
}
this.endEditing();
},
onContextMenu: () => {
this.endEditing();
if (!this.getIsKeepVisible()) {
this.endEditing();
}
},
hiddenType: 'hide',
};
@@ -14,15 +14,17 @@
* limitations under the License.
*/
import type { DocumentDataModel, IUnitRangeWithName, Nullable, Workbook } from '@univerjs/core';
import type { DocumentDataModel, Nullable, Workbook } from '@univerjs/core';
import type { ISetSelectionsOperationParams } from '@univerjs/sheets';
import { BuildTextUtils, ColorKit, createInternalEditorID, CustomRangeType, DisposableCollection, DOCS_ZEN_EDITOR_UNIT_ID_KEY, FOCUSING_SHEET, generateRandomId, ICommandService, IContextService, isValidRange, IUniverInstanceService, LocaleService, ThemeService, Tools, UniverInstanceType, useDependency } from '@univerjs/core';
import { BuildTextUtils, ColorKit, CustomRangeType, DisposableCollection, DOCS_ZEN_EDITOR_UNIT_ID_KEY, FOCUSING_SHEET, generateRandomId, ICommandService, IContextService, isValidRange, IUniverInstanceService, LocaleService, ThemeService, Tools, UniverInstanceType, useDependency } from '@univerjs/core';
import { Button, FormLayout, Input, Select } from '@univerjs/design';
import { DocSelectionManagerService } from '@univerjs/docs';
import { DocBackScrollRenderController, DocSelectionRenderService, RangeSelector } from '@univerjs/docs-ui';
import { DocBackScrollRenderController, DocSelectionRenderService } from '@univerjs/docs-ui';
import { deserializeRangeWithSheet, IDefinedNamesService, serializeRange, serializeRangeToRefString, serializeRangeWithSheet } from '@univerjs/engine-formula';
import { IRenderManagerService } from '@univerjs/engine-render';
import { SetSelectionsOperation, SetWorksheetActiveOperation } from '@univerjs/sheets';
import { RangeSelector } from '@univerjs/sheets-formula-ui';
import { SheetHyperLinkType } from '@univerjs/sheets-hyper-link';
import { IEditorBridgeService, IMarkSelectionService, ScrollToRangeOperation } from '@univerjs/sheets-ui';
import { IZenZoneService, KeyCode, useEvent, useObservable } from '@univerjs/ui';
@@ -44,6 +46,7 @@ export const CellLinkEdit = () => {
const [showLabel, setShowLabel] = useState(true);
const [type, setType] = useState<SheetHyperLinkType | string>(SheetHyperLinkType.URL);
const [payload, setPayload] = useState('');
const localeService = useDependency(LocaleService);
const definedNameService = useDependency(IDefinedNamesService);
const editorBridgeService = useDependency(IEditorBridgeService);
@@ -62,6 +65,8 @@ export const CellLinkEdit = () => {
const themeService = useDependency(ThemeService);
const docSelectionManagerService = useDependency(DocSelectionManagerService);
const rangeSelectorActionsRef = useRef<Parameters<typeof RangeSelector>[0]['actions']>({});
const customHyperLinkSidePanel = useMemo(() => {
if (sidePanelService.isBuiltInLinkType(type)) {
return;
@@ -71,8 +76,18 @@ export const CellLinkEdit = () => {
const [showError, setShowError] = useState(false);
const [isFocusRangeSelector, isFocusRangeSelectorSet] = useState(false);
const setByPayload = useRef(false);
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const subUnitId = workbook?.getActiveSheet().getSheetId() || '';
useEffect(() => {
isFocusRangeSelectorSet(false);
}, [subUnitId]);
useEffect(() => {
if (editing?.row !== undefined && editing.col !== undefined) {
const { label, customRange, row, col } = editing;
@@ -131,21 +146,21 @@ export const CellLinkEdit = () => {
const linkInfo = resolverService.parseHyperLink(link.payload);
setType(linkInfo.type === SheetHyperLinkType.INVALID ? SheetHyperLinkType.RANGE : linkInfo.type);
switch (linkInfo.type) {
case SheetHyperLinkType.URL:{
case SheetHyperLinkType.URL: {
setPayload(linkInfo.url);
if (linkInfo.url === link.display) {
setByPayload.current = true;
}
break;
}
case SheetHyperLinkType.RANGE:{
case SheetHyperLinkType.RANGE: {
const params = linkInfo.searchObj!;
const sheetName = params.gid ?
univerInstanceService
.getUnit<Workbook>(editing.unitId)
?.getSheetBySheetId(params.gid)
?.getName()
?? ''
?? ''
: '';
const payload = (serializeRangeWithSheet(sheetName, deserializeRangeWithSheet(params.range!).range));
setPayload(payload);
@@ -154,12 +169,12 @@ export const CellLinkEdit = () => {
}
break;
}
case SheetHyperLinkType.SHEET:{
case SheetHyperLinkType.SHEET: {
const params = linkInfo.searchObj!;
setPayload(params.gid!);
break;
}
case SheetHyperLinkType.DEFINE_NAME:{
case SheetHyperLinkType.DEFINE_NAME: {
const params = linkInfo.searchObj!;
setPayload(params.rangeid!);
break;
@@ -225,6 +240,22 @@ export const CellLinkEdit = () => {
};
}, [editorBridgeService, renderManagerService]);
useEffect(() => {
popupService.setIsKeepVisible(type === SheetHyperLinkType.RANGE);
return () => {
popupService.setIsKeepVisible(false);
};
}, [type]);
useEffect(() => {
return () => {
if (zenZoneService.temporaryHidden) {
zenZoneService.show();
contextService.setContextValue(FOCUSING_SHEET, false);
}
};
}, [contextService, zenZoneService]);
const linkTypeOptions: Array<{
label: string;
value: SheetHyperLinkType | string;
@@ -248,10 +279,10 @@ export const CellLinkEdit = () => {
...sidePanelOptions,
];
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
if (!workbook) {
return;
}
const hiddens = workbook.getHiddenWorksheets();
const sheetsOption = workbook.getSheets().map((sheet) => ({ label: sheet.getName(), value: sheet.getSheetId() })).filter((opt) => hiddens.indexOf(opt.value) === -1);
const definedNames = Object.values(definedNameService.getDefinedNameMap(workbook.getUnitId()) ?? {}).map((value) => ({
@@ -275,7 +306,8 @@ export const CellLinkEdit = () => {
return `#${type}=${payload}`;
};
const handleRangeChange = useEvent((newValue: IUnitRangeWithName[]) => {
const handleRangeChange = useEvent((rangeText: string) => {
const newValue = rangeText.split(',').map(deserializeRangeWithSheet);
const range = newValue[0];
if (!range || !isValidRange(range.range)) {
return;
@@ -352,13 +384,20 @@ export const CellLinkEdit = () => {
commandService.executeCommand(CloseHyperLinkPopupOperation.id);
};
const handlePanelClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (type !== SheetHyperLinkType.RANGE) {
return;
}
const handleOutClick = rangeSelectorActionsRef.current?.handleOutClick;
handleOutClick && handleOutClick(e, isFocusRangeSelectorSet);
};
if (!editing) {
return null;
}
return (
<div className={styles.cellLinkEdit} style={{ display: hide ? 'none' : 'block' }}>
<div className={styles.cellLinkEdit} style={{ display: hide ? 'none' : 'block' }} onClick={handlePanelClick}>
{showLabel
? (
<FormLayout
@@ -418,13 +457,16 @@ export const CellLinkEdit = () => {
{type === SheetHyperLinkType.RANGE && (
<FormLayout error={showError && !payload ? localeService.t('hyperLink.form.inputError') : ''}>
<RangeSelector
openForSheetUnitId={workbook.getUnitId()}
id={createInternalEditorID('hyper-link-edit')}
isSingleChoice
value={payloadInitial}
unitId={workbook.getUnitId()}
subUnitId={subUnitId}
isOnlyOneRange
isSupportAcrossSheet
initValue={payloadInitial}
onChange={handleRangeChange}
dialogOnly
onSelectorVisibleChange={async (visible) => {
isFocus={isFocusRangeSelector}
onBlur={() => { isFocusRangeSelectorSet(false); }}
actions={rangeSelectorActionsRef.current}
onRangeSelectorDialogVisibleChange={async (visible) => {
if (visible) {
if (editing.type === HyperLinkEditSourceType.ZEN_EDITOR) {
zenZoneService.hide();
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import type { IAccessor, ICommand } from '@univerjs/core';
import { CommandType, IUniverInstanceService, LocaleService } from '@univerjs/core';
import { IEditorService } from '@univerjs/docs-ui';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { ISidebarService } from '@univerjs/ui';
import type { IAccessor, ICommand } from '@univerjs/core';
import { DEFINED_NAME_CONTAINER } from '../../views/defined-name/component-name';
export interface IUIComponentCommandParams {
@@ -42,6 +42,7 @@ export const SidebarDefinedNameOperation: ICommand = {
case 'open':
editorService.setOperationSheetUnitId(unitId);
sidebarService.open({
id: DEFINED_NAME_CONTAINER,
header: { title: localeService.t('definedName.featureTitle') },
children: { label: DEFINED_NAME_CONTAINER },
onClose: () => {
+5
View File
@@ -16,6 +16,11 @@
export const SHEET_ZOOM_RANGE = [10, 400];
/**
* TODO@ggg: To avoid introducing the range-selector of the sheet-formula-ui package into sheet-ui, use a component key for now
*/
export const RANGE_SELECTOR_COMPONENT_KEY = 'RANGE_SELECTOR_COMPONENT_KEY';
export enum SHEET_VIEW_KEY {
MAIN = '__SpreadsheetRender__',
ROW = '__SpreadsheetRowHeader__',
@@ -16,12 +16,6 @@
/* eslint-disable max-lines-per-function */
import type { DocumentDataModel, ICellData, ICommandInfo, IDisposable, IDocumentBody, IDocumentData, IPosition, IStyleData, Nullable, UnitModel, Workbook } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IEditorInputConfig } from '@univerjs/docs-ui';
import type { DocumentSkeleton, IDocumentLayoutObject, IRenderContext, IRenderModule, Scene } from '@univerjs/engine-render';
import type { WorkbookSelections } from '@univerjs/sheets';
import type { IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service';
import {
CellValueType, DEFAULT_EMPTY_DOCUMENT_VALUE, Direction, Disposable, DisposableCollection, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, EDITOR_ACTIVATED,
FOCUSING_EDITOR_BUT_HIDDEN,
@@ -65,6 +59,12 @@ import {
import { ClearSelectionFormatCommand, COMMAND_LISTENER_SKELETON_CHANGE, SetRangeValuesCommand, SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetActivateCommand, SetWorksheetActiveOperation, SheetsSelectionsService } from '@univerjs/sheets';
import { ILayoutService, KeyCode, SetEditorResizeOperation } from '@univerjs/ui';
import { distinctUntilChanged, filter } from 'rxjs';
import type { DocumentDataModel, ICellData, ICommandInfo, IDisposable, IDocumentBody, IDocumentData, IPosition, IStyleData, Nullable, UnitModel, Workbook } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IEditorInputConfig } from '@univerjs/docs-ui';
import type { DocumentSkeleton, IDocumentLayoutObject, IRenderContext, IRenderModule, Scene } from '@univerjs/engine-render';
import type { WorkbookSelections } from '@univerjs/sheets';
import { getEditorObject } from '../../basics/editor/get-editor-object';
import { MoveSelectionCommand, MoveSelectionEnterAndTabCommand } from '../../commands/commands/set-selection.command';
import { SetCellEditVisibleArrowOperation, SetCellEditVisibleOperation, SetCellEditVisibleWithF2Operation } from '../../commands/operations/cell-edit.operation';
@@ -76,6 +76,8 @@ import styles from '../../views/sheet-container/index.module.less';
import { MOVE_SELECTION_KEYCODE_LIST } from '../shortcuts/editor.shortcut';
import { extractStringFromForceString, isForceString } from '../utils/cell-tools';
import { normalizeString } from '../utils/char-tools';
import { isRangeSelector } from './utils/isRangeSelector';
import type { IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service';
const HIDDEN_EDITOR_POSITION = -1000;
@@ -850,7 +852,8 @@ export class EditingRenderController extends Disposable implements IRenderModule
// Only when the sheet it attached to is focused. Maybe we should change it to the render unit sys.
if (
!this._isCurrentSheetFocused() ||
!this._editorService.isSheetEditor(commandUnitId)
!this._editorService.isSheetEditor(commandUnitId) ||
isRangeSelector(commandUnitId)
) {
return;
}
@@ -0,0 +1,22 @@
/**
* 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 const RANGE_SELECTOR_SYMBOLS = 'range-selector';
export const isRangeSelector = (unitId: string) => {
return unitId.includes(RANGE_SELECTOR_SYMBOLS);
};
+2 -1
View File
@@ -73,7 +73,7 @@ export { DragManagerService } from './services/drag-manager.service';
export { CellAlertManagerService, CellAlertType, type ICellAlert } from './services/cell-alert-manager.service';
export { HoverRenderController } from './controllers/hover-render.controller';
export { DragRenderController } from './controllers/drag-render.controller';
export { SHEET_VIEW_KEY } from './common/keys';
export { RANGE_SELECTOR_COMPONENT_KEY, SHEET_VIEW_KEY } from './common/keys';
export { type ICanvasPopup, SheetCanvasPopManagerService } from './services/canvas-pop-manager.service';
export { mergeSetRangeValues } from './services/clipboard/utils';
export type { IAutoFillLocation } from './services/auto-fill/type';
@@ -105,6 +105,7 @@ export { FONT_FAMILY_COMPONENT, FONT_FAMILY_ITEM_COMPONENT } from './components/
export { FONT_SIZE_COMPONENT } from './components/font-size/interface';
export { attachPrimaryWithCoord, attachSelectionWithCoord } from './services/selection/util';
export { SELECTION_SHAPE_DEPTH } from './services/selection/const';
export { isRangeSelector, RANGE_SELECTOR_SYMBOLS } from './controllers/editor/utils/isRangeSelector';
export { menuSchema } from './controllers/menu.schema';
export { getCellRealRange } from './common/utils';
@@ -47,7 +47,7 @@ export class SheetPermissionPanelModel {
private _rule: IPermissionPanelRule = DEFAULT_RANGE_RULE;
private _rule$ = new BehaviorSubject(this._rule);
private _oldRule: Nullable<IPermissionPanelRule>;
private _rangeErrorMsg$ = new BehaviorSubject('');
private _rangeErrorMsg$ = new BehaviorSubject<string | undefined>(undefined);
private _visible = false;
@@ -61,7 +61,7 @@ export class SheetPermissionPanelModel {
rangeErrorMsg$ = this._rangeErrorMsg$.asObservable();
setRangeErrorMsg(msg: string) {
setRangeErrorMsg(msg: string | undefined) {
this._rangeErrorMsg$.next(msg);
}
@@ -18,7 +18,7 @@ import { ICommandService, IContextService, ILogService, Inject, Injector, RANGE_
import { ScrollTimerType, SHEET_VIEWPORT_KEY, Vector2 } from '@univerjs/engine-render';
import { convertSelectionDataToRange, DISABLE_NORMAL_SELECTIONS, SelectionMoveType, SetSelectionsOperation, SheetsSelectionsService } from '@univerjs/sheets';
import { IShortcutService } from '@univerjs/ui';
import { distinctUntilChanged, startWith } from 'rxjs';
import { distinctUntilChanged, merge, startWith } from 'rxjs';
import type { IDisposable, Nullable, Workbook } from '@univerjs/core';
import type { IMouseEvent, IPointerEvent, IRenderContext, IRenderModule } from '@univerjs/engine-render';
import type { ISelectionWithCoordAndStyle, ISetSelectionsOperationParams, WorkbookSelections } from '@univerjs/sheets';
@@ -147,7 +147,7 @@ export class SheetSelectionRenderService extends BaseSelectionRenderService impl
private _initSelectionChangeListener(): void {
// When selection completes, we need to update the selections' rendering and clear event handlers.
this.disposeWithMe(this._workbookSelections.selectionMoveEnd$.subscribe((params) => {
this.disposeWithMe(merge(this._workbookSelections.selectionMoveEnd$, this._workbookSelections.selectionSet$).subscribe((params) => {
this._reset();
for (const selectionWithStyle of params) {
const selectionData = attachSelectionWithCoord(selectionWithStyle, this._skeleton);
@@ -177,7 +177,7 @@ export const DefinedNameContainer = () => {
<IncreaseSingle />
<span className={styles.definedNameContainerAddButtonText}>{localeService.t('definedName.addButton')}</span>
</div>
<DefinedNameInput confirm={insertConfirm} cancel={closeInput} state={editState} inputId="insertDefinedName" name={getInsertDefinedName()} formulaOrRefString={getInertFormulaOrRefString()} />
{editState && <DefinedNameInput confirm={insertConfirm} cancel={closeInput} state={editState} inputId="insertDefinedName" name={getInsertDefinedName()} formulaOrRefString={getInertFormulaOrRefString()} />}
</div>
{definedNames.map((definedName, index) => {
@@ -207,17 +207,20 @@ export const DefinedNameContainer = () => {
<Confirm visible={deleteConformKey === definedName.id} onClose={handleDeleteClose} onConfirm={() => { handleDeleteConfirm(definedName.id); }}>
{localeService.t('definedName.deleteConfirmText')}
</Confirm>
<DefinedNameInput
confirm={insertConfirm}
cancel={closeInput}
state={definedName.id === editorKey}
id={definedName.id}
inputId={definedName.id + index}
name={definedName.name}
formulaOrRefString={definedName.formulaOrRefString}
comment={definedName.comment}
localSheetId={definedName.localSheetId}
/>
{definedName.id === editorKey && (
<DefinedNameInput
confirm={insertConfirm}
cancel={closeInput}
state={definedName.id === editorKey}
id={definedName.id}
inputId={definedName.id + index}
name={definedName.name}
formulaOrRefString={definedName.formulaOrRefString}
comment={definedName.comment}
localSheetId={definedName.localSheetId}
/>
)}
</div>
);
})}
@@ -14,16 +14,19 @@
* limitations under the License.
*/
import { AbsoluteRefType, createInternalEditorID, IUniverInstanceService, LocaleService, Tools, UniverInstanceType, useDependency } from '@univerjs/core';
import type { Nullable, Workbook } from '@univerjs/core';
import { AbsoluteRefType, createInternalEditorID, IUniverInstanceService, LocaleService, Tools, UniverInstanceType, useDependency } from '@univerjs/core';
import { Button, Input, Radio, RadioGroup, Select } from '@univerjs/design';
import { RangeSelector, TextEditor } from '@univerjs/docs-ui';
import { IDefinedNamesService, type IDefinedNamesServiceParam, IFunctionService, isReferenceStrings, isReferenceStringWithEffectiveColumn, LexerTreeBuilder, operatorToken, serializeRangeToRefString } from '@univerjs/engine-formula';
import { TextEditor } from '@univerjs/docs-ui';
import { IDefinedNamesService, type IDefinedNamesServiceParam, IFunctionService, isReferenceStrings, isReferenceStringWithEffectiveColumn, LexerTreeBuilder, operatorToken } from '@univerjs/engine-formula';
import { hasCJKText } from '@univerjs/engine-render';
import { ErrorSingle } from '@univerjs/icons';
import React, { useEffect, useState } from 'react';
import type { IUnitRange, Nullable, Workbook } from '@univerjs/core';
import { ComponentManager } from '@univerjs/ui';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RANGE_SELECTOR_COMPONENT_KEY } from '../../common/keys';
import { SCOPE_WORKBOOK_VALUE } from './component-name';
import styles from './index.module.less';
export interface IDefinedNameInputProps extends Omit<IDefinedNamesServiceParam, 'id'> {
@@ -60,6 +63,9 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
const definedNamesService = useDependency(IDefinedNamesService);
const functionService = useDependency(IFunctionService);
const lexerTreeBuilder = useDependency(LexerTreeBuilder);
const componentManager = useDependency(ComponentManager);
const RangeSelector = useMemo(() => componentManager.get(RANGE_SELECTOR_COMPONENT_KEY), []);
if (workbook == null) {
return;
@@ -67,6 +73,8 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
const unitId = workbook.getUnitId();
const subUnitId = workbook.getActiveSheet().getSheetId();
const [nameValue, setNameValue] = useState(name);
const [formulaOrRefStringValue, setFormulaOrRefStringValue] = useState(formulaOrRefString);
@@ -81,6 +89,9 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
const [validFormulaOrRange, setValidFormulaOrRange] = useState(true);
const rangeSelectorActionsRef = useRef<any>({});
const [isFocusRangeSelector, isFocusRangeSelectorSet] = useState(false);
const options = [{
label: localeService.t('definedName.scopeWorkbook'),
value: SCOPE_WORKBOOK_VALUE,
@@ -90,6 +101,10 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
return !isReferenceStrings(token);
};
useEffect(() => {
isFocusRangeSelectorSet(false);
}, [subUnitId]);
useEffect(() => {
setValidFormulaOrRange(true);
setNameValue(name);
@@ -118,19 +133,8 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
});
});
const convertRangeToString = (ranges: IUnitRange[]) => {
const refs = ranges.map((range) => {
return serializeRangeToRefString({
...range,
sheetName: workbook.getSheetBySheetId(range.sheetId)?.getName() || '',
});
});
return refs.join(',');
};
const rangeSelectorChange = (ranges: IUnitRange[]) => {
setFormulaOrRefStringValue(convertRangeToString(ranges));
const rangeSelectorChange = (rangesText: string) => {
setFormulaOrRefStringValue(rangesText);
};
const formulaEditorChange = (value: Nullable<string>) => {
@@ -198,8 +202,13 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
setTypeValue(type);
};
const handlePanelClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleOutClick = rangeSelectorActionsRef.current?.handleOutClick;
handleOutClick && handleOutClick(e, isFocusRangeSelectorSet);
};
return (
<div className={styles.definedNameInput} style={{ display: state ? 'block' : 'none' }}>
<div className={styles.definedNameInput} style={{ display: state ? 'block' : 'none' }} onClick={handlePanelClick}>
<div>
<Input placeholder={localeService.t('definedName.inputNamePlaceholder')} value={nameValue} allowClear onChange={setNameValue} affixWrapperStyle={widthStyle} />
</div>
@@ -209,12 +218,33 @@ export const DefinedNameInput = (props: IDefinedNameInputProps) => {
<Radio value="formula">{localeService.t('definedName.ratioFormula')}</Radio>
</RadioGroup>
</div>
<div style={{ display: typeValue === 'range' ? 'block' : 'none' }}>
<RangeSelector key={`${inputId}-rangeSelector`} value={formulaOrRefStringValue} onValid={setValidFormulaOrRange} onChange={rangeSelectorChange} placeholder={localeService.t('definedName.inputRangePlaceholder')} id={createInternalEditorID(`${inputId}-rangeSelector`)} width="99%" openForSheetUnitId={unitId} />
</div>
<div style={{ display: typeValue === 'range' ? 'none' : 'block' }}>
<TextEditor key={`${inputId}-editor`} value={formulaOrRefStringValue} onValid={setValidFormulaOrRange} onChange={formulaEditorChange} id={createInternalEditorID(`${inputId}-editor`)} placeholder={localeService.t('definedName.inputFormulaPlaceholder')} openForSheetUnitId={unitId} onlyInputFormula={true} style={{ width: '99%' }} canvasStyle={{ fontSize: 10 }} />
</div>
{RangeSelector && typeValue === 'range' && (
<RangeSelector
unitId={unitId}
subUnitId={subUnitId}
initValue={formulaOrRefStringValue}
onChange={rangeSelectorChange}
isFocus={isFocusRangeSelector}
actions={rangeSelectorActionsRef.current}
isSupportAcrossSheet
onBlur={() => { isFocusRangeSelectorSet(false); }}
/>
)}
{typeValue !== 'range' && (
<TextEditor
key={`${inputId}-editor`}
value={formulaOrRefStringValue}
onValid={setValidFormulaOrRange}
onChange={formulaEditorChange}
id={createInternalEditorID(`${inputId}-editor`)}
placeholder={localeService.t('definedName.inputFormulaPlaceholder')}
openForSheetUnitId={unitId}
onlyInputFormula={true}
style={{ width: '99%' }}
canvasStyle={{ fontSize: 10 }}
/>
)}
<div>
<Select style={widthStyle} value={localSheetIdValue} options={options} onChange={setLocalSheetIdValue} />
</div>
@@ -20,8 +20,10 @@ import type { IDefinedNamesServiceParam } from '@univerjs/engine-formula';
import { ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType, useDependency } from '@univerjs/core';
import { IDefinedNamesService } from '@univerjs/engine-formula';
import { SetWorksheetShowCommand } from '@univerjs/sheets';
import { ISidebarService } from '@univerjs/ui';
import React, { useEffect, useState } from 'react';
import { SidebarDefinedNameOperation } from '../../commands/operations/sidebar-defined-name.operation';
import { DEFINED_NAME_CONTAINER } from './component-name';
import styles from './index.module.less';
export interface IDefinedNameOverlayProps {
@@ -33,6 +35,8 @@ export function DefinedNameOverlay(props: IDefinedNameOverlayProps) {
const localeService = useDependency(LocaleService);
const definedNamesService = useDependency(IDefinedNamesService);
const univerInstanceService = useDependency(IUniverInstanceService);
const sidebarService = useDependency(ISidebarService);
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const unitId = workbook.getUnitId();
@@ -56,6 +60,22 @@ export function DefinedNameOverlay(props: IDefinedNameOverlayProps) {
};
}, []); // Empty dependency array means this effect runs once on mount and clean up on unmount
// When closing the panel, clear the react cache
useEffect(() => {
const d = sidebarService.sidebarOptions$.subscribe((info) => {
if (info.id === DEFINED_NAME_CONTAINER) {
if (!info.visible) {
setTimeout(() => {
sidebarService.sidebarOptions$.next({ visible: false });
});
}
}
});
return () => {
d.unsubscribe();
};
}, []);
const openSlider = () => {
commandService.executeCommand(SidebarDefinedNameOperation.id, { value: 'open' });
};
@@ -14,17 +14,17 @@
* limitations under the License.
*/
import { createInternalEditorID, IAuthzIoService, isValidRange, IUniverInstanceService, LocaleService, RANGE_TYPE, Rectangle, UniverInstanceType, useDependency, UserManagerService } from '@univerjs/core';
import { Avatar, FormLayout, Input, Radio, RadioGroup, Select } from '@univerjs/design';
import { RangeSelector } from '@univerjs/docs-ui';
import { serializeRange } from '@univerjs/engine-formula';
import { ObjectScope, UnitAction, UnitObject, UnitRole } from '@univerjs/protocol';
import { RangeProtectionRuleModel, setEndForRange, SheetsSelectionsService, WorksheetProtectionRuleModel } from '@univerjs/sheets';
import { IDialogService, ISidebarService, useObservable } from '@univerjs/ui';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import type { IRange, Workbook } from '@univerjs/core';
import type { ICollaborator, IUser } from '@univerjs/protocol';
import { IAuthzIoService, isValidRange, IUniverInstanceService, LocaleService, RANGE_TYPE, Rectangle, UniverInstanceType, useDependency, UserManagerService } from '@univerjs/core';
import { Avatar, FormLayout, Input, Radio, RadioGroup, Select } from '@univerjs/design';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import { ObjectScope, UnitAction, UnitObject, UnitRole } from '@univerjs/protocol';
import { RangeProtectionRuleModel, setEndForRange, SheetsSelectionsService, WorksheetProtectionRuleModel } from '@univerjs/sheets';
import { ComponentManager, IDialogService, ISidebarService, useObservable } from '@univerjs/ui';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RANGE_SELECTOR_COMPONENT_KEY } from '../../../common/keys';
import { UNIVER_SHEET_PERMISSION_USER_DIALOG, UNIVER_SHEET_PERMISSION_USER_DIALOG_ID } from '../../../consts/permission';
import { editState, SheetPermissionPanelModel, viewState } from '../../../services/permission/sheet-permission-panel.model';
import { SheetPermissionUserManagerService } from '../../../services/permission/sheet-permission-user-list.service';
@@ -45,8 +45,13 @@ export const SheetPermissionPanelDetail = ({ fromSheetBar }: { fromSheetBar: boo
const sidebarService = useDependency(ISidebarService);
const rangeProtectionRuleModel = useDependency(RangeProtectionRuleModel);
const worksheetRuleModel = useDependency(WorksheetProtectionRuleModel);
const componentManager = useDependency(ComponentManager);
const RangeSelector = useMemo(() => componentManager.get(RANGE_SELECTOR_COMPONENT_KEY), []);
const rangeErrorMsg = useObservable(sheetPermissionPanelModel.rangeErrorMsg$);
const rangeSelectorActionsRef = useRef<any>({});
const [isFocusRangeSelector, isFocusRangeSelectorSet] = useState(false);
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
const unitId = workbook.getUnitId();
@@ -54,7 +59,7 @@ export const SheetPermissionPanelDetail = ({ fromSheetBar }: { fromSheetBar: boo
const selectUserList = useObservable(sheetPermissionUserManagerService.selectUserList$, sheetPermissionUserManagerService.selectUserList);
// The status of these two collaborators is updated directly by calling the interface, and will not be written to the snapshot or undoredo, so they are pulled in real time when they need to be displayed.
// The status of these two collaborators is updated directly by calling the interface, and will not be written to the snapshot or undoredo, so they are pulled in real time when they need to be displayed.
const [editorGroupValue, setEditorGroupValue] = React.useState<editState>(selectUserList.length ? editState.designedUserCanEdit : editState.onlyMe);
const [viewGroupValue, setViewGroupValue] = React.useState(viewState.othersCanView);
const [loading, setLoading] = useState(!!activeRule?.permissionId);
@@ -124,7 +129,7 @@ export const SheetPermissionPanelDetail = ({ fromSheetBar }: { fromSheetBar: boo
rangeErrorString = localeService.t('permission.panel.rangeOverlapOverPermissionError');
}
}
return rangeErrorString;
return rangeErrorString === '' ? undefined : rangeErrorString;
};
useEffect(() => {
@@ -276,8 +281,57 @@ export const SheetPermissionPanelDetail = ({ fromSheetBar }: { fromSheetBar: boo
};
}, [sidebarService, subUnitId, univerInstanceService]);
const handleRangeChange = (rangeText: string) => {
const newRange = rangeText.split(',').map(deserializeRangeWithSheet).map((item) => item.range);
if (newRange.some((i) => !isValidRange(i) || i.endColumn < i.startColumn || i.endRow < i.startRow)) {
return;
}
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const transformedRange = newRange.map((range) => {
const rowCount = worksheet.getRowCount();
const colCount = worksheet.getColumnCount();
setEndForRange(range, rowCount, colCount);
return range;
});
const rangeErrorString = checkRangeValid(transformedRange);
sheetPermissionPanelModel.setRangeErrorMsg(rangeErrorString);
if (rangeErrorString) return;
const sheetName = worksheet.getName();
const rangeStr = transformedRange.map((range) => {
const v = serializeRange(range);
return v === 'NaN' ? '' : v;
}).filter((r) => !!r).join(',');
const rule = {
ranges: transformedRange,
unitId,
subUnitId,
unitType: UnitObject.SelectRange,
name: `${sheetName}(${rangeStr})`,
};
if (rule.ranges.length === 1) {
const { startRow, endRow, startColumn, endColumn } = rule.ranges[0];
if (startRow === 0 && endRow === worksheet.getRowCount() - 1 && startColumn === 0 && worksheet.getColumnCount() - 1 === endColumn) {
rule.unitType = UnitObject.Worksheet;
rule.name = `${sheetName}`;
}
}
sheetPermissionPanelModel.setRule(rule);
};
const handlePanelClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleOutClick = rangeSelectorActionsRef.current?.handleOutClick;
handleOutClick && handleOutClick(e, isFocusRangeSelectorSet);
};
return (
<div className={styles.permissionPanelDetailWrapper}>
<div className={styles.permissionPanelDetailWrapper} onClick={handlePanelClick}>
{/* <FormLayout className={styles.sheetPermissionPanelTitle} label={localeService.t('permission.panel.name')}>
<Input
value={activeRule?.name ?? ''}
@@ -288,58 +342,18 @@ export const SheetPermissionPanelDetail = ({ fromSheetBar }: { fromSheetBar: boo
</FormLayout> */}
<Spin loading={loading}>
<FormLayout className={styles.sheetPermissionPanelTitle} label={localeService.t('permission.panel.protectedRange')}>
<RangeSelector
className={clsx(styles.permissionRangeSelector)}
textEditorClassName={clsx({ [styles.permissionRangeSelectorError]: rangeErrorMsg })}
value={activeRule?.ranges?.map((i) => serializeRange(i)).join(',')}
id={createInternalEditorID('sheet-permission-panel')}
openForSheetUnitId={unitId}
openForSheetSubUnitId={subUnitId}
onChange={(newRange) => {
if (newRange.some((i) => !isValidRange(i.range) || i.range.endColumn < i.range.startColumn || i.range.endRow < i.range.startRow)) {
return;
}
const workbook = univerInstanceService.getCurrentUnitForType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
const worksheet = workbook.getActiveSheet()!;
const unitId = workbook.getUnitId();
const subUnitId = worksheet.getSheetId();
const transformedRange = newRange.map((i) => {
const range = { ...i.range };
const rowCount = worksheet.getRowCount();
const colCount = worksheet.getColumnCount();
setEndForRange(range, rowCount, colCount);
return range;
});
const rangeErrorString = checkRangeValid(transformedRange);
sheetPermissionPanelModel.setRangeErrorMsg(rangeErrorString);
if (rangeErrorString) return;
const sheetName = worksheet.getName();
const rangeStr = transformedRange.map((range) => {
const v = serializeRange(range);
return v === 'NaN' ? '' : v;
}).filter((r) => !!r).join(',');
const rule = {
ranges: transformedRange,
unitId,
subUnitId,
unitType: UnitObject.SelectRange,
name: `${sheetName}(${rangeStr})`,
};
if (rule.ranges.length === 1) {
const { startRow, endRow, startColumn, endColumn } = rule.ranges[0];
if (startRow === 0 && endRow === worksheet.getRowCount() - 1 && startColumn === 0 && worksheet.getColumnCount() - 1 === endColumn) {
rule.unitType = UnitObject.Worksheet;
rule.name = `${sheetName}`;
}
}
sheetPermissionPanelModel.setRule(rule);
}}
/>
{rangeErrorMsg && <span className={styles.sheetPermissionPanelNameInputErrorText}>{rangeErrorMsg}</span>}
{RangeSelector && (
<RangeSelector
unitId={unitId}
errorText={rangeErrorMsg}
subUnitId={subUnitId}
initValue={activeRule?.ranges?.map((i) => serializeRange(i)).join(',')}
onChange={handleRangeChange}
// onVerify={handleVerify}
isFocus={isFocusRangeSelector}
actions={rangeSelectorActionsRef.current}
/>
) }
</FormLayout>
<FormLayout className={styles.sheetPermissionPanelTitle} label={localeService.t('permission.panel.permissionDirection')}>
<Input
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { Disposable, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
import { BehaviorSubject, of, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
import type { DeepReadonly, ISelectionCell, Nullable, Workbook } from '@univerjs/core';
import type { Observable } from 'rxjs';
import type { ISelectionWithStyle } from '../../basics/selection';
import { Disposable, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
import { BehaviorSubject, of, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
export interface ISelectionManagerSearchParam {
unitId: string;
@@ -30,6 +30,10 @@ export enum SelectionMoveType {
MOVE_START,
MOVING,
MOVE_END,
/**
* Events are not triggered by the API
*/
ONLY_SET,
}
export class SheetsSelectionsService extends RxDisposable {
@@ -211,6 +215,9 @@ export class WorkbookSelections extends Disposable {
private readonly _selectionMoveEnd$ = new BehaviorSubject<ISelectionWithStyle[]>([]);
readonly selectionMoveEnd$ = this._selectionMoveEnd$.asObservable();
private readonly _selectionSet$ = new BehaviorSubject<ISelectionWithStyle[]>([]);
readonly selectionSet$ = this._selectionSet$.asObservable();
private readonly _beforeSelectionMoveEnd$ = new BehaviorSubject<ISelectionWithStyle[]>([]);
readonly beforeSelectionMoveEnd$ = this._beforeSelectionMoveEnd$.asObservable();
@@ -265,6 +272,10 @@ export class WorkbookSelections extends Disposable {
case SelectionMoveType.MOVE_END:
this._emitOnEnd(selectionDatas);
break;
case SelectionMoveType.ONLY_SET: {
this._selectionSet$.next(selectionDatas);
break;
}
default:
this._emitOnEnd(selectionDatas);
break;
+553 -727
View File
File diff suppressed because it is too large Load Diff