diff --git a/examples/package.json b/examples/package.json index 5f55fd5664c..5cc9bdeaae5 100644 --- a/examples/package.json +++ b/examples/package.json @@ -50,6 +50,7 @@ "@univerjs/thread-comment-ui": "workspace:*", "@univerjs/ui": "workspace:*", "@univerjs/uniscript": "workspace:*", + "@univerjs/uniui": "workspace:*", "@wendellhu/redi": "0.15.4", "clsx": "^2.1.1", "monaco-editor": "0.49.0", diff --git a/examples/src/data/sheets/demo/default-workbook-data-demo1.ts b/examples/src/data/sheets/demo/default-workbook-data-demo1.ts index ac7a98ee862..dedf64bac1b 100644 --- a/examples/src/data/sheets/demo/default-workbook-data-demo1.ts +++ b/examples/src/data/sheets/demo/default-workbook-data-demo1.ts @@ -18,7 +18,7 @@ import type { IWorkbookData } from '@univerjs/core'; import { BooleanNumber, BorderStyleTypes, LocaleType } from '@univerjs/core'; export const DEFAULT_WORKBOOK_DATA_DEMO1: IWorkbookData = { - id: 'workbook-01', + id: 'workbook-001', locale: LocaleType.ZH_CN, name: 'universheet', sheetOrder: ['sheet-0003'], diff --git a/examples/src/uni/main.ts b/examples/src/uni/main.ts index 05ddd25a71b..156c2410232 100644 --- a/examples/src/uni/main.ts +++ b/examples/src/uni/main.ts @@ -25,12 +25,13 @@ import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'; import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'; import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor'; -import { UniverUIPlugin } from '@univerjs/ui'; +import { UniverUniUIPlugin } from '@univerjs/uniui'; import { UniverDebuggerPlugin } from '@univerjs/debugger'; import { FUniver } from '@univerjs/facade'; import { UniverDrawingPlugin } from '@univerjs/drawing'; import { DEFAULT_WORKBOOK_DATA_DEMO } from '../data/sheets/demo/default-workbook-data-demo'; import { enUS } from '../locales'; +import { DEFAULT_DOCUMENT_DATA_CN } from '../data'; /* eslint-disable-next-line node/prefer-global/process */ const IS_E2E: boolean = !!process.env.IS_E2E; @@ -52,7 +53,7 @@ univer.registerPlugin(UniverDocsPlugin, { }); univer.registerPlugin(UniverFormulaEnginePlugin); univer.registerPlugin(UniverRenderEnginePlugin); -univer.registerPlugin(UniverUIPlugin, { +univer.registerPlugin(UniverUniUIPlugin, { container: 'app', }); @@ -70,6 +71,8 @@ univer.registerPlugin(UniverSheetsFormulaPlugin); // create univer sheet instance if (!IS_E2E) { univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO); + // univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO1); + univer.createUnit(UniverInstanceType.UNIVER_DOC, DEFAULT_DOCUMENT_DATA_CN); } // debugger plugin diff --git a/packages-experimental/README.md b/packages-experimental/README.md new file mode 100644 index 00000000000..269364b78e5 --- /dev/null +++ b/packages-experimental/README.md @@ -0,0 +1,3 @@ +# Experimental Packages + +Experimental packages in development. These packages are not meant to be published to npm or used in production. diff --git a/packages-experimental/uniui/README.md b/packages-experimental/uniui/README.md new file mode 100644 index 00000000000..fb7f05dcb93 --- /dev/null +++ b/packages-experimental/uniui/README.md @@ -0,0 +1,16 @@ +# @univerjs/uniui + +[![npm version](https://img.shields.io/npm/v/@univerjs/uniui)](https://npmjs.org/packages/@univerjs/uniui) +[![license](https://img.shields.io/npm/l/@univerjs/uniui)](https://img.shields.io/npm/l/@univerjs/uniui) + +## Introduction + +> TODO: Introduction + +## Usage + +### Installation + +```shell +npm i @univerjs/uniui +``` diff --git a/packages-experimental/uniui/package.json b/packages-experimental/uniui/package.json new file mode 100644 index 00000000000..b69552c2e17 --- /dev/null +++ b/packages-experimental/uniui/package.json @@ -0,0 +1,91 @@ +{ + "name": "@univerjs/uniui", + "version": "0.0.1", + "private": true, + "description": "", + "author": "DreamNum ", + "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": [], + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": "./src/index.ts", + "./*": "./src/*" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/es/index.js", + "types": "./lib/types/index.d.ts", + "publishConfig": { + "access": "public", + "main": "./lib/cjs/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", + "lint:types": "tsc --noEmit", + "build": "tsc && vite build" + }, + "peerDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/engine-render": "workspace:*", + "@univerjs/ui": "workspace:*", + "@wendellhu/redi": ">=0.12.13", + "clsx": ">=2.0.0", + "react": ">=16.9.0", + "rxjs": ">=7.0.0" + }, + "dependencies": { + "@univerjs/icons": "^0.1.46", + "rc-util": "^5.43.0" + }, + "devDependencies": { + "@univerjs/core": "workspace:*", + "@univerjs/design": "workspace:*", + "@univerjs/engine-render": "workspace:*", + "@univerjs/shared": "workspace:*", + "@univerjs/ui": "workspace:*", + "@wendellhu/redi": "^0.15.2", + "clsx": "^2.1.1", + "less": "^4.2.0", + "react": "18.3.1", + "rxjs": "^7.8.1", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vitest": "^1.6.0" + } +} diff --git a/packages-experimental/uniui/src/controllers/uniui-desktop.controller.tsx b/packages-experimental/uniui/src/controllers/uniui-desktop.controller.tsx new file mode 100644 index 00000000000..9b0933801ea --- /dev/null +++ b/packages-experimental/uniui/src/controllers/uniui-desktop.controller.tsx @@ -0,0 +1,110 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core'; +import { IRenderManagerService } from '@univerjs/engine-render'; +import type { IWorkbenchOptions } from '@univerjs/ui'; +import { BuiltInUIPart, CanvasPopup, FloatDom, ILayoutService, IUIPartsService } from '@univerjs/ui'; +import type { IDisposable } from '@wendellhu/redi'; +import { Inject, Injector, Optional } from '@wendellhu/redi'; +import { connectInjector } from '@wendellhu/redi/react-bindings'; +import React from 'react'; +import { delay, filter, take } from 'rxjs'; +import { render as createRoot, unmount } from 'rc-util/lib/React/render'; + +import { UniWorkbench } from '../views/workbench/UniWorkbench'; + +const STEADY_TIMEOUT = 3000; + +export class UniverUniUIController extends Disposable { + constructor( + @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, + @Inject(Injector) private readonly _injector: Injector, + @Inject(LifecycleService) private readonly _lifecycleService: LifecycleService, + @IUIPartsService private readonly _uiPartsService: IUIPartsService, + @Optional(ILayoutService) private readonly _layoutService?: ILayoutService + ) { + super(); + this._initBuiltinComponents(); + } + + bootstrapWorkbench(options: IWorkbenchOptions): void { + this.disposeWithMe( + bootstrap(this._injector, options, () => { + this._lifecycleService.lifecycle$.pipe( + filter((lifecycle) => lifecycle === LifecycleStages.Ready), + delay(300), + take(1) + ).subscribe(() => { + this._lifecycleService.stage = LifecycleStages.Rendered; + setTimeout(() => this._lifecycleService.stage = LifecycleStages.Steady, STEADY_TIMEOUT); + }); + }) + ); + } + + private _initBuiltinComponents() { + this.disposeWithMe(this._uiPartsService.registerComponent(BuiltInUIPart.CONTENT, () => connectInjector(CanvasPopup, this._injector))); + this.disposeWithMe(this._uiPartsService.registerComponent(BuiltInUIPart.CONTENT, () => connectInjector(FloatDom, this._injector))); + } +} + +function bootstrap( + injector: Injector, + options: IWorkbenchOptions, + callback: () => void +): IDisposable { + let mountContainer: HTMLElement; + + const container = options.container; + if (typeof container === 'string') { + const containerElement = document.getElementById(container); + if (!containerElement) { + mountContainer = createContainer(container); + } else { + mountContainer = containerElement; + } + } else if (container instanceof HTMLElement) { + mountContainer = container; + } else { + mountContainer = createContainer('univer'); + } + + const ConnectedApp = connectInjector(UniWorkbench, injector); + function render() { + createRoot( + , + mountContainer + ); + } + + render(); + + return toDisposable(() => { + unmount(mountContainer); + }); +} + +function createContainer(id: string): HTMLElement { + const element = document.createElement('div'); + element.id = id; + // FIXME: the element is not append to the DOM tree. So it won't be rendered. + return element; +} diff --git a/packages/docs-ui/src/views/doc-background/DocBackground.tsx b/packages-experimental/uniui/src/index.ts similarity index 72% rename from packages/docs-ui/src/views/doc-background/DocBackground.tsx rename to packages-experimental/uniui/src/index.ts index 411817dd765..30056d30d9d 100644 --- a/packages/docs-ui/src/views/doc-background/DocBackground.tsx +++ b/packages-experimental/uniui/src/index.ts @@ -14,15 +14,4 @@ * limitations under the License. */ -import React from 'react'; - -import styles from './index.module.less'; - -/** - * Doc standalone background - */ -export const DocBackground = () => { - return ( -
- ); -}; +export { UniverUniUIPlugin } from './plugin'; diff --git a/packages-experimental/uniui/src/plugin.ts b/packages-experimental/uniui/src/plugin.ts new file mode 100644 index 00000000000..1807396322f --- /dev/null +++ b/packages-experimental/uniui/src/plugin.ts @@ -0,0 +1,142 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DependentOn, IContextService, ILocalStorageService, mergeOverrideWithDependencies, Plugin, Tools } from '@univerjs/core'; +import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; +import type { Dependency } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; +import type { IUniverUIConfig } from '@univerjs/ui'; +import { + BrowserClipboardService, + CanvasFloatDomService, + CanvasPopupService, + ComponentManager, + DesktopBeforeCloseService, + DesktopConfirmService, + DesktopContextMenuService, + DesktopDialogService, + DesktopGlobalZoneService, + DesktopLayoutService, + DesktopLocalStorageService, + DesktopMenuService, + DesktopMessageService, + DesktopNotificationService, + DesktopPlatformService, + DesktopShortcutService, + DesktopSidebarService, + DesktopZenZoneService, + DISABLE_AUTO_FOCUS_KEY, + EditorService, + ErrorController, + IBeforeCloseService, + ICanvasPopupService, + IClipboardInterfaceService, + IConfirmService, + IContextMenuService, + IDialogService, + IEditorService, + IGlobalZoneService, + ILayoutService, + IMenuService, + IMessageService, + INotificationService, + IPlatformService, + IProgressService, + IRangeSelectorService, + IShortcutService, + ISidebarService, + IUIController, + IUIPartsService, + IZenZoneService, + ProgressService, + RangeSelectorService, + SharedController, + ShortcutPanelController, + ShortcutPanelService, + UIPartsService, + UNIVER_UI_PLUGIN_NAME, + ZIndexManager, +} from '@univerjs/ui'; +import { UniverUniUIController } from './controllers/uniui-desktop.controller'; +import { UnitGridService } from './services/unit-grid/unit-grid.service'; + +const UI_BOOTSTRAP_DELAY = 16; + +/** + * This plugin enables the Uni Mode of Univer. It should replace + * `UniverUIPlugin` when registered. + */ +@DependentOn(UniverRenderEnginePlugin) +export class UniverUniUIPlugin extends Plugin { + static override pluginName: string = UNIVER_UI_PLUGIN_NAME; + + constructor( + private _config: Partial = {}, + @Inject(Injector) protected readonly _injector: Injector, + @IContextService private readonly _contextService: IContextService + ) { + super(); + + this._config = Tools.deepMerge({}, this._config); + if (this._config.disableAutoFocus) { + this._contextService.setContextValue(DISABLE_AUTO_FOCUS_KEY, true); + } + } + + override onStarting(injector: Injector): void { + const dependencies: Dependency[] = mergeOverrideWithDependencies([ + [ComponentManager], + [ZIndexManager], + + [ShortcutPanelService], + [UnitGridService], + [IUIPartsService, { useClass: UIPartsService }], + [ILayoutService, { useClass: DesktopLayoutService }], + [IShortcutService, { useClass: DesktopShortcutService }], + [IPlatformService, { useClass: DesktopPlatformService }], + [IMenuService, { useClass: DesktopMenuService }], + [IContextMenuService, { useClass: DesktopContextMenuService }], + [IClipboardInterfaceService, { useClass: BrowserClipboardService, lazy: true }], + [INotificationService, { useClass: DesktopNotificationService, lazy: true }], + [IDialogService, { useClass: DesktopDialogService, lazy: true }], + [IConfirmService, { useClass: DesktopConfirmService, lazy: true }], + [ISidebarService, { useClass: DesktopSidebarService, lazy: true }], + [IZenZoneService, { useClass: DesktopZenZoneService, lazy: true }], + [IGlobalZoneService, { useClass: DesktopGlobalZoneService, lazy: true }], + [IMessageService, { useClass: DesktopMessageService, lazy: true }], + [ILocalStorageService, { useClass: DesktopLocalStorageService, lazy: true }], + [IBeforeCloseService, { useClass: DesktopBeforeCloseService }], + [IEditorService, { useClass: EditorService }], + [IRangeSelectorService, { useClass: RangeSelectorService }], + [ICanvasPopupService, { useClass: CanvasPopupService }], + [IProgressService, { useClass: ProgressService }], + [CanvasFloatDomService], + + // This line is different from the original UI plugin. + [IUIController, { useClass: UniverUniUIController }], + + [SharedController, { useFactory: () => this._injector.createInstance(SharedController, this._config) }], + [ErrorController], + [ShortcutPanelController, { useFactory: () => this._injector.createInstance(ShortcutPanelController, this._config) }], + ], this._config.override); + dependencies.forEach((dependency) => injector.add(dependency)); + } + + override onReady(): void { + // We need to run this async to let other modules do their `onReady` jobs first. + setTimeout(() => this._injector.get(IUIController).bootstrapWorkbench(this._config), UI_BOOTSTRAP_DELAY); + } +} diff --git a/packages-experimental/uniui/src/services/unit-grid/unit-grid.service.ts b/packages-experimental/uniui/src/services/unit-grid/unit-grid.service.ts new file mode 100644 index 00000000000..f234c123965 --- /dev/null +++ b/packages-experimental/uniui/src/services/unit-grid/unit-grid.service.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, isInternalEditorID } from '@univerjs/core'; +import type { IRender } from '@univerjs/engine-render'; +import { IRenderManagerService } from '@univerjs/engine-render'; +import { BehaviorSubject } from 'rxjs'; + +export type IUnitGrid = string[]; + +/** + * This services decides which (now at maximum two) units are going to be rendered in the browser. + */ +export class UnitGridService extends Disposable { + // TODO@wzhudev: currently we only support at maximum 2 units rendered side by side. + // In the future we would introduce a grid system (very much like vscode's) to support more + // units and more complex layout. + private _unitGrid: IUnitGrid = []; + private readonly _unitGrid$ = new BehaviorSubject(this._unitGrid); + readonly unitGrid$ = this._unitGrid$.asObservable(); + + constructor( + @IRenderManagerService private readonly _renderSrv: IRenderManagerService + ) { + super(); + + this._init(); + } + + setContainerForRender(unitId: string, element: HTMLElement) { + this._renderSrv.getRenderById(unitId)?.engine.setContainer(element); + } + + override dispose(): void { + super.dispose(); + + this._unitGrid$.complete(); + } + + private _init(): void { + this._renderSrv.getRenderAll().forEach((renderer) => this._onRendererCreated(renderer)); + this.disposeWithMe(this._renderSrv.created$.subscribe((renderer) => this._onRendererCreated(renderer))); + this.disposeWithMe(this._renderSrv.disposed$.subscribe((unitId) => this._onRenderedDisposed(unitId))); + } + + private _onRendererCreated(renderer: IRender): void { + if (isInternalEditorID(renderer.unitId)) { + return; + } + + this._unitGrid.push(renderer.unitId); + this._emitLayoutChange(); + } + + private _onRenderedDisposed(unitId: string): void { + const idx = this._unitGrid.indexOf(unitId); + if (idx !== -1) { + this._unitGrid.splice(idx, 1); + this._emitLayoutChange(); + } + } + + private _emitLayoutChange(): void { + this._unitGrid$.next(this._unitGrid); + } +} diff --git a/packages-experimental/uniui/src/views/workbench/UniWorkbench.tsx b/packages-experimental/uniui/src/views/workbench/UniWorkbench.tsx new file mode 100644 index 00000000000..3ceadd52496 --- /dev/null +++ b/packages-experimental/uniui/src/views/workbench/UniWorkbench.tsx @@ -0,0 +1,209 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Refer to packages/ui/src/views/App.tsx + +import { IUniverInstanceService, LocaleService, ThemeService } from '@univerjs/core'; +import { ConfigProvider, defaultTheme, themeInstance } from '@univerjs/design'; +import type { ILocale } from '@univerjs/design'; +import { + builtInGlobalComponents, + BuiltInUIPart, + ComponentContainer, + ContextMenu, + IMessageService, + type IWorkbenchOptions, + Sidebar, + Toolbar, + useComponentsOfPart, + useObservable, + ZenZone, +} from '@univerjs/ui'; +import { useDependency } from '@wendellhu/redi/react-bindings'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { UnitGridService } from '../../services/unit-grid/unit-grid.service'; +import styles from './workbench.module.less'; + +// Refer to packages/ui/src/views/workbench/Workbench.tsx + +export interface IUniWorkbenchProps extends IWorkbenchOptions { + mountContainer: HTMLElement; + + onRendered: () => void; +} + +export function UniWorkbench(props: IUniWorkbenchProps) { + const { + header = true, + footer = true, + contextMenu = true, + mountContainer, + onRendered, + } = props; + + const localeService = useDependency(LocaleService); + const themeService = useDependency(ThemeService); + const messageService = useDependency(IMessageService); + const unitGridService = useDependency(UnitGridService); + const instanceService = useDependency(IUniverInstanceService); + + const contentRef = useRef(null); + + const footerComponents = useComponentsOfPart(BuiltInUIPart.FOOTER); + const headerComponents = useComponentsOfPart(BuiltInUIPart.HEADER); + const headerMenuComponents = useComponentsOfPart(BuiltInUIPart.HEADER_MENU); + const contentComponents = useComponentsOfPart(BuiltInUIPart.CONTENT); + const leftSidebarComponents = useComponentsOfPart(BuiltInUIPart.LEFT_SIDEBAR); + const globalComponents = useComponentsOfPart(BuiltInUIPart.GLOBAL); + + const unitGrid = useObservable(unitGridService.unitGrid$, undefined, true); + + useEffect(() => { + if (!themeService.getCurrentTheme()) { + themeService.setTheme(defaultTheme); + } + + onRendered(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [locale, setLocale] = useState(localeService.getLocales() as unknown as ILocale); + + // Create a portal container for injecting global component themes. + const portalContainer = useMemo(() => document.createElement('div'), []); + + useEffect(() => { + document.body.appendChild(portalContainer); + messageService.setContainer(portalContainer); + + const subscriptions = [ + localeService.localeChanged$.subscribe(() => { + setLocale(localeService.getLocales() as unknown as ILocale); + }), + themeService.currentTheme$.subscribe((theme) => { + themeInstance.setTheme(mountContainer, theme); + portalContainer && themeInstance.setTheme(portalContainer, theme); + }), + ]; + + return () => { + // batch unsubscribe + subscriptions.forEach((subscription) => subscription.unsubscribe()); + + // cleanup + document.body.removeChild(portalContainer); + }; + }, [localeService, messageService, mountContainer, portalContainer, themeService.currentTheme$]); + + return ( + + {/** + * IMPORTANT! This `tabIndex` should not be moved. This attribute allows the element to catch + * all focusin event merged from its descendants. The DesktopLayoutService would listen to focusin events + * bubbled to this element and refocus the input element. + */} +
e.stopPropagation()}> + {/* header */} + {header && ( +
+ +
+ )} + + {/* content */} +
+
+ + +
+
+ {header && } +
+ +
e.preventDefault()} + > + {/* Render units. */} + {unitGrid?.map((unitId) => ( + + ))} + +
+
+ + +
+ + {/* footer */} + {footer && ( +
+ +
+ )} + + +
+
+ + + {contextMenu && } +
+ ); +} + +interface IUnitRendererProps { + unitId: string; + + gridService: UnitGridService; + instanceService: IUniverInstanceService; +} + +function UnitRenderer(props: IUnitRendererProps) { + const { unitId, instanceService, gridService } = props; + const mountRef = useRef(null); + + const focus = useCallback(() => { + instanceService.focusUnit(unitId); + instanceService.setCurrentUnitForType(unitId); + }, [unitId, instanceService]); + + useEffect(() => { + if (mountRef.current) { + gridService.setContainerForRender(unitId, mountRef.current); + } + }, [gridService, unitId]); + + return ( +
+
+ ); +} diff --git a/packages-experimental/uniui/src/views/workbench/workbench.module.less b/packages-experimental/uniui/src/views/workbench/workbench.module.less new file mode 100644 index 00000000000..ba68faba7fd --- /dev/null +++ b/packages-experimental/uniui/src/views/workbench/workbench.module.less @@ -0,0 +1,70 @@ +.workbench { + &-layout { + display: flex; + flex-direction: column; + height: 100%; + background-color: rgb(var(--bg-color)); + min-height: 0; + position: relative; + } + + &-header { + // height: 32px; + } + + &-container { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + + &-header { + position: relative; + z-index: 10; + width: 100%; + } + + &-wrapper { + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-rows: 100%; + height: 100%; + overflow: hidden; + } + + &-content { + position: relative; + + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; + flex: 1; + + background-color: rgb(var(--bg-color-secondary)); + border-bottom: 1px solid rgb(var(--border-color)); + } + + &-canvas-container { + display: flex; + } + + &-canvas { + flex: 1; + position: relative; + overflow: hidden; + } + + &-left-sidebar { + height: 100%; + } + + &-sidebar { + height: 100%; + } + } + + &-footer { + // height: 36px; + } +} diff --git a/packages-experimental/uniui/src/vite-env.d.ts b/packages-experimental/uniui/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages-experimental/uniui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages-experimental/uniui/tsconfig.json b/packages-experimental/uniui/tsconfig.json new file mode 100644 index 00000000000..d676ad2a20d --- /dev/null +++ b/packages-experimental/uniui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@univerjs/shared/tsconfigs/base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src"] +} diff --git a/packages-experimental/uniui/tsconfig.node.json b/packages-experimental/uniui/tsconfig.node.json new file mode 100644 index 00000000000..e53dac88688 --- /dev/null +++ b/packages-experimental/uniui/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@univerjs/shared/tsconfigs/node", + "include": ["vite.config.ts"] +} diff --git a/packages-experimental/uniui/vite.config.ts b/packages-experimental/uniui/vite.config.ts new file mode 100644 index 00000000000..925b530b4d5 --- /dev/null +++ b/packages-experimental/uniui/vite.config.ts @@ -0,0 +1,12 @@ +import createViteConfig from '@univerjs/shared/vite'; +import pkg from './package.json'; + +export default ({ mode }) => createViteConfig({}, { + mode, + pkg, + features: { + react: false, + css: true, + dom: true, + }, +}); diff --git a/packages/docs-ui/src/controllers/doc-ui.controller.ts b/packages/docs-ui/src/controllers/doc-ui.controller.ts index 9fb24de79e8..a1f2ac2add6 100644 --- a/packages/docs-ui/src/controllers/doc-ui.controller.ts +++ b/packages/docs-ui/src/controllers/doc-ui.controller.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import type { DocumentDataModel } from '@univerjs/core'; -import { Disposable, IUniverInstanceService, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core'; +import { Disposable, LifecycleStages, OnLifecycle, UniverInstanceType } from '@univerjs/core'; import type { IMenuItemFactory } from '@univerjs/ui'; -import { BuiltInUIPart, ComponentManager, IEditorService, ILayoutService, IMenuService, IUIPartsService } from '@univerjs/ui'; +import { ComponentManager, ILayoutService, IMenuService } from '@univerjs/ui'; import { Inject, Injector } from '@wendellhu/redi'; -import { connectInjector } from '@wendellhu/redi/react-bindings'; import { ITextSelectionRenderManager } from '@univerjs/engine-render'; import { COLOR_PICKER_COMPONENT, ColorPicker } from '../components/color-picker'; import { @@ -30,7 +28,6 @@ import { FontFamilyItem, } from '../components/font-family'; import { FONT_SIZE_COMPONENT, FontSize } from '../components/font-size'; -import { DocBackground } from '../views/doc-background/DocBackground'; import type { IUniverDocsUIConfig } from '../basics'; import { AlignCenterMenuItemFactory, @@ -60,10 +57,7 @@ export class DocUIController extends Disposable { @Inject(Injector) private readonly _injector: Injector, @Inject(ComponentManager) private readonly _componentManager: ComponentManager, @ILayoutService private readonly _layoutService: ILayoutService, - @IEditorService private readonly _editorService: IEditorService, - @IMenuService private readonly _menuService: IMenuService, - @IUIPartsService private readonly _uiPartsService: IUIPartsService, - @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService + @IMenuService private readonly _menuService: IMenuService ) { super(); @@ -110,24 +104,9 @@ export class DocUIController extends Disposable { private _init(): void { this._initCustomComponents(); this._initMenus(); - this._initDocBackground(); this._initFocusHandler(); } - private _initDocBackground() { - const firstDocUnitId = this._univerInstanceService.getAllUnitsForType(UniverInstanceType.UNIVER_DOC)[0].getUnitId(); - if (firstDocUnitId == null) { - return; - } - - const embedded = this._editorService.isEditor(firstDocUnitId); - if (!embedded) { - this.disposeWithMe( - this._uiPartsService.registerComponent(BuiltInUIPart.CONTENT, () => connectInjector(DocBackground, this._injector)) - ); - } - } - private _initFocusHandler(): void { this.disposeWithMe( this._layoutService.registerFocusHandler(UniverInstanceType.UNIVER_DOC, () => { diff --git a/packages/docs-ui/src/views/doc-background/index.module.less b/packages/docs-ui/src/views/doc-background/index.module.less deleted file mode 100644 index f9d6c502944..00000000000 --- a/packages/docs-ui/src/views/doc-background/index.module.less +++ /dev/null @@ -1,11 +0,0 @@ -@size: 100%; - -.docs-ui-canvas-background { - position: relative; - width: @size; - height: @size; - background: rgb(var(--grey-50)); //rgb(233,238,242) - z-index: 1; - user-select: none; - pointer-events: none; -} diff --git a/packages/engine-render/src/engine.ts b/packages/engine-render/src/engine.ts index d4e399ee6ae..bfb1b4b02ad 100644 --- a/packages/engine-render/src/engine.ts +++ b/packages/engine-render/src/engine.ts @@ -171,6 +171,10 @@ export class Engine extends ThinEngine { } setContainer(elem: HTMLElement, resize = true) { + if (this._container === elem) { + return; + } + this._container = elem; this._container.appendChild(this.getCanvasElement()); diff --git a/packages/engine-render/src/render-manager/render-manager.service.ts b/packages/engine-render/src/render-manager/render-manager.service.ts index c0a8f0ce3ac..1a8ea011a5d 100644 --- a/packages/engine-render/src/render-manager/render-manager.service.ts +++ b/packages/engine-render/src/render-manager/render-manager.service.ts @@ -34,6 +34,7 @@ export type RenderComponentType = SheetComponent | DocComponent | Slide | BaseOb export interface IRenderManagerService extends IDisposable { /** @deprecated */ currentRender$: Observable>; + addRender(unitId: string, renderer: IRender): void; createRender(unitId: string): IRender; removeRender(unitId: string): void; @@ -52,6 +53,9 @@ export interface IRenderManagerService extends IDisposable { /** @deprecated this design is very very weird! Remove it. */ create(unitId: string): void; + created$: Observable; + disposed$: Observable; + /** @deprecated There will be multi units to render at the same time, so there is no *current*. */ getCurrent(): Nullable; /** @deprecated There will be multi units to render at the same time, so there is no *first*. */ @@ -80,6 +84,12 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe /** @deprecated */ readonly createRender$ = this._createRender$.asObservable(); + private readonly _renderCreated$ = new Subject(); + readonly created$ = this._renderCreated$.asObservable(); + + private readonly _renderDisposed$ = new Subject(); + readonly disposed$ = this._renderDisposed$.asObservable(); + get defaultEngine() { if (!this._defaultEngine) { this._defaultEngine = new Engine(); @@ -103,6 +113,9 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe this._renderControllers.clear(); this._renderMap.clear(); this._currentRender$.complete(); + + this._renderCreated$.complete(); + this._renderDisposed$.complete(); } registerRenderModule(type: UnitType, ctor: IRenderModuleCtor): IDisposable { @@ -132,7 +145,9 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe } createRender(unitId: string): IRender { - return this._createRender(unitId, new Engine()); + const renderer = this._createRender(unitId, new Engine()); + this._renderCreated$.next(renderer); + return renderer; } withCurrentTypeOfUnit(type: UniverInstanceType, id: DependencyIdentifier): Nullable { @@ -168,7 +183,7 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe if (unit) { const type = this._univerInstanceService.getUnitType(unitId); const ctors = this._getRenderControllersForType(type); - renderUnit = new RenderUnit(this._injector, { + renderUnit = this._injector.createInstance(RenderUnit, { unit, engine, scene, @@ -257,6 +272,8 @@ export class RenderManagerService extends Disposable implements IRenderManagerSe if (shouldDestroyEngine) { engine.dispose(); } + + this._renderDisposed$.next(item.unitId); } } diff --git a/packages/engine-render/src/render-manager/render-unit.ts b/packages/engine-render/src/render-manager/render-unit.ts index d4eef12ea5d..f1996c7cca8 100644 --- a/packages/engine-render/src/render-manager/render-unit.ts +++ b/packages/engine-render/src/render-manager/render-unit.ts @@ -15,8 +15,9 @@ */ import type { Nullable, UnitModel, UnitType } from '@univerjs/core'; -import { Disposable } from '@univerjs/core'; -import type { DependencyIdentifier, IDisposable, Injector } from '@wendellhu/redi'; +import { Disposable, ILogService, IUniverInstanceService } from '@univerjs/core'; +import type { DependencyIdentifier, IDisposable } from '@wendellhu/redi'; +import { Inject, Injector } from '@wendellhu/redi'; import type { Engine } from '../engine'; import type { Scene } from '../scene'; import type { RenderComponentType } from './render-manager.service'; @@ -68,15 +69,16 @@ export class RenderUnit extends Disposable implements IRender { get components() { return this._renderContext.components; } constructor( - parentInjector: Injector, - init: Pick + init: Pick, + @Inject(Injector) parentInjector: Injector, + @IUniverInstanceService private readonly _instanceSrv: IUniverInstanceService, + @ILogService private readonly _logSrv: ILogService ) { super(); this._injector = parentInjector.createChild(); - this._renderContext = { - unit: init.unit, // model + unit: init.unit, unitId: init.unit.getUnitId(), type: init.unit.type, components: new Map(), @@ -89,7 +91,6 @@ export class RenderUnit extends Disposable implements IRender { override dispose() { this._injector.dispose(); - super.dispose(); } diff --git a/packages/ui/src/controllers/ui/ui-desktop.controller.tsx b/packages/ui/src/controllers/ui/ui-desktop.controller.tsx index 25761b71615..705e6cdcfc0 100644 --- a/packages/ui/src/controllers/ui/ui-desktop.controller.tsx +++ b/packages/ui/src/controllers/ui/ui-desktop.controller.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Disposable, isInternalEditorID, IUniverInstanceService, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core'; +import { Disposable, isInternalEditorID, LifecycleService, LifecycleStages, toDisposable } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; import type { IDisposable } from '@wendellhu/redi'; import { Inject, Injector, Optional } from '@wendellhu/redi'; @@ -24,7 +24,7 @@ import React from 'react'; import { delay, filter, take } from 'rxjs'; import { ILayoutService } from '../../services/layout/layout.service'; -import { App } from '../../views/App'; +import { Workbench } from '../../views/workbench/Workbench'; import { BuiltInUIPart, IUIPartsService } from '../../services/parts/parts.service'; import { CanvasPopup } from '../../views/components/popup/CanvasPopup'; import { FloatDom } from '../../views/components/dom/FloatDom'; @@ -34,7 +34,6 @@ const STEADY_TIMEOUT = 3000; export class DesktopUIController extends Disposable { constructor( - @IUniverInstanceService private readonly _instanceService: IUniverInstanceService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @Inject(Injector) private readonly _injector: Injector, @Inject(LifecycleService) private readonly _lifecycleService: LifecycleService, @@ -105,7 +104,7 @@ function bootstrap( mountContainer = createContainer('univer'); } - const ConnectedApp = connectInjector(App, injector); + const ConnectedApp = connectInjector(Workbench, injector); const onRendered = (canvasElement: HTMLElement) => callback(canvasElement, mountContainer); function render() { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 395a613a2e4..16acaa1746f 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -27,7 +27,6 @@ export { SharedController, UndoShortcutItem, } from './controllers/shared-shortcut.controller'; -export { IUIController, type IWorkbenchOptions } from './controllers/ui/ui.controller'; export { DesktopUIController } from './controllers/ui/ui-desktop.controller'; export { IUIPartsService, BuiltInUIPart, UIPartsService } from './services/parts/parts.service'; export { DesktopBeforeCloseService, IBeforeCloseService } from './services/before-close/before-close.service'; @@ -77,7 +76,7 @@ export { DesktopShortcutService, type IShortcutItem, IShortcutService } from './ export { DesktopSidebarService } from './services/sidebar/desktop-sidebar.service'; export { ISidebarService } from './services/sidebar/sidebar.service'; export { IZenZoneService } from './services/zen-zone/zen-zone.service'; -export { UniverUIPlugin, DISABLE_AUTO_FOCUS_KEY } from './ui-plugin'; +export { UNIVER_UI_PLUGIN_NAME, UniverUIPlugin, DISABLE_AUTO_FOCUS_KEY } from './ui-plugin'; export * from './utils'; export { type IConfirmPartMethodOptions } from './views/components/confirm-part/interface'; export { ComponentContainer, useComponentsOfPart, type IComponentContainerProps } from './views/components/ComponentContainer'; @@ -85,14 +84,11 @@ export { IEditorService, EditorService } from './services/editor/editor.service' export { TextEditor } from './components/editor/TextEditor'; export { SetEditorResizeOperation } from './commands/operations/editor/set-editor-resize.operation'; export { RangeSelector } from './components/range-selector/RangeSelector'; -export { IRangeSelectorService } from './services/range-selector/range-selector.service'; -export { DesktopLocalStorageService } from './services/local-storage/local-storage.service'; -export { CanvasPopupService, ICanvasPopupService, type IPopup } from './services/popup/canvas-popup.service'; export { ProgressBar } from './components/progress-bar/ProgressBar'; export { type IMenuGroup, useToolbarGroups, useToolbarItemStatus, useToolbarCollapseObserver } from './views/components/doc-bars/hook'; +export { Toolbar } from './views/components/doc-bars/Toolbar'; +export { FloatDom } from './views/components/dom/FloatDom'; export { mergeMenuConfigs } from './common/menu-merge-configs'; -export { IProgressService, ProgressService } from './services/progress/progress.service'; -export type { IProgressStep } from './services/progress/progress.service'; // #region - workbench components @@ -106,3 +102,20 @@ export { CanvasFloatDomService, type IFloatDomLayout } from './services/dom/canv // #endregion +// #region - controllers + +export { ErrorController } from './controllers/error/error.controller'; +export { ShortcutPanelController } from './controllers/shortcut-display/shortcut-panel.controller'; +export { IUIController, type IUniverUIConfig, type IWorkbenchOptions } from './controllers/ui/ui.controller'; + +// #endregion + +// #region - services + +export { RangeSelectorService, IRangeSelectorService } from './services/range-selector/range-selector.service'; +export { ShortcutPanelService } from './services/shortcut/shortcut-panel.service'; +export { DesktopLocalStorageService } from './services/local-storage/local-storage.service'; +export { CanvasPopupService, ICanvasPopupService, type IPopup } from './services/popup/canvas-popup.service'; +export { IProgressService, ProgressService } from './services/progress/progress.service'; +export type { IProgressStep } from './services/progress/progress.service'; +export { DesktopZenZoneService } from './services/zen-zone/desktop-zen-zone.service'; diff --git a/packages/ui/src/ui-plugin.ts b/packages/ui/src/ui-plugin.ts index 6fafa065398..0790eb6484d 100644 --- a/packages/ui/src/ui-plugin.ts +++ b/packages/ui/src/ui-plugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DependentOn, IContextService, ILocalStorageService, LocaleService, mergeOverrideWithDependencies, Plugin, Tools } from '@univerjs/core'; +import { DependentOn, IContextService, ILocalStorageService, mergeOverrideWithDependencies, Plugin, Tools } from '@univerjs/core'; import type { Dependency } from '@wendellhu/redi'; import { Inject, Injector } from '@wendellhu/redi'; import { UniverRenderEnginePlugin } from '@univerjs/engine-render'; @@ -57,49 +57,36 @@ import { IProgressService, ProgressService } from './services/progress/progress. import { IUIPartsService, UIPartsService } from './services/parts/parts.service'; import { CanvasFloatDomService } from './services/dom/canvas-dom-layer.service'; -const PLUGIN_NAME = 'ui'; - -export const DefaultUiConfig = {}; - +export const UNIVER_UI_PLUGIN_NAME = 'UNIVER_UI_PLUGIN'; export const DISABLE_AUTO_FOCUS_KEY = 'DISABLE_AUTO_FOCUS'; +const UI_BOOTSTRAP_DELAY = 16; + /** * UI plugin provides basic interaction with users. Including workbench (menus, UI parts, notifications etc.), copy paste, shortcut. */ @DependentOn(UniverRenderEnginePlugin) export class UniverUIPlugin extends Plugin { - static override pluginName = PLUGIN_NAME; + static override pluginName = UNIVER_UI_PLUGIN_NAME; constructor( private _config: Partial = {}, @IContextService private readonly _contextService: IContextService, - @Inject(Injector) protected readonly _injector: Injector, - @Inject(LocaleService) private readonly _localeService: LocaleService + @Inject(Injector) protected readonly _injector: Injector ) { super(); - this._config = Tools.deepMerge({}, DefaultUiConfig, this._config); - + this._config = Tools.deepMerge({}, this._config); if (this._config.disableAutoFocus) { this._contextService.setContextValue(DISABLE_AUTO_FOCUS_KEY, true); } } - override onStarting(_injector: Injector): void { - this._initDependencies(_injector); - } - - override onReady(): void { - // TODO@Jocs: this has to be on Ready hook because of editor related modules' sequence problem. - this._initUI(); - } - - private _initDependencies(injector: Injector): void { + override onStarting(injector: Injector): void { const dependencies: Dependency[] = mergeOverrideWithDependencies([ [ComponentManager], [ZIndexManager], - // services [ShortcutPanelService], [IUIPartsService, { useClass: UIPartsService }], [ILayoutService, { useClass: DesktopLayoutService }], @@ -122,28 +109,17 @@ export class UniverUIPlugin extends Plugin { [ICanvasPopupService, { useClass: CanvasPopupService }], [IProgressService, { useClass: ProgressService }], [CanvasFloatDomService], - // controllers + [IUIController, { useClass: DesktopUIController }], - [ - SharedController, - { - useFactory: () => this._injector.createInstance(SharedController, this._config), - }, - ], + [SharedController, { useFactory: () => this._injector.createInstance(SharedController, this._config) }], [ErrorController], - [ - ShortcutPanelController, - { - useFactory: () => this._injector.createInstance(ShortcutPanelController, this._config), - }, - ], + [ShortcutPanelController, { useFactory: () => this._injector.createInstance(ShortcutPanelController, this._config) }], ], this._config.override); - dependencies.forEach((dependency) => injector.add(dependency)); } - private _initUI(): void { + override onReady(): void { // We need to run this async to let other modules do their `onReady` jobs first. - setTimeout(() => this._injector.get(IUIController).bootstrapWorkbench(this._config), 16); + setTimeout(() => this._injector.get(IUIController).bootstrapWorkbench(this._config), UI_BOOTSTRAP_DELAY); } } diff --git a/packages/ui/src/views/App.tsx b/packages/ui/src/views/workbench/Workbench.tsx similarity index 78% rename from packages/ui/src/views/App.tsx rename to packages/ui/src/views/workbench/Workbench.tsx index c87e0f6dd7a..6bedf71537c 100644 --- a/packages/ui/src/views/App.tsx +++ b/packages/ui/src/views/workbench/Workbench.tsx @@ -19,24 +19,25 @@ import type { ILocale } from '@univerjs/design'; import { ConfigProvider, defaultTheme, themeInstance } from '@univerjs/design'; import { useDependency } from '@wendellhu/redi/react-bindings'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import type { IWorkbenchOptions } from '../controllers/ui/ui.controller'; -import { IMessageService } from '../services/message/message.service'; -import { BuiltInUIPart } from '../services/parts/parts.service'; -import styles from './app.module.less'; -import { ComponentContainer, useComponentsOfPart } from './components/ComponentContainer'; -import { Toolbar } from './components/doc-bars/Toolbar'; -import { Sidebar } from './components/sidebar/Sidebar'; -import { ZenZone } from './components/zen-zone/ZenZone'; -import { builtInGlobalComponents } from './parts'; -import { ContextMenu } from './components/context-menu/ContextMenu'; - -export interface IUniverAppProps extends IWorkbenchOptions { +import type { IWorkbenchOptions } from '../../controllers/ui/ui.controller'; +import { IMessageService } from '../../services/message/message.service'; +import { BuiltInUIPart } from '../../services/parts/parts.service'; +import { ComponentContainer, useComponentsOfPart } from '../components/ComponentContainer'; +import { Toolbar } from '../components/doc-bars/Toolbar'; +import { Sidebar } from '../components/sidebar/Sidebar'; +import { ZenZone } from '../components/zen-zone/ZenZone'; +import { builtInGlobalComponents } from '../parts'; +import { ContextMenu } from '../components/context-menu/ContextMenu'; + +import styles from './workbench.module.less'; + +export interface IUniverWorkbenchProps extends IWorkbenchOptions { mountContainer: HTMLElement; onRendered?: (container: HTMLElement) => void; } -export function App(props: IUniverAppProps) { +export function Workbench(props: IUniverWorkbenchProps) { const { header = true, footer = true, @@ -105,28 +106,28 @@ export function App(props: IUniverAppProps) { * all focusin event merged from its descendants. The DesktopLayoutService would listen to focusin events * bubbled to this element and refocus the input element. */} -
e.stopPropagation()}> +
e.stopPropagation()}> {/* header */} {header && ( -
+
)} {/* content */} -
-
-