feat(ui): add DesktopRibbonService and integrate with Ribbon componenta (#6428)

This commit is contained in:
白熱
2026-01-07 15:53:11 +08:00
committed by GitHub
parent 8020985b8b
commit f67b413cde
4 changed files with 187 additions and 113 deletions
+1
View File
@@ -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';
+2
View File
@@ -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 }],
@@ -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<IRibbonService>('univer.ribbon-service');
export interface IRibbonService {
ribbon$: Observable<IMenuSchema[]>;
activatedTab$: Observable<string>;
collapsedIds$: Observable<string[]>;
fakeToolbarVisible$: Observable<boolean>;
setActivatedTab(tab: string): void;
setCollapsedIds(ids: string[]): void;
setFakeToolbarVisible(visible: boolean): void;
}
export class DesktopRibbonService extends Disposable implements IRibbonService {
private readonly _ribbon$ = new BehaviorSubject<IMenuSchema[]>([]);
readonly ribbon$ = this._ribbon$.asObservable();
private readonly _activatedTab$ = new BehaviorSubject<string>(RibbonPosition.START);
readonly activatedTab$ = this._activatedTab$.asObservable();
private readonly _collapsedIds$ = new BehaviorSubject<string[]>([]);
readonly collapsedIds$ = this._collapsedIds$.asObservable();
private readonly _fakeToolbarVisible$ = new BehaviorSubject<boolean>(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<boolean>[] = [];
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();
}
}
@@ -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<HTMLDivElement>(null!);
const toolbarItemRefs = useRef<Record<string, {
el: HTMLElement;
@@ -68,99 +51,31 @@ export function Ribbon(props: IRibbonProps) {
itemOrder: number;
}>>({});
const [ribbon, setRibbon] = useState<IMenuSchema[]>([]);
const [activatedTab, setActivatedTab] = useState<string>(RibbonPosition.START);
const [collapsedIds, setCollapsedIds] = useState<string[]>([]);
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<boolean>[] = [];
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));