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/slides-ui/README.md
Normal file
41
packages/slides-ui/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# @univerjs/slides-ui
|
||||
|
||||
[](https://npmjs.com/package/@univerjs/slides-ui)
|
||||
[](https://npmjs.com/package/@univerjs/slides-ui)
|
||||
[](https://npmjs.com/package/@univerjs/slides-ui)
|
||||
|
||||
`@univerjs/slides-ui` adds the presentation editor UI layer for Univer Slides.
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package | UMD global | CSS | Locales | Facade entry |
|
||||
| --- | --- | :---: | :---: | :---: |
|
||||
| `@univerjs/slides-ui` | `UniverSlidesUi` | Yes | Yes | No |
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @univerjs/slides-ui
|
||||
# or
|
||||
npm install @univerjs/slides-ui
|
||||
```
|
||||
|
||||
Keep all `@univerjs/*` packages on the same version.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import '@univerjs/slides-ui/lib/index.css';
|
||||
import EnUS from '@univerjs/slides-ui/locale/en-US';
|
||||
import { UniverSlidesUIPlugin } from '@univerjs/slides-ui';
|
||||
|
||||
univer.registerPlugin(UniverSlidesUIPlugin);
|
||||
|
||||
// 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/slides-ui)
|
||||
- [GitHub repository](https://github.com/dream-num/univer)
|
||||
96
packages/slides-ui/package.json
Normal file
96
packages/slides-ui/package.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "@univerjs/slides-ui",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": false,
|
||||
"description": "Presentation editor UI layer for Univer Slides.",
|
||||
"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",
|
||||
"slides",
|
||||
"presentation",
|
||||
"editor",
|
||||
"ui"
|
||||
],
|
||||
"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-ui": "workspace:*",
|
||||
"@univerjs/drawing": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/slides": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/slides-ui/postcss.config.mjs
Normal file
1
packages/slides-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* 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 { IPageElement, ISlideData, ISlidePage, SlideDataModel } from '@univerjs/slides';
|
||||
import {
|
||||
ICommandService,
|
||||
IImageIoService,
|
||||
ImageSourceType,
|
||||
ImageUploadStatusType,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { ObjectType } from '@univerjs/engine-render';
|
||||
import { BasicShapes, PageElementType, PageType, UniverSlidesPlugin } from '@univerjs/slides';
|
||||
import { DesktopSidebarService, ILocalFileService, ISidebarService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { CanvasView } from '../../../controllers/canvas-view';
|
||||
import { ActivateSlidePageOperation } from '../activate.operation';
|
||||
import { AppendSlideOperation } from '../append-slide.operation';
|
||||
import { DeleteSlideElementOperation } from '../delete-element.operation';
|
||||
import { InsertSlideFloatImageCommand } from '../insert-image.operation';
|
||||
import {
|
||||
InsertSlideShapeEllipseCommand,
|
||||
InsertSlideShapeEllipseOperation,
|
||||
InsertSlideShapeRectangleCommand,
|
||||
InsertSlideShapeRectangleOperation,
|
||||
ToggleSlideEditSidebarOperation,
|
||||
} from '../insert-shape.operation';
|
||||
import { SlideAddTextCommand, SlideAddTextOperation } from '../insert-text.operation';
|
||||
import { SetSlidePageThumbOperation } from '../set-thumb.operation';
|
||||
import { UpdateSlideElementOperation } from '../update-element.operation';
|
||||
|
||||
const unitId = 'slide-command-unit';
|
||||
const pageId = 'page-1';
|
||||
|
||||
class TestCanvasView {
|
||||
static thumbUnitIds: string[] = [];
|
||||
static activatedPages: string[] = [];
|
||||
static activeObjectIds: string[] = [];
|
||||
static clearedControls = 0;
|
||||
static activateCreatedObject = false;
|
||||
|
||||
constructor(@IUniverInstanceService private readonly _instanceService: IUniverInstanceService) {}
|
||||
|
||||
static reset() {
|
||||
this.thumbUnitIds = [];
|
||||
this.activatedPages = [];
|
||||
this.activeObjectIds = [];
|
||||
this.clearedControls = 0;
|
||||
this.activateCreatedObject = false;
|
||||
}
|
||||
|
||||
createObjectToPage() {
|
||||
if (TestCanvasView.activateCreatedObject) {
|
||||
return { oKey: 'created-object' } as never;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
setObjectActiveByPage(object: { oKey?: string }) {
|
||||
TestCanvasView.activeObjectIds.push(object.oKey ?? '');
|
||||
}
|
||||
|
||||
removeObjectById() {
|
||||
// render boundary is not part of the data-model behavior under test
|
||||
}
|
||||
|
||||
appendPage(unitId: string) {
|
||||
const slide = this._instanceService.getUnit<SlideDataModel>(unitId);
|
||||
const page = slide?.getBlankPage();
|
||||
if (page) {
|
||||
slide?.appendPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
activePage(pageId: string, unitId: string) {
|
||||
const slide = this._instanceService.getUnit<SlideDataModel>(unitId);
|
||||
slide?.setActivePage(slide.getPage(pageId) ?? null);
|
||||
TestCanvasView.activatedPages.push(pageId);
|
||||
}
|
||||
|
||||
getRenderUnitByPageId(pageId: string, unitId: string) {
|
||||
const slide = this._instanceService.getUnit<SlideDataModel>(unitId);
|
||||
if (!slide?.getPage(pageId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scene: {
|
||||
getTransformer: () => ({
|
||||
clearControls: () => {
|
||||
TestCanvasView.clearedControls += 1;
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
createThumbs(unitId: string) {
|
||||
TestCanvasView.thumbUnitIds.push(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
class TestLocalFileService implements ILocalFileService {
|
||||
static files: File[] = [];
|
||||
|
||||
static reset() {
|
||||
this.files = [];
|
||||
}
|
||||
|
||||
openFile(): Promise<File[]> {
|
||||
return Promise.resolve(TestLocalFileService.files);
|
||||
}
|
||||
|
||||
downloadFile(): void {
|
||||
// not used by slide image insertion
|
||||
}
|
||||
}
|
||||
|
||||
class TestImageIoService {
|
||||
saveImage(): Promise<unknown> {
|
||||
return Promise.resolve({
|
||||
imageId: 'image-1',
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
source: 'data:image/png;base64,ZmFrZQ==',
|
||||
base64Cache: 'data:image/png;base64,ZmFrZQ==',
|
||||
status: ImageUploadStatusType.SUCCUSS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TestImage {
|
||||
width = 64;
|
||||
height = 32;
|
||||
onload: (() => void) | null = null;
|
||||
onerror: ((error: unknown) => void) | null = null;
|
||||
|
||||
set src(_value: string) {
|
||||
queueMicrotask(() => this.onload?.());
|
||||
}
|
||||
}
|
||||
|
||||
function createSlideSnapshot(): Partial<ISlideData> {
|
||||
return {
|
||||
id: unitId,
|
||||
title: 'Command test deck',
|
||||
pageSize: { width: 960, height: 540 },
|
||||
body: {
|
||||
pageOrder: [pageId, 'page-2'],
|
||||
pages: {
|
||||
[pageId]: {
|
||||
id: pageId,
|
||||
pageType: PageType.SLIDE,
|
||||
zIndex: 1,
|
||||
title: 'Overview',
|
||||
description: '',
|
||||
pageBackgroundFill: { rgb: '#ffffff' },
|
||||
pageElements: {
|
||||
'title-text': {
|
||||
id: 'title-text',
|
||||
zIndex: 1,
|
||||
left: 40,
|
||||
top: 40,
|
||||
width: 400,
|
||||
height: 60,
|
||||
title: 'Title',
|
||||
description: '',
|
||||
type: PageElementType.TEXT,
|
||||
richText: { text: 'Quarterly review' },
|
||||
},
|
||||
'old-shape': {
|
||||
id: 'old-shape',
|
||||
zIndex: 2,
|
||||
left: 100,
|
||||
top: 120,
|
||||
width: 80,
|
||||
height: 80,
|
||||
title: 'Old shape',
|
||||
description: '',
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Rect,
|
||||
text: '',
|
||||
shapeProperties: {
|
||||
shapeBackgroundFill: { rgb: 'rgb(0,0,255)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'page-2': {
|
||||
id: 'page-2',
|
||||
pageType: PageType.SLIDE,
|
||||
zIndex: 2,
|
||||
title: 'Appendix',
|
||||
description: '',
|
||||
pageBackgroundFill: { rgb: '#ffffff' },
|
||||
pageElements: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getActivePage(slide: SlideDataModel): ISlidePage {
|
||||
return slide.getActivePage()!;
|
||||
}
|
||||
|
||||
function getElementIds(page: ISlidePage): string[] {
|
||||
const ids: string[] = [];
|
||||
for (const id in page.pageElements) {
|
||||
ids.push(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function findElementAddedAfter(page: ISlidePage, beforeIds: string[]): IPageElement {
|
||||
for (const id in page.pageElements) {
|
||||
if (!beforeIds.includes(id)) {
|
||||
return page.pageElements[id];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No new slide element was added.');
|
||||
}
|
||||
|
||||
describe('slide element operations', () => {
|
||||
let univer: Univer;
|
||||
let commandService: ICommandService;
|
||||
let slide: SlideDataModel;
|
||||
let originalImage: typeof Image;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
univer.registerPlugin(UniverSlidesPlugin);
|
||||
TestCanvasView.reset();
|
||||
TestLocalFileService.reset();
|
||||
originalImage = globalThis.Image;
|
||||
(globalThis as unknown as { Image: typeof Image }).Image = TestImage as unknown as typeof Image;
|
||||
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([CanvasView, { useClass: TestCanvasView as never }]);
|
||||
injector.add([ISidebarService, { useClass: DesktopSidebarService }]);
|
||||
injector.add([ILocalFileService, { useClass: TestLocalFileService }]);
|
||||
injector.add([IImageIoService, { useClass: TestImageIoService as never }]);
|
||||
|
||||
slide = univer.createUnit<ISlideData, SlideDataModel>(UniverInstanceType.UNIVER_SLIDE, createSlideSnapshot());
|
||||
injector.get(IUniverInstanceService).focusUnit(unitId);
|
||||
commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(SlideAddTextOperation);
|
||||
commandService.registerCommand(SlideAddTextCommand);
|
||||
commandService.registerCommand(InsertSlideShapeRectangleOperation);
|
||||
commandService.registerCommand(InsertSlideShapeRectangleCommand);
|
||||
commandService.registerCommand(InsertSlideShapeEllipseOperation);
|
||||
commandService.registerCommand(InsertSlideShapeEllipseCommand);
|
||||
commandService.registerCommand(UpdateSlideElementOperation);
|
||||
commandService.registerCommand(DeleteSlideElementOperation);
|
||||
commandService.registerCommand(AppendSlideOperation);
|
||||
commandService.registerCommand(ActivateSlidePageOperation);
|
||||
commandService.registerCommand(SetSlidePageThumbOperation);
|
||||
commandService.registerCommand(InsertSlideFloatImageCommand);
|
||||
commandService.registerCommand(ToggleSlideEditSidebarOperation);
|
||||
injector.get(LocaleService).load({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(globalThis as unknown as { Image: typeof Image }).Image = originalImage;
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('adds a text box to the active slide with the entered content', async () => {
|
||||
const page = getActivePage(slide);
|
||||
const beforeIds = getElementIds(page);
|
||||
|
||||
const result = await commandService.executeCommand(SlideAddTextOperation.id, {
|
||||
unitId,
|
||||
text: 'Revenue is up',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
const element = findElementAddedAfter(page, beforeIds);
|
||||
expect(element).toMatchObject({
|
||||
zIndex: 3,
|
||||
type: PageElementType.TEXT,
|
||||
richText: {
|
||||
text: 'Revenue is up',
|
||||
fs: 30,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('adds rectangle and ellipse shapes to the active slide', async () => {
|
||||
const page = getActivePage(slide);
|
||||
const beforeRectangleIds = getElementIds(page);
|
||||
|
||||
await commandService.executeCommand(InsertSlideShapeRectangleCommand.id);
|
||||
const rectangle = findElementAddedAfter(page, beforeRectangleIds);
|
||||
|
||||
const beforeEllipseIds = getElementIds(page);
|
||||
await commandService.executeCommand(InsertSlideShapeEllipseCommand.id);
|
||||
const ellipse = findElementAddedAfter(page, beforeEllipseIds);
|
||||
|
||||
expect(rectangle).toMatchObject({
|
||||
zIndex: 3,
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Rect,
|
||||
},
|
||||
});
|
||||
expect(ellipse).toMatchObject({
|
||||
zIndex: 4,
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Ellipse,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the focused slide when adding text from the user command', async () => {
|
||||
const page = getActivePage(slide);
|
||||
const beforeIds = getElementIds(page);
|
||||
|
||||
const result = await commandService.executeCommand(SlideAddTextCommand.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const element = findElementAddedAfter(page, beforeIds);
|
||||
expect(element).toMatchObject({
|
||||
type: PageElementType.TEXT,
|
||||
richText: { text: 'A New Text' },
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an existing slide element without replacing the rest of its content', async () => {
|
||||
const result = await commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
unitId,
|
||||
oKey: 'title-text',
|
||||
props: {
|
||||
left: 120,
|
||||
title: 'Updated title box',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(slide.getElement(pageId, 'title-text')).toMatchObject({
|
||||
left: 120,
|
||||
title: 'Updated title box',
|
||||
type: PageElementType.TEXT,
|
||||
richText: { text: 'Quarterly review' },
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the selected element from the active slide', async () => {
|
||||
const result = await commandService.executeCommand(DeleteSlideElementOperation.id, {
|
||||
unitId,
|
||||
id: 'old-shape',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(slide.getElement(pageId, 'old-shape')).toBeUndefined();
|
||||
expect(slide.getElement(pageId, 'title-text')).toBeDefined();
|
||||
});
|
||||
|
||||
it('appends a new page after the active slide', async () => {
|
||||
expect(slide.getPageOrder()).toEqual([pageId, 'page-2']);
|
||||
|
||||
const result = await commandService.executeCommand(AppendSlideOperation.id, { unitId });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(slide.getPageOrder()).toHaveLength(3);
|
||||
expect(slide.getPageOrder()?.[0]).toBe(pageId);
|
||||
expect(slide.getPageOrder()?.[1]).not.toBe('page-2');
|
||||
expect(slide.getPageOrder()?.[2]).toBe('page-2');
|
||||
});
|
||||
|
||||
it('activates another page after clearing the current page controls', async () => {
|
||||
const result = await commandService.executeCommand(ActivateSlidePageOperation.id, {
|
||||
unitId,
|
||||
id: 'page-2',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(slide.getActivePage()?.id).toBe('page-2');
|
||||
expect(TestCanvasView.clearedControls).toBe(1);
|
||||
expect(TestCanvasView.activatedPages).toEqual(['page-2']);
|
||||
});
|
||||
|
||||
it('does not activate a missing page renderer', async () => {
|
||||
const result = await commandService.executeCommand(ActivateSlidePageOperation.id, {
|
||||
unitId: 'missing-slide',
|
||||
id: 'page-2',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(slide.getActivePage()?.id).toBe(pageId);
|
||||
});
|
||||
|
||||
it('creates thumbnails for the current deck', async () => {
|
||||
const result = await commandService.executeCommand(SetSlidePageThumbOperation.id, { unitId });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(TestCanvasView.thumbUnitIds).toEqual([unitId]);
|
||||
});
|
||||
|
||||
it('keeps the deck unchanged when the user cancels image selection', async () => {
|
||||
const page = getActivePage(slide);
|
||||
const beforeIds = getElementIds(page);
|
||||
|
||||
const result = await commandService.executeCommand(InsertSlideFloatImageCommand.id);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(getElementIds(page)).toEqual(beforeIds);
|
||||
});
|
||||
|
||||
it('adds the selected image to the active slide and selects the created object', async () => {
|
||||
const page = getActivePage(slide);
|
||||
const beforeIds = getElementIds(page);
|
||||
TestCanvasView.activateCreatedObject = true;
|
||||
TestLocalFileService.files = [new File(['fake image'], 'chart.png', { type: 'image/png' })];
|
||||
|
||||
const result = await commandService.executeCommand(InsertSlideFloatImageCommand.id);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const element = findElementAddedAfter(page, beforeIds);
|
||||
expect(element).toMatchObject({
|
||||
id: 'image-1',
|
||||
zIndex: 3,
|
||||
width: 64,
|
||||
height: 32,
|
||||
type: PageElementType.IMAGE,
|
||||
image: {
|
||||
imageProperties: {
|
||||
imageSourceType: ImageSourceType.BASE64,
|
||||
source: 'data:image/png;base64,ZmFrZQ==',
|
||||
base64Cache: 'data:image/png;base64,ZmFrZQ==',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(TestCanvasView.activeObjectIds).toEqual(['created-object']);
|
||||
});
|
||||
|
||||
it('opens and closes the slide edit sidebar for the selected object type', async () => {
|
||||
const sidebarService = univer.__getInjector().get(ISidebarService);
|
||||
const objectTypes = [
|
||||
{ type: ObjectType.RECT, title: 'slides-ui.sidebar.shape' },
|
||||
{ type: ObjectType.IMAGE, title: 'slides-ui.sidebar.image' },
|
||||
{ type: ObjectType.RICH_TEXT, title: 'slides-ui.sidebar.text' },
|
||||
];
|
||||
|
||||
for (const objectType of objectTypes) {
|
||||
await commandService.executeCommand(ToggleSlideEditSidebarOperation.id, {
|
||||
visible: '1',
|
||||
objectType: objectType.type,
|
||||
});
|
||||
|
||||
expect(sidebarService.visible).toBe(true);
|
||||
expect(sidebarService.options.header?.title).toBe(objectType.title);
|
||||
}
|
||||
|
||||
await commandService.executeCommand(ToggleSlideEditSidebarOperation.id, {
|
||||
visible: '',
|
||||
objectType: ObjectType.RECT,
|
||||
});
|
||||
|
||||
expect(sidebarService.visible).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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, IOperation } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, IUniverInstanceService } from '@univerjs/core';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
export interface IActiveSlidePageOperationParams {
|
||||
unitId: string;
|
||||
id: string;
|
||||
}
|
||||
export const ActivateSlidePageOperation: IOperation<IActiveSlidePageOperationParams> = {
|
||||
id: 'slide.operation.activate-slide',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor: IAccessor, params: IActiveSlidePageOperationParams) => {
|
||||
const unitId = params.unitId;
|
||||
const canvasView = accessor.get(CanvasView);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const model = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const model = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
const pageId = model?.getActivePage()?.id;
|
||||
|
||||
if (!pageId) return false;
|
||||
|
||||
const page = canvasView.getRenderUnitByPageId(pageId, unitId);
|
||||
if (!page) return false;
|
||||
const transformer = page.scene?.getTransformer();
|
||||
if (transformer) {
|
||||
transformer.clearControls();
|
||||
}
|
||||
|
||||
canvasView.activePage(params.id, unitId);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IOperation } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, IUniverInstanceService } from '@univerjs/core';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
export interface IAppendSlideOperationParams {
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
export const AppendSlideOperation: IOperation<IAppendSlideOperationParams> = {
|
||||
id: 'slide.operation.append-slide',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params: IAppendSlideOperationParams) => {
|
||||
const unitId = params.unitId;
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const slideData = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
|
||||
if (!slideData) return false;
|
||||
|
||||
const canvasView = accessor.get(CanvasView);
|
||||
canvasView.appendPage(unitId);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, IUniverInstanceService } from '@univerjs/core';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
export interface IDeleteElementOperationParams {
|
||||
unitId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const DeleteSlideElementOperation: ICommand<IDeleteElementOperationParams> = {
|
||||
id: 'slide.operation.delete-element',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params) => {
|
||||
if (!params?.id) return false;
|
||||
|
||||
const unitId = params.unitId;
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const slideData = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
|
||||
if (!slideData) return false;
|
||||
|
||||
const activePage = slideData.getActivePage()!;
|
||||
|
||||
delete activePage.pageElements[params.id];
|
||||
|
||||
slideData.updatePage(activePage.id, activePage);
|
||||
|
||||
const canvasview = accessor.get(CanvasView);
|
||||
canvasview.removeObjectById(params.id, activePage.id, unitId);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, IImageIoService, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DRAWING_IMAGE_ALLOW_IMAGE_LIST, getImageSize } from '@univerjs/drawing';
|
||||
import { PageElementType } from '@univerjs/slides';
|
||||
import { ILocalFileService } from '@univerjs/ui';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
export const InsertSlideFloatImageCommand: ICommand<{}> = {
|
||||
id: 'slide.command.insert-float-image',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor, params) => {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const unitId = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SLIDE)?.getUnitId();
|
||||
if (!unitId) return false;
|
||||
|
||||
const fileOpenerService = accessor.get(ILocalFileService);
|
||||
const files = await fileOpenerService.openFile({
|
||||
multiple: true,
|
||||
accept: DRAWING_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','),
|
||||
});
|
||||
if (files.length !== 1) return false;
|
||||
|
||||
const imageIoService = accessor.get(IImageIoService);
|
||||
const imageParam = await imageIoService.saveImage(files[0]);
|
||||
if (!imageParam) return false;
|
||||
|
||||
const { imageId, imageSourceType, source, base64Cache } = imageParam;
|
||||
const { width, height, image } = await getImageSize(base64Cache || '');
|
||||
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
if (!slideData) return false;
|
||||
|
||||
const activePage = slideData.getActivePage()!;
|
||||
const elements = Object.values(activePage.pageElements);
|
||||
const maxIndex = (elements?.length) ? Math.max(...elements.map((element) => element.zIndex)) : 20;
|
||||
const data = {
|
||||
id: imageId,
|
||||
zIndex: maxIndex + 1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width,
|
||||
height,
|
||||
title: '',
|
||||
description: '',
|
||||
type: PageElementType.IMAGE,
|
||||
image: {
|
||||
imageProperties: {
|
||||
contentUrl: base64Cache,
|
||||
imageSourceType,
|
||||
source,
|
||||
base64Cache,
|
||||
image,
|
||||
},
|
||||
},
|
||||
};
|
||||
activePage.pageElements[imageId] = data;
|
||||
slideData.updatePage(activePage.id, activePage);
|
||||
|
||||
const canvasView = accessor.get(CanvasView);
|
||||
const sceneObject = canvasView.createObjectToPage(data, activePage.id, unitId);
|
||||
if (sceneObject) {
|
||||
canvasView.setObjectActiveByPage(sceneObject, activePage.id, unitId);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 { SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, generateRandomId, ICommandService, IUniverInstanceService, LocaleService } from '@univerjs/core';
|
||||
import { ObjectType } from '@univerjs/engine-render';
|
||||
import { BasicShapes, PageElementType } from '@univerjs/slides';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
import { COMPONENT_SLIDE_SIDEBAR } from '../../views/sidebar/Sidebar';
|
||||
|
||||
export interface IInsertShapeOperationParams {
|
||||
unitId: string;
|
||||
};
|
||||
|
||||
export const InsertSlideShapeRectangleCommand: ICommand = {
|
||||
id: 'slide.command.insert-float-shape.rectangle',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const unitId = instanceService.getFocusedUnit()?.getUnitId();
|
||||
return commandService.executeCommand(InsertSlideShapeRectangleOperation.id, { unitId });
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertSlideShapeRectangleOperation: ICommand<IInsertShapeOperationParams> = {
|
||||
id: 'slide.operation.insert-float-shape.rectangle',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor, params: IInsertShapeOperationParams) => {
|
||||
const id = generateRandomId(6);
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const slideData = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const unitId = params.unitId;
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
|
||||
if (!slideData) return false;
|
||||
|
||||
const activePage = slideData.getActivePage()!;
|
||||
const elements = Object.values(activePage.pageElements);
|
||||
const maxIndex = (elements?.length) ? Math.max(...elements.map((element) => element.zIndex)) : 20;
|
||||
const data = {
|
||||
id,
|
||||
zIndex: maxIndex + 1,
|
||||
left: 378,
|
||||
top: 142,
|
||||
width: 250,
|
||||
height: 250,
|
||||
title: id,
|
||||
description: '',
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Rect,
|
||||
text: '',
|
||||
shapeProperties: {
|
||||
shapeBackgroundFill: {
|
||||
rgb: 'rgb(0,0,255)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
activePage.pageElements[id] = data;
|
||||
slideData.updatePage(activePage.id, activePage);
|
||||
|
||||
const canvasview = accessor.get(CanvasView);
|
||||
const sceneObject = canvasview.createObjectToPage(data, activePage.id, unitId);
|
||||
if (sceneObject) {
|
||||
canvasview.setObjectActiveByPage(sceneObject, activePage.id, unitId);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export interface IToggleSlideEditSidebarOperation {
|
||||
visible: string;
|
||||
objectType: ObjectType;
|
||||
}
|
||||
|
||||
export const ToggleSlideEditSidebarOperation: ICommand = {
|
||||
id: 'sidebar.operation.slide-shape',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor, params: IToggleSlideEditSidebarOperation) => {
|
||||
const { visible, objectType } = params;
|
||||
|
||||
const sidebarService = accessor.get(ISidebarService);
|
||||
const localeService = accessor.get(LocaleService);
|
||||
|
||||
let title = '';
|
||||
let children = '';
|
||||
if (objectType === ObjectType.RECT) {
|
||||
title = 'slides-ui.sidebar.shape';
|
||||
children = COMPONENT_SLIDE_SIDEBAR;
|
||||
} else if (objectType === ObjectType.IMAGE) {
|
||||
title = 'slides-ui.sidebar.image';
|
||||
children = COMPONENT_SLIDE_SIDEBAR;
|
||||
} else if (objectType === ObjectType.RICH_TEXT) {
|
||||
title = 'slides-ui.sidebar.text';
|
||||
children = COMPONENT_SLIDE_SIDEBAR;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
sidebarService.open({
|
||||
header: { title: localeService.t(title) },
|
||||
children: { label: children },
|
||||
onClose: () => {
|
||||
// drawingManagerService.focusDrawing(null);
|
||||
},
|
||||
width: 360,
|
||||
});
|
||||
} else {
|
||||
sidebarService.close();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertSlideShapeEllipseCommand: ICommand = {
|
||||
id: 'slide.command.insert-float-shape.ellipse',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const unitId = instanceService.getFocusedUnit()?.getUnitId();
|
||||
return commandService.executeCommand(InsertSlideShapeEllipseOperation.id, { unitId });
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertSlideShapeEllipseOperation: ICommand<IInsertShapeOperationParams> = {
|
||||
id: 'slide.operation.insert-float-shape.ellipse',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor, params: IInsertShapeOperationParams) => {
|
||||
const id = generateRandomId(6);
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const slideData = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const unitId = params.unitId;
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
|
||||
if (!slideData) return false;
|
||||
|
||||
const activePage = slideData.getActivePage()!;
|
||||
const elements = Object.values(activePage.pageElements);
|
||||
const maxIndex = (elements?.length) ? Math.max(...elements.map((element) => element.zIndex)) : 20;
|
||||
const data = {
|
||||
id,
|
||||
zIndex: maxIndex + 1,
|
||||
left: 378,
|
||||
top: 142,
|
||||
width: 250,
|
||||
height: 250,
|
||||
title: id,
|
||||
description: '',
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Ellipse,
|
||||
text: '',
|
||||
shapeProperties: {
|
||||
radius: 100,
|
||||
shapeBackgroundFill: {
|
||||
rgb: 'rgb(0,0,255)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
activePage.pageElements[id] = data;
|
||||
slideData.updatePage(activePage.id, activePage);
|
||||
|
||||
const canvasview = accessor.get(CanvasView);
|
||||
const sceneObject = canvasview.createObjectToPage(data, activePage.id, unitId);
|
||||
if (sceneObject) {
|
||||
canvasview.setObjectActiveByPage(sceneObject, activePage.id, unitId);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { IPageElement, SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, generateRandomId, ICommandService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { PageElementType } from '@univerjs/slides';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
export interface ISlideAddTextParam {
|
||||
text: string;
|
||||
unitId: string;
|
||||
};
|
||||
|
||||
export const SlideAddTextCommand: ICommand = {
|
||||
id: 'slide.command.add-text',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const unitId = univerInstanceService.getFocusedUnit()?.getUnitId();
|
||||
return await commandService.executeCommand(SlideAddTextOperation.id, { unitId });
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export const SlideAddTextOperation: ICommand<ISlideAddTextParam> = {
|
||||
id: 'slide.operation.add-text',
|
||||
type: CommandType.OPERATION,
|
||||
handler: async (accessor, params: ISlideAddTextParam) => {
|
||||
const unitId = params.unitId;
|
||||
|
||||
const elementId = generateRandomId(6);
|
||||
const defaultWidth = 220;
|
||||
const defaultheight = 40;
|
||||
const left = 230;
|
||||
const top = 142;
|
||||
const textContent = params?.text || 'A New Text';
|
||||
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const slideData = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
if (!slideData) return false;
|
||||
|
||||
const activePage = slideData.getActivePage()!;
|
||||
|
||||
const elements = Object.values(activePage.pageElements);
|
||||
const maxIndex = (elements?.length) ? Math.max(...elements.map((element) => element.zIndex)) : 21;
|
||||
const elementData: IPageElement = {
|
||||
id: elementId,
|
||||
zIndex: maxIndex + 1,
|
||||
left,
|
||||
top,
|
||||
width: defaultWidth,
|
||||
height: defaultheight,
|
||||
title: 'text',
|
||||
description: '',
|
||||
type: PageElementType.TEXT,
|
||||
richText: {
|
||||
text: textContent,
|
||||
fs: 30,
|
||||
cl: {
|
||||
rgb: 'rgb(51, 51, 51)',
|
||||
},
|
||||
bl: 1,
|
||||
},
|
||||
};
|
||||
|
||||
activePage.pageElements[elementId] = elementData;
|
||||
slideData.updatePage(activePage.id, activePage);
|
||||
|
||||
const canvasview = accessor.get(CanvasView);
|
||||
const sceneObject = canvasview.createObjectToPage(elementData, activePage.id, unitId);
|
||||
// make object active: a control rect wrap the object.
|
||||
if (sceneObject) {
|
||||
canvasview.setObjectActiveByPage(sceneObject, activePage.id, unitId);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IAccessor, IOperation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
export interface ISlideSetThumbParam {
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
export const SetSlidePageThumbOperation: IOperation = {
|
||||
id: 'slide.operation.set-slide-page-thumb',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor: IAccessor, params: ISlideSetThumbParam) => {
|
||||
const canvasView = accessor.get(CanvasView);
|
||||
canvasView.createThumbs(params.unitId);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IOperation } from '@univerjs/core';
|
||||
import { CommandType } from '@univerjs/core';
|
||||
|
||||
export const SetTextEditArrowOperation: IOperation<any> = {
|
||||
id: 'slide.operation.edit-arrow',
|
||||
type: CommandType.OPERATION,
|
||||
handler: () => true,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { CommandType, IUniverInstanceService, merge } from '@univerjs/core';
|
||||
|
||||
export interface IUpdateElementOperationParams {
|
||||
unitId: string;
|
||||
oKey: string;
|
||||
props: Record<string, any>;
|
||||
};
|
||||
|
||||
export const UpdateSlideElementOperation: ICommand<IUpdateElementOperationParams> = {
|
||||
id: 'slide.operation.update-element',
|
||||
type: CommandType.OPERATION,
|
||||
handler: (accessor, params: IUpdateElementOperationParams) => {
|
||||
const { oKey, props } = params!;
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
// const slideData = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
|
||||
const unitId = params?.unitId;
|
||||
const slideData = univerInstanceService.getUnit<SlideDataModel>(unitId);
|
||||
if (!slideData) return false;
|
||||
|
||||
const activePage = slideData.getActivePage()!;
|
||||
activePage.pageElements[oKey] = merge(activePage.pageElements[oKey], props);
|
||||
slideData.updatePage(activePage.id, activePage);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
29
packages/slides-ui/src/config/config.ts
Normal file
29
packages/slides-ui/src/config/config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DependencyOverride } from '@univerjs/core';
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const SLIDES_UI_PLUGIN_CONFIG_KEY = 'slides-ui.config';
|
||||
|
||||
export const configSymbol = Symbol(SLIDES_UI_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSlidesUIConfig {
|
||||
override?: DependencyOverride;
|
||||
menu?: MenuConfig;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSlidesUIConfig = {};
|
||||
19
packages/slides-ui/src/const.ts
Normal file
19
packages/slides-ui/src/const.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 { createInternalEditorID } from '@univerjs/core';
|
||||
|
||||
export const SLIDE_EDITOR_ID = createInternalEditorID('SLIDE_EDITOR');
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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, Injector } from '@univerjs/core';
|
||||
import type { IRenderContext, IRichTextProps } from '@univerjs/engine-render';
|
||||
import type { ISlideData } from '@univerjs/slides';
|
||||
import {
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DocSelectionManagerService,
|
||||
DocStateEmitService,
|
||||
RichTextEditingMutation,
|
||||
} from '@univerjs/docs';
|
||||
import { EditorService, IEditorService, ReplaceSnapshotCommand } from '@univerjs/docs-ui';
|
||||
import { IRenderManagerService, RichText } from '@univerjs/engine-render';
|
||||
import { SlideDataModel } from '@univerjs/slides';
|
||||
import { DesktopLayoutService, ILayoutService } from '@univerjs/ui';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SLIDE_EDITOR_ID } from '../../const';
|
||||
import { ISlideEditorBridgeService, SlideEditorBridgeService } from '../../services/slide-editor-bridge.service';
|
||||
import { ISlideEditorManagerService, SlideEditorManagerService } from '../../services/slide-editor-manager.service';
|
||||
import { SlideEditingRenderController } from '../slide-editing.render-controller';
|
||||
|
||||
const unitId = 'slide-unit';
|
||||
|
||||
function createSlideSnapshot(): Partial<ISlideData> {
|
||||
return {
|
||||
id: unitId,
|
||||
title: 'Editor test deck',
|
||||
pageSize: { width: 960, height: 540 },
|
||||
body: {
|
||||
pageOrder: [],
|
||||
pages: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createRichText(injector: Injector, id: string, text: string, top: number): RichText {
|
||||
const props: IRichTextProps = {
|
||||
text,
|
||||
zIndex: 1,
|
||||
left: 10,
|
||||
top,
|
||||
width: 200,
|
||||
height: 40,
|
||||
};
|
||||
|
||||
return injector.createInstance(RichText, injector.get(LocaleService), id, props);
|
||||
}
|
||||
|
||||
describe('SlideEditingRenderController', () => {
|
||||
let univer: Univer;
|
||||
let controller: SlideEditingRenderController;
|
||||
|
||||
beforeEach(() => {
|
||||
univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
const renderManagerService = {
|
||||
getRenderUnitById: () => null,
|
||||
} as unknown as IRenderManagerService;
|
||||
|
||||
injector.add([IRenderManagerService, { useValue: renderManagerService }]);
|
||||
injector.add([DocSelectionManagerService]);
|
||||
injector.add([DocStateEmitService]);
|
||||
injector.add([IEditorService, { useClass: EditorService }]);
|
||||
injector.add([ILayoutService, { useClass: DesktopLayoutService }]);
|
||||
injector.add([ISlideEditorBridgeService, { useClass: SlideEditorBridgeService }]);
|
||||
injector.add([ISlideEditorManagerService, { useClass: SlideEditorManagerService }]);
|
||||
|
||||
const instanceService = injector.get(IUniverInstanceService);
|
||||
instanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, SlideDataModel);
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(RichTextEditingMutation);
|
||||
commandService.registerCommand(ReplaceSnapshotCommand);
|
||||
|
||||
const slide = univer.createUnit<ISlideData, SlideDataModel>(UniverInstanceType.UNIVER_SLIDE, createSlideSnapshot());
|
||||
controller = injector.createInstance(SlideEditingRenderController, {
|
||||
unitId,
|
||||
unit: slide,
|
||||
} as IRenderContext<SlideDataModel>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
controller?.dispose();
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('keeps the registered editor unit while replacing its snapshot', () => {
|
||||
const injector = univer.__getInjector();
|
||||
const instanceService = injector.get(IUniverInstanceService);
|
||||
const editorBridgeService = injector.get(ISlideEditorBridgeService);
|
||||
const firstText = createRichText(injector, 'first', 'First text', 20);
|
||||
const secondText = createRichText(injector, 'second', 'Second text', 80);
|
||||
|
||||
editorBridgeService.setEditorRect({
|
||||
scene: null as never,
|
||||
engine: null as never,
|
||||
unitId,
|
||||
pageId: 'page-1',
|
||||
richTextObj: firstText,
|
||||
});
|
||||
|
||||
const editorDocument = instanceService.getUnit<DocumentDataModel>(SLIDE_EDITOR_ID, UniverInstanceType.UNIVER_DOC);
|
||||
expect(editorDocument?.getBody()?.dataStream).toBe('First text\r\n');
|
||||
|
||||
editorBridgeService.setEditorRect({
|
||||
scene: null as never,
|
||||
engine: null as never,
|
||||
unitId,
|
||||
pageId: 'page-1',
|
||||
richTextObj: secondText,
|
||||
});
|
||||
|
||||
expect(instanceService.getUnit(SLIDE_EDITOR_ID, UniverInstanceType.UNIVER_DOC)).toBe(editorDocument);
|
||||
expect(editorDocument?.getBody()?.dataStream).toBe('Second text\r\n');
|
||||
expect(instanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC)).toBe(editorDocument);
|
||||
|
||||
firstText.dispose();
|
||||
secondText.dispose();
|
||||
});
|
||||
});
|
||||
79
packages/slides-ui/src/controllers/canvas-view.ts
Normal file
79
packages/slides-ui/src/controllers/canvas-view.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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 { BaseObject, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { IPageElement } from '@univerjs/slides';
|
||||
import type { PageID } from '../type';
|
||||
import { RxDisposable } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SlideRenderController } from './slide.render-controller';
|
||||
|
||||
export class CanvasView extends RxDisposable implements IRenderModule {
|
||||
constructor(
|
||||
// this controller needs by commands. root injector. T
|
||||
// That means this controller is not init by renderUnit ---> no renderContext.
|
||||
// private readonly _renderContext: IRenderContext<UnitModel>,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private _getSlideRenderControllerFromRenderUnit(unitId: string) {
|
||||
const renderUnit = this._renderManagerService
|
||||
.getRenderUnitById(unitId)!;
|
||||
const slideRC = renderUnit.with(SlideRenderController);
|
||||
return slideRC;
|
||||
}
|
||||
|
||||
createThumbs(unitId: string) {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
slideRC.createThumbs();
|
||||
}
|
||||
|
||||
activePage(pageId: string, unitId: string) {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
slideRC.activePage(pageId);
|
||||
}
|
||||
|
||||
getRenderUnitByPageId(pageId: PageID, unitId: string) {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
return slideRC.getPageRenderUnit(pageId);
|
||||
}
|
||||
|
||||
createObjectToPage(element: IPageElement, pageID: PageID, unitId: string): Nullable<BaseObject> {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
return slideRC.createObjectToPage(element, pageID);
|
||||
}
|
||||
|
||||
setObjectActiveByPage(obj: BaseObject, pageID: PageID, unitId: string) {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
return slideRC.setObjectActiveByPage(obj, pageID);
|
||||
}
|
||||
|
||||
removeObjectById(id: string, pageID: PageID, unitId: string) {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
slideRC.removeObjectById(id, pageID);
|
||||
}
|
||||
|
||||
/**
|
||||
* append blank page
|
||||
*/
|
||||
appendPage(unitId: string) {
|
||||
const slideRC = this._getSlideRenderControllerFromRenderUnit(unitId);
|
||||
slideRC.appendPage();
|
||||
}
|
||||
}
|
||||
48
packages/slides-ui/src/controllers/components.controller.ts
Normal file
48
packages/slides-ui/src/controllers/components.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 { AddImageIcon, GraphIcon, TextIcon } from '@univerjs/icons';
|
||||
import { ComponentManager, IconManager } from '@univerjs/ui';
|
||||
import { COMPONENT_SLIDE_IMAGE_POPUP_MENU } from '../views/image-popup-menu/component-name';
|
||||
import { SlideImagePopupMenu } from '../views/image-popup-menu/ImagePopupMenu';
|
||||
import Sidebar, { COMPONENT_SLIDE_SIDEBAR } from '../views/sidebar/Sidebar';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
|
||||
@Inject(IconManager) private readonly _iconManager: IconManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerParts();
|
||||
this._registerIcons();
|
||||
}
|
||||
|
||||
private _registerParts(): void {
|
||||
const componentManager = this._componentManager;
|
||||
this.disposeWithMe(componentManager.register(COMPONENT_SLIDE_IMAGE_POPUP_MENU, SlideImagePopupMenu));
|
||||
this.disposeWithMe(componentManager.register(COMPONENT_SLIDE_SIDEBAR, Sidebar));
|
||||
}
|
||||
|
||||
private _registerIcons(): void {
|
||||
this.disposeWithMe(this._iconManager.register({
|
||||
AddImageIcon,
|
||||
TextIcon,
|
||||
GraphIcon,
|
||||
}));
|
||||
}
|
||||
}
|
||||
146
packages/slides-ui/src/controllers/shortcuts/editor.shortcuts.ts
Normal file
146
packages/slides-ui/src/controllers/shortcuts/editor.shortcuts.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 { IShortcutItem } from '@univerjs/ui';
|
||||
import { DeleteLeftCommand } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType } from '@univerjs/engine-render';
|
||||
import { KeyCode, MetaKeys } from '@univerjs/ui';
|
||||
import { SetTextEditArrowOperation } from '../../commands/operations/text-edit.operation';
|
||||
import {
|
||||
whenEditorActivated,
|
||||
whenFormulaEditorFocused,
|
||||
} from './utils';
|
||||
|
||||
export const ARROW_SELECTION_KEYCODE_LIST = [
|
||||
KeyCode.ARROW_DOWN,
|
||||
KeyCode.ARROW_UP,
|
||||
KeyCode.ARROW_LEFT,
|
||||
KeyCode.ARROW_RIGHT,
|
||||
];
|
||||
|
||||
export const MOVE_SELECTION_KEYCODE_LIST = [KeyCode.ENTER, KeyCode.TAB, ...ARROW_SELECTION_KEYCODE_LIST];
|
||||
|
||||
export function generateArrowSelectionShortCutItem() {
|
||||
const shortcutList: IShortcutItem[] = [];
|
||||
|
||||
for (const keycode of ARROW_SELECTION_KEYCODE_LIST) {
|
||||
shortcutList.push({
|
||||
id: SetTextEditArrowOperation.id,
|
||||
binding: keycode,
|
||||
preconditions: (contextService) => whenEditorActivated(contextService),
|
||||
staticParameters: {
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode,
|
||||
isShift: false,
|
||||
},
|
||||
});
|
||||
|
||||
shortcutList.push({
|
||||
id: SetTextEditArrowOperation.id,
|
||||
binding: keycode | MetaKeys.SHIFT,
|
||||
preconditions: (contextService) => whenEditorActivated(contextService),
|
||||
staticParameters: {
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode,
|
||||
isShift: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return shortcutList;
|
||||
}
|
||||
|
||||
// export const StartEditWithF2Shortcut: IShortcutItem = {
|
||||
// id: SetCellEditVisibleWithF2Operation.id,
|
||||
// binding: KeyCode.F2,
|
||||
// description: 'shortcut.sheet.start-editing',
|
||||
// group: '4_sheet-edit',
|
||||
// preconditions: whenSheetEditorFocused,
|
||||
// staticParameters: {
|
||||
// visible: true,
|
||||
// eventType: DeviceInputEventType.Keyboard,
|
||||
// keycode: KeyCode.F2,
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const EditorCursorEnterShortcut: IShortcutItem = {
|
||||
// id: SetCellEditVisibleOperation.id,
|
||||
// binding: KeyCode.ENTER,
|
||||
// description: 'shortcut.sheet.toggle-editing',
|
||||
// group: '4_sheet-edit',
|
||||
// preconditions: (contextService) => whenEditorDidNotInputFormulaActivated(contextService),
|
||||
// staticParameters: {
|
||||
// visible: false,
|
||||
// eventType: DeviceInputEventType.Keyboard,
|
||||
// keycode: KeyCode.ENTER,
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const EditorCursorTabShortcut: IShortcutItem = {
|
||||
// id: SetCellEditVisibleOperation.id,
|
||||
// binding: KeyCode.TAB,
|
||||
// preconditions: (contextService) => whenEditorDidNotInputFormulaActivated(contextService),
|
||||
// staticParameters: {
|
||||
// visible: false,
|
||||
// eventType: DeviceInputEventType.Keyboard,
|
||||
// keycode: KeyCode.TAB,
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const EditorCursorEscShortcut: IShortcutItem = {
|
||||
// id: SetCellEditVisibleOperation.id,
|
||||
// binding: KeyCode.ESC,
|
||||
// description: 'shortcut.sheet.abort-editing',
|
||||
// group: '4_sheet-edit',
|
||||
// preconditions: (contextService) => whenEditorDidNotInputFormulaActivated(contextService),
|
||||
// staticParameters: {
|
||||
// visible: false,
|
||||
// eventType: DeviceInputEventType.Keyboard,
|
||||
// keycode: KeyCode.ESC,
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const EditorBreakLineShortcut: IShortcutItem = {
|
||||
// id: BreakLineCommand.id,
|
||||
// description: 'shortcut.sheet.break-line',
|
||||
// group: '4_sheet-edit',
|
||||
// preconditions: (contextService) => whenSheetEditorActivated(contextService),
|
||||
// binding: KeyCode.ENTER | MetaKeys.ALT,
|
||||
// };
|
||||
|
||||
export const EditorDeleteLeftShortcut: IShortcutItem = {
|
||||
id: DeleteLeftCommand.id,
|
||||
preconditions: (contextService) => {
|
||||
return whenEditorActivated(contextService) || whenFormulaEditorFocused(contextService);
|
||||
},
|
||||
binding: KeyCode.BACKSPACE,
|
||||
};
|
||||
|
||||
// export const EditorDeleteLeftShortcutInActive: IShortcutItem = {
|
||||
// id: SetCellEditVisibleOperation.id,
|
||||
// description: 'shortcut.sheet.delete-and-start-editing',
|
||||
// group: '4_sheet-edit',
|
||||
// preconditions: (contextService) =>
|
||||
// whenSheetEditorFocused(contextService) && !whenFormulaEditorFocused(contextService),
|
||||
// binding: KeyCode.BACKSPACE,
|
||||
// staticParameters: {
|
||||
// visible: true,
|
||||
// eventType: DeviceInputEventType.Keyboard,
|
||||
// keycode: KeyCode.BACKSPACE,
|
||||
// },
|
||||
// };
|
||||
114
packages/slides-ui/src/controllers/shortcuts/utils.ts
Normal file
114
packages/slides-ui/src/controllers/shortcuts/utils.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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 {
|
||||
EDITOR_ACTIVATED,
|
||||
FOCUSING_COMMON_DRAWINGS,
|
||||
FOCUSING_EDITOR_INPUT_FORMULA,
|
||||
FOCUSING_EDITOR_STANDALONE,
|
||||
FOCUSING_SLIDE,
|
||||
FOCUSING_UNIVER_EDITOR,
|
||||
FORMULA_EDITOR_ACTIVATED,
|
||||
} from '@univerjs/core';
|
||||
|
||||
export function whenSheetFocused(contextService: IContextService) {
|
||||
return contextService.getContextValue(FOCUSING_SLIDE);
|
||||
}
|
||||
|
||||
// FIXME: why coupled with sheet editors?
|
||||
|
||||
/**
|
||||
* Requires the currently focused unit to be Workbook and the sheet editor is focused but not activated.
|
||||
* @param contextService
|
||||
* @returns If the sheet editor is focused but not activated.
|
||||
*/
|
||||
export function whenSheetEditorFocused(contextService: IContextService): boolean {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_SLIDE) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR) &&
|
||||
!contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
!contextService.getContextValue(FOCUSING_COMMON_DRAWINGS)
|
||||
);
|
||||
}
|
||||
|
||||
export function whenSheetEditorFocusedAndFxNotFocused(contextService: IContextService): boolean {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_SLIDE) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR) &&
|
||||
!contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
!contextService.getContextValue(FORMULA_EDITOR_ACTIVATED) &&
|
||||
!contextService.getContextValue(FOCUSING_COMMON_DRAWINGS)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the currently focused unit to be Workbook and the sheet editor is activated.
|
||||
* @param contextService
|
||||
* @returns If the sheet editor is activated.
|
||||
*/
|
||||
export function whenSheetEditorActivated(contextService: IContextService) {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_SLIDE) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED)
|
||||
);
|
||||
}
|
||||
|
||||
export function whenEditorActivated(contextService: IContextService) {
|
||||
return contextService.getContextValue(FOCUSING_UNIVER_EDITOR) && contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the currently focused editor is a formula editor.
|
||||
* @param contextService
|
||||
* @returns If the formula editor is focused.
|
||||
*/
|
||||
export function whenFormulaEditorFocused(contextService: IContextService) {
|
||||
return (
|
||||
contextService.getContextValue(FORMULA_EDITOR_ACTIVATED) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the currently focused editor is a formula editor, and it is activated.
|
||||
* @param contextService
|
||||
* @returns If the formula editor is activated.
|
||||
*/
|
||||
export function whenFormulaEditorActivated(contextService: IContextService) {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_SLIDE) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
contextService.getContextValue(FOCUSING_EDITOR_INPUT_FORMULA) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the currently focused editor is not a formula editor, and it is activated.
|
||||
* @param contextService
|
||||
* @returns If the editor is activated and the editor is not the formula editor.
|
||||
*/
|
||||
export function whenEditorDidNotInputFormulaActivated(contextService: IContextService) {
|
||||
return (
|
||||
contextService.getContextValue(FOCUSING_SLIDE) &&
|
||||
contextService.getContextValue(FOCUSING_UNIVER_EDITOR) &&
|
||||
contextService.getContextValue(EDITOR_ACTIVATED) &&
|
||||
!contextService.getContextValue(FOCUSING_EDITOR_INPUT_FORMULA) &&
|
||||
!contextService.getContextValue(FOCUSING_EDITOR_STANDALONE)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ICommandInfo,
|
||||
IDisposable,
|
||||
IDocumentBody,
|
||||
IPosition,
|
||||
Nullable,
|
||||
UnitModel,
|
||||
} from '@univerjs/core';
|
||||
import type { IDocObjectParam, IEditorInputConfig } from '@univerjs/docs-ui';
|
||||
import type {
|
||||
DocBackground,
|
||||
Documents,
|
||||
DocumentSkeleton,
|
||||
IDocumentLayoutObject,
|
||||
IRenderContext,
|
||||
IRenderModule,
|
||||
Scene,
|
||||
} from '@univerjs/engine-render';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import type { IEditorBridgeServiceVisibleParam } from '../services/slide-editor-bridge.service';
|
||||
import {
|
||||
createParagraphId,
|
||||
DEFAULT_EMPTY_DOCUMENT_VALUE,
|
||||
Direction,
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
EDITOR_ACTIVATED,
|
||||
FOCUSING_EDITOR_STANDALONE,
|
||||
FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE,
|
||||
HorizontalAlign,
|
||||
ICommandService,
|
||||
IContextService,
|
||||
Inject,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
toDisposable,
|
||||
UniverInstanceType,
|
||||
VerticalAlign,
|
||||
WrapStrategy,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
DocSelectionManagerService,
|
||||
DocSkeletonManagerService,
|
||||
RichTextEditingMutation,
|
||||
} from '@univerjs/docs';
|
||||
import {
|
||||
DOCS_COMPONENT_MAIN_LAYER_INDEX,
|
||||
DOCS_VIEW_KEY,
|
||||
DocSelectionRenderService,
|
||||
IEditorService,
|
||||
MoveCursorOperation,
|
||||
MoveSelectionOperation,
|
||||
ReplaceSnapshotCommand,
|
||||
VIEWPORT_KEY,
|
||||
} from '@univerjs/docs-ui';
|
||||
import {
|
||||
convertTextRotation,
|
||||
DeviceInputEventType,
|
||||
FIX_ONE_PIXEL_BLUR_OFFSET,
|
||||
fixLineWidthByScale,
|
||||
getCurrentTypeOfRenderer,
|
||||
IRenderManagerService,
|
||||
Rect,
|
||||
ScrollBar,
|
||||
} from '@univerjs/engine-render';
|
||||
import { ILayoutService, KeyCode } from '@univerjs/ui';
|
||||
import { filter } from 'rxjs';
|
||||
import { SetTextEditArrowOperation } from '../commands/operations/text-edit.operation';
|
||||
import { SLIDE_EDITOR_ID } from '../const';
|
||||
import { ISlideEditorBridgeService } from '../services/slide-editor-bridge.service';
|
||||
import { ISlideEditorManagerService } from '../services/slide-editor-manager.service';
|
||||
import { CursorChange } from '../type';
|
||||
|
||||
const HIDDEN_EDITOR_POSITION = -1000;
|
||||
|
||||
const EDITOR_INPUT_SELF_EXTEND_GAP = 5;
|
||||
|
||||
const EDITOR_BORDER_SIZE = 2;
|
||||
|
||||
interface ICanvasOffset {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export class SlideEditingRenderController extends Disposable implements IRenderModule {
|
||||
/**
|
||||
* It is used to distinguish whether the user has actively moved the cursor in the editor, mainly through mouse clicks.
|
||||
*/
|
||||
private _cursorChange: CursorChange = CursorChange.InitialState;
|
||||
|
||||
/** If the corresponding unit is active and prepared for editing. */
|
||||
private _isUnitEditing = false;
|
||||
|
||||
private _d: Nullable<IDisposable>;
|
||||
|
||||
constructor(
|
||||
private readonly _renderContext: IRenderContext<UnitModel>,
|
||||
@ILayoutService private readonly _layoutService: ILayoutService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@ISlideEditorBridgeService private readonly _editorBridgeService: ISlideEditorBridgeService,
|
||||
@ISlideEditorManagerService private readonly _cellEditorManagerService: ISlideEditorManagerService,
|
||||
@Inject(DocSelectionManagerService) private readonly _textSelectionManagerService: DocSelectionManagerService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@Inject(LocaleService) protected readonly _localService: LocaleService,
|
||||
@IEditorService private readonly _editorService: IEditorService
|
||||
) {
|
||||
super();
|
||||
|
||||
// EditingRenderController is per unit. It should only handle keyboard events when the unit is
|
||||
// the current of its type.
|
||||
this.disposeWithMe(this._instanceSrv.getCurrentTypeOfUnit$<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE).subscribe((slideDataModel) => {
|
||||
if (slideDataModel && slideDataModel.getUnitId() === this._renderContext.unitId) {
|
||||
this._d = this._init();
|
||||
} else {
|
||||
this._disposeCurrent();
|
||||
|
||||
// Force ending editor when he switch to another workbook.
|
||||
if (this._isUnitEditing) {
|
||||
this._handleEditorInvisible({
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Keyboard,
|
||||
keycode: KeyCode.ESC,
|
||||
unitId: this._renderContext.unitId,
|
||||
});
|
||||
|
||||
this._isUnitEditing = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
this._initEditorVisibilityListener();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this._disposeCurrent();
|
||||
}
|
||||
|
||||
private _disposeCurrent(): void {
|
||||
this._d?.dispose();
|
||||
this._d = null;
|
||||
}
|
||||
|
||||
private _init(): IDisposable {
|
||||
const d = new DisposableCollection();
|
||||
this._subscribeToCurrentCell(d);
|
||||
this._initialKeyboardListener(d);
|
||||
this._initialCursorSync(d);
|
||||
this._listenEditorFocus(d);
|
||||
this._commandExecutedListener(d);
|
||||
|
||||
// FIXME: we should not use React component to create editor render, it would make the control
|
||||
// flow not controllable.
|
||||
setTimeout(() => {
|
||||
this._cursorStateListener(d);
|
||||
}, 1000);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
private _initEditorVisibilityListener(): void {
|
||||
this.disposeWithMe(this._editorBridgeService.visible$
|
||||
// .pipe(distinctUntilChanged((prev, curr) => prev.visible === curr.visible))
|
||||
.subscribe((param) => {
|
||||
if (param.visible) {
|
||||
this._isUnitEditing = true;
|
||||
this._handleEditorVisible(param);
|
||||
} else if (this._isUnitEditing) {
|
||||
this._handleEditorInvisible(param);
|
||||
this._isUnitEditing = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _listenEditorFocus(d: DisposableCollection) {
|
||||
const renderConfig = this._getEditorObject();
|
||||
if (!renderConfig) return;
|
||||
|
||||
d.add(renderConfig.document.onPointerDown$.subscribeEvent(() => {
|
||||
// ...
|
||||
}));
|
||||
}
|
||||
|
||||
private _getEditorSkeleton(editorId: string) {
|
||||
return this._renderManagerService.getRenderUnitById(editorId)?.with(DocSkeletonManagerService).getSkeleton();
|
||||
}
|
||||
|
||||
private _getEditorViewModel(editorId: string) {
|
||||
return this._renderManagerService.getRenderUnitById(editorId)?.with(DocSkeletonManagerService).getViewModel();
|
||||
}
|
||||
|
||||
private _initialCursorSync(d: DisposableCollection) {
|
||||
d.add(this._cellEditorManagerService.focus$.pipe(filter((f) => !!f)).subscribe(() => {
|
||||
getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_DOC, this._instanceSrv, this._renderManagerService)
|
||||
?.with(DocSelectionRenderService)
|
||||
.sync();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set editorUnitId to curr doc.
|
||||
* @param d DisposableCollection
|
||||
*/
|
||||
private _subscribeToCurrentCell(d: DisposableCollection) {
|
||||
// first part of editing.
|
||||
// startEditing --> _updateEditor --> slide-editor-bridge.service.ts@setEditorRect---> currentEditRectState$.next(editCellState)
|
||||
// 2nd part is startEditing --> changeVisible
|
||||
d.add(this._editorBridgeService.currentEditRectState$.subscribe((editCellState) => {
|
||||
if (editCellState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._contextService.getContextValue(FOCUSING_EDITOR_STANDALONE) ||
|
||||
this._contextService.getContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
position: { startX, endX },
|
||||
documentLayoutObject: { textRotation, wrapStrategy, documentModel },
|
||||
scaleX,
|
||||
editorUnitId,
|
||||
} = editCellState;
|
||||
const { vertexAngle: angle } = convertTextRotation(textRotation);
|
||||
documentModel!.updateDocumentId(editorUnitId);
|
||||
|
||||
if (wrapStrategy === WrapStrategy.WRAP && angle === 0) {
|
||||
documentModel!.updateDocumentDataPageSize((endX - startX) / scaleX);
|
||||
}
|
||||
|
||||
const snapshot = documentModel!.getSnapshot();
|
||||
const editorDocument = this._instanceSrv.getUnit(editorUnitId, UniverInstanceType.UNIVER_DOC);
|
||||
if (editorDocument == null) {
|
||||
this._instanceSrv.createUnit(UniverInstanceType.UNIVER_DOC, snapshot);
|
||||
} else {
|
||||
this._commandService.syncExecuteCommand(ReplaceSnapshotCommand.id, {
|
||||
unitId: editorUnitId,
|
||||
snapshot,
|
||||
});
|
||||
}
|
||||
this._instanceSrv.setCurrentUnitForType(editorUnitId);
|
||||
this._textSelectionManagerService.replaceDocRanges([{
|
||||
startOffset: 0,
|
||||
endOffset: 0,
|
||||
}]);
|
||||
|
||||
// the valid pos of activate:
|
||||
// EditorContainer.tsx cellEditorManagerService.setFocus(true) -->
|
||||
// ---> _focus$.next --> editingRenderController
|
||||
// _textSelectionRenderManager.sync() --> _updateInputPosition --> activate(left, top)
|
||||
|
||||
getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_DOC, this._instanceSrv, this._renderManagerService)
|
||||
?.with(DocSelectionRenderService)
|
||||
.activate(HIDDEN_EDITOR_POSITION, HIDDEN_EDITOR_POSITION);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set size and pos of editing area.
|
||||
* @param positionFromEditRectState
|
||||
* @param canvasOffset
|
||||
* @param documentSkeleton
|
||||
* @param documentLayoutObject
|
||||
* @param scaleX
|
||||
* @param scaleY
|
||||
*/
|
||||
private _fitTextSize(
|
||||
positionFromEditRectState: IPosition,
|
||||
canvasOffset: ICanvasOffset,
|
||||
documentSkeleton: DocumentSkeleton,
|
||||
documentLayoutObject: IDocumentLayoutObject,
|
||||
scaleX: number = 1,
|
||||
scaleY: number = 1
|
||||
) {
|
||||
//size & pos derives from slide-editor-bridge.service.ts@getEditRectState.position
|
||||
const { startX, startY, endX, endY } = positionFromEditRectState;
|
||||
const documentDataModel = documentLayoutObject.documentModel;
|
||||
|
||||
if (documentDataModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { actualWidth, actualHeight } = this._predictingSize(
|
||||
positionFromEditRectState,
|
||||
canvasOffset,
|
||||
documentSkeleton,
|
||||
documentLayoutObject,
|
||||
scaleX,
|
||||
scaleY
|
||||
);
|
||||
const { verticalAlign, paddingData, fill } = documentLayoutObject;
|
||||
|
||||
let editorWidth = endX - startX;
|
||||
|
||||
let editorHeight = endY - startY;
|
||||
|
||||
if (editorWidth < actualWidth) {
|
||||
editorWidth = actualWidth;
|
||||
}
|
||||
|
||||
if (editorHeight < actualHeight) {
|
||||
editorHeight = actualHeight;
|
||||
// To restore the page margins for the skeleton.
|
||||
documentDataModel.updateDocumentDataMargin(paddingData);
|
||||
} else {
|
||||
// Set the top margin under vertical alignment.
|
||||
let offsetTop = 0;
|
||||
|
||||
if (verticalAlign === VerticalAlign.MIDDLE) {
|
||||
offsetTop = (editorHeight - actualHeight) / 2 / scaleY;
|
||||
} else if (verticalAlign === VerticalAlign.TOP) {
|
||||
offsetTop = paddingData.t || 0;
|
||||
} else { // VerticalAlign.UNSPECIFIED follow the same rule as HorizontalAlign.BOTTOM.
|
||||
offsetTop = (editorHeight - actualHeight) / scaleY - (paddingData.b || 0);
|
||||
}
|
||||
|
||||
// offsetTop /= scaleY;
|
||||
offsetTop = offsetTop < (paddingData.t || 0) ? paddingData.t || 0 : offsetTop;
|
||||
|
||||
documentDataModel.updateDocumentDataMargin({
|
||||
t: offsetTop,
|
||||
});
|
||||
}
|
||||
|
||||
// re-calculate skeleton(viewModel for component)
|
||||
documentSkeleton.calculate();
|
||||
|
||||
this._editAreaProcessing(editorWidth, editorHeight, positionFromEditRectState, canvasOffset, fill, scaleX, scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mainly used to pre-calculate the width of the editor,
|
||||
* to determine whether it needs to be automatically widened.
|
||||
*/
|
||||
private _predictingSize(
|
||||
actualRangeWithCoord: IPosition,
|
||||
canvasOffset: ICanvasOffset,
|
||||
documentSkeleton: DocumentSkeleton,
|
||||
documentLayoutObject: IDocumentLayoutObject,
|
||||
scaleX: number = 1,
|
||||
scaleY: number = 1
|
||||
) {
|
||||
// startX and startY are the width and height after scaling.
|
||||
const { startX, endX } = actualRangeWithCoord;
|
||||
|
||||
const { textRotation, wrapStrategy } = documentLayoutObject;
|
||||
|
||||
const documentDataModel = documentLayoutObject.documentModel;
|
||||
|
||||
const { vertexAngle: angle } = convertTextRotation(textRotation);
|
||||
|
||||
const clientWidth = document.body.clientWidth;
|
||||
|
||||
if (wrapStrategy === WrapStrategy.WRAP && angle === 0) {
|
||||
const { actualWidth, actualHeight } = documentSkeleton.getActualSize();
|
||||
// The skeleton obtains the original volume, which needs to be multiplied by the magnification factor.
|
||||
return {
|
||||
actualWidth: actualWidth * scaleX,
|
||||
actualHeight: actualHeight * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
documentDataModel?.updateDocumentDataPageSize((clientWidth - startX - canvasOffset.left) / scaleX);
|
||||
documentSkeleton.calculate();
|
||||
|
||||
const size = documentSkeleton.getActualSize();
|
||||
|
||||
let editorWidth = endX - startX;
|
||||
|
||||
if (editorWidth < size.actualWidth * scaleX + EDITOR_INPUT_SELF_EXTEND_GAP * scaleX) {
|
||||
editorWidth = size.actualWidth * scaleX + EDITOR_INPUT_SELF_EXTEND_GAP * scaleX;
|
||||
}
|
||||
|
||||
// Scaling is handled by the renderer, so the skeleton only accepts the original width and height, which need to be divided by the magnification factor.
|
||||
documentDataModel?.updateDocumentDataPageSize(editorWidth / scaleX);
|
||||
|
||||
/**
|
||||
* Do not rely on cell layout logic, depend on the document's internal alignment logic.
|
||||
*/
|
||||
documentDataModel?.updateDocumentRenderConfig({
|
||||
horizontalAlign: HorizontalAlign.UNSPECIFIED,
|
||||
cellValueType: undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
actualWidth: editorWidth,
|
||||
actualHeight: size.actualHeight * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* control the size of editing area
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _editAreaProcessing(
|
||||
editorWidth: number,
|
||||
editorHeight: number,
|
||||
positionFromEditRectState: IPosition,
|
||||
canvasOffset: ICanvasOffset,
|
||||
fill: Nullable<string>,
|
||||
scaleX: number = 1,
|
||||
scaleY: number = 1
|
||||
) {
|
||||
const editorObject = this._getEditorObject();
|
||||
|
||||
if (editorObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
function pxToNum(width: string): number {
|
||||
return Number.parseInt(width.replace('px', ''));
|
||||
}
|
||||
|
||||
const engine = this._renderContext.engine;
|
||||
const canvasElement = engine.getCanvasElement();
|
||||
const canvasClientRect = canvasElement.getBoundingClientRect();
|
||||
|
||||
// We should take the scale into account when canvas is scaled by CSS.
|
||||
const widthOfCanvas = pxToNum(canvasElement.style.width); // declared width
|
||||
const { top, left, width } = canvasClientRect; // real width affected by scale
|
||||
const scaleAdjust = width / widthOfCanvas;
|
||||
|
||||
let { startX, startY } = positionFromEditRectState;
|
||||
|
||||
startX += canvasOffset.left;
|
||||
startY += canvasOffset.top;
|
||||
|
||||
const { document: documentComponent, scene: editorScene, engine: docEngine } = editorObject;
|
||||
|
||||
const viewportMain = editorScene.getViewport(VIEWPORT_KEY.VIEW_MAIN);
|
||||
|
||||
const clientHeight =
|
||||
document.body.clientHeight -
|
||||
startY -
|
||||
canvasOffset.top -
|
||||
EDITOR_BORDER_SIZE * 2;
|
||||
|
||||
const clientWidth = document.body.clientWidth - startX - canvasOffset.left;
|
||||
|
||||
let physicHeight = editorHeight;
|
||||
|
||||
let scrollBar = viewportMain?.getScrollBar() as Nullable<ScrollBar>;
|
||||
|
||||
if (physicHeight > clientHeight) {
|
||||
physicHeight = clientHeight;
|
||||
|
||||
if (scrollBar == null) {
|
||||
if (viewportMain) {
|
||||
const sb = new ScrollBar(viewportMain, { enableHorizontal: false, barSize: 8 });
|
||||
}
|
||||
} else {
|
||||
viewportMain?.resetCanvasSizeAndUpdateScroll();
|
||||
}
|
||||
} else {
|
||||
scrollBar = null;
|
||||
viewportMain?.getScrollBar()?.dispose();
|
||||
}
|
||||
|
||||
editorWidth += scrollBar?.barSize || 0;
|
||||
|
||||
editorWidth = Math.min(editorWidth, clientWidth);
|
||||
|
||||
startX -= FIX_ONE_PIXEL_BLUR_OFFSET;
|
||||
|
||||
startY -= FIX_ONE_PIXEL_BLUR_OFFSET;
|
||||
|
||||
// physicHeight = editorHeight;
|
||||
|
||||
this._addBackground(editorScene, editorWidth / scaleX, editorHeight / scaleY, fill);
|
||||
|
||||
const { scaleX: precisionScaleX, scaleY: precisionScaleY } = editorScene.getPrecisionScale();
|
||||
|
||||
editorScene.transformByState({
|
||||
width: editorWidth * scaleAdjust / scaleX,
|
||||
height: editorHeight * scaleAdjust / scaleY,
|
||||
scaleX: scaleX * scaleAdjust,
|
||||
scaleY: scaleY * scaleAdjust,
|
||||
});
|
||||
|
||||
documentComponent.resize(editorWidth / scaleX, editorHeight / scaleY);
|
||||
|
||||
/**
|
||||
* sometimes requestIdleCallback is invalid, so use setTimeout to ensure the successful execution of the resizeBySize method.
|
||||
* resize canvas
|
||||
* When modifying the selection area for a formula, it is necessary to add a setTimeout to ensure successful updating.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
docEngine.resizeBySize(
|
||||
fixLineWidthByScale(editorWidth, precisionScaleX),
|
||||
fixLineWidthByScale(physicHeight, precisionScaleY)
|
||||
);
|
||||
}, 0);
|
||||
|
||||
const contentBoundingRect = this._layoutService.getContentElement().getBoundingClientRect();
|
||||
const canvasBoundingRect = canvasElement.getBoundingClientRect();
|
||||
startX = startX * scaleAdjust + (canvasBoundingRect.left - contentBoundingRect.left);
|
||||
startY = startY * scaleAdjust + (canvasBoundingRect.top - contentBoundingRect.top);
|
||||
|
||||
// Update cell editor container position and size.
|
||||
this._cellEditorManagerService.setState({
|
||||
startX,
|
||||
startY,
|
||||
endX: editorWidth * scaleAdjust + startX,
|
||||
endY: physicHeight * scaleAdjust + startY,
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the document does not support cell background color, an additional rect needs to be added.
|
||||
*/
|
||||
private _addBackground(scene: Scene, editorWidth: number, editorHeight: number, fill?: Nullable<string>) {
|
||||
const fillRectKey = '_backgroundRectHelperColor_';
|
||||
const rect = scene.getObject(fillRectKey) as Rect;
|
||||
|
||||
if (rect == null && fill == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rect == null) {
|
||||
scene.addObjects(
|
||||
[
|
||||
new Rect(fillRectKey, {
|
||||
width: editorWidth,
|
||||
height: editorHeight,
|
||||
fill,
|
||||
evented: false,
|
||||
}),
|
||||
],
|
||||
DOCS_COMPONENT_MAIN_LAYER_INDEX
|
||||
);
|
||||
} else if (fill == null) {
|
||||
rect.dispose();
|
||||
} else {
|
||||
rect.setProps({
|
||||
fill,
|
||||
});
|
||||
|
||||
rect.transformByState({
|
||||
width: editorWidth,
|
||||
height: editorHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show input area, resize input area and then place input to right place.
|
||||
* @TODO why do resize in show input area?
|
||||
* @param param
|
||||
*/
|
||||
// handleVisible is the 2nd part of editing.
|
||||
// first part: startEditing --> _updateEditor
|
||||
// 2nd part: startEditing --> changeVisible --> slide-editor-bridge.render-controller.ts@changeVisible --> _editorBridgeService.changeVisible
|
||||
private _handleEditorVisible(param: IEditorBridgeServiceVisibleParam) {
|
||||
const { eventType } = param;
|
||||
|
||||
// Change `CursorChange` to changed status, when formula bar clicked.
|
||||
this._cursorChange =
|
||||
[DeviceInputEventType.PointerDown, DeviceInputEventType.Dblclick].includes(eventType)
|
||||
? CursorChange.CursorChange
|
||||
: CursorChange.StartEditor;
|
||||
|
||||
const editCellState = this._editorBridgeService.getEditRectState();
|
||||
if (editCellState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
position,
|
||||
documentLayoutObject,
|
||||
slideCardOffset: canvasOffset,
|
||||
scaleX,
|
||||
scaleY,
|
||||
editorUnitId,
|
||||
unitId,
|
||||
} = editCellState;
|
||||
|
||||
const editorObject = this._getEditorObject();
|
||||
|
||||
if (editorObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scene } = editorObject;
|
||||
|
||||
this._contextService.setContextValue(EDITOR_ACTIVATED, true);
|
||||
|
||||
const { documentModel: documentDataModel } = documentLayoutObject;
|
||||
const skeleton = this._getEditorSkeleton(editorUnitId);
|
||||
if (!skeleton || !documentDataModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// core function! set editing area size and pos
|
||||
this._fitTextSize(position, canvasOffset, skeleton, documentLayoutObject, scaleX, scaleY);
|
||||
|
||||
// TODO: @JOCS, Get the position close to the cursor after clicking on the cell.
|
||||
const cursor = documentDataModel.getBody()!.dataStream.length - 2 || 0;
|
||||
|
||||
scene.getViewport(VIEWPORT_KEY.VIEW_MAIN)?.scrollToViewportPos({
|
||||
viewportScrollX: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
|
||||
this._textSelectionManagerService.replaceDocRanges([
|
||||
{
|
||||
startOffset: cursor,
|
||||
endOffset: cursor,
|
||||
},
|
||||
]);
|
||||
|
||||
this._renderManagerService.getRenderUnitById(unitId)?.scene.resetCursor();
|
||||
}
|
||||
|
||||
private _resetBodyStyle(body: IDocumentBody, removeStyle = false) {
|
||||
body.dataStream = DEFAULT_EMPTY_DOCUMENT_VALUE;
|
||||
|
||||
if (body.textRuns != null) {
|
||||
if (body.textRuns.length === 1 && !removeStyle) {
|
||||
body.textRuns[0].st = 0;
|
||||
body.textRuns[0].ed = 1;
|
||||
} else {
|
||||
body.textRuns = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (body.paragraphs != null) {
|
||||
if (body.paragraphs.length === 1) {
|
||||
body.paragraphs[0].startIndex = 0;
|
||||
} else {
|
||||
body.paragraphs = [
|
||||
{
|
||||
startIndex: 0,
|
||||
paragraphId: createParagraphId(new Set()),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (body.sectionBreaks != null) {
|
||||
body.sectionBreaks = undefined;
|
||||
}
|
||||
|
||||
if (body.tables != null) {
|
||||
body.tables = undefined;
|
||||
}
|
||||
|
||||
if (body.customRanges != null) {
|
||||
body.customRanges = undefined;
|
||||
}
|
||||
|
||||
if (body.customBlocks != null) {
|
||||
body.customBlocks = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should activate the editor when the user inputs text.
|
||||
* @param d DisposableCollection
|
||||
*/
|
||||
private _initialKeyboardListener(d: DisposableCollection) {
|
||||
// d.add(this._textSelectionRenderManager.onInputBefore$.subscribe((config) => {
|
||||
// // const isFocusFormulaEditor = this._contextService.getContextValue(FORMULA_EDITOR_ACTIVATED);
|
||||
// // const isFocusSheets = this._contextService.getContextValue(FOCUSING_SHEET);
|
||||
|
||||
// // // TODO@Jocs: should get editor instead of current doc
|
||||
// // const unitId = this._instanceSrv.getCurrentUniverDocInstance()?.getUnitId();
|
||||
// // if (unitId && isFocusSheets && !isFocusFormulaEditor && this._editorService.isSheetEditor(unitId)) {
|
||||
// // this._showEditorByKeyboard(config);
|
||||
// // }
|
||||
// }));
|
||||
}
|
||||
|
||||
private _showEditorByKeyboard(config: Nullable<IEditorInputConfig>) {
|
||||
if (config == null) {
|
||||
//..
|
||||
}
|
||||
|
||||
// const event = config.event as KeyboardEvent;
|
||||
|
||||
// this._commandService.executeCommand(SetCellEditVisibleOperation.id, {
|
||||
// visible: true,
|
||||
// eventType: DeviceInputEventType.Keyboard,
|
||||
// keycode: event.which,
|
||||
// unitId: this._context.unitId,
|
||||
// });
|
||||
}
|
||||
|
||||
private _commandExecutedListener(d: DisposableCollection) {
|
||||
const moveCursorOP = [SetTextEditArrowOperation.id];
|
||||
const editedMutations = [RichTextEditingMutation.id];
|
||||
|
||||
d.add(this._commandService.onCommandExecuted((command: ICommandInfo) => {
|
||||
// Only should do something when it is the current editor.
|
||||
// FIXME: listen to command execution is pretty expensive. We should
|
||||
// have multi editor instances and only handle event from a single editor.
|
||||
if (this._editorService.getFocusId() !== SLIDE_EDITOR_ID) {
|
||||
// if (this._editorService.getFocusId() !== this._renderContext.unitId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (moveCursorOP.includes(command.id)) {
|
||||
this._moveCursorCmdHandler(command);
|
||||
}
|
||||
|
||||
if (editedMutations.includes(command.id)) {
|
||||
if (this._editorBridgeService.isVisible()) {
|
||||
this._editingChangedHandler();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _moveCursorCmdHandler(command: ICommandInfo) {
|
||||
const params = command.params as IEditorBridgeServiceVisibleParam & { isShift: boolean };
|
||||
const { keycode, isShift } = params;
|
||||
|
||||
/**
|
||||
* After the user enters the editor and actively moves the editor selection area with the mouse,
|
||||
* the up, down, left, and right keys can no longer switch editing cells,
|
||||
* but move the cursor within the editor instead.
|
||||
*/
|
||||
if (keycode != null && this._cursorChange === CursorChange.CursorChange) {
|
||||
this._moveInEditor(keycode, isShift);
|
||||
} else {
|
||||
// TODO @Jocs: After we merging editor related controllers, this seems verbose.
|
||||
this._editorBridgeService.changeVisible(params);
|
||||
}
|
||||
}
|
||||
|
||||
private _editingChangedHandler() {
|
||||
const editRect = this._editorBridgeService.getEditorRect();
|
||||
if (!editRect) {
|
||||
return;
|
||||
}
|
||||
const editingRichText = editRect.richTextObj;
|
||||
editingRichText.refreshDocumentByDocData();
|
||||
editingRichText.resizeToContentSize();
|
||||
|
||||
const { unitId } = this._renderContext;
|
||||
this._handleEditorVisible({ visible: true, eventType: 3, unitId });
|
||||
}
|
||||
|
||||
private _getEditorObject() {
|
||||
return getEditorObject(this._editorBridgeService.getCurrentEditorId(), this._renderManagerService);
|
||||
}
|
||||
|
||||
private async _handleEditorInvisible(param: IEditorBridgeServiceVisibleParam) {
|
||||
const { keycode } = param;
|
||||
|
||||
this._cursorChange = CursorChange.InitialState;
|
||||
|
||||
this._exitInput(param);
|
||||
|
||||
const editCellState = this._editorBridgeService.getEditRectState();
|
||||
if (editCellState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const { unitId, documentLayoutObject } = editCellState;
|
||||
|
||||
// If neither the formula bar editor nor the cell editor has been edited,
|
||||
// it is considered that the content has not changed and returns directly.
|
||||
const editorIsDirty = this._editorBridgeService.getEditorDirty();
|
||||
if (editorIsDirty === false) {
|
||||
this._moveCursor(keycode);
|
||||
return;
|
||||
}
|
||||
|
||||
// moveCursor need to put behind of SetRangeValuesCommand, fix https://github.com/dream-num/univer/issues/1155
|
||||
this._moveCursor(keycode);
|
||||
}
|
||||
|
||||
private _exitInput(param: IEditorBridgeServiceVisibleParam) {
|
||||
this._contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
|
||||
this._cellEditorManagerService.setState({
|
||||
show: param.visible,
|
||||
});
|
||||
const editorUnitId = this._editorBridgeService.getCurrentEditorId();
|
||||
if (editorUnitId == null) {
|
||||
return;
|
||||
}
|
||||
this._undoRedoService.clearUndoRedo(editorUnitId);
|
||||
}
|
||||
|
||||
private _moveCursor(keycode?: KeyCode) {
|
||||
if (keycode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let direction = Direction.LEFT;
|
||||
|
||||
switch (keycode) {
|
||||
case KeyCode.ENTER:
|
||||
direction = Direction.DOWN;
|
||||
break;
|
||||
case KeyCode.TAB:
|
||||
direction = Direction.RIGHT;
|
||||
break;
|
||||
case KeyCode.ARROW_DOWN:
|
||||
direction = Direction.DOWN;
|
||||
break;
|
||||
case KeyCode.ARROW_UP:
|
||||
direction = Direction.UP;
|
||||
break;
|
||||
case KeyCode.ARROW_LEFT:
|
||||
direction = Direction.LEFT;
|
||||
break;
|
||||
case KeyCode.ARROW_RIGHT:
|
||||
direction = Direction.RIGHT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's operations follow the sequence of opening the editor and then moving the cursor.
|
||||
* The logic here predicts the user's first cursor movement behavior based on this rule
|
||||
*/
|
||||
private _cursorStateListener(d: DisposableCollection) {
|
||||
const editorObject = this._getEditorObject();
|
||||
if (!editorObject) {
|
||||
return;
|
||||
}
|
||||
const { document: documentComponent } = editorObject;
|
||||
|
||||
d.add(toDisposable(documentComponent.onPointerDown$.subscribeEvent(() => {
|
||||
if (this._cursorChange === CursorChange.StartEditor) {
|
||||
this._cursorChange = CursorChange.CursorChange;
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
// TODO: @JOCS, is it necessary to move these commands MoveSelectionOperation\MoveCursorOperation to shortcut? and use multi-commands?
|
||||
private _moveInEditor(keycode: KeyCode, isShift: boolean) {
|
||||
let direction = Direction.LEFT;
|
||||
if (keycode === KeyCode.ARROW_DOWN) {
|
||||
direction = Direction.DOWN;
|
||||
} else if (keycode === KeyCode.ARROW_UP) {
|
||||
direction = Direction.UP;
|
||||
} else if (keycode === KeyCode.ARROW_RIGHT) {
|
||||
direction = Direction.RIGHT;
|
||||
}
|
||||
|
||||
if (isShift) {
|
||||
this._commandService.executeCommand(MoveSelectionOperation.id, {
|
||||
direction,
|
||||
});
|
||||
} else {
|
||||
this._commandService.executeCommand(MoveCursorOperation.id, {
|
||||
direction,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getEditorObject(
|
||||
unitId: Nullable<string>,
|
||||
renderManagerService: IRenderManagerService
|
||||
): Nullable<IDocObjectParam> {
|
||||
if (unitId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRender = renderManagerService.getRenderUnitById(unitId);
|
||||
|
||||
if (currentRender == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainComponent, scene, engine, components } = currentRender;
|
||||
|
||||
const document = mainComponent as Documents;
|
||||
const docBackground = components.get(DOCS_VIEW_KEY.BACKGROUND) as DocBackground;
|
||||
|
||||
return {
|
||||
document,
|
||||
docBackground,
|
||||
scene,
|
||||
engine,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDisposable, Nullable, UnitModel } from '@univerjs/core';
|
||||
import type { BaseObject, IRenderContext, IRenderModule, RichText, Scene, Slide } from '@univerjs/engine-render';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import type { ISetEditorInfo } from '../services/slide-editor-bridge.service';
|
||||
import type { ISlideRichTextProps } from '../type';
|
||||
import { DisposableCollection, ICommandService, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { DeviceInputEventType, ObjectType } from '@univerjs/engine-render';
|
||||
import { Subject } from 'rxjs';
|
||||
import { UpdateSlideElementOperation } from '../commands/operations/update-element.operation';
|
||||
import { ISlideEditorBridgeService } from '../services/slide-editor-bridge.service';
|
||||
|
||||
// interface ICanvasOffset {
|
||||
// left: number;
|
||||
// top: number;
|
||||
// }
|
||||
|
||||
// enum CursorChange {
|
||||
// InitialState,
|
||||
// StartEditor,
|
||||
// CursorChange,
|
||||
// }
|
||||
|
||||
export class SlideEditorBridgeRenderController extends RxDisposable implements IRenderModule {
|
||||
/**
|
||||
* It is used to distinguish whether the user has actively moved the cursor in the editor, mainly through mouse clicks.
|
||||
*/
|
||||
// private _cursorChange: CursorChange = CursorChange.InitialState;
|
||||
|
||||
/** If the corresponding unit is active and prepared for editing. */
|
||||
// private _isUnitEditing = false;
|
||||
|
||||
setSlideTextEditor$: Subject<ISlideRichTextProps> = new Subject();
|
||||
|
||||
private _curRichText = null as RichText | null;
|
||||
|
||||
private _d: Nullable<IDisposable>;
|
||||
|
||||
constructor(
|
||||
private readonly _renderContext: IRenderContext<UnitModel>,
|
||||
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ISlideEditorBridgeService private readonly _editorBridgeService: ISlideEditorBridgeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.disposeWithMe(this._instanceSrv.getCurrentTypeOfUnit$<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE).subscribe((slideDataModel) => {
|
||||
if (slideDataModel && slideDataModel.getUnitId() === this._renderContext.unitId) {
|
||||
this._d = this._init();
|
||||
} else {
|
||||
this._disposeCurrent();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _init(): IDisposable {
|
||||
const d = new DisposableCollection();
|
||||
this._initEventListener(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
private _disposeCurrent(): void {
|
||||
this._d?.dispose();
|
||||
this._d = null;
|
||||
}
|
||||
|
||||
private _setEditorRect(pageId: string, targetObject: RichText) {
|
||||
this._curRichText = targetObject as RichText;
|
||||
const { scene, engine } = this._renderContext;
|
||||
const unitId = this._renderContext.unitId;
|
||||
|
||||
const setEditorRect: ISetEditorInfo = {
|
||||
scene,
|
||||
engine,
|
||||
unitId,
|
||||
pageId,
|
||||
richTextObj: targetObject,
|
||||
};
|
||||
|
||||
this._editorBridgeService.setEditorRect(setEditorRect);
|
||||
}
|
||||
|
||||
private _initEventListener(d: DisposableCollection) {
|
||||
const listenersForPageScene = (scene: Scene) => {
|
||||
const transformer = scene.getTransformer();
|
||||
if (!transformer) return;
|
||||
|
||||
d.add(transformer.clearControl$.subscribe(() => {
|
||||
this.setEditorVisible(false);
|
||||
this.pickOtherObjects();
|
||||
}));
|
||||
d.add(transformer.createControl$.subscribe(() => {
|
||||
this.setEditorVisible(false);
|
||||
}));
|
||||
|
||||
// d.add(transformer.changeStart$.subscribe((param: IChangeObserverConfig) => {
|
||||
// const target = param.target;
|
||||
// if (!target) return;
|
||||
// if (target === this._curRichText) {
|
||||
// // do nothing
|
||||
// } else {
|
||||
// this.pickOtherObjects();
|
||||
// }
|
||||
// }));
|
||||
|
||||
d.add(scene.onDblclick$.subscribeEvent(() => {
|
||||
transformer.clearControls();
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) return;
|
||||
|
||||
if (object.objectType !== ObjectType.RICH_TEXT) {
|
||||
this.pickOtherObjects();
|
||||
} else {
|
||||
this.startEditing(scene.sceneKey, object as RichText);
|
||||
}
|
||||
}));
|
||||
|
||||
d.add(this._instanceSrv.focused$.subscribe((fc: Nullable<string>) => {
|
||||
this.endEditing();
|
||||
}));
|
||||
};
|
||||
|
||||
// const model = this._instanceSrv.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
// const pagesMap = model?.getPages() ?? {};
|
||||
// const pages = Object.values(pagesMap);
|
||||
|
||||
const { mainComponent } = this._renderContext;
|
||||
const slide = mainComponent as Slide;
|
||||
|
||||
// for new pages created by user
|
||||
// TODO: better only one pageScene listener. but how?
|
||||
// TODO: this controller is in render stage. but default page created in ready stage.
|
||||
slide.subSceneChanged$.subscribeEvent((pageScene: Scene) => {
|
||||
listenersForPageScene(pageScene);
|
||||
});
|
||||
|
||||
// for default pages that already exist
|
||||
const pageSceneList = Array.from((mainComponent as Slide).getSubScenes().values());
|
||||
for (let i = 0; i < pageSceneList.length; i++) {
|
||||
const pageScene = pageSceneList[i] as Scene;
|
||||
listenersForPageScene(pageScene);
|
||||
}
|
||||
}
|
||||
|
||||
pickOtherObjects() {
|
||||
this.endEditing();
|
||||
}
|
||||
|
||||
/**
|
||||
* invoked when picking other object.
|
||||
*
|
||||
* save editing state to curr richText.
|
||||
*/
|
||||
endEditing() {
|
||||
if (!this._curRichText) return;
|
||||
this.setEditorVisible(false);
|
||||
const curRichText = this._curRichText;
|
||||
|
||||
const slideData = this._instanceSrv.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
if (!slideData) return false;
|
||||
curRichText.refreshDocumentByDocData();
|
||||
curRichText.resizeToContentSize();
|
||||
|
||||
const richText: Record<string, any> = {
|
||||
bl: 1,
|
||||
fs: curRichText.fs,
|
||||
text: curRichText.text,
|
||||
};
|
||||
const textRuns = curRichText.documentData.body?.textRuns;
|
||||
if (textRuns && textRuns.length) {
|
||||
const textRun = textRuns[0];
|
||||
const ts = textRun.ts;
|
||||
|
||||
richText.cl = ts?.cl;
|
||||
}
|
||||
|
||||
this._commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
unitId: this._renderContext.unitId,
|
||||
oKey: curRichText?.oKey,
|
||||
props: {
|
||||
richText,
|
||||
},
|
||||
});
|
||||
this._curRichText = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO calling twice ????
|
||||
* editingParam derives from RichText object.
|
||||
*
|
||||
* TODO @lumixraku need scale param
|
||||
* @param target
|
||||
*/
|
||||
startEditing(pageId: string, target: RichText) {
|
||||
// this.setSlideTextEditor$.next({ content, rect });
|
||||
|
||||
this._setEditorRect(pageId, target);
|
||||
this.setEditorVisible(true);
|
||||
}
|
||||
|
||||
setEditorVisible(visible: boolean) {
|
||||
// if editor is visible, hide curr RichTerxtObject
|
||||
if (visible) {
|
||||
this._curRichText?.hide();
|
||||
} else {
|
||||
this._curRichText?.show();
|
||||
}
|
||||
const { unitId } = this._renderContext;
|
||||
this._editorBridgeService.changeVisible({ visible, eventType: DeviceInputEventType.PointerDown, unitId });
|
||||
}
|
||||
}
|
||||
471
packages/slides-ui/src/controllers/slide.render-controller.ts
Normal file
471
packages/slides-ui/src/controllers/slide.render-controller.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* 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 { EventState, IColorStyle, Nullable, UnitModel } from '@univerjs/core';
|
||||
import type { BaseObject, IRenderContext, IRenderModule, IWheelEvent } from '@univerjs/engine-render';
|
||||
import type { IPageElement, ISlidePage, SlideDataModel } from '@univerjs/slides';
|
||||
import type { PageID } from '../type';
|
||||
import { debounce, getColorStyle, Inject, Injector, IUniverInstanceService, RxDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import {
|
||||
getCurrentTypeOfRenderer,
|
||||
IRenderManagerService,
|
||||
Rect,
|
||||
Scene,
|
||||
ScrollBar,
|
||||
Slide,
|
||||
Viewport,
|
||||
} from '@univerjs/engine-render';
|
||||
import { ObjectProvider, SLIDE_KEY } from '@univerjs/slides';
|
||||
|
||||
export class SlideRenderController extends RxDisposable implements IRenderModule {
|
||||
private _objectProvider: ObjectProvider | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly _renderContext: IRenderContext<UnitModel>,
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
|
||||
) {
|
||||
super();
|
||||
this._objectProvider = this._injector.createInstance(ObjectProvider);
|
||||
this._addNewRender();
|
||||
}
|
||||
|
||||
private _addNewRender() {
|
||||
const { unitId, engine, scene } = this._renderContext;
|
||||
const slideDataModel = this._getCurrUnitModel();
|
||||
|
||||
if (!slideDataModel) return;
|
||||
|
||||
// createRender moved to slideRenderService@this._instanceSrv.getAllUnitsForType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE).forEach((slideModel)
|
||||
// this._renderManagerService.createRender(unitId);
|
||||
|
||||
//#region scene subscribe
|
||||
// const { engine, scene } = currentRender;
|
||||
const observer = engine.onTransformChange$.subscribeEvent(() => {
|
||||
this._scrollToCenter();
|
||||
// add once
|
||||
observer?.unsubscribe();
|
||||
});
|
||||
engine.onTransformChange$.subscribeEvent(() => {
|
||||
setTimeout(() => {
|
||||
this.createThumbs();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const viewMain = new Viewport(SLIDE_KEY.VIEW, scene, {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
explicitViewportWidthSet: false,
|
||||
explicitViewportHeightSet: false,
|
||||
isWheelPreventDefaultX: true,
|
||||
});
|
||||
scene.attachControl();
|
||||
scene.onMouseWheel$.subscribeEvent((evt: unknown, state: EventState) => {
|
||||
const e = evt as IWheelEvent;
|
||||
if (e.ctrlKey) {
|
||||
const deltaFactor = Math.abs(e.deltaX);
|
||||
let scrollNum = deltaFactor < 40 ? 0.2 : deltaFactor < 80 ? 0.4 : 0.2;
|
||||
scrollNum *= e.deltaY > 0 ? -1 : 1;
|
||||
if (scene.scaleX < 1) {
|
||||
scrollNum /= 2;
|
||||
}
|
||||
|
||||
if (scene.scaleX + scrollNum > 4) {
|
||||
scene.scale(4, 4);
|
||||
} else if (scene.scaleX + scrollNum < 0.1) {
|
||||
scene.scale(0.1, 0.1);
|
||||
} else {
|
||||
const value = e.deltaY > 0 ? 0.1 : -0.1;
|
||||
// scene.scaleBy(scrollNum, scrollNum);
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
viewMain.onMouseWheel(e, state);
|
||||
}
|
||||
});
|
||||
scene.onFileLoaded$.subscribeEvent(() => {
|
||||
this._refreshThumb();
|
||||
});
|
||||
//#endregion
|
||||
|
||||
ScrollBar.attachTo(viewMain);
|
||||
// this._renderManagerService.setCurrent(unitId);
|
||||
|
||||
// #region create slide
|
||||
const slide = this._createSlide(scene);
|
||||
this._renderContext.mainComponent = slide;
|
||||
this._createSlidePages(slideDataModel, slide);
|
||||
this.createThumbs();
|
||||
// #endregion
|
||||
|
||||
engine.runRenderLoop(() => {
|
||||
scene.render();
|
||||
});
|
||||
}
|
||||
|
||||
private _scrollToCenter() {
|
||||
const mainScene = this._currentRender()?.scene;
|
||||
const viewMain = mainScene?.getViewport(SLIDE_KEY.VIEW);
|
||||
const getCenterPositionViewPort = this._getCenterPositionViewPort(mainScene);
|
||||
if (!viewMain || !getCenterPositionViewPort) return;
|
||||
const { left: viewPortLeft, top: viewPortTop } = getCenterPositionViewPort;
|
||||
|
||||
const { x, y } = viewMain.transViewportScroll2ScrollValue(viewPortLeft, viewPortTop);
|
||||
|
||||
viewMain.scrollToBarPos({
|
||||
x,
|
||||
y,
|
||||
});
|
||||
}
|
||||
|
||||
private _currentRender() {
|
||||
return getCurrentTypeOfRenderer(UniverInstanceType.UNIVER_SLIDE, this._univerInstanceService, this._renderManagerService);
|
||||
}
|
||||
|
||||
private _refreshThumb = debounce(() => {
|
||||
this.createThumbs();
|
||||
}, 300);
|
||||
|
||||
/**
|
||||
* @param mainScene
|
||||
*/
|
||||
private _createSlide(mainScene: Scene) {
|
||||
const model = this._univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE)!;
|
||||
|
||||
const { width: sceneWidth, height: sceneHeight } = mainScene;
|
||||
|
||||
const pageSize = model.getPageSize();
|
||||
|
||||
const { width = 100, height = 100 } = pageSize;
|
||||
|
||||
const slideComponent = new Slide(SLIDE_KEY.COMPONENT, {
|
||||
left: (sceneWidth - width) / 2,
|
||||
top: (sceneHeight - height) / 2,
|
||||
width,
|
||||
height,
|
||||
zIndex: 10,
|
||||
});
|
||||
|
||||
// slideComponent.enableNav();
|
||||
|
||||
slideComponent.enableSelectedClipElement();
|
||||
|
||||
mainScene.addObject(slideComponent);
|
||||
|
||||
return slideComponent;
|
||||
}
|
||||
|
||||
private _addBackgroundRect(scene: Scene, fill: IColorStyle) {
|
||||
const model = this._univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE)!;
|
||||
|
||||
const pageSize = model.getPageSize();
|
||||
|
||||
const { width: pageWidth = 0, height: pageHeight = 0 } = pageSize;
|
||||
|
||||
const page = new Rect('canvas', {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
strokeWidth: 1,
|
||||
stroke: 'rgba(198,198,198,1)',
|
||||
fill: getColorStyle(fill) || 'rgba(255,255,255,1)',
|
||||
zIndex: 0,
|
||||
evented: false,
|
||||
});
|
||||
scene.addObject(page, 0);
|
||||
}
|
||||
|
||||
private _getCenterPositionViewPort(mainScene?: Scene) {
|
||||
if (!mainScene) return { left: 0, top: 0 };
|
||||
const { width, height } = mainScene;
|
||||
|
||||
const engine = mainScene.getEngine();
|
||||
|
||||
const canvasWidth = engine?.width || 0;
|
||||
const canvasHeight = engine?.height || 0;
|
||||
|
||||
return {
|
||||
left: (width - canvasWidth) / 2,
|
||||
top: (height - canvasHeight) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
private _thumbSceneRender(pageId: string, slide: Slide) {
|
||||
const render = this._renderManagerService.getRenderUnitById(pageId);
|
||||
|
||||
if (render == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { engine: thumbEngine } = render;
|
||||
|
||||
if (thumbEngine == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = slide;
|
||||
|
||||
const { width: pageWidth = width, height: pageHeight = height } = thumbEngine;
|
||||
|
||||
const thumbContext = thumbEngine.getCanvas().getContext();
|
||||
|
||||
slide.renderToThumb(thumbContext, pageId, pageWidth / width, pageHeight / height);
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateScene by pages, and activate first one.
|
||||
* @param slideDataModel
|
||||
* @param slide
|
||||
*/
|
||||
private _createSlidePages(slideDataModel: SlideDataModel, slide: Slide) {
|
||||
const pages = slideDataModel.getPages();
|
||||
|
||||
const pageOrder = slideDataModel.getPageOrder();
|
||||
|
||||
if (!pages || !pageOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageOrder.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0, len = pageOrder.length; i < len; i++) {
|
||||
const pageId = pageOrder[i];
|
||||
|
||||
this.createPageScene(pageId, pages[pageId]);
|
||||
|
||||
this._createThumb(pageId);
|
||||
}
|
||||
|
||||
// setTimeout(() => {
|
||||
// for (let i = 0, len = pageOrder.length; i < len; i++) {
|
||||
// const pageId = pageOrder[i];
|
||||
|
||||
// this._thumbSceneRender(pageId, slide);
|
||||
// }
|
||||
// }, 0);
|
||||
|
||||
slide.activeFirstPage();
|
||||
}
|
||||
|
||||
private _createThumb(pageId: string) {
|
||||
this._renderManagerService.createRender(pageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* SlideDataModel is UnitModel
|
||||
*/
|
||||
private _getCurrUnitModel() {
|
||||
// return this._univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE)!;
|
||||
|
||||
return this._renderContext.unit as SlideDataModel;
|
||||
}
|
||||
|
||||
activePage(_pageId?: string) {
|
||||
let pageId = _pageId;
|
||||
const model = this._getCurrUnitModel();
|
||||
let page: Nullable<ISlidePage>;
|
||||
if (pageId) {
|
||||
page = model.getPage(pageId);
|
||||
} else {
|
||||
const pageElements = model.getPages();
|
||||
const pageOrder = model.getPageOrder();
|
||||
if (pageOrder == null || pageElements == null) {
|
||||
return;
|
||||
}
|
||||
page = pageElements[pageOrder[0]];
|
||||
|
||||
pageId = page.id;
|
||||
}
|
||||
|
||||
const render = this._currentRender();
|
||||
|
||||
if (page == null || render == null || render.mainComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = page;
|
||||
|
||||
const slide = render.mainComponent as Slide;
|
||||
|
||||
model.setActivePage(page);
|
||||
|
||||
if (slide?.hasPage(id)) {
|
||||
slide.changePage(id);
|
||||
return;
|
||||
}
|
||||
|
||||
this.createPageScene(id, page);
|
||||
}
|
||||
|
||||
createThumbs() {
|
||||
const slideDataModel = this._getCurrUnitModel();
|
||||
const pageOrder = slideDataModel.getPageOrder();
|
||||
|
||||
const render = this._currentRender();
|
||||
|
||||
if (!pageOrder || !render) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageOrder.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0, len = pageOrder.length; i < len; i++) {
|
||||
const pageId = pageOrder[i];
|
||||
|
||||
this._thumbSceneRender(pageId, render.mainComponent as Slide);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create scene by page and set to _sceneMap.
|
||||
* @param pageId
|
||||
* @param page
|
||||
*/
|
||||
createPageScene(pageId: string, page: ISlidePage): Nullable<Scene> {
|
||||
// const render = this._currentRender();
|
||||
const render = this._renderContext;
|
||||
if (!render || !this._objectProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scene: mainScene, mainComponent } = render;
|
||||
|
||||
const slide = mainComponent as Slide;
|
||||
const { width, height } = slide;
|
||||
|
||||
const pageScene = new Scene(pageId, slide, {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
const viewMain = new Viewport(`PageViewer_${pageId}`, pageScene, {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
explicitViewportWidthSet: false,
|
||||
explicitViewportHeightSet: false,
|
||||
});
|
||||
viewMain.closeClip();
|
||||
|
||||
const { pageElements, pageBackgroundFill } = page;
|
||||
|
||||
// SceneViewers
|
||||
const objects = this._objectProvider.convertToRenderObjects(pageElements, mainScene);
|
||||
if (!objects || !slide) return;
|
||||
|
||||
this._addBackgroundRect(pageScene, pageBackgroundFill);
|
||||
pageScene.addObjects(objects);
|
||||
|
||||
pageScene.initTransformer();
|
||||
objects.forEach((object) => {
|
||||
pageScene.attachTransformerTo(object);
|
||||
});
|
||||
|
||||
const transformer = pageScene.getTransformer();
|
||||
|
||||
transformer?.changeEnd$.subscribe(() => {
|
||||
this._thumbSceneRender(pageId, slide);
|
||||
});
|
||||
|
||||
transformer?.clearControl$.subscribe(() => {
|
||||
this._thumbSceneRender(pageId, slide);
|
||||
});
|
||||
|
||||
// add SubScene
|
||||
slide.addPageScene(pageScene);
|
||||
|
||||
return pageScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pageScene from Slide.
|
||||
* @param pageId
|
||||
* @returns {Scene, Engine, UnitModel} scene & engine & unit from renderContext
|
||||
*/
|
||||
getPageRenderUnit(pageId: PageID) {
|
||||
//pageScene was added to the mainComponent(Slide) in createPageScene --> slide.addPageScene
|
||||
const subsceneMap = (this._renderContext.mainComponent as Slide).getSubScenes();
|
||||
const pageScene = subsceneMap.get(pageId) as unknown as Scene;
|
||||
const { engine, unit } = this._renderContext;
|
||||
return {
|
||||
scene: pageScene,
|
||||
engine,
|
||||
unit,
|
||||
};
|
||||
}
|
||||
|
||||
createObjectToPage(element: IPageElement, pageID: PageID): Nullable<BaseObject> {
|
||||
const { scene } = this.getPageRenderUnit(pageID);
|
||||
|
||||
if (!scene || !this._objectProvider) {
|
||||
return;
|
||||
}
|
||||
const object = this._objectProvider.convertToRenderObject(element, scene);
|
||||
if (object) {
|
||||
scene.addObject(object);
|
||||
scene.attachTransformerTo(object);
|
||||
scene.getLayer().makeDirty();
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
setObjectActiveByPage(obj: BaseObject, pageID: PageID) {
|
||||
const { scene } = this.getPageRenderUnit(pageID);
|
||||
if (!scene) return;
|
||||
const transformer = scene.getTransformer();
|
||||
transformer?.activeAnObject(obj);
|
||||
}
|
||||
|
||||
removeObjectById(id: string, pageID: PageID) {
|
||||
const { scene } = this.getPageRenderUnit(pageID);
|
||||
if (!scene) return;
|
||||
scene.removeObject(id);
|
||||
const transformer = scene.getTransformer();
|
||||
transformer?.clearControls();
|
||||
}
|
||||
|
||||
appendPage() {
|
||||
const model = this._getCurrUnitModel();
|
||||
const page = model.getBlankPage();
|
||||
const render = this._currentRender();
|
||||
|
||||
if (page == null || render == null || render.mainComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id: pageId } = page;
|
||||
|
||||
const slide = render.mainComponent as Slide;
|
||||
const scene = this.createPageScene(pageId, page);
|
||||
|
||||
if (slide && scene) {
|
||||
slide.addPageScene(scene);
|
||||
}
|
||||
|
||||
model.appendPage(page);
|
||||
model.setActivePage(page);
|
||||
}
|
||||
}
|
||||
102
packages/slides-ui/src/controllers/ui.controller.ts
Normal file
102
packages/slides-ui/src/controllers/ui.controller.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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, Inject, Injector } from '@univerjs/core';
|
||||
import { BuiltInUIPart, connectInjector, IMenuManagerService, IShortcutService, IUIPartsService } from '@univerjs/ui';
|
||||
import { ActivateSlidePageOperation } from '../commands/operations/activate.operation';
|
||||
import { AppendSlideOperation } from '../commands/operations/append-slide.operation';
|
||||
import { DeleteSlideElementOperation } from '../commands/operations/delete-element.operation';
|
||||
import { InsertSlideFloatImageCommand } from '../commands/operations/insert-image.operation';
|
||||
import {
|
||||
InsertSlideShapeEllipseCommand,
|
||||
InsertSlideShapeEllipseOperation,
|
||||
InsertSlideShapeRectangleCommand,
|
||||
InsertSlideShapeRectangleOperation,
|
||||
ToggleSlideEditSidebarOperation,
|
||||
} from '../commands/operations/insert-shape.operation';
|
||||
import { SlideAddTextCommand, SlideAddTextOperation } from '../commands/operations/insert-text.operation';
|
||||
import { SetSlidePageThumbOperation } from '../commands/operations/set-thumb.operation';
|
||||
import { SetTextEditArrowOperation } from '../commands/operations/text-edit.operation';
|
||||
import { UpdateSlideElementOperation } from '../commands/operations/update-element.operation';
|
||||
import { menuSchema } from '../menu/schema';
|
||||
import { SlideEditorContainer } from '../views/editor-container/EditorContainer';
|
||||
import { SlideSideBar } from '../views/slide-bar/SlideBar';
|
||||
import { EditorDeleteLeftShortcut, generateArrowSelectionShortCutItem } from './shortcuts/editor.shortcuts';
|
||||
|
||||
/**
|
||||
* This controller registers UI parts of slide workbench to the base-ui workbench.
|
||||
*/
|
||||
export class SlidesUIController extends Disposable {
|
||||
constructor(
|
||||
@Inject(Injector) protected readonly _injector: Injector,
|
||||
@IMenuManagerService protected readonly _menuManagerService: IMenuManagerService,
|
||||
@IUIPartsService protected readonly _uiPartsService: IUIPartsService,
|
||||
@ICommandService protected readonly _commandService: ICommandService,
|
||||
@IShortcutService protected readonly _shortcutService: IShortcutService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCommands();
|
||||
this._initUIComponents();
|
||||
this._initMenus();
|
||||
this._initShortcuts();
|
||||
}
|
||||
|
||||
private _initMenus(): void {
|
||||
this._menuManagerService.mergeMenu(menuSchema);
|
||||
}
|
||||
|
||||
private _initCommands(): void {
|
||||
[
|
||||
AppendSlideOperation,
|
||||
ActivateSlidePageOperation,
|
||||
SetSlidePageThumbOperation,
|
||||
InsertSlideFloatImageCommand,
|
||||
SlideAddTextOperation,
|
||||
SlideAddTextCommand,
|
||||
InsertSlideShapeEllipseCommand,
|
||||
InsertSlideShapeEllipseOperation,
|
||||
InsertSlideShapeRectangleOperation,
|
||||
InsertSlideShapeRectangleCommand,
|
||||
ToggleSlideEditSidebarOperation,
|
||||
DeleteSlideElementOperation,
|
||||
UpdateSlideElementOperation,
|
||||
|
||||
// commands for editor
|
||||
SetTextEditArrowOperation,
|
||||
|
||||
].forEach((command) => this.disposeWithMe(this._commandService.registerCommand(command)));
|
||||
}
|
||||
|
||||
protected _initUIComponents(): void {
|
||||
this.disposeWithMe(
|
||||
this._uiPartsService.registerComponent(BuiltInUIPart.LEFT_SIDEBAR, () => connectInjector(SlideSideBar, this._injector))
|
||||
);
|
||||
|
||||
this.disposeWithMe(
|
||||
this._uiPartsService.registerComponent(BuiltInUIPart.CONTENT, () => connectInjector(SlideEditorContainer, this._injector))
|
||||
);
|
||||
}
|
||||
|
||||
private _initShortcuts(): void {
|
||||
[
|
||||
EditorDeleteLeftShortcut,
|
||||
...generateArrowSelectionShortCutItem(),
|
||||
].forEach((item) => {
|
||||
this.disposeWithMe(this._shortcutService.registerShortcut(item));
|
||||
});
|
||||
}
|
||||
}
|
||||
2
packages/slides-ui/src/global.css
Normal file
2
packages/slides-ui/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
41
packages/slides-ui/src/index.ts
Normal file
41
packages/slides-ui/src/index.ts
Normal file
@@ -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 './global.css';
|
||||
|
||||
export { ActivateSlidePageOperation } from './commands/operations/activate.operation';
|
||||
export { AppendSlideOperation } from './commands/operations/append-slide.operation';
|
||||
export { DeleteSlideElementOperation } from './commands/operations/delete-element.operation';
|
||||
export { InsertSlideFloatImageCommand } from './commands/operations/insert-image.operation';
|
||||
export {
|
||||
InsertSlideShapeEllipseCommand,
|
||||
InsertSlideShapeEllipseOperation,
|
||||
InsertSlideShapeRectangleCommand,
|
||||
InsertSlideShapeRectangleOperation,
|
||||
} from './commands/operations/insert-shape.operation';
|
||||
export { SlideAddTextCommand, SlideAddTextOperation } from './commands/operations/insert-text.operation';
|
||||
export { SetSlidePageThumbOperation } from './commands/operations/set-thumb.operation';
|
||||
export { UpdateSlideElementOperation } from './commands/operations/update-element.operation';
|
||||
export type { IUniverSlidesUIConfig } from './config/config';
|
||||
export { SLIDE_EDITOR_ID } from './const';
|
||||
export { CanvasView } from './controllers/canvas-view';
|
||||
export { SlidesUIController } from './controllers/ui.controller';
|
||||
export { SLIDES_IMAGE_MENU_ID } from './menu/image.menu';
|
||||
export { menuSchema as SlidesUIMenuSchema } from './menu/schema';
|
||||
export { SHAPE_MENU_ID } from './menu/shape.menu';
|
||||
export { UniverSlidesUIPlugin } from './plugin';
|
||||
export { ISlideEditorBridgeService } from './services/slide-editor-bridge.service';
|
||||
export { SlideCanvasPopMangerService } from './services/slide-popup-manager.service';
|
||||
80
packages/slides-ui/src/locale/ar-SA.ts
Normal file
80
packages/slides-ui/src/locale/ar-SA.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'إلحاق شريحة',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'إدراج نص',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'إدراج شكل',
|
||||
rectangle: 'إدراج مستطيل',
|
||||
ellipse: 'إدراج قطع ناقص',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'إدراج صورة',
|
||||
float: 'إدراج صورة عائمة',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'تحرير',
|
||||
delete: 'حذف',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'تحرير النص',
|
||||
shape: 'تحرير الشكل',
|
||||
image: 'تحرير الصورة',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'ترتيب',
|
||||
forward: 'إحضار إلى الأمام',
|
||||
backward: 'إرسال إلى الخلف',
|
||||
front: 'إحضار إلى المقدمة',
|
||||
back: 'إرسال إلى المؤخرة',
|
||||
},
|
||||
transform: {
|
||||
title: 'تحويل',
|
||||
width: 'العرض (بكسل)',
|
||||
height: 'الارتفاع (بكسل)',
|
||||
x: 'X (بكسل)',
|
||||
y: 'Y (بكسل)',
|
||||
rotate: 'تدوير (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'لون التعبئة',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/ca-ES.ts
Normal file
80
packages/slides-ui/src/locale/ca-ES.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Afegeix diapositiva',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Insereix text',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insereix forma',
|
||||
rectangle: 'Insereix rectangle',
|
||||
ellipse: 'Insereix el·lipse',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Insereix imatge',
|
||||
float: 'Insereix imatge flotant',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Edita',
|
||||
delete: 'Elimina',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Edita text',
|
||||
shape: 'Edita forma',
|
||||
image: 'Edita imatge',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Organitza',
|
||||
forward: 'Porta endavant',
|
||||
backward: 'Envia enrere',
|
||||
front: 'Porta al davant',
|
||||
back: 'Envia al fons',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transforma',
|
||||
width: 'Amplada (px)',
|
||||
height: 'Alçada (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Gira (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Color de farciment',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/de-DE.ts
Normal file
80
packages/slides-ui/src/locale/de-DE.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Folie anhängen',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Text einfügen',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Form einfügen',
|
||||
rectangle: 'Rechteck einfügen',
|
||||
ellipse: 'Ellipse einfügen',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Bild einfügen',
|
||||
float: 'Schwebendes Bild einfügen',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Bearbeiten',
|
||||
delete: 'Löschen',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Text bearbeiten',
|
||||
shape: 'Form bearbeiten',
|
||||
image: 'Bild bearbeiten',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Anordnen',
|
||||
forward: 'Nach vorne',
|
||||
backward: 'Nach hinten',
|
||||
front: 'In den Vordergrund',
|
||||
back: 'In den Hintergrund',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transformieren',
|
||||
width: 'Breite (px)',
|
||||
height: 'Höhe (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Drehen (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Füllfarbe',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
78
packages/slides-ui/src/locale/en-US.ts
Normal file
78
packages/slides-ui/src/locale/en-US.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const locale = {
|
||||
'slides-ui': {
|
||||
append: 'Append Slide',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Insert Text',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insert Shape',
|
||||
rectangle: 'Insert Rectangle',
|
||||
ellipse: 'Insert Ellipse',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Insert Image',
|
||||
float: 'Insert Float Image',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Edit Text',
|
||||
shape: 'Edit Shape',
|
||||
image: 'Edit Image',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Arrange',
|
||||
forward: 'Bring Forward',
|
||||
backward: 'Send Backward',
|
||||
front: 'Bring to Front',
|
||||
back: 'Send to Back',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transform',
|
||||
width: 'Width (px)',
|
||||
height: 'Height (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Rotate (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Fill Color',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/es-ES.ts
Normal file
80
packages/slides-ui/src/locale/es-ES.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Añadir diapositiva',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Insertar texto',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insertar forma',
|
||||
rectangle: 'Insertar rectángulo',
|
||||
ellipse: 'Insertar elipse',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Insertar imagen',
|
||||
float: 'Insertar imagen flotante',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Editar',
|
||||
delete: 'Eliminar',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Editar texto',
|
||||
shape: 'Editar forma',
|
||||
image: 'Editar imagen',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Organizar',
|
||||
forward: 'Traer adelante',
|
||||
backward: 'Enviar atrás',
|
||||
front: 'Traer al frente',
|
||||
back: 'Enviar al fondo',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transformar',
|
||||
width: 'Ancho (px)',
|
||||
height: 'Alto (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Rotar (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Color de relleno',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/fa-IR.ts
Normal file
80
packages/slides-ui/src/locale/fa-IR.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'اضافه کردن اسلاید',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'درج متن',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'درج شکل',
|
||||
rectangle: 'درج مستطیل',
|
||||
ellipse: 'بیضی را درج کنید',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'درج تصویر',
|
||||
float: 'درج تصویر شناور',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'ویرایش',
|
||||
delete: 'حذف',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'ویرایش متن',
|
||||
shape: 'ویرایش شکل',
|
||||
image: 'ویرایش تصویر',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'ترتیبدهی',
|
||||
forward: 'آوردن به جلو',
|
||||
backward: 'فرستادن به عقب',
|
||||
front: 'آوردن به جلوی همه',
|
||||
back: 'فرستادن به پشت همه',
|
||||
},
|
||||
transform: {
|
||||
title: 'تبدیل',
|
||||
width: 'عرض (px)',
|
||||
height: 'ارتفاع (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'چرخش (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'رنگ پر کردن',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/fr-FR.ts
Normal file
80
packages/slides-ui/src/locale/fr-FR.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Ajouter une diapositive',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Insérer du texte',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Insérer une forme',
|
||||
rectangle: 'Insérer un rectangle',
|
||||
ellipse: 'Insérer une ellipse',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Insérer une image',
|
||||
float: 'Insérer une image flottante',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Éditer',
|
||||
delete: 'Supprimer',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Éditer le texte',
|
||||
shape: 'Éditer la forme',
|
||||
image: 'Éditer l\'image',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Arranger',
|
||||
forward: 'Avancer',
|
||||
backward: 'Reculer',
|
||||
front: 'Mettre au premier plan',
|
||||
back: 'Mettre à l\'arrière-plan',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transformer',
|
||||
width: 'Largeur (px)',
|
||||
height: 'Hauteur (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Pivoter (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Couleur de remplissage',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/id-ID.ts
Normal file
80
packages/slides-ui/src/locale/id-ID.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Tambah Slide',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Sisipkan Teks',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Sisipkan Bentuk',
|
||||
rectangle: 'Sisipkan Persegi Panjang',
|
||||
ellipse: 'Sisipkan Elips',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Sisipkan Gambar',
|
||||
float: 'Sisipkan Gambar Mengambang',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Edit',
|
||||
delete: 'Hapus',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Edit Teks',
|
||||
shape: 'Edit Bentuk',
|
||||
image: 'Edit Gambar',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Atur',
|
||||
forward: 'Bawa ke Depan',
|
||||
backward: 'Kirim ke Belakang',
|
||||
front: 'Bawa ke Paling Depan',
|
||||
back: 'Kirim ke Paling Belakang',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transformasi',
|
||||
width: 'Lebar (px)',
|
||||
height: 'Tinggi (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Putar (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Warna Isi',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/it-IT.ts
Normal file
80
packages/slides-ui/src/locale/it-IT.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Aggiungi Diapositiva',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Inserisci Testo',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Inserisci Forma',
|
||||
rectangle: 'Inserisci Rettangolo',
|
||||
ellipse: 'Inserisci Ellisse',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Inserisci Immagine',
|
||||
float: 'Inserisci Immagine Fluttuante',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Modifica',
|
||||
delete: 'Elimina',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Modifica Testo',
|
||||
shape: 'Modifica Forma',
|
||||
image: 'Modifica Immagine',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Disposizione',
|
||||
forward: 'Porta Avanti',
|
||||
backward: 'Manda Indietro',
|
||||
front: 'Porta in Primo Piano',
|
||||
back: 'Manda in Secondo Piano',
|
||||
},
|
||||
transform: {
|
||||
title: 'Trasforma',
|
||||
width: 'Larghezza (px)',
|
||||
height: 'Altezza (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Ruota (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Colore Riempimento',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/ja-JP.ts
Normal file
80
packages/slides-ui/src/locale/ja-JP.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'スライドを追加',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'テキストの挿入',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: '図形の挿入',
|
||||
rectangle: '四角形を挿入',
|
||||
ellipse: '楕円を挿入',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: '画像の挿入',
|
||||
float: 'フローティング画像を挿入',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: '編集',
|
||||
delete: '削除',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'テキスト編集',
|
||||
shape: '図形編集',
|
||||
image: '画像編集',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: '配置',
|
||||
forward: '前面へ移動',
|
||||
backward: '背面へ移動',
|
||||
front: '最前面へ移動',
|
||||
back: '最背面へ移動',
|
||||
},
|
||||
transform: {
|
||||
title: '変形',
|
||||
width: '幅 (px)',
|
||||
height: '高さ (px)',
|
||||
x: 'X座標 (px)',
|
||||
y: 'Y座標 (px)',
|
||||
rotate: '回転 (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: '塗りつぶしの色',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/ko-KR.ts
Normal file
80
packages/slides-ui/src/locale/ko-KR.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: '슬라이드 추가',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: '텍스트 삽입',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: '도형 삽입',
|
||||
rectangle: '사각형 삽입',
|
||||
ellipse: '타원 삽입',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: '이미지 삽입',
|
||||
float: '플로팅 이미지 삽입',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: '편집',
|
||||
delete: '삭제',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: '텍스트 편집',
|
||||
shape: '도형 편집',
|
||||
image: '이미지 편집',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: '정렬',
|
||||
forward: '앞으로 가져오기',
|
||||
backward: '뒤로 보내기',
|
||||
front: '맨 앞으로 가져오기',
|
||||
back: '맨 뒤로 보내기',
|
||||
},
|
||||
transform: {
|
||||
title: '변형',
|
||||
width: '너비 (px)',
|
||||
height: '높이 (px)',
|
||||
x: 'X 좌표 (px)',
|
||||
y: 'Y 좌표 (px)',
|
||||
rotate: '회전 (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: '채우기 색상',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/pl-PL.ts
Normal file
80
packages/slides-ui/src/locale/pl-PL.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Dodaj slajd',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Wstaw tekst',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Wstaw kształt',
|
||||
rectangle: 'Wstaw prostokąt',
|
||||
ellipse: 'Wstaw elipsę',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Wstaw obraz',
|
||||
float: 'Wstaw obraz swobodny',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Edytuj',
|
||||
delete: 'Usuń',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Edytuj tekst',
|
||||
shape: 'Edytuj kształt',
|
||||
image: 'Edytuj obraz',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Rozmieść',
|
||||
forward: 'Przesuń do przodu',
|
||||
backward: 'Przesuń do tyłu',
|
||||
front: 'Przesuń na wierzch',
|
||||
back: 'Przesuń na spód',
|
||||
},
|
||||
transform: {
|
||||
title: 'Przekształć',
|
||||
width: 'Szerokość (px)',
|
||||
height: 'Wysokość (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Obrót (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Kolor wypełnienia',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/pt-BR.ts
Normal file
80
packages/slides-ui/src/locale/pt-BR.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Adicionar Slide',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Inserir Texto',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Inserir Forma',
|
||||
rectangle: 'Inserir Retângulo',
|
||||
ellipse: 'Inserir Elipse',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Inserir Imagem',
|
||||
float: 'Inserir Imagem Flutuante',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Editar',
|
||||
delete: 'Excluir',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Editar Texto',
|
||||
shape: 'Editar Forma',
|
||||
image: 'Editar Imagem',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Organizar',
|
||||
forward: 'Trazer para Frente',
|
||||
backward: 'Enviar para Trás',
|
||||
front: 'Trazer para a Frente',
|
||||
back: 'Enviar para Trás',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transformar',
|
||||
width: 'Largura (px)',
|
||||
height: 'Altura (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Rotacionar (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Cor de Preenchimento',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/ru-RU.ts
Normal file
80
packages/slides-ui/src/locale/ru-RU.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Добавить слайд',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Вставить текст',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Вставить фигуру',
|
||||
rectangle: 'Вставить прямоугольник',
|
||||
ellipse: 'Вставить эллипс',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Вставить изображение',
|
||||
float: 'Вставить плавающее изображение',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Изменить',
|
||||
delete: 'Удалить',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Изменить текст',
|
||||
shape: 'Изменить фигуру',
|
||||
image: 'Изменить изображение',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Упорядочить',
|
||||
forward: 'Переложить вперёд',
|
||||
backward: 'Переложить назад',
|
||||
front: 'На передний план',
|
||||
back: 'На задний план',
|
||||
},
|
||||
transform: {
|
||||
title: 'Преобразование',
|
||||
width: 'Ширина (пкс)',
|
||||
height: 'Высота (пкс)',
|
||||
x: 'X (пкс)',
|
||||
y: 'Y (пкс)',
|
||||
rotate: 'Повернуть (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Цвет заливки',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/sk-SK.ts
Normal file
80
packages/slides-ui/src/locale/sk-SK.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Pridať snímku',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Vložiť text',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Vložiť tvar',
|
||||
rectangle: 'Vložiť obdĺžnik',
|
||||
ellipse: 'Vložiť elipsu',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Vložiť obrázok',
|
||||
float: 'Vložiť plávajúci obrázok',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Upraviť',
|
||||
delete: 'Odstrániť',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Upraviť text',
|
||||
shape: 'Upraviť tvar',
|
||||
image: 'Upraviť obrázok',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Usporiadať',
|
||||
forward: 'Posunúť dopredu',
|
||||
backward: 'Posunúť dozadu',
|
||||
front: 'Presunúť do popredia',
|
||||
back: 'Presunúť do pozadia',
|
||||
},
|
||||
transform: {
|
||||
title: 'Transformovať',
|
||||
width: 'Šírka (px)',
|
||||
height: 'Výška (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Otočiť (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Farba výplne',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
20
packages/slides-ui/src/locale/types.ts
Normal file
20
packages/slides-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>;
|
||||
80
packages/slides-ui/src/locale/vi-VN.ts
Normal file
80
packages/slides-ui/src/locale/vi-VN.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: 'Thêm trang chiếu',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: 'Chèn văn bản',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: 'Chèn hình',
|
||||
rectangle: 'Chèn hình chữ nhật',
|
||||
ellipse: 'Chèn hình elip',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: 'Chèn hình ảnh',
|
||||
float: 'Chèn hình ảnh nổi',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: 'Chỉnh sửa',
|
||||
delete: 'Xóa',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: 'Chỉnh sửa văn bản',
|
||||
shape: 'Chỉnh sửa hình',
|
||||
image: 'Chỉnh sửa hình ảnh',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: 'Sắp xếp',
|
||||
forward: 'Chuyển lên trên',
|
||||
backward: 'Chuyển xuống dưới',
|
||||
front: 'Đưa lên trên cùng',
|
||||
back: 'Đưa xuống dưới cùng',
|
||||
},
|
||||
transform: {
|
||||
title: 'Biến đổi',
|
||||
width: 'Chiều rộng (px)',
|
||||
height: 'Chiều cao (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: 'Xoay (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: 'Màu tô',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/zh-CN.ts
Normal file
80
packages/slides-ui/src/locale/zh-CN.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: '新增幻灯片',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: '插入文本',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: '插入图形',
|
||||
rectangle: '插入矩形',
|
||||
ellipse: '插入椭圆',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: '插入图片',
|
||||
float: '插入浮动图片',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: '编辑文本',
|
||||
shape: '编辑图形',
|
||||
image: '编辑图片',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: '排列',
|
||||
forward: '上移一层',
|
||||
backward: '下移一层',
|
||||
front: '置于顶层',
|
||||
back: '置于底层',
|
||||
},
|
||||
transform: {
|
||||
title: '变换',
|
||||
width: '宽度 (px)',
|
||||
height: '高度 (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: '旋转 (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: '填充',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/zh-HK.ts
Normal file
80
packages/slides-ui/src/locale/zh-HK.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: '新增投影片',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: '插入文字',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: '插入圖形',
|
||||
rectangle: '插入矩形',
|
||||
ellipse: '插入橢圓',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: '插入圖片',
|
||||
float: '插入浮動圖片',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: '編輯',
|
||||
delete: '刪除',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: '編輯文字',
|
||||
shape: '編輯圖形',
|
||||
image: '編輯圖片',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: '排列',
|
||||
forward: '上移一層',
|
||||
backward: '下移一層',
|
||||
front: '至頂',
|
||||
back: '至底',
|
||||
},
|
||||
transform: {
|
||||
title: '變換',
|
||||
width: '寬度 (px)',
|
||||
height: '高度 (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: '旋轉 (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: '填色',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
80
packages/slides-ui/src/locale/zh-TW.ts
Normal file
80
packages/slides-ui/src/locale/zh-TW.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'slides-ui': {
|
||||
append: '新增投影片',
|
||||
|
||||
text: {
|
||||
insert: {
|
||||
title: '插入文字',
|
||||
},
|
||||
},
|
||||
|
||||
shape: {
|
||||
insert: {
|
||||
title: '插入圖形',
|
||||
rectangle: '插入矩形',
|
||||
ellipse: '插入橢圓',
|
||||
},
|
||||
},
|
||||
|
||||
image: {
|
||||
insert: {
|
||||
title: '插入圖片',
|
||||
float: '插入浮動圖片',
|
||||
},
|
||||
},
|
||||
|
||||
popup: {
|
||||
edit: '編輯',
|
||||
delete: '刪除',
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
text: '編輯文字',
|
||||
shape: '編輯圖形',
|
||||
image: '編輯圖片',
|
||||
},
|
||||
|
||||
'image-panel': {
|
||||
arrange: {
|
||||
title: '排列',
|
||||
forward: '上移一層',
|
||||
backward: '下移一層',
|
||||
front: '至頂',
|
||||
back: '至底',
|
||||
},
|
||||
transform: {
|
||||
title: '變換',
|
||||
width: '寬度 (px)',
|
||||
height: '高度 (px)',
|
||||
x: 'X (px)',
|
||||
y: 'Y (px)',
|
||||
rotate: '旋轉 (°)',
|
||||
},
|
||||
},
|
||||
panel: {
|
||||
fill: {
|
||||
title: '填色',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
43
packages/slides-ui/src/menu/image.menu.ts
Normal file
43
packages/slides-ui/src/menu/image.menu.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IAccessor } from '@univerjs/core';
|
||||
import type { IMenuItem } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { UniverInstanceType } from '@univerjs/core';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { InsertSlideFloatImageCommand } from '../commands/operations/insert-image.operation';
|
||||
|
||||
export const SLIDES_IMAGE_MENU_ID = 'slide.menu.image';
|
||||
|
||||
export function SlideImageMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: SLIDES_IMAGE_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
icon: 'AddImageIcon',
|
||||
tooltip: 'slides-ui.image.insert.title',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SLIDE),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadSlideFloatImageMenuFactory(_accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertSlideFloatImageCommand.id,
|
||||
title: 'slides-ui.image.insert.float',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(_accessor, UniverInstanceType.UNIVER_SLIDE),
|
||||
};
|
||||
}
|
||||
201
packages/slides-ui/src/menu/popup-menu.controller.ts
Normal file
201
packages/slides-ui/src/menu/popup-menu.controller.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDisposable, Nullable } from '@univerjs/core';
|
||||
import type { BaseObject, ObjectType, Scene } from '@univerjs/engine-render';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { FOCUSING_COMMON_DRAWINGS, ICommandService, IContextService, Inject, IUniverInstanceService, RxDisposable, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ISidebarService } from '@univerjs/ui';
|
||||
import { DeleteSlideElementOperation } from '../commands/operations/delete-element.operation';
|
||||
import { ToggleSlideEditSidebarOperation } from '../commands/operations/insert-shape.operation';
|
||||
import { UpdateSlideElementOperation } from '../commands/operations/update-element.operation';
|
||||
import { CanvasView } from '../controllers/canvas-view';
|
||||
import { SlideCanvasPopMangerService } from '../services/slide-popup-manager.service';
|
||||
import { COMPONENT_SLIDE_IMAGE_POPUP_MENU } from '../views/image-popup-menu/component-name';
|
||||
|
||||
export class SlidePopupMenuController extends RxDisposable {
|
||||
private _initImagePopupMenu = new Set<string>();
|
||||
|
||||
constructor(
|
||||
@Inject(SlideCanvasPopMangerService) private readonly _canvasPopManagerService: SlideCanvasPopMangerService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@Inject(CanvasView) private readonly _canvasView: CanvasView,
|
||||
@ISidebarService private readonly _sidebarService: ISidebarService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init(): void {
|
||||
this._univerInstanceService.getAllUnitsForType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE).forEach((slide) => this._create(slide));
|
||||
}
|
||||
|
||||
private _create(slide: Nullable<SlideDataModel>) {
|
||||
if (!slide) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unitId = slide.getUnitId();
|
||||
|
||||
if (this._renderManagerService.has(unitId) && !this._initImagePopupMenu.has(unitId)) {
|
||||
this._popupMenuListener(unitId);
|
||||
this._initImagePopupMenu.add(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
private _hasCropObject(scene: Scene) {
|
||||
// const objects = scene.getAllObjects();
|
||||
|
||||
// for (const object of objects) {
|
||||
// if (object instanceof ImageCropperObject) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _popupMenuListener(unitId: string) {
|
||||
const model = this._univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
const pages = model?.getPages() ?? {};
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
Object.keys(pages).forEach((pageId) => {
|
||||
const page = this._canvasView.getRenderUnitByPageId(pageId, unitId);
|
||||
const transformer = page.scene?.getTransformer();
|
||||
|
||||
if (!transformer) return;
|
||||
|
||||
let singletonPopupDisposer: IDisposable;
|
||||
this.disposeWithMe(
|
||||
toDisposable(
|
||||
transformer.createControl$.subscribe(() => {
|
||||
// this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, true);
|
||||
|
||||
// if (this._hasCropObject(scene)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (selectedObjects.size > 1) {
|
||||
singletonPopupDisposer?.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oKey = object.oKey;
|
||||
// const drawingParam = this._drawingManagerService.getDrawingOKey(oKey);
|
||||
// if (!drawingParam) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const { unitId, subUnitId, drawingId } = drawingParam;
|
||||
singletonPopupDisposer?.dispose();
|
||||
singletonPopupDisposer = this.disposeWithMe(this._canvasPopManagerService.attachPopupToObject(object, {
|
||||
componentKey: COMPONENT_SLIDE_IMAGE_POPUP_MENU,
|
||||
direction: 'horizontal',
|
||||
offset: [2, 0],
|
||||
extraProps: {
|
||||
menuItems: this._getMenuItemsByObjectType(object.objectType, oKey, unitId),
|
||||
},
|
||||
}));
|
||||
|
||||
if (this._sidebarService.visible) {
|
||||
this._commandService.executeCommand(ToggleSlideEditSidebarOperation.id, {
|
||||
visible: true,
|
||||
objectType: object.objectType,
|
||||
});
|
||||
}
|
||||
|
||||
// this._drawingManagerService.focusDrawing([{
|
||||
// unitId,
|
||||
// subUnitId,
|
||||
// drawingId,
|
||||
// }]);
|
||||
})
|
||||
)
|
||||
);
|
||||
this.disposeWithMe(
|
||||
transformer.clearControl$.subscribe(() => {
|
||||
singletonPopupDisposer?.dispose();
|
||||
this._contextService.setContextValue(FOCUSING_COMMON_DRAWINGS, false);
|
||||
// this._drawingManagerService.focusDrawing(null);
|
||||
})
|
||||
);
|
||||
this.disposeWithMe(
|
||||
transformer.changing$.subscribe(() => {
|
||||
singletonPopupDisposer?.dispose();
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
if (selectedObjects.size > 1) {
|
||||
singletonPopupDisposer?.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
unitId,
|
||||
oKey: object.oKey,
|
||||
props: {
|
||||
width: object.width,
|
||||
height: object.height,
|
||||
left: object.left,
|
||||
top: object.top,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _getMenuItemsByObjectType(objectType: ObjectType, oKey: string, unitId: string) {
|
||||
const menuItems = [{
|
||||
label: 'slides-ui.popup.edit',
|
||||
index: 0,
|
||||
commandId: ToggleSlideEditSidebarOperation.id,
|
||||
commandParams: {
|
||||
visible: true,
|
||||
objectType,
|
||||
},
|
||||
disable: false,
|
||||
}, {
|
||||
label: 'slides-ui.popup.delete',
|
||||
index: 5,
|
||||
commandId: DeleteSlideElementOperation.id,
|
||||
commandParams: {
|
||||
id: oKey,
|
||||
unitId,
|
||||
},
|
||||
disable: false,
|
||||
}];
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
}
|
||||
56
packages/slides-ui/src/menu/schema.ts
Normal file
56
packages/slides-ui/src/menu/schema.ts
Normal file
@@ -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 { MenuSchemaType } from '@univerjs/ui';
|
||||
import { RibbonStartGroup } from '@univerjs/ui';
|
||||
import { InsertSlideFloatImageCommand } from '../commands/operations/insert-image.operation';
|
||||
import { InsertSlideShapeEllipseCommand, InsertSlideShapeRectangleCommand } from '../commands/operations/insert-shape.operation';
|
||||
import { SlideAddTextCommand } from '../commands/operations/insert-text.operation';
|
||||
import { SlideImageMenuFactory, SLIDES_IMAGE_MENU_ID, UploadSlideFloatImageMenuFactory } from './image.menu';
|
||||
import { SHAPE_MENU_ID, SlideShapeMenuFactory, UploadSlideFloatEllipseShapeMenuFactory, UploadSlideFloatRectangleShapeMenuFactory } from './shape.menu';
|
||||
import { SlideAddTextMenuItemFactory } from './text.menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonStartGroup.FORMAT]: {
|
||||
[SlideAddTextCommand.id]: {
|
||||
order: 0,
|
||||
gridLayout: { row: 1, column: 1, rowSpan: 2, showLabel: true },
|
||||
menuItemFactory: SlideAddTextMenuItemFactory,
|
||||
},
|
||||
[SLIDES_IMAGE_MENU_ID]: {
|
||||
order: 0,
|
||||
gridLayout: { row: 1, column: 2, rowSpan: 2, showLabel: true },
|
||||
menuItemFactory: SlideImageMenuFactory,
|
||||
[InsertSlideFloatImageCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: UploadSlideFloatImageMenuFactory,
|
||||
},
|
||||
},
|
||||
[SHAPE_MENU_ID]: {
|
||||
order: 0,
|
||||
gridLayout: { row: 1, column: 3, rowSpan: 2, showLabel: true },
|
||||
menuItemFactory: SlideShapeMenuFactory,
|
||||
[InsertSlideShapeRectangleCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: UploadSlideFloatRectangleShapeMenuFactory,
|
||||
},
|
||||
[InsertSlideShapeEllipseCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: UploadSlideFloatEllipseShapeMenuFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
55
packages/slides-ui/src/menu/shape.menu.ts
Normal file
55
packages/slides-ui/src/menu/shape.menu.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { 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 {
|
||||
InsertSlideShapeEllipseCommand,
|
||||
InsertSlideShapeRectangleCommand,
|
||||
} from '../commands/operations/insert-shape.operation';
|
||||
|
||||
export const SHAPE_MENU_ID = 'slide.menu.shape';
|
||||
|
||||
export function SlideShapeMenuFactory(accessor: IAccessor): IMenuItem<LocaleKey> {
|
||||
return {
|
||||
id: SHAPE_MENU_ID,
|
||||
type: MenuItemType.SUBITEMS,
|
||||
icon: 'GraphIcon',
|
||||
tooltip: 'slides-ui.shape.insert.title',
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_SLIDE),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadSlideFloatRectangleShapeMenuFactory(_accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertSlideShapeRectangleCommand.id,
|
||||
title: 'slides-ui.shape.insert.rectangle',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(_accessor, UniverInstanceType.UNIVER_SLIDE),
|
||||
};
|
||||
}
|
||||
|
||||
export function UploadSlideFloatEllipseShapeMenuFactory(_accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: InsertSlideShapeEllipseCommand.id,
|
||||
title: 'slides-ui.shape.insert.ellipse',
|
||||
type: MenuItemType.BUTTON,
|
||||
hidden$: getMenuHiddenObservable(_accessor, UniverInstanceType.UNIVER_SLIDE),
|
||||
};
|
||||
}
|
||||
32
packages/slides-ui/src/menu/text.menu.ts
Normal file
32
packages/slides-ui/src/menu/text.menu.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 } from '@univerjs/ui';
|
||||
import type { LocaleKey } from '../locale/types';
|
||||
import { UniverInstanceType } from '@univerjs/core';
|
||||
import { getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { SlideAddTextCommand } from '../commands/operations/insert-text.operation';
|
||||
|
||||
export function SlideAddTextMenuItemFactory(_accessor: IAccessor): IMenuButtonItem<LocaleKey> {
|
||||
return {
|
||||
id: SlideAddTextCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'TextIcon',
|
||||
tooltip: 'slides-ui.text.insert.title',
|
||||
hidden$: getMenuHiddenObservable(_accessor, UniverInstanceType.UNIVER_SLIDE),
|
||||
};
|
||||
}
|
||||
29
packages/slides-ui/src/models/info-bar-model.ts
Normal file
29
packages/slides-ui/src/models/info-bar-model.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class InfoBarModel {
|
||||
constructor(private _name: string) {
|
||||
// super
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this._name = name;
|
||||
}
|
||||
}
|
||||
154
packages/slides-ui/src/plugin.ts
Normal file
154
packages/slides-ui/src/plugin.ts
Normal file
@@ -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 { Dependency } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import type { IUniverSlidesUIConfig } from './config/config';
|
||||
import {
|
||||
DependentOn,
|
||||
IConfigService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
merge,
|
||||
mergeOverrideWithDependencies,
|
||||
Plugin,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverDrawingPlugin } from '@univerjs/drawing';
|
||||
import { IRenderManagerService, UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { UniverSlidesPlugin } from '@univerjs/slides';
|
||||
import pkg from '../package.json';
|
||||
import { defaultPluginConfig, SLIDES_UI_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { CanvasView } from './controllers/canvas-view';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { SlideEditingRenderController } from './controllers/slide-editing.render-controller';
|
||||
import { SlideEditorBridgeRenderController } from './controllers/slide-editor-bridge.render-controller';
|
||||
import { SlideRenderController } from './controllers/slide.render-controller';
|
||||
import { SlidesUIController } from './controllers/ui.controller';
|
||||
import { SlidePopupMenuController } from './menu/popup-menu.controller';
|
||||
import { ISlideEditorBridgeService, SlideEditorBridgeService } from './services/slide-editor-bridge.service';
|
||||
import { ISlideEditorManagerService, SlideEditorManagerService } from './services/slide-editor-manager.service';
|
||||
import { SlideCanvasPopMangerService } from './services/slide-popup-manager.service';
|
||||
import { SlideRenderService } from './services/slide-render.service';
|
||||
|
||||
@DependentOn(
|
||||
UniverDocsPlugin,
|
||||
UniverDrawingPlugin,
|
||||
UniverRenderEnginePlugin,
|
||||
UniverSlidesPlugin,
|
||||
UniverDocsUIPlugin
|
||||
)
|
||||
export class UniverSlidesUIPlugin extends Plugin {
|
||||
static override pluginName = 'UNIVER_SLIDES_UI_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SLIDE;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSlidesUIConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(SLIDES_UI_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
this._injector.add([ComponentsController]);
|
||||
this._injector.get(ComponentsController);
|
||||
mergeOverrideWithDependencies([
|
||||
[SlideRenderService],
|
||||
[ISlideEditorBridgeService, { useClass: SlideEditorBridgeService }],
|
||||
// used by SlideUIController --> EditorContainer
|
||||
[ISlideEditorManagerService, { useClass: SlideEditorManagerService }],
|
||||
[SlideCanvasPopMangerService],
|
||||
] as Dependency[], this._config.override)
|
||||
.forEach((d) => this._injector.add(d));
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
([
|
||||
// SlideRenderService will be init in ready stage, and then calling RenderManagerService@createRender --> init all deps in this rendering register block.
|
||||
|
||||
[SlideRenderController],
|
||||
] as Dependency[]).forEach((m) => {
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SLIDE, m));
|
||||
});
|
||||
|
||||
mergeOverrideWithDependencies([
|
||||
[CanvasView],
|
||||
// cannot register in _renderManagerService now.
|
||||
// [ISlideEditorBridgeService, { useClass: SlideEditorBridgeService }],
|
||||
// // used by SlideUIController --> EditorContainer
|
||||
// [ISlideEditorManagerService, { useClass: SlideEditorManagerService }],
|
||||
|
||||
// SlidesUIController controller should be registered in Ready stage.
|
||||
// SlidesUIController controller would add a new RenderUnit (__INTERNAL_EDITOR__DOCS_NORMAL)
|
||||
[SlidesUIController], // editor service was create in renderManagerService
|
||||
[SlideRenderController],
|
||||
[SlidePopupMenuController],
|
||||
] as Dependency[], this._config.override).forEach((m) => {
|
||||
this._injector.add(m);
|
||||
});
|
||||
|
||||
this._injector.get(CanvasView);
|
||||
this._injector.get(SlideRenderService);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
([
|
||||
// need slideEditorBridgeService
|
||||
// need TextSelectionRenderService which init by EditorContainer
|
||||
[SlideEditorBridgeRenderController],
|
||||
[SlideEditingRenderController],
|
||||
] as Dependency[]).forEach((m) => {
|
||||
// find all renderMap and register module to each item in renderMap
|
||||
this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SLIDE, m));
|
||||
});
|
||||
|
||||
this._markSlideAsFocused();
|
||||
|
||||
this._injector.get(SlidesUIController);
|
||||
}
|
||||
|
||||
override onSteady(): void {
|
||||
this._injector.get(SlidePopupMenuController);
|
||||
}
|
||||
|
||||
private _markSlideAsFocused() {
|
||||
const currentService = this._univerInstanceService;
|
||||
try {
|
||||
const slideDataModel = currentService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE)!;
|
||||
currentService.focusUnit(slideDataModel.getUnitId());
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContextService, IContextService, Injector } from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SLIDE_EDITOR_ID } from '../../const';
|
||||
import { ISlideEditorBridgeService, SlideEditorBridgeService } from '../slide-editor-bridge.service';
|
||||
|
||||
class TestEditorService {
|
||||
readonly focusedEditors: string[] = [];
|
||||
focusedEditor: unknown = {};
|
||||
|
||||
getFocusEditor() {
|
||||
return this.focusedEditor;
|
||||
}
|
||||
|
||||
focus(editorId: string) {
|
||||
this.focusedEditors.push(editorId);
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
getRenderUnitById(unitId: string) {
|
||||
if (unitId !== 'slide-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scene: {
|
||||
getViewport: () => ({ viewportScrollX: 5, viewportScrollY: 10 }),
|
||||
getObject: () => ({ left: 30, top: 45 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('SlideEditorBridgeService', () => {
|
||||
let service: ISlideEditorBridgeService;
|
||||
let editorService: TestEditorService;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = new Injector();
|
||||
injector.add([IEditorService, { useClass: TestEditorService as never }]);
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([ISlideEditorBridgeService, { useClass: SlideEditorBridgeService }]);
|
||||
editorService = injector.get(IEditorService) as unknown as TestEditorService;
|
||||
service = injector.get(ISlideEditorBridgeService);
|
||||
});
|
||||
|
||||
it('publishes editor visibility changes and resets dirty state when editing starts', () => {
|
||||
const states: unknown[] = [];
|
||||
service.visible$.subscribe((state) => states.push(state));
|
||||
service.changeEditorDirty(true);
|
||||
|
||||
service.changeVisible({ visible: true, eventType: DeviceInputEventType.Dblclick, unitId: 'slide-1' });
|
||||
|
||||
expect(service.isVisible()).toBe(true);
|
||||
expect(service.getEditorDirty()).toBe(false);
|
||||
expect(states.at(-1)).toEqual({ visible: true, eventType: DeviceInputEventType.Dblclick, unitId: 'slide-1' });
|
||||
});
|
||||
|
||||
it('calculates editor geometry from the selected rich text object and focuses the slide editor when needed', () => {
|
||||
const states: unknown[] = [];
|
||||
service.currentEditRectState$.subscribe((state) => states.push(state));
|
||||
editorService.focusedEditor = null;
|
||||
|
||||
const editorInfo = {
|
||||
unitId: 'slide-1',
|
||||
pageId: 'page-1',
|
||||
scene: {} as never,
|
||||
engine: {} as never,
|
||||
richTextObj: {
|
||||
left: 100,
|
||||
top: 80,
|
||||
width: 240,
|
||||
height: 60,
|
||||
documentData: {
|
||||
id: 'old-editor-id',
|
||||
body: {
|
||||
dataStream: 'Text\r\n',
|
||||
},
|
||||
documentStyle: {
|
||||
pageSize: { width: 10, height: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
service.setEditorRect(editorInfo as never);
|
||||
|
||||
expect(service.getEditorRect()).toBe(editorInfo);
|
||||
expect(service.getCurrentEditorId()).toBe(SLIDE_EDITOR_ID);
|
||||
expect(editorService.focusedEditors).toEqual([SLIDE_EDITOR_ID]);
|
||||
expect(service.getEditRectState()).toMatchObject({
|
||||
unitId: 'slide-1',
|
||||
editorUnitId: SLIDE_EDITOR_ID,
|
||||
position: {
|
||||
startX: 100,
|
||||
startY: 80,
|
||||
endX: 340,
|
||||
endY: 140,
|
||||
},
|
||||
slideCardOffset: {
|
||||
left: 25,
|
||||
top: 35,
|
||||
},
|
||||
documentLayoutObject: {
|
||||
documentModel: expect.any(Object),
|
||||
},
|
||||
});
|
||||
expect(states.at(-1)).toMatchObject({
|
||||
unitId: 'slide-1',
|
||||
editorUnitId: SLIDE_EDITOR_ID,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injector } from '@univerjs/core';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { ISlideEditorManagerService, SlideEditorManagerService } from '../slide-editor-manager.service';
|
||||
|
||||
describe('SlideEditorManagerService', () => {
|
||||
let service: ISlideEditorManagerService;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = new Injector();
|
||||
injector.add([ISlideEditorManagerService, { useClass: SlideEditorManagerService }]);
|
||||
service = injector.get(ISlideEditorManagerService);
|
||||
});
|
||||
|
||||
it('publishes slide text editor state, bounds and focus changes', () => {
|
||||
const states: unknown[] = [];
|
||||
const rects: unknown[] = [];
|
||||
const focus: boolean[] = [];
|
||||
service.state$.subscribe((state) => states.push(state));
|
||||
service.rect$.subscribe((rect) => rects.push(rect));
|
||||
service.focus$.subscribe((value) => focus.push(value));
|
||||
|
||||
service.setState({ show: true, startX: 12, startY: 24 });
|
||||
service.setRect({ left: 12, top: 24, width: 120, height: 32 });
|
||||
service.setFocus(true);
|
||||
|
||||
expect(states.at(-1)).toEqual({ show: true, startX: 12, startY: 24 });
|
||||
expect(service.getState()).toEqual({ show: true, startX: 12, startY: 24 });
|
||||
expect(rects.at(-1)).toEqual({ left: 12, top: 24, width: 120, height: 32 });
|
||||
expect(service.getRect()).toEqual({ left: 12, top: 24, width: 120, height: 32 });
|
||||
expect(focus).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it('clears editor state and rect when disposed', () => {
|
||||
const states: unknown[] = [];
|
||||
const rects: unknown[] = [];
|
||||
let statesCompleted = false;
|
||||
let rectsCompleted = false;
|
||||
service.state$.subscribe({ next: (state) => states.push(state), complete: () => {
|
||||
statesCompleted = true;
|
||||
} });
|
||||
service.rect$.subscribe({ next: (rect) => rects.push(rect), complete: () => {
|
||||
rectsCompleted = true;
|
||||
} });
|
||||
|
||||
service.setState({ show: true, startX: 1 });
|
||||
service.setRect({ left: 1, top: 2, width: 3, height: 4 });
|
||||
service.dispose();
|
||||
|
||||
expect(service.getState()).toBeNull();
|
||||
expect(service.getRect()).toBeNull();
|
||||
expect(states).toEqual([null, { show: true, startX: 1 }]);
|
||||
expect(rects).toEqual([null, { left: 1, top: 2, width: 3, height: 4 }]);
|
||||
expect(statesCompleted).toBe(true);
|
||||
expect(rectsCompleted).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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, Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { ICanvasPopupService } from '@univerjs/ui';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
SlideCanvasPopMangerService,
|
||||
transformBound2OffsetBound,
|
||||
transformPosition2Offset,
|
||||
} from '../slide-popup-manager.service';
|
||||
|
||||
class TestCanvasPopupService {
|
||||
readonly addedPopups: unknown[] = [];
|
||||
readonly removedPopupIds: string[] = [];
|
||||
|
||||
addPopup(popup: unknown) {
|
||||
this.addedPopups.push(popup);
|
||||
return 'popup-1';
|
||||
}
|
||||
|
||||
removePopup(id: string) {
|
||||
this.removedPopupIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
static hasRender = false;
|
||||
|
||||
static reset() {
|
||||
this.hasRender = false;
|
||||
}
|
||||
|
||||
getRenderUnitById(unitId: string) {
|
||||
if (!TestRenderManagerService.hasRender || unitId !== 'slide-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canvasElement = document.createElement('canvas');
|
||||
canvasElement.style.width = '200px';
|
||||
canvasElement.getBoundingClientRect = () => ({
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 400,
|
||||
height: 300,
|
||||
right: 410,
|
||||
bottom: 320,
|
||||
x: 10,
|
||||
y: 20,
|
||||
toJSON: () => ({}),
|
||||
});
|
||||
|
||||
return {
|
||||
mainComponent: { width: 300, height: 200 },
|
||||
engine: {
|
||||
getCanvasElement: () => canvasElement,
|
||||
},
|
||||
scene: {
|
||||
width: 500,
|
||||
height: 400,
|
||||
getAncestorScale: () => ({ scaleX: 2, scaleY: 3 }),
|
||||
getViewport: () => ({ viewportScrollX: 5, viewportScrollY: 10 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
getCurrentUnitOfType(type: UniverInstanceType) {
|
||||
return type === UniverInstanceType.UNIVER_SLIDE ? { getUnitId: () => 'slide-1' } : null;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandService {}
|
||||
|
||||
describe('SlideCanvasPopMangerService', () => {
|
||||
let service: SlideCanvasPopMangerService;
|
||||
let popupService: TestCanvasPopupService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestRenderManagerService.reset();
|
||||
const injector = new Injector();
|
||||
injector.add([ICanvasPopupService, { useClass: TestCanvasPopupService as never }]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([ICommandService, { useClass: TestCommandService as never }]);
|
||||
injector.add([SlideCanvasPopMangerService]);
|
||||
popupService = injector.get(ICanvasPopupService) as unknown as TestCanvasPopupService;
|
||||
service = injector.get(SlideCanvasPopMangerService);
|
||||
});
|
||||
|
||||
it('does not create a canvas popup when the current slide has no renderer', () => {
|
||||
const disposable = service.attachPopupToObject({ left: 0, top: 0, width: 10, height: 10 } as never, { componentKey: 'slide-comment' });
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(popupService.addedPopups).toEqual([]);
|
||||
expect(popupService.removedPopupIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('attaches a popup to the rendered slide object and removes it on dispose', () => {
|
||||
TestRenderManagerService.hasRender = true;
|
||||
|
||||
const disposable = service.attachPopupToObject({ left: 15, top: 20, width: 30, height: 10 } as never, {
|
||||
componentKey: 'slide-comment',
|
||||
direction: 'horizontal',
|
||||
});
|
||||
|
||||
expect(popupService.addedPopups).toHaveLength(1);
|
||||
expect(popupService.addedPopups[0]).toMatchObject({
|
||||
unitId: 'slide-1',
|
||||
subUnitId: 'default',
|
||||
componentKey: 'slide-comment',
|
||||
direction: 'horizontal',
|
||||
anchorRect: {
|
||||
left: 190,
|
||||
right: 430,
|
||||
top: 300,
|
||||
bottom: 480,
|
||||
},
|
||||
canvasElement: expect.any(HTMLCanvasElement),
|
||||
});
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(popupService.removedPopupIds).toEqual(['popup-1']);
|
||||
});
|
||||
|
||||
it('converts slide coordinates to viewport offsets with scroll and scale applied', () => {
|
||||
const scene = {
|
||||
getAncestorScale: () => ({ scaleX: 2, scaleY: 3 }),
|
||||
getViewport: () => ({ viewportScrollX: 5, viewportScrollY: 10 }),
|
||||
};
|
||||
|
||||
expect(transformPosition2Offset(15, 20, scene as never)).toEqual({ x: 20, y: 30 });
|
||||
expect(transformBound2OffsetBound({ left: 15, top: 20, right: 45, bottom: 30 }, scene as never)).toEqual({
|
||||
left: 20,
|
||||
top: 30,
|
||||
right: 80,
|
||||
bottom: 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContextService, IContextService, Injector, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { Subject } from 'rxjs';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SlideRenderService } from '../slide-render.service';
|
||||
|
||||
function createSlide(unitId: string) {
|
||||
return {
|
||||
getUnitId: () => unitId,
|
||||
};
|
||||
}
|
||||
|
||||
class TestUniverInstanceService {
|
||||
readonly slideAdded$ = new Subject<{ unit: ReturnType<typeof createSlide> }>();
|
||||
readonly slideDisposed$ = new Subject<ReturnType<typeof createSlide>>();
|
||||
private readonly _initialSlides = [createSlide('existing-slide')];
|
||||
|
||||
getTypeOfUnitAdded$(type: UniverInstanceType) {
|
||||
return type === UniverInstanceType.UNIVER_SLIDE ? this.slideAdded$.asObservable() : new Subject().asObservable();
|
||||
}
|
||||
|
||||
getTypeOfUnitDisposed$(type: UniverInstanceType) {
|
||||
return type === UniverInstanceType.UNIVER_SLIDE ? this.slideDisposed$.asObservable() : new Subject().asObservable();
|
||||
}
|
||||
|
||||
getAllUnitsForType(type: UniverInstanceType) {
|
||||
return type === UniverInstanceType.UNIVER_SLIDE ? this._initialSlides : [];
|
||||
}
|
||||
|
||||
getUnit(unitId: string) {
|
||||
return createSlide(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderManagerService {
|
||||
readonly createdRenderers: string[] = [];
|
||||
readonly removedRenderers: string[] = [];
|
||||
|
||||
createRender(unitId: string) {
|
||||
this.createdRenderers.push(unitId);
|
||||
}
|
||||
|
||||
removeRender(unitId: string) {
|
||||
this.removedRenderers.push(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
describe('SlideRenderService', () => {
|
||||
let instanceService: TestUniverInstanceService;
|
||||
let renderManagerService: TestRenderManagerService;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = new Injector();
|
||||
injector.add([IContextService, { useClass: ContextService }]);
|
||||
injector.add([IUniverInstanceService, { useClass: TestUniverInstanceService as never }]);
|
||||
injector.add([IRenderManagerService, { useClass: TestRenderManagerService as never }]);
|
||||
injector.add([SlideRenderService]);
|
||||
instanceService = injector.get(IUniverInstanceService) as unknown as TestUniverInstanceService;
|
||||
renderManagerService = injector.get(IRenderManagerService) as unknown as TestRenderManagerService;
|
||||
injector.get(SlideRenderService);
|
||||
});
|
||||
|
||||
it('creates renderers for existing and newly added slide units', async () => {
|
||||
await Promise.resolve();
|
||||
instanceService.slideAdded$.next({ unit: createSlide('new-slide') });
|
||||
|
||||
expect(renderManagerService.createdRenderers).toEqual(['existing-slide', 'new-slide']);
|
||||
});
|
||||
|
||||
it('removes the renderer when a slide unit is disposed', async () => {
|
||||
await Promise.resolve();
|
||||
instanceService.slideDisposed$.next(createSlide('old-slide'));
|
||||
|
||||
expect(renderManagerService.removedRenderers).toEqual(['old-slide']);
|
||||
});
|
||||
});
|
||||
279
packages/slides-ui/src/services/slide-editor-bridge.service.ts
Normal file
279
packages/slides-ui/src/services/slide-editor-bridge.service.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* 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, IPosition, Nullable } from '@univerjs/core';
|
||||
import type { Engine, IDocumentLayoutObject, RichText, Scene } from '@univerjs/engine-render';
|
||||
import type { KeyCode } from '@univerjs/ui';
|
||||
import type { Observable } from 'rxjs';
|
||||
import {
|
||||
createIdentifier,
|
||||
Disposable,
|
||||
DocumentDataModel,
|
||||
EDITOR_ACTIVATED,
|
||||
FOCUSING_EDITOR_STANDALONE,
|
||||
FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE,
|
||||
HorizontalAlign,
|
||||
IContextService,
|
||||
VerticalAlign,
|
||||
} from '@univerjs/core';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { DeviceInputEventType, IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { SLIDE_KEY } from '@univerjs/slides';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { SLIDE_EDITOR_ID } from '../const';
|
||||
|
||||
// TODO same as @univerjs/slides/views/render/adaptors/index.js
|
||||
export enum SLIDE_VIEW_KEY {
|
||||
MAIN = '__SLIDERender__',
|
||||
SCENE_VIEWER = '__SLIDEViewer__',
|
||||
SCENE = '__SLIDEScene__',
|
||||
VIEWPORT = '__SLIDEViewPort_',
|
||||
}
|
||||
|
||||
export const ISlideEditorBridgeService = createIdentifier<ISlideEditorBridgeService>('univer.slide-editor-bridge.service');
|
||||
|
||||
export interface IEditorBridgeServiceParam {
|
||||
unitId: string;
|
||||
|
||||
/**
|
||||
* pos and size of editing area
|
||||
*/
|
||||
position: IPosition;
|
||||
slideCardOffset: { left: number; top: number };
|
||||
documentLayoutObject: IDocumentLayoutObject;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
editorUnitId: string;
|
||||
}
|
||||
|
||||
export interface IEditorBridgeServiceVisibleParam {
|
||||
visible: boolean;
|
||||
eventType: DeviceInputEventType;
|
||||
unitId: string;
|
||||
keycode?: KeyCode;
|
||||
}
|
||||
|
||||
export interface ISetEditorInfo {
|
||||
scene: Scene;
|
||||
engine: Engine;
|
||||
unitId: string;
|
||||
pageId: string;
|
||||
richTextObj: RichText;
|
||||
}
|
||||
|
||||
export interface ISlideEditorBridgeService {
|
||||
currentEditRectState$: Observable<Nullable<IEditorBridgeServiceParam>>;
|
||||
visible$: Observable<IEditorBridgeServiceVisibleParam>;
|
||||
|
||||
// interceptor: InterceptorManager<{
|
||||
// BEFORE_CELL_EDIT: typeof BEFORE_CELL_EDIT;
|
||||
// AFTER_CELL_EDIT: typeof AFTER_CELL_EDIT;
|
||||
// VALIDATE_CELL: typeof VALIDATE_CELL;
|
||||
// }>;
|
||||
dispose(): void;
|
||||
// refreshEditCellState(): void;
|
||||
|
||||
setEditorRect(param: ISetEditorInfo): void;
|
||||
getEditorRect(): ISetEditorInfo;
|
||||
getEditRectState(): Readonly<Nullable<IEditorBridgeServiceParam>>;
|
||||
// // Gets the DocumentDataModel of the latest table cell based on the latest cell contents
|
||||
// getLatestEditCellState(): Readonly<Nullable<IEditorBridgeServiceParam>>;
|
||||
changeVisible(param: IEditorBridgeServiceVisibleParam): void;
|
||||
changeEditorDirty(dirtyStatus: boolean): void;
|
||||
getEditorDirty(): boolean;
|
||||
isVisible(): boolean;
|
||||
// enableForceKeepVisible(): void;
|
||||
// disableForceKeepVisible(): void;
|
||||
// isForceKeepVisible(): boolean;
|
||||
getCurrentEditorId(): Nullable<string>;
|
||||
}
|
||||
|
||||
export class SlideEditorBridgeService extends Disposable implements ISlideEditorBridgeService, IDisposable {
|
||||
private _editorUnitId: string = SLIDE_EDITOR_ID;
|
||||
|
||||
private _isForceKeepVisible: boolean = false;
|
||||
|
||||
private _editorIsDirty: boolean = false;
|
||||
|
||||
private _currentEditRectState: Nullable<IEditorBridgeServiceParam> = null;
|
||||
private readonly _currentEditRectState$ = new BehaviorSubject<Nullable<IEditorBridgeServiceParam>>(null);
|
||||
readonly currentEditRectState$ = this._currentEditRectState$.asObservable();
|
||||
|
||||
private _visibleParam: IEditorBridgeServiceVisibleParam = {
|
||||
visible: false,
|
||||
eventType: DeviceInputEventType.Dblclick,
|
||||
unitId: '',
|
||||
};
|
||||
|
||||
private readonly _visible$ = new BehaviorSubject<IEditorBridgeServiceVisibleParam>(this._visibleParam);
|
||||
readonly visible$ = this._visible$.asObservable();
|
||||
|
||||
private readonly _afterVisible$ = new BehaviorSubject<IEditorBridgeServiceVisibleParam>(this._visibleParam);
|
||||
readonly afterVisible$ = this._afterVisible$.asObservable();
|
||||
|
||||
private _currentEditRectInfo: ISetEditorInfo;
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
getEditorRect() {
|
||||
return this._currentEditRectInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1st part of startEditing.
|
||||
* @editorInfo editorInfo
|
||||
*/
|
||||
setEditorRect(editorInfo: ISetEditorInfo) {
|
||||
this._currentEditRectInfo = editorInfo;
|
||||
// If there is no editor currently focused, then default to selecting the sheet editor to prevent the editorService from using the previously selected editor object.
|
||||
// todo: wzhudev: In univer mode, it is necessary to switch to the corresponding editorId based on the host's unitId.
|
||||
if (!this._editorService.getFocusEditor()) {
|
||||
this._editorService.focus(SLIDE_EDITOR_ID);
|
||||
this._contextService.setContextValue(EDITOR_ACTIVATED, false);
|
||||
this._contextService.setContextValue(FOCUSING_EDITOR_STANDALONE, false);
|
||||
this._contextService.setContextValue(FOCUSING_UNIVER_EDITOR_STANDALONE_SINGLE_MODE, false);
|
||||
}
|
||||
|
||||
const editRectState = this.getEditRectState();
|
||||
this._currentEditRectState = editRectState;
|
||||
|
||||
// slide-editing.render-controller@_subscribeToCurrentCell(Set editorUnitId to curr doc.)
|
||||
// --> activate(-1000, -1000)
|
||||
this._currentEditRectState$.next(editRectState);
|
||||
}
|
||||
|
||||
changeVisible(param: IEditorBridgeServiceVisibleParam) {
|
||||
/**
|
||||
* Non-sheetEditor and formula selection mode,
|
||||
* double-clicking cannot activate the sheet editor.
|
||||
*/
|
||||
// const editor = this._editorService.getFocusEditor();
|
||||
// if (this._refSelectionsService.getCurrentSelections().length > 0 && editor && !editor.isSheetEditor()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
this._visibleParam = param;
|
||||
|
||||
// Reset the dirty status when the editor is visible.
|
||||
if (param.visible) {
|
||||
this._editorIsDirty = false;
|
||||
}
|
||||
|
||||
// subscriber: slide-editing.render-controller.ts@_handleEditorVisible
|
||||
this._visible$.next(this._visibleParam);
|
||||
this._afterVisible$.next(this._visibleParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* get info from _currentEditRectInfo
|
||||
*
|
||||
* invoked by slide-editing.render-controller.ts@_handleEditorVisible
|
||||
* && this@setEditorRect
|
||||
*/
|
||||
getEditRectState(): Readonly<Nullable<IEditorBridgeServiceParam>> {
|
||||
const editorUnitId = SLIDE_EDITOR_ID;
|
||||
|
||||
//editorBridgeRenderController.slideTextEditor$ ---> editorBridgeRenderController@_updateEditor --> this._currentEditRectInfo = xxx
|
||||
const editorRectInfo = this._currentEditRectInfo;
|
||||
const unitId = editorRectInfo.unitId;
|
||||
|
||||
const docData = editorRectInfo.richTextObj.documentData;
|
||||
docData.id = editorUnitId;
|
||||
docData.documentStyle = {
|
||||
...docData.documentStyle,
|
||||
...{
|
||||
pageSize: { width: editorRectInfo.richTextObj.width, height: Infinity },
|
||||
},
|
||||
};
|
||||
|
||||
const docDataModel = new DocumentDataModel(docData);
|
||||
const documentLayoutObject: IDocumentLayoutObject = {
|
||||
documentModel: docDataModel,
|
||||
fontString: 'document',
|
||||
textRotation: { a: 0, v: 0 },
|
||||
wrapStrategy: 0,
|
||||
verticalAlign: VerticalAlign.TOP,
|
||||
horizontalAlign: HorizontalAlign.LEFT,
|
||||
paddingData: { t: 0, b: 1, l: 2, r: 2 },
|
||||
};
|
||||
|
||||
// see insert-text.operation
|
||||
const editorWidth = editorRectInfo.richTextObj.width;
|
||||
const editorHeight = editorRectInfo.richTextObj.height;
|
||||
const left = editorRectInfo.richTextObj.left;
|
||||
const top = editorRectInfo.richTextObj.top;
|
||||
|
||||
const canvasOffset = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
// canvasOffset will be used in slide-editing.render-controller.ts@_handleEditorVisible
|
||||
// const mainScene = this._mainScene;
|
||||
const renderUnit = this._renderManagerService.getRenderUnitById(unitId);
|
||||
const mainScene = renderUnit?.scene;
|
||||
const mainViewport = mainScene?.getViewport(SLIDE_KEY.VIEW);
|
||||
const slideMainRect = mainScene?.getObject(SLIDE_KEY.COMPONENT);
|
||||
const slidePos = {
|
||||
x: slideMainRect?.left || 0,
|
||||
y: slideMainRect?.top || 0,
|
||||
};
|
||||
const scrollX = mainViewport?.viewportScrollX || 0;
|
||||
const scrollY = mainViewport?.viewportScrollY || 0;
|
||||
canvasOffset.left = slidePos.x - scrollX;
|
||||
canvasOffset.top = slidePos.y - scrollY;
|
||||
|
||||
return {
|
||||
position: {
|
||||
startX: left,
|
||||
startY: top,
|
||||
endX: left + editorWidth,
|
||||
endY: top + editorHeight,
|
||||
},
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
slideCardOffset: canvasOffset,
|
||||
unitId,
|
||||
editorUnitId,
|
||||
documentLayoutObject,
|
||||
} as IEditorBridgeServiceParam;
|
||||
}
|
||||
|
||||
changeEditorDirty(dirtyStatus: boolean) {
|
||||
this._editorIsDirty = dirtyStatus;
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return this._visibleParam.visible;
|
||||
}
|
||||
|
||||
getEditorDirty() {
|
||||
return this._editorIsDirty;
|
||||
}
|
||||
|
||||
getCurrentEditorId() {
|
||||
return this._editorUnitId;
|
||||
}
|
||||
}
|
||||
@@ -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 { IDisposable, IPosition, Nullable } from '@univerjs/core';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { createIdentifier } from '@univerjs/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface ICellEditorManagerParam extends Partial<IPosition> {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export interface ICellEditorBoundingClientRect {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface ISlideEditorManagerService {
|
||||
state$: Observable<Nullable<ICellEditorManagerParam>>;
|
||||
rect$: Observable<Nullable<ICellEditorBoundingClientRect>>;
|
||||
focus$: Observable<boolean>;
|
||||
dispose(): void;
|
||||
setState(param: ICellEditorManagerParam): void;
|
||||
getState(): Readonly<Nullable<ICellEditorManagerParam>>;
|
||||
setRect(param: ICellEditorBoundingClientRect): void;
|
||||
getRect(): Readonly<Nullable<ICellEditorBoundingClientRect>>;
|
||||
setFocus(param: boolean): void;
|
||||
}
|
||||
|
||||
export class SlideEditorManagerService implements ISlideEditorManagerService, IDisposable {
|
||||
private _state: Nullable<ICellEditorManagerParam> = null;
|
||||
|
||||
private _rect: Nullable<ICellEditorBoundingClientRect> = null;
|
||||
|
||||
private readonly _state$ = new BehaviorSubject<Nullable<ICellEditorManagerParam>>(null);
|
||||
readonly state$ = this._state$.asObservable();
|
||||
|
||||
private readonly _rect$ = new BehaviorSubject<Nullable<ICellEditorBoundingClientRect>>(null);
|
||||
readonly rect$ = this._rect$.asObservable();
|
||||
|
||||
private _focus: boolean = false;
|
||||
private readonly _focus$ = new BehaviorSubject<boolean>(this._focus);
|
||||
readonly focus$ = this._focus$.asObservable();
|
||||
|
||||
dispose(): void {
|
||||
this._state$.complete();
|
||||
this._state = null;
|
||||
this._rect$.complete();
|
||||
this._rect = null;
|
||||
}
|
||||
|
||||
setState(param: ICellEditorManagerParam) {
|
||||
this._state = param;
|
||||
|
||||
this._refresh(param);
|
||||
}
|
||||
|
||||
getRect(): Readonly<Nullable<ICellEditorBoundingClientRect>> {
|
||||
return this._rect;
|
||||
}
|
||||
|
||||
setRect(param: ICellEditorBoundingClientRect) {
|
||||
this._rect = param;
|
||||
this._rect$.next(param);
|
||||
}
|
||||
|
||||
getState(): Readonly<Nullable<ICellEditorManagerParam>> {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
setFocus(param: boolean = false) {
|
||||
this._focus = param;
|
||||
this._focus$.next(param);
|
||||
}
|
||||
|
||||
private _refresh(param: ICellEditorManagerParam): void {
|
||||
this._state$.next(param);
|
||||
}
|
||||
}
|
||||
|
||||
export const ISlideEditorManagerService = createIdentifier<ISlideEditorManagerService>(
|
||||
'univer.slide-editor-manager.service'
|
||||
);
|
||||
162
packages/slides-ui/src/services/slide-popup-manager.service.ts
Normal file
162
packages/slides-ui/src/services/slide-popup-manager.service.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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 { BaseObject, IBoundRectNoAngle, IRender, Scene } from '@univerjs/engine-render';
|
||||
import type { IPopup } from '@univerjs/ui';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
ICommandService,
|
||||
Inject,
|
||||
IUniverInstanceService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService, pxToNum } from '@univerjs/engine-render';
|
||||
import { SLIDE_KEY } from '@univerjs/slides';
|
||||
import { ICanvasPopupService } from '@univerjs/ui';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface ISlideCanvasPopup extends Pick<IPopup, 'direction' | 'excludeOutside' | 'componentKey' | 'offset' | 'onClickOutside' | 'hideOnInvisible'> {
|
||||
mask?: boolean;
|
||||
extraProps?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function transformBound2OffsetBound(originBound: IBoundRectNoAngle, scene: Scene): IBoundRectNoAngle {
|
||||
const topLeft = transformPosition2Offset(originBound.left, originBound.top, scene);
|
||||
const bottomRight = transformPosition2Offset(originBound.right, originBound.bottom, scene);
|
||||
|
||||
return {
|
||||
left: topLeft.x,
|
||||
top: topLeft.y,
|
||||
right: bottomRight.x,
|
||||
bottom: bottomRight.y,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformPosition2Offset(x: number, y: number, scene: Scene) {
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
const viewMain = scene.getViewport(SLIDE_KEY.VIEW);
|
||||
if (!viewMain) {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
|
||||
const { viewportScrollX: actualScrollX, viewportScrollY: actualScrollY } = viewMain;
|
||||
|
||||
const offsetX = (x - actualScrollX) * scaleX;
|
||||
|
||||
const offsetY = (y - actualScrollY) * scaleY;
|
||||
|
||||
return {
|
||||
x: offsetX,
|
||||
y: offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
export class SlideCanvasPopMangerService extends Disposable {
|
||||
constructor(
|
||||
@Inject(ICanvasPopupService) private readonly _globalPopupManagerService: ICanvasPopupService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService,
|
||||
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private _createObjectPositionObserver(
|
||||
targetObject: BaseObject,
|
||||
currentRender: IRender
|
||||
) {
|
||||
const calc = () => {
|
||||
const { scene, engine } = currentRender;
|
||||
const { left, top, width, height } = targetObject;
|
||||
|
||||
const horizontalOffset = (scene.width - (currentRender.mainComponent?.width ?? 0)) / 2;
|
||||
const verticalOffset = (scene.height - (currentRender.mainComponent?.height ?? 0)) / 2;
|
||||
|
||||
const bound: IBoundRectNoAngle = {
|
||||
left,
|
||||
right: left + width,
|
||||
top,
|
||||
bottom: top + height,
|
||||
};
|
||||
const canvasElement = engine.getCanvasElement();
|
||||
const canvasClientRect = canvasElement.getBoundingClientRect();
|
||||
const widthOfCanvas = pxToNum(canvasElement.style.width); // declared width
|
||||
|
||||
const { scaleX, scaleY } = scene.getAncestorScale();
|
||||
|
||||
const offsetBound = transformBound2OffsetBound(bound, scene);
|
||||
const { top: topOffset, left: leftOffset, width: domWidth } = canvasClientRect;
|
||||
const scaleAdjust = domWidth / widthOfCanvas;
|
||||
|
||||
const position = {
|
||||
left: (offsetBound.left * scaleAdjust * scaleX) + leftOffset + horizontalOffset,
|
||||
right: (offsetBound.right * scaleAdjust * scaleX) + leftOffset + horizontalOffset,
|
||||
top: (offsetBound.top * scaleAdjust * scaleY) + topOffset + verticalOffset,
|
||||
bottom: (offsetBound.bottom * scaleAdjust * scaleY) + topOffset + verticalOffset,
|
||||
};
|
||||
return position;
|
||||
};
|
||||
|
||||
const position = calc();
|
||||
const position$ = new BehaviorSubject(position);
|
||||
const disposable = new DisposableCollection();
|
||||
|
||||
return {
|
||||
position,
|
||||
position$,
|
||||
disposable,
|
||||
};
|
||||
}
|
||||
|
||||
attachPopupToObject(targetObject: BaseObject, popup: ISlideCanvasPopup): IDisposable {
|
||||
const workbook = this._univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SLIDE)!;
|
||||
const unitId = workbook.getUnitId();
|
||||
// const subUnitId =
|
||||
|
||||
const currentRender = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (!currentRender) {
|
||||
return {
|
||||
dispose: () => {
|
||||
// empty
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { position, position$, disposable } = this._createObjectPositionObserver(targetObject, currentRender);
|
||||
|
||||
const id = this._globalPopupManagerService.addPopup({
|
||||
...popup,
|
||||
unitId,
|
||||
subUnitId: 'default',
|
||||
anchorRect: position,
|
||||
anchorRect$: position$,
|
||||
canvasElement: currentRender.engine.getCanvasElement(),
|
||||
});
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this._globalPopupManagerService.removePopup(id);
|
||||
position$.complete();
|
||||
disposable.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
97
packages/slides-ui/src/services/slide-render.service.ts
Normal file
97
packages/slides-ui/src/services/slide-render.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import {
|
||||
IContextService,
|
||||
IUniverInstanceService,
|
||||
RxDisposable,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { takeUntil } from 'rxjs';
|
||||
|
||||
/**
|
||||
* This controller is responsible for managing units of a specific kind to be rendered on the canvas.
|
||||
*/
|
||||
export class SlideRenderService extends RxDisposable {
|
||||
// private _skeletonChangeMutations = new Set<string>();
|
||||
|
||||
constructor(
|
||||
@IContextService private readonly _contextService: IContextService,
|
||||
@IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
// It should be initialized after all other plugins are ready.
|
||||
Promise.resolve().then(() => this._init());
|
||||
}
|
||||
|
||||
private _init() {
|
||||
this._initSlideDataListener();
|
||||
this._initContextListener();
|
||||
}
|
||||
|
||||
private _initSlideDataListener(): void {
|
||||
this._instanceSrv.getTypeOfUnitAdded$<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE)
|
||||
.pipe(takeUntil(this.dispose$))
|
||||
.subscribe((event) => {
|
||||
// TODO when does this function get called?
|
||||
this._createRenderer(event.unit.getUnitId());
|
||||
});
|
||||
|
||||
this._instanceSrv.getAllUnitsForType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE).forEach((slideModel) => {
|
||||
this._createRenderer(slideModel.getUnitId());
|
||||
});
|
||||
|
||||
this._instanceSrv.getTypeOfUnitDisposed$<Workbook>(UniverInstanceType.UNIVER_SLIDE)
|
||||
.pipe(takeUntil(this.dispose$))
|
||||
.subscribe((workbook) => this._disposeRenderer(workbook));
|
||||
}
|
||||
|
||||
private _createRenderer(unitId: string): void {
|
||||
if (unitId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._instanceSrv.getUnit(unitId, UniverInstanceType.UNIVER_SLIDE);
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderManagerService.createRender(unitId);
|
||||
}
|
||||
|
||||
private _disposeRenderer(workbook: Workbook): void {
|
||||
const unitId = workbook.getUnitId();
|
||||
this._renderManagerService.removeRender(unitId);
|
||||
}
|
||||
|
||||
private _initContextListener(): void {
|
||||
// If we toggle the raw formula, we need to refresh all spreadsheets.
|
||||
// this._contextService.subscribeContextValue$(RENDER_RAW_FORMULA_KEY)
|
||||
// .pipe(distinctUntilChanged(), takeUntil(this.dispose$))
|
||||
// .subscribe(() => {
|
||||
// this._renderManagerService.getRenderAll().forEach((renderer) => {
|
||||
// if (renderer.mainComponent instanceof Spreadsheet) {
|
||||
// (renderer.mainComponent as Spreadsheet).makeForceDirty(true);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
||||
}
|
||||
41
packages/slides-ui/src/type.ts
Normal file
41
packages/slides-ui/src/type.ts
Normal file
@@ -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.
|
||||
*/
|
||||
|
||||
export interface ISlideRichTextProps {
|
||||
/**
|
||||
* top offset value of PPT card area(px).
|
||||
*/
|
||||
top: number;
|
||||
/**
|
||||
* left offset value of PPT card area(px).
|
||||
*/
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
text: string;
|
||||
fs: number;
|
||||
//... align margin...
|
||||
};
|
||||
|
||||
export enum CursorChange {
|
||||
InitialState,
|
||||
StartEditor,
|
||||
CursorChange,
|
||||
}
|
||||
|
||||
export type PageID = string;
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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 { createParagraphId, DEFAULT_EMPTY_DOCUMENT_VALUE, DocumentFlavor, IContextService } from '@univerjs/core';
|
||||
import { borderClassName, clsx } from '@univerjs/design';
|
||||
import { IEditorService } from '@univerjs/docs-ui';
|
||||
import { FIX_ONE_PIXEL_BLUR_OFFSET } from '@univerjs/engine-render';
|
||||
import { DISABLE_AUTO_FOCUS_KEY, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SLIDE_EDITOR_ID } from '../../const';
|
||||
import { ISlideEditorManagerService } from '../../services/slide-editor-manager.service';
|
||||
|
||||
const HIDDEN_EDITOR_POSITION = -1000;
|
||||
|
||||
const EDITOR_DEFAULT_POSITION = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: HIDDEN_EDITOR_POSITION,
|
||||
left: HIDDEN_EDITOR_POSITION,
|
||||
};
|
||||
|
||||
/**
|
||||
* Floating editor's container.
|
||||
*/
|
||||
export function SlideEditorContainer() {
|
||||
const [state, setState] = useState({
|
||||
...EDITOR_DEFAULT_POSITION,
|
||||
});
|
||||
|
||||
const slideEditorManagerService = useDependency(ISlideEditorManagerService);
|
||||
const editorService = useDependency(IEditorService);
|
||||
const contextService = useDependency(IContextService);
|
||||
|
||||
const disableAutoFocus = useObservable(
|
||||
() => contextService.subscribeContextValue$(DISABLE_AUTO_FOCUS_KEY),
|
||||
false,
|
||||
undefined,
|
||||
[contextService, DISABLE_AUTO_FOCUS_KEY]
|
||||
);
|
||||
|
||||
const snapshot: IDocumentData = {
|
||||
id: SLIDE_EDITOR_ID,
|
||||
body: {
|
||||
dataStream: `${DEFAULT_EMPTY_DOCUMENT_VALUE}`,
|
||||
textRuns: [],
|
||||
paragraphs: [
|
||||
{
|
||||
startIndex: 0,
|
||||
paragraphId: createParagraphId(new Set()),
|
||||
},
|
||||
],
|
||||
},
|
||||
documentStyle: {
|
||||
documentFlavor: DocumentFlavor.UNSPECIFIED,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = slideEditorManagerService.state$.subscribe((param) => {
|
||||
if (param == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
startX = HIDDEN_EDITOR_POSITION,
|
||||
startY = HIDDEN_EDITOR_POSITION,
|
||||
endX = 0,
|
||||
endY = 0,
|
||||
show = false,
|
||||
} = param;
|
||||
|
||||
if (!show) {
|
||||
setState({
|
||||
...EDITOR_DEFAULT_POSITION,
|
||||
});
|
||||
} else {
|
||||
setState({
|
||||
width: endX - startX - FIX_ONE_PIXEL_BLUR_OFFSET + 2,
|
||||
height: endY - startY - FIX_ONE_PIXEL_BLUR_OFFSET + 2,
|
||||
left: startX + FIX_ONE_PIXEL_BLUR_OFFSET,
|
||||
top: startY + FIX_ONE_PIXEL_BLUR_OFFSET,
|
||||
});
|
||||
|
||||
const editor = editorService.getEditor(SLIDE_EDITOR_ID);
|
||||
|
||||
if (editor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { left, top, width, height } = editor.getBoundingClientRect();
|
||||
|
||||
slideEditorManagerService.setRect({ left, top, width, height });
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [editorService, slideEditorManagerService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disableAutoFocus) {
|
||||
slideEditorManagerService.setFocus(true);
|
||||
}
|
||||
}, [disableAutoFocus, state]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('univer-absolute univer-z-10 univer-box-border univer-flex', borderClassName)}
|
||||
style={{
|
||||
left: state.left,
|
||||
top: state.top,
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
}}
|
||||
>
|
||||
{/* <RichTextEditor
|
||||
editorId={SLIDE_EDITOR_ID}
|
||||
className={styles.editorInput}
|
||||
initialValue={snapshot}
|
||||
isSingle={false}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
129
packages/slides-ui/src/views/image-popup-menu/ImagePopupMenu.tsx
Normal file
129
packages/slides-ui/src/views/image-popup-menu/ImagePopupMenu.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { borderClassName, clsx, Dropdown } from '@univerjs/design';
|
||||
import { AutofillDoubleIcon, MoreDownIcon } from '@univerjs/icons';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface IImagePopupMenuItem {
|
||||
label: string;
|
||||
index: number;
|
||||
commandId: string;
|
||||
commandParams?: IDrawingSearch[];
|
||||
disable: boolean;
|
||||
}
|
||||
|
||||
export interface IImagePopupMenuExtraProps {
|
||||
menuItems: IImagePopupMenuItem[];
|
||||
}
|
||||
|
||||
export interface IImagePopupMenuProps {
|
||||
popup: {
|
||||
extraProps?: IImagePopupMenuExtraProps;
|
||||
};
|
||||
}
|
||||
|
||||
export function SlideImagePopupMenu(props: IImagePopupMenuProps) {
|
||||
const menuItems = props.popup?.extraProps?.menuItems;
|
||||
const commandService = useDependency(ICommandService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
if (!menuItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
setVisible(visible);
|
||||
};
|
||||
|
||||
const handleClick = (item: IImagePopupMenuItem) => {
|
||||
commandService.executeCommand(item.commandId, item.commandParams);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const showMore = visible || isHovered;
|
||||
|
||||
const availableMenu = menuItems.filter((item) => !item.disable);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Dropdown
|
||||
align="start"
|
||||
overlay={(
|
||||
<ul
|
||||
className={clsx(`
|
||||
univer-m-0 univer-box-border univer-grid univer-list-none univer-items-center univer-gap-1
|
||||
univer-rounded-lg univer-bg-gray-0 univer-p-1.5 univer-text-sm univer-shadow-lg
|
||||
`, borderClassName)}
|
||||
>
|
||||
{availableMenu.map((item) => (
|
||||
<li
|
||||
key={item.index}
|
||||
className={`
|
||||
univer-relative univer-box-border univer-flex univer-h-8 univer-cursor-pointer
|
||||
univer-items-center univer-rounded univer-text-sm univer-transition-colors
|
||||
hover:univer-bg-gray-100
|
||||
`}
|
||||
onClick={() => handleClick(item)}
|
||||
>
|
||||
<span className="univer-px-2 univer-py-1.5 univer-align-middle">
|
||||
{localeService.t(item.label)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
open={visible}
|
||||
onOpenChange={onVisibleChange}
|
||||
>
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-flex univer-items-center univer-gap-2 univer-rounded univer-p-1
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-800
|
||||
`, borderClassName, {
|
||||
'univer-bg-gray-100 dark:!univer-bg-gray-800': visible,
|
||||
'univer-bg-gray-0 dark:!univer-bg-gray-900': !visible,
|
||||
})}
|
||||
>
|
||||
<AutofillDoubleIcon
|
||||
className={`
|
||||
univer-fill-primary-600 univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
/>
|
||||
{showMore && <MoreDownIcon className="dark:!univer-text-gray-0" />}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const COMPONENT_SLIDE_IMAGE_POPUP_MENU = 'COMPONENT_SLIDE_IMAGE_POPUP_MENU';
|
||||
146
packages/slides-ui/src/views/panels/ArrangePanel.tsx
Normal file
146
packages/slides-ui/src/views/panels/ArrangePanel.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 { Image, Rect, RichText } from '@univerjs/engine-render';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { Button } from '@univerjs/design';
|
||||
import { BottomIcon, MoveDownIcon, MoveUpIcon, TopmostIcon } from '@univerjs/icons';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { UpdateSlideElementOperation } from '../../commands/operations/update-element.operation';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
enum ArrangeTypeEnum {
|
||||
forward,
|
||||
backward,
|
||||
front,
|
||||
back,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
pageId: string;
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
export default function ArrangePanel(props: IProps) {
|
||||
const { pageId, unitId } = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const canvasView = useDependency(CanvasView);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const page = canvasView.getRenderUnitByPageId(pageId, unitId);
|
||||
const scene = page?.scene;
|
||||
if (!scene) return null;
|
||||
|
||||
const transformer = scene.getTransformer();
|
||||
if (!transformer) return null;
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
const object = selectedObjects.values().next().value as Nullable<Rect | RichText | Image>;
|
||||
if (!object) return null;
|
||||
|
||||
const onArrangeBtnClick = (arrangeType: ArrangeTypeEnum) => {
|
||||
const allObjects = scene.getAllObjects();
|
||||
|
||||
const [minZIndex, maxZIndex] = allObjects.reduce(([min, max], obj) => {
|
||||
const zIndex = obj.zIndex;
|
||||
const minZIndex = zIndex < min ? zIndex : min;
|
||||
const maxZIndex = zIndex > max ? zIndex : max;
|
||||
|
||||
return [minZIndex, maxZIndex];
|
||||
}, [0, 0]);
|
||||
|
||||
let zIndex = object.zIndex;
|
||||
if (arrangeType === ArrangeTypeEnum.back) {
|
||||
zIndex = minZIndex - 1;
|
||||
} else if (arrangeType === ArrangeTypeEnum.front) {
|
||||
zIndex = maxZIndex + 1;
|
||||
} else if (arrangeType === ArrangeTypeEnum.forward) {
|
||||
zIndex = object.zIndex + 1;
|
||||
} else if (arrangeType === ArrangeTypeEnum.backward) {
|
||||
zIndex = object.zIndex - 1;
|
||||
}
|
||||
|
||||
object.setProps({
|
||||
zIndex,
|
||||
});
|
||||
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
unitId,
|
||||
oKey: object?.oKey,
|
||||
props: {
|
||||
zIndex,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="univer-relative univer-w-full">
|
||||
<div className="univer-relative univer-mt-2.5 univer-flex univer-h-full">
|
||||
<div
|
||||
className={`
|
||||
univer-w-full univer-text-left univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('slides-ui.image-panel.arrange.title')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-relative univer-mt-2.5 univer-flex univer-h-full">
|
||||
<div className="univer-w-1/2">
|
||||
<Button onClick={() => { onArrangeBtnClick(ArrangeTypeEnum.forward); }}>
|
||||
<span className="univer-flex univer-items-center univer-gap-1">
|
||||
<MoveUpIcon />
|
||||
{localeService.t<LocaleKey>('slides-ui.image-panel.arrange.forward')}
|
||||
</span>
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
<div className="univer-w-1/2">
|
||||
<Button onClick={() => { onArrangeBtnClick(ArrangeTypeEnum.backward); }}>
|
||||
<span className="univer-flex univer-items-center univer-gap-1">
|
||||
<MoveDownIcon />
|
||||
{localeService.t<LocaleKey>('slides-ui.image-panel.arrange.backward')}
|
||||
</span>
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-relative univer-mt-2.5 univer-flex univer-h-full">
|
||||
<div className="univer-w-1/2">
|
||||
<Button onClick={() => { onArrangeBtnClick(ArrangeTypeEnum.front); }}>
|
||||
<span className="univer-flex univer-items-center univer-gap-1">
|
||||
<TopmostIcon />
|
||||
{localeService.t<LocaleKey>('slides-ui.image-panel.arrange.front')}
|
||||
</span>
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
<div className="univer-w-1/2">
|
||||
<Button onClick={() => { onArrangeBtnClick(ArrangeTypeEnum.back); }}>
|
||||
<span className="univer-flex univer-items-center univer-gap-1">
|
||||
<BottomIcon />
|
||||
{localeService.t<LocaleKey>('slides-ui.image-panel.arrange.back')}
|
||||
</span>
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
packages/slides-ui/src/views/panels/FillPanel.tsx
Normal file
113
packages/slides-ui/src/views/panels/FillPanel.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import type { Rect } from '@univerjs/engine-render';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { borderTopClassName, clsx, ColorPicker, Dropdown } from '@univerjs/design';
|
||||
import { MoreDownIcon, PaintBucketDoubleIcon } from '@univerjs/icons';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
import { UpdateSlideElementOperation } from '../../commands/operations/update-element.operation';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
interface IProps {
|
||||
pageId: string;
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export default function ArrangePanel(props: IProps) {
|
||||
const { pageId, unitId } = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const canvasView = useDependency(CanvasView);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const page = canvasView.getRenderUnitByPageId(pageId, unitId);
|
||||
const scene = page?.scene;
|
||||
const transformer = scene?.getTransformer();
|
||||
const object = transformer?.getSelectedObjectMap().values().next().value as Nullable<Rect>;
|
||||
const [color, setColor] = useState<string>(object?.fill?.toString() ?? '');
|
||||
|
||||
if (!scene || !transformer || !object) return null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param color
|
||||
*/
|
||||
function handleChangeColor(color: string) {
|
||||
object?.setProps({
|
||||
fill: color,
|
||||
});
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
unitId,
|
||||
oKey: object?.oKey,
|
||||
props: {
|
||||
shape: {
|
||||
shapeProperties: {
|
||||
shapeBackgroundFill: {
|
||||
rgb: color,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setColor(color);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('univer-relative univer-bottom-0 univer-mt-5 univer-w-full', borderTopClassName)}
|
||||
>
|
||||
<div className="univer-relative univer-w-full">
|
||||
<div className="univer-relative univer-mt-2.5 univer-flex univer-h-full">
|
||||
<div
|
||||
className={`
|
||||
univer-w-full univer-text-left univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('slides-ui.panel.fill.title')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-relative univer-mt-2.5 univer-flex univer-h-full">
|
||||
<div className="univer-w-1/2">
|
||||
<Dropdown
|
||||
overlay={(
|
||||
<div className="univer-rounded-lg univer-p-4">
|
||||
<ColorPicker
|
||||
value="#fff"
|
||||
onChange={handleChangeColor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<a className="univer-flex univer-cursor-pointer univer-items-center univer-gap-1">
|
||||
<PaintBucketDoubleIcon className="univer-fill-primary-600" />
|
||||
<MoreDownIcon />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
236
packages/slides-ui/src/views/panels/TransformPanel.tsx
Normal file
236
packages/slides-ui/src/views/panels/TransformPanel.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import type { BaseObject, IChangeObserverConfig, Image, Rect, RichText } from '@univerjs/engine-render';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, LocaleService } from '@univerjs/core';
|
||||
import { borderTopClassName, clsx, InputNumber } from '@univerjs/design';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { UpdateSlideElementOperation } from '../../commands/operations/update-element.operation';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
|
||||
interface IProps {
|
||||
pageId: string;
|
||||
unitId: string;
|
||||
}
|
||||
|
||||
export default function TransformPanel(props: IProps) {
|
||||
const { pageId, unitId } = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const canvasView = useDependency(CanvasView);
|
||||
const commandService = useDependency(ICommandService);
|
||||
|
||||
const page = canvasView.getRenderUnitByPageId(pageId, unitId);
|
||||
const scene = page?.scene;
|
||||
const transformer = scene?.getTransformer();
|
||||
const object = transformer?.getSelectedObjectMap().values().next().value as Nullable<Rect | Image | RichText>;
|
||||
|
||||
const {
|
||||
width: originWidth = 0,
|
||||
height: originHeight = 0,
|
||||
left: originX = 0,
|
||||
top: originY = 0,
|
||||
angle: originRotation = 0,
|
||||
} = object ?? {};
|
||||
|
||||
const [width, setWidth] = useState<number>(originWidth);
|
||||
const [height, setHeight] = useState(originHeight);
|
||||
const [xPosition, setXPosition] = useState(originX);
|
||||
const [yPosition, setYPosition] = useState(originY);
|
||||
const [rotation, setRotation] = useState(originRotation);
|
||||
// const [lockRatio, setLockRatio] = useState(transformer.keepRatio);
|
||||
|
||||
// const handleLockRatioChange = (val: string | number | boolean) => {
|
||||
// setLockRatio(val as boolean);
|
||||
// transformer.keepRatio = val as boolean;
|
||||
// };
|
||||
|
||||
const changeObs = (state: IChangeObserverConfig) => {
|
||||
const { objects } = state;
|
||||
|
||||
const object = objects.values().next().value as BaseObject;
|
||||
|
||||
const {
|
||||
width: originWidth = 0,
|
||||
height: originHeight = 0,
|
||||
left: originX = 0,
|
||||
top: originY = 0,
|
||||
angle: originRotation = 0,
|
||||
} = object;
|
||||
|
||||
setWidth(originWidth);
|
||||
setHeight(originHeight);
|
||||
setXPosition(originX);
|
||||
setYPosition(originY);
|
||||
setRotation(originRotation);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!transformer) return;
|
||||
const changeStartSub = transformer.changeStart$.subscribe((state) => {
|
||||
changeObs(state);
|
||||
});
|
||||
|
||||
const changingSub = transformer.changing$.subscribe((state) => {
|
||||
changeObs(state);
|
||||
});
|
||||
|
||||
return () => {
|
||||
changingSub.unsubscribe();
|
||||
changeStartSub.unsubscribe();
|
||||
// focusSub.unsubscribe();
|
||||
};
|
||||
}, [transformer]);
|
||||
|
||||
if (!scene || !transformer || !object) return null;
|
||||
|
||||
function handleWidthChange(val: number | null) {
|
||||
if (!val || !object) return;
|
||||
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
pageId,
|
||||
oKey: object.oKey,
|
||||
props: {
|
||||
width: val,
|
||||
},
|
||||
});
|
||||
object?.resize(val, object.height);
|
||||
setWidth(val);
|
||||
transformer?.refreshControls();
|
||||
}
|
||||
|
||||
function handleHeightChange(val: number | null) {
|
||||
if (!val || !object) return;
|
||||
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
pageId,
|
||||
oKey: object.oKey,
|
||||
props: {
|
||||
height: val,
|
||||
},
|
||||
});
|
||||
object?.resize(object.width, val);
|
||||
setHeight(val);
|
||||
transformer?.refreshControls();
|
||||
}
|
||||
|
||||
function handleXChange(val: number | null) {
|
||||
if (!val || !object) return;
|
||||
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
pageId,
|
||||
oKey: object.oKey,
|
||||
props: {
|
||||
left: val,
|
||||
},
|
||||
});
|
||||
object?.translate(val, object.top);
|
||||
setXPosition(val);
|
||||
transformer?.refreshControls();
|
||||
}
|
||||
|
||||
function handleYChange(val: number | null) {
|
||||
if (!val || !object) return;
|
||||
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
pageId,
|
||||
oKey: object.oKey,
|
||||
props: {
|
||||
right: val,
|
||||
},
|
||||
});
|
||||
object?.translate(object.left, val);
|
||||
setYPosition(val);
|
||||
transformer?.refreshControls();
|
||||
}
|
||||
|
||||
function handleChangeRotation(val: number | null) {
|
||||
if (!val || !object) return;
|
||||
|
||||
commandService.executeCommand(UpdateSlideElementOperation.id, {
|
||||
pageId,
|
||||
oKey: object.oKey,
|
||||
props: {
|
||||
angle: val,
|
||||
},
|
||||
});
|
||||
object?.transformByState({
|
||||
angle: val,
|
||||
});
|
||||
setRotation(val);
|
||||
transformer?.refreshControls();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('univer-grid univer-gap-2 univer-py-2 univer-text-gray-400', borderTopClassName)}
|
||||
>
|
||||
<header
|
||||
className={`
|
||||
univer-text-gray-600
|
||||
dark:!univer-text-gray-200
|
||||
`}
|
||||
>
|
||||
<div>{localeService.t<LocaleKey>('slides-ui.image-panel.transform.title')}</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-grid univer-grid-cols-3 univer-gap-2
|
||||
[&>div]:univer-grid [&>div]:univer-gap-2
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('slides-ui.image-panel.transform.width')}</span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={width}
|
||||
onChange={(val) => { handleWidthChange(val); }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('slides-ui.image-panel.transform.height')}</span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={height}
|
||||
onChange={(val) => { handleHeightChange(val); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-relative univer-mt-2.5 univer-flex univer-h-full">
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('slides-ui.image-panel.transform.x')}</span>
|
||||
<InputNumber min={0} precision={1} value={xPosition} onChange={(val) => { handleXChange(val); }} />
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('slides-ui.image-panel.transform.y')}</span>
|
||||
<InputNumber min={0} precision={1} value={yPosition} onChange={(val) => { handleYChange(val); }} />
|
||||
</div>
|
||||
<div>
|
||||
<span>{localeService.t<LocaleKey>('slides-ui.image-panel.transform.rotate')}</span>
|
||||
<InputNumber
|
||||
precision={1}
|
||||
value={rotation}
|
||||
onChange={handleChangeRotation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 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 { ISlideData, SlideDataModel } from '@univerjs/slides';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { BasicShapes, PageElementType, PageType, UniverSlidesPlugin } from '@univerjs/slides';
|
||||
import { RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { UpdateSlideElementOperation } from '../../../commands/operations/update-element.operation';
|
||||
import { CanvasView } from '../../../controllers/canvas-view';
|
||||
import locale from '../../../locale/en-US';
|
||||
import ArrangePanel from '../ArrangePanel';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'arrange-panel-unit';
|
||||
const PAGE_ID = 'page-1';
|
||||
const SELECTED_ID = 'selected-shape';
|
||||
|
||||
class TestSlideObject {
|
||||
constructor(
|
||||
readonly oKey: string,
|
||||
public zIndex: number
|
||||
) {}
|
||||
|
||||
setProps(props: { zIndex?: number }) {
|
||||
if (typeof props.zIndex === 'number') {
|
||||
this.zIndex = props.zIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestTransformer {
|
||||
constructor(private readonly _object: TestSlideObject) {}
|
||||
|
||||
getSelectedObjectMap() {
|
||||
return new Map([[this._object.oKey, this._object]]);
|
||||
}
|
||||
}
|
||||
|
||||
class TestScene {
|
||||
private readonly _transformer: TestTransformer;
|
||||
|
||||
constructor(private readonly _objects: TestSlideObject[], selectedObject: TestSlideObject) {
|
||||
this._transformer = new TestTransformer(selectedObject);
|
||||
}
|
||||
|
||||
getTransformer() {
|
||||
return this._transformer;
|
||||
}
|
||||
|
||||
getAllObjects() {
|
||||
return this._objects;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCanvasView {
|
||||
static scene: TestScene | null = null;
|
||||
|
||||
static reset() {
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
getRenderUnitByPageId() {
|
||||
return TestCanvasView.scene
|
||||
? { scene: TestCanvasView.scene }
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSlideSnapshot(): Partial<ISlideData> {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
title: 'Arrange panel deck',
|
||||
pageSize: { width: 960, height: 540 },
|
||||
body: {
|
||||
pageOrder: [PAGE_ID],
|
||||
pages: {
|
||||
[PAGE_ID]: {
|
||||
id: PAGE_ID,
|
||||
pageType: PageType.SLIDE,
|
||||
zIndex: 1,
|
||||
title: 'Slide',
|
||||
description: '',
|
||||
pageBackgroundFill: { rgb: '#ffffff' },
|
||||
pageElements: {
|
||||
[SELECTED_ID]: {
|
||||
id: SELECTED_ID,
|
||||
zIndex: 2,
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 120,
|
||||
height: 80,
|
||||
title: 'Selected shape',
|
||||
description: '',
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Rect,
|
||||
text: '',
|
||||
shapeProperties: {
|
||||
shapeBackgroundFill: { rgb: '#4472c4' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPanelTestBed() {
|
||||
const univer = new Univer();
|
||||
univer.registerPlugin(UniverSlidesPlugin);
|
||||
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([CanvasView, { useClass: TestCanvasView as never }]);
|
||||
|
||||
const slide = univer.createUnit<ISlideData, SlideDataModel>(UniverInstanceType.UNIVER_SLIDE, createSlideSnapshot());
|
||||
injector.get(IUniverInstanceService).focusUnit(UNIT_ID);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(UpdateSlideElementOperation);
|
||||
injector.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: locale,
|
||||
});
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
return { univer, injector, slide };
|
||||
}
|
||||
|
||||
function createRenderScene(selectedZIndex: number, otherZIndexes: number[]) {
|
||||
const selectedObject = new TestSlideObject(SELECTED_ID, selectedZIndex);
|
||||
const objects = [
|
||||
selectedObject,
|
||||
...otherZIndexes.map((zIndex, index) => new TestSlideObject(`object-${index}`, zIndex)),
|
||||
];
|
||||
TestCanvasView.scene = new TestScene(objects, selectedObject);
|
||||
|
||||
return selectedObject;
|
||||
}
|
||||
|
||||
function renderPanel(root: Root, testBed: ReturnType<typeof createPanelTestBed>) {
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<ArrangePanel pageId={PAGE_ID} unitId={UNIT_ID} />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function clickButton(container: HTMLElement, text: string) {
|
||||
const button = Array.from(container.querySelectorAll('button'))
|
||||
.find((button) => button.textContent?.includes(text));
|
||||
if (!button) {
|
||||
throw new Error(`Button not found: ${text}`);
|
||||
}
|
||||
|
||||
act(() => {
|
||||
button.click();
|
||||
});
|
||||
}
|
||||
|
||||
describe('ArrangePanel', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createPanelTestBed> | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
TestCanvasView.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('moves the selected slide element above every rendered object', () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
const selectedObject = createRenderScene(2, [1, 4]);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
clickButton(container, 'Bring to Front');
|
||||
|
||||
expect(selectedObject.zIndex).toBe(5);
|
||||
expect(currentTestBed.slide.getElement(PAGE_ID, SELECTED_ID)).toMatchObject({
|
||||
zIndex: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('moves the selected slide element one layer forward', () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
const selectedObject = createRenderScene(2, [1, 4]);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
clickButton(container, 'Bring Forward');
|
||||
|
||||
expect(selectedObject.zIndex).toBe(3);
|
||||
expect(currentTestBed.slide.getElement(PAGE_ID, SELECTED_ID)).toMatchObject({
|
||||
zIndex: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('moves the selected slide element behind every rendered object', () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
const selectedObject = createRenderScene(2, [3, 5]);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderPanel(root, currentTestBed);
|
||||
|
||||
clickButton(container, 'Send to Back');
|
||||
|
||||
expect(selectedObject.zIndex).toBe(-1);
|
||||
expect(currentTestBed.slide.getElement(PAGE_ID, SELECTED_ID)).toMatchObject({
|
||||
zIndex: -1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 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 { ISlideData, SlideDataModel } from '@univerjs/slides';
|
||||
import type { Root } from 'react-dom/client';
|
||||
import {
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { BasicShapes, PageElementType, PageType, UniverSlidesPlugin } from '@univerjs/slides';
|
||||
import { RediContext } from '@univerjs/ui';
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Subject } from 'rxjs';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { UpdateSlideElementOperation } from '../../../commands/operations/update-element.operation';
|
||||
import { CanvasView } from '../../../controllers/canvas-view';
|
||||
import locale from '../../../locale/en-US';
|
||||
import TransformPanel from '../TransformPanel';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const UNIT_ID = 'transform-panel-unit';
|
||||
const PAGE_ID = 'page-1';
|
||||
const SELECTED_ID = 'selected-shape';
|
||||
|
||||
class TestSlideObject {
|
||||
public refreshCount = 0;
|
||||
|
||||
constructor(
|
||||
readonly oKey: string,
|
||||
public width: number,
|
||||
public height: number,
|
||||
public left: number,
|
||||
public top: number,
|
||||
public angle: number
|
||||
) {}
|
||||
|
||||
resize(width: number, height: number) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
translate(left: number, top: number) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
}
|
||||
|
||||
transformByState(state: { angle?: number }) {
|
||||
if (typeof state.angle === 'number') {
|
||||
this.angle = state.angle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestTransformer {
|
||||
readonly changeStart$ = new Subject<{ objects: Map<string, TestSlideObject> }>();
|
||||
readonly changing$ = new Subject<{ objects: Map<string, TestSlideObject> }>();
|
||||
|
||||
constructor(private readonly _object: TestSlideObject) {}
|
||||
|
||||
getSelectedObjectMap() {
|
||||
return new Map([[this._object.oKey, this._object]]);
|
||||
}
|
||||
|
||||
refreshControls() {
|
||||
this._object.refreshCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class TestScene {
|
||||
private readonly _transformer: TestTransformer;
|
||||
|
||||
constructor(selectedObject: TestSlideObject) {
|
||||
this._transformer = new TestTransformer(selectedObject);
|
||||
}
|
||||
|
||||
getTransformer() {
|
||||
return this._transformer;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCanvasView {
|
||||
static scene: TestScene | null = null;
|
||||
|
||||
static reset() {
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
getRenderUnitByPageId() {
|
||||
return TestCanvasView.scene
|
||||
? { scene: TestCanvasView.scene }
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSlideSnapshot(): Partial<ISlideData> {
|
||||
return {
|
||||
id: UNIT_ID,
|
||||
title: 'Transform panel deck',
|
||||
pageSize: { width: 960, height: 540 },
|
||||
body: {
|
||||
pageOrder: [PAGE_ID],
|
||||
pages: {
|
||||
[PAGE_ID]: {
|
||||
id: PAGE_ID,
|
||||
pageType: PageType.SLIDE,
|
||||
zIndex: 1,
|
||||
title: 'Slide',
|
||||
description: '',
|
||||
pageBackgroundFill: { rgb: '#ffffff' },
|
||||
pageElements: {
|
||||
[SELECTED_ID]: {
|
||||
id: SELECTED_ID,
|
||||
zIndex: 2,
|
||||
left: 100,
|
||||
top: 120,
|
||||
width: 160,
|
||||
height: 90,
|
||||
angle: 15,
|
||||
title: 'Selected shape',
|
||||
description: '',
|
||||
type: PageElementType.SHAPE,
|
||||
shape: {
|
||||
shapeType: BasicShapes.Rect,
|
||||
text: '',
|
||||
shapeProperties: {
|
||||
shapeBackgroundFill: { rgb: '#4472c4' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createPanelTestBed() {
|
||||
const univer = new Univer();
|
||||
univer.registerPlugin(UniverSlidesPlugin);
|
||||
|
||||
const injector = univer.__getInjector();
|
||||
injector.add([CanvasView, { useClass: TestCanvasView as never }]);
|
||||
|
||||
const slide = univer.createUnit<ISlideData, SlideDataModel>(UniverInstanceType.UNIVER_SLIDE, createSlideSnapshot());
|
||||
injector.get(IUniverInstanceService).focusUnit(UNIT_ID);
|
||||
|
||||
const commandService = injector.get(ICommandService);
|
||||
commandService.registerCommand(UpdateSlideElementOperation);
|
||||
injector.get(LocaleService).load({
|
||||
[LocaleType.EN_US]: locale,
|
||||
});
|
||||
injector.get(LocaleService).setLocale(LocaleType.EN_US);
|
||||
|
||||
return { univer, injector, slide };
|
||||
}
|
||||
|
||||
function createRenderScene() {
|
||||
const selectedObject = new TestSlideObject(SELECTED_ID, 160, 90, 100, 120, 15);
|
||||
TestCanvasView.scene = new TestScene(selectedObject);
|
||||
|
||||
return selectedObject;
|
||||
}
|
||||
|
||||
function renderTransformPanel(root: Root, testBed: ReturnType<typeof createPanelTestBed>) {
|
||||
act(() => {
|
||||
root.render(
|
||||
<RediContext.Provider value={{ injector: testBed.injector }}>
|
||||
<TransformPanel pageId={PAGE_ID} unitId={UNIT_ID} />
|
||||
</RediContext.Provider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function setInputValue(input: HTMLInputElement, value: number) {
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||||
|
||||
act(() => {
|
||||
valueSetter?.call(input, String(value));
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
function getInputs(container: HTMLElement) {
|
||||
const inputs = Array.from(container.querySelectorAll('input'));
|
||||
if (inputs.length !== 5) {
|
||||
throw new Error(`Expected five transform inputs, received ${inputs.length}`);
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
describe('TransformPanel', () => {
|
||||
let root: Root | undefined;
|
||||
let container: HTMLDivElement | undefined;
|
||||
let currentTestBed: ReturnType<typeof createPanelTestBed> | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
TestCanvasView.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
container?.remove();
|
||||
currentTestBed?.univer.dispose();
|
||||
root = undefined;
|
||||
container = undefined;
|
||||
currentTestBed = undefined;
|
||||
});
|
||||
|
||||
it('updates the selected slide element when dimension controls change', () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
const selectedObject = createRenderScene();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderTransformPanel(root, currentTestBed);
|
||||
|
||||
const [widthInput, heightInput] = getInputs(container);
|
||||
setInputValue(widthInput, 240);
|
||||
setInputValue(heightInput, 135);
|
||||
|
||||
expect(selectedObject).toMatchObject({
|
||||
width: 240,
|
||||
height: 135,
|
||||
refreshCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the selected slide element when position and rotation controls change', () => {
|
||||
currentTestBed = createPanelTestBed();
|
||||
const selectedObject = createRenderScene();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderTransformPanel(root, currentTestBed);
|
||||
|
||||
const [, , xInput, yInput, rotationInput] = getInputs(container);
|
||||
setInputValue(xInput, 210);
|
||||
setInputValue(yInput, 180);
|
||||
setInputValue(rotationInput, 45);
|
||||
|
||||
expect(selectedObject).toMatchObject({
|
||||
left: 210,
|
||||
top: 180,
|
||||
angle: 45,
|
||||
refreshCount: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
63
packages/slides-ui/src/views/sidebar/Sidebar.tsx
Normal file
63
packages/slides-ui/src/views/sidebar/Sidebar.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import type { BaseObject } from '@univerjs/engine-render';
|
||||
import type { SlideDataModel } from '@univerjs/slides';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { ObjectType } from '@univerjs/engine-render';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { CanvasView } from '../../controllers/canvas-view';
|
||||
import ArrangePanel from '../panels/ArrangePanel';
|
||||
import FillPanel from '../panels/FillPanel';
|
||||
import TransformPanel from '../panels/TransformPanel';
|
||||
|
||||
export const COMPONENT_SLIDE_SIDEBAR = 'COMPONENT_SLIDE_SIDEBAR';
|
||||
|
||||
export default function RectSidebar() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const canvasView = useDependency(CanvasView);
|
||||
|
||||
const currentSlide = univerInstanceService.getCurrentUnitOfType<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE);
|
||||
const pageId = currentSlide?.getActivePage()?.id;
|
||||
|
||||
// see packages/sheets-ui/src/views/permission/permission-dialog/index.tsx@SheetPermissionDialog
|
||||
// see packages/sheets-conditional-formatting-ui/src/views/panel/rule-edit/index.tsx@getUnitId
|
||||
// const unitId = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SLIDE)!.getUnitId();
|
||||
const unitId = univerInstanceService.getFocusedUnit()?.getUnitId() || '';
|
||||
|
||||
if (!pageId || !unitId) return null;
|
||||
|
||||
const page = canvasView.getRenderUnitByPageId(pageId, unitId);
|
||||
const transformer = page.scene?.getTransformer();
|
||||
|
||||
if (!transformer) return null;
|
||||
|
||||
const selectedObjects = transformer.getSelectedObjectMap();
|
||||
|
||||
const object = selectedObjects.values().next().value as Nullable<BaseObject>;
|
||||
if (!object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="univer-p-2 univer-text-center univer-text-sm">
|
||||
<ArrangePanel pageId={pageId} unitId={unitId} />
|
||||
<TransformPanel pageId={pageId} unitId={unitId} />
|
||||
{object.objectType === ObjectType.RECT && <FillPanel pageId={pageId} unitId={unitId} />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
130
packages/slides-ui/src/views/slide-bar/SlideBar.tsx
Normal file
130
packages/slides-ui/src/views/slide-bar/SlideBar.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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 { SlideDataModel } from '@univerjs/slides';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import { ICommandService, IUniverInstanceService, LocaleService, UniverInstanceType } from '@univerjs/core';
|
||||
import { borderClassName, clsx, scrollbarClassName } from '@univerjs/design';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { createRef, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { scan } from 'rxjs';
|
||||
import { ActivateSlidePageOperation } from '../../commands/operations/activate.operation';
|
||||
import { AppendSlideOperation } from '../../commands/operations/append-slide.operation';
|
||||
import { SetSlidePageThumbOperation } from '../../commands/operations/set-thumb.operation';
|
||||
|
||||
/**
|
||||
* This components works as the root component of the left Sidebar of Slide.
|
||||
*/
|
||||
|
||||
export function SlideSideBar() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
const slideBarRef = useRef<HTMLDivElement>(null);
|
||||
const currentSlide = useObservable(
|
||||
() => univerInstanceService.getCurrentTypeOfUnit$<SlideDataModel>(UniverInstanceType.UNIVER_SLIDE),
|
||||
undefined,
|
||||
false,
|
||||
[univerInstanceService]
|
||||
);
|
||||
const pages = currentSlide?.getPages();
|
||||
const pageOrder = currentSlide?.getPageOrder();
|
||||
const slideList = useMemo(() => pages && pageOrder ? pageOrder.map((id) => pages[id]) : [], [pageOrder, pages]);
|
||||
|
||||
const initialActivePageId = currentSlide?.getActivePage()?.id ?? null;
|
||||
const activatePageId = useObservable(
|
||||
currentSlide
|
||||
? () => currentSlide.activePage$.pipe(
|
||||
scan((previousId, page) => page?.id ?? previousId, initialActivePageId)
|
||||
)
|
||||
: null,
|
||||
initialActivePageId,
|
||||
false,
|
||||
[currentSlide, initialActivePageId]
|
||||
);
|
||||
|
||||
const divRefs = useMemo(() => slideList.map(() => createRef<HTMLDivElement>()), [slideList]);
|
||||
|
||||
useEffect(() => {
|
||||
divRefs.forEach((ref, index) => {
|
||||
if (ref.current) {
|
||||
const slide = slideList[index];
|
||||
renderManagerService.getRenderUnitById(slide.id)?.engine.mount(ref.current);
|
||||
}
|
||||
});
|
||||
|
||||
if (divRefs.length > 0) {
|
||||
commandService.syncExecuteCommand(SetSlidePageThumbOperation.id, { unitId: currentSlide?.getUnitId() });
|
||||
}
|
||||
}, [divRefs, slideList, renderManagerService, commandService, currentSlide]); // Depends on changes in divRefs array
|
||||
|
||||
const activatePage = useCallback((page: string) => {
|
||||
commandService.syncExecuteCommand(ActivateSlidePageOperation.id, { id: page, unitId: currentSlide?.getUnitId() });
|
||||
}, [commandService, currentSlide]);
|
||||
|
||||
const handleAppendSlide = useCallback(() => {
|
||||
commandService.syncExecuteCommand(AppendSlideOperation.id, { unitId: currentSlide?.getUnitId() });
|
||||
}, [commandService, currentSlide]);
|
||||
|
||||
if (!pages || !pageOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
ref={slideBarRef}
|
||||
className={clsx(`
|
||||
univer-flex univer-h-full univer-w-64 univer-flex-col univer-overflow-y-auto univer-overflow-x-hidden
|
||||
`, scrollbarClassName)}
|
||||
>
|
||||
<div className="univer-px-4">
|
||||
<header className="univer-flex univer-justify-center univer-pt-4">
|
||||
<a
|
||||
className={clsx(`
|
||||
univer-box-border univer-block univer-h-8 univer-w-full univer-cursor-pointer
|
||||
univer-rounded-md univer-bg-gray-0 univer-text-center univer-text-sm univer-transition-colors
|
||||
`, borderClassName)}
|
||||
onClick={handleAppendSlide}
|
||||
>
|
||||
{localeService.t<LocaleKey>('slides-ui.append')}
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{slideList.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={clsx('univer-my-4 univer-flex univer-gap-2', {
|
||||
'[&>div]:univer-border-primary-600 [&>span]:univer-text-primary-600': item.id === activatePageId,
|
||||
})}
|
||||
onClick={() => activatePage(item.id)}
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
<div
|
||||
ref={divRefs[index]}
|
||||
className={clsx(`
|
||||
univer-relative univer-box-border univer-h-32 univer-w-52 univer-bg-gray-0
|
||||
hover:univer-border-primary-600
|
||||
`, borderClassName)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
27
packages/slides-ui/tailwind.config.ts
Normal file
27
packages/slides-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/slides-ui/tsconfig.json
Normal file
8
packages/slides-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/slides-ui/tsconfig.node.json
Normal file
18
packages/slides-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/slides-ui/vitest.config.ts
Normal file
3
packages/slides-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