snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
41
packages/sheets-drawing/README.md
Normal file
41
packages/sheets-drawing/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/sheets-drawing
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-drawing)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-drawing)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-drawing)
|
||||
|
||||
`@univerjs/sheets-drawing` connects the shared drawing model to Univer Sheets so worksheets can host drawing objects.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-drawing` | `UniverSheetsDrawing` | No | No | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-drawing
|
||||
# or
|
||||
npm install @univerjs/sheets-drawing
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing';
|
||||
|
||||
univer.registerPlugin(UniverSheetsDrawingPlugin);
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
Use this package with `@univerjs/sheets-drawing-ui` when users need drawing interactions in the sheet UI.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-drawing)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
88
packages/sheets-drawing/package.json
Normal file
88
packages/sheets-drawing/package.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-drawing",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Drawing model integration for Univer Sheets.",
|
||||
"author": "DreamNum Co., Ltd. <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"univer",
|
||||
"sheets",
|
||||
"drawing",
|
||||
"spreadsheet",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./facade": "./src/facade/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "./lib/es/index.js",
|
||||
"module": "./lib/es/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib/es/index.js",
|
||||
"require": "./lib/cjs/index.js",
|
||||
"types": "./lib/types/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./lib/es/*",
|
||||
"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/*"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:bundle": "univer-cli build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build": "pnpm run build:bundle && pnpm run build:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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 { Dependency, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
UniverSheetsPlugin,
|
||||
} from '@univerjs/sheets';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import { UniverSheetsDrawingPlugin } from '../plugin';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {},
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Sheet2',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface ISheetsDrawingTestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
injector: Injector;
|
||||
workbook: Workbook;
|
||||
commandService: ICommandService;
|
||||
}
|
||||
|
||||
export function createSheetsDrawingTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ISheetsDrawingTestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.get(IUndoRedoService);
|
||||
|
||||
([
|
||||
[IRenderManagerService, { useClass: RenderManagerService }],
|
||||
] as Dependency[]).forEach((dependency) => this._injector.add(dependency));
|
||||
|
||||
dependencies?.forEach((dependency) => this._injector.add(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
univer.registerPlugin(UniverSheetsPlugin);
|
||||
univer.registerPlugin(UniverDrawingPlugin);
|
||||
univer.registerPlugin(UniverSheetsDrawingPlugin);
|
||||
|
||||
const workbook = univer.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
Tools.deepClone(workbookData ?? TEST_WORKBOOK_DATA)
|
||||
);
|
||||
|
||||
get(IUniverInstanceService).focusUnit(workbook.getUnitId());
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const localeService = get(LocaleService);
|
||||
localeService.load({ enUS });
|
||||
localeService.setLocale(LocaleType.EN_US);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
injector,
|
||||
workbook,
|
||||
commandService: get(ICommandService),
|
||||
};
|
||||
}
|
||||
103
packages/sheets-drawing/src/basics/transform-position.ts
Normal file
103
packages/sheets-drawing/src/basics/transform-position.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { ITransformState, Nullable } from '@univerjs/core';
|
||||
import type { SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { ISheetSkeletonManagerParam } from '@univerjs/sheets';
|
||||
import type { ISheetDrawingPosition } from '../services/sheet-drawing.service';
|
||||
import { convertPositionSheetOverGridToAbsolute } from '@univerjs/sheets';
|
||||
|
||||
export function drawingPositionToTransform(position: ISheetDrawingPosition, sheetSkeletonParam: Nullable<ISheetSkeletonManagerParam>): Nullable<ITransformState> {
|
||||
if (!sheetSkeletonParam) return;
|
||||
|
||||
const { unitId, sheetId, skeleton } = sheetSkeletonParam;
|
||||
const { from, to, flipY = false, flipX = false, angle = 0, skewX = 0, skewY = 0 } = position;
|
||||
const absolutePosition = convertPositionSheetOverGridToAbsolute(unitId, sheetId, { from, to }, skeleton);
|
||||
|
||||
let { left, top, width, height } = absolutePosition;
|
||||
|
||||
const sheetWidth = skeleton.rowHeaderWidth + skeleton.columnTotalWidth;
|
||||
const sheetHeight = skeleton.columnHeaderHeight + skeleton.rowTotalHeight;
|
||||
|
||||
if ((left + width) > sheetWidth) {
|
||||
left = sheetWidth - width;
|
||||
}
|
||||
if ((top + height) > sheetHeight) {
|
||||
top = sheetHeight - height;
|
||||
}
|
||||
|
||||
return {
|
||||
flipY,
|
||||
flipX,
|
||||
angle,
|
||||
skewX,
|
||||
skewY,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
// use transform and originSize convert to ISheetDrawingPosition
|
||||
export function transformToDrawingPosition(transform: ITransformState, skeleton: SpreadsheetSkeleton): ISheetDrawingPosition {
|
||||
const { left = 0, top = 0, width = 0, height = 0, flipY = false, flipX = false, angle = 0, skewX = 0, skewY = 0 } = transform;
|
||||
const startSelectionCell = skeleton.getCellIndexAndOffsetByPosition(left, top);
|
||||
const endSelectionCell = skeleton.getCellIndexAndOffsetByPosition(left + width, top + height);
|
||||
|
||||
return {
|
||||
flipY,
|
||||
flipX,
|
||||
angle,
|
||||
skewX,
|
||||
skewY,
|
||||
from: startSelectionCell,
|
||||
to: endSelectionCell,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* In excel, the basic drawing with rotate bound use major axis switch, axis-aligned bound will bu used.That means the position bound of drawing element will save as nearly axis-aligned rectangle.
|
||||
* Here is the rule to convert transform to axis-aligned position:
|
||||
* [-45°, 45°): use the original bound
|
||||
* [45°, 135°): rotate the bound 90° clockwise,and the left, top, bottom,right will use the rotated bound.
|
||||
* [135°, 225°): use the original bound
|
||||
* [225°, 315°): rotate the bound 90° counterclockwise, and the left, top, bottom, right will use the rotated bound.
|
||||
* @return The axis-aligned position of the drawing element.
|
||||
*/
|
||||
export function transformToAxisAlignPosition(transform: ITransformState, skeleton: SpreadsheetSkeleton): ISheetDrawingPosition {
|
||||
const { left = 0, top = 0, width = 0, height = 0, angle = 0 } = transform;
|
||||
|
||||
const norm = ((angle % 360) + 360) % 360;
|
||||
|
||||
const useSwappedAxis =
|
||||
(norm >= 45 && norm < 135) ||
|
||||
(norm >= 225 && norm < 315);
|
||||
|
||||
if (!useSwappedAxis) {
|
||||
return transformToDrawingPosition(transform, skeleton);
|
||||
}
|
||||
|
||||
const rotatedTransform = {
|
||||
...transform,
|
||||
left: left + width / 2 - height / 2,
|
||||
top: top + height / 2 - width / 2,
|
||||
width: height,
|
||||
height: width,
|
||||
};
|
||||
|
||||
return transformToDrawingPosition(rotatedTransform, skeleton);
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
/**
|
||||
* 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 { Injector, Univer, Workbook } from '@univerjs/core';
|
||||
import type { ISheetDrawing } from '../../../services/sheet-drawing.service';
|
||||
import {
|
||||
ArrangeTypeEnum,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
ImageSourceType,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
RedoCommand,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DRAWING_COPY_CONTEXT_KEY, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { CopySheetCommand, RemoveSheetCommand, SetWorksheetActivateCommand, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createSheetsDrawingTestBed } from '../../../__tests__/create-sheets-drawing-test-bed';
|
||||
import { resolveSheetDrawingRotateEnabled } from '../../../common/rotate-enabled';
|
||||
import { ISheetDrawingService } from '../../../services/sheet-drawing.service';
|
||||
import { DrawingApplyType, SetDrawingApplyMutation } from '../../mutations/set-drawing-apply.mutation';
|
||||
import { InsertSheetDrawingCommand } from '../insert-sheet-drawing.command';
|
||||
import { RemoveSheetDrawingCommand } from '../remove-sheet-drawing.command';
|
||||
import { SetDrawingArrangeCommand } from '../set-drawing-arrange.command';
|
||||
import { SetSheetDrawingCommand } from '../set-sheet-drawing.command';
|
||||
|
||||
function createSheetDrawing(drawingId: string, subUnitId = 'sheet1'): ISheetDrawing {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId,
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: `https://example.com/${drawingId}.png`,
|
||||
sheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: {
|
||||
row: 1,
|
||||
rowOffset: 0,
|
||||
column: 1,
|
||||
columnOffset: 0,
|
||||
},
|
||||
to: {
|
||||
row: 4,
|
||||
rowOffset: 0,
|
||||
column: 3,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: {
|
||||
row: 1,
|
||||
rowOffset: 0,
|
||||
column: 1,
|
||||
columnOffset: 0,
|
||||
},
|
||||
to: {
|
||||
row: 4,
|
||||
rowOffset: 0,
|
||||
column: 3,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSheetDrawingTransform(angle = 0) {
|
||||
return {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 30,
|
||||
height: 40,
|
||||
angle,
|
||||
};
|
||||
}
|
||||
|
||||
function withoutAngle<T extends { angle?: number }>(value: T): Omit<T, 'angle'> {
|
||||
const { angle: _angle, ...rest } = value;
|
||||
return rest;
|
||||
}
|
||||
|
||||
function createGroupSheetDrawing(drawingId: string, angle = 0): ISheetDrawing {
|
||||
const drawing = createSheetDrawing(drawingId);
|
||||
|
||||
return {
|
||||
...drawing,
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: createSheetDrawingTransform(angle),
|
||||
groupBaseBound: createSheetDrawingTransform(angle),
|
||||
sheetTransform: {
|
||||
...drawing.sheetTransform,
|
||||
angle,
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
...drawing.axisAlignSheetTransform,
|
||||
angle,
|
||||
},
|
||||
} as ISheetDrawing;
|
||||
}
|
||||
|
||||
describe('sheet drawing integration', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let commandService: ICommandService;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createSheetsDrawingTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
commandService = testBed.commandService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('inserts a drawing through the real command pipeline and syncs both drawing services', async () => {
|
||||
const drawing = createSheetDrawing('drawing-1');
|
||||
|
||||
expect(await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
})).toBe(true);
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const drawingManagerService = get(IDrawingManagerService);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-1' })).toMatchObject({
|
||||
drawingId: 'drawing-1',
|
||||
subUnitId: 'sheet1',
|
||||
});
|
||||
expect(drawingManagerService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-1' })).toMatchObject({
|
||||
drawingId: 'drawing-1',
|
||||
subUnitId: 'sheet1',
|
||||
});
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-1']);
|
||||
});
|
||||
|
||||
it('removes an inserted drawing through the real command pipeline', async () => {
|
||||
const drawing = createSheetDrawing('drawing-2');
|
||||
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(RemoveSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'drawing-2',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
}],
|
||||
})).toBe(true);
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const drawingManagerService = get(IDrawingManagerService);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-2' })).toBeUndefined();
|
||||
expect(drawingManagerService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-2' })).toBeUndefined();
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips remove sheet drawing when no drawing objects are resolved', async () => {
|
||||
expect(await commandService.executeCommand(RemoveSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [],
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('removes and restores a grouped drawing graph including chart children', async () => {
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [
|
||||
createGroupSheetDrawing('group-delete'),
|
||||
{
|
||||
...createSheetDrawing('image-child'),
|
||||
groupId: 'group-delete',
|
||||
transform: createSheetDrawingTransform(),
|
||||
},
|
||||
{
|
||||
...createSheetDrawing('chart-child'),
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
groupId: 'group-delete',
|
||||
transform: createSheetDrawingTransform(),
|
||||
} as ISheetDrawing,
|
||||
],
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(RemoveSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'group-delete',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
}],
|
||||
})).toBe(true);
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const drawingManagerService = get(IDrawingManagerService);
|
||||
const drawingSearches = ['group-delete', 'image-child', 'chart-child'].map((drawingId) => ({ unitId: 'test', subUnitId: 'sheet1', drawingId }));
|
||||
|
||||
drawingSearches.forEach((drawingSearch) => {
|
||||
expect(sheetDrawingService.getDrawingByParam(drawingSearch)).toBeUndefined();
|
||||
expect(drawingManagerService.getDrawingByParam(drawingSearch)).toBeUndefined();
|
||||
});
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual([]);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-delete' })).toMatchObject({
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
});
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'image-child' })).toMatchObject({
|
||||
groupId: 'group-delete',
|
||||
});
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'chart-child' })).toMatchObject({
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
groupId: 'group-delete',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an existing drawing through the real command pipeline', async () => {
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [createSheetDrawing('drawing-update')],
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [{
|
||||
...createSheetDrawing('drawing-update'),
|
||||
source: 'https://example.com/updated.png',
|
||||
sheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: {
|
||||
row: 2,
|
||||
rowOffset: 1,
|
||||
column: 2,
|
||||
columnOffset: 1,
|
||||
},
|
||||
to: {
|
||||
row: 6,
|
||||
rowOffset: 0,
|
||||
column: 5,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
}],
|
||||
})).toBe(true);
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const drawingManagerService = get(IDrawingManagerService);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-update' })).toMatchObject({
|
||||
source: 'https://example.com/updated.png',
|
||||
sheetTransform: {
|
||||
from: {
|
||||
row: 2,
|
||||
column: 2,
|
||||
},
|
||||
to: {
|
||||
row: 6,
|
||||
column: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(drawingManagerService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-update' })).toMatchObject({
|
||||
source: 'https://example.com/updated.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves angle updates for groups containing charts while applying move and resize fields', async () => {
|
||||
const image = {
|
||||
...createSheetDrawing('image-child'),
|
||||
groupId: 'group-with-chart',
|
||||
transform: createSheetDrawingTransform(),
|
||||
};
|
||||
const chart = {
|
||||
...createSheetDrawing('chart-child'),
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
groupId: 'group-with-chart',
|
||||
transform: createSheetDrawingTransform(),
|
||||
} as ISheetDrawing;
|
||||
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [
|
||||
createGroupSheetDrawing('group-with-chart', 30),
|
||||
image,
|
||||
chart,
|
||||
],
|
||||
});
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const restoredGroup = sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-with-chart' })!;
|
||||
expect(Object.prototype.hasOwnProperty.call(restoredGroup.transform ?? {}, 'rotateEnabled')).toBe(false);
|
||||
expect(restoredGroup.transform?.angle).toBe(30);
|
||||
expect(resolveSheetDrawingRotateEnabled(restoredGroup, sheetDrawingService)).toBe(false);
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'group-with-chart',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: {
|
||||
left: 15,
|
||||
top: 25,
|
||||
width: 35,
|
||||
height: 45,
|
||||
angle: 60,
|
||||
},
|
||||
sheetTransform: {
|
||||
...createSheetDrawing('group-with-chart').sheetTransform,
|
||||
angle: 60,
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
...createSheetDrawing('group-with-chart').axisAlignSheetTransform,
|
||||
angle: 60,
|
||||
},
|
||||
}],
|
||||
})).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-with-chart' })).toMatchObject({
|
||||
transform: {
|
||||
left: 15,
|
||||
top: 25,
|
||||
width: 35,
|
||||
height: 45,
|
||||
angle: 30,
|
||||
},
|
||||
sheetTransform: expect.objectContaining({ angle: 30 }),
|
||||
axisAlignSheetTransform: expect.objectContaining({ angle: 30 }),
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'group-with-chart',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: {
|
||||
angle: 60,
|
||||
},
|
||||
}],
|
||||
})).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-with-chart' })).toMatchObject({
|
||||
transform: {
|
||||
left: 15,
|
||||
top: 25,
|
||||
width: 35,
|
||||
height: 45,
|
||||
angle: 30,
|
||||
},
|
||||
sheetTransform: expect.objectContaining({ angle: 30 }),
|
||||
axisAlignSheetTransform: expect.objectContaining({ angle: 30 }),
|
||||
});
|
||||
});
|
||||
|
||||
it('drops incoming angles for non-rotatable drawings when current angle is absent', async () => {
|
||||
const drawing = createSheetDrawing('chart-without-angle');
|
||||
const chart = {
|
||||
...drawing,
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
transform: withoutAngle(createSheetDrawingTransform()),
|
||||
sheetTransform: withoutAngle(drawing.sheetTransform),
|
||||
axisAlignSheetTransform: withoutAngle(drawing.axisAlignSheetTransform),
|
||||
} as ISheetDrawing;
|
||||
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [chart],
|
||||
});
|
||||
|
||||
expect(await commandService.executeCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'chart-without-angle',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
transform: createSheetDrawingTransform(45),
|
||||
sheetTransform: {
|
||||
...drawing.sheetTransform,
|
||||
angle: 45,
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
...drawing.axisAlignSheetTransform,
|
||||
angle: 45,
|
||||
},
|
||||
}],
|
||||
})).toBe(true);
|
||||
|
||||
const restoredChart = get(ISheetDrawingService).getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'chart-without-angle',
|
||||
})!;
|
||||
|
||||
expect(Object.prototype.hasOwnProperty.call(restoredChart.transform ?? {}, 'angle')).toBe(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(restoredChart.sheetTransform, 'angle')).toBe(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(restoredChart.axisAlignSheetTransform, 'angle')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps angle updates unchanged when the current drawing is missing', () => {
|
||||
const commandServiceMock = { syncExecuteCommand: vi.fn(() => true) };
|
||||
const undoRedoServiceMock = { pushUndoRedo: vi.fn() };
|
||||
const sheetDrawingServiceMock = {
|
||||
getDrawingByParam: vi.fn(() => undefined),
|
||||
getBatchUpdateOp: vi.fn((drawings: ISheetDrawing[]) => ({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
undo: null,
|
||||
redo: null,
|
||||
objects: drawings.map(({ unitId, subUnitId, drawingId }) => ({ unitId, subUnitId, drawingId })),
|
||||
})),
|
||||
};
|
||||
const sheetInterceptorServiceMock = {
|
||||
onCommandExecute: vi.fn(() => ({ redos: [], undos: [] })),
|
||||
};
|
||||
const drawing = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'missing-chart',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
transform: { angle: 45 },
|
||||
};
|
||||
const accessor = {
|
||||
get: vi.fn((token) => {
|
||||
if (token === ICommandService) return commandServiceMock;
|
||||
if (token === IUndoRedoService) return undoRedoServiceMock;
|
||||
if (token === ISheetDrawingService) return sheetDrawingServiceMock;
|
||||
if (token === SheetInterceptorService) return sheetInterceptorServiceMock;
|
||||
throw new Error('unexpected dependency');
|
||||
}),
|
||||
};
|
||||
|
||||
expect(SetSheetDrawingCommand.handler(accessor as never, {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
})).toBe(true);
|
||||
expect(sheetDrawingServiceMock.getBatchUpdateOp).toHaveBeenCalledWith([drawing]);
|
||||
expect(sheetInterceptorServiceMock.onCommandExecute).toHaveBeenCalledWith({
|
||||
id: SetSheetDrawingCommand.id,
|
||||
params: {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('arranges drawing order through the real command pipeline', async () => {
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [
|
||||
createSheetDrawing('drawing-a'),
|
||||
createSheetDrawing('drawing-b'),
|
||||
createSheetDrawing('drawing-c'),
|
||||
],
|
||||
});
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-c', 'drawing-b', 'drawing-a']);
|
||||
|
||||
expect(await commandService.executeCommand(SetDrawingArrangeCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingIds: ['drawing-c'],
|
||||
arrangeType: ArrangeTypeEnum.front,
|
||||
})).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-b', 'drawing-a', 'drawing-c']);
|
||||
|
||||
expect(await commandService.executeCommand(SetDrawingArrangeCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingIds: ['drawing-c'],
|
||||
arrangeType: ArrangeTypeEnum.backward,
|
||||
})).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-b', 'drawing-c', 'drawing-a']);
|
||||
|
||||
expect(await commandService.executeCommand(SetDrawingArrangeCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingIds: ['drawing-b'],
|
||||
arrangeType: ArrangeTypeEnum.forward,
|
||||
})).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-c', 'drawing-b', 'drawing-a']);
|
||||
|
||||
expect(await commandService.executeCommand(SetDrawingArrangeCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingIds: ['drawing-a'],
|
||||
arrangeType: ArrangeTypeEnum.back,
|
||||
})).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-a', 'drawing-c', 'drawing-b']);
|
||||
});
|
||||
|
||||
it('moves one drawing to a clamped zero-based order index with undo', async () => {
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [
|
||||
createSheetDrawing('drawing-a'),
|
||||
createSheetDrawing('drawing-b'),
|
||||
createSheetDrawing('drawing-c'),
|
||||
],
|
||||
});
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-c', 'drawing-b', 'drawing-a']);
|
||||
|
||||
expect(await commandService.executeCommand(SetDrawingArrangeCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingIds: ['drawing-b'],
|
||||
zOrder: 99,
|
||||
})).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-c', 'drawing-a', 'drawing-b']);
|
||||
|
||||
expect(commandService.syncExecuteCommand(UndoCommand.id)).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-c', 'drawing-b', 'drawing-a']);
|
||||
|
||||
expect(commandService.syncExecuteCommand(RedoCommand.id)).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual(['drawing-c', 'drawing-a', 'drawing-b']);
|
||||
});
|
||||
|
||||
it('copies sheet drawings when a worksheet is duplicated', async () => {
|
||||
await commandService.executeCommand(SetWorksheetActivateCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
});
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [createSheetDrawing('drawing-3')],
|
||||
});
|
||||
|
||||
expect(commandService.syncExecuteCommand(CopySheetCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBe(true);
|
||||
|
||||
const workbook = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!;
|
||||
let copiedSheetId = '';
|
||||
const sheets = workbook.getSheets();
|
||||
for (const sheet of sheets) {
|
||||
const sheetId = sheet.getSheetId();
|
||||
if (sheetId !== 'sheet1' && sheetId !== 'sheet2') {
|
||||
copiedSheetId = sheetId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(copiedSheetId).not.toBe('');
|
||||
const copiedDrawings = Object.values(get(ISheetDrawingService).getDrawingData('test', copiedSheetId));
|
||||
|
||||
expect(copiedDrawings).toHaveLength(1);
|
||||
expect(copiedDrawings[0]).toMatchObject({
|
||||
unitId: 'test',
|
||||
subUnitId: copiedSheetId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
});
|
||||
expect(copiedDrawings[0].drawingId).not.toBe('drawing-3');
|
||||
expect(get(IDrawingManagerService).getDrawingOrder('test', copiedSheetId)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('copies grouped drawing records with the same group id map', async () => {
|
||||
await commandService.executeCommand(SetWorksheetActivateCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
});
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [
|
||||
createGroupSheetDrawing('group-copy'),
|
||||
{
|
||||
...createSheetDrawing('image-child'),
|
||||
groupId: 'group-copy',
|
||||
transform: createSheetDrawingTransform(),
|
||||
},
|
||||
{
|
||||
...createSheetDrawing('chart-child'),
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
groupId: 'group-copy',
|
||||
transform: createSheetDrawingTransform(),
|
||||
} as ISheetDrawing,
|
||||
],
|
||||
});
|
||||
|
||||
expect(commandService.syncExecuteCommand(CopySheetCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBe(true);
|
||||
|
||||
const workbook = get(IUniverInstanceService).getUnit<Workbook>('test', UniverInstanceType.UNIVER_SHEET)!;
|
||||
const copiedSheetId = workbook.getSheets()
|
||||
.map((sheet) => sheet.getSheetId())
|
||||
.find((sheetId) => sheetId !== 'sheet1' && sheetId !== 'sheet2')!;
|
||||
const copiedDrawings = Object.values(get(ISheetDrawingService).getDrawingData('test', copiedSheetId));
|
||||
const copiedGroup = copiedDrawings.find((drawing) => drawing.drawingType === DrawingTypeEnum.DRAWING_GROUP);
|
||||
const copiedImage = copiedDrawings.find((drawing) => drawing.drawingType === DrawingTypeEnum.DRAWING_IMAGE);
|
||||
const copiedChart = copiedDrawings.find((drawing) => drawing.drawingType === DrawingTypeEnum.DRAWING_CHART);
|
||||
|
||||
expect(copiedDrawings.map((drawing) => drawing.drawingType).sort()).toEqual([
|
||||
DrawingTypeEnum.DRAWING_CHART,
|
||||
DrawingTypeEnum.DRAWING_IMAGE,
|
||||
DrawingTypeEnum.DRAWING_GROUP,
|
||||
].sort());
|
||||
expect(copiedGroup?.drawingId).not.toBe('group-copy');
|
||||
expect(copiedImage?.drawingId).not.toBe('image-child');
|
||||
expect(copiedChart?.drawingId).not.toBe('chart-child');
|
||||
expect(copiedImage?.groupId).toBe(copiedGroup?.drawingId);
|
||||
expect(copiedChart?.groupId).toBe(copiedGroup?.drawingId);
|
||||
});
|
||||
|
||||
it('includes grouped chart children in remove sheet drawing record mutations', async () => {
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [
|
||||
createGroupSheetDrawing('group-remove'),
|
||||
{
|
||||
...createSheetDrawing('image-child'),
|
||||
groupId: 'group-remove',
|
||||
transform: createSheetDrawingTransform(),
|
||||
},
|
||||
{
|
||||
...createSheetDrawing('chart-child'),
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
groupId: 'group-remove',
|
||||
transform: createSheetDrawingTransform(),
|
||||
} as ISheetDrawing,
|
||||
],
|
||||
});
|
||||
|
||||
const intercepted = get(SheetInterceptorService).onCommandExecute({
|
||||
id: RemoveSheetCommand.id,
|
||||
params: { unitId: 'test', subUnitId: 'sheet1' },
|
||||
});
|
||||
const removeMutation = intercepted.redos.find((mutation) => mutation.id === SetDrawingApplyMutation.id);
|
||||
const removeObjects = (removeMutation?.params as { objects?: Array<{ drawingId: string }>; type?: DrawingApplyType } | undefined)?.objects ?? [];
|
||||
|
||||
expect((removeMutation?.params as { type?: DrawingApplyType } | undefined)?.type).toBe(DrawingApplyType.REMOVE);
|
||||
expect(removeObjects.map((object) => object.drawingId)).toEqual(['chart-child', 'image-child', 'group-remove']);
|
||||
});
|
||||
|
||||
it('copies drawing records that are missing from drawing order after ordered records', () => {
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
sheetDrawingService.registerDrawingData('test', {
|
||||
sheet1: {
|
||||
data: {
|
||||
ordered: createSheetDrawing('ordered'),
|
||||
unordered: createSheetDrawing('unordered'),
|
||||
},
|
||||
order: ['ordered'],
|
||||
},
|
||||
});
|
||||
const copyContext = new Map<string, unknown>();
|
||||
|
||||
const intercepted = get(SheetInterceptorService).onCommandExecute({
|
||||
id: CopySheetCommand.id,
|
||||
params: {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
targetSubUnitId: 'copied-sheet',
|
||||
copyContext,
|
||||
},
|
||||
});
|
||||
const insertMutation = intercepted.redos.find((mutation) => mutation.id === SetDrawingApplyMutation.id);
|
||||
const copiedObjects = (insertMutation?.params as { objects?: Array<{ drawingId: string; subUnitId: string }> } | undefined)?.objects ?? [];
|
||||
const copyPlan = copyContext.get(DRAWING_COPY_CONTEXT_KEY) as { drawings: Array<ISheetDrawing & { source?: string }> };
|
||||
|
||||
expect(copyPlan.drawings.map((drawing) => drawing.source)).toEqual([
|
||||
'https://example.com/ordered.png',
|
||||
'https://example.com/unordered.png',
|
||||
]);
|
||||
expect(copiedObjects.map((drawing) => drawing.drawingId)).toEqual(copyPlan.drawings.map((drawing) => drawing.drawingId));
|
||||
expect(copiedObjects.map((drawing) => drawing.subUnitId)).toEqual(['copied-sheet', 'copied-sheet']);
|
||||
});
|
||||
|
||||
it('does not create remove sheet mutations when batch remove resolves no objects', async () => {
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [createSheetDrawing('empty-remove-guard')],
|
||||
});
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const getBatchRemoveOpSpy = vi.spyOn(sheetDrawingService, 'getBatchRemoveOp').mockReturnValue({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
undo: null,
|
||||
redo: null,
|
||||
objects: [],
|
||||
});
|
||||
|
||||
expect(get(SheetInterceptorService).onCommandExecute({
|
||||
id: RemoveSheetCommand.id,
|
||||
params: { unitId: 'test', subUnitId: 'sheet1' },
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
|
||||
getBatchRemoveOpSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not create sheet lifecycle mutations when the source sheet has no drawings', () => {
|
||||
const sheetInterceptorService = get(SheetInterceptorService);
|
||||
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: RemoveSheetCommand.id,
|
||||
params: { unitId: 'test', subUnitId: 'sheet2' },
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: CopySheetCommand.id,
|
||||
params: {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet2',
|
||||
},
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
|
||||
expect(sheetInterceptorService.onCommandExecute({
|
||||
id: CopySheetCommand.id,
|
||||
params: {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet2',
|
||||
targetSubUnitId: 'copied-empty-sheet',
|
||||
copyContext: new Map<string, unknown>(),
|
||||
},
|
||||
})).toMatchObject({ redos: [], undos: [] });
|
||||
});
|
||||
|
||||
it('removes and restores sheet drawings when a worksheet is deleted and undone', async () => {
|
||||
await commandService.executeCommand(SetWorksheetActivateCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
});
|
||||
await commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [createSheetDrawing('drawing-4')],
|
||||
});
|
||||
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
})).toBe(true);
|
||||
|
||||
const sheetDrawingService = get(ISheetDrawingService);
|
||||
const drawingManagerService = get(IDrawingManagerService);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-4' })).toBeUndefined();
|
||||
expect(drawingManagerService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-4' })).toBeUndefined();
|
||||
expect(sheetDrawingService.getDrawingOrder('test', 'sheet1')).toEqual([]);
|
||||
expect(drawingManagerService.getDrawingOrder('test', 'sheet1')).toEqual([]);
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-4' })).toMatchObject({
|
||||
drawingId: 'drawing-4',
|
||||
});
|
||||
expect(drawingManagerService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-4' })).toMatchObject({
|
||||
drawingId: 'drawing-4',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { SheetInterceptorService } from '@univerjs/sheets';
|
||||
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 IInsertSheetDrawingCommandParams {
|
||||
unitId: string;
|
||||
drawings: ISheetDrawing[];
|
||||
}
|
||||
|
||||
export const InsertSheetDrawingCommand: ICommand = {
|
||||
id: 'sheet.command.insert-sheet-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: IInsertSheetDrawingCommandParams) => {
|
||||
if (!params) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
|
||||
const drawings = params.drawings;
|
||||
const jsonOp = sheetDrawingService.getBatchAddOp(drawings) as IDrawingJsonUndo1;
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
const intercepted = sheetInterceptorService.onCommandExecute({ id: InsertSheetDrawingCommand.id, params });
|
||||
const redoMutations = [
|
||||
...(intercepted.preRedos ?? []),
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
op: redo,
|
||||
objects,
|
||||
type: DrawingApplyType.INSERT,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ClearSheetDrawingTransformerOperation.id,
|
||||
params: [unitId],
|
||||
},
|
||||
...intercepted.redos,
|
||||
];
|
||||
const undoMutations = [
|
||||
...(intercepted.preUndos ?? []),
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
op: undo,
|
||||
objects,
|
||||
type: DrawingApplyType.REMOVE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ClearSheetDrawingTransformerOperation.id,
|
||||
params: [unitId],
|
||||
},
|
||||
...intercepted.undos,
|
||||
];
|
||||
|
||||
const result = sequenceExecute(redoMutations, commandService);
|
||||
|
||||
if (result.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations,
|
||||
redoMutations,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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, IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { SheetInterceptorService } from '@univerjs/sheets';
|
||||
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 IRemoveSheetDrawingCommandParam {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawingId: string;
|
||||
drawingType: DrawingTypeEnum;
|
||||
}
|
||||
|
||||
export interface IRemoveSheetDrawingCommandParams {
|
||||
unitId: string;
|
||||
drawings: IRemoveSheetDrawingCommandParam[];
|
||||
}
|
||||
|
||||
export const RemoveSheetDrawingCommand: ICommand = {
|
||||
id: 'sheet.command.remove-sheet-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: IRemoveSheetDrawingCommandParams) => {
|
||||
if (!params) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
|
||||
const { drawings } = params;
|
||||
const jsonOp = sheetDrawingService.getBatchRemoveOp(drawings) as IDrawingJsonUndo1;
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
if (Array.isArray(objects) && objects.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const intercepted = sheetInterceptorService.onCommandExecute({ id: RemoveSheetDrawingCommand.id, params });
|
||||
const redoMutations = [
|
||||
...(intercepted.preRedos ?? []),
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
op: redo,
|
||||
objects,
|
||||
type: DrawingApplyType.REMOVE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ClearSheetDrawingTransformerOperation.id,
|
||||
params: [unitId],
|
||||
},
|
||||
...intercepted.redos,
|
||||
];
|
||||
const undoMutations = [
|
||||
...(intercepted.preUndos ?? []),
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
op: undo,
|
||||
objects,
|
||||
type: DrawingApplyType.INSERT,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ClearSheetDrawingTransformerOperation.id,
|
||||
params: [unitId],
|
||||
},
|
||||
...intercepted.undos,
|
||||
];
|
||||
|
||||
const result = sequenceExecute(redoMutations, commandService);
|
||||
|
||||
if (result.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations,
|
||||
redoMutations,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand, Nullable } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1, IDrawingOrderMapParam } from '@univerjs/drawing';
|
||||
import {
|
||||
ArrangeTypeEnum,
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
} from '@univerjs/core';
|
||||
import { ISheetDrawingService } from '../../services/sheet-drawing.service';
|
||||
import { DrawingApplyType, SetDrawingApplyMutation } from '../mutations/set-drawing-apply.mutation';
|
||||
|
||||
export type ISetDrawingArrangeCommandParams = IDrawingOrderMapParam & (
|
||||
{ arrangeType: ArrangeTypeEnum; zOrder?: never }
|
||||
| { arrangeType?: never; zOrder: number }
|
||||
);
|
||||
|
||||
export const SetDrawingArrangeCommand: ICommand = {
|
||||
id: 'sheet.command.set-drawing-arrange',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: ISetDrawingArrangeCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
|
||||
if (!params) return false;
|
||||
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
|
||||
const { unitId, subUnitId, drawingIds } = params;
|
||||
|
||||
const drawingOrderMapParam = { unitId, subUnitId, drawingIds } as IDrawingOrderMapParam;
|
||||
|
||||
let jsonOp: Nullable<IDrawingJsonUndo1>;
|
||||
if (params.zOrder !== undefined) {
|
||||
jsonOp = sheetDrawingService.getDrawingOrderOp(drawingOrderMapParam, params.zOrder) as Nullable<IDrawingJsonUndo1>;
|
||||
} else if (params.arrangeType === ArrangeTypeEnum.forward) {
|
||||
jsonOp = sheetDrawingService.getForwardDrawingsOp(drawingOrderMapParam) as IDrawingJsonUndo1;
|
||||
} else if (params.arrangeType === ArrangeTypeEnum.backward) {
|
||||
jsonOp = sheetDrawingService.getBackwardDrawingOp(drawingOrderMapParam) as IDrawingJsonUndo1;
|
||||
} else if (params.arrangeType === ArrangeTypeEnum.front) {
|
||||
jsonOp = sheetDrawingService.getFrontDrawingsOp(drawingOrderMapParam) as IDrawingJsonUndo1;
|
||||
} else if (params.arrangeType === ArrangeTypeEnum.back) {
|
||||
jsonOp = sheetDrawingService.getBackDrawingsOp(drawingOrderMapParam) as IDrawingJsonUndo1;
|
||||
}
|
||||
|
||||
if (jsonOp == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { objects, redo, undo } = jsonOp;
|
||||
|
||||
const result = commandService.syncExecuteCommand(SetDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.ARRANGE });
|
||||
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects, type: DrawingApplyType.ARRANGE } },
|
||||
],
|
||||
redoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.ARRANGE } },
|
||||
],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { ISheetDrawingPlacementInput } from '../../services/sheet-drawing-placement';
|
||||
import { CommandType, ICommandService, IUndoRedoService, sequenceExecute } from '@univerjs/core';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { applySheetDrawingPlacement } from '../../services/sheet-drawing-placement';
|
||||
import { ISheetDrawingService, SheetDrawingAnchorType } from '../../services/sheet-drawing.service';
|
||||
import { DrawingApplyType, SetDrawingApplyMutation } from '../mutations/set-drawing-apply.mutation';
|
||||
import { ClearSheetDrawingTransformerOperation } from '../operations/clear-drawing-transformer.operation';
|
||||
|
||||
export interface ISetSheetDrawingPlacementCommandParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawings: Array<{
|
||||
drawingId: string;
|
||||
placement: ISheetDrawingPlacementInput;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const SetSheetDrawingPlacementCommand: ICommand<ISetSheetDrawingPlacementCommandParams> = {
|
||||
id: 'sheet.command.set-drawing-placement',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params?.drawings.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const drawingService = accessor.get(ISheetDrawingService);
|
||||
const skeleton = params.drawings.every(({ placement }) => placement.kind === SheetDrawingAnchorType.None)
|
||||
? undefined
|
||||
: accessor.get(SheetSkeletonService).ensureSkeleton(params.unitId, params.subUnitId);
|
||||
const updatedDrawings = [];
|
||||
for (const { drawingId, placement } of params.drawings) {
|
||||
const drawing = drawingService.getDrawingByParam({
|
||||
unitId: params.unitId,
|
||||
subUnitId: params.subUnitId,
|
||||
drawingId,
|
||||
});
|
||||
if (!drawing) {
|
||||
return false;
|
||||
}
|
||||
updatedDrawings.push(applySheetDrawingPlacement(drawing, placement, skeleton));
|
||||
}
|
||||
const drawingOp = drawingService.getBatchUpdateOp(updatedDrawings);
|
||||
const { unitId, subUnitId, undo, redo, objects } = drawingOp;
|
||||
const redoMutations = [
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: { unitId, subUnitId, op: redo, objects, type: DrawingApplyType.UPDATE },
|
||||
},
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: [unitId] },
|
||||
];
|
||||
const undoMutations = [
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: { unitId, subUnitId, op: undo, objects, type: DrawingApplyType.UPDATE },
|
||||
},
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: [unitId] },
|
||||
];
|
||||
const result = sequenceExecute(redoMutations, commandService);
|
||||
if (!result.result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations,
|
||||
redoMutations,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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 { ICommand } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { resolveSheetDrawingRotateEnabled } from '../../common/rotate-enabled';
|
||||
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 ISetDrawingCommandParams {
|
||||
unitId: string;
|
||||
drawings: Partial<ISheetDrawing>[];
|
||||
}
|
||||
|
||||
function hasIncomingAngle(drawing: Partial<ISheetDrawing>): boolean {
|
||||
return drawing.transform?.angle !== undefined || drawing.sheetTransform?.angle !== undefined || drawing.axisAlignSheetTransform?.angle !== undefined;
|
||||
}
|
||||
|
||||
type NullableAngleState = { angle?: number } | null | undefined | void;
|
||||
|
||||
function preserveAngle<T extends { angle?: number }>(incoming: T, current: NullableAngleState): T {
|
||||
const currentAngle = (current as { angle?: number } | null | undefined)?.angle;
|
||||
const next = { ...((current ?? {}) as object), ...incoming } as T;
|
||||
if (currentAngle === undefined) {
|
||||
delete next.angle;
|
||||
} else {
|
||||
next.angle = currentAngle;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeNonRotatableAngleUpdate(drawing: Partial<ISheetDrawing>, sheetDrawingService: ISheetDrawingService): Partial<ISheetDrawing> {
|
||||
if (!drawing.drawingId || !drawing.unitId || !drawing.subUnitId || !hasIncomingAngle(drawing)) {
|
||||
return drawing;
|
||||
}
|
||||
|
||||
const current = sheetDrawingService.getDrawingByParam({
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
drawingId: drawing.drawingId,
|
||||
});
|
||||
if (!current) {
|
||||
return drawing;
|
||||
}
|
||||
|
||||
const resolveTarget = {
|
||||
...current,
|
||||
...drawing,
|
||||
drawingType: drawing.drawingType ?? current.drawingType,
|
||||
transform: {
|
||||
...current.transform,
|
||||
...drawing.transform,
|
||||
},
|
||||
} as ISheetDrawing;
|
||||
|
||||
if (resolveSheetDrawingRotateEnabled(resolveTarget, sheetDrawingService)) {
|
||||
return drawing;
|
||||
}
|
||||
|
||||
const normalized = { ...drawing };
|
||||
if (drawing.transform?.angle !== undefined) {
|
||||
normalized.transform = preserveAngle(drawing.transform, current.transform);
|
||||
}
|
||||
if (drawing.sheetTransform?.angle !== undefined) {
|
||||
normalized.sheetTransform = preserveAngle(drawing.sheetTransform, current.sheetTransform);
|
||||
}
|
||||
if (drawing.axisAlignSheetTransform?.angle !== undefined) {
|
||||
normalized.axisAlignSheetTransform = preserveAngle(drawing.axisAlignSheetTransform, current.axisAlignSheetTransform);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export const SetSheetDrawingCommand: ICommand<ISetDrawingCommandParams> = {
|
||||
id: 'sheet.command.set-sheet-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor, params) => {
|
||||
if (!params) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
const sheetInterceptorService = accessor.get(SheetInterceptorService);
|
||||
|
||||
const { drawings } = params;
|
||||
const normalizedDrawings = drawings.map((drawing) => normalizeNonRotatableAngleUpdate(drawing, sheetDrawingService));
|
||||
const normalizedParams = { ...params, drawings: normalizedDrawings };
|
||||
const jsonOp = sheetDrawingService.getBatchUpdateOp(normalizedDrawings as ISheetDrawing[]) as IDrawingJsonUndo1;
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
const intercepted = sheetInterceptorService.onCommandExecute({ id: SetSheetDrawingCommand.id, params: normalizedParams });
|
||||
const redoMutations = [
|
||||
...(intercepted.preRedos ?? []),
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
op: redo,
|
||||
objects,
|
||||
type: DrawingApplyType.UPDATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ClearSheetDrawingTransformerOperation.id,
|
||||
params: [unitId],
|
||||
},
|
||||
...intercepted.redos,
|
||||
];
|
||||
const undoMutations = [
|
||||
...(intercepted.preUndos ?? []),
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
op: undo,
|
||||
objects,
|
||||
type: DrawingApplyType.UPDATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ClearSheetDrawingTransformerOperation.id,
|
||||
params: [unitId],
|
||||
},
|
||||
...intercepted.undos,
|
||||
];
|
||||
|
||||
const result = sequenceExecute(redoMutations, commandService);
|
||||
if (result.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations,
|
||||
redoMutations,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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 { IDrawingSearch, IMutation } from '@univerjs/core';
|
||||
import type { IDrawingGroupUpdateParam, IDrawingJson1Type, IDrawingOrderMapParam } from '@univerjs/drawing';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { ISheetDrawingService } from '../../services/sheet-drawing.service';
|
||||
|
||||
export enum DrawingApplyType {
|
||||
INSERT,
|
||||
REMOVE,
|
||||
UPDATE,
|
||||
ARRANGE,
|
||||
GROUP,
|
||||
UNGROUP,
|
||||
}
|
||||
|
||||
export interface ISetDrawingApplyMutationParams extends IDrawingJson1Type {
|
||||
type: DrawingApplyType;
|
||||
}
|
||||
|
||||
export const SetDrawingApplyMutation: IMutation<ISetDrawingApplyMutationParams> = {
|
||||
id: 'sheet.mutation.set-drawing-apply',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const drawingManagerService = accessor.get(IDrawingManagerService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
|
||||
const { op, unitId, subUnitId, type, objects } = params;
|
||||
|
||||
drawingManagerService.applyJson1(unitId, subUnitId, op);
|
||||
sheetDrawingService.applyJson1(unitId, subUnitId, op);
|
||||
|
||||
switch (type) {
|
||||
case DrawingApplyType.INSERT:
|
||||
drawingManagerService.addNotification(objects as IDrawingSearch[]);
|
||||
sheetDrawingService.addNotification(objects as IDrawingSearch[]);
|
||||
break;
|
||||
case DrawingApplyType.REMOVE:
|
||||
drawingManagerService.removeNotification(objects as IDrawingSearch[]);
|
||||
sheetDrawingService.removeNotification(objects as IDrawingSearch[]);
|
||||
break;
|
||||
case DrawingApplyType.UPDATE:
|
||||
drawingManagerService.updateNotification(objects as IDrawingSearch[]);
|
||||
sheetDrawingService.updateNotification(objects as IDrawingSearch[]);
|
||||
break;
|
||||
case DrawingApplyType.ARRANGE:
|
||||
drawingManagerService.orderNotification(objects as IDrawingOrderMapParam);
|
||||
sheetDrawingService.orderNotification(objects as IDrawingOrderMapParam);
|
||||
break;
|
||||
case DrawingApplyType.GROUP:
|
||||
drawingManagerService.groupUpdateNotification(objects as IDrawingGroupUpdateParam[]);
|
||||
sheetDrawingService.groupUpdateNotification(objects as IDrawingGroupUpdateParam[]);
|
||||
break;
|
||||
case DrawingApplyType.UNGROUP:
|
||||
drawingManagerService.ungroupUpdateNotification(objects as IDrawingGroupUpdateParam[]);
|
||||
sheetDrawingService.ungroupUpdateNotification(objects as IDrawingGroupUpdateParam[]);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
|
||||
export const ClearSheetDrawingTransformerOperation: IMutation<string[]> = {
|
||||
id: 'sheet.operation.clear-drawing-transformer',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
|
||||
params.forEach((unitId) => {
|
||||
renderManagerService.getRenderUnitById(unitId)?.scene.getTransformer()?.debounceRefreshControls();
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 { IDrawingParam } from '@univerjs/core';
|
||||
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
|
||||
import { DrawingTypeEnum } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveSheetDrawingRotateEnabled } from '../rotate-enabled';
|
||||
|
||||
const unitId = 'unit-1';
|
||||
const subUnitId = 'sheet-1';
|
||||
|
||||
function createDrawing(drawingId: string, drawingType = DrawingTypeEnum.DRAWING_IMAGE, groupId?: string, rotateEnabled?: boolean): IDrawingParam {
|
||||
const transform: IDrawingParam['transform'] = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
if (rotateEnabled !== undefined) {
|
||||
transform.rotateEnabled = rotateEnabled;
|
||||
}
|
||||
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
drawingType,
|
||||
groupId,
|
||||
transform,
|
||||
};
|
||||
}
|
||||
|
||||
function createGroup(drawingId: string, groupId?: string, rotateEnabled?: boolean): IDrawingParam {
|
||||
return createDrawing(drawingId, DrawingTypeEnum.DRAWING_GROUP, groupId, rotateEnabled);
|
||||
}
|
||||
|
||||
function createDrawingService(drawings: readonly IDrawingParam[] = []) {
|
||||
return {
|
||||
getDrawingsByGroup: (drawing: IDrawingParam) => drawings.filter((child) =>
|
||||
child.groupId === drawing.drawingId &&
|
||||
child.unitId === drawing.unitId &&
|
||||
child.subUnitId === drawing.subUnitId
|
||||
) as ISheetDrawing[],
|
||||
};
|
||||
}
|
||||
|
||||
describe('sheet drawing rotate-enabled helpers', () => {
|
||||
it('keeps regular drawings rotatable by default', () => {
|
||||
expect(resolveSheetDrawingRotateEnabled(createDrawing('shape'), createDrawingService())).toBe(true);
|
||||
});
|
||||
|
||||
it('treats chart drawings as non-rotatable without persisted rotate flags', () => {
|
||||
const chart = createDrawing('chart', DrawingTypeEnum.DRAWING_CHART);
|
||||
|
||||
expect(Object.prototype.hasOwnProperty.call(chart.transform ?? {}, 'rotateEnabled')).toBe(false);
|
||||
expect(resolveSheetDrawingRotateEnabled(chart, createDrawingService())).toBe(false);
|
||||
});
|
||||
|
||||
it('treats old chart children as non-rotatable', () => {
|
||||
const root = createGroup('root');
|
||||
const chart = createDrawing('chart', DrawingTypeEnum.DRAWING_CHART, 'root');
|
||||
|
||||
expect(resolveSheetDrawingRotateEnabled(root, createDrawingService([chart]))).toBe(false);
|
||||
});
|
||||
|
||||
it('recomputes stale nested group rotate flags from current children', () => {
|
||||
const root = createGroup('root');
|
||||
const nested = createGroup('nested', 'root', false);
|
||||
const child = createDrawing('shape', DrawingTypeEnum.DRAWING_IMAGE, 'nested');
|
||||
|
||||
expect(resolveSheetDrawingRotateEnabled(root, createDrawingService([nested, child]))).toBe(true);
|
||||
});
|
||||
});
|
||||
36
packages/sheets-drawing/src/common/rotate-enabled.ts
Normal file
36
packages/sheets-drawing/src/common/rotate-enabled.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { DrawingType, IDrawingParam } from '@univerjs/core';
|
||||
import type { IUnitDrawingService } from '@univerjs/drawing';
|
||||
import type { ISheetDrawing } from '../services/sheet-drawing.service';
|
||||
import { DrawingTypeEnum } from '@univerjs/core';
|
||||
import { resolveDrawingRotateEnabled } from '@univerjs/drawing';
|
||||
|
||||
export function isKnownSheetNonRotatableDrawingType(drawingType: DrawingType): boolean {
|
||||
return drawingType === DrawingTypeEnum.DRAWING_CHART;
|
||||
}
|
||||
|
||||
export function resolveSheetDrawingRotateEnabled(
|
||||
drawing: IDrawingParam,
|
||||
drawingService: Pick<IUnitDrawingService<ISheetDrawing>, 'getDrawingsByGroup'>,
|
||||
getChildren?: (drawing: IDrawingParam) => readonly IDrawingParam[] | null | undefined
|
||||
): boolean {
|
||||
return resolveDrawingRotateEnabled(drawing, {
|
||||
getChildren: (current) => getChildren?.(current) ?? drawingService.getDrawingsByGroup(current),
|
||||
isKnownNonRotatableType: isKnownSheetNonRotatableDrawingType,
|
||||
});
|
||||
}
|
||||
24
packages/sheets-drawing/src/config/config.ts
Normal file
24
packages/sheets-drawing/src/config/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const SHEETS_DRAWING_PLUGIN_CONFIG_KEY = 'sheets-drawing.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_DRAWING_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsDrawingConfig {
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsDrawingConfig = {};
|
||||
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* 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 { DrawingTypeEnum } from '@univerjs/core';
|
||||
import {
|
||||
ClearSheetDrawingTransformerOperation,
|
||||
DrawingApplyType,
|
||||
SetDrawingApplyMutation,
|
||||
SheetDrawingAnchorType,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetDrawingTransformAffectedController } from '../sheet-drawing-transform-affected.controller';
|
||||
|
||||
const UNIT_ID = 'unit-1';
|
||||
const SUB_UNIT_ID = 'sheet-1';
|
||||
|
||||
function createSkeleton(options: { rowHeights?: number[]; colWidths?: number[] } = {}) {
|
||||
const rowHeights = options.rowHeights ?? [];
|
||||
const colWidths = options.colWidths ?? [];
|
||||
const rowHeightAt = (row: number) => rowHeights[row] ?? 20;
|
||||
const colWidthAt = (column: number) => colWidths[column] ?? 10;
|
||||
const sumRows = (end: number) => Array.from({ length: Math.max(end, 0) }, (_, row) => rowHeightAt(row)).reduce((sum, height) => sum + height, 0);
|
||||
const sumCols = (end: number) => Array.from({ length: Math.max(end, 0) }, (_, column) => colWidthAt(column)).reduce((sum, width) => sum + width, 0);
|
||||
|
||||
return {
|
||||
rowHeaderWidth: 0,
|
||||
columnHeaderHeight: 0,
|
||||
rowTotalHeight: 2000,
|
||||
columnTotalWidth: 1000,
|
||||
makeDirty: vi.fn(),
|
||||
calculate: vi.fn(),
|
||||
getNoMergeCellWithCoordByIndex: vi.fn((row: number, column: number) => {
|
||||
const startY = sumRows(row);
|
||||
const startX = sumCols(column);
|
||||
return {
|
||||
startX,
|
||||
endX: startX + colWidthAt(column),
|
||||
startY,
|
||||
endY: startY + rowHeightAt(row),
|
||||
};
|
||||
}),
|
||||
getCellIndexAndOffsetByPosition: vi.fn((left: number, top: number) => {
|
||||
let column = 0;
|
||||
let colStart = 0;
|
||||
while (left >= colStart + colWidthAt(column)) {
|
||||
colStart += colWidthAt(column);
|
||||
column++;
|
||||
}
|
||||
|
||||
let row = 0;
|
||||
let rowStart = 0;
|
||||
while (top >= rowStart + rowHeightAt(row)) {
|
||||
rowStart += rowHeightAt(row);
|
||||
row++;
|
||||
}
|
||||
|
||||
return {
|
||||
column,
|
||||
row,
|
||||
columnOffset: left - colStart,
|
||||
rowOffset: top - rowStart,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function transformFromPosition(sheetTransform: any, skeleton: any) {
|
||||
const fromCell = skeleton.getNoMergeCellWithCoordByIndex(sheetTransform.from.row, sheetTransform.from.column);
|
||||
const toCell = skeleton.getNoMergeCellWithCoordByIndex(sheetTransform.to.row, sheetTransform.to.column);
|
||||
const left = fromCell.startX + sheetTransform.from.columnOffset;
|
||||
const top = fromCell.startY + sheetTransform.from.rowOffset;
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
width: toCell.startX + sheetTransform.to.columnOffset - left,
|
||||
height: toCell.startY + sheetTransform.to.rowOffset - top,
|
||||
};
|
||||
}
|
||||
|
||||
function createDrawing(overrides: Record<string, any> = {}, skeleton = createSkeleton()) {
|
||||
const sheetTransform = overrides.sheetTransform ?? {
|
||||
from: { row: 1, column: 1, rowOffset: 2, columnOffset: 2 },
|
||||
to: { row: 3, column: 3, rowOffset: 6, columnOffset: 6 },
|
||||
};
|
||||
const transform = overrides.transform ?? transformFromPosition(sheetTransform, skeleton);
|
||||
|
||||
return {
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
drawingId: overrides.drawingId ?? 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
anchorType: overrides.anchorType ?? SheetDrawingAnchorType.Both,
|
||||
sheetTransform,
|
||||
axisAlignSheetTransform: sheetTransform,
|
||||
transform,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createController(options: { drawingData?: Record<string, any>; skeleton?: any } = {}) {
|
||||
const skeleton = options.skeleton ?? createSkeleton();
|
||||
const drawingData = options.drawingData ?? {};
|
||||
const sheetSkeletonParam = { unitId: UNIT_ID, sheetId: SUB_UNIT_ID, skeleton };
|
||||
const sheetDrawingService = {
|
||||
getDrawingData: vi.fn(() => drawingData),
|
||||
getBatchUpdateOp: vi.fn((drawings) => ({ undo: 'update-undo', redo: 'update-redo', objects: drawings })),
|
||||
getBatchRemoveOp: vi.fn((drawings) => ({ undo: 'remove-undo', redo: 'remove-redo', objects: drawings })),
|
||||
refreshTransform: vi.fn(),
|
||||
};
|
||||
const drawingManagerService = {
|
||||
getDrawingData: vi.fn(() => drawingData),
|
||||
refreshTransform: vi.fn(),
|
||||
};
|
||||
const controller = Object.create(SheetDrawingTransformAffectedController.prototype) as any;
|
||||
|
||||
controller._sheetSkeletonService = {
|
||||
getSkeletonParam: vi.fn(() => sheetSkeletonParam),
|
||||
getSkeleton: vi.fn(() => skeleton),
|
||||
};
|
||||
controller._sheetDrawingService = sheetDrawingService;
|
||||
controller._drawingManagerService = drawingManagerService;
|
||||
controller._commandService = { syncExecuteCommand: vi.fn() };
|
||||
controller._transformPlanService = {
|
||||
transform: vi.fn(() => ({ preRedos: [], redos: [], preUndos: [], undos: [] })),
|
||||
};
|
||||
controller._getUnitIdAndSubUnitId = vi.fn(() => ({ unitId: UNIT_ID, subUnitId: SUB_UNIT_ID }));
|
||||
|
||||
return { controller, skeleton, sheetDrawingService, drawingManagerService };
|
||||
}
|
||||
|
||||
describe('SheetDrawingTransformAffectedController', () => {
|
||||
it('finalizes complete drawing candidates through extensions in mutation order', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const drawing = createDrawing({ drawingId: 'extended' }, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: { [drawing.drawingId]: drawing },
|
||||
});
|
||||
controller._transformPlanService.transform.mockImplementation((plan: any) => {
|
||||
const candidate = plan.updates.get(drawing.drawingId);
|
||||
expect(candidate).toMatchObject({
|
||||
drawingId: drawing.drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
anchorType: SheetDrawingAnchorType.Both,
|
||||
});
|
||||
plan.updates.set(drawing.drawingId, {
|
||||
...candidate,
|
||||
transform: { ...candidate.transform, left: 77, top: 88 },
|
||||
});
|
||||
return {
|
||||
preRedos: [{ id: 'feature.pre-redo', params: {} }],
|
||||
redos: [{ id: 'feature.redo', params: {} }],
|
||||
preUndos: [{ id: 'feature.pre-undo', params: {} }],
|
||||
undos: [{ id: 'feature.undo', params: {} }],
|
||||
};
|
||||
});
|
||||
|
||||
const result = controller._moveColInterceptor({
|
||||
range: { startRow: 0, endRow: 9, startColumn: 0, endColumn: 0 },
|
||||
}, 'insert');
|
||||
|
||||
expect(result.redos.map(({ id }: any) => id)).toEqual([
|
||||
'feature.pre-redo',
|
||||
SetDrawingApplyMutation.id,
|
||||
'feature.redo',
|
||||
ClearSheetDrawingTransformerOperation.id,
|
||||
]);
|
||||
expect(result.undos.map(({ id }: any) => id)).toEqual([
|
||||
'feature.pre-undo',
|
||||
SetDrawingApplyMutation.id,
|
||||
'feature.undo',
|
||||
ClearSheetDrawingTransformerOperation.id,
|
||||
]);
|
||||
expect(sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0].transform).toMatchObject({ left: 77, top: 88 });
|
||||
});
|
||||
|
||||
it('removes drawings covered by deleted rows and shifts drawings below into undoable mutations', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const deleted = createDrawing({
|
||||
drawingId: 'deleted',
|
||||
sheetTransform: {
|
||||
from: { row: 2, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 2, rowOffset: 10, columnOffset: 5 },
|
||||
},
|
||||
}, skeleton);
|
||||
const shifted = createDrawing({
|
||||
drawingId: 'shifted',
|
||||
sheetTransform: {
|
||||
from: { row: 5, column: 1, rowOffset: 2, columnOffset: 2 },
|
||||
to: { row: 6, column: 2, rowOffset: 6, columnOffset: 5 },
|
||||
},
|
||||
}, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: { deleted, shifted },
|
||||
});
|
||||
|
||||
const result = controller._moveRowInterceptor({
|
||||
range: { startRow: 2, endRow: 3, startColumn: 0, endColumn: 9 },
|
||||
}, 'remove');
|
||||
|
||||
expect(sheetDrawingService.getBatchUpdateOp).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
drawingId: 'shifted',
|
||||
transform: expect.objectContaining({ top: 62, height: 24 }),
|
||||
sheetTransform: expect.objectContaining({
|
||||
from: expect.objectContaining({ row: 3, rowOffset: 2 }),
|
||||
to: expect.objectContaining({ row: 4, rowOffset: 6 }),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(sheetDrawingService.getBatchRemoveOp).toHaveBeenCalledWith([
|
||||
{ unitId: UNIT_ID, subUnitId: SUB_UNIT_ID, drawingId: 'deleted' },
|
||||
]);
|
||||
expect(result.redos).toEqual([
|
||||
{ id: SetDrawingApplyMutation.id, params: { unitId: UNIT_ID, subUnitId: SUB_UNIT_ID, op: 'update-redo', objects: sheetDrawingService.getBatchUpdateOp.mock.calls[0][0], type: DrawingApplyType.UPDATE } },
|
||||
{ id: SetDrawingApplyMutation.id, params: { unitId: UNIT_ID, subUnitId: SUB_UNIT_ID, op: 'remove-redo', objects: sheetDrawingService.getBatchRemoveOp.mock.calls[0][0], type: DrawingApplyType.REMOVE } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: [UNIT_ID] },
|
||||
]);
|
||||
expect(result.undos[1].params.type).toBe(DrawingApplyType.INSERT);
|
||||
});
|
||||
|
||||
it('moves drawings to the right when columns are inserted before them', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const drawing = createDrawing({
|
||||
drawingId: 'image-after-inserted-cols',
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 3, rowOffset: 2, columnOffset: 2 },
|
||||
to: { row: 2, column: 4, rowOffset: 8, columnOffset: 5 },
|
||||
},
|
||||
}, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: { [drawing.drawingId]: drawing },
|
||||
});
|
||||
|
||||
const result = controller._moveColInterceptor({
|
||||
range: { startRow: 0, endRow: 9, startColumn: 1, endColumn: 2 },
|
||||
}, 'insert');
|
||||
const updatedDrawing = sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0];
|
||||
|
||||
expect(updatedDrawing).toMatchObject({
|
||||
drawingId: 'image-after-inserted-cols',
|
||||
transform: { left: 52, top: 22, width: 13, height: 26 },
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 5, rowOffset: 2, columnOffset: 2 },
|
||||
to: { row: 2, column: 6, rowOffset: 8, columnOffset: 5 },
|
||||
},
|
||||
});
|
||||
expect(result.redos).toContainEqual({ id: ClearSheetDrawingTransformerOperation.id, params: [UNIT_ID] });
|
||||
});
|
||||
|
||||
it('shrinks a both-anchored drawing when deleted rows cover its top anchor', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const drawing = createDrawing({
|
||||
drawingId: 'row-delete-top-anchor',
|
||||
sheetTransform: {
|
||||
from: { row: 2, column: 1, rowOffset: 5, columnOffset: 0 },
|
||||
to: { row: 6, column: 2, rowOffset: 5, columnOffset: 5 },
|
||||
},
|
||||
}, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: { [drawing.drawingId]: drawing },
|
||||
});
|
||||
|
||||
controller._moveRowInterceptor({
|
||||
range: { startRow: 2, endRow: 4, startColumn: 0, endColumn: 9 },
|
||||
}, 'remove');
|
||||
const updatedDrawing = sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0];
|
||||
|
||||
expect(updatedDrawing).toMatchObject({
|
||||
transform: { top: 40, height: 25 },
|
||||
sheetTransform: {
|
||||
from: { row: 2, rowOffset: 0 },
|
||||
to: { row: 3, rowOffset: 5 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shrinks a both-anchored drawing when deleted columns cover its left anchor', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const drawing = createDrawing({
|
||||
drawingId: 'column-delete-left-anchor',
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 2, rowOffset: 0, columnOffset: 5 },
|
||||
to: { row: 2, column: 6, rowOffset: 5, columnOffset: 5 },
|
||||
},
|
||||
}, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: { [drawing.drawingId]: drawing },
|
||||
});
|
||||
|
||||
controller._moveColInterceptor({
|
||||
range: { startRow: 0, endRow: 9, startColumn: 2, endColumn: 4 },
|
||||
}, 'remove');
|
||||
const updatedDrawing = sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0];
|
||||
|
||||
expect(updatedDrawing).toMatchObject({
|
||||
transform: { left: 20, width: 15 },
|
||||
sheetTransform: {
|
||||
from: { column: 2, columnOffset: 0 },
|
||||
to: { column: 3, columnOffset: 5 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates both-anchored drawings when hidden rows cut through their top edge', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const drawing = createDrawing({
|
||||
drawingId: 'row-hidden-top-edge',
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 5, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 5, columnOffset: 0 },
|
||||
},
|
||||
}, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: { [drawing.drawingId]: drawing },
|
||||
});
|
||||
|
||||
const result = controller._getDrawingUndoForRowVisible(UNIT_ID, SUB_UNIT_ID, [
|
||||
{ startRow: 1, endRow: 2, startColumn: 0, endColumn: 9 },
|
||||
]);
|
||||
const updatedDrawing = sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0];
|
||||
|
||||
expect(updatedDrawing).toMatchObject({
|
||||
drawingId: 'row-hidden-top-edge',
|
||||
transform: { left: 10, top: 20, width: 20, height: 20 },
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 2, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
});
|
||||
expect(result.redos).toEqual([
|
||||
{ id: SetDrawingApplyMutation.id, params: { unitId: UNIT_ID, subUnitId: SUB_UNIT_ID, op: 'update-redo', objects: [updatedDrawing], type: DrawingApplyType.UPDATE } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: [UNIT_ID] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('recalculates drawing transforms after row height changes without changing anchors', () => {
|
||||
const resizedSkeleton = createSkeleton({ rowHeights: [20, 20, 40, 20] });
|
||||
const sheetTransform = {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
};
|
||||
const drawing = createDrawing({
|
||||
drawingId: 'resized-by-row-height',
|
||||
sheetTransform,
|
||||
transform: { left: 10, top: 20, width: 20, height: 40 },
|
||||
}, resizedSkeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton: resizedSkeleton,
|
||||
drawingData: { [drawing.drawingId]: drawing },
|
||||
});
|
||||
|
||||
const result = controller._getDrawingUndoForRowAndColSize(UNIT_ID, SUB_UNIT_ID, [
|
||||
{ startRow: 2, endRow: 2, startColumn: 0, endColumn: 9 },
|
||||
]);
|
||||
const updatedDrawing = sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0];
|
||||
|
||||
expect(updatedDrawing).toMatchObject({
|
||||
drawingId: 'resized-by-row-height',
|
||||
sheetTransform,
|
||||
transform: { left: 10, top: 20, width: 20, height: 60 },
|
||||
});
|
||||
expect(result.undos).toEqual([
|
||||
{ id: SetDrawingApplyMutation.id, params: { unitId: UNIT_ID, subUnitId: SUB_UNIT_ID, op: 'update-undo', objects: [updatedDrawing], type: DrawingApplyType.UPDATE } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: [UNIT_ID] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves only fully contained both-anchored drawings when a range is moved', () => {
|
||||
const skeleton = createSkeleton();
|
||||
const contained = createDrawing({
|
||||
drawingId: 'contained',
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 2, columnOffset: 2 },
|
||||
to: { row: 2, column: 2, rowOffset: 6, columnOffset: 6 },
|
||||
},
|
||||
}, skeleton);
|
||||
const positionOnly = createDrawing({
|
||||
drawingId: 'position-only',
|
||||
anchorType: SheetDrawingAnchorType.Position,
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 2, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
}, skeleton);
|
||||
const partlyOutside = createDrawing({
|
||||
drawingId: 'partly-outside',
|
||||
sheetTransform: {
|
||||
from: { row: 0, column: 0, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
}, skeleton);
|
||||
const { controller, sheetDrawingService } = createController({
|
||||
skeleton,
|
||||
drawingData: {
|
||||
contained,
|
||||
[positionOnly.drawingId]: positionOnly,
|
||||
[partlyOutside.drawingId]: partlyOutside,
|
||||
},
|
||||
});
|
||||
|
||||
const result = controller._moveRangeInterceptor(
|
||||
UNIT_ID,
|
||||
SUB_UNIT_ID,
|
||||
{ startRow: 1, endRow: 2, startColumn: 1, endColumn: 2 },
|
||||
{ startRow: 3, endRow: 4, startColumn: 4, endColumn: 5 }
|
||||
);
|
||||
const updatedDrawing = sheetDrawingService.getBatchUpdateOp.mock.calls[0][0][0];
|
||||
|
||||
expect(sheetDrawingService.getBatchUpdateOp).toHaveBeenCalledTimes(1);
|
||||
expect(sheetDrawingService.getBatchUpdateOp.mock.calls[0][0]).toHaveLength(1);
|
||||
expect(updatedDrawing).toMatchObject({
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: SUB_UNIT_ID,
|
||||
drawingId: 'contained',
|
||||
transform: { left: 42, top: 62, width: 14, height: 24 },
|
||||
sheetTransform: {
|
||||
from: { row: 3, column: 4, rowOffset: 2, columnOffset: 2 },
|
||||
to: { row: 4, column: 5, rowOffset: 6, columnOffset: 6 },
|
||||
},
|
||||
});
|
||||
expect(result.redos).toEqual([
|
||||
{ id: SetDrawingApplyMutation.id, params: { unitId: UNIT_ID, subUnitId: SUB_UNIT_ID, op: 'update-redo', objects: [updatedDrawing], type: DrawingApplyType.UPDATE } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: [UNIT_ID] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* 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 {
|
||||
ISheetDrawingPlacement,
|
||||
} from '../../services/sheet-drawing-placement';
|
||||
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
|
||||
import { Direction, DrawingTypeEnum, ImageSourceType, RANGE_TYPE, RedoCommandId, UndoCommandId } from '@univerjs/core';
|
||||
import {
|
||||
DeleteRangeMoveLeftCommand,
|
||||
DeleteRangeMoveUpCommand,
|
||||
InsertColCommand,
|
||||
InsertRangeMoveDownCommand,
|
||||
InsertRangeMoveRightCommand,
|
||||
InsertRowCommand,
|
||||
MoveColsCommand,
|
||||
MoveRangeCommand,
|
||||
MoveRowsCommand,
|
||||
RemoveColCommand,
|
||||
RemoveRowCommand,
|
||||
SetColHiddenCommand,
|
||||
SetColWidthCommand,
|
||||
SetRowHeightCommand,
|
||||
SetRowHiddenCommand,
|
||||
SetWorksheetRowAutoHeightMutation,
|
||||
SheetSkeletonService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
|
||||
import { drawingPositionToTransform } from '../../basics/transform-position';
|
||||
import { InsertSheetDrawingCommand } from '../../commands/commands/insert-sheet-drawing.command';
|
||||
import { SetSheetDrawingPlacementCommand } from '../../commands/commands/set-sheet-drawing-placement.command';
|
||||
import {
|
||||
applySheetDrawingPlacement,
|
||||
getSheetDrawingPlacement,
|
||||
normalizeSheetDrawingPlacement,
|
||||
} from '../../services/sheet-drawing-placement';
|
||||
import { ISheetDrawingService, SheetDrawingAnchorType } from '../../services/sheet-drawing.service';
|
||||
|
||||
describe('sheet drawing transforms without UI plugins', () => {
|
||||
let testBed: ReturnType<typeof createSheetsDrawingTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
testBed = createSheetsDrawingTestBed();
|
||||
});
|
||||
|
||||
afterEach(() => testBed.univer.dispose());
|
||||
|
||||
async function insertDrawing() {
|
||||
const sheetTransform = {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
};
|
||||
const skeleton = testBed.get(SheetSkeletonService).getSkeletonParam('test', 'sheet1');
|
||||
const drawing: ISheetDrawing = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: 'https://example.com/drawing.png',
|
||||
anchorType: SheetDrawingAnchorType.Both,
|
||||
sheetTransform,
|
||||
axisAlignSheetTransform: sheetTransform,
|
||||
transform: drawingPositionToTransform(sheetTransform, skeleton)!,
|
||||
};
|
||||
expect(await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
})).toBe(true);
|
||||
return testBed.get(ISheetDrawingService);
|
||||
}
|
||||
|
||||
function getDrawing(service: ISheetDrawingService) {
|
||||
return service.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-1' })!;
|
||||
}
|
||||
|
||||
it('inserts rows and restores the exact drawing through undo and redo', async () => {
|
||||
const service = await insertDrawing();
|
||||
const before = structuredClone(getDrawing(service));
|
||||
|
||||
expect(await testBed.commandService.executeCommand(InsertRowCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 },
|
||||
direction: Direction.DOWN,
|
||||
})).toBe(true);
|
||||
const after = structuredClone(getDrawing(service));
|
||||
expect(after.transform?.height).toBeGreaterThan(before.transform?.height ?? 0);
|
||||
|
||||
expect(testBed.commandService.syncExecuteCommand(UndoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(before);
|
||||
expect(testBed.commandService.syncExecuteCommand(RedoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(after);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['insert row', InsertRowCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 },
|
||||
direction: Direction.DOWN,
|
||||
}],
|
||||
['delete row', RemoveRowCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 },
|
||||
}],
|
||||
['insert column', InsertColCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 },
|
||||
direction: Direction.RIGHT,
|
||||
}],
|
||||
['delete column', RemoveColCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 },
|
||||
}],
|
||||
['resize row', SetRowHeightCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
value: 60,
|
||||
ranges: [{ startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 }],
|
||||
}],
|
||||
['resize column', SetColWidthCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
value: 120,
|
||||
ranges: [{ startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 }],
|
||||
}],
|
||||
['hide row', SetRowHiddenCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
ranges: [{ startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 }],
|
||||
}],
|
||||
['hide column', SetColHiddenCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
ranges: [{ startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 }],
|
||||
}],
|
||||
])('%s preserves the three placement contracts in headless mode', async (_name, commandId, params) => {
|
||||
const skeleton = testBed.get(SheetSkeletonService).ensureSkeleton('test', 'sheet1');
|
||||
if (!skeleton) {
|
||||
throw new Error('Expected the Sheet skeleton to exist.');
|
||||
}
|
||||
const base = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: 'https://example.com/anchor.png',
|
||||
sheetTransform: {
|
||||
from: { row: 0, column: 0, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 0, column: 0, rowOffset: 1, columnOffset: 1 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: { row: 0, column: 0, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 0, column: 0, rowOffset: 1, columnOffset: 1 },
|
||||
},
|
||||
transform: { left: 0, top: 0, width: 1, height: 1 },
|
||||
};
|
||||
const placements: ISheetDrawingPlacement[] = [
|
||||
{
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
from: { row: 3, column: 3, rowOffset: 4, columnOffset: 6 },
|
||||
width: 240,
|
||||
height: 120,
|
||||
},
|
||||
{
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
from: { row: 1, column: 1, rowOffset: 4, columnOffset: 6 },
|
||||
to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
{
|
||||
kind: SheetDrawingAnchorType.None,
|
||||
left: 640,
|
||||
top: 96,
|
||||
width: 240,
|
||||
height: 120,
|
||||
},
|
||||
];
|
||||
const drawings = placements.map((placement, index) => applySheetDrawingPlacement({
|
||||
...base,
|
||||
drawingId: `placement-${index}`,
|
||||
}, placement, placement.kind === SheetDrawingAnchorType.None ? undefined : skeleton));
|
||||
expect(await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings,
|
||||
})).toBe(true);
|
||||
|
||||
const service = testBed.get(ISheetDrawingService);
|
||||
const readDrawings = () => drawings.map((drawing) => {
|
||||
const current = service.getDrawingByParam(drawing);
|
||||
if (!current) {
|
||||
throw new Error(`Expected drawing "${drawing.drawingId}" to exist.`);
|
||||
}
|
||||
return structuredClone(current);
|
||||
});
|
||||
const before = readDrawings();
|
||||
|
||||
expect(await testBed.commandService.executeCommand(commandId, params)).toBe(true);
|
||||
const after = readDrawings();
|
||||
expect(after[0].transform).not.toEqual(before[0].transform);
|
||||
expect(after[2].transform).toEqual(before[2].transform);
|
||||
expect(getSheetDrawingPlacement(after[2])).toEqual(getSheetDrawingPlacement(before[2]));
|
||||
expect(after[0].transform?.width).toBe(before[0].transform?.width);
|
||||
expect(after[0].transform?.height).toBe(before[0].transform?.height);
|
||||
if (commandId === SetRowHiddenCommand.id || commandId === SetColHiddenCommand.id) {
|
||||
expect(after[1]).not.toEqual(before[1]);
|
||||
} else {
|
||||
expect(after[1].transform).not.toEqual(before[1].transform);
|
||||
expect([
|
||||
after[1].transform?.width !== before[1].transform?.width,
|
||||
after[1].transform?.height !== before[1].transform?.height,
|
||||
]).toContain(true);
|
||||
}
|
||||
expect(after.map(getSheetDrawingPlacement).map(({ kind }) => kind)).toEqual([
|
||||
SheetDrawingAnchorType.Position,
|
||||
SheetDrawingAnchorType.Both,
|
||||
SheetDrawingAnchorType.None,
|
||||
]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(UndoCommandId)).toBe(true);
|
||||
expect(readDrawings()).toEqual(before);
|
||||
expect(await testBed.commandService.executeCommand(RedoCommandId)).toBe(true);
|
||||
expect(readDrawings()).toEqual(after);
|
||||
});
|
||||
|
||||
it('resizes both-anchored drawings when row height changes', async () => {
|
||||
const service = await insertDrawing();
|
||||
const before = structuredClone(getDrawing(service));
|
||||
|
||||
expect(testBed.commandService.syncExecuteCommand(SetRowHeightCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
ranges: [{ startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 }],
|
||||
value: 40,
|
||||
})).toBe(true);
|
||||
const after = structuredClone(getDrawing(service));
|
||||
expect(after.transform?.height).toBeGreaterThan(before.transform?.height ?? 0);
|
||||
|
||||
expect(testBed.commandService.syncExecuteCommand(UndoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(before);
|
||||
expect(testBed.commandService.syncExecuteCommand(RedoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(after);
|
||||
});
|
||||
|
||||
it('updates placement through a command and round-trips through undo and redo', async () => {
|
||||
const service = await insertDrawing();
|
||||
const before = structuredClone(getDrawing(service));
|
||||
const placement: ISheetDrawingPlacement = {
|
||||
kind: SheetDrawingAnchorType.None,
|
||||
left: 640,
|
||||
top: 96,
|
||||
width: 240,
|
||||
height: 120,
|
||||
};
|
||||
|
||||
expect(await testBed.commandService.executeCommand(SetSheetDrawingPlacementCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ drawingId: 'drawing-1', placement }],
|
||||
})).toBe(true);
|
||||
const after = structuredClone(getDrawing(service));
|
||||
expect(getSheetDrawingPlacement(after)).toEqual(placement);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(UndoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(before);
|
||||
expect(await testBed.commandService.executeCommand(RedoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(after);
|
||||
});
|
||||
|
||||
it('requires a loaded Sheet for bounds inference and preserves rotation state', () => {
|
||||
expect(() => normalizeSheetDrawingPlacement({
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
bounds: { left: 120, top: 80, width: 240, height: 120 },
|
||||
})).toThrow('SHEET_DRAWING_PLACEMENT_SKELETON_REQUIRED');
|
||||
|
||||
const skeleton = testBed.get(SheetSkeletonService).ensureSkeleton('test', 'sheet1');
|
||||
if (!skeleton) {
|
||||
throw new Error('Expected the Sheet skeleton to exist.');
|
||||
}
|
||||
const source: ISheetDrawing = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'rotated-drawing',
|
||||
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
|
||||
transform: {
|
||||
left: 120,
|
||||
top: 80,
|
||||
width: 240,
|
||||
height: 120,
|
||||
angle: 37,
|
||||
flipX: true,
|
||||
flipY: false,
|
||||
skewX: 4,
|
||||
skewY: 2,
|
||||
},
|
||||
sheetTransform: {
|
||||
from: { row: 2, column: 2, rowOffset: 4, columnOffset: 6 },
|
||||
to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
|
||||
angle: 37,
|
||||
flipX: true,
|
||||
flipY: false,
|
||||
skewX: 4,
|
||||
skewY: 2,
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: { row: 2, column: 2, rowOffset: 4, columnOffset: 6 },
|
||||
to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
const placements: ISheetDrawingPlacement[] = [
|
||||
{
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
from: { row: 3, column: 3, rowOffset: 8, columnOffset: 8 },
|
||||
width: 320,
|
||||
height: 180,
|
||||
},
|
||||
{
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
from: { row: 3, column: 3, rowOffset: 8, columnOffset: 8 },
|
||||
to: { row: 10, column: 8, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
{
|
||||
kind: SheetDrawingAnchorType.None,
|
||||
left: 640,
|
||||
top: 96,
|
||||
width: 320,
|
||||
height: 180,
|
||||
},
|
||||
];
|
||||
|
||||
for (const placement of placements) {
|
||||
const result = applySheetDrawingPlacement(
|
||||
source,
|
||||
placement,
|
||||
placement.kind === SheetDrawingAnchorType.None ? undefined : skeleton
|
||||
);
|
||||
expect(result.transform).toEqual(expect.objectContaining({
|
||||
angle: 37,
|
||||
flipX: true,
|
||||
flipY: false,
|
||||
skewX: 4,
|
||||
skewY: 2,
|
||||
}));
|
||||
expect(result.sheetTransform).toEqual(expect.objectContaining({
|
||||
angle: 37,
|
||||
flipX: true,
|
||||
flipY: false,
|
||||
skewX: 4,
|
||||
skewY: 2,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
['remove row', RemoveRowCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 },
|
||||
}],
|
||||
['insert column', InsertColCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
direction: Direction.RIGHT,
|
||||
range: { startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 },
|
||||
}],
|
||||
['remove column', RemoveColCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 },
|
||||
}],
|
||||
['insert cells down', InsertRangeMoveDownCommand.id, {
|
||||
range: { startRow: 2, endRow: 2, startColumn: 1, endColumn: 3 },
|
||||
}],
|
||||
['insert cells right', InsertRangeMoveRightCommand.id, {
|
||||
range: { startRow: 1, endRow: 4, startColumn: 2, endColumn: 2 },
|
||||
}],
|
||||
['delete cells up', DeleteRangeMoveUpCommand.id, {
|
||||
range: { startRow: 2, endRow: 2, startColumn: 1, endColumn: 3 },
|
||||
}],
|
||||
['delete cells left', DeleteRangeMoveLeftCommand.id, {
|
||||
range: { startRow: 1, endRow: 4, startColumn: 2, endColumn: 2 },
|
||||
}],
|
||||
['set column width', SetColWidthCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
value: 120,
|
||||
ranges: [{ startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 }],
|
||||
}],
|
||||
['hide row', SetRowHiddenCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
ranges: [{ startRow: 2, endRow: 2, startColumn: 0, endColumn: 19 }],
|
||||
}],
|
||||
['hide column', SetColHiddenCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
ranges: [{ startRow: 0, endRow: 19, startColumn: 2, endColumn: 2 }],
|
||||
}],
|
||||
['move rows', MoveRowsCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 1, endRow: 4, startColumn: 0, endColumn: 19, rangeType: RANGE_TYPE.ROW },
|
||||
fromRange: { startRow: 1, endRow: 4, startColumn: 0, endColumn: 19 },
|
||||
toRange: { startRow: 8, endRow: 11, startColumn: 0, endColumn: 19 },
|
||||
}],
|
||||
['move columns', MoveColsCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
range: { startRow: 0, endRow: 19, startColumn: 1, endColumn: 3, rangeType: RANGE_TYPE.COLUMN },
|
||||
fromRange: { startRow: 0, endRow: 19, startColumn: 1, endColumn: 3 },
|
||||
toRange: { startRow: 0, endRow: 19, startColumn: 7, endColumn: 9 },
|
||||
}],
|
||||
['move range', MoveRangeCommand.id, {
|
||||
fromUnitId: 'test',
|
||||
fromSubUnitId: 'sheet1',
|
||||
toUnitId: 'test',
|
||||
toSubUnitId: 'sheet1',
|
||||
fromRange: { startRow: 1, endRow: 4, startColumn: 1, endColumn: 3 },
|
||||
toRange: { startRow: 8, endRow: 11, startColumn: 7, endColumn: 9 },
|
||||
}],
|
||||
])('%s changes drawing geometry and round-trips exactly', async (_name, commandId, params) => {
|
||||
const service = await insertDrawing();
|
||||
const before = structuredClone(getDrawing(service));
|
||||
|
||||
expect(await testBed.commandService.executeCommand(commandId, params)).toBe(true);
|
||||
const after = structuredClone(getDrawing(service));
|
||||
expect(after).not.toEqual(before);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(UndoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(before);
|
||||
expect(await testBed.commandService.executeCommand(RedoCommandId)).toBe(true);
|
||||
expect(getDrawing(service)).toEqual(after);
|
||||
});
|
||||
|
||||
it('refreshes direct auto-height mutations and their inverse without UI', async () => {
|
||||
const service = await insertDrawing();
|
||||
const before = structuredClone(getDrawing(service));
|
||||
const worksheet = testBed.workbook.getSheetBySheetId('sheet1')!;
|
||||
const originalHeight = worksheet.getRowHeight(2);
|
||||
|
||||
expect(testBed.commandService.syncExecuteCommand(SetWorksheetRowAutoHeightMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
rowsAutoHeightInfo: [{ row: 2, autoHeight: originalHeight + 30 }],
|
||||
})).toBe(true);
|
||||
await Promise.resolve();
|
||||
expect(getDrawing(service).transform?.height).toBeGreaterThan(before.transform?.height ?? 0);
|
||||
|
||||
expect(testBed.commandService.syncExecuteCommand(SetWorksheetRowAutoHeightMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
rowsAutoHeightInfo: [{ row: 2, autoHeight: originalHeight }],
|
||||
})).toBe(true);
|
||||
await Promise.resolve();
|
||||
expect(getDrawing(service)).toEqual(before);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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 { Workbook } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1, IDrawingSubunitMap } from '@univerjs/drawing';
|
||||
import type { ICopySheetCommandInterceptorParams, IRemoveSheetCommandParams } from '@univerjs/sheets';
|
||||
import type { ISheetDrawing } from '../services/sheet-drawing.service';
|
||||
import { Disposable, ICommandService, Inject, IResourceManagerService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { getOrCreateDrawingCopyPlan, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { CopySheetCommand, RemoveSheetCommand, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { InsertSheetDrawingCommand } from '../commands/commands/insert-sheet-drawing.command';
|
||||
import { RemoveSheetDrawingCommand } from '../commands/commands/remove-sheet-drawing.command';
|
||||
import { SetDrawingArrangeCommand } from '../commands/commands/set-drawing-arrange.command';
|
||||
import { SetSheetDrawingPlacementCommand } from '../commands/commands/set-sheet-drawing-placement.command';
|
||||
import { SetSheetDrawingCommand } from '../commands/commands/set-sheet-drawing.command';
|
||||
import { DrawingApplyType, SetDrawingApplyMutation } from '../commands/mutations/set-drawing-apply.mutation';
|
||||
import { ClearSheetDrawingTransformerOperation } from '../commands/operations/clear-drawing-transformer.operation';
|
||||
import { ISheetDrawingService } from '../services/sheet-drawing.service';
|
||||
|
||||
export const SHEET_DRAWING_PLUGIN = 'SHEET_DRAWING_PLUGIN';
|
||||
|
||||
function getDrawingsInOrder(drawingData: Record<string, ISheetDrawing>, drawingOrder: string[]): ISheetDrawing[] {
|
||||
const visited = new Set<string>();
|
||||
const drawings: ISheetDrawing[] = [];
|
||||
|
||||
drawingOrder.forEach((drawingId) => {
|
||||
const drawing = drawingData[drawingId];
|
||||
if (drawing) {
|
||||
visited.add(drawingId);
|
||||
drawings.push(drawing);
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(drawingData).forEach((drawing) => {
|
||||
if (!visited.has(drawing.drawingId)) {
|
||||
drawings.push(drawing);
|
||||
}
|
||||
});
|
||||
|
||||
return drawings;
|
||||
}
|
||||
|
||||
export class SheetsDrawingLoadController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(IUniverInstanceService) private _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ISheetDrawingService private readonly _sheetDrawingService: ISheetDrawingService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IResourceManagerService private _resourceManagerService: IResourceManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initSnapshot();
|
||||
this._initSheetChange();
|
||||
|
||||
this.disposeWithMe(this._commandService.registerCommand(SetDrawingApplyMutation));
|
||||
}
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
SetSheetDrawingCommand,
|
||||
InsertSheetDrawingCommand,
|
||||
RemoveSheetDrawingCommand,
|
||||
SetDrawingArrangeCommand,
|
||||
SetSheetDrawingPlacementCommand,
|
||||
ClearSheetDrawingTransformerOperation,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
private _initSnapshot() {
|
||||
const toJson = (unitId: string, model?: IDrawingSubunitMap<ISheetDrawing>) => {
|
||||
const map = model || this._sheetDrawingService.getDrawingDataForUnit(unitId);
|
||||
if (map) {
|
||||
return JSON.stringify(map);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const parseJson = (json: string): IDrawingSubunitMap<ISheetDrawing> => {
|
||||
if (!json) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
this.disposeWithMe(
|
||||
this._resourceManagerService.registerPluginResource<IDrawingSubunitMap<ISheetDrawing>>({
|
||||
pluginName: SHEET_DRAWING_PLUGIN,
|
||||
businesses: [UniverInstanceType.UNIVER_SHEET],
|
||||
toJson: (unitId, model) => toJson(unitId, model),
|
||||
parseJson: (json) => parseJson(json),
|
||||
onUnLoad: (unitId) => {
|
||||
this._sheetDrawingService.removeDrawingDataForUnit(unitId);
|
||||
this._drawingManagerService.removeDrawingDataForUnit(unitId);
|
||||
},
|
||||
onLoad: (unitId, value) => {
|
||||
this._sheetDrawingService.registerDrawingData(unitId, value);
|
||||
this._drawingManagerService.registerDrawingData(unitId, value);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initSheetChange() {
|
||||
this.disposeWithMe(
|
||||
this._sheetInterceptorService.interceptCommand({
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
getMutations: (commandInfo) => {
|
||||
if (commandInfo.id === RemoveSheetCommand.id) {
|
||||
const params = commandInfo.params as IRemoveSheetCommandParams;
|
||||
const unitId = params.unitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getUnitId();
|
||||
const subUnitId = params.subUnitId || this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!.getActiveSheet()?.getSheetId();
|
||||
|
||||
if (!unitId || !subUnitId) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawings = Object.values(drawingData);
|
||||
|
||||
if (drawings.length === 0) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const jsonOp = this._sheetDrawingService.getBatchRemoveOp(drawings) as IDrawingJsonUndo1;
|
||||
const { unitId: jsonOpUnitId, subUnitId: jsonOpSubUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
if (Array.isArray(objects) && objects.length === 0) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
redos: [
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
op: redo,
|
||||
unitId: jsonOpUnitId,
|
||||
subUnitId: jsonOpSubUnitId,
|
||||
objects,
|
||||
type: DrawingApplyType.REMOVE,
|
||||
},
|
||||
},
|
||||
],
|
||||
undos: [
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
op: undo,
|
||||
unitId: jsonOpUnitId,
|
||||
subUnitId: jsonOpSubUnitId,
|
||||
objects,
|
||||
type: DrawingApplyType.INSERT,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (commandInfo.id === CopySheetCommand.id) {
|
||||
const params = commandInfo.params as ICopySheetCommandInterceptorParams;
|
||||
const { unitId, subUnitId, targetSubUnitId, copyContext } = params;
|
||||
|
||||
if (!unitId || !subUnitId || !targetSubUnitId) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const sourceDrawings = getDrawingsInOrder(drawingData, this._sheetDrawingService.getDrawingOrder(unitId, subUnitId));
|
||||
const copyPlan = getOrCreateDrawingCopyPlan(copyContext, sourceDrawings, {
|
||||
unitId,
|
||||
sourceSubUnitId: subUnitId,
|
||||
targetSubUnitId,
|
||||
});
|
||||
const drawings = copyPlan.drawings;
|
||||
|
||||
if (drawings.length === 0) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const jsonOp = this._sheetDrawingService.getBatchAddOp(drawings) as IDrawingJsonUndo1;
|
||||
const { unitId: jsonOpUnitId, subUnitId: jsonOpSubUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
return {
|
||||
redos: [
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
op: redo,
|
||||
unitId: jsonOpUnitId,
|
||||
subUnitId: jsonOpSubUnitId,
|
||||
objects,
|
||||
type: DrawingApplyType.INSERT,
|
||||
},
|
||||
},
|
||||
],
|
||||
undos: [
|
||||
{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
op: undo,
|
||||
unitId: jsonOpUnitId,
|
||||
subUnitId: jsonOpSubUnitId,
|
||||
objects,
|
||||
type: DrawingApplyType.REMOVE,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { redos: [], undos: [] };
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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 { Injector, Univer } from '@univerjs/core';
|
||||
import type { ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import { DrawingTypeEnum, ICommandService, ImageSourceType } from '@univerjs/core';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { ISheetDrawingService } from '@univerjs/sheets-drawing';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
|
||||
import { InsertSheetDrawingCommand } from '../../commands/commands/insert-sheet-drawing.command';
|
||||
import { FOverGridImage } from '../f-over-grid-image';
|
||||
|
||||
describe('FOverGridImage', () => {
|
||||
let univer: Univer;
|
||||
let injector: Injector;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createSheetsDrawingTestBed();
|
||||
univer = testBed.univer;
|
||||
injector = testBed.injector;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('creates a builder from the image unit and sheet ids', () => {
|
||||
const image: ISheetImage = {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: 'https://example.com/drawing-1.png',
|
||||
sheetTransform: {
|
||||
from: {
|
||||
row: 1,
|
||||
rowOffset: 0,
|
||||
column: 1,
|
||||
columnOffset: 0,
|
||||
},
|
||||
to: {
|
||||
row: 4,
|
||||
rowOffset: 0,
|
||||
column: 3,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: {
|
||||
row: 1,
|
||||
rowOffset: 0,
|
||||
column: 1,
|
||||
columnOffset: 0,
|
||||
},
|
||||
to: {
|
||||
row: 4,
|
||||
rowOffset: 0,
|
||||
column: 3,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
injector.get(SheetSkeletonService).ensureSkeleton('test', 'sheet1');
|
||||
|
||||
const builder = injector.createInstance(FOverGridImage, image).toBuilder();
|
||||
|
||||
expect(builder.getSource()).toBe('https://example.com/drawing-1.png');
|
||||
expect(builder.getSourceType()).toBe(ImageSourceType.URL);
|
||||
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('FOverGridImageBuilder but get 0'));
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('updates image data and removes the image through facade methods', async () => {
|
||||
const image = createImage();
|
||||
const commandService = injector.get(ICommandService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings: [image] })).toBe(true);
|
||||
|
||||
const fImage = injector.createInstance(FOverGridImage, image);
|
||||
|
||||
expect(fImage.setSource('https://example.com/updated.png', ImageSourceType.URL)).toBe(true);
|
||||
expect(getStoredImage('drawing-1').source).toBe('https://example.com/updated.png');
|
||||
|
||||
expect(await fImage.setPositionAsync(4, 5, 6, 7)).toBe(true);
|
||||
expect(getStoredImage('drawing-1').sheetTransform.from).toEqual(expect.objectContaining({
|
||||
row: 4,
|
||||
column: 5,
|
||||
rowOffset: 6,
|
||||
columnOffset: 7,
|
||||
}));
|
||||
|
||||
expect(await fImage.setSizeAsync(160, 90)).toBe(true);
|
||||
expect(getStoredImage('drawing-1').transform).toEqual(expect.objectContaining({
|
||||
width: 160,
|
||||
height: 90,
|
||||
}));
|
||||
|
||||
expect(fImage.setCrop(1, 2, 3, 4)).toBe(true);
|
||||
expect(getStoredImage('drawing-1').srcRect).toEqual({ top: 1, left: 2, bottom: 3, right: 4 });
|
||||
|
||||
expect(fImage.setRotate(30)).toBe(true);
|
||||
expect(getStoredImage('drawing-1').sheetTransform.angle).toBe(30);
|
||||
|
||||
expect(fImage.setForward()).toBe(true);
|
||||
expect(fImage.setBackward()).toBe(true);
|
||||
expect(fImage.setFront()).toBe(true);
|
||||
expect(fImage.setBack()).toBe(true);
|
||||
|
||||
expect(fImage.remove()).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'drawing-1' })).toBeUndefined();
|
||||
});
|
||||
|
||||
function getStoredImage(drawingId: string): ISheetImage {
|
||||
const image = injector.get(ISheetDrawingService).getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
}) as ISheetImage | undefined;
|
||||
|
||||
if (!image) {
|
||||
throw new Error(`Image ${drawingId} was not stored`);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
});
|
||||
|
||||
function createImage(): ISheetImage {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: 'https://example.com/drawing-1.png',
|
||||
transform: {
|
||||
left: 20,
|
||||
top: 30,
|
||||
width: 100,
|
||||
height: 60,
|
||||
},
|
||||
sheetTransform: {
|
||||
from: {
|
||||
row: 1,
|
||||
rowOffset: 0,
|
||||
column: 1,
|
||||
columnOffset: 0,
|
||||
},
|
||||
to: {
|
||||
row: 4,
|
||||
rowOffset: 0,
|
||||
column: 3,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: {
|
||||
row: 1,
|
||||
rowOffset: 0,
|
||||
column: 1,
|
||||
columnOffset: 0,
|
||||
},
|
||||
to: {
|
||||
row: 4,
|
||||
rowOffset: 0,
|
||||
column: 3,
|
||||
columnOffset: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
143
packages/sheets-drawing/src/facade/__tests__/f-univer.spec.ts
Normal file
143
packages/sheets-drawing/src/facade/__tests__/f-univer.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 { Injector, Univer } from '@univerjs/core';
|
||||
import type { ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import { ISheetDrawingService } from '@univerjs/sheets-drawing';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '../index';
|
||||
|
||||
describe('sheets drawing facade image lifecycle', () => {
|
||||
let univer: Univer;
|
||||
let injector: Injector;
|
||||
let univerAPI: FUniver;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createSheetsDrawingTestBed();
|
||||
univer = testBed.univer;
|
||||
injector = testBed.injector;
|
||||
univerAPI = FUniver.newAPI(injector);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps the sheet unchanged when a before-insert listener cancels the image', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const disposable = univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageInsert, (event) => {
|
||||
if (event.insertImageParams[0].source.includes('blocked')) {
|
||||
event.cancel = true;
|
||||
}
|
||||
});
|
||||
const blocked = await worksheet.newOverGridImage()
|
||||
.setSource('https://example.com/blocked.png', univerAPI.Enum.ImageSourceType.URL)
|
||||
.setWidth(40)
|
||||
.setHeight(40)
|
||||
.buildAsync();
|
||||
|
||||
expect(worksheet.insertImages([blocked])).toBe(worksheet);
|
||||
expect(worksheet.getImageById(blocked.drawingId)).toBeNull();
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
it('emits image lifecycle events around insert, change, select, and remove operations', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const commandService = injector.get(ICommandService);
|
||||
const events: string[] = [];
|
||||
const disposables = [
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageInsert, (event) => {
|
||||
events.push(`before-insert:${event.insertImageParams[0].source}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.OverGridImageInserted, (event) => {
|
||||
events.push(`inserted:${event.images[0].getId()}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageChange, (event) => {
|
||||
events.push(`before-change:${event.images[0].changeParam.source}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.OverGridImageChanged, (event) => {
|
||||
events.push(`changed:${event.images[0].toBuilder().getSource()}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageSelect, (event) => {
|
||||
events.push(`before-select:${event.selectedImages[0].getId()}:${event.oldSelectedImages.length}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.OverGridImageSelected, (event) => {
|
||||
events.push(`selected:${event.selectedImages[0].getId()}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageRemove, (event) => {
|
||||
events.push(`before-remove:${event.images[0].getId()}`);
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.OverGridImageRemoved, (event) => {
|
||||
events.push(`removed:${event.removeImageParams[0].drawingId}`);
|
||||
}),
|
||||
];
|
||||
const image = await worksheet.newOverGridImage()
|
||||
.setSource('https://example.com/image.png', univerAPI.Enum.ImageSourceType.URL)
|
||||
.setWidth(40)
|
||||
.setHeight(30)
|
||||
.buildAsync();
|
||||
|
||||
expect(worksheet.insertImages([image])).toBe(worksheet);
|
||||
expect(getStoredImage(injector, image.drawingId).source).toBe('https://example.com/image.png');
|
||||
|
||||
const imageFacade = worksheet.getImageById(image.drawingId)!;
|
||||
expect(imageFacade.setSource('https://example.com/updated.png', univerAPI.Enum.ImageSourceType.URL)).toBe(true);
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetDrawingSelectedOperation.id, [{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: image.drawingId,
|
||||
drawingType: image.drawingType,
|
||||
}])).toBe(true);
|
||||
|
||||
expect(imageFacade.remove()).toBe(true);
|
||||
|
||||
expect(events).toEqual([
|
||||
'before-insert:https://example.com/image.png',
|
||||
`inserted:${image.drawingId}`,
|
||||
'before-change:https://example.com/updated.png',
|
||||
'changed:https://example.com/updated.png',
|
||||
`before-select:${image.drawingId}:0`,
|
||||
`selected:${image.drawingId}`,
|
||||
`before-remove:${image.drawingId}`,
|
||||
`removed:${image.drawingId}`,
|
||||
]);
|
||||
|
||||
for (const disposable of disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getStoredImage(injector: Injector, drawingId: string): ISheetImage {
|
||||
const image = injector.get(ISheetDrawingService).getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
}) as ISheetImage | undefined;
|
||||
|
||||
if (!image) {
|
||||
throw new Error(`Image ${drawingId} was not stored`);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
421
packages/sheets-drawing/src/facade/__tests__/f-worksheet.spec.ts
Normal file
421
packages/sheets-drawing/src/facade/__tests__/f-worksheet.spec.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* 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 {
|
||||
Injector,
|
||||
Workbook,
|
||||
} from '@univerjs/core';
|
||||
import type { FWorkbook } from '@univerjs/sheets/facade';
|
||||
import type { ISheetDrawingPlacement } from '../../services/sheet-drawing-placement';
|
||||
import type { ISheetDrawing } from '../../services/sheet-drawing.service';
|
||||
import {
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
ImageSourceType,
|
||||
IUniverInstanceService,
|
||||
RedoCommand,
|
||||
UndoCommand,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { SheetSkeletonService, UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createSheetsDrawingTestBed } from '../../__tests__/create-sheets-drawing-test-bed';
|
||||
import { InsertSheetDrawingCommand } from '../../commands/commands/insert-sheet-drawing.command';
|
||||
import { resolveSheetDrawingRotateEnabled } from '../../common/rotate-enabled';
|
||||
import { getSheetDrawingPlacement } from '../../services/sheet-drawing-placement';
|
||||
import { ISheetDrawingService, SheetDrawingAnchorType } from '../../services/sheet-drawing.service';
|
||||
import { FWorksheetDrawingMixin } from '../f-worksheet';
|
||||
|
||||
describe('FWorksheetDrawingMixin group drawings', () => {
|
||||
let univer: Univer;
|
||||
let injector: Injector;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createSheetsDrawingTestBed();
|
||||
univer = testBed.univer;
|
||||
injector = testBed.injector;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('uses non-drawing worksheet APIs before drawing services are registered', () => {
|
||||
univer.dispose();
|
||||
univer = new Univer();
|
||||
univer.registerPlugin(UniverSheetsPlugin);
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
|
||||
id: 'test',
|
||||
sheetOrder: ['sheet1'],
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(createFacade(univer.__getInjector()).getSheetName()).toBe('Sheet1');
|
||||
});
|
||||
|
||||
it('groups drawings and exposes parent and child relationships', () => {
|
||||
const drawings = [
|
||||
createDrawing('drawing-1', 10),
|
||||
createDrawing('drawing-2', 80),
|
||||
];
|
||||
const commandService = injector.get(ICommandService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
|
||||
|
||||
const groupId = fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1');
|
||||
const groupDrawing = sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' });
|
||||
|
||||
expect(groupId).toBe('group-1');
|
||||
expect(groupDrawing?.drawingType).toBe(DrawingTypeEnum.DRAWING_GROUP);
|
||||
expect(Object.prototype.hasOwnProperty.call(groupDrawing?.transform ?? {}, 'rotateEnabled')).toBe(false);
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
|
||||
expect(fWorksheet.getDrawingParentGroup('drawing-1')?.drawingId).toBe('group-1');
|
||||
expect(fWorksheet.getDrawingGroupChildren('group-1').map((drawing: ISheetDrawing) => drawing.drawingId))
|
||||
.toEqual(['drawing-1', 'drawing-2']);
|
||||
});
|
||||
|
||||
it('groups chart drawings through the facade and disables group rotation', () => {
|
||||
const drawings = [
|
||||
createDrawing('drawing-1', 10),
|
||||
createDrawing('chart-1', 80, DrawingTypeEnum.DRAWING_CHART),
|
||||
];
|
||||
const commandService = injector.get(ICommandService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
|
||||
|
||||
const groupId = fWorksheet.groupDrawings(['drawing-1', 'chart-1'], 'group-1');
|
||||
const groupDrawing = sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' });
|
||||
|
||||
expect(groupId).toBe('group-1');
|
||||
expect(groupDrawing?.drawingType).toBe(DrawingTypeEnum.DRAWING_GROUP);
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
|
||||
expect(fWorksheet.isDrawingGrouped('chart-1')).toBe(true);
|
||||
expect(fWorksheet.getDrawingGroupChildren('group-1').map((drawing: ISheetDrawing) => drawing.drawingId).sort())
|
||||
.toEqual(['chart-1', 'drawing-1']);
|
||||
expect(resolveSheetDrawingRotateEnabled(groupDrawing!, sheetDrawingService)).toBe(false);
|
||||
});
|
||||
|
||||
it('supports undo and redo for group and ungroup facade operations', async () => {
|
||||
const drawings = [
|
||||
createDrawing('drawing-1', 10),
|
||||
createDrawing('drawing-2', 80),
|
||||
];
|
||||
const commandService = injector.get(ICommandService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
|
||||
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1')).toBe('group-1');
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })).toBeUndefined();
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
|
||||
|
||||
expect(await commandService.executeCommand(RedoCommand.id)).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })?.drawingType)
|
||||
.toBe(DrawingTypeEnum.DRAWING_GROUP);
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
|
||||
|
||||
expect(fWorksheet.ungroupDrawings(['group-1'])).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })).toBeUndefined();
|
||||
|
||||
expect(await commandService.executeCommand(UndoCommand.id)).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })?.drawingType)
|
||||
.toBe(DrawingTypeEnum.DRAWING_GROUP);
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not group with duplicate drawing ids or an existing group id', () => {
|
||||
const drawings = [
|
||||
createDrawing('drawing-1', 10),
|
||||
createDrawing('drawing-2', 80),
|
||||
];
|
||||
const commandService = injector.get(ICommandService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
|
||||
|
||||
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-1'], 'group-1')).toBeNull();
|
||||
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'drawing-1')).toBeNull();
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-2')).toBe(false);
|
||||
});
|
||||
|
||||
it('ungroups drawings and clears parent relationships', () => {
|
||||
const drawings = [
|
||||
createDrawing('drawing-1', 10),
|
||||
createDrawing('drawing-2', 80),
|
||||
];
|
||||
const commandService = injector.get(ICommandService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
|
||||
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1')).toBe('group-1');
|
||||
|
||||
expect(fWorksheet.ungroupDrawings(['group-1'])).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'group-1' })).toBeUndefined();
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-1')).toBe(false);
|
||||
expect(fWorksheet.isDrawingGrouped('drawing-2')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns nested group children when recursive is enabled', () => {
|
||||
const drawings = [
|
||||
createDrawing('drawing-1', 10),
|
||||
createDrawing('drawing-2', 80),
|
||||
createDrawing('drawing-3', 150),
|
||||
];
|
||||
const commandService = injector.get(ICommandService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings })).toBe(true);
|
||||
expect(fWorksheet.groupDrawings(['drawing-1', 'drawing-2'], 'group-1')).toBe('group-1');
|
||||
expect(fWorksheet.groupDrawings(['group-1', 'drawing-3'], 'group-2')).toBe('group-2');
|
||||
|
||||
expect(fWorksheet.getDrawingGroupChildren('group-2').map((drawing: ISheetDrawing) => drawing.drawingId))
|
||||
.toEqual(['drawing-3', 'group-1']);
|
||||
const recursiveChildren = fWorksheet.getDrawingGroupChildren('group-2', true).map((drawing: ISheetDrawing) => drawing.drawingId);
|
||||
expect(recursiveChildren).toHaveLength(4);
|
||||
expect(recursiveChildren).toEqual(expect.arrayContaining(['drawing-1', 'drawing-2', 'drawing-3', 'group-1']));
|
||||
});
|
||||
|
||||
it('inserts, reads, updates, and deletes over-grid images through the worksheet facade', async () => {
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
const image = await fWorksheet.newOverGridImage()
|
||||
.setSource('https://example.com/image.png', ImageSourceType.URL)
|
||||
.setColumn(2)
|
||||
.setRow(3)
|
||||
.setColumnOffset(4)
|
||||
.setRowOffset(5)
|
||||
.setWidth(120)
|
||||
.setHeight(80)
|
||||
.setCropTop(1)
|
||||
.setCropLeft(2)
|
||||
.setCropBottom(3)
|
||||
.setCropRight(4)
|
||||
.setRotate(15)
|
||||
.buildAsync();
|
||||
|
||||
expect(fWorksheet.insertImages([image])).toBe(fWorksheet);
|
||||
|
||||
const images = fWorksheet.getImages();
|
||||
expect(images).toHaveLength(1);
|
||||
expect(images[0].getId()).toBe(image.drawingId);
|
||||
expect(fWorksheet.getImageById(image.drawingId)?.toBuilder().getSource()).toBe('https://example.com/image.png');
|
||||
|
||||
const updatedImage = {
|
||||
...image,
|
||||
source: 'https://example.com/updated.png',
|
||||
};
|
||||
expect(fWorksheet.updateImages([updatedImage])).toBe(fWorksheet);
|
||||
expect((sheetDrawingService.getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: image.drawingId,
|
||||
}) as { source: string }).source).toBe('https://example.com/updated.png');
|
||||
|
||||
expect(fWorksheet.deleteImages(images)).toBe(fWorksheet);
|
||||
expect(fWorksheet.getImageById(image.drawingId)).toBeNull();
|
||||
expect(fWorksheet.getImages()).toEqual([]);
|
||||
});
|
||||
|
||||
it('builds and round-trips an Absolute image placement without a Sheet skeleton', async () => {
|
||||
const fWorksheet = createFacade(injector);
|
||||
const ensureSkeleton = vi.spyOn(injector.get(SheetSkeletonService), 'ensureSkeleton')
|
||||
.mockImplementation(() => {
|
||||
throw new Error('SKELETON_MUST_NOT_BE_READ');
|
||||
});
|
||||
const placement: ISheetDrawingPlacement = {
|
||||
kind: SheetDrawingAnchorType.None,
|
||||
left: 640,
|
||||
top: 96,
|
||||
width: 320,
|
||||
height: 180,
|
||||
};
|
||||
|
||||
const image = await fWorksheet.newOverGridImage()
|
||||
.setSource('https://example.com/absolute.png', ImageSourceType.URL)
|
||||
.setPlacement(placement)
|
||||
.buildAsync();
|
||||
|
||||
expect(ensureSkeleton).not.toHaveBeenCalled();
|
||||
expect(image.transform).toEqual(expect.objectContaining({
|
||||
left: placement.left,
|
||||
top: placement.top,
|
||||
width: placement.width,
|
||||
height: placement.height,
|
||||
}));
|
||||
|
||||
fWorksheet.insertImages([image]);
|
||||
expect(fWorksheet.getDrawingPlacement(image.drawingId)).toEqual(placement);
|
||||
expect(fWorksheet.getImageById(image.drawingId)?.getPlacement()).toEqual(placement);
|
||||
|
||||
const moved: ISheetDrawingPlacement = {
|
||||
...placement,
|
||||
left: 720,
|
||||
top: 128,
|
||||
};
|
||||
expect(fWorksheet.setDrawingPlacement(image.drawingId, moved)).toBe(true);
|
||||
expect(fWorksheet.getDrawingPlacement(image.drawingId)).toEqual(moved);
|
||||
});
|
||||
|
||||
it('infers Position and Both markers from model bounds and returns the headless host layout', () => {
|
||||
const fWorksheet = createFacade(injector);
|
||||
const commandService = injector.get(ICommandService);
|
||||
const drawing = createDrawing('layout-drawing', 120);
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
})).toBe(true);
|
||||
|
||||
const bounds = { left: 120, top: 80, width: 320, height: 160 };
|
||||
const position = fWorksheet.resolveDrawingPlacement({
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
bounds,
|
||||
});
|
||||
const both = fWorksheet.resolveDrawingPlacement({
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
bounds,
|
||||
});
|
||||
|
||||
expect(position).toEqual(expect.objectContaining({
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
}));
|
||||
expect(position).toHaveProperty('from');
|
||||
expect(both).toEqual(expect.objectContaining({
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
}));
|
||||
expect(both).toHaveProperty('from');
|
||||
expect(both).toHaveProperty('to');
|
||||
|
||||
expect(fWorksheet.setDrawingPlacement(drawing.drawingId, {
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
bounds,
|
||||
})).toBe(true);
|
||||
expect(fWorksheet.getDrawingPlacement(drawing.drawingId)).toEqual(both);
|
||||
|
||||
const layout = fWorksheet.getDrawingLayout();
|
||||
expect(layout.gridBounds.width).toBeGreaterThan(0);
|
||||
expect(layout.gridBounds.height).toBeGreaterThan(0);
|
||||
expect(layout.dataBounds.width).toBeGreaterThan(0);
|
||||
expect(layout.dataBounds.height).toBeGreaterThan(0);
|
||||
expect(layout.drawings).toEqual([
|
||||
expect.objectContaining({
|
||||
drawingId: drawing.drawingId,
|
||||
bounds,
|
||||
placement: both,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects invalid bounds and reversed TwoCell markers without mutating the drawing', () => {
|
||||
const fWorksheet = createFacade(injector);
|
||||
const commandService = injector.get(ICommandService);
|
||||
const drawing = createDrawing('validated-drawing', 120);
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [drawing],
|
||||
})).toBe(true);
|
||||
|
||||
expect(() => fWorksheet.setDrawingPlacement(drawing.drawingId, {
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
bounds: { left: 120, top: 80, width: 0, height: 160 },
|
||||
})).toThrow('SHEET_DRAWING_PLACEMENT_EXTENT_INVALID');
|
||||
expect(() => fWorksheet.setDrawingPlacement(drawing.drawingId, {
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
from: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 2, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
})).toThrow('SHEET_DRAWING_PLACEMENT_EXTENT_INVALID');
|
||||
expect(fWorksheet.getDrawingPlacement(drawing.drawingId)).toEqual(getSheetDrawingPlacement(drawing));
|
||||
});
|
||||
|
||||
it('returns only images from sheet drawing data and exposes active images from the drawing selection', () => {
|
||||
const commandService = injector.get(ICommandService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
const image = createDrawing('image-1', 10, DrawingTypeEnum.DRAWING_IMAGE);
|
||||
const shape = createDrawing('shape-1', 80, DrawingTypeEnum.DRAWING_SHAPE);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings: [image, shape] })).toBe(true);
|
||||
expect(fWorksheet.getImages().map((drawing) => drawing.getId())).toEqual(['image-1']);
|
||||
|
||||
injector.get(ISheetDrawingService).focusDrawing([{
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'image-1',
|
||||
}]);
|
||||
|
||||
expect(fWorksheet.getActiveImages().map((drawing) => drawing.getId())).toEqual(['image-1']);
|
||||
});
|
||||
|
||||
it('keeps grouping APIs inert when requested drawings or groups do not exist', () => {
|
||||
const commandService = injector.get(ICommandService);
|
||||
const fWorksheet = createFacade(injector);
|
||||
const image = createDrawing('drawing-1', 10);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: 'test', drawings: [image] })).toBe(true);
|
||||
|
||||
expect(fWorksheet.groupDrawings(['drawing-1', 'missing-drawing'], 'group-1')).toBeNull();
|
||||
expect(fWorksheet.ungroupDrawings(['missing-group'])).toBe(false);
|
||||
expect(fWorksheet.getDrawingGroupChildren('missing-group', true)).toEqual([]);
|
||||
expect(fWorksheet.getDrawingParentGroup('drawing-1')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function createFacade(injector: Injector): FWorksheetDrawingMixin {
|
||||
const workbook = injector.get(IUniverInstanceService).getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const worksheet = workbook.getSheetBySheetId('sheet1')!;
|
||||
return injector.createInstance(FWorksheetDrawingMixin, { getId: () => 'test' } as FWorkbook, workbook, worksheet);
|
||||
}
|
||||
|
||||
function createDrawing(drawingId: string, left: number, drawingType = DrawingTypeEnum.DRAWING_SHAPE): ISheetDrawing {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
drawingType,
|
||||
transform: {
|
||||
left,
|
||||
top: 20,
|
||||
width: 40,
|
||||
height: 30,
|
||||
},
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
42
packages/sheets-drawing/src/facade/f-enum.ts
Normal file
42
packages/sheets-drawing/src/facade/f-enum.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 { DrawingTypeEnum } from '@univerjs/core';
|
||||
import { FEnum } from '@univerjs/core/facade';
|
||||
import { SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFSheetsDrawingEnumMixin {
|
||||
/** Please refer to {@link DrawingTypeEnum}. */
|
||||
DrawingType: typeof DrawingTypeEnum;
|
||||
|
||||
/** Please refer to {@link SheetDrawingAnchorType}. */
|
||||
SheetDrawingAnchorType: typeof SheetDrawingAnchorType;
|
||||
}
|
||||
|
||||
export class FSheetsDrawingEnumMixin extends FEnum implements IFSheetsDrawingEnumMixin {
|
||||
override get DrawingType(): typeof DrawingTypeEnum { return DrawingTypeEnum; };
|
||||
|
||||
override get SheetDrawingAnchorType(): typeof SheetDrawingAnchorType { return SheetDrawingAnchorType; };
|
||||
}
|
||||
|
||||
FEnum.extend(FSheetsDrawingEnumMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FEnum extends IFSheetsDrawingEnumMixin { }
|
||||
}
|
||||
452
packages/sheets-drawing/src/facade/f-event.ts
Normal file
452
packages/sheets-drawing/src/facade/f-event.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
/**
|
||||
* 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 { IDrawingSearch } from '@univerjs/core';
|
||||
import type { IEventBase } from '@univerjs/core/facade';
|
||||
import type { ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import type { FWorkbook } from '@univerjs/sheets/facade';
|
||||
import type { FOverGridImage } from './f-over-grid-image';
|
||||
import { FEventName } from '@univerjs/core/facade';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
interface IFSheetsDrawingEventNameMixin {
|
||||
/**
|
||||
* Triggered before float dom insertion.
|
||||
* @see {@link IBeforeFloatDomAddParam}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeFloatDomAdd, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, drawings } = params;
|
||||
* // Cancel the insertion operation
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeFloatDomAdd: 'BeforeFloatDomAdd';
|
||||
|
||||
/**
|
||||
* Triggered after float dom insertion.
|
||||
* @see {@link IFloatDomAddedParam}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.FloatDomAdded, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, drawings } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly FloatDomAdded: 'FloatDomAdded';
|
||||
|
||||
/**
|
||||
* Triggered before float dom update.
|
||||
* @see {@link IBeforeFloatDomUpdateParam}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeFloatDomUpdate, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, drawings } = params;
|
||||
* // Cancel the update operation
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeFloatDomUpdate: 'BeforeFloatDomUpdate';
|
||||
|
||||
/**
|
||||
* Triggered after float dom update.
|
||||
* @see {@link IFloatDomUpdatedParam}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.FloatDomUpdated, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, drawings } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly FloatDomUpdated: 'FloatDomUpdated';
|
||||
|
||||
/**
|
||||
* Triggered before float dom deletion.
|
||||
* @see {@link IBeforeFloatDomDeleteParam}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeFloatDomDelete, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, drawings } = params;
|
||||
* // Cancel the deletion operation
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeFloatDomDelete: 'BeforeFloatDomDelete';
|
||||
|
||||
/**
|
||||
* Triggered after float dom deletion.
|
||||
* @see {@link IFloatDomDeletedParam}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.FloatDomDeleted, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, drawings } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly FloatDomDeleted: 'FloatDomDeleted';
|
||||
|
||||
/**
|
||||
* Triggered before floating image insertion.
|
||||
* @see {@link IBeforeOverGridImageInsertEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageInsert, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, insertImageParams } = params;
|
||||
* // Cancel the insertion operation
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeOverGridImageInsert: 'BeforeOverGridImageInsert';
|
||||
|
||||
/**
|
||||
* Triggered after floating image insertion.
|
||||
* @see {@link IOverGridImageInsertedEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.OverGridImageInserted, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, images } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly OverGridImageInserted: 'OverGridImageInserted';
|
||||
|
||||
/**
|
||||
* Triggered before floating image removal.
|
||||
* @see {@link IBeforeOverGridImageRemoveEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageRemove, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, images } = params;
|
||||
* // Cancel the removal operation
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeOverGridImageRemove: 'BeforeOverGridImageRemove';
|
||||
|
||||
/**
|
||||
* Triggered after floating image removal.
|
||||
* @see {@link IOverGridImageRemovedEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.OverGridImageRemoved, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, removeImageParams } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly OverGridImageRemoved: 'OverGridImageRemoved';
|
||||
|
||||
/**
|
||||
* Triggered before floating image change.
|
||||
* @see {@link IBeforeOverGridImageChangeEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageChange, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, images } = params;
|
||||
* // Cancel the image change
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeOverGridImageChange: 'BeforeOverGridImageChange';
|
||||
|
||||
/**
|
||||
* Triggered after floating image change.
|
||||
* @see {@link IOverGridImageChangedEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.OverGridImageChanged, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, images } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly OverGridImageChanged: 'OverGridImageChanged';
|
||||
|
||||
/**
|
||||
* Triggered before floating image selection.
|
||||
* @see {@link IBeforeOverGridImageSelectEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.BeforeOverGridImageSelect, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, selectedImages, oldSelectedImages } = params;
|
||||
* // cancel the selection operation
|
||||
* params.cancel = true;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly BeforeOverGridImageSelect: 'BeforeOverGridImageSelect';
|
||||
|
||||
/**
|
||||
* Triggered after floating image selection.
|
||||
* @see {@link IOverGridImageSelectedEventParams}
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.addEvent(univerAPI.Event.OverGridImageSelected, (params) => {
|
||||
* console.log(params);
|
||||
* // do something
|
||||
* const { workbook, selectedImages } = params;
|
||||
* })
|
||||
*
|
||||
* // Remove the event listener, use `disposable.dispose()`
|
||||
* ```
|
||||
*/
|
||||
readonly OverGridImageSelected: 'OverGridImageSelected';
|
||||
}
|
||||
|
||||
export class FSheetsDrawingEventNameMixin extends FEventName implements IFSheetsDrawingEventNameMixin {
|
||||
override get BeforeFloatDomAdd(): 'BeforeFloatDomAdd' {
|
||||
return 'BeforeFloatDomAdd' as const;
|
||||
}
|
||||
|
||||
override get FloatDomAdded(): 'FloatDomAdded' {
|
||||
return 'FloatDomAdded' as const;
|
||||
}
|
||||
|
||||
override get BeforeFloatDomUpdate(): 'BeforeFloatDomUpdate' {
|
||||
return 'BeforeFloatDomUpdate' as const;
|
||||
}
|
||||
|
||||
override get FloatDomUpdated(): 'FloatDomUpdated' {
|
||||
return 'FloatDomUpdated' as const;
|
||||
}
|
||||
|
||||
override get BeforeFloatDomDelete(): 'BeforeFloatDomDelete' {
|
||||
return 'BeforeFloatDomDelete' as const;
|
||||
}
|
||||
|
||||
override get FloatDomDeleted(): 'FloatDomDeleted' {
|
||||
return 'FloatDomDeleted' as const;
|
||||
}
|
||||
|
||||
override get BeforeOverGridImageChange(): 'BeforeOverGridImageChange' {
|
||||
return 'BeforeOverGridImageChange' as const;
|
||||
}
|
||||
|
||||
override get OverGridImageChanged(): 'OverGridImageChanged' {
|
||||
return 'OverGridImageChanged' as const;
|
||||
}
|
||||
|
||||
override get BeforeOverGridImageInsert(): 'BeforeOverGridImageInsert' {
|
||||
return 'BeforeOverGridImageInsert' as const;
|
||||
}
|
||||
|
||||
override get OverGridImageInserted(): 'OverGridImageInserted' {
|
||||
return 'OverGridImageInserted' as const;
|
||||
}
|
||||
|
||||
override get BeforeOverGridImageRemove(): 'BeforeOverGridImageRemove' {
|
||||
return 'BeforeOverGridImageRemove' as const;
|
||||
}
|
||||
|
||||
override get OverGridImageRemoved(): 'OverGridImageRemoved' {
|
||||
return 'OverGridImageRemoved' as const;
|
||||
}
|
||||
|
||||
override get BeforeOverGridImageSelect(): 'BeforeOverGridImageSelect' {
|
||||
return 'BeforeOverGridImageSelect' as const;
|
||||
}
|
||||
|
||||
override get OverGridImageSelected(): 'OverGridImageSelected' {
|
||||
return 'OverGridImageSelected' as const;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IBeforeOverGridImageChangeParamObject {
|
||||
/**
|
||||
* Parameters of the image to be updated. {@link ISheetImage}
|
||||
*/
|
||||
changeParam: Partial<ISheetImage>;
|
||||
|
||||
/**
|
||||
* Instance of the image to be updated. {@link FOverGridImage}
|
||||
*/
|
||||
image: FOverGridImage;
|
||||
}
|
||||
|
||||
export interface IBeforeOverGridImageChangeEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of parameters of the images to be updated, there may be multiple images to be updated. {@link IBeforeOverGridImageChangeParamObject}
|
||||
*/
|
||||
images: IBeforeOverGridImageChangeParamObject[];
|
||||
}
|
||||
|
||||
export interface IOverGridImageChangedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Instances of the images after the update. {@link FOverGridImage}
|
||||
*/
|
||||
images: FOverGridImage[];
|
||||
}
|
||||
|
||||
export interface IBeforeOverGridImageInsertEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of parameters of the images to be inserted. {@link ISheetImage}
|
||||
*/
|
||||
insertImageParams: ISheetImage[];
|
||||
}
|
||||
|
||||
export interface IOverGridImageInsertedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of instances of the images after insertion. {@link FOverGridImage}
|
||||
*/
|
||||
images: FOverGridImage[];
|
||||
}
|
||||
|
||||
export interface IBeforeOverGridImageRemoveEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of instances of the images to be removed. {@link FOverGridImage}
|
||||
*/
|
||||
images: FOverGridImage[];
|
||||
}
|
||||
|
||||
export interface IOverGridImageRemovedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of parameters of the images after removal. {@link ISheetImage}
|
||||
*/
|
||||
removeImageParams: IDrawingSearch[];
|
||||
}
|
||||
|
||||
export interface IBeforeOverGridImageSelectEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
/**
|
||||
* The images before selection. {@link FOverGridImage}
|
||||
*/
|
||||
oldSelectedImages: FOverGridImage[];
|
||||
|
||||
selectedImages: FOverGridImage[];
|
||||
}
|
||||
|
||||
export interface IOverGridImageSelectedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
/**
|
||||
* The selected images. {@link FOverGridImage}
|
||||
*/
|
||||
selectedImages: FOverGridImage[];
|
||||
}
|
||||
|
||||
interface ISheetsDrawingEventParamConfig {
|
||||
BeforeOverGridImageChange: IBeforeOverGridImageChangeEventParams;
|
||||
OverGridImageChanged: IOverGridImageChangedEventParams;
|
||||
|
||||
BeforeOverGridImageInsert: IBeforeOverGridImageInsertEventParams;
|
||||
OverGridImageInserted: IOverGridImageInsertedEventParams;
|
||||
|
||||
BeforeOverGridImageRemove: IBeforeOverGridImageRemoveEventParams;
|
||||
OverGridImageRemoved: IOverGridImageRemovedEventParams;
|
||||
|
||||
BeforeOverGridImageSelect: IBeforeOverGridImageSelectEventParams;
|
||||
OverGridImageSelected: IOverGridImageSelectedEventParams;
|
||||
}
|
||||
|
||||
FEventName.extend(FSheetsDrawingEventNameMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FEventName extends IFSheetsDrawingEventNameMixin { }
|
||||
interface IEventParamConfig extends ISheetsDrawingEventParamConfig { }
|
||||
}
|
||||
1271
packages/sheets-drawing/src/facade/f-over-grid-image.ts
Normal file
1271
packages/sheets-drawing/src/facade/f-over-grid-image.ts
Normal file
File diff suppressed because it is too large
Load Diff
289
packages/sheets-drawing/src/facade/f-univer.ts
Normal file
289
packages/sheets-drawing/src/facade/f-univer.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 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 { IDrawingSearch, Injector } from '@univerjs/core';
|
||||
import type { IInsertSheetDrawingCommandParams, IRemoveSheetDrawingCommandParams, ISetDrawingCommandParams, ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import type {
|
||||
IBeforeOverGridImageChangeEventParams,
|
||||
IBeforeOverGridImageChangeParamObject,
|
||||
IBeforeOverGridImageInsertEventParams,
|
||||
IBeforeOverGridImageRemoveEventParams,
|
||||
IBeforeOverGridImageSelectEventParams,
|
||||
IOverGridImageChangedEventParams,
|
||||
IOverGridImageInsertedEventParams,
|
||||
IOverGridImageRemovedEventParams,
|
||||
IOverGridImageSelectedEventParams,
|
||||
} from './f-event';
|
||||
import { CanceledError, ICommandService } from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { IDrawingManagerService, SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import { InsertSheetDrawingCommand, RemoveSheetDrawingCommand, SetSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
import { FOverGridImage } from './f-over-grid-image';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FUniverSheetsDrawingMixin extends FUniver {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
override _initialize(injector: Injector): void {
|
||||
const commandService = injector.get(ICommandService);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeOverGridImageInsert,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== InsertSheetDrawingCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IInsertSheetDrawingCommandParams;
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null || params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings } = params;
|
||||
const eventParams: IBeforeOverGridImageInsertEventParams = {
|
||||
workbook,
|
||||
insertImageParams: drawings as ISheetImage[],
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeOverGridImageInsert, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeOverGridImageRemove,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== RemoveSheetDrawingCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IRemoveSheetDrawingCommandParams;
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null || params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
|
||||
const { drawings } = params;
|
||||
const willRemoveDrawings = drawings.map((drawing) => {
|
||||
return drawingManagerService.getDrawingByParam(drawing);
|
||||
}) as ISheetImage[];
|
||||
|
||||
const eventParams: IBeforeOverGridImageRemoveEventParams = {
|
||||
workbook,
|
||||
images: this._createFOverGridImage(willRemoveDrawings),
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeOverGridImageRemove, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeOverGridImageChange,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== SetSheetDrawingCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as ISetDrawingCommandParams;
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null || params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings } = params;
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
|
||||
const images: IBeforeOverGridImageChangeParamObject[] = [];
|
||||
drawings.forEach((drawing) => {
|
||||
const image = drawingManagerService.getDrawingByParam(drawing as IDrawingSearch) as ISheetImage;
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
images.push({
|
||||
changeParam: drawing,
|
||||
image: this._injector.createInstance(FOverGridImage, image),
|
||||
});
|
||||
});
|
||||
|
||||
const eventParams: IBeforeOverGridImageChangeEventParams = {
|
||||
workbook,
|
||||
images,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeOverGridImageChange, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
drawingManagerService.updateNotification(drawings as IDrawingSearch[]);
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeOverGridImageSelect,
|
||||
() => commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== SetDrawingSelectedOperation.id) return;
|
||||
|
||||
const drawings = commandInfo.params as IDrawingSearch[];
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null) {
|
||||
return;
|
||||
}
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
|
||||
const oldSelectedDrawings = drawingManagerService.getFocusDrawings() as ISheetImage[];
|
||||
const selectedDrawings = drawings.map((drawing) => {
|
||||
return drawingManagerService.getDrawingByParam(drawing);
|
||||
}) as ISheetImage[];
|
||||
|
||||
const eventParams: IBeforeOverGridImageSelectEventParams = {
|
||||
workbook,
|
||||
selectedImages: this._createFOverGridImage(selectedDrawings),
|
||||
oldSelectedImages: this._createFOverGridImage(oldSelectedDrawings),
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeOverGridImageSelect, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.OverGridImageInserted,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== InsertSheetDrawingCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IInsertSheetDrawingCommandParams;
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null || params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings } = params;
|
||||
const eventParams: IOverGridImageInsertedEventParams = {
|
||||
workbook,
|
||||
images: this._createFOverGridImage(drawings as ISheetImage[]),
|
||||
};
|
||||
this.fireEvent(this.Event.OverGridImageInserted, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.OverGridImageRemoved,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== RemoveSheetDrawingCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as IRemoveSheetDrawingCommandParams;
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null || params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings } = params;
|
||||
const eventParams: IOverGridImageRemovedEventParams = {
|
||||
workbook,
|
||||
removeImageParams: drawings,
|
||||
};
|
||||
this.fireEvent(this.Event.OverGridImageRemoved, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.OverGridImageChanged,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== SetSheetDrawingCommand.id) return;
|
||||
|
||||
const params = commandInfo.params as ISetDrawingCommandParams;
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null || params == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings } = params;
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
|
||||
const images = drawings.map((drawing) => {
|
||||
return this._injector.createInstance(FOverGridImage, drawingManagerService.getDrawingByParam(drawing as IDrawingSearch) as ISheetImage);
|
||||
});
|
||||
const eventParams: IOverGridImageChangedEventParams = {
|
||||
workbook,
|
||||
images,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.OverGridImageChanged, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.OverGridImageSelected,
|
||||
() => commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id !== SetDrawingSelectedOperation.id) return;
|
||||
|
||||
const drawings = commandInfo.params as IDrawingSearch[];
|
||||
const workbook = this.getActiveWorkbook();
|
||||
if (workbook == null) {
|
||||
return;
|
||||
}
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
|
||||
const selectedDrawings = drawings.map((drawing) => {
|
||||
return drawingManagerService.getDrawingByParam(drawing);
|
||||
}) as ISheetImage[];
|
||||
const eventParams: IOverGridImageSelectedEventParams = {
|
||||
workbook,
|
||||
selectedImages: this._createFOverGridImage(selectedDrawings as ISheetImage[]),
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.OverGridImageSelected, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _createFOverGridImage(drawings: ISheetImage[]): FOverGridImage[] {
|
||||
return drawings.map((drawing) => {
|
||||
return this._injector.createInstance(FOverGridImage, drawing);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FUniver.extend(FUniverSheetsDrawingMixin);
|
||||
923
packages/sheets-drawing/src/facade/f-worksheet.ts
Normal file
923
packages/sheets-drawing/src/facade/f-worksheet.ts
Normal file
@@ -0,0 +1,923 @@
|
||||
/**
|
||||
* 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 { IDrawingParam, IGroupBaseBound } from '@univerjs/core';
|
||||
import type { IFBlobSource } from '@univerjs/core/facade';
|
||||
import type { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import type { ISheetDrawing, ISheetDrawingPlacement, ISheetDrawingPlacementInput, ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import { DrawingTypeEnum, generateRandomId, ImageSourceType, IUndoRedoService } from '@univerjs/core';
|
||||
import { isGroupableDrawingType } from '@univerjs/drawing';
|
||||
import { getGroupState, transformObjectOutOfGroup } from '@univerjs/engine-render';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { DrawingApplyType, getSheetDrawingPlacement, InsertSheetDrawingCommand, ISheetDrawingService, normalizeSheetDrawingPlacement, RemoveSheetDrawingCommand, SetDrawingApplyMutation, SetSheetDrawingCommand, SetSheetDrawingPlacementCommand, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
|
||||
import { FWorksheet } from '@univerjs/sheets/facade';
|
||||
import { FOverGridImage, FOverGridImageBuilder } from './f-over-grid-image';
|
||||
|
||||
export interface ISheetDrawingLayoutObject {
|
||||
drawingId: string;
|
||||
drawingType: DrawingTypeEnum;
|
||||
bounds: IGroupBaseBound;
|
||||
placement: ISheetDrawingPlacement;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
export interface ISheetDrawingLayout {
|
||||
/** Entire Sheet grid in model coordinates. */
|
||||
gridBounds: IGroupBaseBound;
|
||||
/** Cell content range in model coordinates. */
|
||||
dataBounds: IGroupBaseBound;
|
||||
/** Drawings ordered from back to front. */
|
||||
drawings: ISheetDrawingLayoutObject[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorksheetDrawingMixin {
|
||||
/**
|
||||
* Insert an image to the sheet
|
||||
* @param {string} url - The image url
|
||||
* @returns {Promise<boolean>} true if the image is inserted successfully
|
||||
* @example
|
||||
* ```ts
|
||||
* // Insert an image to the sheet, default position is A1
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const result = await fWorksheet.insertImage('https://avatars.githubusercontent.com/u/61444807?s=48&v=4');
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
insertImage(url: string): Promise<boolean>;
|
||||
/**
|
||||
* @param {string} url - The image url
|
||||
* @param {number} column - The column index to insert the image
|
||||
* @param {number} row - The row index to insert the image
|
||||
* @returns {Promise<boolean>} True if the image is inserted successfully
|
||||
* @example
|
||||
* ```ts
|
||||
* // Insert an image to the sheet, position is F6
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const result = await fWorksheet.insertImage('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', 5, 5);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
insertImage(url: string, column: number, row: number): Promise<boolean>;
|
||||
/**
|
||||
* @param {string} url - The image url
|
||||
* @param {number} column - The column index to insert the image
|
||||
* @param {number} row - The row index to insert the image
|
||||
* @param {number} offsetX - The column offset, pixel unit
|
||||
* @param {number} offsetY - The row offset, pixel unit
|
||||
* @returns {Promise<boolean>} True if the image is inserted successfully
|
||||
* @example
|
||||
* ```ts
|
||||
* // Insert an image to the sheet, position is F6, offset is 10px
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const result = await fWorksheet.insertImage('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', 5, 5, 10, 10);
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
insertImage(url: string, column: number, row: number, offsetX: number, offsetY: number): Promise<boolean>;
|
||||
insertImage(url: IFBlobSource): Promise<boolean>;
|
||||
insertImage(url: IFBlobSource, column: number, row: number): Promise<boolean>;
|
||||
insertImage(url: IFBlobSource, column: number, row: number, offsetX: number, offsetY: number): Promise<boolean>;
|
||||
insertImage(url: IFBlobSource | string, column: number, row: number, offsetX: number, offsetY: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Insert images to the sheet
|
||||
* @param {ISheetImage[]} sheetImages - The images to insert
|
||||
* @returns {FWorksheet} The FWorksheet instance for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* // create a new image builder and set image source.
|
||||
* // then build `ISheetImage` and insert it into the sheet, position is start from F6 cell, width is 500px, height is 300px
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const image = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(5)
|
||||
* .setRow(5)
|
||||
* .setWidth(500)
|
||||
* .setHeight(300)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image]);
|
||||
*
|
||||
* // update the image width to 100px and height to 50px
|
||||
* setTimeout(async () => {
|
||||
* const imageBuilder = fWorksheet.getImageById(image.drawingId).toBuilder();
|
||||
* const newImage = await imageBuilder.setWidth(100).setHeight(50).buildAsync();
|
||||
* fWorksheet.updateImages([newImage]);
|
||||
* }, 4000);
|
||||
* ```
|
||||
*/
|
||||
insertImages(sheetImages: ISheetImage[]): FWorksheet;
|
||||
|
||||
/**
|
||||
* Get all images of the sheet.
|
||||
* @returns {FOverGridImage[]} The FOverGridImage instances
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const images = fWorksheet.getImages();
|
||||
* images.forEach((image) => {
|
||||
* console.log(image, image.getId());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getImages(): FOverGridImage[];
|
||||
|
||||
/**
|
||||
* Get image by drawing id
|
||||
* @param {string} id - The drawing id of the image
|
||||
* @returns {FOverGridImage | null} The FOverGridImage instance
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const image = fWorksheet.getImageById('xxxx');
|
||||
* console.log(image);
|
||||
* ```
|
||||
*/
|
||||
getImageById(id: string): FOverGridImage | null;
|
||||
|
||||
/**
|
||||
* Delete images from the sheet
|
||||
* @param {FOverGridImage[]} sheetImages - The images to delete
|
||||
* @returns {FWorksheet} The FWorksheet instance for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const image = fWorksheet.getImages()[0];
|
||||
*
|
||||
* // Delete the first image of the sheet
|
||||
* fWorksheet.deleteImages([image]);
|
||||
* ```
|
||||
*/
|
||||
deleteImages(sheetImages: FOverGridImage[]): FWorksheet;
|
||||
|
||||
/**
|
||||
* Update images to the sheet
|
||||
* @param {ISheetImage[]} sheetImages - The images to update
|
||||
* @returns {FWorksheet} The FWorksheet instance for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* // create a new image builder and set image source.
|
||||
* // then build `ISheetImage` and insert it into the sheet, position is start from F6 cell, width is 500px, height is 300px
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const image = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(5)
|
||||
* .setRow(5)
|
||||
* .setWidth(500)
|
||||
* .setHeight(300)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image]);
|
||||
*
|
||||
* // update the image width to 100px and height to 50px after 4 seconds
|
||||
* setTimeout(async () => {
|
||||
* const imageBuilder = fWorksheet.getImageById(image.drawingId).toBuilder();
|
||||
* const newImage = await imageBuilder.setWidth(100).setHeight(50).buildAsync();
|
||||
* fWorksheet.updateImages([newImage]);
|
||||
* }, 4000);
|
||||
* ```
|
||||
*/
|
||||
updateImages(sheetImages: ISheetImage[]): FWorksheet;
|
||||
|
||||
/**
|
||||
* Get the placement of any drawing on this sheet.
|
||||
*
|
||||
* Image, Shape, Chart, and Group use the same placement contract.
|
||||
* @param {string} drawingId Drawing id.
|
||||
* @returns {ISheetDrawingPlacement | null} The placement, or `null` when the drawing does not exist.
|
||||
* @example
|
||||
* ```ts
|
||||
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
|
||||
* const placement = sheet.getDrawingPlacement('drawing-id');
|
||||
* if (placement?.kind === univerAPI.Enum.SheetDrawingAnchorType.Both) {
|
||||
* console.log(placement.from, placement.to);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getDrawingPlacement(drawingId: string): ISheetDrawingPlacement | null;
|
||||
|
||||
/**
|
||||
* Set the placement of any drawing on this sheet through the drawing command.
|
||||
*
|
||||
* @param {string} drawingId Drawing id.
|
||||
* @param {ISheetDrawingPlacementInput} placement Exact markers or model-space bounds with an explicit anchor type.
|
||||
* @returns {boolean} `true` when the command succeeds.
|
||||
* @example OneCell: move with cells, keep pixel size
|
||||
* ```ts
|
||||
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
|
||||
* const drawingId = sheet.getImages()[0]?.getId();
|
||||
* if (!drawingId) throw new Error('No drawing found.');
|
||||
* const changed = sheet.setDrawingPlacement(drawingId, {
|
||||
* kind: univerAPI.Enum.SheetDrawingAnchorType.Position,
|
||||
* from: { row: 2, column: 2, rowOffset: 8, columnOffset: 8 },
|
||||
* width: 240,
|
||||
* height: 120,
|
||||
* });
|
||||
* console.log(changed);
|
||||
* ```
|
||||
* @example TwoCell: move and resize with both cell markers
|
||||
* ```ts
|
||||
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
|
||||
* const drawingId = sheet.getImages()[0]?.getId();
|
||||
* if (!drawingId) throw new Error('No drawing found.');
|
||||
* sheet.setDrawingPlacement(drawingId, {
|
||||
* kind: univerAPI.Enum.SheetDrawingAnchorType.Both,
|
||||
* from: { row: 2, column: 2, rowOffset: 8, columnOffset: 8 },
|
||||
* to: { row: 8, column: 6, rowOffset: 0, columnOffset: 0 },
|
||||
* });
|
||||
* ```
|
||||
* @example Absolute: do not move or resize after row or column changes
|
||||
* ```ts
|
||||
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
|
||||
* const drawingId = sheet.getImages()[0]?.getId();
|
||||
* if (!drawingId) throw new Error('No drawing found.');
|
||||
* sheet.setDrawingPlacement(drawingId, {
|
||||
* kind: univerAPI.Enum.SheetDrawingAnchorType.None,
|
||||
* left: 640,
|
||||
* top: 96,
|
||||
* width: 240,
|
||||
* height: 120,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
setDrawingPlacement(drawingId: string, placement: ISheetDrawingPlacementInput): boolean;
|
||||
|
||||
/**
|
||||
* Resolve exact markers or model-space bounds to a normalized Placement.
|
||||
*
|
||||
* Bounds inference is usually preferable when positioning from an existing
|
||||
* transform. Exact markers are useful when the caller must preserve a
|
||||
* user-selected cell and offset. `Position` maps to OOXML OneCell,
|
||||
* `Both` maps to TwoCell, and `None` maps to Absolute.
|
||||
* @param {ISheetDrawingPlacementInput} placement Exact markers or bounds with an explicit anchor type.
|
||||
* @returns {ISheetDrawingPlacement} The normalized Placement.
|
||||
* @example Infer Position and Both placements from bounds in Node/headless
|
||||
* ```ts
|
||||
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
|
||||
* const positionPlacement = sheet.resolveDrawingPlacement({
|
||||
* kind: univerAPI.Enum.SheetDrawingAnchorType.Position,
|
||||
* bounds: { left: 120, top: 80, width: 320, height: 160 },
|
||||
* });
|
||||
* const bothPlacement = sheet.resolveDrawingPlacement({
|
||||
* kind: univerAPI.Enum.SheetDrawingAnchorType.Both,
|
||||
* bounds: { left: 120, top: 80, width: 320, height: 160 },
|
||||
* });
|
||||
* console.log(positionPlacement.from);
|
||||
* console.log(bothPlacement.from, bothPlacement.to);
|
||||
* ```
|
||||
*/
|
||||
resolveDrawingPlacement(placement: ISheetDrawingPlacementInput): ISheetDrawingPlacement;
|
||||
|
||||
/**
|
||||
* Get the Sheet host layout in model coordinates.
|
||||
*
|
||||
* This API is available in Node/headless and does not include viewport,
|
||||
* scroll, zoom, frozen-pane clipping, or screen coordinates.
|
||||
* @returns {ISheetDrawingLayout} Grid, data, and ordered Drawing bounds.
|
||||
* @example
|
||||
* ```ts
|
||||
* const sheet = univerAPI.getActiveWorkbook().getActiveSheet();
|
||||
* const layout = sheet.getDrawingLayout();
|
||||
* console.log(layout.gridBounds, layout.dataBounds);
|
||||
* for (const drawing of layout.drawings) {
|
||||
* console.log(drawing.drawingId, drawing.bounds);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getDrawingLayout(): ISheetDrawingLayout;
|
||||
|
||||
/**
|
||||
* Get the current selected images.
|
||||
* @returns {FOverGridImage[]} The FOverGridImage instances
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const images = fWorksheet.getActiveImages();
|
||||
* images.forEach((image) => {
|
||||
* console.log(image, image.getId());
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getActiveImages(): FOverGridImage[];
|
||||
|
||||
/**
|
||||
* Create a new over grid image builder.
|
||||
* @returns {FOverGridImageBuilder} The FOverGridImageBuilder instance
|
||||
* @example
|
||||
* ```ts
|
||||
* // create a new image builder and set image source.
|
||||
* // then build `ISheetImage` and insert it into the sheet, position is start from F6 cell, width is 500px, height is 300px
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const image = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(5)
|
||||
* .setRow(5)
|
||||
* .setWidth(500)
|
||||
* .setHeight(300)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image]);
|
||||
* ```
|
||||
*/
|
||||
newOverGridImage(): FOverGridImageBuilder;
|
||||
|
||||
/**
|
||||
* Group drawings on the current sheet.
|
||||
* @param {string[]} drawingIds - The drawing ids to group. At least two drawings are required.
|
||||
* @param {string} [groupId] - Optional group drawing id. If omitted, a new id will be generated.
|
||||
* @returns {string | null} The group id if the operation succeeds, otherwise null.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Group two over-grid images on the active sheet.
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getActiveSheet();
|
||||
* const image1 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(1)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* const image2 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(3)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image1, image2]);
|
||||
* const groupId = fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
|
||||
* console.log(groupId);
|
||||
* ```
|
||||
*/
|
||||
groupDrawings(drawingIds: string[], groupId?: string): string | null;
|
||||
|
||||
/**
|
||||
* Ungroup drawing groups on the current sheet.
|
||||
* @param {string[]} groupIds - The group drawing ids to ungroup.
|
||||
* @returns {boolean} true if the operation succeeds, otherwise false.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Group two images, then ungroup the generated group.
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getActiveSheet();
|
||||
* const image1 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(1)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* const image2 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(3)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image1, image2]);
|
||||
* const groupId = fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
|
||||
* if (groupId) {
|
||||
* const result = fWorksheet.ungroupDrawings([groupId]);
|
||||
* console.log(result);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
ungroupDrawings(groupIds: string[]): boolean;
|
||||
|
||||
/**
|
||||
* Get children of a drawing group on the current sheet.
|
||||
* @param {string} groupId - The group drawing id.
|
||||
* @param {boolean} [recursive] - Whether to return all descendants.
|
||||
* @returns {ISheetDrawing[]} The child drawings.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Get the direct children of a drawing group.
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getActiveSheet();
|
||||
* const image1 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(1)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* const image2 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(3)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image1, image2]);
|
||||
* const groupId = fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
|
||||
* if (groupId) {
|
||||
* const children = fWorksheet.getDrawingGroupChildren(groupId);
|
||||
* console.log(children.map((drawing) => drawing.drawingId));
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getDrawingGroupChildren(groupId: string, recursive?: boolean): ISheetDrawing[];
|
||||
|
||||
/**
|
||||
* Get the parent group of a drawing on the current sheet.
|
||||
* @param {string} drawingId - The child drawing id.
|
||||
* @returns {ISheetDrawing | null} The parent group drawing, or null if the drawing is not grouped.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Get the parent group of a drawing.
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getActiveSheet();
|
||||
* const image1 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(1)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* const image2 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(3)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image1, image2]);
|
||||
* fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
|
||||
* const parentGroup = fWorksheet.getDrawingParentGroup(image1.drawingId);
|
||||
* console.log(parentGroup?.drawingId);
|
||||
* ```
|
||||
*/
|
||||
getDrawingParentGroup(drawingId: string): ISheetDrawing | null;
|
||||
|
||||
/**
|
||||
* Returns whether a drawing is inside a group on the current sheet.
|
||||
* @param {string} drawingId - The drawing id.
|
||||
* @returns {boolean} true if the drawing has a parent group.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Check whether a drawing is inside a group.
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getActiveSheet();
|
||||
* const image1 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(1)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* const image2 = await fWorksheet.newOverGridImage()
|
||||
* .setSource('https://avatars.githubusercontent.com/u/61444807?s=48&v=4', univerAPI.Enum.ImageSourceType.URL)
|
||||
* .setColumn(3)
|
||||
* .setRow(1)
|
||||
* .setWidth(100)
|
||||
* .setHeight(100)
|
||||
* .buildAsync();
|
||||
* fWorksheet.insertImages([image1, image2]);
|
||||
* fWorksheet.groupDrawings([image1.drawingId, image2.drawingId]);
|
||||
* const isGrouped = fWorksheet.isDrawingGrouped(image1.drawingId);
|
||||
* console.log(isGrouped);
|
||||
* ```
|
||||
*/
|
||||
isDrawingGrouped(drawingId: string): boolean;
|
||||
}
|
||||
|
||||
export class FWorksheetDrawingMixin extends FWorksheet implements IFWorksheetDrawingMixin {
|
||||
private get _sheetDrawingService(): ISheetDrawingService {
|
||||
return this._injector.get(ISheetDrawingService);
|
||||
}
|
||||
|
||||
private get _sheetSkeletonService(): SheetSkeletonService {
|
||||
return this._injector.get(SheetSkeletonService);
|
||||
}
|
||||
|
||||
private get _undoRedoService(): IUndoRedoService {
|
||||
return this._injector.get(IUndoRedoService);
|
||||
}
|
||||
|
||||
override async insertImage(url: IFBlobSource | string, column?: number, row?: number, offsetX?: number, offsetY?: number): Promise<boolean> {
|
||||
const imageBuilder = this.newOverGridImage();
|
||||
if (typeof url === 'string') {
|
||||
imageBuilder.setSource(url);
|
||||
} else {
|
||||
const blobSource = url.getBlob();
|
||||
const base64 = await blobSource.getDataAsString();
|
||||
imageBuilder.setSource(base64, ImageSourceType.BASE64);
|
||||
}
|
||||
|
||||
if (column !== undefined) {
|
||||
imageBuilder.setColumn(column);
|
||||
} else {
|
||||
imageBuilder.setColumn(0);
|
||||
}
|
||||
|
||||
if (row !== undefined) {
|
||||
imageBuilder.setRow(row);
|
||||
} else {
|
||||
imageBuilder.setRow(0);
|
||||
}
|
||||
|
||||
if (offsetX !== undefined) {
|
||||
imageBuilder.setColumnOffset(offsetX);
|
||||
} else {
|
||||
imageBuilder.setColumnOffset(0);
|
||||
}
|
||||
|
||||
if (offsetY !== undefined) {
|
||||
imageBuilder.setRowOffset(offsetY);
|
||||
} else {
|
||||
imageBuilder.setRowOffset(0);
|
||||
}
|
||||
|
||||
const param = await imageBuilder.buildAsync();
|
||||
// param.drawingId
|
||||
|
||||
return this._commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: this._fWorkbook.getId(), drawings: [param] });
|
||||
}
|
||||
|
||||
override insertImages(sheetImages: ISheetImage[]): FWorksheet {
|
||||
const param = sheetImages.map((image) => {
|
||||
image.unitId = this._fWorkbook.getId();
|
||||
image.subUnitId = this.getSheetId();
|
||||
return image;
|
||||
});
|
||||
|
||||
this._commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, { unitId: this._fWorkbook.getId(), drawings: param });
|
||||
return this;
|
||||
}
|
||||
|
||||
override deleteImages(sheetImages: FOverGridImage[]): FWorksheet {
|
||||
const drawings = sheetImages.map((image) => {
|
||||
return {
|
||||
unitId: this._fWorkbook.getId(),
|
||||
drawingId: image.getId(),
|
||||
subUnitId: this.getSheetId(),
|
||||
drawingType: image.getType(),
|
||||
};
|
||||
});
|
||||
this._commandService.syncExecuteCommand(RemoveSheetDrawingCommand.id, { unitId: this._fWorkbook.getId(), drawings });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
override getImages(): FOverGridImage[] {
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
const drawingData = sheetDrawingService.getDrawingData(this._fWorkbook.getId(), this.getSheetId());
|
||||
const images: FOverGridImage[] = [];
|
||||
for (const drawingId in drawingData) {
|
||||
const drawing = drawingData[drawingId];
|
||||
if (drawing.drawingType !== DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
continue;
|
||||
}
|
||||
images.push(this._injector.createInstance(FOverGridImage, drawing as ISheetImage));
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
override getImageById(id: string): FOverGridImage | null {
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
const drawing = sheetDrawingService.getDrawingByParam({ unitId: this._fWorkbook.getId(), subUnitId: this.getSheetId(), drawingId: id });
|
||||
if (drawing && drawing.drawingType === DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
return this._injector.createInstance(FOverGridImage, drawing as ISheetImage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
override getActiveImages(): FOverGridImage[] {
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
const drawingData = sheetDrawingService.getFocusDrawings();
|
||||
const images: FOverGridImage[] = [];
|
||||
for (const drawingId in drawingData) {
|
||||
const drawing = drawingData[drawingId];
|
||||
images.push(this._injector.createInstance(FOverGridImage, drawing as ISheetImage));
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
override updateImages(sheetImages: ISheetImage[]): FWorksheet {
|
||||
this._commandService.syncExecuteCommand(SetSheetDrawingCommand.id, { unitId: this._fWorkbook.getId(), drawings: sheetImages });
|
||||
return this;
|
||||
}
|
||||
|
||||
override getDrawingPlacement(drawingId: string): ISheetDrawingPlacement | null {
|
||||
const drawing = this._sheetDrawingService.getDrawingByParam({
|
||||
unitId: this._fWorkbook.getId(),
|
||||
subUnitId: this.getSheetId(),
|
||||
drawingId,
|
||||
});
|
||||
return drawing ? getSheetDrawingPlacement(drawing) : null;
|
||||
}
|
||||
|
||||
override setDrawingPlacement(drawingId: string, placement: ISheetDrawingPlacementInput): boolean {
|
||||
return this._commandService.syncExecuteCommand(SetSheetDrawingPlacementCommand.id, {
|
||||
unitId: this._fWorkbook.getId(),
|
||||
subUnitId: this.getSheetId(),
|
||||
drawings: [{ drawingId, placement }],
|
||||
});
|
||||
}
|
||||
|
||||
override resolveDrawingPlacement(placement: ISheetDrawingPlacementInput): ISheetDrawingPlacement {
|
||||
const skeleton = placement.kind === SheetDrawingAnchorType.None
|
||||
? undefined
|
||||
: this._sheetSkeletonService.ensureSkeleton(this._fWorkbook.getId(), this.getSheetId());
|
||||
if (placement.kind !== SheetDrawingAnchorType.None && !skeleton) {
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_SKELETON_REQUIRED');
|
||||
}
|
||||
return normalizeSheetDrawingPlacement(placement, skeleton);
|
||||
}
|
||||
|
||||
override getDrawingLayout(): ISheetDrawingLayout {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
const skeleton = this._sheetSkeletonService.ensureSkeleton(unitId, subUnitId);
|
||||
if (!skeleton) {
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_SKELETON_REQUIRED');
|
||||
}
|
||||
const { startRow, endRow, startColumn, endColumn } = this._worksheet.getDataRealRange();
|
||||
const startCell = skeleton.getCellWithCoordByIndex(startRow, startColumn, false);
|
||||
const endCell = skeleton.getCellWithCoordByIndex(endRow, endColumn, false);
|
||||
const drawingService = this._sheetDrawingService;
|
||||
const drawingData = drawingService.getDrawingData(unitId, subUnitId);
|
||||
const orderedIds = drawingService.getDrawingOrder(unitId, subUnitId);
|
||||
const drawingIds = [...orderedIds, ...Object.keys(drawingData).filter((drawingId) => !orderedIds.includes(drawingId))];
|
||||
const drawings: ISheetDrawingLayoutObject[] = [];
|
||||
|
||||
for (const drawingId of drawingIds) {
|
||||
const drawing = drawingData[drawingId];
|
||||
const bounds = drawing && getDrawingBounds(drawing);
|
||||
if (!drawing || !bounds) {
|
||||
continue;
|
||||
}
|
||||
drawings.push({
|
||||
drawingId,
|
||||
drawingType: drawing.drawingType,
|
||||
bounds,
|
||||
placement: getSheetDrawingPlacement(drawing),
|
||||
groupId: drawing.groupId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
gridBounds: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: skeleton.columnTotalWidth,
|
||||
height: skeleton.rowTotalHeight,
|
||||
},
|
||||
dataBounds: {
|
||||
left: startCell.startX,
|
||||
top: startCell.startY,
|
||||
width: endCell.endX - startCell.startX,
|
||||
height: endCell.endY - startCell.startY,
|
||||
},
|
||||
drawings,
|
||||
};
|
||||
}
|
||||
|
||||
override newOverGridImage(): FOverGridImageBuilder {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
return this._injector.createInstance(FOverGridImageBuilder, unitId, subUnitId);
|
||||
}
|
||||
|
||||
override groupDrawings(drawingIds: string[], groupId = generateRandomId(10)): string | null {
|
||||
const uniqueDrawingIds = Array.from(new Set(drawingIds));
|
||||
if (uniqueDrawingIds.length < 2) return null;
|
||||
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
if (sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId: groupId })) return null;
|
||||
|
||||
const drawings = uniqueDrawingIds.map((drawingId) => sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId }));
|
||||
|
||||
if (drawings.some((drawing) => !drawing)) return null;
|
||||
if (drawings.some((drawing) => !isGroupableDrawingType(drawing!.drawingType))) return null;
|
||||
|
||||
const validDrawings = drawings as ISheetDrawing[];
|
||||
const groupTransform = getGroupState(0, 0, validDrawings.map((drawing) => drawing.transform || {}));
|
||||
const groupParam = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: groupId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: groupTransform,
|
||||
groupBaseBound: {
|
||||
left: groupTransform.left,
|
||||
top: groupTransform.top,
|
||||
width: groupTransform.width,
|
||||
height: groupTransform.height,
|
||||
},
|
||||
} as IDrawingParam;
|
||||
const children = validDrawings.map((drawing) => ({
|
||||
...drawing,
|
||||
groupId,
|
||||
})) as IDrawingParam[];
|
||||
|
||||
const result = this._applyGroupDrawingOperation([{
|
||||
parent: groupParam,
|
||||
children,
|
||||
}], DrawingApplyType.GROUP);
|
||||
|
||||
return result ? groupId : null;
|
||||
}
|
||||
|
||||
override ungroupDrawings(groupIds: string[]): boolean {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
const groupParams: IDrawingGroupUpdateParam[] = [];
|
||||
|
||||
for (const groupId of groupIds) {
|
||||
const groupDrawing = sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId: groupId });
|
||||
if (!groupDrawing || groupDrawing.drawingType !== DrawingTypeEnum.DRAWING_GROUP) continue;
|
||||
|
||||
const groupTransform = groupDrawing.transform || { width: 0, height: 0 };
|
||||
const children = sheetDrawingService.getDrawingsByGroup({ unitId, subUnitId, drawingId: groupId })
|
||||
.map((drawing) => {
|
||||
const newTransform = transformObjectOutOfGroup(
|
||||
drawing.transform || {},
|
||||
groupTransform,
|
||||
groupTransform.width || 0,
|
||||
groupTransform.height || 0,
|
||||
groupDrawing.groupBaseBound
|
||||
);
|
||||
|
||||
return {
|
||||
...drawing,
|
||||
transform: {
|
||||
...drawing.transform,
|
||||
...newTransform,
|
||||
},
|
||||
groupId: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
if (children.length > 0) {
|
||||
groupParams.push({
|
||||
parent: groupDrawing,
|
||||
children,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this._applyGroupDrawingOperation(groupParams, DrawingApplyType.UNGROUP);
|
||||
}
|
||||
|
||||
override getDrawingGroupChildren(groupId: string, recursive = false): ISheetDrawing[] {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
|
||||
if (!recursive) {
|
||||
return sheetDrawingService.getDrawingsByGroup({ unitId, subUnitId, drawingId: groupId }) as ISheetDrawing[];
|
||||
}
|
||||
|
||||
const nested = sheetDrawingService.getDrawingsByGroupNested({ unitId, subUnitId, drawingId: groupId });
|
||||
if (!nested) return [];
|
||||
|
||||
return [
|
||||
...(nested.flatChildren || []),
|
||||
...nested.groups.filter((group) => group.drawingId !== groupId),
|
||||
] as ISheetDrawing[];
|
||||
}
|
||||
|
||||
override getDrawingParentGroup(drawingId: string): ISheetDrawing | null {
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
const drawing = sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId });
|
||||
|
||||
if (!drawing?.groupId) return null;
|
||||
|
||||
const groupDrawing = sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId: drawing.groupId });
|
||||
if (!groupDrawing || groupDrawing.drawingType !== DrawingTypeEnum.DRAWING_GROUP) return null;
|
||||
|
||||
return groupDrawing;
|
||||
}
|
||||
|
||||
override isDrawingGrouped(drawingId: string): boolean {
|
||||
return this.getDrawingParentGroup(drawingId) !== null;
|
||||
}
|
||||
|
||||
private _applyGroupDrawingOperation(groupParams: IDrawingGroupUpdateParam[], type: DrawingApplyType.GROUP | DrawingApplyType.UNGROUP): boolean {
|
||||
if (groupParams.length === 0) return false;
|
||||
|
||||
const sheetDrawingService = this._sheetDrawingService;
|
||||
const commandService = this._commandService;
|
||||
const undoRedoService = this._undoRedoService;
|
||||
const jsonOp = (type === DrawingApplyType.GROUP
|
||||
? sheetDrawingService.getGroupDrawingOp(groupParams)
|
||||
: sheetDrawingService.getUngroupDrawingOp(groupParams)) as IDrawingJsonUndo1;
|
||||
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
const result = commandService.syncExecuteCommand(SetDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type });
|
||||
|
||||
if (result) {
|
||||
const inverseType = type === DrawingApplyType.GROUP ? DrawingApplyType.UNGROUP : DrawingApplyType.GROUP;
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects: this._invertGroupOperationObjects(objects as IDrawingGroupUpdateParam[], type), type: inverseType } },
|
||||
],
|
||||
redoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type } },
|
||||
],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _invertGroupOperationObjects(groupParams: IDrawingGroupUpdateParam[], type: DrawingApplyType.GROUP | DrawingApplyType.UNGROUP): IDrawingGroupUpdateParam[] {
|
||||
return type === DrawingApplyType.GROUP
|
||||
? groupParams.map((groupParam) => {
|
||||
const { parent, children } = groupParam;
|
||||
const groupTransform = parent.transform || { width: 0, height: 0 };
|
||||
return {
|
||||
parent: {
|
||||
...parent,
|
||||
transform: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
},
|
||||
children: children.map((child) => ({
|
||||
...child,
|
||||
transform: transformObjectOutOfGroup(
|
||||
child.transform || {},
|
||||
groupTransform,
|
||||
groupTransform.width || 0,
|
||||
groupTransform.height || 0,
|
||||
parent.groupBaseBound
|
||||
),
|
||||
groupId: undefined,
|
||||
})),
|
||||
};
|
||||
})
|
||||
: groupParams.map((groupParam) => {
|
||||
const { parent, children } = groupParam;
|
||||
const groupTransform = getGroupState(0, 0, children.map((child) => child.transform || {}));
|
||||
return {
|
||||
parent: {
|
||||
...parent,
|
||||
transform: groupTransform,
|
||||
},
|
||||
children: children.map((child) => ({
|
||||
...child,
|
||||
groupId: parent.drawingId,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getDrawingBounds(drawing: ISheetDrawing): IGroupBaseBound | null {
|
||||
const { left, top, width, height } = drawing.transform ?? {};
|
||||
if (
|
||||
typeof left !== 'number' ||
|
||||
typeof top !== 'number' ||
|
||||
typeof width !== 'number' ||
|
||||
typeof height !== 'number' ||
|
||||
!Number.isFinite(left) ||
|
||||
!Number.isFinite(top) ||
|
||||
!Number.isFinite(width) ||
|
||||
!Number.isFinite(height)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { left, top, width, height };
|
||||
}
|
||||
|
||||
FWorksheet.extend(FWorksheetDrawingMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FWorksheet extends IFWorksheetDrawingMixin { }
|
||||
}
|
||||
27
packages/sheets-drawing/src/facade/index.ts
Normal file
27
packages/sheets-drawing/src/facade/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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-over-grid-image';
|
||||
import './f-worksheet';
|
||||
import './f-enum';
|
||||
import './f-event';
|
||||
import './f-univer';
|
||||
|
||||
export type * from './f-enum';
|
||||
export type * from './f-event';
|
||||
export { FOverGridImage, type FOverGridImageBuilder, type IFOverGridImage } from './f-over-grid-image';
|
||||
export type * from './f-univer';
|
||||
export type * from './f-worksheet';
|
||||
66
packages/sheets-drawing/src/index.ts
Normal file
66
packages/sheets-drawing/src/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
drawingPositionToTransform,
|
||||
transformToAxisAlignPosition,
|
||||
transformToDrawingPosition,
|
||||
} from './basics/transform-position';
|
||||
export { InsertSheetDrawingCommand } 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 { 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 { SetSheetDrawingPlacementCommand } from './commands/commands/set-sheet-drawing-placement.command';
|
||||
export type { ISetSheetDrawingPlacementCommandParams } from './commands/commands/set-sheet-drawing-placement.command';
|
||||
export { SetSheetDrawingCommand } from './commands/commands/set-sheet-drawing.command';
|
||||
export type { ISetDrawingCommandParams } from './commands/commands/set-sheet-drawing.command';
|
||||
export { DrawingApplyType, SetDrawingApplyMutation } from './commands/mutations/set-drawing-apply.mutation';
|
||||
export type { ISetDrawingApplyMutationParams } from './commands/mutations/set-drawing-apply.mutation';
|
||||
export { ClearSheetDrawingTransformerOperation } from './commands/operations/clear-drawing-transformer.operation';
|
||||
export { isKnownSheetNonRotatableDrawingType, resolveSheetDrawingRotateEnabled } from './common/rotate-enabled';
|
||||
export type { IUniverSheetsDrawingConfig } from './config/config';
|
||||
export { SHEET_DRAWING_PLUGIN } from './controllers/sheet-drawing.controller';
|
||||
export { UniverSheetsDrawingPlugin } from './plugin';
|
||||
export {
|
||||
applySheetDrawingPlacement,
|
||||
getSheetDrawingPlacement,
|
||||
normalizeSheetDrawingPlacement,
|
||||
} from './services/sheet-drawing-placement';
|
||||
export type {
|
||||
ISheetDrawingAbsolutePlacement,
|
||||
ISheetDrawingOneCellPlacement,
|
||||
ISheetDrawingPlacement,
|
||||
ISheetDrawingPlacementByBounds,
|
||||
ISheetDrawingPlacementInput,
|
||||
ISheetDrawingTwoCellPlacement,
|
||||
} from './services/sheet-drawing-placement';
|
||||
export { SheetDrawingTransformPlanService } from './services/sheet-drawing-transform-plan.service';
|
||||
export type {
|
||||
ISheetDrawingTransformExtension,
|
||||
ISheetDrawingTransformExtensionResult,
|
||||
ISheetDrawingTransformPlan,
|
||||
} from './services/sheet-drawing-transform-plan.service';
|
||||
export { ISheetDrawingService, SheetDrawingAnchorType, SheetDrawingService } from './services/sheet-drawing.service';
|
||||
export type {
|
||||
IFloatDomData,
|
||||
ISheetDrawing,
|
||||
ISheetDrawingPosition,
|
||||
ISheetFloatDom,
|
||||
ISheetImage,
|
||||
ISheetShape,
|
||||
} from './services/sheet-drawing.service';
|
||||
63
packages/sheets-drawing/src/plugin.ts
Normal file
63
packages/sheets-drawing/src/plugin.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 { Dependency } from '@univerjs/core';
|
||||
import type { IUniverSheetsDrawingConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_DRAWING_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { SheetDrawingTransformAffectedController } from './controllers/sheet-drawing-transform-affected.controller';
|
||||
import { SHEET_DRAWING_PLUGIN, SheetsDrawingLoadController } from './controllers/sheet-drawing.controller';
|
||||
import { SheetDrawingTransformPlanService } from './services/sheet-drawing-transform-plan.service';
|
||||
import { ISheetDrawingService, SheetDrawingService } from './services/sheet-drawing.service';
|
||||
|
||||
@DependentOn(UniverDrawingPlugin, UniverSheetsPlugin)
|
||||
export class UniverSheetsDrawingPlugin extends Plugin {
|
||||
static override pluginName = SHEET_DRAWING_PLUGIN;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsDrawingConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(SHEETS_DRAWING_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
([
|
||||
[SheetsDrawingLoadController],
|
||||
[SheetDrawingTransformPlanService],
|
||||
[SheetDrawingTransformAffectedController],
|
||||
[ISheetDrawingService, { useClass: SheetDrawingService }],
|
||||
] as Dependency[]).forEach((dependency) => this._injector.add(dependency));
|
||||
|
||||
this._injector.get(SheetsDrawingLoadController);
|
||||
this._injector.get(SheetDrawingTransformAffectedController);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 { ISheetDrawingTransformPlan } from '../sheet-drawing-transform-plan.service';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetDrawingTransformPlanService } from '../sheet-drawing-transform-plan.service';
|
||||
|
||||
describe('SheetDrawingTransformPlanService', () => {
|
||||
it('runs extensions by priority and aggregates feature mutations', () => {
|
||||
const service = new SheetDrawingTransformPlanService();
|
||||
const calls: string[] = [];
|
||||
|
||||
service.register({
|
||||
priority: 0,
|
||||
transform: () => {
|
||||
calls.push('low');
|
||||
return {
|
||||
redos: [{ id: 'low-redo', params: {} }],
|
||||
undos: [{ id: 'low-undo', params: {} }],
|
||||
};
|
||||
},
|
||||
});
|
||||
service.register({
|
||||
priority: 100,
|
||||
transform: () => {
|
||||
calls.push('high');
|
||||
return {
|
||||
preRedos: [{ id: 'high-pre-redo', params: {} }],
|
||||
preUndos: [{ id: 'high-pre-undo', params: {} }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const result = service.transform({ mode: 'command' } as ISheetDrawingTransformPlan);
|
||||
|
||||
expect(calls).toEqual(['high', 'low']);
|
||||
expect(result.preRedos).toEqual([{ id: 'high-pre-redo', params: {} }]);
|
||||
expect(result.redos).toEqual([{ id: 'low-redo', params: {} }]);
|
||||
expect(result.preUndos).toEqual([{ id: 'high-pre-undo', params: {} }]);
|
||||
expect(result.undos).toEqual([{ id: 'low-undo', params: {} }]);
|
||||
});
|
||||
|
||||
it('stops running an extension after disposal', () => {
|
||||
const service = new SheetDrawingTransformPlanService();
|
||||
const transform = vi.fn(() => ({}));
|
||||
const disposable = service.register({ transform });
|
||||
|
||||
disposable.dispose();
|
||||
service.transform({ mode: 'command' } as ISheetDrawingTransformPlan);
|
||||
|
||||
expect(transform).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 { Injector } from '@univerjs/core';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ISheetDrawingService, SheetDrawingService } from '../sheet-drawing.service';
|
||||
|
||||
describe('SheetDrawingService', () => {
|
||||
let service: ISheetDrawingService;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = new Injector();
|
||||
injector.add([ISheetDrawingService, { useClass: SheetDrawingService }]);
|
||||
service = injector.get(ISheetDrawingService);
|
||||
});
|
||||
|
||||
it('initializes sheet drawing data and notifies renderers about existing drawings', () => {
|
||||
const added: unknown[] = [];
|
||||
service.add$.subscribe((items) => added.push(items));
|
||||
service.registerDrawingData('book-1', {
|
||||
sheet1: {
|
||||
data: {
|
||||
image1: { drawingId: 'image1' },
|
||||
},
|
||||
order: ['image1'],
|
||||
},
|
||||
} as never);
|
||||
|
||||
service.initializeNotification('book-1');
|
||||
|
||||
expect(service.getDrawingData('book-1', 'sheet1')?.image1.drawingId).toBe('image1');
|
||||
expect(added).toEqual([[{ drawingId: 'image1', unitId: 'book-1', subUnitId: 'sheet1' }]]);
|
||||
});
|
||||
});
|
||||
352
packages/sheets-drawing/src/services/sheet-drawing-placement.ts
Normal file
352
packages/sheets-drawing/src/services/sheet-drawing-placement.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* 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 { IGroupBaseBound, ITransformState } from '@univerjs/core';
|
||||
import type { SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { ICellOverGridPosition } from '@univerjs/sheets';
|
||||
import type {
|
||||
ISheetDrawing,
|
||||
ISheetDrawingPosition,
|
||||
ISheetFloatDom,
|
||||
ISheetImage,
|
||||
} from './sheet-drawing.service';
|
||||
import { convertPositionCellToSheetOverGrid, convertPositionSheetOverGridToAbsolute } from '@univerjs/sheets';
|
||||
import { transformToAxisAlignPosition, transformToDrawingPosition } from '../basics/transform-position';
|
||||
import {
|
||||
SheetDrawingAnchorType,
|
||||
} from './sheet-drawing.service';
|
||||
|
||||
/**
|
||||
* Anchor a drawing to one cell while keeping a fixed pixel extent.
|
||||
*/
|
||||
export interface ISheetDrawingOneCellPlacement {
|
||||
/** Placement discriminator. */
|
||||
kind: SheetDrawingAnchorType.Position;
|
||||
/** Zero-based anchor cell and pixel offsets from its top-left corner. */
|
||||
from: ICellOverGridPosition;
|
||||
/** Drawing width in pixels. */
|
||||
width: number;
|
||||
/** Drawing height in pixels. */
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anchor a drawing between two cell markers.
|
||||
*/
|
||||
export interface ISheetDrawingTwoCellPlacement {
|
||||
/** Placement discriminator. */
|
||||
kind: SheetDrawingAnchorType.Both;
|
||||
/** Zero-based start cell and pixel offsets. */
|
||||
from: ICellOverGridPosition;
|
||||
/** Zero-based end cell and pixel offsets. */
|
||||
to: ICellOverGridPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position a drawing in the Sheet canvas pixel coordinate system.
|
||||
*/
|
||||
export interface ISheetDrawingAbsolutePlacement {
|
||||
/** Placement discriminator. */
|
||||
kind: SheetDrawingAnchorType.None;
|
||||
/** Horizontal pixel offset from the Sheet canvas origin. */
|
||||
left: number;
|
||||
/** Vertical pixel offset from the Sheet canvas origin. */
|
||||
top: number;
|
||||
/** Drawing width in pixels. */
|
||||
width: number;
|
||||
/** Drawing height in pixels. */
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit Sheet drawing placement.
|
||||
*/
|
||||
export type ISheetDrawingPlacement =
|
||||
| ISheetDrawingOneCellPlacement
|
||||
| ISheetDrawingTwoCellPlacement
|
||||
| ISheetDrawingAbsolutePlacement;
|
||||
|
||||
/**
|
||||
* Infer cell markers from model-space bounds after the anchor semantics have
|
||||
* been selected explicitly.
|
||||
*/
|
||||
export interface ISheetDrawingPlacementByBounds {
|
||||
/** Position, Both, or None semantics. */
|
||||
kind: SheetDrawingAnchorType;
|
||||
/** Bounds in the Sheet model coordinate system. */
|
||||
bounds: IGroupBaseBound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact Placement or model-space bounds to normalize into one.
|
||||
*/
|
||||
export type ISheetDrawingPlacementInput = ISheetDrawingPlacement | ISheetDrawingPlacementByBounds;
|
||||
|
||||
export function getSheetDrawingPlacement(drawing: ISheetDrawing): ISheetDrawingPlacement {
|
||||
const anchorType = drawing.anchorType ?? SheetDrawingAnchorType.Position;
|
||||
if (anchorType === SheetDrawingAnchorType.None) {
|
||||
return {
|
||||
kind: SheetDrawingAnchorType.None,
|
||||
left: drawing.transform?.left ?? 0,
|
||||
top: drawing.transform?.top ?? 0,
|
||||
width: drawing.transform?.width ?? 0,
|
||||
height: drawing.transform?.height ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (anchorType === SheetDrawingAnchorType.Both) {
|
||||
return {
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
from: { ...drawing.sheetTransform.from },
|
||||
to: { ...drawing.sheetTransform.to },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
from: { ...drawing.sheetTransform.from },
|
||||
width: drawing.transform?.width ?? 0,
|
||||
height: drawing.transform?.height ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function applySheetDrawingPlacement(
|
||||
drawing: ISheetImage,
|
||||
input: ISheetDrawingPlacementInput,
|
||||
skeleton?: SpreadsheetSkeleton
|
||||
): ISheetImage;
|
||||
export function applySheetDrawingPlacement(
|
||||
drawing: ISheetFloatDom,
|
||||
input: ISheetDrawingPlacementInput,
|
||||
skeleton?: SpreadsheetSkeleton
|
||||
): ISheetFloatDom;
|
||||
export function applySheetDrawingPlacement(
|
||||
drawing: ISheetDrawing,
|
||||
input: ISheetDrawingPlacementInput,
|
||||
skeleton?: SpreadsheetSkeleton
|
||||
): ISheetDrawing;
|
||||
export function applySheetDrawingPlacement(
|
||||
drawing: ISheetDrawing,
|
||||
input: ISheetDrawingPlacementInput,
|
||||
skeleton?: SpreadsheetSkeleton
|
||||
): ISheetDrawing {
|
||||
const placement = normalizeSheetDrawingPlacement(input, skeleton);
|
||||
|
||||
if (placement.kind === SheetDrawingAnchorType.None) {
|
||||
const transform = withExistingTransform(drawing.transform, placement);
|
||||
const sheetTransform = absoluteSheetTransform(placement, drawing.sheetTransform);
|
||||
return {
|
||||
...drawing,
|
||||
anchorType: SheetDrawingAnchorType.None,
|
||||
transform,
|
||||
sheetTransform,
|
||||
axisAlignSheetTransform: sheetTransform,
|
||||
};
|
||||
}
|
||||
|
||||
if (!skeleton) {
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_SKELETON_REQUIRED');
|
||||
}
|
||||
|
||||
if (placement.kind === SheetDrawingAnchorType.Position) {
|
||||
const converted = convertPositionCellToSheetOverGrid(
|
||||
drawing.unitId,
|
||||
drawing.subUnitId,
|
||||
placement.from,
|
||||
placement.width,
|
||||
placement.height,
|
||||
skeleton
|
||||
);
|
||||
const sheetTransform = withExistingSheetTransform(drawing.sheetTransform, converted.sheetTransform);
|
||||
const transform = withExistingTransform(drawing.transform, converted.transform);
|
||||
return {
|
||||
...drawing,
|
||||
anchorType: SheetDrawingAnchorType.Position,
|
||||
sheetTransform,
|
||||
transform,
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(transform, skeleton),
|
||||
};
|
||||
}
|
||||
|
||||
const sheetTransform = withExistingSheetTransform(drawing.sheetTransform, {
|
||||
from: placement.from,
|
||||
to: placement.to,
|
||||
});
|
||||
const bounds = convertPositionSheetOverGridToAbsolute(
|
||||
drawing.unitId,
|
||||
drawing.subUnitId,
|
||||
sheetTransform,
|
||||
skeleton
|
||||
);
|
||||
const transform = withExistingTransform(drawing.transform, bounds);
|
||||
return {
|
||||
...drawing,
|
||||
anchorType: SheetDrawingAnchorType.Both,
|
||||
sheetTransform,
|
||||
transform,
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(transform, skeleton),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize exact markers or model-space bounds to the authoritative
|
||||
* Position, Both, or None Placement.
|
||||
*/
|
||||
export function normalizeSheetDrawingPlacement(
|
||||
input: ISheetDrawingPlacementInput,
|
||||
skeleton?: SpreadsheetSkeleton
|
||||
): ISheetDrawingPlacement {
|
||||
if (!('bounds' in input)) {
|
||||
validatePlacement(input);
|
||||
if (input.kind === SheetDrawingAnchorType.Both && skeleton) {
|
||||
validateBounds(convertPositionSheetOverGridToAbsolute(
|
||||
skeleton.worksheet.getUnitId(),
|
||||
skeleton.worksheet.getSheetId(),
|
||||
{
|
||||
from: input.from,
|
||||
to: input.to,
|
||||
},
|
||||
skeleton
|
||||
));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
validateBounds(input.bounds);
|
||||
if (input.kind === SheetDrawingAnchorType.None) {
|
||||
return {
|
||||
kind: SheetDrawingAnchorType.None,
|
||||
...input.bounds,
|
||||
};
|
||||
}
|
||||
if (!skeleton) {
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_SKELETON_REQUIRED');
|
||||
}
|
||||
|
||||
const sheetTransform = transformToDrawingPosition(input.bounds, skeleton);
|
||||
if (input.kind === SheetDrawingAnchorType.Position) {
|
||||
return {
|
||||
kind: SheetDrawingAnchorType.Position,
|
||||
from: sheetTransform.from,
|
||||
width: input.bounds.width,
|
||||
height: input.bounds.height,
|
||||
};
|
||||
}
|
||||
if (input.kind === SheetDrawingAnchorType.Both) {
|
||||
return {
|
||||
kind: SheetDrawingAnchorType.Both,
|
||||
from: sheetTransform.from,
|
||||
to: sheetTransform.to,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_KIND_INVALID');
|
||||
}
|
||||
|
||||
function withExistingSheetTransform(
|
||||
current: ISheetDrawingPosition,
|
||||
placement: Pick<ISheetDrawingPosition, 'from' | 'to'>
|
||||
): ISheetDrawingPosition {
|
||||
return {
|
||||
...current,
|
||||
from: { ...placement.from },
|
||||
to: { ...placement.to },
|
||||
};
|
||||
}
|
||||
|
||||
function withExistingTransform(
|
||||
current: ITransformState | null | undefined | void,
|
||||
bounds: Pick<ITransformState, 'left' | 'top' | 'width' | 'height'>
|
||||
): ITransformState {
|
||||
return {
|
||||
...current,
|
||||
left: bounds.left,
|
||||
top: bounds.top,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
function absoluteSheetTransform(
|
||||
placement: ISheetDrawingAbsolutePlacement,
|
||||
current: ISheetDrawingPosition
|
||||
): ISheetDrawingPosition {
|
||||
return {
|
||||
...current,
|
||||
from: {
|
||||
column: 0,
|
||||
columnOffset: placement.left,
|
||||
row: 0,
|
||||
rowOffset: placement.top,
|
||||
},
|
||||
to: {
|
||||
column: 0,
|
||||
columnOffset: placement.left + placement.width,
|
||||
row: 0,
|
||||
rowOffset: placement.top + placement.height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function validatePlacement(placement: ISheetDrawingPlacement): void {
|
||||
if (placement.kind === SheetDrawingAnchorType.Position) {
|
||||
validateCellPosition(placement.from);
|
||||
validateExtent(placement.width, placement.height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (placement.kind === SheetDrawingAnchorType.Both) {
|
||||
validateCellPosition(placement.from);
|
||||
validateCellPosition(placement.to);
|
||||
return;
|
||||
}
|
||||
|
||||
if (placement.kind === SheetDrawingAnchorType.None) {
|
||||
if (!Number.isFinite(placement.left) || !Number.isFinite(placement.top)) {
|
||||
throw new TypeError('SHEET_DRAWING_PLACEMENT_POSITION_INVALID');
|
||||
}
|
||||
validateExtent(placement.width, placement.height);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_KIND_INVALID');
|
||||
}
|
||||
|
||||
function validateBounds(bounds: IGroupBaseBound): void {
|
||||
if (!Number.isFinite(bounds.left) || !Number.isFinite(bounds.top)) {
|
||||
throw new TypeError('SHEET_DRAWING_PLACEMENT_POSITION_INVALID');
|
||||
}
|
||||
validateExtent(bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
function validateCellPosition(position: ICellOverGridPosition): void {
|
||||
if (
|
||||
!Number.isInteger(position.row) ||
|
||||
!Number.isInteger(position.column) ||
|
||||
position.row < 0 ||
|
||||
position.column < 0 ||
|
||||
!Number.isFinite(position.rowOffset) ||
|
||||
!Number.isFinite(position.columnOffset)
|
||||
) {
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_CELL_INVALID');
|
||||
}
|
||||
}
|
||||
|
||||
function validateExtent(width: number, height: number): void {
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
throw new Error('SHEET_DRAWING_PLACEMENT_EXTENT_INVALID');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 { ICommandInfo, IDisposable, IMutationInfo } from '@univerjs/core';
|
||||
import type { SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { ISheetDrawing } from './sheet-drawing.service';
|
||||
import { toDisposable } from '@univerjs/core';
|
||||
|
||||
export interface ISheetDrawingTransformPlan {
|
||||
command: ICommandInfo;
|
||||
mode: 'command' | 'refresh';
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
skeleton: SpreadsheetSkeleton;
|
||||
originals: ReadonlyMap<string, ISheetDrawing>;
|
||||
updates: Map<string, ISheetDrawing>;
|
||||
deletes: Set<string>;
|
||||
}
|
||||
|
||||
export interface ISheetDrawingTransformExtensionResult {
|
||||
preRedos?: IMutationInfo[];
|
||||
redos?: IMutationInfo[];
|
||||
preUndos?: IMutationInfo[];
|
||||
undos?: IMutationInfo[];
|
||||
}
|
||||
|
||||
export interface ISheetDrawingTransformExtension {
|
||||
priority?: number;
|
||||
transform(plan: ISheetDrawingTransformPlan): ISheetDrawingTransformExtensionResult | void;
|
||||
}
|
||||
|
||||
function createEmptyResult(): Required<ISheetDrawingTransformExtensionResult> {
|
||||
return { preRedos: [], redos: [], preUndos: [], undos: [] };
|
||||
}
|
||||
|
||||
export class SheetDrawingTransformPlanService {
|
||||
private readonly _extensions: ISheetDrawingTransformExtension[] = [];
|
||||
|
||||
register(extension: ISheetDrawingTransformExtension): IDisposable {
|
||||
this._extensions.push(extension);
|
||||
this._extensions.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
|
||||
return toDisposable(() => {
|
||||
const index = this._extensions.indexOf(extension);
|
||||
if (index >= 0) {
|
||||
this._extensions.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
transform(plan: ISheetDrawingTransformPlan): Required<ISheetDrawingTransformExtensionResult> {
|
||||
return this._extensions.reduce((all, extension) => {
|
||||
const next = extension.transform(plan);
|
||||
if (!next) {
|
||||
return all;
|
||||
}
|
||||
|
||||
all.preRedos.push(...(next.preRedos ?? []));
|
||||
all.redos.push(...(next.redos ?? []));
|
||||
all.preUndos.push(...(next.preUndos ?? []));
|
||||
all.undos.push(...(next.undos ?? []));
|
||||
return all;
|
||||
}, createEmptyResult());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 { IDrawingParam, IRotationSkewFlipTransform, Serializable } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1, IImageData, IUnitDrawingService } from '@univerjs/drawing';
|
||||
import type { ISheetOverGridPosition } from '@univerjs/sheets';
|
||||
import { createIdentifier } from '@univerjs/core';
|
||||
import { UnitDrawingService } from '@univerjs/drawing';
|
||||
|
||||
export enum SheetDrawingAnchorType {
|
||||
/**
|
||||
* Only the position of the drawing follows the cell changes. When rows or columns are inserted or deleted, the position of the drawing changes, but the size remains the same.
|
||||
*/
|
||||
Position = '0',
|
||||
|
||||
/**
|
||||
* The size and position of the drawing follow the cell changes. When rows or columns are inserted or deleted, the size and position of the drawing change accordingly.
|
||||
*/
|
||||
Both = '1',
|
||||
|
||||
/**
|
||||
* The size and position of the drawing do not follow the cell changes. When rows or columns are inserted or deleted, the position and size of the drawing remain unchanged.
|
||||
*/
|
||||
None = '2',
|
||||
}
|
||||
|
||||
export interface ISheetDrawingPosition extends IRotationSkewFlipTransform, ISheetOverGridPosition {
|
||||
|
||||
}
|
||||
|
||||
export interface ISheetDrawingBase {
|
||||
sheetTransform: ISheetDrawingPosition;
|
||||
/**
|
||||
* this property is used to store the excel drawing axis alignment position, which is not always the same as the sheetTransform.
|
||||
*/
|
||||
axisAlignSheetTransform: ISheetDrawingPosition;
|
||||
anchorType?: SheetDrawingAnchorType;
|
||||
}
|
||||
|
||||
export interface ISheetImage extends IImageData, ISheetDrawingBase {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* test type
|
||||
*/
|
||||
export interface ISheetShape extends IDrawingParam, ISheetDrawingBase {
|
||||
|
||||
}
|
||||
|
||||
export interface IFloatDomData extends IDrawingParam {
|
||||
componentKey: string;
|
||||
data?: Serializable;
|
||||
allowTransform?: boolean;
|
||||
/**
|
||||
* this property is used to store the excel drawing axis alignment position, which is not always the same as the sheetTransform.
|
||||
*/
|
||||
axisAlignSheetTransform: ISheetDrawingPosition;
|
||||
}
|
||||
|
||||
// TODO@wzhudev: this shouldn't be here. It should be in the sheets package
|
||||
export interface ISheetFloatDom extends IFloatDomData, ISheetDrawingBase { }
|
||||
|
||||
export type ISheetDrawing = ISheetImage | ISheetShape | ISheetFloatDom;
|
||||
|
||||
type OptionalField<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
export type ISheetUpdateDrawing = OptionalField<ISheetImage | ISheetShape, 'sheetTransform'>;
|
||||
|
||||
export class SheetDrawingService extends UnitDrawingService<ISheetDrawing> { }
|
||||
|
||||
export interface ISheetDrawingService extends IUnitDrawingService<ISheetDrawing> {
|
||||
getBatchUpdateOp(updateParams: ISheetDrawing[]): IDrawingJsonUndo1;
|
||||
}
|
||||
|
||||
export const ISheetDrawingService = createIdentifier<ISheetDrawingService>('sheets-drawing.sheet-drawing.service');
|
||||
8
packages/sheets-drawing/tsconfig.json
Normal file
8
packages/sheets-drawing/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/sheets-drawing/tsconfig.node.json
Normal file
18
packages/sheets-drawing/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/sheets-drawing/vitest.config.ts
Normal file
3
packages/sheets-drawing/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user