feat(docs-quick-insert-ui): implement quick insert menu functionality (#4983)

This commit is contained in:
ZhangWei
2025-04-10 16:02:39 +08:00
committed by GitHub
parent 3478094e61
commit d5dbaa1d46
10 changed files with 190 additions and 24 deletions
+10 -2
View File
@@ -22,9 +22,17 @@ import { ConfigContext } from '../config-provider/ConfigProvider';
import { Tooltip } from '../tooltip';
import styles from './index.module.less';
export const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
export const Menu = React.forwardRef<MenuRef, MenuProps & { wrapperClass?: string }>((props, ref) => {
const { mountContainer } = useContext(ConfigContext);
return mountContainer && <RcMenu ref={ref} prefixCls={clsx(styles.menu, props.className)} getPopupContainer={() => mountContainer} {...props} />;
return mountContainer && (
<RcMenu
ref={ref}
prefixCls={clsx(styles.menu, props.className)}
getPopupContainer={() => mountContainer}
{...props}
className={props.wrapperClass}
/>
);
});
export function MenuItem(props: MenuItemProps) {
@@ -0,0 +1,69 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DocumentDataModel, INeedCheckDisposable, Nullable } from '@univerjs/core';
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
import { Disposable, Inject } from '@univerjs/core';
import { DocCanvasPopManagerService, DocEventManagerService } from '@univerjs/docs-ui';
import { combineLatest } from 'rxjs';
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
import { QuickInsertButton } from '../views/menu';
export class DocQuickInsertMenuController extends Disposable implements IRenderModule {
private _popup: Nullable<{ startIndex: number; disposable: INeedCheckDisposable }> = null;
constructor(
private _context: IRenderContext<DocumentDataModel>,
@Inject(DocEventManagerService) private _docEventManagerService: DocEventManagerService,
@Inject(DocQuickInsertPopupService) private _docQuickInsertPopupService: DocQuickInsertPopupService,
@Inject(DocCanvasPopManagerService) private _docCanvasPopManagerService: DocCanvasPopManagerService
) {
super();
this._init();
}
private _init() {
this.disposeWithMe(combineLatest([this._docEventManagerService.hoverParagraphLeft$, this._docEventManagerService.hoverParagraph$]).subscribe(([left, paragraph]) => {
const p = left ?? paragraph;
if (!p) {
this._hideMenu(true);
return;
}
if (p.paragraphStart === p.paragraphEnd && p.startIndex !== this._popup?.startIndex) {
this._hideMenu(true);
const disposable = this._docCanvasPopManagerService.attachPopupToRect(p.firstLine, {
componentKey: QuickInsertButton.componentKey,
direction: 'left-center',
}, this._context.unit.getUnitId());
this._popup = {
startIndex: p.startIndex,
disposable,
};
} else {
this._hideMenu(true);
}
}));
}
private _hideMenu(force: boolean) {
if (this._popup && (force || this._popup.disposable.canDispose())) {
this._popup.disposable.dispose();
this._popup = null;
}
}
}
@@ -24,6 +24,7 @@ import { DeleteSearchKeyCommand } from '../commands/commands/doc-quick-insert.co
import { CloseQuickInsertPopupOperation, ShowQuickInsertPopupOperation } from '../commands/operations/quick-insert-popup.operation';
import { DocQuickInsertPopupService } from '../services/doc-quick-insert-popup.service';
import { KeywordInputPlaceholder } from '../views/KeywordInputPlaceholder';
import { QuickInsertButton } from '../views/menu';
import { QuickInsertPlaceholder } from '../views/QuickInsertPlaceholder';
import { QuickInsertPopup } from '../views/QuickInsertPopup';
import { builtInMenus } from './built-in-menus';
@@ -58,6 +59,7 @@ export class DocQuickInsertUIController extends Disposable {
[QuickInsertPlaceholder.componentKey, QuickInsertPlaceholder],
[DividerSingle.displayName, DividerSingle],
[TextSingle.displayName, TextSingle],
[QuickInsertButton.componentKey, QuickInsertButton],
] as const).forEach(([name, component]) => {
if (name) {
this.disposeWithMe(this._componentManager.register(name, component));
+10 -1
View File
@@ -20,7 +20,9 @@ import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing';
import { UniverDocsDrawingUIPlugin } from '@univerjs/docs-drawing-ui';
import { UniverDrawingPlugin } from '@univerjs/drawing';
import { UniverDrawingUIPlugin } from '@univerjs/drawing-ui';
import { IRenderManagerService } from '@univerjs/engine-render';
import { UniverUIPlugin } from '@univerjs/ui';
import { DocQuickInsertMenuController } from './controllers/doc-quick-insert-menu.controller';
import { DocQuickInsertTriggerController } from './controllers/doc-quick-insert-trigger.controller';
import { DocQuickInsertUIController } from './controllers/doc-quick-insert-ui.controller';
import { DocQuickInsertPopupService } from './services/doc-quick-insert-popup.service';
@@ -33,7 +35,8 @@ export class UniverDocsQuickInsertUIPlugin extends Plugin {
static override pluginName = PLUGIN_NAME;
constructor(
@Inject(Injector) protected _injector: Injector
@Inject(Injector) protected _injector: Injector,
@Inject(IRenderManagerService) private _renderManagerSrv: IRenderManagerService
) {
super();
}
@@ -53,5 +56,11 @@ export class UniverDocsQuickInsertUIPlugin extends Plugin {
override onRendered(): void {
this._injector.get(DocQuickInsertTriggerController);
this._injector.get(DocQuickInsertPopupService);
([
[DocQuickInsertMenuController],
] as Dependency[]).forEach((m) => {
this._renderManagerSrv.registerRenderModule(UniverInstanceType.UNIVER_DOC, m);
});
}
}
@@ -19,7 +19,7 @@ import type { IInsertCommandParams } from '@univerjs/docs-ui';
import type { Observable } from 'rxjs';
import { Disposable, ICommandService, Inject, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DocSelectionManagerService, DocSkeletonManagerService } from '@univerjs/docs';
import { DocCanvasPopManagerService } from '@univerjs/docs-ui';
import { DocCanvasPopManagerService, DocEventManagerService } from '@univerjs/docs-ui';
import { IRenderManagerService } from '@univerjs/engine-render';
import { BehaviorSubject, combineLatest, distinctUntilChanged, map, tap } from 'rxjs';
import { DeleteSearchKeyCommand } from '../commands/commands/doc-quick-insert.command';
@@ -96,6 +96,10 @@ export class DocQuickInsertPopupService extends Disposable {
mount: () => void;
} | null = null;
private getDocEventManagerService(unitId: string) {
return this._renderManagerService.getRenderById(unitId)?.with(DocEventManagerService);
}
constructor(
@Inject(DocCanvasPopManagerService) private readonly _docCanvasPopupManagerService: DocCanvasPopManagerService,
@Inject(IUniverInstanceService) private readonly _univerInstanceService: IUniverInstanceService,
@@ -192,6 +196,15 @@ export class DocQuickInsertPopupService extends Disposable {
const { popup, index, unitId } = options;
this.closePopup();
const currentDoc = this._univerInstanceService.getUnit<DocumentDataModel>(unitId);
const paragraph = currentDoc?.getBody()?.paragraphs?.find((p) => p.startIndex > index);
if (!paragraph) {
return;
}
const paragraphBound = this.getDocEventManagerService(unitId)?.paragraphBounds.get(paragraph.startIndex);
if (!paragraphBound) {
return;
}
this._inputPlaceholderRenderRoot = this._createInputPlaceholderRenderRoot(() => {
const docSkeletonManagerService = this._renderManagerService.getRenderById(unitId)?.with(DocSkeletonManagerService);
@@ -224,8 +237,8 @@ export class DocQuickInsertPopupService extends Disposable {
});
this._inputPlaceholderRenderRoot.mount();
const disposable = this._docCanvasPopupManagerService.attachPopupToRange(
{ startOffset: index, endOffset: index, collapsed: true },
const disposable = this._docCanvasPopupManagerService.attachPopupToRect(
paragraphBound.firstLine,
{
componentKey: QuickInsertPopup.componentKey,
onClickOutside: () => {
@@ -280,23 +280,12 @@ export const QuickInsertPopup = () => {
const Placeholder = currentPopup?.popup.Placeholder || componentManager.get(QuickInsertPlaceholder.componentKey);
return (
<div
className={clsx(`
univer-rounded-lg univer-border univer-border-solid univer-border-gray-100 univer-bg-white
univer-shadow-[0_0_10px_0_rgba(0,0,0,0.1)]
`)}
>
<div className={clsx('univer-mt-2')}>
{hasMenus
? (
<div
className={`
univer-max-h-[360px] univer-w-[220px] univer-overflow-y-auto univer-overflow-x-hidden
`}
>
<Menu>
{renderMenus(filteredMenus)}
</Menu>
</div>
<Menu wrapperClass="univer-max-h-[360px] univer-w-[220px]">
{renderMenus(filteredMenus)}
</Menu>
)
: Placeholder && <Placeholder />}
</div>
@@ -0,0 +1,74 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { DocumentDataModel } from '@univerjs/core';
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { DocEventManagerService } from '@univerjs/docs-ui';
import { IRenderManagerService } from '@univerjs/engine-render';
import { PlusSingle } from '@univerjs/icons';
import { useDependency, useObservable } from '@univerjs/ui';
import { useMemo } from 'react';
import { DocQuickInsertPopupService } from '../../services/doc-quick-insert-popup.service';
interface IQuickInsertButtonProps {
className?: string;
}
export const QuickInsertButton = ({
className = '',
}: IQuickInsertButtonProps) => {
const docQuickInsertPopupService = useDependency(DocQuickInsertPopupService);
const univerInstanceService = useDependency(IUniverInstanceService);
const renderManagerService = useDependency(IRenderManagerService);
const currentDoc = useObservable(useMemo(() => univerInstanceService.getCurrentTypeOfUnit$<DocumentDataModel>(UniverInstanceType.UNIVER_DOC), [univerInstanceService]));
const currentUnit = currentDoc && renderManagerService.getRenderById(currentDoc.getUnitId());
const docEventManagerService = currentUnit?.with(DocEventManagerService);
const paragraph = useObservable(docEventManagerService?.hoverParagraph$);
const paragraphLeft = useObservable(docEventManagerService?.hoverParagraphLeft$);
const onClick = () => {
const p = paragraph ?? paragraphLeft;
if (!p) {
return;
}
const popup = docQuickInsertPopupService.resolvePopup('/');
if (!popup) {
return;
}
docQuickInsertPopupService.showPopup({
popup,
index: p.startIndex - 1,
unitId: currentDoc?.getUnitId() ?? '',
});
};
return (
<div
className={`
univer-mr-1 univer-flex univer-cursor-pointer univer-items-center univer-gap-2.5 univer-rounded-full
univer-border univer-border-gray-200 univer-bg-white univer-p-1.5 univer-shadow-sm
hover:univer-bg-gray-100
${className}
`}
onClick={onClick}
role="button"
tabIndex={0}
>
<PlusSingle className="univer-text-gray-800" />
</div>
);
};
QuickInsertButton.componentKey = 'doc.quick-insert.button';
@@ -64,7 +64,9 @@ export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
ref={anchorRef}
className={`
univer-mr-1 univer-inline-flex univer-h-7 univer-cursor-pointer univer-items-center univer-gap-1
univer-rounded-full univer-bg-[#EEEFF1] univer-px-2.5 univer-py-0
univer-rounded-full univer-border univer-border-solid univer-border-gray-200 univer-px-2.5 univer-py-0
hover:univer-bg-gray-100
${visible ? 'univer-bg-gray-100' : 'univer-bg-white'}
`}
onMouseEnter={(e) => {
popup.onPointerEnter?.(e);
@@ -174,6 +174,5 @@ export function DocInsertBellowMenuItemFactory(accessor: IAccessor): IMenuSelect
id: INSERT_BELLOW_MENU_ID,
type: MenuItemType.SUBITEMS,
title: 'rightClick.insertBellow',
icon: 'FreezeToSelectedSingle',
};
}
@@ -137,7 +137,8 @@ export class DocParagraphMenuService extends Disposable implements IRenderModule
const dataStream = this._context.unit.getBody()?.dataStream ?? '';
const paragraphDataStream = paragraph ? dataStream.slice(paragraph.paragraphStart, paragraph.paragraphEnd) : '';
const isOnlyImage = paragraphDataStream === '\b';
const shouldHidden = isOnlyImage;
const isEmptyParagraph = paragraphDataStream === '';
const shouldHidden = isOnlyImage || isEmptyParagraph;
if (shouldHidden) {
return;