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/docs-drawing-ui/README.md
Normal file
41
packages/docs-drawing-ui/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/docs-drawing-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/docs-drawing-ui)
|
||||
[](https://npmjs.com/package/@univerjs/docs-drawing-ui)
|
||||
[](https://npmjs.com/package/@univerjs/docs-drawing-ui)
|
||||
|
||||
`@univerjs/docs-drawing-ui` adds the user interface for creating, selecting, editing, and rendering drawing objects in Univer Docs.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/docs-drawing-ui` | `UniverDocsDrawingUi` | Yes | Yes | No |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/docs-drawing-ui
|
||||
# or
|
||||
npm install @univerjs/docs-drawing-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/docs-drawing-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/docs-drawing-ui/locale/en-US';
|
||||
import { UniverDocsDrawingUIPlugin } from '@univerjs/docs-drawing-ui';
|
||||
|
||||
univer.registerPlugin(UniverDocsDrawingUIPlugin);
|
||||
|
||||
// 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/docs-drawing-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
99
packages/docs-drawing-ui/package.json
Normal file
99
packages/docs-drawing-ui/package.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"name": "@univerjs/docs-drawing-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Drawing UI integration for Univer Docs.",
|
||||
"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",
|
||||
"docs",
|
||||
"drawing",
|
||||
"ui",
|
||||
"plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*",
|
||||
"./locale/*": "./src/locale/*.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"
|
||||
},
|
||||
"./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/icons": "1.38.0",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"@univerjs/protocol": "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/docs-drawing-ui/postcss.config.mjs
Normal file
1
packages/docs-drawing-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
197
packages/docs-drawing-ui/src/__tests__/create-doc-ui-test-bed.ts
Normal file
197
packages/docs-drawing-ui/src/__tests__/create-doc-ui-test-bed.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
|
||||
import type { Dependency, DocumentDataModel, IDocumentData, Nullable } from '@univerjs/core';
|
||||
import type { DocumentSkeleton, IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import {
|
||||
BooleanNumber,
|
||||
DisposableCollection,
|
||||
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
RxDisposable,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DocSelectionManagerService,
|
||||
DocSkeletonManagerService,
|
||||
DocStateChangeManagerService,
|
||||
DocStateEmitService,
|
||||
} from '@univerjs/docs';
|
||||
import { DocIMEInputManagerService, DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { DocumentViewModel, IRenderManagerService, RenderManagerService } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject, takeUntil } from 'rxjs';
|
||||
|
||||
const DEFAULT_DOC_DATA: IDocumentData = {
|
||||
id: 'test-doc',
|
||||
body: {
|
||||
dataStream: 'Hello world\r\n',
|
||||
textRuns: [{
|
||||
st: 0,
|
||||
ed: 11,
|
||||
ts: {
|
||||
bl: BooleanNumber.FALSE,
|
||||
},
|
||||
}],
|
||||
paragraphs: [{ startIndex: 11, paragraphId: 'para_drawing_test' }],
|
||||
sectionBreaks: [],
|
||||
customBlocks: [],
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 594.3,
|
||||
height: 840.51,
|
||||
},
|
||||
marginTop: 72,
|
||||
marginBottom: 72,
|
||||
marginRight: 90,
|
||||
marginLeft: 90,
|
||||
},
|
||||
};
|
||||
|
||||
export function createDocUiTestBed(docData?: IDocumentData, 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';
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.get(IUndoRedoService);
|
||||
if (!dependencies?.some((dependency) => Array.isArray(dependency) && dependency[0] === IRenderManagerService)) {
|
||||
this._injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
}
|
||||
this._injector.add([DocSelectionManagerService]);
|
||||
this._injector.add([DocStateEmitService]);
|
||||
this._injector.add([DocStateChangeManagerService]);
|
||||
this._injector.add([DocIMEInputManagerService]);
|
||||
this._injector.add([DocSelectionRenderService]);
|
||||
dependencies?.forEach((dependency) => this._injector.add(dependency));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this._injector.get(DocStateChangeManagerService);
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const doc = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, docData || DEFAULT_DOC_DATA);
|
||||
const univerInstanceService = get(IUniverInstanceService);
|
||||
const fakeDocSkeletonManager = new MockDocSkeletonManagerService({
|
||||
unit: doc,
|
||||
unitId: doc.getUnitId(),
|
||||
type: UniverInstanceType.UNIVER_DOC,
|
||||
engine: null as any,
|
||||
scene: null as any,
|
||||
mainComponent: null as any,
|
||||
components: null as any,
|
||||
isMainScene: true,
|
||||
activated$: new BehaviorSubject(true),
|
||||
activate: () => {},
|
||||
deactivate: () => {},
|
||||
}, univerInstanceService);
|
||||
|
||||
injector.add([DocSkeletonManagerService, { useValue: fakeDocSkeletonManager as unknown as DocSkeletonManagerService }]);
|
||||
const renderManagerService = get(IRenderManagerService);
|
||||
if (renderManagerService.getRenderUnitById(doc.getUnitId()) == null) {
|
||||
renderManagerService.addRender(doc.getUnitId(), {
|
||||
unitId: doc.getUnitId(),
|
||||
type: UniverInstanceType.UNIVER_DOC,
|
||||
engine: new DisposableCollection() as any,
|
||||
scene: new DisposableCollection() as any,
|
||||
mainComponent: null as any,
|
||||
components: new Map(),
|
||||
isMainScene: true,
|
||||
activated$: new BehaviorSubject(true),
|
||||
with: injector.get.bind(injector),
|
||||
activate: () => {},
|
||||
deactivate: () => {},
|
||||
isDisposed: () => false,
|
||||
});
|
||||
}
|
||||
univerInstanceService.focusUnit(doc.getUnitId());
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
injector,
|
||||
get,
|
||||
doc,
|
||||
};
|
||||
}
|
||||
|
||||
class MockDocSkeletonManagerService extends RxDisposable implements IRenderModule {
|
||||
private _docViewModel!: DocumentViewModel;
|
||||
private readonly _currentSkeleton$ = new BehaviorSubject<Nullable<DocumentSkeleton>>(null);
|
||||
readonly currentSkeleton$ = this._currentSkeleton$.asObservable();
|
||||
private readonly _currentSkeletonBefore$ = new BehaviorSubject<Nullable<DocumentSkeleton>>(null);
|
||||
readonly currentSkeletonBefore$ = this._currentSkeletonBefore$.asObservable();
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<DocumentDataModel>,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
|
||||
) {
|
||||
super();
|
||||
this._update();
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC)
|
||||
.pipe(takeUntil(this.dispose$))
|
||||
.subscribe((documentModel) => {
|
||||
if (documentModel?.getUnitId() === this._context.unitId) {
|
||||
this._update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _update() {
|
||||
const documentDataModel = this._context.unit;
|
||||
const unitId = this._context.unitId;
|
||||
|
||||
if (documentDataModel.getBody() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._docViewModel && unitId === DOCS_NORMAL_EDITOR_UNIT_ID_KEY) {
|
||||
this._docViewModel.reset(documentDataModel);
|
||||
} else if (!this._docViewModel) {
|
||||
this._docViewModel = new DocumentViewModel(documentDataModel);
|
||||
}
|
||||
}
|
||||
|
||||
getSkeleton(): DocumentSkeleton {
|
||||
throw new Error('[MockDocSkeletonManagerService]: cannot access to doc skeleton in unit tests!');
|
||||
}
|
||||
|
||||
getViewModel(): DocumentViewModel {
|
||||
return this._docViewModel;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { ICommandService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from '../insert-shape.command';
|
||||
|
||||
function createAccessor() {
|
||||
const executeCommand = vi.fn(async () => true);
|
||||
const getCurrentUnitOfType = vi.fn((type: UniverInstanceType) => {
|
||||
if (type !== UniverInstanceType.UNIVER_DOC) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
getUnitId: () => 'test-doc',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
executeCommand,
|
||||
accessor: {
|
||||
get: (token: unknown) => {
|
||||
if (token === ICommandService) {
|
||||
return {
|
||||
executeCommand,
|
||||
};
|
||||
}
|
||||
|
||||
if (token === IUniverInstanceService) {
|
||||
return {
|
||||
getCurrentUnitOfType,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected token: ${String(token)}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('insert shape commands', () => {
|
||||
it('inserts a rectangle shape through the drawing insertion command', async () => {
|
||||
const { accessor, executeCommand } = createAccessor();
|
||||
|
||||
await InsertDocRectangleShapeCommand.handler(accessor as never);
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledTimes(1);
|
||||
expect(executeCommand).toHaveBeenCalledWith(
|
||||
InsertDocDrawingCommand.id,
|
||||
expect.objectContaining({
|
||||
unitId: 'test-doc',
|
||||
drawings: [
|
||||
expect.objectContaining({
|
||||
source: expect.stringContaining('data:image/svg+xml'),
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts an ellipse shape through the drawing insertion command', async () => {
|
||||
const { accessor, executeCommand } = createAccessor();
|
||||
|
||||
await InsertDocEllipseShapeCommand.handler(accessor as never);
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledTimes(1);
|
||||
expect(executeCommand).toHaveBeenCalledWith(
|
||||
InsertDocDrawingCommand.id,
|
||||
expect.objectContaining({
|
||||
unitId: 'test-doc',
|
||||
drawings: [
|
||||
expect.objectContaining({
|
||||
source: expect.stringContaining('data:image/svg+xml'),
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { groupToUngroup, ungroupToGroup } from '../utils';
|
||||
|
||||
vi.mock('@univerjs/engine-render', () => ({
|
||||
getGroupState: vi.fn(() => ({ left: 10, top: 20, width: 100, height: 50 })),
|
||||
transformObjectOutOfGroup: vi.fn((transform, groupTransform) => ({
|
||||
left: (transform.left ?? 0) + (groupTransform.left ?? 0),
|
||||
top: (transform.top ?? 0) + (groupTransform.top ?? 0),
|
||||
width: transform.width,
|
||||
height: transform.height,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('docs drawing command utils', () => {
|
||||
it('rebuilds group metadata from ungrouped drawings', () => {
|
||||
expect(ungroupToGroup([
|
||||
{
|
||||
parent: { unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'group-1' },
|
||||
children: [
|
||||
{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'child-1', transform: { left: 2, top: 3 } },
|
||||
{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'child-2', transform: { left: 4, top: 5 } },
|
||||
],
|
||||
},
|
||||
] as never)).toEqual([
|
||||
{
|
||||
parent: {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'group-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: { left: 10, top: 20, width: 100, height: 50 },
|
||||
groupBaseBound: { left: 10, top: 20, width: 100, height: 50 },
|
||||
},
|
||||
children: [
|
||||
{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'child-1', transform: { left: 2, top: 3 }, groupId: 'group-1' },
|
||||
{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'child-2', transform: { left: 4, top: 5 }, groupId: 'group-1' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('projects grouped children back out of the group transform', () => {
|
||||
expect(groupToUngroup([
|
||||
{
|
||||
parent: {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'group-1',
|
||||
transform: { left: 10, top: 20, width: 100, height: 50 },
|
||||
groupBaseBound: { left: 10, top: 20, width: 100, height: 50 },
|
||||
},
|
||||
children: [
|
||||
{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'child-1', transform: { left: 2, top: 3, width: 4, height: 5 } },
|
||||
],
|
||||
},
|
||||
] as never)).toEqual([
|
||||
{
|
||||
parent: {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'group-1',
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: { left: 0, top: 0 },
|
||||
},
|
||||
children: [
|
||||
{
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'child-1',
|
||||
transform: { left: 12, top: 23, width: 4, height: 5 },
|
||||
groupId: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 { IDocDrawing, IRemoveDocDrawingCommandParams } from '@univerjs/docs-drawing';
|
||||
import { CommandType, ICommandService } from '@univerjs/core';
|
||||
import { IDocDrawingService, RemoveDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
|
||||
export const DeleteDocDrawingsCommand: ICommand = {
|
||||
id: 'doc.command.delete-drawing',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const docDrawingService = accessor.get(IDocDrawingService);
|
||||
|
||||
const drawings = docDrawingService.getFocusDrawings();
|
||||
|
||||
if (drawings.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId } = drawings[0];
|
||||
const selectionRenderService = accessor.get(IRenderManagerService)
|
||||
.getRenderUnitById(unitId)
|
||||
?.with(DocSelectionRenderService);
|
||||
|
||||
// Keep the command and the immediately following shortcut on the document that owns the drawing.
|
||||
selectionRenderService?.focus();
|
||||
|
||||
const newDrawings = drawings.map((drawing) => {
|
||||
const { unitId, subUnitId, drawingId, drawingType } = drawing as IDocDrawing;
|
||||
|
||||
return {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
drawingType,
|
||||
};
|
||||
});
|
||||
const removed = await commandService.executeCommand<IRemoveDocDrawingCommandParams>(RemoveDocDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: newDrawings,
|
||||
});
|
||||
if (removed) {
|
||||
selectionRenderService?.focus();
|
||||
}
|
||||
return removed;
|
||||
},
|
||||
};
|
||||
@@ -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 type { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IDrawingGroupUpdateParam } from '@univerjs/drawing';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
// import { IDocDrawingService } from '@univerjs/docs-drawing';
|
||||
|
||||
/**
|
||||
* The command to insert new defined name
|
||||
*/
|
||||
export const GroupDocDrawingCommand: ICommand = {
|
||||
id: 'doc.command.group-doc-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: IDrawingGroupUpdateParam[]) => {
|
||||
// const docDrawingService = accessor.get(IDocDrawingService);
|
||||
|
||||
// if (!params) return false;
|
||||
|
||||
// // if the subunit is not a doc type, 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 = docDrawingService.getGroupDrawingOp(params) as IDrawingJsonUndo1;
|
||||
|
||||
// const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
// const result = commandService.syncExecuteCommand(SetDocDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type: DocDrawingApplyType.GROUP });
|
||||
|
||||
// if (result) {
|
||||
// undoRedoService.pushUndoRedo({
|
||||
// unitID: unitId,
|
||||
// undoMutations: [
|
||||
// { id: SetDocDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects: groupToUngroup(objects as IDrawingGroupUpdateParam[]), type: DocDrawingApplyType.UNGROUP } },
|
||||
// { id: ClearDocDrawingTransformerOperation.id, params: unitIds },
|
||||
// ],
|
||||
// redoMutations: [
|
||||
// { id: SetDocDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type: DocDrawingApplyType.GROUP } },
|
||||
// { id: ClearDocDrawingTransformerOperation.id, params: unitIds },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand, Nullable } from '@univerjs/core';
|
||||
import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { getCurrentTypeOfRenderer, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { DocDrawingUpdateRenderController } from '../../controllers/render-controllers/doc-drawing-update.render-controller';
|
||||
|
||||
export interface IInsertDocImageCommandParams {
|
||||
files: Nullable<File[]>;
|
||||
};
|
||||
|
||||
export const InsertDocImageCommand: ICommand<IInsertDocImageCommandParams> = {
|
||||
id: 'doc.command.insert-float-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
return getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_DOC, univerInstanceService, renderManagerService)
|
||||
?.with(DocDrawingUpdateRenderController)
|
||||
.insertDocImage() ?? false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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 { IInsertDocDrawingCommandParams } from '@univerjs/docs-drawing';
|
||||
import {
|
||||
BooleanNumber,
|
||||
CommandType,
|
||||
DrawingTypeEnum,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
ImageSourceType,
|
||||
IUniverInstanceService,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { buildDocTransform, docDrawingPositionToTransform } from '@univerjs/docs';
|
||||
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
|
||||
type DocShapeKind = 'rectangle' | 'ellipse';
|
||||
|
||||
const DOC_SHAPE_FILL = '#3A60F7';
|
||||
const DOC_SHAPE_STROKE = '#1D3EA6';
|
||||
|
||||
function createShapeSvgSource(shape: DocShapeKind, width: number, height: number) {
|
||||
const content = shape === 'ellipse'
|
||||
? `<ellipse cx="${width / 2}" cy="${height / 2}" rx="${(width - 8) / 2}" ry="${(height - 8) / 2}" fill="${DOC_SHAPE_FILL}" stroke="${DOC_SHAPE_STROKE}" stroke-width="4" />`
|
||||
: `<rect x="4" y="4" width="${width - 8}" height="${height - 8}" rx="14" fill="${DOC_SHAPE_FILL}" stroke="${DOC_SHAPE_STROKE}" stroke-width="4" />`;
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" fill="none">${content}</svg>`;
|
||||
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function createShapeInsertCommand(shape: DocShapeKind, width: number, height: number, id: string): ICommand {
|
||||
return {
|
||||
id,
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const unitId = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)?.getUnitId();
|
||||
|
||||
if (!unitId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const docTransform = buildDocTransform(width, height);
|
||||
|
||||
return commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: [{
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId: generateRandomId(6),
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
source: createShapeSvgSource(shape, width, height),
|
||||
transform: docDrawingPositionToTransform(docTransform),
|
||||
docTransform,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
title: '',
|
||||
description: '',
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
wrapText: WrapTextType.BOTH_SIDES,
|
||||
distB: 0,
|
||||
distL: 0,
|
||||
distR: 0,
|
||||
distT: 0,
|
||||
}],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const InsertDocRectangleShapeCommand = createShapeInsertCommand(
|
||||
'rectangle',
|
||||
144,
|
||||
96,
|
||||
'doc.command.insert-float-shape.rectangle'
|
||||
);
|
||||
|
||||
export const InsertDocEllipseShapeCommand = createShapeInsertCommand(
|
||||
'ellipse',
|
||||
112,
|
||||
112,
|
||||
'doc.command.insert-float-shape.ellipse'
|
||||
);
|
||||
@@ -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 { DocumentDataModel, IAccessor, ICommand, IObjectPositionH, IObjectPositionV } from '@univerjs/core';
|
||||
import type { IDocDrawing, IDrawingDocTransform, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
|
||||
import type { IDocumentSkeletonCached } from '@univerjs/engine-render';
|
||||
import {
|
||||
CommandType,
|
||||
Direction,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSkeletonManagerService } from '@univerjs/docs';
|
||||
import { IDocDrawingService, UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { findDrawingAnchorInPage, resolveDrawingAnchorOffsets } from '../../utils/drawing-anchor-position';
|
||||
|
||||
export interface IMoveDrawingsCommandParams {
|
||||
direction: Direction;
|
||||
}
|
||||
|
||||
export const MoveDocDrawingsCommand: ICommand = {
|
||||
|
||||
id: 'doc.command.move-drawing',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params: IMoveDrawingsCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const docDrawingService = accessor.get(IDocDrawingService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
|
||||
const { direction } = params;
|
||||
|
||||
const drawings = docDrawingService.getFocusDrawings();
|
||||
|
||||
if (drawings.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unitId = drawings[0].unitId;
|
||||
|
||||
const renderObject = renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
if (scene == null) {
|
||||
return false;
|
||||
}
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
const skeletonData = renderObject?.with(DocSkeletonManagerService).getSkeleton()?.getSkeletonData();
|
||||
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
const newDrawings = drawings.map((drawing) => {
|
||||
const { drawingId } = drawing as IDocDrawing;
|
||||
const drawingData = documentDataModel?.getSnapshot().drawings?.[drawingId];
|
||||
|
||||
// Inline drawing can not be moved by shortcut.
|
||||
if (drawingData == null || drawingData.layoutType === PositionedObjectLayoutType.INLINE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { positionH, positionV } = drawingData.docTransform;
|
||||
const anchorLookup = skeletonData == null
|
||||
? { anchor: null, previewOnly: false }
|
||||
: findDrawingAnchor(skeletonData, drawingId);
|
||||
if (anchorLookup.previewOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { anchor } = anchorLookup;
|
||||
const offsets = anchor == null
|
||||
? { horizontal: positionH.posOffset ?? 0, vertical: positionV.posOffset ?? 0 }
|
||||
: resolveDrawingAnchorOffsets(anchor, positionH, positionV);
|
||||
const newPositionH = {
|
||||
...(positionH.relativeFrom == null ? {} : { relativeFrom: positionH.relativeFrom }),
|
||||
posOffset: offsets.horizontal,
|
||||
} as IObjectPositionH;
|
||||
const newPositionV = {
|
||||
...(positionV.relativeFrom == null ? {} : { relativeFrom: positionV.relativeFrom }),
|
||||
posOffset: offsets.vertical,
|
||||
} as IObjectPositionV;
|
||||
|
||||
if (direction === Direction.UP) {
|
||||
newPositionV.posOffset = (newPositionV.posOffset ?? 0) - 2;
|
||||
} else if (direction === Direction.DOWN) {
|
||||
newPositionV.posOffset = (newPositionV.posOffset ?? 0) + 2;
|
||||
} else if (direction === Direction.LEFT) {
|
||||
newPositionH.posOffset = (newPositionH.posOffset ?? 0) - 2;
|
||||
} else if (direction === Direction.RIGHT) {
|
||||
newPositionH.posOffset = (newPositionH.posOffset ?? 0) + 2;
|
||||
}
|
||||
|
||||
return {
|
||||
drawingId,
|
||||
key: direction === Direction.UP || direction === Direction.DOWN ? 'positionV' : 'positionH',
|
||||
value: direction === Direction.UP || direction === Direction.DOWN ? newPositionV : newPositionH,
|
||||
};
|
||||
}).filter((drawing) => drawing != null) as IDrawingDocTransform[];
|
||||
|
||||
if (newDrawings.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = commandService.syncExecuteCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings: newDrawings,
|
||||
});
|
||||
|
||||
transformer.refreshControls();
|
||||
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
|
||||
function findDrawingAnchor(
|
||||
skeletonData: IDocumentSkeletonCached,
|
||||
drawingId: string
|
||||
) {
|
||||
let foundInPreview = false;
|
||||
|
||||
for (const page of skeletonData.pages) {
|
||||
const bodyAnchor = findDrawingAnchorInPage(page, drawingId, page.marginTop, page.marginLeft);
|
||||
const header = page.headerId == null
|
||||
? undefined
|
||||
: skeletonData.skeHeaders.get(page.headerId)?.get(page.pageWidth);
|
||||
const headerAnchor = header == null
|
||||
? null
|
||||
: findDrawingAnchorInPage(header, drawingId, header.marginTop, page.marginLeft);
|
||||
const footer = page.footerId == null
|
||||
? undefined
|
||||
: skeletonData.skeFooters.get(page.footerId)?.get(page.pageWidth);
|
||||
const footerTop = footer == null ? 0 : page.pageHeight - page.marginBottom + footer.marginTop;
|
||||
const footerAnchor = footer == null
|
||||
? null
|
||||
: findDrawingAnchorInPage(footer, drawingId, footerTop, page.marginLeft);
|
||||
const anchor = bodyAnchor ?? headerAnchor ?? footerAnchor;
|
||||
if (anchor == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!page.isLayoutPlaceholder && !page.isMaterializationPlaceholder) {
|
||||
return { anchor, previewOnly: false };
|
||||
}
|
||||
|
||||
foundInPreview = true;
|
||||
}
|
||||
|
||||
return { anchor: null, previewOnly: foundInPreview };
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IDrawingGroupUpdateParam, IDrawingJsonUndo1 } from '@univerjs/drawing';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IDocDrawingService } from '@univerjs/docs-drawing';
|
||||
|
||||
/**
|
||||
* The command to insert new defined name
|
||||
*/
|
||||
export const UngroupDocDrawingCommand: ICommand = {
|
||||
id: 'doc.command.ungroup-doc-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor: IAccessor, params?: IDrawingGroupUpdateParam[]) => {
|
||||
const docDrawingService = accessor.get(IDocDrawingService);
|
||||
|
||||
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 = docDrawingService.getUngroupDrawingOp(params) as IDrawingJsonUndo1;
|
||||
|
||||
const { unitId, subUnitId, undo, redo, objects } = jsonOp;
|
||||
|
||||
// const result = commandService.syncExecuteCommand(SetDocDrawingApplyMutation.id, { op: redo, unitId, subUnitId, objects, type: DocDrawingApplyType.UNGROUP });
|
||||
|
||||
// if (result) {
|
||||
// undoRedoService.pushUndoRedo({
|
||||
// unitID: unitId,
|
||||
// undoMutations: [
|
||||
// { id: SetDocDrawingApplyMutation.id, params: { op: undo, unitId, subUnitId, objects: ungroupToGroup(objects as IDrawingGroupUpdateParam[]), type: DocDrawingApplyType.GROUP } },
|
||||
// { id: ClearDocDrawingTransformerOperation.id, params: unitIds },
|
||||
// ],
|
||||
// redoMutations: [
|
||||
// { id: SetDocDrawingApplyMutation.id, params: { op: redo, unitId, subUnitId, objects, type: DocDrawingApplyType.UNGROUP } },
|
||||
// { id: ClearDocDrawingTransformerOperation.id, params: unitIds },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IAccessor, ICommand, IDocDrawingBase, IDocDrawingPosition, IMutationInfo, JSONXActions, WrapTextType } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { IDocDrawing } from '@univerjs/docs-drawing';
|
||||
import {
|
||||
CommandType,
|
||||
getRichTextEditPath,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
JSONX,
|
||||
TextX,
|
||||
TextXActionType,
|
||||
Tools,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service';
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
function getDeleteAndInsertCustomBlockActions(
|
||||
segmentId: string,
|
||||
oldSegmentId: string,
|
||||
segmentPage: number,
|
||||
offset: number,
|
||||
drawingId: string,
|
||||
documentDataModel: DocumentDataModel,
|
||||
docSelectionRenderManager: DocSelectionRenderService
|
||||
) {
|
||||
const textX = new TextX();
|
||||
const jsonX = JSONX.getInstance();
|
||||
const rawActions: JSONXActions = [];
|
||||
|
||||
const oldBody = documentDataModel.getSelfOrHeaderFooterModel(oldSegmentId)?.getBody();
|
||||
const body = documentDataModel.getSelfOrHeaderFooterModel(segmentId)?.getBody();
|
||||
|
||||
if (oldBody == null || body == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldOffset = oldBody.customBlocks?.find((block) => block.blockId === drawingId)?.startIndex;
|
||||
|
||||
if (oldOffset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can not put image after the last \r.
|
||||
offset = Math.min(body.dataStream.length - 2, offset);
|
||||
|
||||
if (segmentId === oldSegmentId) {
|
||||
if (offset < oldOffset) {
|
||||
// Insert first.
|
||||
if (offset > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: offset,
|
||||
});
|
||||
}
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.INSERT,
|
||||
body: {
|
||||
dataStream: '\b',
|
||||
customBlocks: [{
|
||||
startIndex: 0,
|
||||
blockId: drawingId,
|
||||
}],
|
||||
},
|
||||
len: 1,
|
||||
});
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: oldOffset - offset,
|
||||
});
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.DELETE,
|
||||
len: 1,
|
||||
});
|
||||
} else {
|
||||
// Delete first.
|
||||
if (oldOffset > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: oldOffset,
|
||||
});
|
||||
}
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.DELETE,
|
||||
len: 1,
|
||||
});
|
||||
|
||||
if (offset - oldOffset - 1 > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: offset - oldOffset - 1,
|
||||
});
|
||||
}
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.INSERT,
|
||||
body: {
|
||||
dataStream: '\b',
|
||||
customBlocks: [{
|
||||
startIndex: 0,
|
||||
blockId: drawingId,
|
||||
}],
|
||||
},
|
||||
len: 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (offset !== oldOffset) {
|
||||
const path = getRichTextEditPath(documentDataModel, oldSegmentId);
|
||||
const action = jsonX.editOp(textX.serialize(), path);
|
||||
rawActions.push(action!);
|
||||
}
|
||||
} else {
|
||||
if (oldOffset > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: oldOffset,
|
||||
});
|
||||
}
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.DELETE,
|
||||
len: 1,
|
||||
});
|
||||
|
||||
let path = getRichTextEditPath(documentDataModel, oldSegmentId);
|
||||
let action = jsonX.editOp(textX.serialize(), path);
|
||||
rawActions.push(action!);
|
||||
|
||||
textX.empty();
|
||||
|
||||
if (offset > 0) {
|
||||
textX.push({
|
||||
t: TextXActionType.RETAIN,
|
||||
len: offset,
|
||||
});
|
||||
}
|
||||
|
||||
textX.push({
|
||||
t: TextXActionType.INSERT,
|
||||
body: {
|
||||
dataStream: '\b',
|
||||
customBlocks: [{
|
||||
startIndex: 0,
|
||||
blockId: drawingId,
|
||||
}],
|
||||
},
|
||||
len: 1,
|
||||
});
|
||||
|
||||
path = getRichTextEditPath(documentDataModel, segmentId);
|
||||
action = jsonX.editOp(textX.serialize(), path);
|
||||
rawActions.push(action!);
|
||||
|
||||
docSelectionRenderManager.setSegment(segmentId);
|
||||
docSelectionRenderManager.setSegmentPage(segmentPage);
|
||||
}
|
||||
|
||||
return rawActions;
|
||||
}
|
||||
|
||||
interface IDist {
|
||||
distT: number;
|
||||
distB: number;
|
||||
distL: number;
|
||||
distR: number;
|
||||
}
|
||||
|
||||
interface IUpdateDocDrawingDistanceParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawings: IDocDrawing[];
|
||||
dist: IDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command to update drawing wrap text.
|
||||
*/
|
||||
export const UpdateDocDrawingDistanceCommand: ICommand = {
|
||||
id: 'doc.command.update-doc-drawing-distance',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
handler: (accessor: IAccessor, params?: IUpdateDocDrawingDistanceParams) => {
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const { drawings, dist, unitId } = params;
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (documentDataModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const jsonX = JSONX.getInstance();
|
||||
const rawActions: JSONXActions = [];
|
||||
|
||||
const { drawings: oldDrawings = {} } = documentDataModel.getSnapshot();
|
||||
|
||||
for (const drawing of drawings) {
|
||||
const { drawingId } = drawing;
|
||||
|
||||
for (const [key, value] of Object.entries(dist)) {
|
||||
const oldValue = oldDrawings[drawingId][key as keyof IDist];
|
||||
|
||||
if (oldValue !== value) {
|
||||
const action = jsonX.replaceOp(['drawings', drawingId, key], oldValue, value);
|
||||
|
||||
rawActions.push(action!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
actions: [],
|
||||
textRanges: null,
|
||||
},
|
||||
};
|
||||
|
||||
doMutation.params.actions = rawActions.reduce((acc, cur) => {
|
||||
return JSONX.compose(acc, cur as JSONXActions);
|
||||
}, null as JSONXActions);
|
||||
|
||||
const result = commandService.syncExecuteCommand<
|
||||
IRichTextEditingMutationParams,
|
||||
IRichTextEditingMutationParams
|
||||
>(doMutation.id, doMutation.params);
|
||||
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
|
||||
interface IUpdateDocDrawingWrapTextParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawings: IDocDrawing[];
|
||||
wrapText: WrapTextType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command to update drawing wrap text.
|
||||
*/
|
||||
export const UpdateDocDrawingWrapTextCommand: ICommand = {
|
||||
id: 'doc.command.update-doc-drawing-wrap-text',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
handler: (accessor: IAccessor, params?: IUpdateDocDrawingWrapTextParams) => {
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const { drawings, wrapText, unitId } = params;
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (documentDataModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const jsonX = JSONX.getInstance();
|
||||
const rawActions: JSONXActions = [];
|
||||
|
||||
const { drawings: oldDrawings = {} } = documentDataModel.getSnapshot();
|
||||
|
||||
// Update drawing layoutType.
|
||||
for (const drawing of drawings) {
|
||||
const { drawingId } = drawing;
|
||||
|
||||
const oldWrapText = oldDrawings[drawingId].wrapText;
|
||||
|
||||
if (oldWrapText !== wrapText) {
|
||||
const action = jsonX.replaceOp(['drawings', drawingId, 'wrapText'], oldWrapText, wrapText);
|
||||
|
||||
rawActions.push(action!);
|
||||
}
|
||||
}
|
||||
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
actions: [],
|
||||
textRanges: null,
|
||||
},
|
||||
};
|
||||
|
||||
doMutation.params.actions = rawActions.reduce((acc, cur) => {
|
||||
return JSONX.compose(acc, cur as JSONXActions);
|
||||
}, null as JSONXActions);
|
||||
|
||||
const result = commandService.syncExecuteCommand<
|
||||
IRichTextEditingMutationParams,
|
||||
IRichTextEditingMutationParams
|
||||
>(doMutation.id, doMutation.params);
|
||||
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
|
||||
export interface IMoveInlineDrawingParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawing: IDocDrawingBase;
|
||||
offset: number;
|
||||
segmentId: string;
|
||||
segmentPage: number;
|
||||
needRefreshDrawings?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command to move inline drawing.
|
||||
*/
|
||||
export const IMoveInlineDrawingCommand: ICommand = {
|
||||
id: 'doc.command.move-inline-drawing',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
handler: (accessor: IAccessor, params: IMoveInlineDrawingParams) => {
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { drawing, unitId, offset, segmentId: newSegmentId, segmentPage, needRefreshDrawings } = params;
|
||||
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const docSelectionRenderService = renderManagerService.getRenderUnitById(unitId)?.with(DocSelectionRenderService);
|
||||
|
||||
const docRefreshDrawingsService = accessor.get(DocRefreshDrawingsService);
|
||||
const renderObject = renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
const skeleton = renderObject?.with(DocSkeletonManagerService).getSkeleton();
|
||||
if (scene == null || docSelectionRenderService == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (documentDataModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Need to refresh drawings if not find the anchor position.
|
||||
if (needRefreshDrawings) {
|
||||
docRefreshDrawingsService.refreshDrawings(skeleton);
|
||||
transformer.refreshControls();
|
||||
return true;
|
||||
}
|
||||
|
||||
const rawActions: JSONXActions = [];
|
||||
|
||||
const { drawingId } = drawing;
|
||||
const segmentId = docSelectionRenderService.getSegment() ?? '';
|
||||
|
||||
const actions = getDeleteAndInsertCustomBlockActions(
|
||||
newSegmentId,
|
||||
segmentId,
|
||||
segmentPage,
|
||||
offset,
|
||||
drawingId,
|
||||
documentDataModel,
|
||||
docSelectionRenderService
|
||||
);
|
||||
|
||||
if (actions == null || actions.length === 0) {
|
||||
docRefreshDrawingsService.refreshDrawings(skeleton);
|
||||
transformer.refreshControls();
|
||||
return false;
|
||||
}
|
||||
|
||||
rawActions.push(...actions);
|
||||
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
actions: [],
|
||||
textRanges: null,
|
||||
},
|
||||
};
|
||||
|
||||
doMutation.params.actions = rawActions.reduce((acc, cur) => {
|
||||
return JSONX.compose(acc, cur as JSONXActions);
|
||||
}, null as JSONXActions);
|
||||
|
||||
const result = commandService.syncExecuteCommand<
|
||||
IRichTextEditingMutationParams,
|
||||
IRichTextEditingMutationParams
|
||||
>(doMutation.id, doMutation.params);
|
||||
|
||||
transformer.refreshControls();
|
||||
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
|
||||
export interface ITransformNonInlineDrawingParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawing: IDocDrawingBase;
|
||||
offset: number;
|
||||
docTransform: IDocDrawingPosition;
|
||||
segmentId: string;
|
||||
segmentPage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command to transform non-inline drawing.
|
||||
*/
|
||||
export const ITransformNonInlineDrawingCommand: ICommand = {
|
||||
id: 'doc.command.transform-non-inline-drawing',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: (accessor: IAccessor, params?: ITransformNonInlineDrawingParams) => {
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { drawing, unitId, offset, docTransform, segmentId: newSegmentId, segmentPage } = params;
|
||||
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
const docSelectionRenderService = renderManagerService.getRenderUnitById(unitId)?.with(DocSelectionRenderService);
|
||||
|
||||
const renderObject = renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
if (renderObject == null || scene == null || docSelectionRenderService == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (documentDataModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rawActions: JSONXActions = [];
|
||||
|
||||
const { drawingId } = drawing;
|
||||
|
||||
const segmentId = docSelectionRenderService.getSegment() ?? '';
|
||||
|
||||
const actions = getDeleteAndInsertCustomBlockActions(
|
||||
newSegmentId,
|
||||
segmentId,
|
||||
segmentPage,
|
||||
offset,
|
||||
drawingId,
|
||||
documentDataModel,
|
||||
docSelectionRenderService
|
||||
);
|
||||
|
||||
if (actions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
rawActions.push(...actions);
|
||||
}
|
||||
|
||||
const jsonX = JSONX.getInstance();
|
||||
|
||||
const { drawings: oldDrawings = {} } = documentDataModel.getSnapshot();
|
||||
const oldDocTransform = oldDrawings[drawingId].docTransform;
|
||||
const { positionH: oldPositionH, positionV: oldPositionV, size: oldSize, angle: oldAngle } = oldDocTransform;
|
||||
|
||||
if (!Tools.diffValue(oldPositionH, docTransform.positionH)) {
|
||||
const updateAction = jsonX.replaceOp(['drawings', drawingId, 'docTransform', 'positionH'], oldPositionH, docTransform.positionH);
|
||||
|
||||
rawActions.push(updateAction!);
|
||||
}
|
||||
|
||||
if (!Tools.diffValue(oldPositionV, docTransform.positionV)) {
|
||||
const updateAction = jsonX.replaceOp(['drawings', drawingId, 'docTransform', 'positionV'], oldPositionV, docTransform.positionV);
|
||||
|
||||
rawActions.push(updateAction!);
|
||||
}
|
||||
|
||||
if (!Tools.diffValue(oldSize, docTransform.size)) {
|
||||
const updateAction = jsonX.replaceOp(['drawings', drawingId, 'docTransform', 'size'], oldSize, docTransform.size);
|
||||
|
||||
rawActions.push(updateAction!);
|
||||
}
|
||||
|
||||
if (!Tools.diffValue(oldAngle, docTransform.angle)) {
|
||||
const updateAction = jsonX.replaceOp(['drawings', drawingId, 'docTransform', 'angle'], oldAngle, docTransform.angle);
|
||||
|
||||
rawActions.push(updateAction!);
|
||||
}
|
||||
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
actions: [],
|
||||
textRanges: null,
|
||||
debounce: true,
|
||||
},
|
||||
};
|
||||
|
||||
doMutation.params.actions = rawActions.reduce((acc, cur) => {
|
||||
return JSONX.compose(acc, cur as JSONXActions);
|
||||
}, null as JSONXActions);
|
||||
|
||||
const result = commandService.syncExecuteCommand<
|
||||
IRichTextEditingMutationParams,
|
||||
IRichTextEditingMutationParams
|
||||
>(doMutation.id, doMutation.params);
|
||||
|
||||
const skeleton = renderObject.with(DocSkeletonManagerService).getSkeleton();
|
||||
accessor.get(DocRefreshDrawingsService).refreshDrawings(skeleton);
|
||||
transformer.refreshControls();
|
||||
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
109
packages/docs-drawing-ui/src/commands/commands/utils.ts
Normal file
109
packages/docs-drawing-ui/src/commands/commands/utils.ts
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 { IDrawingParam } from '@univerjs/core';
|
||||
import type { IDrawingGroupUpdateParam } from '@univerjs/drawing';
|
||||
import { DrawingTypeEnum } 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! - groupTransform.left,
|
||||
// top: transform.top! - groupTransform.top,
|
||||
},
|
||||
groupId,
|
||||
};
|
||||
}) as IDrawingParam[];
|
||||
|
||||
const groupParam = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId: groupId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_GROUP,
|
||||
transform: groupTransform,
|
||||
groupBaseBound: {
|
||||
left: groupTransform.left,
|
||||
top: groupTransform.top,
|
||||
width: groupTransform.width,
|
||||
height: groupTransform.height,
|
||||
},
|
||||
} as IDrawingParam;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/core';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { EditDocDrawingOperation } from '../edit-doc-drawing.operation';
|
||||
import { SidebarDocDrawingOperation } from '../open-drawing-panel.operation';
|
||||
|
||||
describe('EditDocDrawingOperation', () => {
|
||||
it('returns false when no drawing is provided', () => {
|
||||
const drawingManagerService = {
|
||||
focusDrawing: vi.fn(),
|
||||
};
|
||||
const commandService = {
|
||||
executeCommand: vi.fn(),
|
||||
};
|
||||
const accessor = {
|
||||
get(token: unknown) {
|
||||
if (token === IDrawingManagerService) {
|
||||
return drawingManagerService;
|
||||
}
|
||||
|
||||
if (token === ICommandService) {
|
||||
return commandService;
|
||||
}
|
||||
|
||||
throw new Error('Unknown dependency');
|
||||
},
|
||||
} as IAccessor;
|
||||
|
||||
expect(EditDocDrawingOperation.handler(accessor, null as unknown as IDrawingSearch)).toBe(false);
|
||||
expect(drawingManagerService.focusDrawing).not.toHaveBeenCalled();
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('focuses the drawing and opens the sidebar when a drawing is provided', () => {
|
||||
const drawingManagerService = {
|
||||
focusDrawing: vi.fn(),
|
||||
};
|
||||
const commandService = {
|
||||
executeCommand: vi.fn(),
|
||||
};
|
||||
const accessor = {
|
||||
get(token: unknown) {
|
||||
if (token === IDrawingManagerService) {
|
||||
return drawingManagerService;
|
||||
}
|
||||
|
||||
if (token === ICommandService) {
|
||||
return commandService;
|
||||
}
|
||||
|
||||
throw new Error('Unknown dependency');
|
||||
},
|
||||
} as IAccessor;
|
||||
const params: IDrawingSearch = {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'drawing-1',
|
||||
};
|
||||
|
||||
expect(EditDocDrawingOperation.handler(accessor, params)).toBe(true);
|
||||
expect(drawingManagerService.focusDrawing).toHaveBeenCalledWith([params]);
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(SidebarDocDrawingOperation.id, { value: 'open' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 { LocaleService } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { COMPONENT_DOC_DRAWING_PANEL } from '../../../views/doc-image-panel/component-name';
|
||||
import { SidebarDocDrawingOperation } from '../open-drawing-panel.operation';
|
||||
|
||||
function createAccessor() {
|
||||
const sidebarService = { open: vi.fn(), close: vi.fn() };
|
||||
const drawingManagerService = { focusDrawing: vi.fn() };
|
||||
const accessor = {
|
||||
get(token: unknown) {
|
||||
if (token === ISidebarService) {
|
||||
return sidebarService;
|
||||
}
|
||||
|
||||
if (token === LocaleService) {
|
||||
return { t: (key: string) => key };
|
||||
}
|
||||
|
||||
if (token === IDrawingManagerService) {
|
||||
return drawingManagerService;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown dependency: ${String(token)}`);
|
||||
},
|
||||
} as IAccessor;
|
||||
|
||||
return { accessor, sidebarService, drawingManagerService };
|
||||
}
|
||||
|
||||
describe('SidebarDocDrawingOperation', () => {
|
||||
it('opens the doc drawing sidebar and clears focus when closed', async () => {
|
||||
const { accessor, sidebarService, drawingManagerService } = createAccessor();
|
||||
await expect(SidebarDocDrawingOperation.handler(accessor, { value: 'open' })).resolves.toBe(true);
|
||||
expect(sidebarService.open).toHaveBeenCalledWith({
|
||||
header: { title: 'docs-drawing-ui.panel.title' },
|
||||
children: { label: COMPONENT_DOC_DRAWING_PANEL },
|
||||
onClose: expect.any(Function),
|
||||
width: 360,
|
||||
});
|
||||
const onClose = sidebarService.open.mock.calls[0][0].onClose;
|
||||
onClose();
|
||||
expect(drawingManagerService.focusDrawing).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('closes the sidebar for close and default actions', async () => {
|
||||
const { accessor, sidebarService } = createAccessor();
|
||||
await expect(SidebarDocDrawingOperation.handler(accessor, { value: 'close' })).resolves.toBe(true);
|
||||
await expect(SidebarDocDrawingOperation.handler(accessor, { value: 'anything-else' })).resolves.toBe(true);
|
||||
expect(sidebarService.close).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMutation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
|
||||
export const ClearDocDrawingTransformerOperation: IMutation<string[]> = {
|
||||
id: 'doc.operation.clear-drawing-transformer',
|
||||
type: CommandType.MUTATION,
|
||||
handler: (accessor, params) => {
|
||||
const renderManagerService = accessor.get(IRenderManagerService);
|
||||
|
||||
params.forEach((unitId) => {
|
||||
renderManagerService.getRenderUnitById(unitId)?.scene.getTransformer()?.debounceRefreshControls();
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { SidebarDocDrawingOperation } from './open-drawing-panel.operation';
|
||||
|
||||
// Use to open drawing edit panel.
|
||||
export const EditDocDrawingOperation: IOperation<IDrawingSearch> = {
|
||||
id: 'doc.operation.edit-doc-image',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor: IAccessor, params?: IDrawingSearch) => {
|
||||
const drawingManagerService = accessor.get(IDrawingManagerService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
if (params == null) {
|
||||
return false;
|
||||
}
|
||||
drawingManagerService.focusDrawing([params]);
|
||||
commandService.executeCommand(SidebarDocDrawingOperation.id, { value: 'open' });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 { IMobileSidebarMethodOptions } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../../../locale/types';
|
||||
import type { IUIComponentCommandParams } from '../open-drawing-panel.operation';
|
||||
import { CommandType, LocaleService } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { COMPONENT_DOC_DRAWING_PANEL } from '../../../views/doc-image-panel/component-name';
|
||||
import { SidebarDocDrawingOperation } from '../open-drawing-panel.operation';
|
||||
|
||||
export const SidebarDocDrawingMobileOperation: ICommand = {
|
||||
id: SidebarDocDrawingOperation.id,
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: IUIComponentCommandParams) => {
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const drawingManagerService = accessor.get(IDrawingManagerService);
|
||||
|
||||
switch (params.value) {
|
||||
case 'open': {
|
||||
const options: IMobileSidebarMethodOptions = {
|
||||
layout: 'canvas',
|
||||
header: { title: localeService.t<LocaleKey>('docs-drawing-ui.panel.title') },
|
||||
children: { label: COMPONENT_DOC_DRAWING_PANEL },
|
||||
onClose: () => {
|
||||
drawingManagerService.focusDrawing(null);
|
||||
},
|
||||
width: 360,
|
||||
};
|
||||
sidebarService.open(options);
|
||||
break;
|
||||
}
|
||||
case 'close':
|
||||
default:
|
||||
sidebarService.close();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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, LocaleService } from '@univerjs/core';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import {
|
||||
COMPONENT_DOC_DRAWING_PANEL,
|
||||
} from '../../views/doc-image-panel/component-name';
|
||||
|
||||
export interface IUIComponentCommandParams {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const SidebarDocDrawingOperation: ICommand = {
|
||||
id: 'sidebar.operation.doc-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: IUIComponentCommandParams) => {
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
const drawingManagerService = accessor.get(IDrawingManagerService);
|
||||
|
||||
switch (params.value) {
|
||||
case 'open':
|
||||
sidebarService.open({
|
||||
header: { title: localeService.t<LocaleKey>('docs-drawing-ui.panel.title') },
|
||||
children: { label: COMPONENT_DOC_DRAWING_PANEL },
|
||||
onClose: () => {
|
||||
drawingManagerService.focusDrawing(null);
|
||||
},
|
||||
width: 360,
|
||||
});
|
||||
break;
|
||||
case 'close':
|
||||
default:
|
||||
sidebarService.close();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
25
packages/docs-drawing-ui/src/config/config.ts
Normal file
25
packages/docs-drawing-ui/src/config/config.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.
|
||||
*/
|
||||
|
||||
export const DOCS_DRAWING_UI_PLUGIN_CONFIG_KEY = 'docs-drawing-ui.config';
|
||||
export const DOCS_DRAWING_UI_PLUGIN_NAME = 'DOC_DRAWING_UI_PLUGIN';
|
||||
|
||||
export const configSymbol = Symbol(DOCS_DRAWING_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverDocsDrawingUIConfig {
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverDocsDrawingUIConfig = {};
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IDocumentData, JSONXActions } from '@univerjs/core';
|
||||
import type { IDocDrawing } from '@univerjs/docs-drawing';
|
||||
import type { RenderUnit } from '@univerjs/engine-render';
|
||||
import {
|
||||
BooleanNumber,
|
||||
CustomRangeType,
|
||||
DocumentFlavor,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
JSONX,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
RedoCommand,
|
||||
UndoCommand,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DocLayoutExecutorService,
|
||||
DocSelectionManagerService,
|
||||
DocSkeletonManagerService,
|
||||
DocStateChangeManagerService,
|
||||
DocStateEmitService,
|
||||
RichTextEditingMutation,
|
||||
} from '@univerjs/docs';
|
||||
import {
|
||||
DocDrawingAdapterService,
|
||||
DocDrawingController,
|
||||
DocDrawingService,
|
||||
IDocDrawingAdapterService,
|
||||
IDocDrawingService,
|
||||
InsertDocDrawingCommand,
|
||||
RemoveDocDrawingCommand,
|
||||
TextWrappingStyle,
|
||||
UpdateDocDrawingWrappingStyleCommand,
|
||||
UpdateDrawingDocTransformCommand,
|
||||
} from '@univerjs/docs-drawing';
|
||||
import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import {
|
||||
CanvasColorService,
|
||||
ICanvasColorService,
|
||||
IRenderManagerService,
|
||||
RenderManagerService,
|
||||
} from '@univerjs/engine-render';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service';
|
||||
import { DocDrawingAddRemoveController } from '../doc-drawing-notification.controller';
|
||||
|
||||
describe('DocDrawingAddRemoveController with real commands and services', () => {
|
||||
const unitId = 'drawing-notifications';
|
||||
let univer: Univer;
|
||||
let model: DocumentDataModel;
|
||||
let render: RenderUnit;
|
||||
let commands: ICommandService;
|
||||
let manager: IDrawingManagerService;
|
||||
let docDrawings: IDocDrawingService;
|
||||
let refresh: DocRefreshDrawingsService;
|
||||
|
||||
function drawing(drawingId: string): IDocDrawing {
|
||||
return {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: drawingId === 'a' ? BooleanNumber.TRUE : BooleanNumber.FALSE,
|
||||
transform: { left: 20, top: 30, width: 40, height: 30 },
|
||||
docTransform: {
|
||||
size: { width: 40, height: 30 },
|
||||
angle: 0,
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.MARGIN, posOffset: 20 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 30 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const context = new Proxy({
|
||||
font: '',
|
||||
webkitBackingStorePixelRatio: 1,
|
||||
measureText: (text: string) => ({
|
||||
width: text.length * 8,
|
||||
actualBoundingBoxAscent: 8,
|
||||
actualBoundingBoxDescent: 2,
|
||||
fontBoundingBoxAscent: 8,
|
||||
fontBoundingBoxDescent: 2,
|
||||
}),
|
||||
}, { get: (target, key) => key in target ? Reflect.get(target, key) : () => {} });
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context as never);
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([IRenderManagerService, { useClass: RenderManagerService }]);
|
||||
injector.add([ICanvasColorService, { useClass: CanvasColorService }]);
|
||||
injector.add([IDrawingManagerService, { useClass: DrawingManagerService }]);
|
||||
injector.add([IDocDrawingService, { useClass: DocDrawingService }]);
|
||||
injector.add([IDocDrawingAdapterService, { useClass: DocDrawingAdapterService }]);
|
||||
injector.add([DocLayoutExecutorService]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocStateEmitService]);
|
||||
injector.add([DocStateChangeManagerService]);
|
||||
injector.add([DocRefreshDrawingsService]);
|
||||
injector.add([DocDrawingController]);
|
||||
injector.add([DocDrawingAddRemoveController]);
|
||||
commands = injector.get(ICommandService);
|
||||
commands.registerCommand(RichTextEditingMutation);
|
||||
injector.get(IUndoRedoService);
|
||||
injector.get(DocStateChangeManagerService);
|
||||
const drawingController = injector.get(DocDrawingController);
|
||||
model = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, {
|
||||
id: unitId,
|
||||
body: {
|
||||
dataStream: 'A\b\rB\b\r\n',
|
||||
paragraphs: [{ startIndex: 2, paragraphId: 'p1' }, { startIndex: 5, paragraphId: 'p2' }],
|
||||
sectionBreaks: [{ startIndex: 6, sectionId: 's1' }],
|
||||
customBlocks: [{ startIndex: 1, blockId: 'a' }, { startIndex: 4, blockId: 'b' }],
|
||||
},
|
||||
drawings: { a: drawing('a'), b: drawing('b') },
|
||||
drawingsOrder: ['a', 'b'],
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.TRADITIONAL,
|
||||
autoHyphenation: BooleanNumber.FALSE,
|
||||
pageSize: { width: 300, height: 400 },
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
},
|
||||
});
|
||||
injector.get(IUniverInstanceService).setCurrentUnitForType(unitId);
|
||||
injector.get(IUniverInstanceService).focusUnit(unitId);
|
||||
render = injector.get(IRenderManagerService).createRender(unitId) as RenderUnit;
|
||||
render.deactivate();
|
||||
render.addRenderDependencies([[DocSkeletonManagerService]]);
|
||||
render.with(DocSkeletonManagerService);
|
||||
drawingController.loadDrawingDataForUnit(unitId);
|
||||
manager = injector.get(IDrawingManagerService);
|
||||
docDrawings = injector.get(IDocDrawingService);
|
||||
refresh = injector.get(DocRefreshDrawingsService);
|
||||
injector.get(DocDrawingAddRemoveController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer?.dispose();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function mutate(actions: JSONXActions): void {
|
||||
commands.syncExecuteCommand(RichTextEditingMutation.id, { unitId, actions, textRanges: null });
|
||||
}
|
||||
|
||||
function resize(): void {
|
||||
expect(commands.syncExecuteCommand(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings: [{ drawingId: 'a', key: 'size', value: { width: 60, height: 45 } }],
|
||||
})).toBe(true);
|
||||
}
|
||||
|
||||
it('keeps unrelated rendered positions and visibility when one drawing is resized', () => {
|
||||
manager.refreshTransform([{ ...drawing('b'), hidden: true, transform: { left: 420, top: 880, width: 40, height: 30 } }]);
|
||||
const before = manager.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'b' });
|
||||
const order = manager.getDrawingOrder(unitId, unitId);
|
||||
resize();
|
||||
expect(model.getDrawings()!.a.docTransform.size).toEqual({ width: 60, height: 45 });
|
||||
expect(manager.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'b' })).toBe(before);
|
||||
expect(before).toMatchObject({ hidden: true, transform: { left: 420, top: 880 } });
|
||||
expect(manager.getDrawingOrder(unitId, unitId)).toBe(order);
|
||||
});
|
||||
|
||||
it('does not write layout-only transforms into the persisted drawing snapshot after a resize', () => {
|
||||
resize();
|
||||
const original = { ...model.getDrawings()!.a.transform };
|
||||
manager.refreshTransform([{ ...drawing('a'), transform: { left: 720, top: 990, width: 60, height: 45 } }]);
|
||||
expect(model.getDrawings()!.a.transform).toEqual(original);
|
||||
});
|
||||
|
||||
it('keeps a thousand-drawing document stable during repeated single-image resizing', ({ task }) => {
|
||||
const extraDrawings = Array.from({ length: 998 }, (_, index) => drawing(`extra-${index}`));
|
||||
expect(commands.syncExecuteCommand(InsertDocDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: extraDrawings,
|
||||
textRange: { startOffset: 0, endOffset: 0 },
|
||||
})).toBe(true);
|
||||
const original = manager.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'extra-997' });
|
||||
const order = manager.getDrawingOrder(unitId, unitId);
|
||||
const mutationMs: number[] = [];
|
||||
for (let index = 0; index < 20; index++) {
|
||||
const start = performance.now();
|
||||
commands.syncExecuteCommand(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings: [{ drawingId: 'a', key: 'size', value: { width: 60 + index, height: 45 } }],
|
||||
});
|
||||
mutationMs.push(performance.now() - start);
|
||||
}
|
||||
Object.assign(task.meta, { drawingCount: 1000, mutationMs });
|
||||
expect(Object.keys(model.getDrawings()!)).toHaveLength(1000);
|
||||
expect(model.getDrawings()!.a.docTransform.size.width).toBe(79);
|
||||
expect(manager.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'extra-997' })).toBe(original);
|
||||
expect(manager.getDrawingOrder(unitId, unitId)).toBe(order);
|
||||
});
|
||||
|
||||
it('mirrors actual drawing insertion and removal and emits their notifications', () => {
|
||||
const added = vi.fn();
|
||||
const removed = vi.fn();
|
||||
const addSubscription = manager.add$.subscribe(added);
|
||||
const removeSubscription = manager.remove$.subscribe(removed);
|
||||
expect(commands.syncExecuteCommand(InsertDocDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: [drawing('c')],
|
||||
textRange: { startOffset: 0, endOffset: 0 },
|
||||
})).toBe(true);
|
||||
expect(manager.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'c' })).toBeDefined();
|
||||
expect(added).toHaveBeenCalled();
|
||||
expect(commands.syncExecuteCommand(RemoveDocDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: [{ unitId, subUnitId: unitId, drawingId: 'c', drawingType: DrawingTypeEnum.DRAWING_IMAGE }],
|
||||
textRange: { startOffset: 0, endOffset: 0 },
|
||||
})).toBe(true);
|
||||
expect(manager.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'c' })).toBeUndefined();
|
||||
expect(docDrawings.getDrawingByParam({ unitId, subUnitId: unitId, drawingId: 'c' })).toBeUndefined();
|
||||
expect(removed).toHaveBeenCalled();
|
||||
addSubscription.unsubscribe();
|
||||
removeSubscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('registers scoped note images and removes them when their note is deleted', () => {
|
||||
const body = model.getBody()!;
|
||||
const referencedBody = {
|
||||
...body,
|
||||
dataStream: `\uFFFC${body.dataStream}`,
|
||||
paragraphs: body.paragraphs?.map((paragraph) => ({ ...paragraph, startIndex: paragraph.startIndex + 1 })),
|
||||
sectionBreaks: body.sectionBreaks?.map((section) => ({ ...section, startIndex: section.startIndex + 1 })),
|
||||
customBlocks: body.customBlocks?.map((block) => ({ ...block, startIndex: block.startIndex + 1 })),
|
||||
customRanges: [{
|
||||
rangeId: 'ref',
|
||||
rangeType: CustomRangeType.FOOTNOTE,
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
wholeEntity: true,
|
||||
properties: { noteId: 'note' },
|
||||
}],
|
||||
};
|
||||
const note = { type: 'footnote' as const, noteId: 'note', body: {
|
||||
dataStream: '\b\r\n',
|
||||
paragraphs: [{ startIndex: 1, paragraphId: 'np' }],
|
||||
customBlocks: [{ startIndex: 0, blockId: 'note-image' }],
|
||||
}, drawings: { 'note-image': { ...drawing('note-image'), layoutType: PositionedObjectLayoutType.INLINE } }, drawingsOrder: ['note-image'] };
|
||||
const jsonX = JSONX.getInstance();
|
||||
mutate(JSONX.compose(jsonX.replaceOp(['body'], body, referencedBody), jsonX.insertOp(['notes', 'note'], note)));
|
||||
const search = { unitId, subUnitId: unitId, drawingId: 'note-image' };
|
||||
expect(manager.getDrawingByParam(search)?.drawingId).toBe('note-image');
|
||||
expect(docDrawings.getDrawingOrder(unitId, unitId)).toEqual(['a', 'b', 'note-image']);
|
||||
expect(commands.syncExecuteCommand(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings: [{ drawingId: 'note-image', key: 'size', value: { width: 70, height: 35 } }],
|
||||
})).toBe(true);
|
||||
expect(manager.getDrawingByParam(search)).toMatchObject({ docTransform: { size: { width: 70, height: 35 } } });
|
||||
expect(model.getDrawings()).not.toHaveProperty('note-image');
|
||||
mutate(jsonX.replaceOp(['body'], model.getBody(), body));
|
||||
expect(model.getSnapshot().notes?.note).toBeUndefined();
|
||||
expect(manager.getDrawingByParam(search)).toBeUndefined();
|
||||
expect(docDrawings.getDrawingByParam(search)).toBeUndefined();
|
||||
expect(manager.getDrawingByParam({ ...search, drawingId: 'a' })).toBeDefined();
|
||||
});
|
||||
|
||||
it('updates persisted order and keeps behind-text drawings below foreground drawings', () => {
|
||||
mutate(JSONX.getInstance().moveOp(['drawingsOrder', 0], ['drawingsOrder', 1]));
|
||||
expect(docDrawings.getDrawingOrder(unitId, unitId)).toEqual(['b', 'a']);
|
||||
expect(manager.getDrawingOrder(unitId, unitId)).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('still rebuilds render order when wrapping changes the behind-text layer', () => {
|
||||
mutate(JSONX.getInstance().moveOp(['drawingsOrder', 0], ['drawingsOrder', 1]));
|
||||
expect(manager.getDrawingOrder(unitId, unitId)).toEqual(['a', 'b']);
|
||||
expect(commands.syncExecuteCommand(UpdateDocDrawingWrappingStyleCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings: [{ drawingId: 'a' }],
|
||||
wrappingStyle: TextWrappingStyle.IN_FRONT_OF_TEXT,
|
||||
})).toBe(true);
|
||||
expect(manager.getDrawingOrder(unitId, unitId)).toEqual(['b', 'a']);
|
||||
expect(docDrawings.getDrawingOrder(unitId, unitId)).toEqual(['b', 'a']);
|
||||
});
|
||||
|
||||
it('removes optional metadata from current data while retaining it for old-state comparisons', () => {
|
||||
mutate(JSONX.getInstance().insertOp(['drawings', 'a', 'description'], 'caption'));
|
||||
mutate(JSONX.getInstance().removeOp(['drawings', 'a', 'description'], 'caption'));
|
||||
const search = { unitId, subUnitId: unitId, drawingId: 'a' };
|
||||
expect(manager.getDrawingByParam(search)).not.toHaveProperty('description');
|
||||
expect(manager.getOldDrawingByParam(search)).toHaveProperty('description', 'caption');
|
||||
});
|
||||
|
||||
it('refreshes drawing positions before controls on actual undo and redo', async () => {
|
||||
manager.focusDrawing([{ unitId, subUnitId: unitId, drawingId: 'a' }]);
|
||||
resize();
|
||||
const refreshDrawings = vi.spyOn(refresh, 'refreshDrawings');
|
||||
const refreshControls = vi.spyOn(render.scene.getTransformerByCreate(), 'refreshControls');
|
||||
for (const [commandId, expectedWidth] of [[UndoCommand.id, 40], [RedoCommand.id, 60]] as const) {
|
||||
refreshDrawings.mockClear();
|
||||
refreshControls.mockClear();
|
||||
expect(await commands.executeCommand(commandId)).toBe(true);
|
||||
expect(model.getDrawings()!.a.docTransform.size.width).toBe(expectedWidth);
|
||||
expect(refreshDrawings).toHaveBeenCalledWith(render.with(DocSkeletonManagerService).getSkeleton());
|
||||
expect(refreshDrawings.mock.invocationCallOrder[0]).toBeLessThan(refreshControls.mock.invocationCallOrder[0]);
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves the resolved anchor when changing wrapping and refreshes the drawing afterward', () => {
|
||||
const skeleton = render.with(DocSkeletonManagerService).getSkeleton();
|
||||
const page = skeleton.getSkeletonData()!.pages[0];
|
||||
const anchor = page.skeDrawings.get('a')!;
|
||||
expect(anchor).toBeDefined();
|
||||
const expectedLeft = anchor.aLeft - page.marginLeft;
|
||||
const expectedTop = anchor.aTop - anchor.blockAnchorTop;
|
||||
const refreshDrawings = vi.spyOn(refresh, 'refreshDrawings');
|
||||
expect(commands.syncExecuteCommand(UpdateDocDrawingWrappingStyleCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings: [{ drawingId: 'a' }],
|
||||
wrappingStyle: TextWrappingStyle.WRAP_SQUARE,
|
||||
})).toBe(true);
|
||||
expect(model.getDrawings()!.a.docTransform).toMatchObject({
|
||||
positionH: { posOffset: expectedLeft },
|
||||
positionV: { posOffset: expectedTop },
|
||||
});
|
||||
expect(refreshDrawings).toHaveBeenCalledWith(skeleton);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IDocumentData } from '@univerjs/core';
|
||||
import type { IGalleryProps } from '@univerjs/design';
|
||||
import type { IDocDrawing } from '@univerjs/docs-drawing';
|
||||
import {
|
||||
BooleanNumber,
|
||||
DataStreamTreeTokenType,
|
||||
DisposableCollection,
|
||||
DOC_DRAWING_PRINTING_COMPONENT_KEY,
|
||||
DocumentFlavor,
|
||||
DrawingTypeEnum,
|
||||
IImageIoService,
|
||||
ImageSourceType,
|
||||
IURLImageService,
|
||||
LocaleService,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
TableAlignmentType,
|
||||
TableRowHeightRule,
|
||||
TableSizeType,
|
||||
TableTextWrapType,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocPrintInterceptorService } from '@univerjs/docs-ui';
|
||||
import { DrawingManagerService, getDrawingShapeKeyByDrawingSearch, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DrawingImageClipService, DrawingRenderService } from '@univerjs/drawing-ui';
|
||||
import { Documents, DocumentSkeleton, DocumentViewModel, Engine, Scene } from '@univerjs/engine-render';
|
||||
import { ComponentManager, IGalleryService } from '@univerjs/ui';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mountDocPrintingFloatDom } from '../../views/DocPrintingFloatDom';
|
||||
import { DocDrawingPrintingController } from '../doc-drawing-printing.controller';
|
||||
|
||||
vi.mock('../../views/DocPrintingFloatDom', () => ({
|
||||
mountDocPrintingFloatDom: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
describe('DocDrawingPrintingController with real layout and drawing services', () => {
|
||||
let univer: Univer;
|
||||
let engine: Engine;
|
||||
let scene: Scene;
|
||||
let skeleton: DocumentSkeleton;
|
||||
let documents: Documents;
|
||||
let printService: DocPrintInterceptorService;
|
||||
const unitId = 'print-images';
|
||||
const imageIds = ['body-first', 'cell-image', 'body-second', 'header-image', 'footer-image'];
|
||||
|
||||
beforeEach(() => {
|
||||
const context = new Proxy({
|
||||
font: '',
|
||||
webkitBackingStorePixelRatio: 1,
|
||||
measureText: (text: string) => ({
|
||||
width: text.length * 8,
|
||||
actualBoundingBoxAscent: 8,
|
||||
actualBoundingBoxDescent: 2,
|
||||
fontBoundingBoxAscent: 8,
|
||||
fontBoundingBoxDescent: 2,
|
||||
}),
|
||||
}, { get: (target, key) => key in target ? Reflect.get(target, key) : () => {} });
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context as never);
|
||||
const cachedImage = document.createElement('img');
|
||||
Object.defineProperties(cachedImage, {
|
||||
complete: { value: true },
|
||||
naturalWidth: { value: 16 },
|
||||
naturalHeight: { value: 16 },
|
||||
});
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([IDrawingManagerService, { useClass: DrawingManagerService }]);
|
||||
injector.add([IImageIoService, { useValue: {
|
||||
change$: EMPTY,
|
||||
setWaitCount: () => {},
|
||||
getImage: async () => '',
|
||||
saveImage: async () => null,
|
||||
getImageSourceCache: () => cachedImage,
|
||||
addImageSourceCache: () => {},
|
||||
} }]);
|
||||
injector.add([IURLImageService, { useValue: {
|
||||
getImage: async (url: string) => url,
|
||||
downloadImage: async () => new Blob(),
|
||||
registerURLImageDownloader: () => ({ dispose: () => {} }),
|
||||
} }]);
|
||||
injector.add([IGalleryService, { useValue: {
|
||||
gallery$: new Subject<IGalleryProps>(),
|
||||
open: () => ({ dispose: () => {} }),
|
||||
close: () => {},
|
||||
} }]);
|
||||
injector.add([DrawingImageClipService]);
|
||||
injector.add([DrawingRenderService]);
|
||||
injector.add([ComponentManager]);
|
||||
injector.add([DocPrintInterceptorService]);
|
||||
injector.add([DocDrawingPrintingController]);
|
||||
|
||||
const T = DataStreamTreeTokenType;
|
||||
const table = `${T.TABLE_START}${T.TABLE_ROW_START}${T.TABLE_CELL_START}${T.CUSTOM_BLOCK}${T.PARAGRAPH}${T.SECTION_BREAK}${T.TABLE_CELL_END}${T.TABLE_ROW_END}${T.TABLE_END}`;
|
||||
const dataStream = `${T.CUSTOM_BLOCK}${T.PARAGRAPH}${table}${T.PARAGRAPH}${T.CUSTOM_BLOCK}${T.PARAGRAPH}${T.SECTION_BREAK}`;
|
||||
const drawings: Record<string, IDocDrawing> = Object.fromEntries(imageIds.map((drawingId) => [drawingId, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
source: drawingId,
|
||||
transform: { left: 10, top: 10, width: 16, height: 16, angle: 0 },
|
||||
docTransform: {
|
||||
size: { width: 16, height: 16 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 0 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 0 },
|
||||
angle: 0,
|
||||
},
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
}]));
|
||||
const segmentBody = (blockId: string) => ({
|
||||
dataStream: `${T.CUSTOM_BLOCK}${T.PARAGRAPH}${T.SECTION_BREAK}`,
|
||||
paragraphs: [{ startIndex: 1, paragraphId: `${blockId}-p` }],
|
||||
sectionBreaks: [{ startIndex: 2, sectionId: `${blockId}-s` }],
|
||||
customBlocks: [{ startIndex: 0, blockId }],
|
||||
});
|
||||
const snapshot: IDocumentData = {
|
||||
id: unitId,
|
||||
body: {
|
||||
dataStream,
|
||||
paragraphs: [...dataStream.matchAll(/\r/g)].map((match, index) => ({
|
||||
startIndex: match.index!,
|
||||
paragraphId: `p-${index}`,
|
||||
paragraphStyle: { pageBreakBefore: index === 3 ? BooleanNumber.TRUE : BooleanNumber.FALSE },
|
||||
})),
|
||||
sectionBreaks: [...dataStream.matchAll(/\n/g)].map((match, index) => ({ startIndex: match.index!, sectionId: `s-${index}` })),
|
||||
customBlocks: [
|
||||
{ startIndex: 0, blockId: 'body-first' },
|
||||
{ startIndex: 5, blockId: 'cell-image' },
|
||||
{ startIndex: dataStream.length - 3, blockId: 'body-second' },
|
||||
],
|
||||
tables: [{ startIndex: 2, endIndex: 2 + table.length, tableId: 'table' }],
|
||||
},
|
||||
headers: { header: { headerId: 'header', body: segmentBody('header-image') } },
|
||||
footers: { footer: { footerId: 'footer', body: segmentBody('footer-image') } },
|
||||
drawings,
|
||||
drawingsOrder: imageIds,
|
||||
tableSource: {
|
||||
table: {
|
||||
tableId: 'table',
|
||||
align: TableAlignmentType.START,
|
||||
indent: { v: 0 },
|
||||
textWrap: TableTextWrapType.NONE,
|
||||
position: {
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE },
|
||||
},
|
||||
dist: { distT: 0, distB: 0, distL: 0, distR: 0 },
|
||||
size: { type: TableSizeType.SPECIFIED, width: { v: 160 } },
|
||||
tableRows: [{
|
||||
tableCells: [{}],
|
||||
trHeight: { val: { v: 25 }, hRule: TableRowHeightRule.AT_LEAST },
|
||||
}],
|
||||
tableColumns: [{ size: { type: TableSizeType.SPECIFIED, width: { v: 160 } } }],
|
||||
},
|
||||
},
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.TRADITIONAL,
|
||||
autoHyphenation: BooleanNumber.FALSE,
|
||||
pageSize: { width: 240, height: 400 },
|
||||
marginTop: 50,
|
||||
marginBottom: 50,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
marginHeader: 10,
|
||||
marginFooter: 10,
|
||||
defaultHeaderId: 'header',
|
||||
defaultFooterId: 'footer',
|
||||
},
|
||||
};
|
||||
const model = univer.createUnit<IDocumentData, DocumentDataModel>(UniverInstanceType.UNIVER_DOC, snapshot);
|
||||
skeleton = DocumentSkeleton.create(new DocumentViewModel(model), injector.get(LocaleService));
|
||||
skeleton.calculate();
|
||||
expect(skeleton.getSkeletonData()!.pages).toHaveLength(2);
|
||||
engine = new Engine('print-engine', { elementWidth: 240, elementHeight: 400 });
|
||||
scene = new Scene('print-scene', engine);
|
||||
documents = new Documents('print-document', skeleton);
|
||||
const drawingManager = injector.get(IDrawingManagerService);
|
||||
drawingManager.registerDrawingData(unitId, { [unitId]: { data: drawings, order: imageIds } });
|
||||
drawingManager.setDrawingEditable(false);
|
||||
printService = injector.get(DocPrintInterceptorService);
|
||||
injector.get(DocDrawingPrintingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scene?.dispose();
|
||||
engine?.dispose();
|
||||
skeleton?.dispose();
|
||||
univer?.dispose();
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function collect(pageIndex?: number): void {
|
||||
printService.interceptor.fetchThroughInterceptors(
|
||||
printService.interceptor.getInterceptPoints().PRINTING_COMPONENT_COLLECT
|
||||
)(undefined, { unitId, scene, engine, skeleton, documents, pageIndex, root: document.createElement('div') });
|
||||
}
|
||||
|
||||
function renderedImageIds(): string[] {
|
||||
return imageIds.filter((drawingId) => scene.getObject(getDrawingShapeKeyByDrawingSearch({ unitId, subUnitId: unitId, drawingId })) != null);
|
||||
}
|
||||
|
||||
it('prints table-cell images with their host page and omits the other body page', () => {
|
||||
const page = skeleton.getSkeletonData()!.pages[0];
|
||||
expect(page.skeTables.get('table')!.rows[0].cells[0].skeDrawings.has('cell-image')).toBe(true);
|
||||
collect(0);
|
||||
expect(renderedImageIds()).toEqual(['body-first', 'cell-image', 'header-image', 'footer-image']);
|
||||
});
|
||||
|
||||
it('prints repeated header and footer images on the second page', () => {
|
||||
const data = skeleton.getSkeletonData()!;
|
||||
expect(data.skeHeaders.get(data.pages[1].headerId)!.get(data.pages[1].pageWidth)!.skeDrawings.has('header-image')).toBe(true);
|
||||
expect(data.skeFooters.get(data.pages[1].footerId)!.get(data.pages[1].pageWidth)!.skeDrawings.has('footer-image')).toBe(true);
|
||||
collect(1);
|
||||
expect(renderedImageIds()).toEqual(['body-second', 'header-image', 'footer-image']);
|
||||
});
|
||||
|
||||
it('keeps unfiltered drawing collection when there is no traditional page index', () => {
|
||||
collect();
|
||||
expect(renderedImageIds()).toEqual(imageIds);
|
||||
});
|
||||
|
||||
it('does not render another page when the requested print page is missing', () => {
|
||||
collect(2);
|
||||
expect(renderedImageIds()).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, ['body-first', 'cell-image', 'header-image', 'footer-image']],
|
||||
[1, ['body-second', 'header-image', 'footer-image']],
|
||||
] as const)('collects DOM drawings only from page %i including its tables and repeated segments', (pageIndex, expectedIds) => {
|
||||
const injector = univer.__getInjector();
|
||||
const manager = injector.get(IDrawingManagerService);
|
||||
const drawings = Object.fromEntries(imageIds.map((drawingId) => [drawingId, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
componentKey: 'custom-dom',
|
||||
}]));
|
||||
manager.setDrawingData(unitId, unitId, drawings);
|
||||
injector.get(ComponentManager).register('custom-dom', () => null);
|
||||
const disposables = new DisposableCollection();
|
||||
printService.interceptor.fetchThroughInterceptors(
|
||||
printService.interceptor.getInterceptPoints().PRINTING_DOM_COLLECT
|
||||
)(disposables, {
|
||||
unitId,
|
||||
scene,
|
||||
engine,
|
||||
skeleton,
|
||||
pageIndex,
|
||||
root: document.createElement('div'),
|
||||
offset: { x: 0, y: 0 },
|
||||
bound: { top: 0, left: 0, right: 240, bottom: 400 },
|
||||
});
|
||||
const props = vi.mocked(mountDocPrintingFloatDom).mock.calls[0][0];
|
||||
expect(props.floatDomInfos.map((drawing) => drawing.drawingId)).toEqual(expectedIds);
|
||||
disposables.dispose();
|
||||
});
|
||||
|
||||
it('mounts chart and DOM print components and disposes their DOM mount', () => {
|
||||
const injector = univer.__getInjector();
|
||||
const manager = injector.get(IDrawingManagerService);
|
||||
const domDrawing = { unitId, subUnitId: unitId, drawingId: 'dom', drawingType: DrawingTypeEnum.DRAWING_DOM, componentKey: 'custom-dom' };
|
||||
manager.setDrawingData(unitId, unitId, {
|
||||
chart: { unitId, subUnitId: unitId, drawingId: 'chart', drawingType: DrawingTypeEnum.DRAWING_CHART },
|
||||
dom: domDrawing,
|
||||
});
|
||||
manager.setDrawingOrder(unitId, unitId, ['chart', 'dom']);
|
||||
const chartComponent = () => null;
|
||||
const domComponent = () => null;
|
||||
injector.get(ComponentManager).register(DOC_DRAWING_PRINTING_COMPONENT_KEY, chartComponent);
|
||||
injector.get(ComponentManager).register('custom-dom-print', domComponent);
|
||||
printService.registerPrintComponent('custom-dom', 'custom-dom-print');
|
||||
const unmount = vi.fn();
|
||||
vi.mocked(mountDocPrintingFloatDom).mockReturnValueOnce(unmount);
|
||||
const disposables = new DisposableCollection();
|
||||
const root = document.createElement('div');
|
||||
const result = printService.interceptor.fetchThroughInterceptors(
|
||||
printService.interceptor.getInterceptPoints().PRINTING_DOM_COLLECT
|
||||
)(disposables, { unitId, scene, engine, root, skeleton, offset: { x: 0, y: 0 }, bound: { top: 0, left: 0, right: 240, bottom: 400 } });
|
||||
expect(mountDocPrintingFloatDom).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
floatDomInfos: [
|
||||
expect.objectContaining({ drawingId: 'chart', componentKey: chartComponent }),
|
||||
expect.objectContaining({ drawingId: 'dom', componentKey: domComponent }),
|
||||
],
|
||||
}),
|
||||
root,
|
||||
injector
|
||||
);
|
||||
expect(result).toBe(disposables);
|
||||
disposables.dispose();
|
||||
expect(unmount).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,690 @@
|
||||
/**
|
||||
* 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 {
|
||||
IDocumentSkeletonColumn,
|
||||
IDocumentSkeletonDivide,
|
||||
IDocumentSkeletonGlyph,
|
||||
IDocumentSkeletonLine,
|
||||
IDocumentSkeletonPage,
|
||||
IDocumentSkeletonSection,
|
||||
} from '@univerjs/engine-render';
|
||||
import { BooleanNumber, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType } from '@univerjs/core';
|
||||
import { UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
|
||||
import { NodePositionConvertToCursor } from '@univerjs/docs-ui';
|
||||
import { DocumentSkeletonPageType } from '@univerjs/engine-render';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
IMoveInlineDrawingCommand,
|
||||
ITransformNonInlineDrawingCommand,
|
||||
} from '../../commands/commands/update-doc-drawing.command';
|
||||
import {
|
||||
DocDrawingTransformerController,
|
||||
getDocsTableCellAnchorContext,
|
||||
shouldUseDocsDrawingOuterPageOrigin,
|
||||
} from '../doc-drawing-transformer-update.controller';
|
||||
import { getDocsOverlayRuntimeDrawing } from '../render-controllers/doc-drawing-transform-update.controller';
|
||||
|
||||
function createController() {
|
||||
const controller = Object.create(DocDrawingTransformerController.prototype) as any;
|
||||
controller._commandService = { executeCommand: vi.fn() };
|
||||
controller._drawingManagerService = {
|
||||
getDrawingOKey: vi.fn((oKey: string) => ({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: oKey.replace('object-', 'drawing-'),
|
||||
})),
|
||||
};
|
||||
controller._transformerCache = new Map();
|
||||
controller._liquid = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
reset: vi.fn(),
|
||||
translatePage: vi.fn((_page: any) => {
|
||||
controller._liquid.y += 140;
|
||||
}),
|
||||
};
|
||||
controller._renderManagerService = {
|
||||
getRenderUnitById: vi.fn(() => null),
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
function drawing(drawingId = 'drawing-1') {
|
||||
return {
|
||||
drawingId,
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
docTransform: {
|
||||
size: { width: 80, height: 60 },
|
||||
angle: 0,
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 10,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 20,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('DocDrawingTransformerController business methods', () => {
|
||||
it('uses the latest model transform only while both drawing states are overlay-only', () => {
|
||||
const skeletonOverlay = {
|
||||
...drawing(),
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
};
|
||||
const currentOverlay = {
|
||||
...skeletonOverlay,
|
||||
docTransform: {
|
||||
...skeletonOverlay.docTransform,
|
||||
size: { width: 160, height: 120 },
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getDocsOverlayRuntimeDrawing(skeletonOverlay, currentOverlay)).toBe(currentOverlay);
|
||||
expect(getDocsOverlayRuntimeDrawing(drawing(), currentOverlay)).not.toBe(currentOverlay);
|
||||
expect(getDocsOverlayRuntimeDrawing(skeletonOverlay, drawing())).not.toBe(currentOverlay);
|
||||
});
|
||||
|
||||
it('builds drawing transform updates for multi-select resize, rotation, and move changes', () => {
|
||||
const controller = createController();
|
||||
const drawingData = drawing();
|
||||
controller._getPageContentSize = vi.fn(() => ({ width: 100, height: 90 }));
|
||||
controller._transformerCache.set('drawing-1', {
|
||||
drawing: drawingData,
|
||||
top: 20,
|
||||
left: 10,
|
||||
width: 80,
|
||||
height: 60,
|
||||
angle: 0,
|
||||
});
|
||||
|
||||
controller._updateMultipleDrawingDocTransform(new Map([
|
||||
['object-1', {
|
||||
oKey: 'object-1',
|
||||
left: 25,
|
||||
top: 45,
|
||||
width: 160,
|
||||
height: 120,
|
||||
angle: 30,
|
||||
}],
|
||||
]));
|
||||
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawings: [
|
||||
{ drawingId: 'drawing-1', key: 'size', value: { width: 100, height: 90 } },
|
||||
{ drawingId: 'drawing-1', key: 'angle', value: 30 },
|
||||
{ drawingId: 'drawing-1', key: 'positionV', value: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 45 } },
|
||||
{ drawingId: 'drawing-1', key: 'positionH', value: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 25 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('updates inline drawing size and angle while clamping to page content size', () => {
|
||||
const controller = createController();
|
||||
controller._getPageContentSize = vi.fn(() => ({ width: 120, height: 90 }));
|
||||
|
||||
controller._updateDrawingSize({
|
||||
drawing: drawing(),
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 80,
|
||||
height: 60,
|
||||
angle: 0,
|
||||
}, {
|
||||
width: 300,
|
||||
height: 100,
|
||||
angle: 15,
|
||||
});
|
||||
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawings: [
|
||||
{ drawingId: 'drawing-1', key: 'size', value: { width: 120, height: 90 } },
|
||||
{ drawingId: 'drawing-1', key: 'angle', value: 15 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('moves inline drawings to the resolved anchor and refreshes when no anchor is available', () => {
|
||||
const controller = createController();
|
||||
controller._getInlineDrawingAnchor = vi.fn(() => ({
|
||||
offset: 12,
|
||||
segmentId: 'segment-1',
|
||||
segmentPage: 2,
|
||||
}));
|
||||
|
||||
controller._moveInlineDrawing(drawing(), 40, 50);
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(IMoveInlineDrawingCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'unit-1',
|
||||
drawing: drawing(),
|
||||
offset: 12,
|
||||
segmentId: 'segment-1',
|
||||
segmentPage: 2,
|
||||
needRefreshDrawings: false,
|
||||
});
|
||||
|
||||
controller._commandService.executeCommand.mockClear();
|
||||
controller._getInlineDrawingAnchor.mockReturnValue(null);
|
||||
controller._moveInlineDrawing(drawing(), 400, 500);
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(IMoveInlineDrawingCommand.id, expect.objectContaining({
|
||||
offset: undefined,
|
||||
needRefreshDrawings: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('transforms non-inline drawings through a new anchor and falls back to direct transform when no anchor is found', () => {
|
||||
const controller = createController();
|
||||
const docTransform = {
|
||||
size: { width: 90, height: 70 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.MARGIN, posOffset: 11 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 22 },
|
||||
};
|
||||
controller._limitDrawingInPage = vi.fn((_drawing: any, object: any) => object);
|
||||
controller._getDrawingAnchor = vi.fn(() => ({
|
||||
offset: 23,
|
||||
docTransform,
|
||||
segmentId: 'body',
|
||||
segmentPage: 0,
|
||||
}));
|
||||
|
||||
controller._nonInlineDrawingTransform(drawing(), { left: 8, top: 9, width: 90, height: 70 });
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(ITransformNonInlineDrawingCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'unit-1',
|
||||
drawing: drawing(),
|
||||
offset: 23,
|
||||
docTransform,
|
||||
segmentId: 'body',
|
||||
segmentPage: 0,
|
||||
});
|
||||
|
||||
controller._commandService.executeCommand.mockClear();
|
||||
controller._getDrawingAnchor.mockReturnValue(null);
|
||||
controller._updateMultipleDrawingDocTransform = vi.fn();
|
||||
controller._nonInlineDrawingTransform(drawing(), { left: 18, top: 19, width: 90, height: 70 });
|
||||
expect(controller._updateMultipleDrawingDocTransform).toHaveBeenCalledWith(new Map([['drawing-1', {
|
||||
left: 18,
|
||||
top: 19,
|
||||
width: 90,
|
||||
height: 70,
|
||||
}]]));
|
||||
});
|
||||
|
||||
it('uses the outer page origin only for WRAP_NONE page-relative positions', () => {
|
||||
const controller = createController();
|
||||
const previousPage = {
|
||||
type: DocumentSkeletonPageType.BODY,
|
||||
marginLeft: 30,
|
||||
marginTop: 40,
|
||||
};
|
||||
const anchorPage = {
|
||||
type: DocumentSkeletonPageType.BODY,
|
||||
marginLeft: 48,
|
||||
marginTop: 64,
|
||||
};
|
||||
const glyph = {} as IDocumentSkeletonGlyph;
|
||||
const divide = { glyphGroup: [glyph] } as IDocumentSkeletonDivide;
|
||||
const line = {
|
||||
divides: [divide],
|
||||
paragraphIndex: 0,
|
||||
paragraphStart: true,
|
||||
top: 0,
|
||||
} as IDocumentSkeletonLine;
|
||||
const section = { parent: anchorPage, top: 0 } as unknown as IDocumentSkeletonSection;
|
||||
const column = { left: 0, lines: [line], parent: section } as IDocumentSkeletonColumn;
|
||||
Object.assign(glyph, { parent: divide });
|
||||
Object.assign(divide, { parent: line });
|
||||
Object.assign(line, { parent: column });
|
||||
|
||||
const skeleton = {
|
||||
findNodeByCoord: vi.fn(() => ({
|
||||
node: glyph,
|
||||
segmentId: '',
|
||||
segmentPage: 1,
|
||||
})),
|
||||
findPositionByGlyph: vi.fn(() => ({})),
|
||||
getSkeletonData: vi.fn(() => ({
|
||||
pages: [previousPage, anchorPage],
|
||||
skeFooters: new Map(),
|
||||
skeHeaders: new Map(),
|
||||
})),
|
||||
};
|
||||
const renderDependency = {
|
||||
getSegment: vi.fn(() => ''),
|
||||
getSegmentPage: vi.fn(() => 1),
|
||||
getSkeleton: vi.fn(() => skeleton),
|
||||
};
|
||||
const mainComponent = {
|
||||
getOffsetConfig: vi.fn(() => ({
|
||||
docsLeft: 5,
|
||||
docsTop: 7,
|
||||
pageLayoutType: 0,
|
||||
pageMarginLeft: 0,
|
||||
pageMarginTop: 20,
|
||||
})),
|
||||
};
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
components: new Map(),
|
||||
engine: {},
|
||||
mainComponent,
|
||||
scene: {
|
||||
getViewports: vi.fn(() => [{}]),
|
||||
getViewportScrollXY: vi.fn(() => ({ x: 0, y: 0 })),
|
||||
},
|
||||
with: vi.fn(() => renderDependency),
|
||||
});
|
||||
controller._getPageContentSize = vi.fn(() => ({ width: 1000, height: 1000 }));
|
||||
controller._getTransformCoordForDocumentOffset = vi.fn(() => ({}));
|
||||
controller._liquid = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
reset: vi.fn(() => {
|
||||
controller._liquid.x = 0;
|
||||
controller._liquid.y = 0;
|
||||
}),
|
||||
restorePagePadding: vi.fn((page: { marginLeft: number; marginTop: number }) => {
|
||||
controller._liquid.x -= page.marginLeft;
|
||||
controller._liquid.y -= page.marginTop;
|
||||
}),
|
||||
translatePage: vi.fn(() => {
|
||||
controller._liquid.x += 300;
|
||||
controller._liquid.y += 400;
|
||||
}),
|
||||
translatePagePadding: vi.fn((page: { marginLeft: number; marginTop: number }) => {
|
||||
controller._liquid.x += page.marginLeft;
|
||||
controller._liquid.y += page.marginTop;
|
||||
}),
|
||||
};
|
||||
const rangeSpy = vi.spyOn(NodePositionConvertToCursor.prototype, 'getRangePointData').mockReturnValue({
|
||||
borderBoxPointGroup: [],
|
||||
contentBoxPointGroup: [],
|
||||
cursorList: [{ collapsed: true, endOffset: 0, startOffset: 0 }],
|
||||
} as ReturnType<NodePositionConvertToCursor['getRangePointData']>);
|
||||
const overlayDrawing = {
|
||||
...drawing(),
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
};
|
||||
const object = {
|
||||
angle: 0,
|
||||
height: 60,
|
||||
left: 395,
|
||||
top: 527,
|
||||
width: 80,
|
||||
};
|
||||
|
||||
const anchor = controller._getDrawingAnchor(overlayDrawing, object);
|
||||
|
||||
expect(anchor?.docTransform.positionH).toEqual({
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 90,
|
||||
});
|
||||
expect(anchor?.docTransform.positionV).toEqual({
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 120,
|
||||
});
|
||||
|
||||
const wrappedAnchor = controller._getDrawingAnchor(drawing(), object);
|
||||
expect(wrappedAnchor?.docTransform.positionH.posOffset).toBe(42);
|
||||
expect(wrappedAnchor?.docTransform.positionV.posOffset).toBe(120);
|
||||
|
||||
const topAndBottomAnchor = controller._getDrawingAnchor({
|
||||
...drawing(),
|
||||
layoutType: PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM,
|
||||
}, object);
|
||||
expect(topAndBottomAnchor?.docTransform.positionH.posOffset).toBe(42);
|
||||
expect(topAndBottomAnchor?.docTransform.positionV.posOffset).toBe(120);
|
||||
rangeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('matches the render-side page origin for body and header/footer drawings', () => {
|
||||
const bodyPage = {
|
||||
type: DocumentSkeletonPageType.BODY,
|
||||
pageWidth: 816,
|
||||
pageHeight: 1056,
|
||||
} as IDocumentSkeletonPage;
|
||||
const headerPage = {
|
||||
type: DocumentSkeletonPageType.HEADER,
|
||||
pageWidth: 816,
|
||||
pageHeight: 120,
|
||||
} as IDocumentSkeletonPage;
|
||||
const overlayDrawing = {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
};
|
||||
|
||||
expect(shouldUseDocsDrawingOuterPageOrigin({
|
||||
drawing: overlayDrawing,
|
||||
height: 60,
|
||||
page: bodyPage,
|
||||
width: 80,
|
||||
})).toBe(true);
|
||||
expect(shouldUseDocsDrawingOuterPageOrigin({
|
||||
drawing: overlayDrawing,
|
||||
height: 60,
|
||||
hostPage: bodyPage,
|
||||
page: headerPage,
|
||||
width: 80,
|
||||
})).toBe(false);
|
||||
expect(shouldUseDocsDrawingOuterPageOrigin({
|
||||
drawing: overlayDrawing,
|
||||
height: 1055,
|
||||
hostPage: bodyPage,
|
||||
page: headerPage,
|
||||
width: 815,
|
||||
})).toBe(true);
|
||||
expect(shouldUseDocsDrawingOuterPageOrigin({
|
||||
drawing: {
|
||||
...overlayDrawing,
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
},
|
||||
height: 60,
|
||||
page: bodyPage,
|
||||
width: 80,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('limits a drawing away from the gap between document pages while preserving drawings already on one page', () => {
|
||||
const controller = createController();
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
mainComponent: {
|
||||
top: 5,
|
||||
pageLayoutType: 0,
|
||||
pageMarginLeft: 0,
|
||||
pageMarginTop: 20,
|
||||
getOffsetConfig: () => ({
|
||||
pageLayoutType: 0,
|
||||
pageMarginLeft: 0,
|
||||
pageMarginTop: 20,
|
||||
}),
|
||||
},
|
||||
with: vi.fn(() => ({
|
||||
getSkeleton: () => ({
|
||||
getSkeletonData: () => ({
|
||||
pages: [
|
||||
{ pageHeight: 100, marginBottom: 10, marginTop: 10 },
|
||||
{ pageHeight: 100, marginBottom: 10, marginTop: 10 },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
});
|
||||
|
||||
expect(controller._limitDrawingInPage(drawing(), {
|
||||
left: 1,
|
||||
top: 85,
|
||||
width: 30,
|
||||
height: 20,
|
||||
angle: 0,
|
||||
})).toMatchObject({ top: 75 });
|
||||
expect(controller._limitDrawingInPage(drawing(), {
|
||||
left: 1,
|
||||
top: 30,
|
||||
width: 30,
|
||||
height: 20,
|
||||
angle: 0,
|
||||
})).toMatchObject({ top: 30 });
|
||||
});
|
||||
|
||||
it('resolves only real table-cell anchors to their table, row, and host page context', () => {
|
||||
expect(getDocsTableCellAnchorContext('unit-1', { parent: null } as any)).toBeNull();
|
||||
|
||||
const hostPage = { type: 'body' };
|
||||
const cell = { left: 1, marginLeft: 2, marginTop: 3 };
|
||||
const row = { cells: [], top: 4 };
|
||||
const table = { left: 5, top: 6, tableId: 'table-1#-#0', parent: hostPage };
|
||||
Object.assign(row, { cells: [cell], parent: table });
|
||||
Object.assign(cell, { parent: row });
|
||||
|
||||
expect(getDocsTableCellAnchorContext('unit-1', cell as any)).toMatchObject({
|
||||
cell,
|
||||
row,
|
||||
table,
|
||||
hostPage,
|
||||
});
|
||||
});
|
||||
|
||||
it('limits page content size by the page containing the drawing and falls back when the skeleton is unavailable', () => {
|
||||
const controller = createController();
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
with: vi.fn(() => ({
|
||||
getSkeleton: () => ({
|
||||
getSkeletonData: () => ({
|
||||
pages: [
|
||||
{
|
||||
pageWidth: 720,
|
||||
pageHeight: 960,
|
||||
marginLeft: 40,
|
||||
marginRight: 60,
|
||||
marginTop: 50,
|
||||
marginBottom: 70,
|
||||
skeDrawings: new Map([['drawing-1', {}]]),
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
});
|
||||
|
||||
expect(controller._getPageContentSize(drawing())).toEqual({ width: 620, height: 840 });
|
||||
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
with: vi.fn(() => ({
|
||||
getSkeleton: () => ({
|
||||
getSkeletonData: () => ({
|
||||
pages: [
|
||||
{
|
||||
isLayoutPlaceholder: true,
|
||||
pageWidth: 720,
|
||||
pageHeight: 960,
|
||||
marginLeft: 40,
|
||||
marginRight: 60,
|
||||
marginTop: 50,
|
||||
marginBottom: 70,
|
||||
skeDrawings: new Map([['drawing-1', {}]]),
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
});
|
||||
expect(controller._getPageContentSize(drawing())).toEqual({ width: 500, height: 500 });
|
||||
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue(null);
|
||||
expect(controller._getPageContentSize(drawing())).toEqual({ width: 500, height: 500 });
|
||||
});
|
||||
|
||||
it('creates and updates the inline anchor from resolved text range points', () => {
|
||||
const controller = createController();
|
||||
const scene = { addObject: vi.fn() };
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
mainComponent: {
|
||||
getOffsetConfig: () => ({ docsLeft: 8, docsTop: 12 }),
|
||||
},
|
||||
scene,
|
||||
});
|
||||
|
||||
controller._createOrUpdateInlineAnchor('unit-1', [[
|
||||
{ x: 10, y: 20 },
|
||||
{ x: 10, y: 40 },
|
||||
{ x: 10, y: 60 },
|
||||
]]);
|
||||
|
||||
expect(scene.addObject).toHaveBeenCalledTimes(1);
|
||||
const anchor = controller._anchorShape;
|
||||
const transformByState = vi.spyOn(anchor, 'transformByState');
|
||||
const show = vi.spyOn(anchor, 'show');
|
||||
|
||||
controller._createOrUpdateInlineAnchor('unit-1', [[
|
||||
{ x: 14, y: 26 },
|
||||
{ x: 14, y: 50 },
|
||||
{ x: 14, y: 76 },
|
||||
]]);
|
||||
|
||||
expect(transformByState).toHaveBeenCalledWith(expect.objectContaining({
|
||||
left: 22,
|
||||
top: 38,
|
||||
height: 50,
|
||||
}));
|
||||
expect(show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('finds the scene transformer for a drawing search only when render context exists', () => {
|
||||
const controller = createController();
|
||||
const transformer = {};
|
||||
const scene = { getTransformerByCreate: vi.fn(() => transformer) };
|
||||
|
||||
expect(controller._getSceneAndTransformerByDrawingSearch(null)).toBeUndefined();
|
||||
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({});
|
||||
expect(controller._getSceneAndTransformerByDrawingSearch('unit-1')).toBeUndefined();
|
||||
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({ scene });
|
||||
expect(controller._getSceneAndTransformerByDrawingSearch('unit-1')).toEqual({ scene, transformer });
|
||||
});
|
||||
|
||||
it('updates the inline drawing anchor only when a single drawing is being transformed', () => {
|
||||
const controller = createController();
|
||||
const points = [[
|
||||
{ x: 1, y: 2 },
|
||||
{ x: 1, y: 3 },
|
||||
{ x: 1, y: 4 },
|
||||
]];
|
||||
controller._getInlineDrawingAnchor = vi.fn(() => ({ contentBoxPointGroup: points }));
|
||||
controller._createOrUpdateInlineAnchor = vi.fn();
|
||||
|
||||
controller._updateInlineDrawingAnchor(drawing(), 10, 20);
|
||||
expect(controller._createOrUpdateInlineAnchor).not.toHaveBeenCalled();
|
||||
|
||||
controller._transformerCache.set('drawing-1', {});
|
||||
controller._updateInlineDrawingAnchor(drawing(), 10, 20);
|
||||
expect(controller._createOrUpdateInlineAnchor).toHaveBeenCalledWith('unit-1', points);
|
||||
});
|
||||
|
||||
it('converts pointer offsets from viewport coordinates into document coordinates', () => {
|
||||
const controller = createController();
|
||||
const invertedTransform = {
|
||||
applyPoint: vi.fn((point) => ({ x: point.x - 5, y: point.y - 7 })),
|
||||
};
|
||||
const documentTransform = {
|
||||
clone: vi.fn(() => ({
|
||||
invert: vi.fn(() => invertedTransform),
|
||||
})),
|
||||
};
|
||||
const document = {
|
||||
getOffsetConfig: () => ({ documentTransform }),
|
||||
};
|
||||
const viewport = {
|
||||
transformVector2SceneCoord: vi.fn(() => ({ x: 30, y: 40 })),
|
||||
};
|
||||
|
||||
expect(controller._getTransformCoordForDocumentOffset(document, viewport, 12, 18)).toEqual({ x: 25, y: 33 });
|
||||
|
||||
viewport.transformVector2SceneCoord.mockReturnValueOnce(undefined as never);
|
||||
expect(controller._getTransformCoordForDocumentOffset(document, viewport, 12, 18)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('listens to transformer changes for inline drawings and commits resize or move business actions', () => {
|
||||
const controller = createController();
|
||||
const changeStart$ = new Subject<any>();
|
||||
const changing$ = new Subject<any>();
|
||||
const changeEnd$ = new Subject<any>();
|
||||
const transformer = { changeStart$, changing$, changeEnd$ };
|
||||
const object = {
|
||||
oKey: 'object-inline',
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 80,
|
||||
height: 60,
|
||||
angle: 0,
|
||||
setOpacity: vi.fn(),
|
||||
};
|
||||
const inlineDrawing = {
|
||||
...drawing('drawing-inline'),
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
};
|
||||
controller.disposeWithMe = vi.fn();
|
||||
controller._renderManagerService.getRenderUnitById.mockReturnValue({
|
||||
scene: {
|
||||
getTransformerByCreate: vi.fn(() => transformer),
|
||||
},
|
||||
});
|
||||
controller._drawingManagerService.getDrawingOKey = vi.fn(() => ({
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'drawing-inline',
|
||||
}));
|
||||
controller._univerInstanceService = {
|
||||
getUnit: vi.fn(() => ({
|
||||
getSnapshot: () => ({
|
||||
drawings: {
|
||||
'drawing-inline': inlineDrawing,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
};
|
||||
controller._getPageContentSize = vi.fn(() => ({ width: 500, height: 500 }));
|
||||
controller._updateInlineDrawingAnchor = vi.fn();
|
||||
controller._anchorShape = { hide: vi.fn() };
|
||||
|
||||
controller._listenTransformerChange('unit-1');
|
||||
|
||||
changeStart$.next({ objects: new Map([['object-inline', object]]) });
|
||||
expect(object.setOpacity).toHaveBeenCalledWith(0.2);
|
||||
expect(controller._transformerCache.get('drawing-inline')).toMatchObject({
|
||||
drawing: inlineDrawing,
|
||||
top: 20,
|
||||
left: 10,
|
||||
});
|
||||
|
||||
changing$.next({
|
||||
objects: new Map([['object-inline', { ...object, left: 30 }]]),
|
||||
offsetX: 40,
|
||||
offsetY: 50,
|
||||
});
|
||||
expect(controller._updateInlineDrawingAnchor).toHaveBeenCalledWith(inlineDrawing, 40, 50);
|
||||
|
||||
changeEnd$.next({
|
||||
objects: new Map([['object-inline', { ...object, width: 100 }]]),
|
||||
offsetX: 40,
|
||||
offsetY: 50,
|
||||
});
|
||||
|
||||
expect(object.setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(controller._anchorShape.hide).toHaveBeenCalled();
|
||||
expect(controller._commandService.executeCommand).toHaveBeenCalledWith(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: 'unit-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawings: [
|
||||
{ drawingId: 'drawing-inline', key: 'size', value: { width: 100, height: 60 } },
|
||||
],
|
||||
});
|
||||
expect(controller._transformerCache.size).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,903 @@
|
||||
/**
|
||||
* 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, UniverInstanceType } from '@univerjs/core';
|
||||
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { SetDocZoomRatioOperation } from '@univerjs/docs-ui';
|
||||
import { Rect } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service';
|
||||
import {
|
||||
calcDocFloatDomPositionByRect,
|
||||
DocFloatDomController,
|
||||
mergeDocFloatDomRuntimeProps,
|
||||
} from '../doc-float-dom.controller';
|
||||
|
||||
function createScene() {
|
||||
const viewport = { viewportScrollX: 10, viewportScrollY: 20, onScrollAfter$: new Subject() };
|
||||
return {
|
||||
viewport,
|
||||
getViewport: vi.fn(() => viewport),
|
||||
getAncestorScale: vi.fn(() => ({ scaleX: 2, scaleY: 3 })),
|
||||
getTransformerByCreate: vi.fn(() => ({})),
|
||||
removeObject: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function createController(options: {
|
||||
drawing?: Record<string, unknown>;
|
||||
rects?: Rect[];
|
||||
page?: any;
|
||||
renderType?: UniverInstanceType;
|
||||
refreshDrawingOnAdd?: { left: number; top: number; width: number; height: number; angle: number };
|
||||
resolveRefreshDrawing?: () => { left: number; top: number; width: number; height: number; angle: number } | undefined;
|
||||
} = {}) {
|
||||
const add$ = new Subject<any[]>();
|
||||
const remove$ = new Subject<any[]>();
|
||||
const refreshTransform$ = new Subject<any[]>();
|
||||
const currentSkeleton$ = new Subject<any>();
|
||||
const skeletonDirty$ = new Subject<boolean>();
|
||||
const renderCreated$ = new Subject<any>();
|
||||
const renderDisposed$ = new Subject<string>();
|
||||
const commandHandlers: Array<(command: { id: string; params?: unknown }) => void> = [];
|
||||
const scene = createScene();
|
||||
const canvas = { dispatchEvent: vi.fn() };
|
||||
const docRefreshDrawingsService = new DocRefreshDrawingsService();
|
||||
const refreshDrawings = vi.spyOn(docRefreshDrawingsService, 'refreshDrawings');
|
||||
docRefreshDrawingsService.refreshDrawings$.subscribe((skeleton) => {
|
||||
const transform = options.resolveRefreshDrawing?.() ?? options.refreshDrawingOnAdd;
|
||||
if (skeleton != null && transform) {
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
transform,
|
||||
}]);
|
||||
}
|
||||
});
|
||||
const skeleton = {
|
||||
dirty$: skeletonDirty$,
|
||||
getSkeletonData: () => ({ pages: [options.page ?? { pageWidth: 240, marginLeft: 20, marginRight: 30 }] }),
|
||||
};
|
||||
const renderUnit = {
|
||||
unitId: 'doc-1',
|
||||
type: options.renderType ?? UniverInstanceType.UNIVER_DOC,
|
||||
scene,
|
||||
engine: { getCanvasElement: () => canvas },
|
||||
isDisposed: vi.fn(() => false),
|
||||
with: vi.fn(() => ({
|
||||
currentSkeleton$,
|
||||
getSkeleton: () => skeleton,
|
||||
})),
|
||||
};
|
||||
const renderManagerService = {
|
||||
created$: renderCreated$,
|
||||
disposed$: renderDisposed$,
|
||||
getRenderUnitById: vi.fn(() => renderUnit),
|
||||
};
|
||||
const drawing = {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
componentKey: 'FloatDom',
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
data: { value: 1 },
|
||||
...options.drawing,
|
||||
};
|
||||
const drawingManagerService = {
|
||||
add$,
|
||||
remove$,
|
||||
refreshTransform$,
|
||||
getDrawingByParam: vi.fn(() => drawing),
|
||||
};
|
||||
const drawingRenderService = {
|
||||
renderFloatDom: vi.fn(() => options.rects ?? []),
|
||||
};
|
||||
const domLayers: Array<[string, any]> = [];
|
||||
const canvasFloatDomService = {
|
||||
domLayers,
|
||||
addFloatDom: vi.fn((layer) => {
|
||||
domLayers.push([layer.id, layer]);
|
||||
}),
|
||||
updateFloatDom: vi.fn((id, patch) => {
|
||||
const layer = domLayers.find(([layerId]) => layerId === id);
|
||||
if (layer) {
|
||||
Object.assign(layer[1], patch);
|
||||
}
|
||||
}),
|
||||
removeFloatDom: vi.fn(),
|
||||
};
|
||||
const doc = { getUnitId: () => 'doc-1' };
|
||||
const univerInstanceService = {
|
||||
getUnit: vi.fn(() => doc),
|
||||
getCurrentUnitOfType: vi.fn(() => doc),
|
||||
getCurrentTypeOfUnit$: vi.fn(() => new BehaviorSubject(null)),
|
||||
};
|
||||
const commandService = {
|
||||
onCommandExecuted: vi.fn((handler) => {
|
||||
commandHandlers.push(handler);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
syncExecuteCommand: vi.fn(),
|
||||
};
|
||||
|
||||
const controller = new DocFloatDomController(
|
||||
renderManagerService as never,
|
||||
drawingManagerService as never,
|
||||
drawingRenderService as never,
|
||||
canvasFloatDomService as never,
|
||||
univerInstanceService as never,
|
||||
commandService as never,
|
||||
docRefreshDrawingsService
|
||||
);
|
||||
|
||||
return {
|
||||
controller,
|
||||
add$,
|
||||
currentSkeleton$,
|
||||
remove$,
|
||||
refreshTransform$,
|
||||
commandHandlers,
|
||||
scene,
|
||||
canvas,
|
||||
renderUnit,
|
||||
drawingRenderService,
|
||||
canvasFloatDomService,
|
||||
commandService,
|
||||
refreshDrawings,
|
||||
renderCreated$,
|
||||
renderDisposed$,
|
||||
skeletonDirty$,
|
||||
};
|
||||
}
|
||||
|
||||
function publishEmbedRuntimeGeometry(
|
||||
refreshTransform$: ReturnType<typeof createController>['refreshTransform$'],
|
||||
transform = { left: 30, top: 50, width: 50, height: 40, angle: 0 }
|
||||
) {
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
transform,
|
||||
}]);
|
||||
}
|
||||
|
||||
describe('DocFloatDomController', () => {
|
||||
it('preserves existing props while adding custom block runtime viewport', () => {
|
||||
expect(mergeDocFloatDomRuntimeProps({ keep: true }, {
|
||||
customBlockRenderViewport: { bleedLeft: 96, bleedWidth: 1440, contentHeight: 720, contentWidth: 1280, height: 480, pageContentWidth: 1008, viewScale: 1.5, viewportHeight: 320 },
|
||||
} as never)).toEqual({
|
||||
customBlockRenderViewport: { bleedLeft: 96, bleedWidth: 1440, contentHeight: 720, contentWidth: 1280, height: 480, pageContentWidth: 1008, viewScale: 1.5, viewportHeight: 320 },
|
||||
keep: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps existing props when no valid runtime viewport is available', () => {
|
||||
expect(mergeDocFloatDomRuntimeProps({ keep: true }, {
|
||||
customBlockRenderViewport: { contentWidth: 0 },
|
||||
} as never)).toEqual({ keep: true });
|
||||
});
|
||||
|
||||
it('calculates float dom bounds using doc viewport scroll and ancestor scale', () => {
|
||||
const scene = createScene();
|
||||
|
||||
expect(calcDocFloatDomPositionByRect({ left: 30, top: 50, right: 80, bottom: 90 }, scene as never, 0.4, 15)).toEqual({
|
||||
startX: 40,
|
||||
startY: 90,
|
||||
endX: 140,
|
||||
endY: 210,
|
||||
width: 100,
|
||||
height: 120,
|
||||
rotate: 15,
|
||||
absolute: { left: false, top: false },
|
||||
opacity: 0.4,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates float dom position after host doc zoom without rewriting runtime viewport', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const { add$, canvasFloatDomService, commandHandlers, controller, scene } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
customBlockRenderViewport: {
|
||||
contentHeight: 240,
|
||||
height: 240,
|
||||
viewScale: 1,
|
||||
viewportHeight: 120,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$ as BehaviorSubject<unknown>;
|
||||
scene.getAncestorScale.mockReturnValue({ scaleX: 1, scaleY: 1 });
|
||||
commandHandlers.forEach((handler) => handler({
|
||||
id: SetDocZoomRatioOperation.id,
|
||||
params: { unitId: 'doc-1', zoomRatio: 2 },
|
||||
}));
|
||||
scene.getAncestorScale.mockReturnValue({ scaleX: 2, scaleY: 2 });
|
||||
await Promise.resolve();
|
||||
|
||||
expect(position$.getValue()).toMatchObject({
|
||||
startX: 40,
|
||||
startY: 60,
|
||||
width: 100,
|
||||
height: 480,
|
||||
});
|
||||
expect(canvasFloatDomService.updateFloatDom).not.toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('adds rendered float doms, updates their position, and removes them with their rect object', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
rect.setOpacity(0.8);
|
||||
const { controller, add$, remove$, scene, drawingRenderService, canvasFloatDomService } = createController({ rects: [rect] });
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(drawingRenderService.renderFloatDom).toHaveBeenCalledWith(expect.objectContaining({ drawingId: 'dom-1' }), scene);
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'dom-1',
|
||||
componentKey: 'FloatDom',
|
||||
unitId: 'doc-1',
|
||||
data: { value: 1 },
|
||||
}));
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
rect.transformByState({ left: 40, top: 60 });
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startX: 60,
|
||||
startY: 120,
|
||||
width: 100,
|
||||
height: 120,
|
||||
opacity: 0.8,
|
||||
});
|
||||
|
||||
remove$.next([{ drawingId: 'dom-1' }]);
|
||||
expect(canvasFloatDomService.removeFloatDom).toHaveBeenCalledWith('dom-1');
|
||||
expect(scene.removeObject).toHaveBeenCalledWith(rect);
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps embed custom block float doms visible when focus moves into child units', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledWith(expect.objectContaining({
|
||||
eventPassThrough: false,
|
||||
preserveOnFocusChange: true,
|
||||
}));
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('waits for layout runtime geometry before mounting an embed custom block', () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const { controller, add$, refreshTransform$, drawingRenderService, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
|
||||
expect(drawingRenderService.renderFloatDom).not.toHaveBeenCalled();
|
||||
expect(canvasFloatDomService.addFloatDom).not.toHaveBeenCalled();
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$, { left: 300, top: 500, width: 50, height: 40, angle: 0 });
|
||||
|
||||
expect(drawingRenderService.renderFloatDom).toHaveBeenCalledOnce();
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledOnce();
|
||||
expect(canvasFloatDomService.addFloatDom.mock.calls[0][0].position$.getValue()).toMatchObject({
|
||||
startX: 580,
|
||||
startY: 1440,
|
||||
width: 100,
|
||||
height: 120,
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('refreshes current layout geometry when an embed drawing is added after publication', () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const { controller, add$, refreshDrawings, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
refreshDrawingOnAdd: { left: 300, top: 500, width: 50, height: 40, angle: 0 },
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
|
||||
expect(refreshDrawings).toHaveBeenCalledOnce();
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledOnce();
|
||||
expect(canvasFloatDomService.addFloatDom.mock.calls[0][0].position$.getValue()).toMatchObject({
|
||||
startX: 580,
|
||||
startY: 1440,
|
||||
width: 100,
|
||||
height: 120,
|
||||
});
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not resolve layout services from a disposed render unit', () => {
|
||||
const { controller, add$, renderUnit, refreshDrawings } = createController({
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
renderUnit.isDisposed.mockReturnValue(true);
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
|
||||
expect(renderUnit.with).not.toHaveBeenCalled();
|
||||
expect(refreshDrawings).not.toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not resolve Doc layout services from a non-Doc render unit', () => {
|
||||
const { controller, add$, renderUnit, refreshDrawings } = createController({
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
renderType: UniverInstanceType.UNIVER_SHEET,
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
|
||||
expect(renderUnit.with).not.toHaveBeenCalled();
|
||||
expect(refreshDrawings).not.toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('resumes pending embed geometry when the host render is recreated', () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const {
|
||||
controller,
|
||||
add$,
|
||||
canvasFloatDomService,
|
||||
renderCreated$,
|
||||
renderDisposed$,
|
||||
renderUnit,
|
||||
} = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
refreshDrawingOnAdd: { left: 300, top: 500, width: 50, height: 40, angle: 0 },
|
||||
});
|
||||
renderUnit.isDisposed.mockReturnValue(true);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
expect(canvasFloatDomService.addFloatDom).not.toHaveBeenCalled();
|
||||
|
||||
renderDisposed$.next('doc-1');
|
||||
renderUnit.isDisposed.mockReturnValue(false);
|
||||
renderCreated$.next(renderUnit);
|
||||
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledOnce();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('refreshes a pending embed drawing when the current skeleton becomes dirty', () => {
|
||||
let geometry: { left: number; top: number; width: number; height: number; angle: number } | undefined;
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const { controller, add$, skeletonDirty$, refreshDrawings, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
resolveRefreshDrawing: () => geometry,
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
expect(canvasFloatDomService.addFloatDom).not.toHaveBeenCalled();
|
||||
|
||||
geometry = { left: 300, top: 500, width: 50, height: 40, angle: 0 };
|
||||
skeletonDirty$.next(true);
|
||||
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledOnce();
|
||||
const refreshCountAfterMount = refreshDrawings.mock.calls.length;
|
||||
skeletonDirty$.next(true);
|
||||
expect(refreshDrawings).toHaveBeenCalledTimes(refreshCountAfterMount);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('ignores hidden fallback geometry while an embed layout is incomplete', () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const { controller, add$, refreshTransform$, drawingRenderService, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
hidden: true,
|
||||
transform: { left: 10, top: 20, width: 50, height: 40, angle: 0 },
|
||||
}]);
|
||||
|
||||
expect(drawingRenderService.renderFloatDom).not.toHaveBeenCalled();
|
||||
expect(canvasFloatDomService.addFloatDom).not.toHaveBeenCalled();
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$, { left: 300, top: 500, width: 50, height: 40, angle: 0 });
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const publishedPosition = position$.getValue();
|
||||
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
hidden: true,
|
||||
transform: { left: 10, top: 20, width: 50, height: 40, angle: 0 },
|
||||
}]);
|
||||
|
||||
expect(position$.getValue()).toEqual(publishedPosition);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('does not mount a removed embed custom block when runtime geometry arrives later', () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 50,
|
||||
height: 40,
|
||||
});
|
||||
const { controller, add$, remove$, refreshTransform$, drawingRenderService, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
remove$.next([{ drawingId: 'dom-1' }]);
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
|
||||
expect(drawingRenderService.renderFloatDom).not.toHaveBeenCalled();
|
||||
expect(canvasFloatDomService.addFloatDom).not.toHaveBeenCalled();
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('updates float dom position from its own host viewport scroll even when current doc focus changes', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, scene, canvasFloatDomService } = createController({ rects: [rect] });
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
scene.viewport.viewportScrollY = 80;
|
||||
scene.viewport.onScrollAfter$.next({} as never);
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startX: 40,
|
||||
startY: -90,
|
||||
width: 100,
|
||||
height: 120,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('updates rendered float dom bounds only from the owning document unit', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
const positionCount = positions.length;
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-2',
|
||||
drawingId: 'dom-1',
|
||||
transform: { left: 0, top: 0, width: 160, height: 240, angle: 0 },
|
||||
}]);
|
||||
expect(positions).toHaveLength(positionCount);
|
||||
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
transform: { left: 40, top: 60, width: 160, height: 240, angle: 0 },
|
||||
customBlockRenderViewport: { contentHeight: 240, height: 240, viewportHeight: 120 },
|
||||
}]);
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startX: 60,
|
||||
startY: 120,
|
||||
width: 320,
|
||||
height: 720,
|
||||
});
|
||||
expect(canvasFloatDomService.updateFloatDom).toHaveBeenCalledWith('dom-1', expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
customBlockRenderViewport: { contentHeight: 240, height: 240, viewportHeight: 120 },
|
||||
}),
|
||||
}));
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('uses custom block viewport height when creating the initial float dom position', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
customBlockRenderViewport: { contentHeight: 240, height: 240, viewportHeight: 120 },
|
||||
},
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startX: 40,
|
||||
startY: 90,
|
||||
width: 100,
|
||||
height: 720,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('uses the full content box for sheet-like embed float dom runtimes', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: {
|
||||
version: 1,
|
||||
embedId: 'embed-1',
|
||||
hostAnchorId: 'anchor-1',
|
||||
childType: UniverInstanceType.UNIVER_SHEET,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledWith(expect.objectContaining({
|
||||
contentBox: { contentInset: 0, wrapperInset: 0 },
|
||||
eventPassThrough: false,
|
||||
}));
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps the legacy content box for other embed float dom runtimes', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: {
|
||||
version: 1,
|
||||
embedId: 'embed-1',
|
||||
hostAnchorId: 'anchor-1',
|
||||
childType: UniverInstanceType.UNIVER_SLIDE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(canvasFloatDomService.addFloatDom).toHaveBeenCalledWith(expect.objectContaining({
|
||||
contentBox: undefined,
|
||||
eventPassThrough: false,
|
||||
}));
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('falls back to custom block viewport height when refreshes omit transform data', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
customBlockRenderViewport: { contentHeight: 240, height: 240, viewportHeight: 120 },
|
||||
}]);
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startX: 40,
|
||||
startY: 90,
|
||||
width: 100,
|
||||
height: 720,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps the measured custom block size while following refreshed doc-flow position', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$);
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
transform: { left: 40, top: 60, width: 160, height: 240, angle: 0 },
|
||||
customBlockRenderViewport: { contentHeight: 240, height: 240, viewportHeight: 120 },
|
||||
}]);
|
||||
rect.transformByState({ left: 45, top: 65, width: 160, height: 40, angle: 0 } as never);
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
transform: { left: 45, top: 65, width: 160, height: 40, angle: 0 },
|
||||
}]);
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startX: 70,
|
||||
startY: 135,
|
||||
width: 320,
|
||||
height: 720,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('keeps embed custom block size while moving slide-like blocks with doc flow refreshes', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 240,
|
||||
width: 720,
|
||||
height: 405,
|
||||
} as never);
|
||||
const { controller, add$, refreshTransform$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
data: { version: 1, embedId: 'embed-1', hostAnchorId: 'anchor-1' },
|
||||
},
|
||||
});
|
||||
|
||||
publishEmbedRuntimeGeometry(refreshTransform$, { left: 30, top: 240, width: 720, height: 405, angle: 0 });
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
rect.transformByState({ left: 30, top: 60, width: 720, height: 405, angle: 0 } as never);
|
||||
refreshTransform$.next([{
|
||||
unitId: 'doc-1',
|
||||
drawingId: 'dom-1',
|
||||
transform: { left: 30, top: 60, width: 720, height: 405, angle: 0 },
|
||||
}]);
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
startY: 120,
|
||||
height: 1215,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('falls back to custom block content height when viewport layout height is omitted', async () => {
|
||||
const rect = new Rect('dom-rect', {
|
||||
left: 30,
|
||||
top: 50,
|
||||
width: 50,
|
||||
height: 40,
|
||||
} as never);
|
||||
const { controller, add$, canvasFloatDomService } = createController({
|
||||
rects: [rect],
|
||||
drawing: {
|
||||
customBlockRenderViewport: { contentHeight: 240, viewportHeight: 120 },
|
||||
},
|
||||
});
|
||||
|
||||
add$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'dom-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
const position$ = canvasFloatDomService.addFloatDom.mock.calls[0][0].position$;
|
||||
const positions: unknown[] = [];
|
||||
const sub = position$.subscribe((position: unknown) => positions.push(position));
|
||||
|
||||
expect(positions.at(-1)).toMatchObject({
|
||||
height: 720,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
it('inserts a float dom using page content width when no explicit width is provided', () => {
|
||||
const { controller, commandService } = createController();
|
||||
|
||||
const drawingId = controller.insertFloatDom(
|
||||
{ componentKey: 'FloatDom', data: { text: 'hello' } } as never,
|
||||
{ height: 36, drawingId: 'fixed-dom-id' }
|
||||
);
|
||||
|
||||
expect(drawingId).toBe('fixed-dom-id');
|
||||
expect(commandService.syncExecuteCommand).toHaveBeenCalledWith(
|
||||
InsertDocDrawingCommand.id,
|
||||
expect.objectContaining({
|
||||
unitId: 'doc-1',
|
||||
drawings: [
|
||||
expect.objectContaining({
|
||||
drawingId: 'fixed-dom-id',
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
layoutType: expect.any(Number),
|
||||
docTransform: expect.objectContaining({
|
||||
size: { width: 190, height: 36 },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { COMPONENT_DOC_DRAWING_PANEL } from '../views/doc-image-panel/component-name';
|
||||
import { DocDrawingPanel } from '../views/doc-image-panel/DocDrawingPanel';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
this.disposeWithMe(this._componentManager.register(COMPONENT_DOC_DRAWING_PANEL, DocDrawingPanel));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* 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, ICommandInfo, IDrawingSearch, JSONXActions, Nullable } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type {
|
||||
IDocDrawing,
|
||||
IUpdateDocDrawingWrappingStyleParams,
|
||||
IUpdateDrawingDocTransformCommandParams,
|
||||
} from '@univerjs/docs-drawing';
|
||||
import type { IDrawingJsonUndo1, IDrawingMapItemData, IDrawingOrderMapParam } from '@univerjs/drawing';
|
||||
import type { IDrawingAnchorInPage } from '../utils/drawing-anchor-position';
|
||||
import {
|
||||
BooleanNumber,
|
||||
Disposable,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
JSON1,
|
||||
JSONX,
|
||||
PositionedObjectLayoutType,
|
||||
RedoCommand,
|
||||
UndoCommand,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import {
|
||||
collectDocDrawings,
|
||||
getDocDrawingRenderOrder,
|
||||
IDocDrawingService,
|
||||
TextWrappingStyle,
|
||||
UpdateDocDrawingWrappingStyleCommand,
|
||||
UpdateDrawingDocTransformCommand,
|
||||
} from '@univerjs/docs-drawing';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { DocRefreshDrawingsService } from '../services/doc-refresh-drawings.service';
|
||||
import { findDrawingAnchorInPage, resolveDrawingAnchorOffsets } from '../utils/drawing-anchor-position';
|
||||
|
||||
interface IAddOrRemoveDrawing {
|
||||
type: 'add' | 'remove';
|
||||
drawingId: string;
|
||||
drawing?: IDocDrawing;
|
||||
}
|
||||
|
||||
// Check whether drawings are added or deleted from the mutation and obtain the drawing ID.
|
||||
// eslint-disable-next-line complexity
|
||||
function getAddOrRemoveDrawings(actions: JSONXActions): Nullable<IAddOrRemoveDrawing[]> {
|
||||
if (JSONX.isNoop(actions) || !Array.isArray(actions)) {
|
||||
return null;
|
||||
}
|
||||
const drawingsOp = actions.find((action) => Array.isArray(action) && action?.[0] === 'drawings');
|
||||
|
||||
if (drawingsOp == null || !Array.isArray(drawingsOp) || drawingsOp.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof drawingsOp[1] === 'string' && typeof drawingsOp[2] !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(drawingsOp[1]) && typeof drawingsOp[1][1] !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawings: IAddOrRemoveDrawing[] = [];
|
||||
|
||||
if (Array.isArray(drawingsOp?.[1])) {
|
||||
for (const op of drawingsOp) {
|
||||
if (Array.isArray(op)) {
|
||||
drawings.push({
|
||||
type: (op?.[1] as any)?.i ? 'add' : 'remove',
|
||||
drawingId: op?.[0] as string,
|
||||
drawing: (op?.[1] as any)?.i,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drawings.push({
|
||||
type: (drawingsOp[2] as any)?.i ? 'add' : 'remove',
|
||||
drawingId: drawingsOp[1] as string,
|
||||
drawing: (drawingsOp[2] as any)?.i,
|
||||
});
|
||||
}
|
||||
|
||||
return drawings;
|
||||
}
|
||||
|
||||
// ReOrderedActions data like bellow:
|
||||
// [
|
||||
// "drawingsOrder",
|
||||
// [ 4,
|
||||
// {
|
||||
// "d": 0
|
||||
// }
|
||||
// ],
|
||||
// [ 5,
|
||||
// {
|
||||
// "p": 0
|
||||
// }
|
||||
// ]
|
||||
// ]
|
||||
function getReOrderedDrawings(actions: JSONXActions): number[] {
|
||||
if (!Array.isArray(actions) || actions.length < 3 || actions[0] !== 'drawingsOrder') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const drawingIndexes: number[] = [];
|
||||
|
||||
for (let i = 1; i < actions.length; i++) {
|
||||
const action = actions[i];
|
||||
if (Array.isArray(action) && typeof action[0] === 'number' && typeof action[1] === 'object') {
|
||||
drawingIndexes.push(action[0]);
|
||||
} else {
|
||||
drawingIndexes.length = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return drawingIndexes;
|
||||
}
|
||||
|
||||
function collectUpdatedDrawingIds(actions: JSONXActions, drawingIds = new Set<string>()): Set<string> {
|
||||
if (JSONX.isNoop(actions) || !Array.isArray(actions)) {
|
||||
return drawingIds;
|
||||
}
|
||||
|
||||
if (actions[0] === 'drawings') {
|
||||
const drawingKeyOrOps = actions[1];
|
||||
if (typeof drawingKeyOrOps === 'string') {
|
||||
drawingIds.add(drawingKeyOrOps);
|
||||
return drawingIds;
|
||||
}
|
||||
|
||||
actions.slice(1).forEach((action) => {
|
||||
if (Array.isArray(action) && typeof action[0] === 'string') {
|
||||
drawingIds.add(action[0]);
|
||||
}
|
||||
});
|
||||
return drawingIds;
|
||||
}
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (Array.isArray(action)) {
|
||||
collectUpdatedDrawingIds(action as JSONXActions, drawingIds);
|
||||
}
|
||||
});
|
||||
|
||||
return drawingIds;
|
||||
}
|
||||
|
||||
export class DocDrawingAddRemoveController extends Disposable {
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IDocDrawingService private readonly _docDrawingService: IDocDrawingService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@Inject(DocRefreshDrawingsService) private readonly _docRefreshDrawingsService: DocRefreshDrawingsService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._commandExecutedListener();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _commandExecutedListener() {
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id !== RichTextEditingMutation.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = command.params as IRichTextEditingMutationParams;
|
||||
const { unitId, actions, isSync, syncer } = params;
|
||||
|
||||
const addOrRemoveDrawings = getAddOrRemoveDrawings(actions);
|
||||
if (addOrRemoveDrawings != null) {
|
||||
for (const { type, drawingId, drawing } of addOrRemoveDrawings) {
|
||||
if (isSync && drawing?.unitId === syncer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === 'add') {
|
||||
this._addDrawings(unitId, [drawing!]);
|
||||
} else {
|
||||
this._removeDrawings(unitId, [drawingId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.beforeCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id !== UpdateDocDrawingWrappingStyleCommand.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._preserveWrappingStylePosition(command.params as IUpdateDocDrawingWrappingStyleParams);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id !== RichTextEditingMutation.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = command.params as IRichTextEditingMutationParams;
|
||||
const { unitId, actions } = params;
|
||||
let changedNotes = false;
|
||||
if (!JSONX.isNoop(actions)) {
|
||||
const cursor = JSON1.type.readCursor(actions);
|
||||
cursor.traverse(null, () => {
|
||||
changedNotes ||= cursor.getPath()[0] === 'notes';
|
||||
});
|
||||
}
|
||||
if (changedNotes) {
|
||||
this._syncNoteDrawings(unitId);
|
||||
}
|
||||
const reOrderedDrawings = getReOrderedDrawings(actions);
|
||||
|
||||
if (reOrderedDrawings.length > 0) {
|
||||
this._updateDrawingsOrder(unitId);
|
||||
}
|
||||
|
||||
const updatedDrawingIds = [...collectUpdatedDrawingIds(actions)];
|
||||
if (updatedDrawingIds.length > 0) {
|
||||
this._syncDrawingDataFromSnapshot(unitId, updatedDrawingIds);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (
|
||||
command.id !== UpdateDrawingDocTransformCommand.id &&
|
||||
command.id !== UpdateDocDrawingWrappingStyleCommand.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId } = command.params as IUpdateDrawingDocTransformCommandParams | IUpdateDocDrawingWrappingStyleParams;
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
if (renderObject == null || scene == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform mutations are already refreshed incrementally by
|
||||
// DocDrawingTransformUpdateController from the nested rich-text
|
||||
// mutation. Wrapping changes can move anchors and still need a
|
||||
// conservative full refresh.
|
||||
if (command.id === UpdateDocDrawingWrappingStyleCommand.id) {
|
||||
this._docRefreshDrawingsService.refreshDrawings(renderObject.with(DocSkeletonManagerService).getSkeleton());
|
||||
}
|
||||
scene.getTransformerByCreate().refreshControls();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
if (command.id !== UndoCommand.id && command.id !== RedoCommand.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = this._univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC)?.getUnitId();
|
||||
const focusedDrawings = this._drawingManagerService.getFocusDrawings();
|
||||
|
||||
if (unitId == null || focusedDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject?.scene;
|
||||
if (renderObject == null || scene == null) {
|
||||
return false;
|
||||
}
|
||||
this._docRefreshDrawingsService.refreshDrawings(renderObject.with(DocSkeletonManagerService).getSkeleton());
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
transformer.refreshControls();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _preserveWrappingStylePosition(params: IUpdateDocDrawingWrappingStyleParams): void {
|
||||
if (params.wrappingStyle === TextWrappingStyle.INLINE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId } = params;
|
||||
const documentDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
|
||||
unitId,
|
||||
UniverInstanceType.UNIVER_DOC
|
||||
);
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const skeletonManager = renderObject?.with(DocSkeletonManagerService);
|
||||
const skeletonData = skeletonManager?.getSkeleton().getSkeletonData();
|
||||
const viewModel = skeletonManager?.getViewModel();
|
||||
if (!documentDataModel || !skeletonData || !viewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editArea = viewModel.getEditArea();
|
||||
const { pages, skeHeaders, skeFooters } = skeletonData;
|
||||
const oldDrawings = documentDataModel.getDrawings() ?? {};
|
||||
|
||||
params.drawings = params.drawings.map((drawing) => {
|
||||
const oldDrawing = oldDrawings[drawing.drawingId] as IDocDrawing | undefined;
|
||||
if (!oldDrawing) {
|
||||
return drawing;
|
||||
}
|
||||
|
||||
let drawingAnchor: IDrawingAnchorInPage | null = null;
|
||||
for (const page of pages) {
|
||||
const { headerId, footerId, marginTop, marginLeft, marginBottom, pageWidth, pageHeight } = page;
|
||||
if (editArea === DocumentEditArea.HEADER) {
|
||||
const header = skeHeaders.get(headerId)?.get(pageWidth);
|
||||
if (header) {
|
||||
drawingAnchor = findDrawingAnchorInPage(header, drawing.drawingId, header.marginTop, marginLeft);
|
||||
}
|
||||
} else if (editArea === DocumentEditArea.FOOTER) {
|
||||
const footer = skeFooters.get(footerId)?.get(pageWidth);
|
||||
if (footer) {
|
||||
drawingAnchor = findDrawingAnchorInPage(
|
||||
footer,
|
||||
drawing.drawingId,
|
||||
pageHeight - marginBottom + footer.marginTop,
|
||||
marginLeft
|
||||
);
|
||||
}
|
||||
} else {
|
||||
drawingAnchor = findDrawingAnchorInPage(page, drawing.drawingId, marginTop, marginLeft);
|
||||
}
|
||||
|
||||
if (drawingAnchor) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawingAnchor) {
|
||||
return drawing;
|
||||
}
|
||||
|
||||
const oldPositionH = oldDrawing.docTransform.positionH;
|
||||
const oldPositionV = oldDrawing.docTransform.positionV;
|
||||
const { horizontal: posOffsetH, vertical: posOffsetV } = resolveDrawingAnchorOffsets(
|
||||
drawingAnchor,
|
||||
oldPositionH,
|
||||
oldPositionV
|
||||
);
|
||||
|
||||
return {
|
||||
...oldDrawing,
|
||||
...drawing,
|
||||
docTransform: {
|
||||
...oldDrawing.docTransform,
|
||||
...drawing.docTransform,
|
||||
positionH: { relativeFrom: oldPositionH.relativeFrom, posOffset: posOffsetH },
|
||||
positionV: { relativeFrom: oldPositionV.relativeFrom, posOffset: posOffsetV },
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _addDrawings(unitId: string, drawings: IDocDrawing[]) {
|
||||
const drawingManagerService = this._drawingManagerService;
|
||||
const docDrawingService = this._docDrawingService;
|
||||
|
||||
const jsonOp = this._docDrawingService.getBatchAddOp(drawings) as IDrawingJsonUndo1;
|
||||
|
||||
const { subUnitId, redo: op, objects } = jsonOp;
|
||||
|
||||
drawingManagerService.applyJson1(unitId, subUnitId, op);
|
||||
docDrawingService.applyJson1(unitId, subUnitId, op);
|
||||
|
||||
drawingManagerService.addNotification(objects as IDrawingSearch[]);
|
||||
docDrawingService.addNotification(objects as IDrawingSearch[]);
|
||||
}
|
||||
|
||||
private _removeDrawings(unitId: string, drawingIds: string[]) {
|
||||
const drawingManagerService = this._drawingManagerService;
|
||||
const docDrawingService = this._docDrawingService;
|
||||
|
||||
const jsonOp = this._docDrawingService.getBatchRemoveOp(drawingIds.map((drawingId) => {
|
||||
return {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId,
|
||||
};
|
||||
}) as IDrawingSearch[]) as IDrawingJsonUndo1;
|
||||
|
||||
const { subUnitId, redo: op, objects } = jsonOp;
|
||||
|
||||
drawingManagerService.applyJson1(unitId, subUnitId, op);
|
||||
docDrawingService.applyJson1(unitId, subUnitId, op);
|
||||
|
||||
drawingManagerService.removeNotification(objects as IDrawingSearch[]);
|
||||
docDrawingService.removeNotification(objects as IDrawingSearch[]);
|
||||
}
|
||||
|
||||
private _updateDrawingsOrder(unitId: string) {
|
||||
const documentDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
if (documentDataModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings, drawingsOrder } = collectDocDrawings(documentDataModel.getSnapshot());
|
||||
|
||||
if (drawingsOrder == null) {
|
||||
return;
|
||||
}
|
||||
const renderOrder = getDocDrawingRenderOrder(drawingsOrder, drawings);
|
||||
|
||||
const drawingManagerService = this._drawingManagerService;
|
||||
const docDrawingService = this._docDrawingService;
|
||||
|
||||
drawingManagerService.setDrawingOrder(unitId, unitId, renderOrder);
|
||||
docDrawingService.setDrawingOrder(unitId, unitId, drawingsOrder);
|
||||
|
||||
// FIXME: @Jocs, Only need to update the affected drawings.
|
||||
const objects: IDrawingOrderMapParam = {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingIds: renderOrder,
|
||||
};
|
||||
|
||||
drawingManagerService.orderNotification(objects);
|
||||
docDrawingService.orderNotification({
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingIds: drawingsOrder,
|
||||
});
|
||||
}
|
||||
|
||||
private _syncNoteDrawings(unitId: string): void {
|
||||
const model = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const { drawings } = collectDocDrawings(model.getSnapshot());
|
||||
const previous = this._docDrawingService.getDrawingData(unitId, unitId) ?? {};
|
||||
const added = Object.keys(drawings).filter((id) => !previous[id]);
|
||||
const removed = Object.keys(previous).filter((id) => !drawings[id]);
|
||||
const changed = Object.keys(drawings).filter((id) => drawings[id] !== previous[id]);
|
||||
if (added.length > 0) {
|
||||
this._addDrawings(unitId, added.map((id) => ({ ...drawings[id], unitId, subUnitId: unitId })));
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
this._removeDrawings(unitId, removed);
|
||||
}
|
||||
if (changed.length > 0) {
|
||||
this._syncDrawingDataFromSnapshot(unitId, changed);
|
||||
}
|
||||
}
|
||||
|
||||
private _syncDrawingDataFromSnapshot(unitId: string, drawingIds: string[]) {
|
||||
const documentDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
if (documentDataModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawings, drawingsOrder } = collectDocDrawings(documentDataModel.getSnapshot());
|
||||
const drawingData = drawings as IDrawingMapItemData<IDocDrawing>;
|
||||
const previousDrawings = this._docDrawingService.getDrawingData(unitId, unitId);
|
||||
const orderChanged = drawingsOrder !== this._docDrawingService.getDrawingOrder(unitId, unitId) ||
|
||||
drawingIds.some((drawingId) => {
|
||||
const previous = previousDrawings[drawingId];
|
||||
const current = drawingData[drawingId];
|
||||
const wasBehind = previous?.layoutType === PositionedObjectLayoutType.WRAP_NONE &&
|
||||
previous.behindDoc === BooleanNumber.TRUE;
|
||||
const isBehind = current?.layoutType === PositionedObjectLayoutType.WRAP_NONE &&
|
||||
current.behindDoc === BooleanNumber.TRUE;
|
||||
return wasBehind !== isBehind;
|
||||
});
|
||||
|
||||
const renderedDrawings = { ...this._drawingManagerService.getDrawingData(unitId, unitId) };
|
||||
for (const drawingId of drawingIds) {
|
||||
const current = drawingData[drawingId];
|
||||
if (current) {
|
||||
// Layout mutates render transforms. Never share the persisted drawing object or
|
||||
// replace unrelated drawings whose published positions are still authoritative.
|
||||
renderedDrawings[drawingId] = { ...current };
|
||||
} else {
|
||||
delete renderedDrawings[drawingId];
|
||||
}
|
||||
}
|
||||
|
||||
this._docDrawingService.setDrawingData(unitId, unitId, drawingData);
|
||||
this._drawingManagerService.setDrawingData(unitId, unitId, renderedDrawings);
|
||||
if (orderChanged) {
|
||||
this._docDrawingService.setDrawingOrder(unitId, unitId, drawingsOrder);
|
||||
this._drawingManagerService.setDrawingOrder(unitId, unitId, getDocDrawingRenderOrder(drawingsOrder, drawings));
|
||||
}
|
||||
|
||||
const objects = drawingIds
|
||||
.filter((drawingId) => drawingData[drawingId] != null)
|
||||
.map((drawingId) => ({ unitId, subUnitId: unitId, drawingId }));
|
||||
|
||||
if (objects.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._docDrawingService.updateNotification(objects);
|
||||
this._drawingManagerService.updateNotification(objects);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDocFloatDom } from '@univerjs/docs-drawing';
|
||||
import { Disposable, DOC_DRAWING_PRINTING_COMPONENT_KEY, DrawingTypeEnum, Inject, Injector } from '@univerjs/core';
|
||||
import { DocPrintInterceptorService } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DrawingRenderService } from '@univerjs/drawing-ui';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { mountDocPrintingFloatDom } from '../views/DocPrintingFloatDom';
|
||||
|
||||
export class DocDrawingPrintingController extends Disposable {
|
||||
constructor(
|
||||
@Inject(DocPrintInterceptorService) private readonly _docPrintInterceptorService: DocPrintInterceptorService,
|
||||
@Inject(DrawingRenderService) private readonly _drawingRenderService: DrawingRenderService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(ComponentManager) private readonly _componetManager: ComponentManager,
|
||||
@Inject(Injector) private readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initPrinting();
|
||||
this._initPrintingDom();
|
||||
}
|
||||
|
||||
private _initPrinting() {
|
||||
this.disposeWithMe(
|
||||
this._docPrintInterceptorService.interceptor.intercept(
|
||||
this._docPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_COMPONENT_COLLECT,
|
||||
{
|
||||
handler: (_param, pos, next) => {
|
||||
const { unitId, scene, skeleton, pageIndex } = pos;
|
||||
const unitData = this._drawingManagerService.getDrawingDataForUnit(unitId);
|
||||
const subUnitData = unitData?.[unitId];
|
||||
const visibleDrawingIds = this._docPrintInterceptorService.getPageDrawingIds(skeleton, pageIndex);
|
||||
if (subUnitData) {
|
||||
subUnitData.order.forEach((id) => {
|
||||
if (visibleDrawingIds && !visibleDrawingIds.has(id)) {
|
||||
return;
|
||||
}
|
||||
const drawing = subUnitData.data[id];
|
||||
if (drawing.drawingType !== DrawingTypeEnum.DRAWING_CHART && drawing.drawingType !== DrawingTypeEnum.DRAWING_DOM) {
|
||||
this._drawingRenderService.renderDrawing(drawing, scene);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _initPrintingDom() {
|
||||
this.disposeWithMe(
|
||||
this._docPrintInterceptorService.interceptor.intercept(
|
||||
this._docPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_DOM_COLLECT,
|
||||
{
|
||||
handler: (disposableCollection, pos, next) => {
|
||||
const { unitId, skeleton, pageIndex } = pos;
|
||||
const unitData = this._drawingManagerService.getDrawingDataForUnit(unitId);
|
||||
const subUnitData = unitData?.[unitId];
|
||||
const visibleDrawingIds = this._docPrintInterceptorService.getPageDrawingIds(skeleton, pageIndex);
|
||||
if (subUnitData) {
|
||||
const floatDomInfos = subUnitData.order.filter((id) => !visibleDrawingIds || visibleDrawingIds.has(id)).map((id) => {
|
||||
const drawing = subUnitData.data[id] as IDocFloatDom;
|
||||
if (drawing.drawingType === DrawingTypeEnum.DRAWING_CHART) {
|
||||
return {
|
||||
...drawing,
|
||||
componentKey: this._componetManager.get(DOC_DRAWING_PRINTING_COMPONENT_KEY) as any,
|
||||
};
|
||||
}
|
||||
|
||||
if (drawing.drawingType === DrawingTypeEnum.DRAWING_DOM) {
|
||||
const printingComponentKey = this._docPrintInterceptorService.getPrintComponent(drawing.componentKey);
|
||||
|
||||
return {
|
||||
...drawing,
|
||||
componentKey: this._componetManager.get(printingComponentKey || drawing.componentKey) as any,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Boolean) as IDocFloatDom[];
|
||||
const unmountPrintingFloatDom = mountDocPrintingFloatDom(
|
||||
{
|
||||
unitId,
|
||||
floatDomInfos,
|
||||
scene: pos.scene,
|
||||
skeleton: pos.skeleton,
|
||||
offset: pos.offset,
|
||||
bound: pos.bound,
|
||||
},
|
||||
pos.root,
|
||||
this._injector
|
||||
);
|
||||
|
||||
disposableCollection?.add(() => {
|
||||
unmountPrintingFloatDom();
|
||||
});
|
||||
|
||||
return next(disposableCollection);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,717 @@
|
||||
/**
|
||||
* 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, IDisposable, IDrawingSearch, ITransformState, Nullable } from '@univerjs/core';
|
||||
import type { IDocFloatDom, IInsertDocDrawingCommandParams } from '@univerjs/docs-drawing';
|
||||
import type { ISetDocZoomRatioOperationParams } from '@univerjs/docs-ui';
|
||||
import type { IDocFloatDomDataBase } from '@univerjs/drawing';
|
||||
import type { IBoundRectNoAngle, IDocsCustomBlockRenderViewport, IRender, Rect, Scene } from '@univerjs/engine-render';
|
||||
import type { IFloatDomLayout } from '@univerjs/ui';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
DrawingTypeEnum,
|
||||
fromEventSubject,
|
||||
generateRandomId,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { docDrawingPositionToTransform, DocSkeletonManagerService, isSheetLikeDocsCustomBlockChildType } from '@univerjs/docs';
|
||||
import { InsertDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { SetDocZoomRatioOperation, VIEWPORT_KEY } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DrawingRenderService } from '@univerjs/drawing-ui';
|
||||
import { CURSOR_TYPE, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { CanvasFloatDomService } from '@univerjs/ui';
|
||||
import { BehaviorSubject, map, of, switchMap } from 'rxjs';
|
||||
import { DocRefreshDrawingsService } from '../services/doc-refresh-drawings.service';
|
||||
|
||||
export function calcDocFloatDomPositionByRect(
|
||||
rect: IBoundRectNoAngle,
|
||||
scene: Scene,
|
||||
opacity = 1,
|
||||
angle = 0
|
||||
): IFloatDomLayout {
|
||||
const { top, left, bottom, right } = rect;
|
||||
const width = right - left;
|
||||
const height = bottom - top;
|
||||
|
||||
const viewMain = scene.getViewport(VIEWPORT_KEY.VIEW_MAIN)!;
|
||||
const { viewportScrollX, viewportScrollY } = viewMain;
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
return {
|
||||
startX: (left - viewportScrollX) * scaleX,
|
||||
startY: (top - viewportScrollY) * scaleY,
|
||||
endX: (left + width - viewportScrollX) * scaleX,
|
||||
endY: (top + height - viewportScrollY) * scaleY,
|
||||
width: width * scaleX,
|
||||
height: height * scaleY,
|
||||
rotate: angle,
|
||||
absolute: {
|
||||
left: false,
|
||||
top: false,
|
||||
},
|
||||
opacity: opacity ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
function calcDocFloatDomPosition(
|
||||
object: Rect,
|
||||
renderUnit: IRender
|
||||
): IFloatDomLayout {
|
||||
const { top, left, width, height, angle, opacity } = object;
|
||||
return calcDocFloatDomPositionByRect({ top, left, bottom: top + height, right: left + width }, renderUnit.scene, opacity, angle);
|
||||
}
|
||||
|
||||
interface ICanvasFloatDomInfo {
|
||||
position$: BehaviorSubject<IFloatDomLayout>;
|
||||
dispose: IDisposable;
|
||||
preserveRuntimeGeometry?: boolean;
|
||||
rect: Rect;
|
||||
runtimeTransform?: Partial<ITransformState>;
|
||||
runtimeViewport?: IDocFloatDomRuntimeViewport;
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
interface IDocFloatDomParams extends IDocFloatDomDataBase {
|
||||
}
|
||||
|
||||
type IDocFloatDomRuntimeViewport = Partial<Pick<IDocsCustomBlockRenderViewport, 'bleedLeft' | 'bleedWidth' | 'contentHeight' | 'contentWidth' | 'height' | 'pageContentWidth' | 'viewScale' | 'viewportHeight'>>;
|
||||
|
||||
interface IDocFloatDomRuntimeGeometry {
|
||||
customBlockRenderViewport?: IDocFloatDomRuntimeViewport;
|
||||
hidden?: boolean;
|
||||
transform?: Nullable<ITransformState>;
|
||||
transforms?: Nullable<ITransformState[]>;
|
||||
}
|
||||
|
||||
export function mergeDocFloatDomRuntimeProps(existingProps: Record<string, unknown> | undefined, param: IDocFloatDomRuntimeGeometry): Record<string, unknown> | undefined {
|
||||
const customBlockRenderViewport = pickValidCustomBlockRenderViewport(param.customBlockRenderViewport);
|
||||
if (!customBlockRenderViewport) {
|
||||
return existingProps;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingProps,
|
||||
customBlockRenderViewport,
|
||||
};
|
||||
}
|
||||
|
||||
function pickValidCustomBlockRenderViewport(viewport: IDocFloatDomRuntimeViewport | undefined): IDocFloatDomRuntimeViewport | undefined {
|
||||
const result: IDocFloatDomRuntimeViewport = {};
|
||||
|
||||
if (isNonNegativeNumber(viewport?.bleedLeft)) {
|
||||
result.bleedLeft = viewport!.bleedLeft;
|
||||
}
|
||||
if (isPositiveNumber(viewport?.bleedWidth)) {
|
||||
result.bleedWidth = viewport!.bleedWidth;
|
||||
}
|
||||
if (isPositiveNumber(viewport?.contentHeight)) {
|
||||
result.contentHeight = viewport!.contentHeight;
|
||||
}
|
||||
if (isPositiveNumber(viewport?.contentWidth)) {
|
||||
result.contentWidth = viewport!.contentWidth;
|
||||
}
|
||||
if (isPositiveNumber(viewport?.height)) {
|
||||
result.height = viewport!.height;
|
||||
}
|
||||
if (isPositiveNumber(viewport?.pageContentWidth)) {
|
||||
result.pageContentWidth = viewport!.pageContentWidth;
|
||||
}
|
||||
const viewScale = viewport?.viewScale;
|
||||
if (isPositiveNumber(viewScale)) {
|
||||
result.viewScale = viewScale;
|
||||
}
|
||||
if (isPositiveNumber(viewport?.viewportHeight)) {
|
||||
result.viewportHeight = viewport!.viewportHeight;
|
||||
}
|
||||
|
||||
return Object.keys(result).length ? result : undefined;
|
||||
}
|
||||
|
||||
function isPositiveNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
function isNonNegativeNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
||||
}
|
||||
|
||||
export class DocFloatDomController extends Disposable {
|
||||
private _domLayerInfoMap = new Map<string, ICanvasFloatDomInfo>();
|
||||
private _pendingRuntimeGeometry = new Map<string, IDocFloatDomRuntimeGeometry>();
|
||||
private _pendingRuntimeGeometryInsert = new Map<string, IDrawingSearch>();
|
||||
private _pendingRuntimeGeometryRefresh = new Map<string, IDisposable>();
|
||||
|
||||
constructor(
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(DrawingRenderService) private readonly _drawingRenderService: DrawingRenderService,
|
||||
@Inject(CanvasFloatDomService) private readonly _canvasFloatDomService: CanvasFloatDomService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(DocRefreshDrawingsService) private readonly _docRefreshDrawingsService: DocRefreshDrawingsService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._pendingRuntimeGeometry.clear();
|
||||
this._pendingRuntimeGeometryInsert.clear();
|
||||
for (const disposable of this._pendingRuntimeGeometryRefresh.values()) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this._pendingRuntimeGeometryRefresh.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._renderLifecycleListener();
|
||||
this._drawingAddRemoveListener();
|
||||
this._drawingRuntimePropsListener();
|
||||
this._initScrollAndZoomEvent();
|
||||
}
|
||||
|
||||
private _renderLifecycleListener(): void {
|
||||
this.disposeWithMe(this._renderManagerService.disposed$.subscribe((unitId) => {
|
||||
this._disposePendingRuntimeGeometryRefresh(unitId);
|
||||
}));
|
||||
this.disposeWithMe(this._renderManagerService.created$.subscribe((render) => {
|
||||
if (this._hasPendingRuntimeGeometryForUnit(render.unitId)) {
|
||||
this._refreshDrawingsFromCurrentLayout(render.unitId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _getSceneAndTransformerByDrawingSearch(unitId: Nullable<string>) {
|
||||
if (unitId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderObject = this._renderManagerService.getRenderUnitById(unitId);
|
||||
|
||||
if (renderObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scene = renderObject.scene;
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
return { scene, transformer, renderUnit: renderObject, canvas: renderObject.engine.getCanvasElement() };
|
||||
}
|
||||
|
||||
private _drawingAddRemoveListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.add$.subscribe((params) => {
|
||||
const ready: IDrawingSearch[] = [];
|
||||
const refreshUnitIds = new Set<string>();
|
||||
for (const param of params) {
|
||||
const drawing = this._drawingManagerService.getDrawingByParam(param);
|
||||
if (
|
||||
isEmbedFloatDomRuntimeParam(drawing) &&
|
||||
this._univerInstanceService.getUnit(param.unitId, UniverInstanceType.UNIVER_DOC) != null
|
||||
) {
|
||||
this._pendingRuntimeGeometryInsert.set(param.drawingId, param);
|
||||
refreshUnitIds.add(param.unitId);
|
||||
if (this._pendingRuntimeGeometry.has(param.drawingId)) {
|
||||
ready.push(param);
|
||||
}
|
||||
} else {
|
||||
ready.push(param);
|
||||
}
|
||||
}
|
||||
this._insertRects(ready);
|
||||
for (const unitId of refreshUnitIds) {
|
||||
this._refreshDrawingsFromCurrentLayout(unitId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.remove$.subscribe((params) => {
|
||||
params.forEach((param) => {
|
||||
this._removeDom(param.drawingId);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _refreshDrawingsFromCurrentLayout(unitId: string): void {
|
||||
const render = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (
|
||||
render == null ||
|
||||
render.type !== UniverInstanceType.UNIVER_DOC ||
|
||||
render.isDisposed()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skeleton = render.with(DocSkeletonManagerService).getSkeleton();
|
||||
if (skeleton != null) {
|
||||
if (!this._pendingRuntimeGeometryRefresh.has(unitId)) {
|
||||
const subscription = skeleton.dirty$.subscribe(() => {
|
||||
if (!this._hasPendingRuntimeGeometryForUnit(unitId)) {
|
||||
this._disposePendingRuntimeGeometryRefresh(unitId);
|
||||
return;
|
||||
}
|
||||
this._docRefreshDrawingsService.refreshDrawings(skeleton);
|
||||
});
|
||||
this._pendingRuntimeGeometryRefresh.set(
|
||||
unitId,
|
||||
toDisposable(() => subscription.unsubscribe())
|
||||
);
|
||||
}
|
||||
this._docRefreshDrawingsService.refreshDrawings(skeleton);
|
||||
}
|
||||
}
|
||||
|
||||
private _hasPendingRuntimeGeometryForUnit(unitId: string): boolean {
|
||||
for (const pending of this._pendingRuntimeGeometryInsert.values()) {
|
||||
if (pending.unitId === unitId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _disposePendingRuntimeGeometryRefresh(unitId: string): void {
|
||||
this._pendingRuntimeGeometryRefresh.get(unitId)?.dispose();
|
||||
this._pendingRuntimeGeometryRefresh.delete(unitId);
|
||||
}
|
||||
|
||||
private _insertRects(params: IDrawingSearch[]) {
|
||||
for (const param of params) {
|
||||
const { unitId } = param;
|
||||
const documentDataModel = this._univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (!documentDataModel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderObject = this._getSceneAndTransformerByDrawingSearch(unitId);
|
||||
|
||||
if (renderObject == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rectParam = this._drawingManagerService.getDrawingByParam(param) as IDocFloatDom;
|
||||
if (rectParam == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const preserveRuntimeGeometry = isEmbedFloatDomRuntimeParam(rectParam);
|
||||
const publishedRuntimeParam = this._pendingRuntimeGeometry.get(param.drawingId);
|
||||
if (
|
||||
preserveRuntimeGeometry &&
|
||||
(publishedRuntimeParam == null || this._pendingRuntimeGeometryInsert.get(param.drawingId) !== param)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rects = this._drawingRenderService.renderFloatDom(rectParam, renderObject.scene);
|
||||
if (rects == null || rects.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rect of rects) {
|
||||
const runtimeParam: IDocFloatDomRuntimeGeometry = publishedRuntimeParam ?? rectParam;
|
||||
const runtimeViewport = pickValidCustomBlockRenderViewport(runtimeParam.customBlockRenderViewport);
|
||||
syncRectWithRuntimeParam(rect, runtimeParam, runtimeViewport, undefined, preserveRuntimeGeometry);
|
||||
const runtimeTransform = runtimeViewport || preserveRuntimeGeometry ? createTransformFromRect(rect) : undefined;
|
||||
this._addHoverForRect(rect);
|
||||
const disposableCollection = new DisposableCollection();
|
||||
const initPosition = calcDocFloatDomPosition(rect, renderObject.renderUnit);
|
||||
const position$ = new BehaviorSubject<IFloatDomLayout>(initPosition);
|
||||
const canvas = renderObject.canvas;
|
||||
const data = rectParam.data;
|
||||
|
||||
const info: ICanvasFloatDomInfo = {
|
||||
dispose: disposableCollection,
|
||||
preserveRuntimeGeometry,
|
||||
rect,
|
||||
runtimeTransform,
|
||||
runtimeViewport,
|
||||
position$,
|
||||
unitId,
|
||||
};
|
||||
|
||||
this._canvasFloatDomService.addFloatDom({
|
||||
position$,
|
||||
id: rectParam.drawingId,
|
||||
componentKey: rectParam.componentKey,
|
||||
contentBox: isSheetLikeEmbedFloatDomRuntimeParam(rectParam)
|
||||
? { contentInset: 0, wrapperInset: 0 }
|
||||
: undefined,
|
||||
eventPassThrough: preserveRuntimeGeometry ? false : undefined,
|
||||
preserveOnFocusChange: preserveRuntimeGeometry,
|
||||
onPointerDown: (evt) => {
|
||||
canvas.dispatchEvent(new PointerEvent(evt.type, evt));
|
||||
},
|
||||
onPointerMove: (evt: PointerEvent | MouseEvent) => {
|
||||
canvas.dispatchEvent(new PointerEvent(evt.type, evt));
|
||||
},
|
||||
onPointerUp: (evt: PointerEvent | MouseEvent) => {
|
||||
canvas.dispatchEvent(new PointerEvent(evt.type, evt));
|
||||
},
|
||||
onWheel: (evt: WheelEvent) => {
|
||||
canvas.dispatchEvent(new WheelEvent(evt.type, evt));
|
||||
},
|
||||
data,
|
||||
props: mergeDocFloatDomRuntimeProps(undefined, rectParam),
|
||||
unitId,
|
||||
});
|
||||
|
||||
const listener = rect.onTransformChange$.subscribeEvent(() => {
|
||||
const newPosition = calcDocFloatDomPosition(rect, renderObject.renderUnit);
|
||||
position$.next(
|
||||
newPosition
|
||||
);
|
||||
});
|
||||
const scrollListener = subscribeViewportScrollAfter(
|
||||
renderObject.scene.getViewport(VIEWPORT_KEY.VIEW_MAIN)?.onScrollAfter$,
|
||||
() => position$.next(calcDocFloatDomPosition(rect, renderObject.renderUnit))
|
||||
);
|
||||
|
||||
disposableCollection.add(() => {
|
||||
this._canvasFloatDomService.removeFloatDom(rectParam.drawingId);
|
||||
});
|
||||
listener && disposableCollection.add(listener);
|
||||
scrollListener && disposableCollection.add(scrollListener);
|
||||
this._domLayerInfoMap.set(rectParam.drawingId, info);
|
||||
}
|
||||
this._pendingRuntimeGeometry.delete(param.drawingId);
|
||||
this._pendingRuntimeGeometryInsert.delete(param.drawingId);
|
||||
if (!this._hasPendingRuntimeGeometryForUnit(unitId)) {
|
||||
this._disposePendingRuntimeGeometryRefresh(unitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _drawingRuntimePropsListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.refreshTransform$.subscribe((params) => {
|
||||
params.forEach((param) => {
|
||||
const floatDomInfo = this._domLayerInfoMap.get(param.drawingId);
|
||||
if (!floatDomInfo) {
|
||||
const pendingInsert = this._pendingRuntimeGeometryInsert.get(param.drawingId);
|
||||
const drawing = this._drawingManagerService.getDrawingByParam(param);
|
||||
if (pendingInsert != null || isEmbedFloatDomRuntimeParam(drawing)) {
|
||||
if (param.hidden === true) {
|
||||
return;
|
||||
}
|
||||
this._pendingRuntimeGeometry.set(param.drawingId, param);
|
||||
if (pendingInsert != null) {
|
||||
this._insertRects([pendingInsert]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (floatDomInfo.unitId !== param.unitId) {
|
||||
return;
|
||||
}
|
||||
if (floatDomInfo.preserveRuntimeGeometry && param.hidden === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeParam: IDocFloatDomRuntimeGeometry = param;
|
||||
const runtimeViewport = pickValidCustomBlockRenderViewport(runtimeParam.customBlockRenderViewport);
|
||||
if (runtimeViewport) {
|
||||
floatDomInfo.runtimeViewport = runtimeViewport;
|
||||
}
|
||||
|
||||
const synced = syncRectWithRuntimeParam(
|
||||
floatDomInfo.rect,
|
||||
runtimeParam,
|
||||
floatDomInfo.runtimeViewport,
|
||||
floatDomInfo.runtimeTransform,
|
||||
floatDomInfo.preserveRuntimeGeometry
|
||||
);
|
||||
if (synced) {
|
||||
if (runtimeViewport || floatDomInfo.preserveRuntimeGeometry) {
|
||||
floatDomInfo.runtimeTransform = createTransformFromRect(floatDomInfo.rect);
|
||||
}
|
||||
const renderObject = this._getSceneAndTransformerByDrawingSearch(floatDomInfo.unitId);
|
||||
if (renderObject) {
|
||||
floatDomInfo.position$.next(calcDocFloatDomPosition(floatDomInfo.rect, renderObject.renderUnit));
|
||||
}
|
||||
}
|
||||
|
||||
const currentProps = this._canvasFloatDomService.domLayers.find(([id]) => id === param.drawingId)?.[1].props;
|
||||
this._canvasFloatDomService.updateFloatDom(param.drawingId, {
|
||||
props: mergeDocFloatDomRuntimeProps(currentProps, param),
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _addHoverForRect(o: Rect) {
|
||||
this.disposeWithMe(
|
||||
toDisposable(
|
||||
o.onPointerEnter$.subscribeEvent(() => {
|
||||
o.cursor = CURSOR_TYPE.GRAB;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
toDisposable(
|
||||
o.onPointerLeave$.subscribeEvent(() => {
|
||||
o.cursor = CURSOR_TYPE.DEFAULT;
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _removeDom(id: string) {
|
||||
const pendingUnitId = this._pendingRuntimeGeometryInsert.get(id)?.unitId;
|
||||
this._pendingRuntimeGeometry.delete(id);
|
||||
this._pendingRuntimeGeometryInsert.delete(id);
|
||||
if (pendingUnitId != null && !this._hasPendingRuntimeGeometryForUnit(pendingUnitId)) {
|
||||
this._disposePendingRuntimeGeometryRefresh(pendingUnitId);
|
||||
}
|
||||
const info = this._domLayerInfoMap.get(id);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
const { unitId } = info;
|
||||
this._domLayerInfoMap.delete(id);
|
||||
|
||||
info.dispose.dispose();
|
||||
const renderObject = this._getSceneAndTransformerByDrawingSearch(unitId);
|
||||
if (renderObject) {
|
||||
renderObject.scene.removeObject(info.rect);
|
||||
}
|
||||
}
|
||||
|
||||
private _initScrollAndZoomEvent() {
|
||||
const updateDoc = (unitId: string) => {
|
||||
const renderObject = this._getSceneAndTransformerByDrawingSearch(unitId);
|
||||
if (!renderObject) {
|
||||
return;
|
||||
}
|
||||
this._domLayerInfoMap.forEach((floatDomInfo) => {
|
||||
if (floatDomInfo.unitId !== unitId) return;
|
||||
const position = calcDocFloatDomPosition(floatDomInfo.rect, renderObject.renderUnit);
|
||||
floatDomInfo.position$.next(position);
|
||||
});
|
||||
};
|
||||
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(
|
||||
map((documentDataModel) => {
|
||||
if (!documentDataModel) return null;
|
||||
const unitId = documentDataModel.getUnitId();
|
||||
const render = this._renderManagerService.getRenderUnitById(unitId);
|
||||
return render ? { render, unitId } : null;
|
||||
}),
|
||||
switchMap((render) =>
|
||||
render
|
||||
? fromEventSubject(render.render.scene.getViewport(VIEWPORT_KEY.VIEW_MAIN)!.onScrollAfter$)
|
||||
.pipe(map(() => ({ unitId: render.unitId })))
|
||||
: of(null)
|
||||
)
|
||||
).subscribe((value) => {
|
||||
if (!value) return;
|
||||
const { unitId } = value;
|
||||
updateDoc(unitId);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === SetDocZoomRatioOperation.id) {
|
||||
const params = (commandInfo.params) as ISetDocZoomRatioOperationParams;
|
||||
const { unitId } = params;
|
||||
globalThis.queueMicrotask(() => {
|
||||
if (!this._disposed) updateDoc(unitId);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
insertFloatDom(floatDom: IDocFloatDomParams, opts: { width?: number; height: number; drawingId?: string }) {
|
||||
const currentDoc = this._univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC);
|
||||
if (!currentDoc) return false;
|
||||
const render = this._getSceneAndTransformerByDrawingSearch(currentDoc.getUnitId());
|
||||
if (!render) return false;
|
||||
const docSkeletonManagerService = render.renderUnit.with(DocSkeletonManagerService);
|
||||
const skeleton = docSkeletonManagerService.getSkeleton();
|
||||
const page = skeleton.getSkeletonData()?.pages[0];
|
||||
if (!page) return false;
|
||||
const { pageWidth, marginLeft, marginRight } = page;
|
||||
const width = pageWidth - marginLeft - marginRight;
|
||||
const docTransform = {
|
||||
size: {
|
||||
width: opts.width ?? width,
|
||||
height: opts.height,
|
||||
},
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 0,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 0,
|
||||
},
|
||||
angle: 0,
|
||||
};
|
||||
const drawingId = opts.drawingId ?? generateRandomId();
|
||||
const params: IInsertDocDrawingCommandParams = {
|
||||
unitId: currentDoc.getUnitId(),
|
||||
drawings: [
|
||||
{
|
||||
drawingId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_DOM,
|
||||
subUnitId: currentDoc.getUnitId(),
|
||||
unitId: currentDoc.getUnitId(),
|
||||
...floatDom,
|
||||
title: '',
|
||||
description: '',
|
||||
docTransform,
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
transform: docDrawingPositionToTransform(docTransform),
|
||||
},
|
||||
],
|
||||
};
|
||||
this._commandService.syncExecuteCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, params);
|
||||
|
||||
return drawingId;
|
||||
}
|
||||
}
|
||||
|
||||
function syncRectWithRuntimeParam(
|
||||
rect: Rect,
|
||||
param: IDocFloatDomRuntimeGeometry,
|
||||
fallbackViewport?: IDocFloatDomRuntimeViewport,
|
||||
fallbackTransform?: Partial<ITransformState>,
|
||||
preserveRuntimeGeometry?: boolean
|
||||
): boolean {
|
||||
const transform = getRuntimeTransform(param, rect, fallbackViewport, fallbackTransform, preserveRuntimeGeometry);
|
||||
if (!transform) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rect.transformByState(transform as never);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getRuntimeTransform(
|
||||
param: IDocFloatDomRuntimeGeometry,
|
||||
rect: Rect,
|
||||
fallbackViewport?: IDocFloatDomRuntimeViewport,
|
||||
fallbackTransform?: Partial<ITransformState>,
|
||||
preserveRuntimeGeometry?: boolean
|
||||
): Partial<ITransformState> | undefined {
|
||||
const transform = param.transform ?? param.transforms?.[0];
|
||||
const runtimeViewport = param.customBlockRenderViewport ?? fallbackViewport;
|
||||
if (!param.customBlockRenderViewport && preserveRuntimeGeometry && fallbackTransform && transform) {
|
||||
return {
|
||||
...transform,
|
||||
width: fallbackTransform.width ?? transform.width,
|
||||
height: fallbackTransform.height ?? transform.height,
|
||||
};
|
||||
}
|
||||
|
||||
if (!transform) {
|
||||
const height = runtimeViewport?.height ?? runtimeViewport?.contentHeight;
|
||||
if (!isPositiveNumber(height)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height,
|
||||
angle: rect.angle,
|
||||
};
|
||||
}
|
||||
|
||||
const height = runtimeViewport?.height ?? runtimeViewport?.contentHeight;
|
||||
if (!isPositiveNumber(height)) {
|
||||
return transform;
|
||||
}
|
||||
|
||||
return {
|
||||
...transform,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
function isEmbedFloatDomRuntimeParam(param: unknown): param is IDocFloatDom {
|
||||
if (param == null || typeof param !== 'object' || !('data' in param)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = param.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = data as { embedId?: unknown; hostAnchorId?: unknown; version?: unknown };
|
||||
return candidate.version === 1 && typeof candidate.embedId === 'string' && typeof candidate.hostAnchorId === 'string';
|
||||
}
|
||||
|
||||
function isSheetLikeEmbedFloatDomRuntimeParam(param: unknown): param is IDocFloatDom {
|
||||
if (!isEmbedFloatDomRuntimeParam(param)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = param.data;
|
||||
return !!data &&
|
||||
typeof data === 'object' &&
|
||||
'childType' in data &&
|
||||
typeof data.childType === 'number' &&
|
||||
isSheetLikeDocsCustomBlockChildType(data.childType);
|
||||
}
|
||||
|
||||
function createTransformFromRect(rect: Rect): Partial<ITransformState> {
|
||||
return {
|
||||
angle: rect.angle,
|
||||
height: rect.height,
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
};
|
||||
}
|
||||
|
||||
function subscribeViewportScrollAfter(scrollEvent: unknown, callback: () => void): IDisposable | undefined {
|
||||
if (!scrollEvent || typeof scrollEvent !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const eventSubject = scrollEvent as { subscribeEvent?: (listener: () => void) => IDisposable };
|
||||
if (typeof eventSubject.subscribeEvent === 'function') {
|
||||
return eventSubject.subscribeEvent(callback);
|
||||
}
|
||||
|
||||
const observable = scrollEvent as { subscribe?: (listener: () => void) => { unsubscribe?: () => void } };
|
||||
if (typeof observable.subscribe === 'function') {
|
||||
const subscription = observable.subscribe(callback);
|
||||
return toDisposable(() => subscription.unsubscribe?.());
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { COMPONENT_DOC_DRAWING_PANEL } from '../../views/doc-image-panel/component-name';
|
||||
import { MobileDocDrawingPanel } from '../../views/doc-image-panel/MobileDocDrawingPanel';
|
||||
|
||||
export class MobileComponentsController extends Disposable {
|
||||
constructor(@Inject(ComponentManager) componentManager: ComponentManager) {
|
||||
super();
|
||||
this.disposeWithMe(componentManager.register(COMPONENT_DOC_DRAWING_PANEL, MobileDocDrawingPanel));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* 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, IDisposable, INeedCheckDisposable, Nullable } from '@univerjs/core';
|
||||
import type { BaseObject, Scene } from '@univerjs/engine-render';
|
||||
import {
|
||||
DrawingTypeEnum,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
IPermissionService,
|
||||
isInternalEditorID,
|
||||
IUniverInstanceService,
|
||||
RxDisposable,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
canEditDocumentTargets,
|
||||
getDocumentDrawingSegmentId,
|
||||
getDocumentEntityParentPermissionObjectIds,
|
||||
getDocumentEntityPermissionObjectId,
|
||||
} from '@univerjs/docs';
|
||||
import { IDocDrawingAdapterService, IDocDrawingService, RemoveDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { DocCanvasPopManagerService, MOBILE_DOC_ELEMENT_MENU } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import {
|
||||
COMPONENT_MOBILE_IMAGE_POPUP_MENU,
|
||||
ImageCropperObject,
|
||||
ImageResetSizeOperation,
|
||||
OpenImageCropOperation,
|
||||
} from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { FloatingObjectToolbarPosition, IMenuManagerService, MenuItemType } from '@univerjs/ui';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import { EditDocDrawingOperation } from '../../commands/operations/edit-doc-drawing.operation';
|
||||
import { SidebarDocDrawingMobileOperation } from '../../commands/operations/mobile/open-drawing-panel.operation';
|
||||
import { DocDrawingFloatingToolbarAdapterService } from '../../services/doc-drawing-floating-toolbar-adapter.service';
|
||||
|
||||
interface IDrawingPopupTarget {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawingId: string;
|
||||
}
|
||||
|
||||
export class MobileDocDrawingPopupMenuController extends RxDisposable {
|
||||
private _initImagePopupMenu = new Set<string>();
|
||||
private _embeddedRenderUnits = new Set<string>();
|
||||
private _popupMenuListeners = new Map<string, IDisposable>();
|
||||
private _disposePopupsByUnit = new Map<string, INeedCheckDisposable[]>();
|
||||
private _popupTargetsByUnit = new Map<string, IDrawingPopupTarget>();
|
||||
private _isDrawingPanelOpen = false;
|
||||
|
||||
constructor(
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(DocCanvasPopManagerService) private readonly _canvasPopManagerService: DocCanvasPopManagerService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IDocDrawingAdapterService private readonly _drawingAdapterService: IDocDrawingAdapterService,
|
||||
@IDocDrawingService private readonly _docDrawingService: IDocDrawingService,
|
||||
@Inject(DocDrawingFloatingToolbarAdapterService) private readonly _floatingToolbarAdapterService: DocDrawingFloatingToolbarAdapterService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IPermissionService private readonly _permissionService: IPermissionService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _init(): void {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command) => {
|
||||
const opensDrawingEditor = [...this._popupTargetsByUnit.values()].some((target) => {
|
||||
const drawing = this._docDrawingService.getDrawingByParam(target);
|
||||
const edit = drawing && this._drawingAdapterService.getEditDrawingCommandInfo({ ...target, drawing });
|
||||
return edit?.commandId === command.id;
|
||||
});
|
||||
if (command.id === EditDocDrawingOperation.id || opensDrawingEditor) {
|
||||
this._isDrawingPanelOpen = true;
|
||||
this._clearPopups(undefined, true);
|
||||
}
|
||||
if (command.id === SidebarDocDrawingMobileOperation.id) {
|
||||
const params = command.params as { value?: string } | undefined;
|
||||
this._isDrawingPanelOpen = params?.value === 'open';
|
||||
if (this._isDrawingPanelOpen) {
|
||||
this._clearPopups(undefined, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.focus$.subscribe((params) => {
|
||||
if (params.length === 0) {
|
||||
this._isDrawingPanelOpen = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(this._drawingManagerService.remove$.subscribe((drawings) => {
|
||||
for (const drawing of drawings) {
|
||||
for (const [unitId, target] of this._popupTargetsByUnit) {
|
||||
if (
|
||||
target.unitId === drawing.unitId &&
|
||||
target.subUnitId === drawing.subUnitId &&
|
||||
target.drawingId === drawing.drawingId
|
||||
) {
|
||||
this._clearPopups(unitId, true);
|
||||
this._renderManagerService.getRenderUnitById(unitId)?.scene.getTransformerByCreate().clearSelectedObjects();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.disposeWithMe(this._permissionService.permissionPointUpdate$.subscribe(() => {
|
||||
for (const [popupUnitId, drawing] of this._popupTargetsByUnit) {
|
||||
if (!this._canEditDrawing(drawing.unitId, drawing.drawingId)) {
|
||||
this._clearPopups(popupUnitId, true);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(takeUntil(this.dispose$)).subscribe((documentDataModel) => this._create(documentDataModel))
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService.getTypeOfUnitDisposed$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(takeUntil(this.dispose$)).subscribe((documentDataModel) => this._dispose(documentDataModel))
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._renderManagerService.created$.pipe(takeUntil(this.dispose$)).subscribe((render) => {
|
||||
if (render.type !== UniverInstanceType.UNIVER_DOC || render.isMainScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._embeddedRenderUnits.add(render.unitId);
|
||||
this._create(this._univerInstanceService.getUnit<DocumentDataModel>(render.unitId, UniverInstanceType.UNIVER_DOC));
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._renderManagerService.disposed$.pipe(takeUntil(this.dispose$)).subscribe((unitId) => {
|
||||
if (!this._embeddedRenderUnits.delete(unitId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposePopupMenuListener(unitId);
|
||||
})
|
||||
);
|
||||
|
||||
this._univerInstanceService.getAllUnitsForType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).forEach((documentDataModel) => this._create(documentDataModel));
|
||||
}
|
||||
|
||||
private _dispose(documentDataModel: DocumentDataModel) {
|
||||
const unitId = documentDataModel.getUnitId();
|
||||
this._clearPopups(unitId, true);
|
||||
this._disposePopupMenuListener(unitId);
|
||||
this._renderManagerService.removeRender(unitId);
|
||||
}
|
||||
|
||||
private _clearPopups(unitId?: string, force = false) {
|
||||
if (unitId == null) {
|
||||
[...this._disposePopupsByUnit.keys()].forEach((popupUnitId) => this._clearPopups(popupUnitId, force));
|
||||
return;
|
||||
}
|
||||
|
||||
const popups = this._disposePopupsByUnit.get(unitId);
|
||||
if (!popups) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = popups.length - 1; index >= 0; index -= 1) {
|
||||
const popup = popups[index];
|
||||
if (force || popup.canDispose()) {
|
||||
popup.dispose();
|
||||
popups.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (popups.length === 0) {
|
||||
this._disposePopupsByUnit.delete(unitId);
|
||||
this._popupTargetsByUnit.delete(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _getDisposePopups(unitId: string): INeedCheckDisposable[] {
|
||||
const popups = this._disposePopupsByUnit.get(unitId);
|
||||
if (popups) {
|
||||
return popups;
|
||||
}
|
||||
|
||||
const nextPopups: INeedCheckDisposable[] = [];
|
||||
this._disposePopupsByUnit.set(unitId, nextPopups);
|
||||
return nextPopups;
|
||||
}
|
||||
|
||||
private _create(documentDataModel: Nullable<DocumentDataModel>) {
|
||||
if (!documentDataModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = documentDataModel.getUnitId();
|
||||
if (isInternalEditorID(unitId)) {
|
||||
return;
|
||||
}
|
||||
if (this._renderManagerService.has(unitId) && !this._initImagePopupMenu.has(unitId)) {
|
||||
const listener = this._popupMenuListener(unitId);
|
||||
if (listener) {
|
||||
this._popupMenuListeners.set(unitId, listener);
|
||||
this._initImagePopupMenu.add(unitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _disposePopupMenuListener(unitId: string) {
|
||||
this._popupMenuListeners.get(unitId)?.dispose();
|
||||
this._popupMenuListeners.delete(unitId);
|
||||
this._initImagePopupMenu.delete(unitId);
|
||||
this._clearPopups(unitId, true);
|
||||
}
|
||||
|
||||
private _hasCropObject(scene: Scene) {
|
||||
const objects = scene.getAllObjects();
|
||||
|
||||
for (const object of objects) {
|
||||
if (object instanceof ImageCropperObject) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleClearControl(
|
||||
unitId: string,
|
||||
transformer: ReturnType<Scene['getTransformerByCreate']>,
|
||||
changeSelf: boolean
|
||||
): void {
|
||||
this._clearPopups(unitId, changeSelf);
|
||||
queueMicrotask(() => {
|
||||
if (transformer.getSelectedObjectMap().size > 0) {
|
||||
return;
|
||||
}
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
this._drawingManagerService.focusDrawing(null);
|
||||
});
|
||||
}
|
||||
|
||||
private _registerDrawingPopup(
|
||||
unitId: string,
|
||||
popup: INeedCheckDisposable,
|
||||
drawing: IDrawingPopupTarget
|
||||
): void {
|
||||
this.disposeWithMe(popup);
|
||||
this._getDisposePopups(unitId).push(popup);
|
||||
this._popupTargetsByUnit.set(unitId, drawing);
|
||||
|
||||
const focusDrawings = this._drawingManagerService.getFocusDrawings();
|
||||
const alreadyFocused = focusDrawings.find((focusedDrawing) =>
|
||||
focusedDrawing.unitId === drawing.unitId &&
|
||||
focusedDrawing.subUnitId === drawing.subUnitId &&
|
||||
focusedDrawing.drawingId === drawing.drawingId
|
||||
);
|
||||
if (!alreadyFocused) {
|
||||
this._drawingManagerService.focusDrawing([drawing]);
|
||||
}
|
||||
}
|
||||
|
||||
private _isSamePopupTarget(previous: IDrawingPopupTarget | undefined, next: IDrawingPopupTarget): boolean {
|
||||
return previous?.unitId === next.unitId &&
|
||||
previous.subUnitId === next.subUnitId &&
|
||||
previous.drawingId === next.drawingId;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _popupMenuListener(unitId: string): IDisposable | undefined {
|
||||
const scene = this._renderManagerService.getRenderUnitById(unitId)?.scene;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const subscriptions = [
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
transformer.createControl$.subscribe(() => {
|
||||
if (this._hasCropObject(scene)) {
|
||||
this._clearPopups(unitId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (this._isDrawingPanelOpen || selectedObjects.size > 1) {
|
||||
this._clearPopups(unitId, this._isDrawingPanelOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) {
|
||||
this._clearPopups(unitId);
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingParam = this._drawingManagerService.getDrawingOKey(object.oKey);
|
||||
if (
|
||||
!drawingParam ||
|
||||
drawingParam.drawingType === DrawingTypeEnum.DRAWING_DOM ||
|
||||
drawingParam.drawingType === DrawingTypeEnum.DRAWING_SHAPE
|
||||
) {
|
||||
this._clearPopups(unitId);
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId: drawingUnitId, subUnitId, drawingId, drawingType } = drawingParam;
|
||||
if (!this._canEditDrawing(drawingUnitId, drawingId)) {
|
||||
this._clearPopups(unitId, true);
|
||||
return;
|
||||
}
|
||||
const disposePopups = this._disposePopupsByUnit.get(unitId);
|
||||
const previousPopupTarget = this._popupTargetsByUnit.get(unitId);
|
||||
const popupTarget = { unitId: drawingUnitId, subUnitId, drawingId };
|
||||
if (this._isSamePopupTarget(previousPopupTarget, popupTarget) && disposePopups && disposePopups.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearPopups(unitId, previousPopupTarget != null);
|
||||
const isImage = drawingType === DrawingTypeEnum.DRAWING_IMAGE;
|
||||
// Charts use the document toolbar placement, while retaining chart-specific actions and controls.
|
||||
const isChart = drawingType === DrawingTypeEnum.DRAWING_CHART;
|
||||
const popup = this._canvasPopManagerService.attachPopupToObject(
|
||||
object,
|
||||
{
|
||||
componentKey: isImage || isChart ? COMPONENT_MOBILE_IMAGE_POPUP_MENU : MOBILE_DOC_ELEMENT_MENU,
|
||||
requiresStableLayout: false,
|
||||
direction: isImage || isChart ? 'top-center' : 'horizontal',
|
||||
offset: isImage || isChart ? [0, 8] : [2, 0],
|
||||
extraProps: {
|
||||
onClose: () => this._clearPopups(unitId, true),
|
||||
onEdit: () => {
|
||||
const drawing = this._docDrawingService.getDrawingByParam(drawingParam);
|
||||
const edit = drawing && this._drawingAdapterService.getEditDrawingCommandInfo({
|
||||
unitId: drawingUnitId,
|
||||
subUnitId,
|
||||
drawing,
|
||||
});
|
||||
this._clearPopups(unitId, true);
|
||||
return this._commandService.executeCommand(edit?.commandId ?? EditDocDrawingOperation.id, edit?.commandParams ?? { unitId: drawingUnitId, subUnitId, drawingId });
|
||||
},
|
||||
onDelete: () => {
|
||||
this._clearPopups(unitId, true);
|
||||
return this._commandService.executeCommand(RemoveDocDrawingCommand.id, {
|
||||
unitId: drawingUnitId,
|
||||
drawings: [{ unitId: drawingUnitId, subUnitId, drawingId }],
|
||||
});
|
||||
},
|
||||
menuItems: this._getDrawingPopupMenuItems(drawingUnitId, subUnitId, drawingId, drawingType),
|
||||
variant: isImage ? 'doc-floating-toolbar' : isChart ? 'doc-chart-floating-toolbar' : undefined,
|
||||
unitId: drawingUnitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
},
|
||||
},
|
||||
drawingUnitId
|
||||
);
|
||||
|
||||
this._registerDrawingPopup(unitId, popup, { unitId: drawingUnitId, subUnitId, drawingId });
|
||||
}),
|
||||
transformer.clearControl$.subscribe((changeSelf) => this._handleClearControl(unitId, transformer, changeSelf)),
|
||||
transformer.changing$.subscribe(() => {
|
||||
this._clearPopups(unitId, true);
|
||||
}),
|
||||
];
|
||||
const disposable = toDisposable(() => subscriptions.forEach((subscription) => subscription.unsubscribe()));
|
||||
this.disposeWithMe(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
private _canEditDrawing(unitId: string, drawingId: string): boolean {
|
||||
const documentDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
|
||||
unitId,
|
||||
UniverInstanceType.UNIVER_DOC
|
||||
);
|
||||
if (!documentDataModel) {
|
||||
return false;
|
||||
}
|
||||
const segmentId = getDocumentDrawingSegmentId(documentDataModel, drawingId);
|
||||
return canEditDocumentTargets(this._permissionService, unitId, [
|
||||
...getDocumentEntityParentPermissionObjectIds(documentDataModel, segmentId, 'drawing', drawingId),
|
||||
getDocumentEntityPermissionObjectId(segmentId, 'drawing', drawingId),
|
||||
]);
|
||||
}
|
||||
|
||||
private _getDrawingPopupMenuItems(unitId: string, subUnitId: string, drawingId: string, drawingType: number) {
|
||||
const drawing = this._docDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId });
|
||||
const floatingToolbarMenuItems = drawing
|
||||
? this._floatingToolbarAdapterService.getItems({ unitId, subUnitId, drawing })
|
||||
: null;
|
||||
const editCommandInfo = drawing
|
||||
? this._drawingAdapterService.getEditDrawingCommandInfo({ unitId, subUnitId, drawing })
|
||||
: null;
|
||||
|
||||
const defaultItems = [
|
||||
{
|
||||
label: editCommandInfo?.label ?? 'docs-drawing-ui.image-popup.edit',
|
||||
index: 0,
|
||||
commandId: editCommandInfo?.commandId ?? EditDocDrawingOperation.id,
|
||||
commandParams: editCommandInfo?.commandParams ?? { unitId, subUnitId, drawingId },
|
||||
disable: editCommandInfo?.disable ?? drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
hideOnClick: true,
|
||||
icon: 'DrawingEditIcon',
|
||||
},
|
||||
{
|
||||
label: 'docs-drawing-ui.image-popup.crop',
|
||||
index: 1,
|
||||
commandId: OpenImageCropOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
icon: 'DrawingCropIcon',
|
||||
},
|
||||
{
|
||||
label: 'docs-drawing-ui.image-popup.delete',
|
||||
index: 2,
|
||||
commandId: RemoveDocDrawingCommand.id,
|
||||
commandParams: { unitId, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: false,
|
||||
icon: 'DrawingDeleteIcon',
|
||||
},
|
||||
{
|
||||
label: 'docs-drawing-ui.image-popup.reset',
|
||||
index: 3,
|
||||
commandId: ImageResetSizeOperation.id,
|
||||
commandParams: [{ unitId, subUnitId, drawingId }],
|
||||
disable: true, // TODO: @JOCS, feature is not ready.
|
||||
},
|
||||
];
|
||||
|
||||
if (drawingType === DrawingTypeEnum.DRAWING_IMAGE) {
|
||||
return defaultItems.slice(0, 3);
|
||||
}
|
||||
if (drawingType === DrawingTypeEnum.DRAWING_CHART) {
|
||||
return [{ ...defaultItems[0], label: 'docs-drawing-ui.image-popup.edit' }, defaultItems[2]];
|
||||
}
|
||||
return [...(floatingToolbarMenuItems ?? defaultItems), ...this._getFloatingObjectMenuItems()];
|
||||
}
|
||||
|
||||
private _getFloatingObjectMenuItems() {
|
||||
return this._menuManagerService
|
||||
.getFlatMenuByPositionKey(FloatingObjectToolbarPosition.DOC)
|
||||
.flatMap(({ item }, index) => {
|
||||
if (!item || item.type !== MenuItemType.BUTTON || !item.title || typeof item.icon !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
type: 'button' as const,
|
||||
label: item.title,
|
||||
index: 100 + index,
|
||||
commandId: item.commandId ?? item.id,
|
||||
commandParams: typeof item.params === 'function' ? item.params() : item.params,
|
||||
disable: false,
|
||||
icon: item.icon,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 { Disposable, ICommandService } from '@univerjs/core';
|
||||
import { IMenuManagerService } from '@univerjs/ui';
|
||||
import { DeleteDocDrawingsCommand } from '../../commands/commands/delete-doc-drawing.command';
|
||||
import { GroupDocDrawingCommand } from '../../commands/commands/group-doc-drawing.command';
|
||||
import { InsertDocImageCommand } from '../../commands/commands/insert-image.command';
|
||||
import { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from '../../commands/commands/insert-shape.command';
|
||||
import { MoveDocDrawingsCommand } from '../../commands/commands/move-drawings.command';
|
||||
import { UngroupDocDrawingCommand } from '../../commands/commands/ungroup-doc-drawing.command';
|
||||
import {
|
||||
IMoveInlineDrawingCommand,
|
||||
ITransformNonInlineDrawingCommand,
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
} from '../../commands/commands/update-doc-drawing.command';
|
||||
import { ClearDocDrawingTransformerOperation } from '../../commands/operations/clear-drawing-transformer.operation';
|
||||
import { EditDocDrawingOperation } from '../../commands/operations/edit-doc-drawing.operation';
|
||||
import { SidebarDocDrawingMobileOperation } from '../../commands/operations/mobile/open-drawing-panel.operation';
|
||||
import { menuSchema } from '../../menu/schema';
|
||||
|
||||
export class MobileDocDrawingUIController extends Disposable {
|
||||
constructor(
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initMenus();
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
InsertDocImageCommand,
|
||||
InsertDocRectangleShapeCommand,
|
||||
InsertDocEllipseShapeCommand,
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
IMoveInlineDrawingCommand,
|
||||
ITransformNonInlineDrawingCommand,
|
||||
SidebarDocDrawingMobileOperation,
|
||||
ClearDocDrawingTransformerOperation,
|
||||
EditDocDrawingOperation,
|
||||
GroupDocDrawingCommand,
|
||||
UngroupDocDrawingCommand,
|
||||
MoveDocDrawingsCommand,
|
||||
DeleteDocDrawingsCommand,
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
private _initMenus(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 { IDocDrawing } from '@univerjs/docs-drawing';
|
||||
import type { IImageData } from '@univerjs/drawing';
|
||||
import { DrawingTypeEnum, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getDocImageCropUpdates } from '../doc-drawing-crop';
|
||||
|
||||
function drawing(): IDocDrawing {
|
||||
return {
|
||||
unitId: 'doc',
|
||||
subUnitId: 'doc',
|
||||
drawingId: 'note-image',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
transform: { left: 40, top: 600, width: 100, height: 40 },
|
||||
docTransform: {
|
||||
angle: 0,
|
||||
size: { width: 150, height: 60 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 40 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 600 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('document image crop updates', () => {
|
||||
it('does not turn an uncropped transformer notification into a second size mutation', () => {
|
||||
const image = drawing();
|
||||
const notification = { ...image, srcRect: undefined } as IImageData;
|
||||
expect(getDocImageCropUpdates(notification, image, notification)).toEqual([]);
|
||||
expect(image.docTransform.size).toEqual({ width: 150, height: 60 });
|
||||
});
|
||||
|
||||
it('persists a real crop and permits an explicit reset without moving an inline anchor', () => {
|
||||
const image = drawing();
|
||||
const renderImage = image as IImageData;
|
||||
const crop = { left: 10, right: 10, top: 4, bottom: 4 };
|
||||
expect(getDocImageCropUpdates({
|
||||
...renderImage,
|
||||
srcRect: crop,
|
||||
transform: { left: 50, top: 604, width: 80, height: 32 },
|
||||
}, image, renderImage)).toEqual([
|
||||
{ drawingId: 'note-image', key: 'srcRect', value: crop },
|
||||
{ drawingId: 'note-image', key: 'size', value: { width: 80, height: 32 } },
|
||||
]);
|
||||
expect(getDocImageCropUpdates({ ...renderImage, srcRect: null }, image, renderImage)).toEqual([
|
||||
{ drawingId: 'note-image', key: 'srcRect', value: null },
|
||||
{ drawingId: 'note-image', key: 'size', value: { width: 100, height: 40 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,833 @@
|
||||
/**
|
||||
* 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 { DocumentDrawingPublicationProgress } from '../doc-drawing-transform-update.controller';
|
||||
import {
|
||||
AlignTypeH,
|
||||
AlignTypeV,
|
||||
BooleanNumber,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
} from '@univerjs/core';
|
||||
import { Liquid, setDocsTableRenderViewportProvider } from '@univerjs/engine-render';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getDocsTableCellAnchorContext } from '../../doc-drawing-transformer-update.controller';
|
||||
import { doesDocMutationAffectDrawingPresentation, getDocMutationAffectedDrawingIds } from '../doc-drawing-mutation';
|
||||
import {
|
||||
DocDrawingPublicationTracker,
|
||||
DocDrawingTransformUpdateController,
|
||||
|
||||
getDocsDrawingBehindText,
|
||||
getDocsDrawingClipPage,
|
||||
getDocsDrawingPageClipBounds,
|
||||
getDocsPageRelativeDrawingAnchorPage,
|
||||
getDocsPageRelativeDrawingLeft,
|
||||
getDocsPageRelativeDrawingTop,
|
||||
getDocsTableCellDrawingOffset,
|
||||
shouldRefreshDocDrawingTransform,
|
||||
} from '../doc-drawing-transform-update.controller';
|
||||
|
||||
describe('DocDrawingTransformUpdateController', () => {
|
||||
afterEach(() => {
|
||||
setDocsTableRenderViewportProvider(null);
|
||||
});
|
||||
|
||||
it('does not eagerly refresh drawings for table metadata mutations', () => {
|
||||
expect(doesDocMutationAffectDrawingPresentation([
|
||||
'tableSource',
|
||||
'table-1',
|
||||
'tableColumns',
|
||||
0,
|
||||
'size',
|
||||
'width',
|
||||
'v',
|
||||
{ r: 28, i: 32 },
|
||||
] as never)).toBe(false);
|
||||
expect(doesDocMutationAffectDrawingPresentation([
|
||||
'drawings',
|
||||
'drawing-1',
|
||||
'docTransform',
|
||||
{ r: {}, i: { angle: 1 } },
|
||||
] as never)).toBe(true);
|
||||
expect([...getDocMutationAffectedDrawingIds([
|
||||
'drawings',
|
||||
'drawing-1',
|
||||
'docTransform',
|
||||
{ r: {}, i: { angle: 1 } },
|
||||
] as never)!]).toEqual(['drawing-1']);
|
||||
expect(getDocMutationAffectedDrawingIds([
|
||||
'drawingsOrder',
|
||||
{ r: [], i: ['drawing-1'] },
|
||||
] as never)).toBeNull();
|
||||
});
|
||||
|
||||
it('filters unchanged drawing transform notifications but preserves viewport refreshes', () => {
|
||||
const current = {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'drawing-1',
|
||||
drawingType: 0,
|
||||
behindText: false,
|
||||
hidden: false,
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
transform: { left: 10, top: 20, width: 30, height: 40 },
|
||||
transforms: [],
|
||||
};
|
||||
|
||||
expect(shouldRefreshDocDrawingTransform(current as never, { ...current } as never)).toBe(false);
|
||||
expect(shouldRefreshDocDrawingTransform(current as never, {
|
||||
...current,
|
||||
transform: { ...current.transform, left: 11 },
|
||||
} as never)).toBe(true);
|
||||
expect(shouldRefreshDocDrawingTransform(current as never, {
|
||||
...current,
|
||||
customBlockRenderViewport: { contentWidth: 100 },
|
||||
} as never)).toBe(true);
|
||||
expect(shouldRefreshDocDrawingTransform(current as never, {
|
||||
...current,
|
||||
selectable: false,
|
||||
} as never)).toBe(true);
|
||||
});
|
||||
|
||||
it('publishes preview-only drawings as non-selectable and restores canonical drawings', () => {
|
||||
const controller = Object.create(DocDrawingTransformUpdateController.prototype);
|
||||
controller._context = {
|
||||
unit: { getSnapshot: () => ({ drawings: {} }) },
|
||||
};
|
||||
controller._liquid = new Liquid();
|
||||
const drawing = {
|
||||
aLeft: 10,
|
||||
aTop: 20,
|
||||
width: 30,
|
||||
height: 40,
|
||||
angle: 0,
|
||||
drawingId: 'drawing-1',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
docTransform: { positionH: {}, positionV: {} },
|
||||
},
|
||||
};
|
||||
const page = {
|
||||
footerId: '',
|
||||
headerId: '',
|
||||
isLayoutPlaceholder: true,
|
||||
marginBottom: 10,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
pageHeight: 300,
|
||||
pageWidth: 200,
|
||||
skeColumnGroups: new Map(),
|
||||
skeDrawings: new Map([['drawing-1', drawing]]),
|
||||
skeTables: new Map(),
|
||||
};
|
||||
const previewDrawings = {} as Record<string, { selectable: boolean }>;
|
||||
|
||||
controller._collectPublishedPageDrawingPositions(
|
||||
'doc-1',
|
||||
page,
|
||||
new Map(),
|
||||
new Map(),
|
||||
0,
|
||||
0,
|
||||
previewDrawings
|
||||
);
|
||||
expect(previewDrawings['drawing-1']).toMatchObject({ selectable: false });
|
||||
|
||||
page.isLayoutPlaceholder = false;
|
||||
const canonicalDrawings = {} as Record<string, { selectable: boolean }>;
|
||||
controller._collectPublishedPageDrawingPositions(
|
||||
'doc-1',
|
||||
page,
|
||||
new Map(),
|
||||
new Map(),
|
||||
0,
|
||||
0,
|
||||
canonicalDrawings
|
||||
);
|
||||
expect(canonicalDrawings['drawing-1']).toMatchObject({ selectable: true });
|
||||
});
|
||||
|
||||
it('refreshes only incremental publications that can change drawing transforms', () => {
|
||||
const drawing = {};
|
||||
const pages = [
|
||||
{ headerId: '', footerId: '', pageWidth: 100, skeDrawings: new Map([['cover', drawing]]), skeTables: new Map(), skeColumnGroups: new Map() },
|
||||
{ headerId: '', footerId: '', pageWidth: 100, skeDrawings: new Map(), skeTables: new Map(), skeColumnGroups: new Map() },
|
||||
{ headerId: '', footerId: '', pageWidth: 100, skeDrawings: new Map([['later', drawing]]), skeTables: new Map(), skeColumnGroups: new Map() },
|
||||
];
|
||||
const skeleton = {
|
||||
getSkeletonData: () => ({
|
||||
pages,
|
||||
skeHeaders: new Map(),
|
||||
skeFooters: new Map(),
|
||||
}),
|
||||
};
|
||||
const tracker = new DocDrawingPublicationTracker();
|
||||
const progress = (overrides: Partial<DocumentDrawingPublicationProgress>): DocumentDrawingPublicationProgress => ({
|
||||
generation: 1,
|
||||
didPublish: true,
|
||||
didPublishAnchor: false,
|
||||
publishedPageCount: 1,
|
||||
reason: 'initial',
|
||||
complete: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
expect(tracker.shouldRefresh(skeleton, progress({}))).toBe(true);
|
||||
expect(tracker.shouldRefresh(skeleton, progress({ publishedPageCount: 2 }))).toBe(false);
|
||||
expect(tracker.shouldRefresh(skeleton, progress({ publishedPageCount: 3 }))).toBe(true);
|
||||
expect(tracker.shouldRefresh(skeleton, progress({
|
||||
generation: 2,
|
||||
reason: 'edit',
|
||||
publishedPageCount: 3,
|
||||
}))).toBe(false);
|
||||
expect(tracker.shouldRefresh(skeleton, progress({
|
||||
generation: 2,
|
||||
reason: 'edit',
|
||||
didPublishAnchor: true,
|
||||
publishedPageCount: 3,
|
||||
}))).toBe(true);
|
||||
});
|
||||
|
||||
it('refreshes when a later anchor resolves a drawing onto an already published page', () => {
|
||||
const firstPageDrawings = new Map();
|
||||
const pages = [
|
||||
{ headerId: '', footerId: '', pageWidth: 100, skeDrawings: firstPageDrawings, skeTables: new Map(), skeColumnGroups: new Map() },
|
||||
{ headerId: '', footerId: '', pageWidth: 100, skeDrawings: new Map(), skeTables: new Map(), skeColumnGroups: new Map() },
|
||||
];
|
||||
const skeleton = {
|
||||
getSkeletonData: () => ({
|
||||
pages,
|
||||
skeHeaders: new Map(),
|
||||
skeFooters: new Map(),
|
||||
}),
|
||||
};
|
||||
const tracker = new DocDrawingPublicationTracker();
|
||||
const progress = (publishedPageCount: number): DocumentDrawingPublicationProgress => ({
|
||||
generation: 1,
|
||||
didPublish: true,
|
||||
didPublishAnchor: false,
|
||||
publishedPageCount,
|
||||
reason: 'edit',
|
||||
complete: false,
|
||||
});
|
||||
|
||||
expect(tracker.shouldRefresh(skeleton, progress(1))).toBe(false);
|
||||
expect(tracker.shouldRefresh(skeleton, progress(2))).toBe(false);
|
||||
|
||||
firstPageDrawings.set('late-drawing', {});
|
||||
|
||||
expect(tracker.shouldRefresh(skeleton, progress(2))).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['column', 'footnote'])('refreshes when a published %s contains drawings', (scope) => {
|
||||
const nestedPage = {
|
||||
skeDrawings: new Map([['column-drawing', {}]]),
|
||||
skeTables: new Map(),
|
||||
skeColumnGroups: new Map(),
|
||||
};
|
||||
const pages = [{
|
||||
headerId: '',
|
||||
footerId: '',
|
||||
pageWidth: 100,
|
||||
skeDrawings: new Map(),
|
||||
skeTables: new Map(),
|
||||
skeColumnGroups: new Map(scope === 'column'
|
||||
? [['column-group', {
|
||||
columns: [{ page: nestedPage }],
|
||||
}]]
|
||||
: []),
|
||||
notes: scope === 'footnote' ? [{ page: nestedPage }] : [],
|
||||
}];
|
||||
const skeleton = {
|
||||
getSkeletonData: () => ({
|
||||
pages,
|
||||
skeHeaders: new Map(),
|
||||
skeFooters: new Map(),
|
||||
}),
|
||||
};
|
||||
const tracker = new DocDrawingPublicationTracker();
|
||||
|
||||
expect(tracker.shouldRefresh(skeleton, {
|
||||
generation: 1,
|
||||
didPublish: true,
|
||||
didPublishAnchor: false,
|
||||
publishedPageCount: 1,
|
||||
reason: 'initial',
|
||||
complete: false,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('projects drawings in table cells through table, row, cell and scroll offsets', () => {
|
||||
setDocsTableRenderViewportProvider((unitId, tableId) => {
|
||||
if (unitId !== 'unit-1' || tableId !== 'table-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
contentWidth: 480,
|
||||
scrollLeft: 30,
|
||||
viewportWidth: 160,
|
||||
};
|
||||
});
|
||||
|
||||
const table = {
|
||||
left: 40,
|
||||
tableId: 'table-1#-#0',
|
||||
top: 80,
|
||||
};
|
||||
const row = {
|
||||
top: 12,
|
||||
};
|
||||
const cell = {
|
||||
left: 120,
|
||||
marginLeft: 8,
|
||||
marginTop: 6,
|
||||
};
|
||||
|
||||
expect(getDocsTableCellDrawingOffset('unit-1', table as never, row as never, cell as never)).toEqual({
|
||||
left: 138,
|
||||
top: 98,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves a table cell drawing anchor to the host page and scrolled cell offset', () => {
|
||||
setDocsTableRenderViewportProvider((unitId, tableId) => {
|
||||
if (unitId !== 'unit-1' || tableId !== 'table-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
contentWidth: 480,
|
||||
scrollLeft: 30,
|
||||
viewportWidth: 160,
|
||||
};
|
||||
});
|
||||
|
||||
const hostPage = { type: 'body' };
|
||||
const cell = {
|
||||
left: 120,
|
||||
marginLeft: 8,
|
||||
marginTop: 6,
|
||||
};
|
||||
const row = {
|
||||
cells: [cell],
|
||||
top: 12,
|
||||
};
|
||||
const table = {
|
||||
left: 40,
|
||||
parent: hostPage,
|
||||
rows: [row],
|
||||
tableId: 'table-1#-#0',
|
||||
top: 80,
|
||||
};
|
||||
Object.assign(row, { parent: table });
|
||||
Object.assign(cell, { parent: row });
|
||||
|
||||
expect(getDocsTableCellAnchorContext('unit-1', cell as never)).toMatchObject({
|
||||
hostPage,
|
||||
offset: {
|
||||
left: 138,
|
||||
top: 98,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes normal, table-cell, header, and multi-transform drawings into drawing notifications', () => {
|
||||
const selectedShape = {};
|
||||
const currentDrawings: Record<string, unknown> = {};
|
||||
const transformer = {
|
||||
getSelectedObjectMap: vi.fn(() => new Map([['multi-drawing', selectedShape]])),
|
||||
setSelectedControl: vi.fn(),
|
||||
};
|
||||
const context = {
|
||||
unitId: 'unit-1',
|
||||
unit: {
|
||||
getSnapshot: vi.fn(() => ({ drawings: currentDrawings })),
|
||||
},
|
||||
mainComponent: {
|
||||
left: 5,
|
||||
top: 7,
|
||||
pageLayoutType: 0,
|
||||
pageMarginLeft: 0,
|
||||
pageMarginTop: 20,
|
||||
},
|
||||
scene: {
|
||||
getTransformerByCreate: vi.fn(() => transformer),
|
||||
getObject: vi.fn((key) => key === 'multi-drawing' ? selectedShape : null),
|
||||
},
|
||||
};
|
||||
const drawingManagerService = {
|
||||
refreshTransform: vi.fn(),
|
||||
getDrawingByParam: vi.fn((drawing) => ({ ...drawing })),
|
||||
getDrawingData: vi.fn(() => ({
|
||||
'multi-drawing': { drawingId: 'multi-drawing', isMultiTransform: BooleanNumber.TRUE },
|
||||
'stale-multi': { drawingId: 'stale-multi', isMultiTransform: BooleanNumber.TRUE },
|
||||
'stale-normal': {
|
||||
drawingId: 'stale-normal',
|
||||
transform: { left: 0, top: 0, width: 100, height: 100, angle: 0 },
|
||||
},
|
||||
})),
|
||||
removeNotification: vi.fn(),
|
||||
addNotification: vi.fn(),
|
||||
};
|
||||
const controller = Object.create(DocDrawingTransformUpdateController.prototype);
|
||||
controller._context = context;
|
||||
controller._drawingManagerService = drawingManagerService;
|
||||
controller._liquid = new Liquid();
|
||||
const defaultDocTransform = {
|
||||
positionH: {},
|
||||
positionV: {},
|
||||
};
|
||||
|
||||
const normalDrawing = {
|
||||
aLeft: 10,
|
||||
aTop: 20,
|
||||
width: 30,
|
||||
height: 40,
|
||||
angle: 5,
|
||||
drawingId: 'normal-drawing',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: BooleanNumber.TRUE,
|
||||
docTransform: defaultDocTransform,
|
||||
},
|
||||
};
|
||||
const pageRelativeDrawing = {
|
||||
...normalDrawing,
|
||||
drawingId: 'page-relative-drawing',
|
||||
drawingOrigin: {
|
||||
...normalDrawing.drawingOrigin,
|
||||
docTransform: {
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 99 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 88 },
|
||||
},
|
||||
},
|
||||
};
|
||||
currentDrawings['page-relative-drawing'] = {
|
||||
...pageRelativeDrawing.drawingOrigin,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
docTransform: {
|
||||
angle: 6,
|
||||
size: { width: 31, height: 41 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 109 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 98 },
|
||||
},
|
||||
};
|
||||
const squareDrawing = {
|
||||
...normalDrawing,
|
||||
drawingId: 'square-drawing',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
docTransform: pageRelativeDrawing.drawingOrigin.docTransform,
|
||||
},
|
||||
};
|
||||
const topBottomDrawing = {
|
||||
...squareDrawing,
|
||||
aLeft: 12,
|
||||
aTop: 22,
|
||||
drawingId: 'top-bottom-drawing',
|
||||
drawingOrigin: {
|
||||
...squareDrawing.drawingOrigin,
|
||||
layoutType: PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM,
|
||||
},
|
||||
};
|
||||
const headerDrawing = {
|
||||
aLeft: 3,
|
||||
aTop: 4,
|
||||
width: 9,
|
||||
height: 10,
|
||||
angle: 0,
|
||||
drawingId: 'header-drawing',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
docTransform: defaultDocTransform,
|
||||
},
|
||||
};
|
||||
const tableCellDrawing = {
|
||||
aLeft: 1,
|
||||
aTop: 2,
|
||||
width: 11,
|
||||
height: 12,
|
||||
angle: 0,
|
||||
drawingId: 'cell-drawing',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
docTransform: defaultDocTransform,
|
||||
},
|
||||
};
|
||||
const columnDrawing = {
|
||||
...tableCellDrawing,
|
||||
aLeft: 2,
|
||||
aTop: 3,
|
||||
drawingId: 'column-drawing',
|
||||
};
|
||||
const columnTableCellDrawing = {
|
||||
...tableCellDrawing,
|
||||
drawingId: 'column-table-cell-drawing',
|
||||
};
|
||||
const inlineDrawing = {
|
||||
aLeft: 15,
|
||||
aTop: 25,
|
||||
width: 35,
|
||||
height: 45,
|
||||
angle: 0,
|
||||
drawingId: 'inline-drawing',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
docTransform: {
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 99 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 88 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const multiDrawingOnPage = {
|
||||
aLeft: 6,
|
||||
aTop: 8,
|
||||
width: 13,
|
||||
height: 14,
|
||||
angle: 2,
|
||||
drawingId: 'multi-drawing',
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
isMultiTransform: BooleanNumber.TRUE,
|
||||
docTransform: defaultDocTransform,
|
||||
},
|
||||
};
|
||||
const multiDrawingInHeader = {
|
||||
...multiDrawingOnPage,
|
||||
aLeft: 16,
|
||||
aTop: 18,
|
||||
};
|
||||
const cell = {
|
||||
left: 7,
|
||||
marginLeft: 2,
|
||||
marginTop: 3,
|
||||
skeDrawings: new Map([['cell-drawing', tableCellDrawing]]),
|
||||
skeTables: [],
|
||||
};
|
||||
const table = {
|
||||
left: 30,
|
||||
top: 40,
|
||||
tableId: 'table-1#-#0',
|
||||
rows: [{
|
||||
top: 4,
|
||||
cells: [cell],
|
||||
}],
|
||||
};
|
||||
const columnTableCell = {
|
||||
left: 3,
|
||||
marginLeft: 1,
|
||||
marginTop: 1,
|
||||
skeDrawings: new Map([['column-table-cell-drawing', columnTableCellDrawing]]),
|
||||
skeTables: new Map(),
|
||||
skeColumnGroups: new Map(),
|
||||
};
|
||||
const columnTable = {
|
||||
left: 4,
|
||||
top: 5,
|
||||
tableId: 'column-table#-#0',
|
||||
rows: [{
|
||||
top: 2,
|
||||
cells: [columnTableCell],
|
||||
}],
|
||||
};
|
||||
const columnPage = {
|
||||
marginLeft: 0,
|
||||
marginTop: 0,
|
||||
skeDrawings: new Map([['column-drawing', columnDrawing]]),
|
||||
skeTables: new Map([['column-table', columnTable]]),
|
||||
skeColumnGroups: new Map(),
|
||||
};
|
||||
const columnGroup = {
|
||||
left: 50,
|
||||
top: 60,
|
||||
columns: [{
|
||||
left: 8,
|
||||
top: 9,
|
||||
page: columnPage,
|
||||
}],
|
||||
};
|
||||
const page = {
|
||||
headerId: 'header-1',
|
||||
pageWidth: 200,
|
||||
pageHeight: 300,
|
||||
marginLeft: 11,
|
||||
marginTop: 13,
|
||||
marginBottom: 17,
|
||||
skeDrawings: new Map([
|
||||
['normal-drawing', normalDrawing],
|
||||
['page-relative-drawing', pageRelativeDrawing],
|
||||
['square-drawing', squareDrawing],
|
||||
['top-bottom-drawing', topBottomDrawing],
|
||||
['inline-drawing', inlineDrawing],
|
||||
['multi-drawing', multiDrawingOnPage],
|
||||
]),
|
||||
skeTables: [table],
|
||||
skeColumnGroups: new Map([['column-group', columnGroup]]),
|
||||
};
|
||||
const headerPage = {
|
||||
marginTop: 19,
|
||||
skeDrawings: new Map([
|
||||
['header-drawing', headerDrawing],
|
||||
['multi-drawing', multiDrawingInHeader],
|
||||
]),
|
||||
skeTables: [],
|
||||
};
|
||||
const skeleton = {
|
||||
getSkeletonData: () => ({
|
||||
pages: [page],
|
||||
skeHeaders: new Map([['header-1', new Map([[200, headerPage]])]]),
|
||||
skeFooters: new Map(),
|
||||
}),
|
||||
};
|
||||
|
||||
controller._refreshDrawing(skeleton);
|
||||
|
||||
expect(drawingManagerService.refreshTransform).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
drawingId: 'header-drawing',
|
||||
transform: expect.objectContaining({ left: 19, top: 30, width: 9, height: 10, angle: 0 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'normal-drawing',
|
||||
behindText: true,
|
||||
transform: expect.objectContaining({ left: 26, top: 40, width: 30, height: 40, angle: 5 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'page-relative-drawing',
|
||||
behindText: false,
|
||||
transform: expect.objectContaining({ left: 114, top: 105, width: 31, height: 41, angle: 6 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'square-drawing',
|
||||
transform: expect.objectContaining({ left: 26, top: 40, width: 30, height: 40, angle: 5 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'top-bottom-drawing',
|
||||
transform: expect.objectContaining({ left: 28, top: 42, width: 30, height: 40, angle: 5 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'inline-drawing',
|
||||
transform: expect.objectContaining({ left: 31, top: 45, width: 35, height: 45, angle: 0 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'cell-drawing',
|
||||
transform: expect.objectContaining({ left: 56, top: 69, width: 11, height: 12, angle: 0 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'column-drawing',
|
||||
transform: expect.objectContaining({ left: 76, top: 92, width: 11, height: 12, angle: 0 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'column-table-cell-drawing',
|
||||
transform: expect.objectContaining({ left: 83, top: 99, width: 11, height: 12, angle: 0 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
drawingId: 'stale-normal',
|
||||
hidden: true,
|
||||
transform: expect.objectContaining({ left: 0, top: 0, width: 100, height: 100, angle: 0 }),
|
||||
}),
|
||||
]);
|
||||
expect(drawingManagerService.removeNotification).toHaveBeenCalledWith([
|
||||
{ drawingId: 'multi-drawing', isMultiTransform: BooleanNumber.TRUE },
|
||||
{ drawingId: 'stale-multi', isMultiTransform: BooleanNumber.TRUE },
|
||||
]);
|
||||
expect(drawingManagerService.addNotification).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
drawingId: 'multi-drawing',
|
||||
isMultiTransform: BooleanNumber.TRUE,
|
||||
transforms: [
|
||||
expect.objectContaining({ left: 32, top: 44, width: 13, height: 14, angle: 2 }),
|
||||
expect.objectContaining({ left: 22, top: 28, width: 13, height: 14, angle: 2 }),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
expect(transformer.setSelectedControl).toHaveBeenCalledWith(selectedShape);
|
||||
});
|
||||
|
||||
it('projects a document page clip bound in scene coordinates before page padding is applied', () => {
|
||||
expect(getDocsDrawingPageClipBounds({
|
||||
docsLeft: 20,
|
||||
docsTop: 30,
|
||||
pageOffsetLeft: 100,
|
||||
pageOffsetTop: 200,
|
||||
page: {
|
||||
pageWidth: 793,
|
||||
pageHeight: 1122,
|
||||
marginLeft: 48,
|
||||
marginTop: 48,
|
||||
},
|
||||
} as never)).toEqual({
|
||||
left: 120,
|
||||
top: 230,
|
||||
width: 793,
|
||||
height: 1122,
|
||||
});
|
||||
});
|
||||
|
||||
it('projects a table cell drawing clip bound through the cell offset', () => {
|
||||
expect(getDocsDrawingPageClipBounds({
|
||||
docsLeft: 20,
|
||||
docsTop: 30,
|
||||
pageOffsetLeft: 100,
|
||||
pageOffsetTop: 200,
|
||||
clipOffsetLeft: 138,
|
||||
clipOffsetTop: 98,
|
||||
page: {
|
||||
pageWidth: 321,
|
||||
pageHeight: 300,
|
||||
},
|
||||
})).toEqual({
|
||||
left: 258,
|
||||
top: 328,
|
||||
width: 321,
|
||||
height: 300,
|
||||
});
|
||||
});
|
||||
|
||||
it('clips page-sized header background drawings to the host document page', () => {
|
||||
const headerPage = {
|
||||
pageWidth: 589,
|
||||
pageHeight: 476,
|
||||
};
|
||||
const hostPage = {
|
||||
pageWidth: 816,
|
||||
pageHeight: 1056,
|
||||
};
|
||||
|
||||
expect(getDocsDrawingClipPage({
|
||||
drawing: {
|
||||
behindText: true,
|
||||
transform: {
|
||||
width: 815,
|
||||
height: 1055,
|
||||
},
|
||||
},
|
||||
hostPage,
|
||||
page: headerPage,
|
||||
})).toBe(hostPage);
|
||||
|
||||
expect(getDocsDrawingClipPage({
|
||||
drawing: {
|
||||
behindText: true,
|
||||
transform: {
|
||||
width: 120,
|
||||
height: 40,
|
||||
},
|
||||
},
|
||||
hostPage,
|
||||
page: headerPage,
|
||||
})).toBe(headerPage);
|
||||
});
|
||||
|
||||
it('clips overflowing header cover drawings to the host document page', () => {
|
||||
const headerPage = {
|
||||
pageWidth: 601,
|
||||
pageHeight: 510,
|
||||
};
|
||||
const hostPage = {
|
||||
pageWidth: 794,
|
||||
pageHeight: 1123,
|
||||
};
|
||||
|
||||
expect(getDocsDrawingClipPage({
|
||||
drawing: {
|
||||
behindText: true,
|
||||
transform: {
|
||||
width: 803,
|
||||
height: 682,
|
||||
},
|
||||
},
|
||||
hostPage,
|
||||
page: headerPage,
|
||||
})).toBe(hostPage);
|
||||
});
|
||||
|
||||
it('positions page-sized header backgrounds against the host page width', () => {
|
||||
expect(getDocsPageRelativeDrawingLeft({
|
||||
hostPage: {
|
||||
pageWidth: 816,
|
||||
},
|
||||
positionH: {
|
||||
align: AlignTypeH.RIGHT,
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
},
|
||||
width: 815,
|
||||
})).toBe(1);
|
||||
});
|
||||
|
||||
it('positions page-sized header backgrounds against the host page height', () => {
|
||||
expect(getDocsPageRelativeDrawingTop({
|
||||
hostPage: {
|
||||
pageHeight: 1056,
|
||||
},
|
||||
positionV: {
|
||||
align: AlignTypeV.TOP,
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
},
|
||||
height: 1055,
|
||||
})).toBe(0);
|
||||
|
||||
expect(getDocsPageRelativeDrawingTop({
|
||||
hostPage: {
|
||||
pageHeight: 1056,
|
||||
},
|
||||
positionV: {
|
||||
align: AlignTypeV.BOTTOM,
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
},
|
||||
height: 1055,
|
||||
})).toBe(1);
|
||||
});
|
||||
|
||||
it('uses body pages as page-relative drawing anchors when no host page exists', () => {
|
||||
const bodyPage = {
|
||||
pageWidth: 816,
|
||||
pageHeight: 1056,
|
||||
};
|
||||
|
||||
expect(getDocsPageRelativeDrawingAnchorPage({
|
||||
page: bodyPage,
|
||||
clipPage: bodyPage,
|
||||
})).toBe(bodyPage);
|
||||
});
|
||||
|
||||
it('renders header and footer drawings in the host page behind-text layer', () => {
|
||||
expect(getDocsDrawingBehindText({
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
},
|
||||
hostPage: {
|
||||
pageWidth: 816,
|
||||
pageHeight: 1056,
|
||||
},
|
||||
} as never)).toBe(true);
|
||||
|
||||
expect(getDocsDrawingBehindText({
|
||||
drawingOrigin: {
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
},
|
||||
} as never)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,684 @@
|
||||
/**
|
||||
* 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 { BooleanNumber, DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, ICommandService, IContextService, IImageIoService, Injector, IPermissionService, LocaleService, ObjectRelativeFromH, ObjectRelativeFromV, PermissionService, PositionedObjectLayoutType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, RichTextEditingMutation, setDocumentPermissionValue } from '@univerjs/docs';
|
||||
import { IDocDrawingService, SetDocDrawingArrangeCommand, UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { UnitAction } from '@univerjs/protocol';
|
||||
import { ILocalFileService, IMessageService } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { GroupDocDrawingCommand } from '../../../commands/commands/group-doc-drawing.command';
|
||||
import { UngroupDocDrawingCommand } from '../../../commands/commands/ungroup-doc-drawing.command';
|
||||
import { DocRefreshDrawingsService } from '../../../services/doc-refresh-drawings.service';
|
||||
import { DocDrawingUpdateRenderController } from '../doc-drawing-update.render-controller';
|
||||
import { MobileDocDrawingUpdateRenderController } from '../mobile/doc-drawing-update.render-controller';
|
||||
|
||||
function createController(options: {
|
||||
editArea?: DocumentEditArea;
|
||||
drawings?: Record<string, unknown>;
|
||||
isFocusing?: boolean;
|
||||
mobile?: boolean;
|
||||
openFile?: () => Promise<File[]>;
|
||||
saveImage?: (file: File) => Promise<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 add$ = new Subject<Array<{ unitId: string }>>();
|
||||
const update$ = new Subject<Array<{ unitId: string }>>();
|
||||
const changeEnd$ = new Subject<any>();
|
||||
const editAreaChange$ = new Subject<void>();
|
||||
const refreshDrawings$ = new Subject<unknown>();
|
||||
const onFocus$ = new Subject<unknown>();
|
||||
const onBlur$ = new Subject<unknown>();
|
||||
const commandHandlers: Array<(command: { id: string; params?: Record<string, unknown> }) => void> = [];
|
||||
let editArea = options.editArea ?? DocumentEditArea.BODY;
|
||||
let isFocusing = options.isFocusing ?? true;
|
||||
let focusDrawings: Array<{ unitId: string; subUnitId: string; drawingId: string }> = [];
|
||||
|
||||
focus$.subscribe((drawings) => {
|
||||
focusDrawings = drawings ?? [];
|
||||
});
|
||||
|
||||
const transformer = {
|
||||
changeEnd$,
|
||||
clearControlByIds: vi.fn(),
|
||||
resetProps: vi.fn(),
|
||||
};
|
||||
const shapeByDrawingId = new Map<string, any>();
|
||||
const getShape = (drawingId: string) => {
|
||||
if (!shapeByDrawingId.has(drawingId)) {
|
||||
shapeByDrawingId.set(drawingId, {
|
||||
drawingId,
|
||||
evented: true,
|
||||
oKey: `doc-1#-#doc-1#-#${drawingId}`,
|
||||
setOpacity: vi.fn(),
|
||||
transformerConfig: {},
|
||||
});
|
||||
}
|
||||
return shapeByDrawingId.get(drawingId);
|
||||
};
|
||||
Object.keys(options.drawings ?? {}).forEach(getShape);
|
||||
const scene = {
|
||||
getTransformerByCreate: vi.fn(() => transformer),
|
||||
fuzzyMathObjects: vi.fn((objectKey: string) => {
|
||||
const drawingId = objectKey.split('#-#').at(-1) ?? objectKey;
|
||||
return [getShape(drawingId)];
|
||||
}),
|
||||
getAllObjects: vi.fn(() => [...shapeByDrawingId.values()]),
|
||||
attachTransformerTo: vi.fn(),
|
||||
detachTransformerFrom: vi.fn(),
|
||||
getTransformer: vi.fn(() => transformer),
|
||||
};
|
||||
const viewModel = {
|
||||
editAreaChange$,
|
||||
getEditArea: vi.fn(() => editArea),
|
||||
};
|
||||
const renderUnit = {
|
||||
with: vi.fn(() => ({
|
||||
getViewModel: () => viewModel,
|
||||
})),
|
||||
};
|
||||
const snapshot = {
|
||||
body: {
|
||||
dataStream: 'abcdefghij\r\n',
|
||||
customBlocks: [
|
||||
{ blockId: 'body-drawing', startIndex: 4 },
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
'header-1': {
|
||||
body: {
|
||||
dataStream: 'abcdefghij\r\n',
|
||||
customBlocks: [
|
||||
{ blockId: 'header-drawing', startIndex: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
footers: {
|
||||
'footer-1': {
|
||||
body: {
|
||||
dataStream: 'abcdefghij\r\n',
|
||||
customBlocks: [
|
||||
{ blockId: 'footer-drawing', startIndex: 7 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
drawings: options.drawings ?? {},
|
||||
};
|
||||
const context = {
|
||||
unitId: 'doc-1',
|
||||
unit: {
|
||||
getDrawings: vi.fn(() => snapshot.drawings),
|
||||
getMutationRevision: vi.fn(() => 0),
|
||||
getSnapshot: vi.fn(() => snapshot),
|
||||
getBody: vi.fn(() => snapshot.body),
|
||||
getSelfOrHeaderFooterModel: vi.fn((segmentId: string) => ({
|
||||
getBody: () => segmentId ? snapshot.headers[segmentId as keyof typeof snapshot.headers]?.body ?? snapshot.footers[segmentId as keyof typeof snapshot.footers]?.body : snapshot.body,
|
||||
})),
|
||||
},
|
||||
scene,
|
||||
mainComponent: {
|
||||
getOffsetConfig: () => ({ docsLeft: 12, docsTop: 18 }),
|
||||
},
|
||||
};
|
||||
const commandService = {
|
||||
executeCommand: vi.fn(),
|
||||
onCommandExecuted: vi.fn((handler) => {
|
||||
commandHandlers.push(handler);
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
};
|
||||
const docSelectionManagerService = {
|
||||
getActiveTextRange: vi.fn(() => null),
|
||||
refreshSelection: vi.fn(),
|
||||
replaceDocRanges: vi.fn(),
|
||||
};
|
||||
const renderManagerSrv = {
|
||||
getRenderUnitById: vi.fn(() => renderUnit),
|
||||
};
|
||||
const docDrawingService = {
|
||||
focusDrawing: vi.fn(),
|
||||
getDrawingByParam: vi.fn(({ drawingId }: { drawingId: string }) => options.drawings?.[drawingId]),
|
||||
};
|
||||
const drawingManagerService = {
|
||||
add$,
|
||||
featurePluginUpdate$,
|
||||
featurePluginOrderUpdate$,
|
||||
featurePluginGroupUpdate$,
|
||||
featurePluginUngroupUpdate$,
|
||||
focus$,
|
||||
update$,
|
||||
getDrawingByParam: vi.fn(({ drawingId }: { drawingId: string }) => options.drawings?.[drawingId]),
|
||||
focusDrawing: vi.fn(),
|
||||
getFocusDrawings: vi.fn(() => focusDrawings),
|
||||
};
|
||||
const contextService = {
|
||||
getContextValue: vi.fn(() => false),
|
||||
setContextValue: vi.fn(),
|
||||
};
|
||||
const docSelectionRenderService = {
|
||||
get isFocusing() {
|
||||
return isFocusing;
|
||||
},
|
||||
getActiveTextRange: vi.fn(() => null),
|
||||
getAllTextRanges: vi.fn(() => []),
|
||||
getSegment: vi.fn(() => ''),
|
||||
onBlur$,
|
||||
onFocus$,
|
||||
setSegment: vi.fn(),
|
||||
};
|
||||
const imageIoService = {
|
||||
addImageSourceCache: vi.fn(),
|
||||
saveImage: vi.fn(options.saveImage),
|
||||
};
|
||||
const fileOpenerService = {
|
||||
openFile: vi.fn(options.openFile ?? (async () => [])),
|
||||
};
|
||||
|
||||
const injector = new Injector([
|
||||
[IPermissionService, { useClass: PermissionService }],
|
||||
[ICommandService, { useValue: commandService }],
|
||||
[DocSelectionManagerService, { useValue: docSelectionManagerService }],
|
||||
[IRenderManagerService, { useValue: renderManagerSrv }],
|
||||
[IImageIoService, { useValue: imageIoService }],
|
||||
[IDocDrawingService, { useValue: docDrawingService }],
|
||||
[IDrawingManagerService, { useValue: drawingManagerService }],
|
||||
[IContextService, { useValue: contextService }],
|
||||
[IMessageService, { useValue: { show: vi.fn() } }],
|
||||
[LocaleService],
|
||||
[DocSelectionRenderService, { useValue: docSelectionRenderService }],
|
||||
[DocRefreshDrawingsService, { useValue: { refreshDrawings$ } }],
|
||||
[ILocalFileService, { useValue: fileOpenerService }],
|
||||
]);
|
||||
const permissionService = injector.get(IPermissionService);
|
||||
const controller = injector.createInstance(
|
||||
options.mobile ? MobileDocDrawingUpdateRenderController : DocDrawingUpdateRenderController,
|
||||
context as never
|
||||
);
|
||||
|
||||
return {
|
||||
controller,
|
||||
commandHandlers,
|
||||
commandService,
|
||||
contextService,
|
||||
docDrawingService,
|
||||
docSelectionManagerService,
|
||||
docSelectionRenderService,
|
||||
drawingManagerService,
|
||||
editAreaChange$,
|
||||
featurePluginUpdate$,
|
||||
focus$,
|
||||
getShape: (drawingId: string) => shapeByDrawingId.get(drawingId),
|
||||
onBlur$,
|
||||
onFocus$,
|
||||
permissionService,
|
||||
refreshDrawings$,
|
||||
scene,
|
||||
setEditArea: (value: DocumentEditArea) => {
|
||||
editArea = value;
|
||||
},
|
||||
setIsFocusing: (value: boolean) => {
|
||||
isFocusing = value;
|
||||
},
|
||||
transformer,
|
||||
update$,
|
||||
};
|
||||
}
|
||||
|
||||
describe('DocDrawingUpdateRenderController', () => {
|
||||
it('persists inline image crop data without moving its text anchor', () => {
|
||||
const drawing = {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'body-drawing',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
layoutType: PositionedObjectLayoutType.INLINE,
|
||||
transform: { left: 10, top: 20, width: 100, height: 80 },
|
||||
docTransform: {
|
||||
size: { width: 100, height: 80 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 10 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 20 },
|
||||
angle: 0,
|
||||
},
|
||||
};
|
||||
const { commandService, featurePluginUpdate$ } = createController({ drawings: { 'body-drawing': drawing } });
|
||||
|
||||
featurePluginUpdate$.next([{
|
||||
...drawing,
|
||||
transform: { left: 25, top: 30, width: 70, height: 60 },
|
||||
srcRect: { left: 15, top: 10, right: 15, bottom: 10 },
|
||||
}]);
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawings: [
|
||||
{ drawingId: 'body-drawing', key: 'srcRect', value: { left: 15, top: 10, right: 15, bottom: 10 } },
|
||||
{ drawingId: 'body-drawing', key: 'size', value: { width: 70, height: 60 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('persists floating crop position and ignores unrelated drawing updates', () => {
|
||||
const drawing = {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'body-drawing',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
transform: { left: 10, top: 20, width: 100, height: 80 },
|
||||
docTransform: {
|
||||
size: { width: 100, height: 80 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 40 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 50 },
|
||||
angle: 0,
|
||||
},
|
||||
};
|
||||
const { commandService, featurePluginUpdate$ } = createController({ drawings: { 'body-drawing': drawing } });
|
||||
|
||||
featurePluginUpdate$.next([{ ...drawing, transform: { left: 12, top: 24, width: 90, height: 70 } }]);
|
||||
featurePluginUpdate$.next([{ ...drawing, unitId: 'other-doc', srcRect: null }]);
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalled();
|
||||
|
||||
featurePluginUpdate$.next([{
|
||||
...drawing,
|
||||
transform: { left: 25, top: 26, width: 70, height: 60 },
|
||||
srcRect: { left: 15, top: 6, right: 15, bottom: 14 },
|
||||
}]);
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawings: [
|
||||
{ drawingId: 'body-drawing', key: 'srcRect', value: { left: 15, top: 6, right: 15, bottom: 14 } },
|
||||
{ drawingId: 'body-drawing', key: 'size', value: { width: 70, height: 60 } },
|
||||
{ drawingId: 'body-drawing', key: 'positionH', value: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 55 } },
|
||||
{ drawingId: 'body-drawing', key: 'positionV', value: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 56 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('persists shared crop data for repeated header and footer drawings without moving each occurrence', () => {
|
||||
const drawing = {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'header-drawing',
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
isMultiTransform: BooleanNumber.TRUE,
|
||||
transform: { left: 10, top: 20, width: 100, height: 80 },
|
||||
transforms: [
|
||||
{ left: 10, top: 20, width: 100, height: 80 },
|
||||
{ left: 10, top: 820, width: 100, height: 80 },
|
||||
],
|
||||
docTransform: {
|
||||
size: { width: 100, height: 80 },
|
||||
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 10 },
|
||||
positionV: { relativeFrom: ObjectRelativeFromV.PAGE, posOffset: 20 },
|
||||
angle: 0,
|
||||
},
|
||||
};
|
||||
const { commandService, featurePluginUpdate$ } = createController({ drawings: { 'header-drawing': drawing } });
|
||||
|
||||
featurePluginUpdate$.next([{ ...drawing, srcRect: { left: 10, top: 0, right: 10, bottom: 0 } }]);
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenCalledWith(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawings: [
|
||||
{ drawingId: 'header-drawing', key: 'srcRect', value: { left: 10, top: 0, right: 10, bottom: 0 } },
|
||||
{ drawingId: 'header-drawing', key: 'size', value: { width: 100, height: 80 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards drawing order and group events to doc drawing commands', () => {
|
||||
const { commandService, drawingManagerService } = createController();
|
||||
|
||||
drawingManagerService.featurePluginOrderUpdate$.next({
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingIds: ['drawing-1'],
|
||||
arrangeType: 'forward',
|
||||
});
|
||||
drawingManagerService.featurePluginGroupUpdate$.next({ unitId: 'doc-1', drawingIds: ['a', 'b'] });
|
||||
drawingManagerService.featurePluginUngroupUpdate$.next({ unitId: 'doc-1', drawingIds: ['group-1'] });
|
||||
|
||||
expect(commandService.executeCommand).toHaveBeenNthCalledWith(1, SetDocDrawingArrangeCommand.id, {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingIds: ['drawing-1'],
|
||||
arrangeType: 'forward',
|
||||
});
|
||||
expect(commandService.executeCommand).toHaveBeenNthCalledWith(2, GroupDocDrawingCommand.id, { unitId: 'doc-1', drawingIds: ['a', 'b'] });
|
||||
expect(commandService.executeCommand).toHaveBeenNthCalledWith(3, UngroupDocDrawingCommand.id, { unitId: 'doc-1', drawingIds: ['group-1'] });
|
||||
});
|
||||
|
||||
it('keeps document selection and segment in sync when drawings gain or lose focus', () => {
|
||||
const {
|
||||
contextService,
|
||||
docDrawingService,
|
||||
docSelectionManagerService,
|
||||
docSelectionRenderService,
|
||||
focus$,
|
||||
transformer,
|
||||
} = createController();
|
||||
|
||||
focus$.next([]);
|
||||
expect(contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_COMMON_DRAWINGS, false);
|
||||
expect(docDrawingService.focusDrawing).toHaveBeenCalledWith([]);
|
||||
expect(transformer.resetProps).toHaveBeenLastCalledWith({ zeroTop: 0, zeroLeft: 0 });
|
||||
|
||||
focus$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'body-drawing' }]);
|
||||
expect(contextService.setContextValue).toHaveBeenCalledWith(FOCUSING_COMMON_DRAWINGS, true);
|
||||
expect(docDrawingService.focusDrawing).toHaveBeenLastCalledWith([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'body-drawing' }]);
|
||||
expect(docSelectionManagerService.replaceDocRanges).toHaveBeenCalledWith([{ startOffset: 4, endOffset: 5, segmentId: '' }]);
|
||||
expect(docSelectionRenderService.setSegment).not.toHaveBeenCalled();
|
||||
expect(transformer.resetProps).toHaveBeenLastCalledWith({ zeroTop: 18, zeroLeft: 12 });
|
||||
|
||||
focus$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'header-drawing' }]);
|
||||
expect(docSelectionRenderService.setSegment).toHaveBeenCalledWith('header-1');
|
||||
expect(docSelectionManagerService.replaceDocRanges).toHaveBeenLastCalledWith([{ startOffset: 2, endOffset: 3, segmentId: 'header-1' }]);
|
||||
});
|
||||
|
||||
it('refreshes doc selection after transformer changes finish', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { docSelectionManagerService, transformer } = createController();
|
||||
|
||||
transformer.changeEnd$.next({});
|
||||
await vi.advanceTimersByTimeAsync(30);
|
||||
|
||||
expect(docSelectionManagerService.refreshSelection).toHaveBeenCalledTimes(1);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it.each([false, true])('configures selection-first movement for mobile mode %s', (mobile) => {
|
||||
const { transformer } = createController({ mobile });
|
||||
|
||||
expect(transformer.resetProps).toHaveBeenCalledWith({ moveOnlyWhenSelected: mobile });
|
||||
});
|
||||
|
||||
it('cancels an image insertion when the render controller is disposed while saving', async () => {
|
||||
let finishSaving: (value: unknown) => void = () => {};
|
||||
const saveImage = vi.fn(() => new Promise<unknown>((resolve) => {
|
||||
finishSaving = resolve;
|
||||
}));
|
||||
const { commandService, controller } = createController({
|
||||
openFile: async () => [{} as File],
|
||||
saveImage,
|
||||
});
|
||||
|
||||
const insertion = controller.insertDocImage();
|
||||
await vi.waitFor(() => expect(saveImage).toHaveBeenCalledTimes(1));
|
||||
|
||||
controller.dispose();
|
||||
finishSaving({
|
||||
imageId: 'image-1',
|
||||
imageSourceType: 'URL',
|
||||
source: 'image.png',
|
||||
base64Cache: 'data:image/png;base64,',
|
||||
});
|
||||
|
||||
await expect(insertion).resolves.toBe(false);
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not open the image picker when Document Edit is denied', async () => {
|
||||
const openFile = vi.fn(async () => [{} as File]);
|
||||
const { commandService, controller, permissionService } = createController({ openFile });
|
||||
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
|
||||
|
||||
await expect(controller.insertDocImage()).resolves.toBe(false);
|
||||
|
||||
expect(openFile).not.toHaveBeenCalled();
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not insert an image when Document Edit is revoked during upload', async () => {
|
||||
let finishSaving: (value: unknown) => void = () => {};
|
||||
const saveImage = vi.fn(() => new Promise<unknown>((resolve) => {
|
||||
finishSaving = resolve;
|
||||
}));
|
||||
const { commandService, controller, permissionService } = createController({
|
||||
openFile: async () => [{} as File],
|
||||
saveImage,
|
||||
});
|
||||
|
||||
const insertion = controller.insertDocImage();
|
||||
await vi.waitFor(() => expect(saveImage).toHaveBeenCalledTimes(1));
|
||||
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
|
||||
finishSaving({
|
||||
imageId: 'image-1',
|
||||
imageSourceType: 'URL',
|
||||
source: 'image.png',
|
||||
base64Cache: 'data:image/png;base64,',
|
||||
});
|
||||
|
||||
await expect(insertion).resolves.toBe(false);
|
||||
expect(commandService.executeCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toggles drawing editability between body and header/footer edit areas', () => {
|
||||
const bodyDrawing = {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
};
|
||||
const headerDrawing = {
|
||||
drawingId: 'header-drawing',
|
||||
isMultiTransform: BooleanNumber.TRUE,
|
||||
};
|
||||
const { editAreaChange$, getShape, scene, setEditArea } = createController({
|
||||
drawings: {
|
||||
'body-drawing': bodyDrawing,
|
||||
'header-drawing': headerDrawing,
|
||||
},
|
||||
});
|
||||
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(getShape('body-drawing'));
|
||||
expect(getShape('body-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(getShape('header-drawing').setOpacity).toHaveBeenLastCalledWith(0.5);
|
||||
|
||||
scene.attachTransformerTo.mockClear();
|
||||
setEditArea(DocumentEditArea.HEADER);
|
||||
editAreaChange$.next();
|
||||
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(getShape('header-drawing'));
|
||||
expect(getShape('header-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(getShape('body-drawing').setOpacity).toHaveBeenLastCalledWith(0.5);
|
||||
});
|
||||
|
||||
it('removes read-only drawings from picking and transformer interaction, then restores both', async () => {
|
||||
const { getShape, permissionService, scene, transformer } = createController({
|
||||
drawings: {
|
||||
'body-drawing': {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
|
||||
|
||||
expect(getShape('body-drawing').evented).toBe(false);
|
||||
expect(transformer.clearControlByIds).toHaveBeenCalledWith(['doc-1#-#doc-1#-#body-drawing']);
|
||||
expect(getShape('body-drawing').transformerConfig).toMatchObject({
|
||||
moveEnabled: false,
|
||||
resizeEnabled: false,
|
||||
rotateEnabled: false,
|
||||
});
|
||||
|
||||
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, true);
|
||||
expect(getShape('body-drawing').evented).toBe(true);
|
||||
expect(scene.attachTransformerTo).toHaveBeenLastCalledWith(getShape('body-drawing'));
|
||||
expect(getShape('body-drawing').transformerConfig).not.toHaveProperty('moveEnabled', false);
|
||||
expect(getShape('body-drawing').transformerConfig).not.toHaveProperty('resizeEnabled', false);
|
||||
expect(getShape('body-drawing').transformerConfig).not.toHaveProperty('rotateEnabled', false);
|
||||
});
|
||||
|
||||
it('keeps a remotely updated drawing non-interactive while local permission is read-only', async () => {
|
||||
const { getShape, permissionService, scene, update$ } = createController({
|
||||
drawings: {
|
||||
'body-drawing': {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
});
|
||||
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
|
||||
scene.attachTransformerTo.mockClear();
|
||||
|
||||
update$.next([{ unitId: 'doc-1' }]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(getShape('body-drawing').evented).toBe(false);
|
||||
expect(scene.attachTransformerTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps all drawings opaque while the document input is not focused', () => {
|
||||
const bodyDrawing = {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
};
|
||||
const headerDrawing = {
|
||||
drawingId: 'header-drawing',
|
||||
isMultiTransform: BooleanNumber.TRUE,
|
||||
};
|
||||
const {
|
||||
getShape,
|
||||
onBlur$,
|
||||
onFocus$,
|
||||
scene,
|
||||
setIsFocusing,
|
||||
} = createController({
|
||||
drawings: {
|
||||
'body-drawing': bodyDrawing,
|
||||
'header-drawing': headerDrawing,
|
||||
},
|
||||
isFocusing: false,
|
||||
});
|
||||
|
||||
expect(scene.attachTransformerTo).not.toHaveBeenCalled();
|
||||
expect(getShape('body-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(getShape('header-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
|
||||
setIsFocusing(true);
|
||||
onFocus$.next({});
|
||||
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(getShape('body-drawing'));
|
||||
expect(getShape('body-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(getShape('header-drawing').setOpacity).toHaveBeenLastCalledWith(0.5);
|
||||
|
||||
setIsFocusing(false);
|
||||
onBlur$.next({});
|
||||
|
||||
expect(getShape('body-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(getShape('header-drawing').setOpacity).toHaveBeenLastCalledWith(1);
|
||||
});
|
||||
|
||||
it('indexes scene drawings once and ignores duplicate focus notifications', () => {
|
||||
const { getShape, onFocus$, scene } = createController({
|
||||
drawings: {
|
||||
'body-drawing': {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
},
|
||||
'header-drawing': {
|
||||
drawingId: 'header-drawing',
|
||||
isMultiTransform: BooleanNumber.TRUE,
|
||||
},
|
||||
},
|
||||
isFocusing: true,
|
||||
});
|
||||
|
||||
expect(scene.getAllObjects).toHaveBeenCalledTimes(1);
|
||||
expect(scene.fuzzyMathObjects).not.toHaveBeenCalled();
|
||||
getShape('body-drawing').setOpacity.mockClear();
|
||||
getShape('header-drawing').setOpacity.mockClear();
|
||||
|
||||
onFocus$.next({});
|
||||
|
||||
expect(scene.getAllObjects).toHaveBeenCalledTimes(1);
|
||||
expect(getShape('body-drawing').setOpacity).not.toHaveBeenCalled();
|
||||
expect(getShape('header-drawing').setOpacity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps peer drawings interactive while a drawing owns focus', () => {
|
||||
const {
|
||||
focus$,
|
||||
getShape,
|
||||
scene,
|
||||
} = createController({
|
||||
drawings: {
|
||||
'body-drawing': {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
},
|
||||
'peer-drawing': {
|
||||
drawingId: 'peer-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
isFocusing: false,
|
||||
});
|
||||
|
||||
scene.attachTransformerTo.mockClear();
|
||||
focus$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'body-drawing' }]);
|
||||
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(getShape('body-drawing'));
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(getShape('peer-drawing'));
|
||||
});
|
||||
|
||||
it('ignores rich text mutations from other document units', async () => {
|
||||
const { commandHandlers, getShape, scene } = createController({
|
||||
drawings: {
|
||||
'body-drawing': {
|
||||
drawingId: 'body-drawing',
|
||||
isMultiTransform: BooleanNumber.FALSE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
scene.attachTransformerTo.mockClear();
|
||||
getShape('body-drawing').setOpacity.mockClear();
|
||||
|
||||
commandHandlers.forEach((handler) => handler({
|
||||
id: RichTextEditingMutation.id,
|
||||
params: { unitId: 'other-doc' },
|
||||
}));
|
||||
await Promise.resolve();
|
||||
|
||||
expect(scene.attachTransformerTo).not.toHaveBeenCalled();
|
||||
expect(getShape('body-drawing').setOpacity).not.toHaveBeenCalled();
|
||||
|
||||
commandHandlers.forEach((handler) => handler({
|
||||
id: RichTextEditingMutation.id,
|
||||
params: { unitId: 'doc-1' },
|
||||
}));
|
||||
await Promise.resolve();
|
||||
|
||||
expect(scene.attachTransformerTo).toHaveBeenCalledWith(getShape('body-drawing'));
|
||||
expect(getShape('body-drawing').setOpacity).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDrawingParam } from '@univerjs/core';
|
||||
import type { IDocDrawing, IDrawingDocTransform } from '@univerjs/docs-drawing';
|
||||
import type { IImageData } from '@univerjs/drawing';
|
||||
import { BooleanNumber, DrawingTypeEnum, PositionedObjectLayoutType } from '@univerjs/core';
|
||||
|
||||
export function getDocImageCropUpdates(
|
||||
param: IDrawingParam & Partial<IImageData>,
|
||||
docDrawing: IDocDrawing | undefined,
|
||||
renderDrawing: IDrawingParam | undefined | null
|
||||
): IDrawingDocTransform[] {
|
||||
const { drawingId, transform } = param;
|
||||
if (docDrawing?.drawingType !== DrawingTypeEnum.DRAWING_IMAGE || renderDrawing?.transform == null) {
|
||||
return [];
|
||||
}
|
||||
const shapeKeys: Array<'prstGeom' | 'adjustValues'> = ['prstGeom', 'adjustValues'];
|
||||
const drawings: IDrawingDocTransform[] = shapeKeys
|
||||
.filter((key) => Object.prototype.hasOwnProperty.call(param, key))
|
||||
.map((key) => ({ drawingId, key, value: param[key] ?? null }));
|
||||
// Transformer updates without crop data already persist size through the transformer controller.
|
||||
if (transform == null || param.srcRect === undefined) {
|
||||
return drawings;
|
||||
}
|
||||
const nextTransform = { ...renderDrawing.transform, ...transform };
|
||||
drawings.push({ drawingId, key: 'srcRect', value: param.srcRect });
|
||||
|
||||
if (nextTransform.width != null && nextTransform.height != null) {
|
||||
drawings.push({
|
||||
drawingId,
|
||||
key: 'size',
|
||||
value: { width: nextTransform.width, height: nextTransform.height },
|
||||
});
|
||||
}
|
||||
|
||||
if (docDrawing.layoutType === PositionedObjectLayoutType.INLINE || renderDrawing.isMultiTransform === BooleanNumber.TRUE) {
|
||||
return drawings;
|
||||
}
|
||||
|
||||
const horizontalDelta = (nextTransform.left ?? 0) - (renderDrawing.transform.left ?? 0);
|
||||
const verticalDelta = (nextTransform.top ?? 0) - (renderDrawing.transform.top ?? 0);
|
||||
const { positionH, positionV } = docDrawing.docTransform;
|
||||
|
||||
if (horizontalDelta !== 0 && positionH.posOffset != null) {
|
||||
drawings.push({
|
||||
drawingId,
|
||||
key: 'positionH',
|
||||
value: { relativeFrom: positionH.relativeFrom, posOffset: positionH.posOffset + horizontalDelta },
|
||||
});
|
||||
}
|
||||
if (verticalDelta !== 0 && positionV.posOffset != null) {
|
||||
drawings.push({
|
||||
drawingId,
|
||||
key: 'positionV',
|
||||
value: { relativeFrom: positionV.relativeFrom, posOffset: positionV.posOffset + verticalDelta },
|
||||
});
|
||||
}
|
||||
|
||||
return drawings;
|
||||
}
|
||||
@@ -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 type { JSONXActions } from '@univerjs/core';
|
||||
import { JSON1 } from '@univerjs/core';
|
||||
|
||||
export type DocMutationAffectedDrawingIds = ReadonlySet<string> | null;
|
||||
|
||||
/**
|
||||
* Returns the drawing ids that can be refreshed independently. `null` means
|
||||
* the mutation is drawing-related but needs a conservative full refresh.
|
||||
*/
|
||||
export function getDocMutationAffectedDrawingIds(actions: JSONXActions): DocMutationAffectedDrawingIds {
|
||||
if (!Array.isArray(actions) || actions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawingIds = new Set<string>();
|
||||
let sawComponent = false;
|
||||
let needsFullRefresh = false;
|
||||
try {
|
||||
const cursor = JSON1.type.readCursor(actions);
|
||||
cursor.traverse(null, () => {
|
||||
sawComponent = true;
|
||||
const path = cursor.getPath();
|
||||
const root = String(path[0]);
|
||||
const drawingIndex = root === 'notes' ? 2 : 0;
|
||||
if (path[drawingIndex] === 'drawings') {
|
||||
const drawingId = path[drawingIndex + 1];
|
||||
if (typeof drawingId === 'string' && drawingId.length > 0) {
|
||||
drawingIds.add(drawingId);
|
||||
} else {
|
||||
needsFullRefresh = true;
|
||||
}
|
||||
} else if (path[drawingIndex] === 'drawingsOrder' || (root === 'notes' && path.length <= 2) || path.includes('customBlocks')) {
|
||||
needsFullRefresh = true;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return !sawComponent || needsFullRefresh ? null : drawingIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drawing editability and immediate overlay transforms only need refreshing
|
||||
* when a rich-text mutation changes drawing metadata or custom-block records.
|
||||
* Text, table, list, and document-style mutations are repainted from the next
|
||||
* document-layout publication instead of scanning every drawing eagerly.
|
||||
*/
|
||||
export function doesDocMutationAffectDrawingPresentation(actions: JSONXActions): boolean {
|
||||
const drawingIds = getDocMutationAffectedDrawingIds(actions);
|
||||
return drawingIds == null || drawingIds.size > 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
/**
|
||||
* 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, ICommandInfo, IDocDrawingPosition, IDrawingParam, IImageIoServiceParam, ITextRangeParam, Nullable } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { IDocDrawing, IDrawingDocTransform, IInsertDocDrawingCommandParams, ISetDocDrawingArrangeCommandParams, IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
|
||||
import type { BaseObject, Documents, Image, IRenderContext, IRenderModule, ITransformerConfig } from '@univerjs/engine-render';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import {
|
||||
BooleanNumber,
|
||||
Disposable,
|
||||
DrawingTypeEnum,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
IImageIoService,
|
||||
ImageUploadStatusType,
|
||||
Inject,
|
||||
IPermissionService,
|
||||
LocaleService,
|
||||
PositionedObjectLayoutType,
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import {
|
||||
buildDocTransform,
|
||||
canEditDocumentTargets,
|
||||
docDrawingPositionToTransform,
|
||||
DocSelectionManagerService,
|
||||
DocSkeletonManagerService,
|
||||
getDocumentDrawingSegmentId,
|
||||
getDocumentEditTargetObjectIds,
|
||||
getDocumentEntityParentPermissionObjectIds,
|
||||
getDocumentEntityPermissionObjectId,
|
||||
RichTextEditingMutation,
|
||||
} from '@univerjs/docs';
|
||||
import { collectDocDrawings, findDocDrawing, IDocDrawingService, InsertDocDrawingCommand, SetDocDrawingArrangeCommand, UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import {
|
||||
DRAWING_IMAGE_ALLOW_IMAGE_LIST,
|
||||
DRAWING_IMAGE_COUNT_LIMIT,
|
||||
DRAWING_IMAGE_HEIGHT_LIMIT,
|
||||
DRAWING_IMAGE_WIDTH_LIMIT,
|
||||
getDrawingImageAllowSize,
|
||||
getDrawingShapeKeyByDrawingSearch,
|
||||
getImageSize,
|
||||
IDrawingManagerService,
|
||||
} from '@univerjs/drawing';
|
||||
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ILocalFileService, IMessageService } from '@univerjs/ui';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { GroupDocDrawingCommand } from '../../commands/commands/group-doc-drawing.command';
|
||||
import { UngroupDocDrawingCommand } from '../../commands/commands/ungroup-doc-drawing.command';
|
||||
import { DocRefreshDrawingsService } from '../../services/doc-refresh-drawings.service';
|
||||
import { getDocImageCropUpdates } from './doc-drawing-crop';
|
||||
import { getDocMutationAffectedDrawingIds } from './doc-drawing-mutation';
|
||||
|
||||
interface IImageInsertPosition {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export class DocDrawingUpdateRenderController extends Disposable implements IRenderModule {
|
||||
private readonly _editableTransformerConfigs = new WeakMap<BaseObject, ITransformerConfig>();
|
||||
private readonly _editableEventedStates = new WeakMap<BaseObject, boolean>();
|
||||
private _editStatusUpdateScheduled = false;
|
||||
private _pendingEditStatusDrawingIds: Set<string> | null | undefined;
|
||||
private _lastEditStatusContextKey: string | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly _context: IRenderContext<DocumentDataModel>,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(DocSelectionManagerService) private readonly _docSelectionManagerService: DocSelectionManagerService,
|
||||
@IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService,
|
||||
@IImageIoService private readonly _imageIoService: IImageIoService,
|
||||
@IDocDrawingService private readonly _docDrawingService: IDocDrawingService,
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@IPermissionService private readonly _permissionService: IPermissionService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IMessageService private readonly _messageService: IMessageService,
|
||||
@Inject(LocaleService) private readonly _localeService: LocaleService,
|
||||
@Inject(DocSelectionRenderService) private readonly _docSelectionRenderService: DocSelectionRenderService,
|
||||
@Inject(DocRefreshDrawingsService) private readonly _docRefreshDrawingsService: DocRefreshDrawingsService,
|
||||
@ILocalFileService private readonly _fileOpenerService: ILocalFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._updateOrderListener();
|
||||
this._updateImageCropListener();
|
||||
this._groupDrawingListener();
|
||||
this._focusDrawingListener();
|
||||
this._transformDrawingListener();
|
||||
this._editAreaChangeListener();
|
||||
const scheduleEditStatusUpdate = (drawings: Array<{ unitId: string; drawingId: string }>) => {
|
||||
const relevantDrawings = drawings.filter((drawing) => drawing.unitId === this._context?.unitId);
|
||||
if (relevantDrawings.length > 0) {
|
||||
const drawingIds = relevantDrawings
|
||||
.map((drawing) => drawing.drawingId)
|
||||
.filter((drawingId): drawingId is string => typeof drawingId === 'string' && drawingId.length > 0);
|
||||
this._scheduleDrawingsEditStatusUpdate(
|
||||
drawingIds.length === relevantDrawings.length ? new Set(drawingIds) : undefined
|
||||
);
|
||||
}
|
||||
};
|
||||
this.disposeWithMe(this._drawingManagerService.add$.subscribe(scheduleEditStatusUpdate));
|
||||
this.disposeWithMe(this._drawingManagerService.update$.subscribe(scheduleEditStatusUpdate));
|
||||
this.disposeWithMe(this._permissionService.permissionPointUpdate$.subscribe(() => {
|
||||
this._updateDrawingsEditStatus(undefined, true);
|
||||
}));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
// @ts-ignore
|
||||
delete this._context;
|
||||
}
|
||||
|
||||
async insertDocImage(): Promise<boolean> {
|
||||
const insertPosition = this._getCurrentImageInsertPosition();
|
||||
const textRange = this._getCurrentImageInsertTextRange();
|
||||
if (!this._canInsertDocImage(textRange)) {
|
||||
return false;
|
||||
}
|
||||
const files = await this._fileOpenerService.openFile({
|
||||
multiple: true,
|
||||
accept: DRAWING_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','),
|
||||
});
|
||||
|
||||
if (this._disposed || !this._canInsertDocImage(textRange)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileLength = files.length;
|
||||
if (fileLength > DRAWING_IMAGE_COUNT_LIMIT) {
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content: this._localeService.t<LocaleKey>('docs-drawing-ui.update-status.exceedMaxCount', String(DRAWING_IMAGE_COUNT_LIMIT)),
|
||||
});
|
||||
return false;
|
||||
} else if (fileLength === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this._insertFloatImages(files, insertPosition, textRange);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private async _insertFloatImages(
|
||||
files: File[],
|
||||
insertPosition: Nullable<IImageInsertPosition>,
|
||||
textRange: Nullable<ITextRangeParam>
|
||||
) {
|
||||
let imageParams: Nullable<IImageIoServiceParam>[] = [];
|
||||
|
||||
try {
|
||||
imageParams = await Promise.all(files.map((file) => this._imageIoService.saveImage(file)));
|
||||
} catch (error) {
|
||||
const type = (error as Error).message;
|
||||
let content = '';
|
||||
|
||||
switch (type) {
|
||||
case ImageUploadStatusType.ERROR_EXCEED_SIZE:
|
||||
content = this._localeService.t<LocaleKey>('docs-drawing-ui.update-status.exceedMaxSize', String(getDrawingImageAllowSize() / (1024 * 1024)));
|
||||
break;
|
||||
case ImageUploadStatusType.ERROR_IMAGE_TYPE:
|
||||
content = this._localeService.t<LocaleKey>('docs-drawing-ui.update-status.invalidImageType');
|
||||
break;
|
||||
case ImageUploadStatusType.ERROR_IMAGE:
|
||||
content = this._localeService.t<LocaleKey>('docs-drawing-ui.update-status.invalidImage');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this._messageService.show({
|
||||
type: MessageType.Error,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._disposed || imageParams.length === 0 || !this._canInsertDocImage(textRange)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { unitId } = this._context;
|
||||
const docDrawingParams: IDocDrawing[] = [];
|
||||
|
||||
for (const imageParam of imageParams) {
|
||||
if (imageParam == null) {
|
||||
continue;
|
||||
}
|
||||
const { imageId, imageSourceType, source, base64Cache } = imageParam;
|
||||
const { width, height, image } = await getImageSize(base64Cache || '');
|
||||
|
||||
if (this._disposed || !this._canInsertDocImage(textRange)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._imageIoService.addImageSourceCache(imageId, 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.min(scaleWidth, scaleHeight);
|
||||
}
|
||||
|
||||
const imagePosition = insertPosition ?? this._getCurrentImageInsertPosition();
|
||||
const docTransform = this._getImagePosition(width * scale, height * scale, imagePosition);
|
||||
|
||||
if (docTransform == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transform = docDrawingPositionToTransform(docTransform);
|
||||
if (transform != null && imagePosition != null) {
|
||||
transform.top = imagePosition.top;
|
||||
}
|
||||
|
||||
const docDrawingParam: IDocDrawing = {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId: imageId,
|
||||
drawingType: DrawingTypeEnum.DRAWING_IMAGE,
|
||||
imageSourceType,
|
||||
source,
|
||||
transform,
|
||||
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 isInHeaderFooter = this._isInsertInHeaderFooter();
|
||||
|
||||
if (isInHeaderFooter) {
|
||||
docDrawingParam.isMultiTransform = BooleanNumber.TRUE;
|
||||
docDrawingParam.transforms = docDrawingParam.transform ? [docDrawingParam.transform] : null;
|
||||
}
|
||||
|
||||
docDrawingParams.push(docDrawingParam);
|
||||
}
|
||||
|
||||
return await this._commandService.executeCommand<IInsertDocDrawingCommandParams>(InsertDocDrawingCommand.id, {
|
||||
unitId,
|
||||
drawings: docDrawingParams,
|
||||
textRange: textRange ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _isInsertInHeaderFooter() {
|
||||
const { unitId } = this._context;
|
||||
const viewModel = this._renderManagerSrv.getRenderUnitById(unitId)
|
||||
?.with(DocSkeletonManagerService)
|
||||
.getViewModel();
|
||||
|
||||
const editArea = viewModel?.getEditArea();
|
||||
|
||||
return editArea === DocumentEditArea.HEADER || editArea === DocumentEditArea.FOOTER;
|
||||
}
|
||||
|
||||
private _getImagePosition(
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
insertPosition?: Nullable<IImageInsertPosition>
|
||||
): Nullable<IDocDrawingPosition> {
|
||||
// TODO: NO need to get the cursor position, because the insert image is inline.
|
||||
const position = insertPosition ?? this._getCurrentImageInsertPosition() ?? {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
|
||||
return buildDocTransform(imageWidth, imageHeight, {
|
||||
left: position.left,
|
||||
});
|
||||
}
|
||||
|
||||
private _getCurrentImageInsertPosition(): Nullable<IImageInsertPosition> {
|
||||
const position = this._docSelectionRenderService.getActiveTextRange()?.getAbsolutePosition();
|
||||
|
||||
if (position == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
left: position.left,
|
||||
top: position.top,
|
||||
};
|
||||
}
|
||||
|
||||
private _getCurrentImageInsertTextRange(): Nullable<ITextRangeParam> {
|
||||
return this._docSelectionRenderService.getAllTextRanges().find((range) => range.isActive)
|
||||
?? this._docSelectionManagerService.getActiveTextRange();
|
||||
}
|
||||
|
||||
private _updateOrderListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.featurePluginOrderUpdate$.subscribe((params) => {
|
||||
const { unitId, subUnitId, drawingIds, arrangeType } = params;
|
||||
|
||||
this._commandService.executeCommand<ISetDocDrawingArrangeCommandParams>(SetDocDrawingArrangeCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingIds,
|
||||
arrangeType,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateImageCropListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.featurePluginUpdate$.subscribe((params) => {
|
||||
const drawings = params.flatMap((param) => this._getImageCropUpdates(param));
|
||||
|
||||
if (drawings.length > 0) {
|
||||
const { unitId } = this._context;
|
||||
this._commandService.executeCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawings,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _getImageCropUpdates(param: IDrawingParam): IDrawingDocTransform[] {
|
||||
const { unitId, subUnitId, drawingId } = param;
|
||||
if ([unitId, subUnitId].some((id) => id !== this._context.unitId)) {
|
||||
return [];
|
||||
}
|
||||
return getDocImageCropUpdates(
|
||||
param,
|
||||
this._getDocDrawing(drawingId),
|
||||
this._drawingManagerService.getDrawingByParam({ unitId, subUnitId, drawingId }) ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _getDocDrawing(drawingId: string): IDocDrawing | undefined {
|
||||
return findDocDrawing(this._context.unit.getSnapshot(), drawingId)?.drawing as IDocDrawing | undefined;
|
||||
}
|
||||
|
||||
private _groupDrawingListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.featurePluginGroupUpdate$.subscribe((params) => {
|
||||
this._commandService.executeCommand(GroupDocDrawingCommand.id, params);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.featurePluginUngroupUpdate$.subscribe((params) => {
|
||||
this._commandService.executeCommand(UngroupDocDrawingCommand.id, params);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _getCurrentSceneAndTransformer() {
|
||||
const { scene, mainComponent } = this._context;
|
||||
|
||||
if (scene == null || mainComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const { docsLeft, docsTop } = (mainComponent as Documents).getOffsetConfig();
|
||||
|
||||
return { scene, transformer, docsLeft, docsTop };
|
||||
}
|
||||
|
||||
protected _getTransformerInteractionOptions(): ITransformerConfig {
|
||||
return { moveOnlyWhenSelected: false };
|
||||
}
|
||||
|
||||
protected _isDocumentInteractionFocusing(unitId: string): boolean {
|
||||
return this._docSelectionRenderService.isFocusing
|
||||
|| this._drawingManagerService.getFocusDrawings().some((drawing) => drawing.unitId === unitId);
|
||||
}
|
||||
|
||||
private _transformDrawingListener() {
|
||||
const res = this._getCurrentSceneAndTransformer();
|
||||
if (res && res.transformer) {
|
||||
res.transformer.resetProps(this._getTransformerInteractionOptions());
|
||||
this.disposeWithMe(res.transformer.changeEnd$.pipe(debounceTime(30)).subscribe(() => {
|
||||
this._docSelectionManagerService.refreshSelection();
|
||||
}));
|
||||
} else {
|
||||
throw new Error('transformer is not init');
|
||||
}
|
||||
}
|
||||
|
||||
private _focusDrawingListener() {
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.focus$.subscribe((params) => {
|
||||
const { transformer, docsLeft, docsTop } = this._getCurrentSceneAndTransformer() ?? {};
|
||||
if (params == null || params.length === 0) {
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
this._docDrawingService.focusDrawing([]);
|
||||
|
||||
if (transformer) {
|
||||
transformer.resetProps({
|
||||
zeroTop: 0,
|
||||
zeroLeft: 0,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, true);
|
||||
this._docDrawingService.focusDrawing(params);
|
||||
this._setDrawingSelections(params);
|
||||
const prevSegmentId = this._docSelectionRenderService.getSegment();
|
||||
const segmentId = getDocumentDrawingSegmentId(this._context.unit, params[0].drawingId);
|
||||
|
||||
// Change segmentId when click drawing in different segment.
|
||||
if (prevSegmentId !== segmentId) {
|
||||
this._docSelectionRenderService.setSegment(segmentId);
|
||||
}
|
||||
|
||||
if (transformer) {
|
||||
transformer.resetProps({
|
||||
zeroTop: docsTop,
|
||||
zeroLeft: docsLeft,
|
||||
});
|
||||
}
|
||||
}
|
||||
this._updateDrawingsEditStatus();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Update drawings edit status and opacity. You can not edit header footer images when you are editing body. and vice verse.
|
||||
private _updateDrawingsEditStatus(drawingIds?: ReadonlySet<string>, force = false) {
|
||||
if (!this._context) return;
|
||||
|
||||
const { unit: docDataModel, scene, unitId } = this._context;
|
||||
const viewModel = this._renderManagerSrv
|
||||
.getRenderUnitById(unitId)
|
||||
?.with(DocSkeletonManagerService)
|
||||
.getViewModel();
|
||||
|
||||
if (viewModel == null || docDataModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = docDataModel.getSnapshot();
|
||||
const { drawings } = collectDocDrawings(snapshot);
|
||||
const isEditBody = viewModel.getEditArea() === DocumentEditArea.BODY;
|
||||
const isDocInteractionFocusing = this._isDocumentInteractionFocusing(unitId);
|
||||
const contextKey = `${viewModel.getEditArea()}:${isDocInteractionFocusing}`;
|
||||
const effectiveDrawingIds = drawingIds != null && contextKey === this._lastEditStatusContextKey
|
||||
? drawingIds
|
||||
: undefined;
|
||||
if (effectiveDrawingIds == null) {
|
||||
if (!force && contextKey === this._lastEditStatusContextKey) {
|
||||
return;
|
||||
}
|
||||
this._lastEditStatusContextKey = contextKey;
|
||||
}
|
||||
const readOnlyDrawingIds = new Set<string>();
|
||||
const drawingShapesById = this._indexDrawingShapes(scene.getAllObjects(), unitId);
|
||||
|
||||
for (const key of Object.keys(drawings)) {
|
||||
const drawing = drawings[key];
|
||||
if (effectiveDrawingIds != null && !effectiveDrawingIds.has(drawing.drawingId)) {
|
||||
continue;
|
||||
}
|
||||
const segmentId = getDocumentDrawingSegmentId(docDataModel, drawing.drawingId);
|
||||
const editable = canEditDocumentTargets(this._permissionService, unitId, [
|
||||
...getDocumentEntityParentPermissionObjectIds(docDataModel, segmentId, 'drawing', drawing.drawingId),
|
||||
getDocumentEntityPermissionObjectId(segmentId, 'drawing', drawing.drawingId),
|
||||
]);
|
||||
this._recordReadOnlyDrawing(readOnlyDrawingIds, drawing.drawingId, editable);
|
||||
const drawingShapes = drawingShapesById.get(drawing.drawingId) ?? [];
|
||||
|
||||
if (drawingShapes.length) {
|
||||
for (const shape of drawingShapes) {
|
||||
scene.detachTransformerFrom(shape);
|
||||
this._setTransformerEditable(shape, editable);
|
||||
try {
|
||||
(shape as Image).setOpacity(isDocInteractionFocusing ? 0.5 : 1);
|
||||
} catch {
|
||||
}
|
||||
if (!isDocInteractionFocusing) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(isEditBody && drawing.isMultiTransform !== BooleanNumber.TRUE)
|
||||
|| (!isEditBody && drawing.isMultiTransform === BooleanNumber.TRUE)
|
||||
) {
|
||||
this._attachTransformerIfEditable(shape, editable, drawing.allowTransform);
|
||||
|
||||
try {
|
||||
(shape as Image).setOpacity(1);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._clearReadOnlyDrawingFocus(unitId, readOnlyDrawingIds);
|
||||
}
|
||||
|
||||
private _indexDrawingShapes(shapes: BaseObject[], unitId: string): Map<string, BaseObject[]> {
|
||||
const prefix = getDrawingShapeKeyByDrawingSearch({ unitId, subUnitId: unitId, drawingId: '' });
|
||||
const result = new Map<string, BaseObject[]>();
|
||||
for (const shape of shapes) {
|
||||
if (!shape.oKey.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const drawingId = shape.oKey.slice(prefix.length).split('#-#', 1)[0];
|
||||
if (!drawingId) {
|
||||
continue;
|
||||
}
|
||||
const drawingShapes = result.get(drawingId) ?? [];
|
||||
drawingShapes.push(shape);
|
||||
result.set(drawingId, drawingShapes);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _recordReadOnlyDrawing(readOnlyDrawingIds: Set<string>, drawingId: string, editable: boolean): void {
|
||||
if (!editable) {
|
||||
readOnlyDrawingIds.add(drawingId);
|
||||
}
|
||||
}
|
||||
|
||||
private _attachTransformerIfEditable(shape: BaseObject, editable: boolean, allowTransform?: boolean): void {
|
||||
if (editable && allowTransform !== false) {
|
||||
this._context.scene.attachTransformerTo(shape);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearReadOnlyDrawingFocus(unitId: string, readOnlyDrawingIds: Set<string>): void {
|
||||
if (readOnlyDrawingIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
const focusedDrawings = this._drawingManagerService.getFocusDrawings();
|
||||
const editableFocus = focusedDrawings.filter((drawing) =>
|
||||
drawing.unitId !== unitId || !readOnlyDrawingIds.has(drawing.drawingId));
|
||||
if (editableFocus.length !== focusedDrawings.length) {
|
||||
this._drawingManagerService.focusDrawing(editableFocus);
|
||||
}
|
||||
}
|
||||
|
||||
private _setTransformerEditable(shape: BaseObject, editable: boolean): void {
|
||||
if (editable) {
|
||||
const config = this._editableTransformerConfigs.get(shape);
|
||||
if (config) {
|
||||
shape.transformerConfig = config;
|
||||
this._editableTransformerConfigs.delete(shape);
|
||||
}
|
||||
const evented = this._editableEventedStates.get(shape);
|
||||
if (evented !== undefined) {
|
||||
shape.evented = evented;
|
||||
this._editableEventedStates.delete(shape);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._editableTransformerConfigs.has(shape)) {
|
||||
this._editableTransformerConfigs.set(shape, shape.transformerConfig);
|
||||
}
|
||||
if (!this._editableEventedStates.has(shape)) {
|
||||
this._editableEventedStates.set(shape, shape.evented);
|
||||
}
|
||||
shape.evented = false;
|
||||
this._context.scene.getTransformer()?.clearControlByIds([shape.oKey]);
|
||||
shape.transformerConfig = {
|
||||
...shape.transformerConfig,
|
||||
moveEnabled: false,
|
||||
resizeEnabled: false,
|
||||
rotateEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
private _scheduleDrawingsEditStatusUpdate(drawingIds?: ReadonlySet<string>): void {
|
||||
if (drawingIds == null) {
|
||||
this._pendingEditStatusDrawingIds = null;
|
||||
this._lastEditStatusContextKey = null;
|
||||
} else if (this._pendingEditStatusDrawingIds !== null) {
|
||||
this._pendingEditStatusDrawingIds ??= new Set<string>();
|
||||
drawingIds.forEach((drawingId) => this._pendingEditStatusDrawingIds?.add(drawingId));
|
||||
}
|
||||
if (this._editStatusUpdateScheduled) {
|
||||
return;
|
||||
}
|
||||
this._editStatusUpdateScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
this._editStatusUpdateScheduled = false;
|
||||
if (!this._disposed) {
|
||||
const pendingDrawingIds = this._pendingEditStatusDrawingIds;
|
||||
this._pendingEditStatusDrawingIds = undefined;
|
||||
this._updateDrawingsEditStatus(pendingDrawingIds ?? undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _canInsertDocImage(textRange: Nullable<ITextRangeParam>): boolean {
|
||||
const { unit, unitId } = this._context;
|
||||
const objectIds = textRange?.startOffset == null || textRange.endOffset == null
|
||||
? []
|
||||
: getDocumentEditTargetObjectIds(unit, textRange.segmentId ?? '', textRange);
|
||||
return canEditDocumentTargets(this._permissionService, unitId, objectIds);
|
||||
}
|
||||
|
||||
private _editAreaChangeListener() {
|
||||
const { unitId } = this._context;
|
||||
const viewModel = this._renderManagerSrv
|
||||
.getRenderUnitById(unitId)
|
||||
?.with(DocSkeletonManagerService)
|
||||
.getViewModel();
|
||||
|
||||
if (viewModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateDrawingsEditStatus();
|
||||
|
||||
this.disposeWithMe(
|
||||
viewModel.editAreaChange$.subscribe(() => {
|
||||
this._updateDrawingsEditStatus();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._docSelectionRenderService.onFocus$.subscribe(() => {
|
||||
this._updateDrawingsEditStatus();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._docSelectionRenderService.onBlur$.subscribe(() => {
|
||||
this._updateDrawingsEditStatus();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._docRefreshDrawingsService.refreshDrawings$.subscribe((skeleton) => {
|
||||
if (skeleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To wait the image is rendered.
|
||||
queueMicrotask(() => {
|
||||
this._updateDrawingsEditStatus(undefined, true);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted(async (command: ICommandInfo) => {
|
||||
if (command.id !== RichTextEditingMutation.id) {
|
||||
return;
|
||||
}
|
||||
const params = command.params as Partial<IRichTextEditingMutationParams> | undefined;
|
||||
if (params?.unitId && params.unitId !== this._context.unitId) {
|
||||
return;
|
||||
}
|
||||
const drawingIds = getDocMutationAffectedDrawingIds(params?.actions ?? null);
|
||||
if (drawingIds?.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To wait the image is rendered.
|
||||
this._scheduleDrawingsEditStatusUpdate(drawingIds ?? undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _setDrawingSelections(params: IDrawingParam[]) {
|
||||
const { unit } = this._context;
|
||||
const ranges = params.flatMap((item) => {
|
||||
const segmentId = getDocumentDrawingSegmentId(unit, item.drawingId);
|
||||
const body = unit.getSelfOrHeaderFooterModel(segmentId)?.getBody();
|
||||
const block = body?.customBlocks?.find((block) => block.blockId === item.drawingId);
|
||||
return block ? [{ startOffset: block.startIndex, endOffset: block.startIndex + 1, segmentId }] : [];
|
||||
});
|
||||
this._docSelectionManagerService.replaceDocRanges(ranges);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DocDrawingUpdateRenderController } from '../doc-drawing-update.render-controller';
|
||||
|
||||
export class MobileDocDrawingUpdateRenderController extends DocDrawingUpdateRenderController {
|
||||
protected override _getTransformerInteractionOptions() {
|
||||
return { moveOnlyWhenSelected: true };
|
||||
}
|
||||
|
||||
protected override _isDocumentInteractionFocusing(_unitId: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { Direction, FOCUSING_COMMON_DRAWINGS, FOCUSING_DOC, FOCUSING_UNIVER_EDITOR } from '@univerjs/core';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { DeleteDocDrawingsCommand } from '../../../commands/commands/delete-doc-drawing.command';
|
||||
import { MoveDocDrawingsCommand } from '../../../commands/commands/move-drawings.command';
|
||||
import {
|
||||
DeleteDrawingsShortcutItem,
|
||||
MoveDrawingDownShortcutItem,
|
||||
MoveDrawingLeftShortcutItem,
|
||||
MoveDrawingRightShortcutItem,
|
||||
MoveDrawingUpShortcutItem,
|
||||
whenDocDrawingFocused,
|
||||
} from '../drawing.shortcut';
|
||||
|
||||
function createContext(values: Record<string, boolean>) {
|
||||
return {
|
||||
getContextValue: vi.fn((key: string) => values[key]),
|
||||
};
|
||||
}
|
||||
|
||||
describe('doc drawing shortcuts', () => {
|
||||
it('enables drawing shortcuts only when doc editor and common drawing focus are both active', () => {
|
||||
expect(whenDocDrawingFocused(createContext({
|
||||
[FOCUSING_DOC]: true,
|
||||
[FOCUSING_UNIVER_EDITOR]: true,
|
||||
[FOCUSING_COMMON_DRAWINGS]: true,
|
||||
}) as never)).toBe(true);
|
||||
|
||||
expect(whenDocDrawingFocused(createContext({
|
||||
[FOCUSING_DOC]: true,
|
||||
[FOCUSING_UNIVER_EDITOR]: true,
|
||||
[FOCUSING_COMMON_DRAWINGS]: false,
|
||||
}) as never)).toBe(false);
|
||||
});
|
||||
|
||||
it('binds arrow keys to one-pixel drawing move commands', () => {
|
||||
expect([
|
||||
MoveDrawingDownShortcutItem,
|
||||
MoveDrawingUpShortcutItem,
|
||||
MoveDrawingLeftShortcutItem,
|
||||
MoveDrawingRightShortcutItem,
|
||||
]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
binding: KeyCode.ARROW_DOWN,
|
||||
staticParameters: { direction: Direction.DOWN },
|
||||
priority: 100,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
binding: KeyCode.ARROW_UP,
|
||||
staticParameters: { direction: Direction.UP },
|
||||
priority: 100,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
binding: KeyCode.ARROW_LEFT,
|
||||
staticParameters: { direction: Direction.LEFT },
|
||||
priority: 100,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
binding: KeyCode.ARROW_RIGHT,
|
||||
staticParameters: { direction: Direction.RIGHT },
|
||||
priority: 100,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('binds delete and backspace to drawing deletion with the same focus guard', () => {
|
||||
expect(DeleteDrawingsShortcutItem).toEqual(expect.objectContaining({
|
||||
id: DeleteDocDrawingsCommand.id,
|
||||
binding: KeyCode.DELETE,
|
||||
mac: KeyCode.BACKSPACE,
|
||||
preconditions: whenDocDrawingFocused,
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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_DOC, FOCUSING_UNIVER_EDITOR } from '@univerjs/core';
|
||||
import { KeyCode } from '@univerjs/ui';
|
||||
import { DeleteDocDrawingsCommand } from '../../commands/commands/delete-doc-drawing.command';
|
||||
import { MoveDocDrawingsCommand } from '../../commands/commands/move-drawings.command';
|
||||
|
||||
export function whenDocDrawingFocused(contextService: IContextService): boolean {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_DOC) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR) &&
|
||||
contextService.getContextValue(FOCUSING_COMMON_DRAWINGS)
|
||||
);
|
||||
}
|
||||
|
||||
export const MoveDrawingDownShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
description: 'docs-drawing-ui.shortcut.drawing-move-down',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'docs-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_DOWN,
|
||||
priority: 100,
|
||||
preconditions: whenDocDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.DOWN,
|
||||
},
|
||||
};
|
||||
|
||||
export const MoveDrawingUpShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
description: 'docs-drawing-ui.shortcut.drawing-move-up',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'docs-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_UP,
|
||||
priority: 100,
|
||||
preconditions: whenDocDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.UP,
|
||||
},
|
||||
};
|
||||
|
||||
export const MoveDrawingLeftShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
description: 'docs-drawing-ui.shortcut.drawing-move-left',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'docs-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_LEFT,
|
||||
priority: 100,
|
||||
preconditions: whenDocDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.LEFT,
|
||||
},
|
||||
};
|
||||
|
||||
export const MoveDrawingRightShortcutItem: IShortcutItem<IMoveDrawingsCommandParams> = {
|
||||
id: MoveDocDrawingsCommand.id,
|
||||
description: 'docs-drawing-ui.shortcut.drawing-move-right',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'docs-drawing-ui.shortcut.drawing-view',
|
||||
binding: KeyCode.ARROW_RIGHT,
|
||||
priority: 100,
|
||||
preconditions: whenDocDrawingFocused,
|
||||
staticParameters: {
|
||||
direction: Direction.RIGHT,
|
||||
},
|
||||
};
|
||||
export const DeleteDrawingsShortcutItem: IShortcutItem = {
|
||||
id: DeleteDocDrawingsCommand.id,
|
||||
description: 'docs-drawing-ui.shortcut.drawing-delete',
|
||||
group: '4_drawing-view',
|
||||
groupTitle: 'docs-drawing-ui.shortcut.drawing-view',
|
||||
// when focusing on any other input tag do not trigger this shortcut
|
||||
preconditions: whenDocDrawingFocused,
|
||||
binding: KeyCode.DELETE,
|
||||
mac: KeyCode.BACKSPACE,
|
||||
};
|
||||
99
packages/docs-drawing-ui/src/controllers/ui.controller.ts
Normal file
99
packages/docs-drawing-ui/src/controllers/ui.controller.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, ICommandService } from '@univerjs/core';
|
||||
import { IMenuManagerService, IShortcutService } from '@univerjs/ui';
|
||||
import { DeleteDocDrawingsCommand } from '../commands/commands/delete-doc-drawing.command';
|
||||
import { GroupDocDrawingCommand } from '../commands/commands/group-doc-drawing.command';
|
||||
import { InsertDocImageCommand } from '../commands/commands/insert-image.command';
|
||||
import { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from '../commands/commands/insert-shape.command';
|
||||
import { MoveDocDrawingsCommand } from '../commands/commands/move-drawings.command';
|
||||
import { UngroupDocDrawingCommand } from '../commands/commands/ungroup-doc-drawing.command';
|
||||
import {
|
||||
IMoveInlineDrawingCommand,
|
||||
ITransformNonInlineDrawingCommand,
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
} from '../commands/commands/update-doc-drawing.command';
|
||||
import { ClearDocDrawingTransformerOperation } from '../commands/operations/clear-drawing-transformer.operation';
|
||||
import { EditDocDrawingOperation } from '../commands/operations/edit-doc-drawing.operation';
|
||||
import { SidebarDocDrawingOperation } from '../commands/operations/open-drawing-panel.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import {
|
||||
DeleteDrawingsShortcutItem,
|
||||
MoveDrawingDownShortcutItem,
|
||||
MoveDrawingLeftShortcutItem,
|
||||
MoveDrawingRightShortcutItem,
|
||||
MoveDrawingUpShortcutItem,
|
||||
} from './shortcuts/drawing.shortcut';
|
||||
|
||||
export class DocDrawingUIController extends Disposable {
|
||||
constructor(
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IShortcutService private readonly _shortcutService: IShortcutService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _initCustomComponents(): void {
|
||||
}
|
||||
|
||||
private _initMenus(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _initCommands() {
|
||||
[
|
||||
InsertDocImageCommand,
|
||||
InsertDocRectangleShapeCommand,
|
||||
InsertDocEllipseShapeCommand,
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
IMoveInlineDrawingCommand,
|
||||
ITransformNonInlineDrawingCommand,
|
||||
SidebarDocDrawingOperation,
|
||||
ClearDocDrawingTransformerOperation,
|
||||
EditDocDrawingOperation,
|
||||
GroupDocDrawingCommand,
|
||||
UngroupDocDrawingCommand,
|
||||
MoveDocDrawingsCommand,
|
||||
DeleteDocDrawingsCommand,
|
||||
].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 _init(): void {
|
||||
this._initCommands();
|
||||
this._initCustomComponents();
|
||||
this._initMenus();
|
||||
this._initShortcuts();
|
||||
}
|
||||
}
|
||||
2
packages/docs-drawing-ui/src/global.css
Normal file
2
packages/docs-drawing-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
47
packages/docs-drawing-ui/src/index.ts
Normal file
47
packages/docs-drawing-ui/src/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './global.css';
|
||||
|
||||
export { DeleteDocDrawingsCommand } from './commands/commands/delete-doc-drawing.command';
|
||||
export { GroupDocDrawingCommand } from './commands/commands/group-doc-drawing.command';
|
||||
export { InsertDocImageCommand } from './commands/commands/insert-image.command';
|
||||
export { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from './commands/commands/insert-shape.command';
|
||||
export { MoveDocDrawingsCommand } from './commands/commands/move-drawings.command';
|
||||
export { UngroupDocDrawingCommand } from './commands/commands/ungroup-doc-drawing.command';
|
||||
export { ClearDocDrawingTransformerOperation } from './commands/operations/clear-drawing-transformer.operation';
|
||||
export { EditDocDrawingOperation } from './commands/operations/edit-doc-drawing.operation';
|
||||
export { SidebarDocDrawingOperation } from './commands/operations/open-drawing-panel.operation';
|
||||
export type { IUniverDocsDrawingUIConfig } from './config/config';
|
||||
export { DocFloatDomController } from './controllers/doc-float-dom.controller';
|
||||
export { DOCS_IMAGE_MENU_ID } from './menu/image.menu';
|
||||
export { menuSchema as DocsDrawingUIMenuSchema } from './menu/schema';
|
||||
export { DOCS_SHAPE_BELOW_MENU_ID, DOCS_SHAPE_MENU_ID } from './menu/shape.menu';
|
||||
export { UniverDocsDrawingMobileUIPlugin } from './mobile-plugin';
|
||||
export { UniverDocsDrawingUIPlugin } from './plugin';
|
||||
export { DocDrawingFloatingToolbarAdapterService } from './services/doc-drawing-floating-toolbar-adapter.service';
|
||||
export type {
|
||||
IDocDrawingFloatingToolbarAdapter,
|
||||
IDocDrawingFloatingToolbarButtonItem,
|
||||
IDocDrawingFloatingToolbarItem,
|
||||
IDocDrawingFloatingToolbarOption,
|
||||
IDocDrawingFloatingToolbarParams,
|
||||
IDocDrawingFloatingToolbarSelectItem,
|
||||
} from './services/doc-drawing-floating-toolbar-adapter.service';
|
||||
export { DocDrawingPosition } from './views/doc-image-panel/DocDrawingPosition';
|
||||
export { DocDrawingTextWrap } from './views/doc-image-panel/DocDrawingTextWrap';
|
||||
export { MobileDocDrawingPosition } from './views/doc-image-panel/MobileDocDrawingPosition';
|
||||
export { MobileDocDrawingTextWrap } from './views/doc-image-panel/MobileDocDrawingTextWrap';
|
||||
91
packages/docs-drawing-ui/src/locale/ar-SA.ts
Normal file
91
packages/docs-drawing-ui/src/locale/ar-SA.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'صورة',
|
||||
upload: {
|
||||
float: 'إدراج صورة',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'إدراج شكل',
|
||||
rectangle: 'إدراج مستطيل',
|
||||
ellipse: 'إدراج قطع ناقص',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'تحرير الصورة',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'حذف',
|
||||
edit: 'تحرير',
|
||||
crop: 'قص',
|
||||
reset: 'إعادة تعيين الحجم',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'التفاف النص',
|
||||
wrappingStyle: 'نمط التفاف',
|
||||
square: 'مربع',
|
||||
topAndBottom: 'أعلى وأسفل',
|
||||
inline: 'في نفس السطر مع النص',
|
||||
behindText: 'خلف النص',
|
||||
inFrontText: 'أمام النص',
|
||||
wrapText: 'تفاف النص',
|
||||
bothSide: 'كلا الجانبين',
|
||||
leftOnly: 'يسار فقط',
|
||||
rightOnly: 'يمين فقط',
|
||||
distanceFromText: 'المسافة من النص',
|
||||
top: 'أعلى(بكسل)',
|
||||
left: 'يسار(بكسل)',
|
||||
bottom: 'أسفل(بكسل)',
|
||||
right: 'يمين(بكسل)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'موضع',
|
||||
horizontal: 'أفقي',
|
||||
vertical: 'عمودي',
|
||||
absolutePosition: 'موضع مطلق(بكسل)',
|
||||
toTheRightOf: 'إلى يمين',
|
||||
bellow: 'أسفل',
|
||||
options: 'خيارات',
|
||||
moveObjectWithText: 'نقل الكائن مع النص',
|
||||
column: 'عمود',
|
||||
margin: 'هامش',
|
||||
page: 'صفحة',
|
||||
line: 'سطر',
|
||||
paragraph: 'فقرة',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'حجم الصورة يتجاوز الحد، الحد هو {0}م',
|
||||
invalidImageType: 'نوع الصورة غير صالح',
|
||||
exceedMaxCount: 'يمكن رفع {0} صور فقط في المرة الواحدة',
|
||||
invalidImage: 'صورة غير صالحة',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'عرض الرسم',
|
||||
'drawing-move-down': 'نقل الرسم لأسفل',
|
||||
'drawing-move-up': 'نقل الرسم لأعلى',
|
||||
'drawing-move-left': 'نقل الرسم لليسار',
|
||||
'drawing-move-right': 'نقل الرسم لليمين',
|
||||
'drawing-delete': 'حذف الرسم',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/ca-ES.ts
Normal file
91
packages/docs-drawing-ui/src/locale/ca-ES.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Imatge',
|
||||
upload: {
|
||||
float: 'Insereix imatge',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insereix forma',
|
||||
rectangle: 'Insereix rectangle',
|
||||
ellipse: 'Insereix el·lipse',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Edita la imatge',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Elimina',
|
||||
edit: 'Edita',
|
||||
crop: 'Retalla',
|
||||
reset: 'Restableix la mida',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Ajust del text',
|
||||
wrappingStyle: 'Estil d\'ajust',
|
||||
square: 'Quadrat',
|
||||
topAndBottom: 'A dalt i a baix',
|
||||
inline: 'En línia amb el text',
|
||||
behindText: 'Darrere del text',
|
||||
inFrontText: 'Davant del text',
|
||||
wrapText: 'Ajusta el text',
|
||||
bothSide: 'Ambdós costats',
|
||||
leftOnly: 'Només esquerra',
|
||||
rightOnly: 'Només dreta',
|
||||
distanceFromText: 'Distància del text',
|
||||
top: 'A dalt(px)',
|
||||
left: 'Esquerra(px)',
|
||||
bottom: 'A baix(px)',
|
||||
right: 'Dreta(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Posició',
|
||||
horizontal: 'Horitzontal',
|
||||
vertical: 'Vertical',
|
||||
absolutePosition: 'Posició absoluta(px)',
|
||||
toTheRightOf: 'a la dreta de',
|
||||
bellow: 'a sota',
|
||||
options: 'Opcions',
|
||||
moveObjectWithText: 'Mou l\'objecte amb el text',
|
||||
column: 'Columna',
|
||||
margin: 'Marge',
|
||||
page: 'Pàgina',
|
||||
line: 'Línia',
|
||||
paragraph: 'Paràgraf',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/de-DE.ts
Normal file
91
packages/docs-drawing-ui/src/locale/de-DE.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Bild',
|
||||
upload: {
|
||||
float: 'Bild einfügen',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Form einfügen',
|
||||
rectangle: 'Rechteck einfügen',
|
||||
ellipse: 'Ellipse einfügen',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Bild bearbeiten',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Löschen',
|
||||
edit: 'Bearbeiten',
|
||||
crop: 'Zuschneiden',
|
||||
reset: 'Größe zurücksetzen',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Textumbruch',
|
||||
wrappingStyle: 'Umbruchstil',
|
||||
square: 'Quadratisch',
|
||||
topAndBottom: 'Oben und unten',
|
||||
inline: 'Im Textfluss',
|
||||
behindText: 'Hinter dem Text',
|
||||
inFrontText: 'Vor dem Text',
|
||||
wrapText: 'Text umbrechen',
|
||||
bothSide: 'Beide Seiten',
|
||||
leftOnly: 'Nur links',
|
||||
rightOnly: 'Nur rechts',
|
||||
distanceFromText: 'Abstand vom Text',
|
||||
top: 'Oben(px)',
|
||||
left: 'Links(px)',
|
||||
bottom: 'Unten(px)',
|
||||
right: 'Rechts(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Position',
|
||||
horizontal: 'Horizontal',
|
||||
vertical: 'Vertikal',
|
||||
absolutePosition: 'Absolute Position(px)',
|
||||
toTheRightOf: 'rechts von',
|
||||
bellow: 'unterhalb',
|
||||
options: 'Optionen',
|
||||
moveObjectWithText: 'Objekt mit Text verschieben',
|
||||
column: 'Spalte',
|
||||
margin: 'Rand',
|
||||
page: 'Seite',
|
||||
line: 'Zeile',
|
||||
paragraph: 'Absatz',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Bildgröße überschreitet das Limit, das Limit ist {0}M',
|
||||
invalidImageType: 'Ungültiger Bildtyp',
|
||||
exceedMaxCount: 'Es können nur {0} Bilder gleichzeitig hochgeladen werden',
|
||||
invalidImage: 'Ungültiges Bild',
|
||||
},
|
||||
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;
|
||||
89
packages/docs-drawing-ui/src/locale/en-US.ts
Normal file
89
packages/docs-drawing-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Image',
|
||||
upload: {
|
||||
float: 'Insert Image',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insert Shape',
|
||||
rectangle: 'Insert Rectangle',
|
||||
ellipse: 'Insert Ellipse',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Edit Image',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
crop: 'Crop',
|
||||
reset: 'Reset Size',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Text Wrapping',
|
||||
wrappingStyle: 'Wrapping Style',
|
||||
square: 'Square',
|
||||
topAndBottom: 'Top and Bottom',
|
||||
inline: 'In line with text',
|
||||
behindText: 'Behind text',
|
||||
inFrontText: 'In front of text',
|
||||
wrapText: 'Wrap text',
|
||||
bothSide: 'Both sides',
|
||||
leftOnly: 'Left only',
|
||||
rightOnly: 'Right only',
|
||||
distanceFromText: 'Distance from text',
|
||||
top: 'Top(px)',
|
||||
left: 'Left(px)',
|
||||
bottom: 'Bottom(px)',
|
||||
right: 'Right(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Position',
|
||||
horizontal: 'Horizontal',
|
||||
vertical: 'Vertical',
|
||||
absolutePosition: 'Absolute Position(px)',
|
||||
toTheRightOf: 'to the right of',
|
||||
bellow: 'bellow',
|
||||
options: 'Options',
|
||||
moveObjectWithText: 'Move object with text',
|
||||
column: 'Column',
|
||||
margin: 'Margin',
|
||||
page: 'Page',
|
||||
line: 'Line',
|
||||
paragraph: 'Paragraph',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/es-ES.ts
Normal file
91
packages/docs-drawing-ui/src/locale/es-ES.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Imagen',
|
||||
upload: {
|
||||
float: 'Insertar imagen',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insertar forma',
|
||||
rectangle: 'Insertar rectángulo',
|
||||
ellipse: 'Insertar elipse',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Editar imagen',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Eliminar',
|
||||
edit: 'Editar',
|
||||
crop: 'Recortar',
|
||||
reset: 'Restablecer tamaño',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Ajuste de texto',
|
||||
wrappingStyle: 'Estilo de ajuste',
|
||||
square: 'Cuadrado',
|
||||
topAndBottom: 'Arriba y abajo',
|
||||
inline: 'En línea con el texto',
|
||||
behindText: 'Detrás del texto',
|
||||
inFrontText: 'Delante del texto',
|
||||
wrapText: 'Ajustar texto',
|
||||
bothSide: 'Ambos lados',
|
||||
leftOnly: 'Solo izquierda',
|
||||
rightOnly: 'Solo derecha',
|
||||
distanceFromText: 'Distancia del texto',
|
||||
top: 'Arriba(px)',
|
||||
left: 'Izquierda(px)',
|
||||
bottom: 'Abajo(px)',
|
||||
right: 'Derecha(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Posición',
|
||||
horizontal: 'Horizontal',
|
||||
vertical: 'Vertical',
|
||||
absolutePosition: 'Posición absoluta(px)',
|
||||
toTheRightOf: 'a la derecha de',
|
||||
bellow: 'debajo',
|
||||
options: 'Opciones',
|
||||
moveObjectWithText: 'Mover objeto con el texto',
|
||||
column: 'Columna',
|
||||
margin: 'Margen',
|
||||
page: 'Página',
|
||||
line: 'Línea',
|
||||
paragraph: 'Párrafo',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'El tamaño de la imagen excede 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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/fa-IR.ts
Normal file
91
packages/docs-drawing-ui/src/locale/fa-IR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'تصویر',
|
||||
upload: {
|
||||
float: 'درج تصویر',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'درج شکل',
|
||||
rectangle: 'درج مستطیل',
|
||||
ellipse: 'بیضی را درج کنید',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'ویرایش تصویر',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'حذف',
|
||||
edit: 'ویرایش',
|
||||
crop: 'کراپ',
|
||||
reset: 'بازنشانی اندازه',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'پیچش متن',
|
||||
wrappingStyle: 'سبک پیچش',
|
||||
square: 'مربع',
|
||||
topAndBottom: 'بالا و پایین',
|
||||
inline: 'در یک خط با متن',
|
||||
behindText: 'پشت متن',
|
||||
inFrontText: 'جلوی متن',
|
||||
wrapText: 'پیچش متن',
|
||||
bothSide: 'هر دو طرف',
|
||||
leftOnly: 'فقط سمت چپ',
|
||||
rightOnly: 'فقط سمت راست',
|
||||
distanceFromText: 'فاصله از متن',
|
||||
top: 'بالا(px)',
|
||||
left: 'چپ(px)',
|
||||
bottom: 'پایین(px)',
|
||||
right: 'راست(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'موقعیت',
|
||||
horizontal: 'افقی',
|
||||
vertical: 'عمودی',
|
||||
absolutePosition: 'موقعیت مطلق(px)',
|
||||
toTheRightOf: 'سمت راست',
|
||||
bellow: 'زیر',
|
||||
options: 'گزینهها',
|
||||
moveObjectWithText: 'جابهجایی شیء با متن',
|
||||
column: 'ستون',
|
||||
margin: 'حاشیه',
|
||||
page: 'صفحه',
|
||||
line: 'خط',
|
||||
paragraph: 'پاراگراف',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'اندازه تصویر از حد مجاز فراتر رفته است، حد مجاز {0}M است',
|
||||
invalidImageType: 'نوع تصویر نامعتبر است',
|
||||
exceedMaxCount: 'فقط {0} تصویر میتوانند همزمان آپلود شوند',
|
||||
invalidImage: 'تصویر نامعتبر است',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'نمای طراحی',
|
||||
'drawing-move-down': 'جابهجایی طراحی به پایین',
|
||||
'drawing-move-up': 'جابهجایی طراحی به بالا',
|
||||
'drawing-move-left': 'جابهجایی طراحی به چپ',
|
||||
'drawing-move-right': 'جابهجایی طراحی به راست',
|
||||
'drawing-delete': 'حذف طراحی',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/fr-FR.ts
Normal file
91
packages/docs-drawing-ui/src/locale/fr-FR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Image',
|
||||
upload: {
|
||||
float: 'Insérer une image',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insérer une forme',
|
||||
rectangle: 'Insérer un rectangle',
|
||||
ellipse: 'Insérer une ellipse',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Modifier l\'image',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Supprimer',
|
||||
edit: 'Modifier',
|
||||
crop: 'Rogner',
|
||||
reset: 'Réinitialiser la taille',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Habillage du texte',
|
||||
wrappingStyle: 'Style d\'habillage',
|
||||
square: 'Carré',
|
||||
topAndBottom: 'Haut et bas',
|
||||
inline: 'Aligné avec le texte',
|
||||
behindText: 'Derrière le texte',
|
||||
inFrontText: 'Devant le texte',
|
||||
wrapText: 'Habiller le texte',
|
||||
bothSide: 'Des deux côtés',
|
||||
leftOnly: 'Seulement à gauche',
|
||||
rightOnly: 'Seulement à droite',
|
||||
distanceFromText: 'Distance du texte',
|
||||
top: 'Haut(px)',
|
||||
left: 'Gauche(px)',
|
||||
bottom: 'Bas(px)',
|
||||
right: 'Droite(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Position',
|
||||
horizontal: 'Horizontal',
|
||||
vertical: 'Vertical',
|
||||
absolutePosition: 'Position absolue(px)',
|
||||
toTheRightOf: 'à droite de',
|
||||
bellow: 'en dessous',
|
||||
options: 'Options',
|
||||
moveObjectWithText: 'Déplacer l\'objet avec le texte',
|
||||
column: 'Colonne',
|
||||
margin: 'Marge',
|
||||
page: 'Page',
|
||||
line: 'Ligne',
|
||||
paragraph: 'Paragraphe',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/id-ID.ts
Normal file
91
packages/docs-drawing-ui/src/locale/id-ID.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Gambar',
|
||||
upload: {
|
||||
float: 'Sisipkan Gambar',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Sisipkan Bentuk',
|
||||
rectangle: 'Sisipkan Persegi Panjang',
|
||||
ellipse: 'Sisipkan Elips',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Edit Gambar',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Hapus',
|
||||
edit: 'Edit',
|
||||
crop: 'Pangkas',
|
||||
reset: 'Atur Ulang Ukuran',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Pembungkus Teks',
|
||||
wrappingStyle: 'Gaya Pembungkus',
|
||||
square: 'Kotak',
|
||||
topAndBottom: 'Atas dan Bawah',
|
||||
inline: 'Sejajar dengan teks',
|
||||
behindText: 'Di belakang teks',
|
||||
inFrontText: 'Di depan teks',
|
||||
wrapText: 'Bungkus teks',
|
||||
bothSide: 'Kedua sisi',
|
||||
leftOnly: 'Kiri saja',
|
||||
rightOnly: 'Kanan saja',
|
||||
distanceFromText: 'Jarak dari teks',
|
||||
top: 'Atas(px)',
|
||||
left: 'Kiri(px)',
|
||||
bottom: 'Bawah(px)',
|
||||
right: 'Kanan(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Posisi',
|
||||
horizontal: 'Horizontal',
|
||||
vertical: 'Vertikal',
|
||||
absolutePosition: 'Posisi Absolut(px)',
|
||||
toTheRightOf: 'di sebelah kanan',
|
||||
bellow: 'di bawah',
|
||||
options: 'Opsi',
|
||||
moveObjectWithText: 'Pindahkan objek dengan teks',
|
||||
column: 'Kolom',
|
||||
margin: 'Margin',
|
||||
page: 'Halaman',
|
||||
line: 'Baris',
|
||||
paragraph: 'Paragraf',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Ukuran gambar melebihi batas, batasnya adalah {0}M',
|
||||
invalidImageType: 'Jenis gambar tidak valid',
|
||||
exceedMaxCount: 'Hanya {0} gambar yang dapat diunggah dalam satu waktu',
|
||||
invalidImage: 'Gambar tidak valid',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/it-IT.ts
Normal file
91
packages/docs-drawing-ui/src/locale/it-IT.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Immagine',
|
||||
upload: {
|
||||
float: 'Inserisci immagine',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Inserisci Forma',
|
||||
rectangle: 'Inserisci Rettangolo',
|
||||
ellipse: 'Inserisci Ellisse',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Modifica immagine',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Elimina',
|
||||
edit: 'Modifica',
|
||||
crop: 'Ritaglia',
|
||||
reset: 'Reimposta dimensione',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Testo a capo',
|
||||
wrappingStyle: 'Stile a capo',
|
||||
square: 'Quadrato',
|
||||
topAndBottom: 'Superiore e inferiore',
|
||||
inline: 'In linea con il testo',
|
||||
behindText: 'Dietro il testo',
|
||||
inFrontText: 'Davanti al testo',
|
||||
wrapText: 'Testo a capo',
|
||||
bothSide: 'Entrambi i lati',
|
||||
leftOnly: 'Solo sinistra',
|
||||
rightOnly: 'Solo destra',
|
||||
distanceFromText: 'Distanza dal testo',
|
||||
top: 'Superiore(px)',
|
||||
left: 'Sinistra(px)',
|
||||
bottom: 'Inferiore(px)',
|
||||
right: 'Destra(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Posizione',
|
||||
horizontal: 'Orizzontale',
|
||||
vertical: 'Verticale',
|
||||
absolutePosition: 'Posizione assoluta(px)',
|
||||
toTheRightOf: 'a destra di',
|
||||
bellow: 'sotto',
|
||||
options: 'Opzioni',
|
||||
moveObjectWithText: 'Sposta oggetto con il testo',
|
||||
column: 'Colonna',
|
||||
margin: 'Margine',
|
||||
page: 'Pagina',
|
||||
line: 'Riga',
|
||||
paragraph: 'Paragrafo',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/ja-JP.ts
Normal file
91
packages/docs-drawing-ui/src/locale/ja-JP.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: '画像',
|
||||
upload: {
|
||||
float: '画像を挿入',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: '図形の挿入',
|
||||
rectangle: '四角形を挿入',
|
||||
ellipse: '楕円を挿入',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: '画像の編集',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '削除',
|
||||
edit: '編集',
|
||||
crop: 'トリミング',
|
||||
reset: 'サイズをリセット',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: '文字列の折り返し',
|
||||
wrappingStyle: '折り返しのスタイル',
|
||||
square: '四角形',
|
||||
topAndBottom: '上下',
|
||||
inline: '行内',
|
||||
behindText: '文字列の背面',
|
||||
inFrontText: '文字列の前面',
|
||||
wrapText: '文字列の折り返し',
|
||||
bothSide: '両側',
|
||||
leftOnly: '左のみ',
|
||||
rightOnly: '右のみ',
|
||||
distanceFromText: '文字列からの距離',
|
||||
top: '上(px)',
|
||||
left: '左(px)',
|
||||
bottom: '下(px)',
|
||||
right: '右(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: '位置',
|
||||
horizontal: '水平方向',
|
||||
vertical: '垂直方向',
|
||||
absolutePosition: '絶対位置(px)',
|
||||
toTheRightOf: '右側に',
|
||||
bellow: '下に',
|
||||
options: 'オプション',
|
||||
moveObjectWithText: '文字列と一緒にオブジェクトを移動',
|
||||
column: '列',
|
||||
margin: '余白',
|
||||
page: 'ページ',
|
||||
line: '行',
|
||||
paragraph: '段落',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '画像のサイズが制限を超えています。制限: {0}MB',
|
||||
invalidImageType: '無効な画像形式です',
|
||||
exceedMaxCount: '一度にアップロードできる最大数は{0}件です',
|
||||
invalidImage: '無効な画像です',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '図形表示',
|
||||
'drawing-move-down': '図形を下へ移動',
|
||||
'drawing-move-up': '図形を上へ移動',
|
||||
'drawing-move-left': '図形を左へ移動',
|
||||
'drawing-move-right': '図形を右へ移動',
|
||||
'drawing-delete': '図形を削除',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/ko-KR.ts
Normal file
91
packages/docs-drawing-ui/src/locale/ko-KR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: '이미지',
|
||||
upload: {
|
||||
float: '이미지 삽입',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: '도형 삽입',
|
||||
rectangle: '사각형 삽입',
|
||||
ellipse: '타원 삽입',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: '이미지 편집',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '삭제',
|
||||
edit: '편집',
|
||||
crop: '자르기',
|
||||
reset: '크기 초기화',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: '텍스트 감싸기',
|
||||
wrappingStyle: '감싸기 스타일',
|
||||
square: '사각형',
|
||||
topAndBottom: '위아래',
|
||||
inline: '텍스트와 같은 줄',
|
||||
behindText: '텍스트 뒤',
|
||||
inFrontText: '텍스트 앞',
|
||||
wrapText: '텍스트 감싸기',
|
||||
bothSide: '양쪽',
|
||||
leftOnly: '왼쪽만',
|
||||
rightOnly: '오른쪽만',
|
||||
distanceFromText: '텍스트와 거리',
|
||||
top: '위(px)',
|
||||
left: '왼쪽(px)',
|
||||
bottom: '아래(px)',
|
||||
right: '오른쪽(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: '위치',
|
||||
horizontal: '수평',
|
||||
vertical: '수직',
|
||||
absolutePosition: '절대 위치(px)',
|
||||
toTheRightOf: '우측에',
|
||||
bellow: '아래에',
|
||||
options: '옵션',
|
||||
moveObjectWithText: '텍스트와 함께 객체 이동',
|
||||
column: '열',
|
||||
margin: '여백',
|
||||
page: '페이지',
|
||||
line: '줄',
|
||||
paragraph: '문단',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '이미지 크기가 제한을 초과했습니다. 제한: {0}MB',
|
||||
invalidImageType: '유효하지 않은 이미지 유형입니다',
|
||||
exceedMaxCount: '한 번에 최대 {0}개만 업로드할 수 있습니다',
|
||||
invalidImage: '유효하지 않은 이미지입니다',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '그리기 보기',
|
||||
'drawing-move-down': '그리기 아래로 이동',
|
||||
'drawing-move-up': '그리기 위로 이동',
|
||||
'drawing-move-left': '그리기 왼쪽으로 이동',
|
||||
'drawing-move-right': '그리기 오른쪽으로 이동',
|
||||
'drawing-delete': '그리기 삭제',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/pl-PL.ts
Normal file
91
packages/docs-drawing-ui/src/locale/pl-PL.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Obraz',
|
||||
upload: {
|
||||
float: 'Wstaw obraz',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Wstaw kształt',
|
||||
rectangle: 'Wstaw prostokąt',
|
||||
ellipse: 'Wstaw elipsę',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Edytuj obraz',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Usuń',
|
||||
edit: 'Edytuj',
|
||||
crop: 'Przytnij',
|
||||
reset: 'Resetuj rozmiar',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Zawijanie tekstu',
|
||||
wrappingStyle: 'Styl zawijania',
|
||||
square: 'Kwadrat',
|
||||
topAndBottom: 'Góra i dół',
|
||||
inline: 'W linii z tekstem',
|
||||
behindText: 'Za tekstem',
|
||||
inFrontText: 'Przed tekstem',
|
||||
wrapText: 'Zawijaj tekst',
|
||||
bothSide: 'Obie strony',
|
||||
leftOnly: 'Tylko lewa',
|
||||
rightOnly: 'Tylko prawa',
|
||||
distanceFromText: 'Odległość od tekstu',
|
||||
top: 'Góra(px)',
|
||||
left: 'Lewo(px)',
|
||||
bottom: 'Dół(px)',
|
||||
right: 'Prawo(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Pozycja',
|
||||
horizontal: 'Pozioma',
|
||||
vertical: 'Pionowa',
|
||||
absolutePosition: 'Pozycja bezwzględna(px)',
|
||||
toTheRightOf: 'po prawej stronie',
|
||||
bellow: 'poniżej',
|
||||
options: 'Opcje',
|
||||
moveObjectWithText: 'Przesuwaj obiekt razem z tekstem',
|
||||
column: 'Kolumna',
|
||||
margin: 'Margines',
|
||||
page: 'Strona',
|
||||
line: 'Wiersz',
|
||||
paragraph: 'Akapit',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/pt-BR.ts
Normal file
91
packages/docs-drawing-ui/src/locale/pt-BR.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Imagem',
|
||||
upload: {
|
||||
float: 'Inserir imagem',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Inserir Forma',
|
||||
rectangle: 'Inserir Retângulo',
|
||||
ellipse: 'Inserir Elipse',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Editar imagem',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Excluir',
|
||||
edit: 'Editar',
|
||||
crop: 'Cortar',
|
||||
reset: 'Redefinir tamanho',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Quebra de texto',
|
||||
wrappingStyle: 'Estilo de quebra',
|
||||
square: 'Quadrado',
|
||||
topAndBottom: 'Superior e inferior',
|
||||
inline: 'Em linha com o texto',
|
||||
behindText: 'Atrás do texto',
|
||||
inFrontText: 'À frente do texto',
|
||||
wrapText: 'Quebrar texto automaticamente',
|
||||
bothSide: 'Ambos os lados',
|
||||
leftOnly: 'Somente esquerda',
|
||||
rightOnly: 'Somente direita',
|
||||
distanceFromText: 'Distância do texto',
|
||||
top: 'Superior(px)',
|
||||
left: 'Esquerda(px)',
|
||||
bottom: 'Inferior(px)',
|
||||
right: 'Direita(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Posição',
|
||||
horizontal: 'Horizontal',
|
||||
vertical: 'Vertical',
|
||||
absolutePosition: 'Posição absoluta(px)',
|
||||
toTheRightOf: 'à direita de',
|
||||
bellow: 'abaixo',
|
||||
options: 'Opções',
|
||||
moveObjectWithText: 'Mover objeto com o texto',
|
||||
column: 'Coluna',
|
||||
margin: 'Margem',
|
||||
page: 'Página',
|
||||
line: 'Linha',
|
||||
paragraph: 'Parágrafo',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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;
|
||||
91
packages/docs-drawing-ui/src/locale/ru-RU.ts
Normal file
91
packages/docs-drawing-ui/src/locale/ru-RU.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Изображение',
|
||||
upload: {
|
||||
float: 'Вставить изображение',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Вставить фигуру',
|
||||
rectangle: 'Вставить прямоугольник',
|
||||
ellipse: 'Вставить эллипс',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Редактировать изображение',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Удалить',
|
||||
edit: 'Редактировать',
|
||||
crop: 'Обрезать',
|
||||
reset: 'Сбросить размер',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Обтекание текстом',
|
||||
wrappingStyle: 'Стиль обтекания',
|
||||
square: 'Квадрат',
|
||||
topAndBottom: 'Сверху и снизу',
|
||||
inline: 'В тексте',
|
||||
behindText: 'За текстом',
|
||||
inFrontText: 'Перед текстом',
|
||||
wrapText: 'Обтекание текстом',
|
||||
bothSide: 'С обеих сторон',
|
||||
leftOnly: 'Только слева',
|
||||
rightOnly: 'Только справа',
|
||||
distanceFromText: 'Расстояние от текста',
|
||||
top: 'Сверху (px)',
|
||||
left: 'Слева (px)',
|
||||
bottom: 'Снизу (px)',
|
||||
right: 'Справа (px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Положение',
|
||||
horizontal: 'По горизонтали',
|
||||
vertical: 'По вертикали',
|
||||
absolutePosition: 'Абсолютное положение (px)',
|
||||
toTheRightOf: 'справа от',
|
||||
bellow: 'снизу',
|
||||
options: 'Параметры',
|
||||
moveObjectWithText: 'Перемещать объект с текстом',
|
||||
column: 'Колонка',
|
||||
margin: 'Поле',
|
||||
page: 'Страница',
|
||||
line: 'Строка',
|
||||
paragraph: 'Абзац',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: 'Размер изображения превышает лимит, лимит составляет {0}М',
|
||||
invalidImageType: 'Недопустимый тип изображения',
|
||||
exceedMaxCount: 'За один раз можно загрузить только {0} изображений',
|
||||
invalidImage: 'Недопустимое изображение',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Вид изображения',
|
||||
'drawing-move-down': 'Переместить изображение вниз',
|
||||
'drawing-move-up': 'Переместить изображение вверх',
|
||||
'drawing-move-left': 'Переместить изображение влево',
|
||||
'drawing-move-right': 'Переместить изображение вправо',
|
||||
'drawing-delete': 'Удалить изображение',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/sk-SK.ts
Normal file
91
packages/docs-drawing-ui/src/locale/sk-SK.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Obrázok',
|
||||
upload: {
|
||||
float: 'Vložiť obrázok',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Vložiť tvar',
|
||||
rectangle: 'Vložiť obdĺžnik',
|
||||
ellipse: 'Vložiť elipsu',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Upraviť obrázok',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Odstrániť',
|
||||
edit: 'Upraviť',
|
||||
crop: 'Orezať',
|
||||
reset: 'Obnoviť veľkosť',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Obtekanie textu',
|
||||
wrappingStyle: 'Štýl obtekania',
|
||||
square: 'Obdĺžnikové',
|
||||
topAndBottom: 'Hore a dole',
|
||||
inline: 'V riadku s textom',
|
||||
behindText: 'Za textom',
|
||||
inFrontText: 'Pred textom',
|
||||
wrapText: 'Obtekať text',
|
||||
bothSide: 'Obe strany',
|
||||
leftOnly: 'Len ľavá',
|
||||
rightOnly: 'Len pravá',
|
||||
distanceFromText: 'Vzdialenosť od textu',
|
||||
top: 'Hore (px)',
|
||||
left: 'Vľavo (px)',
|
||||
bottom: 'Dole (px)',
|
||||
right: 'Vpravo (px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Pozícia',
|
||||
horizontal: 'Vodorovne',
|
||||
vertical: 'Zvisle',
|
||||
absolutePosition: 'Absolútna pozícia (px)',
|
||||
toTheRightOf: 'vpravo od',
|
||||
bellow: 'pod',
|
||||
options: 'Možnosti',
|
||||
moveObjectWithText: 'Presunúť objekt s textom',
|
||||
column: 'Stĺpec',
|
||||
margin: 'Okraj',
|
||||
page: 'Strana',
|
||||
line: 'Riadok',
|
||||
paragraph: 'Odsek',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
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/docs-drawing-ui/src/locale/types.ts
Normal file
20
packages/docs-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>;
|
||||
91
packages/docs-drawing-ui/src/locale/vi-VN.ts
Normal file
91
packages/docs-drawing-ui/src/locale/vi-VN.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: 'Hình ảnh',
|
||||
upload: {
|
||||
float: 'Chèn hình ảnh',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Chèn hình',
|
||||
rectangle: 'Chèn hình chữ nhật',
|
||||
ellipse: 'Chèn hình elip',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: 'Chỉnh sửa hình ảnh',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: 'Xóa',
|
||||
edit: 'Chỉnh sửa',
|
||||
crop: 'Cắt',
|
||||
reset: 'Đặt lại kích thước',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: 'Bọc văn bản',
|
||||
wrappingStyle: 'Kiểu bọc văn bản',
|
||||
square: 'Hình vuông',
|
||||
topAndBottom: 'Trên và dưới',
|
||||
inline: 'Cùng dòng với văn bản',
|
||||
behindText: 'Phía sau văn bản',
|
||||
inFrontText: 'Phía trước văn bản',
|
||||
wrapText: 'Bọc văn bản',
|
||||
bothSide: 'Cả hai bên',
|
||||
leftOnly: 'Chỉ bên trái',
|
||||
rightOnly: 'Chỉ bên phải',
|
||||
distanceFromText: 'Khoảng cách với văn bản',
|
||||
top: 'Trên (px)',
|
||||
left: 'Trái (px)',
|
||||
bottom: 'Dưới (px)',
|
||||
right: 'Phải (px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: 'Vị trí',
|
||||
horizontal: 'Ngang',
|
||||
vertical: 'Dọc',
|
||||
absolutePosition: 'Vị trí tuyệt đối (px)',
|
||||
toTheRightOf: 'ở bên phải',
|
||||
bellow: 'bên dưới',
|
||||
options: 'Tùy chọn',
|
||||
moveObjectWithText: 'Di chuyển đối tượng cùng văn bản',
|
||||
column: 'Cột',
|
||||
margin: 'Lề',
|
||||
page: 'Trang',
|
||||
line: 'Dòng',
|
||||
paragraph: 'Đoạn văn',
|
||||
},
|
||||
'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 tối đa {0} hình ảnh một lần',
|
||||
invalidImage: 'Hình ảnh không hợp lệ',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': 'Chế độ xem bản vẽ',
|
||||
'drawing-move-down': 'Di chuyển bản vẽ xuống',
|
||||
'drawing-move-up': 'Di chuyển bản vẽ lên',
|
||||
'drawing-move-left': 'Di chuyển bản vẽ sang trái',
|
||||
'drawing-move-right': 'Di chuyển bản vẽ sang phải',
|
||||
'drawing-delete': 'Xóa bản vẽ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/zh-CN.ts
Normal file
91
packages/docs-drawing-ui/src/locale/zh-CN.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: '图片',
|
||||
upload: {
|
||||
float: '插入图片',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: '插入图形',
|
||||
rectangle: '插入矩形',
|
||||
ellipse: '插入椭圆',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: '编辑图片',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
crop: '裁剪',
|
||||
reset: '重置大小',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: '文字环绕',
|
||||
wrappingStyle: '环绕方式',
|
||||
square: '四周型',
|
||||
topAndBottom: '上下型',
|
||||
inline: '嵌入型',
|
||||
behindText: '衬于文字下方',
|
||||
inFrontText: '浮于文字上方',
|
||||
wrapText: '自动换行',
|
||||
bothSide: '两侧',
|
||||
leftOnly: '左侧',
|
||||
rightOnly: '右侧',
|
||||
distanceFromText: '距正文',
|
||||
top: '上(px)',
|
||||
left: '左(px)',
|
||||
bottom: '下(px)',
|
||||
right: '右(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: '位置',
|
||||
horizontal: '水平',
|
||||
vertical: '垂直',
|
||||
absolutePosition: '绝对位置(px)',
|
||||
toTheRightOf: '右侧',
|
||||
bellow: '下方',
|
||||
options: '选项',
|
||||
moveObjectWithText: '对象随文字移动',
|
||||
column: '栏',
|
||||
margin: '页边距',
|
||||
page: '页面',
|
||||
line: '行',
|
||||
paragraph: '段落',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '图片大小超过限制, 限制为{0}M',
|
||||
invalidImageType: '图片类型错误',
|
||||
exceedMaxCount: '图片只能一次上传{0}张',
|
||||
invalidImage: '无效图片',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '绘图视图',
|
||||
'drawing-move-down': '下移绘图',
|
||||
'drawing-move-up': '上移绘图',
|
||||
'drawing-move-left': '左移绘图',
|
||||
'drawing-move-right': '右移绘图',
|
||||
'drawing-delete': '删除绘图',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/zh-HK.ts
Normal file
91
packages/docs-drawing-ui/src/locale/zh-HK.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: '圖片',
|
||||
upload: {
|
||||
float: '插入圖片',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: '插入圖形',
|
||||
rectangle: '插入矩形',
|
||||
ellipse: '插入橢圓形',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: '編圖',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '刪除',
|
||||
edit: '編輯',
|
||||
crop: '裁切',
|
||||
reset: '重置大小',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: '文繞圖',
|
||||
wrappingStyle: '文繞圖方式',
|
||||
square: '矩形',
|
||||
topAndBottom: '上及下',
|
||||
inline: '與文字排列',
|
||||
behindText: '文字在前',
|
||||
inFrontText: '文字在後',
|
||||
wrapText: '自動換行',
|
||||
bothSide: '兩側',
|
||||
leftOnly: '僅左側',
|
||||
rightOnly: '僅右側',
|
||||
distanceFromText: '與文字距離',
|
||||
top: '上(px)',
|
||||
left: '左(px)',
|
||||
bottom: '下(px)',
|
||||
right: '右(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: '位置',
|
||||
horizontal: '水平',
|
||||
vertical: '垂直',
|
||||
absolutePosition: '絕對位置(px)',
|
||||
toTheRightOf: '右側',
|
||||
bellow: '下方',
|
||||
options: '選項',
|
||||
moveObjectWithText: '物件隨文字移動',
|
||||
column: '欄',
|
||||
margin: '邊界',
|
||||
page: '頁面',
|
||||
line: '行',
|
||||
paragraph: '段落',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '圖片大小超過限制, 限制為{0}M',
|
||||
invalidImageType: '圖片類型錯誤',
|
||||
exceedMaxCount: '圖片只能一次上傳{0}張',
|
||||
invalidImage: '無效圖片',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '繪圖檢視',
|
||||
'drawing-move-down': '下移繪圖',
|
||||
'drawing-move-up': '上移繪圖',
|
||||
'drawing-move-left': '左移繪圖',
|
||||
'drawing-move-right': '右移繪圖',
|
||||
'drawing-delete': '刪除繪圖',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
91
packages/docs-drawing-ui/src/locale/zh-TW.ts
Normal file
91
packages/docs-drawing-ui/src/locale/zh-TW.ts
Normal file
@@ -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 enUS from './en-US';
|
||||
|
||||
const locale: typeof enUS = {
|
||||
'docs-drawing-ui': {
|
||||
title: '圖片',
|
||||
upload: {
|
||||
float: '插入圖片',
|
||||
},
|
||||
shape: {
|
||||
insert: {
|
||||
title: '插入圖形',
|
||||
rectangle: '插入矩形',
|
||||
ellipse: '插入橢圓形',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
title: '編圖',
|
||||
},
|
||||
'image-popup': {
|
||||
delete: '刪除',
|
||||
edit: '編輯',
|
||||
crop: '裁切',
|
||||
reset: '重置大小',
|
||||
},
|
||||
'image-text-wrap': {
|
||||
title: '文繞圖',
|
||||
wrappingStyle: '文繞圖方式',
|
||||
square: '矩形',
|
||||
topAndBottom: '上及下',
|
||||
inline: '與文字排列',
|
||||
behindText: '文字在前',
|
||||
inFrontText: '文字在後',
|
||||
wrapText: '自動換行',
|
||||
bothSide: '兩側',
|
||||
leftOnly: '僅左側',
|
||||
rightOnly: '僅右側',
|
||||
distanceFromText: '與文字距離',
|
||||
top: '上(px)',
|
||||
left: '左(px)',
|
||||
bottom: '下(px)',
|
||||
right: '右(px)',
|
||||
},
|
||||
'image-position': {
|
||||
title: '位置',
|
||||
horizontal: '水平',
|
||||
vertical: '垂直',
|
||||
absolutePosition: '絕對位置(px)',
|
||||
toTheRightOf: '右側',
|
||||
bellow: '下方',
|
||||
options: '選項',
|
||||
moveObjectWithText: '物件隨文字移動',
|
||||
column: '欄',
|
||||
margin: '邊界',
|
||||
page: '頁面',
|
||||
line: '行',
|
||||
paragraph: '段落',
|
||||
},
|
||||
'update-status': {
|
||||
exceedMaxSize: '圖片大小超過限制, 限制為{0}M',
|
||||
invalidImageType: '圖片類型錯誤',
|
||||
exceedMaxCount: '圖片只能一次上傳{0}張',
|
||||
invalidImage: '無效圖片',
|
||||
},
|
||||
shortcut: {
|
||||
'drawing-view': '繪圖檢視',
|
||||
'drawing-move-down': '下移繪圖',
|
||||
'drawing-move-up': '上移繪圖',
|
||||
'drawing-move-left': '左移繪圖',
|
||||
'drawing-move-right': '右移繪圖',
|
||||
'drawing-delete': '刪除繪圖',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDrawingSearch } from '@univerjs/core';
|
||||
import {
|
||||
ContextService,
|
||||
DocumentDataModel,
|
||||
DrawingTypeEnum,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Injector,
|
||||
IPermissionService,
|
||||
IUniverInstanceService,
|
||||
PermissionService,
|
||||
toDisposable,
|
||||
} from '@univerjs/core';
|
||||
import { setDocumentPermissionValue } from '@univerjs/docs';
|
||||
import { DocDrawingService, IDocDrawingAdapterService, IDocDrawingService, RemoveDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { DocCanvasPopManagerService, MOBILE_DOC_ELEMENT_MENU } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { COMPONENT_MOBILE_IMAGE_POPUP_MENU, OpenImageCropOperation } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { UnitAction } from '@univerjs/protocol';
|
||||
import { IMenuManagerService } from '@univerjs/ui';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { EditDocDrawingOperation } from '../../commands/operations/edit-doc-drawing.operation';
|
||||
import { MobileDocDrawingPopupMenuController } from '../../controllers/mobile/drawing-popup-menu.controller';
|
||||
import { DocDrawingFloatingToolbarAdapterService } from '../../services/doc-drawing-floating-toolbar-adapter.service';
|
||||
import { DocDrawingPopupMenuController } from '../drawing-popup-menu.controller';
|
||||
|
||||
function createControllerHarness(drawingType = DrawingTypeEnum.DRAWING_IMAGE, mobile = false) {
|
||||
const unitId = 'doc-1';
|
||||
const drawingId = 'drawing-1';
|
||||
const injector = new Injector();
|
||||
const createControl$ = new Subject<void>();
|
||||
const clearControl$ = new Subject<boolean>();
|
||||
const changing$ = new Subject<void>();
|
||||
const focus$ = new Subject<never[]>();
|
||||
const remove$ = new Subject<IDrawingSearch[]>();
|
||||
const currentDocument$ = new Subject<never>();
|
||||
const disposedDocument$ = new Subject<never>();
|
||||
const renderCreated$ = new Subject<never>();
|
||||
const renderDisposed$ = new Subject<never>();
|
||||
const drawingObject = { oKey: drawingId };
|
||||
const selectedObjects = new Map([[drawingId, drawingObject]]);
|
||||
const popupDisposable = {
|
||||
dispose: vi.fn(),
|
||||
canDispose: () => false,
|
||||
};
|
||||
const drawing = {
|
||||
unitId,
|
||||
subUnitId: unitId,
|
||||
drawingId,
|
||||
drawingType,
|
||||
};
|
||||
const documentDataModel = new DocumentDataModel({
|
||||
id: unitId,
|
||||
body: {
|
||||
dataStream: '\b\r\n',
|
||||
customBlocks: [{ startIndex: 0, blockId: drawingId }],
|
||||
},
|
||||
});
|
||||
const transformer = {
|
||||
createControl$,
|
||||
clearControl$,
|
||||
changing$,
|
||||
getSelectedObjectMap: () => selectedObjects,
|
||||
clearSelectedObjects: vi.fn(),
|
||||
};
|
||||
const scene = {
|
||||
getAllObjects: () => [],
|
||||
getTransformerByCreate: () => transformer,
|
||||
};
|
||||
|
||||
const executeCommand = vi.fn();
|
||||
injector.add([ICommandService, {
|
||||
useValue: {
|
||||
executeCommand,
|
||||
onCommandExecuted: () => toDisposable(() => undefined),
|
||||
} as never,
|
||||
}]);
|
||||
const contextService = new ContextService();
|
||||
injector.add([IContextService, { useValue: contextService }]);
|
||||
injector.add([IPermissionService, { useClass: PermissionService }]);
|
||||
injector.add([IUniverInstanceService, {
|
||||
useValue: {
|
||||
getAllUnitsForType: () => [documentDataModel],
|
||||
getCurrentTypeOfUnit$: () => currentDocument$,
|
||||
getTypeOfUnitDisposed$: () => disposedDocument$,
|
||||
getUnit: () => documentDataModel,
|
||||
getUnitType: () => undefined,
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IRenderManagerService, {
|
||||
useValue: {
|
||||
created$: renderCreated$,
|
||||
disposed$: renderDisposed$,
|
||||
getRenderUnitById: () => ({ scene }),
|
||||
has: () => true,
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IDrawingManagerService, {
|
||||
useValue: {
|
||||
focus$,
|
||||
remove$,
|
||||
focusDrawing: vi.fn(),
|
||||
getDrawingByParam: () => drawing,
|
||||
getDrawingOKey: () => drawing,
|
||||
getFocusDrawings: () => [],
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IDocDrawingAdapterService, {
|
||||
useValue: {
|
||||
getEditDrawingCommandInfo: () => null,
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IDocDrawingService, { useClass: DocDrawingService }]);
|
||||
const attachPopupToObject = vi.fn<DocCanvasPopManagerService['attachPopupToObject']>(() => popupDisposable);
|
||||
injector.add([DocCanvasPopManagerService, {
|
||||
useValue: {
|
||||
attachPopupToObject,
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([IMenuManagerService, {
|
||||
useValue: {
|
||||
getFlatMenuByPositionKey: () => [],
|
||||
} as never,
|
||||
}]);
|
||||
injector.add([DocDrawingFloatingToolbarAdapterService]);
|
||||
if (mobile) {
|
||||
injector.add([MobileDocDrawingPopupMenuController]);
|
||||
} else {
|
||||
injector.add([DocDrawingPopupMenuController]);
|
||||
}
|
||||
const controller = mobile
|
||||
? injector.get(MobileDocDrawingPopupMenuController)
|
||||
: injector.get(DocDrawingPopupMenuController);
|
||||
|
||||
return {
|
||||
clearControl$,
|
||||
createControl$,
|
||||
contextService,
|
||||
controller,
|
||||
executeCommand,
|
||||
injector,
|
||||
attachPopupToObject,
|
||||
popupDisposable,
|
||||
selectedObjects,
|
||||
remove$,
|
||||
transformer,
|
||||
};
|
||||
}
|
||||
|
||||
describe('DocDrawingPopupMenuController', () => {
|
||||
it('keeps the mobile popup controller independent from the desktop controller', () => {
|
||||
expect(MobileDocDrawingPopupMenuController.prototype).not.toBeInstanceOf(DocDrawingPopupMenuController);
|
||||
});
|
||||
|
||||
it('does not expose mobile callbacks through the desktop popup', () => {
|
||||
const harness = createControllerHarness();
|
||||
const { controller } = harness;
|
||||
try {
|
||||
harness.createControl$.next();
|
||||
const extraProps = harness.attachPopupToObject.mock.calls[0][1].extraProps;
|
||||
|
||||
expect(extraProps).not.toHaveProperty('onClose');
|
||||
expect(extraProps).not.toHaveProperty('onEdit');
|
||||
expect(extraProps).not.toHaveProperty('onDelete');
|
||||
} finally {
|
||||
controller.dispose();
|
||||
harness.injector.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('closes a mobile popup without clearing the object being edited', () => {
|
||||
const harness = createControllerHarness(DrawingTypeEnum.DRAWING_CHART, true);
|
||||
const { controller } = harness;
|
||||
try {
|
||||
harness.createControl$.next();
|
||||
const onClose = harness.attachPopupToObject.mock.calls[0][1].extraProps?.onClose;
|
||||
expect(onClose).toBeTypeOf('function');
|
||||
if (typeof onClose === 'function') {
|
||||
onClose();
|
||||
}
|
||||
expect(harness.popupDisposable.dispose).toHaveBeenCalledOnce();
|
||||
expect(harness.selectedObjects.size).toBe(1);
|
||||
expect(harness.transformer.clearSelectedObjects).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
controller.dispose();
|
||||
harness.injector.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps generic drawing edit and delete callbacks in the mobile controller', async () => {
|
||||
const harness = createControllerHarness(DrawingTypeEnum.DRAWING_TABLE, true);
|
||||
const { controller } = harness;
|
||||
try {
|
||||
harness.createControl$.next();
|
||||
const popupOptions = harness.attachPopupToObject.mock.calls[0][1];
|
||||
const extraProps = popupOptions.extraProps as typeof popupOptions.extraProps & {
|
||||
onEdit?: () => unknown;
|
||||
onDelete?: () => unknown;
|
||||
};
|
||||
|
||||
expect(popupOptions.componentKey).toBe(MOBILE_DOC_ELEMENT_MENU);
|
||||
await extraProps?.onEdit?.();
|
||||
expect(harness.executeCommand).toHaveBeenCalledWith(EditDocDrawingOperation.id, {
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawingId: 'drawing-1',
|
||||
});
|
||||
|
||||
await extraProps?.onDelete?.();
|
||||
expect(harness.executeCommand).toHaveBeenCalledWith(RemoveDocDrawingCommand.id, {
|
||||
unitId: 'doc-1',
|
||||
drawings: [{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'drawing-1' }],
|
||||
});
|
||||
} finally {
|
||||
controller.dispose();
|
||||
harness.injector.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([false, true])('removes the floating menu and selection handles when its drawing is deleted (mobile: %s)', (mobile) => {
|
||||
const { controller, createControl$, injector, popupDisposable, remove$, transformer } = createControllerHarness(DrawingTypeEnum.DRAWING_IMAGE, mobile);
|
||||
try {
|
||||
createControl$.next();
|
||||
remove$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'another-drawing' }]);
|
||||
expect(popupDisposable.dispose).not.toHaveBeenCalled();
|
||||
remove$.next([{ unitId: 'doc-1', subUnitId: 'doc-1', drawingId: 'drawing-1' }]);
|
||||
expect(popupDisposable.dispose).toHaveBeenCalledOnce();
|
||||
expect(transformer.clearSelectedObjects).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
controller.dispose();
|
||||
injector.dispose();
|
||||
}
|
||||
});
|
||||
it.each([false, true])('keeps an active popup during refresh and removes it after the selected drawing is deleted (mobile: %s)', (mobile) => {
|
||||
const { controller, clearControl$, createControl$, injector, popupDisposable, selectedObjects } = createControllerHarness(DrawingTypeEnum.DRAWING_IMAGE, mobile);
|
||||
|
||||
try {
|
||||
createControl$.next();
|
||||
clearControl$.next(false);
|
||||
expect(popupDisposable.dispose).not.toHaveBeenCalled();
|
||||
|
||||
selectedObjects.clear();
|
||||
clearControl$.next(true);
|
||||
|
||||
expect(popupDisposable.dispose).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
controller.dispose();
|
||||
injector.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([false, true])('removes an active popup when document editing is revoked even if drawing focus is empty (mobile: %s)', (mobile) => {
|
||||
const { controller, createControl$, injector, popupDisposable } = createControllerHarness(DrawingTypeEnum.DRAWING_IMAGE, mobile);
|
||||
|
||||
try {
|
||||
createControl$.next();
|
||||
expect(popupDisposable.dispose).not.toHaveBeenCalled();
|
||||
|
||||
setDocumentPermissionValue(injector.get(IPermissionService), 'doc-1', 'doc-1', UnitAction.Edit, false);
|
||||
|
||||
expect(popupDisposable.dispose).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
controller.dispose();
|
||||
injector.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('uses the mobile drawing menu for images and charts', () => {
|
||||
const imageHarness = createControllerHarness(DrawingTypeEnum.DRAWING_IMAGE, true);
|
||||
const imageController = imageHarness.controller;
|
||||
|
||||
try {
|
||||
imageHarness.createControl$.next();
|
||||
expect(imageHarness.attachPopupToObject).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
componentKey: COMPONENT_MOBILE_IMAGE_POPUP_MENU,
|
||||
extraProps: expect.objectContaining({
|
||||
variant: 'doc-floating-toolbar',
|
||||
menuItems: [
|
||||
expect.objectContaining({ commandId: EditDocDrawingOperation.id }),
|
||||
expect.objectContaining({ commandId: OpenImageCropOperation.id }),
|
||||
expect.objectContaining({ commandId: RemoveDocDrawingCommand.id }),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
'doc-1'
|
||||
);
|
||||
} finally {
|
||||
imageController.dispose();
|
||||
imageHarness.injector.dispose();
|
||||
}
|
||||
|
||||
const chartHarness = createControllerHarness(DrawingTypeEnum.DRAWING_CHART, true);
|
||||
const chartController = chartHarness.controller;
|
||||
|
||||
try {
|
||||
chartHarness.createControl$.next();
|
||||
expect(chartHarness.attachPopupToObject).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
componentKey: COMPONENT_MOBILE_IMAGE_POPUP_MENU,
|
||||
extraProps: expect.objectContaining({
|
||||
variant: 'doc-chart-floating-toolbar',
|
||||
menuItems: [
|
||||
expect.objectContaining({ commandId: EditDocDrawingOperation.id, label: 'docs-drawing-ui.image-popup.edit' }),
|
||||
expect.objectContaining({ commandId: RemoveDocDrawingCommand.id }),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
'doc-1'
|
||||
);
|
||||
} finally {
|
||||
chartController.dispose();
|
||||
chartHarness.injector.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* 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, IDisposable, INeedCheckDisposable, Nullable } from '@univerjs/core';
|
||||
import type { BaseObject, Scene } from '@univerjs/engine-render';
|
||||
import {
|
||||
DrawingTypeEnum,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
IPermissionService,
|
||||
isInternalEditorID,
|
||||
IUniverInstanceService,
|
||||
RxDisposable,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
canEditDocumentTargets,
|
||||
getDocumentDrawingSegmentId,
|
||||
getDocumentEntityParentPermissionObjectIds,
|
||||
getDocumentEntityPermissionObjectId,
|
||||
} from '@univerjs/docs';
|
||||
import { IDocDrawingAdapterService, IDocDrawingService, RemoveDocDrawingCommand } from '@univerjs/docs-drawing';
|
||||
import { DocCanvasPopManagerService } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import {
|
||||
COMPONENT_IMAGE_POPUP_MENU,
|
||||
ImageCropperObject,
|
||||
ImageResetSizeOperation,
|
||||
OpenImageCropOperation,
|
||||
} from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { FloatingObjectToolbarPosition, IMenuManagerService, MenuItemType } from '@univerjs/ui';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import { EditDocDrawingOperation } from '../commands/operations/edit-doc-drawing.operation';
|
||||
import { SidebarDocDrawingOperation } from '../commands/operations/open-drawing-panel.operation';
|
||||
import { DocDrawingFloatingToolbarAdapterService } from '../services/doc-drawing-floating-toolbar-adapter.service';
|
||||
|
||||
interface IDrawingPopupTarget {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawingId: string;
|
||||
}
|
||||
|
||||
export class DocDrawingPopupMenuController extends RxDisposable {
|
||||
private _initImagePopupMenu = new Set<string>();
|
||||
private _embeddedRenderUnits = new Set<string>();
|
||||
private _popupMenuListeners = new Map<string, IDisposable>();
|
||||
private _disposePopupsByUnit = new Map<string, INeedCheckDisposable[]>();
|
||||
private _popupTargetsByUnit = new Map<string, IDrawingPopupTarget>();
|
||||
private _isDrawingPanelOpen = false;
|
||||
|
||||
constructor(
|
||||
@IDrawingManagerService private readonly _drawingManagerService: IDrawingManagerService,
|
||||
@Inject(DocCanvasPopManagerService) private readonly _canvasPopManagerService: DocCanvasPopManagerService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IDocDrawingAdapterService private readonly _drawingAdapterService: IDocDrawingAdapterService,
|
||||
@IDocDrawingService private readonly _docDrawingService: IDocDrawingService,
|
||||
@Inject(DocDrawingFloatingToolbarAdapterService) private readonly _floatingToolbarAdapterService: DocDrawingFloatingToolbarAdapterService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
|
||||
@IPermissionService private readonly _permissionService: IPermissionService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _init(): void {
|
||||
this.disposeWithMe(
|
||||
this._commandService.onCommandExecuted((command) => {
|
||||
const opensDrawingEditor = [...this._popupTargetsByUnit.values()].some((target) => {
|
||||
const drawing = this._docDrawingService.getDrawingByParam(target);
|
||||
const edit = drawing && this._drawingAdapterService.getEditDrawingCommandInfo({ ...target, drawing });
|
||||
return edit?.commandId === command.id;
|
||||
});
|
||||
if (command.id === EditDocDrawingOperation.id || opensDrawingEditor) {
|
||||
this._isDrawingPanelOpen = true;
|
||||
this._clearPopups(undefined, true);
|
||||
}
|
||||
if (command.id === SidebarDocDrawingOperation.id) {
|
||||
const params = command.params as { value?: string } | undefined;
|
||||
this._isDrawingPanelOpen = params?.value === 'open';
|
||||
if (this._isDrawingPanelOpen) {
|
||||
this._clearPopups(undefined, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
this._drawingManagerService.focus$.subscribe((params) => {
|
||||
if (params.length === 0) {
|
||||
this._isDrawingPanelOpen = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(this._drawingManagerService.remove$.subscribe((drawings) => {
|
||||
for (const drawing of drawings) {
|
||||
for (const [unitId, target] of this._popupTargetsByUnit) {
|
||||
if (
|
||||
target.unitId === drawing.unitId &&
|
||||
target.subUnitId === drawing.subUnitId &&
|
||||
target.drawingId === drawing.drawingId
|
||||
) {
|
||||
this._clearPopups(unitId, true);
|
||||
this._renderManagerService.getRenderUnitById(unitId)?.scene.getTransformerByCreate().clearSelectedObjects();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.disposeWithMe(this._permissionService.permissionPointUpdate$.subscribe(() => {
|
||||
for (const [popupUnitId, drawing] of this._popupTargetsByUnit) {
|
||||
if (!this._canEditDrawing(drawing.unitId, drawing.drawingId)) {
|
||||
this._clearPopups(popupUnitId, true);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(takeUntil(this.dispose$)).subscribe((documentDataModel) => this._create(documentDataModel))
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._univerInstanceService.getTypeOfUnitDisposed$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).pipe(takeUntil(this.dispose$)).subscribe((documentDataModel) => this._dispose(documentDataModel))
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._renderManagerService.created$.pipe(takeUntil(this.dispose$)).subscribe((render) => {
|
||||
if (render.type !== UniverInstanceType.UNIVER_DOC || render.isMainScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._embeddedRenderUnits.add(render.unitId);
|
||||
this._create(this._univerInstanceService.getUnit<DocumentDataModel>(render.unitId, UniverInstanceType.UNIVER_DOC));
|
||||
})
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._renderManagerService.disposed$.pipe(takeUntil(this.dispose$)).subscribe((unitId) => {
|
||||
if (!this._embeddedRenderUnits.delete(unitId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposePopupMenuListener(unitId);
|
||||
})
|
||||
);
|
||||
|
||||
this._univerInstanceService.getAllUnitsForType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC).forEach((documentDataModel) => this._create(documentDataModel));
|
||||
}
|
||||
|
||||
private _dispose(documentDataModel: DocumentDataModel) {
|
||||
const unitId = documentDataModel.getUnitId();
|
||||
this._clearPopups(unitId, true);
|
||||
this._disposePopupMenuListener(unitId);
|
||||
this._renderManagerService.removeRender(unitId);
|
||||
}
|
||||
|
||||
private _clearPopups(unitId?: string, force = false) {
|
||||
if (unitId == null) {
|
||||
[...this._disposePopupsByUnit.keys()].forEach((popupUnitId) => this._clearPopups(popupUnitId, force));
|
||||
return;
|
||||
}
|
||||
|
||||
const popups = this._disposePopupsByUnit.get(unitId);
|
||||
if (!popups) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = popups.length - 1; index >= 0; index -= 1) {
|
||||
const popup = popups[index];
|
||||
if (force || popup.canDispose()) {
|
||||
popup.dispose();
|
||||
popups.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (popups.length === 0) {
|
||||
this._disposePopupsByUnit.delete(unitId);
|
||||
this._popupTargetsByUnit.delete(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _getDisposePopups(unitId: string): INeedCheckDisposable[] {
|
||||
const popups = this._disposePopupsByUnit.get(unitId);
|
||||
if (popups) {
|
||||
return popups;
|
||||
}
|
||||
|
||||
const nextPopups: INeedCheckDisposable[] = [];
|
||||
this._disposePopupsByUnit.set(unitId, nextPopups);
|
||||
return nextPopups;
|
||||
}
|
||||
|
||||
private _create(documentDataModel: Nullable<DocumentDataModel>) {
|
||||
if (!documentDataModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = documentDataModel.getUnitId();
|
||||
if (isInternalEditorID(unitId)) {
|
||||
return;
|
||||
}
|
||||
if (this._renderManagerService.has(unitId) && !this._initImagePopupMenu.has(unitId)) {
|
||||
const listener = this._popupMenuListener(unitId);
|
||||
if (listener) {
|
||||
this._popupMenuListeners.set(unitId, listener);
|
||||
this._initImagePopupMenu.add(unitId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _disposePopupMenuListener(unitId: string) {
|
||||
this._popupMenuListeners.get(unitId)?.dispose();
|
||||
this._popupMenuListeners.delete(unitId);
|
||||
this._initImagePopupMenu.delete(unitId);
|
||||
this._clearPopups(unitId, true);
|
||||
}
|
||||
|
||||
private _hasCropObject(scene: Scene) {
|
||||
const objects = scene.getAllObjects();
|
||||
|
||||
for (const object of objects) {
|
||||
if (object instanceof ImageCropperObject) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleClearControl(
|
||||
unitId: string,
|
||||
transformer: ReturnType<Scene['getTransformerByCreate']>,
|
||||
changeSelf: boolean
|
||||
): void {
|
||||
this._clearPopups(unitId, changeSelf);
|
||||
queueMicrotask(() => {
|
||||
if (transformer.getSelectedObjectMap().size > 0) {
|
||||
return;
|
||||
}
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
this._drawingManagerService.focusDrawing(null);
|
||||
});
|
||||
}
|
||||
|
||||
private _registerDrawingPopup(
|
||||
unitId: string,
|
||||
popup: INeedCheckDisposable,
|
||||
drawing: IDrawingPopupTarget
|
||||
): void {
|
||||
this.disposeWithMe(popup);
|
||||
this._getDisposePopups(unitId).push(popup);
|
||||
this._popupTargetsByUnit.set(unitId, drawing);
|
||||
|
||||
const focusDrawings = this._drawingManagerService.getFocusDrawings();
|
||||
const alreadyFocused = focusDrawings.find((focusedDrawing) =>
|
||||
focusedDrawing.unitId === drawing.unitId &&
|
||||
focusedDrawing.subUnitId === drawing.subUnitId &&
|
||||
focusedDrawing.drawingId === drawing.drawingId
|
||||
);
|
||||
if (!alreadyFocused) {
|
||||
this._drawingManagerService.focusDrawing([drawing]);
|
||||
}
|
||||
}
|
||||
|
||||
private _isSamePopupTarget(previous: IDrawingPopupTarget | undefined, next: IDrawingPopupTarget): boolean {
|
||||
return previous?.unitId === next.unitId &&
|
||||
previous.subUnitId === next.subUnitId &&
|
||||
previous.drawingId === next.drawingId;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _popupMenuListener(unitId: string): IDisposable | undefined {
|
||||
const scene = this._renderManagerService.getRenderUnitById(unitId)?.scene;
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const subscriptions = [
|
||||
transformer.createControl$.subscribe(() => {
|
||||
if (this._hasCropObject(scene)) {
|
||||
this._clearPopups(unitId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (this._isDrawingPanelOpen || selectedObjects.size > 1) {
|
||||
this._clearPopups(unitId, this._isDrawingPanelOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) {
|
||||
this._clearPopups(unitId);
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingParam = this._drawingManagerService.getDrawingOKey(object.oKey);
|
||||
if (
|
||||
!drawingParam ||
|
||||
drawingParam.drawingType === DrawingTypeEnum.DRAWING_DOM ||
|
||||
drawingParam.drawingType === DrawingTypeEnum.DRAWING_SHAPE
|
||||
) {
|
||||
this._clearPopups(unitId);
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId: drawingUnitId, subUnitId, drawingId, drawingType } = drawingParam;
|
||||
if (!this._canEditDrawing(drawingUnitId, drawingId)) {
|
||||
this._clearPopups(unitId, true);
|
||||
return;
|
||||
}
|
||||
const disposePopups = this._disposePopupsByUnit.get(unitId);
|
||||
const previousPopupTarget = this._popupTargetsByUnit.get(unitId);
|
||||
const popupTarget = { unitId: drawingUnitId, subUnitId, drawingId };
|
||||
if (this._isSamePopupTarget(previousPopupTarget, popupTarget) && disposePopups && disposePopups.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearPopups(unitId, previousPopupTarget != null);
|
||||
const isImage = drawingType === DrawingTypeEnum.DRAWING_IMAGE;
|
||||
// Charts use the document toolbar placement, while retaining chart-specific actions and controls.
|
||||
const isChart = drawingType === DrawingTypeEnum.DRAWING_CHART;
|
||||
const popup = this._canvasPopManagerService.attachPopupToObject(
|
||||
object,
|
||||
{
|
||||
componentKey: COMPONENT_IMAGE_POPUP_MENU,
|
||||
requiresStableLayout: false,
|
||||
direction: isImage || isChart ? 'top-center' : 'horizontal',
|
||||
offset: isImage || isChart ? [0, 8] : [2, 0],
|
||||
extraProps: {
|
||||
menuItems: this._getDrawingPopupMenuItems(drawingUnitId, subUnitId, drawingId, drawingType),
|
||||
variant: isImage ? 'doc-floating-toolbar' : isChart ? 'doc-chart-floating-toolbar' : undefined,
|
||||
unitId: drawingUnitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
},
|
||||
},
|
||||
drawingUnitId
|
||||
);
|
||||
|
||||
this._registerDrawingPopup(unitId, popup, { unitId: drawingUnitId, subUnitId, drawingId });
|
||||
}),
|
||||
transformer.clearControl$.subscribe((changeSelf) => this._handleClearControl(unitId, transformer, changeSelf)),
|
||||
transformer.changing$.subscribe(() => {
|
||||
this._clearPopups(unitId, true);
|
||||
}),
|
||||
];
|
||||
const disposable = toDisposable(() => subscriptions.forEach((subscription) => subscription.unsubscribe()));
|
||||
this.disposeWithMe(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
private _canEditDrawing(unitId: string, drawingId: string): boolean {
|
||||
const documentDataModel = this._univerInstanceService.getUnit<DocumentDataModel>(
|
||||
unitId,
|
||||
UniverInstanceType.UNIVER_DOC
|
||||
);
|
||||
if (!documentDataModel) {
|
||||
return false;
|
||||
}
|
||||
const segmentId = getDocumentDrawingSegmentId(documentDataModel, drawingId);
|
||||
return canEditDocumentTargets(this._permissionService, unitId, [
|
||||
...getDocumentEntityParentPermissionObjectIds(documentDataModel, segmentId, 'drawing', drawingId),
|
||||
getDocumentEntityPermissionObjectId(segmentId, 'drawing', drawingId),
|
||||
]);
|
||||
}
|
||||
|
||||
private _getDrawingPopupMenuItems(unitId: string, subUnitId: string, drawingId: string, drawingType: number) {
|
||||
const drawing = this._docDrawingService.getDrawingByParam({ unitId, subUnitId, drawingId });
|
||||
const floatingToolbarMenuItems = drawing
|
||||
? this._floatingToolbarAdapterService.getItems({ unitId, subUnitId, drawing })
|
||||
: null;
|
||||
const editCommandInfo = drawing
|
||||
? this._drawingAdapterService.getEditDrawingCommandInfo({ unitId, subUnitId, drawing })
|
||||
: null;
|
||||
|
||||
const defaultItems = [
|
||||
{
|
||||
label: editCommandInfo?.label ?? 'docs-drawing-ui.image-popup.edit',
|
||||
index: 0,
|
||||
commandId: editCommandInfo?.commandId ?? EditDocDrawingOperation.id,
|
||||
commandParams: editCommandInfo?.commandParams ?? { unitId, subUnitId, drawingId },
|
||||
disable: editCommandInfo?.disable ?? drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
hideOnClick: true,
|
||||
icon: 'DrawingEditIcon',
|
||||
},
|
||||
{
|
||||
label: 'docs-drawing-ui.image-popup.crop',
|
||||
index: 1,
|
||||
commandId: OpenImageCropOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
icon: 'DrawingCropIcon',
|
||||
},
|
||||
{
|
||||
label: 'docs-drawing-ui.image-popup.delete',
|
||||
index: 2,
|
||||
commandId: RemoveDocDrawingCommand.id,
|
||||
commandParams: { unitId, drawings: [{ unitId, subUnitId, drawingId }] },
|
||||
disable: false,
|
||||
icon: 'DrawingDeleteIcon',
|
||||
},
|
||||
{
|
||||
label: 'docs-drawing-ui.image-popup.reset',
|
||||
index: 3,
|
||||
commandId: ImageResetSizeOperation.id,
|
||||
commandParams: [{ unitId, subUnitId, drawingId }],
|
||||
disable: true, // TODO: @JOCS, feature is not ready.
|
||||
},
|
||||
];
|
||||
|
||||
return [...(floatingToolbarMenuItems ?? defaultItems), ...this._getFloatingObjectMenuItems()];
|
||||
}
|
||||
|
||||
private _getFloatingObjectMenuItems() {
|
||||
return this._menuManagerService
|
||||
.getFlatMenuByPositionKey(FloatingObjectToolbarPosition.DOC)
|
||||
.flatMap(({ item }, index) => {
|
||||
if (!item || item.type !== MenuItemType.BUTTON || !item.title || typeof item.icon !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
type: 'button' as const,
|
||||
label: item.title,
|
||||
index: 100 + index,
|
||||
commandId: item.commandId ?? item.id,
|
||||
commandParams: typeof item.params === 'function' ? item.params() : item.params,
|
||||
disable: false,
|
||||
icon: item.icon,
|
||||
}];
|
||||
});
|
||||
}
|
||||
}
|
||||
98
packages/docs-drawing-ui/src/menu/image.menu.ts
Normal file
98
packages/docs-drawing-ui/src/menu/image.menu.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, IAccessor } from '@univerjs/core';
|
||||
import type { IMenuItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { Observable } from 'rxjs';
|
||||
import { InsertDocImageCommand } from '../commands/commands/insert-image.command';
|
||||
|
||||
export const DOCS_IMAGE_MENU_ID = 'doc.menu.image';
|
||||
export const IMAGE_MENU_UPLOAD_FLOAT_ID = InsertDocImageCommand.id;
|
||||
|
||||
// TODO: @Jocs, remove this when cell support drawing.
|
||||
const getDisableWhenSelectionInTableObservable = (accessor: IAccessor) => {
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
return new Observable<boolean>((subscriber) => {
|
||||
const observable = docSelectionManagerService.textSelection$.subscribe(() => {
|
||||
const activeRange = docSelectionManagerService.getActiveTextRange();
|
||||
|
||||
if (activeRange) {
|
||||
const { segmentId, startOffset, endOffset } = activeRange;
|
||||
const docDataModel = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const tables = docDataModel?.getSelfOrHeaderFooterModel(segmentId)?.getBody()?.tables;
|
||||
|
||||
if (tables && tables.length) {
|
||||
if (tables.some((table) => {
|
||||
const { startIndex, endIndex } = table;
|
||||
return (startOffset >= startIndex && startOffset < endIndex) || (endOffset >= startIndex && endOffset < endIndex);
|
||||
})) {
|
||||
subscriber.next(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscriber.next(true);
|
||||
return;
|
||||
}
|
||||
|
||||
subscriber.next(false);
|
||||
});
|
||||
|
||||
return () => observable.unsubscribe();
|
||||
});
|
||||
};
|
||||
|
||||
export function ImageMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: DOCS_IMAGE_MENU_ID,
|
||||
commandId: IMAGE_MENU_UPLOAD_FLOAT_ID,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'AddImageIcon',
|
||||
tooltip: 'docs-drawing-ui.title',
|
||||
disabled$: getDisableWhenSelectionInTableObservable(accessor),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadFloatImageMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: IMAGE_MENU_UPLOAD_FLOAT_ID,
|
||||
title: 'docs-drawing-ui.upload.float',
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'AddImageIcon',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadFloatImageBelowMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: `${IMAGE_MENU_UPLOAD_FLOAT_ID}.below`,
|
||||
commandId: IMAGE_MENU_UPLOAD_FLOAT_ID,
|
||||
title: 'docs-drawing-ui.upload.float',
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'AddImageIcon',
|
||||
params: {
|
||||
paragraphMenuPlacement: 'below',
|
||||
},
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
122
packages/docs-drawing-ui/src/menu/schema.ts
Normal file
122
packages/docs-drawing-ui/src/menu/schema.ts
Normal file
@@ -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 { MenuSchemaType } from '@univerjs/ui';
|
||||
import { DOC_CONTENT_INSERT_MENU_ID, DOC_PARAGRAPH_T_INSERT_BELOW_MENU_ID, DOC_PARAGRAPH_T_INSERT_MENU_ID, EMPTY_PARAGRAPH_MENU_ID, INSERT_BELLOW_MENU_ID } from '@univerjs/docs-ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonInsertGroup } from '@univerjs/ui';
|
||||
import { InsertDocEllipseShapeCommand, InsertDocRectangleShapeCommand } from '../commands/commands/insert-shape.command';
|
||||
import {
|
||||
DOCS_IMAGE_MENU_ID,
|
||||
IMAGE_MENU_UPLOAD_FLOAT_ID,
|
||||
ImageMenuFactory,
|
||||
UploadFloatImageBelowMenuFactory,
|
||||
UploadFloatImageMenuFactory,
|
||||
} from './image.menu';
|
||||
import {
|
||||
DOCS_SHAPE_BELOW_MENU_ID,
|
||||
DOCS_SHAPE_MENU_ID,
|
||||
InsertEllipseShapeBelowMenuFactory,
|
||||
InsertEllipseShapeMenuFactory,
|
||||
InsertRectangleShapeBelowMenuFactory,
|
||||
InsertRectangleShapeMenuFactory,
|
||||
ShapeBelowMenuFactory,
|
||||
ShapeMenuFactory,
|
||||
} from './shape.menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonInsertGroup.MEDIA]: {
|
||||
[DOCS_IMAGE_MENU_ID]: {
|
||||
order: 0,
|
||||
menuItemFactory: ImageMenuFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.PARAGRAPH]: {
|
||||
[ContextMenuGroup.LAYOUT]: {
|
||||
[INSERT_BELLOW_MENU_ID]: {
|
||||
[IMAGE_MENU_UPLOAD_FLOAT_ID]: {
|
||||
order: 5,
|
||||
menuItemFactory: UploadFloatImageMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[EMPTY_PARAGRAPH_MENU_ID]: {
|
||||
[ContextMenuGroup.LAYOUT]: {
|
||||
[IMAGE_MENU_UPLOAD_FLOAT_ID]: {
|
||||
order: 5,
|
||||
menuItemFactory: UploadFloatImageMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOC_CONTENT_INSERT_MENU_ID]: {
|
||||
[ContextMenuGroup.LAYOUT]: {
|
||||
[IMAGE_MENU_UPLOAD_FLOAT_ID]: {
|
||||
order: 5,
|
||||
menuItemFactory: UploadFloatImageMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOC_PARAGRAPH_T_INSERT_MENU_ID]: {
|
||||
insert: {
|
||||
[IMAGE_MENU_UPLOAD_FLOAT_ID]: {
|
||||
order: 1,
|
||||
menuItemFactory: UploadFloatImageMenuFactory,
|
||||
},
|
||||
[DOCS_SHAPE_MENU_ID]: {
|
||||
order: 2,
|
||||
menuItemFactory: ShapeMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOC_PARAGRAPH_T_INSERT_BELOW_MENU_ID]: {
|
||||
insert: {
|
||||
[`${IMAGE_MENU_UPLOAD_FLOAT_ID}.below`]: {
|
||||
order: 1,
|
||||
menuItemFactory: UploadFloatImageBelowMenuFactory,
|
||||
},
|
||||
[DOCS_SHAPE_BELOW_MENU_ID]: {
|
||||
order: 2,
|
||||
menuItemFactory: ShapeBelowMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOCS_SHAPE_MENU_ID]: {
|
||||
shapes: {
|
||||
order: 0,
|
||||
[InsertDocRectangleShapeCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: InsertRectangleShapeMenuFactory,
|
||||
},
|
||||
[InsertDocEllipseShapeCommand.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: InsertEllipseShapeMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOCS_SHAPE_BELOW_MENU_ID]: {
|
||||
shapes: {
|
||||
order: 0,
|
||||
[`${InsertDocRectangleShapeCommand.id}.below`]: {
|
||||
order: 0,
|
||||
menuItemFactory: InsertRectangleShapeBelowMenuFactory,
|
||||
},
|
||||
[`${InsertDocEllipseShapeCommand.id}.below`]: {
|
||||
order: 1,
|
||||
menuItemFactory: InsertEllipseShapeBelowMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
94
packages/docs-drawing-ui/src/menu/shape.menu.ts
Normal file
94
packages/docs-drawing-ui/src/menu/shape.menu.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 { IMenuButtonItem, IMenuItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { UniverInstanceType } from '@univerjs/core';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import {
|
||||
InsertDocEllipseShapeCommand,
|
||||
InsertDocRectangleShapeCommand,
|
||||
} from '../commands/commands/insert-shape.command';
|
||||
|
||||
export const DOCS_SHAPE_MENU_ID = 'doc.command.menu-insert-shape';
|
||||
export const DOCS_SHAPE_BELOW_MENU_ID = 'doc.command.menu-insert-shape.below';
|
||||
|
||||
export function ShapeMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: DOCS_SHAPE_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
icon: 'ShapeIcon',
|
||||
title: 'docs-drawing-ui.shape.insert.title',
|
||||
tooltip: 'docs-drawing-ui.shape.insert.title',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function ShapeBelowMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: DOCS_SHAPE_BELOW_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
icon: 'ShapeIcon',
|
||||
title: 'docs-drawing-ui.shape.insert.title',
|
||||
tooltip: 'docs-drawing-ui.shape.insert.title',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function InsertRectangleShapeMenuFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertDocRectangleShapeCommand.id,
|
||||
title: 'docs-drawing-ui.shape.insert.rectangle',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function InsertEllipseShapeMenuFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertDocEllipseShapeCommand.id,
|
||||
title: 'docs-drawing-ui.shape.insert.ellipse',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function InsertRectangleShapeBelowMenuFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: `${InsertDocRectangleShapeCommand.id}.below`,
|
||||
commandId: InsertDocRectangleShapeCommand.id,
|
||||
title: 'docs-drawing-ui.shape.insert.rectangle',
|
||||
type: MenuItemType.BUTTON,
|
||||
params: {
|
||||
paragraphMenuPlacement: 'below',
|
||||
},
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
|
||||
export function InsertEllipseShapeBelowMenuFactory(accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: `${InsertDocEllipseShapeCommand.id}.below`,
|
||||
commandId: InsertDocEllipseShapeCommand.id,
|
||||
title: 'docs-drawing-ui.shape.insert.ellipse',
|
||||
type: MenuItemType.BUTTON,
|
||||
params: {
|
||||
paragraphMenuPlacement: 'below',
|
||||
},
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
};
|
||||
}
|
||||
104
packages/docs-drawing-ui/src/mobile-plugin.ts
Normal file
104
packages/docs-drawing-ui/src/mobile-plugin.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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 { IUniverDocsDrawingUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
|
||||
import { UniverDocsMobileUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverDrawingMobileUIPlugin } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, DOCS_DRAWING_UI_PLUGIN_CONFIG_KEY, DOCS_DRAWING_UI_PLUGIN_NAME } from './config/config';
|
||||
import { DocDrawingAddRemoveController } from './controllers/doc-drawing-notification.controller';
|
||||
import { DocDrawingPrintingController } from './controllers/doc-drawing-printing.controller';
|
||||
import { DocDrawingTransformerController } from './controllers/doc-drawing-transformer-update.controller';
|
||||
import { DocFloatDomController } from './controllers/doc-float-dom.controller';
|
||||
import { MobileComponentsController } from './controllers/mobile/components.controller';
|
||||
import { MobileDocDrawingPopupMenuController } from './controllers/mobile/drawing-popup-menu.controller';
|
||||
import { MobileDocDrawingUIController } from './controllers/mobile/ui.controller';
|
||||
import { DocDrawingTransformUpdateController } from './controllers/render-controllers/doc-drawing-transform-update.controller';
|
||||
import { DocDrawingUpdateRenderController } from './controllers/render-controllers/doc-drawing-update.render-controller';
|
||||
import { MobileDocDrawingUpdateRenderController } from './controllers/render-controllers/mobile/doc-drawing-update.render-controller';
|
||||
import { DocDrawingFloatingToolbarAdapterService } from './services/doc-drawing-floating-toolbar-adapter.service';
|
||||
import { DocRefreshDrawingsService } from './services/doc-refresh-drawings.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsDrawingPlugin,
|
||||
UniverDocsMobileUIPlugin,
|
||||
UniverDrawingMobileUIPlugin
|
||||
)
|
||||
export class UniverDocsDrawingMobileUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
static override pluginName = DOCS_DRAWING_UI_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsDrawingUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected override _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(DOCS_DRAWING_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[MobileComponentsController],
|
||||
[MobileDocDrawingUIController],
|
||||
[MobileDocDrawingPopupMenuController],
|
||||
[DocDrawingTransformerController],
|
||||
[DocDrawingAddRemoveController],
|
||||
[DocRefreshDrawingsService],
|
||||
[DocDrawingFloatingToolbarAdapterService],
|
||||
[DocFloatDomController],
|
||||
[DocDrawingPrintingController],
|
||||
];
|
||||
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
this._injector.get(MobileComponentsController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
([
|
||||
[DocDrawingUpdateRenderController, { useClass: MobileDocDrawingUpdateRenderController }],
|
||||
[DocDrawingTransformUpdateController],
|
||||
] as Dependency[]).forEach((m) => this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, m));
|
||||
|
||||
this._injector.get(DocDrawingAddRemoveController);
|
||||
this._injector.get(MobileDocDrawingUIController);
|
||||
this._injector.get(DocDrawingTransformerController);
|
||||
this._injector.get(DocDrawingPrintingController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(MobileDocDrawingPopupMenuController);
|
||||
this._injector.get(DocFloatDomController);
|
||||
}
|
||||
}
|
||||
108
packages/docs-drawing-ui/src/plugin.ts
Normal file
108
packages/docs-drawing-ui/src/plugin.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 type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverDocsDrawingUIConfig } from './config/config';
|
||||
import { DependentOn, IConfigService, Inject, Injector, merge, Plugin, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { UniverDrawingUIPlugin } from '@univerjs/drawing-ui';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, DOCS_DRAWING_UI_PLUGIN_CONFIG_KEY, DOCS_DRAWING_UI_PLUGIN_NAME } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { DocDrawingAddRemoveController } from './controllers/doc-drawing-notification.controller';
|
||||
import { DocDrawingPrintingController } from './controllers/doc-drawing-printing.controller';
|
||||
import { DocDrawingTransformerController } from './controllers/doc-drawing-transformer-update.controller';
|
||||
import { DocFloatDomController } from './controllers/doc-float-dom.controller';
|
||||
import {
|
||||
DocDrawingTransformUpdateController,
|
||||
} from './controllers/render-controllers/doc-drawing-transform-update.controller';
|
||||
import {
|
||||
DocDrawingUpdateRenderController,
|
||||
} from './controllers/render-controllers/doc-drawing-update.render-controller';
|
||||
import { DocDrawingUIController } from './controllers/ui.controller';
|
||||
import { DocDrawingPopupMenuController } from './menu/drawing-popup-menu.controller';
|
||||
import { DocDrawingFloatingToolbarAdapterService } from './services/doc-drawing-floating-toolbar-adapter.service';
|
||||
import { DocRefreshDrawingsService } from './services/doc-refresh-drawings.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverDocsDrawingPlugin,
|
||||
UniverDocsUIPlugin,
|
||||
UniverDrawingUIPlugin
|
||||
)
|
||||
export class UniverDocsDrawingUIPlugin extends Plugin {
|
||||
static override type = UniverInstanceType.UNIVER_DOC;
|
||||
static override pluginName = DOCS_DRAWING_UI_PLUGIN_NAME;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverDocsDrawingUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) protected _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
this._configService.setConfig(DOCS_DRAWING_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[ComponentsController],
|
||||
[DocDrawingUIController],
|
||||
[DocDrawingPopupMenuController],
|
||||
[DocDrawingTransformerController],
|
||||
[DocDrawingAddRemoveController],
|
||||
[DocRefreshDrawingsService],
|
||||
[DocDrawingFloatingToolbarAdapterService],
|
||||
[DocFloatDomController],
|
||||
[DocDrawingPrintingController],
|
||||
];
|
||||
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
this._injector.get(ComponentsController);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
([
|
||||
[DocDrawingUpdateRenderController],
|
||||
[DocDrawingTransformUpdateController],
|
||||
] as Dependency[]).forEach((m) => this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, m));
|
||||
|
||||
this._injector.get(DocDrawingAddRemoveController);
|
||||
this._injector.get(DocDrawingUIController);
|
||||
this._injector.get(DocDrawingTransformerController);
|
||||
this._injector.get(DocDrawingPrintingController);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
this._injector.get(DocDrawingPopupMenuController);
|
||||
this._injector.get(DocFloatDomController);
|
||||
}
|
||||
}
|
||||
@@ -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 { describe, expect, it } from 'vitest';
|
||||
import { DocDrawingFloatingToolbarAdapterService } from '../doc-drawing-floating-toolbar-adapter.service';
|
||||
|
||||
describe('DocDrawingFloatingToolbarAdapterService', () => {
|
||||
it('returns the first matching adapter items sorted by index', () => {
|
||||
const service = new DocDrawingFloatingToolbarAdapterService();
|
||||
service.registerAdapter({
|
||||
getItems: () => null,
|
||||
});
|
||||
service.registerAdapter({
|
||||
getItems: () => [
|
||||
{
|
||||
label: 'delete',
|
||||
index: 2,
|
||||
commandId: 'delete-command',
|
||||
disable: false,
|
||||
},
|
||||
{
|
||||
label: 'edit',
|
||||
index: 1,
|
||||
commandId: 'edit-command',
|
||||
disable: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(service.getItems({
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawing: { drawingId: 'drawing-1' } as never,
|
||||
})?.map((item) => item.commandId)).toEqual(['edit-command', 'delete-command']);
|
||||
});
|
||||
|
||||
it('unregisters disposed adapters', () => {
|
||||
const service = new DocDrawingFloatingToolbarAdapterService();
|
||||
const disposable = service.registerAdapter({
|
||||
getItems: () => [{
|
||||
label: 'edit',
|
||||
index: 0,
|
||||
commandId: 'edit-command',
|
||||
disable: false,
|
||||
}],
|
||||
});
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(service.getItems({
|
||||
unitId: 'doc-1',
|
||||
subUnitId: 'doc-1',
|
||||
drawing: { drawingId: 'drawing-1' } as never,
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -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 { Injector } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DocRefreshDrawingsService } from '../doc-refresh-drawings.service';
|
||||
|
||||
function createService(): DocRefreshDrawingsService {
|
||||
const injector = new Injector();
|
||||
injector.add([DocRefreshDrawingsService]);
|
||||
return injector.get(DocRefreshDrawingsService);
|
||||
}
|
||||
|
||||
describe('DocRefreshDrawingsService', () => {
|
||||
it('emits the latest document skeleton refresh request', () => {
|
||||
const service = createService();
|
||||
const values: unknown[] = [];
|
||||
const sub = service.refreshDrawings$.subscribe((value) => values.push(value));
|
||||
const skeleton = { id: 'skeleton-1' };
|
||||
|
||||
service.refreshDrawings(skeleton as never);
|
||||
service.refreshDrawings(null);
|
||||
|
||||
expect(values).toEqual([null, skeleton, null]);
|
||||
sub.unsubscribe();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDisposable } from '@univerjs/core';
|
||||
import type { IDocDrawing } from '@univerjs/docs-drawing';
|
||||
import type { IValueOption } from '@univerjs/ui';
|
||||
import { toDisposable } from '@univerjs/core';
|
||||
|
||||
export interface IDocDrawingFloatingToolbarParams {
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawing: IDocDrawing;
|
||||
}
|
||||
|
||||
export interface IDocDrawingFloatingToolbarOption {
|
||||
icon?: string;
|
||||
label: IValueOption['label'];
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface IDocDrawingFloatingToolbarBaseItem {
|
||||
label: string;
|
||||
index: number;
|
||||
commandId: string;
|
||||
commandParams?: object;
|
||||
disable: boolean;
|
||||
hideOnClick?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IDocDrawingFloatingToolbarButtonItem extends IDocDrawingFloatingToolbarBaseItem {
|
||||
type?: 'button';
|
||||
}
|
||||
|
||||
export interface IDocDrawingFloatingToolbarSelectItem extends IDocDrawingFloatingToolbarBaseItem {
|
||||
type: 'select';
|
||||
value: string;
|
||||
options: IDocDrawingFloatingToolbarOption[];
|
||||
commandParamsFactory: (value: string) => object;
|
||||
}
|
||||
|
||||
export type IDocDrawingFloatingToolbarItem =
|
||||
| IDocDrawingFloatingToolbarButtonItem
|
||||
| IDocDrawingFloatingToolbarSelectItem;
|
||||
|
||||
export interface IDocDrawingFloatingToolbarAdapter {
|
||||
getItems: (
|
||||
params: IDocDrawingFloatingToolbarParams
|
||||
) => readonly IDocDrawingFloatingToolbarItem[] | null | undefined;
|
||||
}
|
||||
|
||||
export class DocDrawingFloatingToolbarAdapterService {
|
||||
private readonly _adapters: IDocDrawingFloatingToolbarAdapter[] = [];
|
||||
|
||||
registerAdapter(adapter: IDocDrawingFloatingToolbarAdapter): IDisposable {
|
||||
this._adapters.push(adapter);
|
||||
|
||||
return toDisposable(() => {
|
||||
const index = this._adapters.indexOf(adapter);
|
||||
if (index >= 0) {
|
||||
this._adapters.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getItems(params: IDocDrawingFloatingToolbarParams): IDocDrawingFloatingToolbarItem[] | null {
|
||||
for (const adapter of this._adapters) {
|
||||
const items = adapter.getItems(params);
|
||||
if (items) {
|
||||
return [...items].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 { Nullable } from '@univerjs/core';
|
||||
import type { DocumentSkeleton } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export class DocRefreshDrawingsService {
|
||||
private readonly _refreshDrawings$ = new BehaviorSubject<Nullable<DocumentSkeleton>>(null);
|
||||
readonly refreshDrawings$ = this._refreshDrawings$.asObservable();
|
||||
|
||||
refreshDrawings(skeleton: Nullable<DocumentSkeleton>) {
|
||||
this._refreshDrawings$.next(skeleton);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IObjectPositionH, IObjectPositionV } from '@univerjs/core';
|
||||
import type { IDocumentSkeletonDrawing, IDocumentSkeletonHeaderFooter, IDocumentSkeletonPage } from '@univerjs/engine-render';
|
||||
import { ObjectRelativeFromH, ObjectRelativeFromV } from '@univerjs/core';
|
||||
|
||||
export interface IDrawingAnchorInPage {
|
||||
skeDrawing: IDocumentSkeletonDrawing;
|
||||
pageMarginTop: number;
|
||||
pageMarginLeft: number;
|
||||
}
|
||||
|
||||
export function findDrawingAnchorInPage(
|
||||
page: IDocumentSkeletonPage | IDocumentSkeletonHeaderFooter,
|
||||
drawingId: string,
|
||||
pageMarginTop: number,
|
||||
pageMarginLeft: number
|
||||
): IDrawingAnchorInPage | null {
|
||||
const skeDrawing = page.skeDrawings.get(drawingId);
|
||||
if (skeDrawing) {
|
||||
return { skeDrawing, pageMarginTop, pageMarginLeft };
|
||||
}
|
||||
|
||||
for (const table of page.skeTables.values()) {
|
||||
for (const row of table.rows) {
|
||||
for (const cell of row.cells) {
|
||||
const cellAnchor = findDrawingAnchorInPage(cell, drawingId, cell.marginTop, cell.marginLeft);
|
||||
if (cellAnchor) {
|
||||
return cellAnchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveDrawingAnchorOffsets(
|
||||
anchor: IDrawingAnchorInPage,
|
||||
positionH: IObjectPositionH,
|
||||
positionV: IObjectPositionV
|
||||
): { horizontal: number; vertical: number } {
|
||||
const { skeDrawing, pageMarginTop, pageMarginLeft } = anchor;
|
||||
let horizontal = skeDrawing.aLeft;
|
||||
let vertical = skeDrawing.aTop;
|
||||
|
||||
if (positionH.relativeFrom === ObjectRelativeFromH.MARGIN) {
|
||||
horizontal -= pageMarginLeft;
|
||||
} else if (positionH.relativeFrom === ObjectRelativeFromH.COLUMN) {
|
||||
horizontal -= skeDrawing.columnLeft;
|
||||
}
|
||||
|
||||
if (positionV.relativeFrom === ObjectRelativeFromV.PAGE) {
|
||||
vertical += pageMarginTop;
|
||||
} else if (positionV.relativeFrom === ObjectRelativeFromV.LINE) {
|
||||
vertical -= skeDrawing.lineTop;
|
||||
} else if (positionV.relativeFrom === ObjectRelativeFromV.PARAGRAPH) {
|
||||
vertical -= skeDrawing.blockAnchorTop;
|
||||
}
|
||||
|
||||
return { horizontal, vertical };
|
||||
}
|
||||
88
packages/docs-drawing-ui/src/views/DocPrintingFloatDom.tsx
Normal file
88
packages/docs-drawing-ui/src/views/DocPrintingFloatDom.tsx
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 { Injector } from '@univerjs/core';
|
||||
import type { IDocFloatDom } from '@univerjs/docs-drawing';
|
||||
import type { DocumentSkeleton, IBoundRectNoAngle, Scene } from '@univerjs/engine-render';
|
||||
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 { calcDocFloatDomPositionByRect } from '../controllers/doc-float-dom.controller';
|
||||
|
||||
export interface IPrintingFloatDomProps {
|
||||
floatDomInfos: IDocFloatDom[];
|
||||
scene: Scene;
|
||||
skeleton: DocumentSkeleton;
|
||||
unitId: string;
|
||||
offset: { x: number; y: number };
|
||||
bound: IBoundRectNoAngle;
|
||||
};
|
||||
|
||||
export const DocPrintingFloatDom = (props: IPrintingFloatDomProps) => {
|
||||
const { floatDomInfos, scene, offset, bound } = props;
|
||||
const width = bound.right - bound.left;
|
||||
const height = bound.bottom - bound.top;
|
||||
|
||||
const floatDomParams = useMemo(() =>
|
||||
floatDomInfos.map((info) => {
|
||||
const { width = 0, height = 0, left = 0, top = 0 } = info.transform!;
|
||||
const offsetBound = calcDocFloatDomPositionByRect(
|
||||
{
|
||||
left,
|
||||
right: left + width,
|
||||
top,
|
||||
bottom: top + height,
|
||||
},
|
||||
scene
|
||||
);
|
||||
|
||||
const domPos: IFloatDomLayout = offsetBound;
|
||||
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,
|
||||
};
|
||||
|
||||
return [info.drawingId, floatDom] as const;
|
||||
})
|
||||
.filter(([_, floatDom]) => !(floatDom.position.endX < 0 || floatDom.position.endY < 0 || floatDom.position.startX > width || floatDom.position.startY > height)), [floatDomInfos, scene, offset, width, height]);
|
||||
|
||||
return (
|
||||
<div className="univer-absolute univer-left-0 univer-top-0">
|
||||
{floatDomParams.map(([id, floatDom]) => (
|
||||
<PrintFloatDomSingle key={id} layer={floatDom} id={id} position={floatDom.position} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function mountDocPrintingFloatDom(props: IPrintingFloatDomProps, root: HTMLElement, injector: Injector): () => void {
|
||||
const PrintingFloatDomInjector = connectInjector(DocPrintingFloatDom, injector) as ComponentType<IPrintingFloatDomProps>;
|
||||
|
||||
render(<PrintingFloatDomInjector {...props} />, root);
|
||||
|
||||
return () => unmount(root);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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 { IDocumentData } from '@univerjs/core';
|
||||
import type { IDocFloatDom } from '@univerjs/docs-drawing';
|
||||
import type { IBoundRectNoAngle, Scene } from '@univerjs/engine-render';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createDocUiTestBed } from '../../__tests__/create-doc-ui-test-bed';
|
||||
import { mountDocPrintingFloatDom } from '../DocPrintingFloatDom';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'doc-printing-float-dom-doc';
|
||||
const PRINTING_COMPONENT_KEY = 'printing-visible-content';
|
||||
|
||||
class TestScene {
|
||||
constructor(
|
||||
private readonly _scroll = { x: 0, y: 0 },
|
||||
private readonly _scale = { x: 1, y: 1 }
|
||||
) {
|
||||
}
|
||||
|
||||
getViewport() {
|
||||
return {
|
||||
viewportScrollX: this._scroll.x,
|
||||
viewportScrollY: this._scroll.y,
|
||||
};
|
||||
}
|
||||
|
||||
getAncestorScale() {
|
||||
return {
|
||||
scaleX: this._scale.x,
|
||||
scaleY: this._scale.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function PrintingVisibleContent(props: { data?: { label?: string } }) {
|
||||
return <span>{props.data?.label}</span>;
|
||||
}
|
||||
|
||||
function createDocData(): IDocumentData {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
body: {
|
||||
dataStream: '\r\n',
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 200,
|
||||
height: 100,
|
||||
},
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
marginRight: 0,
|
||||
marginLeft: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFloatDom(drawingId: string, label: string, bound: IBoundRectNoAngle): IDocFloatDom {
|
||||
return {
|
||||
drawingId,
|
||||
unitId: UNIT_ID,
|
||||
componentKey: PRINTING_COMPONENT_KEY,
|
||||
transform: {
|
||||
left: bound.left,
|
||||
top: bound.top,
|
||||
width: bound.right - bound.left,
|
||||
height: bound.bottom - bound.top,
|
||||
},
|
||||
data: { label },
|
||||
} as unknown as IDocFloatDom;
|
||||
}
|
||||
|
||||
describe('DocPrintingFloatDom', () => {
|
||||
let root: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createDocUiTestBed> | undefined;
|
||||
let disposePrinting: (() => void) | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
disposePrinting?.();
|
||||
});
|
||||
root?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
currentTestBed = undefined;
|
||||
disposePrinting = undefined;
|
||||
});
|
||||
|
||||
it('prints only floating DOM content whose transformed bounds intersect the page bound', () => {
|
||||
currentTestBed = createDocUiTestBed(createDocData(), [
|
||||
[ComponentManager],
|
||||
]);
|
||||
currentTestBed.injector.get(ComponentManager).register(PRINTING_COMPONENT_KEY, PrintingVisibleContent);
|
||||
|
||||
root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
act(() => {
|
||||
disposePrinting = mountDocPrintingFloatDom({
|
||||
floatDomInfos: [
|
||||
createFloatDom('inside-image', 'Inside printable page', { left: 20, top: 10, right: 70, bottom: 40 }),
|
||||
createFloatDom('outside-image', 'Outside printable page', { left: 240, top: 10, right: 300, bottom: 40 }),
|
||||
],
|
||||
scene: new TestScene() as unknown as Scene,
|
||||
skeleton: {} as never,
|
||||
unitId: UNIT_ID,
|
||||
offset: { x: 0, y: 0 },
|
||||
bound: { left: 0, top: 0, right: 200, bottom: 100 },
|
||||
}, root!, currentTestBed!.injector);
|
||||
});
|
||||
|
||||
expect(root.textContent).toContain('Inside printable page');
|
||||
expect(root.textContent).not.toContain('Outside printable page');
|
||||
|
||||
act(() => {
|
||||
disposePrinting?.();
|
||||
disposePrinting = undefined;
|
||||
});
|
||||
|
||||
expect(root.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('prints floating DOM content based on viewport scroll and ancestor scale', () => {
|
||||
currentTestBed = createDocUiTestBed(createDocData(), [
|
||||
[ComponentManager],
|
||||
]);
|
||||
currentTestBed.injector.get(ComponentManager).register(PRINTING_COMPONENT_KEY, PrintingVisibleContent);
|
||||
|
||||
root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
act(() => {
|
||||
disposePrinting = mountDocPrintingFloatDom({
|
||||
floatDomInfos: [
|
||||
createFloatDom('visible-after-scroll', 'Visible after scroll and zoom', { left: 110, top: 60, right: 130, bottom: 80 }),
|
||||
createFloatDom('before-scroll', 'Before scrolled viewport', { left: 0, top: 60, right: 20, bottom: 80 }),
|
||||
createFloatDom('below-scaled-page', 'Below scaled page', { left: 120, top: 140, right: 150, bottom: 170 }),
|
||||
],
|
||||
scene: new TestScene({ x: 100, y: 50 }, { x: 2, y: 1.5 }) as unknown as Scene,
|
||||
skeleton: {} as never,
|
||||
unitId: UNIT_ID,
|
||||
offset: { x: 0, y: 0 },
|
||||
bound: { left: 0, top: 0, right: 200, bottom: 100 },
|
||||
}, root!, currentTestBed!.injector);
|
||||
});
|
||||
|
||||
expect(root.textContent).toContain('Visible after scroll and zoom');
|
||||
expect(root.textContent).not.toContain('Before scrolled viewport');
|
||||
expect(root.textContent).not.toContain('Below scaled page');
|
||||
});
|
||||
|
||||
it('prints floating DOM content that partially intersects the page edge', () => {
|
||||
currentTestBed = createDocUiTestBed(createDocData(), [
|
||||
[ComponentManager],
|
||||
]);
|
||||
currentTestBed.injector.get(ComponentManager).register(PRINTING_COMPONENT_KEY, PrintingVisibleContent);
|
||||
|
||||
root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
|
||||
act(() => {
|
||||
disposePrinting = mountDocPrintingFloatDom({
|
||||
floatDomInfos: [
|
||||
createFloatDom('partially-visible-left', 'Partially visible on left edge', { left: -20, top: 10, right: 20, bottom: 40 }),
|
||||
createFloatDom('partially-visible-bottom', 'Partially visible on bottom edge', { left: 40, top: 80, right: 80, bottom: 120 }),
|
||||
createFloatDom('fully-before-page', 'Fully before printable page', { left: -60, top: 10, right: -10, bottom: 40 }),
|
||||
],
|
||||
scene: new TestScene() as unknown as Scene,
|
||||
skeleton: {} as never,
|
||||
unitId: UNIT_ID,
|
||||
offset: { x: 0, y: 0 },
|
||||
bound: { left: 0, top: 0, right: 200, bottom: 100 },
|
||||
}, root!, currentTestBed!.injector);
|
||||
});
|
||||
|
||||
expect(root.textContent).toContain('Partially visible on left edge');
|
||||
expect(root.textContent).toContain('Partially visible on bottom edge');
|
||||
expect(root.textContent).not.toContain('Fully before printable page');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DrawingCommonPanel } from '@univerjs/drawing-ui';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { DocDrawingPosition } from './DocDrawingPosition';
|
||||
import { DocDrawingTextWrap } from './DocDrawingTextWrap';
|
||||
|
||||
export const DocDrawingPanel = () => {
|
||||
const drawingManagerService = useDependency(IDrawingManagerService);
|
||||
const drawings = useObservable(
|
||||
() => drawingManagerService.focus$,
|
||||
drawingManagerService.getFocusDrawings(),
|
||||
false,
|
||||
[drawingManagerService]
|
||||
);
|
||||
|
||||
return !!drawings?.length && (
|
||||
<div className="univer-text-sm">
|
||||
<DrawingCommonPanel drawings={drawings} hasAlign={false} hasCropper hasGroup={false} hasTransform={false} />
|
||||
<DocDrawingTextWrap drawings={drawings} />
|
||||
<DocDrawingPosition drawings={drawings} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { IDrawingParam } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
|
||||
import { Checkbox, clsx, InputNumber, Select } from '@univerjs/design';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useDocDrawingPosition } from './use-doc-drawing-position';
|
||||
|
||||
export interface IDocDrawingPositionProps {
|
||||
drawings: IDrawingParam[];
|
||||
}
|
||||
|
||||
export const DocDrawingPosition = (props: IDocDrawingPositionProps) => {
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const drawingParam = props.drawings[0];
|
||||
const scene = drawingParam ? renderManagerService.getRenderUnitById(drawingParam.unitId)?.scene : undefined;
|
||||
|
||||
return drawingParam && scene ? <DocDrawingPositionContent {...props} /> : null;
|
||||
};
|
||||
|
||||
function DocDrawingPositionContent(props: IDocDrawingPositionProps) {
|
||||
const { localeService, showPanel, disabled, hPosition, vPosition, followTextMove, HORIZONTAL_RELATIVE_FROM, VERTICAL_RELATIVE_FROM, handlePositionChange, handleHorizontalRelativeFromChange, handleVerticalRelativeFromChange, handleFollowTextMoveCheck, MIN_OFFSET, MAX_OFFSET } = useDocDrawingPosition(props);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('univer-grid univer-gap-2 univer-py-2 univer-text-gray-400', {
|
||||
'univer-hidden': !showPanel,
|
||||
})}
|
||||
>
|
||||
<header
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.title')}</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.horizontal')}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-2 univer-gap-2
|
||||
[&>div]:univer-grid [&>div]:univer-gap-2
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.absolutePosition')}</span>
|
||||
<InputNumber
|
||||
min={MIN_OFFSET}
|
||||
max={MAX_OFFSET}
|
||||
precision={1}
|
||||
disabled={disabled}
|
||||
value={hPosition.posOffset}
|
||||
onChange={(val) => {
|
||||
if (val == null) {
|
||||
return;
|
||||
}
|
||||
handlePositionChange('positionH', {
|
||||
relativeFrom: hPosition.relativeFrom,
|
||||
posOffset: val,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.toTheRightOf')}</span>
|
||||
<Select
|
||||
value={String(hPosition.relativeFrom)}
|
||||
disabled={disabled}
|
||||
options={HORIZONTAL_RELATIVE_FROM}
|
||||
onChange={handleHorizontalRelativeFromChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.vertical')}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-2 univer-gap-2
|
||||
[&>div]:univer-grid [&>div]:univer-gap-2
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.absolutePosition')}</span>
|
||||
<InputNumber
|
||||
min={MIN_OFFSET}
|
||||
max={MAX_OFFSET}
|
||||
precision={1}
|
||||
disabled={disabled}
|
||||
value={vPosition.posOffset}
|
||||
onChange={(val) => {
|
||||
if (val == null) {
|
||||
return;
|
||||
}
|
||||
handlePositionChange('positionV', {
|
||||
relativeFrom: vPosition.relativeFrom,
|
||||
posOffset: val,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.bellow')}</span>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
value={String(vPosition.relativeFrom)}
|
||||
options={VERTICAL_RELATIVE_FROM}
|
||||
onChange={handleVerticalRelativeFromChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-position.options')}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
checked={followTextMove}
|
||||
onChange={handleFollowTextMoveCheck}
|
||||
>
|
||||
{localeService.t<LocaleKey>('docs-drawing-ui.image-position.moveObjectWithText')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 { IDrawingParam } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import {
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { clsx, InputNumber, Radio, RadioGroup } from '@univerjs/design';
|
||||
import { TextWrappingStyle } from '@univerjs/docs-drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
import { useDocDrawingTextWrap } from './use-doc-drawing-text-wrap';
|
||||
|
||||
export interface IDocDrawingTextWrapProps {
|
||||
drawings: IDrawingParam[];
|
||||
}
|
||||
|
||||
export const DocDrawingTextWrap = (props: IDocDrawingTextWrapProps) => {
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const drawingParam = props.drawings[0];
|
||||
const scene = drawingParam ? renderManagerService.getRenderUnitById(drawingParam.unitId)?.scene : undefined;
|
||||
|
||||
return drawingParam && scene ? <DocDrawingTextWrapContent {...props} /> : null;
|
||||
};
|
||||
|
||||
function DocDrawingTextWrapContent(props: IDocDrawingTextWrapProps) {
|
||||
const { localeService, showPanel, wrappingStyle, wrapText, distToText, disableWrapText, disableDistTB, disableDistLR, handleWrappingStyleChange, handleWrapTextChange, handleDistToTextChange, MIN_MARGIN, MAX_MARGIN } = useDocDrawingTextWrap(props);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('univer-grid univer-gap-2 univer-py-2 univer-text-gray-400', {
|
||||
'univer-hidden': !showPanel,
|
||||
})}
|
||||
>
|
||||
<header
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.title')}</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.wrappingStyle')}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<RadioGroup value={wrappingStyle} onChange={handleWrappingStyleChange} direction="vertical">
|
||||
<Radio value={TextWrappingStyle.INLINE}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.inline')}</Radio>
|
||||
<Radio value={TextWrappingStyle.WRAP_SQUARE}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.square')}</Radio>
|
||||
<Radio value={TextWrappingStyle.WRAP_TOP_AND_BOTTOM}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.topAndBottom')}</Radio>
|
||||
<Radio value={TextWrappingStyle.BEHIND_TEXT}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.behindText')}</Radio>
|
||||
<Radio value={TextWrappingStyle.IN_FRONT_OF_TEXT}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.inFrontText')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.wrapText')}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<RadioGroup disabled={disableWrapText} value={String(wrapText)} onChange={handleWrapTextChange} direction="horizontal">
|
||||
<Radio value={String(WrapTextType.BOTH_SIDES)}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.bothSide')}</Radio>
|
||||
<Radio value={String(WrapTextType.LEFT)}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.leftOnly')}</Radio>
|
||||
<Radio value={String(WrapTextType.RIGHT)}>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.rightOnly')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.distanceFromText')}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-2 univer-gap-2
|
||||
[&>div]:univer-grid [&>div]:univer-gap-2
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.top')}</span>
|
||||
<InputNumber
|
||||
min={MIN_MARGIN}
|
||||
max={MAX_MARGIN}
|
||||
disabled={disableDistTB}
|
||||
precision={1}
|
||||
value={distToText.distT}
|
||||
onChange={(val) => { handleDistToTextChange(val, 'distT'); }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.left')}</span>
|
||||
<InputNumber
|
||||
min={MIN_MARGIN}
|
||||
max={MAX_MARGIN}
|
||||
disabled={disableDistLR}
|
||||
precision={1}
|
||||
value={distToText.distL}
|
||||
onChange={(val) => { handleDistToTextChange(val, 'distL'); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-2 univer-gap-2
|
||||
[&>div]:univer-grid [&>div]:univer-gap-2
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.bottom')}</span>
|
||||
<InputNumber
|
||||
min={MIN_MARGIN}
|
||||
max={MAX_MARGIN}
|
||||
disabled={disableDistTB}
|
||||
precision={1}
|
||||
value={distToText.distB}
|
||||
onChange={(val) => { handleDistToTextChange(val, 'distB'); }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.right')}</span>
|
||||
<InputNumber
|
||||
min={MIN_MARGIN}
|
||||
max={MAX_MARGIN}
|
||||
disabled={disableDistLR}
|
||||
precision={1}
|
||||
value={distToText.distR}
|
||||
onChange={(val) => { handleDistToTextChange(val, 'distR'); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { MobileActionRow } from '@univerjs/design';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DrawingCommonPanel } from '@univerjs/drawing-ui';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
import { SidebarDocDrawingOperation } from '../../commands/operations/open-drawing-panel.operation';
|
||||
import { MobileDocDrawingPosition } from './MobileDocDrawingPosition';
|
||||
import { MobileDocDrawingTextWrap } from './MobileDocDrawingTextWrap';
|
||||
|
||||
const tabs: { value: 'image' | 'wrap' | 'position'; label: LocaleKey }[] = [
|
||||
{ value: 'image', label: 'docs-drawing-ui.title' },
|
||||
{ value: 'wrap', label: 'docs-drawing-ui.image-text-wrap.title' },
|
||||
{ value: 'position', label: 'docs-drawing-ui.image-position.title' },
|
||||
];
|
||||
|
||||
export function MobileDocDrawingPanel() {
|
||||
const drawingManagerService = useDependency(IDrawingManagerService);
|
||||
const drawings = useObservable(drawingManagerService.focus$, drawingManagerService.getFocusDrawings()) ?? [];
|
||||
const localeService = useDependency(LocaleService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const [tab, setTab] = useState<'image' | 'wrap' | 'position'>('image');
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-min-w-0 univer-flex-col univer-gap-4 univer-pb-2">
|
||||
<div className="univer-grid univer-grid-cols-3 univer-gap-2" role="group">
|
||||
{tabs.map((item) => (
|
||||
<MobileActionRow
|
||||
key={item.value}
|
||||
aria-pressed={tab === item.value}
|
||||
title={localeService.t<LocaleKey>(item.label)}
|
||||
variant="subtle"
|
||||
className="
|
||||
univer-min-w-0 univer-px-2 univer-text-center univer-text-sm
|
||||
aria-pressed:!univer-bg-primary-50 aria-pressed:!univer-text-primary-600
|
||||
"
|
||||
onClick={() => setTab(item.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{tab === 'image' && (
|
||||
<DrawingCommonPanel
|
||||
drawings={drawings}
|
||||
hasAlign={false}
|
||||
hasCropper
|
||||
hasGroup={false}
|
||||
hasTransform={false}
|
||||
onCropStart={() => commandService.executeCommand(SidebarDocDrawingOperation.id, { value: 'close' })}
|
||||
/>
|
||||
)}
|
||||
{tab === 'wrap' && <MobileDocDrawingTextWrap drawings={drawings} />}
|
||||
{tab === 'position' && <MobileDocDrawingPosition drawings={drawings} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IObjectPositionH, IObjectPositionV } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { IDocDrawingPositionProps } from './DocDrawingPosition';
|
||||
import { DocumentFlavor, LocaleService } from '@univerjs/core';
|
||||
import { InputNumber, MobileActionRow } from '@univerjs/design';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { CheckMarkIcon } from '@univerjs/icons';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useDocDrawingPosition } from './use-doc-drawing-position';
|
||||
|
||||
interface IMobileDocDrawingPositionProps {
|
||||
disabled: boolean;
|
||||
followTextMove: boolean;
|
||||
disableFollowTextMove: boolean;
|
||||
hPosition: IObjectPositionH;
|
||||
vPosition: IObjectPositionV;
|
||||
horizontalOptions: { label: string; value: string }[];
|
||||
verticalOptions: { label: string; value: string; disabled?: boolean }[];
|
||||
minOffset: number;
|
||||
maxOffset: number;
|
||||
onHorizontalOffsetChange: (value: number) => void;
|
||||
onVerticalOffsetChange: (value: number) => void;
|
||||
onHorizontalReferenceChange: (value: string) => void;
|
||||
onVerticalReferenceChange: (value: string) => void;
|
||||
onFollowTextMoveChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
function MobileDocDrawingPositionControls(props: IMobileDocDrawingPositionProps) {
|
||||
const localeService = useDependency(LocaleService);
|
||||
const axes: {
|
||||
title: LocaleKey;
|
||||
reference: LocaleKey;
|
||||
position: IObjectPositionH | IObjectPositionV;
|
||||
options: { label: string; value: string; disabled?: boolean }[];
|
||||
onOffsetChange: (value: number) => void;
|
||||
onReferenceChange: (value: string) => void;
|
||||
}[] = [
|
||||
{
|
||||
title: 'docs-drawing-ui.image-position.horizontal',
|
||||
reference: 'docs-drawing-ui.image-position.toTheRightOf',
|
||||
position: props.hPosition,
|
||||
options: props.horizontalOptions,
|
||||
onOffsetChange: props.onHorizontalOffsetChange,
|
||||
onReferenceChange: props.onHorizontalReferenceChange,
|
||||
},
|
||||
{
|
||||
title: 'docs-drawing-ui.image-position.vertical',
|
||||
reference: 'docs-drawing-ui.image-position.bellow',
|
||||
position: props.vPosition,
|
||||
options: props.verticalOptions,
|
||||
onOffsetChange: props.onVerticalOffsetChange,
|
||||
onReferenceChange: props.onVerticalReferenceChange,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="univer-grid univer-gap-5">
|
||||
{axes.map((axis) => (
|
||||
<section key={axis.title} className="univer-grid univer-gap-3">
|
||||
<h3 className="univer-m-0 univer-text-sm univer-font-medium">{localeService.t<LocaleKey>(axis.title)}</h3>
|
||||
<label className="univer-grid univer-gap-2 univer-text-sm">
|
||||
{localeService.t<LocaleKey>('docs-drawing-ui.image-position.absolutePosition')}
|
||||
<InputNumber
|
||||
disabled={props.disabled}
|
||||
className="univer-h-12 univer-w-full"
|
||||
inputClassName="[&_input]:!univer-h-12 [&_input]:!univer-text-base"
|
||||
controls={false}
|
||||
min={props.minOffset}
|
||||
max={props.maxOffset}
|
||||
precision={1}
|
||||
value={axis.position.posOffset ?? 0}
|
||||
onChange={(value) => {
|
||||
if (value != null) {
|
||||
axis.onOffsetChange(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="univer-grid univer-grid-cols-2 univer-gap-2" role="group" aria-label={localeService.t<LocaleKey>(axis.reference)}>
|
||||
{axis.options.map((option) => (
|
||||
<MobileActionRow
|
||||
key={option.value}
|
||||
title={option.label}
|
||||
variant="subtle"
|
||||
disabled={props.disabled || option.disabled}
|
||||
aria-pressed={String(axis.position.relativeFrom) === option.value}
|
||||
className="
|
||||
univer-px-3 univer-text-center univer-text-sm
|
||||
aria-pressed:!univer-bg-primary-50 aria-pressed:!univer-text-primary-600
|
||||
"
|
||||
onClick={() => axis.onReferenceChange(option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
<MobileActionRow
|
||||
role="switch"
|
||||
title={localeService.t<LocaleKey>('docs-drawing-ui.image-position.moveObjectWithText')}
|
||||
variant="subtle"
|
||||
aria-checked={props.followTextMove}
|
||||
disabled={props.disabled || props.disableFollowTextMove}
|
||||
trailing={props.followTextMove ? <CheckMarkIcon className="univer-text-primary-600" /> : undefined}
|
||||
onClick={() => props.onFollowTextMoveChange(!props.followTextMove)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function MobileDocDrawingPosition(props: IDocDrawingPositionProps) {
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const drawing = props.drawings[0];
|
||||
return drawing && renderManagerService.getRenderUnitById(drawing.unitId)?.scene
|
||||
? <MobileDocDrawingPositionContent {...props} />
|
||||
: null;
|
||||
}
|
||||
function MobileDocDrawingPositionContent(props: IDocDrawingPositionProps) {
|
||||
const { showPanel, disabled, hPosition, vPosition, followTextMove, documentFlavor, HORIZONTAL_RELATIVE_FROM, VERTICAL_RELATIVE_FROM, handlePositionChange, handleHorizontalRelativeFromChange, handleVerticalRelativeFromChange, handleFollowTextMoveCheck, MIN_OFFSET, MAX_OFFSET } = useDocDrawingPosition(props);
|
||||
return showPanel
|
||||
? (
|
||||
<MobileDocDrawingPositionControls
|
||||
disabled={disabled}
|
||||
followTextMove={followTextMove}
|
||||
disableFollowTextMove={documentFlavor === DocumentFlavor.MODERN}
|
||||
hPosition={hPosition}
|
||||
vPosition={vPosition}
|
||||
horizontalOptions={HORIZONTAL_RELATIVE_FROM}
|
||||
verticalOptions={VERTICAL_RELATIVE_FROM}
|
||||
minOffset={MIN_OFFSET}
|
||||
maxOffset={MAX_OFFSET}
|
||||
onHorizontalOffsetChange={(posOffset) => handlePositionChange('positionH', { ...hPosition, posOffset })}
|
||||
onVerticalOffsetChange={(posOffset) => handlePositionChange('positionV', { ...vPosition, posOffset })}
|
||||
onHorizontalReferenceChange={handleHorizontalRelativeFromChange}
|
||||
onVerticalReferenceChange={handleVerticalRelativeFromChange}
|
||||
onFollowTextMoveChange={handleFollowTextMoveCheck}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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 { IDocDrawingBase } from '@univerjs/core';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { IDocDrawingTextWrapProps } from './DocDrawingTextWrap';
|
||||
import { LocaleService, WrapTextType } from '@univerjs/core';
|
||||
import { InputNumber, MobileActionRow } from '@univerjs/design';
|
||||
import { TextWrappingStyle } from '@univerjs/docs-drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { CheckMarkIcon } from '@univerjs/icons';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useDocDrawingTextWrap } from './use-doc-drawing-text-wrap';
|
||||
|
||||
const styles: { value: TextWrappingStyle; label: LocaleKey }[] = [
|
||||
{ value: TextWrappingStyle.INLINE, label: 'docs-drawing-ui.image-text-wrap.inline' },
|
||||
{ value: TextWrappingStyle.WRAP_SQUARE, label: 'docs-drawing-ui.image-text-wrap.square' },
|
||||
{ value: TextWrappingStyle.WRAP_TOP_AND_BOTTOM, label: 'docs-drawing-ui.image-text-wrap.topAndBottom' },
|
||||
{ value: TextWrappingStyle.BEHIND_TEXT, label: 'docs-drawing-ui.image-text-wrap.behindText' },
|
||||
{ value: TextWrappingStyle.IN_FRONT_OF_TEXT, label: 'docs-drawing-ui.image-text-wrap.inFrontText' },
|
||||
];
|
||||
const sides: { value: WrapTextType; label: LocaleKey }[] = [
|
||||
{ value: WrapTextType.BOTH_SIDES, label: 'docs-drawing-ui.image-text-wrap.bothSide' },
|
||||
{ value: WrapTextType.LEFT, label: 'docs-drawing-ui.image-text-wrap.leftOnly' },
|
||||
{ value: WrapTextType.RIGHT, label: 'docs-drawing-ui.image-text-wrap.rightOnly' },
|
||||
];
|
||||
type DistanceKey = 'distT' | 'distB' | 'distL' | 'distR';
|
||||
const distances: { key: DistanceKey; label: LocaleKey }[] = [
|
||||
{ key: 'distT', label: 'docs-drawing-ui.image-text-wrap.top' },
|
||||
{ key: 'distB', label: 'docs-drawing-ui.image-text-wrap.bottom' },
|
||||
{ key: 'distL', label: 'docs-drawing-ui.image-text-wrap.left' },
|
||||
{ key: 'distR', label: 'docs-drawing-ui.image-text-wrap.right' },
|
||||
];
|
||||
|
||||
interface IMobileDocDrawingTextWrapProps {
|
||||
wrappingStyle: TextWrappingStyle;
|
||||
wrapText: WrapTextType;
|
||||
distToText: Pick<IDocDrawingBase, DistanceKey>;
|
||||
minMargin: number;
|
||||
maxMargin: number;
|
||||
disableWrapText: boolean;
|
||||
disableDistTB: boolean;
|
||||
disableDistLR: boolean;
|
||||
onStyleChange: (value: TextWrappingStyle) => void;
|
||||
onWrapTextChange: (value: WrapTextType) => void;
|
||||
onDistanceChange: (value: number | null, direction: DistanceKey) => void;
|
||||
}
|
||||
|
||||
function MobileDocDrawingTextWrapControls(props: IMobileDocDrawingTextWrapProps) {
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-flex-col univer-gap-5">
|
||||
<div
|
||||
role="group"
|
||||
aria-label={localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.wrappingStyle')}
|
||||
className="univer-grid univer-gap-2"
|
||||
>
|
||||
{styles.map((option) => (
|
||||
<MobileActionRow
|
||||
key={option.value}
|
||||
title={localeService.t<LocaleKey>(option.label)}
|
||||
variant="subtle"
|
||||
aria-pressed={props.wrappingStyle === option.value}
|
||||
className="aria-pressed:!univer-bg-primary-50 aria-pressed:!univer-text-primary-600"
|
||||
trailing={props.wrappingStyle === option.value ? <CheckMarkIcon /> : undefined}
|
||||
onClick={() => props.onStyleChange(option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!props.disableWrapText && (
|
||||
<section className="univer-grid univer-gap-2">
|
||||
<h3 className="univer-m-0 univer-text-sm univer-font-medium">{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.wrapText')}</h3>
|
||||
<div className="univer-grid univer-grid-cols-3 univer-gap-2">
|
||||
{sides.map((option) => (
|
||||
<MobileActionRow
|
||||
key={option.value}
|
||||
title={localeService.t<LocaleKey>(option.label)}
|
||||
variant="subtle"
|
||||
aria-pressed={props.wrapText === option.value}
|
||||
className="
|
||||
univer-px-2 univer-text-center univer-text-sm
|
||||
aria-pressed:!univer-bg-primary-50 aria-pressed:!univer-text-primary-600
|
||||
"
|
||||
onClick={() => props.onWrapTextChange(option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{(!props.disableDistTB || !props.disableDistLR) && (
|
||||
<section className="univer-grid univer-gap-3">
|
||||
<h3 className="univer-m-0 univer-text-sm univer-font-medium">{localeService.t<LocaleKey>('docs-drawing-ui.image-text-wrap.distanceFromText')}</h3>
|
||||
<div className="univer-grid univer-grid-cols-2 univer-gap-3">
|
||||
{distances.filter(({ key }) => (key === 'distT' || key === 'distB') ? !props.disableDistTB : !props.disableDistLR).map(({ key, label }) => (
|
||||
<label key={key} className="univer-grid univer-gap-2 univer-text-sm">
|
||||
{localeService.t<LocaleKey>(label)}
|
||||
<InputNumber
|
||||
className="univer-h-12 univer-w-full"
|
||||
inputClassName="[&_input]:!univer-h-12 [&_input]:!univer-text-base"
|
||||
controls={false}
|
||||
min={props.minMargin}
|
||||
max={props.maxMargin}
|
||||
precision={1}
|
||||
value={props.distToText[key] ?? 0}
|
||||
onChange={(value) => props.onDistanceChange(value, key)}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function MobileDocDrawingTextWrap(props: IDocDrawingTextWrapProps) {
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const drawing = props.drawings[0];
|
||||
return drawing && renderManagerService.getRenderUnitById(drawing.unitId)?.scene
|
||||
? <MobileDocDrawingTextWrapContent {...props} />
|
||||
: null;
|
||||
}
|
||||
function MobileDocDrawingTextWrapContent(props: IDocDrawingTextWrapProps) {
|
||||
const { showPanel, wrappingStyle, wrapText, distToText, disableWrapText, disableDistTB, disableDistLR, handleWrappingStyleChange, handleWrapTextChange, handleDistToTextChange, MIN_MARGIN, MAX_MARGIN } = useDocDrawingTextWrap(props);
|
||||
return showPanel
|
||||
? (
|
||||
<MobileDocDrawingTextWrapControls
|
||||
wrappingStyle={wrappingStyle}
|
||||
wrapText={wrapText}
|
||||
distToText={distToText}
|
||||
minMargin={MIN_MARGIN}
|
||||
maxMargin={MAX_MARGIN}
|
||||
disableWrapText={disableWrapText}
|
||||
disableDistTB={disableDistTB}
|
||||
disableDistLR={disableDistLR}
|
||||
onStyleChange={handleWrappingStyleChange}
|
||||
onWrapTextChange={handleWrapTextChange}
|
||||
onDistanceChange={handleDistToTextChange}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* 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 { DependencyIdentifier, DocumentDataModel, ICommand, IDocumentData } from '@univerjs/core';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import { within } from '@testing-library/react';
|
||||
import {
|
||||
DocumentFlavor,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { ConfigProvider } from '@univerjs/design';
|
||||
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DocDrawingController, DocDrawingService, IDocDrawingService } from '@univerjs/docs-drawing';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createDocUiTestBed } from '../../../__tests__/create-doc-ui-test-bed';
|
||||
import locale from '../../../locale/en-US';
|
||||
import { DocDrawingPosition } from '../DocDrawingPosition';
|
||||
import { MobileDocDrawingPosition } from '../MobileDocDrawingPosition';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'doc-drawing-position-doc';
|
||||
const DRAWING_ID = 'image-1';
|
||||
|
||||
class TestDocSkeletonManagerService {
|
||||
private readonly _page = { marginTop: 72 };
|
||||
private readonly _column = {
|
||||
lines: [] as Array<{
|
||||
top: number;
|
||||
paragraphIndex: number;
|
||||
paragraphStart?: boolean;
|
||||
}>,
|
||||
parent: {
|
||||
parent: this._page,
|
||||
},
|
||||
};
|
||||
|
||||
private readonly _paragraphStartLine = {
|
||||
top: 20,
|
||||
paragraphIndex: 1,
|
||||
paragraphStart: true,
|
||||
};
|
||||
|
||||
private readonly _line = {
|
||||
top: 44,
|
||||
paragraphIndex: 1,
|
||||
parent: this._column,
|
||||
};
|
||||
|
||||
private readonly _glyph = {
|
||||
parent: {
|
||||
parent: this._line,
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this._column.lines.push(this._paragraphStartLine, this._line);
|
||||
}
|
||||
|
||||
getSkeleton() {
|
||||
return {
|
||||
findNodeByCharIndex: () => this._glyph,
|
||||
getSkeletonData: () => ({
|
||||
pages: [{
|
||||
marginTop: 72,
|
||||
marginLeft: 90,
|
||||
marginBottom: 72,
|
||||
pageWidth: 594,
|
||||
pageHeight: 840,
|
||||
headerId: '',
|
||||
footerId: '',
|
||||
skeDrawings: new Map([
|
||||
[DRAWING_ID, {
|
||||
drawingId: DRAWING_ID,
|
||||
aLeft: 24,
|
||||
aTop: 42,
|
||||
columnLeft: 8,
|
||||
lineTop: 10,
|
||||
blockAnchorTop: 12,
|
||||
}],
|
||||
]),
|
||||
skeTables: new Map(),
|
||||
}],
|
||||
skeHeaders: new Map(),
|
||||
skeFooters: new Map(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel() {
|
||||
return {
|
||||
getEditArea: () => DocumentEditArea.BODY,
|
||||
reset: () => undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestDocSelectionRenderService {
|
||||
getSegment() {
|
||||
return '';
|
||||
}
|
||||
|
||||
getSegmentPage() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
blur() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class TestScene {
|
||||
getTransformerByCreate() {
|
||||
return {
|
||||
refreshControls: () => undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
private readonly _scene = new TestScene();
|
||||
private readonly _skeletonManager = new TestDocSkeletonManagerService();
|
||||
private readonly _selectionRenderService = new TestDocSelectionRenderService();
|
||||
private _render: unknown;
|
||||
|
||||
addRender(_unitId: string, render: unknown) {
|
||||
this._render = render;
|
||||
}
|
||||
|
||||
private _getRender() {
|
||||
if (this._render == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...(this._render as object),
|
||||
scene: this._scene,
|
||||
with: <T,>(token: DependencyIdentifier<T>) => {
|
||||
if (token === DocSkeletonManagerService) {
|
||||
return this._skeletonManager as T;
|
||||
}
|
||||
|
||||
if (token === DocSelectionRenderService) {
|
||||
return this._selectionRenderService as T;
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected render dependency: ${String(token)}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getRenderUnitById(_unitId: string) {
|
||||
return this._getRender();
|
||||
}
|
||||
}
|
||||
|
||||
function createDocData(): IDocumentData {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
body: {
|
||||
dataStream: '\b\r\n',
|
||||
customBlocks: [{
|
||||
startIndex: 0,
|
||||
blockId: DRAWING_ID,
|
||||
}],
|
||||
},
|
||||
drawings: {
|
||||
[DRAWING_ID]: {
|
||||
drawingId: DRAWING_ID,
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: UNIT_ID,
|
||||
drawingType: 'image',
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
wrapText: WrapTextType.BOTH_SIDES,
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 0,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PARAGRAPH,
|
||||
posOffset: 12,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
drawingsOrder: [DRAWING_ID],
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.TRADITIONAL,
|
||||
pageSize: {
|
||||
width: 594.3,
|
||||
height: 840.51,
|
||||
},
|
||||
marginTop: 72,
|
||||
marginBottom: 72,
|
||||
marginRight: 90,
|
||||
marginLeft: 90,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPositionTestBed() {
|
||||
const testBed = createDocUiTestBed(createDocData(), [
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const { injector } = testBed;
|
||||
|
||||
injector.add([DocDrawingService]);
|
||||
injector.add([IDocDrawingService, { useClass: DocDrawingService }]);
|
||||
injector.add([IDrawingManagerService, { useClass: DrawingManagerService }]);
|
||||
injector.add([DocDrawingController]);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
[
|
||||
RichTextEditingMutation as unknown as ICommand,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
injector.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: locale,
|
||||
});
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
injector.get(DocDrawingController).loadDrawingDataForUnit(UNIT_ID);
|
||||
injector.get(IDrawingManagerService).focusDrawing([{
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: UNIT_ID,
|
||||
drawingId: DRAWING_ID,
|
||||
}]);
|
||||
|
||||
return testBed;
|
||||
}
|
||||
|
||||
function renderPanel(root: Root, testBed: ReturnType<typeof createPositionTestBed>, mobile = false) {
|
||||
const Panel = mobile ? MobileDocDrawingPosition : DocDrawingPosition;
|
||||
const drawing = testBed.doc.getSnapshot().drawings![DRAWING_ID];
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<ConfigProvider mountContainer={document.body}>
|
||||
<Panel drawings={[drawing as never]} />
|
||||
</ConfigProvider>
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function currentDrawing(testBed: ReturnType<typeof createPositionTestBed>) {
|
||||
return testBed.get(IUniverInstanceService)
|
||||
.getUnit<DocumentDataModel>(UNIT_ID, UniverInstanceType.UNIVER_DOC)!
|
||||
.getSnapshot()
|
||||
.drawings![DRAWING_ID];
|
||||
}
|
||||
|
||||
function getNumberInput(container: HTMLElement, index: number): HTMLInputElement {
|
||||
return container.querySelectorAll<HTMLInputElement>('[data-u-comp="input"] input')[index];
|
||||
}
|
||||
|
||||
function getMoveWithTextInput(container: HTMLElement): HTMLInputElement {
|
||||
return container.querySelector<HTMLInputElement>('[data-u-comp="checkbox"] input')!;
|
||||
}
|
||||
|
||||
function setInputValue(input: HTMLInputElement, value: string) {
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
valueSetter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
async function selectPositionRelativeFrom(container: HTMLDivElement, selectIndex: number, optionIndex: number) {
|
||||
const select = Array.from(container.querySelectorAll('[data-u-comp="select"]'))[selectIndex] as HTMLElement | undefined;
|
||||
|
||||
expect(select).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
select!.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const option = document.querySelectorAll<HTMLElement>('[data-slot="dropdown-menu-radio-item"]')[optionIndex];
|
||||
|
||||
expect(option).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
option!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
describe('DocDrawingPosition', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createPositionTestBed> | undefined;
|
||||
|
||||
it('applies mobile position options without a dropdown and keeps selected state in sync', async () => {
|
||||
const testBed = createPositionTestBed();
|
||||
currentTestBed = testBed;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, testBed, true);
|
||||
const view = within(container);
|
||||
const marginButton = view.getAllByRole('button', { name: 'Margin' })[0];
|
||||
|
||||
await act(async () => marginButton.click());
|
||||
expect(currentDrawing(testBed).docTransform.positionH).toEqual({
|
||||
relativeFrom: ObjectRelativeFromH.MARGIN,
|
||||
posOffset: -90,
|
||||
});
|
||||
expect(marginButton.getAttribute('aria-pressed')).toBe('true');
|
||||
|
||||
await act(async () => setInputValue(getNumberInput(container!, 0), '36.5'));
|
||||
expect(currentDrawing(testBed).docTransform.positionH.posOffset).toBe(36.5);
|
||||
|
||||
const follow = view.getByRole('switch');
|
||||
await act(async () => follow.click());
|
||||
expect(follow.getAttribute('aria-checked')).toBe('false');
|
||||
expect(currentDrawing(testBed).docTransform.positionV.relativeFrom).toBe(ObjectRelativeFromV.PAGE);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('anchors the focused drawing to the page when move-with-text is turned off', async () => {
|
||||
currentTestBed = createPositionTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
const moveWithText = getMoveWithTextInput(container);
|
||||
expect(moveWithText.checked).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
moveWithText.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(moveWithText.checked).toBe(false);
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
docTransform: {
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 104,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('persists horizontal absolute position on the focused drawing', async () => {
|
||||
currentTestBed = createPositionTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
const horizontalPositionInput = getNumberInput(container, 0);
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(horizontalPositionInput, '36.5');
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 36.5,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the image visually fixed when horizontal positioning changes from page to margin', async () => {
|
||||
currentTestBed = createPositionTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
await selectPositionRelativeFrom(container, 0, 2);
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.MARGIN,
|
||||
posOffset: -90,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('persists vertical absolute position on the focused drawing', async () => {
|
||||
currentTestBed = createPositionTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
const verticalPositionInput = getNumberInput(container, 1);
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(verticalPositionInput, '18.5');
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
docTransform: {
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PARAGRAPH,
|
||||
posOffset: 18.5,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the position controls when doc drawing focus is cleared', async () => {
|
||||
currentTestBed = createPositionTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
expect(container.firstElementChild?.classList.contains('univer-hidden')).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
currentTestBed!.get(IDrawingManagerService).focusDrawing([]);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.firstElementChild?.classList.contains('univer-hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,488 @@
|
||||
/**
|
||||
* 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 { DependencyIdentifier, DocumentDataModel, ICommand, IDocumentData } from '@univerjs/core';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import { within } from '@testing-library/react';
|
||||
import {
|
||||
BooleanNumber,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { ConfigProvider } from '@univerjs/design';
|
||||
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { DocDrawingController, DocDrawingService, IDocDrawingService } from '@univerjs/docs-drawing';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { DrawingManagerService, IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { DocumentEditArea, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createDocUiTestBed } from '../../../__tests__/create-doc-ui-test-bed';
|
||||
import {
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
} from '../../../commands/commands/update-doc-drawing.command';
|
||||
import { DocDrawingAddRemoveController } from '../../../controllers/doc-drawing-notification.controller';
|
||||
import locale from '../../../locale/en-US';
|
||||
import { DocRefreshDrawingsService } from '../../../services/doc-refresh-drawings.service';
|
||||
import { DocDrawingTextWrap } from '../DocDrawingTextWrap';
|
||||
import { MobileDocDrawingTextWrap } from '../MobileDocDrawingTextWrap';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'doc-drawing-text-wrap-doc';
|
||||
const DRAWING_ID = 'image-1';
|
||||
|
||||
class TestDocSkeletonManagerService {
|
||||
getSkeleton() {
|
||||
return {
|
||||
getSkeletonData: () => ({
|
||||
pages: [{
|
||||
marginTop: 72,
|
||||
marginLeft: 90,
|
||||
marginBottom: 72,
|
||||
pageWidth: 594,
|
||||
pageHeight: 840,
|
||||
headerId: '',
|
||||
footerId: '',
|
||||
skeDrawings: new Map([
|
||||
[DRAWING_ID, {
|
||||
drawingId: DRAWING_ID,
|
||||
aLeft: 24,
|
||||
aTop: 42,
|
||||
columnLeft: 8,
|
||||
lineTop: 10,
|
||||
blockAnchorTop: 12,
|
||||
}],
|
||||
]),
|
||||
skeTables: new Map(),
|
||||
}],
|
||||
skeHeaders: new Map(),
|
||||
skeFooters: new Map(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel() {
|
||||
return {
|
||||
getEditArea: () => DocumentEditArea.BODY,
|
||||
reset: () => undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestDocSelectionRenderService {
|
||||
private _segment = '';
|
||||
private _segmentPage = 0;
|
||||
|
||||
getSegment() {
|
||||
return this._segment;
|
||||
}
|
||||
|
||||
setSegment(segment: string) {
|
||||
this._segment = segment;
|
||||
}
|
||||
|
||||
getSegmentPage() {
|
||||
return this._segmentPage;
|
||||
}
|
||||
|
||||
setSegmentPage(page: number) {
|
||||
this._segmentPage = page;
|
||||
}
|
||||
|
||||
blur() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class TestScene {
|
||||
controlRefreshCount = 0;
|
||||
|
||||
getTransformerByCreate() {
|
||||
return {
|
||||
refreshControls: () => {
|
||||
this.controlRefreshCount += 1;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
private readonly _scene = new TestScene();
|
||||
private readonly _skeletonManager = new TestDocSkeletonManagerService();
|
||||
private readonly _selectionRenderService = new TestDocSelectionRenderService();
|
||||
private _render: unknown;
|
||||
|
||||
addRender(_unitId: string, render: unknown) {
|
||||
this._render = render;
|
||||
}
|
||||
|
||||
getRenderUnitById(_unitId: string) {
|
||||
return {
|
||||
...(this._render as object),
|
||||
scene: this._scene,
|
||||
with: <T,>(token: DependencyIdentifier<T>) => {
|
||||
if (token === DocSkeletonManagerService) {
|
||||
return this._skeletonManager as T;
|
||||
}
|
||||
|
||||
if (token === DocSelectionRenderService) {
|
||||
return this._selectionRenderService as T;
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected render dependency: ${String(token)}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createDocData(): IDocumentData {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
body: {
|
||||
dataStream: '\b\r\n',
|
||||
customBlocks: [{
|
||||
startIndex: 0,
|
||||
blockId: DRAWING_ID,
|
||||
}],
|
||||
},
|
||||
drawings: {
|
||||
[DRAWING_ID]: {
|
||||
drawingId: DRAWING_ID,
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: UNIT_ID,
|
||||
drawingType: 'image',
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
wrapText: WrapTextType.BOTH_SIDES,
|
||||
distT: 1,
|
||||
distL: 2,
|
||||
distB: 3,
|
||||
distR: 4,
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 0,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 0,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
drawingsOrder: [DRAWING_ID],
|
||||
documentStyle: {
|
||||
pageSize: {
|
||||
width: 594.3,
|
||||
height: 840.51,
|
||||
},
|
||||
marginTop: 72,
|
||||
marginBottom: 72,
|
||||
marginRight: 90,
|
||||
marginLeft: 90,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPanelTestBed() {
|
||||
const testBed = createDocUiTestBed(createDocData(), [
|
||||
[IRenderManagerService, { useClass: TestRenderManagerService as never }],
|
||||
]);
|
||||
const { injector } = testBed;
|
||||
|
||||
injector.add([DocDrawingService]);
|
||||
injector.add([IDocDrawingService, { useClass: DocDrawingService }]);
|
||||
injector.add([IDrawingManagerService, { useClass: DrawingManagerService }]);
|
||||
injector.add([DocRefreshDrawingsService]);
|
||||
injector.add([DocDrawingController]);
|
||||
injector.add([DocDrawingAddRemoveController]);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
[
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
RichTextEditingMutation as unknown as ICommand,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
|
||||
injector.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: locale,
|
||||
});
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
injector.get(DocDrawingController).loadDrawingDataForUnit(UNIT_ID);
|
||||
injector.get(DocDrawingAddRemoveController);
|
||||
injector.get(IDrawingManagerService).focusDrawing([{
|
||||
unitId: UNIT_ID,
|
||||
subUnitId: UNIT_ID,
|
||||
drawingId: DRAWING_ID,
|
||||
}]);
|
||||
|
||||
return testBed;
|
||||
}
|
||||
|
||||
function renderPanel(root: Root, testBed: ReturnType<typeof createPanelTestBed>, mobile = false) {
|
||||
const Panel = mobile ? MobileDocDrawingTextWrap : DocDrawingTextWrap;
|
||||
const drawing = testBed.doc.getSnapshot().drawings![DRAWING_ID];
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<ConfigProvider mountContainer={document.body}>
|
||||
<Panel drawings={[drawing as never]} />
|
||||
</ConfigProvider>
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function currentDrawing(testBed: ReturnType<typeof createPanelTestBed>) {
|
||||
return testBed.get(IUniverInstanceService)
|
||||
.getUnit<DocumentDataModel>(UNIT_ID, UniverInstanceType.UNIVER_DOC)!
|
||||
.getSnapshot()
|
||||
.drawings![DRAWING_ID];
|
||||
}
|
||||
|
||||
function getRadioInput(container: HTMLElement, index: number): HTMLInputElement {
|
||||
return container.querySelectorAll<HTMLInputElement>('[data-u-comp="radio"] input')[index];
|
||||
}
|
||||
|
||||
function getNumberInput(container: HTMLElement, index: number): HTMLInputElement {
|
||||
return container.querySelectorAll<HTMLInputElement>('[data-u-comp="input"] input')[index];
|
||||
}
|
||||
|
||||
function setInputValue(input: HTMLInputElement, value: string) {
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
valueSetter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
describe('DocDrawingTextWrap', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createPanelTestBed> | undefined;
|
||||
|
||||
it('applies mobile wrapping and distance controls and reflects the current model', async () => {
|
||||
const testBed = createPanelTestBed();
|
||||
currentTestBed = testBed;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, testBed, true);
|
||||
const view = within(container);
|
||||
|
||||
await act(async () => {
|
||||
view.getByRole('button', { name: 'Right only' }).click();
|
||||
});
|
||||
expect(currentDrawing(testBed).wrapText).toBe(WrapTextType.RIGHT);
|
||||
expect(view.getByRole('button', { name: 'Right only' }).getAttribute('aria-pressed')).toBe('true');
|
||||
|
||||
const topInput = view.getByLabelText<HTMLInputElement>('Top(px)');
|
||||
await act(async () => setInputValue(topInput, '15'));
|
||||
expect(currentDrawing(testBed).distT).toBe(15);
|
||||
|
||||
await act(async () => {
|
||||
view.getByRole('button', { name: 'Top and Bottom' }).click();
|
||||
});
|
||||
expect(currentDrawing(testBed).layoutType).toBe(PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM);
|
||||
expect(view.queryByRole('button', { name: 'Right only' })).toBeNull();
|
||||
expect(view.queryByLabelText('Left(px)')).toBeNull();
|
||||
expect(view.getByLabelText<HTMLInputElement>('Top(px)').value).toBe('15');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('persists behind-text wrapping on the focused drawing', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
await act(async () => {
|
||||
getRadioInput(container!, 3).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: BooleanNumber.TRUE,
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 24,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 114,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('persists in-front-of-text wrapping on the focused drawing', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
await act(async () => {
|
||||
getRadioInput(container!, 4).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
layoutType: PositionedObjectLayoutType.WRAP_NONE,
|
||||
behindDoc: BooleanNumber.FALSE,
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 24,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 114,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves wrap metadata when switching to top-and-bottom text flow', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
await act(async () => {
|
||||
getRadioInput(container!, 2).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
layoutType: PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM,
|
||||
wrapText: WrapTextType.BOTH_SIDES,
|
||||
distT: 1,
|
||||
distL: 2,
|
||||
distB: 3,
|
||||
distR: 4,
|
||||
docTransform: {
|
||||
positionH: {
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 24,
|
||||
},
|
||||
positionV: {
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 114,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('persists left distance from text on the focused drawing', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
const leftDistanceInput = getNumberInput(container, 1);
|
||||
|
||||
await act(async () => {
|
||||
setInputValue(leftDistanceInput, '12.5');
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
distT: 1,
|
||||
distL: 12.5,
|
||||
distB: 3,
|
||||
distR: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables side distance controls when text only wraps above and below the drawing', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
await act(async () => {
|
||||
getRadioInput(container!, 2).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(getNumberInput(container, 0).disabled).toBe(false);
|
||||
expect(getNumberInput(container, 2).disabled).toBe(false);
|
||||
expect(getNumberInput(container, 1).disabled).toBe(true);
|
||||
expect(getNumberInput(container, 3).disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('persists right-only text wrapping on the focused drawing', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
await act(async () => {
|
||||
getRadioInput(container!, 7).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(currentDrawing(currentTestBed)).toMatchObject({
|
||||
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
|
||||
wrapText: WrapTextType.RIGHT,
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the text wrapping controls when doc drawing focus is cleared', async () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
expect(container.firstElementChild?.classList.contains('univer-hidden')).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
currentTestBed!.get(IDrawingManagerService).focusDrawing([]);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.firstElementChild?.classList.contains('univer-hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 COMPONENT_DOC_DRAWING_PANEL = 'COMPONENT_DOC_DRAWING_PANEL';
|
||||
export const MOBILE_DOC_DRAWING_PANEL_DIALOG_ID = 'docs-drawing-ui.mobile-panel';
|
||||
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* 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, ICommandInfo, IDrawingParam, IObjectPositionH, IObjectPositionV, Nullable } from '@univerjs/core';
|
||||
import type { IUpdateDrawingDocTransformCommandParams } from '@univerjs/docs-drawing';
|
||||
import type { IDocumentSkeletonDrawing } from '@univerjs/engine-render';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import type { IDocDrawingPositionProps } from './DocDrawingPosition';
|
||||
import {
|
||||
DocumentFlavor,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
ObjectRelativeFromH,
|
||||
ObjectRelativeFromV,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { DocSkeletonManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
|
||||
import { DocSelectionRenderService } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const MIN_OFFSET = -1000;
|
||||
const MAX_OFFSET = 1000;
|
||||
|
||||
export function useDocDrawingPosition(props: IDocDrawingPositionProps) {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const drawingManagerService = useDependency(IDrawingManagerService);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
const { drawings } = props;
|
||||
|
||||
const drawingParam = drawings[0];
|
||||
|
||||
const { unitId } = drawingParam;
|
||||
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
const documentFlavor = documentDataModel?.getSnapshot().documentStyle.documentFlavor;
|
||||
|
||||
const renderObject = renderManagerService.getRenderUnitById(unitId);
|
||||
const scene = renderObject!.scene!;
|
||||
const transformer = scene.getTransformerByCreate();
|
||||
|
||||
const HORIZONTAL_RELATIVE_FROM = [{
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.column'),
|
||||
value: String(ObjectRelativeFromH.COLUMN),
|
||||
}, {
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.page'),
|
||||
value: String(ObjectRelativeFromH.PAGE),
|
||||
}, {
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.margin'),
|
||||
value: String(ObjectRelativeFromH.MARGIN),
|
||||
}];
|
||||
|
||||
const VERTICAL_RELATIVE_FROM = [{
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.line'),
|
||||
value: String(ObjectRelativeFromV.LINE),
|
||||
disabled: documentFlavor === DocumentFlavor.MODERN,
|
||||
}, {
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.page'),
|
||||
value: String(ObjectRelativeFromV.PAGE),
|
||||
disabled: documentFlavor === DocumentFlavor.MODERN,
|
||||
}, {
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.margin'),
|
||||
value: String(ObjectRelativeFromV.MARGIN),
|
||||
disabled: documentFlavor === DocumentFlavor.MODERN,
|
||||
}, {
|
||||
label: localeService.t<LocaleKey>('docs-drawing-ui.image-position.paragraph'),
|
||||
value: String(ObjectRelativeFromV.PARAGRAPH),
|
||||
}];
|
||||
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const [hPosition, setHPosition] = useState<IObjectPositionH>({
|
||||
relativeFrom: ObjectRelativeFromH.PAGE,
|
||||
posOffset: 0,
|
||||
});
|
||||
const [vPosition, setVPosition] = useState<IObjectPositionV>({
|
||||
relativeFrom: ObjectRelativeFromV.PAGE,
|
||||
posOffset: 0,
|
||||
});
|
||||
const [followTextMove, setFollowTextMove] = useState(true);
|
||||
const [showPanel, setShowPanel] = useState(true);
|
||||
|
||||
function handlePositionChange(
|
||||
direction: 'positionH' | 'positionV',
|
||||
value: IObjectPositionH | IObjectPositionV
|
||||
) {
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawings = focusDrawings.map((drawing) => {
|
||||
return {
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
drawingId: drawing.drawingId,
|
||||
};
|
||||
});
|
||||
|
||||
commandService.executeCommand<IUpdateDrawingDocTransformCommandParams>(UpdateDrawingDocTransformCommand.id, {
|
||||
unitId: focusDrawings[0].unitId,
|
||||
subUnitId: focusDrawings[0].subUnitId,
|
||||
drawings: drawings.map((drawing) => ({
|
||||
drawingId: drawing.drawingId,
|
||||
key: direction,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
|
||||
const docSelectionRenderService = renderManagerService.getRenderUnitById(unitId)?.with(DocSelectionRenderService);
|
||||
|
||||
if (docSelectionRenderService) {
|
||||
docSelectionRenderService.blur();
|
||||
}
|
||||
|
||||
transformer.refreshControls();
|
||||
}
|
||||
|
||||
function handleHorizontalRelativeFromChange(value: string) {
|
||||
const prevRelativeFrom = hPosition.relativeFrom;
|
||||
const prevPosOffset = hPosition.posOffset;
|
||||
const relativeFrom = [ObjectRelativeFromH.COLUMN, ObjectRelativeFromH.PAGE, ObjectRelativeFromH.MARGIN]
|
||||
.find((reference) => String(reference) === value);
|
||||
if (relativeFrom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevRelativeFrom === relativeFrom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawingId = focusDrawings[0].drawingId;
|
||||
const unitId = focusDrawings[0].unitId;
|
||||
|
||||
let drawing: Nullable<IDocumentSkeletonDrawing> = null;
|
||||
let pageMarginLeft = 0;
|
||||
const skeleton = renderManagerService.getRenderUnitById(unitId)
|
||||
?.with(DocSkeletonManagerService)
|
||||
.getSkeleton();
|
||||
|
||||
const skeletonData = skeleton?.getSkeletonData();
|
||||
|
||||
if (skeletonData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { pages, skeHeaders, skeFooters } = skeletonData;
|
||||
|
||||
for (const page of pages) {
|
||||
const { marginLeft, skeDrawings, headerId, footerId, pageWidth } = page;
|
||||
|
||||
if (skeDrawings.has(drawingId)) {
|
||||
drawing = skeDrawings.get(drawingId);
|
||||
pageMarginLeft = marginLeft;
|
||||
break;
|
||||
}
|
||||
|
||||
const headerPage = skeHeaders.get(headerId)?.get(pageWidth);
|
||||
if (headerPage?.skeDrawings.has(drawingId)) {
|
||||
drawing = headerPage?.skeDrawings.get(drawingId);
|
||||
pageMarginLeft = marginLeft;
|
||||
break;
|
||||
}
|
||||
|
||||
const footerPage = skeFooters.get(footerId)?.get(pageWidth);
|
||||
if (footerPage?.skeDrawings.has(drawingId)) {
|
||||
drawing = footerPage?.skeDrawings.get(drawingId);
|
||||
pageMarginLeft = marginLeft;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (drawing == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = 0;
|
||||
|
||||
if (prevRelativeFrom === ObjectRelativeFromH.COLUMN) {
|
||||
delta -= drawing.columnLeft;
|
||||
} else if (prevRelativeFrom === ObjectRelativeFromH.MARGIN) {
|
||||
delta -= pageMarginLeft;
|
||||
}
|
||||
|
||||
if (relativeFrom === ObjectRelativeFromH.COLUMN) {
|
||||
delta += drawing.columnLeft;
|
||||
} else if (relativeFrom === ObjectRelativeFromH.MARGIN) {
|
||||
delta += pageMarginLeft;
|
||||
} else if (relativeFrom === ObjectRelativeFromH.PAGE) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
const newPositionH = {
|
||||
relativeFrom,
|
||||
posOffset: (prevPosOffset ?? 0) - delta,
|
||||
};
|
||||
|
||||
handlePositionChange('positionH', newPositionH);
|
||||
}
|
||||
|
||||
function handleVerticalRelativeFromChange(value: string) {
|
||||
const prevRelativeFrom = vPosition.relativeFrom;
|
||||
const prevPosOffset = vPosition.posOffset;
|
||||
const relativeFrom = [ObjectRelativeFromV.LINE, ObjectRelativeFromV.PAGE, ObjectRelativeFromV.MARGIN, ObjectRelativeFromV.PARAGRAPH]
|
||||
.find((reference) => String(reference) === value);
|
||||
if (relativeFrom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevRelativeFrom === relativeFrom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawingId, unitId } = focusDrawings[0];
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
const skeleton = renderManagerService.getRenderUnitById(unitId)
|
||||
?.with(DocSkeletonManagerService)
|
||||
.getSkeleton();
|
||||
|
||||
const docSelectionRenderService = renderManagerService.getRenderUnitById(unitId)?.with(DocSelectionRenderService);
|
||||
|
||||
const segmentId = docSelectionRenderService?.getSegment();
|
||||
const segmentPage = docSelectionRenderService?.getSegmentPage();
|
||||
|
||||
const drawing = documentDataModel?.getSelfOrHeaderFooterModel(segmentId)?.getBody()?.customBlocks?.find((c) => c.blockId === drawingId);
|
||||
|
||||
if (drawing == null || skeleton == null || docSelectionRenderService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { startIndex } = drawing;
|
||||
|
||||
const glyph = skeleton.findNodeByCharIndex(startIndex, segmentId, segmentPage);
|
||||
const line = glyph?.parent?.parent;
|
||||
const column = line?.parent;
|
||||
const paragraphStartLine = column?.lines.find((l) => l.paragraphIndex === line?.paragraphIndex && l.paragraphStart);
|
||||
const page = column?.parent?.parent;
|
||||
|
||||
if (glyph == null || line == null || paragraphStartLine == null || column == null || page == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = 0;
|
||||
|
||||
if (prevRelativeFrom === ObjectRelativeFromV.PARAGRAPH) {
|
||||
delta -= paragraphStartLine.top;
|
||||
} else if (prevRelativeFrom === ObjectRelativeFromV.LINE) {
|
||||
delta -= line.top;
|
||||
} else if (prevRelativeFrom === ObjectRelativeFromV.PAGE) {
|
||||
delta += page.marginTop;
|
||||
}
|
||||
|
||||
if (relativeFrom === ObjectRelativeFromV.PARAGRAPH) {
|
||||
delta += paragraphStartLine.top;
|
||||
} else if (relativeFrom === ObjectRelativeFromV.LINE) {
|
||||
delta += line.top;
|
||||
} else if (relativeFrom === ObjectRelativeFromV.PAGE) {
|
||||
delta -= page.marginTop;
|
||||
}
|
||||
|
||||
const newPositionV = {
|
||||
relativeFrom,
|
||||
posOffset: (prevPosOffset ?? 0) - delta,
|
||||
};
|
||||
|
||||
handlePositionChange('positionV', newPositionV);
|
||||
}
|
||||
|
||||
function handleFollowTextMoveCheck(val: string | number | boolean) {
|
||||
handleVerticalRelativeFromChange(val ? String(ObjectRelativeFromV.PARAGRAPH) : String(ObjectRelativeFromV.PAGE));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function updateState(drawingParam: IDrawingParam) {
|
||||
const snapshot = documentDataModel?.getSnapshot();
|
||||
const drawing = snapshot?.drawings?.[drawingParam.drawingId];
|
||||
if (drawing == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { layoutType } = drawing;
|
||||
const {
|
||||
positionH,
|
||||
positionV,
|
||||
} = drawing.docTransform;
|
||||
|
||||
setHPosition(positionH);
|
||||
setVPosition(positionV);
|
||||
setDisabled(layoutType === PositionedObjectLayoutType.INLINE);
|
||||
setFollowTextMove(positionV.relativeFrom === ObjectRelativeFromV.PARAGRAPH || positionV.relativeFrom === ObjectRelativeFromV.LINE);
|
||||
}
|
||||
|
||||
function updateFocusDrawingState() {
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateState(focusDrawings[0]);
|
||||
}
|
||||
|
||||
// Get the init focus drawing position.
|
||||
updateFocusDrawingState();
|
||||
|
||||
// Need to update focus drawing position when focus drawing changes.
|
||||
const subscription = drawingManagerService.focus$.subscribe((drawingParams) => {
|
||||
if (drawingParams.length === 0) {
|
||||
setShowPanel(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowPanel(true);
|
||||
updateState(drawingParams[0]);
|
||||
});
|
||||
|
||||
// Need to update focus drawing position when focus drawing wrap style changed or other edit which will affect the position.
|
||||
const mutationListener = commandService.onCommandExecuted(async (command: ICommandInfo) => {
|
||||
if (command.id === RichTextEditingMutation.id) {
|
||||
updateFocusDrawingState();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
mutationListener.dispose();
|
||||
};
|
||||
}, [commandService, documentDataModel, drawingManagerService]);
|
||||
|
||||
return {
|
||||
localeService,
|
||||
showPanel,
|
||||
disabled,
|
||||
hPosition,
|
||||
vPosition,
|
||||
followTextMove,
|
||||
documentFlavor,
|
||||
HORIZONTAL_RELATIVE_FROM,
|
||||
VERTICAL_RELATIVE_FROM,
|
||||
handlePositionChange,
|
||||
handleHorizontalRelativeFromChange,
|
||||
handleVerticalRelativeFromChange,
|
||||
handleFollowTextMoveCheck,
|
||||
MIN_OFFSET,
|
||||
MAX_OFFSET,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ICommandInfo, IDrawingParam, Nullable } from '@univerjs/core';
|
||||
import type { IDocDrawingTextWrapProps } from './DocDrawingTextWrap';
|
||||
import {
|
||||
BooleanNumber,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
WrapTextType,
|
||||
} from '@univerjs/core';
|
||||
import { RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { TextWrappingStyle, UpdateDocDrawingWrappingStyleCommand } from '@univerjs/docs-drawing';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
UpdateDocDrawingDistanceCommand,
|
||||
UpdateDocDrawingWrapTextCommand,
|
||||
} from '../../commands/commands/update-doc-drawing.command';
|
||||
|
||||
const MIN_MARGIN = 0;
|
||||
const MAX_MARGIN = 100;
|
||||
|
||||
interface IDistToText {
|
||||
distT: number;
|
||||
distL: number;
|
||||
distB: number;
|
||||
distR: number;
|
||||
}
|
||||
|
||||
export function useDocDrawingTextWrap(props: IDocDrawingTextWrapProps) {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const drawingManagerService = useDependency(IDrawingManagerService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
const { drawings } = props;
|
||||
|
||||
const drawingParam = drawings[0];
|
||||
|
||||
const { unitId } = drawingParam;
|
||||
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(unitId, UniverInstanceType.UNIVER_DOC);
|
||||
|
||||
const [disableWrapText, setDisableWrapText] = useState(true);
|
||||
const [disableDistTB, setDisableDistTB] = useState(true);
|
||||
const [disableDistLR, setDisableDistLR] = useState(true);
|
||||
const [wrappingStyle, setWrappingStyle] = useState(TextWrappingStyle.INLINE);
|
||||
const [wrapText, setWrapText] = useState(WrapTextType.BOTH_SIDES);
|
||||
const [distToText, setDistToText] = useState<IDistToText>({
|
||||
distT: 0,
|
||||
distL: 0,
|
||||
distB: 0,
|
||||
distR: 0,
|
||||
});
|
||||
const [showPanel, setShowPanel] = useState(true);
|
||||
|
||||
function handleWrappingStyleChange(value: number | string | boolean) {
|
||||
const style = Object.values(TextWrappingStyle).find((style) => style === value);
|
||||
if (style == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { unitId, subUnitId } = focusDrawings[0];
|
||||
|
||||
const drawings = focusDrawings.map(({ unitId, subUnitId, drawingId }) => ({
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawingId,
|
||||
}));
|
||||
|
||||
commandService.executeCommand(UpdateDocDrawingWrappingStyleCommand.id, {
|
||||
unitId,
|
||||
subUnitId,
|
||||
drawings,
|
||||
wrappingStyle: style,
|
||||
});
|
||||
}
|
||||
|
||||
function handleWrapTextChange(value: number | string | boolean) {
|
||||
const wrapText = [WrapTextType.BOTH_SIDES, WrapTextType.LEFT, WrapTextType.RIGHT].find((side) => String(side) === String(value));
|
||||
if (wrapText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawings = focusDrawings.map((drawing) => {
|
||||
return {
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
drawingId: drawing.drawingId,
|
||||
};
|
||||
});
|
||||
|
||||
commandService.executeCommand(UpdateDocDrawingWrapTextCommand.id, {
|
||||
unitId: focusDrawings[0].unitId,
|
||||
subUnitId: focusDrawings[0].subUnitId,
|
||||
drawings,
|
||||
wrapText,
|
||||
});
|
||||
}
|
||||
|
||||
function handleDistToTextChange(value: Nullable<number>, direction: keyof IDistToText) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawings = focusDrawings.map((drawing) => {
|
||||
return {
|
||||
unitId: drawing.unitId,
|
||||
subUnitId: drawing.subUnitId,
|
||||
drawingId: drawing.drawingId,
|
||||
};
|
||||
});
|
||||
|
||||
commandService.executeCommand(UpdateDocDrawingDistanceCommand.id, {
|
||||
unitId: focusDrawings[0].unitId,
|
||||
subUnitId: focusDrawings[0].subUnitId,
|
||||
drawings,
|
||||
dist: {
|
||||
[direction]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function updateFocusDrawingState() {
|
||||
const focusDrawings = drawingManagerService.getFocusDrawings();
|
||||
if (focusDrawings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateState(focusDrawings[0]);
|
||||
}
|
||||
|
||||
function updateState(drawingParam: IDrawingParam) {
|
||||
const drawing = documentDataModel?.getSnapshot()?.drawings?.[drawingParam.drawingId];
|
||||
if (drawing == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
distT = 0,
|
||||
distL = 0,
|
||||
distB = 0,
|
||||
distR = 0,
|
||||
layoutType = PositionedObjectLayoutType.INLINE,
|
||||
behindDoc = BooleanNumber.FALSE,
|
||||
wrapText = WrapTextType.BOTH_SIDES,
|
||||
} = drawing;
|
||||
const distToText = {
|
||||
distT,
|
||||
distL,
|
||||
distB,
|
||||
distR,
|
||||
};
|
||||
|
||||
setDistToText(distToText);
|
||||
|
||||
setWrapText(wrapText);
|
||||
|
||||
setDisableWrapText(layoutType !== PositionedObjectLayoutType.WRAP_SQUARE);
|
||||
|
||||
if (
|
||||
layoutType === PositionedObjectLayoutType.WRAP_NONE ||
|
||||
layoutType === PositionedObjectLayoutType.INLINE
|
||||
) {
|
||||
setDisableDistTB(true);
|
||||
} else {
|
||||
setDisableDistTB(false);
|
||||
}
|
||||
|
||||
if (
|
||||
layoutType === PositionedObjectLayoutType.WRAP_NONE ||
|
||||
layoutType === PositionedObjectLayoutType.INLINE ||
|
||||
layoutType === PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM
|
||||
) {
|
||||
setDisableDistLR(true);
|
||||
} else {
|
||||
setDisableDistLR(false);
|
||||
}
|
||||
|
||||
if (layoutType === PositionedObjectLayoutType.WRAP_NONE) {
|
||||
if (behindDoc === BooleanNumber.TRUE) {
|
||||
setWrappingStyle(TextWrappingStyle.BEHIND_TEXT);
|
||||
} else {
|
||||
setWrappingStyle(TextWrappingStyle.IN_FRONT_OF_TEXT);
|
||||
}
|
||||
} else {
|
||||
switch (layoutType) {
|
||||
case PositionedObjectLayoutType.INLINE:
|
||||
setWrappingStyle(TextWrappingStyle.INLINE);
|
||||
break;
|
||||
case PositionedObjectLayoutType.WRAP_SQUARE:
|
||||
setWrappingStyle(TextWrappingStyle.WRAP_SQUARE);
|
||||
break;
|
||||
case PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM:
|
||||
setWrappingStyle(TextWrappingStyle.WRAP_TOP_AND_BOTTOM);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported layout type: ${layoutType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateFocusDrawingState();
|
||||
|
||||
const subscription = drawingManagerService.focus$.subscribe((drawingParams) => {
|
||||
if (drawingParams.length === 0) {
|
||||
setShowPanel(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowPanel(true);
|
||||
updateState(drawingParams[0]);
|
||||
});
|
||||
|
||||
// Need to update focus drawing position when focus drawing wrap style changed or other edit which will affect the position.
|
||||
const mutationListener = commandService.onCommandExecuted(async (command: ICommandInfo) => {
|
||||
if (command.id === RichTextEditingMutation.id) {
|
||||
updateFocusDrawingState();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
mutationListener.dispose();
|
||||
};
|
||||
}, [commandService, documentDataModel, drawingManagerService]);
|
||||
|
||||
return {
|
||||
localeService,
|
||||
showPanel,
|
||||
wrappingStyle,
|
||||
wrapText,
|
||||
distToText,
|
||||
disableWrapText,
|
||||
disableDistTB,
|
||||
disableDistLR,
|
||||
handleWrappingStyleChange,
|
||||
handleWrapTextChange,
|
||||
handleDistToTextChange,
|
||||
MIN_MARGIN,
|
||||
MAX_MARGIN,
|
||||
};
|
||||
}
|
||||
27
packages/docs-drawing-ui/tailwind.config.ts
Normal file
27
packages/docs-drawing-ui/tailwind.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Config } from 'tailwindcss';
|
||||
import preset from '@univerjs-infra/shared/tailwind';
|
||||
|
||||
const config: Config = {
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
packages/docs-drawing-ui/tsconfig.json
Normal file
8
packages/docs-drawing-ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
18
packages/docs-drawing-ui/tsconfig.node.json
Normal file
18
packages/docs-drawing-ui/tsconfig.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/node",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/**",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
3
packages/docs-drawing-ui/vitest.config.ts
Normal file
3
packages/docs-drawing-ui/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createConfig from '@univerjs-infra/shared/vitest';
|
||||
|
||||
export default createConfig();
|
||||
Reference in New Issue
Block a user