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-ui/README.md
Normal file
41
packages/sheets-drawing-ui/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/sheets-drawing-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/sheets-drawing-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-drawing-ui)
|
||||
[](https://npmjs.com/package/@univerjs/sheets-drawing-ui)
|
||||
|
||||
`@univerjs/sheets-drawing-ui` adds the UI for creating, selecting, editing, and exporting drawing objects in Univer Sheets.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/sheets-drawing-ui` | `UniverSheetsDrawingUi` | Yes | Yes | Yes |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/sheets-drawing-ui
|
||||
# or
|
||||
npm install @univerjs/sheets-drawing-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/sheets-drawing-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/sheets-drawing-ui/locale/en-US';
|
||||
import { UniverSheetsDrawingUIPlugin } from '@univerjs/sheets-drawing-ui';
|
||||
|
||||
univer.registerPlugin(UniverSheetsDrawingUIPlugin);
|
||||
|
||||
// Merge EnUS into your Univer locale map when this package contributes UI text.
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.univer.ai)
|
||||
- [NPM package](https://npmjs.com/package/@univerjs/sheets-drawing-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
110
packages/sheets-drawing-ui/package.json
Normal file
110
packages/sheets-drawing-ui/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "@univerjs/sheets-drawing-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Drawing UI 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",
|
||||
"ui",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.ts",
|
||||
"./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"
|
||||
},
|
||||
"./locale/*": {
|
||||
"import": "./lib/es/locale/*.js",
|
||||
"require": "./lib/cjs/locale/*.js",
|
||||
"types": "./lib/types/locale/*.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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs": "workspace:*",
|
||||
"@univerjs/docs-drawing": "workspace:*",
|
||||
"@univerjs/docs-ui": "workspace:*",
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/drawing-ui": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-drawing": "workspace:*",
|
||||
"@univerjs/sheets-ui": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/sheets-drawing-ui/postcss.config.mjs
Normal file
1
packages/sheets-drawing-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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, IDisposable, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
BooleanNumber,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
toDisposable,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing';
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import { ISidebarService, UndoRedoGroupService } from '@univerjs/ui';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 'A1' },
|
||||
},
|
||||
},
|
||||
hidden: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class TestSidebarService extends Disposable {
|
||||
private _sidebarOptions = { visible: false } as any;
|
||||
readonly sidebarOptions$ = new BehaviorSubject(this._sidebarOptions);
|
||||
readonly scrollEvent$ = new Subject<Event>();
|
||||
private _container?: HTMLElement;
|
||||
private _width?: number;
|
||||
|
||||
get visible(): boolean {
|
||||
return Boolean(this._sidebarOptions.visible);
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this._sidebarOptions;
|
||||
}
|
||||
|
||||
get width(): number | undefined {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
setWidth(value: number): void {
|
||||
this._width = value;
|
||||
}
|
||||
|
||||
open(params: any): IDisposable {
|
||||
this._sidebarOptions = {
|
||||
...params,
|
||||
visible: true,
|
||||
};
|
||||
this.sidebarOptions$.next(this._sidebarOptions);
|
||||
params.onOpen?.();
|
||||
|
||||
return toDisposable(() => this.close());
|
||||
}
|
||||
|
||||
close(id?: string): void {
|
||||
if (id && this._sidebarOptions.id !== id) {
|
||||
return;
|
||||
}
|
||||
this._sidebarOptions = {
|
||||
...this._sidebarOptions,
|
||||
visible: false,
|
||||
};
|
||||
this.sidebarOptions$.next(this._sidebarOptions);
|
||||
this._sidebarOptions.onClose?.();
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement | undefined {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
setContainer(element?: HTMLElement): void {
|
||||
this._container = element;
|
||||
}
|
||||
}
|
||||
|
||||
function hasDependency(dependencies: Dependency[] | undefined, token: unknown): boolean {
|
||||
if (!dependencies) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const dependency of dependencies) {
|
||||
if (Array.isArray(dependency) && dependency[0] === token) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createSheetsDrawingUiTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const get = injector.get.bind(injector);
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'sheets-drawing-ui-test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
if (!hasDependency(dependencies, IRenderManagerService)) {
|
||||
this._injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
}
|
||||
this._injector.add([ISidebarService, { useClass: TestSidebarService as never }]);
|
||||
this._injector.add([UndoRedoGroupService]);
|
||||
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,
|
||||
injector,
|
||||
get,
|
||||
workbook,
|
||||
commandService: get(ICommandService),
|
||||
sidebarService: get(ISidebarService) as unknown as TestSidebarService,
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 { ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { DrawingTypeEnum, ImageSourceType } from '@univerjs/core';
|
||||
import { InsertSheetDrawingCommand, ISheetDrawingService } from '@univerjs/sheets-drawing';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { DeleteDrawingsCommand } from '../delete-drawings.command';
|
||||
|
||||
function createSheetDrawing(drawingId: string): ISheetDrawing {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
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, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('DeleteDrawingsCommand', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
});
|
||||
|
||||
it('removes the focused drawing from the real sheet drawing model', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
testBed.commandService.registerCommand(DeleteDrawingsCommand);
|
||||
|
||||
await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings: [createSheetDrawing('drawing-1')],
|
||||
});
|
||||
sheetDrawingService.focusDrawing([{
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
}]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(DeleteDrawingsCommand.id)).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
})).toBeUndefined();
|
||||
expect(sheetDrawingService.getDrawingOrder(testBed.unitId, testBed.subUnitId)).toEqual([]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps the model unchanged when there is no focused drawing', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
testBed.commandService.registerCommand(DeleteDrawingsCommand);
|
||||
|
||||
await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings: [createSheetDrawing('drawing-2')],
|
||||
});
|
||||
|
||||
expect(await testBed.commandService.executeCommand(DeleteDrawingsCommand.id)).toBe(false);
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-2',
|
||||
})).toMatchObject({
|
||||
drawingId: 'drawing-2',
|
||||
});
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* 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 { ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { Direction, DrawingTypeEnum, ImageSourceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { InsertSheetDrawingCommand, ISheetDrawingService } from '@univerjs/sheets-drawing';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { FlipSheetDrawingCommand } from '../flip-drawings.command';
|
||||
import { GroupSheetDrawingCommand } from '../group-sheet-drawing.command';
|
||||
import { MoveDrawingsCommand } from '../move-drawings.command';
|
||||
import { UngroupSheetDrawingCommand } from '../ungroup-sheet-drawing.command';
|
||||
|
||||
function createSheetDrawing(drawingId: string, left: number, top: number): ISheetDrawing {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: `https://example.com/${drawingId}.png`,
|
||||
transform: {
|
||||
left,
|
||||
top,
|
||||
width: 100,
|
||||
height: 80,
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
},
|
||||
sheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function insertDrawings(testBed: ReturnType<typeof createSheetsDrawingUiTestBed>, drawings: ISheetDrawing[]) {
|
||||
await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings,
|
||||
});
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
private _srcRect: { left?: number; top?: number; right?: number; bottom?: number } | undefined;
|
||||
|
||||
setImageSrcRect(srcRect: { left?: number; top?: number; right?: number; bottom?: number }): void {
|
||||
this._srcRect = srcRect;
|
||||
}
|
||||
|
||||
getRenderUnitById() {
|
||||
return {
|
||||
scene: {
|
||||
getObject: () => ({
|
||||
srcRect: this._srcRect,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('sheet drawing UI commands', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
});
|
||||
|
||||
it('moves focused drawings by one pixel in the selected direction', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
testBed.commandService.registerCommand(MoveDrawingsCommand);
|
||||
await insertDrawings(testBed, [createSheetDrawing('drawing-1', 10, 20)]);
|
||||
sheetDrawingService.focusDrawing([{
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
}]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(MoveDrawingsCommand.id, { direction: Direction.RIGHT })).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
})?.transform).toMatchObject({
|
||||
left: 11,
|
||||
top: 20,
|
||||
});
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps drawing positions unchanged when move is triggered without a focused drawing', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
testBed.commandService.registerCommand(MoveDrawingsCommand);
|
||||
await insertDrawings(testBed, [createSheetDrawing('drawing-2', 30, 40)]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(MoveDrawingsCommand.id, { direction: Direction.UP })).toBe(false);
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-2',
|
||||
})?.transform).toMatchObject({
|
||||
left: 30,
|
||||
top: 40,
|
||||
});
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('returns false when flip is triggered without command parameters', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
testBed.commandService.registerCommand(FlipSheetDrawingCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(FlipSheetDrawingCommand.id)).toBe(false);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('skips drawings that are missing from the drawing model or lack a sheet skeleton', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
testBed.commandService.registerCommand(FlipSheetDrawingCommand);
|
||||
await insertDrawings(testBed, [
|
||||
{
|
||||
...createSheetDrawing('detached-drawing', 5, 6),
|
||||
unitId: 'detached-unit',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(FlipSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings: [
|
||||
{
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'missing-drawing',
|
||||
},
|
||||
{
|
||||
unitId: 'detached-unit',
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'detached-drawing',
|
||||
},
|
||||
],
|
||||
flipH: true,
|
||||
})).toBe(false);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('flips a drawing horizontally and vertically in the sheet drawing model', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
testBed.commandService.registerCommand(FlipSheetDrawingCommand);
|
||||
await insertDrawings(testBed, [createSheetDrawing('drawing-3', 50, 60)]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(FlipSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings: [{
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-3',
|
||||
}],
|
||||
flipH: true,
|
||||
flipV: true,
|
||||
})).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-3',
|
||||
})?.transform).toMatchObject({
|
||||
flipX: true,
|
||||
flipY: true,
|
||||
});
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('updates the image crop rectangle when flipping a cropped drawing', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
renderManagerService.setImageSrcRect({ right: 24, bottom: 36 });
|
||||
testBed.commandService.registerCommand(FlipSheetDrawingCommand);
|
||||
await insertDrawings(testBed, [createSheetDrawing('cropped-drawing', 50, 60)]);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(FlipSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings: [{
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'cropped-drawing',
|
||||
}],
|
||||
flipH: true,
|
||||
flipV: true,
|
||||
})).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'cropped-drawing',
|
||||
})).toMatchObject({
|
||||
srcRect: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 24,
|
||||
bottom: 36,
|
||||
},
|
||||
transform: {
|
||||
flipX: true,
|
||||
flipY: true,
|
||||
},
|
||||
});
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('groups two drawings and then ungroups them back to standalone drawings', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const sheetDrawingService = testBed.get(ISheetDrawingService);
|
||||
testBed.commandService.registerCommand(GroupSheetDrawingCommand);
|
||||
testBed.commandService.registerCommand(UngroupSheetDrawingCommand);
|
||||
await insertDrawings(testBed, [
|
||||
createSheetDrawing('child-1', 10, 20),
|
||||
createSheetDrawing('child-2', 40, 60),
|
||||
]);
|
||||
|
||||
const parent = {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'group-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: { left: 0, top: 0, width: 140, height: 120 },
|
||||
groupBaseBound: { left: 0, top: 0, width: 140, height: 120 },
|
||||
};
|
||||
const groupedChildren = [
|
||||
{
|
||||
...createSheetDrawing('child-1', 10, 20),
|
||||
groupId: 'group-1',
|
||||
},
|
||||
{
|
||||
...createSheetDrawing('child-2', 40, 60),
|
||||
groupId: 'group-1',
|
||||
},
|
||||
];
|
||||
|
||||
expect(await testBed.commandService.executeCommand(GroupSheetDrawingCommand.id, [{
|
||||
parent,
|
||||
children: groupedChildren,
|
||||
}])).toBe(true);
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'group-1',
|
||||
})).toMatchObject({
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
});
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'child-1',
|
||||
})).toMatchObject({
|
||||
groupId: 'group-1',
|
||||
});
|
||||
|
||||
const ungroupedChildren = [
|
||||
{
|
||||
...createSheetDrawing('child-1', 10, 20),
|
||||
groupId: undefined,
|
||||
},
|
||||
{
|
||||
...createSheetDrawing('child-2', 40, 60),
|
||||
groupId: undefined,
|
||||
},
|
||||
];
|
||||
expect(await testBed.commandService.executeCommand(UngroupSheetDrawingCommand.id, [{
|
||||
parent,
|
||||
children: ungroupedChildren,
|
||||
}])).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'group-1',
|
||||
})).toBeUndefined();
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'child-1',
|
||||
})?.groupId).toBeUndefined();
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -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 { Inject } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { SheetDrawingUpdateController } from '../../../controllers/sheet-drawing-update.controller';
|
||||
import { InsertCellImageCommand, InsertFloatImageCommand } from '../insert-image.command';
|
||||
|
||||
class TestSheetDrawingUpdateController {
|
||||
private _floatImages: string[] = [];
|
||||
private _cellImageCount = 0;
|
||||
private _filePickerCount = 0;
|
||||
private _failedFileNames = new Set<string>();
|
||||
|
||||
failFile(name: string): void {
|
||||
this._failedFileNames.add(name);
|
||||
}
|
||||
|
||||
get floatImages(): string[] {
|
||||
return this._floatImages;
|
||||
}
|
||||
|
||||
get cellImageCount(): number {
|
||||
return this._cellImageCount;
|
||||
}
|
||||
|
||||
get filePickerCount(): number {
|
||||
return this._filePickerCount;
|
||||
}
|
||||
|
||||
async insertFloatImage(): Promise<boolean> {
|
||||
this._filePickerCount += 1;
|
||||
this._floatImages.push('picked-file');
|
||||
return true;
|
||||
}
|
||||
|
||||
async insertFloatImageByFile(file: File): Promise<boolean> {
|
||||
if (this._failedFileNames.has(file.name)) {
|
||||
return false;
|
||||
}
|
||||
this._floatImages.push(file.name);
|
||||
return true;
|
||||
}
|
||||
|
||||
async insertCellImage(): Promise<boolean> {
|
||||
this._cellImageCount += 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
constructor(
|
||||
@Inject(TestSheetDrawingUpdateController) private readonly _controller: TestSheetDrawingUpdateController
|
||||
) {}
|
||||
|
||||
get controller(): TestSheetDrawingUpdateController {
|
||||
return this._controller;
|
||||
}
|
||||
|
||||
getRenderUnitById() {
|
||||
return {
|
||||
with: (token: unknown) => {
|
||||
if (token === SheetDrawingUpdateController) {
|
||||
return this._controller;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createImageFile(name: string): File {
|
||||
return new File(['image'], name, { type: 'image/png' });
|
||||
}
|
||||
|
||||
describe('insert image commands', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
});
|
||||
|
||||
it('inserts every selected file as a floating image', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[TestSheetDrawingUpdateController],
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
testBed.commandService.registerCommand(InsertFloatImageCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(InsertFloatImageCommand.id, {
|
||||
files: [
|
||||
createImageFile('first.png'),
|
||||
createImageFile('second.png'),
|
||||
],
|
||||
})).toBe(true);
|
||||
|
||||
expect(renderManagerService.controller.floatImages).toEqual(['first.png', 'second.png']);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('returns false when one selected floating image fails to insert', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[TestSheetDrawingUpdateController],
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
renderManagerService.controller.failFile('broken.png');
|
||||
testBed.commandService.registerCommand(InsertFloatImageCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(InsertFloatImageCommand.id, {
|
||||
files: [
|
||||
createImageFile('ok.png'),
|
||||
createImageFile('broken.png'),
|
||||
],
|
||||
})).toBe(false);
|
||||
expect(renderManagerService.controller.floatImages).toEqual(['ok.png']);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('opens the floating image picker when no files are provided', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[TestSheetDrawingUpdateController],
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
testBed.commandService.registerCommand(InsertFloatImageCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(InsertFloatImageCommand.id, { files: null })).toBe(true);
|
||||
expect(renderManagerService.controller.filePickerCount).toBe(1);
|
||||
expect(renderManagerService.controller.floatImages).toEqual(['picked-file']);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('inserts an image into the selected cell', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[TestSheetDrawingUpdateController],
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const renderManagerService = testBed.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
testBed.commandService.registerCommand(InsertCellImageCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(InsertCellImageCommand.id)).toBe(true);
|
||||
expect(renderManagerService.controller.cellImageCount).toBe(1);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange } from '@univerjs/core';
|
||||
import type { IBatchSaveImagesConfig, ICellImageInfo } from '../../../services/batch-save-images.service';
|
||||
import { Disposable, ImageSourceType, toDisposable } from '@univerjs/core';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { IBatchSaveImagesService } from '../../../services/batch-save-images.service';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from '../../../views/batch-save-images/component-name';
|
||||
import { SaveCellImagesCommand } from '../save-cell-images.command';
|
||||
|
||||
class TestDialogService extends Disposable implements IDialogService {
|
||||
private _dialogs: any[] = [];
|
||||
private readonly _dialogs$ = new BehaviorSubject<any[]>([]);
|
||||
|
||||
open(params: any): IDisposable {
|
||||
this._dialogs.push(params);
|
||||
this._dialogs$.next(this._dialogs);
|
||||
|
||||
return toDisposable(() => this.close(params.id));
|
||||
}
|
||||
|
||||
close(id: string): void {
|
||||
const dialogs = [];
|
||||
for (const dialog of this._dialogs) {
|
||||
if (dialog.id !== id) {
|
||||
dialogs.push(dialog);
|
||||
}
|
||||
}
|
||||
this._dialogs = dialogs;
|
||||
this._dialogs$.next(this._dialogs);
|
||||
}
|
||||
|
||||
closeAll(expectIds?: string[]): void {
|
||||
if (!expectIds) {
|
||||
this._dialogs = [];
|
||||
this._dialogs$.next([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogs = [];
|
||||
for (const dialog of this._dialogs) {
|
||||
if (expectIds.includes(dialog.id)) {
|
||||
dialogs.push(dialog);
|
||||
}
|
||||
}
|
||||
this._dialogs = dialogs;
|
||||
this._dialogs$.next(this._dialogs);
|
||||
}
|
||||
|
||||
getDialogs$() {
|
||||
return this._dialogs$.asObservable();
|
||||
}
|
||||
|
||||
get dialogs() {
|
||||
return this._dialogs;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._dialogs$.complete();
|
||||
this._dialogs = [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestBatchSaveImagesService implements IBatchSaveImagesService {
|
||||
private _images: ICellImageInfo[] = [];
|
||||
private _downloaded: ICellImageInfo[] = [];
|
||||
private _downloadFails = false;
|
||||
|
||||
setImages(images: ICellImageInfo[]): void {
|
||||
this._images = images;
|
||||
}
|
||||
|
||||
failDownload(): void {
|
||||
this._downloadFails = true;
|
||||
}
|
||||
|
||||
get downloaded(): ICellImageInfo[] {
|
||||
return this._downloaded;
|
||||
}
|
||||
|
||||
getCellImagesInSelection(): ICellImageInfo[] {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
getCellImagesFromRanges(_unitId: string, _subUnitId: string, _ranges: IRange[]): ICellImageInfo[] {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
getDataColumns(): Array<{ index: number; label: string }> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getDataColumnsForRanges(): Array<{ index: number; label: string }> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getSelectionRangeNotation(): string {
|
||||
return 'A1:B2';
|
||||
}
|
||||
|
||||
generateFileName(imageInfo: ICellImageInfo): string {
|
||||
return imageInfo.cellAddress;
|
||||
}
|
||||
|
||||
generateFileNameWithContext(imageInfo: ICellImageInfo): string {
|
||||
return imageInfo.cellAddress;
|
||||
}
|
||||
|
||||
async saveImages(_images: ICellImageInfo[], _config: IBatchSaveImagesConfig): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async saveImagesWithContext(_images: ICellImageInfo[], _config: IBatchSaveImagesConfig): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async downloadSingleImage(imageInfo: ICellImageInfo): Promise<void> {
|
||||
if (this._downloadFails) {
|
||||
throw new Error('download failed');
|
||||
}
|
||||
this._downloaded.push(imageInfo);
|
||||
}
|
||||
|
||||
getSelectionRowRange(): { startRow: number; endRow: number } | null {
|
||||
return { startRow: 0, endRow: 1 };
|
||||
}
|
||||
|
||||
getSelectionColumnIndices(): Set<number> {
|
||||
return new Set([0, 1]);
|
||||
}
|
||||
}
|
||||
|
||||
function createImage(imageId: string, cellAddress: string): ICellImageInfo {
|
||||
return {
|
||||
row: 0,
|
||||
col: 0,
|
||||
cellAddress,
|
||||
imageId,
|
||||
source: `https://example.com/${imageId}.png`,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SaveCellImagesCommand', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
});
|
||||
|
||||
it('downloads a single selected cell image directly', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[IDialogService, { useClass: TestDialogService }],
|
||||
[IBatchSaveImagesService, { useClass: TestBatchSaveImagesService }],
|
||||
]);
|
||||
const batchSaveService = testBed.get(IBatchSaveImagesService) as TestBatchSaveImagesService;
|
||||
const dialogService = testBed.get(IDialogService) as unknown as TestDialogService;
|
||||
batchSaveService.setImages([createImage('image-1', 'A1')]);
|
||||
testBed.commandService.registerCommand(SaveCellImagesCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(SaveCellImagesCommand.id)).toBe(true);
|
||||
expect(batchSaveService.downloaded).toEqual([expect.objectContaining({
|
||||
imageId: 'image-1',
|
||||
cellAddress: 'A1',
|
||||
})]);
|
||||
expect(dialogService.dialogs).toEqual([]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('opens the batch save dialog for multiple selected images', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[IDialogService, { useClass: TestDialogService }],
|
||||
[IBatchSaveImagesService, { useClass: TestBatchSaveImagesService }],
|
||||
]);
|
||||
const batchSaveService = testBed.get(IBatchSaveImagesService) as TestBatchSaveImagesService;
|
||||
const dialogService = testBed.get(IDialogService) as unknown as TestDialogService;
|
||||
batchSaveService.setImages([
|
||||
createImage('image-2', 'A1'),
|
||||
createImage('image-3', 'B2'),
|
||||
]);
|
||||
testBed.commandService.registerCommand(SaveCellImagesCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(SaveCellImagesCommand.id)).toBe(true);
|
||||
expect(dialogService.dialogs).toEqual([expect.objectContaining({
|
||||
id: BATCH_SAVE_IMAGES_DIALOG_ID,
|
||||
title: { title: 'sheets-drawing-ui.save.title (A1:B2)' },
|
||||
children: { label: BATCH_SAVE_IMAGES_DIALOG_ID },
|
||||
})]);
|
||||
dialogService.dialogs[0].onClose();
|
||||
expect(dialogService.dialogs).toEqual([]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('returns false when the single image download fails', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[IDialogService, { useClass: TestDialogService }],
|
||||
[IBatchSaveImagesService, { useClass: TestBatchSaveImagesService }],
|
||||
]);
|
||||
const batchSaveService = testBed.get(IBatchSaveImagesService) as TestBatchSaveImagesService;
|
||||
batchSaveService.setImages([createImage('image-4', 'C3')]);
|
||||
batchSaveService.failDownload();
|
||||
testBed.commandService.registerCommand(SaveCellImagesCommand);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(SaveCellImagesCommand.id)).toBe(false);
|
||||
expect(batchSaveService.downloaded).toEqual([]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 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 { describe, expect, it } from 'vitest';
|
||||
import { cloneGroupParams, groupToUngroup, ungroupToGroup } from '../utils';
|
||||
|
||||
describe('sheets drawing command utils', () => {
|
||||
it('restores grouped sheet drawings with bounds calculated from child transforms', () => {
|
||||
const grouped = ungroupToGroup([
|
||||
{
|
||||
parent: {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
groupBaseBound: { left: 1, top: 2, width: 3, height: 4 },
|
||||
},
|
||||
children: [
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'child-1',
|
||||
transform: { left: 2, top: 3, width: 10, height: 20 },
|
||||
},
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'child-2',
|
||||
transform: { left: 20, top: 5, width: 5, height: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as never);
|
||||
|
||||
expect(grouped).toHaveLength(1);
|
||||
expect(grouped[0].parent).toMatchObject({
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
groupBaseBound: { left: 1, top: 2, width: 3, height: 4 },
|
||||
transform: {
|
||||
left: 2,
|
||||
top: 3,
|
||||
width: 23,
|
||||
height: 20,
|
||||
},
|
||||
});
|
||||
expect(grouped[0].children[0]).toMatchObject({
|
||||
drawingId: 'child-1',
|
||||
groupId: 'group-1',
|
||||
transform: { left: 2, top: 3, width: 10, height: 20 },
|
||||
});
|
||||
expect(grouped[0].children[1]).toMatchObject({
|
||||
drawingId: 'child-2',
|
||||
groupId: 'group-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('ungroups sheet drawings back into standalone transforms in the parent coordinate space', () => {
|
||||
const ungrouped = groupToUngroup([
|
||||
{
|
||||
parent: {
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
transform: { left: 30, top: 40, width: 80, height: 60 },
|
||||
groupBaseBound: { left: 0, top: 0, width: 80, height: 60 },
|
||||
},
|
||||
children: [
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'child-1',
|
||||
transform: { left: 5, top: 6, width: 7, height: 8 },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as never);
|
||||
|
||||
expect(ungrouped).toHaveLength(1);
|
||||
expect(ungrouped[0].parent).toEqual({
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: { left: 0, top: 0 },
|
||||
});
|
||||
expect(ungrouped[0].children[0]).toMatchObject({
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'child-1',
|
||||
transform: {
|
||||
left: 35,
|
||||
top: 46,
|
||||
width: 7,
|
||||
height: 8,
|
||||
},
|
||||
groupId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('clones nested group ids so pasted groups do not share original drawing ids', () => {
|
||||
const result = cloneGroupParams({
|
||||
nestedIdRecord: {
|
||||
'group-root': {
|
||||
drawingId: 'group-root',
|
||||
children: ['group-child', 'shape-1'],
|
||||
},
|
||||
'group-child': {
|
||||
drawingId: 'group-child',
|
||||
children: ['shape-2'],
|
||||
},
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-root',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
},
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-child',
|
||||
groupId: 'group-root',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
},
|
||||
],
|
||||
flatChildren: [
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'shape-1',
|
||||
groupId: 'group-root',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
},
|
||||
{
|
||||
unitId: 'book-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'shape-2',
|
||||
groupId: 'group-child',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const clonedRootId = result.idMap.get('group-root');
|
||||
const clonedChildGroupId = result.idMap.get('group-child');
|
||||
const clonedShapeId = result.idMap.get('shape-1');
|
||||
|
||||
expect(clonedRootId).toBeDefined();
|
||||
expect(clonedRootId).not.toBe('group-root');
|
||||
expect(clonedChildGroupId).toBeDefined();
|
||||
expect(clonedShapeId).toBeDefined();
|
||||
const flatChildren = result.cloned.flatChildren!;
|
||||
expect(result.cloned.groups).toHaveLength(2);
|
||||
expect(flatChildren).toHaveLength(2);
|
||||
expect(result.cloned.groups[1].groupId).toBe(clonedRootId);
|
||||
expect(flatChildren[0].drawingId).toBe(clonedShapeId);
|
||||
expect(flatChildren[0].groupId).toBe(clonedRootId);
|
||||
expect(result.cloned.nestedIdRecord[clonedRootId!].children).toContain(clonedChildGroupId);
|
||||
expect(result.cloned.nestedIdRecord[clonedRootId!].children).toContain(clonedShapeId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 { IRemoveSheetDrawingCommandParams, ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { CommandType, ICommandService } from '@univerjs/core';
|
||||
import { ISheetDrawingService, RemoveSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
|
||||
export const DeleteDrawingsCommand: ICommand = {
|
||||
id: 'sheet.command.delete-drawing',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const drawingManagerService = accessor.get(ISheetDrawingService);
|
||||
|
||||
const drawings = drawingManagerService.getFocusDrawings();
|
||||
|
||||
if (drawings.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unitId = drawings[0].unitId;
|
||||
|
||||
const newDrawings = drawings.map((drawing) => {
|
||||
const { unitId, subUnitId, drawingId, drawingType } = drawing as ISheetDrawing;
|
||||
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
drawingType,
|
||||
};
|
||||
});
|
||||
return commandService.executeCommand<IRemoveSheetDrawingCommandParams>(RemoveSheetDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: newDrawings,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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, ITransformState } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import type { Image } from '@univerjs/engine-render';
|
||||
import type { ISheetDrawingPosition } from '@univerjs/sheets-drawing';
|
||||
import {
|
||||
CommandType,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import { getDrawingShapeKeyByDrawingSearch } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { ClearSheetDrawingTransformerOperation, DrawingApplyType, ISheetDrawingService, SetDrawingApplyMutation, transformToAxisAlignPosition, transformToDrawingPosition } from '@univerjs/sheets-drawing';
|
||||
|
||||
interface IFlipDrawingCommandParam {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawingId: string;
|
||||
}
|
||||
export interface IFlipSheetDrawingCommandParams {
|
||||
unitId: string;
|
||||
drawings: IFlipDrawingCommandParam[];
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
}
|
||||
/**
|
||||
* The command to flip sheet drawing elements
|
||||
*/
|
||||
export const FlipSheetDrawingCommand: ICommand = {
|
||||
id: 'sheet.command.toggle-flip-drawings',
|
||||
type: CommandType.COMMAND,
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
handler: (accessor: IAccessor, params?: IFlipSheetDrawingCommandParams) => {
|
||||
if (!params) return false;
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
const sheetSkeletonService = accessor.get(SheetSkeletonService);
|
||||
|
||||
const { drawings } = params;
|
||||
const flipH = params.flipH;
|
||||
const flipV = params.flipV;
|
||||
|
||||
const unitIds: string[] = [];
|
||||
const updateParams: any[] = [];
|
||||
|
||||
for (const param of drawings) {
|
||||
const { unitId, subUnitId, drawingId } = param;
|
||||
unitIds.push(unitId);
|
||||
|
||||
const drawingData = sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const existing = drawingData?.[drawingId];
|
||||
if (!existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!skeleton) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const transform = { ...existing.transform } as ITransformState;
|
||||
|
||||
if (flipH) {
|
||||
transform.flipX = !transform.flipX;
|
||||
}
|
||||
if (flipV) {
|
||||
transform.flipY = !transform.flipY;
|
||||
}
|
||||
|
||||
const sheetTransform = transformToDrawingPosition(transform, skeleton);
|
||||
const axisAlignSheetTransform = transformToAxisAlignPosition(transform, skeleton) as ISheetDrawingPosition;
|
||||
|
||||
const updateParamItem: any = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingType: existing.drawingType,
|
||||
drawingId,
|
||||
transform,
|
||||
sheetTransform,
|
||||
axisAlignSheetTransform,
|
||||
};
|
||||
|
||||
const drawingType = existing.drawingType;
|
||||
if (drawingType === DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
const scene = getSceneByDrawingSearch(accessor, unitId);
|
||||
if (scene) {
|
||||
const drawingShapeKey = getDrawingShapeKeyByDrawingSearch({ unitId, subUnitId, drawingId });
|
||||
const imageShape = scene.getObject(drawingShapeKey) as Image;
|
||||
|
||||
if (imageShape) {
|
||||
const srcRect = imageShape.srcRect;
|
||||
if (srcRect) {
|
||||
let newSrcRect;
|
||||
const { left = 0, top = 0, right = 0, bottom = 0 } = srcRect;
|
||||
if (flipH) {
|
||||
const centerX = left + (right - left) / 2;
|
||||
const newLeft = centerX - (right - left) / 2;
|
||||
const newRight = centerX + (right - left) / 2;
|
||||
newSrcRect = {
|
||||
left: newLeft,
|
||||
top,
|
||||
right: newRight,
|
||||
bottom,
|
||||
};
|
||||
}
|
||||
if (flipV) {
|
||||
const centerY = top + (bottom - top) / 2;
|
||||
const newTop = centerY - (bottom - top) / 2;
|
||||
const newBottom = centerY + (bottom - top) / 2;
|
||||
newSrcRect = {
|
||||
left,
|
||||
top: newTop,
|
||||
right,
|
||||
bottom: newBottom,
|
||||
};
|
||||
}
|
||||
|
||||
if (newSrcRect) {
|
||||
updateParamItem.srcRect = newSrcRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updateParams.push(updateParamItem);
|
||||
}
|
||||
|
||||
if (updateParams.length === 0) return false;
|
||||
|
||||
const jsonOp = sheetDrawingService.getBatchUpdateOp(updateParams) as IDrawingJsonUndo1;
|
||||
const { unitId: opUnitId, subUnitId: opSubUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
const updateMutation = { id: SetDrawingApplyMutation.id, params: { unitId: opUnitId, subUnitId: opSubUnitId, op: redo, objects, type: DrawingApplyType.UPDATE } };
|
||||
const undoUpdateMutation = { id: SetDrawingApplyMutation.id, params: { unitId: opUnitId, subUnitId: opSubUnitId, op: undo, objects, type: DrawingApplyType.UPDATE } };
|
||||
|
||||
const result = sequenceExecute([updateMutation], commandService);
|
||||
|
||||
if (result.result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: opUnitId,
|
||||
undoMutations: [undoUpdateMutation, { id: ClearSheetDrawingTransformerOperation.id, params: unitIds }],
|
||||
redoMutations: [updateMutation, { id: ClearSheetDrawingTransformerOperation.id, params: unitIds }],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
function getSceneByDrawingSearch(accessor: IAccessor, unitId: string) {
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const render = renderManagerService.getRenderUnitById(unitId);
|
||||
if (!render) {
|
||||
return null;
|
||||
}
|
||||
return render.scene;
|
||||
}
|
||||
@@ -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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
ClearSheetDrawingTransformerOperation,
|
||||
DrawingApplyType,
|
||||
ISheetDrawingService,
|
||||
SetDrawingApplyMutation,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import { groupToUngroup } from './utils';
|
||||
|
||||
/**
|
||||
* The command to insert new defined name
|
||||
*/
|
||||
export const GroupSheetDrawingCommand: ICommand = {
|
||||
id: 'sheet.command.group-sheet-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: IDrawingGroupUpdateParam[]) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
|
||||
if (!params) return false;
|
||||
|
||||
const unitIds: string[] = [];
|
||||
params.forEach(({ parent, children }) => {
|
||||
unitIds.push(parent.unitId);
|
||||
children.forEach((child) => {
|
||||
unitIds.push(child.unitId);
|
||||
});
|
||||
});
|
||||
|
||||
// execute do mutations and add undo mutations to undo stack if completed
|
||||
const jsonOp = sheetDrawingService.getGroupDrawingOp(params) as IDrawingJsonUndo1;
|
||||
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
const result = commandService.syncExecuteCommand(SetDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.GROUP });
|
||||
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects: groupToUngroup(objects as IDrawingGroupUpdateParam[]), type: DrawingApplyType.UNGROUP } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: unitIds },
|
||||
],
|
||||
redoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.GROUP } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: unitIds },
|
||||
],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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, Nullable } from '@univerjs/core';
|
||||
import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { getCurrentTypeOfRenderer, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetDrawingUpdateController } from '../../controllers/sheet-drawing-update.controller';
|
||||
|
||||
export interface IInsertImageCommandParams {
|
||||
files: Nullable<File[]>;
|
||||
};
|
||||
|
||||
export const InsertFloatImageCommand: ICommand<IInsertImageCommandParams> = {
|
||||
id: 'sheet.command.insert-float-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor, params) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const sheetDrawingUpdateController = getCurrentTypeOfRenderer(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
univerInstanceService,
|
||||
renderManagerService
|
||||
)
|
||||
?.with(SheetDrawingUpdateController);
|
||||
|
||||
if (!sheetDrawingUpdateController) {
|
||||
return false;
|
||||
}
|
||||
const files = params?.files;
|
||||
|
||||
if (files) {
|
||||
const awaitFiles = files.map((file) => sheetDrawingUpdateController.insertFloatImageByFile(file));
|
||||
|
||||
return (await Promise.all(awaitFiles)).every((result) => result);
|
||||
} else {
|
||||
return sheetDrawingUpdateController.insertFloatImage() ?? false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertCellImageCommand: ICommand = {
|
||||
id: 'sheet.command.insert-cell-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
|
||||
return getCurrentTypeOfRenderer(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
univerInstanceService,
|
||||
renderManagerService
|
||||
)
|
||||
?.with(SheetDrawingUpdateController)
|
||||
.insertCellImage() ?? false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 { ISetDrawingCommandParams, ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { CommandType, Direction, ICommandService } from '@univerjs/core';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { ClearSheetDrawingTransformerOperation, ISheetDrawingService, SetSheetDrawingCommand, transformToAxisAlignPosition, transformToDrawingPosition } from '@univerjs/sheets-drawing';
|
||||
import { UndoRedoGroupService } from '@univerjs/ui';
|
||||
|
||||
export interface IMoveDrawingsCommandParams {
|
||||
direction: Direction;
|
||||
}
|
||||
|
||||
export const MoveDrawingsCommand: ICommand = {
|
||||
id: 'sheet.command.move-drawing',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params: IMoveDrawingsCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const drawingManagerService = accessor.get(ISheetDrawingService);
|
||||
const sheetSkeletonService = accessor.get(SheetSkeletonService);
|
||||
const undoRedoGroupService = accessor.get(UndoRedoGroupService);
|
||||
|
||||
const { direction } = params;
|
||||
|
||||
const drawings = drawingManagerService.getFocusDrawings();
|
||||
|
||||
if (drawings.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unitId = drawings[0].unitId;
|
||||
|
||||
const newDrawings = drawings.map((drawing) => {
|
||||
const { transform, unitId, subUnitId } = drawing as ISheetDrawing;
|
||||
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!transform || !skeleton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newTransform = { ...transform };
|
||||
const { left = 0, top = 0 } = transform;
|
||||
|
||||
if (direction === Direction.UP) {
|
||||
newTransform.top = top - 1;
|
||||
} else if (direction === Direction.DOWN) {
|
||||
newTransform.top = top + 1;
|
||||
} else if (direction === Direction.LEFT) {
|
||||
newTransform.left = left - 1;
|
||||
} else if (direction === Direction.RIGHT) {
|
||||
newTransform.left = left + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...drawing,
|
||||
transform: newTransform,
|
||||
sheetTransform: transformToDrawingPosition(newTransform, skeleton),
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(newTransform, skeleton),
|
||||
} as ISheetDrawing;
|
||||
}).filter((drawing) => drawing != null) as ISheetDrawing[];
|
||||
|
||||
const result = undoRedoGroupService.runTimed(
|
||||
unitId,
|
||||
`sheet-drawing:nudge:${drawings[0].subUnitId}:${drawings.map((drawing) => drawing.drawingId).sort().join(',')}`,
|
||||
() => commandService.syncExecuteCommand<ISetDrawingCommandParams>(SetSheetDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: newDrawings,
|
||||
})
|
||||
);
|
||||
|
||||
if (result) {
|
||||
commandService.syncExecuteCommand(ClearSheetDrawingTransformerOperation.id, [unitId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -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 { ICommand } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, LocaleService } from '@univerjs/core';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
import { IBatchSaveImagesService } from '../../services/batch-save-images.service';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from '../../views/batch-save-images/component-name';
|
||||
|
||||
export const SaveCellImagesCommand: ICommand = {
|
||||
id: 'sheet.command.save-cell-images',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const dialogService = accessor.get(IDialogService);
|
||||
const batchSaveService = accessor.get(IBatchSaveImagesService);
|
||||
|
||||
const images = batchSaveService.getCellImagesInSelection();
|
||||
|
||||
// If only one image, download directly without dialog
|
||||
if (images.length === 1) {
|
||||
try {
|
||||
await batchSaveService.downloadSingleImage(images[0]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to download image:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple images: open batch save dialog
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const selectionRange = batchSaveService.getSelectionRangeNotation();
|
||||
const titleText = `${localeService.t<LocaleKey>('sheets-drawing-ui.save.title')} (${selectionRange})`;
|
||||
|
||||
dialogService.open({
|
||||
id: BATCH_SAVE_IMAGES_DIALOG_ID,
|
||||
draggable: true,
|
||||
width: 360,
|
||||
title: { title: titleText },
|
||||
children: {
|
||||
label: BATCH_SAVE_IMAGES_DIALOG_ID,
|
||||
},
|
||||
preservePositionOnDestroy: true,
|
||||
onClose: () => dialogService.close(BATCH_SAVE_IMAGES_DIALOG_ID),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
} from '@univerjs/core';
|
||||
import { ClearSheetDrawingTransformerOperation, DrawingApplyType, ISheetDrawingService, SetDrawingApplyMutation } from '@univerjs/sheets-drawing';
|
||||
import { ungroupToGroup } from './utils';
|
||||
|
||||
/**
|
||||
* The command to insert new defined name
|
||||
*/
|
||||
export const UngroupSheetDrawingCommand: ICommand = {
|
||||
id: 'sheet.command.ungroup-sheet-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: IDrawingGroupUpdateParam[]) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
const sheetDrawingService = accessor.get(ISheetDrawingService);
|
||||
|
||||
if (!params) return false;
|
||||
|
||||
const unitIds: string[] = [];
|
||||
params.forEach(({ parent, children }) => {
|
||||
unitIds.push(parent.unitId);
|
||||
children.forEach((child) => {
|
||||
unitIds.push(child.unitId);
|
||||
});
|
||||
});
|
||||
|
||||
// execute do mutations and add undo mutations to undo stack if completed
|
||||
const jsonOp = sheetDrawingService.getUngroupDrawingOp(params) as IDrawingJsonUndo1;
|
||||
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
const result = commandService.syncExecuteCommand(SetDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.UNGROUP });
|
||||
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects: ungroupToGroup(objects as IDrawingGroupUpdateParam[]), type: DrawingApplyType.GROUP } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: unitIds },
|
||||
],
|
||||
redoMutations: [
|
||||
{ id: SetDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.UNGROUP } },
|
||||
{ id: ClearSheetDrawingTransformerOperation.id, params: unitIds },
|
||||
],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
165
packages/sheets-drawing-ui/src/commands/commands/utils.ts
Normal file
165
packages/sheets-drawing-ui/src/commands/commands/utils.ts
Normal file
@@ -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 { IDrawingGroupNestedIds, IDrawingGroupNestedParam, IDrawingParam } from '@univerjs/core';
|
||||
import type { IDrawingGroupUpdateParam } from '@univerjs/drawing';
|
||||
import { DrawingTypeEnum, generateRandomId } from '@univerjs/core';
|
||||
import { getGroupState, transformObjectOutOfGroup } from '@univerjs/engine-render';
|
||||
|
||||
export function ungroupToGroup(ungroupParams: IDrawingGroupUpdateParam[]) {
|
||||
const newGroupParams: IDrawingGroupUpdateParam[] = [];
|
||||
|
||||
ungroupParams.forEach((ungroupParam) => {
|
||||
const { parent, children } = ungroupParam;
|
||||
const { unitId, subUnitId, drawingId: groupId } = parent;
|
||||
const groupTransform = getGroupState(0, 0, children.map((o) => o.transform || {}));
|
||||
|
||||
const newChildren = children.map((drawing) => {
|
||||
const transform = drawing.transform || { left: 0, top: 0 };
|
||||
const { unitId, subUnitId, drawingId } = drawing;
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
transform: {
|
||||
...transform,
|
||||
left: transform.left!,
|
||||
top: transform.top!,
|
||||
},
|
||||
groupId,
|
||||
};
|
||||
}) as IDrawingParam[];
|
||||
|
||||
const groupParam = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: groupId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
groupBaseBound: { ...parent.groupBaseBound },
|
||||
transform: groupTransform,
|
||||
} as IDrawingParam;
|
||||
|
||||
newGroupParams.push({
|
||||
parent: groupParam,
|
||||
children: newChildren,
|
||||
});
|
||||
});
|
||||
|
||||
return newGroupParams;
|
||||
}
|
||||
|
||||
export function groupToUngroup(groupParams: IDrawingGroupUpdateParam[]) {
|
||||
const newGroupParams: IDrawingGroupUpdateParam[] = [];
|
||||
|
||||
groupParams.forEach((groupParam) => {
|
||||
const { parent, children } = groupParam;
|
||||
const { unitId, subUnitId, drawingId: groupId, transform: groupTransform = { width: 0, height: 0 } } = parent;
|
||||
if (groupTransform == null) {
|
||||
return;
|
||||
}
|
||||
const newChildren = children.map((object) => {
|
||||
const { transform } = object;
|
||||
const { unitId, subUnitId, drawingId } = object;
|
||||
const newTransform = transformObjectOutOfGroup(transform || {}, groupTransform, groupTransform.width || 0, groupTransform.height || 0, parent.groupBaseBound);
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
transform: newTransform,
|
||||
groupId: undefined,
|
||||
};
|
||||
}) as IDrawingParam[];
|
||||
|
||||
const ungroupParam = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: groupId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
} as IDrawingParam;
|
||||
|
||||
newGroupParams.push({
|
||||
parent: ungroupParam,
|
||||
children: newChildren,
|
||||
});
|
||||
});
|
||||
|
||||
return newGroupParams;
|
||||
}
|
||||
|
||||
export interface ICloneGroupResult {
|
||||
cloned: IDrawingGroupNestedParam;
|
||||
/** Map from original drawingId → new drawingId, for updating connector/line relations */
|
||||
idMap: Map<string, string>;
|
||||
}
|
||||
|
||||
// only paste can use this function
|
||||
export function cloneGroupParams(groupParams: IDrawingGroupNestedParam): ICloneGroupResult {
|
||||
const idMap = new Map<string, string>();
|
||||
|
||||
groupParams.flatChildren?.forEach((p) => idMap.set(p.drawingId, generateRandomId(10)));
|
||||
groupParams.groups.forEach((p) => idMap.set(p.drawingId, generateRandomId(10)));
|
||||
|
||||
const clonedNestedIdRecord: Record<string, IDrawingGroupNestedIds> = {};
|
||||
for (const [oldGroupId, entry] of Object.entries(groupParams.nestedIdRecord)) {
|
||||
const newGroupId = idMap.get(oldGroupId) ?? oldGroupId;
|
||||
clonedNestedIdRecord[newGroupId] = {
|
||||
drawingId: newGroupId,
|
||||
children: entry.children?.map((id) => idMap.get(id) ?? id),
|
||||
};
|
||||
}
|
||||
|
||||
const flatChildren: IDrawingParam[] = [];
|
||||
const groups: IDrawingParam[] = [];
|
||||
|
||||
for (const group of groupParams.groups) {
|
||||
const groupDrawingId = idMap.get(group.drawingId) ?? group.drawingId;
|
||||
const parentGroupId = group.groupId ? idMap.get(group.groupId) ?? group.groupId : undefined;
|
||||
groups.push(cloneDrawingParam(group, groupDrawingId, parentGroupId));
|
||||
}
|
||||
|
||||
for (const child of groupParams.flatChildren || []) {
|
||||
const childDrawingId = idMap.get(child.drawingId) ?? child.drawingId;
|
||||
const parentGroupId = child.groupId ? idMap.get(child.groupId) ?? child.groupId : undefined;
|
||||
flatChildren.push(cloneDrawingParam(child, childDrawingId, parentGroupId));
|
||||
}
|
||||
|
||||
return {
|
||||
cloned: {
|
||||
nestedIdRecord: clonedNestedIdRecord,
|
||||
flatChildren,
|
||||
groups,
|
||||
},
|
||||
idMap,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneDrawingParam(param: IDrawingParam, newDrawingId: string, parentGroupId: string | undefined): IDrawingParam {
|
||||
const newParam = { ...param };
|
||||
if (newDrawingId) {
|
||||
newParam.drawingId = newDrawingId;
|
||||
}
|
||||
if (parentGroupId) {
|
||||
newParam.groupId = parentGroupId;
|
||||
} else {
|
||||
delete newParam.groupId;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(newParam)) as IDrawingParam;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 { ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { DrawingTypeEnum, ImageSourceType } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { InsertSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { EditSheetDrawingOperation } from '../edit-sheet-drawing.operation';
|
||||
import { SidebarSheetDrawingOperation } from '../open-drawing-panel.operation';
|
||||
|
||||
function createSheetDrawing(drawingId: string): ISheetDrawing {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
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, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('EditSheetDrawingOperation', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
});
|
||||
|
||||
it('keeps focus and sidebar unchanged when no drawing is provided', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const drawingManagerService = testBed.get(IDrawingManagerService);
|
||||
testBed.commandService.registerCommand(EditSheetDrawingOperation);
|
||||
testBed.commandService.registerCommand(SidebarSheetDrawingOperation);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(EditSheetDrawingOperation.id, null as never)).toBe(false);
|
||||
expect(drawingManagerService.getFocusDrawings()).toEqual([]);
|
||||
expect(testBed.sidebarService.visible).toBe(false);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('selects a drawing and opens the sheet drawing editor', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const drawingManagerService = testBed.get(IDrawingManagerService);
|
||||
testBed.commandService.registerCommand(EditSheetDrawingOperation);
|
||||
testBed.commandService.registerCommand(SidebarSheetDrawingOperation);
|
||||
|
||||
await testBed.commandService.executeCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: testBed.unitId,
|
||||
drawings: [createSheetDrawing('drawing-1')],
|
||||
});
|
||||
|
||||
expect(await testBed.commandService.executeCommand(EditSheetDrawingOperation.id, {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
})).toBe(true);
|
||||
|
||||
expect(drawingManagerService.getFocusDrawings()).toEqual([expect.objectContaining({
|
||||
drawingId: 'drawing-1',
|
||||
})]);
|
||||
expect(testBed.sidebarService.visible).toBe(true);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { COMPONENT_SHEET_DRAWING_PANEL } from '../../../views/sheet-image-panel/component-name';
|
||||
import { SidebarSheetDrawingOperation } from '../open-drawing-panel.operation';
|
||||
|
||||
describe('SidebarSheetDrawingOperation', () => {
|
||||
afterEach(() => {
|
||||
// each test disposes its own univer instance
|
||||
});
|
||||
|
||||
it('opens the sheet drawing panel for the active worksheet', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
testBed.commandService.registerCommand(SidebarSheetDrawingOperation);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(SidebarSheetDrawingOperation.id, { value: 'open' })).toBe(true);
|
||||
|
||||
expect(testBed.sidebarService.visible).toBe(true);
|
||||
expect(testBed.sidebarService.options).toMatchObject({
|
||||
id: COMPONENT_SHEET_DRAWING_PANEL,
|
||||
header: { title: 'sheets-drawing-ui.panel.title' },
|
||||
children: { label: COMPONENT_SHEET_DRAWING_PANEL },
|
||||
width: 360,
|
||||
});
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('clears the focused drawing when the panel is closed', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
const drawingManagerService = testBed.get(IDrawingManagerService);
|
||||
testBed.commandService.registerCommand(SidebarSheetDrawingOperation);
|
||||
|
||||
drawingManagerService.registerDrawingData(testBed.unitId, {
|
||||
[testBed.subUnitId]: {
|
||||
data: {
|
||||
'drawing-1': {
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
},
|
||||
},
|
||||
order: ['drawing-1'],
|
||||
},
|
||||
});
|
||||
drawingManagerService.focusDrawing([{
|
||||
unitId: testBed.unitId,
|
||||
subUnitId: testBed.subUnitId,
|
||||
drawingId: 'drawing-1',
|
||||
}]);
|
||||
|
||||
await testBed.commandService.executeCommand(SidebarSheetDrawingOperation.id, { value: 'open' });
|
||||
testBed.sidebarService.close();
|
||||
|
||||
expect(drawingManagerService.getFocusDrawings()).toEqual([]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('closes the panel for non-open actions', async () => {
|
||||
const testBed = createSheetsDrawingUiTestBed();
|
||||
testBed.commandService.registerCommand(SidebarSheetDrawingOperation);
|
||||
|
||||
await testBed.commandService.executeCommand(SidebarSheetDrawingOperation.id, { value: 'open' });
|
||||
expect(testBed.sidebarService.visible).toBe(true);
|
||||
|
||||
expect(await testBed.commandService.executeCommand(SidebarSheetDrawingOperation.id, { value: 'close' })).toBe(true);
|
||||
expect(testBed.sidebarService.visible).toBe(false);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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, IDrawingSearch, IOperation } from '@univerjs/core';
|
||||
import {
|
||||
CommandType,
|
||||
ICommandService,
|
||||
} from '@univerjs/core';
|
||||
import { SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import { SidebarSheetDrawingOperation } from './open-drawing-panel.operation';
|
||||
|
||||
export const EditSheetDrawingOperation: IOperation<IDrawingSearch> = {
|
||||
id: 'sheet.operation.edit-sheet-image',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor: IAccessor, params?: IDrawingSearch) => {
|
||||
// const drawingManagerService = accessor.get(IDrawingManagerService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
commandService.syncExecuteCommand(SetDrawingSelectedOperation.id, [params]);
|
||||
commandService.executeCommand(SidebarSheetDrawingOperation.id, { value: 'open' });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import { CommandType, DrawingTypeEnum, ICommandService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { IDrawingManagerService, SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import { getSheetCommandTarget } from '@univerjs/sheets';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { COMPONENT_SHEET_DRAWING_PANEL } from '../../views/sheet-image-panel/component-name';
|
||||
|
||||
export interface IUIComponentCommandParams {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const SidebarSheetDrawingOperation: ICommand = {
|
||||
id: 'sidebar.operation.sheet-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: IUIComponentCommandParams) => {
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const drawingManagerService = accessor.get(IDrawingManagerService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
switch (params.value) {
|
||||
case 'open':
|
||||
sidebarService.open({
|
||||
id: COMPONENT_SHEET_DRAWING_PANEL,
|
||||
header: { title: localeService.t<LocaleKey>('sheets-drawing-ui.panel.title') },
|
||||
children: { label: COMPONENT_SHEET_DRAWING_PANEL },
|
||||
onClose: () => {
|
||||
const focusedDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusedDrawings.length === 1 && focusedDrawings[0].drawingType === DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
commandService.syncExecuteCommand(SetDrawingSelectedOperation.id, []);
|
||||
}
|
||||
},
|
||||
width: 360,
|
||||
});
|
||||
break;
|
||||
case 'close':
|
||||
default:
|
||||
sidebarService.close(COMPONENT_SHEET_DRAWING_PANEL);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
29
packages/sheets-drawing-ui/src/config/config.ts
Normal file
29
packages/sheets-drawing-ui/src/config/config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { DependencyOverride } from '@univerjs/core';
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const SHEETS_DRAWING_UI_PLUGIN_CONFIG_KEY = 'sheets-drawing-ui.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_DRAWING_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsDrawingUIConfig {
|
||||
menu?: MenuConfig;
|
||||
overrides?: DependencyOverride;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsDrawingUIConfig = {};
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 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 { ContextMenuPosition } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DrawingContextMenuService } from '../../services/drawing-context-menu.service';
|
||||
import { DrawingContextMenuController } from '../drawing-context-menu.controller';
|
||||
|
||||
function createController(options?: {
|
||||
selectedObjects?: Map<string, { oKey: string }>;
|
||||
drawingLookup?: (oKey: string) => unknown;
|
||||
scene?: any;
|
||||
drawingContextMenuService?: DrawingContextMenuService;
|
||||
}) {
|
||||
const changeEnd$ = new Subject<any>();
|
||||
const transformer = {
|
||||
changeEnd$,
|
||||
getSelectedObjectMap: vi.fn(() => options?.selectedObjects ?? new Map([['shape-1', { oKey: 'shape-1' }]])),
|
||||
};
|
||||
const scene = options?.scene ?? {
|
||||
getTransformerByCreate: vi.fn(() => transformer),
|
||||
};
|
||||
const contextMenuService = {
|
||||
triggerContextMenu: vi.fn(),
|
||||
};
|
||||
const drawingContextMenuService = options?.drawingContextMenuService ?? new DrawingContextMenuService();
|
||||
const controller = new DrawingContextMenuController(
|
||||
{ getDrawingOKey: vi.fn(options?.drawingLookup ?? (() => ({ unitId: 'u1' }))) } as any,
|
||||
contextMenuService as any,
|
||||
{ getRenderUnitById: vi.fn(() => ({ scene })) } as any,
|
||||
{ getAllUnitsForType: vi.fn(() => [{ getUnitId: () => 'unit-1' }]) } as any,
|
||||
drawingContextMenuService
|
||||
);
|
||||
|
||||
return { controller, changeEnd$, contextMenuService, drawingContextMenuService, transformer };
|
||||
}
|
||||
|
||||
describe('DrawingContextMenuController', () => {
|
||||
it('opens drawing context menu on right click when all selected objects belong to drawings', () => {
|
||||
const { controller, changeEnd$, contextMenuService } = createController();
|
||||
const event = { button: 2 };
|
||||
|
||||
changeEnd$.next({ event });
|
||||
|
||||
expect(contextMenuService.triggerContextMenu).toHaveBeenCalledWith(event, ContextMenuPosition.DRAWING);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('uses a drawing-type provider for a single selected drawing', () => {
|
||||
const drawingContextMenuService = new DrawingContextMenuService();
|
||||
const provider = {
|
||||
getContextMenuPosition: vi.fn(() => 'chart-context-menu'),
|
||||
};
|
||||
drawingContextMenuService.registerProvider(DrawingTypeEnum.DRAWING_CHART, provider);
|
||||
const drawing = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'chart-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
};
|
||||
const { controller, changeEnd$, contextMenuService } = createController({
|
||||
drawingLookup: () => drawing,
|
||||
drawingContextMenuService,
|
||||
});
|
||||
const event = { button: 2 };
|
||||
|
||||
changeEnd$.next({ event });
|
||||
|
||||
expect(provider.getContextMenuPosition).toHaveBeenCalledWith({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'chart-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
drawing,
|
||||
});
|
||||
expect(contextMenuService.triggerContextMenu).toHaveBeenCalledWith(event, 'chart-context-menu');
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('falls back to the drawing menu when there is no provider', () => {
|
||||
const { controller, changeEnd$, contextMenuService } = createController({
|
||||
drawingLookup: () => ({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'shape-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
|
||||
}),
|
||||
});
|
||||
const event = { button: 2 };
|
||||
|
||||
changeEnd$.next({ event });
|
||||
|
||||
expect(contextMenuService.triggerContextMenu).toHaveBeenCalledWith(event, ContextMenuPosition.DRAWING);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps the drawing menu for multiple selected drawings', () => {
|
||||
const drawingContextMenuService = new DrawingContextMenuService();
|
||||
const provider = {
|
||||
getContextMenuPosition: vi.fn(() => 'chart-context-menu'),
|
||||
};
|
||||
drawingContextMenuService.registerProvider(DrawingTypeEnum.DRAWING_CHART, provider);
|
||||
const { controller, changeEnd$, contextMenuService } = createController({
|
||||
selectedObjects: new Map([
|
||||
['chart-1', { oKey: 'chart-1' }],
|
||||
['chart-2', { oKey: 'chart-2' }],
|
||||
]),
|
||||
drawingLookup: (oKey) => ({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: oKey,
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
}),
|
||||
drawingContextMenuService,
|
||||
});
|
||||
const event = { button: 2 };
|
||||
|
||||
changeEnd$.next({ event });
|
||||
|
||||
expect(provider.getContextMenuPosition).not.toHaveBeenCalled();
|
||||
expect(contextMenuService.triggerContextMenu).toHaveBeenCalledWith(event, ContextMenuPosition.DRAWING);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps newer providers when an older provider is disposed', () => {
|
||||
const drawingContextMenuService = new DrawingContextMenuService();
|
||||
const firstProvider = {
|
||||
getContextMenuPosition: vi.fn(() => 'first-menu'),
|
||||
};
|
||||
const secondProvider = {
|
||||
getContextMenuPosition: vi.fn(() => 'second-menu'),
|
||||
};
|
||||
const firstDisposable = drawingContextMenuService.registerProvider(DrawingTypeEnum.DRAWING_CHART, firstProvider);
|
||||
drawingContextMenuService.registerProvider(DrawingTypeEnum.DRAWING_CHART, secondProvider);
|
||||
|
||||
firstDisposable.dispose();
|
||||
|
||||
expect(drawingContextMenuService.getContextMenuPosition({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'chart-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
drawing: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'chart-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
},
|
||||
})).toBe('second-menu');
|
||||
});
|
||||
|
||||
it('ignores non-right-clicks, empty selections and non-drawing selected objects', () => {
|
||||
const { controller, changeEnd$, contextMenuService, transformer } = createController({
|
||||
selectedObjects: new Map(),
|
||||
});
|
||||
|
||||
changeEnd$.next({ event: { button: 0 } });
|
||||
changeEnd$.next({ event: { button: 2 } });
|
||||
transformer.getSelectedObjectMap.mockReturnValue(new Map([['shape-1', { oKey: 'shape-1' }]]));
|
||||
(controller as any)._drawingManagerService.getDrawingOKey = vi.fn(() => null);
|
||||
changeEnd$.next({ event: { button: 2 } });
|
||||
|
||||
expect(contextMenuService.triggerContextMenu).not.toHaveBeenCalled();
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetCellImageAutofillController } from '../sheet-cell-image-autofill.controller';
|
||||
import { resizeImageByCell } from '../sheet-cell-image.controller';
|
||||
|
||||
vi.mock('../sheet-cell-image.controller', () => ({
|
||||
resizeImageByCell: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('SheetCellImageAutofillController', () => {
|
||||
it('registers an autofill hook that resizes every pasted cell image by its target cell', () => {
|
||||
let hook: any;
|
||||
const disposable = { dispose: vi.fn() };
|
||||
const autoFillService = {
|
||||
addHook: vi.fn((registeredHook) => {
|
||||
hook = registeredHook;
|
||||
return disposable;
|
||||
}),
|
||||
};
|
||||
const injector = { id: 'injector' };
|
||||
const controller = new SheetCellImageAutofillController(autoFillService as any, injector as any);
|
||||
const location = { unitId: 'unit-1', subUnitId: 'sheet-1' };
|
||||
const cellA = { p: { drawingsOrder: ['a'] } };
|
||||
const cellB = { p: { drawingsOrder: ['b'] } };
|
||||
|
||||
hook.onBeforeSubmit(location, 'down', 'copy', {
|
||||
2: {
|
||||
3: cellA,
|
||||
4: cellB,
|
||||
},
|
||||
});
|
||||
|
||||
expect(autoFillService.addHook).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'sheet-cell-image-autofill',
|
||||
onBeforeSubmit: expect.any(Function),
|
||||
}));
|
||||
expect(resizeImageByCell).toHaveBeenCalledWith(injector, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 3,
|
||||
}, cellA);
|
||||
expect(resizeImageByCell).toHaveBeenCalledWith(injector, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 2,
|
||||
col: 4,
|
||||
}, cellB);
|
||||
|
||||
controller.dispose();
|
||||
expect(disposable.dispose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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 { DataStreamTreeTokenType, DOCS_NORMAL_EDITOR_UNIT_ID_KEY } from '@univerjs/core';
|
||||
import { INTERCEPTOR_POINT } from '@univerjs/sheets';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { resizeImageByCell, SheetCellImageController } from '../sheet-cell-image.controller';
|
||||
|
||||
const DRAWING_ID = 'drawing-1';
|
||||
|
||||
function createCellImage(dataStream: string) {
|
||||
return {
|
||||
p: {
|
||||
body: { dataStream },
|
||||
drawingsOrder: [DRAWING_ID],
|
||||
drawings: {
|
||||
[DRAWING_ID]: {
|
||||
transform: { width: 20, height: 10, left: 1, top: 1 },
|
||||
docTransform: {
|
||||
size: { width: 20, height: 10 },
|
||||
angle: 0,
|
||||
positionH: { posOffset: 1 },
|
||||
positionV: { posOffset: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
documentStyle: { pageSize: { width: 20, height: 10 } },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createInjector() {
|
||||
const skeleton = {
|
||||
getCellWithCoordByIndex: vi.fn(() => ({
|
||||
mergeInfo: { startX: 0, endX: 100, startY: 0, endY: 50 },
|
||||
})),
|
||||
};
|
||||
const skeletonManagerService = {
|
||||
getSkeletonParam: vi.fn(() => ({ skeleton })),
|
||||
};
|
||||
const render = {
|
||||
with: vi.fn(() => skeletonManagerService),
|
||||
};
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: vi.fn(() => render),
|
||||
};
|
||||
|
||||
return { get: vi.fn(() => renderManagerService) } as never;
|
||||
}
|
||||
|
||||
describe('SheetCellImageController', () => {
|
||||
it.each([
|
||||
DataStreamTreeTokenType.CUSTOM_BLOCK + DataStreamTreeTokenType.PARAGRAPH + DataStreamTreeTokenType.SECTION_BREAK,
|
||||
DataStreamTreeTokenType.PARAGRAPH + DataStreamTreeTokenType.CUSTOM_BLOCK + DataStreamTreeTokenType.PARAGRAPH + DataStreamTreeTokenType.SECTION_BREAK,
|
||||
])('resizes cell images for supported document structures', (dataStream) => {
|
||||
const cell = createCellImage(dataStream);
|
||||
|
||||
expect(resizeImageByCell(createInjector(), {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 2,
|
||||
}, cell as never)).toBe(true);
|
||||
expect(cell.p.drawings[DRAWING_ID].transform).toEqual({ width: 96, height: 48, left: 0, top: 0 });
|
||||
expect(cell.p.drawings[DRAWING_ID].docTransform.size).toEqual({ width: 96, height: 48 });
|
||||
});
|
||||
|
||||
it('reloads editor drawing data when the embedded editor opens and clears it when closed', () => {
|
||||
let interceptor: any;
|
||||
const visible$ = new Subject<{ visible: boolean }>();
|
||||
const drawingManagerService = {
|
||||
removeDrawingDataForUnit: vi.fn(),
|
||||
initializeNotification: vi.fn(),
|
||||
};
|
||||
const docDrawingController = { loadDrawingDataForUnit: vi.fn() };
|
||||
const controller = new SheetCellImageController(
|
||||
{
|
||||
intercept: vi.fn((point, config) => {
|
||||
expect(point).toBe(INTERCEPTOR_POINT.CELL_CONTENT);
|
||||
interceptor = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as never,
|
||||
{} as never,
|
||||
drawingManagerService as never,
|
||||
docDrawingController as never,
|
||||
{ visible$ } as never
|
||||
);
|
||||
|
||||
visible$.next({ visible: true });
|
||||
expect(drawingManagerService.removeDrawingDataForUnit).toHaveBeenCalledWith(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
expect(docDrawingController.loadDrawingDataForUnit).toHaveBeenCalledWith(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
expect(drawingManagerService.initializeNotification).toHaveBeenCalledWith(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
|
||||
visible$.next({ visible: false });
|
||||
expect(drawingManagerService.removeDrawingDataForUnit).toHaveBeenCalledTimes(2);
|
||||
|
||||
const rawCell = {
|
||||
p: {
|
||||
body: { dataStream: 'image\r\n' },
|
||||
drawingsOrder: ['drawing-1'],
|
||||
},
|
||||
};
|
||||
const result = interceptor.handler(rawCell, {
|
||||
rawData: rawCell,
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
row: 1,
|
||||
col: 2,
|
||||
}, (cell: unknown) => cell);
|
||||
|
||||
expect(result).not.toBe(rawCell);
|
||||
expect(result.interceptorStyle.tr).toEqual({ a: 0 });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,438 @@
|
||||
/**
|
||||
* 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 { IRange } from '@univerjs/core';
|
||||
import type { ISheetClipboardHook } from '@univerjs/sheets-ui';
|
||||
import { DrawingTypeEnum, ICommandService, ImageSourceType, Injector, ObjectMatrix } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { RemoveSheetDrawingCommand, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
|
||||
import { COPY_TYPE, ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
|
||||
import { IClipboardInterfaceService } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { InsertFloatImageCommand } from '../../commands/commands/insert-image.command';
|
||||
import { SheetsDrawingCopyPasteController } from '../sheet-drawing-copy-paste.controller';
|
||||
|
||||
function createSkeleton() {
|
||||
return {
|
||||
getNoMergeCellWithCoordByIndex: vi.fn((row: number, column: number) => ({
|
||||
startX: column * 10,
|
||||
endX: column * 10 + 10,
|
||||
startY: row * 20,
|
||||
endY: row * 20 + 20,
|
||||
})),
|
||||
getCellIndexAndOffsetByPosition: vi.fn((left: number, top: number) => {
|
||||
const column = Math.floor(left / 10);
|
||||
const row = Math.floor(top / 20);
|
||||
return {
|
||||
column,
|
||||
row,
|
||||
columnOffset: left - column * 10,
|
||||
rowOffset: top - row * 20,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createImageDrawing(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'image-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
source: 'data:image/png;base64,AA==',
|
||||
anchorType: SheetDrawingAnchorType.Both,
|
||||
transform: { left: 5, top: 5, width: 10, height: 20 },
|
||||
sheetTransform: {
|
||||
from: { row: 0, column: 0, rowOffset: 5, columnOffset: 5 },
|
||||
to: { row: 1, column: 1, rowOffset: 5, columnOffset: 5 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: { row: 0, column: 0, rowOffset: 5, columnOffset: 5 },
|
||||
to: { row: 1, column: 1, rowOffset: 5, columnOffset: 5 },
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
type IImageDrawing = ReturnType<typeof createImageDrawing>;
|
||||
|
||||
interface IPrivateControllerAccess {
|
||||
_copyInfo: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
copyRange?: IRange;
|
||||
drawings: IImageDrawing[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface ITestClipboardHook extends ISheetClipboardHook {
|
||||
onBeforeCopy: NonNullable<ISheetClipboardHook['onBeforeCopy']>;
|
||||
onBeforeCopyFocusedObject?: (unitId: string, subUnitId: string, copyType: COPY_TYPE) => boolean;
|
||||
onPasteCells: NonNullable<ISheetClipboardHook['onPasteCells']>;
|
||||
onPasteFiles: NonNullable<ISheetClipboardHook['onPasteFiles']>;
|
||||
onPasteUnrecognized: NonNullable<ISheetClipboardHook['onPasteUnrecognized']>;
|
||||
}
|
||||
|
||||
function createController(options?: { focusedDrawings?: IImageDrawing[]; drawingData?: Record<string, object> }) {
|
||||
let hook: ISheetClipboardHook | undefined;
|
||||
const skeleton = createSkeleton();
|
||||
const drawingService = {
|
||||
getFocusDrawings: vi.fn(() => options?.focusedDrawings ?? []),
|
||||
getDrawingData: vi.fn(() => options?.drawingData ?? {}),
|
||||
getBatchAddOp: vi.fn((drawings: IImageDrawing[]) => ({ undo: 'add-undo', redo: 'add-redo', objects: drawings })),
|
||||
getBatchUpdateOp: vi.fn((drawings: IImageDrawing[]) => ({ undo: 'update-undo', redo: 'update-redo', objects: drawings })),
|
||||
};
|
||||
const commandService = {
|
||||
executeCommand: vi.fn(),
|
||||
};
|
||||
const sheetClipboardService = {
|
||||
addClipboardHook: vi.fn((config: ISheetClipboardHook) => {
|
||||
hook = config;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
};
|
||||
|
||||
const injector = new Injector([
|
||||
[ISheetClipboardService, { useValue: sheetClipboardService }],
|
||||
[IRenderManagerService, { useValue: {} }],
|
||||
[SheetSkeletonService, { useValue: { getSkeleton: vi.fn(() => skeleton) } }],
|
||||
[IDrawingManagerService, { useValue: drawingService }],
|
||||
[IClipboardInterfaceService, { useValue: { writeText: vi.fn() } }],
|
||||
[ICommandService, { useValue: commandService }],
|
||||
[SheetsDrawingCopyPasteController],
|
||||
]);
|
||||
const controller = injector.get(SheetsDrawingCopyPasteController);
|
||||
|
||||
return { controller, hook: hook as ITestClipboardHook, skeleton, drawingService, commandService };
|
||||
}
|
||||
|
||||
describe('SheetsDrawingCopyPasteController', () => {
|
||||
it('copies default position-anchored images contained in a cell range', () => {
|
||||
const positionOnlyDrawing = createImageDrawing({
|
||||
drawingId: 'position-only',
|
||||
anchorType: undefined,
|
||||
});
|
||||
const { controller, hook, drawingService } = createController({
|
||||
drawingData: {
|
||||
[positionOnlyDrawing.drawingId]: positionOnlyDrawing,
|
||||
},
|
||||
});
|
||||
|
||||
hook.onBeforeCopy('unit-1', 'sheet-1', {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
}, COPY_TYPE.COPY);
|
||||
hook.onPasteCells(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0, 1], cols: [0, 1] },
|
||||
},
|
||||
{
|
||||
unitId: 'unit-2',
|
||||
subUnitId: 'sheet-2',
|
||||
range: { rows: [2, 3], cols: [3, 4] },
|
||||
},
|
||||
new ObjectMatrix(),
|
||||
{ copyId: 'range-copy', copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(drawingService.getBatchAddOp).toHaveBeenCalledTimes(1);
|
||||
expect(drawingService.getBatchAddOp.mock.calls[0][0]).toMatchObject([{
|
||||
unitId: 'unit-2',
|
||||
subUnitId: 'sheet-2',
|
||||
transform: { left: 35, top: 45, width: 10, height: 20 },
|
||||
}]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('copies drawings contained in a cell range and pastes them with the range offset', () => {
|
||||
const containedDrawing = createImageDrawing();
|
||||
const outsideDrawing = createImageDrawing({
|
||||
drawingId: 'outside-image',
|
||||
transform: { left: 80, top: 80, width: 10, height: 20 },
|
||||
});
|
||||
const positionOnlyDrawing = createImageDrawing({
|
||||
drawingId: 'position-only',
|
||||
anchorType: SheetDrawingAnchorType.Position,
|
||||
transform: { left: 50, top: 50, width: 10, height: 20 },
|
||||
});
|
||||
const unanchoredDrawing = createImageDrawing({
|
||||
drawingId: 'unanchored',
|
||||
anchorType: SheetDrawingAnchorType.None,
|
||||
});
|
||||
const { controller, hook, drawingService } = createController({
|
||||
focusedDrawings: [outsideDrawing],
|
||||
drawingData: {
|
||||
[containedDrawing.drawingId]: containedDrawing,
|
||||
[outsideDrawing.drawingId]: outsideDrawing,
|
||||
[positionOnlyDrawing.drawingId]: positionOnlyDrawing,
|
||||
[unanchoredDrawing.drawingId]: unanchoredDrawing,
|
||||
chart: { drawingId: 'chart', drawingType: DrawingTypeEnum.DRAWING_CHART },
|
||||
},
|
||||
});
|
||||
|
||||
hook.onBeforeCopy('unit-1', 'sheet-1', {
|
||||
startRow: 0,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
}, COPY_TYPE.COPY);
|
||||
const mutations = hook.onPasteCells(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0, 1], cols: [0, 1] },
|
||||
},
|
||||
{
|
||||
unitId: 'unit-2',
|
||||
subUnitId: 'sheet-2',
|
||||
range: { rows: [2, 3], cols: [3, 4] },
|
||||
},
|
||||
new ObjectMatrix(),
|
||||
{ copyId: 'range-copy', copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(drawingService.getBatchAddOp).toHaveBeenCalledTimes(1);
|
||||
const pastedDrawing = drawingService.getBatchAddOp.mock.calls[0][0][0];
|
||||
expect(pastedDrawing).toMatchObject({
|
||||
unitId: 'unit-2',
|
||||
subUnitId: 'sheet-2',
|
||||
transform: { left: 35, top: 45, width: 10, height: 20 },
|
||||
sheetTransform: {
|
||||
from: { row: 2, column: 3, rowOffset: 5, columnOffset: 5 },
|
||||
to: { row: 3, column: 4, rowOffset: 5, columnOffset: 5 },
|
||||
},
|
||||
});
|
||||
expect(pastedDrawing.drawingId).not.toBe(containedDrawing.drawingId);
|
||||
expect(mutations.redos).toEqual([
|
||||
expect.objectContaining({ params: expect.objectContaining({ op: 'add-redo', objects: [pastedDrawing] }) }),
|
||||
]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('cuts a focused image drawing and pastes it as a new drawing at the target range', () => {
|
||||
const focusedDrawing = createImageDrawing({ drawingId: 'focused-image' });
|
||||
const { controller, hook, drawingService, commandService } = createController({
|
||||
focusedDrawings: [focusedDrawing],
|
||||
});
|
||||
|
||||
expect(hook.onBeforeCopyFocusedObject).toBeTypeOf('function');
|
||||
expect(hook.onBeforeCopyFocusedObject?.('unit-1', 'sheet-1', COPY_TYPE.CUT)).toBe(true);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(RemoveSheetDrawingCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
drawings: [focusedDrawing],
|
||||
});
|
||||
|
||||
const mutations = hook.onPasteCells(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
},
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [3], cols: [4] },
|
||||
},
|
||||
new ObjectMatrix(),
|
||||
{ copyId: 'image-cut', copyType: COPY_TYPE.CUT, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(drawingService.getBatchAddOp).toHaveBeenCalledTimes(1);
|
||||
const pastedDrawing = drawingService.getBatchAddOp.mock.calls[0][0][0];
|
||||
expect(pastedDrawing).toMatchObject({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
transform: { left: 40, top: 60, width: 10, height: 20 },
|
||||
});
|
||||
expect(pastedDrawing.drawingId).not.toBe(focusedDrawing.drawingId);
|
||||
expect(mutations.redos).toEqual([
|
||||
expect.objectContaining({ params: expect.objectContaining({ op: 'add-redo', objects: [pastedDrawing] }) }),
|
||||
]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('copies all focused image drawings', () => {
|
||||
const firstDrawing = createImageDrawing({ drawingId: 'first-image' });
|
||||
const secondDrawing = createImageDrawing({
|
||||
drawingId: 'second-image',
|
||||
transform: { left: 30, top: 40, width: 10, height: 20 },
|
||||
});
|
||||
const { controller, hook, drawingService } = createController({
|
||||
focusedDrawings: [firstDrawing, secondDrawing],
|
||||
});
|
||||
|
||||
expect(hook.onBeforeCopyFocusedObject?.('unit-1', 'sheet-1', COPY_TYPE.COPY)).toBe(true);
|
||||
|
||||
const mutations = hook.onPasteFiles(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [3], cols: [4] },
|
||||
},
|
||||
[new File(['clipboard'], 'clipboard-image.png', { type: 'image/png' })],
|
||||
{ pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(drawingService.getBatchAddOp).toHaveBeenCalledTimes(2);
|
||||
expect(drawingService.getBatchAddOp.mock.calls.map(([drawings]) => drawings[0].transform)).toEqual([
|
||||
expect.objectContaining({ left: 40, top: 60 }),
|
||||
expect.objectContaining({ left: 65, top: 95 }),
|
||||
]);
|
||||
expect(mutations.redos).toHaveLength(2);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('pastes external image files instead of the previous copied drawing', () => {
|
||||
const focusedDrawing = createImageDrawing({ drawingId: 'focused-image' });
|
||||
const { controller, hook } = createController({
|
||||
focusedDrawings: [focusedDrawing],
|
||||
});
|
||||
const internalImage = new File(['internal'], 'clipboard-image.png', { type: 'image/png' });
|
||||
const externalImage = new File(['external'], 'external.png', { type: 'image/png' });
|
||||
|
||||
(controller as unknown as IPrivateControllerAccess)._copyInfo = {
|
||||
unitId: focusedDrawing.unitId,
|
||||
subUnitId: focusedDrawing.subUnitId,
|
||||
drawings: [focusedDrawing],
|
||||
};
|
||||
|
||||
hook.onPasteFiles(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
},
|
||||
[internalImage],
|
||||
{ pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
const mutations = hook.onPasteFiles(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
},
|
||||
[externalImage],
|
||||
{ pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(mutations).toEqual({
|
||||
undos: [],
|
||||
redos: [{
|
||||
id: InsertFloatImageCommand.id,
|
||||
params: { files: [externalImage] },
|
||||
}],
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('clears a copied image after pasting external cell content', () => {
|
||||
const focusedDrawing = createImageDrawing({ drawingId: 'focused-image' });
|
||||
const { controller, hook } = createController({
|
||||
focusedDrawings: [focusedDrawing],
|
||||
});
|
||||
|
||||
(controller as unknown as IPrivateControllerAccess)._copyInfo = {
|
||||
unitId: focusedDrawing.unitId,
|
||||
subUnitId: focusedDrawing.subUnitId,
|
||||
drawings: [focusedDrawing],
|
||||
};
|
||||
|
||||
const mutations = hook.onPasteCells(
|
||||
null,
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
},
|
||||
new ObjectMatrix(),
|
||||
{ copyType: COPY_TYPE.COPY, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(mutations).toEqual({ undos: [], redos: [] });
|
||||
|
||||
const externalImage = new File(['external'], 'external.png', { type: 'image/png' });
|
||||
expect(hook.onPasteFiles(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
},
|
||||
[externalImage],
|
||||
{ pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
).redos).toEqual([{
|
||||
id: InsertFloatImageCommand.id,
|
||||
params: { files: [externalImage] },
|
||||
}]);
|
||||
|
||||
expect(hook.onPasteUnrecognized({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
})).toEqual({ undos: [], redos: [] });
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('clears a copied image after an unrecognized internal paste', () => {
|
||||
const focusedDrawing = createImageDrawing({ drawingId: 'focused-image' });
|
||||
const { controller, hook } = createController({
|
||||
focusedDrawings: [focusedDrawing],
|
||||
});
|
||||
const externalImage = new File(['external'], 'external.png', { type: 'image/png' });
|
||||
|
||||
(controller as unknown as IPrivateControllerAccess)._copyInfo = {
|
||||
unitId: focusedDrawing.unitId,
|
||||
subUnitId: focusedDrawing.subUnitId,
|
||||
drawings: [focusedDrawing],
|
||||
};
|
||||
|
||||
hook.onPasteUnrecognized({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
});
|
||||
|
||||
const mutations = hook.onPasteFiles(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
range: { rows: [0], cols: [0] },
|
||||
},
|
||||
[externalImage],
|
||||
{ pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }
|
||||
);
|
||||
|
||||
expect(mutations.redos).toEqual([{
|
||||
id: InsertFloatImageCommand.id,
|
||||
params: { files: [externalImage] },
|
||||
}]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 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 { ObjectType } from '@univerjs/engine-render';
|
||||
import { InsertSheetDrawingCommand, SetDrawingArrangeCommand, SetSheetDrawingPlacementCommand } from '@univerjs/sheets-drawing';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetDrawingPermissionController } from '../sheet-drawing-permission.controller';
|
||||
|
||||
function createController() {
|
||||
let beforeCommandHandler: ((command: { id: string; params?: unknown }) => void) | undefined;
|
||||
const viewPermission$ = new Subject<Array<{ value: boolean }>>();
|
||||
const editPermission$ = new Subject<Array<{ value: boolean }>>();
|
||||
const activeSheet$ = new BehaviorSubject({ getSheetId: () => 'sheet-1' });
|
||||
const workbook = {
|
||||
activeSheet$,
|
||||
getUnitId: () => 'unit-1',
|
||||
getActiveSheet: () => activeSheet$.value,
|
||||
};
|
||||
const workbook$ = new BehaviorSubject(workbook);
|
||||
const drawingObject = { objectType: ObjectType.IMAGE, oKey: 'image-drawing-1' };
|
||||
const unrelatedObject = { objectType: ObjectType.IMAGE, oKey: 'image-other' };
|
||||
const transformer = { clearSelectedObjects: vi.fn() };
|
||||
const scene = {
|
||||
getAllObjectsByOrder: vi.fn(() => [drawingObject, unrelatedObject]),
|
||||
getTransformerByCreate: vi.fn(() => transformer),
|
||||
removeObject: vi.fn(),
|
||||
attachTransformerTo: vi.fn(),
|
||||
detachTransformerFrom: vi.fn(),
|
||||
};
|
||||
const sheetDrawingService = {
|
||||
setDrawingVisible: vi.fn(),
|
||||
setDrawingEditable: vi.fn(),
|
||||
getDrawingData: vi.fn(() => ({
|
||||
'drawing-1': { drawingId: 'drawing-1' },
|
||||
})),
|
||||
addNotification: vi.fn(),
|
||||
};
|
||||
let composePermissionCall = 0;
|
||||
const permissionService = {
|
||||
composePermission: vi.fn(() => [{ value: true }]),
|
||||
composePermission$: vi.fn(() => {
|
||||
composePermissionCall += 1;
|
||||
return composePermissionCall === 1 ? viewPermission$ : editPermission$;
|
||||
}),
|
||||
};
|
||||
const sheetPermissionCheckController = {
|
||||
permissionCheckWithoutRange: vi.fn(() => false),
|
||||
blockExecuteWithoutPermission: vi.fn(),
|
||||
};
|
||||
|
||||
const controller = new SheetDrawingPermissionController(
|
||||
{
|
||||
beforeCommandExecuted: vi.fn((handler) => {
|
||||
beforeCommandHandler = handler;
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
} as never,
|
||||
{ t: (key: string) => key } as never,
|
||||
{ getRenderUnitById: vi.fn(() => ({ scene })) } as never,
|
||||
permissionService as never,
|
||||
{
|
||||
getCurrentTypeOfUnit$: vi.fn(() => workbook$),
|
||||
getCurrentUnitOfType: vi.fn(() => workbook),
|
||||
} as never,
|
||||
{ currentUser$: new BehaviorSubject({ userID: 'user-1' }) } as never,
|
||||
sheetPermissionCheckController as never,
|
||||
sheetDrawingService as never
|
||||
);
|
||||
|
||||
return {
|
||||
controller,
|
||||
viewPermission$,
|
||||
editPermission$,
|
||||
scene,
|
||||
transformer,
|
||||
drawingObject,
|
||||
unrelatedObject,
|
||||
sheetDrawingService,
|
||||
beforeCommandHandler: () => beforeCommandHandler,
|
||||
sheetPermissionCheckController,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SheetDrawingPermissionController', () => {
|
||||
it('hides sheet drawings and removes rendered drawing objects when view permission is revoked', () => {
|
||||
const { controller, viewPermission$, scene, transformer, drawingObject, unrelatedObject, sheetDrawingService } = createController();
|
||||
|
||||
viewPermission$.next([{ value: false }]);
|
||||
|
||||
expect(sheetDrawingService.setDrawingVisible).toHaveBeenLastCalledWith(false);
|
||||
expect(scene.removeObject).toHaveBeenCalledWith(drawingObject);
|
||||
expect(scene.removeObject).not.toHaveBeenCalledWith(unrelatedObject);
|
||||
expect(transformer.clearSelectedObjects).toHaveBeenCalled();
|
||||
|
||||
viewPermission$.next([{ value: true }]);
|
||||
|
||||
expect(sheetDrawingService.setDrawingVisible).toHaveBeenLastCalledWith(true);
|
||||
expect(sheetDrawingService.addNotification).toHaveBeenCalledWith([{ drawingId: 'drawing-1' }]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('detaches and reattaches transformers when edit permission changes, and blocks protected commands', () => {
|
||||
const {
|
||||
controller,
|
||||
editPermission$,
|
||||
scene,
|
||||
transformer,
|
||||
drawingObject,
|
||||
sheetDrawingService,
|
||||
beforeCommandHandler,
|
||||
sheetPermissionCheckController,
|
||||
} = createController();
|
||||
|
||||
editPermission$.next([{ value: false }]);
|
||||
|
||||
expect(sheetDrawingService.setDrawingEditable).toHaveBeenLastCalledWith(false);
|
||||
expect(scene.detachTransformerFrom).toHaveBeenCalledWith(drawingObject);
|
||||
expect(transformer.clearSelectedObjects).toHaveBeenCalled();
|
||||
|
||||
editPermission$.next([{ value: true }]);
|
||||
|
||||
expect(sheetDrawingService.setDrawingEditable).toHaveBeenLastCalledWith(true);
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(drawingObject);
|
||||
expect(sheetDrawingService.addNotification).toHaveBeenCalledWith([{ drawingId: 'drawing-1' }]);
|
||||
|
||||
beforeCommandHandler()?.({
|
||||
id: InsertSheetDrawingCommand.id,
|
||||
params: {
|
||||
drawings: [{ unitId: 'unit-1', subUnitId: 'sheet-1' }],
|
||||
},
|
||||
});
|
||||
beforeCommandHandler()?.({
|
||||
id: SetDrawingArrangeCommand.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
},
|
||||
});
|
||||
beforeCommandHandler()?.({
|
||||
id: SetSheetDrawingPlacementCommand.id,
|
||||
params: {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawings: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(sheetPermissionCheckController.permissionCheckWithoutRange).toHaveBeenCalledTimes(3);
|
||||
expect(sheetPermissionCheckController.blockExecuteWithoutPermission).toHaveBeenCalledWith('sheets-drawing-ui.permission.dialog.editErr');
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 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 { describe, expect, it, vi } from 'vitest';
|
||||
import { mountPrintingFloatDom } from '../../views/PrintingFloatDom';
|
||||
import { SheetDrawingPrintingController } from '../sheet-drawing-printing.controller';
|
||||
|
||||
vi.mock('../../views/PrintingFloatDom', () => ({
|
||||
mountPrintingFloatDom: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
function createController(options?: { hasDrawingData?: boolean }) {
|
||||
const interceptors = new Map<string, any>();
|
||||
const points = {
|
||||
PRINTING_COMPONENT_COLLECT: 'PRINTING_COMPONENT_COLLECT',
|
||||
PRINTING_RANGE: 'PRINTING_RANGE',
|
||||
PRINTING_DOM_COLLECT: 'PRINTING_DOM_COLLECT',
|
||||
};
|
||||
const drawingData = {
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
order: ['image-1', 'chart-1', 'dom-1', 'block-1', 'grouped-image', 'hidden-image', 'hidden-chart'],
|
||||
data: {
|
||||
'image-1': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'image-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
transform: { left: 20, top: 30, width: 120, height: 60 },
|
||||
},
|
||||
'chart-1': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'chart-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
transform: { left: 40, top: 50, width: 120, height: 60 },
|
||||
},
|
||||
'dom-1': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'dom-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
componentKey: 'custom-dom',
|
||||
transform: { left: 80, top: 90, width: 40, height: 20 },
|
||||
},
|
||||
'block-1': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'block-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_BLOCK,
|
||||
componentKey: 'custom-block',
|
||||
transform: { left: 80, top: 80, width: 20, height: 20 },
|
||||
},
|
||||
'grouped-image': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'grouped-image',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
groupId: 'group-1',
|
||||
transform: { left: -100, top: -100, width: 20, height: 20 },
|
||||
},
|
||||
'hidden-image': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'hidden-image',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
hidden: true,
|
||||
transform: { left: 1000, top: 1000, width: 500, height: 500 },
|
||||
},
|
||||
'hidden-chart': {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'hidden-chart',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
hidden: true,
|
||||
transform: { left: 1000, top: 1000, width: 500, height: 500 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const render = {
|
||||
scene: { scaleX: 1, scaleY: 1 },
|
||||
with: vi.fn(() => ({
|
||||
getSkeletonParam: vi.fn(() => ({
|
||||
skeleton: {
|
||||
getCellIndexByOffset: vi.fn((x: number, y: number) => ({
|
||||
row: Math.floor(y / 10),
|
||||
column: Math.floor(x / 10),
|
||||
})),
|
||||
},
|
||||
})),
|
||||
})),
|
||||
};
|
||||
const controller = new SheetDrawingPrintingController(
|
||||
{
|
||||
getPrintComponent: vi.fn((key: string) => `${key}-print`),
|
||||
interceptor: {
|
||||
getInterceptPoints: vi.fn(() => points),
|
||||
intercept: vi.fn((point, config) => {
|
||||
interceptors.set(point, config);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
},
|
||||
} as never,
|
||||
{ renderDrawing: vi.fn(() => Promise.resolve()) } as never,
|
||||
{ getDrawingDataForUnit: vi.fn(() => options?.hasDrawingData === false ? undefined : drawingData['unit-1']) } as never,
|
||||
{ getRenderUnitById: vi.fn(() => render) } as never,
|
||||
{
|
||||
get: vi.fn((key: string) => `${key}-Component`),
|
||||
} as never,
|
||||
{} as never
|
||||
);
|
||||
|
||||
return { controller, interceptors, points };
|
||||
}
|
||||
|
||||
describe('SheetDrawingPrintingController', () => {
|
||||
it('renders printable images while excluding charts and floating DOM drawings', () => {
|
||||
const { controller, interceptors, points } = createController();
|
||||
const scene = {};
|
||||
const resourceCollector = { add: vi.fn() };
|
||||
const next = vi.fn(() => 'next-result');
|
||||
|
||||
const result = interceptors.get(points.PRINTING_COMPONENT_COLLECT).handler(null, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
scene,
|
||||
resourceCollector,
|
||||
}, next);
|
||||
|
||||
expect((controller as any)._drawingRenderService.renderDrawing).toHaveBeenCalledTimes(2);
|
||||
expect((controller as any)._drawingRenderService.renderDrawing).toHaveBeenCalledWith(expect.objectContaining({ drawingId: 'image-1' }), scene, { allowInactiveSheet: true });
|
||||
expect((controller as any)._drawingRenderService.renderDrawing).toHaveBeenCalledWith(expect.objectContaining({ drawingId: 'grouped-image' }), scene, { allowInactiveSheet: true });
|
||||
expect(resourceCollector.add).toHaveBeenCalledTimes(2);
|
||||
expect(result).toBe('next-result');
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('expands the printed range to include ungrouped drawing bounds', () => {
|
||||
const { controller, interceptors, points } = createController();
|
||||
const next = vi.fn((range) => range);
|
||||
|
||||
const range = interceptors.get(points.PRINTING_RANGE).handler(
|
||||
{ startRow: 4, endRow: 4, startColumn: 4, endColumn: 4 },
|
||||
{ unitId: 'unit-1', subUnitId: 'sheet-1' },
|
||||
next
|
||||
);
|
||||
|
||||
expect(range).toEqual({
|
||||
startRow: 3,
|
||||
endRow: 11,
|
||||
startColumn: 2,
|
||||
endColumn: 16,
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('defers React root cleanup when replacing printed float components', async () => {
|
||||
const { controller, interceptors, points } = createController();
|
||||
const unmount = vi.fn();
|
||||
vi.mocked(mountPrintingFloatDom).mockReturnValueOnce(unmount);
|
||||
const disposers: Array<() => void> = [];
|
||||
const add = vi.fn((dispose: () => void) => disposers.push(dispose));
|
||||
const disposableCollection = { add };
|
||||
const resourceCollector = { add: vi.fn() };
|
||||
const next = vi.fn((value) => value);
|
||||
const root = document.createElement('div');
|
||||
|
||||
const result = interceptors.get(points.PRINTING_DOM_COLLECT).handler(disposableCollection, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
scene: 'scene',
|
||||
skeleton: 'skeleton',
|
||||
worksheet: 'worksheet',
|
||||
root,
|
||||
resourceCollector,
|
||||
}, next);
|
||||
|
||||
expect(mountPrintingFloatDom).toHaveBeenCalledWith(
|
||||
{
|
||||
floatDomInfos: [
|
||||
expect.objectContaining({ drawingId: 'dom-1', componentKey: 'custom-dom-print-Component' }),
|
||||
expect.objectContaining({ drawingId: 'block-1', componentKey: 'custom-block-print-Component' }),
|
||||
],
|
||||
scene: 'scene',
|
||||
skeleton: 'skeleton',
|
||||
worksheet: 'worksheet',
|
||||
},
|
||||
expect.any(HTMLElement),
|
||||
expect.any(Object)
|
||||
);
|
||||
const printingRoot = vi.mocked(mountPrintingFloatDom).mock.calls[0][1];
|
||||
expect(printingRoot).not.toBe(root);
|
||||
expect(printingRoot.parentElement).toBe(root);
|
||||
expect(add).toHaveBeenCalled();
|
||||
expect(resourceCollector.add).not.toHaveBeenCalled();
|
||||
expect(unmount).not.toHaveBeenCalled();
|
||||
expect(result).toBe(disposableCollection);
|
||||
|
||||
disposers.forEach((dispose) => dispose());
|
||||
expect(unmount).not.toHaveBeenCalled();
|
||||
expect(printingRoot.parentElement).toBeNull();
|
||||
|
||||
await Promise.resolve();
|
||||
expect(unmount).toHaveBeenCalledOnce();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('continues DOM collection when the sheet has no drawing data', () => {
|
||||
const { controller, interceptors, points } = createController({ hasDrawingData: false });
|
||||
const disposableCollection = { add: vi.fn() };
|
||||
const next = vi.fn(() => 'next-result');
|
||||
const mountCallCount = vi.mocked(mountPrintingFloatDom).mock.calls.length;
|
||||
|
||||
const result = interceptors.get(points.PRINTING_DOM_COLLECT).handler(disposableCollection, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
}, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(disposableCollection);
|
||||
expect(result).toBe('next-result');
|
||||
expect(mountPrintingFloatDom).toHaveBeenCalledTimes(mountCallCount);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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, FOCUSING_COMMON_DRAWINGS } from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import { SetDrawingArrangeCommand, SetSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { GroupSheetDrawingCommand } from '../../commands/commands/group-sheet-drawing.command';
|
||||
import { UngroupSheetDrawingCommand } from '../../commands/commands/ungroup-sheet-drawing.command';
|
||||
import { SheetDrawingUpdateController } from '../sheet-drawing-update.controller';
|
||||
|
||||
function createSkeleton() {
|
||||
return {
|
||||
rowHeaderWidth: 0,
|
||||
columnHeaderHeight: 0,
|
||||
columnTotalWidth: 1000,
|
||||
rowTotalHeight: 1000,
|
||||
getNoMergeCellWithCoordByIndex: vi.fn((row: number, column: number) => ({
|
||||
startX: column * 10,
|
||||
endX: column * 10 + 10,
|
||||
startY: row * 20,
|
||||
endY: row * 20 + 20,
|
||||
})),
|
||||
getCellIndexAndOffsetByPosition: vi.fn((left: number, top: number) => {
|
||||
const column = Math.floor(left / 10);
|
||||
const row = Math.floor(top / 20);
|
||||
return {
|
||||
column,
|
||||
row,
|
||||
columnOffset: left - column * 10,
|
||||
rowOffset: top - row * 20,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createController(options?: { openFiles?: unknown[] }) {
|
||||
const featurePluginOrderUpdate$ = new Subject<any>();
|
||||
const featurePluginUpdate$ = new Subject<any[]>();
|
||||
const featurePluginGroupUpdate$ = new Subject<any[]>();
|
||||
const featurePluginUngroupUpdate$ = new Subject<any[]>();
|
||||
const focus$ = new Subject<any[] | null>();
|
||||
const skeleton = createSkeleton();
|
||||
const commandService = {
|
||||
executeCommand: vi.fn(),
|
||||
syncExecuteCommand: vi.fn(() => true),
|
||||
};
|
||||
const sheetDrawingService = {
|
||||
getDrawingByParam: vi.fn(({ drawingId }) => ({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
transform: { left: 10, top: 20, width: 30, height: 40 },
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 4, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
})),
|
||||
getDrawingsByGroup: vi.fn(() => []),
|
||||
focusDrawing: vi.fn(),
|
||||
};
|
||||
const selectionManagerService = {
|
||||
getWorkbookSelections: vi.fn(() => ({
|
||||
getCurrentSelections: vi.fn(() => []),
|
||||
getCurrentLastSelection: vi.fn(() => undefined),
|
||||
})),
|
||||
};
|
||||
const controller = new SheetDrawingUpdateController(
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
unit: {
|
||||
getUnitId: () => 'unit-1',
|
||||
getActiveSheet: () => ({ getSheetId: () => 'sheet-1' }),
|
||||
},
|
||||
scene: { width: 400, height: 300 },
|
||||
} as never,
|
||||
commandService as never,
|
||||
{ interceptAfterCommand: vi.fn(() => ({ dispose: vi.fn() })) } as never,
|
||||
{ getCellWithCoordByOffset: vi.fn() } as never,
|
||||
{ saveImage: vi.fn(), addImageSourceCache: vi.fn() } as never,
|
||||
{ openFile: vi.fn(async () => options?.openFiles ?? []) } as never,
|
||||
sheetDrawingService as never,
|
||||
{
|
||||
featurePluginOrderUpdate$,
|
||||
featurePluginUpdate$,
|
||||
featurePluginGroupUpdate$,
|
||||
featurePluginUngroupUpdate$,
|
||||
focus$,
|
||||
} as never,
|
||||
{ setContextValue: vi.fn() } as never,
|
||||
{ show: vi.fn() } as never,
|
||||
{ t: vi.fn((key: string, value?: string) => `${key}:${value ?? ''}`) } as never,
|
||||
selectionManagerService as never,
|
||||
{
|
||||
getSkeleton: vi.fn(() => skeleton),
|
||||
getSkeletonParam: vi.fn(() => ({ unitId: 'unit-1', sheetId: 'sheet-1', skeleton })),
|
||||
} as never,
|
||||
{ get: vi.fn() } as never,
|
||||
{ getImage: vi.fn() } as never
|
||||
);
|
||||
|
||||
return {
|
||||
controller,
|
||||
commandService,
|
||||
sheetDrawingService,
|
||||
featurePluginOrderUpdate$,
|
||||
featurePluginUpdate$,
|
||||
featurePluginGroupUpdate$,
|
||||
featurePluginUngroupUpdate$,
|
||||
focus$,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SheetDrawingUpdateController', () => {
|
||||
it('reports empty and over-limit file selections without inserting images', async () => {
|
||||
const empty = createController({ openFiles: [] });
|
||||
|
||||
await expect(empty.controller.insertFloatImage()).resolves.toBe(false);
|
||||
await expect(empty.controller.insertCellImage()).resolves.toBe(false);
|
||||
expect((empty.controller as any)._messageService.show).not.toHaveBeenCalled();
|
||||
empty.controller.dispose();
|
||||
|
||||
const overLimitFiles = Array.from({ length: 30 }, (_, index) => ({ name: `image-${index}.png` }));
|
||||
const overLimit = createController({ openFiles: overLimitFiles });
|
||||
|
||||
await expect(overLimit.controller.insertFloatImage()).resolves.toBe(false);
|
||||
|
||||
expect((overLimit.controller as any)._messageService.show).toHaveBeenCalledWith({
|
||||
type: MessageType.Error,
|
||||
content: expect.stringContaining('sheets-drawing-ui.update-status.exceedMaxCount'),
|
||||
});
|
||||
expect(overLimit.commandService.executeCommand).not.toHaveBeenCalled();
|
||||
overLimit.controller.dispose();
|
||||
});
|
||||
|
||||
it('converts drawing transform updates into sheet drawing commands', () => {
|
||||
const { controller, commandService, featurePluginUpdate$ } = createController();
|
||||
|
||||
featurePluginUpdate$.next([{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'image-1',
|
||||
transform: { left: 25, top: 45 },
|
||||
}]);
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(SetSheetDrawingCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
drawings: [
|
||||
expect.objectContaining({
|
||||
drawingId: 'image-1',
|
||||
transform: expect.objectContaining({ left: 25, top: 45, width: 30, height: 40 }),
|
||||
sheetTransform: expect.objectContaining({
|
||||
from: { row: 2, column: 2, rowOffset: 5, columnOffset: 5 },
|
||||
to: { row: 4, column: 5, rowOffset: 5, columnOffset: 5 },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('forwards arrange, group, ungroup and focus drawing events to sheet services', () => {
|
||||
const {
|
||||
controller,
|
||||
commandService,
|
||||
sheetDrawingService,
|
||||
featurePluginOrderUpdate$,
|
||||
featurePluginGroupUpdate$,
|
||||
featurePluginUngroupUpdate$,
|
||||
focus$,
|
||||
} = createController();
|
||||
const parent = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
transform: { left: 0, top: 0, width: 40, height: 40 },
|
||||
};
|
||||
const child = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'image-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
transform: { left: 10, top: 20, width: 30, height: 40 },
|
||||
};
|
||||
|
||||
featurePluginOrderUpdate$.next({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingIds: ['image-1'],
|
||||
arrangeType: 'front',
|
||||
});
|
||||
featurePluginGroupUpdate$.next([{ parent, children: [child] }]);
|
||||
featurePluginUngroupUpdate$.next([{ parent, children: [child] }]);
|
||||
focus$.next([{ unitId: 'unit-1', subUnitId: 'sheet-1', drawingId: 'image-1' }]);
|
||||
focus$.next([]);
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(SetDrawingArrangeCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingIds: ['image-1'],
|
||||
arrangeType: 'front',
|
||||
});
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(GroupSheetDrawingCommand.id, [
|
||||
expect.objectContaining({
|
||||
parent: expect.objectContaining({ drawingId: 'group-1', sheetTransform: expect.any(Object) }),
|
||||
children: [expect.objectContaining({ drawingId: 'image-1', sheetTransform: expect.any(Object) })],
|
||||
}),
|
||||
]);
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenCalledWith(SetDrawingSelectedOperation.id, [{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
}]);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(UngroupSheetDrawingCommand.id, [
|
||||
expect.objectContaining({
|
||||
children: [expect.objectContaining({ drawingId: 'image-1', sheetTransform: expect.any(Object) })],
|
||||
}),
|
||||
]);
|
||||
expect((controller as any)._contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_COMMON_DRAWINGS, true);
|
||||
expect((controller as any)._contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_COMMON_DRAWINGS, false);
|
||||
expect(sheetDrawingService.focusDrawing).toHaveBeenLastCalledWith([]);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not persist derived group rotateEnabled before grouping', () => {
|
||||
const {
|
||||
controller,
|
||||
commandService,
|
||||
featurePluginGroupUpdate$,
|
||||
} = createController();
|
||||
const parent = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'group-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: { left: 0, top: 0, width: 80, height: 40 },
|
||||
};
|
||||
const children = [
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'image-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
transform: { left: 10, top: 20, width: 30, height: 40 },
|
||||
},
|
||||
{
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'chart-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
transform: { left: 40, top: 20, width: 30, height: 40 },
|
||||
},
|
||||
];
|
||||
|
||||
featurePluginGroupUpdate$.next([{ parent, children }]);
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(GroupSheetDrawingCommand.id, [
|
||||
expect.objectContaining({
|
||||
parent: expect.objectContaining({
|
||||
drawingId: 'group-1',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
const groupCommandParams = commandService.executeCommand.mock.calls.find(([commandId]) => commandId === GroupSheetDrawingCommand.id)?.[1] as any[];
|
||||
expect(Object.prototype.hasOwnProperty.call(groupCommandParams[0].parent.transform ?? {}, 'rotateEnabled')).toBe(false);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { Disposable, Inject } from '@univerjs/core';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID, BatchSaveImagesDialog } from '../views/batch-save-images';
|
||||
import { COMPONENT_SHEET_DRAWING_PANEL } from '../views/sheet-image-panel/component-name';
|
||||
import { SheetDrawingPanel } from '../views/sheet-image-panel/SheetDrawingPanel';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(COMPONENT_SHEET_DRAWING_PANEL, SheetDrawingPanel));
|
||||
|
||||
this.disposeWithMe(this._componentManager.register(BATCH_SAVE_IMAGES_DIALOG_ID, BatchSaveImagesDialog));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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, IDrawingParam, Nullable, Workbook } from '@univerjs/core';
|
||||
import { IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ContextMenuPosition, IContextMenuService } from '@univerjs/ui';
|
||||
import { IDrawingContextMenuService } from '../services/drawing-context-menu.service';
|
||||
|
||||
export class DrawingContextMenuController extends RxDisposable {
|
||||
constructor(
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IDrawingContextMenuService private readonly _drawingContextMenuService: IDrawingContextMenuService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this._univerInstanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET).forEach((workbook) => this._contextMenuListener(workbook));
|
||||
}
|
||||
|
||||
private _contextMenuListener(workbook: Nullable<Workbook>) {
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scene = this._renderManagerService.getRenderUnitById(workbook.getUnitId())?.scene;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
if (!transformer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeWithMe(transformer.changeEnd$.subscribe((params) => {
|
||||
const { event } = params;
|
||||
if (event.button !== 2) return;
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (selectedObjects.size === 0) return;
|
||||
|
||||
const drawings: IDrawingParam[] = [];
|
||||
for (const object of selectedObjects.values()) {
|
||||
const oKey = object.oKey;
|
||||
const drawingParam = this._drawingManagerService.getDrawingOKey(oKey);
|
||||
|
||||
if (!drawingParam) return;
|
||||
|
||||
drawings.push(drawingParam);
|
||||
}
|
||||
|
||||
this._contextMenuService.triggerContextMenu(event, this._getContextMenuPosition(drawings));
|
||||
}));
|
||||
}
|
||||
|
||||
private _getContextMenuPosition(drawings: IDrawingParam[]): string {
|
||||
if (drawings.length !== 1) {
|
||||
return ContextMenuPosition.DRAWING;
|
||||
}
|
||||
|
||||
const [drawing] = drawings;
|
||||
|
||||
return this._drawingContextMenuService.getContextMenuPosition({
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
drawingId: drawing.drawingId,
|
||||
drawingType: drawing.drawingType as DrawingTypeEnum,
|
||||
drawing,
|
||||
}) ?? ContextMenuPosition.DRAWING;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { Disposable, Inject } from '@univerjs/core';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from '../../views/batch-save-images';
|
||||
import { MobileBatchSaveImagesDialog } from '../../views/batch-save-images/MobileBatchSaveImagesDialog';
|
||||
import { COMPONENT_SHEET_DRAWING_PANEL } from '../../views/sheet-image-panel/component-name';
|
||||
import { SheetDrawingPanel } from '../../views/sheet-image-panel/SheetDrawingPanel';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(@Inject(ComponentManager) componentManager: ComponentManager) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(componentManager.register(COMPONENT_SHEET_DRAWING_PANEL, SheetDrawingPanel));
|
||||
this.disposeWithMe(componentManager.register(BATCH_SAVE_IMAGES_DIALOG_ID, MobileBatchSaveImagesDialog));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 { ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { DrawingTypeEnum } from '@univerjs/core';
|
||||
import { drawingPositionToTransform, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetsDrawingRenderController } from '../sheet-drawing.render-controller';
|
||||
|
||||
vi.mock('@univerjs/sheets-drawing', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/sheets-drawing')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
drawingPositionToTransform: vi.fn(() => ({ left: 24, top: 36, width: 120, height: 80 })),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SheetsDrawingRenderController', () => {
|
||||
it('initializes sheet drawing data and materializes sheet transforms for render objects', () => {
|
||||
const drawingWithSheetTransform: Partial<ISheetDrawing> = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'drawing-1',
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 4, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
const drawingWithoutSkeleton: Partial<ISheetDrawing> = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'missing-sheet',
|
||||
drawingId: 'drawing-2',
|
||||
sheetTransform: {
|
||||
from: { row: 5, column: 6, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 7, column: 8, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
const groupedDrawing: Partial<ISheetDrawing> = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'drawing-4',
|
||||
groupId: 'group-1',
|
||||
transform: { left: 2, top: 3, width: 40, height: 50 },
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 4, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
const drawingGroup: Partial<ISheetDrawing> = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'drawing-5',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 3, column: 4, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
const absoluteDrawing: Partial<ISheetDrawing> = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'drawing-6',
|
||||
anchorType: SheetDrawingAnchorType.None,
|
||||
transform: { left: 240, top: 96, width: 120, height: 80 },
|
||||
sheetTransform: {
|
||||
from: { row: 0, column: 0, rowOffset: 96, columnOffset: 240 },
|
||||
to: { row: 0, column: 0, rowOffset: 176, columnOffset: 360 },
|
||||
},
|
||||
};
|
||||
const drawingData = {
|
||||
'sheet-1': {
|
||||
data: {
|
||||
'drawing-1': drawingWithSheetTransform,
|
||||
'drawing-3': { unitId: 'unit-1', subUnitId: 'sheet-1', drawingId: 'drawing-3' },
|
||||
'drawing-4': groupedDrawing,
|
||||
'drawing-5': drawingGroup,
|
||||
'drawing-6': absoluteDrawing,
|
||||
},
|
||||
},
|
||||
'missing-sheet': {
|
||||
data: {
|
||||
'drawing-2': drawingWithoutSkeleton,
|
||||
},
|
||||
},
|
||||
};
|
||||
const sheetDrawingService = {
|
||||
initializeNotification: vi.fn(),
|
||||
getDrawingDataForUnit: vi.fn(() => drawingData),
|
||||
};
|
||||
const drawingManagerService = {
|
||||
registerDrawingData: vi.fn(),
|
||||
initializeNotification: vi.fn(),
|
||||
};
|
||||
const skeletonParam = {
|
||||
skeleton: {
|
||||
id: 'skeleton-1',
|
||||
rowHeaderWidthAndMarginLeft: 10,
|
||||
columnHeaderHeightAndMarginTop: 20,
|
||||
},
|
||||
};
|
||||
const sheetSkeletonService = {
|
||||
getSkeletonParam: vi.fn((unitId: string, subUnitId: string) => {
|
||||
if (unitId === 'unit-1' && subUnitId === 'sheet-1') {
|
||||
return skeletonParam;
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
||||
const controller = new SheetsDrawingRenderController(
|
||||
{ unitId: 'unit-1' } as never,
|
||||
sheetDrawingService as never,
|
||||
drawingManagerService as never,
|
||||
sheetSkeletonService as never
|
||||
);
|
||||
|
||||
expect(sheetDrawingService.initializeNotification).toHaveBeenCalledWith('unit-1');
|
||||
expect(drawingPositionToTransform).toHaveBeenCalledWith(drawingWithSheetTransform.sheetTransform, skeletonParam);
|
||||
expect(drawingWithSheetTransform.transform).toEqual({ left: 24, top: 36, width: 120, height: 80 });
|
||||
expect(drawingGroup.transform).toEqual({ left: 14, top: 16, width: 120, height: 80 });
|
||||
expect(groupedDrawing.transform).toEqual({ left: 2, top: 3, width: 40, height: 50 });
|
||||
expect(absoluteDrawing.transform).toEqual({ left: 240, top: 96, width: 120, height: 80 });
|
||||
expect(drawingPositionToTransform).not.toHaveBeenCalledWith(absoluteDrawing.sheetTransform, skeletonParam);
|
||||
expect(drawingWithoutSkeleton).not.toHaveProperty('transform');
|
||||
expect(drawingManagerService.registerDrawingData).toHaveBeenCalledWith('unit-1', drawingData);
|
||||
expect(drawingManagerService.initializeNotification).toHaveBeenCalledWith('unit-1');
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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 { IDocImage } from '@univerjs/docs-drawing';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { DrawingRenderService } from '@univerjs/drawing-ui';
|
||||
import { CURSOR_TYPE } from '@univerjs/engine-render';
|
||||
import { isCellImage, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { HoverManagerService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { throttleTime } from 'rxjs';
|
||||
|
||||
const CELL_IMAGE_PREVIEW_KEY = 'preview-cell-image';
|
||||
|
||||
export class SheetCellImageHoverRenderController extends Disposable implements IRenderModule {
|
||||
private _isSetCursor = false;
|
||||
|
||||
constructor(
|
||||
private _context: IRenderContext<Workbook>,
|
||||
@Inject(HoverManagerService) private _hoverManagerService: HoverManagerService,
|
||||
@Inject(SheetsSelectionsService) private _selectionsService: SheetsSelectionsService,
|
||||
@Inject(DrawingRenderService) private _drawingRenderService: DrawingRenderService,
|
||||
@Inject(SheetSkeletonManagerService) private _sheetSkeletonManagerService: SheetSkeletonManagerService
|
||||
) {
|
||||
super();
|
||||
this._initHover();
|
||||
this._initImageClick();
|
||||
}
|
||||
|
||||
private _initHover() {
|
||||
this.disposeWithMe(this._hoverManagerService.currentRichTextNoDistinct$
|
||||
.pipe(throttleTime(33))
|
||||
.subscribe((richText) => {
|
||||
let currentSelections: Readonly<ISelectionWithStyle[]> = [];
|
||||
// null richText means the unit was disposed, should not get current selections
|
||||
if (richText !== null) {
|
||||
currentSelections = this._selectionsService.getWorkbookSelections(this._context.unitId).getCurrentSelections();
|
||||
}
|
||||
|
||||
if (
|
||||
richText?.unitId === this._context.unitId &&
|
||||
richText?.drawing &&
|
||||
currentSelections.length === 1 &&
|
||||
currentSelections[0].primary
|
||||
) {
|
||||
const { row, col } = richText;
|
||||
const { actualRow, actualColumn, startRow, startColumn, endRow, endColumn, isMerged, isMergedMainCell } = currentSelections[0].primary;
|
||||
|
||||
if (
|
||||
(isMerged || isMergedMainCell) &&
|
||||
row >= startRow &&
|
||||
row <= endRow &&
|
||||
col >= startColumn &&
|
||||
col <= endColumn
|
||||
) {
|
||||
this._isSetCursor = true;
|
||||
this._context.scene.setCursor(CURSOR_TYPE.ZOOM_IN);
|
||||
} else if (row === actualRow && col === actualColumn) {
|
||||
this._isSetCursor = true;
|
||||
this._context.scene.setCursor(CURSOR_TYPE.ZOOM_IN);
|
||||
} else if (this._isSetCursor) {
|
||||
this._isSetCursor = false;
|
||||
this._context.scene.resetCursor();
|
||||
}
|
||||
} else if (this._isSetCursor) {
|
||||
this._isSetCursor = false;
|
||||
this._context.scene.resetCursor();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _initImageClick() {
|
||||
this.disposeWithMe(this._hoverManagerService.currentClickedCell$.subscribe((click) => {
|
||||
if (click?.drawing && this._isSetCursor) {
|
||||
const imageDrawing = click.drawing.drawing.drawingOrigin as IDocImage;
|
||||
this._previewImage(imageDrawing);
|
||||
}
|
||||
}));
|
||||
|
||||
const currentDbClickedCell$ = this._hoverManagerService.currentDbClickedCell$;
|
||||
if (!currentDbClickedCell$) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeWithMe(currentDbClickedCell$.subscribe(({ location }) => {
|
||||
if (location.unitId !== this._context.unitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const worksheet = this._context.unit.getSheetBySheetId(location.subUnitId);
|
||||
const documentData = worksheet?.getCellRaw(location.row, location.col)?.p;
|
||||
const drawingId = documentData?.drawingsOrder?.[0];
|
||||
const imageDrawing = isCellImage(documentData) && drawingId
|
||||
? documentData.drawings?.[drawingId] as IDocImage | undefined
|
||||
: undefined;
|
||||
|
||||
if (imageDrawing) {
|
||||
this._previewImage(imageDrawing);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _previewImage(imageDrawing: IDocImage) {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageCacheMap = this._sheetSkeletonManagerService.getCurrentSkeleton()?.imageCacheMap;
|
||||
const imageEle = imageCacheMap?.getImage(
|
||||
imageDrawing.imageSourceType,
|
||||
imageDrawing.source,
|
||||
() => this._previewImage(imageDrawing)
|
||||
);
|
||||
if (!imageEle) return;
|
||||
this._drawingRenderService.previewImage(CELL_IMAGE_PREVIEW_KEY, imageEle.src, imageEle.width, imageEle.height);
|
||||
this._context.scene.resetCursor();
|
||||
this._isSetCursor = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable, DrawingTypeEnum, Inject } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { drawingPositionToTransform, ISheetDrawingService, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
|
||||
|
||||
export class SheetsDrawingRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private _context: IRenderContext,
|
||||
@ISheetDrawingService private readonly _sheetDrawingService: ISheetDrawingService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this._drawingInitializeListener();
|
||||
}
|
||||
|
||||
private _drawingInitializeListener() {
|
||||
// initialize drawing data and add to sheet canvas
|
||||
this._sheetDrawingService.initializeNotification(this._context.unitId);
|
||||
const data = this._sheetDrawingService.getDrawingDataForUnit(this._context.unitId);
|
||||
for (const subUnit in data) {
|
||||
const subUnitData = data[subUnit];
|
||||
for (const drawingId in subUnitData.data) {
|
||||
const drawingData = subUnitData.data[drawingId];
|
||||
const { unitId, subUnitId } = drawingData;
|
||||
const skeletonParam = this._sheetSkeletonService.getSkeletonParam(unitId, subUnitId);
|
||||
|
||||
if (
|
||||
skeletonParam &&
|
||||
drawingData.sheetTransform &&
|
||||
!drawingData.groupId &&
|
||||
drawingData.anchorType !== SheetDrawingAnchorType.None
|
||||
) {
|
||||
const transform = drawingPositionToTransform(drawingData.sheetTransform, skeletonParam);
|
||||
drawingData.transform = transform && drawingData.drawingType === DrawingTypeEnum.DRAWING_GROUP
|
||||
? {
|
||||
...transform,
|
||||
left: (transform.left ?? 0) - skeletonParam.skeleton.rowHeaderWidthAndMarginLeft,
|
||||
top: (transform.top ?? 0) - skeletonParam.skeleton.columnHeaderHeightAndMarginTop,
|
||||
}
|
||||
: transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._drawingManagerService.registerDrawingData(this._context.unitId, this._sheetDrawingService.getDrawingDataForUnit(this._context.unitId));
|
||||
this._drawingManagerService.initializeNotification(this._context.unitId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 { Disposable, Inject, Injector, ObjectMatrix } from '@univerjs/core';
|
||||
import { IAutoFillService } from '@univerjs/sheets';
|
||||
import { resizeImageByCell } from './sheet-cell-image.controller';
|
||||
|
||||
export class SheetCellImageAutofillController extends Disposable {
|
||||
constructor(
|
||||
@Inject(IAutoFillService) private readonly _autoFillService: IAutoFillService,
|
||||
@Inject(Injector) private readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initAutoFillHooks();
|
||||
}
|
||||
|
||||
private _initAutoFillHooks(): void {
|
||||
this.disposeWithMe(
|
||||
this._autoFillService.addHook({
|
||||
id: 'sheet-cell-image-autofill',
|
||||
onBeforeSubmit: (location, direction, applyType, cellValue) => {
|
||||
new ObjectMatrix(cellValue).forValue((row, col, cell) => {
|
||||
resizeImageByCell(this._injector, { unitId: location.unitId, subUnitId: location.subUnitId, row, col }, cell);
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IDocDrawingBase } from '@univerjs/core';
|
||||
import type { IInnerPasteCommandParams } from '@univerjs/docs-ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
BuildTextUtils,
|
||||
createDocumentModelWithStyle,
|
||||
Disposable,
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { InnerPasteCommand } from '@univerjs/docs-ui';
|
||||
import { getCurrentTypeOfRenderer, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { EditingRenderController, SetCellEditVisibleOperation } from '@univerjs/sheets-ui';
|
||||
import { IDialogService } from '@univerjs/ui';
|
||||
|
||||
const DISABLE_UNITS = [
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY,
|
||||
];
|
||||
export class SheetCellImageCopyPasteController extends Disposable {
|
||||
constructor(
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService
|
||||
) {
|
||||
super();
|
||||
this._initDocImageCopyPasteHooks();
|
||||
}
|
||||
|
||||
private _setCellImage(drwaing: IDocDrawingBase) {
|
||||
const docDataModel = createDocumentModelWithStyle('', {});
|
||||
const editingRenderController = getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_SHEET, this._univerInstanceService, this._renderManagerService)?.with(EditingRenderController);
|
||||
const jsonXActions = BuildTextUtils.drawing.add({
|
||||
documentDataModel: docDataModel,
|
||||
drawings: [drwaing],
|
||||
selection: {
|
||||
collapsed: true,
|
||||
startOffset: 0,
|
||||
endOffset: 0,
|
||||
},
|
||||
});
|
||||
if (jsonXActions) {
|
||||
docDataModel.apply(jsonXActions);
|
||||
if (editingRenderController) {
|
||||
editingRenderController.submitCellData(docDataModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _initDocImageCopyPasteHooks() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === InnerPasteCommand.id) {
|
||||
const params = commandInfo.params as IInnerPasteCommandParams;
|
||||
const { doc } = params;
|
||||
const currentDoc = this._univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (currentDoc == null || !Object.keys(doc.drawings ?? {}).length) {
|
||||
return;
|
||||
}
|
||||
const docUnitId = currentDoc.getUnitId();
|
||||
if (DISABLE_UNITS.includes(docUnitId)) {
|
||||
const handleCloseDialog = () => {
|
||||
this._dialogService.close('sheet-cell-image-copy-paste');
|
||||
this._commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, {
|
||||
visible: false,
|
||||
});
|
||||
};
|
||||
// empty
|
||||
if (currentDoc.getBody()?.dataStream === '\r\n') {
|
||||
this._commandService.syncExecuteCommand(SetCellEditVisibleOperation.id, {
|
||||
visible: false,
|
||||
});
|
||||
this._setCellImage(Object.values(doc.drawings!)[0]);
|
||||
} else {
|
||||
this._dialogService.open({
|
||||
id: 'sheet-cell-image-copy-paste',
|
||||
title: {
|
||||
label: this._localeService.t<LocaleKey>('sheets-drawing-ui.cell-image.pasteTitle'),
|
||||
},
|
||||
children: {
|
||||
label: this._localeService.t<LocaleKey>('sheets-drawing-ui.cell-image.pasteContent'),
|
||||
},
|
||||
width: 320,
|
||||
onClose: handleCloseDialog,
|
||||
showOk: true,
|
||||
showCancel: true,
|
||||
onOk: () => {
|
||||
handleCloseDialog();
|
||||
this._setCellImage(Object.values(doc.drawings!)[0]);
|
||||
},
|
||||
onCancel: handleCloseDialog,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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 { ICellData, IDocDrawingBase, Nullable } from '@univerjs/core';
|
||||
import type { IImageData } from '@univerjs/drawing';
|
||||
import type { ISheetLocationBase } from '@univerjs/sheets';
|
||||
import { Disposable, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, Inject, Injector, InterceptorEffectEnum } from '@univerjs/core';
|
||||
import { DocDrawingController } from '@univerjs/docs-drawing';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { InterceptCellContentPriority, INTERCEPTOR_POINT, isCellImage, SheetInterceptorService } from '@univerjs/sheets';
|
||||
import { IEditorBridgeService } from '@univerjs/sheets-ui';
|
||||
import { getDrawingSizeByCell } from './sheet-drawing-update.controller';
|
||||
|
||||
export function resizeImageByCell(injector: Injector, location: ISheetLocationBase, cell: Nullable<ICellData>) {
|
||||
const documentData = cell?.p;
|
||||
const drawingId = isCellImage(documentData) && documentData?.drawingsOrder?.length === 1 ? documentData.drawingsOrder[0] : undefined;
|
||||
const image = drawingId ? documentData?.drawings?.[drawingId] as IImageData & IDocDrawingBase : undefined;
|
||||
|
||||
if (image && documentData) {
|
||||
const imageSize = getDrawingSizeByCell(
|
||||
injector,
|
||||
{
|
||||
unitId: location.unitId,
|
||||
subUnitId: location.subUnitId,
|
||||
row: location.row,
|
||||
col: location.col,
|
||||
},
|
||||
image.docTransform!.size.width!,
|
||||
image.docTransform!.size.height!,
|
||||
image.docTransform!.angle
|
||||
);
|
||||
|
||||
if (imageSize) {
|
||||
image.transform!.width = imageSize.width;
|
||||
image.transform!.height = imageSize.height;
|
||||
image.docTransform!.size.width = imageSize.width;
|
||||
image.docTransform!.size.height = imageSize.height;
|
||||
image.transform!.left = 0;
|
||||
image.transform!.top = 0;
|
||||
image.docTransform!.positionH.posOffset = 0;
|
||||
image.docTransform!.positionV.posOffset = 0;
|
||||
|
||||
documentData.documentStyle.pageSize!.width = Infinity;
|
||||
documentData.documentStyle.pageSize!.height = Infinity;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export class SheetCellImageController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(DocDrawingController) private readonly _docDrawingController: DocDrawingController,
|
||||
@Inject(IEditorBridgeService) private readonly _editorBridgeService: IEditorBridgeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._handleInitEditor();
|
||||
this._initCellContentInterceptor();
|
||||
}
|
||||
|
||||
private _handleInitEditor() {
|
||||
this.disposeWithMe(this._editorBridgeService.visible$.subscribe((param) => {
|
||||
if (!param.visible) {
|
||||
this._drawingManagerService.removeDrawingDataForUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
} else if (param.visible) {
|
||||
this._drawingManagerService.removeDrawingDataForUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
this._docDrawingController.loadDrawingDataForUnit(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
this._drawingManagerService.initializeNotification(DOCS_NORMAL_EDITOR_UNIT_ID_KEY);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _initCellContentInterceptor() {
|
||||
this.disposeWithMe(
|
||||
this._sheetInterceptorService.intercept(
|
||||
INTERCEPTOR_POINT.CELL_CONTENT,
|
||||
{
|
||||
effect: InterceptorEffectEnum.Style,
|
||||
priority: InterceptCellContentPriority.CELL_IMAGE,
|
||||
handler: (cell, pos, next) => {
|
||||
if (cell && isCellImage(cell.p)) {
|
||||
if (cell === pos.rawData) {
|
||||
cell = { ...pos.rawData };
|
||||
}
|
||||
|
||||
if (!cell.interceptorStyle) {
|
||||
cell.interceptorStyle = {};
|
||||
}
|
||||
cell.interceptorStyle.tr = { a: 0 };
|
||||
|
||||
resizeImageByCell(this._injector, { unitId: pos.unitId, subUnitId: pos.subUnitId, row: pos.row, col: pos.col }, cell);
|
||||
}
|
||||
|
||||
return next(cell);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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, IDrawingParam, Workbook } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { ISetWorksheetActiveOperationParams } from '@univerjs/sheets';
|
||||
import type { ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import { Disposable, ICommandService, Inject } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { SetWorksheetActiveOperation, SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { drawingPositionToTransform, ISheetDrawingService, SheetDrawingAnchorType } from '@univerjs/sheets-drawing';
|
||||
|
||||
export class SheetDrawingActiveRenderController extends Disposable implements IRenderModule {
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService,
|
||||
@ISheetDrawingService private readonly _sheetDrawingService: ISheetDrawingService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService
|
||||
) {
|
||||
super();
|
||||
this._commandListener();
|
||||
}
|
||||
|
||||
private _commandListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id === SetWorksheetActiveOperation.id) {
|
||||
const { unitId, subUnitId } = command.params as ISetWorksheetActiveOperationParams;
|
||||
this._updateDrawings(unitId, subUnitId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._context.activated$.subscribe((activated) => {
|
||||
const { unit, unitId } = this._context;
|
||||
if (activated) {
|
||||
this._updateDrawings(unitId, unit.getActiveSheet().getSheetId());
|
||||
} else {
|
||||
this._clearDrawings(unitId);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _clearDrawings(selfUnitId: string): void {
|
||||
setTimeout(() => {
|
||||
const drawingMap = this._drawingManagerService.drawingManagerData;
|
||||
const removeDrawings: IDrawingParam[] = [];
|
||||
|
||||
Object.keys(drawingMap ?? {}).forEach((unitId) => {
|
||||
const subUnitMap = drawingMap[unitId] ?? {};
|
||||
Object.keys(subUnitMap).forEach((subUnitId) => {
|
||||
const drawingData = subUnitMap[subUnitId]?.data ?? {};
|
||||
Object.keys(drawingData).forEach((drawingId) => {
|
||||
if (unitId === selfUnitId) {
|
||||
removeDrawings.push(drawingData[drawingId]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this._sheetDrawingService.removeNotification(removeDrawings);
|
||||
this._drawingManagerService.removeNotification(removeDrawings);
|
||||
});
|
||||
}
|
||||
|
||||
private _updateDrawings(showUnitId: string, showSubunitId: string): void {
|
||||
setTimeout(() => {
|
||||
const sheetSkeletonParam = this._sheetSkeletonService.getSkeletonParam(showUnitId, showSubunitId);
|
||||
const drawingMap = this._drawingManagerService.drawingManagerData;
|
||||
const insertDrawings: IDrawingParam[] = [];
|
||||
const removeDrawings: IDrawingParam[] = [];
|
||||
|
||||
Object.keys(drawingMap ?? {}).forEach((unitId) => {
|
||||
if (unitId !== showUnitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subUnitMap = drawingMap[unitId] ?? {};
|
||||
Object.keys(subUnitMap).forEach((subUnitId) => {
|
||||
const drawingData = subUnitMap[subUnitId]?.data ?? {};
|
||||
Object.keys(drawingData).forEach((drawingId) => {
|
||||
if (unitId === showUnitId && subUnitId === showSubunitId) {
|
||||
const drawing = drawingData[drawingId] as ISheetDrawing;
|
||||
if (drawing.sheetTransform && drawing.anchorType !== SheetDrawingAnchorType.None) {
|
||||
drawing.transform = drawingPositionToTransform(drawing.sheetTransform, sheetSkeletonParam);
|
||||
}
|
||||
insertDrawings.push(drawingData[drawingId]);
|
||||
} else {
|
||||
removeDrawings.push(drawingData[drawingId]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this._sheetDrawingService.removeNotification(removeDrawings);
|
||||
this._sheetDrawingService.addNotification(insertDrawings);
|
||||
this._drawingManagerService.removeNotification(removeDrawings);
|
||||
this._drawingManagerService.addNotification(insertDrawings);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* 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 { IMutationInfo, IRange, Nullable } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import type { IDiscreteRange } from '@univerjs/sheets';
|
||||
import type { IRemoveSheetDrawingCommandParams, ISheetDrawing, ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import type { IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
|
||||
import { Disposable, DrawingTypeEnum, generateRandomId, ICommandService, ImageSourceType, Inject } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { attachRangeWithCoord, discreteRangeToRange, SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { DrawingApplyType, RemoveSheetDrawingCommand, SetDrawingApplyMutation, SheetDrawingAnchorType, transformToAxisAlignPosition, transformToDrawingPosition } from '@univerjs/sheets-drawing';
|
||||
import {
|
||||
COPY_TYPE,
|
||||
ISheetClipboardService,
|
||||
PREDEFINED_HOOK_NAME_PASTE,
|
||||
virtualizeDiscreteRanges,
|
||||
} from '@univerjs/sheets-ui';
|
||||
import { IClipboardInterfaceService } from '@univerjs/ui';
|
||||
import { InsertFloatImageCommand } from '../commands/commands/insert-image.command';
|
||||
|
||||
const IMAGE_PNG_MIME_TYPE = 'image/png';
|
||||
|
||||
function base64ToBlob(base64: string) {
|
||||
const arr = base64.split(',');
|
||||
const binStr = atob(arr[1]);
|
||||
const len = binStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
return new Blob([bytes], { type: IMAGE_PNG_MIME_TYPE });
|
||||
}
|
||||
|
||||
function copyBase64ToClipboard(base64: string) {
|
||||
const item = new ClipboardItem({ [IMAGE_PNG_MIME_TYPE]: base64ToBlob(base64) });
|
||||
navigator.clipboard.write([item]).catch((err) => {
|
||||
console.error('Could not copy image using clipboard API: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
function focusDocument() {
|
||||
function createInputElement() {
|
||||
const input = document.createElement('input');
|
||||
input.style.position = 'absolute';
|
||||
input.style.height = '1px';
|
||||
input.style.width = '1px';
|
||||
input.style.opacity = '0';
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
const activeElement = document.activeElement;
|
||||
const input = createInputElement();
|
||||
document.body.appendChild(input);
|
||||
input.focus();
|
||||
|
||||
return () => {
|
||||
input.blur();
|
||||
document.body.removeChild(input);
|
||||
if (activeElement instanceof HTMLElement) {
|
||||
activeElement.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const specialPastes: IPasteHookValueType[] = [
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
];
|
||||
|
||||
export class SheetsDrawingCopyPasteController extends Disposable {
|
||||
private _copyInfo: Nullable<{
|
||||
drawings: ISheetDrawing[];
|
||||
copyRange?: IRange;
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
@ISheetClipboardService private _sheetClipboardService: ISheetClipboardService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService,
|
||||
@IDrawingManagerService private readonly _drawingService: IDrawingManagerService,
|
||||
@IClipboardInterfaceService private readonly _clipboardInterfaceService: IClipboardInterfaceService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
this._initCopyPaste();
|
||||
}
|
||||
|
||||
private get _focusedDrawings() {
|
||||
return this._drawingService.getFocusDrawings() as ISheetDrawing[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initCopyPaste() {
|
||||
this._sheetClipboardService.addClipboardHook({
|
||||
id: 'SHEET_IMAGE_UI_PLUGIN',
|
||||
|
||||
onBeforeCopyFocusedObject: (unitId, _subUnitId, copyType) => {
|
||||
this._copyInfo = null;
|
||||
|
||||
const drawings = this._focusedDrawings.filter((drawing) => drawing.drawingType === DrawingTypeEnum.DRAWING_IMAGE);
|
||||
if (drawings.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [drawing] = drawings;
|
||||
|
||||
if (copyType === COPY_TYPE.CUT) {
|
||||
const params: IRemoveSheetDrawingCommandParams = {
|
||||
unitId,
|
||||
drawings,
|
||||
};
|
||||
// Delete the drawing when it is cut
|
||||
this._commandService.executeCommand<IRemoveSheetDrawingCommandParams>(RemoveSheetDrawingCommand.id, params);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// Rewrite the clipboard content to prevent the default copy behavior, prevent to call the 'onPasteCells'
|
||||
const dispose = focusDocument();
|
||||
if (drawing.drawingType === DrawingTypeEnum.DRAWING_IMAGE
|
||||
&& (drawing as ISheetImage).imageSourceType === ImageSourceType.BASE64) {
|
||||
copyBase64ToClipboard((drawing as ISheetImage).source);
|
||||
} else {
|
||||
this._clipboardInterfaceService.writeText('');
|
||||
}
|
||||
dispose();
|
||||
}, 200);
|
||||
|
||||
this._copyInfo = {
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
drawings,
|
||||
};
|
||||
return true;
|
||||
},
|
||||
|
||||
onBeforeCopy: (unitId, subUnitId, range) => {
|
||||
this._copyInfo = this._createDrawingsCopyInfoByRange(unitId, subUnitId, range);
|
||||
},
|
||||
|
||||
onPasteCells: (pasteFrom, pasteTo, _data, payload) => {
|
||||
if (!payload.copyId) {
|
||||
this._copyInfo = null;
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
if (!pasteFrom || !this._copyInfo) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { copyType = COPY_TYPE.COPY, pasteType } = payload;
|
||||
const { range: copyRange, unitId: fromUnitId, subUnitId: fromSubUnitId } = pasteFrom;
|
||||
const { range: pasteRange, unitId: toUnitId, subUnitId: toSubUnitId } = pasteTo;
|
||||
|
||||
const mutations = this._copyInfo.copyRange
|
||||
? this._generateRangeDrawingsPasteMutations(
|
||||
{ unitId: toUnitId, subUnitId: toSubUnitId, pasteRange, pasteType },
|
||||
{ unitId: fromUnitId, subUnitId: fromSubUnitId, copyType, copyRange }
|
||||
)
|
||||
: this._generateSingleDrawingPasteMutations({ pasteTo, pasteType }, COPY_TYPE.COPY);
|
||||
|
||||
return mutations;
|
||||
},
|
||||
|
||||
onPastePlainText: (_pasteTo: ISheetDiscreteRangeLocation, _clipText: string) => {
|
||||
this._copyInfo = null;
|
||||
return { undos: [], redos: [] };
|
||||
},
|
||||
|
||||
onPasteUnrecognized: (pasteTo: ISheetDiscreteRangeLocation) => {
|
||||
if (!this._copyInfo || this._copyInfo.copyRange) {
|
||||
return { undos: [], redos: [] };
|
||||
}
|
||||
|
||||
const mutations = this._generateSingleDrawingPasteMutations({ pasteTo, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }, COPY_TYPE.COPY);
|
||||
this._copyInfo = null;
|
||||
return mutations;
|
||||
},
|
||||
|
||||
onPasteFiles: (pasteTo: ISheetDiscreteRangeLocation, files) => {
|
||||
if (this._copyInfo && !this._copyInfo.copyRange) {
|
||||
const mutations = this._generateSingleDrawingPasteMutations({ pasteTo, pasteType: PREDEFINED_HOOK_NAME_PASTE.DEFAULT_PASTE }, COPY_TYPE.COPY);
|
||||
this._copyInfo = null;
|
||||
return mutations;
|
||||
}
|
||||
|
||||
const images = files.filter((file) => file.type.includes('image'));
|
||||
if (images.length) {
|
||||
this._copyInfo = null;
|
||||
return {
|
||||
undos: [],
|
||||
redos: [
|
||||
{
|
||||
id: InsertFloatImageCommand.id,
|
||||
params: { files: images },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return { undos: [], redos: [] };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _createDrawingsCopyInfoByRange(unitId: string, subUnitId: string, range: IRange) {
|
||||
const skeleton = this._sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!skeleton) return;
|
||||
|
||||
const selectionRect = attachRangeWithCoord(skeleton, range);
|
||||
if (!selectionRect) return;
|
||||
|
||||
const { startX, endX, startY, endY } = selectionRect;
|
||||
const drawings = this._drawingService.getDrawingData(unitId, subUnitId);
|
||||
|
||||
const containedDrawings: ISheetDrawing[] = [];
|
||||
|
||||
Object.keys(drawings).forEach((drawingId) => {
|
||||
const drawing = drawings[drawingId];
|
||||
if (drawing.drawingType !== DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
return;
|
||||
}
|
||||
const anchorType = (drawing as ISheetDrawing).anchorType ?? SheetDrawingAnchorType.Position;
|
||||
if (anchorType === SheetDrawingAnchorType.None) {
|
||||
return;
|
||||
}
|
||||
const { transform } = drawing;
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const { left = 0, top = 0, width = 0, height = 0 } = transform;
|
||||
const { drawingStartX, drawingEndX, drawingStartY, drawingEndY } = {
|
||||
drawingStartX: left,
|
||||
drawingEndX: left + width,
|
||||
drawingStartY: top,
|
||||
drawingEndY: top + height,
|
||||
};
|
||||
|
||||
if (startX <= drawingStartX && drawingEndX <= endX && startY <= drawingStartY && drawingEndY <= endY) {
|
||||
containedDrawings.push(drawing as ISheetDrawing);
|
||||
}
|
||||
});
|
||||
|
||||
if (containedDrawings.length) {
|
||||
return {
|
||||
copyRange: range,
|
||||
drawings: containedDrawings,
|
||||
unitId,
|
||||
subUnitId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _generateSingleDrawingPasteMutations(pasteContext: {
|
||||
pasteTo: ISheetDiscreteRangeLocation;
|
||||
pasteType: IPasteHookValueType;
|
||||
}, copyType: COPY_TYPE) {
|
||||
const { pasteType, pasteTo } = pasteContext;
|
||||
|
||||
if (specialPastes.includes(pasteType)) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, range } = pasteTo;
|
||||
const pasteToSkeleton = this._sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!pasteToSkeleton) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const copyInfo = this._copyInfo!;
|
||||
const { drawings } = copyInfo;
|
||||
const pasteRange = discreteRangeToRange(range);
|
||||
let drawingAnchor: { left: number; top: number } | undefined;
|
||||
if (drawings.length > 1) {
|
||||
let anchorLeft = Infinity;
|
||||
let anchorTop = Infinity;
|
||||
for (const drawing of drawings) {
|
||||
const { left = 0, top = 0 } = drawing.transform ?? {};
|
||||
anchorLeft = Math.min(anchorLeft, left);
|
||||
anchorTop = Math.min(anchorTop, top);
|
||||
}
|
||||
drawingAnchor = { left: anchorLeft, top: anchorTop };
|
||||
}
|
||||
|
||||
return this._generateMutations(drawings, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
isCut: copyType === COPY_TYPE.CUT,
|
||||
getTransform: (transform, sheetTransform) => {
|
||||
const pasteRect = attachRangeWithCoord(pasteToSkeleton, {
|
||||
startRow: pasteRange.startRow,
|
||||
endRow: pasteRange.endRow,
|
||||
startColumn: pasteRange.startColumn,
|
||||
endColumn: pasteRange.endColumn,
|
||||
});
|
||||
|
||||
const newTransform = {
|
||||
...transform,
|
||||
left: drawingAnchor && pasteRect
|
||||
? pasteRect.startX + (transform?.left ?? 0) - drawingAnchor.left
|
||||
: pasteRect?.startX,
|
||||
top: drawingAnchor && pasteRect
|
||||
? pasteRect.startY + (transform?.top ?? 0) - drawingAnchor.top
|
||||
: pasteRect?.startY,
|
||||
};
|
||||
|
||||
return {
|
||||
transform: newTransform,
|
||||
sheetTransform: transformToDrawingPosition(newTransform, pasteToSkeleton) ?? sheetTransform,
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(newTransform, pasteToSkeleton) ?? sheetTransform,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _generateMutations(drawings: ISheetDrawing[], payload: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
getTransform: (transform: ISheetDrawing['transform'], sheetTransform: ISheetDrawing['sheetTransform']) => {
|
||||
transform: ISheetDrawing['transform'];
|
||||
sheetTransform: ISheetDrawing['sheetTransform'];
|
||||
axisAlignSheetTransform: ISheetDrawing['sheetTransform'];
|
||||
};
|
||||
isCut: boolean;
|
||||
}) {
|
||||
const {
|
||||
unitId,
|
||||
subUnitId,
|
||||
getTransform,
|
||||
isCut,
|
||||
} = payload;
|
||||
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
|
||||
const { _drawingService } = this;
|
||||
|
||||
drawings.forEach((drawing) => {
|
||||
const { transform, sheetTransform } = drawing;
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const transformContext = getTransform(transform, sheetTransform);
|
||||
const drawingObject: Partial<ISheetDrawing> = {
|
||||
...drawing,
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: isCut ? drawing.drawingId : generateRandomId(),
|
||||
transform: transformContext.transform,
|
||||
sheetTransform: transformContext.sheetTransform,
|
||||
axisAlignSheetTransform: transformContext.axisAlignSheetTransform,
|
||||
};
|
||||
|
||||
if (isCut) {
|
||||
const { undo, redo, objects } = _drawingService.getBatchUpdateOp([drawingObject] as ISheetDrawing[]) as IDrawingJsonUndo1;
|
||||
redos.push({
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
type: DrawingApplyType.UPDATE,
|
||||
op: redo,
|
||||
objects,
|
||||
},
|
||||
});
|
||||
undos.push({
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
type: DrawingApplyType.UPDATE,
|
||||
op: undo,
|
||||
objects,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { undo, redo, objects } = _drawingService.getBatchAddOp([drawingObject] as ISheetDrawing[]) as IDrawingJsonUndo1;
|
||||
redos.push({ id: SetDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type: DrawingApplyType.INSERT } });
|
||||
undos.push({ id: SetDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects, type: DrawingApplyType.REMOVE } });
|
||||
}
|
||||
});
|
||||
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _generateRangeDrawingsPasteMutations(
|
||||
pasteContext: {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
pasteRange: IDiscreteRange;
|
||||
pasteType: IPasteHookValueType;
|
||||
},
|
||||
copyContext: {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
copyRange?: IDiscreteRange;
|
||||
copyType: COPY_TYPE;
|
||||
}
|
||||
) {
|
||||
if (!this._copyInfo) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId: toUnitId, subUnitId: toSubUnitId, pasteRange, pasteType } = pasteContext;
|
||||
const { unitId: fromUnitId, subUnitId: fromSubUnitId, copyRange, copyType } = copyContext;
|
||||
|
||||
if (specialPastes.includes(pasteType)) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const toSkeleton = this._sheetSkeletonService.getSkeleton(toUnitId, toSubUnitId);
|
||||
if (!toSkeleton) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { drawings } = this._copyInfo;
|
||||
|
||||
if (!copyRange) {
|
||||
return this._generateSingleDrawingPasteMutations({
|
||||
pasteTo: { unitId: toUnitId, subUnitId: toSubUnitId, range: discreteRangeToRange(pasteRange) as unknown as IDiscreteRange },
|
||||
pasteType,
|
||||
}, copyType);
|
||||
}
|
||||
|
||||
const fromSkeleton = this._sheetSkeletonService.getSkeleton(fromUnitId!, fromSubUnitId!);
|
||||
if (!fromSkeleton) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { ranges: [vCopyRange, vPastedRange], mapFunc } = virtualizeDiscreteRanges([copyRange, pasteRange]);
|
||||
const { row: copyRow, col: copyCol } = mapFunc(vCopyRange.startRow, vCopyRange.startColumn);
|
||||
const { row: pasteRow, col: pasteCol } = mapFunc(vPastedRange.startRow, vPastedRange.startColumn);
|
||||
|
||||
const copyRect = attachRangeWithCoord(fromSkeleton, {
|
||||
startRow: copyRow,
|
||||
endRow: copyRow,
|
||||
startColumn: copyCol,
|
||||
endColumn: copyCol,
|
||||
});
|
||||
const pasteRect = attachRangeWithCoord(toSkeleton, {
|
||||
startRow: pasteRow,
|
||||
endRow: pasteRow,
|
||||
startColumn: pasteCol,
|
||||
endColumn: pasteCol,
|
||||
});
|
||||
|
||||
if (!copyRect || !pasteRect) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const leftOffset = pasteRect.startX - copyRect.startX;
|
||||
const topOffset = pasteRect.startY - copyRect.startY;
|
||||
const rowOffset = pasteRow - copyRow;
|
||||
const columnOffset = pasteCol - copyCol;
|
||||
|
||||
return this._generateMutations(drawings, {
|
||||
unitId: toUnitId,
|
||||
subUnitId: toSubUnitId,
|
||||
getTransform: (transform, sheetTransform) => {
|
||||
const newTransform = {
|
||||
...transform,
|
||||
left: (transform?.left ?? 0) + leftOffset,
|
||||
top: (transform?.top ?? 0) + topOffset,
|
||||
};
|
||||
|
||||
return {
|
||||
transform: newTransform,
|
||||
sheetTransform: {
|
||||
...sheetTransform,
|
||||
to: {
|
||||
...sheetTransform.to,
|
||||
row: sheetTransform.to.row + rowOffset,
|
||||
column: sheetTransform.to.column + columnOffset,
|
||||
},
|
||||
from: {
|
||||
...sheetTransform.from,
|
||||
row: sheetTransform.from.row + rowOffset,
|
||||
column: sheetTransform.from.column + columnOffset,
|
||||
},
|
||||
},
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(newTransform, toSkeleton) ?? sheetTransform,
|
||||
};
|
||||
},
|
||||
isCut: copyType === COPY_TYPE.CUT,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 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 { IDrawingGroupNestedParam, IMutationInfo, Nullable } from '@univerjs/core';
|
||||
import type { IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import type { ISheetDrawing } from '@univerjs/sheets-drawing';
|
||||
import type { IPasteHookValueType, ISheetDiscreteRangeLocation } from '@univerjs/sheets-ui';
|
||||
import { Disposable, DrawingTypeEnum, Inject } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { attachRangeWithCoord, discreteRangeToRange, SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { DrawingApplyType, ISheetDrawingService, SetDrawingApplyMutation, transformToAxisAlignPosition, transformToDrawingPosition } from '@univerjs/sheets-drawing';
|
||||
import { ISheetClipboardService, PREDEFINED_HOOK_NAME_PASTE } from '@univerjs/sheets-ui';
|
||||
import { cloneGroupParams } from '../commands/commands/utils';
|
||||
|
||||
export interface IGroupFeaturePasteHookParams {
|
||||
/** Unit ID of the copy source */
|
||||
fromUnitId: string;
|
||||
/** Sub-unit ID of the copy source */
|
||||
fromSubUnitId: string;
|
||||
/** Unit ID of the paste destination */
|
||||
toUnitId: string;
|
||||
/** Sub-unit ID of the paste destination */
|
||||
toSubUnitId: string;
|
||||
/** Maps original drawingId → new (cloned) drawingId */
|
||||
idMap: Map<string, string>;
|
||||
/** The fully cloned group param with remapped IDs */
|
||||
cloned: IDrawingGroupNestedParam;
|
||||
}
|
||||
|
||||
export type GroupFeaturePasteHook = (params: IGroupFeaturePasteHookParams) => { redos: IMutationInfo[]; undos: IMutationInfo[] };
|
||||
|
||||
const specialPastes: IPasteHookValueType[] = [
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_COL_WIDTH,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_VALUE,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMAT,
|
||||
PREDEFINED_HOOK_NAME_PASTE.SPECIAL_PASTE_FORMULA,
|
||||
];
|
||||
|
||||
export class SheetsDrawingGroupCopyPasteController extends Disposable {
|
||||
private readonly _featurePasteHooks: GroupFeaturePasteHook[] = [];
|
||||
|
||||
private _copyInfo: Nullable<{
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
groupNestedParam: IDrawingGroupNestedParam;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService,
|
||||
@ISheetDrawingService private readonly _sheetDrawingService: ISheetDrawingService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService
|
||||
) {
|
||||
super();
|
||||
this._initCopyPaste();
|
||||
}
|
||||
|
||||
private get _focusedDrawings() {
|
||||
return this._sheetDrawingService.getFocusDrawings() as ISheetDrawing[];
|
||||
}
|
||||
|
||||
private _initCopyPaste() {
|
||||
this._sheetClipboardService.addClipboardHook({
|
||||
id: 'SHEET_DRAWING_GROUP',
|
||||
|
||||
onBeforeCopyFocusedObject: (_unitId, _subUnitId) => {
|
||||
this._copyInfo = null;
|
||||
|
||||
const focusDrawings = this._focusedDrawings;
|
||||
if (focusDrawings.length === 0) return false;
|
||||
|
||||
// Only handle the first focused group drawing.
|
||||
const groupDrawing = focusDrawings.find((d) => d.drawingType === DrawingTypeEnum.DRAWING_GROUP);
|
||||
if (!groupDrawing) return false;
|
||||
|
||||
const groupNestedParam = this._drawingManagerService.getDrawingsByGroupNested({
|
||||
unitId: groupDrawing.unitId,
|
||||
subUnitId: groupDrawing.subUnitId,
|
||||
drawingId: groupDrawing.drawingId,
|
||||
});
|
||||
if (!groupNestedParam) return false;
|
||||
|
||||
this._copyInfo = {
|
||||
unitId: groupDrawing.unitId,
|
||||
subUnitId: groupDrawing.subUnitId,
|
||||
groupNestedParam,
|
||||
};
|
||||
return true;
|
||||
},
|
||||
|
||||
onPasteCells: (pasteFrom, pasteTo, _data, payload) => {
|
||||
if (!payload.copyId || !pasteFrom || !this._copyInfo) return { redos: [], undos: [] };
|
||||
|
||||
const { pasteType } = payload;
|
||||
if (specialPastes.includes(pasteType)) return { redos: [], undos: [] };
|
||||
|
||||
return this._generateGroupPasteMutations(pasteTo);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public registerFeaturePasteHook(hook: GroupFeaturePasteHook): void {
|
||||
this._featurePasteHooks.push(hook);
|
||||
}
|
||||
|
||||
private _getGroupFeaturePasteMutations(params: IGroupFeaturePasteHookParams): { redos: IMutationInfo[]; undos: IMutationInfo[] } {
|
||||
const redos: IMutationInfo[] = [];
|
||||
const undos: IMutationInfo[] = [];
|
||||
for (const hook of this._featurePasteHooks) {
|
||||
const result = hook(params);
|
||||
redos.push(...result.redos);
|
||||
undos.push(...result.undos);
|
||||
}
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
private _generateGroupPasteMutations(pasteTo: ISheetDiscreteRangeLocation): { redos: IMutationInfo[]; undos: IMutationInfo[] } {
|
||||
if (!this._copyInfo) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, range } = pasteTo;
|
||||
const pasteToSkeleton = this._sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!pasteToSkeleton) {
|
||||
return { redos: [], undos: [] };
|
||||
}
|
||||
|
||||
const { groupNestedParam } = this._copyInfo;
|
||||
// Root group is the last element of the post-order groups array.
|
||||
const origRootGroup = groupNestedParam.groups[groupNestedParam.groups.length - 1] as ISheetDrawing;
|
||||
|
||||
// Clone all drawing IDs.
|
||||
const { cloned, idMap } = cloneGroupParams(groupNestedParam);
|
||||
const newRootGroupId = cloned.groups[cloned.groups.length - 1].drawingId;
|
||||
|
||||
// Resolve the paste destination rectangle.
|
||||
const pasteRange = discreteRangeToRange(range);
|
||||
const pasteRect = attachRangeWithCoord(pasteToSkeleton, {
|
||||
startRow: pasteRange.startRow,
|
||||
endRow: pasteRange.endRow,
|
||||
startColumn: pasteRange.startColumn,
|
||||
endColumn: pasteRange.endColumn,
|
||||
});
|
||||
if (!pasteRect) return { redos: [], undos: [] };
|
||||
|
||||
// Only the root group's absolute position changes; all other drawings keep their
|
||||
// group-relative transforms as-is.
|
||||
const newTransform = { ...(origRootGroup.transform), left: pasteRect.startX, top: pasteRect.startY };
|
||||
|
||||
const allDrawings: ISheetDrawing[] = [
|
||||
...(cloned.flatChildren ?? []).map((d) => ({ ...d, unitId, subUnitId } as ISheetDrawing)),
|
||||
...cloned.groups.map((d) => {
|
||||
if (d.drawingId !== newRootGroupId) {
|
||||
return { ...d, unitId, subUnitId } as ISheetDrawing;
|
||||
}
|
||||
return {
|
||||
...d,
|
||||
unitId,
|
||||
subUnitId,
|
||||
transform: newTransform,
|
||||
sheetTransform: transformToDrawingPosition(newTransform, pasteToSkeleton) ?? origRootGroup.sheetTransform,
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(newTransform, pasteToSkeleton) ?? origRootGroup.sheetTransform,
|
||||
} as ISheetDrawing;
|
||||
}),
|
||||
];
|
||||
|
||||
// A single batch INSERT is sufficient: the shape rendering layer calls insertGroupObject()
|
||||
// for each shape's groupId, which automatically reconstructs the full group hierarchy in
|
||||
// the scene from the data that was just written by applyJson1.
|
||||
const { undo: removeOp, redo: insertOp, objects } = this._sheetDrawingService.getBatchAddOp(allDrawings) as IDrawingJsonUndo1;
|
||||
|
||||
const redos: IMutationInfo[] = [{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: { op: insertOp, unitId, subUnitId, objects, type: DrawingApplyType.INSERT },
|
||||
}];
|
||||
|
||||
const undos: IMutationInfo[] = [{
|
||||
id: SetDrawingApplyMutation.id,
|
||||
params: { op: removeOp, unitId, subUnitId, objects, type: DrawingApplyType.REMOVE },
|
||||
}];
|
||||
|
||||
const featureMutations = this._getGroupFeaturePasteMutations({
|
||||
fromUnitId: this._copyInfo.unitId,
|
||||
fromSubUnitId: this._copyInfo.subUnitId,
|
||||
toUnitId: unitId,
|
||||
toSubUnitId: subUnitId,
|
||||
idMap,
|
||||
cloned,
|
||||
});
|
||||
redos.push(...featureMutations.redos);
|
||||
undos.push(...featureMutations.undos);
|
||||
|
||||
return { redos, undos };
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._copyInfo = null;
|
||||
this._featurePasteHooks.length = 0;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* 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, Worksheet } from '@univerjs/core';
|
||||
import type {
|
||||
IInsertSheetDrawingCommandParams,
|
||||
IRemoveSheetDrawingCommandParams,
|
||||
ISetDrawingArrangeCommandParams,
|
||||
ISetDrawingCommandParams,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IPermissionService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
UniverInstanceType,
|
||||
UserManagerService,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService, ObjectType } from '@univerjs/engine-render';
|
||||
import {
|
||||
SheetPermissionCheckController,
|
||||
WorkbookEditablePermission,
|
||||
WorkbookViewPermission,
|
||||
WorksheetEditPermission,
|
||||
WorksheetViewPermission,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
InsertSheetDrawingCommand,
|
||||
ISheetDrawingService,
|
||||
RemoveSheetDrawingCommand,
|
||||
SetDrawingArrangeCommand,
|
||||
SetSheetDrawingCommand,
|
||||
SetSheetDrawingPlacementCommand,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import { combineLatest, distinctUntilChanged, EMPTY, map, switchMap, tap } from 'rxjs';
|
||||
|
||||
const drawingObjectTypes = [
|
||||
ObjectType.IMAGE, // floating image
|
||||
ObjectType.SHAPE, // shape
|
||||
ObjectType.CHART, // chart rect
|
||||
ObjectType.DRAWING_DOM, // floating dom rect
|
||||
];
|
||||
|
||||
export class SheetDrawingPermissionController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(LocaleService) private _localeService: LocaleService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IPermissionService private readonly _permissionService: IPermissionService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(UserManagerService) private _userManagerService: UserManagerService,
|
||||
@Inject(SheetPermissionCheckController) private _sheetPermissionCheckController: SheetPermissionCheckController,
|
||||
@Inject(ISheetDrawingService) private readonly _sheetDrawingService: ISheetDrawingService
|
||||
) {
|
||||
super();
|
||||
this._initDrawingVisible();
|
||||
this._initDrawingEditable();
|
||||
this._initViewPermissionChange();
|
||||
this._initEditPermissionChange();
|
||||
this._initCommandPermissionCheck();
|
||||
}
|
||||
|
||||
private _initDrawingVisible() {
|
||||
const workbook$ = this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentUser$ = this._userManagerService.currentUser$;
|
||||
const combined$ = combineLatest([workbook$, currentUser$]);
|
||||
|
||||
this.disposeWithMe(
|
||||
combined$
|
||||
.pipe(
|
||||
switchMap(([workbook, _]) => {
|
||||
if (!workbook) {
|
||||
this._sheetDrawingService.setDrawingVisible(false);
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return workbook.activeSheet$.pipe(
|
||||
tap((sheet) => {
|
||||
if (!sheet) {
|
||||
this._sheetDrawingService.setDrawingVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = sheet.getSheetId();
|
||||
|
||||
const worksheetViewPermission = this._permissionService
|
||||
.composePermission([
|
||||
new WorkbookViewPermission(unitId).id,
|
||||
new WorksheetViewPermission(unitId, subUnitId).id,
|
||||
])
|
||||
.every((permission) => permission.value);
|
||||
|
||||
if (worksheetViewPermission) {
|
||||
this._sheetDrawingService.setDrawingVisible(true);
|
||||
} else {
|
||||
this._handleDrawingVisibilityFalse(workbook, sheet);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDrawingVisibilityFalse(workbook: Workbook, sheet: Worksheet) {
|
||||
this._sheetDrawingService.setDrawingVisible(false);
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = sheet.getSheetId();
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawingDataValues = Object.values(drawingData);
|
||||
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objects = scene.getAllObjectsByOrder();
|
||||
|
||||
objects.forEach((object) => {
|
||||
if (
|
||||
drawingObjectTypes.includes(object.objectType) &&
|
||||
drawingDataValues.some((item) => object.oKey.includes(item.drawingId))
|
||||
) {
|
||||
scene.removeObject(object);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _initDrawingEditable() {
|
||||
const workbook$ = this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentUser$ = this._userManagerService.currentUser$;
|
||||
|
||||
const combined$ = combineLatest([workbook$, currentUser$]);
|
||||
|
||||
this.disposeWithMe(
|
||||
combined$
|
||||
.pipe(
|
||||
switchMap(([workbook, _]) => {
|
||||
if (!workbook) {
|
||||
this._sheetDrawingService.setDrawingEditable(false);
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return workbook.activeSheet$.pipe(
|
||||
tap((sheet) => {
|
||||
if (!sheet) {
|
||||
this._sheetDrawingService.setDrawingEditable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = sheet.getSheetId();
|
||||
|
||||
const worksheetEditPermission = this._permissionService
|
||||
.composePermission([
|
||||
new WorkbookEditablePermission(unitId).id,
|
||||
new WorksheetEditPermission(unitId, subUnitId).id,
|
||||
])
|
||||
.every((permission) => permission.value);
|
||||
|
||||
if (worksheetEditPermission) {
|
||||
this._sheetDrawingService.setDrawingEditable(true);
|
||||
} else {
|
||||
this._handleDrawingEditableFalse(workbook, sheet);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDrawingEditableFalse(workbook: Workbook, sheet: Worksheet) {
|
||||
this._sheetDrawingService.setDrawingEditable(false);
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = sheet.getSheetId();
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawingDataValues = Object.values(drawingData);
|
||||
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objects = scene.getAllObjectsByOrder();
|
||||
|
||||
objects.forEach((object) => {
|
||||
if (
|
||||
drawingObjectTypes.includes(object.objectType) &&
|
||||
drawingDataValues.some((item) => object.oKey.includes(item.drawingId))
|
||||
) {
|
||||
scene.detachTransformerFrom(object);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initViewPermissionChange() {
|
||||
const workbook$ = this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentUser$ = this._userManagerService.currentUser$;
|
||||
this.disposeWithMe(
|
||||
combineLatest([workbook$, currentUser$])
|
||||
.pipe(
|
||||
switchMap(([workbook, _]) => {
|
||||
if (!workbook) return EMPTY;
|
||||
|
||||
return workbook.activeSheet$.pipe(
|
||||
switchMap((sheet) => {
|
||||
if (!sheet) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = sheet.getSheetId();
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
|
||||
if (!scene) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const worksheetViewPermission$ = this._permissionService
|
||||
.composePermission$([
|
||||
new WorkbookViewPermission(unitId).id,
|
||||
new WorksheetViewPermission(unitId, subUnitId).id,
|
||||
])
|
||||
.pipe(
|
||||
map((permissions) => permissions.every((item) => item.value)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return worksheetViewPermission$.pipe(
|
||||
map((permission) => ({
|
||||
permission,
|
||||
scene,
|
||||
transformer,
|
||||
unitId,
|
||||
subUnitId,
|
||||
}))
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ permission, scene, transformer, unitId, subUnitId }) => {
|
||||
this._sheetDrawingService.setDrawingVisible(permission);
|
||||
|
||||
const objects = scene.getAllObjectsByOrder();
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawingDataValues = Object.values(drawingData);
|
||||
|
||||
if (permission) {
|
||||
this._sheetDrawingService.addNotification(drawingDataValues);
|
||||
} else {
|
||||
objects.forEach((object) => {
|
||||
if (
|
||||
drawingObjectTypes.includes(object.objectType) &&
|
||||
drawingDataValues.some((item) => object.oKey.includes(item.drawingId))
|
||||
) {
|
||||
scene.removeObject(object);
|
||||
}
|
||||
});
|
||||
transformer.clearSelectedObjects();
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this._sheetDrawingService.setDrawingVisible(true);
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const sheet = workbook?.getActiveSheet();
|
||||
const unitId = workbook?.getUnitId();
|
||||
const subUnitId = sheet?.getSheetId();
|
||||
if (!unitId || !subUnitId) {
|
||||
return;
|
||||
}
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawingDataValues = Object.values(drawingData);
|
||||
this._sheetDrawingService.addNotification(drawingDataValues);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initEditPermissionChange() {
|
||||
const workbook$ = this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const currentUser$ = this._userManagerService.currentUser$;
|
||||
|
||||
this.disposeWithMe(
|
||||
combineLatest([workbook$, currentUser$])
|
||||
.pipe(
|
||||
switchMap(([workbook, _]) => {
|
||||
if (!workbook) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return workbook.activeSheet$.pipe(
|
||||
switchMap((sheet) => {
|
||||
if (!sheet) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = sheet.getSheetId();
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
|
||||
if (!scene) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const composeWorksheetEditPermission$ = this._permissionService
|
||||
.composePermission$([
|
||||
new WorkbookEditablePermission(unitId).id,
|
||||
new WorksheetEditPermission(unitId, subUnitId).id,
|
||||
])
|
||||
.pipe(
|
||||
map((permissions) => permissions.every((item) => item.value)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return composeWorksheetEditPermission$.pipe(
|
||||
map((permission) => ({
|
||||
permission,
|
||||
scene,
|
||||
transformer,
|
||||
unitId,
|
||||
subUnitId,
|
||||
}))
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ permission, scene, transformer, unitId, subUnitId }) => {
|
||||
this._sheetDrawingService.setDrawingEditable(permission);
|
||||
|
||||
const objects = scene.getAllObjectsByOrder();
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawingDataValues = Object.values(drawingData);
|
||||
|
||||
if (permission) {
|
||||
objects.forEach((object) => {
|
||||
if (
|
||||
drawingObjectTypes.includes(object.objectType) &&
|
||||
drawingDataValues.some((item) => object.oKey.includes(item.drawingId))
|
||||
) {
|
||||
scene.attachTransformerTo(object);
|
||||
}
|
||||
});
|
||||
|
||||
this._sheetDrawingService.addNotification(drawingDataValues);
|
||||
} else {
|
||||
objects.forEach((object) => {
|
||||
if (
|
||||
drawingObjectTypes.includes(object.objectType) &&
|
||||
drawingDataValues.some((item) => object.oKey.includes(item.drawingId))
|
||||
) {
|
||||
scene.detachTransformerFrom(object);
|
||||
}
|
||||
});
|
||||
|
||||
transformer.clearSelectedObjects();
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
const sheet = workbook.getActiveSheet();
|
||||
if (!sheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subUnitId = sheet.getSheetId();
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingData = this._sheetDrawingService.getDrawingData(unitId, subUnitId);
|
||||
const drawingDataValues = Object.values(drawingData);
|
||||
|
||||
this._sheetDrawingService.setDrawingEditable(true);
|
||||
|
||||
const objects = scene.getAllObjectsByOrder();
|
||||
objects.forEach((object) => {
|
||||
if (
|
||||
drawingObjectTypes.includes(object.objectType) &&
|
||||
drawingDataValues.some((item) => object.oKey.includes(item.drawingId))
|
||||
) {
|
||||
scene.detachTransformerFrom(object);
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initCommandPermissionCheck() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command) => {
|
||||
let unitId: string | undefined;
|
||||
let subUnitId: string | undefined;
|
||||
|
||||
if (command.id === InsertSheetDrawingCommand.id || command.id === RemoveSheetDrawingCommand.id || command.id === SetSheetDrawingCommand.id) {
|
||||
const params = command.params as IInsertSheetDrawingCommandParams | IRemoveSheetDrawingCommandParams | ISetDrawingCommandParams;
|
||||
const { drawings } = params;
|
||||
unitId = drawings?.[0]?.unitId;
|
||||
subUnitId = drawings?.[0]?.subUnitId;
|
||||
} else if (command.id === SetSheetDrawingPlacementCommand.id) {
|
||||
const target = getPlacementCommandTarget(command.params);
|
||||
unitId = target.unitId;
|
||||
subUnitId = target.subUnitId;
|
||||
} else if (command.id === SetDrawingArrangeCommand.id) {
|
||||
const params = command.params as ISetDrawingArrangeCommandParams;
|
||||
unitId = params.unitId;
|
||||
subUnitId = params.subUnitId;
|
||||
}
|
||||
|
||||
if (!unitId || !subUnitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const permission = this._sheetPermissionCheckController.permissionCheckWithoutRange({
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission],
|
||||
}, unitId, subUnitId);
|
||||
if (!permission) {
|
||||
this._sheetPermissionCheckController.blockExecuteWithoutPermission(this._localeService.t<LocaleKey>('sheets-drawing-ui.permission.dialog.editErr'));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getPlacementCommandTarget(params: object | undefined): {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
} {
|
||||
if (!params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const unitId = 'unitId' in params && typeof params.unitId === 'string'
|
||||
? params.unitId
|
||||
: undefined;
|
||||
const subUnitId = 'subUnitId' in params && typeof params.subUnitId === 'string'
|
||||
? params.subUnitId
|
||||
: undefined;
|
||||
return { unitId, subUnitId };
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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 { IRange } from '@univerjs/core';
|
||||
import type { IFloatDomData } from '@univerjs/sheets-drawing';
|
||||
import { Disposable, DrawingTypeEnum, Inject, Injector, Tools } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DrawingRenderService } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetPrintInterceptorService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { mountPrintingFloatDom } from '../views/PrintingFloatDom';
|
||||
|
||||
export class SheetDrawingPrintingController extends Disposable {
|
||||
constructor(
|
||||
@Inject(SheetPrintInterceptorService) private readonly _sheetPrintInterceptorService: SheetPrintInterceptorService,
|
||||
@Inject(DrawingRenderService) private readonly _drawingRenderService: DrawingRenderService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(ComponentManager) private readonly _componetManager: ComponentManager,
|
||||
@Inject(Injector) private readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initPrinting();
|
||||
this._initPrintingDom();
|
||||
}
|
||||
|
||||
private _initPrinting() {
|
||||
this.disposeWithMe(
|
||||
this._sheetPrintInterceptorService.interceptor.intercept(
|
||||
this._sheetPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_COMPONENT_COLLECT,
|
||||
{
|
||||
handler: (_param, pos, next) => {
|
||||
const { unitId, scene, subUnitId, resourceCollector } = pos;
|
||||
const unitData = this._drawingManagerService.getDrawingDataForUnit(unitId);
|
||||
const subUnitData = unitData?.[subUnitId];
|
||||
if (subUnitData) {
|
||||
subUnitData.order.forEach((id) => {
|
||||
const drawing = subUnitData.data[id];
|
||||
if (drawing.hidden) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
drawing.drawingType !== DrawingTypeEnum.DRAWING_CHART &&
|
||||
drawing.drawingType !== DrawingTypeEnum.DRAWING_DOM &&
|
||||
drawing.drawingType !== DrawingTypeEnum.DRAWING_BLOCK
|
||||
) {
|
||||
const resource = this._drawingRenderService.renderDrawing(drawing, scene, { allowInactiveSheet: true });
|
||||
if (resource) {
|
||||
resourceCollector.add(Promise.resolve(resource));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._sheetPrintInterceptorService.interceptor.intercept(
|
||||
this._sheetPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_RANGE,
|
||||
{
|
||||
handler: (range, pos, next) => {
|
||||
const { unitId, subUnitId } = pos;
|
||||
const renderer = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (!renderer) {
|
||||
return next(range);
|
||||
}
|
||||
|
||||
const skeleton = renderer.with(SheetSkeletonManagerService).getSkeletonParam(subUnitId);
|
||||
if (!skeleton) {
|
||||
return next(range);
|
||||
}
|
||||
const unitData = this._drawingManagerService.getDrawingDataForUnit(unitId);
|
||||
const subUnitData = unitData?.[pos.subUnitId];
|
||||
if (!subUnitData) {
|
||||
return next(range);
|
||||
}
|
||||
|
||||
const { scaleX, scaleY } = renderer.scene;
|
||||
const newRange: IRange = range ? { ...range } : { startColumn: 0, endColumn: 0, endRow: 0, startRow: 0 };
|
||||
const data = subUnitData.order.map((key) => subUnitData.data[key]);
|
||||
if (data.length) {
|
||||
data.forEach((param) => {
|
||||
if (!param.hidden && !param.groupId && param.transform && Tools.isDefine(param.transform.left) && Tools.isDefine(param.transform.top) && Tools.isDefine(param.transform.width) && Tools.isDefine(param.transform.height)) {
|
||||
const start = skeleton.skeleton.getCellIndexByOffset(param.transform.left, param.transform.top, scaleX, scaleY, { x: 0, y: 0 });
|
||||
const end = skeleton.skeleton.getCellIndexByOffset(param.transform.left + param.transform.width, param.transform.top + param.transform.height, scaleX, scaleY, { x: 0, y: 0 });
|
||||
if (start.column < newRange.startColumn) {
|
||||
newRange.startColumn = start.column;
|
||||
}
|
||||
|
||||
if (start.row < newRange.startRow) {
|
||||
newRange.startRow = start.row;
|
||||
}
|
||||
|
||||
if (newRange.endRow < end.row) {
|
||||
newRange.endRow = end.row;
|
||||
}
|
||||
|
||||
if (newRange.endColumn < end.column) {
|
||||
newRange.endColumn = end.column;
|
||||
}
|
||||
}
|
||||
});
|
||||
return next(newRange);
|
||||
}
|
||||
|
||||
return next(range);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _initPrintingDom() {
|
||||
this.disposeWithMe(
|
||||
this._sheetPrintInterceptorService.interceptor.intercept(
|
||||
this._sheetPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_DOM_COLLECT,
|
||||
{
|
||||
handler: (disposableCollection, pos, next) => {
|
||||
const { unitId, subUnitId } = pos;
|
||||
const unitData = this._drawingManagerService.getDrawingDataForUnit(unitId);
|
||||
const subUnitData = unitData?.[subUnitId];
|
||||
if (subUnitData) {
|
||||
const floatDomInfos = subUnitData.order.map((id) => {
|
||||
const drawing = subUnitData.data[id] as IFloatDomData;
|
||||
if (drawing.hidden) {
|
||||
return null;
|
||||
}
|
||||
if (drawing.drawingType === DrawingTypeEnum.DRAWING_DOM || drawing.drawingType === DrawingTypeEnum.DRAWING_BLOCK) {
|
||||
const printingComponentKey = this._sheetPrintInterceptorService.getPrintComponent(drawing.componentKey);
|
||||
return {
|
||||
...drawing,
|
||||
componentKey: this._componetManager.get(printingComponentKey || drawing.componentKey) as any,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Boolean) as IFloatDomData[];
|
||||
// Each prepare cycle gets its own React container. Removing the old
|
||||
// container immediately and unmounting it later prevents its cleanup
|
||||
// from racing with a new render mounted into the persistent page root.
|
||||
const printingRoot = document.createElement('div');
|
||||
printingRoot.style.position = 'absolute';
|
||||
printingRoot.style.inset = '0';
|
||||
pos.root.appendChild(printingRoot);
|
||||
const unmountPrintingFloatDom = mountPrintingFloatDom(
|
||||
{
|
||||
floatDomInfos,
|
||||
scene: pos.scene,
|
||||
skeleton: pos.skeleton,
|
||||
worksheet: pos.worksheet,
|
||||
},
|
||||
printingRoot,
|
||||
this._injector
|
||||
);
|
||||
|
||||
disposableCollection?.add(() => {
|
||||
printingRoot.remove();
|
||||
queueMicrotask(unmountPrintingFloatDom);
|
||||
});
|
||||
|
||||
return next(disposableCollection);
|
||||
}
|
||||
|
||||
// Do not terminate the interceptor chain on sheets without drawings.
|
||||
return next(disposableCollection);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,722 @@
|
||||
/**
|
||||
* 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, IDrawingParam, IImageIoServiceParam, IRange, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { IImageData } from '@univerjs/drawing';
|
||||
import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { ISheetLocationBase, WorkbookSelectionModel } from '@univerjs/sheets';
|
||||
import type {
|
||||
IInsertSheetDrawingCommandParams,
|
||||
ISetDrawingArrangeCommandParams,
|
||||
ISetDrawingCommandParams,
|
||||
ISheetDrawing,
|
||||
ISheetDrawingPosition,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
BooleanNumber,
|
||||
BuildTextUtils,
|
||||
createDocumentModelWithStyle,
|
||||
Disposable,
|
||||
DrawingTypeEnum,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
IImageIoService,
|
||||
ImageSourceType,
|
||||
ImageUploadStatusType,
|
||||
Inject,
|
||||
Injector,
|
||||
IURLImageService,
|
||||
LocaleService,
|
||||
PositionedObjectLayoutType,
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { buildDocTransform, docDrawingPositionToTransform } from '@univerjs/docs';
|
||||
import {
|
||||
DRAWING_IMAGE_ALLOW_IMAGE_LIST,
|
||||
DRAWING_IMAGE_COUNT_LIMIT,
|
||||
DRAWING_IMAGE_HEIGHT_LIMIT,
|
||||
DRAWING_IMAGE_WIDTH_LIMIT,
|
||||
getDrawingImageAllowSize,
|
||||
getImageSize,
|
||||
IDrawingManagerService,
|
||||
SetDrawingSelectedOperation,
|
||||
} from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import {
|
||||
attachRangeWithCoord,
|
||||
SetRangeValuesCommand,
|
||||
SheetInterceptorService,
|
||||
SheetSkeletonService,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
drawingPositionToTransform,
|
||||
InsertSheetDrawingCommand,
|
||||
ISheetDrawingService,
|
||||
SetDrawingArrangeCommand,
|
||||
SetSheetDrawingCommand,
|
||||
transformToAxisAlignPosition,
|
||||
transformToDrawingPosition,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import { ISheetSelectionRenderService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import { ILocalFileService, IMessageService } from '@univerjs/ui';
|
||||
import { GroupSheetDrawingCommand } from '../commands/commands/group-sheet-drawing.command';
|
||||
import { UngroupSheetDrawingCommand } from '../commands/commands/ungroup-sheet-drawing.command';
|
||||
|
||||
/**
|
||||
* Calculate the bounding box after rotation
|
||||
* @param {number} width Width
|
||||
* @param {number} height Height
|
||||
* @param {number} angleDegrees Rotation angle in degrees (0-360)
|
||||
* @returns {{ rotatedWidth: number; rotatedHeight: number }} Rotated width and height
|
||||
*/
|
||||
function rotatedBoundingBox(width: number, height: number, angleDegrees: number): { rotatedWidth: number; rotatedHeight: number } {
|
||||
const angle = angleDegrees * Math.PI / 180; // Convert angle to radians
|
||||
const rotatedWidth = Math.abs(width * Math.cos(angle)) + Math.abs(height * Math.sin(angle));
|
||||
const rotatedHeight = Math.abs(width * Math.sin(angle)) + Math.abs(height * Math.cos(angle));
|
||||
return { rotatedWidth, rotatedHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the drawing within the cell
|
||||
* @param {IAccessor} accessor Accessor
|
||||
* @param {ISheetLocationBase} location Cell location
|
||||
* @param {number} originImageWidth Original image width
|
||||
* @param {number} originImageHeight Original image height
|
||||
* @param {number} angle Rotation angle in degrees (0-360)
|
||||
* @returns {{ width: number; height: number }} Drawing size
|
||||
*/
|
||||
export function getDrawingSizeByCell(
|
||||
accessor: IAccessor,
|
||||
location: ISheetLocationBase,
|
||||
originImageWidth: number,
|
||||
originImageHeight: number,
|
||||
angle: number
|
||||
) {
|
||||
const { rotatedHeight, rotatedWidth } = rotatedBoundingBox(originImageWidth, originImageHeight, angle);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const currentRender = renderManagerService.getRenderUnitById(location.unitId);
|
||||
if (!currentRender) {
|
||||
return false;
|
||||
}
|
||||
const skeletonManagerService = currentRender.with(SheetSkeletonManagerService);
|
||||
const skeleton = skeletonManagerService.getSkeletonParam(location.subUnitId)?.skeleton;
|
||||
if (skeleton == null) {
|
||||
return false;
|
||||
}
|
||||
const cellInfo = skeleton.getCellWithCoordByIndex(location.row, location.col);
|
||||
|
||||
const cellWidth = cellInfo.mergeInfo.endX - cellInfo.mergeInfo.startX - 2;
|
||||
const cellHeight = cellInfo.mergeInfo.endY - cellInfo.mergeInfo.startY - 2;
|
||||
const imageRatio = rotatedWidth / rotatedHeight;
|
||||
const imageWidth = Math.ceil(Math.min(cellWidth, cellHeight * imageRatio));
|
||||
const scale = imageWidth / rotatedWidth;
|
||||
const realScale = !(scale) || Number.isNaN(scale) ? 0.001 : scale;
|
||||
|
||||
return {
|
||||
width: originImageWidth * realScale,
|
||||
height: originImageHeight * realScale,
|
||||
};
|
||||
}
|
||||
|
||||
export class SheetDrawingUpdateController extends Disposable implements IRenderModule {
|
||||
private readonly _workbookSelections: WorkbookSelectionModel;
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<Workbook>,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService,
|
||||
@ISheetSelectionRenderService private readonly _selectionRenderService: ISheetSelectionRenderService,
|
||||
@IImageIoService private readonly _imageIoService: IImageIoService,
|
||||
@ILocalFileService private readonly _fileOpenerService: ILocalFileService,
|
||||
@ISheetDrawingService private readonly _sheetDrawingService: ISheetDrawingService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@Inject(SheetsSelectionsService) selectionManagerService: SheetsSelectionsService,
|
||||
@Inject(SheetSkeletonService) private readonly _sheetSkeletonService: SheetSkeletonService,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IURLImageService private readonly _urlImageService: IURLImageService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._workbookSelections = selectionManagerService.getWorkbookSelections(this._context.unitId);
|
||||
|
||||
this._updateDrawingListener();
|
||||
this._updateOrderListener();
|
||||
this._groupDrawingListener();
|
||||
this._focusDrawingListener();
|
||||
}
|
||||
|
||||
async insertFloatImage(): Promise<boolean> {
|
||||
const files = await this._fileOpenerService.openFile({
|
||||
multiple: true,
|
||||
accept: DRAWING_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','),
|
||||
});
|
||||
|
||||
const fileLength = files.length;
|
||||
if (fileLength > DRAWING_IMAGE_COUNT_LIMIT) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.exceedMaxCount', String(DRAWING_IMAGE_COUNT_LIMIT)),
|
||||
});
|
||||
return false;
|
||||
} else if (fileLength === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
files.forEach(async (file) => await this.insertFloatImageByFile(file));
|
||||
return true;
|
||||
}
|
||||
|
||||
async insertCellImage(): Promise<boolean> {
|
||||
const files = await this._fileOpenerService.openFile({
|
||||
multiple: false,
|
||||
accept: DRAWING_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','),
|
||||
});
|
||||
const file = files[0];
|
||||
if (file) {
|
||||
await this._insertCellImage(file);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
insertCellImageByFile(file: File, location?: ISheetLocationBase) {
|
||||
return this._insertCellImage(file, location);
|
||||
}
|
||||
|
||||
async insertFloatImageByFile(file: File) {
|
||||
let imageParam: Nullable<IImageIoServiceParam>;
|
||||
|
||||
try {
|
||||
imageParam = await this._imageIoService.saveImage(file);
|
||||
} catch (error) {
|
||||
const type = (error as Error).message;
|
||||
if (type === ImageUploadStatusType.ERROR_EXCEED_SIZE) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.exceedMaxSize', String(getDrawingImageAllowSize() / (1024 * 1024))),
|
||||
});
|
||||
} else if (type === ImageUploadStatusType.ERROR_IMAGE_TYPE) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.invalidImageType'),
|
||||
});
|
||||
} else if (type === ImageUploadStatusType.ERROR_IMAGE) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.invalidImage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (imageParam == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = this._getUnitInfo();
|
||||
const { unitId, subUnitId } = info;
|
||||
const { imageId, imageSourceType, source, base64Cache } = imageParam;
|
||||
const { width, height, image } = await getImageSize(base64Cache || '');
|
||||
const { width: sceneWidth, height: sceneHeight } = this._context.scene;
|
||||
|
||||
this._imageIoService.addImageSourceCache(source, imageSourceType, image);
|
||||
|
||||
let scale = 1;
|
||||
if (width > DRAWING_IMAGE_WIDTH_LIMIT || height > DRAWING_IMAGE_HEIGHT_LIMIT) {
|
||||
const scaleWidth = DRAWING_IMAGE_WIDTH_LIMIT / width;
|
||||
const scaleHeight = DRAWING_IMAGE_HEIGHT_LIMIT / height;
|
||||
scale = Math.max(scaleWidth, scaleHeight);
|
||||
}
|
||||
|
||||
const sheetSkeletonParam = this._sheetSkeletonService.getSkeletonParam(unitId, subUnitId);
|
||||
if (!sheetSkeletonParam) return;
|
||||
|
||||
const { skeleton } = sheetSkeletonParam;
|
||||
const sheetTransform = this._getImagePosition(width * scale, height * scale, sceneWidth, sceneHeight, skeleton);
|
||||
if (!sheetTransform) return;
|
||||
|
||||
const newTransform = drawingPositionToTransform(sheetTransform, sheetSkeletonParam);
|
||||
if (!newTransform) return;
|
||||
|
||||
const sheetDrawingParam: ISheetDrawing = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: imageId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType,
|
||||
source,
|
||||
transform: newTransform,
|
||||
sheetTransform,
|
||||
axisAlignSheetTransform: transformToAxisAlignPosition(newTransform, skeleton) ?? sheetTransform,
|
||||
};
|
||||
|
||||
return this._commandService.executeCommand<IInsertSheetDrawingCommandParams>(InsertSheetDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: [sheetDrawingParam],
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private async _insertCellImage(file: File, location?: ISheetLocationBase) {
|
||||
let imageParam: Nullable<IImageIoServiceParam>;
|
||||
try {
|
||||
imageParam = await this._imageIoService.saveImage(file);
|
||||
} catch (error) {
|
||||
const type = (error as Error).message;
|
||||
if (type === ImageUploadStatusType.ERROR_EXCEED_SIZE) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.exceedMaxSize', String(getDrawingImageAllowSize() / (1024 * 1024))),
|
||||
});
|
||||
} else if (type === ImageUploadStatusType.ERROR_IMAGE_TYPE) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.invalidImageType'),
|
||||
});
|
||||
} else if (type === ImageUploadStatusType.ERROR_IMAGE) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('sheets-drawing-ui.update-status.invalidImage'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (imageParam == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { imageId, imageSourceType, source, base64Cache } = imageParam;
|
||||
const { width, height, image } = await getImageSize(base64Cache || '');
|
||||
this._imageIoService.addImageSourceCache(source, imageSourceType, image);
|
||||
const selection = this._workbookSelections.getCurrentLastSelection();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// use start row and col when upload image to a merged cell
|
||||
let row = selection.primary.actualRow;
|
||||
let col = selection.primary.actualColumn;
|
||||
if (selection.primary.isMerged) {
|
||||
row = selection.primary.startRow;
|
||||
col = selection.primary.startColumn;
|
||||
}
|
||||
const docDataModel = createDocumentModelWithStyle('', {});
|
||||
|
||||
const imageSize = getDrawingSizeByCell(
|
||||
this._injector,
|
||||
{
|
||||
unitId: this._context.unitId,
|
||||
subUnitId: this._context.unit.getActiveSheet().getSheetId(),
|
||||
row,
|
||||
col,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
0
|
||||
);
|
||||
if (!imageSize) {
|
||||
return false;
|
||||
}
|
||||
const docTransform = buildDocTransform(imageSize.width, imageSize.height);
|
||||
const docDrawingParam = {
|
||||
unitId: docDataModel.getUnitId(),
|
||||
subUnitId: docDataModel.getUnitId(),
|
||||
drawingId: imageId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType,
|
||||
source,
|
||||
transform: docDrawingPositionToTransform(docTransform),
|
||||
docTransform,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
title: '',
|
||||
description: '',
|
||||
layoutType: PositionedObjectLayoutType.INLINE, // Insert inline drawing by default.
|
||||
wrapText: WrapTextType.BOTH_SIDES,
|
||||
distB: 0,
|
||||
distL: 0,
|
||||
distR: 0,
|
||||
distT: 0,
|
||||
};
|
||||
|
||||
const jsonXActions = BuildTextUtils.drawing.add({
|
||||
documentDataModel: docDataModel,
|
||||
drawings: [docDrawingParam],
|
||||
selection: {
|
||||
collapsed: true,
|
||||
startOffset: 0,
|
||||
endOffset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (jsonXActions) {
|
||||
docDataModel.apply(jsonXActions);
|
||||
|
||||
return this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, {
|
||||
value: {
|
||||
[location?.row ?? row]: {
|
||||
[location?.col ?? col]: {
|
||||
p: (docDataModel.getSnapshot()),
|
||||
t: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
unitId: location?.unitId,
|
||||
subUnitId: location?.subUnitId,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async insertCellImageByUrl(url: string, location?: ISheetLocationBase) {
|
||||
let src = url;
|
||||
try {
|
||||
src = await this._urlImageService.getImage(url);
|
||||
} catch (error) {
|
||||
console.error(`Failed to get image from URLImageService: ${url}`, error);
|
||||
}
|
||||
const { width, height, image } = await getImageSize(src || '');
|
||||
this._imageIoService.addImageSourceCache(url, ImageSourceType.URL, image);
|
||||
const selection = this._workbookSelections.getCurrentLastSelection();
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
const docDataModel = createDocumentModelWithStyle('', {});
|
||||
|
||||
const imageSize = getDrawingSizeByCell(
|
||||
this._injector,
|
||||
{
|
||||
unitId: this._context.unitId,
|
||||
subUnitId: this._context.unit.getActiveSheet().getSheetId(),
|
||||
row: selection.primary.actualRow,
|
||||
col: selection.primary.actualColumn,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
0
|
||||
);
|
||||
if (!imageSize) {
|
||||
return false;
|
||||
}
|
||||
const docTransform = buildDocTransform(imageSize.width, imageSize.height);
|
||||
const docDrawingParam = {
|
||||
unitId: docDataModel.getUnitId(),
|
||||
subUnitId: docDataModel.getUnitId(),
|
||||
drawingId: generateRandomId(),
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
source: url,
|
||||
transform: docDrawingPositionToTransform(docTransform),
|
||||
docTransform,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
title: '',
|
||||
description: '',
|
||||
layoutType: PositionedObjectLayoutType.INLINE, // Insert inline drawing by default.
|
||||
wrapText: WrapTextType.BOTH_SIDES,
|
||||
distB: 0,
|
||||
distL: 0,
|
||||
distR: 0,
|
||||
distT: 0,
|
||||
};
|
||||
|
||||
const jsonXActions = BuildTextUtils.drawing.add({
|
||||
documentDataModel: docDataModel,
|
||||
drawings: [docDrawingParam],
|
||||
selection: {
|
||||
collapsed: true,
|
||||
startOffset: 0,
|
||||
endOffset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (jsonXActions) {
|
||||
docDataModel.apply(jsonXActions);
|
||||
return this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, {
|
||||
value: {
|
||||
[location?.row ?? selection.primary.actualRow]: {
|
||||
[location?.col ?? selection.primary.actualColumn]: {
|
||||
p: (docDataModel.getSnapshot()),
|
||||
t: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
unitId: location?.unitId,
|
||||
subUnitId: location?.subUnitId,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getUnitInfo() {
|
||||
const workbook = this._context.unit;
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
const unitId = workbook.getUnitId();
|
||||
const subUnitId = worksheet.getSheetId();
|
||||
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
};
|
||||
}
|
||||
|
||||
private _getImagePosition(imageWidth: number, imageHeight: number, sceneWidth: number, sceneHeight: number, skeleton: SpreadsheetSkeleton): Nullable<ISheetDrawingPosition> {
|
||||
const selections = this._workbookSelections.getCurrentSelections();
|
||||
let range: IRange = {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
};
|
||||
if (selections && selections.length > 0) {
|
||||
range = selections[selections.length - 1].range;
|
||||
}
|
||||
|
||||
const rangeWithCoord = attachRangeWithCoord(skeleton, range);
|
||||
if (rangeWithCoord == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { startColumn, startRow, startX, startY } = rangeWithCoord;
|
||||
|
||||
let isChangeStart = false;
|
||||
if (startX + imageWidth > sceneWidth) {
|
||||
startX = sceneWidth - imageWidth;
|
||||
|
||||
if (startX < 0) {
|
||||
startX = 0;
|
||||
imageWidth = sceneWidth;
|
||||
}
|
||||
|
||||
isChangeStart = true;
|
||||
}
|
||||
|
||||
if (startY + imageHeight > sceneHeight) {
|
||||
startY = sceneHeight - imageHeight;
|
||||
|
||||
if (startY < 0) {
|
||||
startY = 0;
|
||||
imageHeight = sceneHeight;
|
||||
}
|
||||
|
||||
isChangeStart = true;
|
||||
}
|
||||
|
||||
if (isChangeStart) {
|
||||
const newCoord = this._selectionRenderService.getCellWithCoordByOffset(startX, startY);
|
||||
if (newCoord == null) {
|
||||
return;
|
||||
}
|
||||
startX = newCoord.startX;
|
||||
startY = newCoord.startY;
|
||||
startColumn = newCoord.actualColumn;
|
||||
startRow = newCoord.actualRow;
|
||||
}
|
||||
|
||||
const from = {
|
||||
column: startColumn,
|
||||
columnOffset: 0,
|
||||
row: startRow,
|
||||
rowOffset: 0,
|
||||
};
|
||||
|
||||
const endSelectionCell = this._selectionRenderService.getCellWithCoordByOffset(startX + imageWidth, startY + imageHeight);
|
||||
|
||||
if (endSelectionCell == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const to = {
|
||||
column: endSelectionCell.actualColumn,
|
||||
columnOffset: startX + imageWidth - endSelectionCell.startX,
|
||||
row: endSelectionCell.actualRow,
|
||||
rowOffset: startY + imageHeight - endSelectionCell.startY,
|
||||
};
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
private _updateOrderListener() {
|
||||
this.disposeWithMe(this._drawingManagerService.featurePluginOrderUpdate$.subscribe((params) => {
|
||||
const { unitId, subUnitId, drawingIds, arrangeType } = params;
|
||||
|
||||
this._commandService.executeCommand(SetDrawingArrangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingIds,
|
||||
arrangeType,
|
||||
} as ISetDrawingArrangeCommandParams);
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateDrawingListener() {
|
||||
this.disposeWithMe(this._drawingManagerService.featurePluginUpdate$.subscribe((params) => {
|
||||
const drawings: Partial<ISheetDrawing>[] = [];
|
||||
|
||||
if (params.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(params as IImageData[]).forEach((param) => {
|
||||
const { unitId, subUnitId, drawingId, transform } = param;
|
||||
const sheetSkeletonParam = this._sheetSkeletonService.getSkeletonParam(unitId, subUnitId);
|
||||
if (!transform || !sheetSkeletonParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { skeleton } = sheetSkeletonParam;
|
||||
const sheetDrawing = this._sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId });
|
||||
if (sheetDrawing == null || sheetDrawing.unitId !== this._context.unitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetTransform = transformToDrawingPosition({ ...sheetDrawing.transform, ...transform }, skeleton);
|
||||
const axisAlignSheetTransform = transformToAxisAlignPosition({ ...sheetDrawing.transform, ...transform }, skeleton);
|
||||
|
||||
if (sheetTransform == null || axisAlignSheetTransform == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDrawing: Partial<ISheetDrawing> = {
|
||||
...param,
|
||||
transform: { ...sheetDrawing.transform, ...transform, ...drawingPositionToTransform(sheetTransform, sheetSkeletonParam) },
|
||||
sheetTransform: { ...sheetTransform },
|
||||
axisAlignSheetTransform: { ...axisAlignSheetTransform },
|
||||
};
|
||||
|
||||
drawings.push(newDrawing);
|
||||
});
|
||||
|
||||
if (drawings.length > 0) {
|
||||
this._commandService.executeCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: params[0].unitId,
|
||||
drawings,
|
||||
} as ISetDrawingCommandParams);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _getSheetTransformByParam(param: IDrawingParam, isCreate: boolean): Nullable<{
|
||||
sheetTransform: ISheetDrawingPosition;
|
||||
axisAlignSheetTransform: ISheetDrawingPosition;
|
||||
}> {
|
||||
const { unitId, subUnitId, drawingId, transform } = param;
|
||||
const skeleton = this._sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!transform || !skeleton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sheetDrawing = this._sheetDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId });
|
||||
|
||||
let sheetDrawingTransform = sheetDrawing?.transform;
|
||||
if (isCreate) {
|
||||
sheetDrawingTransform = {};
|
||||
}
|
||||
|
||||
if (!isCreate && (!sheetDrawing || sheetDrawing.unitId !== this._context.unitId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sheetTransform = transformToDrawingPosition({ ...sheetDrawingTransform, ...transform }, skeleton);
|
||||
const axisAlignSheetTransform = transformToAxisAlignPosition({ ...sheetDrawingTransform, ...transform }, skeleton);
|
||||
|
||||
if (!sheetTransform || !axisAlignSheetTransform) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { sheetTransform, axisAlignSheetTransform };
|
||||
}
|
||||
|
||||
private _groupDrawingListener() {
|
||||
this.disposeWithMe(this._drawingManagerService.featurePluginGroupUpdate$.subscribe((params) => {
|
||||
const grpParams = [];
|
||||
for (const param of params) {
|
||||
const parent = param.parent;
|
||||
const grpSheetTransform = this._getSheetTransformByParam(parent, true);
|
||||
|
||||
const children = [];
|
||||
for (const child of param.children) {
|
||||
const childSheetTransformInfo = this._getSheetTransformByParam(child, false);
|
||||
if (childSheetTransformInfo != null) {
|
||||
children.push({
|
||||
...child,
|
||||
sheetTransform: childSheetTransformInfo.sheetTransform,
|
||||
axisAlignSheetTransform: childSheetTransformInfo.axisAlignSheetTransform,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const grpParam = {
|
||||
parent: { ...parent, sheetTransform: grpSheetTransform?.sheetTransform, axisAlignSheetTransform: grpSheetTransform?.axisAlignSheetTransform },
|
||||
children,
|
||||
|
||||
};
|
||||
grpParams.push(grpParam);
|
||||
}
|
||||
if (grpParams.length > 0) {
|
||||
this._commandService.executeCommand(GroupSheetDrawingCommand.id, grpParams);
|
||||
const { unitId, subUnitId, drawingId } = params[0].parent;
|
||||
this._commandService.syncExecuteCommand(SetDrawingSelectedOperation.id, [{ unitId, subUnitId, drawingId }]);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._drawingManagerService.featurePluginUngroupUpdate$.subscribe((params) => {
|
||||
const unGroupParams = [];
|
||||
for (const param of params) {
|
||||
const { children } = param;
|
||||
const childParams = [];
|
||||
for (const child of children) {
|
||||
const childSheetTransform = this._getSheetTransformByParam(child, false);
|
||||
if (childSheetTransform != null) {
|
||||
childParams.push({
|
||||
...child,
|
||||
sheetTransform: childSheetTransform.sheetTransform,
|
||||
axisAlignSheetTransform: childSheetTransform.axisAlignSheetTransform,
|
||||
});
|
||||
}
|
||||
}
|
||||
unGroupParams.push({
|
||||
...param,
|
||||
children: childParams,
|
||||
});
|
||||
}
|
||||
this._commandService.executeCommand(UngroupSheetDrawingCommand.id, unGroupParams);
|
||||
}));
|
||||
}
|
||||
|
||||
private _focusDrawingListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.focus$.subscribe((params) => {
|
||||
if (params == null || params.length === 0) {
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
this._sheetDrawingService.focusDrawing([]);
|
||||
} else {
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, true);
|
||||
this._sheetDrawingService.focusDrawing(params);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 { IContextService } from '@univerjs/core';
|
||||
import type { IShortcutItem } from '@univerjs/ui';
|
||||
import type { IMoveDrawingsCommandParams } from '../../commands/commands/move-drawings.command';
|
||||
import { Direction, FOCUSING_COMMON_DRAWINGS, FOCUSING_FX_BAR_EDITOR, FOCUSING_PANEL_EDITOR, FOCUSING_SHAPE_TEXT_EDITOR, FOCUSING_SHEET } from '@univerjs/core';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { DeleteDrawingsCommand } from '../../commands/commands/delete-drawings.command';
|
||||
import { MoveDrawingsCommand } from '../../commands/commands/move-drawings.command';
|
||||
|
||||
export function whenSheetDrawingFocused(contextService: IContextService): boolean {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_SHEET) &&
|
||||
!contextService.getContextValue(FOCUSING_FX_BAR_EDITOR) &&
|
||||
!contextService.getContextValue(FOCUSING_PANEL_EDITOR) &&
|
||||
contextService.getContextValue(FOCUSING_COMMON_DRAWINGS) &&
|
||||
!contextService.getContextValue(FOCUSING_SHAPE_TEXT_EDITOR)
|
||||
);
|
||||
}
|
||||
|
||||
export const MoveDrawingDownShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDrawingsCommand.id,
|
||||
description: 'sheets-drawing-ui.shortcut.drawing-move-down',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'sheets-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_DOWN,
|
||||
priority: 100,
|
||||
preconditions: whenSheetDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.DOWN,
|
||||
},
|
||||
};
|
||||
|
||||
export const MoveDrawingUpShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDrawingsCommand.id,
|
||||
description: 'sheets-drawing-ui.shortcut.drawing-move-up',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'sheets-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_UP,
|
||||
priority: 100,
|
||||
preconditions: whenSheetDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.UP,
|
||||
},
|
||||
};
|
||||
|
||||
export const MoveDrawingLeftShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDrawingsCommand.id,
|
||||
description: 'sheets-drawing-ui.shortcut.drawing-move-left',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'sheets-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_LEFT,
|
||||
priority: 100,
|
||||
preconditions: whenSheetDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.LEFT,
|
||||
},
|
||||
};
|
||||
|
||||
export const MoveDrawingRightShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDrawingsCommand.id,
|
||||
description: 'sheets-drawing-ui.shortcut.drawing-move-right',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'sheets-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_RIGHT,
|
||||
priority: 100,
|
||||
preconditions: whenSheetDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.RIGHT,
|
||||
},
|
||||
};
|
||||
export const DeleteDrawingsShortcutItem: IShortcutItem = {
|
||||
id: DeleteDrawingsCommand.id,
|
||||
description: 'sheets-drawing-ui.shortcut.drawing-delete',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'sheets-drawing-ui.shortcut.drawing-view',
|
||||
priority: 100,
|
||||
// when focusing on any other input tag do not trigger this shortcut
|
||||
preconditions: whenSheetDrawingFocused,
|
||||
binding: KeyCode.DELETE,
|
||||
mac: KeyCode.BACKSPACE,
|
||||
};
|
||||
108
packages/sheets-drawing-ui/src/controllers/ui.controller.ts
Normal file
108
packages/sheets-drawing-ui/src/controllers/ui.controller.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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 { Disposable, DrawingTypeEnum, ICommandService, Inject } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IMenuManagerService, IShortcutService, ISidebarService } from '@univerjs/ui';
|
||||
import { DeleteDrawingsCommand } from '../commands/commands/delete-drawings.command';
|
||||
import { FlipSheetDrawingCommand } from '../commands/commands/flip-drawings.command';
|
||||
import { GroupSheetDrawingCommand } from '../commands/commands/group-sheet-drawing.command';
|
||||
import { InsertCellImageCommand, InsertFloatImageCommand } from '../commands/commands/insert-image.command';
|
||||
import { MoveDrawingsCommand } from '../commands/commands/move-drawings.command';
|
||||
import { SaveCellImagesCommand } from '../commands/commands/save-cell-images.command';
|
||||
import { UngroupSheetDrawingCommand } from '../commands/commands/ungroup-sheet-drawing.command';
|
||||
import { EditSheetDrawingOperation } from '../commands/operations/edit-sheet-drawing.operation';
|
||||
import { SidebarSheetDrawingOperation } from '../commands/operations/open-drawing-panel.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import { COMPONENT_SHEET_DRAWING_PANEL } from '../views/sheet-image-panel/component-name';
|
||||
import {
|
||||
DeleteDrawingsShortcutItem,
|
||||
MoveDrawingDownShortcutItem,
|
||||
MoveDrawingLeftShortcutItem,
|
||||
MoveDrawingRightShortcutItem,
|
||||
MoveDrawingUpShortcutItem,
|
||||
} from './shortcuts/drawing.shortcut';
|
||||
|
||||
export class SheetDrawingUIController extends Disposable {
|
||||
constructor(
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(ISidebarService) private readonly _sidebarService: ISidebarService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _initCustomComponents(): void {
|
||||
}
|
||||
|
||||
private _initMenus(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
InsertFloatImageCommand,
|
||||
InsertCellImageCommand,
|
||||
SidebarSheetDrawingOperation,
|
||||
EditSheetDrawingOperation,
|
||||
GroupSheetDrawingCommand,
|
||||
UngroupSheetDrawingCommand,
|
||||
MoveDrawingsCommand,
|
||||
DeleteDrawingsCommand,
|
||||
SaveCellImagesCommand,
|
||||
FlipSheetDrawingCommand,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
private _initShortcuts(): void {
|
||||
[
|
||||
// sheet drawing shortcuts
|
||||
MoveDrawingDownShortcutItem,
|
||||
MoveDrawingUpShortcutItem,
|
||||
MoveDrawingLeftShortcutItem,
|
||||
MoveDrawingRightShortcutItem,
|
||||
DeleteDrawingsShortcutItem,
|
||||
].forEach((item) => {
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut(item));
|
||||
});
|
||||
}
|
||||
|
||||
private _initImagePanel(): void {
|
||||
this.disposeWithMe(this._drawingManagerService.focus$.subscribe((drawings) => {
|
||||
if (!this._sidebarService.visible || this._sidebarService.options.id !== COMPONENT_SHEET_DRAWING_PANEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawings.length === 1 && drawings[0].drawingType === DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sidebarService.close(COMPONENT_SHEET_DRAWING_PANEL);
|
||||
}));
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this._initCommands();
|
||||
this._initCustomComponents();
|
||||
this._initMenus();
|
||||
this._initShortcuts();
|
||||
this._initImagePanel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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 { LifecycleService, LifecycleStages } from '@univerjs/core';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { SheetCanvasFloatDomManagerService } from '../../../services/canvas-float-dom-manager.service';
|
||||
import { registerSheetsDrawingFloatingHostCapability, SHEETS_DRAWING_FLOATING_HOST_DEPENDENCIES, touchSheetsDrawingFloatingHostCapabilityWhenReady } from '../register-sheets-drawing-floating-host';
|
||||
|
||||
const registerDependencies = vi.hoisted(() => vi.fn());
|
||||
const touchDependencies = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@univerjs/core', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@univerjs/core')>();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
registerDependencies,
|
||||
touchDependencies,
|
||||
};
|
||||
});
|
||||
|
||||
describe('registerSheetsDrawingFloatingHostCapability', () => {
|
||||
beforeEach(() => {
|
||||
registerDependencies.mockClear();
|
||||
touchDependencies.mockClear();
|
||||
});
|
||||
|
||||
it('registers the minimal sheet float DOM host dependency', () => {
|
||||
const injector = {
|
||||
has: () => false,
|
||||
};
|
||||
|
||||
registerSheetsDrawingFloatingHostCapability(injector as never);
|
||||
|
||||
expect(SHEETS_DRAWING_FLOATING_HOST_DEPENDENCIES).toEqual([
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
]);
|
||||
expect(registerDependencies).toHaveBeenCalledWith(injector, [
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
]);
|
||||
expect(touchDependencies).toHaveBeenCalledWith(injector, [
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not register the floating host dependency twice', () => {
|
||||
const injector = {
|
||||
has: (token: unknown) => token === SheetCanvasFloatDomManagerService,
|
||||
};
|
||||
|
||||
registerSheetsDrawingFloatingHostCapability(injector as never);
|
||||
|
||||
expect(registerDependencies).not.toHaveBeenCalled();
|
||||
expect(touchDependencies).toHaveBeenCalledWith(injector, [
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
]);
|
||||
});
|
||||
|
||||
it('defers touching the floating host until lifecycle ready', async () => {
|
||||
let resolveReady: () => void = () => {};
|
||||
const onStage = vi.fn(() => new Promise<void>((resolve) => {
|
||||
resolveReady = resolve;
|
||||
}));
|
||||
const lifecycleService = {
|
||||
stage: LifecycleStages.Starting,
|
||||
onStage,
|
||||
};
|
||||
const injector = {
|
||||
has: (token: unknown) => token === LifecycleService,
|
||||
get: (token: unknown) => {
|
||||
if (token === LifecycleService) {
|
||||
return lifecycleService;
|
||||
}
|
||||
throw new Error('Unexpected dependency');
|
||||
},
|
||||
};
|
||||
|
||||
registerSheetsDrawingFloatingHostCapability(injector as never);
|
||||
|
||||
expect(onStage).toHaveBeenCalledWith(LifecycleStages.Ready);
|
||||
expect(touchDependencies).not.toHaveBeenCalled();
|
||||
|
||||
resolveReady();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(touchDependencies).toHaveBeenCalledWith(injector, [
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows the full sheets drawing UI plugin to touch an already registered floating host', async () => {
|
||||
let resolveReady: () => void = () => {};
|
||||
const onStage = vi.fn(() => new Promise<void>((resolve) => {
|
||||
resolveReady = resolve;
|
||||
}));
|
||||
const lifecycleService = {
|
||||
stage: LifecycleStages.Starting,
|
||||
onStage,
|
||||
};
|
||||
const injector = {
|
||||
has: (token: unknown) => token === LifecycleService || token === SheetCanvasFloatDomManagerService,
|
||||
get: (token: unknown) => {
|
||||
if (token === LifecycleService) {
|
||||
return lifecycleService;
|
||||
}
|
||||
throw new Error('Unexpected dependency');
|
||||
},
|
||||
};
|
||||
|
||||
touchSheetsDrawingFloatingHostCapabilityWhenReady(injector as never);
|
||||
|
||||
expect(registerDependencies).not.toHaveBeenCalled();
|
||||
expect(onStage).toHaveBeenCalledWith(LifecycleStages.Ready);
|
||||
expect(touchDependencies).not.toHaveBeenCalled();
|
||||
|
||||
resolveReady();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(touchDependencies).toHaveBeenCalledWith(injector, [
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
]);
|
||||
});
|
||||
});
|
||||
21
packages/sheets-drawing-ui/src/embed/floating-host/index.ts
Normal file
21
packages/sheets-drawing-ui/src/embed/floating-host/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
registerSheetsDrawingFloatingHostCapability,
|
||||
SHEETS_DRAWING_FLOATING_HOST_DEPENDENCIES,
|
||||
touchSheetsDrawingFloatingHostCapabilityWhenReady,
|
||||
} from './register-sheets-drawing-floating-host';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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, Injector } from '@univerjs/core';
|
||||
import { LifecycleService, LifecycleStages, registerDependencies, touchDependencies } from '@univerjs/core';
|
||||
import { SheetCanvasFloatDomManagerService } from '../../services/canvas-float-dom-manager.service';
|
||||
|
||||
export const SHEETS_DRAWING_FLOATING_HOST_DEPENDENCIES: Dependency[] = [
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
];
|
||||
|
||||
export function registerSheetsDrawingFloatingHostCapability(injector: Injector): void {
|
||||
if (!injector.has(SheetCanvasFloatDomManagerService)) {
|
||||
registerDependencies(injector, SHEETS_DRAWING_FLOATING_HOST_DEPENDENCIES);
|
||||
}
|
||||
touchSheetsDrawingFloatingHostCapabilityWhenReady(injector);
|
||||
}
|
||||
|
||||
function touchSheetsDrawingFloatingHostCapability(injector: Injector): void {
|
||||
touchDependencies(injector, [[SheetCanvasFloatDomManagerService]]);
|
||||
}
|
||||
|
||||
export function touchSheetsDrawingFloatingHostCapabilityWhenReady(injector: Injector): void {
|
||||
if (!injector.has(LifecycleService)) {
|
||||
touchSheetsDrawingFloatingHostCapability(injector);
|
||||
return;
|
||||
}
|
||||
|
||||
const lifecycleService = injector.get(LifecycleService);
|
||||
if (lifecycleService.stage >= LifecycleStages.Ready) {
|
||||
touchSheetsDrawingFloatingHostCapability(injector);
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycleService.onStage(LifecycleStages.Ready).then(() => {
|
||||
touchSheetsDrawingFloatingHostCapability(injector);
|
||||
});
|
||||
}
|
||||
17
packages/sheets-drawing-ui/src/embed/index.ts
Normal file
17
packages/sheets-drawing-ui/src/embed/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 * from './floating-host';
|
||||
@@ -0,0 +1,827 @@
|
||||
/**
|
||||
* 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, ICellData, IDrawingParam, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import type { IRender, Rect, Scene } from '@univerjs/engine-render';
|
||||
import type { ISheetFloatDom, ISheetImage } from '@univerjs/sheets-drawing';
|
||||
import {
|
||||
Disposable,
|
||||
DrawingTypeEnum,
|
||||
EventSubject,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
ImageSourceType,
|
||||
Inject,
|
||||
Injector,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
IURLImageService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Tools,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { IDrawingManagerService, UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, RenderManagerService, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import {
|
||||
InsertSheetDrawingCommand,
|
||||
ISheetDrawingService,
|
||||
RemoveSheetDrawingCommand,
|
||||
SetSheetDrawingCommand,
|
||||
UniverSheetsDrawingPlugin,
|
||||
} from '@univerjs/sheets-drawing';
|
||||
import {
|
||||
BatchSaveImagesService,
|
||||
IBatchSaveImagesService,
|
||||
SheetCanvasFloatDomManagerService,
|
||||
} from '@univerjs/sheets-drawing-ui';
|
||||
import { ISheetSelectionRenderService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
|
||||
import sheetsEnUS from '@univerjs/sheets/locale/en-US';
|
||||
import { CanvasFloatDomService, ComponentManager } from '@univerjs/ui';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import '@univerjs/sheets/facade';
|
||||
import '@univerjs/sheets-drawing-ui/facade';
|
||||
|
||||
const TEST_WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function createDrawingUITestBed(dependencies: Dependency[] = []) {
|
||||
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);
|
||||
|
||||
const baseDependencies: Dependency[] = [
|
||||
[ComponentManager],
|
||||
[CanvasFloatDomService],
|
||||
[IBatchSaveImagesService, { useClass: BatchSaveImagesService }],
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
...dependencies,
|
||||
];
|
||||
if (!hasDependency(dependencies, IRenderManagerService)) {
|
||||
baseDependencies.unshift([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
}
|
||||
baseDependencies.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(TEST_WORKBOOK_DATA)
|
||||
);
|
||||
|
||||
get(IUniverInstanceService).focusUnit(workbook.getUnitId());
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
const localeService = get(LocaleService);
|
||||
localeService.load({ [LocaleType.EN_US]: sheetsEnUS });
|
||||
localeService.setLocale(LocaleType.EN_US);
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
workbook,
|
||||
};
|
||||
}
|
||||
|
||||
function hasDependency(dependencies: Dependency[], token: unknown): boolean {
|
||||
for (const dependency of dependencies) {
|
||||
if (Array.isArray(dependency) && dependency[0] === token) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
class TestRenderManagerService extends Disposable implements Partial<IRenderManagerService> {
|
||||
private _render: IRender | null = null;
|
||||
readonly created$ = new Subject<IRender>();
|
||||
readonly disposed$ = new Subject<string>();
|
||||
|
||||
configure(render: IRender): void {
|
||||
this._render = render;
|
||||
}
|
||||
|
||||
getRenderUnitById(): IRender | null {
|
||||
return this._render;
|
||||
}
|
||||
|
||||
has(): boolean {
|
||||
return this._render != null;
|
||||
}
|
||||
|
||||
getRenderAll(): Map<string, IRender> {
|
||||
const renderMap = new Map<string, IRender>();
|
||||
if (this._render) {
|
||||
renderMap.set(this._render.unitId, this._render);
|
||||
}
|
||||
return renderMap;
|
||||
}
|
||||
}
|
||||
|
||||
class TestSheetSkeletonManager {
|
||||
readonly currentSkeleton$: BehaviorSubject<{ sheetId: string } | null>;
|
||||
|
||||
constructor(private readonly _skeleton: SpreadsheetSkeleton) {
|
||||
this.currentSkeleton$ = new BehaviorSubject<{ sheetId: string } | null>({ sheetId: 'sheet1' });
|
||||
}
|
||||
|
||||
getSkeletonParam() {
|
||||
return {
|
||||
sheetId: 'sheet1',
|
||||
skeleton: this._skeleton,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestSheetSelectionRenderService {
|
||||
getCellWithCoordByOffset(x: number, y: number) {
|
||||
const column = Math.max(0, Math.floor(x / 72));
|
||||
const row = Math.max(0, Math.floor(y / 24));
|
||||
return {
|
||||
actualColumn: column,
|
||||
actualRow: row,
|
||||
startColumn: column,
|
||||
endColumn: column,
|
||||
startRow: row,
|
||||
endRow: row,
|
||||
startX: column * 72,
|
||||
endX: (column + 1) * 72,
|
||||
startY: row * 24,
|
||||
endY: (row + 1) * 24,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderScene {
|
||||
private readonly _objects = new Map<string, Rect>();
|
||||
readonly onScrollAfter$ = new EventSubject();
|
||||
private readonly _transformer = {
|
||||
clearControlByIds: () => undefined,
|
||||
clearSelectedObjects: () => undefined,
|
||||
debounceRefreshControls: () => undefined,
|
||||
};
|
||||
|
||||
getAncestorScale() {
|
||||
return { scaleX: 1, scaleY: 1 };
|
||||
}
|
||||
|
||||
getViewport() {
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 360,
|
||||
bottom: 220,
|
||||
viewportScrollX: 0,
|
||||
viewportScrollY: 0,
|
||||
onScrollAfter$: this.onScrollAfter$,
|
||||
};
|
||||
}
|
||||
|
||||
getMainViewport() {
|
||||
return this.getViewport();
|
||||
}
|
||||
|
||||
getTransformerByCreate() {
|
||||
return this._transformer;
|
||||
}
|
||||
|
||||
getTransformer() {
|
||||
return this._transformer;
|
||||
}
|
||||
|
||||
addObject(object: Rect) {
|
||||
object.parent = this as never;
|
||||
this._objects.set(object.oKey, object);
|
||||
}
|
||||
|
||||
removeObject(object: Rect) {
|
||||
this._objects.delete(object.oKey);
|
||||
}
|
||||
|
||||
getObject(key: string) {
|
||||
return this._objects.get(key);
|
||||
}
|
||||
|
||||
getObjectIncludeInGroup(key: string) {
|
||||
return this.getObject(key);
|
||||
}
|
||||
|
||||
attachTransformerTo() { }
|
||||
|
||||
detachTransformerFrom() { }
|
||||
}
|
||||
|
||||
function createRender(skeleton: SpreadsheetSkeleton, scene: Scene): IRender {
|
||||
const sheetSkeletonManager = new TestSheetSkeletonManager(skeleton);
|
||||
const sheetSelectionRenderService = new TestSheetSelectionRenderService();
|
||||
const canvasElement = document.createElement('div');
|
||||
const activated$ = new BehaviorSubject(true);
|
||||
|
||||
return {
|
||||
unitId: 'test',
|
||||
type: UniverInstanceType.UNIVER_SHEET,
|
||||
engine: {
|
||||
clientRect$: new BehaviorSubject({ width: 360, height: 220 }),
|
||||
getCanvasElement: () => canvasElement,
|
||||
} as unknown as IRender['engine'],
|
||||
scene,
|
||||
mainComponent: null,
|
||||
components: new Map(),
|
||||
isMainScene: true,
|
||||
render: { scene },
|
||||
activated$,
|
||||
with: (dependency: unknown) => {
|
||||
if (dependency === SheetSkeletonManagerService) {
|
||||
return sheetSkeletonManager;
|
||||
}
|
||||
if (dependency === ISheetSelectionRenderService) {
|
||||
return sheetSelectionRenderService;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
deactivate: () => activated$.next(false),
|
||||
activate: () => activated$.next(true),
|
||||
isDisposed: () => false,
|
||||
} as unknown as IRender;
|
||||
}
|
||||
|
||||
function createDrawingUITestBedWithRender() {
|
||||
const testBed = createDrawingUITestBed([
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService }],
|
||||
]);
|
||||
const modelWorksheet = testBed.workbook.getActiveSheet();
|
||||
const skeleton = testBed.injector.createInstance(
|
||||
SpreadsheetSkeleton,
|
||||
modelWorksheet,
|
||||
testBed.workbook.getStyles()
|
||||
).calculate() as SpreadsheetSkeleton;
|
||||
const renderManager = testBed.injector.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
renderManager.configure(createRender(skeleton, new TestRenderScene() as unknown as Scene));
|
||||
|
||||
return testBed;
|
||||
}
|
||||
|
||||
describe('sheets-drawing-ui facade', () => {
|
||||
let univer: Univer;
|
||||
let injector: Injector;
|
||||
let univerAPI: FUniver;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createDrawingUITestBed();
|
||||
univer = testBed.univer;
|
||||
injector = testBed.injector;
|
||||
univerAPI = FUniver.newAPI(injector);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer?.dispose();
|
||||
});
|
||||
|
||||
it('uses real facade instances for empty float dom and cell image flows', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const range = worksheet.getRange('A1:B2');
|
||||
|
||||
expect(worksheet.getFloatDomById('missing')).toBeNull();
|
||||
expect(worksheet.getAllFloatDoms()).toEqual([]);
|
||||
expect(worksheet.removeFloatDom('missing')).toBe(worksheet);
|
||||
await expect(range.insertCellImageAsync('https://univer.ai/image.png')).resolves.toBe(false);
|
||||
await expect(range.saveCellImagesAsync()).resolves.toBe(false);
|
||||
await expect(worksheet.saveCellImagesAsync()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('registers a URL image downloader through the facade', async () => {
|
||||
const downloader = async (url: string) => `downloaded:${url}`;
|
||||
const disposable = univerAPI.registerURLImageDownloader(downloader);
|
||||
const urlImageService = injector.get(IURLImageService);
|
||||
|
||||
await expect(urlImageService.getImage('https://univer.ai/image.png')).resolves.toBe('downloaded:https://univer.ai/image.png');
|
||||
disposable.dispose();
|
||||
await expect(urlImageService.getImage('https://univer.ai/image.png')).resolves.toBe('https://univer.ai/image.png');
|
||||
});
|
||||
|
||||
it('adds, updates and removes float doms through worksheet facade state', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
|
||||
const added = worksheet.addFloatDomToPosition({
|
||||
componentKey: 'OrderCard',
|
||||
initPosition: {
|
||||
startX: 20,
|
||||
startY: 30,
|
||||
endX: 140,
|
||||
endY: 90,
|
||||
},
|
||||
data: { label: 'Draft order' },
|
||||
allowTransform: true,
|
||||
}, 'order-card');
|
||||
await Promise.resolve();
|
||||
|
||||
expect(added).toBeNull();
|
||||
expect(injector.get(IDrawingManagerService).getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'order-card',
|
||||
})).toBeUndefined();
|
||||
|
||||
expect(worksheet.addFloatDomToRange(worksheet.getRange('A1:B2'), {
|
||||
componentKey: 'RangeCard',
|
||||
data: { label: 'Range order' },
|
||||
}, {}, 'range-card')).toBeNull();
|
||||
expect(worksheet.addFloatDomToColumnHeader(1, {
|
||||
componentKey: 'HeaderCard',
|
||||
data: { label: 'Header order' },
|
||||
}, { width: 80, height: 24, marginX: 0, marginY: 0 }, 'header-card')).toBeNull();
|
||||
|
||||
expect(worksheet.updateFloatDom('missing-dom', { data: { label: 'Ignored' } })).toBe(worksheet);
|
||||
expect(worksheet.batchUpdateFloatDoms([{ id: 'missing-dom', config: { data: { label: 'Ignored' } } }])).toBe(worksheet);
|
||||
|
||||
worksheet.removeFloatDom('order-card');
|
||||
});
|
||||
|
||||
it('adds, reads, updates, batch updates and disposes float doms through the worksheet facade', async () => {
|
||||
univer.dispose();
|
||||
const testBed = createDrawingUITestBedWithRender();
|
||||
univer = testBed.univer;
|
||||
injector = testBed.injector;
|
||||
univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
|
||||
const primary = worksheet.addFloatDomToPosition({
|
||||
componentKey: 'OrderCard',
|
||||
initPosition: {
|
||||
startX: 72,
|
||||
startY: 24,
|
||||
endX: 172,
|
||||
endY: 64,
|
||||
},
|
||||
data: { label: 'Draft order' },
|
||||
allowTransform: true,
|
||||
}, 'order-card')!;
|
||||
const secondary = worksheet.addFloatDomToPosition({
|
||||
componentKey: 'StatusCard',
|
||||
initPosition: {
|
||||
startX: 144,
|
||||
startY: 48,
|
||||
endX: 244,
|
||||
endY: 88,
|
||||
},
|
||||
data: { label: 'Pending' },
|
||||
allowTransform: true,
|
||||
}, 'status-card')!;
|
||||
await Promise.resolve();
|
||||
|
||||
expect(primary.id).toBe('order-card');
|
||||
expect(secondary.id).toBe('status-card');
|
||||
expect(worksheet.getFloatDomById('order-card')).toEqual(expect.objectContaining({
|
||||
id: 'order-card',
|
||||
componentKey: 'OrderCard',
|
||||
allowTransform: true,
|
||||
data: { label: 'Draft order' },
|
||||
position: expect.objectContaining({
|
||||
left: 72,
|
||||
top: 24,
|
||||
width: 100,
|
||||
height: 40,
|
||||
}),
|
||||
}));
|
||||
|
||||
const allDoms = worksheet.getAllFloatDoms();
|
||||
const allDomIds: string[] = [];
|
||||
for (const dom of allDoms) {
|
||||
allDomIds.push(dom.id);
|
||||
}
|
||||
expect(allDomIds).toEqual(['order-card', 'status-card']);
|
||||
|
||||
expect(worksheet.updateFloatDom('order-card', {
|
||||
componentKey: 'OrderCardUpdated',
|
||||
allowTransform: false,
|
||||
data: { label: 'Approved' },
|
||||
position: {
|
||||
left: 90,
|
||||
top: 36,
|
||||
width: 120,
|
||||
height: 50,
|
||||
},
|
||||
})).toBe(worksheet);
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'order-card',
|
||||
})).toEqual(expect.objectContaining({
|
||||
componentKey: 'OrderCardUpdated',
|
||||
allowTransform: false,
|
||||
data: { label: 'Approved' },
|
||||
transform: expect.objectContaining({
|
||||
left: 90,
|
||||
top: 36,
|
||||
width: 120,
|
||||
height: 50,
|
||||
}),
|
||||
}));
|
||||
|
||||
expect(worksheet.batchUpdateFloatDoms([
|
||||
{
|
||||
id: 'order-card',
|
||||
config: {
|
||||
data: { label: 'Shipped' },
|
||||
position: {
|
||||
left: 100,
|
||||
top: 40,
|
||||
width: 130,
|
||||
height: 54,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'status-card',
|
||||
config: {
|
||||
componentKey: 'StatusCardUpdated',
|
||||
data: { label: 'Done' },
|
||||
allowTransform: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'missing-card',
|
||||
config: {
|
||||
data: { label: 'Ignored' },
|
||||
},
|
||||
},
|
||||
])).toBe(worksheet);
|
||||
|
||||
expect(worksheet.getFloatDomById('order-card')).toEqual(expect.objectContaining({
|
||||
componentKey: 'OrderCardUpdated',
|
||||
data: { label: 'Shipped' },
|
||||
}));
|
||||
expect(sheetDrawingService.getDrawingByParam({
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId: 'order-card',
|
||||
})).toEqual(expect.objectContaining({
|
||||
transform: expect.objectContaining({
|
||||
left: 100,
|
||||
top: 40,
|
||||
width: 130,
|
||||
height: 54,
|
||||
}),
|
||||
}));
|
||||
expect(worksheet.getFloatDomById('status-card')).toEqual(expect.objectContaining({
|
||||
componentKey: 'StatusCardUpdated',
|
||||
allowTransform: false,
|
||||
data: { label: 'Done' },
|
||||
}));
|
||||
|
||||
primary.dispose();
|
||||
expect(worksheet.getFloatDomById('order-card')).toBeNull();
|
||||
worksheet.removeFloatDom('status-card');
|
||||
expect(worksheet.getAllFloatDoms()).toEqual([]);
|
||||
});
|
||||
|
||||
it('omits float doms without backing drawing params', async () => {
|
||||
univer.dispose();
|
||||
const testBed = createDrawingUITestBedWithRender();
|
||||
univer = testBed.univer;
|
||||
injector = testBed.injector;
|
||||
univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
worksheet.addFloatDomToPosition({
|
||||
componentKey: 'OrderCard',
|
||||
initPosition: { startX: 20, startY: 30, endX: 140, endY: 90 },
|
||||
}, 'order-card');
|
||||
worksheet.addFloatDomToPosition({
|
||||
componentKey: 'MissingCard',
|
||||
initPosition: { startX: 40, startY: 50, endX: 160, endY: 110 },
|
||||
}, 'missing-card');
|
||||
await Promise.resolve();
|
||||
|
||||
const drawingData = sheetDrawingService.getDrawingData('test', 'sheet1');
|
||||
delete drawingData['missing-card'];
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'missing-card' })).toBeUndefined();
|
||||
expect(worksheet.getAllFloatDoms().map((dom) => dom.id)).toEqual(['order-card']);
|
||||
});
|
||||
|
||||
it('saves cell images from range and worksheet selections', async () => {
|
||||
const worksheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const savedFiles: string[] = [];
|
||||
const clickedDownloads: string[] = [];
|
||||
const originalShowDirectoryPicker = window.showDirectoryPicker;
|
||||
const originalCreateObjectURL = URL.createObjectURL;
|
||||
const originalRevokeObjectURL = URL.revokeObjectURL;
|
||||
const originalClick = HTMLAnchorElement.prototype.click;
|
||||
|
||||
window.showDirectoryPicker = async () => ({
|
||||
getFileHandle: async (name: string) => {
|
||||
savedFiles.push(name);
|
||||
return {
|
||||
createWritable: async () => ({
|
||||
write: async () => {},
|
||||
close: async () => {},
|
||||
}),
|
||||
};
|
||||
},
|
||||
}) as unknown as FileSystemDirectoryHandle;
|
||||
URL.createObjectURL = () => 'blob:cell-image';
|
||||
URL.revokeObjectURL = () => {};
|
||||
HTMLAnchorElement.prototype.click = function click() {
|
||||
clickedDownloads.push(this.download);
|
||||
};
|
||||
|
||||
const model = worksheet.getWorkbook().getSheetBySheetId(worksheet.getSheetId())!;
|
||||
model.getCellMatrix().setValue(0, 0, createCellImage('image-a', 'data:image/png;base64,aGVsbG8='));
|
||||
model.getCellMatrix().setValue(1, 1, createCellImage('image-b', 'data:image/jpeg;base64,aGVsbG8='));
|
||||
model.getCellMatrix().setValue(0, 2, { v: 'Customer/West' });
|
||||
|
||||
await expect(worksheet.getRange('A1').saveCellImagesAsync()).resolves.toBe(true);
|
||||
expect(clickedDownloads).toEqual(['A1.png']);
|
||||
|
||||
await expect(worksheet.saveCellImagesAsync(undefined, [worksheet.getRange('A1')])).resolves.toBe(true);
|
||||
expect(clickedDownloads).toEqual(['A1.png', 'A1.png']);
|
||||
|
||||
await expect(worksheet.getRange('A1:B2').saveCellImagesAsync({ useCellAddress: false, useColumnIndex: 2 })).resolves.toBe(true);
|
||||
expect(savedFiles).toEqual(['Customer_West.png', 'B2.jpg']);
|
||||
|
||||
savedFiles.length = 0;
|
||||
await expect(worksheet.saveCellImagesAsync({ useCellAddress: true, useColumnIndex: 2 }, [worksheet.getRange('A1:B2')])).resolves.toBe(true);
|
||||
expect(savedFiles).toEqual(['A1_Customer_West.png', 'B2.jpg']);
|
||||
|
||||
URL.createObjectURL = () => {
|
||||
throw new Error('download denied');
|
||||
};
|
||||
await expect(worksheet.getRange('A1').saveCellImagesAsync()).resolves.toBe(false);
|
||||
await expect(worksheet.saveCellImagesAsync(undefined, [worksheet.getRange('A1')])).resolves.toBe(false);
|
||||
|
||||
URL.createObjectURL = () => 'blob:cell-image';
|
||||
window.showDirectoryPicker = async () => {
|
||||
throw new Error('directory denied');
|
||||
};
|
||||
await expect(worksheet.getRange('A1:B2').saveCellImagesAsync()).resolves.toBe(false);
|
||||
await expect(worksheet.saveCellImagesAsync(undefined, [worksheet.getRange('A1:B2')])).resolves.toBe(false);
|
||||
|
||||
window.showDirectoryPicker = originalShowDirectoryPicker;
|
||||
URL.createObjectURL = originalCreateObjectURL;
|
||||
URL.revokeObjectURL = originalRevokeObjectURL;
|
||||
HTMLAnchorElement.prototype.click = originalClick;
|
||||
});
|
||||
|
||||
it('emits float dom add, update and delete events from drawing commands', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook()!;
|
||||
const commandService = injector.get(ICommandService);
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const floatDom = createFloatDom('float-dom-1');
|
||||
|
||||
const logs: string[] = [];
|
||||
const disposables = [
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomAdd, ({ drawings }) => logs.push(`before-add:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.FloatDomAdded, ({ drawings }) => logs.push(`add:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomUpdate, ({ drawings }) => logs.push(`before-update:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.FloatDomUpdated, ({ drawings }) => logs.push(`update:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomDelete, ({ drawings }) => logs.push(`before-delete:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.FloatDomDeleted, ({ drawings }) => logs.push(`delete:${drawings[0]}`)),
|
||||
];
|
||||
|
||||
commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: workbook.getId(),
|
||||
drawings: [floatDom],
|
||||
});
|
||||
drawingManagerService.registerDrawingData('test', sheetDrawingService.getDrawingDataForUnit('test') as Record<string, { data: Record<string, IDrawingParam>; order: string[] }>);
|
||||
drawingManagerService.initializeNotification('test');
|
||||
expect(commandService.syncExecuteCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: workbook.getId(),
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ ...floatDom, transform: { ...floatDom.transform, left: 24 }, data: { label: 'Updated order' } }],
|
||||
})).toBe(true);
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetDrawingCommand.id, {
|
||||
unitId: workbook.getId(),
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ unitId: workbook.getId(), subUnitId: 'sheet1', drawingId: floatDom.drawingId, drawingType: DrawingTypeEnum.DRAWING_DOM }],
|
||||
})).toBe(true);
|
||||
|
||||
expect(logs).toEqual([
|
||||
'before-add:float-dom-1',
|
||||
'add:float-dom-1',
|
||||
'before-update:float-dom-1',
|
||||
'update:float-dom-1',
|
||||
'before-delete:float-dom-1',
|
||||
'delete:float-dom-1',
|
||||
]);
|
||||
|
||||
disposables.forEach((disposable) => disposable.dispose());
|
||||
});
|
||||
|
||||
it('cancels float dom commands before they mutate drawing state', () => {
|
||||
const commandService = injector.get(ICommandService);
|
||||
const drawingManagerService = injector.get(IDrawingManagerService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const floatDom = createFloatDom('guarded-dom');
|
||||
const disposables = [
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomAdd, (event) => {
|
||||
if (event.drawings[0].drawingId === 'guarded-dom') event.cancel = true;
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomUpdate, (event) => {
|
||||
if (event.drawings[0].drawingId === 'existing-dom') event.cancel = true;
|
||||
}),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomDelete, (event) => {
|
||||
if (event.drawings[0].drawingId === 'existing-dom') event.cancel = true;
|
||||
}),
|
||||
];
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [floatDom],
|
||||
})).toBe(false);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'guarded-dom' })).toBeUndefined();
|
||||
|
||||
const existing = createFloatDom('existing-dom');
|
||||
sheetDrawingService.registerDrawingData('test', {
|
||||
sheet1: { data: { [existing.drawingId]: existing }, order: [existing.drawingId] },
|
||||
});
|
||||
drawingManagerService.registerDrawingData('test', {
|
||||
sheet1: { data: { [existing.drawingId]: existing as unknown as IDrawingParam }, order: [existing.drawingId] },
|
||||
});
|
||||
drawingManagerService.initializeNotification('test');
|
||||
|
||||
expect(commandService.syncExecuteCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ ...existing, data: { label: 'Blocked update' } }],
|
||||
})).toBe(false);
|
||||
expect((sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'existing-dom' }) as ISheetFloatDom | undefined)?.data)
|
||||
.toEqual({ label: 'Order card' });
|
||||
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ unitId: 'test', subUnitId: 'sheet1', drawingId: 'existing-dom', drawingType: DrawingTypeEnum.DRAWING_DOM }],
|
||||
})).toBe(false);
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: 'existing-dom' })).toBeDefined();
|
||||
|
||||
disposables.forEach((disposable) => disposable.dispose());
|
||||
});
|
||||
|
||||
it('does not emit float dom events for over-grid image drawing commands', () => {
|
||||
const commandService = injector.get(ICommandService);
|
||||
const sheetDrawingService = injector.get(ISheetDrawingService);
|
||||
const image = createOverGridImage('image-drawing');
|
||||
const logs: string[] = [];
|
||||
const disposables = [
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomAdd, ({ drawings }) => logs.push(`before-add:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.FloatDomAdded, ({ drawings }) => logs.push(`add:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomUpdate, ({ drawings }) => logs.push(`before-update:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.FloatDomUpdated, ({ drawings }) => logs.push(`update:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.BeforeFloatDomDelete, ({ drawings }) => logs.push(`before-delete:${drawings[0].drawingId}`)),
|
||||
univerAPI.addEvent(univerAPI.Event.FloatDomDeleted, ({ drawings }) => logs.push(`delete:${drawings[0]}`)),
|
||||
];
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id)).toBe(false);
|
||||
expect(commandService.syncExecuteCommand(SetSheetDrawingCommand.id)).toBe(false);
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetDrawingCommand.id)).toBe(false);
|
||||
|
||||
expect(commandService.syncExecuteCommand(InsertSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
drawings: [image],
|
||||
})).toBe(true);
|
||||
expect(commandService.syncExecuteCommand(SetSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ ...image, source: 'data:image/png;base64,Zm9v' }],
|
||||
})).toBe(true);
|
||||
expect(commandService.syncExecuteCommand(RemoveSheetDrawingCommand.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawings: [{ unitId: 'test', subUnitId: 'sheet1', drawingId: image.drawingId, drawingType: DrawingTypeEnum.DRAWING_IMAGE }],
|
||||
})).toBe(true);
|
||||
|
||||
expect(sheetDrawingService.getDrawingByParam({ unitId: 'test', subUnitId: 'sheet1', drawingId: image.drawingId })).toBeUndefined();
|
||||
expect(logs).toEqual([]);
|
||||
|
||||
disposables.forEach((disposable) => disposable.dispose());
|
||||
});
|
||||
});
|
||||
|
||||
function createFloatDom(drawingId: string): ISheetFloatDom {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
componentKey: 'FloatDom',
|
||||
allowTransform: true,
|
||||
data: { label: 'Order card' },
|
||||
transform: {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 120,
|
||||
height: 80,
|
||||
},
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createCellImage(drawingId: string, source: string): ICellData {
|
||||
return {
|
||||
p: {
|
||||
id: `${drawingId}-doc`,
|
||||
documentStyle: {},
|
||||
drawingsOrder: [drawingId],
|
||||
drawings: {
|
||||
[drawingId]: {
|
||||
drawingId,
|
||||
source,
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
},
|
||||
},
|
||||
} as unknown as ICellData['p'],
|
||||
};
|
||||
}
|
||||
|
||||
function createOverGridImage(drawingId: string): ISheetImage {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
source: 'data:image/png;base64,aGVsbG8=',
|
||||
transform: {
|
||||
left: 10,
|
||||
top: 10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
sheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 2, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 2, column: 2, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
246
packages/sheets-drawing-ui/src/facade/f-event.ts
Normal file
246
packages/sheets-drawing-ui/src/facade/f-event.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 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 { IEventBase } from '@univerjs/core/facade';
|
||||
import type { ISheetFloatDom } from '@univerjs/sheets-drawing';
|
||||
import type { FWorkbook } from '@univerjs/sheets/facade';
|
||||
import { FEventName } from '@univerjs/core/facade';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
interface IFSheetsDrawingUIEventNameMixin {
|
||||
/**
|
||||
* Triggered before float dom insertion.
|
||||
* @see {@link IBeforeFloatDomAddEventParams}
|
||||
* @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 IFloatDomAddedEventParams}
|
||||
* @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 IBeforeFloatDomUpdateEventParams}
|
||||
* @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 IFloatDomUpdatedEventParams}
|
||||
* @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 IBeforeFloatDomDeleteEventParams}
|
||||
* @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 IFloatDomDeletedEventParams}
|
||||
* @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';
|
||||
}
|
||||
|
||||
export class FSheetsDrawingUIEventNameMixin extends FEventName implements IFSheetsDrawingUIEventNameMixin {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IBeforeFloatDomAddEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of float dom drawings to be added.
|
||||
*/
|
||||
drawings: ISheetFloatDom[];
|
||||
}
|
||||
|
||||
export interface IFloatDomAddedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of float dom drawings that were added.
|
||||
*/
|
||||
drawings: ISheetFloatDom[];
|
||||
}
|
||||
|
||||
export interface IBeforeFloatDomUpdateEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of float dom drawings to be updated.
|
||||
*/
|
||||
drawings: ISheetFloatDom[];
|
||||
}
|
||||
|
||||
export interface IFloatDomUpdatedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of float dom drawings that were updated.
|
||||
*/
|
||||
drawings: ISheetFloatDom[];
|
||||
}
|
||||
|
||||
export interface IBeforeFloatDomDeleteEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of float dom drawings to be deleted.
|
||||
*/
|
||||
drawings: ISheetFloatDom[];
|
||||
}
|
||||
|
||||
export interface IFloatDomDeletedEventParams extends IEventBase {
|
||||
/**
|
||||
* The workbook instance currently being operated on. {@link FWorkbook}
|
||||
*/
|
||||
workbook: FWorkbook;
|
||||
|
||||
/**
|
||||
* Collection of float dom drawing ids that were deleted.
|
||||
*/
|
||||
drawings: string[];
|
||||
}
|
||||
|
||||
interface ISheetsDrawingUIEventParamConfig {
|
||||
BeforeFloatDomAdd: IBeforeFloatDomAddEventParams;
|
||||
FloatDomAdded: IFloatDomAddedEventParams;
|
||||
|
||||
BeforeFloatDomUpdate: IBeforeFloatDomUpdateEventParams;
|
||||
FloatDomUpdated: IFloatDomUpdatedEventParams;
|
||||
|
||||
BeforeFloatDomDelete: IBeforeFloatDomDeleteEventParams;
|
||||
FloatDomDeleted: IFloatDomDeletedEventParams;
|
||||
}
|
||||
|
||||
FEventName.extend(FSheetsDrawingUIEventNameMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FEventName extends IFSheetsDrawingUIEventNameMixin { }
|
||||
interface IEventParamConfig extends ISheetsDrawingUIEventParamConfig { }
|
||||
}
|
||||
165
packages/sheets-drawing-ui/src/facade/f-range.ts
Normal file
165
packages/sheets-drawing-ui/src/facade/f-range.ts
Normal file
@@ -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 { ISheetLocationBase } from '@univerjs/sheets';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { getCurrentTypeOfRenderer, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { FileNamePart, IBatchSaveImagesService, SheetDrawingUpdateController } from '@univerjs/sheets-drawing-ui';
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
|
||||
/**
|
||||
* Options for saving cell images
|
||||
*/
|
||||
export interface ISaveCellImagesOptions {
|
||||
/**
|
||||
* Whether to use cell address in file name (e.g., A1, B2)
|
||||
* @default true
|
||||
*/
|
||||
useCellAddress?: boolean;
|
||||
/**
|
||||
* Column index to use for file name (0-based). If specified, the value from this column will be used in file name.
|
||||
*/
|
||||
useColumnIndex?: number;
|
||||
}
|
||||
|
||||
export interface IFRangeSheetsDrawingUIMixin {
|
||||
/**
|
||||
* Inserts an image into the current cell.
|
||||
*
|
||||
* @param {string | File} file File or URL string
|
||||
* @returns {Promise<boolean>} True if the image is inserted successfully, otherwise false
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Insert an image into the cell A10
|
||||
* const fRange = fWorksheet.getRange('A10');
|
||||
* const result = await fRange.insertCellImageAsync('https://avatars.githubusercontent.com/u/61444807?s=48&v=4');
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
insertCellImageAsync(file: File | string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Save all cell images in this range to the file system.
|
||||
* This method will open a directory picker dialog and save all images to the selected directory.
|
||||
*
|
||||
* @param {ISaveCellImagesOptions} [options] Options for saving images
|
||||
* @returns {Promise<boolean>} True if images are saved successfully, otherwise false
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Save all cell images in range A1:D10
|
||||
* const fRange = fWorksheet.getRange('A1:D10');
|
||||
*
|
||||
* // Save with default options (using cell address as file name)
|
||||
* await fRange.saveCellImagesAsync();
|
||||
*
|
||||
* // Save with custom options
|
||||
* await fRange.saveCellImagesAsync({
|
||||
* useCellAddress: true,
|
||||
* useColumnIndex: 0, // Use values from column A for file names
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
saveCellImagesAsync(options?: ISaveCellImagesOptions): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class FRangeSheetsDrawingUIMixin extends FRange implements IFRangeSheetsDrawingUIMixin {
|
||||
override async insertCellImageAsync(file: File | string): Promise<boolean> {
|
||||
const renderManagerService = this._injector.get(IRenderManagerService);
|
||||
const controller = getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_SHEET, this._injector.get(IUniverInstanceService), renderManagerService)
|
||||
?.with(SheetDrawingUpdateController);
|
||||
if (!controller) {
|
||||
return false;
|
||||
}
|
||||
const location: ISheetLocationBase = {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
row: this.getRow(),
|
||||
col: this.getColumn(),
|
||||
};
|
||||
|
||||
if (typeof file === 'string') {
|
||||
return controller.insertCellImageByUrl(file, location);
|
||||
} else {
|
||||
return controller.insertCellImageByFile(file, location);
|
||||
}
|
||||
}
|
||||
|
||||
override async saveCellImagesAsync(options?: ISaveCellImagesOptions): Promise<boolean> {
|
||||
const batchSaveService = this._injector.get(IBatchSaveImagesService);
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
const range = this.getRange();
|
||||
|
||||
// Get images in the range
|
||||
const images = batchSaveService.getCellImagesFromRanges(unitId, subUnitId, [range]);
|
||||
|
||||
if (images.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If only one image, download directly
|
||||
if (images.length === 1) {
|
||||
try {
|
||||
await batchSaveService.downloadSingleImage(images[0]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to download image:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Build config from options
|
||||
const fileNameParts: FileNamePart[] = [];
|
||||
const useCellAddress = options?.useCellAddress ?? true;
|
||||
const useColumnIndex = options?.useColumnIndex;
|
||||
|
||||
if (useCellAddress) {
|
||||
fileNameParts.push(FileNamePart.CELL_ADDRESS);
|
||||
}
|
||||
if (useColumnIndex !== undefined) {
|
||||
fileNameParts.push(FileNamePart.COLUMN_VALUE);
|
||||
}
|
||||
|
||||
// Ensure at least one naming option
|
||||
if (fileNameParts.length === 0) {
|
||||
fileNameParts.push(FileNamePart.CELL_ADDRESS);
|
||||
}
|
||||
|
||||
try {
|
||||
await batchSaveService.saveImagesWithContext(images, {
|
||||
fileNameParts,
|
||||
columnIndex: useColumnIndex,
|
||||
}, unitId, subUnitId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save images:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FRange.extend(FRangeSheetsDrawingUIMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FRange extends IFRangeSheetsDrawingUIMixin { }
|
||||
}
|
||||
289
packages/sheets-drawing-ui/src/facade/f-univer.ts
Normal file
289
packages/sheets-drawing-ui/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 { IDisposable, IDrawingSearch, Injector } from '@univerjs/core';
|
||||
import type { IInsertSheetDrawingCommandParams, IRemoveSheetDrawingCommandParams, ISetDrawingCommandParams, ISheetFloatDom } from '@univerjs/sheets-drawing';
|
||||
import type { IBeforeFloatDomAddEventParams, IBeforeFloatDomDeleteEventParams, IBeforeFloatDomUpdateEventParams, IFloatDomAddedEventParams, IFloatDomDeletedEventParams, IFloatDomUpdatedEventParams } from './f-event';
|
||||
import { CanceledError, DrawingTypeEnum, ICommandService, IURLImageService } from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { InsertSheetDrawingCommand, RemoveSheetDrawingCommand, SetSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
|
||||
export interface IFUniverSheetsDrawingUIMixin {
|
||||
/**
|
||||
* Register a custom image downloader for URL images
|
||||
* @param downloader The downloader function that takes a URL and returns a base64 string
|
||||
* @returns A disposable object to unregister the downloader
|
||||
* @example
|
||||
* ```ts
|
||||
* const disposable = univerAPI.registerURLImageDownloader(async (url) => {
|
||||
* const response = await fetch(url);
|
||||
* const blob = await response.blob();
|
||||
* const base64 = await new Promise<string>((resolve) => {
|
||||
* const reader = new FileReader();
|
||||
* reader.onloadend = () => resolve(reader.result as string);
|
||||
* reader.readAsDataURL(blob);
|
||||
* });
|
||||
* return base64;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
registerURLImageDownloader(downloader: (url: string) => Promise<string>): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class FUniverSheetsDrawingUIMixin extends FUniver implements IFUniverSheetsDrawingUIMixin {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
override _initialize(injector: Injector): void {
|
||||
const commandService = injector.get(ICommandService);
|
||||
|
||||
// Float DOM Add Events
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeFloatDomAdd,
|
||||
() => 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 floatDomDrawings = drawings.filter(
|
||||
(drawing) => drawing.drawingType === DrawingTypeEnum.DRAWING_DOM
|
||||
) as ISheetFloatDom[];
|
||||
|
||||
if (floatDomDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventParams: IBeforeFloatDomAddEventParams = {
|
||||
workbook,
|
||||
drawings: floatDomDrawings,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeFloatDomAdd, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.FloatDomAdded,
|
||||
() => 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 floatDomDrawings = drawings.filter(
|
||||
(drawing) => drawing.drawingType === DrawingTypeEnum.DRAWING_DOM
|
||||
) as ISheetFloatDom[];
|
||||
|
||||
if (floatDomDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventParams: IFloatDomAddedEventParams = {
|
||||
workbook,
|
||||
drawings: floatDomDrawings,
|
||||
};
|
||||
this.fireEvent(this.Event.FloatDomAdded, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Float DOM Update Events
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeFloatDomUpdate,
|
||||
() => 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 floatDomDrawings: ISheetFloatDom[] = [];
|
||||
drawings.forEach((drawing) => {
|
||||
const dom = drawingManagerService.getDrawingByParam(drawing as IDrawingSearch) as ISheetFloatDom;
|
||||
if (dom?.drawingType === DrawingTypeEnum.DRAWING_DOM) {
|
||||
floatDomDrawings.push(dom);
|
||||
}
|
||||
});
|
||||
|
||||
if (floatDomDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventParams: IBeforeFloatDomUpdateEventParams = {
|
||||
workbook,
|
||||
drawings: floatDomDrawings,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeFloatDomUpdate, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
drawingManagerService.updateNotification(drawings as IDrawingSearch[]);
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Float DOM Update Events
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.FloatDomUpdated,
|
||||
() => 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 floatDomDrawings: ISheetFloatDom[] = [];
|
||||
drawings.forEach((drawing) => {
|
||||
const dom = drawingManagerService.getDrawingByParam(drawing as IDrawingSearch) as ISheetFloatDom;
|
||||
if (dom?.drawingType === DrawingTypeEnum.DRAWING_DOM) {
|
||||
floatDomDrawings.push(dom);
|
||||
}
|
||||
});
|
||||
|
||||
if (floatDomDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventParams: IFloatDomUpdatedEventParams = {
|
||||
workbook,
|
||||
drawings: floatDomDrawings,
|
||||
};
|
||||
this.fireEvent(this.Event.FloatDomUpdated, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Float DOM Delete Events
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.BeforeFloatDomDelete,
|
||||
() => 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 floatDomDrawings = drawings
|
||||
.map((drawing) => drawingManagerService.getDrawingByParam(drawing))
|
||||
.filter((drawing): drawing is ISheetFloatDom =>
|
||||
drawing?.drawingType === DrawingTypeEnum.DRAWING_DOM
|
||||
);
|
||||
|
||||
if (floatDomDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventParams: IBeforeFloatDomDeleteEventParams = {
|
||||
workbook,
|
||||
drawings: floatDomDrawings,
|
||||
};
|
||||
|
||||
this.fireEvent(this.Event.BeforeFloatDomDelete, eventParams);
|
||||
|
||||
if (eventParams.cancel) {
|
||||
throw new CanceledError();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this.registerEventHandler(
|
||||
this.Event.FloatDomDeleted,
|
||||
() => 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 floatDomDrawingIds: string[] = [];
|
||||
for (let i = 0; i < drawings.length; i++) {
|
||||
const drawing = drawings[i];
|
||||
if (drawing.drawingType === DrawingTypeEnum.DRAWING_DOM) {
|
||||
floatDomDrawingIds.push(drawing.drawingId);
|
||||
}
|
||||
}
|
||||
|
||||
if (floatDomDrawingIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventParams: IFloatDomDeletedEventParams = {
|
||||
workbook,
|
||||
drawings: floatDomDrawingIds,
|
||||
};
|
||||
this.fireEvent(this.Event.FloatDomDeleted, eventParams);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override registerURLImageDownloader(downloader: (url: string) => Promise<string>): IDisposable {
|
||||
const urlImageService = this._injector.get(IURLImageService);
|
||||
return urlImageService.registerURLImageDownloader(downloader);
|
||||
}
|
||||
}
|
||||
|
||||
FUniver.extend(FUniverSheetsDrawingUIMixin);
|
||||
declare module '@univerjs/core/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FUniver extends IFUniverSheetsDrawingUIMixin { }
|
||||
}
|
||||
733
packages/sheets-drawing-ui/src/facade/f-worksheet.ts
Normal file
733
packages/sheets-drawing-ui/src/facade/f-worksheet.ts
Normal file
@@ -0,0 +1,733 @@
|
||||
/**
|
||||
* 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 { ISheetFloatDom } from '@univerjs/sheets-drawing';
|
||||
import type { ICanvasFloatDom, ICanvasFloatDomInfo, IDOMAnchor } from '@univerjs/sheets-drawing-ui';
|
||||
import type { IFComponentKey } from '@univerjs/sheets-ui/facade';
|
||||
import type { FRange } from '@univerjs/sheets/facade';
|
||||
import type { ISaveCellImagesOptions } from './f-range';
|
||||
import { SheetSkeletonService } from '@univerjs/sheets';
|
||||
import { ISheetDrawingService, SetSheetDrawingCommand, transformToAxisAlignPosition, transformToDrawingPosition } from '@univerjs/sheets-drawing';
|
||||
import { FileNamePart, IBatchSaveImagesService, SheetCanvasFloatDomManagerService } from '@univerjs/sheets-drawing-ui';
|
||||
import { transformComponentKey } from '@univerjs/sheets-ui/facade';
|
||||
import { FWorksheet } from '@univerjs/sheets/facade';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
|
||||
// why omit this key? if componentKey is missing, then which component should be used?
|
||||
export interface IFICanvasFloatDom extends Omit<ICanvasFloatDom, 'componentKey' | 'unitId' | 'subUnitId'>, IFComponentKey { }
|
||||
|
||||
export interface IFCanvasFloatDomResult extends Omit<ICanvasFloatDom, 'componentKey' | 'unitId' | 'subUnitId' | 'initPosition'>,
|
||||
IFComponentKey,
|
||||
Pick<ICanvasFloatDomInfo, 'id'> {
|
||||
position: ITransformState;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFWorksheetDrawingUIMixin {
|
||||
/**
|
||||
* Get float dom by id
|
||||
* @param {string} id - float dom id
|
||||
* @returns {IFCanvasFloatDomResult | null} float dom info or null if not found
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const floatDom = fWorksheet.getFloatDomById('myFloatDomId');
|
||||
* if (floatDom) {
|
||||
* console.log('Float dom position:', floatDom.position);
|
||||
* console.log('Component key:', floatDom.componentKey);
|
||||
* console.log('Custom data:', floatDom.data);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getFloatDomById(id: string): Nullable<IFCanvasFloatDomResult>;
|
||||
|
||||
/**
|
||||
* Get all float doms in current worksheet
|
||||
* @returns {IFCanvasFloatDomResult[]} array of float dom info
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const allFloatDoms = fWorksheet.getAllFloatDoms();
|
||||
* allFloatDoms.forEach(floatDom => {
|
||||
* console.log('Float dom ID:', floatDom.id);
|
||||
* console.log('Position:', floatDom.position);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
getAllFloatDoms(): IFCanvasFloatDomResult[];
|
||||
|
||||
/**
|
||||
* Update float dom position and properties
|
||||
* @param {string} id - float dom id
|
||||
* @param {Partial<IFCanvasFloatDomResult>} config - new float dom config
|
||||
* @returns {FWorksheet} The worksheet instance for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const firstFloatDom = fWorksheet.getAllFloatDoms()[0];
|
||||
*
|
||||
* if (!firstFloatDom) return;
|
||||
*
|
||||
* // Update first float dom position and size
|
||||
* fWorksheet.updateFloatDom(firstFloatDom.id, {
|
||||
* position: {
|
||||
* left: 100,
|
||||
* top: 100,
|
||||
* width: 200,
|
||||
* height: 150,
|
||||
* angle: 45, // rotate 45 degrees
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Update first float dom data
|
||||
* fWorksheet.updateFloatDom(firstFloatDom.id, {
|
||||
* data: {
|
||||
* label: 'Updated Label',
|
||||
* color: '#ff0000'
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Disable the first float dom from transform
|
||||
* fWorksheet.updateFloatDom(firstFloatDom.id, {
|
||||
* allowTransform: false
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
updateFloatDom(id: string, config: Partial<IFCanvasFloatDomResult>): this;
|
||||
|
||||
/**
|
||||
* Batch update float doms
|
||||
* @param {Array<{id: string, config: Partial<IFCanvasFloatDomResult>}>} updates - array of update configs
|
||||
* @returns {FWorksheet} The worksheet instance for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Update multiple float doms at once
|
||||
* const allFloatDoms = fWorksheet.getAllFloatDoms();
|
||||
* fWorksheet.batchUpdateFloatDoms(allFloatDoms.map((floatDom, index) => {
|
||||
* if (floatDom.id === 'MyFloatDomId') {
|
||||
* return {
|
||||
* id: floatDom.id,
|
||||
* config: {
|
||||
* position: {
|
||||
* left: 100,
|
||||
* top: 100
|
||||
* },
|
||||
* data: {
|
||||
* label: 'Updated'
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* return {
|
||||
* id: floatDom.id,
|
||||
* config: {
|
||||
* position: {
|
||||
* left: 300,
|
||||
* top: 100
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }));
|
||||
* ```
|
||||
*/
|
||||
batchUpdateFloatDoms(updates: Array<{ id: string; config: Partial<IFCanvasFloatDomResult> }>): this;
|
||||
|
||||
/**
|
||||
* Remove float dom by id
|
||||
* @param {string} id - float dom id
|
||||
* @returns {FWorksheet} The worksheet instance for chaining
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const firstFloatDom = fWorksheet.getAllFloatDoms()[0];
|
||||
*
|
||||
* if (!firstFloatDom) return;
|
||||
*
|
||||
* // Remove the first float dom
|
||||
* fWorksheet.removeFloatDom(firstFloatDom.id);
|
||||
* ```
|
||||
*/
|
||||
removeFloatDom(id: string): this;
|
||||
|
||||
/**
|
||||
* Add a float dom to position.
|
||||
* @param {IFICanvasFloatDom} layer - The float dom layer configuration.
|
||||
* @param {string} [id] - The float dom id, if not given will be auto generated.
|
||||
* @returns float dom id and dispose function
|
||||
* @example
|
||||
* ```tsx
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // You should register components at an appropriate time (e.g., when Univer is loaded).
|
||||
* // This is a React component. For other frameworks, pass a matching adapter option, such as `{ framework: 'vue3' }`.
|
||||
* univerAPI.registerComponent(
|
||||
* 'myFloatDom',
|
||||
* ({ data }) => (
|
||||
* <div style={{ width: '100%', height: '100%', background: '#fff', border: '1px solid #ccc', boxSizing: 'border-box' }}>
|
||||
* popup content:
|
||||
* {' '}
|
||||
* {data?.label}
|
||||
* </div>
|
||||
* ),
|
||||
* );
|
||||
*
|
||||
* // Add a floating DOM
|
||||
* // If disposable is null, floating DOM addition failed
|
||||
* const disposable = fWorksheet.addFloatDomToPosition({
|
||||
* componentKey: 'myFloatDom',
|
||||
* initPosition: {
|
||||
* startX: 100,
|
||||
* endX: 300,
|
||||
* startY: 100,
|
||||
* endY: 200,
|
||||
* },
|
||||
*
|
||||
* // Component data
|
||||
* data: {
|
||||
* label: 'hahah',
|
||||
* },
|
||||
* });
|
||||
* console.log(disposable?.id); // The id of the floating DOM
|
||||
*
|
||||
* // Remove the floating DOM after 2 seconds
|
||||
* setTimeout(() => {
|
||||
* disposable?.dispose();
|
||||
* }, 2000);
|
||||
* ```
|
||||
*/
|
||||
addFloatDomToPosition(layer: IFICanvasFloatDom, id?: string): Nullable<{
|
||||
id: string;
|
||||
dispose: () => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Add dom over range to FloatDOM, And FloatDOM is registerComponent(BuiltInUIPart.CONTENT)
|
||||
* @param {FRange} range - The range to add the float dom.
|
||||
* @param {Partial<IFICanvasFloatDom>} layer - The float dom layer configuration.
|
||||
* @param {Partial<IDOMAnchor>} domLayout - The anchor configuration of the float dom.
|
||||
* @param {string} [id] - The float dom id, if not given will be auto generated
|
||||
* @returns float dom id and dispose function
|
||||
* @example
|
||||
* ```tsx
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Register a range loading component
|
||||
* const RangeLoading = () => {
|
||||
* const divStyle = {
|
||||
* width: '100%',
|
||||
* height: '100%',
|
||||
* backgroundColor: '#fff',
|
||||
* border: '1px solid #ccc',
|
||||
* boxSizing: 'border-box' as const,
|
||||
* display: 'flex',
|
||||
* justifyContent: 'center',
|
||||
* alignItems: 'center',
|
||||
* textAlign: 'center' as const,
|
||||
* transformOrigin: 'top left',
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <div style={divStyle}>
|
||||
* Loading...
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* univerAPI.registerComponent('RangeLoading', RangeLoading);
|
||||
*
|
||||
* // Add the range loading component covering the range A1:C3
|
||||
* const fRange = fWorksheet.getRange('A1:C3');
|
||||
* const disposable = fWorksheet.addFloatDomToRange(fRange, { componentKey: 'RangeLoading' }, {}, 'myRangeLoading');
|
||||
* console.log(disposable?.id); // The id of the floating DOM
|
||||
*
|
||||
* // Remove the floating DOM after 2 seconds
|
||||
* setTimeout(() => {
|
||||
* disposable?.dispose();
|
||||
* }, 2000);
|
||||
*
|
||||
* // another example-------------------
|
||||
* // Register a float button component
|
||||
* const FloatButton = () => {
|
||||
* const divStyle = {
|
||||
* width: '100px',
|
||||
* height: '30px',
|
||||
* backgroundColor: '#fff',
|
||||
* border: '1px solid #ccc',
|
||||
* boxSizing: 'border-box' as const,
|
||||
* display: 'flex',
|
||||
* justifyContent: 'center',
|
||||
* alignItems: 'center',
|
||||
* textAlign: 'center' as const,
|
||||
* cursor: 'pointer',
|
||||
* };
|
||||
*
|
||||
* const clickHandler = () => {
|
||||
* console.warn('click');
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <div style={divStyle} onClick={clickHandler}>
|
||||
* FloatButton
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* univerAPI.registerComponent('FloatButton', FloatButton);
|
||||
*
|
||||
* // Add the float button to the range A5:C7, position is start from A5 cell, and width is 100px, height is 30px, margin is 100% of range width and height
|
||||
* const fRange2 = fWorksheet.getRange('A5:C7');
|
||||
* const disposable2 = fWorksheet.addFloatDomToRange(
|
||||
* fRange2,
|
||||
* {
|
||||
* componentKey: 'FloatButton',
|
||||
* },
|
||||
* {
|
||||
* width: 100,
|
||||
* height: 30,
|
||||
* marginX: '100%', // margin percent to range width, or pixel
|
||||
* marginY: '100%'
|
||||
* },
|
||||
* 'myFloatButton'
|
||||
* );
|
||||
* console.log(disposable2?.id); // The id of the floating DOM
|
||||
* ```
|
||||
*/
|
||||
addFloatDomToRange(range: FRange, layer: Partial<IFICanvasFloatDom>, domLayout: Partial<IDOMAnchor>, id?: string): Nullable<{
|
||||
id: string;
|
||||
dispose: () => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Add dom at column header, And FloatDOM is registerComponent(BuiltInUIPart.CONTENT)
|
||||
* @param {number} column - The column index to add the float dom.
|
||||
* @param {Partial<IFICanvasFloatDom>} layer - The float dom layer configuration.
|
||||
* @param {IDOMAnchor} domPos - The anchor configuration of the float dom.
|
||||
* @param {string} [id] - The float dom id, if not given will be auto generated
|
||||
* @returns float dom id and dispose function
|
||||
* @example
|
||||
* ```tsx
|
||||
* const fWorksheet = univerAPI.getActiveWorkbook().getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Register a float button component
|
||||
* const FloatButton = () => {
|
||||
* const divStyle = {
|
||||
* width: '100px',
|
||||
* height: '30px',
|
||||
* backgroundColor: '#fff',
|
||||
* border: '1px solid #ccc',
|
||||
* boxSizing: 'border-box' as const,
|
||||
* display: 'flex',
|
||||
* justifyContent: 'center',
|
||||
* alignItems: 'center',
|
||||
* textAlign: 'center' as const,
|
||||
* cursor: 'pointer',
|
||||
* };
|
||||
*
|
||||
* const clickHandler = () => {
|
||||
* console.warn('click');
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <div style={divStyle} onClick={clickHandler}>
|
||||
* FloatButton
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* univerAPI.registerComponent('FloatButton', FloatButton);
|
||||
*
|
||||
* // Add the float button to the column D header, position is right align, width is 100px, height is 30px, margin is 0
|
||||
* const disposable = fWorksheet.addFloatDomToColumnHeader(
|
||||
* 3,
|
||||
* {
|
||||
* componentKey: 'FloatButton',
|
||||
* allowTransform: false,
|
||||
* },
|
||||
* {
|
||||
* width: 100,
|
||||
* height: 30,
|
||||
* marginX: 0,
|
||||
* marginY: 0,
|
||||
* horizonOffsetAlign: 'right',
|
||||
* },
|
||||
* 'myFloatButton'
|
||||
* );
|
||||
* console.log(disposable?.id); // The id of the floating DOM
|
||||
*
|
||||
* // Remove the float button after 2 seconds
|
||||
* setTimeout(() => {
|
||||
* disposable?.dispose();
|
||||
* }, 2000);
|
||||
* ```
|
||||
*/
|
||||
addFloatDomToColumnHeader(column: number, layer: Partial<IFICanvasFloatDom>, domPos: IDOMAnchor, id?: string): Nullable<{
|
||||
id: string;
|
||||
dispose: () => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Save all cell images from specified ranges to the file system.
|
||||
* This method will open a directory picker dialog and save all images to the selected directory.
|
||||
*
|
||||
* @param {ISaveCellImagesOptions} [options] - Options for saving images
|
||||
* @param {FRange[]} [ranges] - The ranges to get cell images from. If not provided, all images in the worksheet will be saved.
|
||||
* @returns {Promise<boolean>} True if images are saved successfully, otherwise false
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Save cell images from multiple ranges
|
||||
* const range1 = fWorksheet.getRange('A1:B10');
|
||||
* const range2 = fWorksheet.getRange('D1:E10');
|
||||
*
|
||||
* // Save with default options (using cell address as file name)
|
||||
* await fWorksheet.saveCellImagesAsync(undefined, [range1, range2]);
|
||||
*
|
||||
* // Save with custom options
|
||||
* await fWorksheet.saveCellImagesAsync({
|
||||
* useCellAddress: true,
|
||||
* useColumnIndex: 2, // Use values from column C for file names
|
||||
* }, [range1, range2]);
|
||||
* ```
|
||||
*/
|
||||
saveCellImagesAsync(options?: ISaveCellImagesOptions, ranges?: FRange[]): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class FWorksheetDrawingUIMixin extends FWorksheet implements IFWorksheetDrawingUIMixin {
|
||||
override getFloatDomById(id: string): Nullable<IFCanvasFloatDomResult> {
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const info = floatDomService.getFloatDomInfo(id);
|
||||
if (!info) return null;
|
||||
|
||||
const { unitId, subUnitId } = info;
|
||||
const { rect } = info;
|
||||
const state = rect.getState();
|
||||
const { left = 0, top = 0, width = 0, height = 0, flipX = false, flipY = false, angle = 0, skewX = 0, skewY = 0 } = state;
|
||||
const drawingParm = this._injector.get(ISheetDrawingService).getDrawingByParam({
|
||||
drawingId: info.id,
|
||||
unitId,
|
||||
subUnitId,
|
||||
})! as ISheetFloatDom;
|
||||
|
||||
if (!drawingParm) return null;
|
||||
|
||||
return {
|
||||
position: {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
flipX,
|
||||
flipY,
|
||||
angle,
|
||||
skewX,
|
||||
skewY,
|
||||
},
|
||||
componentKey: drawingParm.componentKey,
|
||||
allowTransform: drawingParm.allowTransform,
|
||||
data: drawingParm.data,
|
||||
id: info.id,
|
||||
};
|
||||
}
|
||||
|
||||
override getAllFloatDoms(): IFCanvasFloatDomResult[] {
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
|
||||
return Array.from(floatDomService.getFloatDomsBySubUnitId(unitId, subUnitId).values())
|
||||
.map((info): IFCanvasFloatDomResult | null => {
|
||||
const { rect } = info;
|
||||
const drawingParm = this._injector.get(ISheetDrawingService).getDrawingByParam({
|
||||
drawingId: info.id,
|
||||
unitId,
|
||||
subUnitId,
|
||||
})! as ISheetFloatDom;
|
||||
|
||||
if (!drawingParm) return null;
|
||||
|
||||
const { left, top, width, height, flipX, flipY, angle, skewX, skewY } = rect.getState();
|
||||
|
||||
return {
|
||||
position: {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
flipX,
|
||||
flipY,
|
||||
angle,
|
||||
skewX,
|
||||
skewY,
|
||||
},
|
||||
componentKey: drawingParm.componentKey,
|
||||
allowTransform: drawingParm.allowTransform,
|
||||
data: drawingParm.data,
|
||||
id: info.id,
|
||||
};
|
||||
})
|
||||
.filter((dom): dom is IFCanvasFloatDomResult => dom != null);
|
||||
}
|
||||
|
||||
override updateFloatDom(id: string, config: Partial<Omit<IFCanvasFloatDomResult, 'id'>>): this {
|
||||
const sheetSkeletonService = this._injector.get(SheetSkeletonService);
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const info = floatDomService.getFloatDomInfo(id);
|
||||
if (!info) return this;
|
||||
|
||||
const { unitId, subUnitId } = info;
|
||||
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!skeleton) return this;
|
||||
|
||||
const drawingParm = this._injector.get(ISheetDrawingService).getDrawingByParam({
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: id,
|
||||
})! as ISheetFloatDom;
|
||||
|
||||
const newParam: ISheetFloatDom = {
|
||||
...drawingParm,
|
||||
componentKey: config.componentKey || drawingParm.componentKey,
|
||||
allowTransform: config.allowTransform !== undefined ? config.allowTransform : drawingParm.allowTransform,
|
||||
data: config.data || drawingParm.data,
|
||||
sheetTransform: config.position
|
||||
? transformToDrawingPosition(config.position, skeleton) ?? drawingParm.sheetTransform
|
||||
: drawingParm.sheetTransform,
|
||||
transform: {
|
||||
...drawingParm.transform,
|
||||
...config.position, // Merge with existing transform
|
||||
},
|
||||
axisAlignSheetTransform: config.position
|
||||
? transformToAxisAlignPosition(config.position, skeleton) ?? drawingParm.sheetTransform
|
||||
: drawingParm.sheetTransform,
|
||||
};
|
||||
|
||||
const res = this._commandService.syncExecuteCommand(SetSheetDrawingCommand.id, { unitId, subUnitId, drawings: [newParam] });
|
||||
if (!res) {
|
||||
throw new Error('updateFloatDom failed');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
override batchUpdateFloatDoms(updates: Array<{ id: string; config: Partial<Omit<IFCanvasFloatDomResult, 'id'>> }>): this {
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const drawingService = this._injector.get(ISheetDrawingService);
|
||||
const sheetSkeletonService = this._injector.get(SheetSkeletonService);
|
||||
|
||||
const drawings: ISheetFloatDom[] = [];
|
||||
|
||||
for (const update of updates) {
|
||||
const info = floatDomService.getFloatDomInfo(update.id);
|
||||
if (!info) continue;
|
||||
|
||||
const { unitId, subUnitId } = info;
|
||||
const skeleton = sheetSkeletonService.getSkeleton(unitId, subUnitId);
|
||||
if (!skeleton) continue;
|
||||
|
||||
const drawingParm = drawingService.getDrawingByParam({
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: update.id,
|
||||
}) as ISheetFloatDom;
|
||||
if (!drawingParm) continue;
|
||||
|
||||
const newParam: ISheetFloatDom = {
|
||||
...drawingParm,
|
||||
componentKey: update.config.componentKey || drawingParm.componentKey,
|
||||
allowTransform: update.config.allowTransform !== undefined ? update.config.allowTransform : drawingParm.allowTransform,
|
||||
data: update.config.data || drawingParm.data,
|
||||
sheetTransform: update.config.position
|
||||
? transformToDrawingPosition(update.config.position, skeleton) ?? drawingParm.sheetTransform
|
||||
: drawingParm.sheetTransform,
|
||||
transform: {
|
||||
...drawingParm.transform,
|
||||
...update.config.position, // Merge with existing transform
|
||||
},
|
||||
axisAlignSheetTransform: update.config.position
|
||||
? transformToAxisAlignPosition(update.config.position, skeleton) ?? drawingParm.sheetTransform
|
||||
: drawingParm.sheetTransform,
|
||||
};
|
||||
|
||||
drawings.push(newParam);
|
||||
}
|
||||
|
||||
if (drawings.length > 0) {
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
const res = this._commandService.syncExecuteCommand(SetSheetDrawingCommand.id, { unitId, subUnitId, drawings });
|
||||
if (!res) {
|
||||
throw new Error('batchUpdateFloatDoms failed');
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
override removeFloatDom(id: string): this {
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
floatDomService.removeFloatDom(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
override addFloatDomToPosition(layer: IFICanvasFloatDom, id?: string): Nullable<{
|
||||
id: string;
|
||||
dispose: () => void;
|
||||
}> {
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
const { key, disposableCollection } = transformComponentKey(layer, this._injector.get(ComponentManager));
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const res = floatDomService.addFloatDomToPosition({ ...layer, componentKey: key, unitId, subUnitId }, id);
|
||||
|
||||
if (res) {
|
||||
disposableCollection.add(res.dispose);
|
||||
return {
|
||||
id: res.id,
|
||||
dispose: (): void => {
|
||||
disposableCollection.dispose();
|
||||
res.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
disposableCollection.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
override addFloatDomToRange(fRange: FRange, layer: IFICanvasFloatDom, domLayout: IDOMAnchor, id?: string): Nullable<{
|
||||
id: string;
|
||||
dispose: () => void;
|
||||
}> {
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
const { key, disposableCollection } = transformComponentKey(layer, this._injector.get(ComponentManager));
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const res = floatDomService.addFloatDomToRange(fRange.getRange(), { ...layer, componentKey: key, unitId, subUnitId }, domLayout, id);
|
||||
|
||||
if (res) {
|
||||
disposableCollection.add(res.dispose);
|
||||
return {
|
||||
id: res.id,
|
||||
dispose: (): void => {
|
||||
disposableCollection.dispose();
|
||||
res.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
disposableCollection.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
override addFloatDomToColumnHeader(column: number, layer: IFICanvasFloatDom, domLayout: IDOMAnchor, id?: string): Nullable<{
|
||||
id: string;
|
||||
dispose: () => void;
|
||||
}> {
|
||||
const unitId = this._workbook.getUnitId();
|
||||
const subUnitId = this._worksheet.getSheetId();
|
||||
const { key, disposableCollection } = transformComponentKey(layer, this._injector.get(ComponentManager));
|
||||
const floatDomService = this._injector.get(SheetCanvasFloatDomManagerService);
|
||||
const domRangeDispose = floatDomService.addFloatDomToColumnHeader(column, { ...layer, componentKey: key, unitId, subUnitId }, domLayout, id);
|
||||
|
||||
if (domRangeDispose) {
|
||||
disposableCollection.add(domRangeDispose.dispose);
|
||||
return {
|
||||
id: domRangeDispose.id,
|
||||
dispose: (): void => {
|
||||
disposableCollection.dispose();
|
||||
domRangeDispose.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
disposableCollection.dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
override async saveCellImagesAsync(options?: ISaveCellImagesOptions, ranges?: FRange[]): Promise<boolean> {
|
||||
const batchSaveService = this._injector.get(IBatchSaveImagesService);
|
||||
const unitId = this._fWorkbook.getId();
|
||||
const subUnitId = this.getSheetId();
|
||||
|
||||
// Get all ranges as IRange array
|
||||
const iRanges = ranges ? ranges.map((r) => r.getRange()) : [this._worksheet.getCellMatrix().getDataRange()];
|
||||
|
||||
// Get images from all ranges
|
||||
const images = batchSaveService.getCellImagesFromRanges(unitId, subUnitId, iRanges);
|
||||
|
||||
if (images.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If only one image, download directly
|
||||
if (images.length === 1) {
|
||||
try {
|
||||
await batchSaveService.downloadSingleImage(images[0]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to download image:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Build config from options
|
||||
const fileNameParts: FileNamePart[] = [];
|
||||
const useCellAddress = options?.useCellAddress ?? true;
|
||||
const useColumnIndex = options?.useColumnIndex;
|
||||
|
||||
if (useCellAddress) {
|
||||
fileNameParts.push(FileNamePart.CELL_ADDRESS);
|
||||
}
|
||||
if (useColumnIndex !== undefined) {
|
||||
fileNameParts.push(FileNamePart.COLUMN_VALUE);
|
||||
}
|
||||
|
||||
// Ensure at least one naming option
|
||||
if (fileNameParts.length === 0) {
|
||||
fileNameParts.push(FileNamePart.CELL_ADDRESS);
|
||||
}
|
||||
|
||||
try {
|
||||
await batchSaveService.saveImagesWithContext(images, {
|
||||
fileNameParts,
|
||||
columnIndex: useColumnIndex,
|
||||
}, unitId, subUnitId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save images:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FWorksheet.extend(FWorksheetDrawingUIMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FWorksheet extends IFWorksheetDrawingUIMixin { }
|
||||
}
|
||||
25
packages/sheets-drawing-ui/src/facade/index.ts
Normal file
25
packages/sheets-drawing-ui/src/facade/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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-worksheet';
|
||||
import './f-event';
|
||||
import './f-univer';
|
||||
import './f-range';
|
||||
|
||||
export type * from './f-event';
|
||||
export type * from './f-range';
|
||||
export type * from './f-univer';
|
||||
export type * from './f-worksheet';
|
||||
2
packages/sheets-drawing-ui/src/global.css
Normal file
2
packages/sheets-drawing-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
49
packages/sheets-drawing-ui/src/index.ts
Normal file
49
packages/sheets-drawing-ui/src/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 './global.css';
|
||||
|
||||
export { DeleteDrawingsCommand } from './commands/commands/delete-drawings.command';
|
||||
export { GroupSheetDrawingCommand } from './commands/commands/group-sheet-drawing.command';
|
||||
export { InsertFloatImageCommand } from './commands/commands/insert-image.command';
|
||||
export type { IInsertImageCommandParams } from './commands/commands/insert-image.command';
|
||||
export { MoveDrawingsCommand } from './commands/commands/move-drawings.command';
|
||||
export { SaveCellImagesCommand } from './commands/commands/save-cell-images.command';
|
||||
export { UngroupSheetDrawingCommand } from './commands/commands/ungroup-sheet-drawing.command';
|
||||
export { EditSheetDrawingOperation } from './commands/operations/edit-sheet-drawing.operation';
|
||||
export { SidebarSheetDrawingOperation } from './commands/operations/open-drawing-panel.operation';
|
||||
export type { IUniverSheetsDrawingUIConfig } from './config/config';
|
||||
export { SheetsDrawingGroupCopyPasteController } from './controllers/sheet-drawing-group-copy-paste.controller';
|
||||
export { SheetDrawingUpdateController } from './controllers/sheet-drawing-update.controller';
|
||||
export { registerSheetsDrawingFloatingHostCapability, SHEETS_DRAWING_FLOATING_HOST_DEPENDENCIES } from './embed';
|
||||
export { SHEETS_IMAGE_MENU_ID } from './menu/image.menu';
|
||||
export { menuSchema as SheetsDrawingUIMenuSchema } from './menu/schema';
|
||||
export { UniverSheetsDrawingMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverSheetsDrawingUIPlugin } from './plugin';
|
||||
export { BatchSaveImagesService, FileNamePart, IBatchSaveImagesService } from './services/batch-save-images.service';
|
||||
export type { IBatchSaveImagesConfig, ICellImageInfo } from './services/batch-save-images.service';
|
||||
export {
|
||||
calcSheetFloatDomPosition,
|
||||
SHEET_FLOAT_DOM_PREFIX,
|
||||
SheetCanvasFloatDomManagerService,
|
||||
} from './services/canvas-float-dom-manager.service';
|
||||
export type { ICanvasFloatDom, ICanvasFloatDomInfo, IDOMAnchor } from './services/canvas-float-dom-manager.service';
|
||||
export { DrawingContextMenuService, IDrawingContextMenuService } from './services/drawing-context-menu.service';
|
||||
export type { IDrawingContextMenuProvider, IDrawingContextMenuProviderContext } from './services/drawing-context-menu.service';
|
||||
export { SHEET_CHART_RENDER_OBJECT_CONFIG } from './services/sheet-chart-render-object.config';
|
||||
export { SheetDrawingHitTestService } from './services/sheet-drawing-hit-test.service';
|
||||
export type { ISheetDrawingDoubleClickEvent, ISheetDrawingHitTestResult } from './services/sheet-drawing-hit-test.service';
|
||||
export { SheetDrawingAnchor } from './views/sheet-image-panel/SheetDrawingAnchor';
|
||||
88
packages/sheets-drawing-ui/src/locale/ar-SA.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/ar-SA.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'صورة',
|
||||
uploadLoading: {
|
||||
loading: 'جارٍ التحميل..., المتبقي',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'صورة عائمة',
|
||||
cell: 'صورة خلية',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'تحرير الصورة',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'حفظ صور الخلايا',
|
||||
menuLabel: 'حفظ صور الخلايا',
|
||||
imageCount: 'عدد الصور',
|
||||
fileNameConfig: 'اسم الملف',
|
||||
useRowCol: 'استخدام عنوان الخلية (A1، B2...)',
|
||||
useColumnValue: 'استخدام قيمة العمود',
|
||||
selectColumn: 'تحديد العمود',
|
||||
cancel: 'إلغاء',
|
||||
confirm: 'حفظ',
|
||||
saving: 'جارٍ الحفظ...',
|
||||
error: 'فشل في حفظ صور الخلايا',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'حذف',
|
||||
edit: 'تحرير',
|
||||
crop: 'قص',
|
||||
reset: 'إعادة تعيين الحجم',
|
||||
flipH: 'قلب أفقي',
|
||||
flipV: 'قلب عمودي',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'حجم الصورة يتجاوز الحد، الحد هو {0}م',
|
||||
invalidImageType: 'نوع الصورة غير صالح',
|
||||
exceedMaxCount: 'يمكن رفع {0} صور فقط في المرة الواحدة',
|
||||
invalidImage: 'صورة غير صالحة',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'خصائص المرساة',
|
||||
both: 'النقل والتحجيم مع الخلايا',
|
||||
position: 'النقل دون التحجيم مع الخلايا',
|
||||
none: 'عدم النقل أو التحجيم مع الخلايا',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'لصق كصورة خلية',
|
||||
pasteContent: 'سيؤدي لصق صورة خلية إلى استبدال المحتوى الموجود في الخلية، هل تريد المتابعة؟',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'النطاق محمي، وليس لديك إذن التحرير. للتحرير، يرجى التواصل مع المنشئ.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'عرض الرسم',
|
||||
'drawing-move-down': 'نقل الرسم لأسفل',
|
||||
'drawing-move-up': 'نقل الرسم لأعلى',
|
||||
'drawing-move-left': 'نقل الرسم لليسار',
|
||||
'drawing-move-right': 'نقل الرسم لليمين',
|
||||
'drawing-delete': 'حذف الرسم',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/ca-ES.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/ca-ES.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Imatge',
|
||||
uploadLoading: {
|
||||
loading: 'S\'està carregant..., restant',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Imatge flotant',
|
||||
cell: 'Imatge de cel·la',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Edita la imatge',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Desa les imatges de cel·la',
|
||||
menuLabel: 'Desa les imatges de cel·la',
|
||||
imageCount: 'Nombre d\'imatges',
|
||||
fileNameConfig: 'Nom del fitxer',
|
||||
useRowCol: 'Utilitza l\'adreça de la cel·la (A1, B2...)',
|
||||
useColumnValue: 'Utilitza el valor de la columna',
|
||||
selectColumn: 'Selecciona la columna',
|
||||
cancel: 'Cancel·la',
|
||||
confirm: 'Desa',
|
||||
saving: 'Desant...',
|
||||
error: 'No s\'han pogut desar les imatges de cel·la',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Elimina',
|
||||
edit: 'Edita',
|
||||
crop: 'Retalla',
|
||||
reset: 'Restableix la mida',
|
||||
flipH: 'Gira horizontalmente',
|
||||
flipV: 'Gira verticalmente',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'La mida de la imatge supera el límit, el límit és {0}M',
|
||||
invalidImageType: 'Tipus d’imatge no vàlid',
|
||||
exceedMaxCount: 'Només es poden pujar {0} imatges alhora',
|
||||
invalidImage: 'Imatge no vàlida',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Propietats d’ancoratge',
|
||||
both: 'Mou i canvia la mida amb les cel·les',
|
||||
position: 'Mou però no canviïs la mida amb les cel·les',
|
||||
none: 'No moguis ni canviïs la mida amb les cel·les',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Enganxa com a imatge de cel·la',
|
||||
pasteContent: 'Enganxar una imatge de cel·la sobreescriurà el contingut existent de la cel·la, continuar enganxant',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'L\'interval està protegit i no teniu permís d\'edició. Per editar, contacteu amb el creador.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Vista del dibuix',
|
||||
'drawing-move-down': 'Mou el dibuix cap avall',
|
||||
'drawing-move-up': 'Mou el dibuix cap amunt',
|
||||
'drawing-move-left': 'Mou el dibuix a l’esquerra',
|
||||
'drawing-move-right': 'Mou el dibuix a la dreta',
|
||||
'drawing-delete': 'Elimina el dibuix',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/de-DE.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/de-DE.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Bild',
|
||||
uploadLoading: {
|
||||
loading: 'Wird geladen..., verbleibend',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Schwebendes Bild',
|
||||
cell: 'Zellenbild',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Bild bearbeiten',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Zellenbilder speichern',
|
||||
menuLabel: 'Zellenbilder speichern',
|
||||
imageCount: 'Bildanzahl',
|
||||
fileNameConfig: 'Dateiname',
|
||||
useRowCol: 'Zellenadresse verwenden (A1, B2...)',
|
||||
useColumnValue: 'Spaltenwert verwenden',
|
||||
selectColumn: 'Spalte auswählen',
|
||||
cancel: 'Abbrechen',
|
||||
confirm: 'Speichern',
|
||||
saving: 'Speichern...',
|
||||
error: 'Zellenbilder konnten nicht gespeichert werden',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Löschen',
|
||||
edit: 'Bearbeiten',
|
||||
crop: 'Zuschneiden',
|
||||
reset: 'Größe zurücksetzen',
|
||||
flipH: 'Horizontal spiegeln',
|
||||
flipV: 'Vertikal spiegeln',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Bildgröße überschreitet das Limit, Limit ist {0}M',
|
||||
invalidImageType: 'Ungültiger Bildtyp',
|
||||
exceedMaxCount: 'Es können nur {0} Bilder gleichzeitig hochgeladen werden',
|
||||
invalidImage: 'Ungültiges Bild',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Anker-Eigenschaften',
|
||||
both: 'Mit Zellen verschieben und skalieren',
|
||||
position: 'Mit Zellen verschieben, aber nicht skalieren',
|
||||
none: 'Weder verschieben noch skalieren mit Zellen',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Als Zellenbild einfügen',
|
||||
pasteContent: 'Das Einfügen eines Zellenbilds überschreibt den bestehenden Inhalt der Zelle, mit dem Einfügen fortfahren?',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'Der Bereich ist geschützt, und Sie haben keine Bearbeitungsberechtigung. Um zu bearbeiten, wenden Sie sich bitte an den Ersteller.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Zeichnungsansicht',
|
||||
'drawing-move-down': 'Zeichnung nach unten verschieben',
|
||||
'drawing-move-up': 'Zeichnung nach oben verschieben',
|
||||
'drawing-move-left': 'Zeichnung nach links verschieben',
|
||||
'drawing-move-right': 'Zeichnung nach rechts verschieben',
|
||||
'drawing-delete': 'Zeichnung löschen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
86
packages/sheets-drawing-ui/src/locale/en-US.ts
Normal file
86
packages/sheets-drawing-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Image',
|
||||
uploadLoading: {
|
||||
loading: 'Loading..., remaining',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Float Image',
|
||||
cell: 'Cell Image',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Edit Image',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Save Cell Images',
|
||||
menuLabel: 'Save Cell Images',
|
||||
imageCount: 'Image Count',
|
||||
fileNameConfig: 'File Name',
|
||||
useRowCol: 'Use Cell Address (A1, B2...)',
|
||||
useColumnValue: 'Use Column Value',
|
||||
selectColumn: 'Select Column',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Save',
|
||||
saving: 'Saving...',
|
||||
error: 'Failed to save cell images',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
crop: 'Crop',
|
||||
reset: 'Reset Size',
|
||||
flipH: 'Flip Horizontal',
|
||||
flipV: 'Flip Vertical',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Image size exceeds limit, limit is {0}M',
|
||||
invalidImageType: 'Invalid image type',
|
||||
exceedMaxCount: 'Only {0} images can be uploaded at a time',
|
||||
invalidImage: 'Invalid image',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Anchor Properties',
|
||||
both: 'Move and size with cells',
|
||||
position: "Move but don't size with cells",
|
||||
none: "Don't move or size with cells",
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Paste as cell image',
|
||||
pasteContent: 'Pasting a cell image will overwrite the existing content of the cell, continue pasting',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'The range is protected, and you do not have edit permission. To edit, please contact the creator.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Drawing View',
|
||||
'drawing-move-down': 'Move Drawing down',
|
||||
'drawing-move-up': 'Move Drawing up',
|
||||
'drawing-move-left': 'Move Drawing left',
|
||||
'drawing-move-right': 'Move Drawing right',
|
||||
'drawing-delete': 'Delete Drawing',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/es-ES.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/es-ES.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Imagen',
|
||||
uploadLoading: {
|
||||
loading: 'Cargando..., restante',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Imagen flotante',
|
||||
cell: 'Imagen de celda',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Editar imagen',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Guardar imágenes de celda',
|
||||
menuLabel: 'Guardar imágenes de celda',
|
||||
imageCount: 'Cantidad de imágenes',
|
||||
fileNameConfig: 'Nombre del archivo',
|
||||
useRowCol: 'Usar dirección de celda (A1, B2...)',
|
||||
useColumnValue: 'Usar valor de columna',
|
||||
selectColumn: 'Seleccionar columna',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Guardar',
|
||||
saving: 'Guardando...',
|
||||
error: 'Error al guardar las imágenes de celda',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Eliminar',
|
||||
edit: 'Editar',
|
||||
crop: 'Recortar',
|
||||
reset: 'Restablecer tamaño',
|
||||
flipH: 'Voltear horizontalmente',
|
||||
flipV: 'Voltear verticalmente',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'El tamaño de la imagen supera el límite, el límite es {0}M',
|
||||
invalidImageType: 'Tipo de imagen no válido',
|
||||
exceedMaxCount: 'Solo se pueden subir {0} imágenes a la vez',
|
||||
invalidImage: 'Imagen no válida',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Propiedades de anclaje',
|
||||
both: 'Mover y cambiar tamaño con las celdas',
|
||||
position: 'Mover pero no cambiar tamaño con las celdas',
|
||||
none: 'No mover ni cambiar tamaño con las celdas',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Pegar como imagen de celda',
|
||||
pasteContent: 'Pegar una imagen de celda sobrescribirá el contenido existente de la celda, continuar pegando',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'El rango está protegido y no tienes permiso de edición. Para editar, contacta al creador.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Vista de dibujo',
|
||||
'drawing-move-down': 'Mover dibujo hacia abajo',
|
||||
'drawing-move-up': 'Mover dibujo hacia arriba',
|
||||
'drawing-move-left': 'Mover dibujo a la izquierda',
|
||||
'drawing-move-right': 'Mover dibujo a la derecha',
|
||||
'drawing-delete': 'Eliminar dibujo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/fa-IR.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/fa-IR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'تصویر',
|
||||
uploadLoading: {
|
||||
loading: 'در حال بارگیری...، باقی مانده',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'شناور کردن تصویر',
|
||||
cell: 'تصویر سلولی',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'ویرایش تصویر',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'ذخیره تصاویر سلول',
|
||||
menuLabel: 'ذخیره تصاویر سلول',
|
||||
imageCount: 'تعداد تصاویر',
|
||||
fileNameConfig: 'نام فایل',
|
||||
useRowCol: 'استفاده از آدرس سلول (A1, B2...)',
|
||||
useColumnValue: 'استفاده از مقدار ستون',
|
||||
selectColumn: 'انتخاب ستون',
|
||||
cancel: 'لغو',
|
||||
confirm: 'ذخیره',
|
||||
saving: 'در حال ذخیره...',
|
||||
error: 'ذخیره تصاویر سلول ناموفق بود',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'حذف',
|
||||
edit: 'ویرایش',
|
||||
crop: 'کراپ',
|
||||
reset: 'بازنشانی اندازه',
|
||||
flipH: 'چرخش افقی',
|
||||
flipV: 'چرخش عمودی',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'اندازه تصویر از حد مجاز فراتر رفته است، حد مجاز {0}M است',
|
||||
invalidImageType: 'نوع تصویر نامعتبر است',
|
||||
exceedMaxCount: 'فقط {0} تصویر میتوانند همزمان آپلود شوند',
|
||||
invalidImage: 'تصویر نامعتبر است',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'خواص لنگر',
|
||||
both: 'جابهجایی و تغییر اندازه با سلولها',
|
||||
position: 'جابهجایی اما تغییر اندازه نکردن با سلولها',
|
||||
none: 'جابهجایی یا تغییر اندازه نکردن با سلولها',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'چسباندن به عنوان تصویر سلول',
|
||||
pasteContent: 'چسباندن تصویر سلول محتوای موجود سلول را پاک خواهد کرد، ادامه چسباندن',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'محدوده محافظت شده است و شما مجوز ویرایش را ندارید. برای ویرایش، لطفا با سازنده تماس بگیرید.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'نمای طراحی',
|
||||
'drawing-move-down': 'جابهجایی طراحی به پایین',
|
||||
'drawing-move-up': 'جابهجایی طراحی به بالا',
|
||||
'drawing-move-left': 'جابهجایی طراحی به چپ',
|
||||
'drawing-move-right': 'جابهجایی طراحی به راست',
|
||||
'drawing-delete': 'حذف طراحی',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/fr-FR.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/fr-FR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Image',
|
||||
uploadLoading: {
|
||||
loading: 'Chargement..., restant',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Image flottante',
|
||||
cell: 'Image de cellule',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Modifier l\'image',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Enregistrer les images de cellule',
|
||||
menuLabel: 'Enregistrer les images de cellule',
|
||||
imageCount: 'Nombre d\'images',
|
||||
fileNameConfig: 'Nom du fichier',
|
||||
useRowCol: 'Utiliser l\'adresse de cellule (A1, B2...)',
|
||||
useColumnValue: 'Utiliser la valeur de la colonne',
|
||||
selectColumn: 'Sélectionner la colonne',
|
||||
cancel: 'Annuler',
|
||||
confirm: 'Enregistrer',
|
||||
saving: 'Enregistrement...',
|
||||
error: 'Échec de l\'enregistrement des images de cellule',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Supprimer',
|
||||
edit: 'Éditer',
|
||||
crop: 'Rogner',
|
||||
reset: 'Réinitialiser la taille',
|
||||
flipH: 'Retournement horizontal',
|
||||
flipV: 'Retournement vertical',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'La taille de l\'image dépasse la limite, la limite est de {0}M',
|
||||
invalidImageType: 'Type d\'image invalide',
|
||||
exceedMaxCount: 'Seulement {0} images peuvent être téléchargées à la fois',
|
||||
invalidImage: 'Image invalide',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Propriétés de l\'ancre',
|
||||
both: 'Déplacer et redimensionner avec les cellules',
|
||||
position: 'Déplacer mais ne pas redimensionner avec les cellules',
|
||||
none: 'Ne pas déplacer ni redimensionner avec les cellules',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Coller comme image de cellule',
|
||||
pasteContent: 'Coller une image de cellule écrasera le contenu existant de la cellule, continuer le collage',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'La plage est protégée, et vous n\'avez pas la permission de modifier. Pour modifier, veuillez contacter le créateur.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Vue du dessin',
|
||||
'drawing-move-down': 'Déplacer le dessin vers le bas',
|
||||
'drawing-move-up': 'Déplacer le dessin vers le haut',
|
||||
'drawing-move-left': 'Déplacer le dessin vers la gauche',
|
||||
'drawing-move-right': 'Déplacer le dessin vers la droite',
|
||||
'drawing-delete': 'Supprimer le dessin',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/id-ID.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/id-ID.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Gambar',
|
||||
uploadLoading: {
|
||||
loading: 'Memuat..., tersisa',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Gambar Mengambang',
|
||||
cell: 'Gambar Sel',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Edit Gambar',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Simpan Gambar Sel',
|
||||
menuLabel: 'Simpan Gambar Sel',
|
||||
imageCount: 'Jumlah Gambar',
|
||||
fileNameConfig: 'Nama File',
|
||||
useRowCol: 'Gunakan Alamat Sel (A1, B2...)',
|
||||
useColumnValue: 'Gunakan Nilai Kolom',
|
||||
selectColumn: 'Pilih Kolom',
|
||||
cancel: 'Batal',
|
||||
confirm: 'Simpan',
|
||||
saving: 'Menyimpan...',
|
||||
error: 'Gagal menyimpan gambar sel',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Hapus',
|
||||
edit: 'Edit',
|
||||
crop: 'Pangkas',
|
||||
reset: 'Atur Ulang Ukuran',
|
||||
flipH: 'Balik Horizontal',
|
||||
flipV: 'Balik Vertikal',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Ukuran gambar melebihi batas, batasnya adalah {0}M',
|
||||
invalidImageType: 'Tipe gambar tidak valid',
|
||||
exceedMaxCount: 'Hanya {0} gambar yang dapat diunggah dalam satu waktu',
|
||||
invalidImage: 'Gambar tidak valid',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Properti Jangkar',
|
||||
both: 'Pindah dan ukur bersama sel',
|
||||
position: 'Pindah tapi tidak ukur bersama sel',
|
||||
none: 'Jangan pindah atau ukur bersama sel',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Tempel sebagai gambar sel',
|
||||
pasteContent: 'Menempel gambar sel akan menimpa konten yang ada di sel, lanjutkan menempel',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'Rentang ini dilindungi, dan Anda tidak memiliki izin edit. Untuk mengedit, silakan hubungi pembuatnya.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Tampilan Gambar',
|
||||
'drawing-move-down': 'Pindahkan Gambar ke Bawah',
|
||||
'drawing-move-up': 'Pindahkan Gambar ke Atas',
|
||||
'drawing-move-left': 'Pindahkan Gambar ke Kiri',
|
||||
'drawing-move-right': 'Pindahkan Gambar ke Kanan',
|
||||
'drawing-delete': 'Hapus Gambar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/it-IT.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/it-IT.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Immagine',
|
||||
uploadLoading: {
|
||||
loading: 'Caricamento..., rimanente',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Immagine fluttuante',
|
||||
cell: 'Immagine cella',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Modifica immagine',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Salva immagini celle',
|
||||
menuLabel: 'Salva immagini celle',
|
||||
imageCount: 'Numero immagini',
|
||||
fileNameConfig: 'Nome file',
|
||||
useRowCol: 'Usa indirizzo cella (A1, B2...)',
|
||||
useColumnValue: 'Usa valore colonna',
|
||||
selectColumn: 'Seleziona colonna',
|
||||
cancel: 'Annulla',
|
||||
confirm: 'Salva',
|
||||
saving: 'Salvataggio...',
|
||||
error: 'Salvataggio immagini celle non riuscito',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Elimina',
|
||||
edit: 'Modifica',
|
||||
crop: 'Ritaglia',
|
||||
reset: 'Reimposta dimensione',
|
||||
flipH: 'Capovolgi orizzontalmente',
|
||||
flipV: 'Capovolgi verticalmente',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'La dimensione dell\'immagine supera il limite, il limite è {0}M',
|
||||
invalidImageType: 'Tipo di immagine non valido',
|
||||
exceedMaxCount: 'È possibile caricare solo {0} immagini alla volta',
|
||||
invalidImage: 'Immagine non valida',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Proprietà ancoraggio',
|
||||
both: 'Sposta e ridimensiona con le celle',
|
||||
position: 'Sposta ma non ridimensionare con le celle',
|
||||
none: 'Non spostare né ridimensionare con le celle',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Incolla come immagine cella',
|
||||
pasteContent: "L'incollaggio di un'immagine cella sovrascriverà il contenuto esistente della cella, continuare con l'incollaggio",
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: "L'intervallo è protetto e non hai l'autorizzazione di modifica. Per modificare, contatta il creatore.",
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Vista disegno',
|
||||
'drawing-move-down': 'Sposta disegno in basso',
|
||||
'drawing-move-up': 'Sposta disegno in alto',
|
||||
'drawing-move-left': 'Sposta disegno a sinistra',
|
||||
'drawing-move-right': 'Sposta disegno a destra',
|
||||
'drawing-delete': 'Elimina disegno',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/ja-JP.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/ja-JP.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: '画像',
|
||||
uploadLoading: {
|
||||
loading: '読み込み中...',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'セル上に配置',
|
||||
cell: 'セル内に配置',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: '画像の編集',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'セル画像を保存',
|
||||
menuLabel: 'セル画像を保存',
|
||||
imageCount: '画像数',
|
||||
fileNameConfig: 'ファイル名',
|
||||
useRowCol: 'セルアドレスを使用 (A1, B2...)',
|
||||
useColumnValue: '列の値を使用',
|
||||
selectColumn: '列を選択',
|
||||
cancel: 'キャンセル',
|
||||
confirm: '保存',
|
||||
saving: '保存中...',
|
||||
error: 'セル画像の保存に失敗しました',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '削除',
|
||||
edit: '編集',
|
||||
crop: 'トリミング',
|
||||
reset: '元のサイズに戻す',
|
||||
flipH: '左右反転',
|
||||
flipV: '上下反転',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '画像サイズが制限({0}MB)を超えています',
|
||||
invalidImageType: 'サポートされていない画像形式です',
|
||||
exceedMaxCount: '一度にアップロードできるのは {0} 個までです',
|
||||
invalidImage: '無効な画像です',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'プロパティ',
|
||||
both: 'セルに合わせて移動やサイズ変更をする',
|
||||
position: 'セルに合わせて移動するがサイズ変更はしない',
|
||||
none: 'セルに合わせて移動やサイズ変更をしない',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'セル内画像として貼り付け',
|
||||
pasteContent: 'セル内画像を貼り付けると、既存のセル内容が上書きされます。続行しますか?',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'この範囲は保護されており、編集権限がありません。編集するには作成者に連絡してください。',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '図形表示',
|
||||
'drawing-move-down': '図形を下へ移動',
|
||||
'drawing-move-up': '図形を上へ移動',
|
||||
'drawing-move-left': '図形を左へ移動',
|
||||
'drawing-move-right': '図形を右へ移動',
|
||||
'drawing-delete': '図形を削除',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/ko-KR.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/ko-KR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: '이미지',
|
||||
uploadLoading: {
|
||||
loading: '로딩중...',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: '플로팅 이미지',
|
||||
cell: '셀 내 이미지',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: '이미지 편집',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '셀 이미지 저장',
|
||||
menuLabel: '셀 이미지 저장',
|
||||
imageCount: '이미지 수',
|
||||
fileNameConfig: '파일 이름',
|
||||
useRowCol: '셀 주소 사용 (A1, B2...)',
|
||||
useColumnValue: '열 값 사용',
|
||||
selectColumn: '열 선택',
|
||||
cancel: '취소',
|
||||
confirm: '저장',
|
||||
saving: '저장 중...',
|
||||
error: '셀 이미지 저장 실패',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '삭제',
|
||||
edit: '편집',
|
||||
crop: '자르기',
|
||||
reset: '원래 크기',
|
||||
flipH: '가로 뒤집기',
|
||||
flipV: '세로 뒤집기',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '이미지 크기가 제한({0}MB)을 초과했습니다',
|
||||
invalidImageType: '지원하지 않는 이미지 형식입니다',
|
||||
exceedMaxCount: '한 번에 {0}개까지만 업로드할 수 있습니다',
|
||||
invalidImage: '유효하지 않은 이미지입니다',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: '고정 위치 설정',
|
||||
both: '셀과 함께 이동 및 크기 조절',
|
||||
position: '셀과 함께 이동하되 크기 변경 안 함',
|
||||
none: '셀과 함께 이동 및 크기 조절 안 함',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: '셀 이미지로 붙여넣기',
|
||||
pasteContent: '셀 이미지 붙여넣기는 기존 셀 내용을 덮어씁니다. 계속 진행하시겠습니까?',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: '해당 범위는 보호되어 있어 편집 권한이 없습니다. 편집하려면 작성자에게 문의하세요.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '그리기 보기',
|
||||
'drawing-move-down': '그리기 아래로 이동',
|
||||
'drawing-move-up': '그리기 위로 이동',
|
||||
'drawing-move-left': '그리기 왼쪽으로 이동',
|
||||
'drawing-move-right': '그리기 오른쪽으로 이동',
|
||||
'drawing-delete': '그리기 삭제',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/pl-PL.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/pl-PL.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Obraz',
|
||||
uploadLoading: {
|
||||
loading: 'Ładowanie..., pozostało',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Obraz swobodny',
|
||||
cell: 'Obraz w komórce',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Edytuj obraz',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Zapisz obrazy w komórkach',
|
||||
menuLabel: 'Zapisz obrazy w komórkach',
|
||||
imageCount: 'Liczba obrazów',
|
||||
fileNameConfig: 'Nazwa pliku',
|
||||
useRowCol: 'Użyj adresu komórki (A1, B2...)',
|
||||
useColumnValue: 'Użyj wartości kolumny',
|
||||
selectColumn: 'Wybierz kolumnę',
|
||||
cancel: 'Anuluj',
|
||||
confirm: 'Zapisz',
|
||||
saving: 'Zapisywanie...',
|
||||
error: 'Nie udało się zapisać obrazów w komórkach',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Usuń',
|
||||
edit: 'Edytuj',
|
||||
crop: 'Przytnij',
|
||||
reset: 'Resetuj rozmiar',
|
||||
flipH: 'Przerzuć w poziomie',
|
||||
flipV: 'Przerzuć w pionie',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Rozmiar obrazu przekracza limit, limit wynosi {0}M',
|
||||
invalidImageType: 'Nieprawidłowy typ obrazu',
|
||||
exceedMaxCount: 'Jednocześnie można przesłać tylko {0} obrazów',
|
||||
invalidImage: 'Nieprawidłowy obraz',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Właściwości kotwicy',
|
||||
both: 'Przesuwaj i zmieniaj rozmiar razem z komórkami',
|
||||
position: 'Przesuwaj, ale nie zmieniaj rozmiaru razem z komórkami',
|
||||
none: 'Nie przesuwaj ani nie zmieniaj rozmiaru razem z komórkami',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Wklej jako obraz w komórce',
|
||||
pasteContent: 'Wklejenie obrazu w komórce spowoduje zastąpienie istniejącej zawartości komórki, kontynuować wklejanie',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'Zakres jest chroniony i nie masz uprawnień do edycji. Aby edytować, skontaktuj się z twórcą.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Widok rysunku',
|
||||
'drawing-move-down': 'Przesuń rysunek w dół',
|
||||
'drawing-move-up': 'Przesuń rysunek w górę',
|
||||
'drawing-move-left': 'Przesuń rysunek w lewo',
|
||||
'drawing-move-right': 'Przesuń rysunek w prawo',
|
||||
'drawing-delete': 'Usuń rysunek',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/pt-BR.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/pt-BR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Imagem',
|
||||
uploadLoading: {
|
||||
loading: 'Carregando..., restante',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Imagem flutuante',
|
||||
cell: 'Imagem de célula',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Editar imagem',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Salvar imagens de célula',
|
||||
menuLabel: 'Salvar imagens de célula',
|
||||
imageCount: 'Contagem de imagens',
|
||||
fileNameConfig: 'Nome do arquivo',
|
||||
useRowCol: 'Usar endereço da célula (A1, B2...)',
|
||||
useColumnValue: 'Usar valor da coluna',
|
||||
selectColumn: 'Selecionar coluna',
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Salvar',
|
||||
saving: 'Salvando...',
|
||||
error: 'Falha ao salvar imagens de célula',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Excluir',
|
||||
edit: 'Editar',
|
||||
crop: 'Cortar',
|
||||
reset: 'Redefinir tamanho',
|
||||
flipH: 'Inverter horizontalmente',
|
||||
flipV: 'Inverter verticalmente',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'O tamanho da imagem excede o limite, o limite é {0}M',
|
||||
invalidImageType: 'Tipo de imagem inválido',
|
||||
exceedMaxCount: 'Apenas {0} imagens podem ser enviadas de cada vez',
|
||||
invalidImage: 'Imagem inválida',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Propriedades da âncora',
|
||||
both: 'Mover e dimensionar com as células',
|
||||
position: 'Mover, mas não dimensionar com as células',
|
||||
none: 'Não mover nem dimensionar com as células',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Colar como imagem de célula',
|
||||
pasteContent: 'Colar uma imagem de célula substituirá o conteúdo existente da célula, continuar colando',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'O intervalo está protegido e você não tem permissão de edição. Para editar, entre em contato com o criador.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Visualização do desenho',
|
||||
'drawing-move-down': 'Mover desenho para baixo',
|
||||
'drawing-move-up': 'Mover desenho para cima',
|
||||
'drawing-move-left': 'Mover desenho para esquerda',
|
||||
'drawing-move-right': 'Mover desenho para direita',
|
||||
'drawing-delete': 'Excluir desenho',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/ru-RU.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/ru-RU.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Изображение',
|
||||
uploadLoading: {
|
||||
loading: 'Загрузка...',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Плавающее изображение',
|
||||
cell: 'Изображение в ячейке',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Редактировать изображение',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Сохранить изображения ячеек',
|
||||
menuLabel: 'Сохранить изображения ячеек',
|
||||
imageCount: 'Количество изображений',
|
||||
fileNameConfig: 'Имя файла',
|
||||
useRowCol: 'Использовать адрес ячейки (A1, B2...)',
|
||||
useColumnValue: 'Использовать значение столбца',
|
||||
selectColumn: 'Выбрать столбец',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Сохранить',
|
||||
saving: 'Сохранение...',
|
||||
error: 'Не удалось сохранить изображения ячеек',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Удалить',
|
||||
edit: 'Редактировать',
|
||||
crop: 'Обрезать',
|
||||
reset: 'Сбросить размер',
|
||||
flipH: 'Отразить по горизонтали',
|
||||
flipV: 'Отразить по вертикали',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Размер изображения превышает лимит, лимит составляет {0}М',
|
||||
invalidImageType: 'Недопустимый тип изображения',
|
||||
exceedMaxCount: 'За один раз можно загрузить только {0} изображений',
|
||||
invalidImage: 'Недопустимое изображение',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Свойства привязки',
|
||||
both: 'Перемещать и изменять размер с ячейками',
|
||||
position: 'Перемещать, но не изменять размер с ячейками',
|
||||
none: 'Не перемещать и не изменять размер с ячейками',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Вставить как изображение ячейки',
|
||||
pasteContent: 'Вставка изображения ячейки перезапишет существующее содержимое ячейки, продолжить вставку',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'Диапазон защищен, и у вас нет разрешения на редактирование. Для редактирования свяжитесь с создателем.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Вид изображения',
|
||||
'drawing-move-down': 'Переместить изображение вниз',
|
||||
'drawing-move-up': 'Переместить изображение вверх',
|
||||
'drawing-move-left': 'Переместить изображение влево',
|
||||
'drawing-move-right': 'Переместить изображение вправо',
|
||||
'drawing-delete': 'Удалить изображение',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/sk-SK.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/sk-SK.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Obrázok',
|
||||
uploadLoading: {
|
||||
loading: 'Načítava sa..., zostáva',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Plávajúci obrázok',
|
||||
cell: 'Obrázok v bunke',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Upraviť obrázok',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Uložiť obrázky buniek',
|
||||
menuLabel: 'Uložiť obrázky buniek',
|
||||
imageCount: 'Počet obrázkov',
|
||||
fileNameConfig: 'Názov súboru',
|
||||
useRowCol: 'Použiť adresu bunky (A1, B2...)',
|
||||
useColumnValue: 'Použiť hodnotu stĺpca',
|
||||
selectColumn: 'Vybrať stĺpec',
|
||||
cancel: 'Zrušiť',
|
||||
confirm: 'Uložiť',
|
||||
saving: 'Ukladá sa...',
|
||||
error: 'Nepodarilo sa uložiť obrázky buniek',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Odstrániť',
|
||||
edit: 'Upraviť',
|
||||
crop: 'Orezať',
|
||||
reset: 'Obnoviť veľkosť',
|
||||
flipH: 'Prevrátiť vodorovne',
|
||||
flipV: 'Prevrátiť zvisle',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Veľkosť obrázka prekračuje limit, limit je {0}M',
|
||||
invalidImageType: 'Neplatný typ obrázka',
|
||||
exceedMaxCount: 'Naraz možno nahrať iba {0} obrázkov',
|
||||
invalidImage: 'Neplatný obrázok',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Vlastnosti ukotvenia',
|
||||
both: 'Presúvať a meniť veľkosť s bunkami',
|
||||
position: 'Presúvať, ale nemeniť veľkosť s bunkami',
|
||||
none: 'Nepresúvať ani nemeniť veľkosť s bunkami',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Prilepiť ako obrázok bunky',
|
||||
pasteContent: 'Prilepením obrázka bunky sa prepíše existujúci obsah bunky, pokračovať',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'Rozsah je chránený a nemáte oprávnenie na úpravy. Ak chcete upravovať, kontaktujte autora.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Zobrazenie kresby',
|
||||
'drawing-move-down': 'Posunúť kresbu nadol',
|
||||
'drawing-move-up': 'Posunúť kresbu nahor',
|
||||
'drawing-move-left': 'Posunúť kresbu doľava',
|
||||
'drawing-move-right': 'Posunúť kresbu doprava',
|
||||
'drawing-delete': 'Odstrániť kresbu',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/sheets-drawing-ui/src/locale/types.ts
Normal file
20
packages/sheets-drawing-ui/src/locale/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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 { LocaleLeafKeys } from '@univerjs/core';
|
||||
import type enUS from './en-US';
|
||||
|
||||
export type LocaleKey = LocaleLeafKeys<typeof enUS>;
|
||||
88
packages/sheets-drawing-ui/src/locale/vi-VN.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/vi-VN.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: 'Hình ảnh',
|
||||
uploadLoading: {
|
||||
loading: 'Đang tải lên, hiện còn lại',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: 'Hình ảnh nổi',
|
||||
cell: 'Hình ảnh trong ô',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: 'Chỉnh sửa hình ảnh',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: 'Lưu hình ảnh ô',
|
||||
menuLabel: 'Lưu hình ảnh ô',
|
||||
imageCount: 'Số lượng hình ảnh',
|
||||
fileNameConfig: 'Tên tệp',
|
||||
useRowCol: 'Sử dụng địa chỉ ô (A1, B2...)',
|
||||
useColumnValue: 'Sử dụng giá trị cột',
|
||||
selectColumn: 'Chọn cột',
|
||||
cancel: 'Hủy',
|
||||
confirm: 'Lưu',
|
||||
saving: 'Đang lưu...',
|
||||
error: 'Lưu hình ảnh ô thất bại',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Xóa',
|
||||
edit: 'Chỉnh sửa',
|
||||
crop: 'Cắt',
|
||||
reset: 'Đặt lại kích thước',
|
||||
flipH: 'Lật ngang',
|
||||
flipV: 'Lật dọc',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Kích thước hình ảnh vượt quá giới hạn, giới hạn là {0}M',
|
||||
invalidImageType: 'Loại hình ảnh không hợp lệ',
|
||||
exceedMaxCount: 'Chỉ có thể tải lên {0} hình ảnh một lần',
|
||||
invalidImage: 'Hình ảnh không hợp lệ',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: 'Thuộc tính neo',
|
||||
both: 'Di chuyển và thay đổi kích thước cùng với ô',
|
||||
position: 'Di chuyển nhưng không thay đổi kích thước với ô',
|
||||
none: 'Không di chuyển hoặc thay đổi kích thước với ô',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: 'Dán như hình ảnh ô',
|
||||
pasteContent: 'Dán hình ảnh ô sẽ ghi đè lên nội dung hiện có của ô, tiếp tục dán',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: 'Phạm vi này đã được bảo vệ, hiện không có quyền chỉnh sửa. Nếu cần chỉnh sửa, vui lòng liên hệ với người tạo.',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Chế độ xem bản vẽ',
|
||||
'drawing-move-down': 'Di chuyển hình vẽ xuống',
|
||||
'drawing-move-up': 'Di chuyển hình vẽ lên',
|
||||
'drawing-move-left': 'Di chuyển hình vẽ sang trái',
|
||||
'drawing-move-right': 'Di chuyển hình vẽ sang phải',
|
||||
'drawing-delete': 'Xóa hình vẽ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/zh-CN.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/zh-CN.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: '图片',
|
||||
uploadLoading: {
|
||||
loading: '正在上传,当前剩余',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: '浮动图片',
|
||||
cell: '单元格图片',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: '编辑图片',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '保存单元格图片',
|
||||
menuLabel: '保存单元格图片',
|
||||
imageCount: '图片数量',
|
||||
fileNameConfig: '文件名',
|
||||
useRowCol: '使用单元格地址 (A1, B2...)',
|
||||
useColumnValue: '使用某列的值',
|
||||
selectColumn: '选择列',
|
||||
cancel: '取消',
|
||||
confirm: '保存',
|
||||
saving: '保存中...',
|
||||
error: '保存单元格图片失败',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
crop: '裁剪',
|
||||
reset: '重置大小',
|
||||
flipH: '水平翻转',
|
||||
flipV: '垂直翻转',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '图片大小超过限制, 限制为{0}M',
|
||||
invalidImageType: '图片类型错误',
|
||||
exceedMaxCount: '图片只能一次上传{0}张',
|
||||
invalidImage: '无效图片',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: '锚点属性',
|
||||
both: '与单元格一起移动和调整大小',
|
||||
position: '移动但不调整大小与单元格',
|
||||
none: '不要移动或调整大小与单元格',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: '粘贴为单元格图片',
|
||||
pasteContent: '粘贴单元格图片将覆盖单元格中的现有内容,继续粘贴',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: '该范围已被保护,目前无编辑权限。如需编辑,请联系创建者。',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '绘图视图',
|
||||
'drawing-move-down': '下移绘图',
|
||||
'drawing-move-up': '上移绘图',
|
||||
'drawing-move-left': '左移绘图',
|
||||
'drawing-move-right': '右移绘图',
|
||||
'drawing-delete': '删除绘图',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/zh-HK.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/zh-HK.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: '圖片',
|
||||
uploadLoading: {
|
||||
loading: '正在上傳,目前剩餘',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: '浮動圖片',
|
||||
cell: '儲存格圖片',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: '編圖',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '儲存儲存格圖片',
|
||||
menuLabel: '儲存儲存格圖片',
|
||||
imageCount: '圖片數量',
|
||||
fileNameConfig: '檔案名稱',
|
||||
useRowCol: '使用儲存格地址 (A1, B2...)',
|
||||
useColumnValue: '使用某欄的值',
|
||||
selectColumn: '選擇欄',
|
||||
cancel: '取消',
|
||||
confirm: '儲存',
|
||||
saving: '儲存中...',
|
||||
error: '儲存儲存格圖片失敗',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '刪除',
|
||||
edit: '編輯',
|
||||
crop: '裁切',
|
||||
reset: '重置大小',
|
||||
flipH: '水平翻轉',
|
||||
flipV: '垂直翻轉',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '圖片大小超過限制, 限制為{0}M',
|
||||
invalidImageType: '圖片類型錯誤',
|
||||
exceedMaxCount: '圖片只能一次上傳{0}張',
|
||||
invalidImage: '無效圖片',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: '錨點屬性',
|
||||
both: '與儲存格一起移動和調整大小',
|
||||
position: '移動但不調整大小與儲存格',
|
||||
none: '不要移動或調整大小與儲存格',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: '貼上為儲存格圖片',
|
||||
pasteContent: '貼上儲存格圖片將覆蓋儲存格中的現有內容,繼續貼上',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: '該範圍已被保護,目前無編輯權限。如需編輯,請聯絡創建者。 ',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '繪圖檢視',
|
||||
'drawing-move-down': '下移繪圖',
|
||||
'drawing-move-up': '上移繪圖',
|
||||
'drawing-move-left': '左移繪圖',
|
||||
'drawing-move-right': '右移繪圖',
|
||||
'drawing-delete': '刪除繪圖',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
88
packages/sheets-drawing-ui/src/locale/zh-TW.ts
Normal file
88
packages/sheets-drawing-ui/src/locale/zh-TW.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'sheets-drawing-ui': {
|
||||
title: '圖片',
|
||||
uploadLoading: {
|
||||
loading: '正在上傳,目前剩餘',
|
||||
},
|
||||
|
||||
upload: {
|
||||
float: '浮動圖片',
|
||||
cell: '儲存格圖片',
|
||||
},
|
||||
|
||||
panel: {
|
||||
title: '編圖',
|
||||
},
|
||||
|
||||
save: {
|
||||
title: '儲存儲存格圖片',
|
||||
menuLabel: '儲存儲存格圖片',
|
||||
imageCount: '圖片數量',
|
||||
fileNameConfig: '檔案名稱',
|
||||
useRowCol: '使用儲存格地址 (A1, B2...)',
|
||||
useColumnValue: '使用某欄的值',
|
||||
selectColumn: '選擇欄',
|
||||
cancel: '取消',
|
||||
confirm: '儲存',
|
||||
saving: '儲存中...',
|
||||
error: '儲存儲存格圖片失敗',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '刪除',
|
||||
edit: '編輯',
|
||||
crop: '裁切',
|
||||
reset: '重置大小',
|
||||
flipH: '水平翻轉',
|
||||
flipV: '垂直翻轉',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '圖片大小超過限制, 限制為{0}M',
|
||||
invalidImageType: '圖片類型錯誤',
|
||||
exceedMaxCount: '圖片只能一次上傳{0}張',
|
||||
invalidImage: '無效圖片',
|
||||
},
|
||||
'drawing-anchor': {
|
||||
title: '錨點屬性',
|
||||
both: '與儲存格一起移動和調整大小',
|
||||
position: '移動但不調整大小與儲存格',
|
||||
none: '不要移動或調整大小與儲存格',
|
||||
},
|
||||
'cell-image': {
|
||||
pasteTitle: '貼上為儲存格圖片',
|
||||
pasteContent: '貼上儲存格圖片將覆蓋儲存格中的現有內容,繼續貼上',
|
||||
},
|
||||
permission: {
|
||||
dialog: {
|
||||
editErr: '該範圍已被保護,目前無編輯權限。如需編輯,請聯絡創建者。 ',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '繪圖檢視',
|
||||
'drawing-move-down': '下移繪圖',
|
||||
'drawing-move-up': '上移繪圖',
|
||||
'drawing-move-left': '左移繪圖',
|
||||
'drawing-move-right': '右移繪圖',
|
||||
'drawing-delete': '刪除繪圖',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 {
|
||||
ContextService,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
IImageIoService,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import { IDialogService, IMenuManagerService, IMessageService, MenuItemType } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DrawingPopupMenuController } from '../drawing-popup-menu.controller';
|
||||
|
||||
function createControllerHarness(initialDrawingType: DrawingTypeEnum) {
|
||||
const injector = new Injector();
|
||||
const createControl$ = new Subject<void>();
|
||||
const clearControl$ = new Subject<void>();
|
||||
const changing$ = new Subject<void>();
|
||||
const imageIoChange$ = new Subject<number>();
|
||||
const currentWorkbook$ = new Subject<never>();
|
||||
const disposedWorkbook$ = new Subject<never>();
|
||||
const dialogs$ = new Subject<never>();
|
||||
const drawingObject = { oKey: 'drawing-1' };
|
||||
const popupDisposable = {
|
||||
dispose: vi.fn(),
|
||||
canDispose: () => true,
|
||||
};
|
||||
let drawingType = initialDrawingType;
|
||||
let attachedPopup: { extraProps?: Record<string, unknown> } | undefined;
|
||||
const attachPopupToObject = vi.fn((_targetObject: unknown, popup: { extraProps?: Record<string, unknown> }) => {
|
||||
attachedPopup = popup;
|
||||
return popupDisposable;
|
||||
});
|
||||
|
||||
injector.add([LocaleService]);
|
||||
injector.add([IDialogService, {
|
||||
useValue: {
|
||||
open: () => toDisposable(() => undefined),
|
||||
close: () => undefined,
|
||||
closeAll: () => undefined,
|
||||
getDialogs$: () => dialogs$,
|
||||
},
|
||||
}]);
|
||||
injector.add([IDrawingManagerService, {
|
||||
useValue: {
|
||||
getDrawingOKey: () => ({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'sheet-1',
|
||||
drawingId: 'drawing-1',
|
||||
drawingType,
|
||||
}),
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([SheetCanvasPopManagerService, {
|
||||
useValue: {
|
||||
attachPopupToObject,
|
||||
getFeatureMenu: () => undefined,
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IRenderManagerService, {
|
||||
useValue: {
|
||||
has: () => true,
|
||||
getRenderUnitById: () => ({
|
||||
scene: {
|
||||
getAllObjectsByOrder: () => [],
|
||||
getTransformerByCreate: () => ({
|
||||
createControl$,
|
||||
clearControl$,
|
||||
changing$,
|
||||
getSelectedObjectMap: () => new Map([['drawing-1', drawingObject]]),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IUniverInstanceService, {
|
||||
useValue: {
|
||||
getCurrentTypeOfUnit$: () => currentWorkbook$,
|
||||
getTypeOfUnitDisposed$: () => disposedWorkbook$,
|
||||
getAllUnitsForType: () => [{ getUnitId: () => 'unit-1' }],
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IMessageService, { useValue: { show: () => toDisposable(() => {}) } as never }]);
|
||||
injector.add([IMenuManagerService, {
|
||||
useValue: {
|
||||
getFlatMenuByPositionKey: () => [{
|
||||
key: 'add-comment',
|
||||
order: 0,
|
||||
item: {
|
||||
id: 'sheet.operation.add-drawing-comment',
|
||||
type: MenuItemType.BUTTON,
|
||||
title: 'sheets-thread-comment-ui.menu.addComment',
|
||||
},
|
||||
}],
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IContextService, { useValue: new ContextService() }]);
|
||||
injector.add([IImageIoService, { useValue: { change$: imageIoChange$ } as never }]);
|
||||
injector.add([ICommandService, { useValue: { syncExecuteCommand: vi.fn() } as never }]);
|
||||
injector.add([DrawingPopupMenuController]);
|
||||
|
||||
return {
|
||||
attachPopupToObject,
|
||||
createControl$,
|
||||
drawingObject,
|
||||
getAttachedPopup: () => attachedPopup,
|
||||
injector,
|
||||
popupDisposable,
|
||||
setDrawingType: (nextDrawingType: DrawingTypeEnum) => {
|
||||
drawingType = nextDrawingType;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('DrawingPopupMenuController', () => {
|
||||
it.each([
|
||||
{ drawingType: DrawingTypeEnum.DRAWING_IMAGE, label: 'image' },
|
||||
{ drawingType: DrawingTypeEnum.DRAWING_DOM, label: 'Float DOM' },
|
||||
])('constrains the $label popup to the canvas and places it on the left in RTL', ({ drawingType }) => {
|
||||
const { attachPopupToObject, createControl$, drawingObject, getAttachedPopup, injector } = createControllerHarness(drawingType);
|
||||
injector.get(LocaleService).setDirection('rtl');
|
||||
const controller = injector.get(DrawingPopupMenuController);
|
||||
createControl$.next();
|
||||
|
||||
expect(attachPopupToObject).toHaveBeenCalledWith(
|
||||
drawingObject,
|
||||
expect.objectContaining({
|
||||
constrainToCanvas: true,
|
||||
direction: 'left',
|
||||
})
|
||||
);
|
||||
expect(getAttachedPopup()?.extraProps?.menuItems).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
commandId: 'sheet.operation.add-drawing-comment',
|
||||
label: 'sheets-thread-comment-ui.menu.addComment',
|
||||
}),
|
||||
]));
|
||||
|
||||
controller.dispose();
|
||||
injector.dispose();
|
||||
});
|
||||
|
||||
it('removes a chart popup when focus moves to a shape', () => {
|
||||
const {
|
||||
attachPopupToObject,
|
||||
createControl$,
|
||||
injector,
|
||||
popupDisposable,
|
||||
setDrawingType,
|
||||
} = createControllerHarness(DrawingTypeEnum.DRAWING_CHART);
|
||||
const controller = injector.get(DrawingPopupMenuController);
|
||||
createControl$.next();
|
||||
|
||||
setDrawingType(DrawingTypeEnum.DRAWING_SHAPE);
|
||||
createControl$.next();
|
||||
|
||||
expect(popupDisposable.dispose).toHaveBeenCalledOnce();
|
||||
expect(attachPopupToObject).toHaveBeenCalledOnce();
|
||||
|
||||
controller.dispose();
|
||||
injector.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 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 { IDisposable, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { ImageIoService } from '@univerjs/drawing';
|
||||
import type { BaseObject, Scene } from '@univerjs/engine-render';
|
||||
import type { ISheetFloatDom } from '@univerjs/sheets-drawing';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
DrawingTypeEnum,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
IImageIoService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
RxDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IDrawingManagerService, SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import {
|
||||
COMPONENT_IMAGE_POPUP_MENU,
|
||||
ImageCropperObject,
|
||||
ImageResetSizeOperation,
|
||||
OpenImageCropOperation,
|
||||
} from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { RemoveSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import {
|
||||
FloatingObjectToolbarPosition,
|
||||
IMenuManagerService,
|
||||
IMessageService,
|
||||
MenuItemType,
|
||||
} from '@univerjs/ui';
|
||||
import { FlipSheetDrawingCommand } from '../commands/commands/flip-drawings.command';
|
||||
import { EditSheetDrawingOperation } from '../commands/operations/edit-sheet-drawing.operation';
|
||||
|
||||
export class DrawingPopupMenuController extends RxDisposable {
|
||||
private _initImagePopupMenu = new Set<string>();
|
||||
|
||||
constructor(
|
||||
@Inject(LocaleService) protected readonly _localeService: LocaleService,
|
||||
@IDrawingManagerService protected readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(SheetCanvasPopManagerService) protected readonly _canvasPopManagerService: SheetCanvasPopManagerService,
|
||||
@IRenderManagerService protected readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IMessageService protected readonly _messageService: IMessageService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IContextService protected readonly _contextService: IContextService,
|
||||
@IImageIoService private readonly _ioService: ImageIoService,
|
||||
@ICommandService protected readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => this._create(workbook));
|
||||
this._univerInstanceService.getTypeOfUnitDisposed$<Workbook>(UniverInstanceType.UNIVER_SHEET).subscribe((workbook) => this._dispose(workbook));
|
||||
this._univerInstanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET).forEach((workbook) => this._create(workbook));
|
||||
|
||||
this._setupLoadingStatus();
|
||||
}
|
||||
|
||||
private _setupLoadingStatus() {
|
||||
const MESSAGE_ID = 'image-upload-loading';
|
||||
|
||||
let messageDisposable: IDisposable | undefined;
|
||||
this.disposeWithMe(this._ioService.change$.subscribe((status) => {
|
||||
if (status > 0 && !messageDisposable) {
|
||||
messageDisposable = this._messageService.show({
|
||||
id: MESSAGE_ID,
|
||||
type: MessageType.Loading,
|
||||
content: `${this._localeService.t<LocaleKey>('sheets-drawing-ui.uploadLoading.loading')}: ${status}`,
|
||||
duration: 0,
|
||||
});
|
||||
} else if (status === 0) {
|
||||
messageDisposable?.dispose();
|
||||
messageDisposable = undefined;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _dispose(workbook: Workbook) {
|
||||
super.dispose();
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
this._renderManagerService.removeRender(unitId);
|
||||
this._initImagePopupMenu.delete(unitId);
|
||||
}
|
||||
|
||||
private _create(workbook: Nullable<Workbook>) {
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
if (this._renderManagerService.has(unitId) && !this._initImagePopupMenu.has(unitId)) {
|
||||
this._popupMenuListener(unitId);
|
||||
this._initImagePopupMenu.add(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _hasCropObject(scene: Scene) {
|
||||
const objects = scene.getAllObjectsByOrder();
|
||||
|
||||
for (const object of objects) {
|
||||
if (object instanceof ImageCropperObject) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _popupMenuListener(unitId: string) {
|
||||
const scene = this._renderManagerService.getRenderUnitById(unitId)?.scene;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
if (!transformer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let singletonPopupDisposer: IDisposable | undefined;
|
||||
this.disposeWithMe(transformer.createControl$.subscribe(() => {
|
||||
singletonPopupDisposer?.dispose();
|
||||
singletonPopupDisposer = this._createPopup(scene, transformer);
|
||||
}));
|
||||
this.disposeWithMe(
|
||||
transformer.clearControl$.subscribe(() => {
|
||||
singletonPopupDisposer?.dispose();
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
this._commandService.syncExecuteCommand(SetDrawingSelectedOperation.id, []);
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._contextService.contextChanged$.subscribe((event) => {
|
||||
if (event[FOCUSING_COMMON_DRAWINGS] === false) {
|
||||
singletonPopupDisposer?.dispose();
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
transformer.changing$.subscribe(() => {
|
||||
singletonPopupDisposer?.dispose();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _createPopup(scene: Scene, transformer: ReturnType<Scene['getTransformerByCreate']>): IDisposable | undefined {
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, true);
|
||||
|
||||
if (this._hasCropObject(scene)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (selectedObjects.size > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingParam = this._drawingManagerService.getDrawingOKey(object.oKey);
|
||||
if (!drawingParam || drawingParam.drawingType === DrawingTypeEnum.DRAWING_SHAPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, drawingId, drawingType } = drawingParam;
|
||||
const data = (drawingParam as ISheetFloatDom).data as Record<string, unknown> | undefined;
|
||||
if (data && (data.disablePopup || (data.version === 1 && typeof data.embedId === 'string'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menus = this._canvasPopManagerService.getFeatureMenu(unitId, subUnitId, drawingId, drawingType);
|
||||
const menuItems = [
|
||||
...(menus || this._getImageMenuItems(unitId, subUnitId, drawingId, drawingType)),
|
||||
...this._getFloatingObjectMenuItems(),
|
||||
];
|
||||
|
||||
return this.disposeWithMe(this._canvasPopManagerService.attachPopupToObject(object, {
|
||||
componentKey: COMPONENT_IMAGE_POPUP_MENU,
|
||||
constrainToCanvas: true,
|
||||
direction: this._localeService.getDirection() === 'rtl' ? 'left' : 'horizontal',
|
||||
offset: [2, 0],
|
||||
extraProps: {
|
||||
menuItems,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private _getImageMenuItems(unitId: string, subUnitId: string, drawingId: string, drawingType: number) {
|
||||
return [
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.edit',
|
||||
index: 0,
|
||||
commandId: EditSheetDrawingOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.delete',
|
||||
index: 1,
|
||||
commandId: RemoveSheetDrawingCommand.id,
|
||||
commandParams: { unitId, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: false,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.crop',
|
||||
index: 2,
|
||||
commandId: OpenImageCropOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.flipH',
|
||||
index: 2,
|
||||
commandId: FlipSheetDrawingCommand.id,
|
||||
commandParams: { unitId, flipH: true, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.flipV',
|
||||
index: 2,
|
||||
commandId: FlipSheetDrawingCommand.id,
|
||||
commandParams: { unitId, flipV: true, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.reset',
|
||||
index: 3,
|
||||
commandId: ImageResetSizeOperation.id,
|
||||
commandParams: [{ unitId, subUnitId, drawingId }],
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloatingObjectMenuItems() {
|
||||
return this._menuManagerService
|
||||
.getFlatMenuByPositionKey(FloatingObjectToolbarPosition.SHEET)
|
||||
.flatMap(({ item }, index) => {
|
||||
if (!item || item.type !== MenuItemType.BUTTON || !item.title) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
label: item.title,
|
||||
index: 100 + index,
|
||||
commandId: item.commandId ?? item.id,
|
||||
commandParams: typeof item.params === 'function' ? item.params() : item.params,
|
||||
disable: false,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
63
packages/sheets-drawing-ui/src/menu/image.menu.ts
Normal file
63
packages/sheets-drawing-ui/src/menu/image.menu.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 { IAccessor } from '@univerjs/core';
|
||||
import type { IMenuItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
RangeProtectionPermissionEditPoint,
|
||||
WorkbookEditablePermission,
|
||||
WorksheetEditPermission,
|
||||
} from '@univerjs/sheets';
|
||||
import { getCurrentRangeDisable$ } from '@univerjs/sheets-ui';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { InsertCellImageCommand, InsertFloatImageCommand } from '../commands/commands/insert-image.command';
|
||||
|
||||
export const SHEETS_IMAGE_MENU_ID = 'sheet.menu.image';
|
||||
|
||||
export function ImageMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: SHEETS_IMAGE_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
icon: 'AddImageIcon',
|
||||
tooltip: 'sheets-drawing-ui.title',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
disabled$: getCurrentRangeDisable$(accessor, {
|
||||
workbookTypes: [WorkbookEditablePermission],
|
||||
worksheetTypes: [WorksheetEditPermission],
|
||||
rangeTypes: [RangeProtectionPermissionEditPoint],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadFloatImageMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertFloatImageCommand.id,
|
||||
title: 'sheets-drawing-ui.upload.float',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadCellImageMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertCellImageCommand.id,
|
||||
title: 'sheets-drawing-ui.upload.cell',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* 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 { IDisposable, IDrawingSearch, Nullable, Workbook } from '@univerjs/core';
|
||||
import type { ImageIoService } from '@univerjs/drawing';
|
||||
import type { BaseObject, Scene } from '@univerjs/engine-render';
|
||||
import type { ISheetFloatDom } from '@univerjs/sheets-drawing';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import {
|
||||
DrawingTypeEnum,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
IImageIoService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
RxDisposable,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IDrawingManagerService, SetDrawingSelectedOperation } from '@univerjs/drawing';
|
||||
import {
|
||||
COMPONENT_MOBILE_IMAGE_POPUP_MENU,
|
||||
ImageCropperObject,
|
||||
ImageResetSizeOperation,
|
||||
OpenImageCropOperation,
|
||||
} from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { RemoveSheetDrawingCommand } from '@univerjs/sheets-drawing';
|
||||
import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui';
|
||||
import {
|
||||
FloatingObjectToolbarPosition,
|
||||
IDialogService,
|
||||
ILayoutService,
|
||||
IMenuManagerService,
|
||||
IMessageService,
|
||||
MenuItemType,
|
||||
} from '@univerjs/ui';
|
||||
import { FlipSheetDrawingCommand } from '../commands/commands/flip-drawings.command';
|
||||
import { EditSheetDrawingOperation } from '../commands/operations/edit-sheet-drawing.operation';
|
||||
|
||||
const MOBILE_IMAGE_ACTIONS_DIALOG_ID = 'sheet-mobile-image-actions';
|
||||
const MOBILE_DRAWING_LONG_PRESS_DURATION = 500;
|
||||
|
||||
export class MobileDrawingPopupMenuController extends RxDisposable {
|
||||
private readonly _initImagePopupMenu = new Set<string>();
|
||||
|
||||
constructor(
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(SheetCanvasPopManagerService)
|
||||
private readonly _canvasPopManagerService: SheetCanvasPopManagerService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IImageIoService private readonly _ioService: ImageIoService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
@ILayoutService private readonly _layoutService: ILayoutService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService
|
||||
.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET)
|
||||
.subscribe((workbook) => this._create(workbook))
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService
|
||||
.getTypeOfUnitDisposed$<Workbook>(UniverInstanceType.UNIVER_SHEET)
|
||||
.subscribe((workbook) => this._dispose(workbook))
|
||||
);
|
||||
this._univerInstanceService
|
||||
.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET)
|
||||
.forEach((workbook) => this._create(workbook));
|
||||
|
||||
this._setupLoadingStatus();
|
||||
}
|
||||
|
||||
private _setupLoadingStatus(): void {
|
||||
const messageId = 'image-upload-loading';
|
||||
let messageDisposable: IDisposable | undefined;
|
||||
|
||||
this.disposeWithMe(this._ioService.change$.subscribe((status) => {
|
||||
if (status > 0 && !messageDisposable) {
|
||||
messageDisposable = this._messageService.show({
|
||||
id: messageId,
|
||||
type: MessageType.Loading,
|
||||
content: `${this._localeService.t<LocaleKey>('sheets-drawing-ui.uploadLoading.loading')}: ${status}`,
|
||||
duration: 0,
|
||||
});
|
||||
} else if (status === 0) {
|
||||
messageDisposable?.dispose();
|
||||
messageDisposable = undefined;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _dispose(workbook: Workbook): void {
|
||||
super.dispose();
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
this._renderManagerService.removeRender(unitId);
|
||||
this._initImagePopupMenu.delete(unitId);
|
||||
}
|
||||
|
||||
private _create(workbook: Nullable<Workbook>): void {
|
||||
if (!workbook) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = workbook.getUnitId();
|
||||
if (this._renderManagerService.has(unitId) && !this._initImagePopupMenu.has(unitId)) {
|
||||
this._popupMenuListener(unitId);
|
||||
this._initImagePopupMenu.add(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _hasCropObject(scene: Scene): boolean {
|
||||
for (const object of scene.getAllObjectsByOrder()) {
|
||||
if (object instanceof ImageCropperObject) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _openDrawingPopup(scene: Scene, object: BaseObject, closeCurrentPopup: () => void): Nullable<IDisposable> {
|
||||
if (this._hasCropObject(scene)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawingParam = this._drawingManagerService.getDrawingOKey(object.oKey);
|
||||
if (!drawingParam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId, drawingId, drawingType } = drawingParam;
|
||||
const data = (drawingParam as ISheetFloatDom).data as Record<string, unknown> | undefined;
|
||||
if (data && (data.disablePopup || (data.version === 1 && typeof data.embedId === 'string'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menus = this._canvasPopManagerService.getFeatureMenu(unitId, subUnitId, drawingId, drawingType);
|
||||
const menuItems = [
|
||||
...(menus || this._getImageMenuItems(unitId, subUnitId, drawingId, drawingType)),
|
||||
...this._getFloatingObjectMenuItems(),
|
||||
];
|
||||
const titleKey = this._canvasPopManagerService.getFeatureMenuTitle(drawingType) ??
|
||||
(drawingType === DrawingTypeEnum.DRAWING_IMAGE
|
||||
? 'sheets-drawing-ui.title'
|
||||
: 'sheets-drawing-ui.image-popup.edit');
|
||||
closeCurrentPopup();
|
||||
this._dialogService.open({
|
||||
id: MOBILE_IMAGE_ACTIONS_DIALOG_ID,
|
||||
title: { title: this._localeService.t(titleKey) },
|
||||
children: {
|
||||
label: {
|
||||
name: COMPONENT_MOBILE_IMAGE_POPUP_MENU,
|
||||
props: {
|
||||
popup: {
|
||||
extraProps: { menuItems, dialogId: MOBILE_IMAGE_ACTIONS_DIALOG_ID },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return toDisposable(() => this._dialogService.close(MOBILE_IMAGE_ACTIONS_DIALOG_ID));
|
||||
}
|
||||
|
||||
private _trackActivePointer(clearLongPressTimer: () => void) {
|
||||
const contentElement = this._layoutService.getContentElement();
|
||||
let activePointerId: number | undefined;
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (event.isPrimary && event.button === 0) {
|
||||
activePointerId = event.pointerId;
|
||||
}
|
||||
};
|
||||
const handlePointerEnd = (event: PointerEvent) => {
|
||||
if (event.pointerId === activePointerId) {
|
||||
activePointerId = undefined;
|
||||
clearLongPressTimer();
|
||||
}
|
||||
};
|
||||
|
||||
contentElement?.addEventListener('pointerdown', handlePointerDown, true);
|
||||
contentElement?.addEventListener('pointerup', handlePointerEnd, true);
|
||||
contentElement?.addEventListener('pointercancel', handlePointerEnd, true);
|
||||
|
||||
return {
|
||||
getActivePointerId: () => activePointerId,
|
||||
disposable: toDisposable(() => {
|
||||
contentElement?.removeEventListener('pointerdown', handlePointerDown, true);
|
||||
contentElement?.removeEventListener('pointerup', handlePointerEnd, true);
|
||||
contentElement?.removeEventListener('pointercancel', handlePointerEnd, true);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private _getSelectedDrawingObject(
|
||||
transformer: { getSelectedObjectMap: () => Map<string, BaseObject> },
|
||||
drawing: IDrawingSearch
|
||||
): Nullable<BaseObject> {
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (selectedObjects.size !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedObject = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
const selectedDrawing = selectedObject
|
||||
? this._drawingManagerService.getDrawingOKey(selectedObject.oKey)
|
||||
: null;
|
||||
return selectedObject &&
|
||||
selectedDrawing?.unitId === drawing.unitId &&
|
||||
selectedDrawing.subUnitId === drawing.subUnitId &&
|
||||
selectedDrawing.drawingId === drawing.drawingId
|
||||
? selectedObject
|
||||
: null;
|
||||
}
|
||||
|
||||
private _popupMenuListener(unitId: string): void {
|
||||
const scene = this._renderManagerService.getRenderUnitById(unitId)?.scene;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
if (!transformer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let singletonPopupDisposer: IDisposable;
|
||||
let longPressTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const clearLongPressTimer = () => {
|
||||
if (longPressTimer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = undefined;
|
||||
};
|
||||
const pointerTracker = this._trackActivePointer(clearLongPressTimer);
|
||||
this.disposeWithMe(pointerTracker.disposable);
|
||||
this.disposeWithMe(toDisposable(() => {
|
||||
clearLongPressTimer();
|
||||
singletonPopupDisposer?.dispose();
|
||||
}));
|
||||
this.disposeWithMe(transformer.createControl$.subscribe(() => {
|
||||
clearLongPressTimer();
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, true);
|
||||
}));
|
||||
this.disposeWithMe(transformer.changeStart$.subscribe(({ target, objects }) => {
|
||||
clearLongPressTimer();
|
||||
singletonPopupDisposer?.dispose();
|
||||
if (pointerTracker.getActivePointerId() === undefined || !target || objects.size !== 1 || this._hasCropObject(scene)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingParam = this._drawingManagerService.getDrawingOKey(target.oKey);
|
||||
if (!drawingParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
longPressTimer = setTimeout(() => {
|
||||
longPressTimer = undefined;
|
||||
const selectedObject = this._getSelectedDrawingObject(transformer, drawingParam);
|
||||
if (!selectedObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popupDisposer = this._openDrawingPopup(
|
||||
scene,
|
||||
selectedObject,
|
||||
() => singletonPopupDisposer?.dispose()
|
||||
);
|
||||
if (popupDisposer) {
|
||||
singletonPopupDisposer = popupDisposer;
|
||||
}
|
||||
}, MOBILE_DRAWING_LONG_PRESS_DURATION);
|
||||
}));
|
||||
this.disposeWithMe(transformer.clearControl$.subscribe(() => {
|
||||
clearLongPressTimer();
|
||||
singletonPopupDisposer?.dispose();
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
this._commandService.syncExecuteCommand(SetDrawingSelectedOperation.id, []);
|
||||
}));
|
||||
this.disposeWithMe(this._contextService.contextChanged$.subscribe((event) => {
|
||||
if (event[FOCUSING_COMMON_DRAWINGS] === false) {
|
||||
singletonPopupDisposer?.dispose();
|
||||
}
|
||||
}));
|
||||
this.disposeWithMe(transformer.changing$.subscribe(() => {
|
||||
clearLongPressTimer();
|
||||
singletonPopupDisposer?.dispose();
|
||||
}));
|
||||
this.disposeWithMe(transformer.changeEnd$.subscribe(() => {
|
||||
clearLongPressTimer();
|
||||
}));
|
||||
}
|
||||
|
||||
private _getImageMenuItems(unitId: string, subUnitId: string, drawingId: string, drawingType: number) {
|
||||
return [
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.edit',
|
||||
index: 0,
|
||||
commandId: EditSheetDrawingOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.delete',
|
||||
index: 1,
|
||||
commandId: RemoveSheetDrawingCommand.id,
|
||||
commandParams: { unitId, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: false,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.crop',
|
||||
index: 2,
|
||||
commandId: OpenImageCropOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.flipH',
|
||||
index: 2,
|
||||
commandId: FlipSheetDrawingCommand.id,
|
||||
commandParams: { unitId, flipH: true, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.flipV',
|
||||
index: 2,
|
||||
commandId: FlipSheetDrawingCommand.id,
|
||||
commandParams: { unitId, flipV: true, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
{
|
||||
label: 'sheets-drawing-ui.image-popup.reset',
|
||||
index: 3,
|
||||
commandId: ImageResetSizeOperation.id,
|
||||
commandParams: [{ unitId, subUnitId, drawingId }],
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloatingObjectMenuItems() {
|
||||
return this._menuManagerService
|
||||
.getFlatMenuByPositionKey(FloatingObjectToolbarPosition.SHEET)
|
||||
.flatMap(({ item }, index) => {
|
||||
if (!item || item.type !== MenuItemType.BUTTON || !item.title) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
label: item.title,
|
||||
index: 100 + index,
|
||||
commandId: item.commandId ?? item.id,
|
||||
commandParams: typeof item.params === 'function' ? item.params() : item.params,
|
||||
disable: false,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
106
packages/sheets-drawing-ui/src/menu/save-images.menu.ts
Normal file
106
packages/sheets-drawing-ui/src/menu/save-images.menu.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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, IRange, Workbook } from '@univerjs/core';
|
||||
import type { IMenuItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { isCellImage, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { combineLatest, map, of, switchMap } from 'rxjs';
|
||||
import { SaveCellImagesCommand } from '../commands/commands/save-cell-images.command';
|
||||
|
||||
export const SAVE_CELL_IMAGES_MENU_ID = 'sheet.menu.save-cell-images';
|
||||
|
||||
/**
|
||||
* Check if selection range has any images
|
||||
*/
|
||||
function selectionHasImages(
|
||||
workbook: Workbook,
|
||||
selection: IRange
|
||||
): boolean {
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return false;
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const { startRow, endRow, startColumn, endColumn } = selection;
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
if (isCellImage(cell?.p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if File System Access API is supported
|
||||
*/
|
||||
function isFileSystemAccessSupported(): boolean {
|
||||
return 'showDirectoryPicker' in window;
|
||||
}
|
||||
|
||||
export function SaveCellImagesMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selectionService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
// Hide menu if File System Access API is not supported or no images in selection
|
||||
const hidden$ = combineLatest([
|
||||
getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SHEET),
|
||||
univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET).pipe(
|
||||
switchMap((workbook) => {
|
||||
if (!workbook) return of(true);
|
||||
|
||||
return selectionService.selectionMoveEnd$.pipe(
|
||||
map(() => {
|
||||
// Hide if File System Access API is not supported
|
||||
if (!isFileSystemAccessSupported()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selections = selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any selection has images
|
||||
for (const selection of selections) {
|
||||
if (selectionHasImages(workbook, selection.range)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
})
|
||||
),
|
||||
]).pipe(
|
||||
map(([hidden, noImages]) => hidden || noImages)
|
||||
);
|
||||
|
||||
return {
|
||||
id: SaveCellImagesCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'DownloadImageIcon',
|
||||
title: 'sheets-drawing-ui.save.menuLabel',
|
||||
hidden$,
|
||||
};
|
||||
}
|
||||
64
packages/sheets-drawing-ui/src/menu/schema.ts
Normal file
64
packages/sheets-drawing-ui/src/menu/schema.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 { MenuSchemaType } from '@univerjs/ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { InsertCellImageCommand, InsertFloatImageCommand } from '../commands/commands/insert-image.command';
|
||||
import { SaveCellImagesCommand } from '../commands/commands/save-cell-images.command';
|
||||
import { ImageMenuFactory, SHEETS_IMAGE_MENU_ID, UploadCellImageMenuFactory, UploadFloatImageMenuFactory } from './image.menu';
|
||||
import { SaveCellImagesMenuFactory } from './save-images.menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonInsertGroup.MEDIA]: {
|
||||
[SHEETS_IMAGE_MENU_ID]: {
|
||||
order: 0,
|
||||
gridLayout: { row: 1, column: 1, rowSpan: 2, showLabel: true },
|
||||
menuItemFactory: ImageMenuFactory,
|
||||
[InsertFloatImageCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: UploadFloatImageMenuFactory,
|
||||
},
|
||||
[InsertCellImageCommand.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: UploadCellImageMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[SaveCellImagesCommand.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: SaveCellImagesMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.COL_HEADER]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[SaveCellImagesCommand.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: SaveCellImagesMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.ROW_HEADER]: {
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
[SaveCellImagesCommand.id]: {
|
||||
order: 10,
|
||||
menuItemFactory: SaveCellImagesMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
152
packages/sheets-drawing-ui/src/mobile-plugin.ts
Normal file
152
packages/sheets-drawing-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsDrawingUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
mergeOverrideWithDependencies,
|
||||
Plugin,
|
||||
registerDependencies,
|
||||
touchDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverDrawingMobileUIPlugin } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing';
|
||||
import { UniverSheetsMobileUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { UniverMobileUIPlugin } from '@univerjs/ui';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_DRAWING_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { DrawingContextMenuController } from './controllers/drawing-context-menu.controller';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { SheetCellImageHoverRenderController } from './controllers/render-controllers/sheet-celll-image-hover.render-controller';
|
||||
import { SheetsDrawingRenderController } from './controllers/render-controllers/sheet-drawing.render-controller';
|
||||
import { SheetCellImageAutofillController } from './controllers/sheet-cell-image-autofill.controller';
|
||||
import { SheetCellImageCopyPasteController } from './controllers/sheet-cell-image-copy-paste.controller';
|
||||
import { SheetCellImageController } from './controllers/sheet-cell-image.controller';
|
||||
import { SheetDrawingActiveRenderController } from './controllers/sheet-drawing-active-render.controller';
|
||||
import { SheetsDrawingCopyPasteController } from './controllers/sheet-drawing-copy-paste.controller';
|
||||
import { SheetsDrawingGroupCopyPasteController } from './controllers/sheet-drawing-group-copy-paste.controller';
|
||||
import { SheetDrawingPermissionController } from './controllers/sheet-drawing-permission.controller';
|
||||
import { SheetDrawingPrintingController } from './controllers/sheet-drawing-printing.controller';
|
||||
import { SheetDrawingUpdateController } from './controllers/sheet-drawing-update.controller';
|
||||
import { SheetDrawingUIController } from './controllers/ui.controller';
|
||||
import { touchSheetsDrawingFloatingHostCapabilityWhenReady } from './embed/floating-host';
|
||||
import { MobileDrawingPopupMenuController } from './menu/mobile-drawing-popup-menu.controller';
|
||||
import { BatchSaveImagesService, IBatchSaveImagesService } from './services/batch-save-images.service';
|
||||
import { SheetCanvasFloatDomManagerService } from './services/canvas-float-dom-manager.service';
|
||||
import { DrawingContextMenuService, IDrawingContextMenuService } from './services/drawing-context-menu.service';
|
||||
import { SheetDrawingHitTestService } from './services/sheet-drawing-hit-test.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsDrawingPlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverSheetsDrawingPlugin,
|
||||
UniverDrawingMobileUIPlugin,
|
||||
UniverSheetsMobileUIPlugin,
|
||||
UniverMobileUIPlugin
|
||||
)
|
||||
export class UniverSheetsDrawingMobileUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SHEET_IMAGE_UI_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsDrawingUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { menu, ...rest } = merge({}, defaultPluginConfig, this._config);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SHEETS_DRAWING_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies = [
|
||||
[MobileComponentsController],
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
[SheetDrawingHitTestService],
|
||||
[SheetDrawingUIController],
|
||||
[MobileDrawingPopupMenuController],
|
||||
[SheetDrawingPrintingController],
|
||||
[SheetDrawingPermissionController],
|
||||
[SheetsDrawingCopyPasteController],
|
||||
[SheetsDrawingGroupCopyPasteController],
|
||||
[SheetCellImageController],
|
||||
[SheetCellImageAutofillController],
|
||||
[SheetCellImageCopyPasteController],
|
||||
[IBatchSaveImagesService, { useClass: BatchSaveImagesService }],
|
||||
[IDrawingContextMenuService, { useClass: DrawingContextMenuService }],
|
||||
[DrawingContextMenuController],
|
||||
] as Dependency[];
|
||||
|
||||
registerDependencies(this._injector, mergeOverrideWithDependencies(dependencies, this._config.overrides));
|
||||
touchSheetsDrawingFloatingHostCapabilityWhenReady(this._injector);
|
||||
this._injector.get(MobileComponentsController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsDrawingCopyPasteController],
|
||||
[SheetCellImageCopyPasteController],
|
||||
[SheetsDrawingGroupCopyPasteController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._registerRenderModules();
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[SheetDrawingPermissionController],
|
||||
[SheetDrawingPrintingController],
|
||||
[SheetDrawingUIController],
|
||||
[SheetCellImageController],
|
||||
[SheetCellImageAutofillController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onSteady(): void {
|
||||
this._injector.get(MobileDrawingPopupMenuController);
|
||||
this._injector.get(DrawingContextMenuController);
|
||||
}
|
||||
|
||||
private _registerRenderModules(): void {
|
||||
([
|
||||
[SheetDrawingUpdateController],
|
||||
[SheetDrawingActiveRenderController],
|
||||
[SheetsDrawingRenderController],
|
||||
[SheetCellImageHoverRenderController],
|
||||
] as Dependency[]).forEach((module) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, module));
|
||||
});
|
||||
}
|
||||
}
|
||||
153
packages/sheets-drawing-ui/src/plugin.ts
Normal file
153
packages/sheets-drawing-ui/src/plugin.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 { IUniverSheetsDrawingUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
merge,
|
||||
Plugin,
|
||||
registerDependencies,
|
||||
touchDependencies,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverDrawingUIPlugin } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SHEETS_DRAWING_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { DrawingContextMenuController } from './controllers/drawing-context-menu.controller';
|
||||
import { SheetCellImageHoverRenderController } from './controllers/render-controllers/sheet-celll-image-hover.render-controller';
|
||||
import { SheetsDrawingRenderController } from './controllers/render-controllers/sheet-drawing.render-controller';
|
||||
import { SheetCellImageAutofillController } from './controllers/sheet-cell-image-autofill.controller';
|
||||
import { SheetCellImageCopyPasteController } from './controllers/sheet-cell-image-copy-paste.controller';
|
||||
import { SheetCellImageController } from './controllers/sheet-cell-image.controller';
|
||||
import { SheetDrawingActiveRenderController } from './controllers/sheet-drawing-active-render.controller';
|
||||
import { SheetsDrawingCopyPasteController } from './controllers/sheet-drawing-copy-paste.controller';
|
||||
import { SheetsDrawingGroupCopyPasteController } from './controllers/sheet-drawing-group-copy-paste.controller';
|
||||
import { SheetDrawingPermissionController } from './controllers/sheet-drawing-permission.controller';
|
||||
import { SheetDrawingPrintingController } from './controllers/sheet-drawing-printing.controller';
|
||||
import { SheetDrawingUpdateController } from './controllers/sheet-drawing-update.controller';
|
||||
import { SheetDrawingUIController } from './controllers/ui.controller';
|
||||
import { touchSheetsDrawingFloatingHostCapabilityWhenReady } from './embed/floating-host';
|
||||
import { DrawingPopupMenuController } from './menu/drawing-popup-menu.controller';
|
||||
import { BatchSaveImagesService, IBatchSaveImagesService } from './services/batch-save-images.service';
|
||||
import { SheetCanvasFloatDomManagerService } from './services/canvas-float-dom-manager.service';
|
||||
import { DrawingContextMenuService, IDrawingContextMenuService } from './services/drawing-context-menu.service';
|
||||
import { SheetDrawingHitTestService } from './services/sheet-drawing-hit-test.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsDrawingPlugin,
|
||||
UniverSheetsPlugin,
|
||||
UniverSheetsDrawingPlugin,
|
||||
UniverDrawingUIPlugin,
|
||||
UniverSheetsUIPlugin
|
||||
)
|
||||
export class UniverSheetsDrawingUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
static override pluginName = 'SHEET_IMAGE_UI_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsDrawingUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SHEETS_DRAWING_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
registerDependencies(this._injector, [
|
||||
[ComponentsController],
|
||||
[SheetCanvasFloatDomManagerService],
|
||||
[SheetDrawingHitTestService],
|
||||
[SheetDrawingUIController],
|
||||
[DrawingPopupMenuController],
|
||||
[SheetDrawingPrintingController],
|
||||
[SheetDrawingPermissionController],
|
||||
[SheetsDrawingCopyPasteController],
|
||||
[SheetsDrawingGroupCopyPasteController],
|
||||
[SheetCellImageController],
|
||||
[SheetCellImageAutofillController],
|
||||
[SheetCellImageCopyPasteController],
|
||||
[IBatchSaveImagesService, { useClass: BatchSaveImagesService }],
|
||||
[IDrawingContextMenuService, { useClass: DrawingContextMenuService }],
|
||||
[DrawingContextMenuController],
|
||||
]);
|
||||
|
||||
touchSheetsDrawingFloatingHostCapabilityWhenReady(this._injector);
|
||||
this._injector.get(ComponentsController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsDrawingCopyPasteController],
|
||||
[SheetCellImageCopyPasteController],
|
||||
[SheetsDrawingGroupCopyPasteController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._registerRenderModules();
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[SheetDrawingPermissionController],
|
||||
[SheetDrawingPrintingController],
|
||||
[SheetDrawingUIController],
|
||||
[SheetCellImageController],
|
||||
[SheetCellImageAutofillController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onSteady(): void {
|
||||
this._injector.get(DrawingPopupMenuController);
|
||||
this._injector.get(DrawingContextMenuController);
|
||||
}
|
||||
|
||||
private _registerRenderModules(): void {
|
||||
([
|
||||
[SheetDrawingUpdateController],
|
||||
[SheetDrawingActiveRenderController],
|
||||
[SheetsDrawingRenderController],
|
||||
[SheetCellImageHoverRenderController],
|
||||
] as Dependency[]).forEach((m) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, m));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 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 { ICellData, IWorkbookData } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import { BooleanNumber, ImageSourceType, LocaleType } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { BatchSaveImagesService, FileNamePart, IBatchSaveImagesService } from '../batch-save-images.service';
|
||||
|
||||
const WORKBOOK_DATA: IWorkbookData = {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.EN_US,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1'],
|
||||
styles: {},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
0: {
|
||||
0: createImageCell('img-a1', 'data:image/png;base64,aGVsbG8=', ImageSourceType.BASE64),
|
||||
3: { v: 'Report:Q1' },
|
||||
},
|
||||
1: {
|
||||
0: { v: 'Client/Name' },
|
||||
1: createImageCell('img-b2', 'https://cdn.example.com/photo.jpeg?version=1', ImageSourceType.URL),
|
||||
3: { p: { body: { dataStream: 'Summary' } } as ICellData['p'] },
|
||||
},
|
||||
},
|
||||
hidden: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function createImageCell(drawingId: string, source: string, imageSourceType: ImageSourceType): ICellData {
|
||||
return {
|
||||
p: {
|
||||
id: `${drawingId}-doc`,
|
||||
documentStyle: {},
|
||||
drawingsOrder: [drawingId],
|
||||
drawings: {
|
||||
[drawingId]: {
|
||||
drawingId,
|
||||
source,
|
||||
imageSourceType,
|
||||
},
|
||||
},
|
||||
} as unknown as ICellData['p'],
|
||||
};
|
||||
}
|
||||
|
||||
function createSelection(startRow: number, endRow: number, startColumn: number, endColumn: number): ISelectionWithStyle {
|
||||
return {
|
||||
range: {
|
||||
startRow,
|
||||
endRow,
|
||||
startColumn,
|
||||
endColumn,
|
||||
},
|
||||
primary: {
|
||||
actualRow: startRow,
|
||||
actualColumn: startColumn,
|
||||
startRow,
|
||||
endRow,
|
||||
startColumn,
|
||||
endColumn,
|
||||
isMerged: false,
|
||||
isMergedMainCell: false,
|
||||
},
|
||||
style: null,
|
||||
};
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const testBed = createSheetsDrawingUiTestBed(WORKBOOK_DATA, [
|
||||
[IBatchSaveImagesService, { useClass: BatchSaveImagesService }],
|
||||
]);
|
||||
const service = testBed.get(IBatchSaveImagesService);
|
||||
const selectionsService = testBed.get(SheetsSelectionsService);
|
||||
selectionsService.setSelections([
|
||||
createSelection(0, 0, 0, 1),
|
||||
createSelection(1, 1, 1, 1),
|
||||
]);
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
service,
|
||||
selectionsService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('BatchSaveImagesService', () => {
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(window, 'showDirectoryPicker');
|
||||
});
|
||||
|
||||
it('collects images from the active selection and reports the selected address space', () => {
|
||||
const { service, univer } = setup();
|
||||
|
||||
expect(service.getCellImagesInSelection()).toEqual([
|
||||
{
|
||||
row: 0,
|
||||
col: 0,
|
||||
cellAddress: 'A1',
|
||||
source: 'data:image/png;base64,aGVsbG8=',
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
imageId: 'img-a1',
|
||||
},
|
||||
{
|
||||
row: 1,
|
||||
col: 1,
|
||||
cellAddress: 'B2',
|
||||
source: 'https://cdn.example.com/photo.jpeg?version=1',
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
imageId: 'img-b2',
|
||||
},
|
||||
]);
|
||||
expect(service.getCellImagesFromRanges('test', 'sheet1', [
|
||||
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
])).toEqual([
|
||||
{
|
||||
row: 0,
|
||||
col: 0,
|
||||
cellAddress: 'A1',
|
||||
source: 'data:image/png;base64,aGVsbG8=',
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
imageId: 'img-a1',
|
||||
},
|
||||
{
|
||||
row: 1,
|
||||
col: 1,
|
||||
cellAddress: 'B2',
|
||||
source: 'https://cdn.example.com/photo.jpeg?version=1',
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
imageId: 'img-b2',
|
||||
},
|
||||
]);
|
||||
expect(service.getSelectionRangeNotation()).toBe('A1:B1, B2');
|
||||
expect(service.getSelectionRowRange()).toEqual({ startRow: 0, endRow: 1 });
|
||||
expect(Array.from(service.getSelectionColumnIndices()).sort((a, b) => a - b)).toEqual([0, 1]);
|
||||
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('uses neighboring data columns to build spreadsheet-friendly image file names', () => {
|
||||
const { service, univer } = setup();
|
||||
const imageInfo = {
|
||||
row: 1,
|
||||
col: 1,
|
||||
cellAddress: 'B2',
|
||||
source: 'https://cdn.example.com/photo.jpeg?version=1',
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
imageId: 'img-b2',
|
||||
};
|
||||
|
||||
expect(service.getDataColumns()).toEqual([
|
||||
{ index: 3, label: 'D' },
|
||||
]);
|
||||
expect(service.getDataColumnsForRanges('test', 'sheet1', [
|
||||
{ startRow: 0, endRow: 1, startColumn: 0, endColumn: 1 },
|
||||
])).toEqual([
|
||||
{ index: 3, label: 'D' },
|
||||
]);
|
||||
expect(service.generateFileName(imageInfo, {
|
||||
fileNameParts: [FileNamePart.COLUMN_VALUE, FileNamePart.CELL_ADDRESS],
|
||||
columnIndex: 0,
|
||||
})).toBe('Client_Name_B2.jpeg');
|
||||
expect(service.generateFileNameWithContext(imageInfo, {
|
||||
fileNameParts: [FileNamePart.CELL_ADDRESS, FileNamePart.COLUMN_VALUE],
|
||||
columnIndex: 3,
|
||||
}, 'test', 'sheet1')).toBe('B2_Summary.jpeg');
|
||||
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps every image when batch saving would otherwise produce duplicate file names', async () => {
|
||||
const { service, univer } = setup();
|
||||
const savedFiles: string[] = [];
|
||||
window.showDirectoryPicker = async () => ({
|
||||
getFileHandle: async (name: string) => {
|
||||
savedFiles.push(name);
|
||||
return {
|
||||
createWritable: async () => ({
|
||||
write: async () => undefined,
|
||||
close: async () => undefined,
|
||||
}),
|
||||
};
|
||||
},
|
||||
}) as unknown as FileSystemDirectoryHandle;
|
||||
|
||||
await service.saveImages([
|
||||
{
|
||||
row: 0,
|
||||
col: 0,
|
||||
cellAddress: 'A1',
|
||||
source: 'data:image/png;base64,aGVsbG8=',
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
imageId: 'img-a1',
|
||||
},
|
||||
{
|
||||
row: 0,
|
||||
col: 1,
|
||||
cellAddress: 'A1',
|
||||
source: 'data:image/png;base64,aGVsbG8=',
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
imageId: 'img-a1-copy',
|
||||
},
|
||||
], {
|
||||
fileNameParts: [FileNamePart.CELL_ADDRESS],
|
||||
});
|
||||
|
||||
expect(savedFiles).toEqual(['A1.png', 'A1_1.png']);
|
||||
|
||||
univer.dispose();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 { DrawingTypeEnum, UniverInstanceType } from '@univerjs/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SheetDrawingHitTestService } from '../sheet-drawing-hit-test.service';
|
||||
|
||||
function createDrawing(drawingId: string, subUnitId = 'sheet-1'): IDrawingParam {
|
||||
return {
|
||||
unitId: 'unit-1',
|
||||
subUnitId,
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_CHART,
|
||||
};
|
||||
}
|
||||
|
||||
function createScene(oKey = 'unit-1#-#sheet-1#-#chart-1') {
|
||||
const handlers = new Set<(event: { offsetX: number; offsetY: number }) => void>();
|
||||
const unsubscribers: Array<ReturnType<typeof vi.fn>> = [];
|
||||
const scene = {
|
||||
pick: vi.fn(() => ({ oKey })),
|
||||
onDblclick$: {
|
||||
subscribeEvent: vi.fn((handler: (event: { offsetX: number; offsetY: number }) => void) => {
|
||||
handlers.add(handler);
|
||||
const unsubscribe = vi.fn(() => handlers.delete(handler));
|
||||
unsubscribers.push(unsubscribe);
|
||||
return { unsubscribe };
|
||||
}),
|
||||
},
|
||||
emitDoubleClick(event: { offsetX: number; offsetY: number }) {
|
||||
Array.from(handlers).forEach((handler) => handler(event));
|
||||
},
|
||||
unsubscribers,
|
||||
};
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
function createServiceHarness(options: { drawing?: IDrawingParam | null } = {}) {
|
||||
const currentSheet$ = new Subject<unknown>();
|
||||
const renderCreated$ = new Subject<unknown>();
|
||||
const renderDisposed$ = new Subject<string>();
|
||||
const scene = createScene();
|
||||
let currentRender = { unitId: 'unit-1', scene };
|
||||
const drawing = options.drawing === undefined ? createDrawing('chart-1') : options.drawing;
|
||||
const drawingManagerService = {
|
||||
getDrawingOKey: vi.fn(() => drawing),
|
||||
};
|
||||
const workbook = {
|
||||
getUnitId: vi.fn(() => 'unit-1'),
|
||||
getActiveSheet: vi.fn(() => ({ getSheetId: vi.fn(() => 'sheet-1') })),
|
||||
};
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: vi.fn((unitId: string) => unitId === 'unit-1' ? currentRender : null),
|
||||
created$: renderCreated$.asObservable(),
|
||||
disposed$: renderDisposed$.asObservable(),
|
||||
};
|
||||
const service = new SheetDrawingHitTestService(
|
||||
drawingManagerService as never,
|
||||
{
|
||||
getCurrentTypeOfUnit$: vi.fn(() => currentSheet$.asObservable()),
|
||||
getCurrentUnitOfType: vi.fn((type: UniverInstanceType) => type === UniverInstanceType.UNIVER_SHEET ? workbook : null),
|
||||
} as never,
|
||||
renderManagerService as never
|
||||
);
|
||||
const doubleClickEvents: unknown[] = [];
|
||||
const subscription = service.onDoubleClick$.subscribe((event) => doubleClickEvents.push(event));
|
||||
|
||||
return {
|
||||
currentSheet$,
|
||||
doubleClickEvents,
|
||||
drawing,
|
||||
drawingManagerService,
|
||||
renderCreated$,
|
||||
renderDisposed$,
|
||||
renderManagerService,
|
||||
scene,
|
||||
service,
|
||||
setRender(nextRender: typeof currentRender) {
|
||||
currentRender = nextRender;
|
||||
},
|
||||
subscription,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SheetDrawingHitTestService', () => {
|
||||
it('emits the current sheet drawing on scene double-click', () => {
|
||||
const { doubleClickEvents, drawing, drawingManagerService, scene, service, subscription } = createServiceHarness();
|
||||
|
||||
scene.emitDoubleClick({ offsetX: 140, offsetY: 90 });
|
||||
|
||||
expect(scene.pick).toHaveBeenCalled();
|
||||
expect(drawingManagerService.getDrawingOKey).toHaveBeenCalledWith('unit-1#-#sheet-1#-#chart-1');
|
||||
expect(doubleClickEvents[0]).toMatchObject({
|
||||
drawing,
|
||||
oKey: 'unit-1#-#sheet-1#-#chart-1',
|
||||
offsetX: 140,
|
||||
offsetY: 90,
|
||||
});
|
||||
|
||||
subscription.unsubscribe();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('ignores drawings outside the active sheet', () => {
|
||||
const { doubleClickEvents, scene, service, subscription } = createServiceHarness({
|
||||
drawing: createDrawing('chart-1', 'sheet-2'),
|
||||
});
|
||||
|
||||
scene.emitDoubleClick({ offsetX: 140, offsetY: 90 });
|
||||
|
||||
expect(doubleClickEvents).toHaveLength(0);
|
||||
|
||||
subscription.unsubscribe();
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
it('rebinds when the active workbook render is recreated', () => {
|
||||
const { doubleClickEvents, renderCreated$, scene, service, setRender, subscription } = createServiceHarness();
|
||||
const nextScene = createScene();
|
||||
const nextRender = { unitId: 'unit-1', scene: nextScene };
|
||||
|
||||
setRender(nextRender);
|
||||
renderCreated$.next(nextRender);
|
||||
scene.emitDoubleClick({ offsetX: 140, offsetY: 90 });
|
||||
nextScene.emitDoubleClick({ offsetX: 150, offsetY: 100 });
|
||||
|
||||
expect(scene.unsubscribers[0]).toHaveBeenCalled();
|
||||
expect(doubleClickEvents).toHaveLength(1);
|
||||
expect(doubleClickEvents[0]).toMatchObject({
|
||||
offsetX: 150,
|
||||
offsetY: 100,
|
||||
});
|
||||
|
||||
subscription.unsubscribe();
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,674 @@
|
||||
/**
|
||||
* 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 { ICellData, IRange, Workbook } from '@univerjs/core';
|
||||
import type { IImageData } from '@univerjs/drawing';
|
||||
import { createIdentifier, Disposable, IImageIoService, ImageSourceType, Inject, IUniverInstanceService, IURLImageService, UniverInstanceType } from '@univerjs/core';
|
||||
import { isCellImage, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface Window {
|
||||
showDirectoryPicker(options?: { mode?: 'read' | 'readwrite' }): Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* File name part type for multi-select
|
||||
*/
|
||||
export enum FileNamePart {
|
||||
/**
|
||||
* Use cell address as part of file name (e.g., A1, B2)
|
||||
*/
|
||||
CELL_ADDRESS = 'cellAddress',
|
||||
/**
|
||||
* Use value from a specific column as part of file name
|
||||
*/
|
||||
COLUMN_VALUE = 'columnValue',
|
||||
}
|
||||
|
||||
export interface ICellImageInfo {
|
||||
row: number;
|
||||
col: number;
|
||||
cellAddress: string;
|
||||
source: string;
|
||||
imageSourceType: ImageSourceType;
|
||||
imageId: string;
|
||||
}
|
||||
|
||||
export interface IBatchSaveImagesConfig {
|
||||
/**
|
||||
* Selected file name parts (multi-select)
|
||||
*/
|
||||
fileNameParts: FileNamePart[];
|
||||
/**
|
||||
* Column index for COLUMN_VALUE part
|
||||
*/
|
||||
columnIndex?: number;
|
||||
}
|
||||
|
||||
export interface IBatchSaveImagesService {
|
||||
/**
|
||||
* Get all cell images in the current selection
|
||||
*/
|
||||
getCellImagesInSelection(): ICellImageInfo[];
|
||||
|
||||
/**
|
||||
* Get cell images from specified ranges
|
||||
* @param unitId The workbook unit ID
|
||||
* @param subUnitId The worksheet ID
|
||||
* @param ranges The ranges to get images from
|
||||
*/
|
||||
getCellImagesFromRanges(unitId: string, subUnitId: string, ranges: IRange[]): ICellImageInfo[];
|
||||
|
||||
/**
|
||||
* Get columns that have data in the current selection
|
||||
*/
|
||||
getDataColumns(): Array<{ index: number; label: string }>;
|
||||
|
||||
/**
|
||||
* Get columns that have data for specified ranges
|
||||
* @param unitId The workbook unit ID
|
||||
* @param subUnitId The worksheet ID
|
||||
* @param ranges The ranges to check
|
||||
*/
|
||||
getDataColumnsForRanges(unitId: string, subUnitId: string, ranges: IRange[]): Array<{ index: number; label: string }>;
|
||||
|
||||
/**
|
||||
* Get current selection range as A1 notation
|
||||
*/
|
||||
getSelectionRangeNotation(): string;
|
||||
|
||||
/**
|
||||
* Generate file name for a cell image based on config
|
||||
* @param imageInfo The cell image info
|
||||
* @param config The file name configuration
|
||||
*/
|
||||
generateFileName(imageInfo: ICellImageInfo, config: IBatchSaveImagesConfig): string;
|
||||
|
||||
/**
|
||||
* Generate file name with specified worksheet context
|
||||
* @param imageInfo The cell image info
|
||||
* @param config The file name configuration
|
||||
* @param unitId The workbook unit ID
|
||||
* @param subUnitId The worksheet ID
|
||||
*/
|
||||
generateFileNameWithContext(imageInfo: ICellImageInfo, config: IBatchSaveImagesConfig, unitId: string, subUnitId: string): string;
|
||||
|
||||
/**
|
||||
* Save images to the file system
|
||||
* @param images The images to save
|
||||
* @param config The file name configuration
|
||||
*/
|
||||
saveImages(images: ICellImageInfo[], config: IBatchSaveImagesConfig): Promise<void>;
|
||||
|
||||
/**
|
||||
* Save images to the file system with specified worksheet context
|
||||
* @param images The images to save
|
||||
* @param config The file name configuration
|
||||
* @param unitId The workbook unit ID
|
||||
* @param subUnitId The worksheet ID
|
||||
*/
|
||||
saveImagesWithContext(images: ICellImageInfo[], config: IBatchSaveImagesConfig, unitId: string, subUnitId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Download a single image directly
|
||||
* @param imageInfo The cell image info
|
||||
*/
|
||||
downloadSingleImage(imageInfo: ICellImageInfo): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the row range of current selection
|
||||
* Returns the min and max row indices
|
||||
*/
|
||||
getSelectionRowRange(): { startRow: number; endRow: number } | null;
|
||||
|
||||
/**
|
||||
* Get all column indices that are within the current selection
|
||||
*/
|
||||
/**
|
||||
* Get all column indices that are within the current selection
|
||||
*/
|
||||
getSelectionColumnIndices(): Set<number>;
|
||||
}
|
||||
|
||||
export const IBatchSaveImagesService = createIdentifier<IBatchSaveImagesService>('sheets-drawing-ui.batch-save-images.service');
|
||||
|
||||
/**
|
||||
* Convert column index to letter (0 -> A, 1 -> B, etc.)
|
||||
*/
|
||||
function columnIndexToLetter(index: number): string {
|
||||
let letter = '';
|
||||
let temp = index;
|
||||
|
||||
while (temp >= 0) {
|
||||
letter = String.fromCharCode((temp % 26) + 65) + letter;
|
||||
temp = Math.floor(temp / 26) - 1;
|
||||
}
|
||||
|
||||
return letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert row and column to A1 notation
|
||||
*/
|
||||
function toA1Notation(row: number, col: number): string {
|
||||
return `${columnIndexToLetter(col)}${row + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert range to A1 notation
|
||||
*/
|
||||
function rangeToA1Notation(range: IRange): string {
|
||||
const start = toA1Notation(range.startRow, range.startColumn);
|
||||
const end = toA1Notation(range.endRow, range.endColumn);
|
||||
return start === end ? start : `${start}:${end}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image data from cell
|
||||
*/
|
||||
function getCellImageData(cell: ICellData): IImageData | null {
|
||||
const documentData = cell.p;
|
||||
const drawingId = documentData?.drawingsOrder?.[0];
|
||||
if (!isCellImage(documentData) || !drawingId || !documentData?.drawings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawing = documentData.drawings[drawingId];
|
||||
|
||||
if (!drawing || !('source' in drawing) || !('imageSourceType' in drawing)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return drawing as unknown as IImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension from mime type or source
|
||||
*/
|
||||
function getFileExtension(source: string, imageSourceType: ImageSourceType): string {
|
||||
if (imageSourceType === ImageSourceType.BASE64) {
|
||||
const match = source.match(/^data:image\/(\w+);/);
|
||||
if (match) {
|
||||
return match[1] === 'jpeg' ? 'jpg' : match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get extension from URL
|
||||
if (imageSourceType === ImageSourceType.URL) {
|
||||
const urlMatch = source.match(/\.(\w+)(?:\?|$)/);
|
||||
if (urlMatch) {
|
||||
return urlMatch[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Default to png
|
||||
return 'png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert image source to blob
|
||||
*/
|
||||
async function imageSourceToBlob(source: string, imageSourceType: ImageSourceType): Promise<Blob> {
|
||||
if (imageSourceType === ImageSourceType.BASE64) {
|
||||
const response = await fetch(source);
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
if (imageSourceType === ImageSourceType.URL) {
|
||||
const response = await fetch(source);
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
// For UUID type, we need to get the actual URL from the service
|
||||
throw new Error('UUID image type requires additional handling');
|
||||
}
|
||||
|
||||
export class BatchSaveImagesService extends Disposable implements IBatchSaveImagesService {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@Inject(SheetsSelectionsService) private readonly _selectionService: SheetsSelectionsService,
|
||||
@IImageIoService private readonly _imageIoService: IImageIoService,
|
||||
@IURLImageService private readonly _urlImageService: IURLImageService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getCellImagesInSelection(): ICellImageInfo[] {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return [];
|
||||
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return [];
|
||||
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return [];
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const images: ICellImageInfo[] = [];
|
||||
|
||||
for (const selection of selections) {
|
||||
const { startRow, endRow, startColumn, endColumn } = selection.range;
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
|
||||
if (isCellImage(cell?.p)) {
|
||||
const imageData = getCellImageData(cell!);
|
||||
if (imageData) {
|
||||
images.push({
|
||||
row,
|
||||
col,
|
||||
cellAddress: toA1Notation(row, col),
|
||||
source: imageData.source,
|
||||
imageSourceType: imageData.imageSourceType,
|
||||
imageId: imageData.drawingId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
getCellImagesFromRanges(unitId: string, subUnitId: string, ranges: IRange[]): ICellImageInfo[] {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return [];
|
||||
|
||||
const worksheet = workbook.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) return [];
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const images: ICellImageInfo[] = [];
|
||||
|
||||
for (const range of ranges) {
|
||||
const { startRow, endRow, startColumn, endColumn } = range;
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
|
||||
if (isCellImage(cell?.p)) {
|
||||
const imageData = getCellImageData(cell!);
|
||||
if (imageData) {
|
||||
images.push({
|
||||
row,
|
||||
col,
|
||||
cellAddress: toA1Notation(row, col),
|
||||
source: imageData.source,
|
||||
imageSourceType: imageData.imageSourceType,
|
||||
imageId: imageData.drawingId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
getDataColumns(): Array<{ index: number; label: string }> {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return [];
|
||||
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) return [];
|
||||
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return [];
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const dataRange = cellMatrix.getDataRange();
|
||||
|
||||
// Get row range and column indices from selection
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
const selectionColumnIndices = new Set<number>();
|
||||
|
||||
for (const selection of selections) {
|
||||
minRow = Math.min(minRow, selection.range.startRow);
|
||||
maxRow = Math.max(maxRow, selection.range.endRow);
|
||||
|
||||
// Collect all column indices within selection
|
||||
for (let col = selection.range.startColumn; col <= selection.range.endColumn; col++) {
|
||||
selectionColumnIndices.add(col);
|
||||
}
|
||||
}
|
||||
|
||||
// Find columns that have values in the selection row range, excluding selection columns
|
||||
const columnsWithData = new Set<number>();
|
||||
|
||||
for (let col = dataRange.startColumn; col <= dataRange.endColumn; col++) {
|
||||
// Skip columns that are within the selection
|
||||
if (selectionColumnIndices.has(col)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let row = minRow; row <= maxRow; row++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
if (cell) {
|
||||
const value = cell.v?.toString() || cell.p?.body?.dataStream?.trim() || '';
|
||||
if (value) {
|
||||
columnsWithData.add(col);
|
||||
break; // Found data in this column, move to next column
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array and sort
|
||||
const columns: Array<{ index: number; label: string }> = [];
|
||||
const sortedCols = Array.from(columnsWithData).sort((a, b) => a - b);
|
||||
|
||||
for (const col of sortedCols) {
|
||||
columns.push({
|
||||
index: col,
|
||||
label: columnIndexToLetter(col),
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
getDataColumnsForRanges(unitId: string, subUnitId: string, ranges: IRange[]): Array<{ index: number; label: string }> {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
if (!workbook) return [];
|
||||
|
||||
const worksheet = workbook.getSheetBySheetId(subUnitId);
|
||||
if (!worksheet) return [];
|
||||
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const dataRange = cellMatrix.getDataRange();
|
||||
|
||||
// Get row range and column indices from ranges
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
const rangeColumnIndices = new Set<number>();
|
||||
|
||||
for (const range of ranges) {
|
||||
minRow = Math.min(minRow, range.startRow);
|
||||
maxRow = Math.max(maxRow, range.endRow);
|
||||
|
||||
// Collect all column indices within ranges
|
||||
for (let col = range.startColumn; col <= range.endColumn; col++) {
|
||||
rangeColumnIndices.add(col);
|
||||
}
|
||||
}
|
||||
|
||||
// Find columns that have values in the row range, excluding range columns
|
||||
const columnsWithData = new Set<number>();
|
||||
|
||||
for (let col = dataRange.startColumn; col <= dataRange.endColumn; col++) {
|
||||
// Skip columns that are within the ranges
|
||||
if (rangeColumnIndices.has(col)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let row = minRow; row <= maxRow; row++) {
|
||||
const cell = cellMatrix.getValue(row, col);
|
||||
if (cell) {
|
||||
const value = cell.v?.toString() || cell.p?.body?.dataStream?.trim() || '';
|
||||
if (value) {
|
||||
columnsWithData.add(col);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array and sort
|
||||
const columns: Array<{ index: number; label: string }> = [];
|
||||
const sortedCols = Array.from(columnsWithData).sort((a, b) => a - b);
|
||||
|
||||
for (const col of sortedCols) {
|
||||
columns.push({
|
||||
index: col,
|
||||
label: columnIndexToLetter(col),
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
getSelectionRangeNotation(): string {
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return '';
|
||||
|
||||
return selections.map((s) => rangeToA1Notation(s.range)).join(', ');
|
||||
}
|
||||
|
||||
getSelectionRowRange(): { startRow: number; endRow: number } | null {
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return null;
|
||||
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
|
||||
for (const selection of selections) {
|
||||
minRow = Math.min(minRow, selection.range.startRow);
|
||||
maxRow = Math.max(maxRow, selection.range.endRow);
|
||||
}
|
||||
|
||||
return { startRow: minRow, endRow: maxRow };
|
||||
}
|
||||
|
||||
getSelectionColumnIndices(): Set<number> {
|
||||
const selections = this._selectionService.getCurrentSelections();
|
||||
if (!selections || selections.length === 0) return new Set();
|
||||
|
||||
const columnIndices = new Set<number>();
|
||||
for (const selection of selections) {
|
||||
for (let col = selection.range.startColumn; col <= selection.range.endColumn; col++) {
|
||||
columnIndices.add(col);
|
||||
}
|
||||
}
|
||||
|
||||
return columnIndices;
|
||||
}
|
||||
|
||||
generateFileName(imageInfo: ICellImageInfo, config: IBatchSaveImagesConfig): string {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const extension = getFileExtension(imageInfo.source, imageInfo.imageSourceType);
|
||||
const parts: string[] = [];
|
||||
|
||||
// Process each selected file name part in order
|
||||
for (const part of config.fileNameParts) {
|
||||
if (part === FileNamePart.CELL_ADDRESS) {
|
||||
parts.push(imageInfo.cellAddress);
|
||||
} else if (part === FileNamePart.COLUMN_VALUE && config.columnIndex !== undefined) {
|
||||
const worksheet = workbook?.getActiveSheet();
|
||||
if (worksheet) {
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const cell = cellMatrix.getValue(imageInfo.row, config.columnIndex);
|
||||
|
||||
if (cell) {
|
||||
// Get cell display value
|
||||
const value = cell.v?.toString() || cell.p?.body?.dataStream?.trim() || '';
|
||||
if (value) {
|
||||
// Sanitize file name (remove invalid characters)
|
||||
const sanitized = value.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||
if (sanitized) {
|
||||
parts.push(sanitized);
|
||||
}
|
||||
// If sanitized is empty, skip this part (don't add anything)
|
||||
}
|
||||
// If value is empty, skip this part
|
||||
}
|
||||
// If cell is empty, skip this part
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no parts selected or all parts failed, use cell address as default
|
||||
if (parts.length === 0) {
|
||||
return `${imageInfo.cellAddress}.${extension}`;
|
||||
}
|
||||
|
||||
// Join parts with underscore
|
||||
return `${parts.join('_')}.${extension}`;
|
||||
}
|
||||
|
||||
generateFileNameWithContext(imageInfo: ICellImageInfo, config: IBatchSaveImagesConfig, unitId: string, subUnitId: string): string {
|
||||
const workbook = this._univerInstanceService.getUnit<Workbook>(unitId, UniverInstanceType.UNIVER_SHEET);
|
||||
const extension = getFileExtension(imageInfo.source, imageInfo.imageSourceType);
|
||||
const parts: string[] = [];
|
||||
|
||||
// Process each selected file name part in order
|
||||
for (const part of config.fileNameParts) {
|
||||
if (part === FileNamePart.CELL_ADDRESS) {
|
||||
parts.push(imageInfo.cellAddress);
|
||||
} else if (part === FileNamePart.COLUMN_VALUE && config.columnIndex !== undefined) {
|
||||
const worksheet = workbook?.getSheetBySheetId(subUnitId);
|
||||
if (worksheet) {
|
||||
const cellMatrix = worksheet.getCellMatrix();
|
||||
const cell = cellMatrix.getValue(imageInfo.row, config.columnIndex);
|
||||
|
||||
if (cell) {
|
||||
// Get cell display value
|
||||
const value = cell.v?.toString() || cell.p?.body?.dataStream?.trim() || '';
|
||||
if (value) {
|
||||
// Sanitize file name (remove invalid characters)
|
||||
const sanitized = value.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||
if (sanitized) {
|
||||
parts.push(sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no parts selected or all parts failed, use cell address as default
|
||||
if (parts.length === 0) {
|
||||
return `${imageInfo.cellAddress}.${extension}`;
|
||||
}
|
||||
|
||||
// Join parts with underscore
|
||||
return `${parts.join('_')}.${extension}`;
|
||||
}
|
||||
|
||||
async saveImages(images: ICellImageInfo[], config: IBatchSaveImagesConfig): Promise<void> {
|
||||
// Request directory access using File System Access API
|
||||
const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||
// Track file names to handle duplicates
|
||||
const fileNameCounts = new Map<string, number>();
|
||||
|
||||
for (const imageInfo of images) {
|
||||
let fileName = this.generateFileName(imageInfo, config);
|
||||
|
||||
// Handle duplicate file names
|
||||
const baseName = fileName.replace(/\.\w+$/, '');
|
||||
const ext = fileName.match(/\.\w+$/)?.[0] || '.png';
|
||||
|
||||
const count = fileNameCounts.get(baseName) || 0;
|
||||
if (count > 0) {
|
||||
fileName = `${baseName}_${count}${ext}`;
|
||||
}
|
||||
fileNameCounts.set(baseName, count + 1);
|
||||
|
||||
try {
|
||||
// Get image blob
|
||||
const blob = await this._getImageBlob(imageInfo);
|
||||
|
||||
// Create file and write
|
||||
const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
console.error(`Failed to save image ${fileName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveImagesWithContext(images: ICellImageInfo[], config: IBatchSaveImagesConfig, unitId: string, subUnitId: string): Promise<void> {
|
||||
// Request directory access using File System Access API
|
||||
const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||
|
||||
// Track file names to handle duplicates
|
||||
const fileNameCounts = new Map<string, number>();
|
||||
|
||||
for (const imageInfo of images) {
|
||||
let fileName = this.generateFileNameWithContext(imageInfo, config, unitId, subUnitId);
|
||||
|
||||
// Handle duplicate file names
|
||||
const baseName = fileName.replace(/\.\w+$/, '');
|
||||
const ext = fileName.match(/\.\w+$/)?.[0] || '.png';
|
||||
|
||||
const count = fileNameCounts.get(baseName) || 0;
|
||||
if (count > 0) {
|
||||
fileName = `${baseName}_${count}${ext}`;
|
||||
}
|
||||
fileNameCounts.set(baseName, count + 1);
|
||||
|
||||
try {
|
||||
// Get image blob
|
||||
const blob = await this._getImageBlob(imageInfo);
|
||||
|
||||
// Create file and write
|
||||
const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
console.error(`Failed to save image ${fileName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async downloadSingleImage(imageInfo: ICellImageInfo): Promise<void> {
|
||||
const extension = getFileExtension(imageInfo.source, imageInfo.imageSourceType);
|
||||
const fileName = `${imageInfo.cellAddress}.${extension}`;
|
||||
|
||||
try {
|
||||
const blob = await this._getImageBlob(imageInfo);
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error(`Failed to download image ${fileName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getImageBlob(imageInfo: ICellImageInfo): Promise<Blob> {
|
||||
if (imageInfo.imageSourceType === ImageSourceType.UUID) {
|
||||
// For UUID, we need to get the actual image from the service
|
||||
const imageUrl = await this._imageIoService.getImage(imageInfo.source);
|
||||
return imageSourceToBlob(imageUrl, ImageSourceType.URL);
|
||||
}
|
||||
|
||||
if (imageInfo.imageSourceType === ImageSourceType.URL) {
|
||||
return this._urlImageService.downloadImage(imageInfo.source);
|
||||
}
|
||||
|
||||
return imageSourceToBlob(imageInfo.source, imageInfo.imageSourceType);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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, IDisposable, IDrawingParam } from '@univerjs/core';
|
||||
import { createIdentifier, Disposable, toDisposable } from '@univerjs/core';
|
||||
|
||||
export interface IDrawingContextMenuProviderContext {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawingId: string;
|
||||
drawingType: DrawingTypeEnum;
|
||||
drawing: IDrawingParam;
|
||||
}
|
||||
|
||||
export interface IDrawingContextMenuProvider {
|
||||
getContextMenuPosition(context: IDrawingContextMenuProviderContext): string | undefined;
|
||||
}
|
||||
|
||||
export interface IDrawingContextMenuService {
|
||||
registerProvider(drawingType: DrawingTypeEnum, provider: IDrawingContextMenuProvider): IDisposable;
|
||||
getContextMenuPosition(context: IDrawingContextMenuProviderContext): string | undefined;
|
||||
}
|
||||
|
||||
export const IDrawingContextMenuService = createIdentifier<IDrawingContextMenuService>('sheets-drawing-ui.drawing-context-menu.service');
|
||||
|
||||
export class DrawingContextMenuService extends Disposable implements IDrawingContextMenuService {
|
||||
private readonly _providers = new Map<DrawingTypeEnum, IDrawingContextMenuProvider>();
|
||||
|
||||
registerProvider(drawingType: DrawingTypeEnum, provider: IDrawingContextMenuProvider): IDisposable {
|
||||
this._providers.set(drawingType, provider);
|
||||
|
||||
return toDisposable(() => {
|
||||
if (this._providers.get(drawingType) === provider) {
|
||||
this._providers.delete(drawingType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getContextMenuPosition(context: IDrawingContextMenuProviderContext): string | undefined {
|
||||
return this._providers.get(context.drawingType)?.getContextMenuPosition(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { IRectProps } from '@univerjs/engine-render';
|
||||
|
||||
type SheetChartRenderObjectConfig = Pick<
|
||||
IRectProps,
|
||||
'borderEnabled' | 'paintFirst' | 'radius' | 'rotateEnabled' | 'strokeWidth'
|
||||
>;
|
||||
|
||||
/** Shared Chart frame config for editor and isolated print Scene objects. */
|
||||
export const SHEET_CHART_RENDER_OBJECT_CONFIG = Object.freeze({
|
||||
borderEnabled: false,
|
||||
paintFirst: 'stroke',
|
||||
radius: 8,
|
||||
rotateEnabled: false,
|
||||
strokeWidth: 1,
|
||||
} satisfies SheetChartRenderObjectConfig);
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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 { IDisposable, IDrawingParam, Workbook } from '@univerjs/core';
|
||||
import type { IMouseEvent, IPointerEvent, IRender, Scene } from '@univerjs/engine-render';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { Disposable, IUniverInstanceService, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, Vector2 } from '@univerjs/engine-render';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface ISheetDrawingHitTestResult {
|
||||
drawing: IDrawingParam;
|
||||
oKey: string;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
export interface ISheetDrawingDoubleClickEvent extends ISheetDrawingHitTestResult {
|
||||
nativeEvent: IPointerEvent | IMouseEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves sheet canvas pointer coordinates to drawing objects.
|
||||
*
|
||||
* Callers consume drawing-level events and should not depend on renderer object
|
||||
* keys, scene picking, or active sheet filtering.
|
||||
*/
|
||||
export class SheetDrawingHitTestService extends Disposable {
|
||||
private readonly _onDoubleClick$ = new Subject<ISheetDrawingDoubleClickEvent>();
|
||||
readonly onDoubleClick$: Observable<ISheetDrawingDoubleClickEvent> = this._onDoubleClick$.asObservable();
|
||||
|
||||
private _doubleClickDisposable: IDisposable | null = null;
|
||||
private _boundRender: IRender | null = null;
|
||||
private _boundScene: Scene | null = null;
|
||||
|
||||
constructor(
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._disposeDoubleClickListener();
|
||||
this._onDoubleClick$.complete();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
hitTestCurrentSheet(offsetX: number, offsetY: number): ISheetDrawingHitTestResult | null {
|
||||
const context = this._getActiveSheetContext();
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.hitTest(context.unitId, context.subUnitId, offsetX, offsetY);
|
||||
}
|
||||
|
||||
hitTest(unitId: string, subUnitId: string, offsetX: number, offsetY: number): ISheetDrawingHitTestResult | null {
|
||||
const scene = this._renderManagerService.getRenderUnitById(unitId)?.scene;
|
||||
if (!scene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._hitTestScene(scene, unitId, subUnitId, offsetX, offsetY);
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this.disposeWithMe(toDisposable(
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET).subscribe(() => {
|
||||
this._bindDoubleClickListener();
|
||||
})
|
||||
));
|
||||
|
||||
this.disposeWithMe(toDisposable(
|
||||
this._renderManagerService.created$.subscribe((render) => {
|
||||
const context = this._getActiveSheetContext();
|
||||
if (render.unitId === context?.unitId) {
|
||||
this._bindDoubleClickListener();
|
||||
}
|
||||
})
|
||||
));
|
||||
|
||||
this.disposeWithMe(toDisposable(
|
||||
this._renderManagerService.disposed$.subscribe((unitId) => {
|
||||
if (unitId === this._boundRender?.unitId) {
|
||||
this._disposeDoubleClickListener();
|
||||
}
|
||||
})
|
||||
));
|
||||
|
||||
this._bindDoubleClickListener();
|
||||
}
|
||||
|
||||
private _bindDoubleClickListener(): void {
|
||||
const context = this._getActiveSheetContext();
|
||||
if (!context) {
|
||||
this._disposeDoubleClickListener();
|
||||
return;
|
||||
}
|
||||
|
||||
const render = this._renderManagerService.getRenderUnitById(context.unitId);
|
||||
const scene = render?.scene;
|
||||
if (!render || !scene) {
|
||||
this._disposeDoubleClickListener();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._boundRender === render && this._boundScene === scene && this._doubleClickDisposable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposeDoubleClickListener();
|
||||
|
||||
const subscription = scene.onDblclick$.subscribeEvent((event: IPointerEvent | IMouseEvent) => {
|
||||
const activeContext = this._getActiveSheetContext();
|
||||
if (!activeContext || activeContext.unitId !== context.unitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hit = this._hitTestScene(scene, activeContext.unitId, activeContext.subUnitId, event.offsetX, event.offsetY);
|
||||
if (!hit) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._onDoubleClick$.next({
|
||||
...hit,
|
||||
nativeEvent: event,
|
||||
});
|
||||
});
|
||||
|
||||
this._doubleClickDisposable = { dispose: () => subscription.unsubscribe() };
|
||||
this._boundRender = render;
|
||||
this._boundScene = scene;
|
||||
}
|
||||
|
||||
private _disposeDoubleClickListener(): void {
|
||||
this._doubleClickDisposable?.dispose();
|
||||
this._doubleClickDisposable = null;
|
||||
this._boundRender = null;
|
||||
this._boundScene = null;
|
||||
}
|
||||
|
||||
private _hitTestScene(scene: Scene, unitId: string, subUnitId: string, offsetX: number, offsetY: number): ISheetDrawingHitTestResult | null {
|
||||
const pickedObject = scene.pick(Vector2.FromArray([offsetX, offsetY])) as {
|
||||
oKey?: string;
|
||||
} | null | undefined;
|
||||
const oKey = typeof pickedObject?.oKey === 'string' ? pickedObject.oKey : null;
|
||||
if (!oKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawing = this._drawingManagerService.getDrawingOKey(oKey);
|
||||
if (!drawing || drawing.unitId !== unitId || drawing.subUnitId !== subUnitId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
drawing,
|
||||
oKey,
|
||||
offsetX,
|
||||
offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
private _getActiveSheetContext(): { unitId: string; subUnitId: string } | null {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const unitId = workbook?.getUnitId();
|
||||
const subUnitId = workbook?.getActiveSheet()?.getSheetId();
|
||||
if (!unitId || !subUnitId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { unitId, subUnitId };
|
||||
}
|
||||
}
|
||||
109
packages/sheets-drawing-ui/src/views/PrintingFloatDom.tsx
Normal file
109
packages/sheets-drawing-ui/src/views/PrintingFloatDom.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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, Worksheet } from '@univerjs/core';
|
||||
import type { Scene, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { IFloatDomData } from '@univerjs/sheets-drawing';
|
||||
import type { IFloatDom, IFloatDomLayout } from '@univerjs/ui';
|
||||
import type { ComponentType } from 'react';
|
||||
import { render, unmount } from '@univerjs/design';
|
||||
import { connectInjector, PrintFloatDomSingle } from '@univerjs/ui';
|
||||
import { useMemo } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { transformBound2DOMBound } from '../services/canvas-float-dom-manager.service';
|
||||
|
||||
export interface IPrintingFloatDomProps {
|
||||
floatDomInfos: IFloatDomData[];
|
||||
scene: Scene;
|
||||
skeleton: SpreadsheetSkeleton;
|
||||
worksheet: Worksheet;
|
||||
};
|
||||
|
||||
export function createPrintingFloatDom(
|
||||
info: IFloatDomData,
|
||||
scene: Scene,
|
||||
skeleton: SpreadsheetSkeleton,
|
||||
worksheet: Worksheet
|
||||
) {
|
||||
const { width, height, angle, left, top } = info.transform!;
|
||||
// The print root starts at the sheet content origin and does not include row/column headers.
|
||||
const offsetBound = transformBound2DOMBound(
|
||||
{
|
||||
left: left ?? 0,
|
||||
right: (left ?? 0) + (width ?? 0),
|
||||
top: top ?? 0,
|
||||
bottom: (top ?? 0) + (height ?? 0),
|
||||
},
|
||||
scene,
|
||||
skeleton,
|
||||
worksheet,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
const domPos: IFloatDomLayout = {
|
||||
startX: offsetBound.left,
|
||||
endX: offsetBound.right,
|
||||
startY: offsetBound.top,
|
||||
endY: offsetBound.bottom,
|
||||
rotate: angle!,
|
||||
width: width! * scaleX,
|
||||
height: height! * scaleY,
|
||||
absolute: offsetBound.absolute,
|
||||
};
|
||||
|
||||
const floatDom: IFloatDom & { position: IFloatDomLayout } = {
|
||||
position$: new BehaviorSubject<IFloatDomLayout>(domPos),
|
||||
position: domPos,
|
||||
id: info.drawingId,
|
||||
componentKey: info.componentKey,
|
||||
onPointerMove: () => { },
|
||||
onPointerDown: () => { },
|
||||
onPointerUp: () => { },
|
||||
onWheel: () => { },
|
||||
unitId: info.unitId,
|
||||
data: info.data,
|
||||
// Preserve props required by custom printable floating DOM components.
|
||||
props: (info as IFloatDomData & { props?: Record<string, unknown> }).props,
|
||||
};
|
||||
|
||||
return [info.drawingId, floatDom] as const;
|
||||
}
|
||||
|
||||
export const PrintingFloatDom = (props: IPrintingFloatDomProps) => {
|
||||
const { floatDomInfos, scene, skeleton, worksheet } = props;
|
||||
const floatDomParams = useMemo(
|
||||
() => floatDomInfos.map((info) => createPrintingFloatDom(info, scene, skeleton, worksheet)),
|
||||
[floatDomInfos, scene, skeleton, worksheet]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', top: 0, left: 0 }}>
|
||||
{floatDomParams.map(([id, floatDom]) => (
|
||||
<PrintFloatDomSingle key={id} layer={floatDom} id={id} position={floatDom.position} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function mountPrintingFloatDom(props: IPrintingFloatDomProps, root: HTMLElement, injector: Injector): () => void {
|
||||
const PrintingFloatDomInjector = connectInjector(PrintingFloatDom, injector) as ComponentType<IPrintingFloatDomProps>;
|
||||
|
||||
render(<PrintingFloatDomInjector {...props} />, root);
|
||||
|
||||
return () => unmount(root);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 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 { Worksheet } from '@univerjs/core';
|
||||
import type { Scene, SpreadsheetSkeleton } from '@univerjs/engine-render';
|
||||
import type { IFloatDomData } from '@univerjs/sheets-drawing';
|
||||
import { DrawingTypeEnum } from '@univerjs/core';
|
||||
import { SHEET_VIEWPORT_KEY } from '@univerjs/engine-render';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { createPrintingFloatDom, mountPrintingFloatDom } from '../PrintingFloatDom';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const PRINTING_COMPONENT_KEY = 'printing-visible-content';
|
||||
|
||||
class TestPrintingScene {
|
||||
getAncestorScale() {
|
||||
return { scaleX: 1.5, scaleY: 2 };
|
||||
}
|
||||
|
||||
getViewport(key: string) {
|
||||
if (key !== SHEET_VIEWPORT_KEY.VIEW_MAIN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 600,
|
||||
bottom: 500,
|
||||
viewportScrollX: 0,
|
||||
viewportScrollY: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestPrintingSkeleton {
|
||||
readonly rowHeaderWidth = 46;
|
||||
readonly columnHeaderHeight = 28;
|
||||
|
||||
rowStartY(row: number): number {
|
||||
return row * 24;
|
||||
}
|
||||
|
||||
colStartX(column: number): number {
|
||||
return column * 72;
|
||||
}
|
||||
}
|
||||
|
||||
class TestPrintingWorksheet {
|
||||
getFreeze() {
|
||||
return {
|
||||
startColumn: 0,
|
||||
startRow: 0,
|
||||
xSplit: 0,
|
||||
ySplit: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function PrintingContent(props: { data?: { label?: string }; printingLabel?: string }) {
|
||||
return <span data-testid="printed-content">{props.printingLabel ?? props.data?.label}</span>;
|
||||
}
|
||||
|
||||
function createFloatDomInfo(drawingId = 'printed-float-dom', label = 'printing'): IFloatDomData {
|
||||
return {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
componentKey: PRINTING_COMPONENT_KEY,
|
||||
transform: {
|
||||
left: 20,
|
||||
top: 40,
|
||||
width: 200,
|
||||
height: 100,
|
||||
angle: 27,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
},
|
||||
data: {
|
||||
label,
|
||||
},
|
||||
axisAlignSheetTransform: {
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
skewX: 0,
|
||||
skewY: 0,
|
||||
from: { row: 1, column: 1, rowOffset: 0, columnOffset: 0 },
|
||||
to: { row: 4, column: 3, rowOffset: 0, columnOffset: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('PrintingFloatDom', () => {
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createSheetsDrawingUiTestBed> | undefined;
|
||||
let unmountPrintingFloatDom: (() => void) | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
unmountPrintingFloatDom?.();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
unmountPrintingFloatDom = undefined;
|
||||
});
|
||||
|
||||
it('prints floating doms with the scene scale applied to size and rotation', async () => {
|
||||
currentTestBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[ComponentManager],
|
||||
]);
|
||||
currentTestBed.injector.get(ComponentManager).register(PRINTING_COMPONENT_KEY, PrintingContent);
|
||||
const root = document.createElement('div');
|
||||
container = root;
|
||||
document.body.appendChild(root);
|
||||
|
||||
await act(async () => {
|
||||
unmountPrintingFloatDom = mountPrintingFloatDom({
|
||||
floatDomInfos: [createFloatDomInfo()],
|
||||
scene: new TestPrintingScene() as unknown as Scene,
|
||||
skeleton: new TestPrintingSkeleton() as unknown as SpreadsheetSkeleton,
|
||||
worksheet: new TestPrintingWorksheet() as unknown as Worksheet,
|
||||
}, root, currentTestBed!.injector);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const printedFloatDom = root.querySelector<HTMLElement>('#printed-float-dom');
|
||||
if (!printedFloatDom?.parentElement) {
|
||||
throw new Error('Printed float dom was not rendered');
|
||||
}
|
||||
|
||||
expect(printedFloatDom.style.width).toBe('296px');
|
||||
expect(printedFloatDom.style.height).toBe('196px');
|
||||
expect(printedFloatDom.parentElement.style.width).toBe('298px');
|
||||
expect(printedFloatDom.parentElement.style.height).toBe('198px');
|
||||
expect(printedFloatDom.parentElement.style.transform).toBe('rotate(27deg)');
|
||||
});
|
||||
|
||||
it('prints each floating DOM with its own component data', async () => {
|
||||
currentTestBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[ComponentManager],
|
||||
]);
|
||||
currentTestBed.injector.get(ComponentManager).register(PRINTING_COMPONENT_KEY, PrintingContent);
|
||||
const root = document.createElement('div');
|
||||
container = root;
|
||||
document.body.appendChild(root);
|
||||
|
||||
await act(async () => {
|
||||
unmountPrintingFloatDom = mountPrintingFloatDom({
|
||||
floatDomInfos: [
|
||||
createFloatDomInfo('first-printed-float-dom', 'First printed label'),
|
||||
createFloatDomInfo('second-printed-float-dom', 'Second printed label'),
|
||||
],
|
||||
scene: new TestPrintingScene() as unknown as Scene,
|
||||
skeleton: new TestPrintingSkeleton() as unknown as SpreadsheetSkeleton,
|
||||
worksheet: new TestPrintingWorksheet() as unknown as Worksheet,
|
||||
}, root, currentTestBed!.injector);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(root.textContent).toContain('First printed label');
|
||||
expect(root.textContent).toContain('Second printed label');
|
||||
});
|
||||
|
||||
it('forwards printing-only component props', () => {
|
||||
const info = {
|
||||
...createFloatDomInfo(),
|
||||
props: { printingLabel: 'Ready callback props' },
|
||||
} as IFloatDomData;
|
||||
|
||||
const [, floatDom] = createPrintingFloatDom(
|
||||
info,
|
||||
new TestPrintingScene() as unknown as Scene,
|
||||
new TestPrintingSkeleton() as unknown as SpreadsheetSkeleton,
|
||||
new TestPrintingWorksheet() as unknown as Worksheet
|
||||
);
|
||||
|
||||
expect(floatDom.props).toEqual({ printingLabel: 'Ready callback props' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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 { IRange } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { Button, Checkbox, CheckboxGroup, FormLayout, Select } from '@univerjs/design';
|
||||
import { useHighlightRange } from '@univerjs/sheets-ui';
|
||||
import { IDialogService, useDependency } from '@univerjs/ui';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { FileNamePart, IBatchSaveImagesService } from '../../services/batch-save-images.service';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from './component-name';
|
||||
|
||||
export interface IBatchSaveImagesDialogProps {
|
||||
SelectComponent?: typeof Select;
|
||||
}
|
||||
|
||||
export function BatchSaveImagesDialog({ SelectComponent = Select }: IBatchSaveImagesDialogProps = {}) {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const dialogService = useDependency(IDialogService);
|
||||
const batchSaveService = useDependency(IBatchSaveImagesService);
|
||||
|
||||
const [fileNameParts, setFileNameParts] = useState<Array<string | number | boolean>>([FileNamePart.CELL_ADDRESS]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const images = useMemo(() => batchSaveService.getCellImagesInSelection(), [batchSaveService]);
|
||||
const dataColumns = useMemo(() => batchSaveService.getDataColumns(), [batchSaveService]);
|
||||
const rowRange = useMemo(() => batchSaveService.getSelectionRowRange(), [batchSaveService]);
|
||||
|
||||
// Check if there are available columns to select (columns outside the selection range)
|
||||
const hasAvailableColumns = dataColumns.length > 0;
|
||||
|
||||
const columnOptions = useMemo(() => {
|
||||
return dataColumns.map((col) => ({
|
||||
label: col.label,
|
||||
value: String(col.index),
|
||||
}));
|
||||
}, [dataColumns]);
|
||||
|
||||
const [selectedColumn, setSelectedColumn] = useState<string>(
|
||||
() => columnOptions.length > 0 ? columnOptions[0].value : '0'
|
||||
);
|
||||
|
||||
// Calculate highlight range based on selected column and original selection row range
|
||||
const highlightRanges = useMemo<IRange[]>(() => {
|
||||
const showColumnSelect = fileNameParts.includes(FileNamePart.COLUMN_VALUE);
|
||||
if (!showColumnSelect || !rowRange) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const colIndex = Number(selectedColumn);
|
||||
return [{
|
||||
startRow: rowRange.startRow,
|
||||
endRow: rowRange.endRow,
|
||||
startColumn: colIndex,
|
||||
endColumn: colIndex,
|
||||
}];
|
||||
}, [fileNameParts, selectedColumn, rowRange]);
|
||||
|
||||
// Highlight the selected column range
|
||||
useHighlightRange(highlightRanges);
|
||||
|
||||
const handleFileNamePartsChange = useCallback((value: Array<string | number | boolean>) => {
|
||||
// Ensure at least one option is selected
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
setFileNameParts(value);
|
||||
}, []);
|
||||
|
||||
const handleColumnChange = useCallback((value: string | number | boolean) => {
|
||||
setSelectedColumn(String(value));
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dialogService.close(BATCH_SAVE_IMAGES_DIALOG_ID);
|
||||
}, [dialogService]);
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await batchSaveService.saveImages(images, {
|
||||
fileNameParts: fileNameParts as FileNamePart[],
|
||||
columnIndex: fileNameParts.includes(FileNamePart.COLUMN_VALUE) ? Number(selectedColumn) : undefined,
|
||||
});
|
||||
|
||||
dialogService.close(BATCH_SAVE_IMAGES_DIALOG_ID);
|
||||
} catch (err) {
|
||||
console.error('Failed to save images:', err);
|
||||
setError(localeService.t<LocaleKey>('sheets-drawing-ui.save.error'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [batchSaveService, images, fileNameParts, selectedColumn, dialogService, localeService]);
|
||||
|
||||
const showColumnSelect = fileNameParts.includes(FileNamePart.COLUMN_VALUE);
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-flex-col">
|
||||
<FormLayout label={localeService.t<LocaleKey>('sheets-drawing-ui.save.imageCount')}>
|
||||
<div className="univer-text-sm univer-text-gray-600">{images.length}</div>
|
||||
</FormLayout>
|
||||
|
||||
<FormLayout label={localeService.t<LocaleKey>('sheets-drawing-ui.save.fileNameConfig')}>
|
||||
<CheckboxGroup value={fileNameParts} onChange={handleFileNamePartsChange} direction="vertical">
|
||||
<Checkbox value={FileNamePart.CELL_ADDRESS} disabled={!hasAvailableColumns}>
|
||||
{localeService.t<LocaleKey>('sheets-drawing-ui.save.useRowCol')}
|
||||
</Checkbox>
|
||||
{hasAvailableColumns && (
|
||||
<Checkbox value={FileNamePart.COLUMN_VALUE}>
|
||||
{localeService.t<LocaleKey>('sheets-drawing-ui.save.useColumnValue')}
|
||||
</Checkbox>
|
||||
)}
|
||||
</CheckboxGroup>
|
||||
</FormLayout>
|
||||
|
||||
{showColumnSelect && (
|
||||
<FormLayout label={localeService.t<LocaleKey>('sheets-drawing-ui.save.selectColumn')}>
|
||||
<SelectComponent
|
||||
value={selectedColumn}
|
||||
options={columnOptions}
|
||||
onChange={handleColumnChange}
|
||||
/>
|
||||
</FormLayout>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="univer-text-xs univer-text-red-500">{error}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-justify-end univer-gap-2 univer-border-t univer-border-gray-200 univer-pt-3
|
||||
`}
|
||||
>
|
||||
<Button onClick={handleCancel} disabled={saving}>
|
||||
{localeService.t<LocaleKey>('sheets-drawing-ui.save.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
disabled={saving || images.length === 0}
|
||||
>
|
||||
{saving ? localeService.t<LocaleKey>('sheets-drawing-ui.save.saving') : localeService.t<LocaleKey>('sheets-drawing-ui.save.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { MobileSelect } from '@univerjs/design';
|
||||
import { BatchSaveImagesDialog } from './BatchSaveImagesDialog';
|
||||
|
||||
export function MobileBatchSaveImagesDialog() {
|
||||
return <BatchSaveImagesDialog SelectComponent={MobileSelect} />;
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* 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 { IDisposable, IRange } from '@univerjs/core';
|
||||
import type { ISelectionWithStyle } from '@univerjs/sheets';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import type { IBatchSaveImagesConfig, ICellImageInfo } from '../../../services/batch-save-images.service';
|
||||
import { Disposable, ImageSourceType, toDisposable } from '@univerjs/core';
|
||||
import { IMarkSelectionService } from '@univerjs/sheets-ui';
|
||||
import { IDialogService, RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createSheetsDrawingUiTestBed } from '../../../__tests__/create-sheets-drawing-ui-test-bed';
|
||||
import { FileNamePart, IBatchSaveImagesService } from '../../../services/batch-save-images.service';
|
||||
import { BatchSaveImagesDialog } from '../BatchSaveImagesDialog';
|
||||
import { BATCH_SAVE_IMAGES_DIALOG_ID } from '../component-name';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
class TestDialogService extends Disposable implements IDialogService {
|
||||
private readonly _closedIds: string[] = [];
|
||||
private readonly _dialogs$ = new BehaviorSubject<any[]>([]);
|
||||
|
||||
open(): IDisposable {
|
||||
return toDisposable(() => undefined);
|
||||
}
|
||||
|
||||
close(id: string): void {
|
||||
this._closedIds.push(id);
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
this._closedIds.push('*');
|
||||
}
|
||||
|
||||
getDialogs$() {
|
||||
return this._dialogs$.asObservable();
|
||||
}
|
||||
|
||||
get closedIds(): string[] {
|
||||
return this._closedIds;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._dialogs$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class TestMarkSelectionService implements IMarkSelectionService {
|
||||
private _nextId = 1;
|
||||
private readonly _shapeMap = new Map<string, ISelectionWithStyle>();
|
||||
private readonly _addedRanges: IRange[] = [];
|
||||
private readonly _removedIds: string[] = [];
|
||||
|
||||
addShape(selection: ISelectionWithStyle): string {
|
||||
const id = `shape-${this._nextId++}`;
|
||||
this._shapeMap.set(id, selection);
|
||||
this._addedRanges.push(selection.range);
|
||||
return id;
|
||||
}
|
||||
|
||||
addShapeWithNoFresh(selection: ISelectionWithStyle): string {
|
||||
return this.addShape(selection);
|
||||
}
|
||||
|
||||
removeShape(id: string): void {
|
||||
this._shapeMap.delete(id);
|
||||
this._removedIds.push(id);
|
||||
}
|
||||
|
||||
removeAllShapes(): void {
|
||||
this._shapeMap.clear();
|
||||
}
|
||||
|
||||
refreshShapes(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getShapeMap(): ReturnType<IMarkSelectionService['getShapeMap']> {
|
||||
return this._shapeMap as unknown as ReturnType<IMarkSelectionService['getShapeMap']>;
|
||||
}
|
||||
|
||||
get addedRanges(): IRange[] {
|
||||
return this._addedRanges;
|
||||
}
|
||||
|
||||
get activeRanges(): IRange[] {
|
||||
return Array.from(this._shapeMap.values()).map((selection) => selection.range);
|
||||
}
|
||||
|
||||
get removedIds(): string[] {
|
||||
return this._removedIds;
|
||||
}
|
||||
}
|
||||
|
||||
class TestBatchSaveImagesService implements IBatchSaveImagesService {
|
||||
private readonly _images = [
|
||||
createImage('image-a', 'B3', 2, 1),
|
||||
createImage('image-b', 'B4', 3, 1),
|
||||
];
|
||||
|
||||
private readonly _dataColumns = [
|
||||
{ index: 4, label: 'Name' },
|
||||
{ index: 6, label: 'SKU' },
|
||||
];
|
||||
|
||||
private _failSaving = false;
|
||||
private _savedImages: ICellImageInfo[] = [];
|
||||
private _savedConfig: IBatchSaveImagesConfig | null = null;
|
||||
|
||||
failSaving(): void {
|
||||
this._failSaving = true;
|
||||
}
|
||||
|
||||
get savedImages(): ICellImageInfo[] {
|
||||
return this._savedImages;
|
||||
}
|
||||
|
||||
get savedConfig(): IBatchSaveImagesConfig | null {
|
||||
return this._savedConfig;
|
||||
}
|
||||
|
||||
getCellImagesInSelection(): ICellImageInfo[] {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
getCellImagesFromRanges(): ICellImageInfo[] {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
getDataColumns(): Array<{ index: number; label: string }> {
|
||||
return this._dataColumns;
|
||||
}
|
||||
|
||||
getDataColumnsForRanges(): Array<{ index: number; label: string }> {
|
||||
return this._dataColumns;
|
||||
}
|
||||
|
||||
getSelectionRangeNotation(): string {
|
||||
return 'B3:B4';
|
||||
}
|
||||
|
||||
generateFileName(imageInfo: ICellImageInfo): string {
|
||||
return imageInfo.cellAddress;
|
||||
}
|
||||
|
||||
generateFileNameWithContext(imageInfo: ICellImageInfo): string {
|
||||
return imageInfo.cellAddress;
|
||||
}
|
||||
|
||||
async saveImages(images: ICellImageInfo[], config: IBatchSaveImagesConfig): Promise<void> {
|
||||
if (this._failSaving) {
|
||||
throw new Error('save failed');
|
||||
}
|
||||
|
||||
this._savedImages = images;
|
||||
this._savedConfig = config;
|
||||
}
|
||||
|
||||
async saveImagesWithContext(images: ICellImageInfo[], config: IBatchSaveImagesConfig): Promise<void> {
|
||||
await this.saveImages(images, config);
|
||||
}
|
||||
|
||||
async downloadSingleImage(): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getSelectionRowRange(): { startRow: number; endRow: number } {
|
||||
return { startRow: 2, endRow: 3 };
|
||||
}
|
||||
|
||||
getSelectionColumnIndices(): Set<number> {
|
||||
return new Set([1]);
|
||||
}
|
||||
}
|
||||
|
||||
class TestEmptyBatchSaveImagesService extends TestBatchSaveImagesService {
|
||||
override getCellImagesInSelection(): ICellImageInfo[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
override getCellImagesFromRanges(): ICellImageInfo[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestNoDataColumnsBatchSaveImagesService extends TestBatchSaveImagesService {
|
||||
override getDataColumns(): Array<{ index: number; label: string }> {
|
||||
return [];
|
||||
}
|
||||
|
||||
override getDataColumnsForRanges(): Array<{ index: number; label: string }> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function createImage(imageId: string, cellAddress: string, row: number, col: number): ICellImageInfo {
|
||||
return {
|
||||
row,
|
||||
col,
|
||||
cellAddress,
|
||||
imageId,
|
||||
source: `https://example.com/${imageId}.png`,
|
||||
imageSourceType: ImageSourceType.URL,
|
||||
};
|
||||
}
|
||||
|
||||
function getButton(container: HTMLElement, text: string): HTMLButtonElement {
|
||||
const button = Array.from(container.querySelectorAll('button')).find((button) => button.textContent === text);
|
||||
if (!button) {
|
||||
throw new Error(`Button ${text} was not rendered`);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
function getCheckbox(container: HTMLElement, index: number): HTMLInputElement {
|
||||
const checkbox = container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')[index];
|
||||
if (!checkbox) {
|
||||
throw new Error(`Checkbox ${index} was not rendered`);
|
||||
}
|
||||
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
function openSelect(select: HTMLElement): void {
|
||||
select.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
|
||||
}
|
||||
|
||||
describe('BatchSaveImagesDialog', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
});
|
||||
|
||||
async function renderDialog(batchSaveImagesService = TestBatchSaveImagesService) {
|
||||
const testBed = createSheetsDrawingUiTestBed(undefined, [
|
||||
[IDialogService, { useClass: TestDialogService }],
|
||||
[IMarkSelectionService, { useClass: TestMarkSelectionService }],
|
||||
[IBatchSaveImagesService, { useClass: batchSaveImagesService }],
|
||||
]);
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root!.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<BatchSaveImagesDialog />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
return {
|
||||
testBed,
|
||||
batchSaveService: testBed.get(IBatchSaveImagesService) as TestBatchSaveImagesService,
|
||||
dialogService: testBed.get(IDialogService) as unknown as TestDialogService,
|
||||
markSelectionService: testBed.get(IMarkSelectionService) as TestMarkSelectionService,
|
||||
};
|
||||
}
|
||||
|
||||
it('saves selected images with the chosen column value in file names', async () => {
|
||||
const { testBed, batchSaveService, dialogService, markSelectionService } = await renderDialog();
|
||||
const columnValueCheckbox = getCheckbox(container!, 1);
|
||||
|
||||
await act(async () => {
|
||||
columnValueCheckbox.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([{
|
||||
startRow: 2,
|
||||
endRow: 3,
|
||||
startColumn: 4,
|
||||
endColumn: 4,
|
||||
}]);
|
||||
|
||||
await act(async () => {
|
||||
openSelect(container!.querySelector<HTMLElement>('[data-u-comp="select"]')!);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const skuOption = Array.from(document.body.querySelectorAll<HTMLElement>('[data-slot="dropdown-menu-radio-item"]'))
|
||||
.find((item) => item.textContent === 'SKU');
|
||||
if (!skuOption) {
|
||||
throw new Error('SKU column option was not rendered');
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
skuOption.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([{
|
||||
startRow: 2,
|
||||
endRow: 3,
|
||||
startColumn: 6,
|
||||
endColumn: 6,
|
||||
}]);
|
||||
expect(markSelectionService.removedIds).toEqual(['shape-1']);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'sheets-drawing-ui.save.confirm').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedImages.map((image) => image.imageId)).toEqual(['image-a', 'image-b']);
|
||||
expect(batchSaveService.savedConfig).toEqual({
|
||||
fileNameParts: [FileNamePart.CELL_ADDRESS, FileNamePart.COLUMN_VALUE],
|
||||
columnIndex: 6,
|
||||
});
|
||||
expect(dialogService.closedIds).toEqual([BATCH_SAVE_IMAGES_DIALOG_ID]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('removes the highlighted lookup column when column values are no longer used in file names', async () => {
|
||||
const { testBed, batchSaveService, dialogService, markSelectionService } = await renderDialog();
|
||||
const columnValueCheckbox = getCheckbox(container!, 1);
|
||||
|
||||
await act(async () => {
|
||||
columnValueCheckbox.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([{
|
||||
startRow: 2,
|
||||
endRow: 3,
|
||||
startColumn: 4,
|
||||
endColumn: 4,
|
||||
}]);
|
||||
|
||||
await act(async () => {
|
||||
columnValueCheckbox.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([]);
|
||||
expect(markSelectionService.removedIds).toEqual(['shape-1']);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'sheets-drawing-ui.save.confirm').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedImages.map((image) => image.imageId)).toEqual(['image-a', 'image-b']);
|
||||
expect(batchSaveService.savedConfig).toEqual({
|
||||
fileNameParts: [FileNamePart.CELL_ADDRESS],
|
||||
columnIndex: undefined,
|
||||
});
|
||||
expect(dialogService.closedIds).toEqual([BATCH_SAVE_IMAGES_DIALOG_ID]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps cell address naming when the user tries to clear the last file name option', async () => {
|
||||
const { testBed, batchSaveService, dialogService, markSelectionService } = await renderDialog();
|
||||
const cellAddressCheckbox = getCheckbox(container!, 0);
|
||||
|
||||
await act(async () => {
|
||||
cellAddressCheckbox.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([]);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'sheets-drawing-ui.save.confirm').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedImages.map((image) => image.imageId)).toEqual(['image-a', 'image-b']);
|
||||
expect(batchSaveService.savedConfig).toEqual({
|
||||
fileNameParts: [FileNamePart.CELL_ADDRESS],
|
||||
columnIndex: undefined,
|
||||
});
|
||||
expect(dialogService.closedIds).toEqual([BATCH_SAVE_IMAGES_DIALOG_ID]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('clears lookup column highlight without saving when the dialog is cancelled', async () => {
|
||||
const { testBed, batchSaveService, dialogService, markSelectionService } = await renderDialog();
|
||||
const columnValueCheckbox = getCheckbox(container!, 1);
|
||||
|
||||
await act(async () => {
|
||||
columnValueCheckbox.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([{
|
||||
startRow: 2,
|
||||
endRow: 3,
|
||||
startColumn: 4,
|
||||
endColumn: 4,
|
||||
}]);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'sheets-drawing-ui.save.cancel').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedConfig).toBeNull();
|
||||
expect(dialogService.closedIds).toEqual([BATCH_SAVE_IMAGES_DIALOG_ID]);
|
||||
|
||||
await act(async () => {
|
||||
root!.unmount();
|
||||
root = undefined;
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(markSelectionService.activeRanges).toEqual([]);
|
||||
expect(markSelectionService.removedIds).toEqual(['shape-1']);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps the dialog open and shows the save error when saving fails', async () => {
|
||||
const { testBed, batchSaveService, dialogService } = await renderDialog();
|
||||
batchSaveService.failSaving();
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'sheets-drawing-ui.save.confirm').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedConfig).toBeNull();
|
||||
expect(dialogService.closedIds).toEqual([]);
|
||||
expect(container!.textContent).toContain('sheets-drawing-ui.save.error');
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('saves by cell address when no lookup columns are available', async () => {
|
||||
const { testBed, batchSaveService, dialogService, markSelectionService } = await renderDialog(TestNoDataColumnsBatchSaveImagesService);
|
||||
|
||||
expect(container!.querySelector('[data-u-comp="select"]')).toBeNull();
|
||||
expect(container!.textContent).not.toContain('sheets-drawing-ui.save.useColumnValue');
|
||||
expect(markSelectionService.activeRanges).toEqual([]);
|
||||
|
||||
await act(async () => {
|
||||
getButton(container!, 'sheets-drawing-ui.save.confirm').click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedImages.map((image) => image.imageId)).toEqual(['image-a', 'image-b']);
|
||||
expect(batchSaveService.savedConfig).toEqual({
|
||||
fileNameParts: [FileNamePart.CELL_ADDRESS],
|
||||
columnIndex: undefined,
|
||||
});
|
||||
expect(dialogService.closedIds).toEqual([BATCH_SAVE_IMAGES_DIALOG_ID]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
|
||||
it('prevents confirming when the selection contains no images', async () => {
|
||||
const { testBed, batchSaveService, dialogService, markSelectionService } = await renderDialog(TestEmptyBatchSaveImagesService);
|
||||
const confirmButton = getButton(container!, 'sheets-drawing-ui.save.confirm');
|
||||
|
||||
expect(confirmButton.disabled).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
confirmButton.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(batchSaveService.savedConfig).toBeNull();
|
||||
expect(dialogService.closedIds).toEqual([]);
|
||||
expect(markSelectionService.activeRanges).toEqual([]);
|
||||
|
||||
testBed.univer.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 BATCH_SAVE_IMAGES_DIALOG_ID = 'sheet.dialog.batch-save-images';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user