feat(docs): move drawing commands and add image facade (#7278)

This commit is contained in:
wpxp123456
2026-07-16 20:37:49 +08:00
committed by GitHub
parent 7d651b335a
commit e853a22770
56 changed files with 2209 additions and 691 deletions
@@ -15,7 +15,7 @@
*/
import type { ITextRange, ITextRangeParam } from '../../../../sheets/typedef';
import type { IDocumentBody } from '../../../../types/interfaces';
import type { IDocumentBody, IDrawingParam } from '../../../../types/interfaces';
import type { DocumentDataModel } from '../../document-data-model';
import type { JSONXActions } from '../../json-x/json-x';
import { createParagraphId } from '../../../paragraph-id';
@@ -29,7 +29,7 @@ import { deleteSelectionTextX } from './text-x-utils';
export interface IAddDrawingParam {
selection: ITextRangeParam;
documentDataModel: DocumentDataModel;
drawings: any[];
drawings: IDrawingParam[];
}
export function getCustomBlockIdsInSelections(body: IDocumentBody, selections: ITextRange[]): string[] {
@@ -147,7 +147,7 @@ function normalizeDrawingInsertOffset(body: IDocumentBody, offset: number): numb
return offset === 0 && body.dataStream[0] === DataStreamTreeTokenType.PARAGRAPH ? 1 : offset;
}
function buildDrawingInsertBody(body: IDocumentBody, drawings: any[], insertOffset: number): IDocumentBody {
function buildDrawingInsertBody(body: IDocumentBody, drawings: IDrawingParam[], insertOffset: number): IDocumentBody {
const placeholders = DataStreamTreeTokenType.CUSTOM_BLOCK.repeat(drawings.length);
const needsTrailingParagraph = body.dataStream[insertOffset] === DataStreamTreeTokenType.SECTION_BREAK || body.dataStream[insertOffset] === undefined;
const dataStream = needsTrailingParagraph ? `${placeholders}${DataStreamTreeTokenType.PARAGRAPH}` : placeholders;
@@ -81,6 +81,7 @@ export class BuildTextUtils {
export { getSingleDataStreamChange } from './data-stream-change';
export type { IDataStreamChange } from './data-stream-change';
export { getCustomBlockIdsInSelections } from './drawings';
export { getParagraphContentStartOffset, getParagraphContentStartOffsets, getParagraphFollowingBlockOffset } from './paragraph';
export {
containsInteriorInsertionOffset,
@@ -15,6 +15,7 @@
*/
import type { Dependency, DependencyIdentifier, DocumentDataModel, ICommand, IDocumentData } from '@univerjs/core';
import type { IInsertDocDrawingCommandParams, ISetDocDrawingArrangeCommandParams, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
import {
ArrangeTypeEnum,
awaitTime,
@@ -45,6 +46,12 @@ import {
DocDrawingService,
IDocDrawingAdapterService,
IDocDrawingService,
InsertDocDrawingCommand,
RemoveDocDrawingCommand,
SetDocDrawingArrangeCommand,
TextWrappingStyle,
UpdateDocDrawingWrappingStyleCommand,
UpdateDrawingDocTransformCommand,
} from '@univerjs/docs-drawing';
import { DocSelectionRenderService } from '@univerjs/docs-ui';
import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing';
@@ -58,20 +65,14 @@ import { DocRefreshDrawingsService } from '../../../services/doc-refresh-drawing
import { ClearDocDrawingTransformerOperation } from '../../operations/clear-drawing-transformer.operation';
import { DeleteDocDrawingsCommand } from '../delete-doc-drawing.command';
import { GroupDocDrawingCommand } from '../group-doc-drawing.command';
import { InsertDocDrawingCommand } from '../insert-doc-drawing.command';
import { InsertDocImageCommand } from '../insert-image.command';
import { MoveDocDrawingsCommand } from '../move-drawings.command';
import { RemoveDocDrawingCommand } from '../remove-doc-drawing.command';
import { SetDocDrawingArrangeCommand } from '../set-drawing-arrange.command';
import { UngroupDocDrawingCommand } from '../ungroup-doc-drawing.command';
import {
IMoveInlineDrawingCommand,
ITransformNonInlineDrawingCommand,
TextWrappingStyle,
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingWrapTextCommand,
UpdateDrawingDocTransformCommand,
} from '../update-doc-drawing.command';
function createBaseDocData(): IDocumentData {
@@ -133,6 +134,35 @@ function createDrawingDocData(): IDocumentData {
};
}
function createHeaderDrawingDocData(): IDocumentData {
const docData = createBaseDocData();
docData.headers = {
'header-1': {
headerId: 'header-1',
body: {
dataStream: '\b\r\n',
customBlocks: [{ startIndex: 0, blockId: 'header-shape-1' }],
},
},
};
docData.drawings = {
'header-shape-1': {
drawingId: 'header-shape-1',
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
docTransform: {
positionH: { posOffset: 1 },
positionV: { posOffset: 2 },
},
} as never,
};
docData.drawingsOrder = ['header-shape-1'];
return docData;
}
function createChartDrawingDocData(): IDocumentData {
return {
...createDrawingDocData(),
@@ -336,21 +366,16 @@ function setupDrawingTestBed(docData: IDocumentData, dependencies: Dependency[]
const commandService = get(ICommandService);
[
InsertDocDrawingCommand,
RemoveDocDrawingCommand,
DeleteDocDrawingsCommand,
MoveDocDrawingsCommand,
GroupDocDrawingCommand,
UngroupDocDrawingCommand,
SetDocDrawingArrangeCommand,
ClearDocDrawingTransformerOperation,
InsertDocImageCommand,
IMoveInlineDrawingCommand,
ITransformNonInlineDrawingCommand,
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingWrapTextCommand,
UpdateDrawingDocTransformCommand,
RichTextEditingMutation as unknown as ICommand,
].forEach((command) => commandService.registerCommand(command));
@@ -393,16 +418,19 @@ describe('docs drawing commands integration', () => {
style: null as never,
}]);
expect(await testBed.commandService.executeCommand(InsertDocDrawingCommand.id, {
expect(await testBed.commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId: 'test-doc',
drawings: [{
drawingId: 'shape-1',
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingType: 'image',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
docTransform: {
positionH: { posOffset: 1 },
positionV: { posOffset: 2 },
size: { width: 1, height: 1 },
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 1 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 2 },
angle: 0,
},
}],
})).toBe(true);
@@ -441,16 +469,19 @@ describe('docs drawing commands integration', () => {
endOffset: insertOffset,
});
expect(await testBed.commandService.executeCommand(InsertDocDrawingCommand.id, {
expect(await testBed.commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId: 'test-doc',
drawings: [{
drawingId: 'shape-1',
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingType: 'image',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
docTransform: {
positionH: { posOffset: 1 },
positionV: { posOffset: 2 },
size: { width: 1, height: 1 },
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 1 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 2 },
angle: 0,
},
}],
})).toBe(true);
@@ -478,7 +509,8 @@ describe('docs drawing commands integration', () => {
style: null as never,
}]);
expect(await testBed.commandService.executeCommand(InsertDocDrawingCommand.id, {
expect(await testBed.commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId: 'test-doc',
textRange: {
startOffset: insertOffset,
endOffset: insertOffset,
@@ -489,11 +521,13 @@ describe('docs drawing commands integration', () => {
drawingId: 'shape-1',
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingType: 'image',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
docTransform: {
positionH: { posOffset: 1 },
positionV: { posOffset: 2 },
size: { width: 1, height: 1 },
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 1 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 2 },
angle: 0,
},
}],
})).toBe(true);
@@ -508,6 +542,44 @@ describe('docs drawing commands integration', () => {
testBed.univer.dispose();
});
it('inserts into the document specified by the command params', async () => {
const testBed = setupDrawingTestBed(createBaseDocData());
const targetData = createBaseDocData();
targetData.id = 'target-doc';
const targetDoc = testBed.univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, targetData);
testBed.injector.get(CoreDocDrawingController).loadDrawingDataForUnit('target-doc');
testBed.get(IUniverInstanceService).setCurrentUnitForType('test-doc');
expect(await testBed.commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId: 'target-doc',
textRange: {
startOffset: 5,
endOffset: 5,
collapsed: true,
segmentId: '',
},
drawings: [{
drawingId: 'target-shape-1',
unitId: 'target-doc',
subUnitId: 'target-doc',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
docTransform: {
size: { width: 1, height: 1 },
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 1 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 2 },
angle: 0,
},
}],
})).toBe(true);
expect(targetDoc.getBody()?.customBlocks).toEqual([{ startIndex: 5, blockId: 'target-shape-1' }]);
expect(testBed.get(IUniverInstanceService).getUnit<DocumentDataModel>('test-doc', UniverInstanceType.UNIVER_DOC)?.getBody()?.customBlocks).toEqual([]);
testBed.univer.dispose();
});
it('replaces the selected drawing block when inserting a new drawing over a selection', async () => {
const testBed = setupDrawingTestBed(createDrawingDocData());
@@ -520,16 +592,19 @@ describe('docs drawing commands integration', () => {
style: null as never,
}]);
expect(await testBed.commandService.executeCommand(InsertDocDrawingCommand.id, {
expect(await testBed.commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId: 'test-doc',
drawings: [{
drawingId: 'shape-2',
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingType: 'image',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
docTransform: {
positionH: { posOffset: 10 },
positionV: { posOffset: 20 },
size: { width: 1, height: 1 },
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 10 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 20 },
angle: 0,
},
}],
})).toBe(true);
@@ -568,6 +643,62 @@ describe('docs drawing commands integration', () => {
testBed.univer.dispose();
});
it('deletes from the explicit text range segment instead of the active selection segment', async () => {
const testBed = setupDrawingTestBed(createHeaderDrawingDocData());
expect(await testBed.commandService.executeCommand(RemoveDocDrawingCommand.id, {
unitId: 'test-doc',
drawings: [{
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingId: 'header-shape-1',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
}],
textRange: {
startOffset: 0,
endOffset: 0,
collapsed: true,
segmentId: 'header-1',
},
} as never)).toBe(true);
const doc = testBed.get(IUniverInstanceService)
.getUnit<DocumentDataModel>('test-doc', UniverInstanceType.UNIVER_DOC)!;
expect(doc.getSelfOrHeaderFooterModel('header-1')?.getBody()?.customBlocks).toEqual([]);
expect(doc.getSnapshot().drawings?.['header-shape-1']).toBeUndefined();
testBed.univer.dispose();
});
it('deletes from the document specified by the command params', async () => {
const testBed = setupDrawingTestBed(createBaseDocData());
const targetData = createDrawingDocData();
targetData.id = 'target-doc';
targetData.drawings!['shape-1'].unitId = 'target-doc';
targetData.drawings!['shape-1'].subUnitId = 'target-doc';
const targetDoc = testBed.univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, targetData);
testBed.injector.get(CoreDocDrawingController).loadDrawingDataForUnit('target-doc');
testBed.get(IUniverInstanceService).setCurrentUnitForType('test-doc');
expect(await testBed.commandService.executeCommand(RemoveDocDrawingCommand.id, {
unitId: 'target-doc',
drawings: [{
unitId: 'target-doc',
subUnitId: 'target-doc',
drawingId: 'shape-1',
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
}],
})).toBe(true);
expect(targetDoc.getBody()?.customBlocks).toEqual([]);
expect(targetDoc.getSnapshot().drawings).toEqual({});
expect(testBed.get(IUniverInstanceService).getUnit<DocumentDataModel>('test-doc', UniverInstanceType.UNIVER_DOC)?.getBody()?.customBlocks).toEqual([]);
testBed.univer.dispose();
});
it('includes drawing adapter resource mutations in the same undo item when deleting a drawing', async () => {
const testBed = setupDrawingTestBed(createChartDrawingDocData());
const removeResourceMutation = {
@@ -877,13 +1008,14 @@ describe('docs drawing commands integration', () => {
vi.spyOn(testBed.get(DocSkeletonManagerService), 'getSkeleton').mockReturnValue(skeleton);
const refreshDrawings = vi.spyOn(testBed.get(DocRefreshDrawingsService), 'refreshDrawings');
expect(await testBed.commandService.executeCommand(UpdateDrawingDocTransformCommand.id, {
expect(await testBed.commandService.executeCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [{
drawingId: 'shape-1',
key: 'positionV',
value: {
relativeFrom: ObjectRelativeFromV.PAGE,
posOffset: 18,
},
}],
@@ -893,7 +1025,10 @@ describe('docs drawing commands integration', () => {
const doc = testBed.get(IUniverInstanceService)
.getUnit<DocumentDataModel>('test-doc', UniverInstanceType.UNIVER_DOC)!;
expect(doc.getSnapshot().drawings?.['shape-1'].docTransform.positionV).toEqual({ posOffset: 18 });
expect(doc.getSnapshot().drawings?.['shape-1'].docTransform.positionV).toEqual({
relativeFrom: ObjectRelativeFromV.PAGE,
posOffset: 18,
});
expect(testBed.refreshControls).toHaveBeenCalled();
expect(refreshDrawings).toHaveBeenCalledWith(skeleton);
@@ -1039,7 +1174,7 @@ describe('docs drawing commands integration', () => {
expect(doc.getSnapshot().drawingsOrder).toEqual(['drawing-a', 'drawing-b', 'drawing-c']);
expect(await testBed.commandService.executeCommand(SetDocDrawingArrangeCommand.id, {
expect(await testBed.commandService.executeCommand<ISetDocDrawingArrangeCommandParams>(SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: ['drawing-a'],
@@ -1048,7 +1183,7 @@ describe('docs drawing commands integration', () => {
await awaitTime(0);
expect(doc.getSnapshot().drawingsOrder).toEqual(['drawing-b', 'drawing-c', 'drawing-a']);
expect(await testBed.commandService.executeCommand(SetDocDrawingArrangeCommand.id, {
expect(await testBed.commandService.executeCommand<ISetDocDrawingArrangeCommandParams>(SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: ['drawing-a'],
@@ -1057,7 +1192,7 @@ describe('docs drawing commands integration', () => {
await awaitTime(0);
expect(doc.getSnapshot().drawingsOrder).toEqual(['drawing-b', 'drawing-a', 'drawing-c']);
expect(await testBed.commandService.executeCommand(SetDocDrawingArrangeCommand.id, {
expect(await testBed.commandService.executeCommand<ISetDocDrawingArrangeCommandParams>(SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: ['drawing-b'],
@@ -1066,7 +1201,7 @@ describe('docs drawing commands integration', () => {
await awaitTime(0);
expect(doc.getSnapshot().drawingsOrder).toEqual(['drawing-a', 'drawing-b', 'drawing-c']);
expect(await testBed.commandService.executeCommand(SetDocDrawingArrangeCommand.id, {
expect(await testBed.commandService.executeCommand<ISetDocDrawingArrangeCommandParams>(SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: ['drawing-c'],
@@ -15,8 +15,8 @@
*/
import { ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
import { describe, expect, it, vi } from 'vitest';
import { InsertDocDrawingCommand } from '../insert-doc-drawing.command';
import { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from '../insert-shape.command';
function createAccessor() {
@@ -15,11 +15,9 @@
*/
import type { IAccessor, ICommand } from '@univerjs/core';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { IDeleteDrawingCommandParams } from './interfaces';
import type { IDocDrawing, IRemoveDocDrawingCommandParams } from '@univerjs/docs-drawing';
import { CommandType, ICommandService } from '@univerjs/core';
import { IDocDrawingService } from '@univerjs/docs-drawing';
import { RemoveDocDrawingCommand } from './remove-doc-drawing.command';
import { IDocDrawingService, RemoveDocDrawingCommand } from '@univerjs/docs-drawing';
export const DeleteDocDrawingsCommand: ICommand = {
id: 'doc.command.delete-drawing',
@@ -46,7 +44,7 @@ export const DeleteDocDrawingsCommand: ICommand = {
drawingType,
};
});
return commandService.executeCommand<IDeleteDrawingCommandParams>(RemoveDocDrawingCommand.id, {
return commandService.executeCommand<IRemoveDocDrawingCommandParams>(RemoveDocDrawingCommand.id, {
unitId,
drawings: newDrawings,
});
@@ -15,7 +15,7 @@
*/
import type { IAccessor, ICommand } from '@univerjs/core';
import type { IInsertDrawingCommandParams } from './interfaces';
import type { IInsertDocDrawingCommandParams } from '@univerjs/docs-drawing';
import {
BooleanNumber,
CommandType,
@@ -29,7 +29,7 @@ import {
WrapTextType,
} from '@univerjs/core';
import { buildDocTransform, docDrawingPositionToTransform } from '@univerjs/docs';
import { InsertDocDrawingCommand } from './insert-doc-drawing.command';
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
type DocShapeKind = 'rectangle' | 'ellipse';
@@ -60,7 +60,7 @@ function createShapeInsertCommand(shape: DocShapeKind, width: number, height: nu
const docTransform = buildDocTransform(width, height);
return commandService.executeCommand(InsertDocDrawingCommand.id, {
return commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId,
drawings: [{
unitId,
@@ -81,7 +81,7 @@ function createShapeInsertCommand(shape: DocShapeKind, width: number, height: nu
distR: 0,
distT: 0,
}],
} as IInsertDrawingCommandParams);
});
},
};
}
@@ -1,41 +0,0 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DrawingTypeEnum, ITextRangeParam } from '@univerjs/core';
import type { IDocDrawing } from '@univerjs/docs-drawing';
export interface IInsertDrawingCommandParams {
unitId: string;
drawings: IDocDrawing[];
textRange?: ITextRangeParam;
}
export interface IDeleteDrawingCommandParam {
unitId: string;
subUnitId: string;
drawingId: string;
drawingType: DrawingTypeEnum;
}
export interface IDeleteDrawingCommandParams {
unitId: string;
drawings: IDeleteDrawingCommandParam[];
}
export interface ISetDrawingCommandParams {
unitId: string;
drawings: Partial<IDocDrawing>[];
}
@@ -14,9 +14,8 @@
* limitations under the License.
*/
import type { DocumentDataModel, IAccessor, ICommand } from '@univerjs/core';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { IDrawingDocTransform, IUpdateDrawingDocTransformParams } from './update-doc-drawing.command';
import type { DocumentDataModel, IAccessor, ICommand, IObjectPositionH, IObjectPositionV } from '@univerjs/core';
import type { IDocDrawing, IDrawingDocTransform, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
import {
CommandType,
Direction,
@@ -25,9 +24,8 @@ import {
PositionedObjectLayoutType,
UniverInstanceType,
} from '@univerjs/core';
import { IDocDrawingService } from '@univerjs/docs-drawing';
import { IDocDrawingService, UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import { UpdateDrawingDocTransformCommand } from './update-doc-drawing.command';
export interface IMoveDrawingsCommandParams {
direction: Direction;
@@ -74,8 +72,8 @@ export const MoveDocDrawingsCommand: ICommand = {
const { positionH, positionV } = drawingData.docTransform;
const newPositionH = { ...positionH };
const newPositionV = { ...positionV };
const newPositionH: IObjectPositionH = { ...positionH };
const newPositionV: IObjectPositionV = { ...positionV };
if (direction === Direction.UP) {
newPositionV.posOffset = (newPositionV.posOffset ?? 0) - 2;
@@ -91,14 +89,14 @@ export const MoveDocDrawingsCommand: ICommand = {
drawingId,
key: direction === Direction.UP || direction === Direction.DOWN ? 'positionV' : 'positionH',
value: direction === Direction.UP || direction === Direction.DOWN ? newPositionV : newPositionH,
} as IDrawingDocTransform;
};
}).filter((drawing) => drawing != null) as IDrawingDocTransform[];
if (newDrawings.length === 0) {
return false;
}
const result = commandService.syncExecuteCommand<IUpdateDrawingDocTransformParams>(UpdateDrawingDocTransformCommand.id, {
const result = commandService.syncExecuteCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
unitId,
subUnitId: unitId,
drawings: newDrawings,
@@ -14,20 +14,15 @@
* limitations under the License.
*/
import type { DocumentDataModel, IAccessor, ICommand, IDocDrawingBase, IDocDrawingPosition, IMutationInfo, IObjectPositionH, IObjectPositionV, ISize, JSONXActions, WrapTextType } from '@univerjs/core';
import type { DocumentDataModel, IAccessor, ICommand, IDocDrawingBase, IDocDrawingPosition, IMutationInfo, JSONXActions, WrapTextType } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { IDocumentSkeletonDrawing, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage } from '@univerjs/engine-render';
import {
BooleanNumber,
CommandType,
getRichTextEditPath,
ICommandService,
IUniverInstanceService,
JSONX,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
TextX,
TextXActionType,
Tools,
@@ -35,60 +30,9 @@ import {
} from '@univerjs/core';
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { DocSelectionRenderService } from '@univerjs/docs-ui';
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
import { IRenderManagerService } from '@univerjs/engine-render';
import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service';
export enum TextWrappingStyle {
INLINE = 'inline',
BEHIND_TEXT = 'behindText',
IN_FRONT_OF_TEXT = 'inFrontOfText',
WRAP_SQUARE = 'wrapSquare',
WRAP_TOP_AND_BOTTOM = 'wrapTopAndBottom',
}
const WRAPPING_STYLE_TO_LAYOUT_TYPE = {
[TextWrappingStyle.INLINE]: PositionedObjectLayoutType.INLINE,
[TextWrappingStyle.WRAP_SQUARE]: PositionedObjectLayoutType.WRAP_SQUARE,
[TextWrappingStyle.WRAP_TOP_AND_BOTTOM]: PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM,
[TextWrappingStyle.IN_FRONT_OF_TEXT]: PositionedObjectLayoutType.WRAP_NONE,
[TextWrappingStyle.BEHIND_TEXT]: PositionedObjectLayoutType.WRAP_NONE,
};
interface IDrawingAnchorInPage {
skeDrawing: IDocumentSkeletonDrawing;
pageMarginTop: number;
pageMarginLeft: number;
}
export function findDrawingAnchorInPage(
page: IDocumentSkeletonPage | IDocumentSkeletonHeaderFooter,
drawingId: string,
pageMarginTop: number,
pageMarginLeft: number
): IDrawingAnchorInPage | null {
const skeDrawing = page.skeDrawings.get(drawingId);
if (skeDrawing) {
return {
skeDrawing,
pageMarginTop,
pageMarginLeft,
};
}
for (const table of page.skeTables.values()) {
for (const row of table.rows) {
for (const cell of row.cells) {
const cellAnchor = findDrawingAnchorInPage(cell, drawingId, cell.marginTop, cell.marginLeft);
if (cellAnchor) {
return cellAnchor;
}
}
}
}
return null;
}
// eslint-disable-next-line max-lines-per-function
function getDeleteAndInsertCustomBlockActions(
segmentId: string,
@@ -238,191 +182,6 @@ function getDeleteAndInsertCustomBlockActions(
return rawActions;
}
interface IUpdateDocDrawingWrappingStyleParams {
unitId: string;
subUnitId: string;
drawings: IDocDrawing[];
wrappingStyle: TextWrappingStyle;
}
/**
* The command to update drawing wrapping style.
*/
export const UpdateDocDrawingWrappingStyleCommand: ICommand = {
id: 'doc.command.update-doc-drawing-wrapping-style',
type: CommandType.COMMAND,
// eslint-disable-next-line max-lines-per-function, complexity
handler: (accessor: IAccessor, params?: IUpdateDocDrawingWrappingStyleParams) => {
if (params == null) {
return false;
}
const { drawings, wrappingStyle, unitId } = params;
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const renderManagerService = accessor.get(IRenderManagerService);
const renderObject = renderManagerService.getRenderById(unitId);
const skeletonData = renderObject?.with(DocSkeletonManagerService)
.getSkeleton()
.getSkeletonData();
const viewModel = renderObject?.with(DocSkeletonManagerService).getViewModel();
const scene = renderObject?.scene;
const documentDataModel = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
if (documentDataModel == null || skeletonData == null || scene == null || viewModel == null) {
return false;
}
const editArea = viewModel.getEditArea();
const transformer = scene.getTransformerByCreate();
const { pages, skeHeaders, skeFooters } = skeletonData;
const jsonX = JSONX.getInstance();
const rawActions: JSONXActions = [];
const { drawings: oldDrawings = {} } = documentDataModel.getSnapshot();
// Update drawing layoutType.
for (const drawing of drawings) {
const { drawingId } = drawing;
// Update layoutType.
const oldLayoutType = oldDrawings[drawingId].layoutType;
const newLayoutType = WRAPPING_STYLE_TO_LAYOUT_TYPE[wrappingStyle];
if (oldLayoutType !== newLayoutType) {
const updateLayoutTypeAction = jsonX.replaceOp(['drawings', drawingId, 'layoutType'], oldLayoutType, newLayoutType);
rawActions.push(updateLayoutTypeAction!);
}
// Update behindDoc if layoutType is WRAP_NONE.
if (wrappingStyle === TextWrappingStyle.BEHIND_TEXT || wrappingStyle === TextWrappingStyle.IN_FRONT_OF_TEXT) {
const oldBehindDoc = oldDrawings[drawingId].behindDoc;
const newBehindDoc = wrappingStyle === TextWrappingStyle.BEHIND_TEXT ? BooleanNumber.TRUE : BooleanNumber.FALSE;
if (oldBehindDoc !== newBehindDoc) {
const updateBehindDocAction = jsonX.replaceOp(['drawings', drawingId, 'behindDoc'], oldBehindDoc, newBehindDoc);
rawActions.push(updateBehindDocAction!);
}
}
if (wrappingStyle === TextWrappingStyle.INLINE) {
continue;
}
// Update positionH and positionV if layoutType is not inline.
let drawingAnchor: IDrawingAnchorInPage | null = null;
for (const page of pages) {
const { headerId, footerId, marginTop, marginLeft, marginBottom, pageWidth, pageHeight } = page;
switch (editArea) {
case DocumentEditArea.HEADER: {
const headerSke = skeHeaders.get(headerId)?.get(pageWidth);
if (headerSke != null) {
drawingAnchor = findDrawingAnchorInPage(headerSke, drawingId, headerSke.marginTop, marginLeft);
}
break;
}
case DocumentEditArea.FOOTER: {
const footerSke = skeFooters.get(footerId)?.get(pageWidth);
if (footerSke != null) {
drawingAnchor = findDrawingAnchorInPage(footerSke, drawingId, pageHeight - marginBottom + footerSke.marginTop, marginLeft);
}
break;
}
case DocumentEditArea.BODY: {
drawingAnchor = findDrawingAnchorInPage(page, drawingId, marginTop, marginLeft);
break;
}
}
if (drawingAnchor != null) {
break;
}
}
if (drawingAnchor != null) {
const { skeDrawing, pageMarginTop, pageMarginLeft } = drawingAnchor;
const { aTop, aLeft } = skeDrawing;
const oldPositionH = oldDrawings[drawingId].docTransform.positionH;
let posOffsetH = aLeft;
if (oldPositionH.relativeFrom === ObjectRelativeFromH.MARGIN) {
posOffsetH -= pageMarginLeft;
} else if (oldPositionH.relativeFrom === ObjectRelativeFromH.COLUMN) {
posOffsetH -= skeDrawing.columnLeft;
}
const newPositionH = {
relativeFrom: oldPositionH.relativeFrom,
posOffset: posOffsetH,
};
if (oldPositionH.posOffset !== newPositionH.posOffset) {
const action = jsonX.replaceOp(['drawings', drawingId, 'docTransform', 'positionH'], oldPositionH, newPositionH);
rawActions.push(action!);
}
const oldPositionV = oldDrawings[drawingId].docTransform.positionV;
let posOffsetV = aTop;
if (oldPositionV.relativeFrom === ObjectRelativeFromV.PAGE) {
posOffsetV += pageMarginTop;
} else if (oldPositionV.relativeFrom === ObjectRelativeFromV.LINE) {
posOffsetV -= skeDrawing.lineTop;
} else if (oldPositionV.relativeFrom === ObjectRelativeFromV.PARAGRAPH) {
posOffsetV -= skeDrawing.blockAnchorTop;
}
const newPositionV = {
relativeFrom: oldPositionV.relativeFrom,
posOffset: posOffsetV,
};
if (oldPositionV.posOffset !== newPositionV.posOffset) {
const action = jsonX.replaceOp(['drawings', drawingId, 'docTransform', 'positionV'], oldPositionV, newPositionV);
rawActions.push(action!);
}
}
}
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
id: RichTextEditingMutation.id,
params: {
unitId,
actions: [],
textRanges: null,
},
};
doMutation.params.actions = rawActions.reduce((acc, cur) => {
return JSONX.compose(acc, cur as JSONXActions);
}, null as JSONXActions);
const result = commandService.syncExecuteCommand<
IRichTextEditingMutationParams,
IRichTextEditingMutationParams
>(doMutation.id, doMutation.params);
transformer.refreshControls();
return Boolean(result);
},
};
interface IDist {
distT: number;
distB: number;
@@ -569,99 +328,6 @@ export const UpdateDocDrawingWrapTextCommand: ICommand = {
},
};
export interface IDrawingDocTransform {
drawingId: string;
key: 'size' | 'angle' | 'positionH' | 'positionV';
value: ISize | number | IObjectPositionH | IObjectPositionV;
}
export interface IUpdateDrawingDocTransformParams {
unitId: string;
subUnitId: string;
drawings: IDrawingDocTransform[];
}
/**
* The command to update drawing position.
*/
export const UpdateDrawingDocTransformCommand: ICommand = {
id: 'doc.command.update-drawing-doc-transform',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IUpdateDrawingDocTransformParams) => {
if (params == null) {
return false;
}
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const renderManagerService = accessor.get(IRenderManagerService);
const { drawings, unitId } = params;
const renderObject = renderManagerService.getRenderUnitById(unitId);
const scene = renderObject?.scene;
if (scene == null) {
return false;
}
const transformer = scene.getTransformerByCreate();
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
if (documentDataModel == null) {
return false;
}
const jsonX = JSONX.getInstance();
const rawActions: JSONXActions = [];
const { drawings: oldDrawings = {} } = documentDataModel.getSnapshot();
// Update drawing layoutType.
for (const drawing of drawings) {
const { drawingId, key, value } = drawing;
const oldValue = oldDrawings[drawingId].docTransform[key];
if (!Tools.diffValue(oldValue, value)) {
const action = jsonX.replaceOp(['drawings', drawingId, 'docTransform', key], oldValue, value);
rawActions.push(action!);
}
}
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
id: RichTextEditingMutation.id,
params: {
unitId,
actions: [],
textRanges: null,
debounce: true,
},
};
doMutation.params.actions = rawActions.reduce((acc, cur) => {
return JSONX.compose(acc, cur as JSONXActions);
}, null as JSONXActions);
const result = commandService.syncExecuteCommand<
IRichTextEditingMutationParams,
IRichTextEditingMutationParams
>(doMutation.id, doMutation.params);
// RichTextEditingMutation recalculates the document skeleton before the
// synchronous command returns. Publish that fresh geometry so the drawing
// manager, renderer, and transformer do not keep the pre-mutation size.
if (accessor.has(DocRefreshDrawingsService)) {
const skeleton = renderObject?.with(DocSkeletonManagerService).getSkeleton() ?? null;
accessor.get(DocRefreshDrawingsService).refreshDrawings(skeleton);
}
transformer.refreshControls();
return Boolean(result);
},
};
export interface IMoveInlineDrawingParams {
unitId: string;
subUnitId: string;
@@ -14,8 +14,16 @@
* limitations under the License.
*/
import { BooleanNumber, PositionedObjectLayoutType, UndoCommand } from '@univerjs/core';
import {
BooleanNumber,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
UndoCommand,
} from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
import { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand } from '@univerjs/docs-drawing';
import { DocumentEditArea } from '@univerjs/engine-render';
import { describe, expect, it, vi } from 'vitest';
import { DocDrawingAddRemoveController } from '../doc-drawing-notification.controller';
@@ -23,6 +31,52 @@ function createController() {
const beforeHandlers: Array<(command: { id: string; params?: unknown }) => void> = [];
const executedHandlers: Array<(command: { id: string; params?: unknown }) => void> = [];
const refreshControls = vi.fn();
const refreshDrawings = vi.fn();
const drawing = {
unitId: 'doc-1',
subUnitId: 'doc-1',
drawingId: 'drawing-1',
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.TRUE,
docTransform: {
size: { width: 100, height: 60 },
angle: 0,
positionH: { relativeFrom: ObjectRelativeFromH.MARGIN, posOffset: 1 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 2 },
},
};
const skeleton = {
getSkeletonData: () => ({
pages: [{
marginTop: 20,
marginLeft: 30,
marginBottom: 20,
pageWidth: 600,
pageHeight: 800,
headerId: '',
footerId: '',
skeDrawings: new Map([['drawing-1', {
drawingId: 'drawing-1',
aLeft: 130,
aTop: 180,
columnLeft: 30,
lineTop: 150,
blockAnchorTop: 140,
drawingOrigin: drawing,
}]]),
skeTables: new Map(),
}],
skeHeaders: new Map(),
skeFooters: new Map(),
}),
};
const renderObject = {
scene: { getTransformerByCreate: () => ({ refreshControls }) },
with: () => ({
getSkeleton: () => skeleton,
getViewModel: () => ({ getEditArea: () => DocumentEditArea.BODY }),
}),
};
const drawingManagerService = {
applyJson1: vi.fn(),
addNotification: vi.fn(),
@@ -44,12 +98,16 @@ function createController() {
{
getCurrentUnitOfType: vi.fn(() => ({ getUnitId: () => 'doc-1' })),
getUnit: vi.fn(() => ({
getDrawings: () => ({
'drawing-1': drawing,
'drawing-2': {
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.FALSE,
},
}),
getSnapshot: () => ({
drawings: {
'drawing-1': {
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.TRUE,
},
'drawing-1': drawing,
'drawing-2': {
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.FALSE,
@@ -72,12 +130,23 @@ function createController() {
drawingManagerService as never,
docDrawingService as never,
{
getRenderById: vi.fn(() => ({ scene: { getTransformerByCreate: () => ({ refreshControls }) } })),
getRenderUnitById: vi.fn(() => ({ scene: { getTransformerByCreate: () => ({ refreshControls }) } })),
} as never
getRenderById: vi.fn(() => renderObject),
getRenderUnitById: vi.fn(() => renderObject),
} as never,
{ refreshDrawings } as never
);
return { controller, beforeHandlers, executedHandlers, drawingManagerService, docDrawingService, refreshControls };
return {
controller,
beforeHandlers,
executedHandlers,
drawingManagerService,
docDrawingService,
drawing,
refreshControls,
refreshDrawings,
skeleton,
};
}
describe('DocDrawingAddRemoveController', () => {
@@ -123,7 +192,7 @@ describe('DocDrawingAddRemoveController', () => {
actions: ['drawingsOrder', [0, { d: 0 }], [1, { p: 0 }]],
},
});
executedHandlers[1]({ id: UndoCommand.id });
executedHandlers.forEach((handler) => handler({ id: UndoCommand.id }));
expect(drawingManagerService.setDrawingOrder).toHaveBeenCalledWith('doc-1', 'doc-1', ['drawing-1', 'drawing-2']);
expect(docDrawingService.setDrawingOrder).toHaveBeenCalledWith('doc-1', 'doc-1', ['drawing-2', 'drawing-1']);
@@ -141,4 +210,43 @@ describe('DocDrawingAddRemoveController', () => {
controller.dispose();
});
it('preserves skeleton position before wrapping changes and refreshes drawings afterward', () => {
const {
controller,
beforeHandlers,
executedHandlers,
drawing,
refreshControls,
refreshDrawings,
skeleton,
} = createController();
const params = {
unitId: 'doc-1',
subUnitId: 'doc-1',
drawings: [{ drawingId: 'drawing-1' }],
wrappingStyle: TextWrappingStyle.WRAP_SQUARE,
};
const command = { id: UpdateDocDrawingWrappingStyleCommand.id, params };
beforeHandlers.forEach((handler) => handler(command));
expect(params.drawings[0]).toMatchObject({
drawingId: 'drawing-1',
docTransform: {
positionH: { relativeFrom: ObjectRelativeFromH.MARGIN, posOffset: 100 },
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 40 },
},
});
expect(drawing.docTransform).toMatchObject({
positionH: { posOffset: 1 },
positionV: { posOffset: 2 },
});
executedHandlers.forEach((handler) => handler(command));
expect(refreshDrawings).toHaveBeenCalledWith(skeleton);
expect(refreshControls).toHaveBeenCalled();
controller.dispose();
});
});
@@ -15,12 +15,12 @@
*/
import { ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType } from '@univerjs/core';
import { UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import {
IMoveInlineDrawingCommand,
ITransformNonInlineDrawingCommand,
UpdateDrawingDocTransformCommand,
} from '../../commands/commands/update-doc-drawing.command';
import { DocDrawingTransformerController, getDocsTableCellAnchorContext } from '../doc-drawing-transformer-update.controller';
@@ -15,10 +15,10 @@
*/
import { DrawingTypeEnum } from '@univerjs/core';
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
import { Rect } from '@univerjs/engine-render';
import { BehaviorSubject, Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { InsertDocDrawingCommand } from '../../commands/commands/insert-doc-drawing.command';
import { calcDocFloatDomPositionByRect, DocFloatDomController } from '../doc-float-dom.controller';
function createScene() {
@@ -18,21 +18,32 @@
import type { DocumentDataModel, ICommandInfo, IDrawingSearch, JSONXActions, Nullable } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { IDocDrawing, IUpdateDocDrawingWrappingStyleParams, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
import type { IDrawingJsonUndo1, IDrawingMapItemData, IDrawingOrderMapParam } from '@univerjs/drawing';
import type { IDocumentSkeletonDrawing, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage } from '@univerjs/engine-render';
import {
Disposable,
ICommandService,
Inject,
IUniverInstanceService,
JSONX,
ObjectRelativeFromH,
ObjectRelativeFromV,
RedoCommand,
UndoCommand,
UniverInstanceType,
} from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
import { getDocDrawingRenderOrder, IDocDrawingService } from '@univerjs/docs-drawing';
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
import {
getDocDrawingRenderOrder,
IDocDrawingService,
TextWrappingStyle,
UpdateDocDrawingWrappingStyleCommand,
UpdateDrawingDocTransformCommand,
} from '@univerjs/docs-drawing';
import { IDrawingManagerService } from '@univerjs/drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
import { DocRefreshDrawingsService } from '../services/doc-refresh-drawings.service';
interface IAddOrRemoveDrawing {
type: 'add' | 'remove';
@@ -40,6 +51,37 @@ interface IAddOrRemoveDrawing {
drawing?: IDocDrawing;
}
interface IDrawingAnchorInPage {
skeDrawing: IDocumentSkeletonDrawing;
pageMarginTop: number;
pageMarginLeft: number;
}
function findDrawingAnchorInPage(
page: IDocumentSkeletonPage | IDocumentSkeletonHeaderFooter,
drawingId: string,
pageMarginTop: number,
pageMarginLeft: number
): IDrawingAnchorInPage | null {
const skeDrawing = page.skeDrawings.get(drawingId);
if (skeDrawing) {
return { skeDrawing, pageMarginTop, pageMarginLeft };
}
for (const table of page.skeTables.values()) {
for (const row of table.rows) {
for (const cell of row.cells) {
const cellAnchor = findDrawingAnchorInPage(cell, drawingId, cell.marginTop, cell.marginLeft);
if (cellAnchor) {
return cellAnchor;
}
}
}
}
return null;
}
// Check whether drawings are added or deleted from the mutation and obtain the drawing ID.
// eslint-disable-next-line complexity
function getAddOrRemoveDrawings(actions: JSONXActions): Nullable<IAddOrRemoveDrawing[]> {
@@ -152,7 +194,8 @@ export class DocDrawingAddRemoveController extends Disposable {
@ICommandService private readonly _commandService: ICommandService,
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
@IDocDrawingService private readonly _docDrawingService: IDocDrawingService,
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
@Inject(DocRefreshDrawingsService) private readonly _docRefreshDrawingsService: DocRefreshDrawingsService
) {
super();
@@ -163,6 +206,7 @@ export class DocDrawingAddRemoveController extends Disposable {
this._commandExecutedListener();
}
// eslint-disable-next-line max-lines-per-function
private _commandExecutedListener() {
this.disposeWithMe(
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
@@ -190,6 +234,16 @@ export class DocDrawingAddRemoveController extends Disposable {
})
);
this.disposeWithMe(
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
if (command.id !== UpdateDocDrawingWrappingStyleCommand.id) {
return;
}
this._preserveWrappingStylePosition(command.params as IUpdateDocDrawingWrappingStyleParams);
})
);
this.disposeWithMe(
this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (command.id !== RichTextEditingMutation.id) {
@@ -211,6 +265,27 @@ export class DocDrawingAddRemoveController extends Disposable {
})
);
this.disposeWithMe(
this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (
command.id !== UpdateDrawingDocTransformCommand.id &&
command.id !== UpdateDocDrawingWrappingStyleCommand.id
) {
return;
}
const { unitId } = command.params as IUpdateDrawingDocTransformCommandParams | IUpdateDocDrawingWrappingStyleParams;
const renderObject = this._renderManagerService.getRenderById(unitId);
const scene = renderObject?.scene;
if (renderObject == null || scene == null) {
return;
}
this._docRefreshDrawingsService.refreshDrawings(renderObject.with(DocSkeletonManagerService).getSkeleton());
scene.getTransformerByCreate().refreshControls();
})
);
this.disposeWithMe(
this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (command.id !== UndoCommand.id && command.id !== RedoCommand.id) {
@@ -236,6 +311,99 @@ export class DocDrawingAddRemoveController extends Disposable {
);
}
// eslint-disable-next-line max-lines-per-function
private _preserveWrappingStylePosition(params: IUpdateDocDrawingWrappingStyleParams): void {
if (params.wrappingStyle === TextWrappingStyle.INLINE) {
return;
}
const { unitId } = params;
const documentDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
unitId,
UniverInstanceType.UNIVER_DOC
);
const renderObject = this._renderManagerService.getRenderById(unitId);
const skeletonManager = renderObject?.with(DocSkeletonManagerService);
const skeletonData = skeletonManager?.getSkeleton().getSkeletonData();
const viewModel = skeletonManager?.getViewModel();
if (!documentDataModel || !skeletonData || !viewModel) {
return;
}
const editArea = viewModel.getEditArea();
const { pages, skeHeaders, skeFooters } = skeletonData;
const oldDrawings = documentDataModel.getDrawings() ?? {};
params.drawings = params.drawings.map((drawing) => {
const oldDrawing = oldDrawings[drawing.drawingId] as IDocDrawing | undefined;
if (!oldDrawing) {
return drawing;
}
let drawingAnchor: IDrawingAnchorInPage | null = null;
for (const page of pages) {
const { headerId, footerId, marginTop, marginLeft, marginBottom, pageWidth, pageHeight } = page;
if (editArea === DocumentEditArea.HEADER) {
const header = skeHeaders.get(headerId)?.get(pageWidth);
if (header) {
drawingAnchor = findDrawingAnchorInPage(header, drawing.drawingId, header.marginTop, marginLeft);
}
} else if (editArea === DocumentEditArea.FOOTER) {
const footer = skeFooters.get(footerId)?.get(pageWidth);
if (footer) {
drawingAnchor = findDrawingAnchorInPage(
footer,
drawing.drawingId,
pageHeight - marginBottom + footer.marginTop,
marginLeft
);
}
} else {
drawingAnchor = findDrawingAnchorInPage(page, drawing.drawingId, marginTop, marginLeft);
}
if (drawingAnchor) {
break;
}
}
if (!drawingAnchor) {
return drawing;
}
const { skeDrawing, pageMarginTop, pageMarginLeft } = drawingAnchor;
const oldPositionH = oldDrawing.docTransform.positionH;
const oldPositionV = oldDrawing.docTransform.positionV;
let posOffsetH = skeDrawing.aLeft;
let posOffsetV = skeDrawing.aTop;
if (oldPositionH.relativeFrom === ObjectRelativeFromH.MARGIN) {
posOffsetH -= pageMarginLeft;
} else if (oldPositionH.relativeFrom === ObjectRelativeFromH.COLUMN) {
posOffsetH -= skeDrawing.columnLeft;
}
if (oldPositionV.relativeFrom === ObjectRelativeFromV.PAGE) {
posOffsetV += pageMarginTop;
} else if (oldPositionV.relativeFrom === ObjectRelativeFromV.LINE) {
posOffsetV -= skeDrawing.lineTop;
} else if (oldPositionV.relativeFrom === ObjectRelativeFromV.PARAGRAPH) {
posOffsetV -= skeDrawing.blockAnchorTop;
}
return {
...oldDrawing,
...drawing,
docTransform: {
...oldDrawing.docTransform,
...drawing.docTransform,
positionH: { relativeFrom: oldPositionH.relativeFrom, posOffset: posOffsetH },
positionV: { relativeFrom: oldPositionV.relativeFrom, posOffset: posOffsetV },
},
};
});
}
private _addDrawings(unitId: string, drawings: IDocDrawing[]) {
const drawingManagerService = this._drawingManagerService;
const docDrawingService = this._docDrawingService;
@@ -15,8 +15,8 @@
*/
import type { DocumentDataModel, IDocDrawingBase, IDocDrawingPosition, Nullable } from '@univerjs/core';
import type { IDrawingDocTransform, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
import type { BaseObject, Documents, IDocumentSkeletonGlyph, IDocumentSkeletonPage, IDocumentSkeletonRow, IDocumentSkeletonTable, Image, IPoint, Viewport } from '@univerjs/engine-render';
import type { IDrawingDocTransform } from '../commands/commands/update-doc-drawing.command';
import {
BooleanNumber,
COLORS,
@@ -33,10 +33,11 @@ import {
UniverInstanceType,
} from '@univerjs/core';
import { DocSkeletonManagerService } from '@univerjs/docs';
import { UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
import { DocSelectionRenderService, getAnchorBounding, getDocObject, getOneTextSelectionRange, NodePositionConvertToCursor, TEXT_RANGE_LAYER_INDEX } from '@univerjs/docs-ui';
import { IDrawingManagerService } from '@univerjs/drawing';
import { DocumentSkeletonPageType, getColor, IRenderManagerService, Liquid, PageLayoutType, Rect, Vector2 } from '@univerjs/engine-render';
import { IMoveInlineDrawingCommand, ITransformNonInlineDrawingCommand, UpdateDrawingDocTransformCommand } from '../commands/commands/update-doc-drawing.command';
import { IMoveInlineDrawingCommand, ITransformNonInlineDrawingCommand } from '../commands/commands/update-doc-drawing.command';
import { getDocsTableCellDrawingOffset } from './render-controllers/doc-drawing-transform-update.controller';
const INLINE_DRAWING_ANCHOR_KEY_PREFIX = '__InlineDrawingAnchor__';
@@ -368,7 +369,7 @@ export class DocDrawingTransformerController extends Disposable {
}
if (drawings.length > 0 && unitId && subUnitId) {
this._commandService.executeCommand(UpdateDrawingDocTransformCommand.id, {
this._commandService.executeCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
unitId,
subUnitId,
drawings,
@@ -718,7 +719,7 @@ export class DocDrawingTransformerController extends Disposable {
}
if (drawings.length > 0 && unitId && subUnitId) {
this._commandService.executeCommand(UpdateDrawingDocTransformCommand.id, {
this._commandService.executeCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
unitId,
subUnitId,
drawings,
@@ -15,12 +15,11 @@
*/
import type { DocumentDataModel, IDisposable, IDrawingSearch, ITransformState, Nullable } from '@univerjs/core';
import type { IDocFloatDom } from '@univerjs/docs-drawing';
import type { IDocFloatDom, IInsertDocDrawingCommandParams } from '@univerjs/docs-drawing';
import type { ISetDocZoomRatioOperationParams } from '@univerjs/docs-ui';
import type { IDocFloatDomDataBase } from '@univerjs/drawing';
import type { IBoundRectNoAngle, IDocsCustomBlockRenderViewport, IRender, Rect, Scene } from '@univerjs/engine-render';
import type { IFloatDomLayout } from '@univerjs/ui';
import type { IInsertDrawingCommandParams } from '../commands/commands/interfaces';
import {
Disposable,
DisposableCollection,
@@ -37,13 +36,13 @@ import {
UniverInstanceType,
} from '@univerjs/core';
import { docDrawingPositionToTransform, DocSkeletonManagerService } from '@univerjs/docs';
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
import { SetDocZoomRatioOperation, VIEWPORT_KEY } from '@univerjs/docs-ui';
import { IDrawingManagerService } from '@univerjs/drawing';
import { DrawingRenderService } from '@univerjs/drawing-ui';
import { CURSOR_TYPE, IRenderManagerService } from '@univerjs/engine-render';
import { CanvasFloatDomService } from '@univerjs/ui';
import { BehaviorSubject, map, of, switchMap } from 'rxjs';
import { InsertDocDrawingCommand } from '../commands/commands/insert-doc-drawing.command';
export function calcDocFloatDomPositionByRect(
rect: IBoundRectNoAngle,
@@ -444,7 +443,7 @@ export class DocFloatDomController extends Disposable {
angle: 0,
};
const drawingId = opts.drawingId ?? generateRandomId();
const params: IInsertDrawingCommandParams = {
const params: IInsertDocDrawingCommandParams = {
unitId: currentDoc.getUnitId(),
drawings: [
{
@@ -461,7 +460,7 @@ export class DocFloatDomController extends Disposable {
},
],
};
this._commandService.syncExecuteCommand(InsertDocDrawingCommand.id, params);
this._commandService.syncExecuteCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, params);
return drawingId;
}
@@ -16,11 +16,11 @@
import { BooleanNumber, FOCUSING_COMMON_DRAWINGS } from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
import { SetDocDrawingArrangeCommand } from '@univerjs/docs-drawing';
import { DocumentEditArea } from '@univerjs/engine-render';
import { Subject } from 'rxjs';
import { describe, expect, it, vi } from 'vitest';
import { GroupDocDrawingCommand } from '../../../commands/commands/group-doc-drawing.command';
import { SetDocDrawingArrangeCommand } from '../../../commands/commands/set-drawing-arrange.command';
import { UngroupDocDrawingCommand } from '../../../commands/commands/ungroup-doc-drawing.command';
import { DocDrawingUpdateRenderController } from '../doc-drawing-update.render-controller';
@@ -16,10 +16,8 @@
import type { DocumentDataModel, ICommandInfo, IDocDrawingPosition, IDrawingParam, IImageIoServiceParam, Nullable } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { IDocDrawing, IInsertDocDrawingCommandParams, ISetDocDrawingArrangeCommandParams } from '@univerjs/docs-drawing';
import type { Documents, Image, IRenderContext, IRenderModule } from '@univerjs/engine-render';
import type { IInsertDrawingCommandParams } from '../../commands/commands/interfaces';
import type { ISetDrawingArrangeCommandParams } from '../../commands/commands/set-drawing-arrange.command';
import type { LocaleKey } from '../../locale/types';
import {
BooleanNumber,
@@ -37,7 +35,7 @@ import {
} from '@univerjs/core';
import { MessageType } from '@univerjs/design';
import { buildDocTransform, docDrawingPositionToTransform, DocSelectionManagerService, DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { IDocDrawingService } from '@univerjs/docs-drawing';
import { IDocDrawingService, InsertDocDrawingCommand, SetDocDrawingArrangeCommand } from '@univerjs/docs-drawing';
import { DocSelectionRenderService } from '@univerjs/docs-ui';
import {
DRAWING_IMAGE_ALLOW_IMAGE_LIST,
@@ -53,8 +51,6 @@ import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render
import { ILocalFileService, IMessageService } from '@univerjs/ui';
import { debounceTime } from 'rxjs';
import { GroupDocDrawingCommand } from '../../commands/commands/group-doc-drawing.command';
import { InsertDocDrawingCommand } from '../../commands/commands/insert-doc-drawing.command';
import { SetDocDrawingArrangeCommand } from '../../commands/commands/set-drawing-arrange.command';
import { UngroupDocDrawingCommand } from '../../commands/commands/ungroup-doc-drawing.command';
import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service';
@@ -211,10 +207,10 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen
docDrawingParams.push(docDrawingParam);
}
this._commandService.executeCommand(InsertDocDrawingCommand.id, {
this._commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
unitId,
drawings: docDrawingParams,
} as IInsertDrawingCommandParams);
});
}
private _isInsertInHeaderFooter() {
@@ -262,12 +258,12 @@ export class DocDrawingUpdateRenderController extends Disposable implements IRen
this._drawingManagerService.featurePluginOrderUpdate$.subscribe((params) => {
const { unitId, subUnitId, drawingIds, arrangeType } = params;
this._commandService.executeCommand(SetDocDrawingArrangeCommand.id, {
this._commandService.executeCommand<ISetDocDrawingArrangeCommandParams>(SetDocDrawingArrangeCommand.id, {
unitId,
subUnitId,
drawingIds,
arrangeType,
} as ISetDrawingArrangeCommandParams);
});
})
);
}
@@ -18,20 +18,15 @@ import { Disposable, ICommandService } from '@univerjs/core';
import { IMenuManagerService, IShortcutService } from '@univerjs/ui';
import { DeleteDocDrawingsCommand } from '../commands/commands/delete-doc-drawing.command';
import { GroupDocDrawingCommand } from '../commands/commands/group-doc-drawing.command';
import { InsertDocDrawingCommand } from '../commands/commands/insert-doc-drawing.command';
import { InsertDocImageCommand } from '../commands/commands/insert-image.command';
import { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from '../commands/commands/insert-shape.command';
import { MoveDocDrawingsCommand } from '../commands/commands/move-drawings.command';
import { RemoveDocDrawingCommand } from '../commands/commands/remove-doc-drawing.command';
import { SetDocDrawingArrangeCommand } from '../commands/commands/set-drawing-arrange.command';
import { UngroupDocDrawingCommand } from '../commands/commands/ungroup-doc-drawing.command';
import {
IMoveInlineDrawingCommand,
ITransformNonInlineDrawingCommand,
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingWrapTextCommand,
UpdateDrawingDocTransformCommand,
} from '../commands/commands/update-doc-drawing.command';
import { ClearDocDrawingTransformerOperation } from '../commands/operations/clear-drawing-transformer.operation';
import { EditDocDrawingOperation } from '../commands/operations/edit-doc-drawing.operation';
@@ -68,14 +63,10 @@ export class DocDrawingUIController extends Disposable {
InsertDocImageCommand,
InsertDocRectangleShapeCommand,
InsertDocEllipseShapeCommand,
InsertDocDrawingCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrapTextCommand,
UpdateDrawingDocTransformCommand,
IMoveInlineDrawingCommand,
ITransformNonInlineDrawingCommand,
RemoveDocDrawingCommand,
SidebarDocDrawingOperation,
ClearDocDrawingTransformerOperation,
EditDocDrawingOperation,
@@ -83,7 +74,6 @@ export class DocDrawingUIController extends Disposable {
UngroupDocDrawingCommand,
MoveDocDrawingsCommand,
DeleteDocDrawingsCommand,
SetDocDrawingArrangeCommand,
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
}
-4
View File
@@ -18,14 +18,10 @@ import './global.css';
export { DeleteDocDrawingsCommand } from './commands/commands/delete-doc-drawing.command';
export { GroupDocDrawingCommand } from './commands/commands/group-doc-drawing.command';
export { InsertDocDrawingCommand } from './commands/commands/insert-doc-drawing.command';
export { InsertDocImageCommand } from './commands/commands/insert-image.command';
export { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from './commands/commands/insert-shape.command';
export { MoveDocDrawingsCommand } from './commands/commands/move-drawings.command';
export { RemoveDocDrawingCommand } from './commands/commands/remove-doc-drawing.command';
export { SetDocDrawingArrangeCommand } from './commands/commands/set-drawing-arrange.command';
export { UngroupDocDrawingCommand } from './commands/commands/ungroup-doc-drawing.command';
export { UpdateDrawingDocTransformCommand } from './commands/commands/update-doc-drawing.command';
export { ClearDocDrawingTransformerOperation } from './commands/operations/clear-drawing-transformer.operation';
export { EditDocDrawingOperation } from './commands/operations/edit-doc-drawing.operation';
export { SidebarDocDrawingOperation } from './commands/operations/open-drawing-panel.operation';
@@ -28,7 +28,7 @@ import {
RxDisposable,
UniverInstanceType,
} from '@univerjs/core';
import { IDocDrawingAdapterService } from '@univerjs/docs-drawing';
import { IDocDrawingAdapterService, RemoveDocDrawingCommand } from '@univerjs/docs-drawing';
import { DocCanvasPopManagerService } from '@univerjs/docs-ui';
import { IDrawingManagerService } from '@univerjs/drawing';
import {
@@ -39,7 +39,6 @@ import {
} from '@univerjs/drawing-ui';
import { IRenderManagerService } from '@univerjs/engine-render';
import { takeUntil } from 'rxjs';
import { RemoveDocDrawingCommand } from '../commands/commands/remove-doc-drawing.command';
import { EditDocDrawingOperation } from '../commands/operations/edit-doc-drawing.operation';
import { SidebarDocDrawingOperation } from '../commands/operations/open-drawing-panel.operation';
import { DocDrawingFloatingToolbarAdapterService } from '../services/doc-drawing-floating-toolbar-adapter.service';
@@ -15,7 +15,7 @@
*/
import type { DocumentDataModel, ICommandInfo, IDrawingParam, IObjectPositionH, IObjectPositionV, Nullable } from '@univerjs/core';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { IDocDrawing, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
import type { IDocumentSkeletonDrawing } from '@univerjs/engine-render';
import type { LocaleKey } from '../../locale/types';
import {
@@ -30,12 +30,12 @@ import {
} from '@univerjs/core';
import { Checkbox, clsx, InputNumber, Select } from '@univerjs/design';
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
import { DocSelectionRenderService } from '@univerjs/docs-ui';
import { IDrawingManagerService } from '@univerjs/drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import { useDependency } from '@univerjs/ui';
import { useEffect, useState } from 'react';
import { UpdateDrawingDocTransformCommand } from '../../commands/commands/update-doc-drawing.command';
const MIN_OFFSET = -1000;
const MAX_OFFSET = 1000;
@@ -135,7 +135,7 @@ export const DocDrawingPosition = (props: IDocDrawingPositionProps) => {
};
});
commandService.executeCommand(UpdateDrawingDocTransformCommand.id, {
commandService.executeCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
unitId: focusDrawings[0].unitId,
subUnitId: focusDrawings[0].unitId,
drawings: drawings.map((drawing) => ({
@@ -28,14 +28,13 @@ import {
} from '@univerjs/core';
import { clsx, InputNumber, Radio, RadioGroup } from '@univerjs/design';
import { RichTextEditingMutation } from '@univerjs/docs';
import { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand } from '@univerjs/docs-drawing';
import { IDrawingManagerService } from '@univerjs/drawing';
import { IRenderManagerService } from '@univerjs/engine-render';
import { useDependency } from '@univerjs/ui';
import { useEffect, useState } from 'react';
import {
TextWrappingStyle,
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingWrapTextCommand,
} from '../../commands/commands/update-doc-drawing.command';
@@ -37,7 +37,6 @@ import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it } from 'vitest';
import { createDocUiTestBed } from '../../../__tests__/create-doc-ui-test-bed';
import { UpdateDrawingDocTransformCommand } from '../../../commands/commands/update-doc-drawing.command';
import locale from '../../../locale/en-US';
import { DocDrawingPosition } from '../DocDrawingPosition';
@@ -239,7 +238,6 @@ function createPositionTestBed() {
const commandService = injector.get(ICommandService);
[
UpdateDrawingDocTransformCommand,
RichTextEditingMutation as unknown as ICommand,
].forEach((command) => commandService.registerCommand(command));
@@ -40,10 +40,11 @@ import { afterEach, describe, expect, it } from 'vitest';
import { createDocUiTestBed } from '../../../__tests__/create-doc-ui-test-bed';
import {
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingWrapTextCommand,
} from '../../../commands/commands/update-doc-drawing.command';
import { DocDrawingAddRemoveController } from '../../../controllers/doc-drawing-notification.controller';
import locale from '../../../locale/en-US';
import { DocRefreshDrawingsService } from '../../../services/doc-refresh-drawings.service';
import { DocDrawingTextWrap } from '../DocDrawingTextWrap';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -215,12 +216,13 @@ function createPanelTestBed() {
injector.add([DocDrawingService]);
injector.add([IDocDrawingService, { useClass: DocDrawingService }]);
injector.add([IDrawingManagerService, { useClass: DrawingManagerService }]);
injector.add([DocRefreshDrawingsService]);
injector.add([DocDrawingController]);
injector.add([DocDrawingAddRemoveController]);
const commandService = injector.get(ICommandService);
[
UpdateDocDrawingDistanceCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDocDrawingWrapTextCommand,
RichTextEditingMutation as unknown as ICommand,
].forEach((command) => commandService.registerCommand(command));
@@ -231,6 +233,7 @@ function createPanelTestBed() {
injector.get(LocaleService).setLocale(LocaleType.EN_US);
injector.get(DocDrawingController).loadDrawingDataForUnit(UNIT_ID);
injector.get(DocDrawingAddRemoveController);
injector.get(IDrawingManagerService).focusDrawing([{
unitId: UNIT_ID,
subUnitId: UNIT_ID,
+14 -1
View File
@@ -26,7 +26,8 @@
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
"./*": "./src/*",
"./facade": "./src/facade/index.ts"
},
"main": "./src/index.ts",
"types": "./lib/types/index.d.ts",
@@ -45,6 +46,16 @@
"require": "./lib/cjs/*",
"types": "./lib/types/index.d.ts"
},
"./facade": {
"import": "./lib/es/facade.js",
"require": "./lib/cjs/facade.js",
"types": "./lib/types/facade/index.d.ts"
},
"./lib/facade": {
"import": "./lib/es/facade.js",
"require": "./lib/cjs/facade.js",
"types": "./lib/types/facade/index.d.ts"
},
"./lib/*": "./lib/*"
}
},
@@ -65,10 +76,12 @@
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/docs": "workspace:*",
"@univerjs/drawing": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"@univerjs/engine-render": "workspace:*",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
}
@@ -0,0 +1,110 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DocumentDataModel, IDocumentData } from '@univerjs/core';
import type { IUpdateDocDrawingWrappingStyleParams } from '@univerjs/docs-drawing';
import {
BooleanNumber,
ICommandService,
ImageSourceType,
IUniverInstanceService,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
UniverInstanceType,
} from '@univerjs/core';
import { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand } from '@univerjs/docs-drawing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFacadeTestBed } from '../../../facade/__tests__/create-test-bed';
class MockImage {
width = 800;
height = 400;
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
get src(): string {
return '';
}
set src(_value: string) {
queueMicrotask(() => this.onload?.());
}
}
describe('UpdateDocDrawingWrappingStyleCommand', () => {
let testBed: ReturnType<typeof createFacadeTestBed>;
beforeEach(() => {
vi.stubGlobal('Image', MockImage);
testBed = createFacadeTestBed();
});
afterEach(() => {
testBed.univer.dispose();
vi.unstubAllGlobals();
});
it('updates the requested document without render services or current-unit dependence', async () => {
const image = await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
width: 160,
height: 90,
textRange: {
startOffset: 3,
endOffset: 3,
collapsed: true,
segmentId: '',
},
});
const positionH = { relativeFrom: ObjectRelativeFromH.MARGIN, posOffset: 80 };
const positionV = { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 120 };
const drawing = image!.getImageData()!;
const otherDocument = testBed.univer.createUnit<IDocumentData, DocumentDataModel>(
UniverInstanceType.UNIVER_DOC,
{
id: 'other-doc',
documentStyle: {},
body: { dataStream: '\r\n', paragraphs: [{ startIndex: 0, paragraphId: 'other-paragraph' }], customBlocks: [] },
drawings: {},
drawingsOrder: [],
}
);
testBed.injector.get(IUniverInstanceService).focusUnit(otherDocument.getUnitId());
const result = testBed.injector.get(ICommandService).syncExecuteCommand<IUpdateDocDrawingWrappingStyleParams>(
UpdateDocDrawingWrappingStyleCommand.id,
{
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [{
...drawing,
docTransform: { ...drawing.docTransform, positionH, positionV },
}],
wrappingStyle: TextWrappingStyle.BEHIND_TEXT,
}
);
expect(result).toBe(true);
expect(testBed.document.getImage(image!.getId())?.getImageData()).toMatchObject({
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.TRUE,
docTransform: { positionH, positionV },
});
expect(otherDocument.getSnapshot().drawings).toEqual({});
});
});
@@ -16,10 +16,12 @@
import type { DocumentDataModel, IAccessor, ICommand, IMutationInfo, ITextRangeParam, JSONXActions } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IInsertDrawingCommandParams } from './interfaces';
import type { IDocDrawing } from '../../services/doc-drawing.service';
import {
BooleanNumber,
BuildTextUtils,
CommandType,
getCustomBlockIdsInSelections,
getRichTextEditPath,
ICommandService,
IUniverInstanceService,
@@ -28,8 +30,13 @@ import {
TextXActionType,
UniverInstanceType,
} from '@univerjs/core';
import { DocContentInsertService, DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { getCustomBlockIdsInSelections } from '@univerjs/docs-ui';
import { DocSelectionManagerService, getContentInsertRange, normalizeTextRange, RichTextEditingMutation } from '@univerjs/docs';
export interface IInsertDocDrawingCommandParams {
unitId: string;
drawings: IDocDrawing[];
textRange?: ITextRangeParam;
}
/**
* The command to insert new drawings
@@ -40,49 +47,46 @@ export const InsertDocDrawingCommand: ICommand = {
type: CommandType.COMMAND,
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params?: IInsertDrawingCommandParams) => {
if (params == null) {
handler: (accessor: IAccessor, params?: IInsertDocDrawingCommandParams) => {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const activeTextRange = docSelectionManagerService.getActiveTextRange();
const documentDataModel = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
if (documentDataModel == null) {
const { unitId, drawings, textRange } = params;
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
if (!documentDataModel) {
return false;
}
const unitId = documentDataModel.getUnitId();
const explicitTextRange = params.textRange == null ? null : normalizeTextRange(params.textRange);
const contentInsertRange = explicitTextRange ?? getContentInsertRange(accessor, unitId);
const targetTextRange = contentInsertRange
? {
...activeTextRange,
startOffset: contentInsertRange.startOffset,
endOffset: contentInsertRange.endOffset,
collapsed: contentInsertRange.startOffset === contentInsertRange.endOffset,
segmentId: contentInsertRange.segmentId ?? activeTextRange?.segmentId ?? '',
}
: activeTextRange;
const targetTextRange = resolveDocDrawingInsertTextRange(accessor, unitId, textRange);
if (targetTextRange == null) {
if (!targetTextRange) {
return false;
}
const { drawings } = params;
const { collapsed, startOffset, segmentId = '' } = targetTextRange;
const body = documentDataModel.getSelfOrHeaderFooterModel(segmentId)?.getBody();
if (body == null) {
if (!body) {
return false;
}
const textX = new TextX();
const jsonX = JSONX.getInstance();
const rawActions: JSONXActions = [];
const drawingOrderLength = documentDataModel.getSnapshot().drawingsOrder?.length ?? 0;
const snapshot = documentDataModel.getSnapshot();
const isHeaderFooter = !!snapshot.headers?.[segmentId] || !!snapshot.footers?.[segmentId];
const targetDrawings = isHeaderFooter
? drawings.map((drawing) => ({
...drawing,
isMultiTransform: BooleanNumber.TRUE,
transforms: drawing.transforms ?? (drawing.transform ? [drawing.transform] : null),
}))
: drawings;
const drawingOrderLength = snapshot.drawingsOrder?.length ?? 0;
let removeDrawingLen = 0;
// Step 1: Insert placeholder `\b` in dataStream and add drawing to customBlocks.
@@ -132,13 +136,13 @@ export const InsertDocDrawingCommand: ICommand = {
textX.push({
t: TextXActionType.INSERT,
body: {
dataStream: '\b'.repeat(drawings.length),
customBlocks: drawings.map((drawing, i) => ({
dataStream: '\b'.repeat(targetDrawings.length),
customBlocks: targetDrawings.map((drawing, i) => ({
startIndex: i,
blockId: drawing.drawingId,
})),
},
len: drawings.length,
len: targetDrawings.length,
});
const path = getRichTextEditPath(documentDataModel, segmentId);
@@ -147,7 +151,7 @@ export const InsertDocDrawingCommand: ICommand = {
rawActions.push(placeHolderAction!);
// Step 2: add drawing to drawings and drawingsOrder fields.
for (const drawing of drawings) {
for (const drawing of targetDrawings) {
const { drawingId } = drawing;
const addDrawingAction = jsonX.insertOp(['drawings', drawingId], drawing);
const addDrawingOrderAction = jsonX.insertOp(['drawingsOrder', drawingOrderLength - removeDrawingLen], drawingId);
@@ -178,31 +182,23 @@ export const InsertDocDrawingCommand: ICommand = {
},
};
function getContentInsertRange(accessor: IAccessor, unitId: string): ITextRangeParam | null {
try {
const range = accessor.get(DocContentInsertService).consumeInsertRange(unitId);
if (range == null) {
return null;
}
return {
startOffset: range.startOffset,
endOffset: range.endOffset,
collapsed: range.startOffset === range.endOffset,
segmentId: range.segmentId,
};
} catch {
return null;
function resolveDocDrawingInsertTextRange(
accessor: IAccessor,
unitId: string,
textRange?: ITextRangeParam
): ITextRangeParam | null {
const activeTextRange = accessor.get(DocSelectionManagerService).getActiveTextRange();
const explicitTextRange = textRange ? normalizeTextRange(textRange) : null;
const contentInsertRange = explicitTextRange ?? getContentInsertRange(accessor, unitId);
if (!contentInsertRange) {
return activeTextRange ?? null;
}
}
function normalizeTextRange(textRange: ITextRangeParam): ITextRangeParam {
const endOffset = textRange.endOffset ?? textRange.startOffset;
return {
...textRange,
endOffset,
collapsed: textRange.collapsed ?? textRange.startOffset === endOffset,
segmentId: textRange.segmentId ?? '',
...activeTextRange,
startOffset: contentInsertRange.startOffset,
endOffset: contentInsertRange.endOffset,
collapsed: contentInsertRange.startOffset === contentInsertRange.endOffset,
segmentId: contentInsertRange.segmentId ?? activeTextRange?.segmentId ?? '',
};
}
@@ -14,11 +14,9 @@
* limitations under the License.
*/
import type { DocumentDataModel, IAccessor, ICommand, IDisposable, IMutationInfo, JSONXActions } from '@univerjs/core';
import type { DocumentDataModel, DrawingTypeEnum, IAccessor, ICommand, IDisposable, IMutationInfo, ITextRangeParam, JSONXActions } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocDrawing } from '@univerjs/docs-drawing';
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
import type { IDeleteDrawingCommandParams } from './interfaces';
import type { IDocDrawing } from '../../services/doc-drawing.service';
import {
CommandType,
getRichTextEditPath,
@@ -31,35 +29,47 @@ import {
TextXActionType,
UniverInstanceType,
} from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
import { IDocDrawingAdapterService } from '@univerjs/docs-drawing';
import { DocSelectionRenderService } from '@univerjs/docs-ui';
import { IRenderManagerService } from '@univerjs/engine-render';
import { DocSelectionManagerService, getContentInsertRange, normalizeTextRange, RichTextEditingMutation } from '@univerjs/docs';
import { IDocDrawingAdapterService } from '../../services/doc-drawing-adapter.service';
export interface IRemoveDocDrawingCommandParam {
unitId: string;
subUnitId: string;
drawingId: string;
drawingType: DrawingTypeEnum;
}
export interface IRemoveDocDrawingCommandParams {
unitId: string;
drawings: IRemoveDocDrawingCommandParam[];
textRange?: ITextRangeParam;
}
/**
* The command to remove new sheet image
*/
export const RemoveDocDrawingCommand: ICommand = {
id: 'doc.command.remove-doc-image',
type: CommandType.COMMAND,
// eslint-disable-next-line max-lines-per-function
handler: (accessor: IAccessor, params?: IDeleteDrawingCommandParams) => {
handler: (accessor: IAccessor, params?: IRemoveDocDrawingCommandParams) => {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const undoRedoService = accessor.get(IUndoRedoService);
const drawingAdapterService = accessor.get(IDocDrawingAdapterService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const renderManagerService = accessor.get(IRenderManagerService);
const documentDataModel = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
if (params == null || documentDataModel == null) {
const { unitId, drawings: removeDrawings, textRange } = params;
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
if (!documentDataModel || removeDrawings.length === 0) {
return false;
}
const docSelectionRenderService = renderManagerService.getRenderById(params.unitId)!.with(DocSelectionRenderService)!;
const { drawings: removeDrawings } = params;
const segmentId = docSelectionRenderService.getSegment() ?? '';
const activeTextRange = docSelectionManagerService.getActiveTextRange();
const explicitTextRange = !textRange ? null : normalizeTextRange(textRange);
const contentInsertRange = explicitTextRange ?? getContentInsertRange(accessor, unitId);
const segmentId = contentInsertRange?.segmentId ?? activeTextRange?.segmentId ?? '';
const textX = new TextX();
const jsonX = JSONX.getInstance();
@@ -69,8 +79,7 @@ export const RemoveDocDrawingCommand: ICommand = {
.filter((block) => !!block)
.sort((a, b) => a!.startIndex > b!.startIndex ? 1 : -1);
const unitId = removeDrawings[0]?.unitId;
if (unitId == null || removeCustomBlocks.length === 0) {
if (removeCustomBlocks.length === 0) {
return false;
}
@@ -125,70 +134,34 @@ export const RemoveDocDrawingCommand: ICommand = {
}
const memoryCursor = new MemoryCursor();
memoryCursor.reset();
const cursorIndex = removeCustomBlocks[0]!.startIndex;
const textRanges = [
{
startOffset: cursorIndex,
endOffset: cursorIndex,
},
] as ITextRangeWithStyle[];
const textRanges = [{ startOffset: cursorIndex, endOffset: cursorIndex }] as IRichTextEditingMutationParams['textRanges'];
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
id: RichTextEditingMutation.id,
params: {
unitId,
actions: [],
textRanges,
},
params: { unitId, actions: [], textRanges },
};
const rawActions: JSONXActions = [];
for (const block of removeCustomBlocks) {
const { startIndex } = block!;
if (startIndex > memoryCursor.cursor) {
textX.push({
t: TextXActionType.RETAIN,
len: startIndex - memoryCursor.cursor,
});
textX.push({ t: TextXActionType.RETAIN, len: startIndex - memoryCursor.cursor });
}
textX.push({
t: TextXActionType.DELETE,
len: 1,
});
textX.push({ t: TextXActionType.DELETE, len: 1 });
memoryCursor.moveCursorTo(startIndex + 1);
}
const path = getRichTextEditPath(documentDataModel, segmentId);
rawActions.push(jsonX.editOp(textX.serialize(), path)!);
rawActions.push(jsonX.editOp(textX.serialize(), getRichTextEditPath(documentDataModel, segmentId))!);
for (const block of removeCustomBlocks) {
const { blockId } = block!;
const drawingOrder = documentDataModel.getDrawingsOrder();
const drawingIndex = drawingOrder!.indexOf(blockId);
const removeDrawingAction = jsonX.removeOp(['drawings', blockId], drawings[blockId]);
const removeDrawingOrderAction = jsonX.removeOp(['drawingsOrder', drawingIndex], blockId);
rawActions.push(removeDrawingAction!);
rawActions.push(removeDrawingOrderAction!);
const drawingIndex = documentDataModel.getDrawingsOrder()!.indexOf(blockId);
rawActions.push(jsonX.removeOp(['drawings', blockId], drawings[blockId])!);
rawActions.push(jsonX.removeOp(['drawingsOrder', drawingIndex], blockId)!);
}
doMutation.params.actions = rawActions.reduce((acc, cur) => {
return JSONX.compose(acc, cur as JSONXActions);
}, null as JSONXActions);
const result = commandService.syncExecuteCommand<
IRichTextEditingMutationParams,
IRichTextEditingMutationParams
>(doMutation.id, doMutation.params);
doMutation.params.actions = rawActions.reduce((acc, cur) => JSONX.compose(acc, cur as JSONXActions), null as JSONXActions);
const result = commandService.syncExecuteCommand<IRichTextEditingMutationParams, IRichTextEditingMutationParams>(doMutation.id, doMutation.params);
if (!result && batchingDisposable != null) {
batchingDisposable.dispose();
@@ -208,12 +181,10 @@ function executeResourceMutationGroups(
const executedUndoGroups: IMutationInfo[][] = [];
for (const mutationGroup of mutationGroups) {
const result = executeMutations(mutationGroup.redoMutations, commandService);
if (!result) {
if (!executeMutations(mutationGroup.redoMutations, commandService)) {
executeMutationGroups([...executedUndoGroups].reverse(), commandService);
return false;
}
executedUndoGroups.push(mutationGroup.undoMutations);
}
@@ -221,9 +192,7 @@ function executeResourceMutationGroups(
}
function executeMutationGroups(mutationGroups: IMutationInfo[][], commandService: ICommandService): void {
mutationGroups.forEach((mutations) => {
executeMutations(mutations, commandService);
});
mutationGroups.forEach((mutations) => executeMutations(mutations, commandService));
}
function executeMutations(mutations: IMutationInfo[], commandService: ICommandService): boolean {
@@ -25,9 +25,9 @@ import {
Tools,
} from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
import { IDocDrawingService } from '@univerjs/docs-drawing';
import { IDocDrawingService } from '../../services/doc-drawing.service';
export interface ISetDrawingArrangeCommandParams extends IDrawingOrderMapParam {
export interface ISetDocDrawingArrangeCommandParams extends IDrawingOrderMapParam {
arrangeType: ArrangeTypeEnum;
}
@@ -39,7 +39,7 @@ export const SetDocDrawingArrangeCommand: ICommand = {
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: ISetDrawingArrangeCommandParams) => {
handler: (accessor: IAccessor, params?: ISetDocDrawingArrangeCommandParams) => {
const commandService = accessor.get(ICommandService);
const docDrawingService = accessor.get(IDocDrawingService);
@@ -0,0 +1,76 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DocumentDataModel, IAccessor, ICommand, IObjectPositionH, IObjectPositionV, ISize, JSONXActions } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import {
CommandType,
ICommandService,
IUniverInstanceService,
JSONX,
Tools,
UniverInstanceType,
} from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
export interface IDrawingDocTransform {
drawingId: string;
key: 'size' | 'angle' | 'positionH' | 'positionV';
value: ISize | number | IObjectPositionH | IObjectPositionV;
}
export interface IUpdateDrawingDocTransformCommandParams {
unitId: string;
subUnitId: string;
drawings: IDrawingDocTransform[];
}
export const UpdateDrawingDocTransformCommand: ICommand = {
id: 'doc.command.update-drawing-doc-transform',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IUpdateDrawingDocTransformCommandParams) => {
if (!params) {
return false;
}
const commandService = accessor.get(ICommandService);
const univerInstanceService = accessor.get(IUniverInstanceService);
const { unitId, drawings } = params;
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
if (!documentDataModel) {
return false;
}
const oldDrawings = documentDataModel.getSnapshot().drawings ?? {};
const jsonX = JSONX.getInstance();
const actions: JSONXActions = [];
for (const { drawingId, key, value } of drawings) {
const oldValue = oldDrawings[drawingId]?.docTransform?.[key];
if (oldValue == null || !Tools.diffValue(oldValue, value)) {
actions.push(jsonX.replaceOp(['drawings', drawingId, 'docTransform', key], oldValue, value)!);
}
}
return Boolean(commandService.syncExecuteCommand<IRichTextEditingMutationParams, IRichTextEditingMutationParams>(RichTextEditingMutation.id, {
unitId,
actions: actions.reduce((acc, action) => JSONX.compose(acc, action as JSONXActions), null as JSONXActions),
textRanges: null,
debounce: true,
}));
},
};
@@ -0,0 +1,123 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DocumentDataModel, IAccessor, ICommand, IMutationInfo, JSONXActions } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IDocDrawing } from '../../services/doc-drawing.service';
import {
BooleanNumber,
CommandType,
ICommandService,
IUniverInstanceService,
JSONX,
PositionedObjectLayoutType,
Tools,
UniverInstanceType,
} from '@univerjs/core';
import { RichTextEditingMutation } from '@univerjs/docs';
export enum TextWrappingStyle {
INLINE = 'inline',
BEHIND_TEXT = 'behindText',
IN_FRONT_OF_TEXT = 'inFrontOfText',
WRAP_SQUARE = 'wrapSquare',
WRAP_TOP_AND_BOTTOM = 'wrapTopAndBottom',
}
export const WRAPPING_STYLE_TO_LAYOUT_TYPE: Record<TextWrappingStyle, PositionedObjectLayoutType> = {
[TextWrappingStyle.INLINE]: PositionedObjectLayoutType.INLINE,
[TextWrappingStyle.WRAP_SQUARE]: PositionedObjectLayoutType.WRAP_SQUARE,
[TextWrappingStyle.WRAP_TOP_AND_BOTTOM]: PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM,
[TextWrappingStyle.IN_FRONT_OF_TEXT]: PositionedObjectLayoutType.WRAP_NONE,
[TextWrappingStyle.BEHIND_TEXT]: PositionedObjectLayoutType.WRAP_NONE,
};
export interface IUpdateDocDrawingWrappingStyleParams {
unitId: string;
subUnitId: string;
drawings: IDocDrawing[];
wrappingStyle: TextWrappingStyle;
}
/**
* Updates document drawing wrapping styles and optional persisted positions.
*/
export const UpdateDocDrawingWrappingStyleCommand: ICommand = {
id: 'doc.command.update-doc-drawing-wrapping-style',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IUpdateDocDrawingWrappingStyleParams) => {
if (!params) {
return false;
}
const { drawings, wrappingStyle, unitId } = params;
const commandService = accessor.get(ICommandService);
const documentDataModel = accessor.get(IUniverInstanceService).getUnit<DocumentDataModel>(
unitId,
UniverInstanceType.UNIVER_DOC
);
if (!documentDataModel) {
return false;
}
const oldDrawings = documentDataModel.getDrawings() ?? {};
const jsonX = JSONX.getInstance();
const rawActions: JSONXActions = [];
for (const drawing of drawings) {
const oldDrawing = oldDrawings[drawing.drawingId] as IDocDrawing | undefined;
if (!oldDrawing) {
continue;
}
const layoutType = WRAPPING_STYLE_TO_LAYOUT_TYPE[wrappingStyle];
if (oldDrawing.layoutType !== layoutType) {
rawActions.push(jsonX.replaceOp(['drawings', drawing.drawingId, 'layoutType'], oldDrawing.layoutType, layoutType)!);
}
if (wrappingStyle === TextWrappingStyle.BEHIND_TEXT || wrappingStyle === TextWrappingStyle.IN_FRONT_OF_TEXT) {
const behindDoc = wrappingStyle === TextWrappingStyle.BEHIND_TEXT ? BooleanNumber.TRUE : BooleanNumber.FALSE;
if (oldDrawing.behindDoc !== behindDoc) {
rawActions.push(jsonX.replaceOp(['drawings', drawing.drawingId, 'behindDoc'], oldDrawing.behindDoc, behindDoc)!);
}
}
if (wrappingStyle !== TextWrappingStyle.INLINE) {
for (const key of ['positionH', 'positionV'] as const) {
const value = drawing.docTransform?.[key];
const oldValue = oldDrawing.docTransform[key];
if (value && !Tools.diffValue(oldValue, value)) {
rawActions.push(jsonX.replaceOp(['drawings', drawing.drawingId, 'docTransform', key], oldValue, value)!);
}
}
}
}
const mutation: IMutationInfo<IRichTextEditingMutationParams> = {
id: RichTextEditingMutation.id,
params: {
unitId,
actions: rawActions.reduce(
(actions, action) => JSONX.compose(actions, action as JSONXActions),
null as JSONXActions
),
textRanges: null,
},
};
return Boolean(commandService.syncExecuteCommand(mutation.id, mutation.params));
},
};
@@ -53,7 +53,8 @@ describe('DocDrawingController', () => {
{ registerDrawingData } as any,
{ registerDrawingData: registerDrawingDataForManager } as any,
resourceManagerService as any,
univerInstanceService as any
univerInstanceService as any,
{ registerCommand: vi.fn(() => ({ dispose: vi.fn() })) } as any
);
expect(resourceManagerService.registerPluginResource).toHaveBeenCalledTimes(1);
@@ -17,8 +17,13 @@
import type { DocumentDataModel, IDocumentData } from '@univerjs/core';
import type { IDrawingMapItem, IDrawingMapItemData } from '@univerjs/drawing';
import type { IDocDrawing } from '../services/doc-drawing.service';
import { BooleanNumber, Disposable, IResourceManagerService, IUniverInstanceService, PositionedObjectLayoutType, UniverInstanceType } from '@univerjs/core';
import { BooleanNumber, Disposable, ICommandService, IResourceManagerService, IUniverInstanceService, PositionedObjectLayoutType, UniverInstanceType } from '@univerjs/core';
import { IDrawingManagerService } from '@univerjs/drawing';
import { InsertDocDrawingCommand } from '../commands/commands/insert-doc-drawing.command';
import { RemoveDocDrawingCommand } from '../commands/commands/remove-doc-drawing.command';
import { SetDocDrawingArrangeCommand } from '../commands/commands/set-drawing-arrange.command';
import { UpdateDrawingDocTransformCommand } from '../commands/commands/update-doc-drawing-transform.command';
import { UpdateDocDrawingWrappingStyleCommand } from '../commands/commands/update-doc-drawing-wrapping-style.command';
import { IDocDrawingService } from '../services/doc-drawing.service';
export const DOCS_DRAWING_PLUGIN = 'DOC_DRAWING_PLUGIN';
@@ -52,7 +57,8 @@ export class DocDrawingController extends Disposable {
@IDocDrawingService private readonly _docDrawingService: IDocDrawingService,
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
@IResourceManagerService private _resourceManagerService: IResourceManagerService,
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService
@IUniverInstanceService private _univerInstanceService: IUniverInstanceService,
@ICommandService private readonly _commandService: ICommandService
) {
super();
@@ -61,6 +67,7 @@ export class DocDrawingController extends Disposable {
private _init(): void {
this._initSnapshot();
this._initCommands();
}
private _initSnapshot() {
@@ -151,4 +158,14 @@ export class DocDrawingController extends Disposable {
this._drawingManagerService.registerDrawingData(unitId, renderSubDrawings);
return true;
}
private _initCommands() {
[
InsertDocDrawingCommand,
RemoveDocDrawingCommand,
UpdateDrawingDocTransformCommand,
UpdateDocDrawingWrappingStyleCommand,
SetDocDrawingArrangeCommand,
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
}
}
@@ -0,0 +1,61 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { DocumentDataModel, IDocumentData } from '@univerjs/core';
import { DocumentFlavor, ILogService, IUniverInstanceService, LogLevel, Univer, UniverInstanceType } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
import { UniverDocsPlugin } from '@univerjs/docs';
import { UniverDrawingPlugin } from '@univerjs/drawing';
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
import { UniverDocsDrawingPlugin } from '../../plugin';
import '../index';
const DEFAULT_DOC_DATA: IDocumentData = {
id: 'test-doc',
documentStyle: { documentFlavor: DocumentFlavor.TRADITIONAL },
body: {
dataStream: 'Hello world\r\n',
paragraphs: [{ startIndex: 11, paragraphId: 'paragraph-1' }],
sectionBreaks: [],
customBlocks: [],
},
drawings: {},
drawingsOrder: [],
};
export function createFacadeTestBed() {
const univer = new Univer();
const injector = univer.__getInjector();
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
univer.registerPlugin(UniverDocsPlugin);
univer.registerPlugin(UniverDrawingPlugin);
univer.registerPlugin(UniverDocsDrawingPlugin);
const documentDataModel = univer.createUnit<IDocumentData, DocumentDataModel>(
UniverInstanceType.UNIVER_DOC,
DEFAULT_DOC_DATA
);
injector.get(IUniverInstanceService).focusUnit(documentDataModel.getUnitId());
injector.get(ILogService).setLogLevel(LogLevel.SILENT);
return {
univer,
injector,
documentDataModel,
document: FUniver.newAPI(injector).getActiveDocument()!,
};
}
@@ -0,0 +1,277 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IObjectPositionH, IObjectPositionV } from '@univerjs/core';
import {
ArrangeTypeEnum,
BooleanNumber,
ICommandService,
ImageSourceType,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
} from '@univerjs/core';
import { DocSelectionManagerService } from '@univerjs/docs';
import { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand } from '@univerjs/docs-drawing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { RemoveDocDrawingCommand } from '../../commands/commands/remove-doc-drawing.command';
import { SetDocDrawingArrangeCommand } from '../../commands/commands/set-drawing-arrange.command';
import { UpdateDrawingDocTransformCommand } from '../../commands/commands/update-doc-drawing-transform.command';
import { createFacadeTestBed } from './create-test-bed';
class MockImage {
width = 800;
height = 400;
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
get src(): string {
return '';
}
set src(_value: string) {
queueMicrotask(() => this.onload?.());
}
}
describe('FDocument image facade', () => {
let testBed: ReturnType<typeof createFacadeTestBed>;
beforeEach(() => {
vi.stubGlobal('Image', MockImage);
testBed = createFacadeTestBed();
});
afterEach(() => {
testBed.univer.dispose();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('inserts an image with optional transform and text range options', async () => {
const positionH: IObjectPositionH = {
relativeFrom: ObjectRelativeFromH.MARGIN,
posOffset: 12,
};
const positionV: IObjectPositionV = {
relativeFrom: ObjectRelativeFromV.PARAGRAPH,
posOffset: 34,
};
const image = await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
width: 200,
angle: 15,
positionH,
positionV,
textRange: {
startOffset: 5,
endOffset: 5,
collapsed: true,
segmentId: '',
},
});
expect(image).not.toBeNull();
expect(image?.getSource()).toBe('data:image/png;base64,image');
expect(image?.getSourceType()).toBe(ImageSourceType.BASE64);
expect(image?.getSize()).toEqual({ width: 200, height: 100 });
expect(image?.getAngle()).toBe(15);
expect(image?.getPositionH()).toEqual(positionH);
expect(image?.getPositionV()).toEqual(positionV);
expect(image?.getImageData()).toMatchObject({
drawingId: image?.getId(),
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
});
expect(testBed.document.save().body?.dataStream).toBe('Hello\b world\r\n');
expect(testBed.document.getImage(image!.getId())).not.toBeNull();
expect(testBed.document.getImages().map((item) => item.getId())).toEqual([image!.getId()]);
});
it('resolves the insertion range only once', async () => {
const selectionManager = testBed.injector.get(DocSelectionManagerService);
const getActiveTextRange = vi.spyOn(selectionManager, 'getActiveTextRange');
await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
textRange: {
startOffset: 0,
endOffset: 0,
collapsed: true,
segmentId: '',
},
});
expect(getActiveTextRange).toHaveBeenCalledTimes(1);
});
it('uses a capped intrinsic size when width and height are omitted', async () => {
const image = await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
textRange: {
startOffset: 0,
endOffset: 0,
collapsed: true,
segmentId: '',
},
});
expect(image?.getSize()).toEqual({ width: 500, height: 250 });
});
it('inserts an image with a non-inline wrapping style', async () => {
const image = await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
wrappingStyle: TextWrappingStyle.BEHIND_TEXT,
textRange: {
startOffset: 2,
endOffset: 2,
collapsed: true,
segmentId: '',
},
});
expect(image?.getImageData()).toMatchObject({
layoutType: PositionedObjectLayoutType.WRAP_NONE,
behindDoc: BooleanNumber.TRUE,
});
});
it('inserts a header image with multi-page transforms', async () => {
const segmentId = testBed.document.ensurePageHeader();
const image = await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
width: 160,
height: 90,
textRange: {
startOffset: 0,
endOffset: 0,
collapsed: true,
segmentId,
},
});
const imageData = image?.getImageData();
expect(testBed.document.save().headers?.[segmentId].body.dataStream).toBe('\b\r\n');
expect(imageData?.isMultiTransform).toBe(BooleanNumber.TRUE);
expect(imageData?.transforms).toEqual(imageData?.transform ? [imageData.transform] : null);
});
it('routes image updates, arranging, and removal through docs-drawing commands', async () => {
const image = await testBed.document.insertImage({
source: 'data:image/png;base64,image',
imageSourceType: ImageSourceType.BASE64,
width: 160,
height: 90,
textRange: {
startOffset: 3,
endOffset: 3,
collapsed: true,
segmentId: '',
},
});
const commandService = testBed.injector.get(ICommandService);
const execute = vi.spyOn(commandService, 'syncExecuteCommand').mockReturnValue(true);
const positionH: IObjectPositionH = { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 20 };
const positionV: IObjectPositionV = { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 30 };
const drawingId = image!.getId();
expect(image!.setSize(320, 180)).toBe(true);
expect(image!.setRotate(25)).toBe(true);
expect(image!.setPositionH(positionH)).toBe(true);
expect(image!.setPositionV(positionV)).toBe(true);
expect(image!.setWrappingStyle(TextWrappingStyle.WRAP_SQUARE)).toBe(true);
expect(image!.setForward()).toBe(true);
expect(image!.setBackward()).toBe(true);
expect(image!.setBack()).toBe(true);
expect(image!.setFront()).toBe(true);
expect(image!.remove()).toBe(true);
expect(execute.mock.calls).toEqual([
[UpdateDrawingDocTransformCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [{ drawingId, key: 'size', value: { width: 320, height: 180 } }],
}],
[UpdateDrawingDocTransformCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [{ drawingId, key: 'angle', value: 25 }],
}],
[UpdateDrawingDocTransformCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [{ drawingId, key: 'positionH', value: positionH }],
}],
[UpdateDrawingDocTransformCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [{ drawingId, key: 'positionV', value: positionV }],
}],
[UpdateDocDrawingWrappingStyleCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawings: [image!.getImageData()],
wrappingStyle: TextWrappingStyle.WRAP_SQUARE,
}],
[SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: [drawingId],
arrangeType: ArrangeTypeEnum.forward,
}],
[SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: [drawingId],
arrangeType: ArrangeTypeEnum.backward,
}],
[SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: [drawingId],
arrangeType: ArrangeTypeEnum.back,
}],
[SetDocDrawingArrangeCommand.id, {
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingIds: [drawingId],
arrangeType: ArrangeTypeEnum.front,
}],
[RemoveDocDrawingCommand.id, {
unitId: 'test-doc',
drawings: [{
unitId: 'test-doc',
subUnitId: 'test-doc',
drawingId,
drawingType: 0,
}],
textRange: {
startOffset: 3,
endOffset: 3,
collapsed: true,
segmentId: '',
},
}],
]);
});
});
@@ -0,0 +1,468 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ImageSourceType, Injector, IObjectPositionH, IObjectPositionV, ISize, ITextRangeParam } from '@univerjs/core';
import type { IDocImage, IUpdateDocDrawingWrappingStyleParams, TextWrappingStyle } from '@univerjs/docs-drawing';
import type { FDocument } from '@univerjs/docs/facade';
import {
ArrangeTypeEnum,
DrawingTypeEnum,
ICommandService,
} from '@univerjs/core';
import {
RemoveDocDrawingCommand,
SetDocDrawingArrangeCommand,
UpdateDocDrawingWrappingStyleCommand,
UpdateDrawingDocTransformCommand,
} from '@univerjs/docs-drawing';
/**
* Facade API for an image in a document.
* @hideconstructor
*/
export class FDocumentImage {
constructor(
private readonly _document: FDocument,
private readonly _imageId: string,
private readonly _injector: Injector
) {}
/**
* Gets the id of the document containing the image.
* @returns {string} The document unit id.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getUnitId());
* }
* ```
*/
getUnitId(): string {
return this._document.getId();
}
/**
* Gets the drawing id of the image.
* @returns {string} The drawing id.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getId());
* }
* ```
*/
getId(): string {
return this._imageId;
}
/**
* Gets the image source supplied at insertion time.
* @returns {string | undefined} The image source, or `undefined` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getSource());
* }
* ```
*/
getSource(): string | undefined {
return this.getImageData()?.source;
}
/**
* Gets the image source type supplied at insertion time.
* @returns {ImageSourceType | undefined} The image source type, or `undefined` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getSourceType());
* }
* ```
*/
getSourceType(): ImageSourceType | undefined {
return this.getImageData()?.imageSourceType;
}
/**
* Gets the current image size.
* @returns {ISize | null} The width and height in pixels, or `null` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getSize());
* }
* ```
*/
getSize(): ISize | null {
return this.getImageData()?.docTransform.size ?? null;
}
/**
* Gets the current clockwise rotation angle.
* @returns {number | undefined} The rotation angle in degrees, or `undefined` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getAngle());
* }
* ```
*/
getAngle(): number | undefined {
return this.getImageData()?.docTransform.angle;
}
/**
* Gets the current horizontal position.
* @returns {IObjectPositionH | null} The horizontal position, or `null` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getPositionH());
* }
* ```
*/
getPositionH(): IObjectPositionH | null {
return this.getImageData()?.docTransform.positionH ?? null;
}
/**
* Gets the current vertical position.
* @returns {IObjectPositionV | null} The vertical position, or `null` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getPositionV());
* }
* ```
*/
getPositionV(): IObjectPositionV | null {
return this.getImageData()?.docTransform.positionV ?? null;
}
/**
* Gets the current raw document image data.
* @returns {IDocImage | null} The image data, or `null` when the image no longer exists.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* console.log(image.getImageData());
* }
* ```
*/
getImageData(): IDocImage | null {
const drawing = this._document.getDocumentDataModel().getDrawings()?.[this._imageId];
if (!drawing || drawing.drawingType !== DrawingTypeEnum.DRAWING_IMAGE) {
return null;
}
return drawing as IDocImage;
}
/**
* Sets the image size.
* @param {number} width The width in pixels.
* @param {number} height The height in pixels.
* @returns {boolean} `true` when the update command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setSize(400, 300);
* console.log(success);
* }
* ```
*/
setSize(width: number, height: number): boolean {
return this._updateTransform('size', { width, height });
}
/**
* Sets the clockwise rotation angle.
* @param {number} angle The rotation angle in degrees.
* @returns {boolean} `true` when the update command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setRotate(45);
* console.log(success);
* }
* ```
*/
setRotate(angle: number): boolean {
return this._updateTransform('angle', angle);
}
/**
* Sets the horizontal position of the image.
* Inline images are positioned by their document placeholder, so this has a visible effect only when the image
* wrapping style is not `TextWrappingStyle.INLINE`.
* @param {IObjectPositionH} positionH The horizontal position relative to the document.
* @returns {boolean} `true` when the update command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setPositionH({
* relativeFrom: univerAPI.Enum.DocsImageRelativeFromH.MARGIN,
* posOffset: 100
* });
* console.log(success);
* }
* ```
*/
setPositionH(positionH: IObjectPositionH): boolean {
return this._updateTransform('positionH', positionH);
}
/**
* Sets the vertical position of the image.
* Inline images are positioned by their document placeholder, so this has a visible effect only when the image
* wrapping style is not `TextWrappingStyle.INLINE`.
* @param {IObjectPositionV} positionV The vertical position relative to the document.
* @returns {boolean} `true` when the update command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setPositionV({
* relativeFrom: univerAPI.Enum.DocsImageRelativeFromV.MARGIN,
* posOffset: 100
* });
* console.log(success);
* }
* ```
*/
setPositionV(positionV: IObjectPositionV): boolean {
return this._updateTransform('positionV', positionV);
}
/**
* Sets the image wrapping style.
* When switching from inline to a floating style in a UI environment, the current visual position is preserved.
* @param {TextWrappingStyle} wrappingStyle The wrapping style to apply.
* @returns {boolean} `true` when the update command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setWrappingStyle(univerAPI.Enum.DocsImageWrappingStyle.WRAP_SQUARE);
* console.log(success);
* }
* ```
*/
setWrappingStyle(wrappingStyle: TextWrappingStyle): boolean {
const image = this.getImageData();
if (!image) {
return false;
}
return this._injector.get(ICommandService).syncExecuteCommand<IUpdateDocDrawingWrappingStyleParams>(
UpdateDocDrawingWrappingStyleCommand.id,
{
unitId: this.getUnitId(),
subUnitId: this.getUnitId(),
drawings: [image],
wrappingStyle,
}
);
}
/**
* Moves the image forward by one level in the drawing order.
* @returns {boolean} `true` when the arrange command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setForward();
* console.log(success);
* }
* ```
*/
setForward(): boolean {
return this._arrange(ArrangeTypeEnum.forward);
}
/**
* Moves the image backward by one level in the drawing order.
* @returns {boolean} `true` when the arrange command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setBackward();
* console.log(success);
* }
* ```
*/
setBackward(): boolean {
return this._arrange(ArrangeTypeEnum.backward);
}
/**
* Moves the image to the back of the drawing order.
* @returns {boolean} `true` when the arrange command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setBack();
* console.log(success);
* }
* ```
*/
setBack(): boolean {
return this._arrange(ArrangeTypeEnum.back);
}
/**
* Moves the image to the front of the drawing order.
* @returns {boolean} `true` when the arrange command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setFront();
* console.log(success);
* }
* ```
*/
setFront(): boolean {
return this._arrange(ArrangeTypeEnum.front);
}
/**
* Removes the image and its document placeholder.
* @returns {boolean} `true` when the remove command succeeds; otherwise, `false`.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.remove();
* console.log(success);
* }
* ```
*/
remove(): boolean {
const image = this.getImageData();
const textRange = this._getTextRange();
if (!image || !textRange) {
return false;
}
return this._injector.get(ICommandService).syncExecuteCommand(RemoveDocDrawingCommand.id, {
unitId: this.getUnitId(),
drawings: [{
unitId: this.getUnitId(),
subUnitId: this.getUnitId(),
drawingId: this._imageId,
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
}],
textRange,
});
}
private _updateTransform(
key: 'size' | 'angle' | 'positionH' | 'positionV',
value: ISize | number | IObjectPositionH | IObjectPositionV
): boolean {
return this._injector.get(ICommandService).syncExecuteCommand(UpdateDrawingDocTransformCommand.id, {
unitId: this.getUnitId(),
subUnitId: this.getUnitId(),
drawings: [{ drawingId: this._imageId, key, value }],
});
}
private _arrange(arrangeType: ArrangeTypeEnum): boolean {
return this._injector.get(ICommandService).syncExecuteCommand(SetDocDrawingArrangeCommand.id, {
unitId: this.getUnitId(),
subUnitId: this.getUnitId(),
drawingIds: [this._imageId],
arrangeType,
});
}
private _getTextRange(): ITextRangeParam | null {
const snapshot = this._document.getDocumentDataModel().getSnapshot();
const { body, headers = {}, footers = {} } = snapshot;
const segments = [
{ segmentId: '', body },
...Object.entries(headers).map(([segmentId, header]) => ({ segmentId, body: header.body })),
...Object.entries(footers).map(([segmentId, footer]) => ({ segmentId, body: footer.body })),
];
for (const { segmentId, body } of segments) {
const customBlock = body?.customBlocks?.find((block) => block.blockId === this._imageId);
if (customBlock) {
return {
startOffset: customBlock.startIndex,
endOffset: customBlock.startIndex,
collapsed: true,
segmentId,
};
}
}
return null;
}
}
@@ -0,0 +1,247 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IObjectPositionH, IObjectPositionV, ISize, ITextRangeParam } from '@univerjs/core';
import type { IDocImage } from '@univerjs/docs-drawing';
import {
BooleanNumber,
DrawingTypeEnum,
generateRandomId,
ICommandService,
IImageIoService,
ImageSourceType,
IURLImageService,
WrapTextType,
} from '@univerjs/core';
import { buildDocTransform, docDrawingPositionToTransform } from '@univerjs/docs';
import {
InsertDocDrawingCommand,
TextWrappingStyle,
WRAPPING_STYLE_TO_LAYOUT_TYPE,
} from '@univerjs/docs-drawing';
import { FDocument } from '@univerjs/docs/facade';
import { DRAWING_IMAGE_HEIGHT_LIMIT, DRAWING_IMAGE_WIDTH_LIMIT, getImageSize } from '@univerjs/drawing';
import { FDocumentImage } from './f-document-image';
/** Options for inserting an image into a document. */
export interface IFDocumentInsertImageOptions {
/** The image source. It cannot be changed after insertion. */
source: string;
/** The image source type. It cannot be changed after insertion. */
imageSourceType: ImageSourceType;
/** The width in pixels. When only width is provided, height is calculated from the intrinsic aspect ratio. */
width?: number;
/** The height in pixels. When only height is provided, width is calculated from the intrinsic aspect ratio. */
height?: number;
/** The clockwise rotation angle in degrees. Defaults to `0`. */
angle?: number;
/**
* The horizontal position relative to the document. Defaults to the page with an offset of `0`.
* It has a visible positioning effect only when `wrappingStyle` is not `INLINE`.
*/
positionH?: IObjectPositionH;
/**
* The vertical position relative to the document. Defaults to the paragraph with an offset of `0`.
* It has a visible positioning effect only when `wrappingStyle` is not `INLINE`.
*/
positionV?: IObjectPositionV;
/** The image wrapping style. Defaults to `TextWrappingStyle.INLINE`. */
wrappingStyle?: TextWrappingStyle;
/** The document range at which to insert the image. The current selection is used when omitted. */
textRange?: ITextRangeParam;
}
/** Image facade methods mixed into `FDocument`. */
export interface IFDocumentImageMixin {
/**
* Inserts an image into the document.
*
* When width and height are both omitted, the intrinsic size is proportionally limited to 500 by 500 pixels.
*
* @param {IFDocumentInsertImageOptions} options The image source, optional transform, and insertion range.
* @returns {Promise<FDocumentImage | null>} The inserted image facade, or `null` when the insertion command fails.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = await fDocument.insertImage({
* source: 'https://avatars.githubusercontent.com/u/61444807?s=48&v=4',
* imageSourceType: univerAPI.Enum.ImageSourceType.URL,
* width: 320,
* textRange: {
* startOffset: 30,
* },
* });
* console.log(image);
* ```
*
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = await fDocument.insertImage({
* source: 'https://avatars.githubusercontent.com/u/61444807?s=48&v=4',
* imageSourceType: univerAPI.Enum.ImageSourceType.URL,
* width: 320,
* wrappingStyle: univerAPI.Enum.DocsImageWrappingStyle.WRAP_SQUARE,
* textRange: {
* startOffset: 30,
* },
* });
* console.log(image);
* ```
*/
insertImage(options: IFDocumentInsertImageOptions): Promise<FDocumentImage | null>;
/**
* Gets an image by its drawing id.
* @param {string} imageId The drawing id of the image.
* @returns {FDocumentImage | null} The image facade, or `null` when the image does not exist.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImage('image-1');
* console.log(image);
* ```
*/
getImage(imageId: string): FDocumentImage | null;
/**
* Gets all images in the document in drawing order.
* @returns {FDocumentImage[]} The image facades in drawing order.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const images = fDocument.getImages();
* console.log(images);
* ```
*/
getImages(): FDocumentImage[];
}
export class FDocumentImageMixin extends FDocument implements IFDocumentImageMixin {
override async insertImage(options: IFDocumentInsertImageOptions): Promise<FDocumentImage | null> {
const unitId = this.getId();
const imageId = generateRandomId(6);
const intrinsicSize = await this._getIntrinsicSize(options.source, options.imageSourceType);
const size = resolveImageSize(intrinsicSize, options);
const defaultTransform = buildDocTransform(size.width, size.height);
const wrappingStyle = options.wrappingStyle ?? TextWrappingStyle.INLINE;
const docTransform = {
...defaultTransform,
angle: options.angle ?? defaultTransform.angle,
positionH: options.positionH ?? defaultTransform.positionH,
positionV: options.positionV ?? defaultTransform.positionV,
};
const transform = docDrawingPositionToTransform(docTransform);
const drawing: IDocImage = {
unitId,
subUnitId: unitId,
drawingId: imageId,
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
imageSourceType: options.imageSourceType,
source: options.source,
transform,
docTransform,
behindDoc: wrappingStyle === TextWrappingStyle.BEHIND_TEXT ? BooleanNumber.TRUE : BooleanNumber.FALSE,
title: '',
description: '',
layoutType: WRAPPING_STYLE_TO_LAYOUT_TYPE[wrappingStyle],
wrapText: WrapTextType.BOTH_SIDES,
distB: 0,
distL: 0,
distR: 0,
distT: 0,
};
const inserted = this._injector.get(ICommandService).syncExecuteCommand(InsertDocDrawingCommand.id, {
unitId,
drawings: [drawing],
textRange: options.textRange,
});
if (!inserted) {
return null;
}
return this._injector.createInstance(FDocumentImage, this, imageId, this._injector);
}
override getImage(imageId: string): FDocumentImage | null {
const drawing = this.getDocumentDataModel().getDrawings()?.[imageId];
if (!drawing || drawing.drawingType !== DrawingTypeEnum.DRAWING_IMAGE) {
return null;
}
return this._injector.createInstance(FDocumentImage, this, imageId, this._injector);
}
override getImages(): FDocumentImage[] {
const documentDataModel = this.getDocumentDataModel();
const drawings = documentDataModel.getDrawings() ?? {};
const drawingIds = documentDataModel.getDrawingsOrder() ?? Object.keys(drawings);
return drawingIds
.filter((drawingId) => drawings[drawingId]?.drawingType === DrawingTypeEnum.DRAWING_IMAGE)
.map((drawingId) => this._injector.createInstance(FDocumentImage, this, drawingId, this._injector));
}
private async _getIntrinsicSize(source: string, imageSourceType: ImageSourceType): Promise<Required<ISize>> {
const imageIoService = this._injector.has(IImageIoService) ? this._injector.get(IImageIoService) : null;
let resolvedSource = source;
if (imageSourceType === ImageSourceType.UUID && imageIoService) {
resolvedSource = await imageIoService.getImage(source);
} else if (imageSourceType === ImageSourceType.URL && this._injector.has(IURLImageService)) {
try {
resolvedSource = await this._injector.get(IURLImageService).getImage(source);
} catch {
resolvedSource = source;
}
}
const { width, height, image } = await getImageSize(resolvedSource);
imageIoService?.addImageSourceCache(source, imageSourceType, image);
return { width, height };
}
}
function resolveImageSize(
intrinsicSize: Required<ISize>,
options: Pick<IFDocumentInsertImageOptions, 'width' | 'height'>
): Required<ISize> {
const { width: intrinsicWidth, height: intrinsicHeight } = intrinsicSize;
if (options.width != null && options.height != null) {
return { width: options.width, height: options.height };
}
if (options.width != null) {
return { width: options.width, height: intrinsicHeight * options.width / intrinsicWidth };
}
if (options.height != null) {
return { width: intrinsicWidth * options.height / intrinsicHeight, height: options.height };
}
const scale = Math.min(
1,
DRAWING_IMAGE_WIDTH_LIMIT / intrinsicWidth,
DRAWING_IMAGE_HEIGHT_LIMIT / intrinsicHeight
);
return { width: intrinsicWidth * scale, height: intrinsicHeight * scale };
}
FDocument.extend(FDocumentImageMixin);
declare module '@univerjs/docs/facade' {
// eslint-disable-next-line ts/naming-convention
interface FDocument extends IFDocumentImageMixin {}
}
@@ -0,0 +1,99 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { ObjectRelativeFromH, ObjectRelativeFromV } from '@univerjs/core';
import { FEnum } from '@univerjs/core/facade';
import { TextWrappingStyle } from '@univerjs/docs-drawing';
/**
* @ignore
*/
export interface IFDocumentImageEnumMixin {
/**
* Represents the wrapping styles used by docs image operations.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = await fDocument.insertImage({
* source: 'https://avatars.githubusercontent.com/u/61444807?s=48&v=4',
* imageSourceType: univerAPI.Enum.ImageSourceType.URL,
* width: 320,
* wrappingStyle: univerAPI.Enum.DocsImageWrappingStyle.WRAP_SQUARE,
* textRange: {
* startOffset: 30,
* },
* });
* console.log(image);
* ```
*/
DocsImageWrappingStyle: typeof TextWrappingStyle;
/**
* Represents the horizontal position types used by docs image operations.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setPositionH({
* relativeFrom: univerAPI.Enum.DocsImageRelativeFromH.MARGIN,
* posOffset: 100
* });
* console.log(success);
* }
* ```
*/
DocsImageRelativeFromH: typeof ObjectRelativeFromH;
/**
* Represents the vertical position types used by docs image operations.
* @example
* ```ts
* const fDocument = univerAPI.getActiveDocument();
* const image = fDocument.getImages()[0];
*
* if (image) {
* const success = image.setPositionV({
* relativeFrom: univerAPI.Enum.DocsImageRelativeFromV.MARGIN,
* posOffset: 100
* });
* console.log(success);
* }
* ```
*/
DocsImageRelativeFromV: typeof ObjectRelativeFromV;
}
export class FDocumentImageEnumMixin extends FEnum implements IFDocumentImageEnumMixin {
override get DocsImageWrappingStyle(): typeof TextWrappingStyle {
return TextWrappingStyle;
}
override get DocsImageRelativeFromH(): typeof ObjectRelativeFromH {
return ObjectRelativeFromH;
}
override get DocsImageRelativeFromV(): typeof ObjectRelativeFromV {
return ObjectRelativeFromV;
}
}
FEnum.extend(FDocumentImageEnumMixin);
declare module '@univerjs/core/facade' {
// eslint-disable-next-line ts/naming-convention
interface FEnum extends IFDocumentImageEnumMixin {}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 './f-document';
import './f-enum';
export type { IFDocumentInsertImageOptions } from './f-document';
export { FDocumentImage } from './f-document-image';
+10
View File
@@ -14,6 +14,16 @@
* limitations under the License.
*/
export { InsertDocDrawingCommand } from './commands/commands/insert-doc-drawing.command';
export type { IInsertDocDrawingCommandParams } from './commands/commands/insert-doc-drawing.command';
export { RemoveDocDrawingCommand } from './commands/commands/remove-doc-drawing.command';
export type { IRemoveDocDrawingCommandParam, IRemoveDocDrawingCommandParams } from './commands/commands/remove-doc-drawing.command';
export { SetDocDrawingArrangeCommand } from './commands/commands/set-drawing-arrange.command';
export type { ISetDocDrawingArrangeCommandParams } from './commands/commands/set-drawing-arrange.command';
export { UpdateDrawingDocTransformCommand } from './commands/commands/update-doc-drawing-transform.command';
export type { IDrawingDocTransform, IUpdateDrawingDocTransformCommandParams } from './commands/commands/update-doc-drawing-transform.command';
export { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand, WRAPPING_STYLE_TO_LAYOUT_TYPE } from './commands/commands/update-doc-drawing-wrapping-style.command';
export type { IUpdateDocDrawingWrappingStyleParams } from './commands/commands/update-doc-drawing-wrapping-style.command';
export type { IUniverDocsDrawingConfig } from './config/config';
export { DOCS_DRAWING_PLUGIN, getDocDrawingRenderOrder } from './controllers/doc-drawing.controller';
export type { IDocDrawingModel } from './controllers/doc-drawing.controller';
@@ -20,7 +20,6 @@ import type {
ICommand,
ICustomTable,
IDisposable,
IDocumentBody,
IDocumentData,
IDrawingParam,
IMutationInfo,
@@ -30,10 +29,12 @@ import type {
} from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { DocumentViewModel, IRectRangeWithStyle, ITextRangeWithStyle } from '@univerjs/engine-render';
import type { IDocClipboardPasteCustomBlockMapping } from '../../services/clipboard/doc-paste-mutation-adapter.service';
import {
BuildTextUtils,
CommandType,
generateRandomId,
getCustomBlockIdsInSelections,
getRichTextEditPath,
ICommandService,
IUndoRedoService,
@@ -48,34 +49,10 @@ import {
} from '@univerjs/core';
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
import { getCustomDecorationAtPosition, getCustomRangeAtPosition } from '../../basics/paragraph';
import type { IDocClipboardPasteCustomBlockMapping } from '../../services/clipboard/doc-paste-mutation-adapter.service';
import { IDocClipboardPasteAdapterService } from '../../services/clipboard/doc-paste-mutation-adapter.service';
import { getCommandSkeleton } from '../util';
import { getDeleteRowContentActionParams, getDeleteRowsActionsParams, getDeleteTableActionParams } from './table/table';
export function getCustomBlockIdsInSelections(body: IDocumentBody, selections: ITextRange[]): string[] {
const customBlockIds: string[] = [];
const { customBlocks = [] } = body;
for (const selection of selections) {
const { startOffset, endOffset } = selection;
if (startOffset == null || endOffset == null) {
continue;
}
for (const customBlock of customBlocks) {
const { startIndex } = customBlock;
if (startIndex >= startOffset && startIndex < endOffset) {
customBlockIds.push(customBlock.blockId);
}
}
}
return customBlockIds;
}
function hasRangeInTable(ranges: ITextRangeWithStyle[]): boolean {
return ranges.some((range) => {
const { startNodePosition } = range;
+1 -1
View File
@@ -33,7 +33,7 @@ export { DocCopyCommand, DocCutCommand, DocPasteCommand } from './commands/comma
export { CutContentCommand, InnerPasteCommand } from './commands/commands/clipboard.inner.command';
export type { IInnerPasteCommandParams } from './commands/commands/clipboard.inner.command';
export type { IInnerCutCommandParams } from './commands/commands/clipboard.inner.command';
export { getCustomBlockIdsInSelections, getCutActionsFromDocRanges } from './commands/commands/clipboard.inner.command';
export { getCutActionsFromDocRanges } from './commands/commands/clipboard.inner.command';
export { buildMoveDocBlockActions, MoveDocBlockCommand } from './commands/commands/doc-block-move.command';
export type { IMoveDocBlockCommandParams } from './commands/commands/doc-block-move.command';
export {
+1 -1
View File
@@ -89,4 +89,4 @@ export { replaceSelectionFactory } from './utils/replace-selection-factory';
export { createSectionColumnProperties } from './utils/section-columns';
export { getTopLevelSectionBreaks } from './utils/sections';
export { buildDocTransform, docDrawingPositionToTransform, transformToDocDrawingPosition } from './utils/transform-position';
export { consumeContentInsertRange, isHeaderFooterSelection } from './utils/util';
export { consumeContentInsertRange, getContentInsertRange, isHeaderFooterSelection, normalizeTextRange } from './utils/util';
+34 -2
View File
@@ -14,11 +14,13 @@
* limitations under the License.
*/
import type { IAccessor } from '@univerjs/core';
import type { DocumentDataModel, IAccessor, ITextRangeParam } from '@univerjs/core';
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
import type { IDocContentInsertRange } from '../services/doc-content-insert.service';
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DocContentInsertService } from '../services/doc-content-insert.service';
export function consumeContentInsertRange(accessor: IAccessor, unitId: string) {
export function consumeContentInsertRange(accessor: IAccessor, unitId: string): IDocContentInsertRange | null {
try {
return accessor.get(DocContentInsertService).consumeInsertRange(unitId);
} catch {
@@ -26,6 +28,36 @@ export function consumeContentInsertRange(accessor: IAccessor, unitId: string) {
}
}
export function getContentInsertRange(accessor: IAccessor, unitId?: string): (IDocContentInsertRange & {
collapsed: boolean;
}) | null {
const _unitId = unitId ?? accessor.get(IUniverInstanceService).getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC)?.getUnitId();
if (!_unitId) {
return null;
}
const insertRange = consumeContentInsertRange(accessor, _unitId);
if (!insertRange) {
return null;
}
return {
...insertRange,
collapsed: insertRange?.startOffset === insertRange?.endOffset,
};
}
export function isHeaderFooterSelection(range?: ITextRangeWithStyle): boolean {
return Boolean(range?.segmentId);
}
export function normalizeTextRange(textRange: ITextRangeParam): ITextRangeParam {
const endOffset = textRange.endOffset ?? textRange.startOffset;
return {
...textRange,
endOffset,
collapsed: textRange.collapsed ?? textRange.startOffset === endOffset,
segmentId: textRange.segmentId ?? '',
};
}
@@ -15,7 +15,7 @@
*/
import type { IAccessor, ICommand } from '@univerjs/core';
import type { IDeleteDrawingCommandParams, ISheetDrawing } from '@univerjs/sheets-drawing';
import type { IRemoveSheetDrawingCommandParams, ISheetDrawing } from '@univerjs/sheets-drawing';
import { CommandType, ICommandService } from '@univerjs/core';
import { ISheetDrawingService, RemoveSheetDrawingCommand } from '@univerjs/sheets-drawing';
@@ -44,7 +44,7 @@ export const DeleteDrawingsCommand: ICommand = {
drawingType,
};
});
return commandService.executeCommand<IDeleteDrawingCommandParams>(RemoveSheetDrawingCommand.id, {
return commandService.executeCommand<IRemoveSheetDrawingCommandParams>(RemoveSheetDrawingCommand.id, {
unitId,
drawings: newDrawings,
});
@@ -17,7 +17,7 @@
import type { IMutationInfo, IRange, Nullable } from '@univerjs/core';
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
import type { IDiscreteRange } from '@univerjs/sheets';
import type { IDeleteDrawingCommandParams, ISheetDrawing, ISheetImage } from '@univerjs/sheets-drawing';
import type { IRemoveSheetDrawingCommandParams, ISheetDrawing, ISheetImage } from '@univerjs/sheets-drawing';
import type { IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
import { Disposable, DrawingTypeEnum, generateRandomId, ICommandService, ImageSourceType, Inject } from '@univerjs/core';
import { IDrawingManagerService } from '@univerjs/drawing';
@@ -129,12 +129,12 @@ export class SheetsDrawingCopyPasteController extends Disposable {
}
if (copyType === COPY_TYPE.CUT) {
const params: IDeleteDrawingCommandParams = {
const params: IRemoveSheetDrawingCommandParams = {
unitId,
drawings: [drawing],
};
// Delete the drawing when it is cut
this._commandService.executeCommand(RemoveSheetDrawingCommand.id, params);
this._commandService.executeCommand<IRemoveSheetDrawingCommandParams>(RemoveSheetDrawingCommand.id, params);
}
setTimeout(() => {
@@ -16,8 +16,8 @@
import type { Workbook, Worksheet } from '@univerjs/core';
import type {
IDeleteDrawingCommandParams,
IInsertDrawingCommandParams,
IInsertSheetDrawingCommandParams,
IRemoveSheetDrawingCommandParams,
ISetDrawingArrangeCommandParams,
ISetDrawingCommandParams,
} from '@univerjs/sheets-drawing';
@@ -438,7 +438,7 @@ export class SheetDrawingPermissionController extends Disposable {
let subUnitId: string | undefined;
if (command.id === InsertSheetDrawingCommand.id || command.id === RemoveSheetDrawingCommand.id || command.id === SetSheetDrawingCommand.id) {
const params = command.params as IInsertDrawingCommandParams | IDeleteDrawingCommandParams | ISetDrawingCommandParams;
const params = command.params as IInsertSheetDrawingCommandParams | IRemoveSheetDrawingCommandParams | ISetDrawingCommandParams;
const { drawings } = params;
unitId = drawings?.[0]?.unitId;
subUnitId = drawings?.[0]?.subUnitId;
@@ -19,7 +19,7 @@ import type { IImageData } from '@univerjs/drawing';
import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render';
import type { ISheetLocationBase, WorkbookSelectionModel } from '@univerjs/sheets';
import type {
IInsertDrawingCommandParams,
IInsertSheetDrawingCommandParams,
ISetDrawingArrangeCommandParams,
ISetDrawingCommandParams,
ISheetDrawing,
@@ -269,10 +269,10 @@ export class SheetDrawingUpdateController extends Disposable implements IRenderM
axisAlignSheetTransform: transformToAxisAlignPosition(newTransform, skeleton) ?? sheetTransform,
};
return this._commandService.executeCommand(InsertSheetDrawingCommand.id, {
return this._commandService.executeCommand<IInsertSheetDrawingCommandParams>(InsertSheetDrawingCommand.id, {
unitId,
drawings: [sheetDrawingParam],
} as IInsertDrawingCommandParams);
});
}
// eslint-disable-next-line max-lines-per-function
@@ -15,7 +15,7 @@
*/
import type { IDisposable, IDrawingSearch, Injector } from '@univerjs/core';
import type { IDeleteDrawingCommandParams, IInsertDrawingCommandParams, ISetDrawingCommandParams, ISheetFloatDom } from '@univerjs/sheets-drawing';
import type { IInsertSheetDrawingCommandParams, IRemoveSheetDrawingCommandParams, ISetDrawingCommandParams, ISheetFloatDom } from '@univerjs/sheets-drawing';
import type { IBeforeFloatDomAddEventParams, IBeforeFloatDomDeleteEventParams, IBeforeFloatDomUpdateEventParams, IFloatDomAddedEventParams, IFloatDomDeletedEventParams, IFloatDomUpdatedEventParams } from './f-event';
import { CanceledError, DrawingTypeEnum, ICommandService, IURLImageService } from '@univerjs/core';
import { FUniver } from '@univerjs/core/facade';
@@ -62,7 +62,7 @@ export class FUniverSheetsDrawingUIMixin extends FUniver implements IFUniverShee
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== InsertSheetDrawingCommand.id) return;
const params = commandInfo.params as IInsertDrawingCommandParams;
const params = commandInfo.params as IInsertSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -97,7 +97,7 @@ export class FUniverSheetsDrawingUIMixin extends FUniver implements IFUniverShee
() => commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id !== InsertSheetDrawingCommand.id) return;
const params = commandInfo.params as IInsertDrawingCommandParams;
const params = commandInfo.params as IInsertSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -208,7 +208,7 @@ export class FUniverSheetsDrawingUIMixin extends FUniver implements IFUniverShee
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== RemoveSheetDrawingCommand.id) return;
const params = commandInfo.params as IDeleteDrawingCommandParams;
const params = commandInfo.params as IRemoveSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -247,7 +247,7 @@ export class FUniverSheetsDrawingUIMixin extends FUniver implements IFUniverShee
() => commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id !== RemoveSheetDrawingCommand.id) return;
const params = commandInfo.params as IDeleteDrawingCommandParams;
const params = commandInfo.params as IRemoveSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -42,7 +42,7 @@ import type {
} from '@univerjs/sheets';
import type {
IFloatDomData,
IInsertDrawingCommandParams,
IInsertSheetDrawingCommandParams,
ISetDrawingCommandParams,
ISheetDrawing,
ISheetDrawingPosition,
@@ -1809,10 +1809,10 @@ export class SheetCanvasFloatDomManagerService extends Disposable {
// mutation
// ---> this._drawingManagerService.add$.subscribe
this._commandService.executeCommand(InsertSheetDrawingCommand.id, {
this._commandService.executeCommand<IInsertSheetDrawingCommandParams>(InsertSheetDrawingCommand.id, {
unitId,
drawings: [sheetDrawingParam],
} as IInsertDrawingCommandParams);
});
this._add$.next({ unitId, subUnitId, id });
@@ -28,7 +28,7 @@ import { ISheetDrawingService } from '../../services/sheet-drawing.service';
import { DrawingApplyType, SetDrawingApplyMutation } from '../mutations/set-drawing-apply.mutation';
import { ClearSheetDrawingTransformerOperation } from '../operations/clear-drawing-transformer.operation';
export interface IInsertDrawingCommandParams {
export interface IInsertSheetDrawingCommandParams {
unitId: string;
drawings: ISheetDrawing[];
}
@@ -36,7 +36,7 @@ export interface IInsertDrawingCommandParams {
export const InsertSheetDrawingCommand: ICommand = {
id: 'sheet.command.insert-sheet-image',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IInsertDrawingCommandParams) => {
handler: (accessor: IAccessor, params?: IInsertSheetDrawingCommandParams) => {
if (!params) return false;
const commandService = accessor.get(ICommandService);
@@ -27,22 +27,22 @@ import { ISheetDrawingService } from '../../services/sheet-drawing.service';
import { DrawingApplyType, SetDrawingApplyMutation } from '../mutations/set-drawing-apply.mutation';
import { ClearSheetDrawingTransformerOperation } from '../operations/clear-drawing-transformer.operation';
export interface IDeleteDrawingCommandParam {
export interface IRemoveSheetDrawingCommandParam {
unitId: string;
subUnitId: string;
drawingId: string;
drawingType: DrawingTypeEnum;
}
export interface IDeleteDrawingCommandParams {
export interface IRemoveSheetDrawingCommandParams {
unitId: string;
drawings: IDeleteDrawingCommandParam[];
drawings: IRemoveSheetDrawingCommandParam[];
}
export const RemoveSheetDrawingCommand: ICommand = {
id: 'sheet.command.remove-sheet-image',
type: CommandType.COMMAND,
handler: (accessor: IAccessor, params?: IDeleteDrawingCommandParams) => {
handler: (accessor: IAccessor, params?: IRemoveSheetDrawingCommandParams) => {
if (!params) return false;
const commandService = accessor.get(ICommandService);
@@ -15,7 +15,7 @@
*/
import type { IDrawingSearch, Injector } from '@univerjs/core';
import type { IDeleteDrawingCommandParams, IInsertDrawingCommandParams, ISetDrawingCommandParams, ISheetImage } from '@univerjs/sheets-drawing';
import type { IInsertSheetDrawingCommandParams, IRemoveSheetDrawingCommandParams, ISetDrawingCommandParams, ISheetImage } from '@univerjs/sheets-drawing';
import type {
IBeforeOverGridImageChangeEventParams,
IBeforeOverGridImageChangeParamObject,
@@ -50,7 +50,7 @@ export class FUniverSheetsDrawingMixin extends FUniver {
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== InsertSheetDrawingCommand.id) return;
const params = commandInfo.params as IInsertDrawingCommandParams;
const params = commandInfo.params as IInsertSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -77,7 +77,7 @@ export class FUniverSheetsDrawingMixin extends FUniver {
() => commandService.beforeCommandExecuted((commandInfo) => {
if (commandInfo.id !== RemoveSheetDrawingCommand.id) return;
const params = commandInfo.params as IDeleteDrawingCommandParams;
const params = commandInfo.params as IRemoveSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -186,7 +186,7 @@ export class FUniverSheetsDrawingMixin extends FUniver {
() => commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id !== InsertSheetDrawingCommand.id) return;
const params = commandInfo.params as IInsertDrawingCommandParams;
const params = commandInfo.params as IInsertSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
@@ -208,7 +208,7 @@ export class FUniverSheetsDrawingMixin extends FUniver {
() => commandService.onCommandExecuted((commandInfo) => {
if (commandInfo.id !== RemoveSheetDrawingCommand.id) return;
const params = commandInfo.params as IDeleteDrawingCommandParams;
const params = commandInfo.params as IRemoveSheetDrawingCommandParams;
const workbook = this.getActiveWorkbook();
if (workbook == null || params == null) {
return;
+2 -5
View File
@@ -20,12 +20,9 @@ export {
transformToDrawingPosition,
} from './basics/transform-position';
export { InsertSheetDrawingCommand } from './commands/commands/insert-sheet-drawing.command';
export type { IInsertDrawingCommandParams } from './commands/commands/insert-sheet-drawing.command';
export type { IInsertSheetDrawingCommandParams } from './commands/commands/insert-sheet-drawing.command';
export { RemoveSheetDrawingCommand } from './commands/commands/remove-sheet-drawing.command';
export type {
IDeleteDrawingCommandParam,
IDeleteDrawingCommandParams,
} from './commands/commands/remove-sheet-drawing.command';
export type { IRemoveSheetDrawingCommandParam, IRemoveSheetDrawingCommandParams } from './commands/commands/remove-sheet-drawing.command';
export { SetDrawingArrangeCommand } from './commands/commands/set-drawing-arrange.command';
export type { ISetDrawingArrangeCommandParams } from './commands/commands/set-drawing-arrange.command';
export { SetSheetDrawingCommand } from './commands/commands/set-sheet-drawing.command';
+6
View File
@@ -952,6 +952,9 @@ importers:
'@univerjs/core':
specifier: workspace:*
version: link:../core
'@univerjs/docs':
specifier: workspace:*
version: link:../docs
'@univerjs/drawing':
specifier: workspace:*
version: link:../drawing
@@ -959,6 +962,9 @@ importers:
'@univerjs-infra/shared':
specifier: workspace:*
version: link:../../common/shared
'@univerjs/engine-render':
specifier: workspace:*
version: link:../engine-render
typescript:
specifier: ^6.0.3
version: 6.0.3
@@ -21,6 +21,10 @@ import { UniverDocsDrawingUIPlugin } from '@univerjs/docs-drawing-ui';
import { UniverDrawingPlugin } from '@univerjs/drawing';
import { UniverDrawingUIPlugin } from '@univerjs/drawing-ui';
import '@univerjs/docs-drawing/facade';
export type * from '@univerjs/docs-drawing/facade';
export interface IUniverDocsDrawingPresetConfig {
collaboration?: boolean;
}