mirror of
https://github.com/dream-num/univer.git
synced 2026-08-28 23:01:30 +08:00
fix(docs): preserve floating drawings during structural edits (#7541)
This commit is contained in:
@@ -15,13 +15,72 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { PositionedObjectLayoutType } from '../../../../../types/interfaces/i-document-data';
|
||||
import { DrawingTypeEnum } from '../../../../../types/interfaces/i-drawing';
|
||||
import { DocumentDataModel } from '../../../document-data-model';
|
||||
import { JSONX } from '../../../json-x/json-x';
|
||||
import { DataStreamTreeTokenType } from '../../../types';
|
||||
import { getRichTextEditPath } from '../../utils';
|
||||
import { addDrawing, getCustomBlockIdsInSelections } from '../drawings';
|
||||
import { addDrawing, getCustomBlockIdsInSelections, removeDrawingReferences } from '../drawings';
|
||||
|
||||
describe('drawing build utils', () => {
|
||||
it('removes drawing references in reverse order for a structural text deletion', () => {
|
||||
const doc = new DocumentDataModel({
|
||||
id: 'doc-remove-drawings',
|
||||
body: {
|
||||
dataStream: '\bA\bB\b\r\n',
|
||||
customBlocks: [
|
||||
{ startIndex: 0, blockId: 'drawing-1' },
|
||||
{ startIndex: 2, blockId: 'drawing-2' },
|
||||
{ startIndex: 4, blockId: 'stale-drawing' },
|
||||
],
|
||||
},
|
||||
drawings: {
|
||||
'drawing-1': {
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
docTransform: {
|
||||
angle: 0,
|
||||
positionH: { posOffset: 0, relativeFrom: 0 },
|
||||
positionV: { posOffset: 0, relativeFrom: 0 },
|
||||
size: { height: 40, width: 80 },
|
||||
},
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
subUnitId: 'doc-remove-drawings',
|
||||
unitId: 'doc-remove-drawings',
|
||||
},
|
||||
'drawing-2': {
|
||||
drawingId: 'drawing-2',
|
||||
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
|
||||
docTransform: {
|
||||
angle: 0,
|
||||
positionH: { posOffset: 0, relativeFrom: 0 },
|
||||
positionV: { posOffset: 0, relativeFrom: 0 },
|
||||
size: { height: 40, width: 80 },
|
||||
},
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
subUnitId: 'doc-remove-drawings',
|
||||
unitId: 'doc-remove-drawings',
|
||||
},
|
||||
},
|
||||
drawingsOrder: ['drawing-1', 'stale-drawing', 'drawing-2'],
|
||||
});
|
||||
|
||||
const actions = removeDrawingReferences(doc.getSnapshot(), [
|
||||
{ startOffset: 0, endOffset: 5, collapsed: false },
|
||||
{ startOffset: 2, endOffset: 5, collapsed: false },
|
||||
]);
|
||||
let composedActions = actions[0];
|
||||
for (const action of actions.slice(1)) {
|
||||
composedActions = JSONX.compose(composedActions, action);
|
||||
}
|
||||
if (!composedActions) throw new Error('Expected drawing removal actions');
|
||||
doc.apply(composedActions);
|
||||
|
||||
expect(doc.getDrawings()).toEqual({});
|
||||
expect(doc.getDrawingsOrder()).toEqual([]);
|
||||
});
|
||||
|
||||
it('anchors a drawing in the current table-cell paragraph at the cell structural tail', () => {
|
||||
const T = DataStreamTreeTokenType;
|
||||
const tableStream = `${T.TABLE_START}${T.TABLE_ROW_START}${T.TABLE_CELL_START}Cell${T.PARAGRAPH}${T.SECTION_BREAK}${T.TABLE_CELL_END}${T.TABLE_ROW_END}${T.TABLE_END}`;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { ITextRange, ITextRangeParam } from '../../../../sheets/typedef';
|
||||
import type { IDocumentBody, IDrawingParam } from '../../../../types/interfaces';
|
||||
import type { IDocumentBody, IDocumentData, IDrawingParam } from '../../../../types/interfaces';
|
||||
import type { DocumentDataModel } from '../../document-data-model';
|
||||
import type { JSONXActions } from '../../json-x/json-x';
|
||||
import { createParagraphId } from '../../../paragraph-id';
|
||||
@@ -55,6 +55,43 @@ export function getCustomBlockIdsInSelections(body: IDocumentBody, selections: I
|
||||
return customBlockIds;
|
||||
}
|
||||
|
||||
export function removeDrawingReferences(
|
||||
documentData: Pick<IDocumentData, 'body' | 'drawings' | 'drawingsOrder'>,
|
||||
selections: ITextRange[],
|
||||
body: IDocumentBody | undefined = documentData.body
|
||||
): JSONXActions[] {
|
||||
if (!body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const drawings = documentData.drawings ?? {};
|
||||
const drawingOrder = documentData.drawingsOrder ?? [];
|
||||
const blockIds = [...new Set(getCustomBlockIdsInSelections(body, selections))]
|
||||
.sort((left, right) => drawingOrder.indexOf(right) - drawingOrder.indexOf(left));
|
||||
const jsonX = JSONX.getInstance();
|
||||
const actions: JSONXActions[] = [];
|
||||
|
||||
for (const blockId of blockIds) {
|
||||
const drawing = drawings[blockId];
|
||||
if (drawing != null) {
|
||||
const removeDrawingAction = jsonX.removeOp(['drawings', blockId], drawing);
|
||||
if (removeDrawingAction) {
|
||||
actions.push(removeDrawingAction);
|
||||
}
|
||||
}
|
||||
|
||||
const drawingIndex = drawingOrder.indexOf(blockId);
|
||||
if (drawingIndex >= 0) {
|
||||
const removeDrawingOrderAction = jsonX.removeOp(['drawingsOrder', drawingIndex], blockId);
|
||||
if (removeDrawingOrderAction) {
|
||||
actions.push(removeDrawingOrderAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const addDrawing = (param: IAddDrawingParam) => {
|
||||
const { selection, documentDataModel, drawings } = param;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { addCustomDecorationTextX, deleteCustomDecorationTextX } from './custom-decoration';
|
||||
import { copyCustomRange, getCustomRangesInterestsWithSelection, isIntersecting } from './custom-range';
|
||||
import { addDrawing } from './drawings';
|
||||
import { addDrawing, removeDrawingReferences } from './drawings';
|
||||
import {
|
||||
changeParagraphBulletNestLevel,
|
||||
setParagraphBullet,
|
||||
@@ -96,6 +96,7 @@ export class BuildTextUtils {
|
||||
|
||||
static drawing = {
|
||||
add: addDrawing,
|
||||
remove: removeDrawingReferences,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeleteDirection, getRichTextEditPath, ICommandService, JSONX, TextXActionType } from '@univerjs/core';
|
||||
import { BlockType, DeleteDirection, DrawingTypeEnum, getRichTextEditPath, ICommandService, IUndoRedoService, JSONX, PositionedObjectLayoutType, TextXActionType } from '@univerjs/core';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTestBed } from '../../../facade/__tests__/create-test-bed';
|
||||
import { RichTextEditingMutation } from '../../mutations/core-editing.mutation';
|
||||
@@ -48,6 +48,62 @@ describe('core editing commands', () => {
|
||||
expect(testBed.univerAPI.getActiveDocument()?.save().body?.dataStream).toBe('Hello\r\n');
|
||||
});
|
||||
|
||||
it('deletes a drawing with backspace and restores its references through history', () => {
|
||||
testBed.univer.dispose();
|
||||
testBed = createTestBed({
|
||||
id: 'test',
|
||||
body: {
|
||||
dataStream: '\b\r\n',
|
||||
paragraphs: [{ startIndex: 1, paragraphId: 'paragraph-drawing' }],
|
||||
customBlocks: [{ blockId: 'drawing-1', blockType: BlockType.DRAWING, startIndex: 0 }],
|
||||
},
|
||||
drawings: {
|
||||
'drawing-1': {
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
|
||||
docTransform: {
|
||||
angle: 0,
|
||||
positionH: { posOffset: 0, relativeFrom: 0 },
|
||||
positionV: { posOffset: 0, relativeFrom: 0 },
|
||||
size: { height: 40, width: 80 },
|
||||
},
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
subUnitId: 'test',
|
||||
unitId: 'test',
|
||||
},
|
||||
},
|
||||
drawingsOrder: ['drawing-1'],
|
||||
documentStyle: {},
|
||||
});
|
||||
commandService = testBed.get(ICommandService);
|
||||
testBed.get(IUndoRedoService);
|
||||
|
||||
expect(commandService.syncExecuteCommand(DeleteTextCommand.id, {
|
||||
unitId: 'test',
|
||||
range: { startOffset: 1, endOffset: 1, collapsed: true },
|
||||
direction: DeleteDirection.LEFT,
|
||||
})).toBe(true);
|
||||
expect(testBed.doc.getSnapshot()).toMatchObject({
|
||||
body: { customBlocks: [] },
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
});
|
||||
|
||||
expect(testBed.univerAPI.getActiveDocument()?.undo()).toBe(true);
|
||||
expect(testBed.doc.getSnapshot()).toMatchObject({
|
||||
body: { customBlocks: [{ blockId: 'drawing-1', startIndex: 0 }] },
|
||||
drawings: { 'drawing-1': { drawingId: 'drawing-1' } },
|
||||
drawingsOrder: ['drawing-1'],
|
||||
});
|
||||
|
||||
expect(testBed.univerAPI.getActiveDocument()?.redo()).toBe(true);
|
||||
expect(testBed.doc.getSnapshot()).toMatchObject({
|
||||
body: { customBlocks: [] },
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not delete the document minimum paragraph sentinel', () => {
|
||||
const result = commandService.syncExecuteCommand(DeleteTextCommand.id, {
|
||||
unitId: 'test',
|
||||
|
||||
@@ -14,15 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
DocumentDataModel,
|
||||
ICommand,
|
||||
IDocumentBody,
|
||||
IDocumentData,
|
||||
IMutationInfo,
|
||||
ITextRange,
|
||||
UpdateDocsAttributeType,
|
||||
} from '@univerjs/core';
|
||||
import type { DocumentDataModel, ICommand, IDocumentBody, IDocumentData, IMutationInfo, ITextRange, Nullable, UpdateDocsAttributeType } from '@univerjs/core';
|
||||
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
import type { IRichTextEditingMutationParams } from '../mutations/core-editing.mutation';
|
||||
import {
|
||||
@@ -46,6 +38,10 @@ export interface IInsertTextCommandParams {
|
||||
range: ITextRange;
|
||||
segmentId?: string;
|
||||
cursorOffset?: number;
|
||||
debounce?: boolean;
|
||||
textRanges?: Nullable<ITextRangeWithStyle[]>;
|
||||
noNeedSetTextRange?: boolean;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +52,17 @@ export const InsertTextCommand: ICommand<IInsertTextCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params: IInsertTextCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const { range, segmentId, body, unitId, cursorOffset } = params;
|
||||
const {
|
||||
range,
|
||||
segmentId,
|
||||
body,
|
||||
unitId,
|
||||
cursorOffset,
|
||||
debounce = true,
|
||||
textRanges,
|
||||
noNeedSetTextRange,
|
||||
isEditing,
|
||||
} = params;
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
@@ -67,7 +73,7 @@ export const InsertTextCommand: ICommand<IInsertTextCommandParams> = {
|
||||
}
|
||||
|
||||
const activeRange = docSelectionManagerService.getActiveTextRange();
|
||||
const rangeSegmentId = 'segmentId' in range ? (range as ITextRange & { segmentId?: string }).segmentId : undefined;
|
||||
const rangeSegmentId = 'segmentId' in range && typeof range.segmentId === 'string' ? range.segmentId : undefined;
|
||||
const targetSegmentId = segmentId ?? rangeSegmentId ?? activeRange?.segmentId ?? '';
|
||||
const originBody = docDataModel.getSelfOrHeaderFooterModel(targetSegmentId)?.getBody();
|
||||
|
||||
@@ -77,54 +83,26 @@ export const InsertTextCommand: ICommand<IInsertTextCommandParams> = {
|
||||
|
||||
const { startOffset, collapsed } = range;
|
||||
const cursorMove = cursorOffset ?? body.dataStream.length;
|
||||
const textRanges = [
|
||||
{
|
||||
startOffset: startOffset + cursorMove,
|
||||
endOffset: startOffset + cursorMove,
|
||||
style: activeRange?.style,
|
||||
collapsed,
|
||||
},
|
||||
];
|
||||
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
actions: [],
|
||||
textRanges,
|
||||
debounce: true,
|
||||
},
|
||||
};
|
||||
|
||||
const textX = new TextX();
|
||||
const jsonX = JSONX.getInstance();
|
||||
|
||||
if (collapsed) {
|
||||
if (startOffset > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: startOffset,
|
||||
});
|
||||
}
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.INSERT,
|
||||
body,
|
||||
len: body.dataStream.length,
|
||||
});
|
||||
} else {
|
||||
const dos = BuildTextUtils.selection.delete([range], originBody, 0, body);
|
||||
textX.push(...dos);
|
||||
}
|
||||
|
||||
doMutation.params.textRanges = [{
|
||||
const mutationTextRanges = textRanges ?? [{
|
||||
startOffset: startOffset + cursorMove,
|
||||
endOffset: startOffset + cursorMove,
|
||||
collapsed,
|
||||
}];
|
||||
|
||||
const path = getRichTextEditPath(docDataModel, segmentId);
|
||||
doMutation.params.actions = jsonX.editOp(textX.serialize(), path);
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
segmentId: targetSegmentId,
|
||||
actions: [],
|
||||
textRanges: mutationTextRanges,
|
||||
debounce,
|
||||
noNeedSetTextRange,
|
||||
isEditing,
|
||||
},
|
||||
};
|
||||
|
||||
doMutation.params.actions = buildInsertTextActions(docDataModel, originBody, range, body, targetSegmentId);
|
||||
|
||||
const result = commandService.syncExecuteCommand<
|
||||
IRichTextEditingMutationParams,
|
||||
@@ -135,6 +113,47 @@ export const InsertTextCommand: ICommand<IInsertTextCommandParams> = {
|
||||
},
|
||||
};
|
||||
|
||||
function buildInsertTextActions(
|
||||
documentDataModel: DocumentDataModel,
|
||||
originBody: IDocumentBody,
|
||||
range: ITextRange,
|
||||
insertBody: IDocumentBody,
|
||||
segmentId?: string
|
||||
): IRichTextEditingMutationParams['actions'] {
|
||||
const { startOffset, endOffset, collapsed } = range;
|
||||
const textX = new TextX();
|
||||
if (collapsed) {
|
||||
if (startOffset > 0) {
|
||||
textX.push({ t: TextXActionType.RETAIN, len: startOffset });
|
||||
}
|
||||
textX.push({ t: TextXActionType.INSERT, body: insertBody, len: insertBody.dataStream.length });
|
||||
} else {
|
||||
textX.push(...BuildTextUtils.selection.delete([range], originBody, 0, insertBody));
|
||||
}
|
||||
|
||||
const textActions = JSONX.getInstance().editOp(textX.serialize(), getRichTextEditPath(documentDataModel, segmentId));
|
||||
return collapsed
|
||||
? textActions
|
||||
: appendRemovedDrawingActions(textActions, documentDataModel, originBody, startOffset, endOffset);
|
||||
}
|
||||
|
||||
function appendRemovedDrawingActions(
|
||||
textActions: IRichTextEditingMutationParams['actions'],
|
||||
documentDataModel: DocumentDataModel,
|
||||
body: IDocumentBody,
|
||||
startOffset: number,
|
||||
endOffset: number
|
||||
): IRichTextEditingMutationParams['actions'] {
|
||||
const drawingActions = BuildTextUtils.drawing.remove(documentDataModel.getSnapshot(), [{
|
||||
startOffset,
|
||||
endOffset,
|
||||
collapsed: false,
|
||||
}], body);
|
||||
const rawActions = [textActions, ...drawingActions];
|
||||
|
||||
return rawActions.reduce((accumulator, action) => JSONX.compose(accumulator, action));
|
||||
}
|
||||
|
||||
export interface IDeleteTextCommandParams {
|
||||
unitId: string;
|
||||
range: ITextRange;
|
||||
@@ -175,6 +194,7 @@ export const DeleteTextCommand: ICommand<IDeleteTextCommandParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
segmentId,
|
||||
actions: [],
|
||||
textRanges: [{
|
||||
startOffset: start,
|
||||
@@ -196,7 +216,13 @@ export const DeleteTextCommand: ICommand<IDeleteTextCommandParams> = {
|
||||
}], body));
|
||||
|
||||
const path = getRichTextEditPath(docDataModel, segmentId);
|
||||
doMutation.params.actions = jsonX.editOp(textX.serialize(), path);
|
||||
doMutation.params.actions = appendRemovedDrawingActions(
|
||||
jsonX.editOp(textX.serialize(), path),
|
||||
docDataModel,
|
||||
body,
|
||||
start,
|
||||
end + 1
|
||||
);
|
||||
|
||||
const result = commandService.syncExecuteCommand<
|
||||
IRichTextEditingMutationParams,
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
import type { IDocumentData, Univer } from '@univerjs/core';
|
||||
import type { FDocument } from '../f-document';
|
||||
import { ColumnSeparatorType, DataStreamTreeTokenType, DocumentFlavor, ICommandService, IResourceManagerService, IUndoRedoService, PageOrientType, SectionType, UniverInstanceType } from '@univerjs/core';
|
||||
import { InsertTextCommand } from '@univerjs/docs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { BlockType, ColumnSeparatorType, DataStreamTreeTokenType, DocumentFlavor, DrawingTypeEnum, ICommandService, IResourceManagerService, IUndoRedoService, PageOrientType, PositionedObjectLayoutType, SectionType, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, InsertTextCommand } from '@univerjs/docs';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createDocumentData, createSimpleDocument, createTestBed } from './create-test-bed';
|
||||
|
||||
describe('FDocument', () => {
|
||||
@@ -187,6 +187,90 @@ describe('FDocument', () => {
|
||||
expect(document.getParagraphs()[0].getText()).toBe('Document title suffix');
|
||||
});
|
||||
|
||||
it('deletes drawing references atomically without changing the interactive selection', async () => {
|
||||
univer.dispose();
|
||||
const data = createDocumentData('drawing-delete-doc', {
|
||||
dataStream: '\b\r\n',
|
||||
paragraphs: [{ startIndex: 1, paragraphId: 'para-drawing' }],
|
||||
customBlocks: [{ blockId: 'drawing-1', blockType: BlockType.DRAWING, startIndex: 0 }],
|
||||
});
|
||||
data.drawings = { 'drawing-1': createDrawingData('drawing-delete-doc', 'drawing-1') };
|
||||
data.drawingsOrder = ['drawing-1'];
|
||||
createDocumentFacade(data);
|
||||
get(IUndoRedoService);
|
||||
const replaceDocRanges = vi.spyOn(get(DocSelectionManagerService), 'replaceDocRanges');
|
||||
|
||||
expect(document.deleteRange({ startOffset: 0, endOffset: 1 })).toBe(true);
|
||||
await Promise.resolve();
|
||||
expect(replaceDocRanges).not.toHaveBeenCalled();
|
||||
expect(document.save()).toMatchObject({
|
||||
body: { customBlocks: [] },
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
});
|
||||
|
||||
expect(document.undo()).toBe(true);
|
||||
expect(document.save()).toMatchObject({
|
||||
body: { customBlocks: [{ blockId: 'drawing-1', startIndex: 0 }] },
|
||||
drawings: { 'drawing-1': { drawingId: 'drawing-1' } },
|
||||
drawingsOrder: ['drawing-1'],
|
||||
});
|
||||
|
||||
expect(document.redo()).toBe(true);
|
||||
expect(document.save()).toMatchObject({
|
||||
body: { customBlocks: [] },
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes header drawing references in the header segment history', () => {
|
||||
univer.dispose();
|
||||
const data = createDocumentData('header-drawing-delete-doc', {
|
||||
dataStream: 'Body\r\n',
|
||||
paragraphs: [{ startIndex: 4, paragraphId: 'body-paragraph' }],
|
||||
});
|
||||
data.headers = {
|
||||
'header-drawing': {
|
||||
headerId: 'header-drawing',
|
||||
body: {
|
||||
dataStream: '\b\r\n',
|
||||
paragraphs: [{ startIndex: 1, paragraphId: 'header-paragraph' }],
|
||||
customBlocks: [{ blockId: 'header-shape', blockType: BlockType.DRAWING, startIndex: 0 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
data.drawings = { 'header-shape': createDrawingData(data.id, 'header-shape') };
|
||||
data.drawingsOrder = ['header-shape'];
|
||||
createDocumentFacade(data);
|
||||
get(IUndoRedoService);
|
||||
|
||||
expect(document.deleteRange({ startOffset: 0, endOffset: 1, segmentId: 'header-drawing' })).toBe(true);
|
||||
expect(document.save()).toMatchObject({
|
||||
headers: { 'header-drawing': { body: { customBlocks: [] } } },
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
});
|
||||
|
||||
expect(document.undo()).toBe(true);
|
||||
expect(document.save()).toMatchObject({
|
||||
headers: {
|
||||
'header-drawing': {
|
||||
body: { customBlocks: [{ blockId: 'header-shape', startIndex: 0 }] },
|
||||
},
|
||||
},
|
||||
drawings: { 'header-shape': { drawingId: 'header-shape' } },
|
||||
drawingsOrder: ['header-shape'],
|
||||
});
|
||||
|
||||
expect(document.redo()).toBe(true);
|
||||
expect(document.save()).toMatchObject({
|
||||
headers: { 'header-drawing': { body: { customBlocks: [] } } },
|
||||
drawings: {},
|
||||
drawingsOrder: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures header and footer segments independently', () => {
|
||||
univer.dispose();
|
||||
const documentData = createDocumentData('classic-doc', {
|
||||
@@ -542,3 +626,19 @@ describe('FDocument', () => {
|
||||
expect(rule?.getInfo().paragraph.paragraphStyle?.spaceBelow).toEqual({ v: 10 });
|
||||
});
|
||||
});
|
||||
|
||||
function createDrawingData(unitId: string, drawingId: string) {
|
||||
return {
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
|
||||
docTransform: {
|
||||
angle: 0,
|
||||
positionH: { posOffset: 0, relativeFrom: 0 },
|
||||
positionV: { posOffset: 0, relativeFrom: 0 },
|
||||
size: { height: 40, width: 80 },
|
||||
},
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
subUnitId: unitId,
|
||||
unitId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ export class FDocumentParagraph extends FBaseInitialable {
|
||||
},
|
||||
buildPlainTextInsertBody(text),
|
||||
this._document.getDocumentDataModel(),
|
||||
this._injector
|
||||
this._commandService
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import type { Injector, ITextStyle } from '@univerjs/core';
|
||||
import type { FDocument } from './f-document';
|
||||
import type { IFDocumentTextRange } from './utils';
|
||||
import { Tools, UpdateDocsAttributeType } from '@univerjs/core';
|
||||
import { ICommandService, Tools, UpdateDocsAttributeType } from '@univerjs/core';
|
||||
import { FBaseInitialable } from '@univerjs/core/facade';
|
||||
import { buildPlainTextInsertBody, replaceBodyRange, retainBodyRange } from './utils';
|
||||
|
||||
@@ -54,7 +54,8 @@ export class FDocumentTextRange extends FBaseInitialable {
|
||||
protected readonly _startOffset: number,
|
||||
protected readonly _endOffset: number,
|
||||
protected readonly _segmentId: string,
|
||||
protected override readonly _injector: Injector
|
||||
protected override readonly _injector: Injector,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super(_injector);
|
||||
this._validateRange();
|
||||
@@ -207,7 +208,7 @@ export class FDocumentTextRange extends FBaseInitialable {
|
||||
this.getRange(),
|
||||
buildPlainTextInsertBody(text),
|
||||
this._document.getDocumentDataModel(),
|
||||
this._injector
|
||||
this._commandService
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -384,7 +384,7 @@ export class FDocument extends FBaseInitialable {
|
||||
},
|
||||
buildPlainTextInsertBody(text),
|
||||
this._documentDataModel,
|
||||
this._injector
|
||||
this._commandService
|
||||
);
|
||||
}
|
||||
|
||||
@@ -633,7 +633,7 @@ export class FDocument extends FBaseInitialable {
|
||||
paragraphs,
|
||||
},
|
||||
this._documentDataModel,
|
||||
this._injector
|
||||
this._commandService
|
||||
);
|
||||
|
||||
return success ? this.getParagraph(paragraphId, segmentId) : null;
|
||||
@@ -766,7 +766,7 @@ export class FDocument extends FBaseInitialable {
|
||||
},
|
||||
buildPlainTextInsertBody(`${text}\r`),
|
||||
this._documentDataModel,
|
||||
this._injector
|
||||
this._commandService
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
@@ -828,7 +828,7 @@ export class FDocument extends FBaseInitialable {
|
||||
dataStream: '',
|
||||
},
|
||||
this._documentDataModel,
|
||||
this._injector
|
||||
this._commandService
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,16 @@
|
||||
|
||||
import type { DocumentDataModel, IDocumentBody, Injector, IParagraphStyle, UpdateDocsAttributeType } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import { createParagraphId, DataStreamTreeTokenType, getRichTextEditPath, ICommandService, JSONX, TextX, TextXActionType } from '@univerjs/core';
|
||||
import { RichTextEditingMutation } from '@univerjs/docs';
|
||||
import {
|
||||
createParagraphId,
|
||||
DataStreamTreeTokenType,
|
||||
getRichTextEditPath,
|
||||
ICommandService,
|
||||
JSONX,
|
||||
TextX,
|
||||
TextXActionType,
|
||||
} from '@univerjs/core';
|
||||
import { InsertTextCommand, RichTextEditingMutation } from '@univerjs/docs';
|
||||
|
||||
export interface IBuildPlainTextInsertBodyOptions {
|
||||
paragraphStyle?: IParagraphStyle;
|
||||
@@ -114,43 +122,29 @@ export function replaceBodyRange(
|
||||
range: IFDocumentTextRange,
|
||||
insertBody: IDocumentBody,
|
||||
docDataModel: DocumentDataModel,
|
||||
injector: Injector
|
||||
commandService: ICommandService
|
||||
): boolean {
|
||||
const { startOffset, endOffset, segmentId } = range;
|
||||
const textX = new TextX();
|
||||
|
||||
if (startOffset > 0) {
|
||||
textX.push({ t: TextXActionType.RETAIN, len: startOffset });
|
||||
}
|
||||
|
||||
if (endOffset > startOffset) {
|
||||
textX.push({ t: TextXActionType.DELETE, len: endOffset - startOffset });
|
||||
}
|
||||
|
||||
if (insertBody.dataStream.length > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.INSERT,
|
||||
body: insertBody,
|
||||
len: insertBody.dataStream.length,
|
||||
});
|
||||
}
|
||||
|
||||
const jsonX = JSONX.getInstance();
|
||||
const actions = jsonX.editOp(textX.serialize(), getRichTextEditPath(docDataModel, segmentId));
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
const result = commandService.syncExecuteCommand<IRichTextEditingMutationParams, IRichTextEditingMutationParams>(
|
||||
RichTextEditingMutation.id,
|
||||
const result = commandService.syncExecuteCommand(
|
||||
InsertTextCommand.id,
|
||||
{
|
||||
unitId: docDataModel.getUnitId(),
|
||||
body: insertBody,
|
||||
range: {
|
||||
startOffset,
|
||||
endOffset,
|
||||
collapsed: startOffset === endOffset,
|
||||
segmentId,
|
||||
},
|
||||
segmentId,
|
||||
actions,
|
||||
debounce: false,
|
||||
textRanges: [],
|
||||
noNeedSetTextRange: true,
|
||||
isEditing: false,
|
||||
}
|
||||
);
|
||||
|
||||
return Boolean(result?.actions && result.actions.length > 0);
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
export function retainBodyRange(
|
||||
|
||||
Reference in New Issue
Block a user