mirror of
https://github.com/dream-num/univer.git
synced 2026-10-02 04:24:34 +08:00
fix(ui): preserve scoped formatting and virtualized document selections (#7756)
This commit is contained in:
@@ -201,6 +201,10 @@ export class DocMoveCursorController extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._deferUnmaterializedCursorMove(docDataModel, skeleton, nextOffset, normalizedSegmentId, () => this._handleShiftMoveSelection(direction, granularity))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedNextOffset = this._normalizeRenderableCursorOffset(
|
||||
skeleton,
|
||||
body.dataStream,
|
||||
|
||||
@@ -1493,6 +1493,10 @@ export class DocRenderController extends RxDisposable implements IRenderModule {
|
||||
Tools.now() - hydrationStartedAt
|
||||
);
|
||||
}
|
||||
this._textSelectionManagerService.refreshSelection(
|
||||
{ unitId: this._context.unitId, subUnitId: this._context.unitId },
|
||||
false
|
||||
);
|
||||
this._markDocumentRenderDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +400,7 @@ describe('selection utils', () => {
|
||||
|
||||
it('does not create ranges when a character boundary cannot be resolved', () => {
|
||||
const skeleton = {
|
||||
getSkeletonData: () => undefined,
|
||||
findNodePositionByCharIndex: vi.fn(() => undefined),
|
||||
};
|
||||
const document = createDocument();
|
||||
|
||||
@@ -29,7 +29,11 @@ import type {
|
||||
} from '@univerjs/engine-render';
|
||||
import type { IDocRange } from './range-interface';
|
||||
import { DataStreamTreeNodeType, RANGE_DIRECTION, Tools } from '@univerjs/core';
|
||||
import { DocumentSkeletonPageType, getDocumentSkeletonColumnPagePathInfo, getOffsetRectForDom } from '@univerjs/engine-render';
|
||||
import {
|
||||
DocumentSkeletonPageType,
|
||||
getDocumentSkeletonColumnPagePathInfo,
|
||||
getOffsetRectForDom,
|
||||
} from '@univerjs/engine-render';
|
||||
import { isInSameTableCell, isInSameTableCellData, isValidRectRange } from './convert-rect-range';
|
||||
import { compareNodePosition } from './convert-text-range';
|
||||
import { convertPositionsToRectRanges, RectRange } from './rect-range';
|
||||
@@ -138,6 +142,16 @@ export function getTextRangeFromCharIndex(
|
||||
const endNodePosition = skeleton.findNodePositionByCharIndex(endOffset, endIsBack, segmentId, segmentPage);
|
||||
|
||||
if (startNodePosition == null || endNodePosition == null) {
|
||||
const pages = skeleton.getSkeletonData()?.pages;
|
||||
const startPage = pages?.[skeleton.findBodyPageIndexByCharIndex(startOffset)];
|
||||
const endPage = pages?.[skeleton.findBodyPageIndexByCharIndex(endOffset)];
|
||||
if (segmentId === '' && startOffset !== endOffset && startPage && endPage &&
|
||||
(startPage.isMaterializationPlaceholder || endPage.isMaterializationPlaceholder)) {
|
||||
return new TextRange(scene, document, skeleton, undefined, undefined, style, segmentId, segmentPage, {
|
||||
startOffset: startIsBack ? startOffset : startOffset + 1,
|
||||
endOffset: endIsBack ? endOffset : endOffset + 1,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,6 +192,10 @@ export function getRangeListFromCharIndex(
|
||||
const endNodePosition = skeleton.findNodePositionByCharIndex(endOffset, true, segmentId, segmentPage);
|
||||
|
||||
if (startNodePosition == null || endNodePosition == null) {
|
||||
const textRange = getTextRangeFromCharIndex(startOffset, endOffset, scene, document, skeleton, style, segmentId, segmentPage);
|
||||
if (textRange) {
|
||||
return { textRanges: [textRange], rectRanges: [] };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,26 @@ import type {
|
||||
Scene,
|
||||
} from '@univerjs/engine-render';
|
||||
import type { IDocRange } from './range-interface';
|
||||
import { BooleanNumber, ColorKit, COLORS, DOC_RANGE_TYPE, generateRandomId, getColorStyle, RANGE_DIRECTION } from '@univerjs/core';
|
||||
import { DocumentSkeletonPageType, getColor, NORMAL_TEXT_SELECTION_PLUGIN_STYLE, Rect, RegularPolygon } from '@univerjs/engine-render';
|
||||
import {
|
||||
BooleanNumber,
|
||||
ColorKit,
|
||||
COLORS,
|
||||
DOC_RANGE_TYPE,
|
||||
generateRandomId,
|
||||
getColorStyle,
|
||||
RANGE_DIRECTION,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DocumentSkeletonPageType,
|
||||
getColor,
|
||||
NORMAL_TEXT_SELECTION_PLUGIN_STYLE,
|
||||
Rect,
|
||||
RegularPolygon,
|
||||
} from '@univerjs/engine-render';
|
||||
import {
|
||||
compareNodePosition,
|
||||
compareNodePositionLogic,
|
||||
findDocRangeNodePositions,
|
||||
getOneTextSelectionRange,
|
||||
NodePositionConvertToCursor,
|
||||
NodePositionMap,
|
||||
@@ -131,7 +146,8 @@ export class TextRange implements IDocRange {
|
||||
public focusNodePosition?: Nullable<INodePosition>,
|
||||
public style: ITextSelectionStyle = NORMAL_TEXT_SELECTION_PLUGIN_STYLE,
|
||||
private _segmentId: string = '',
|
||||
private _segmentPage: number = -1
|
||||
private _segmentPage: number = -1,
|
||||
private _logicalRange?: { startOffset: number; endOffset: number }
|
||||
) {
|
||||
this._anchorBlink();
|
||||
|
||||
@@ -162,6 +178,9 @@ export class TextRange implements IDocRange {
|
||||
|
||||
// The start position of the range
|
||||
get startOffset() {
|
||||
if (this._logicalRange) {
|
||||
return Math.min(this._logicalRange.startOffset, this._logicalRange.endOffset);
|
||||
}
|
||||
const { startOffset } = getOneTextSelectionRange(this._cursorList) ?? {};
|
||||
const body = this._docSkeleton
|
||||
.getViewModel()
|
||||
@@ -180,6 +199,9 @@ export class TextRange implements IDocRange {
|
||||
|
||||
// The end position of the range
|
||||
get endOffset() {
|
||||
if (this._logicalRange) {
|
||||
return Math.max(this._logicalRange.startOffset, this._logicalRange.endOffset);
|
||||
}
|
||||
const { endOffset } = getOneTextSelectionRange(this._cursorList) ?? {};
|
||||
const body = this._docSkeleton
|
||||
.getViewModel()
|
||||
@@ -231,6 +253,11 @@ export class TextRange implements IDocRange {
|
||||
}
|
||||
|
||||
get direction() {
|
||||
if (this._logicalRange) {
|
||||
return this._logicalRange.startOffset <= this._logicalRange.endOffset
|
||||
? RANGE_DIRECTION.FORWARD
|
||||
: RANGE_DIRECTION.BACKWARD;
|
||||
}
|
||||
const { collapsed, anchorNodePosition, focusNodePosition } = this;
|
||||
|
||||
if (collapsed || anchorNodePosition == null || focusNodePosition == null) {
|
||||
@@ -359,6 +386,7 @@ export class TextRange implements IDocRange {
|
||||
|
||||
// render cursor and selection.
|
||||
refresh() {
|
||||
this._refreshLogicalRangePositions();
|
||||
const { _document, _docSkeleton } = this;
|
||||
const anchor = this.anchorNodePosition;
|
||||
const focus = this.focusNodePosition;
|
||||
@@ -398,6 +426,38 @@ export class TextRange implements IDocRange {
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshLogicalRangePositions(): void {
|
||||
const range = this._logicalRange;
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
// Keep the full selection in document offsets; only its visible geometry
|
||||
// depends on the bounded page window retained by the layout worker.
|
||||
const start = Math.min(range.startOffset, range.endOffset);
|
||||
const end = Math.max(range.startOffset, range.endOffset);
|
||||
let first: Nullable<INodePosition>;
|
||||
let last: Nullable<INodePosition>;
|
||||
for (const page of this._docSkeleton.getSkeletonData()?.pages ?? []) {
|
||||
if (page.isLayoutPlaceholder || page.isMaterializationPlaceholder || page.ed < start || page.st >= end) {
|
||||
continue;
|
||||
}
|
||||
const positions = findDocRangeNodePositions(
|
||||
this._docSkeleton,
|
||||
Math.max(start, page.st),
|
||||
Math.min(end - 1, page.ed),
|
||||
this._segmentId,
|
||||
this._segmentPage
|
||||
);
|
||||
if (positions) {
|
||||
first ??= positions.startPosition;
|
||||
last = positions.endPosition;
|
||||
}
|
||||
}
|
||||
const forward = range.startOffset <= range.endOffset;
|
||||
this.anchorNodePosition = forward ? first : last;
|
||||
this.focusNodePosition = forward ? last : first;
|
||||
}
|
||||
|
||||
private _isEmpty() {
|
||||
return this.anchorNodePosition == null && this.focusNodePosition == null;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ export function focusFormulaEditor(
|
||||
return;
|
||||
}
|
||||
|
||||
// Blurring the previous editor can publish its stale rendered selection.
|
||||
const selections = [...editor.getSelectionRanges()];
|
||||
editorService.focus(editor.getEditorId());
|
||||
focusFormulaEditorElement(editor);
|
||||
if (editor.docSelectionRenderService.isOnPointerEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selections = [...editor.getSelectionRanges()];
|
||||
if (Tools.isDefine(offset)) {
|
||||
editor.setSelectionRanges([{ startOffset: offset, endOffset: offset }]);
|
||||
} else if (!selections.length) {
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
SetRangeValuesCommand,
|
||||
SetRangeValuesMutation,
|
||||
SetSelectionsOperation,
|
||||
WorkbookPermissionService,
|
||||
WorksheetPermissionService,
|
||||
} from '@univerjs/sheets';
|
||||
import { FormatPainterSessionService } from '@univerjs/ui';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
@@ -228,6 +230,8 @@ describe('Test format painter rules in controller', () => {
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
get(WorkbookPermissionService);
|
||||
get(WorksheetPermissionService);
|
||||
get(FormatPainterController);
|
||||
commandService.registerCommand(SetFormatPainterOperation);
|
||||
commandService.registerCommand(SetInfiniteFormatPainterCommand);
|
||||
|
||||
@@ -48,8 +48,10 @@ import {
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { FormatPainterSessionService } from '@univerjs/ui';
|
||||
import { tap } from 'rxjs';
|
||||
import { ApplyFormatPainterCommand } from '../../commands/commands/set-format-painter.command';
|
||||
import { checkCellContentInRanges } from '../../common/utils';
|
||||
import { ClearSelectionFormatMenuItemFactory } from '../../menu/clear.menu';
|
||||
import { FormatPainterStatus, IFormatPainterService } from '../../services/format-painter/format-painter.service';
|
||||
|
||||
export class FormatPainterController extends Disposable {
|
||||
@@ -70,13 +72,15 @@ export class FormatPainterController extends Disposable {
|
||||
private _initialize() {
|
||||
this._addDefaultHook();
|
||||
const session = this._injector.get(FormatPainterSessionService);
|
||||
let disabled = true;
|
||||
const disabled$ = this._injector.invoke(ClearSelectionFormatMenuItemFactory).disabled$!;
|
||||
this.disposeWithMe(session.register({
|
||||
id: 'sheet-cells',
|
||||
priority: 0,
|
||||
clear: () => this._commandService.executeCommand(ClearSelectionFormatCommand.id),
|
||||
changes$: this._selectionManagerService.selectionMoveEnd$,
|
||||
changes$: disabled$.pipe(tap((value) => { disabled = value; })),
|
||||
isActive: () => this._univerInstanceService.getFocusedUnit()?.type === UniverInstanceType.UNIVER_SHEET,
|
||||
canStart: () => !!this._selectionManagerService.getCurrentLastSelection(),
|
||||
canStart: () => !disabled && !!this._selectionManagerService.getCurrentLastSelection(),
|
||||
capture: () => {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const format = this._collectSelectionRangeFormat();
|
||||
|
||||
Reference in New Issue
Block a user