diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 963f0e01a6..20a067bd8a 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -113,6 +113,7 @@ export { INotificationService } from './services/notification/notification.servi export { BuiltInUIPart, IUIPartsService, UIPartsService } from './services/parts/parts.service'; export { IPlatformService, PlatformService } from './services/platform/platform.service'; export { CanvasPopupService, ICanvasPopupService, type IPopup } from './services/popup/canvas-popup.service'; +export { DesktopRibbonService, IRibbonService } from './services/ribbon/ribbon.service'; export { KeyCode, MetaKeys } from './services/shortcut/keycode'; export { ShortcutPanelService } from './services/shortcut/shortcut-panel.service'; export { type IShortcutItem, IShortcutService, ShortcutService } from './services/shortcut/shortcut.service'; diff --git a/packages/ui/src/plugin.ts b/packages/ui/src/plugin.ts index f004d6d537..8acfe8fcd1 100644 --- a/packages/ui/src/plugin.ts +++ b/packages/ui/src/plugin.ts @@ -49,6 +49,7 @@ import { INotificationService } from './services/notification/notification.servi import { IUIPartsService, UIPartsService } from './services/parts/parts.service'; import { IPlatformService, PlatformService } from './services/platform/platform.service'; import { CanvasPopupService, ICanvasPopupService } from './services/popup/canvas-popup.service'; +import { DesktopRibbonService, IRibbonService } from './services/ribbon/ribbon.service'; import { ShortcutPanelService } from './services/shortcut/shortcut-panel.service'; import { IShortcutService, ShortcutService } from './services/shortcut/shortcut.service'; import { DesktopSidebarService } from './services/sidebar/desktop-sidebar.service'; @@ -102,6 +103,7 @@ export class UniverUIPlugin extends Plugin { [ShortcutPanelService], [IUIPartsService, { useClass: UIPartsService }], [ILayoutService, { useClass: DesktopLayoutService }], + [IRibbonService, { useClass: DesktopRibbonService }], [IShortcutService, { useClass: ShortcutService }], [IPlatformService, { useClass: PlatformService }], [IMenuManagerService, { useClass: MenuManagerService }], diff --git a/packages/ui/src/services/ribbon/ribbon.service.ts b/packages/ui/src/services/ribbon/ribbon.service.ts new file mode 100644 index 0000000000..267d7eb760 --- /dev/null +++ b/packages/ui/src/services/ribbon/ribbon.service.ts @@ -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 { Observable } from 'rxjs'; +import type { IMenuSchema } from '../menu/menu-manager.service'; +import { createIdentifier, Disposable, IUniverInstanceService } from '@univerjs/core'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { startWith } from 'rxjs/operators'; +import { IMenuManagerService } from '../menu/menu-manager.service'; +import { MenuManagerPosition, RibbonPosition } from '../menu/types'; + +export const IRibbonService = createIdentifier('univer.ribbon-service'); + +export interface IRibbonService { + ribbon$: Observable; + activatedTab$: Observable; + collapsedIds$: Observable; + fakeToolbarVisible$: Observable; + + setActivatedTab(tab: string): void; + setCollapsedIds(ids: string[]): void; + setFakeToolbarVisible(visible: boolean): void; +} + +export class DesktopRibbonService extends Disposable implements IRibbonService { + private readonly _ribbon$ = new BehaviorSubject([]); + readonly ribbon$ = this._ribbon$.asObservable(); + + private readonly _activatedTab$ = new BehaviorSubject(RibbonPosition.START); + readonly activatedTab$ = this._activatedTab$.asObservable(); + + private readonly _collapsedIds$ = new BehaviorSubject([]); + readonly collapsedIds$ = this._collapsedIds$.asObservable(); + + private readonly _fakeToolbarVisible$ = new BehaviorSubject(false); + readonly fakeToolbarVisible$ = this._fakeToolbarVisible$.asObservable(); + + constructor( + @IMenuManagerService private readonly _menuManagerService: IMenuManagerService, + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService + ) { + super(); + this._initRibbonSubscription(); + } + + setActivatedTab(tab: string): void { + this._activatedTab$.next(tab); + } + + setCollapsedIds(ids: string[]): void { + this._collapsedIds$.next(ids); + } + + setFakeToolbarVisible(visible: boolean): void { + this._fakeToolbarVisible$.next(visible); + } + + private _initRibbonSubscription(): void { + this.disposeWithMe( + combineLatest([ + this._menuManagerService.menuChanged$.pipe(startWith(undefined)), + this._univerInstanceService.focused$.pipe(startWith(undefined)), + ]).subscribe(() => { + this._updateRibbon(); + }) + ); + } + + private _updateRibbon() { + const ribbon = this._menuManagerService.getMenuByPositionKey(MenuManagerPosition.RIBBON); + + // Collect all hidden$ Observables and their corresponding paths + const hiddenObservableMap: Observable[] = []; + const hiddenKeyMap: string[] = []; + for (const group of ribbon) { + if (group.children) { + for (const item of group.children) { + if (item.children) { + for (const child of item.children) { + if (child.item?.hidden$) { + hiddenObservableMap.push(child.item.hidden$); + hiddenKeyMap.push(child.key); + } + } + } + } + } + } + + if (hiddenObservableMap.length === 0) { + this._ribbon$.next(ribbon); + return; + } + + // Only get the current value once, not continuously subscribe + combineLatest(hiddenObservableMap) + .pipe(startWith(new Array(hiddenObservableMap.length).fill(false))) + .subscribe((hiddenMap) => { + const newRibbon: IMenuSchema[] = []; + + const hiddenPathMap = hiddenMap.map((hidden, index) => { + if (hidden) { + return hiddenKeyMap[index]; + } + return null; + }).filter((item) => !!item) as string[]; + + for (const group of ribbon) { + const newGroup: IMenuSchema = { ...group, children: [] }; + + if (group.children?.length) { + for (const item of group.children) { + const newItem: IMenuSchema = { ...item, children: [] }; + let shouldAddItem = true; + + if (item.children?.length) { + for (const child of item.children) { + if (!hiddenPathMap.includes(child.key)) { + newItem.children?.push(child); + } + } + + if (newItem.children?.every((child) => child.children?.length === 0)) { + shouldAddItem = false; + } + } + + if (shouldAddItem) { + newGroup.children?.push(newItem); + } + } + } + + if (newGroup.children?.length && newGroup.children.every((item) => item.children?.length)) { + newRibbon.push(newGroup); + } + } + + this._ribbon$.next(newRibbon); + }) + .unsubscribe(); + } +} diff --git a/packages/ui/src/views/components/ribbon/Ribbon.tsx b/packages/ui/src/views/components/ribbon/Ribbon.tsx index 38157a6dee..63cf4dd185 100644 --- a/packages/ui/src/views/components/ribbon/Ribbon.tsx +++ b/packages/ui/src/views/components/ribbon/Ribbon.tsx @@ -15,16 +15,14 @@ */ import type { ComponentType } from 'react'; -import type { Observable } from 'rxjs'; import type { RibbonType } from '../../../controllers/ui/ui.controller'; import type { IMenuSchema } from '../../../services/menu/menu-manager.service'; -import { IUniverInstanceService, LocaleService, throttle } from '@univerjs/core'; +import { LocaleService, throttle } from '@univerjs/core'; import { borderBottomClassName, clsx, divideXClassName, Dropdown } from '@univerjs/design'; import { MoreFunctionIcon } from '@univerjs/icons'; -import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { combineLatest } from 'rxjs'; -import { IMenuManagerService } from '../../../services/menu/menu-manager.service'; -import { MenuManagerPosition, RibbonPosition } from '../../../services/menu/types'; +import { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'; +import { RibbonPosition } from '../../../services/menu/types'; +import { IRibbonService } from '../../../services/ribbon/ribbon.service'; import { useDependency, useObservable } from '../../../utils/di'; import { ComponentContainer } from '../ComponentContainer'; import { ClassicMenu } from './ribbon-menu/ClassicMenu'; @@ -41,24 +39,9 @@ interface IRibbonProps { export function Ribbon(props: IRibbonProps) { const { ribbonType, headerMenuComponents, headerMenu = true } = props; - const menuManagerService = useDependency(IMenuManagerService); - const univerInstanceService = useDependency(IUniverInstanceService); + const ribbonService = useDependency(IRibbonService); const localeService = useDependency(LocaleService); - const [menuChangedTimes, setMenuChangedTimes] = useState(0); - - const focusedUnit = useObservable(univerInstanceService.focused$); - - useEffect(() => { - const subscription = menuManagerService.menuChanged$.subscribe(() => { - setMenuChangedTimes((prev) => prev + 1); - }); - - return () => { - subscription.unsubscribe(); - }; - }, []); - const containerRef = useRef(null!); const toolbarItemRefs = useRef>({}); - const [ribbon, setRibbon] = useState([]); - const [activatedTab, setActivatedTab] = useState(RibbonPosition.START); - const [collapsedIds, setCollapsedIds] = useState([]); - const [fakeToolbarVisible, setFakeToolbarVisible] = useState(false); + const ribbonData = useObservable(ribbonService.ribbon$, []); + const activatedTab = useObservable(ribbonService.activatedTab$, RibbonPosition.START); + const collapsedIds = useObservable(ribbonService.collapsedIds$, []); + const fakeToolbarVisible = useObservable(ribbonService.fakeToolbarVisible$, false); + + const ribbon = useMemo(() => { + if (ribbonType === 'simple') { + const simpleRibbon: IMenuSchema[] = [{ key: RibbonPosition.START, children: [], order: 0 }]; + ribbonData.forEach((group) => { + group.children?.forEach((item) => { + simpleRibbon[0].children?.push(item); + }); + }); + + return simpleRibbon; + } + + return ribbonData; + }, [ribbonType, ribbonData]); const handleSelectTab = useCallback((group: IMenuSchema) => { toolbarItemRefs.current = {}; - setActivatedTab(group.key); + ribbonService.setActivatedTab(group.key); }, []); - // process menu changes - useEffect(() => { - const ribbon = menuManagerService.getMenuByPositionKey(MenuManagerPosition.RIBBON); - - // Collect all hidden$ Observables and their corresponding paths - const hiddenObservableMap: Observable[] = []; - const hiddenKeyMap: string[] = []; - for (const group of ribbon) { - if (group.children) { - for (const item of group.children) { - if (item.children) { - for (const child of item.children) { - if (child.item?.hidden$) { - hiddenObservableMap.push(child.item.hidden$); - hiddenKeyMap.push(`${group.key}/${item.key}/${child.key}`); - } - } - } - } - } - } - - // Only get the current value once, not continuously subscribe - combineLatest(hiddenObservableMap) - .subscribe((hiddenMap) => { - const newRibbon: IMenuSchema[] = []; - - const hiddenPathMap = hiddenMap.map((hidden, index) => { - if (hidden) { - return hiddenKeyMap[index]; - } - return null; - }).filter((item) => !!item) as string[]; - - for (const group of ribbon) { - const newGroup: IMenuSchema = { ...group, children: [] }; - - if (group.children?.length) { - for (const item of group.children) { - const newItem: IMenuSchema = { ...item, children: [] }; - let shouldAddItem = true; - - if (item.children?.length) { - for (const child of item.children) { - const path = `${group.key}/${item.key}/${child.key}`; - - if (!hiddenPathMap.includes(path)) { - newItem.children?.push(child); - } - } - - if (newItem.children?.every((child) => child.children?.length === 0)) { - shouldAddItem = false; - } - } - - if (shouldAddItem) { - newGroup.children?.push(newItem); - } - } - } - - if (newGroup.children?.length && newGroup.children.every((item) => item.children?.length)) { - newRibbon.push(newGroup); - } - } - - if (ribbonType === 'simple') { - const simpleRibbon: IMenuSchema[] = [{ key: RibbonPosition.START, children: [], order: 0 }]; - newRibbon.forEach((group) => { - group.children?.forEach((item) => { - simpleRibbon[0].children?.push(item); - }); - }); - - setRibbon(simpleRibbon); - } else { - setRibbon(newRibbon); - } - }) - .unsubscribe(); - }, [menuChangedTimes, focusedUnit]); - const activeGroup = useMemo(() => { const allGroups = ribbon.find((group) => group.key === activatedTab)?.children ?? []; const visibleGroups: IMenuSchema[] = []; @@ -196,7 +111,7 @@ export function Ribbon(props: IRibbonProps) { let timer: number | null = null; const observer = new ResizeObserver(throttle((entries) => { for (const entry of entries) { - setFakeToolbarVisible(true); + ribbonService.setFakeToolbarVisible(true); timer = requestAnimationFrame(() => { const { width: avaliableWidth } = entry.contentRect; @@ -221,9 +136,9 @@ export function Ribbon(props: IRibbonProps) { } } - setCollapsedIds(newCollapsedIds); + ribbonService.setCollapsedIds(newCollapsedIds); - setFakeToolbarVisible(false); + ribbonService.setFakeToolbarVisible(false); }); } }, 100));