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

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

37
packages/slides/README.md Normal file
View File

@@ -0,0 +1,37 @@
# @univerjs/slides
[![npm version](https://img.shields.io/npm/v/@univerjs/slides?style=flat-square)](https://npmjs.com/package/@univerjs/slides)
[![license](https://img.shields.io/npm/l/@univerjs/slides?style=flat-square)](https://npmjs.com/package/@univerjs/slides)
[![downloads](https://img.shields.io/npm/dm/@univerjs/slides?style=flat-square)](https://npmjs.com/package/@univerjs/slides)
`@univerjs/slides` provides the core presentation data model and services for Univer Slides.
## Package Overview
| Package | UMD global | CSS | Locales | Facade entry |
| --- | --- | :---: | :---: | :---: |
| `@univerjs/slides` | `UniverSlides` | No | No | No |
## Installation
```sh
pnpm add @univerjs/slides
# or
npm install @univerjs/slides
```
Keep all `@univerjs/*` packages on the same version.
## Usage
```ts
import { UniverSlidesPlugin } from '@univerjs/slides';
univer.registerPlugin(UniverSlidesPlugin);
```
## Resources
- [Documentation](https://docs.univer.ai)
- [NPM package](https://npmjs.com/package/@univerjs/slides)
- [GitHub repository](https://github.com/dream-num/univer)

View File

@@ -0,0 +1,79 @@
{
"name": "@univerjs/slides",
"version": "1.0.0-rc.0",
"private": false,
"description": "Core presentation model and services 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",
"deck",
"plugin"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
},
"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"
},
"./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": {
"rxjs": ">=7.0.0"
},
"dependencies": {
"@univerjs/core": "workspace:*",
"@univerjs/engine-render": "workspace:*"
},
"devDependencies": {
"@univerjs-infra/shared": "workspace:*",
"rxjs": "^7.8.2",
"typescript": "^6.0.3",
"vitest": "^5.0.0"
}
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const DEFAULT_SLIDE = {
id: 'default_slide',
title: 'defaultSlide',
pageSize: {
width: 300,
height: 300,
},
};

View File

@@ -0,0 +1,17 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SLIDES_PLUGIN_NAME = 'slides';

View File

@@ -0,0 +1,219 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 {
BaselineOffset,
BooleanNumber,
ColumnSeparatorType,
DocumentFlavor,
DrawingTypeEnum,
ObjectRelativeFromH,
ObjectRelativeFromV,
PositionedObjectLayoutType,
PresetListType,
SectionType,
WrapTextType,
} from '@univerjs/core';
export const docsDemoData: IDocumentData = {
id: 'd',
drawings: {
shapeTest1: {
unitId: 'd',
subUnitId: 'd',
drawingType: DrawingTypeEnum.DRAWING_SHAPE,
drawingId: 'shapeTest1',
title: 'test shape',
description: 'test shape',
docTransform: {
size: {
width: 100,
height: 400,
},
positionH: {
relativeFrom: ObjectRelativeFromH.COLUMN,
posOffset: 100,
},
positionV: {
relativeFrom: ObjectRelativeFromV.PARAGRAPH,
posOffset: 160,
},
angle: 0,
// imageProperties: {
// contentUrl: 'https://cnbabylon.com/assets/img/agents.png',
// },
},
layoutType: PositionedObjectLayoutType.WRAP_SQUARE,
behindDoc: BooleanNumber.FALSE,
wrapText: WrapTextType.BOTH_SIDES,
},
},
body: {
dataStream: '在“第1题”工作表中完成以下操作\b上标日期列单元格数据验证,限制只能输入日期(介于1949年1月1日至2099年1月1日)\r细化埋点上报链路和指标方案梳理目前通过每日定时任务发送报告\r自2018年首届进博会举办以来,进博会已经成为全球新品的首发地、前沿技术的首选地、创新服务的首推地。中国这十年”对外开放成就展湖北展区主要以图文、图表、数据、视频、企业展品、实物模型、光电科技等体现湖北十年开放成就、重大开放平台及产业。湖北省共计17家企业、机构的展品将在这一展区展示\r国家主席习近平以视频方式出席在上海举行的第五届中国国际进口博览会开幕式并发表题为《共创开放繁荣的美好未来》的致辞中国将推动各国各方共享深化国际合作机遇,全面深入参与世界贸易组织改革谈判\r中国男排两名现役国手彭世坤和张秉龙分别效力的三得利太阳鸟和东京大熊本轮遭遇。双方经过激战,主场作战的卫冕冠军三得利技高一筹3-2逆转击败对手,力夺第3场胜利\r\n',
textRuns: [
{
st: 0,
ed: 15,
ts: {
fs: 12,
bg: {
rgb: 'rgb(200,0,90)',
},
cl: {
rgb: 'rgb(255,130,0)',
},
},
},
{
st: 17,
ed: 18,
ts: {
fs: 14,
bg: {
rgb: 'rgb(2,128,2)',
},
cl: {
rgb: 'rgb(0,1,55)',
},
va: BaselineOffset.SUPERSCRIPT,
},
},
{
st: 19,
ed: 60,
ts: {
fs: 14,
bg: {
rgb: 'rgb(90,128,255)',
},
cl: {
rgb: 'rgb(0,1,255)',
},
},
},
{
st: 62,
ed: 90,
},
{
st: 92,
ed: 233,
},
{
st: 235,
ed: 326,
},
{
st: 328,
ed: 405,
},
],
paragraphs: [
{
paragraphId: 'para_slides_demo_order_1',
startIndex: 60,
bullet: {
listId: 'orderList',
listType: PresetListType.ORDER_LIST,
nestingLevel: 0,
textStyle: {
fs: 20,
},
},
},
{
paragraphId: 'para_slides_demo_order_2',
startIndex: 91,
bullet: {
listId: 'orderList',
listType: PresetListType.ORDER_LIST,
nestingLevel: 1,
textStyle: {
fs: 20,
},
},
},
{
paragraphId: 'para_slides_demo_order_3',
startIndex: 234,
bullet: {
listId: 'orderList',
listType: PresetListType.ORDER_LIST,
nestingLevel: 1,
textStyle: {
fs: 20,
},
},
},
{
paragraphId: 'para_slides_demo_order_4',
startIndex: 327,
bullet: {
listId: 'orderList',
listType: PresetListType.ORDER_LIST,
nestingLevel: 1,
textStyle: {
fs: 20,
},
},
},
{
paragraphId: 'para_slides_demo_order_5',
startIndex: 406,
bullet: {
listId: 'orderList',
listType: PresetListType.ORDER_LIST,
nestingLevel: 2,
textStyle: {
fs: 20,
},
},
},
],
sectionBreaks: [
{
sectionId: 'section_slides_demo',
startIndex: 407,
columnProperties: [
{
width: 200,
paddingEnd: 20,
},
],
columnSeparatorType: ColumnSeparatorType.NONE,
sectionType: SectionType.SECTION_TYPE_UNSPECIFIED,
// textDirection: textDirectionDocument,
// contentDirection: textDirection!,
},
],
},
documentStyle: {
pageSize: {
width: 594.3,
height: 840.51,
},
documentFlavor: DocumentFlavor.UNSPECIFIED,
marginTop: 72,
marginBottom: 72,
marginRight: 90,
marginLeft: 90,
},
};

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SLIDES_PLUGIN_CONFIG_KEY = 'slides.config';
export const configSymbol = Symbol(SLIDES_PLUGIN_CONFIG_KEY);
export interface IUniverSlidesConfig {
}
export const defaultPluginConfig: IUniverSlidesConfig = {};

View File

@@ -0,0 +1,151 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 { ISlideData, ISlidePage } from '../../types/interfaces/i-slide-data';
import { afterEach, describe, expect, it } from 'vitest';
import { PageElementType, PageType } from '../../types/interfaces/i-slide-data';
import { SlideDataModel } from '../slide-data-model';
const slideSnapshotFactory = (): Partial<ISlideData> => ({
id: 'slide-unit',
title: 'Quarterly review',
pageSize: { width: 960, height: 540 },
body: {
pageOrder: ['page-1', 'page-2'],
pages: {
'page-1': {
id: 'page-1',
pageType: PageType.SLIDE,
zIndex: 1,
title: 'Overview',
description: 'Overview slide',
pageBackgroundFill: { rgb: '#ffffff' },
pageElements: {
'element-1': {
id: 'element-1',
zIndex: 1,
title: 'Intro',
description: 'Hero copy',
type: PageElementType.TEXT,
richText: { text: 'Hello Univer' },
},
},
},
'page-2': {
id: 'page-2',
pageType: PageType.SLIDE,
zIndex: 2,
title: 'Roadmap',
description: 'Roadmap slide',
pageBackgroundFill: { rgb: '#f5f5f5' },
pageElements: {},
},
},
},
});
describe('SlideDataModel', () => {
let slide: SlideDataModel;
afterEach(() => {
slide?.dispose();
});
it('should manage pages and active-page state', () => {
slide = new SlideDataModel(slideSnapshotFactory());
const activePages: string[] = [];
const names: string[] = [];
const activePageSubscription = slide.activePage$.subscribe((page: Nullable<ISlidePage>) => {
if (page) {
activePages.push(page.id);
}
});
const nameSubscription = slide.name$.subscribe((name: string) => names.push(name));
expect(slide.getUnitId()).toBe('slide-unit');
expect(slide.getSnapshot().title).toBe('Quarterly review');
expect(slide.getPages()).toHaveProperty('page-1');
expect(slide.getPageOrder()).toEqual(['page-1', 'page-2']);
expect(slide.getPage('page-1')?.title).toBe('Overview');
expect(slide.getElementsByPage('page-1')).toHaveProperty('element-1');
expect(slide.getElement('page-1', 'element-1')).toMatchObject({
id: 'element-1',
type: PageElementType.TEXT,
});
expect(slide.getPageSize()).toEqual({ width: 960, height: 540 });
expect(slide.getActivePage()?.id).toBe('page-1');
expect(slide.getRev()).toBe(0);
slide.incrementRev();
slide.setRev(5);
expect(slide.getRev()).toBe(0);
slide.setActivePage(slide.getPage('page-2')!);
expect(slide.getActivePage()?.id).toBe('page-2');
expect(activePages).toEqual(['page-2']);
const blankPage = slide.getBlankPage();
expect(blankPage).toMatchObject({
pageType: PageType.SLIDE,
zIndex: 10,
pageBackgroundFill: { rgb: 'rgb(255,255,255)' },
});
slide.appendPage(blankPage);
expect(slide.getPageOrder()).toEqual(['page-1', 'page-2', blankPage.id]);
expect(slide.getPage(blankPage.id)).toEqual(blankPage);
const updatedPage: ISlidePage = {
...blankPage,
title: 'Agenda',
description: 'Agenda slide',
};
slide.updatePage(blankPage.id, updatedPage);
expect(slide.getPage(blankPage.id)).toEqual(updatedPage);
slide.setName('Quarterly review - updated');
expect(slide.getSnapshot().title).toBe('Quarterly review - updated');
expect(slide.getUnitId()).toBe('slide-unit');
expect(names.at(-1)).toBe('Quarterly review - updated');
activePageSubscription.unsubscribe();
nameSubscription.unsubscribe();
});
it('should keep empty slide snapshots stable when page collections are absent', () => {
slide = new SlideDataModel({
id: 'empty-slide',
title: 'Empty deck',
});
const blankPage = slide.getBlankPage();
expect(slide.getActivePage()).toBeNull();
expect(slide.getPages()).toBeUndefined();
expect(slide.getPageOrder()).toBeUndefined();
expect(slide.getPage(blankPage.id)).toBeUndefined();
slide.appendPage(blankPage);
slide.updatePage(blankPage.id, blankPage);
expect(slide.getPages()).toBeUndefined();
expect(slide.getSnapshot()).toMatchObject({
id: 'empty-slide',
title: 'Empty deck',
});
});
});

View 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 { Nullable } from '@univerjs/core';
import type { Observable } from 'rxjs';
import type { ISlideData, ISlidePage } from '../types/interfaces/i-slide-data';
import { generateRandomId, UnitModel, UniverInstanceType } from '@univerjs/core';
import { BehaviorSubject } from 'rxjs';
import { DEFAULT_SLIDE } from '../basics/const/default-slide';
import { PageType } from '../types/interfaces/i-slide-data';
export class SlideDataModel extends UnitModel<ISlideData, UniverInstanceType.UNIVER_SLIDE> {
override type: UniverInstanceType.UNIVER_SLIDE = UniverInstanceType.UNIVER_SLIDE;
private readonly _activePage$ = new BehaviorSubject<Nullable<ISlidePage>>(null);
private get _activePage(): Nullable<ISlidePage> {
const activePage = this._activePage$.getValue();
if (!activePage) {
const activePageId = this.getPageOrder()?.[0];
if (!activePageId) {
return null;
}
return this.getPages()?.[activePageId];
}
return activePage;
}
readonly activePage$ = this._activePage$.asObservable();
private readonly _name$: BehaviorSubject<string>;
override name$: Observable<string>;
private _snapshot: ISlideData;
private _unitId: string;
constructor(snapshot: Partial<ISlideData>) {
super();
this._snapshot = { ...DEFAULT_SLIDE, ...snapshot };
this._unitId = this._snapshot.id ?? generateRandomId(6);
this._name$ = new BehaviorSubject(this._snapshot.title);
this.name$ = this._name$.asObservable();
}
override setName(name: string): void {
this._snapshot.title = name;
this._name$.next(name);
this._unitId = this._snapshot.id ?? generateRandomId(6);
}
override getRev(): number {
return 0; // TODO@jikkai: slide has not implement collaborative editing yet
}
override incrementRev(): void {
// do nothing
}
override setRev(_rev: number): void {
// do nothing
}
getSnapshot() {
return this._snapshot;
}
getUnitId(): string {
return this._unitId;
}
getPages() {
return this._snapshot.body?.pages;
}
getPageOrder() {
return this._snapshot.body?.pageOrder;
}
getPage(pageId: string) {
const pages = this.getPages();
return pages?.[pageId];
}
getElementsByPage(pageId: string) {
return this.getPage(pageId)?.pageElements;
}
getElement(pageId: string, elementId: string) {
return this.getElementsByPage(pageId)?.[elementId];
}
getPageSize() {
return this._snapshot.pageSize;
}
getBlankPage() {
const id = generateRandomId(6);
const page = {
id,
pageType: PageType.SLIDE,
zIndex: 10,
title: id,
description: '',
pageBackgroundFill: {
rgb: 'rgb(255,255,255)',
},
pageElements: {},
};
return page;
}
setActivePage(page: Nullable<ISlidePage>) {
this._activePage$.next(page);
}
getActivePage() {
return this._activePage;
}
updatePage(pageId: string, page: ISlidePage) {
if (!this._snapshot.body) return;
this._snapshot.body.pages[pageId] = page;
}
appendPage(page: ISlidePage) {
if (!this._snapshot.body) return;
this._snapshot.body.pages[page.id] = page;
const activePage = this._activePage;
const index = this._snapshot.body.pageOrder.indexOf(activePage?.id ?? '');
this._snapshot.body.pageOrder.splice(index + 1, 0, page.id);
}
}

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 { DEFAULT_SLIDE } from './basics/const/default-slide';
export type { IUniverSlidesConfig } from './config/config';
export { SlideDataModel } from './data-model/slide-data-model';
export { UniverSlidesPlugin } from './plugin';
export * from './types/enum/prst-geom-type';
export * from './types/interfaces/i-slide-data';
export * from './views/render';

View 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 { Dependency } from '@univerjs/core';
import { DependentOn, IConfigService, Inject, Injector, IUniverInstanceService, merge, Plugin, UniverInstanceType } from '@univerjs/core';
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
import pkg from '../package.json';
import { defaultPluginConfig, SLIDES_PLUGIN_CONFIG_KEY } from './config/config';
import { SlideDataModel } from './data-model/slide-data-model';
// import { DocSelectionManagerService } from '@univerjs/docs';
// import { CanvasView } from './views/render';
export interface IUniverSlidesConfig {}
@DependentOn(UniverRenderEnginePlugin)
export class UniverSlidesPlugin extends Plugin {
static override pluginName = 'UNIVER_SLIDES_PLUGIN';
static override packageName = pkg.name;
static override version = pkg.version;
static override type = UniverInstanceType.UNIVER_SLIDE;
// private _canvasView: CanvasView | null = null;
constructor(
private readonly _config: Partial<IUniverSlidesConfig> = defaultPluginConfig,
@Inject(Injector) override readonly _injector: Injector,
@IConfigService private readonly _configService: IConfigService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService
) {
super();
// Manage the plugin configuration.
const { ...rest } = merge(
{},
defaultPluginConfig,
this._config
);
this._configService.setConfig(SLIDES_PLUGIN_CONFIG_KEY, rest);
this._initializeDependencies(this._injector);
}
override onStarting(): void {
// Register the SlideDataModel constructor for the UNIVER_SLIDE type.
this._univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_SLIDE, SlideDataModel);
}
override onReady(): void {
}
getConfig() {
return this._config;
}
private _initializeDependencies(slideInjector: Injector) {
const dependencies: Dependency[] = [
// [CanvasView],
// [DocSelectionManagerService],
];
dependencies.forEach((d) => {
slideInjector.add(d);
});
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO@Jocs: should be moved out of core.
export enum BasicShapes {
Line = 'line',
LineInv = 'lineInv',
Triangle = 'triangle',
RtTriangle = 'rtTriangle',
Rect = 'rect',
Diamond = 'diamond',
Parallelogram = 'parallelogram',
Trapezoid = 'trapezoid',
NonIsocelesTrapezoid = 'nonIsocelesTrapezoid',
Pentagon = 'pentagon',
Hexagon = 'hexagon',
Heptagon = 'heptagon',
Octagon = 'octagon',
Decagon = 'decagon',
Dodecagon = 'dodecagon',
Star4 = 'star4',
Star5 = 'star5',
Star6 = 'star6',
Star7 = 'star7',
Star8 = 'star8',
Star10 = 'star10',
Star12 = 'star12',
Star16 = 'star16',
Star24 = 'star24',
Star32 = 'star32',
RoundRect = 'roundRect',
Round1Rect = 'round1Rect',
Round2SameRect = 'round2SameRect',
Round2DiagRect = 'round2DiagRect',
Ellipse = 'ellipse',
}
export enum ArrowsAndMarkersShapes {
RightArrow = 'rightArrow',
LeftArrow = 'leftArrow',
UpArrow = 'upArrow',
DownArrow = 'downArrow',
LeftRightArrow = 'leftRightArrow',
UpDownArrow = 'upDownArrow',
QuadArrow = 'quadArrow',
LeftRightUpArrow = 'leftRightUpArrow',
BentArrow = 'bentArrow',
UturnArrow = 'uturnArrow',
CircularArrow = 'circularArrow',
NotchedRightArrow = 'notchedRightArrow',
HomePlate = 'homePlate',
Chevron = 'chevron',
LeftCircularArrow = 'leftCircularArrow',
LeftRightCircularArrow = 'leftRightCircularArrow',
}
export enum OtherShapes {
Plaque = 'plaque',
Can = 'can',
Cube = 'cube',
Bevel = 'bevel',
Donut = 'donut',
NoSmoking = 'noSmoking',
BlockArc = 'blockArc',
FoldedCorner = 'foldedCorner',
}
export enum SpecialShapes {
SmileyFace = 'smileyFace',
Heart = 'heart',
LightningBolt = 'lightningBolt',
Sun = 'sun',
Moon = 'moon',
Cloud = 'cloud',
Arc = 'arc',
Backpack = 'backpack',
Frame = 'frame',
HalfFrame = 'halfFrame',
Corner = 'corner',
Chord = 'chord',
Pie = 'pie',
Teardrop = 'teardrop',
WedgeRectCallout = 'wedgeRectCallout',
WedgeRRectCallout = 'wedgeRRectCallout',
WedgeEllipseCallout = 'wedgeEllipseCallout',
CloudCallout = 'cloudCallout',
BorderCallout1 = 'borderCallout1',
BorderCallout2 = 'borderCallout2',
BorderCallout3 = 'borderCallout3',
AccentCallout1 = 'accentCallout1',
AccentCallout2 = 'accentCallout2',
AccentCallout3 = 'accentCallout3',
Callout1 = 'callout1',
Callout2 = 'callout2',
Callout3 = 'callout3',
ActionButtonBackPrevious = 'actionButtonBackPrevious',
ActionButtonEnd = 'actionButtonEnd',
ActionButtonForwardNext = 'actionButtonForwardNext',
ActionButtonHelp = 'actionButtonHelp',
ActionButtonHome = 'actionButtonHome',
ActionButtonInformation = 'actionButtonInformation',
ActionButtonMovie = 'actionButtonMovie',
ActionButtonReturn = 'actionButtonReturn',
ActionButtonSound = 'actionButtonSound',
}
/**
* 20.1.9.18 prstGeom (Preset geometry)
*/
export type ShapeType = BasicShapes | ArrowsAndMarkersShapes | OtherShapes | SpecialShapes;
export type PresetGeometryType = ShapeType | 'custom';

View File

@@ -0,0 +1,187 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 {
IColorStyle,
ICustomBlock,
IDocumentData,
IImageProperties,
ILists,
IPlaceholder,
IShapeProperties,
ISize,
IStyleBase,
IStyleData,
ITransformState,
IWorksheetData,
LocaleType,
Nullable,
ThemeColorType,
} from '@univerjs/core';
import type { ShapeType } from '../enum/prst-geom-type';
export interface ISlideData extends IReferenceSource {
id: string; // unit id
locale?: LocaleType;
title: string;
pageSize: ISize;
body?: ISlidePageBody;
}
interface IReferenceSource {
master?: { [id: string]: ISlidePage };
handoutMaster?: { [id: string]: ISlidePage };
notesMaster?: { [id: string]: ISlidePage };
layouts?: { [id: string]: ISlidePage };
lists?: ILists;
}
interface ISlidePageBody {
pages: { [id: string]: ISlidePage };
pageOrder: string[];
}
export interface ISlidePage {
id: string;
pageType: PageType;
zIndex: number;
title: string;
description: string;
pageBackgroundFill: IColorStyle;
colorScheme?: ThemeColorType;
pageElements: { [elementId: string]: IPageElement };
// Union field properties. Properties that are specific for each page type. Masters do not require any additional properties. properties can be only one of the following:
slideProperties?: ISlideProperties;
layoutProperties?: ILayoutProperties;
notesProperties?: INotesProperties;
handoutProperties?: IHandoutProperties;
masterProperties?: IMasterProperties;
}
interface ISlideProperties {
layoutObjectId: string;
masterObjectId: string;
isSkipped: boolean;
}
interface ILayoutProperties {
masterObjectId: string;
name: string;
}
interface INotesProperties {
name: string;
}
interface IHandoutProperties {
name: string;
}
interface IMasterProperties {
name: string;
}
export interface ISlideRichTextProps extends ITransformState, IStyleBase {
text?: string;
rich?: IDocumentData;
}
export interface IPageElement {
id: string;
zIndex: number;
left?: number;
top?: number;
width?: number;
height?: number;
angle?: number;
scaleX?: number;
scaleY?: number;
skewX?: number;
skewY?: number;
flipX?: boolean;
flipY?: boolean;
title: string;
description: string;
type: PageElementType;
// Union field element_kind can be only one of the following:
// elementGroup: IGroup;
shape?: IShape;
image?: IImage;
richText?: ISlideRichTextProps;
spreadsheet?: {
worksheet: IWorksheetData;
styles: Record<string, Nullable<IStyleData>>;
};
document?: IDocumentData;
slide?: ISlideData;
// video: IVideo;
// line: ILine;
// table: ITable;
// chart: IChartProperties;
customBlock?: ICustomBlock; // customBlock block customized by user through plugin
}
export enum PageType {
SLIDE, // A slide page.
MASTER, // A master slide page.
LAYOUT, // A layout page.
HANDOUT_MASTER, // A handout master page.
NOTES_MASTER, // A notes master page.
}
export enum PageElementType {
SHAPE,
IMAGE,
TEXT,
SPREADSHEET,
DOCUMENT,
SLIDE,
}
/**
* IShape
*/
export interface IShape {
shapeType: ShapeType;
text: string;
shapeProperties: IShapeProperties;
placeholder?: IPlaceholder;
link?: ILink;
}
export interface IImage {
imageProperties?: IImageProperties;
placeholder?: IPlaceholder;
link?: ILink;
}
interface ILink {
relativeLink: RelativeSlideLink;
pageId?: string;
slideIndex?: number;
}
export enum RelativeSlideLink {
RELATIVE_SLIDE_LINK_UNSPECIFIED, // An unspecified relative slide link.
NEXT_SLIDE, // A link to the next slide.
PREVIOUS_SLIDE, // A link to the previous slide.
FIRST_SLIDE, // A link to the first slide in the presentation.
LAST_SLIDE, // A link to the last slide in the presentation.
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Injector, Nullable } from '@univerjs/core';
import type { BaseObject, Scene } from '@univerjs/engine-render';
import type { IPageElement, PageElementType } from '../../types/interfaces/i-slide-data';
import { Registry } from '@univerjs/core';
export abstract class ObjectAdaptor {
zIndex = 0;
viewKey: PageElementType | null = null;
check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
abstract convert(pageElement: IPageElement, mainScene: Scene): Nullable<BaseObject>;
create(injector: Injector): void {
// FIXME: should not be empty
// empty
}
}
export const CanvasObjectProviderRegistry = Registry.create();

View File

@@ -0,0 +1,156 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 { Rect } from '@univerjs/engine-render';
import type { IPageElement, ISlideData } from '../../../../types/interfaces/i-slide-data';
import { ConfigService, ContextService, IConfigService, IContextService, Injector, LocaleService } from '@univerjs/core';
import { Scene, SceneViewer } from '@univerjs/engine-render';
import { describe, expect, it } from 'vitest';
import { BasicShapes } from '../../../../types/enum/prst-geom-type';
import { PageElementType, PageType } from '../../../../types/interfaces/i-slide-data';
import { SlideAdaptor } from '../slide-adaptor';
function createRenderTestBed() {
const injector = new Injector();
injector.add([LocaleService, { useClass: LocaleService }]);
injector.add([IContextService, { useClass: ContextService }]);
injector.add([IConfigService, { useClass: ConfigService }]);
return {
injector,
slideAdaptor: injector.createInstance(SlideAdaptor),
};
}
function createMainScene() {
return new Scene(
'main-scene',
new SceneViewer('main-viewer', {
width: 960,
height: 540,
})
);
}
function createDeckElement(): IPageElement {
return {
id: 'embedded-deck',
zIndex: 9,
left: 16,
top: 24,
width: 720,
height: 405,
title: 'Embedded sales deck',
description: 'Deck inside a page element',
type: PageElementType.SLIDE,
slide: createEmbeddedSlideData(),
};
}
function createEmbeddedSlideData(): ISlideData {
return {
id: 'sales-deck',
title: 'Sales update',
pageSize: { width: 720, height: 405 },
body: {
pageOrder: ['summary', 'pipeline'],
pages: {
summary: {
id: 'summary',
pageType: PageType.SLIDE,
zIndex: 1,
title: 'Summary',
description: 'Executive summary',
pageBackgroundFill: { rgb: '#112233' },
pageElements: {
'sales-box': {
id: 'sales-box',
zIndex: 3,
left: 40,
top: 32,
width: 180,
height: 72,
title: 'Revenue card',
description: 'Revenue highlight',
type: PageElementType.SHAPE,
shape: {
shapeType: BasicShapes.RoundRect,
text: '',
shapeProperties: {
shapeBackgroundFill: { rgb: '#44aa88' },
radius: 12,
outline: {
outlineFill: { rgb: '#0b3128' },
weight: 2,
},
},
},
},
},
},
pipeline: {
id: 'pipeline',
pageType: PageType.SLIDE,
zIndex: 2,
title: 'Pipeline',
description: 'Next page',
pageBackgroundFill: { rgb: '#ffeecc' },
pageElements: {},
},
},
},
};
}
describe('SlideAdaptor', () => {
it('converts slide pages in source order with page backgrounds and child shape styling', () => {
const { slideAdaptor } = createRenderTestBed();
const deck = slideAdaptor.convert(createDeckElement(), createMainScene())!;
expect(deck.left).toBe(16);
expect(deck.top).toBe(24);
expect(deck.width).toBe(720);
expect(deck.height).toBe(405);
expect(deck.zIndex).toBe(9);
expect([...deck.getSubScenes().keys()]).toEqual(['summary', 'pipeline']);
expect(deck.getActiveSubScene()?.sceneKey).toBe('summary');
const summaryScene = deck.getSubScene('summary')!;
const summaryBackground = summaryScene.getObject('canvas') as Rect;
expect(summaryBackground.fill).toBe('#112233');
expect(summaryBackground.width).toBe(720);
expect(summaryBackground.height).toBe(405);
expect(summaryBackground.evented).toBe(false);
const salesBox = summaryScene.getObject('sales-box') as Rect;
expect(salesBox.left).toBe(40);
expect(salesBox.top).toBe(32);
expect(salesBox.width).toBe(180);
expect(salesBox.height).toBe(72);
expect(salesBox.zIndex).toBe(3);
expect(salesBox.fill).toBe('#44aa88');
expect(salesBox.stroke).toBe('#0b3128');
expect(salesBox.strokeWidth).toBe(2);
expect(salesBox.getPropByKey('radius')).toBe(12);
const pipelineScene = deck.getSubScene('pipeline')!;
expect((pipelineScene.getObject('canvas') as Rect).fill).toBe('#ffeecc');
deck.changePage('pipeline');
expect(deck.getActiveSubScene()?.sceneKey).toBe('pipeline');
});
});

View File

@@ -0,0 +1,371 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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, Injector } from '@univerjs/core';
import type { BaseObject, IDocumentSkeletonDrawing, IPageRenderConfig, IWheelEvent } from '@univerjs/engine-render';
import type { IPageElement } from '../../../types/interfaces/i-slide-data';
import { DocumentDataModel, Inject, LocaleService } from '@univerjs/core';
import {
Documents,
DocumentSkeleton,
DocumentViewModel,
Image,
Liquid,
PageLayoutType,
// Rect,
Scene,
SceneViewer,
ScrollBar,
Viewport,
} from '@univerjs/engine-render';
import { PageElementType } from '../../../types/interfaces/i-slide-data';
import { CanvasObjectProviderRegistry, ObjectAdaptor } from '../adaptor';
export enum DOCS_VIEW_KEY {
MAIN = '__DocsRender__',
SCENE_VIEWER = '__DocsViewer__',
SCENE = '__DocsScene__',
VIEWPORT = '__DocsViewPort_',
}
export class DocsAdaptor extends ObjectAdaptor {
override zIndex = 5;
override viewKey = PageElementType.DOCUMENT;
private _liquid = new Liquid();
constructor(@Inject(LocaleService) private readonly _localeService: LocaleService) {
super();
}
override check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
// eslint-disable-next-line max-lines-per-function
override convert(pageElement: IPageElement, mainScene: Scene) {
const {
id,
zIndex,
left = 0,
top = 0,
width,
height,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
title,
description,
document: documentData,
} = pageElement;
if (documentData == null) {
return;
}
const docDataModel = new DocumentDataModel(documentData);
const docViewModel = new DocumentViewModel(docDataModel);
const documentSkeleton = DocumentSkeleton.create(docViewModel, this._localeService);
const documents = new Documents(DOCS_VIEW_KEY.MAIN, documentSkeleton);
documentSkeleton.calculate();
const sv = new SceneViewer(DOCS_VIEW_KEY.SCENE_VIEWER + id, {
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
});
const scene = new Scene(DOCS_VIEW_KEY.SCENE + id, sv);
const viewMain = new Viewport(DOCS_VIEW_KEY.VIEWPORT + id, 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);
}
});
const scrollbar = new ScrollBar(viewMain, {
mainScene,
});
scene.addObject(documents);
// documents.calculatePagePosition();
// documents.enableEditor();
const size = documentSkeleton.getActualSize();
documents.resize(size.actualWidth, size.actualHeight);
scene.transformByState({ width: size.actualWidth, height: size.actualHeight + 200 });
const pageSize = documents.getSkeleton()?.getPageSize();
documents.pageRender$.subscribe((config: IPageRenderConfig) => {
const { page, pageLeft, pageTop, ctx } = config;
const { width, height, marginBottom, marginLeft, marginRight, marginTop } = page;
ctx.save();
ctx.translate(pageLeft - 0.5, pageTop - 0.5);
// Rect.drawWith(ctx, {
// width: pageSize?.width || width,
// height: pageSize?.height || height,
// strokeWidth: 1,
// stroke: 'rgba(198,198,198, 1)',
// fill: 'rgba(255,255,255, 1)',
// zIndex: 3,
// });
ctx.restore();
});
const { left: docsLeft, top: docsTop } = documents;
const skeletonData = documentSkeleton.getSkeletonData();
if (skeletonData == null) {
return;
}
const { pages } = skeletonData;
const objectList: BaseObject[] = [];
const pageMarginCache = new Map<string, { marginLeft: number; marginTop: number }>();
this._recalculateSizeBySkeleton(documents, scene, documentSkeleton);
for (let i = 0, len = pages.length; i < len; i++) {
const page = pages[i];
const { skeDrawings, marginLeft, marginTop, pageWidth, pageHeight } = page;
// cumPageLeft + = pageWidth + documents.pageMarginLeft;
this._liquid.translatePagePadding(page);
skeDrawings.forEach((drawing: IDocumentSkeletonDrawing) => {
const { aLeft, aTop, height, width, drawingOrigin } = drawing;
const { docTransform } = drawingOrigin;
const rect = new Image(drawing.drawingId, {
// url: docTransform.imageProperties?.contentUrl || '',
left: aLeft + docsLeft + this._liquid.x,
top: aTop + docsTop + this._liquid.y,
width,
height,
zIndex: 11,
});
pageMarginCache.set(drawing.drawingId, {
marginLeft: this._liquid.x,
marginTop: this._liquid.y,
});
objectList.push(rect);
});
this._liquid.translatePage(
page,
documents.pageLayoutType,
documents.pageMarginLeft,
documents.pageMarginTop
);
}
scene.addObjects(objectList);
objectList.forEach((object) => {
scene.attachTransformerTo(object);
});
scene.getTransformer()?.changing$.subscribe((state) => {
const { objects } = state;
objects.forEach((object) => {
const { oKey, left, top, height, width } = object;
const cache = pageMarginCache.get(oKey);
const marginLeft = cache?.marginLeft || 0;
const marginTop = cache?.marginTop || 0;
// console.log('onChangingObservable', top, docsTop, marginTop, top - docsTop - marginTop);
documentSkeleton
?.getViewModel()
.getDataModel()
.updateDrawing(oKey, {
left: left - docsLeft - marginLeft,
top: top - docsTop - marginTop,
height,
width,
});
});
documentSkeleton?.calculate();
});
this._calculatePagePosition(documents, scene, viewMain);
return sv;
}
private _recalculateSizeBySkeleton(docsComponent: Documents, scene: Scene, skeleton: DocumentSkeleton) {
const pages = skeleton.getSkeletonData()?.pages;
if (pages == null) {
return;
}
let width = 0;
let height = 0;
for (let i = 0, len = pages.length; i < len; i++) {
const page = pages[i];
const { pageWidth, pageHeight } = page;
if (docsComponent.pageLayoutType === PageLayoutType.VERTICAL) {
height += pageHeight;
height += docsComponent.pageMarginTop;
if (i === len - 1) {
height += docsComponent.pageMarginTop;
}
width = Math.max(width, pageWidth);
} else if (docsComponent.pageLayoutType === PageLayoutType.HORIZONTAL) {
width += pageWidth;
if (i !== len - 1) {
width += docsComponent.pageMarginLeft;
}
height = Math.max(height, pageHeight);
}
}
docsComponent.resize(width, height);
scene.transformByState({ width, height });
}
private _calculatePagePosition(docsComponent: Documents, scene: Scene, viewport: Viewport, zoomRatio: number = 1) {
const parent = scene?.getParent();
const { width: docsWidth, height: docsHeight, pageMarginLeft, pageMarginTop } = docsComponent;
if (parent == null || docsWidth === Number.POSITIVE_INFINITY || docsHeight === Number.POSITIVE_INFINITY) {
return;
}
const { width: engineWidth, height: engineHeight } = parent;
let docsLeft = 0;
let docsTop = 0;
let sceneWidth = 0;
let sceneHeight = 0;
let scrollToX = Number.POSITIVE_INFINITY;
if (engineWidth > (docsWidth + pageMarginLeft * 2) * zoomRatio) {
docsLeft = engineWidth / 2 - (docsWidth * zoomRatio) / 2;
docsLeft /= zoomRatio;
sceneWidth = (engineWidth - pageMarginLeft * 2) / zoomRatio;
scrollToX = 0;
} else {
docsLeft = pageMarginLeft;
sceneWidth = docsWidth + pageMarginLeft * 2;
scrollToX = (sceneWidth - engineWidth / zoomRatio) / 2;
}
if (engineHeight > docsHeight) {
docsTop = engineHeight / 2 - docsHeight / 2;
sceneHeight = (engineHeight - pageMarginTop * 2) / zoomRatio;
} else {
docsTop = pageMarginTop;
sceneHeight = docsHeight + pageMarginTop * 2;
}
// this.docsLeft = docsLeft;
// this.docsTop = docsTop;
scene.transformByState({ width: sceneWidth, height: sceneHeight + 200 });
docsComponent.translate(docsLeft, docsTop);
if (scrollToX !== Number.POSITIVE_INFINITY && viewport != null) {
const actualX = viewport.transScroll2ViewportScrollValue(scrollToX, 0).x;
viewport.scrollToBarPos({
x: actualX,
});
}
return this;
}
}
export class DocsAdaptorFactory {
readonly zIndex = 5;
create(injector: Injector): DocsAdaptor {
const docsAdaptor = injector.createInstance(DocsAdaptor);
return docsAdaptor;
}
}
CanvasObjectProviderRegistry.add(new DocsAdaptorFactory());

View File

@@ -0,0 +1,86 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Injector } from '@univerjs/core';
import type { IPageElement } from '../../../types/interfaces/i-slide-data';
import { Image } from '@univerjs/engine-render';
import { PageElementType } from '../../../types/interfaces/i-slide-data';
import { CanvasObjectProviderRegistry, ObjectAdaptor } from '../adaptor';
export class ImageAdaptor extends ObjectAdaptor {
override zIndex = 1;
override viewKey = PageElementType.IMAGE;
override check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
override convert(pageElement: IPageElement) {
const {
id,
zIndex,
left = 0,
top = 0,
width,
height,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
title,
description,
image = {},
} = pageElement;
const { imageProperties, placeholder, link } = image;
const contentUrl = imageProperties?.contentUrl || '';
return new Image(id, {
url: contentUrl,
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
});
}
}
export class ImageAdaptorFactory {
readonly zIndex = 4;
create(injector: Injector): ImageAdaptor {
const imageAdaptor = injector.createInstance(ImageAdaptor);
return imageAdaptor;
}
}
CanvasObjectProviderRegistry.add(new ImageAdaptorFactory());

View File

@@ -0,0 +1,22 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './docs-adaptor';
export * from './image-adaptor';
export * from './rich-text-adaptor';
export * from './shape-adaptor';
export * from './slide-adaptor';
export * from './spreadsheet-adaptor';

View File

@@ -0,0 +1,101 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Injector } from '@univerjs/core';
import type { IRichTextProps, Scene } from '@univerjs/engine-render';
import type { IPageElement } from '../../../types/interfaces/i-slide-data';
import { Inject, LocaleService } from '@univerjs/core';
import { RichText } from '@univerjs/engine-render';
import { PageElementType } from '../../../types/interfaces/i-slide-data';
import { CanvasObjectProviderRegistry, ObjectAdaptor } from '../adaptor';
export class RichTextAdaptor extends ObjectAdaptor {
override zIndex = 2;
override viewKey = PageElementType.TEXT;
constructor(@Inject(LocaleService) private readonly _localeService: LocaleService) {
super();
}
override check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
override convert(pageElement: IPageElement, _mainScene: Scene) {
const {
id,
zIndex,
left = 0,
top = 0,
width,
height,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
title,
description,
richText = {},
} = pageElement;
const { text, ff, fs, it, bl, ul, st, ol, bg, bd, cl, rich } = richText;
let config: IRichTextProps = {
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
};
let isNotNull = false;
if (text != null) {
config = { ...config, text, ff, fs, it, bl, ul, st, ol, bg, bd, cl };
isNotNull = true;
} else if (rich != null) {
config = { ...config, richText: rich };
isNotNull = true;
}
if (isNotNull === false) {
return;
}
return new RichText(this._localeService, id, config);
}
}
export class RichTextAdaptorFactory {
readonly zIndex = 0;
create(injector: Injector): RichTextAdaptor {
const richTextAdaptor = injector.createInstance(RichTextAdaptor);
return richTextAdaptor;
}
}
CanvasObjectProviderRegistry.add(new RichTextAdaptorFactory());

View File

@@ -0,0 +1,143 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Injector } from '@univerjs/core';
import type { IPageElement } from '../../../types/interfaces/i-slide-data';
import { getColorStyle } from '@univerjs/core';
import { Circle, Rect } from '@univerjs/engine-render';
import { BasicShapes } from '../../../types/enum/prst-geom-type';
import { PageElementType } from '../../../types/interfaces/i-slide-data';
import { CanvasObjectProviderRegistry, ObjectAdaptor } from '../adaptor';
export class ShapeAdaptor extends ObjectAdaptor {
override zIndex = 2;
override viewKey = PageElementType.SHAPE;
override check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
override convert(pageElement: IPageElement) {
const {
id,
zIndex,
left = 0,
top = 0,
width,
height,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
title,
description,
} = pageElement;
const { shapeType, text, shapeProperties, placeholder, link } = pageElement.shape || {};
const fill =
shapeProperties == null ? '' : getColorStyle(shapeProperties.shapeBackgroundFill) || 'rgba(255,255,255,1)';
const outline = shapeProperties?.outline;
const strokeStyle: { [key: string]: string | number } = {};
if (outline) {
const { outlineFill, weight } = outline;
strokeStyle.strokeWidth = weight;
strokeStyle.stroke = getColorStyle(outlineFill) || 'rgba(0,0,0,1)';
}
if (shapeType === BasicShapes.Rect) {
return new Rect(id, {
fill,
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
...strokeStyle,
});
}
if (shapeType === BasicShapes.RoundRect) {
const radius = shapeProperties?.radius || 0;
return new Rect(id, {
fill,
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
radius,
...strokeStyle,
});
}
if (shapeType === BasicShapes.Ellipse) {
console.warn(shapeProperties?.radius);
const radius = shapeProperties?.radius || 0;
return new Circle(id, {
fill,
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
radius,
...strokeStyle,
});
}
}
}
export class ShapeAdaptorFactory {
readonly zIndex = 2;
create(injector: Injector): ShapeAdaptor {
const shapeAdaptor = injector.createInstance(ShapeAdaptor);
return shapeAdaptor;
}
}
CanvasObjectProviderRegistry.add(new ShapeAdaptorFactory());

View File

@@ -0,0 +1,183 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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 { IColorStyle } from '@univerjs/core';
import type { Engine } from '@univerjs/engine-render';
import type { IPageElement, ISlidePage } from '../../../types/interfaces/i-slide-data';
import { getColorStyle, Inject, Injector } from '@univerjs/core';
import { Rect, Scene, Slide, Viewport } from '@univerjs/engine-render';
import { SlideDataModel } from '../../../data-model/slide-data-model';
import { PageElementType } from '../../../types/interfaces/i-slide-data';
import { CanvasObjectProviderRegistry, ObjectAdaptor } from '../adaptor';
import { ObjectProvider } from '../object-provider';
// import { DocsAdaptor, ImageAdaptor, RichTextAdaptor, ShapeAdaptor } from './';
export enum SLIDE_VIEW_KEY {
MAIN = '__SLIDERender__',
SCENE_VIEWER = '__SLIDEViewer__',
SCENE = '__SLIDEScene__',
VIEWPORT = '__SLIDEViewPort_',
}
export class SlideAdaptor extends ObjectAdaptor {
override zIndex = 6;
override viewKey = PageElementType.SLIDE;
private _ObjectProvider: ObjectProvider | null = null;
constructor(@Inject(Injector) private _injector: Injector) {
super();
}
override check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
override convert(pageElement: IPageElement, mainScene: Scene) {
const {
id,
zIndex,
left = 0,
top = 0,
width,
height,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
title,
description,
slide: slideData,
} = pageElement;
if (slideData == null) {
return;
}
const model = new SlideDataModel(slideData);
const slideComponent = new Slide(SLIDE_VIEW_KEY.MAIN + id, {
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
});
slideComponent.enableNav();
slideComponent.enableSelectedClipElement();
const pageOrder = model.getPageOrder();
const pages = model.getPages();
if (!pageOrder || !pages) {
return slideComponent;
}
this._ObjectProvider = new ObjectProvider(this._injector);
for (let i = 0, len = pageOrder.length; i < len; i++) {
const page = pages[pageOrder[i]];
const { id } = page;
slideComponent.addPageScene(this._createScene(id, slideComponent, page, mainScene, model));
}
slideComponent.activeFirstPage();
return slideComponent;
}
private _createScene(pageId: string, parent: Engine | Slide, page: ISlidePage, mainScene: Scene, model: SlideDataModel) {
const { width, height } = parent;
const scene = new Scene(pageId, parent, {
width,
height,
});
const viewMain = new Viewport(`PageViewer_${pageId}`, scene, {
left: 0,
top: 0,
bottom: 0,
right: 0,
explicitViewportWidthSet: false,
explicitViewportHeightSet: false,
});
viewMain.closeClip();
const { pageElements, pageBackgroundFill } = page;
const objects = this._ObjectProvider?.convertToRenderObjects(pageElements, mainScene);
this._addBackgroundRect(scene, pageBackgroundFill, model);
scene.addObjects(objects!);
objects?.forEach((object) => {
scene.attachTransformerTo(object);
});
return scene;
}
private _addBackgroundRect(scene: Scene, fill: IColorStyle, model: SlideDataModel) {
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);
}
}
export class SlideAdaptorFactory {
readonly zIndex = 6;
create(injector: Injector): SlideAdaptor {
const slideAdaptor = injector.createInstance(SlideAdaptor);
return slideAdaptor;
}
}
CanvasObjectProviderRegistry.add(new SlideAdaptorFactory());

View File

@@ -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 { EventState } from '@univerjs/core';
import type { IScrollObserverParam, IWheelEvent } from '@univerjs/engine-render';
import type { IPageElement } from '../../../types/interfaces/i-slide-data';
import { IConfigService, IContextService, Inject, Injector, LocaleService, Styles, Worksheet } from '@univerjs/core';
import {
getColor,
Rect,
Scene,
SceneViewer,
ScrollBar,
Spreadsheet,
SpreadsheetColumnHeader,
SpreadsheetRowHeader,
SpreadsheetSkeleton,
Viewport,
} from '@univerjs/engine-render';
import { PageElementType } from '../../../types/interfaces/i-slide-data';
import { CanvasObjectProviderRegistry, ObjectAdaptor } from '../adaptor';
enum SHEET_VIEW_KEY {
MAIN = 'spreadInSlide',
SCENE = 'spreadInSlideScene',
SCENE_VIEWER = 'spreadInSlideSceneViewer',
ROW = 'spreadInSlideRow',
COLUMN = 'spreadInSlideColumn',
LEFT_TOP = 'spreadInSlideLeftTop',
VIEW_MAIN = 'spreadInSlideViewMain',
VIEW_TOP = 'spreadInSlideViewTop',
VIEW_LEFT = 'spreadInSlideViewLeft',
VIEW_LEFT_TOP = 'spreadInSlideViewLeftTop',
}
export class SpreadsheetAdaptor extends ObjectAdaptor {
override zIndex = 4;
override viewKey = PageElementType.SPREADSHEET;
constructor(
@Inject(LocaleService) private readonly _localeService: LocaleService,
@IContextService private readonly _contextService: IContextService,
@IConfigService private readonly _configService: IConfigService,
@Inject(Injector) private readonly _injector: Injector
) {
super();
}
override check(type: PageElementType) {
if (type !== this.viewKey) {
return;
}
return this;
}
override convert(pageElement: IPageElement, mainScene: Scene) {
const {
id,
zIndex,
left = 0,
top = 0,
width,
height,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
spreadsheet: spreadsheetModel,
} = pageElement;
if (spreadsheetModel == null) {
return;
}
const { worksheet, styles } = spreadsheetModel;
const styleModel = new Styles(styles);
const spreadsheetSkeleton = new SpreadsheetSkeleton(
new Worksheet(id, worksheet, styleModel), // FIXME: worksheet in slide doesn't has a Worksheet object
styleModel,
this._localeService,
this._contextService,
this._configService,
this._injector
);
const { rowTotalHeight, columnTotalWidth, rowHeaderWidth, columnHeaderHeight } = spreadsheetSkeleton;
const allWidth = columnTotalWidth + worksheet.rowHeader.width || 0;
const allHeight = rowTotalHeight + worksheet.columnHeader.height || 0;
const sv = new SceneViewer(SHEET_VIEW_KEY.SCENE_VIEWER + id, {
top,
left,
width,
height,
zIndex,
angle,
scaleX,
scaleY,
skewX,
skewY,
flipX,
flipY,
forceRender: true,
});
const scene = new Scene(SHEET_VIEW_KEY.SCENE + id, sv, {
width: allWidth,
height: allHeight,
});
spreadsheetSkeleton.setScene(scene);
scene.disposeWithMe(spreadsheetSkeleton);
this._updateViewport(id, rowHeaderWidth, columnHeaderHeight, scene, mainScene);
const spreadsheet = new Spreadsheet('testSheetViewer', spreadsheetSkeleton, false);
const spreadsheetRowHeader = new SpreadsheetRowHeader(SHEET_VIEW_KEY.ROW, spreadsheetSkeleton);
const spreadsheetColumnHeader = new SpreadsheetColumnHeader(SHEET_VIEW_KEY.COLUMN, spreadsheetSkeleton);
const SpreadsheetLeftTopPlaceholder = new Rect(SHEET_VIEW_KEY.LEFT_TOP, {
zIndex: 2,
left: -1,
top: -1,
width: rowHeaderWidth,
height: columnHeaderHeight,
fill: getColor([248, 249, 250]),
stroke: getColor([217, 217, 217]),
strokeWidth: 1,
});
spreadsheet.zIndex = 10;
scene.addObjects([spreadsheet], 1);
scene.addObjects([spreadsheetRowHeader, spreadsheetColumnHeader, SpreadsheetLeftTopPlaceholder], 2);
return sv;
}
private _updateViewport(
id: string,
rowHeaderWidth: number,
columnHeaderHeight: number,
scene: Scene,
mainScene: Scene
) {
if (mainScene == null) {
return;
}
const rowHeaderWidthScale = rowHeaderWidth * scene.scaleX;
const columnHeaderHeightScale = columnHeaderHeight * scene.scaleY;
const viewMain = new Viewport(SHEET_VIEW_KEY.VIEW_MAIN + id, scene, {
left: rowHeaderWidthScale,
top: columnHeaderHeightScale,
bottom: 0,
right: 0,
explicitViewportWidthSet: false,
explicitViewportHeightSet: false,
isWheelPreventDefaultX: true,
});
const viewTop = new Viewport(SHEET_VIEW_KEY.VIEW_TOP + id, scene, {
left: rowHeaderWidthScale,
height: columnHeaderHeightScale,
top: 0,
right: 0,
explicitViewportWidthSet: false,
isWheelPreventDefaultX: true,
});
const viewLeft = new Viewport(SHEET_VIEW_KEY.VIEW_LEFT + id, scene, {
left: 0,
bottom: 0,
top: columnHeaderHeightScale,
width: rowHeaderWidthScale,
explicitViewportHeightSet: false,
isWheelPreventDefaultX: true,
});
const _viewLeftTop = new Viewport(SHEET_VIEW_KEY.VIEW_LEFT_TOP + id, scene, {
left: 0,
top: 0,
width: rowHeaderWidthScale,
height: columnHeaderHeightScale,
isWheelPreventDefaultX: true,
});
viewMain.onScrollAfter$.subscribeEvent((param: IScrollObserverParam) => {
const { scrollX, scrollY, viewportScrollX, viewportScrollY } = param;
viewTop
.updateScrollVal({
scrollX,
viewportScrollX,
});
viewLeft
.updateScrollVal({
scrollY,
viewportScrollY,
});
});
scene.attachControl();
const _scrollbar = new ScrollBar(viewMain, {
mainScene,
});
this._bindWheel(scene, viewMain);
}
private _bindWheel(scene: Scene, viewMain: Viewport): void {
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.05 : deltaFactor < 80 ? 0.02 : 0.01;
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 {
scene.scaleBy(scrollNum, scrollNum);
e.preventDefault();
}
} else {
viewMain.onMouseWheel(e, state);
}
});
}
}
export class SpreadsheetAdaptorFactory {
readonly zIndex = 4;
create(injector: Injector): SpreadsheetAdaptor {
const spreadsheetAdaptor = injector.createInstance(SpreadsheetAdaptor);
return spreadsheetAdaptor;
}
}
CanvasObjectProviderRegistry.add(new SpreadsheetAdaptorFactory());

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// export * from '../../../../slides-ui/src/controllers/canvas-view';
export * from './adaptor';
export { SLIDE_VIEW_KEY } from './adaptors';
export * from './object-provider';
export enum SLIDE_KEY {
COMPONENT = '__slideRender__',
SCENE = '__mainScene__',
VIEW = '__mainView__',
}
export type PageID = string;

View File

@@ -0,0 +1,66 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BaseObject, Scene } from '@univerjs/engine-render';
import type { IPageElement } from '../../types/interfaces/i-slide-data';
import type { ObjectAdaptor } from './adaptor';
import { Inject, Injector, sortRules } from '@univerjs/core';
import { CanvasObjectProviderRegistry } from './adaptor';
import './adaptors';
export class ObjectProvider {
private _adaptors: ObjectAdaptor[] = [];
constructor(@Inject(Injector) private readonly _injector: Injector) {
this._adaptorLoader();
}
convertToRenderObjects(pageElements: { [elementId: string]: IPageElement }, mainScene: Scene) {
const pageKeys = Object.keys(pageElements);
const objects: BaseObject[] = [];
pageKeys.forEach((key) => {
const pageElement = pageElements[key];
const o = this._executor(pageElement, mainScene);
if (o != null) {
objects.push(o);
}
});
return objects;
}
convertToRenderObject(pageElement: IPageElement, mainScene: Scene) {
return this._executor(pageElement, mainScene);
}
private _executor(pageElement: IPageElement, mainScene: Scene) {
const { id: pageElementId, type } = pageElement;
for (const adaptor of this._adaptors) {
const o = adaptor.check(type)?.convert(pageElement, mainScene);
if (o != null) {
return o;
}
}
}
private _adaptorLoader() {
CanvasObjectProviderRegistry.getData()
.sort(sortRules)
.forEach((adaptorFactory: ObjectAdaptor) => {
this._adaptors.push(adaptorFactory.create(this._injector) as unknown as ObjectAdaptor);
});
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@univerjs-infra/shared/tsconfigs/base",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"]
}

View 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"
]
}

View File

@@ -0,0 +1,3 @@
import createConfig from '@univerjs-infra/shared/vitest';
export default createConfig();