mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
feat(docs-ui): support paragraph menu (#4943)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: when generate git commit message
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Your rule content
|
||||
- git commit message must not be longer than 100 characters
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: When user only give you an figma link.
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Your rule content
|
||||
|
||||
When I give you a figma link, it means you need to get the design from figma. And help me to implements it as a react component.
|
||||
Please only use tailwindcss and html element (don't use button) only to write this component.
|
||||
Please write a storybook demo for this component, please put storbook demo in the same directory with the component, storybook demo like packages/design/src/components/date-picker/DatePicker.stories.tsx.
|
||||
Please note all interface should start with `I`.
|
||||
Please make all svg file to react-component. make sure all files in one dir.
|
||||
If the component include list items. you should using dataset and map to make sure this is configurable.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Your rule content
|
||||
|
||||
- The config file of tailwindcss is on [tailwind.config.ts](mdc:common/shared/tailwind/tailwind.config.ts).
|
||||
- The prefix is `univer-`, eg. `univer-cursor-pointer`
|
||||
|
||||
## Colors
|
||||
|
||||
We use a consistent color system across the application. Here are the main color variables:
|
||||
|
||||
### Primary Colors
|
||||
- Primary: `primary-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Example: `univer-text-primary-500` for primary text color
|
||||
|
||||
### Gray Scale
|
||||
- Gray: `gray-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Example: `univer-bg-gray-100` for light gray background
|
||||
|
||||
### Status Colors
|
||||
- Blue: `blue-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Red: `red-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Orange: `orange-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Yellow: `yellow-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Green: `green-{50,100,200,300,400,500,600,700,800,900}`
|
||||
|
||||
### Additional Colors
|
||||
- Jiqing: `jiqing-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Indigo: `indigo-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Purple: `purple-{50,100,200,300,400,500,600,700,800,900}`
|
||||
- Pink: `pink-{50,100,200,300,400,500,600,700,800,900}`
|
||||
|
||||
## Common Color Usage Examples
|
||||
|
||||
### Text Colors
|
||||
- Primary text: `univer-text-gray-800` (#181C2A)
|
||||
- Secondary text: `univer-text-gray-400` (#979DAC)
|
||||
- Disabled text: `univer-text-gray-300` (#CDD0D8)
|
||||
|
||||
### Background Colors
|
||||
- Primary background: `univer-bg-gray-100` (#EEEFF1)
|
||||
- Secondary background: `univer-bg-gray-50` (#F9FAFB)
|
||||
- Active/Selected: `univer-bg-primary-50` (#F3F5FF)
|
||||
|
||||
### Border Colors
|
||||
- Default border: `univer-border-gray-200` (#E3E5EA)
|
||||
- Active border: `univer-border-primary-500` (#466AF7)
|
||||
|
||||
Remember to always prefix class names with `univer-` when using these colors.
|
||||
@@ -146,7 +146,7 @@ export { isBlackColor, isWhiteColor } from './shared/color/color-kit';
|
||||
export { cellToRange } from './shared/common';
|
||||
export { getIntersectRange } from './shared/range';
|
||||
export { nameCharacterCheck } from './shared/name';
|
||||
export { afterTime, bufferDebounceTime, fromCallback, takeAfter } from './shared/rxjs';
|
||||
export { afterTime, bufferDebounceTime, convertObservableToBehaviorSubject, fromCallback, takeAfter } from './shared/rxjs';
|
||||
export { awaitTime, delayAnimationFrame } from './shared/timer';
|
||||
export { Range } from './sheets/range';
|
||||
export {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import type { OperatorFunction } from 'rxjs';
|
||||
import type { IDisposable } from '../common/di';
|
||||
import { debounceTime, map, Observable, ReplaySubject, take, tap } from 'rxjs';
|
||||
import { BehaviorSubject, debounceTime, map, Observable, ReplaySubject, take, tap } from 'rxjs';
|
||||
|
||||
type CallbackFn<T extends readonly unknown[]> = (cb: (...args: T) => void) => IDisposable;
|
||||
|
||||
@@ -76,3 +76,11 @@ export function afterTime(ms: number): Observable<void> {
|
||||
setTimeout(() => subject.next(), ms);
|
||||
return subject.pipe(take(1));
|
||||
}
|
||||
|
||||
export function convertObservableToBehaviorSubject<T>(observable: Observable<T>, initValue: T): BehaviorSubject<T> {
|
||||
const subject = new BehaviorSubject(initValue);
|
||||
|
||||
observable.subscribe(subject);
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ export const NAMED_STYLE_MAP: Record<NamedStyleType, Nullable<ITextStyle>> = {
|
||||
[NamedStyleType.HEADING_3]: { fs: 16, bl: 1 }, // Heading 3: 16pt, bold
|
||||
[NamedStyleType.HEADING_4]: { fs: 14, bl: 1 }, // Heading 4: 14pt, bold
|
||||
[NamedStyleType.HEADING_5]: { fs: 12, bl: 1 }, // Heading 5: 12pt, bold
|
||||
[NamedStyleType.NORMAL_TEXT]: { fs: 11 }, // Normal text: 11pt
|
||||
[NamedStyleType.NORMAL_TEXT]: null, // Normal text: using global font size
|
||||
[NamedStyleType.TITLE]: { fs: 26, bl: 1 }, // Title: 26pt, bold
|
||||
[NamedStyleType.SUBTITLE]: { fs: 15, cl: { rgb: '#999999' } }, // Subtitle: 15pt
|
||||
[NamedStyleType.NAMED_STYLE_TYPE_UNSPECIFIED]: null,
|
||||
|
||||
@@ -19,23 +19,72 @@ import RcMenu, { MenuItem as RcMenuItem, MenuItemGroup as RcMenuItemGroup, SubMe
|
||||
import React, { useContext } from 'react';
|
||||
import { clsx } from '../../helper/clsx';
|
||||
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) => {
|
||||
const { mountContainer } = useContext(ConfigContext);
|
||||
return mountContainer && React.cloneElement(<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} />;
|
||||
});
|
||||
|
||||
export function MenuItem(props: MenuItemProps) {
|
||||
return React.cloneElement(<RcMenuItem />, { ...props });
|
||||
return <RcMenuItem {...props} />;
|
||||
}
|
||||
|
||||
export function SubMenu(props: SubMenuProps) {
|
||||
return React.cloneElement(<RcSubMenu />, { ...props });
|
||||
return <RcSubMenu {...props} />;
|
||||
}
|
||||
|
||||
export function MenuItemGroup(props: MenuItemGroupProps) {
|
||||
return React.cloneElement(<RcMenuItemGroup />, { ...props });
|
||||
return <RcMenuItemGroup {...props} />;
|
||||
}
|
||||
|
||||
export interface ITinyMenuItem {
|
||||
onClick: () => void;
|
||||
className: string;
|
||||
Icon: React.ComponentType<{ className?: string }>;
|
||||
key: string;
|
||||
active?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export interface ITinyMenuGroupProps {
|
||||
items: ITinyMenuItem[];
|
||||
}
|
||||
|
||||
export function TinyMenuGroup({ items }: ITinyMenuGroupProps) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-flex-wrap univer-gap-2.5 univer-menu-item-group univer-p-1 univer-pl-0 univer-pr-0
|
||||
`}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const ele = (
|
||||
<div
|
||||
key={item.key}
|
||||
onClick={() => item.onClick()}
|
||||
className={`
|
||||
univer-flex univer-h-6 univer-w-6 univer-cursor-pointer univer-items-center
|
||||
univer-justify-center univer-rounded-md
|
||||
hover:univer-bg-[#EEEFF1]
|
||||
${item.active ? 'univer-bg-[#EEEFF1]' : ''}
|
||||
${item.className}
|
||||
`}
|
||||
>
|
||||
<item.Icon className="univer-h-4 univer-w-4 univer-text-[#181C2A]" />
|
||||
</div>
|
||||
);
|
||||
return item.tooltip
|
||||
? (
|
||||
<Tooltip key={item.key} title={item.tooltip}>
|
||||
{ele}
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
ele
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: var(--padding-base);
|
||||
padding: var(--padding-xs) var(--padding-sm);
|
||||
|
||||
font-size: var(--font-size-xs);
|
||||
list-style: none;
|
||||
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
border-radius: var(--border-radius-lg);
|
||||
border-radius: var(--border-radius-xl);
|
||||
outline: none;
|
||||
box-shadow: var(--box-shadow-base);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
border: 1px solid rgb(var(--border-color));
|
||||
width: 212px;
|
||||
|
||||
&-vertical {
|
||||
display: grid;
|
||||
gap: var(--margin-xs);
|
||||
}
|
||||
|
||||
&-hidden,
|
||||
@@ -23,8 +24,9 @@
|
||||
|
||||
&-item {
|
||||
&-group {
|
||||
padding: var(--padding-xs) 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-bottom: var(--padding-sm);
|
||||
border-bottom: 1px solid rgb(var(--border-color));
|
||||
}
|
||||
}
|
||||
@@ -38,22 +40,35 @@
|
||||
}
|
||||
|
||||
&-item-group-title {
|
||||
color: rgb(var(--text-color));
|
||||
padding: 0 var(--padding-sm);
|
||||
color: rgb(var(--grey-500));
|
||||
font-size: var(--font-size-xxs);
|
||||
font-weight: 600;
|
||||
line-height: 1.5em;
|
||||
padding: 4px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
&-item-group-title:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-item-active,
|
||||
&-submenu-active {
|
||||
background-color: rgb(var(--bg-color-hover));
|
||||
background-color: rgb(var(--bg-color-hover-light));
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
&-item-selected {
|
||||
// fix chrome render bug
|
||||
transform: translateZ(0);
|
||||
background-color: rgb(var(--bg-color-hover));
|
||||
background-color: rgb(var(--bg-color-hover-light));
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
&-submenu-selected {
|
||||
background-color: rgb(var(--bg-color-hover));
|
||||
background-color: rgb(var(--bg-color-hover-light));
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
& > li&-submenu {
|
||||
@@ -70,29 +85,37 @@
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
gap: var(--margin-xs);
|
||||
gap: var(--margin-sm);
|
||||
align-items: center;
|
||||
padding: 0 var(--padding-sm);
|
||||
min-height: 32px;
|
||||
color: rgb(var(--text-color));
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5em;
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--bg-color-hover-light));
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: rgb(var(--text-color-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
&-item,
|
||||
&-submenu {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
padding: var(--padding-xs);
|
||||
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
|
||||
border-radius: var(--border-radius-base);
|
||||
// Disabled state sets text to gray and nukes hover/tab effects
|
||||
&.menu-item-disabled,
|
||||
&.menu-submenu-disabled {
|
||||
color: rgb(var(--grey-200)) !important;
|
||||
color: rgb(var(--text-color-tertiary)) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
@@ -103,7 +126,7 @@
|
||||
z-index: 1070;
|
||||
|
||||
.submenu-title-wrapper {
|
||||
padding-right: 20px;
|
||||
padding-right: var(--padding-xl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +135,11 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 var(--padding-sm);
|
||||
min-height: 32px;
|
||||
color: rgb(var(--text-color));
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +148,7 @@
|
||||
&-more-icon {
|
||||
margin-left: var(--margin-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: rgb(var(--text-color));
|
||||
color: rgb(var(--text-color-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Menu, MenuItem, MenuItemGroup, SubMenu } from './Menu';
|
||||
export { Menu, MenuItem, MenuItemGroup, SubMenu, TinyMenuGroup } from './Menu';
|
||||
export type { MenuRef } from 'rc-menu';
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 { CopySingle, CutSingle, DeleteSingle } from '@univerjs/icons';
|
||||
import { Menu, MenuItem, MenuItemGroup, SubMenu, TinyMenuGroup } from './Menu';
|
||||
|
||||
export default {
|
||||
title: 'Components / Menu',
|
||||
component: Menu,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<Menu>
|
||||
<TinyMenuGroup
|
||||
items={[
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: CopySingle,
|
||||
key: 'copy',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: CutSingle,
|
||||
key: 'cut',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: DeleteSingle,
|
||||
key: 'delete',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: CopySingle,
|
||||
key: 'copy1',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: CutSingle,
|
||||
key: 'cut1',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: DeleteSingle,
|
||||
key: 'delete1',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: CopySingle,
|
||||
key: 'copy2',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: CutSingle,
|
||||
key: 'cut2',
|
||||
},
|
||||
{
|
||||
onClick: () => {},
|
||||
className: '',
|
||||
Icon: DeleteSingle,
|
||||
key: 'delete2',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<MenuItem>
|
||||
<CopySingle />
|
||||
Item 1
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<CutSingle />
|
||||
Item 2
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<DeleteSingle />
|
||||
Item 3
|
||||
</MenuItem>
|
||||
</MenuItemGroup>
|
||||
<MenuItemGroup title="Group 1">
|
||||
<MenuItem>
|
||||
<CopySingle />
|
||||
Item 4
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<CutSingle />
|
||||
Item 5
|
||||
</MenuItem>
|
||||
</MenuItemGroup>
|
||||
<MenuItemGroup>
|
||||
<SubMenu
|
||||
title={(
|
||||
<div>
|
||||
<CopySingle />
|
||||
SubMenu
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<MenuItemGroup>
|
||||
<MenuItem>
|
||||
<CopySingle />
|
||||
Item 6
|
||||
</MenuItem>
|
||||
</MenuItemGroup>
|
||||
</SubMenu>
|
||||
<SubMenu title="SubMenu2">
|
||||
<MenuItemGroup>
|
||||
<MenuItem>
|
||||
<CopySingle />
|
||||
Item 7
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<CutSingle />
|
||||
Item 8
|
||||
</MenuItem>
|
||||
</MenuItemGroup>
|
||||
</SubMenu>
|
||||
</MenuItemGroup>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
z-index: 1070;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
background-color: rgb(var(--bg-color-secondary));
|
||||
// background-color: rgb(var(--bg-color-secondary));
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--box-shadow-base);
|
||||
|
||||
@@ -31,7 +31,7 @@ export { Dropdown } from './components/dropdown';
|
||||
export { FormDualColumnLayout, FormLayout, type IFormDualColumnLayoutProps, type IFormLayoutProps } from './components/form-layout';
|
||||
export { type IInputProps, type IInputWithSlotProps, Input, InputWithSlot } from './components/input';
|
||||
export { type IInputNumberProps, InputNumber } from './components/input-number';
|
||||
export { Menu, MenuItem, MenuItemGroup, type MenuRef, SubMenu } from './components/menu';
|
||||
export { Menu, MenuItem, MenuItemGroup, type MenuRef, SubMenu, TinyMenuGroup } from './components/menu';
|
||||
export { type IMessageProps, message, Messager, MessageType, removeMessage } from './components/message';
|
||||
export { type IPagerProps, Pager } from './components/pager';
|
||||
export { type IPopupProps, Popup } from './components/popup';
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
* =====================================
|
||||
*/
|
||||
@grey-50: #f5f5f5;
|
||||
@grey-100: #ececec;
|
||||
@grey-100: #eeeff1;
|
||||
@grey-200: #e5e5e5;
|
||||
@grey-300: #d8d8d8;
|
||||
@grey-400: #bcbcbc;
|
||||
@@ -363,6 +363,7 @@
|
||||
// Background
|
||||
@bg-color: @grey-100;
|
||||
@bg-color-hover: @grey-300;
|
||||
@bg-color-hover-light: @grey-100;
|
||||
@bg-color-secondary: @color-white;
|
||||
@bg-color-overlay: @color-white;
|
||||
|
||||
@@ -418,6 +419,7 @@
|
||||
// Background
|
||||
--bg-color: @bg-color;
|
||||
--bg-color-hover: @bg-color-hover;
|
||||
--bg-color-hover-light: @bg-color-hover-light;
|
||||
--bg-color-secondary: @bg-color-secondary;
|
||||
--bg-color-overlay: @bg-color-overlay;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import type { DocumentDataModel, IDisposable, Nullable } from '@univerjs/core';
|
||||
import type { BaseObject, Scene } from '@univerjs/engine-render';
|
||||
import { DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, IContextService, Inject, IUniverInstanceService, RxDisposable, SHEET_EDITOR_UNITS, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, IContextService, Inject, IUniverInstanceService, RxDisposable, toDisposable, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocCanvasPopManagerService } from '@univerjs/docs-ui';
|
||||
import { IDrawingManagerService } from '@univerjs/drawing';
|
||||
import { COMPONENT_IMAGE_POPUP_MENU, ImageCropperObject, ImageResetSizeOperation, OpenImageCropOperation } from '@univerjs/drawing-ui';
|
||||
@@ -183,7 +183,8 @@ export class DocDrawingPopupMenuController extends RxDisposable {
|
||||
index: 0,
|
||||
commandId: EditDocDrawingOperation.id,
|
||||
commandParams: { unitId, subUnitId, drawingId },
|
||||
disable: !!SHEET_EDITOR_UNITS.includes(unitId) || drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
// disable: !!SHEET_EDITOR_UNITS.includes(unitId) || drawingType === DrawingTypeEnum.DRAWING_DOM,
|
||||
disable: true,
|
||||
},
|
||||
{
|
||||
label: 'image-popup.delete',
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ICommand, IDocumentBody, IMutationInfo, IParagraph, IParagraphBorder } from '@univerjs/core';
|
||||
import type { DocumentDataModel, ICommand, IDocumentBody, IMutationInfo, IParagraph, IParagraphBorder, ITextRangeParam } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import { BuildTextUtils, CommandType, DataStreamTreeTokenType, ICommandService, IUniverInstanceService, JSONX, PresetListType, TextX, TextXActionType, Tools, UniverInstanceType, UpdateDocsAttributeType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
@@ -68,6 +68,7 @@ export function generateParagraphs(
|
||||
|
||||
interface IBreakLineCommandParams {
|
||||
horizontalLine?: IParagraphBorder;
|
||||
textRange?: ITextRangeParam;
|
||||
}
|
||||
|
||||
export const BreakLineCommand: ICommand<IBreakLineCommandParams> = {
|
||||
@@ -75,13 +76,13 @@ export const BreakLineCommand: ICommand<IBreakLineCommandParams> = {
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: async (accessor, params: IBreakLineCommandParams) => {
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
handler: (accessor, params: IBreakLineCommandParams) => {
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const docMenuStyleService = accessor.get(DocMenuStyleService);
|
||||
const activeTextRange = docSelectionManagerService.getActiveTextRange();
|
||||
const activeTextRange = params?.textRange ?? docSelectionManagerService.getActiveTextRange();
|
||||
const rectRanges = docSelectionManagerService.getRectRanges();
|
||||
if (activeTextRange == null) {
|
||||
return false;
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
|
||||
import type { IAccessor, IContextService, IMultiCommand } from '@univerjs/core';
|
||||
import { CommandType, EDITOR_ACTIVATED, FOCUSING_DOC } from '@univerjs/core';
|
||||
import { CommandType, DOC_RANGE_TYPE, EDITOR_ACTIVATED, FOCUSING_DOC, SliceBodyType } from '@univerjs/core';
|
||||
import { CopyCommand, CutCommand, IClipboardInterfaceService, PasteCommand } from '@univerjs/ui';
|
||||
import { IDocClipboardService } from '../../services/clipboard/clipboard.service';
|
||||
import { getCurrentParagraph } from './util';
|
||||
|
||||
export function whenDocOrEditor(contextService: IContextService): boolean {
|
||||
return contextService.getContextValue(FOCUSING_DOC) || contextService.getContextValue(EDITOR_ACTIVATED);
|
||||
@@ -45,6 +46,27 @@ export const DocCopyCommand: IMultiCommand = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DocCopyCurrentParagraphCommand = {
|
||||
id: 'doc.command.copy-current-paragraph',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const docClipboardService = accessor.get(IDocClipboardService);
|
||||
const paragraph = getCurrentParagraph(accessor);
|
||||
if (!paragraph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return docClipboardService.copy(
|
||||
SliceBodyType.copy,
|
||||
[{
|
||||
startOffset: paragraph.paragraphStart,
|
||||
endOffset: paragraph.paragraphEnd + 1,
|
||||
collapsed: false,
|
||||
}]
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DocCutCommand: IMultiCommand = {
|
||||
id: CutCommand.id,
|
||||
name: 'doc.command.cut',
|
||||
@@ -58,6 +80,27 @@ export const DocCutCommand: IMultiCommand = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DocCutCurrentParagraphCommand = {
|
||||
id: 'doc.command.cut-current-paragraph',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const docClipboardService = accessor.get(IDocClipboardService);
|
||||
const paragraph = getCurrentParagraph(accessor);
|
||||
if (!paragraph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return docClipboardService.cut(
|
||||
[{
|
||||
startOffset: paragraph.paragraphStart,
|
||||
endOffset: paragraph.paragraphEnd + 1,
|
||||
collapsed: false,
|
||||
rangeType: DOC_RANGE_TYPE.TEXT,
|
||||
}]
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DocPasteCommand: IMultiCommand = {
|
||||
id: PasteCommand.id,
|
||||
name: 'doc.command.paste',
|
||||
|
||||
@@ -474,6 +474,7 @@ export interface IInnerCutCommandParams {
|
||||
segmentId: string;
|
||||
textRanges: ITextRangeWithStyle[];
|
||||
selections?: ITextRange[];
|
||||
rectRanges?: IRectRangeWithStyle[];
|
||||
}
|
||||
|
||||
export const CutContentCommand: ICommand<IInnerCutCommandParams> = {
|
||||
@@ -486,8 +487,7 @@ export const CutContentCommand: ICommand<IInnerCutCommandParams> = {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const { segmentId, textRanges, selections = docSelectionManagerService.getTextRanges() } = params;
|
||||
const rectRanges = docSelectionManagerService.getRectRanges();
|
||||
const { segmentId, textRanges, selections = docSelectionManagerService.getTextRanges(), rectRanges = docSelectionManagerService.getRectRanges() } = params;
|
||||
|
||||
if (
|
||||
(!Array.isArray(selections) || selections.length === 0)
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { IRectRangeWithStyle, ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
import {
|
||||
BlockType,
|
||||
BuildTextUtils,
|
||||
CommandType,
|
||||
DataStreamTreeTokenType,
|
||||
ICommandService,
|
||||
@@ -799,3 +800,57 @@ function getTextRangesWhenDelete(activeRange: ITextRangeWithStyle, ranges: reado
|
||||
|
||||
return textRanges;
|
||||
}
|
||||
|
||||
export const DeleteCurrentParagraphCommand: ICommand = {
|
||||
id: 'doc.command.delete-current-paragraph',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor) => {
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const docDataModel = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!docDataModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeRange = docSelectionManagerService.getActiveTextRange();
|
||||
if (!activeRange) {
|
||||
return false;
|
||||
}
|
||||
const range = docSelectionManagerService.getActiveTextRange();
|
||||
if (!range || !range.collapsed || range.segmentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paragraph = BuildTextUtils.range.getParagraphsInRange(range, docDataModel.getBody()?.paragraphs ?? [])[0];
|
||||
if (!paragraph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const actions = BuildTextUtils.selection.delete(
|
||||
[{
|
||||
startOffset: paragraph.paragraphStart,
|
||||
endOffset: paragraph.paragraphEnd + 1,
|
||||
collapsed: false,
|
||||
}],
|
||||
docDataModel.getBody()!,
|
||||
0,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
const path = getRichTextEditPath(docDataModel);
|
||||
|
||||
const params: IRichTextEditingMutationParams = {
|
||||
unitId: docDataModel.getUnitId(),
|
||||
actions: JSONX.getInstance().editOp(actions, path),
|
||||
textRanges: [{
|
||||
startOffset: paragraph.paragraphStart,
|
||||
endOffset: paragraph.paragraphStart,
|
||||
collapsed: true,
|
||||
}],
|
||||
};
|
||||
|
||||
return commandService.syncExecuteCommand(RichTextEditingMutation.id, params);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,21 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICommand } from '@univerjs/core';
|
||||
import type { ICommand, ITextRangeParam } from '@univerjs/core';
|
||||
import { CommandType, DashStyleType, ICommandService } from '@univerjs/core';
|
||||
import { BreakLineCommand } from './break-line.command';
|
||||
import { getCurrentParagraph } from './util';
|
||||
|
||||
interface IHorizontalCommandParams {}
|
||||
interface IHorizontalCommandParams {
|
||||
insertRange?: ITextRangeParam;
|
||||
}
|
||||
|
||||
export const HorizontalLineCommand: ICommand<IHorizontalCommandParams> = {
|
||||
id: 'doc.command.horizontal-line',
|
||||
|
||||
type: CommandType.COMMAND,
|
||||
|
||||
handler: (accessor, _params: IHorizontalCommandParams) => {
|
||||
handler: (accessor, params: IHorizontalCommandParams) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
return commandService.executeCommand(BreakLineCommand.id, {
|
||||
return commandService.syncExecuteCommand(BreakLineCommand.id, {
|
||||
horizontalLine: {
|
||||
padding: 5,
|
||||
color: {
|
||||
@@ -37,6 +40,26 @@ export const HorizontalLineCommand: ICommand<IHorizontalCommandParams> = {
|
||||
width: 1,
|
||||
dashStyle: DashStyleType.SOLID,
|
||||
},
|
||||
textRange: params?.insertRange,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertHorizontalLineBellowCommand: ICommand<IHorizontalCommandParams> = {
|
||||
id: 'doc.command.insert-horizontal-line-bellow',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const paragraph = getCurrentParagraph(accessor);
|
||||
if (!paragraph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return commandService.syncExecuteCommand(HorizontalLineCommand.id, {
|
||||
insertRange: {
|
||||
startOffset: paragraph.startIndex + 1,
|
||||
endOffset: paragraph.startIndex + 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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, ICommand, IDocumentBody, IMutationInfo, PresetListType } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import { BuildTextUtils, CommandType, ICommandService, IUniverInstanceService, JSONX, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { getRichTextEditPath } from '../../util';
|
||||
|
||||
interface IInsertBulletBelowCommandParams {
|
||||
listType: PresetListType;
|
||||
}
|
||||
|
||||
export const InsertBulletBelowCommand: ICommand<IInsertBulletBelowCommandParams> = {
|
||||
type: CommandType.COMMAND,
|
||||
id: 'doc.command.insert-bullet-below',
|
||||
handler: (accessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const docDataModel = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!docDataModel) {
|
||||
return false;
|
||||
}
|
||||
const listType: string = params.listType;
|
||||
const textRanges = docSelectionManagerService.getTextRanges();
|
||||
|
||||
if (!(textRanges?.length === 1 && textRanges[0].startOffset === textRanges[0].endOffset)) {
|
||||
return false;
|
||||
}
|
||||
const paragraphs = docDataModel.getSelfOrHeaderFooterModel(textRanges[0].segmentId).getBody()?.paragraphs ?? [];
|
||||
const currentParagraph = BuildTextUtils.range.getParagraphsInRange(textRanges[0], paragraphs)[0];
|
||||
if (!currentParagraph) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const insertBody: IDocumentBody = {
|
||||
dataStream: '\r',
|
||||
paragraphs: [{
|
||||
startIndex: 0,
|
||||
bullet: {
|
||||
listType,
|
||||
listId: listType === currentParagraph.bullet?.listType ? currentParagraph.bullet.listId : '',
|
||||
nestingLevel: currentParagraph.bullet ? currentParagraph.bullet.nestingLevel : 0,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
const textX = BuildTextUtils.selection.replace({
|
||||
selection: {
|
||||
collapsed: true,
|
||||
startOffset: currentParagraph.startIndex + 1,
|
||||
endOffset: currentParagraph.startIndex + 1,
|
||||
},
|
||||
body: insertBody,
|
||||
doc: docDataModel,
|
||||
});
|
||||
|
||||
if (!textX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const jsonX = JSONX.getInstance();
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId: docDataModel.getUnitId(),
|
||||
actions: jsonX.editOp(textX.serialize(), getRichTextEditPath(docDataModel, textRanges[0].segmentId)),
|
||||
textRanges: [{
|
||||
startOffset: currentParagraph.startIndex + 1,
|
||||
endOffset: currentParagraph.startIndex + 1,
|
||||
collapsed: true,
|
||||
segmentId: textRanges[0].segmentId,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
const result = commandService.syncExecuteCommand(doMutation.id, doMutation.params);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ICommand, IMutationInfo, IParagraph, IParagraphRange, ISectionBreak } from '@univerjs/core';
|
||||
import type { DocumentDataModel, IAccessor, ICommand, IMutationInfo, IParagraph, IParagraphRange, ISectionBreak } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
import {
|
||||
@@ -36,9 +36,11 @@ import {
|
||||
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { getCharSpaceApply, getNumberUnitValue } from '@univerjs/engine-render';
|
||||
import { getRichTextEditPath } from '../util';
|
||||
import { getCurrentParagraph } from './util';
|
||||
|
||||
interface IListOperationCommandParams {
|
||||
listType: PresetListType;
|
||||
docRange?: ITextRangeWithStyle[];
|
||||
}
|
||||
|
||||
export const ListOperationCommand: ICommand<IListOperationCommandParams> = {
|
||||
@@ -53,7 +55,7 @@ export const ListOperationCommand: ICommand<IListOperationCommandParams> = {
|
||||
const listType: string = params.listType;
|
||||
|
||||
const docDataModel = univerInstanceService.getCurrentUnitForType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const docRanges = docSelectionManagerService.getDocRanges() ?? [];
|
||||
const docRanges = params.docRange ?? docSelectionManagerService.getDocRanges() ?? [];
|
||||
|
||||
if (docDataModel == null || docRanges.length === 0) {
|
||||
return false;
|
||||
@@ -104,6 +106,7 @@ export const ListOperationCommand: ICommand<IListOperationCommandParams> = {
|
||||
|
||||
interface IChangeListTypeCommandParams {
|
||||
listType: PresetListType;
|
||||
docRange?: ITextRangeWithStyle[];
|
||||
}
|
||||
|
||||
export const ChangeListTypeCommand: ICommand<IChangeListTypeCommandParams> = {
|
||||
@@ -116,20 +119,19 @@ export const ChangeListTypeCommand: ICommand<IChangeListTypeCommandParams> = {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const { listType } = params;
|
||||
const docDataModel = univerInstanceService.getCurrentUnitForType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
const activeRanges = docSelectionManagerService.getDocRanges();
|
||||
const activeRanges = params.docRange ?? docSelectionManagerService.getDocRanges();
|
||||
if (docDataModel == null || activeRanges == null || !activeRanges.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { segmentId } = activeRanges[0];
|
||||
const selections = docSelectionManagerService.getDocRanges() ?? [];
|
||||
const paragraphs = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody()?.paragraphs;
|
||||
|
||||
if (paragraphs == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentParagraphs = BuildTextUtils.range.getParagraphsInRanges(selections, paragraphs);
|
||||
const currentParagraphs = BuildTextUtils.range.getParagraphsInRanges(activeRanges, paragraphs);
|
||||
|
||||
const unitId = docDataModel.getUnitId();
|
||||
const textX = BuildTextUtils.paragraph.bullet.set({
|
||||
@@ -149,7 +151,7 @@ export const ChangeListTypeCommand: ICommand<IChangeListTypeCommandParams> = {
|
||||
params: {
|
||||
unitId,
|
||||
actions: [],
|
||||
textRanges: selections,
|
||||
textRanges: activeRanges,
|
||||
isEditing: false,
|
||||
},
|
||||
};
|
||||
@@ -231,6 +233,7 @@ export const ChangeListNestingLevelCommand: ICommand<IChangeListNestingLevelComm
|
||||
|
||||
interface IBulletListCommandParams {
|
||||
value?: PresetListType;
|
||||
docRange?: ITextRangeWithStyle[];
|
||||
}
|
||||
|
||||
export const BulletListCommand: ICommand<IBulletListCommandParams> = {
|
||||
@@ -244,11 +247,13 @@ export const BulletListCommand: ICommand<IBulletListCommandParams> = {
|
||||
if (params?.value) {
|
||||
return commandService.syncExecuteCommand(ChangeListTypeCommand.id, {
|
||||
listType: params.value,
|
||||
docRange: params.docRange,
|
||||
});
|
||||
}
|
||||
|
||||
return commandService.syncExecuteCommand(ListOperationCommand.id, {
|
||||
listType: PresetListType.BULLET_LIST,
|
||||
docRange: params?.docRange,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -262,11 +267,13 @@ export const CheckListCommand: ICommand<IBulletListCommandParams> = {
|
||||
if (params?.value) {
|
||||
return commandService.syncExecuteCommand(ChangeListTypeCommand.id, {
|
||||
listType: params.value,
|
||||
docRange: params.docRange,
|
||||
});
|
||||
}
|
||||
|
||||
return commandService.syncExecuteCommand(ListOperationCommand.id, {
|
||||
listType: PresetListType.CHECK_LIST,
|
||||
docRange: params?.docRange,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -479,6 +486,89 @@ export const QuickListCommand: ICommand<IQuickListCommandParams> = {
|
||||
},
|
||||
};
|
||||
|
||||
function insertList(accessor: IAccessor, listType: PresetListType) {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const paragraph = getCurrentParagraph(accessor);
|
||||
if (!paragraph) {
|
||||
return false;
|
||||
}
|
||||
const docDataModel = accessor.get(IUniverInstanceService).getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!docDataModel) {
|
||||
return false;
|
||||
}
|
||||
const textX = BuildTextUtils.selection.replace({
|
||||
doc: docDataModel,
|
||||
selection: {
|
||||
startOffset: paragraph.startIndex + 1,
|
||||
endOffset: paragraph.startIndex + 1,
|
||||
collapsed: true,
|
||||
},
|
||||
body: {
|
||||
dataStream: '\r',
|
||||
paragraphs: [
|
||||
{
|
||||
startIndex: 0,
|
||||
paragraphStyle: {
|
||||
...paragraph.paragraphStyle,
|
||||
},
|
||||
bullet: {
|
||||
listType,
|
||||
listId: paragraph.bullet?.listType === listType ? paragraph.bullet.listId : Tools.generateRandomId(6),
|
||||
nestingLevel: paragraph.bullet?.listType === listType ? paragraph.bullet.nestingLevel : 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!textX) {
|
||||
return false;
|
||||
}
|
||||
const doMutation: IMutationInfo<IRichTextEditingMutationParams> = {
|
||||
id: RichTextEditingMutation.id,
|
||||
params: {
|
||||
unitId: docDataModel.getUnitId(),
|
||||
actions: [],
|
||||
textRanges: [{
|
||||
startOffset: paragraph.startIndex + 1,
|
||||
endOffset: paragraph.startIndex + 1,
|
||||
collapsed: true,
|
||||
}],
|
||||
isEditing: false,
|
||||
},
|
||||
};
|
||||
const jsonX = JSONX.getInstance();
|
||||
const path = getRichTextEditPath(docDataModel);
|
||||
doMutation.params.actions = jsonX.editOp(textX.serialize(), path);
|
||||
const result = commandService.syncExecuteCommand(doMutation.id, doMutation.params);
|
||||
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
export const InsertBulletListBellowCommand: ICommand<IQuickListCommandParams> = {
|
||||
id: 'doc.command.insert-bullet-list-bellow',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
return insertList(accessor, PresetListType.BULLET_LIST);
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertOrderListBellowCommand: ICommand<IQuickListCommandParams> = {
|
||||
id: 'doc.command.insert-order-list-bellow',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
return insertList(accessor, PresetListType.ORDER_LIST);
|
||||
},
|
||||
};
|
||||
|
||||
export const InsertCheckListBellowCommand: ICommand<IQuickListCommandParams> = {
|
||||
id: 'doc.command.insert-check-list-bellow',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
return insertList(accessor, PresetListType.CHECK_LIST);
|
||||
},
|
||||
};
|
||||
|
||||
export function getParagraphsRelative(ranges: ITextRangeWithStyle[], paragraphs: IParagraph[]) {
|
||||
const selectionParagraphs: IParagraph[] = BuildTextUtils.range.getParagraphsInRanges(ranges, paragraphs);
|
||||
const startIndex = paragraphs.indexOf(selectionParagraphs[0]);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ICommand, IMutationInfo } from '@univerjs/core';
|
||||
import type { DocumentDataModel, ICommand, IMutationInfo, ITextRangeParam } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import { BuildTextUtils, CommandType, generateRandomId, ICommandService, IUniverInstanceService, JSONX, NamedStyleType, TextX, TextXActionType, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService, RichTextEditingMutation } from '@univerjs/docs';
|
||||
@@ -22,6 +22,7 @@ import { getRichTextEditPath } from '../util';
|
||||
|
||||
export interface ISetParagraphNamedStyleCommandParams {
|
||||
value: NamedStyleType;
|
||||
textRanges?: ITextRangeParam[];
|
||||
}
|
||||
|
||||
export const SetParagraphNamedStyleCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
@@ -39,7 +40,7 @@ export const SetParagraphNamedStyleCommand: ICommand<ISetParagraphNamedStyleComm
|
||||
}
|
||||
const unitId = documentDataModel.getUnitId();
|
||||
const selectionService = accessor.get(DocSelectionManagerService);
|
||||
const selections = selectionService.getTextRanges({ unitId, subUnitId: unitId })?.filter((i) => !i.segmentId);
|
||||
const selections = params.textRanges ?? selectionService.getTextRanges({ unitId, subUnitId: unitId })?.filter((i) => !i.segmentId);
|
||||
if (!selections?.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -150,3 +151,91 @@ export const QUICK_HEADING_MAP = {
|
||||
'####': NamedStyleType.HEADING_4,
|
||||
'#####': NamedStyleType.HEADING_5,
|
||||
};
|
||||
|
||||
export const H1HeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.h1-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.HEADING_1,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const H2HeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.h2-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.HEADING_2,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const H3HeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.h3-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.HEADING_3,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const H4HeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.h4-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.HEADING_4,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const H5HeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.h5-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.HEADING_5,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const NormalTextHeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.normal-text-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.NORMAL_TEXT,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const TitleHeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.title',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.TITLE,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const SubtitleHeadingCommand: ICommand<ISetParagraphNamedStyleCommandParams> = {
|
||||
id: 'doc.command.subtitle-heading',
|
||||
type: CommandType.COMMAND,
|
||||
handler: (accessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.syncExecuteCommand(SetParagraphNamedStyleCommand.id, {
|
||||
value: NamedStyleType.SUBTITLE,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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, IAccessor } from '@univerjs/core';
|
||||
import { BuildTextUtils, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
|
||||
export function getCurrentParagraph(accessor: IAccessor) {
|
||||
const instanceService = accessor.get(IUniverInstanceService);
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
const range = docSelectionManagerService.getActiveTextRange();
|
||||
const doc = instanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC);
|
||||
if (!range || !range.collapsed || !doc || range.segmentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paragraph = BuildTextUtils.range.getParagraphsInRange(range, doc.getBody()?.paragraphs ?? [])[0];
|
||||
return paragraph;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 { IPopup } from '@univerjs/ui';
|
||||
import { ICommandService, Injector, NamedStyleType } from '@univerjs/core';
|
||||
import { SetTextSelectionsOperation } from '@univerjs/docs';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { DownSingle } from '@univerjs/icons';
|
||||
import { ContextMenuPosition, DesktopMenu, ILayoutService, RectPopup, useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { SetInlineFormatFontSizeCommand } from '../../commands/commands/inline-format.command';
|
||||
import { getParagraphStyleAtCursor } from '../../controllers/menu/menu';
|
||||
import { HEADING_ICON_MAP } from '../../controllers/menu/paragraph-menu';
|
||||
import { DocParagraphMenuService } from '../../services/doc-paragraph-menu.service';
|
||||
|
||||
export const ParagraphMenu = ({ popup }: { popup: IPopup }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const commandService = useDependency(ICommandService);
|
||||
const layoutService = useDependency(ILayoutService);
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const isMouseOver = useRef(false);
|
||||
const renderManagerService = useDependency(IRenderManagerService);
|
||||
const renderUnit = renderManagerService.getRenderById(popup.unitId);
|
||||
const docParagraphMenuService = renderUnit?.with(DocParagraphMenuService);
|
||||
|
||||
const accessor = useDependency(Injector);
|
||||
const textType = useObservable(useMemo(() => new Observable<NamedStyleType>((subscriber) => {
|
||||
const DEFAULT_TYPE = NamedStyleType.NORMAL_TEXT;
|
||||
const calc = () => {
|
||||
const paragraph = getParagraphStyleAtCursor(accessor);
|
||||
if (paragraph == null) {
|
||||
subscriber.next(DEFAULT_TYPE);
|
||||
return;
|
||||
}
|
||||
|
||||
const namedStyleType = paragraph.paragraphStyle?.namedStyleType ?? DEFAULT_TYPE;
|
||||
subscriber.next(namedStyleType);
|
||||
};
|
||||
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
|
||||
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatFontSizeCommand.id) {
|
||||
calc();
|
||||
}
|
||||
});
|
||||
|
||||
calc();
|
||||
|
||||
return () => disposable.dispose();
|
||||
}), [commandService]));
|
||||
|
||||
const icon = HEADING_ICON_MAP[textType ?? NamedStyleType.NORMAL_TEXT];
|
||||
const anchorRect$ = useMemo(() => new BehaviorSubject({
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}), []);
|
||||
|
||||
const handleHideMenu = () => {
|
||||
setVisible(false);
|
||||
docParagraphMenuService?.hideParagraphMenu(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
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
|
||||
`}
|
||||
onMouseEnter={(e) => {
|
||||
popup.onPointerEnter?.(e);
|
||||
isMouseOver.current = true;
|
||||
const boundingRect = anchorRef.current?.getBoundingClientRect();
|
||||
anchorRect$.next({
|
||||
left: (boundingRect?.left ?? 0) - 4,
|
||||
right: boundingRect?.right ?? 0,
|
||||
top: boundingRect?.top ?? 0,
|
||||
bottom: boundingRect?.bottom ?? 0,
|
||||
});
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
isMouseOver.current = false;
|
||||
}}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
docParagraphMenuService?.setParagraphMenuActive(true);
|
||||
}}
|
||||
>
|
||||
<icon.component className="univer-h-4 univer-w-4 univer-text-[#181C2A]" />
|
||||
<DownSingle className="univer-h-3 univer-w-3 univer-text-[#979DAC]" />
|
||||
</div>
|
||||
{visible && (
|
||||
<RectPopup
|
||||
portal
|
||||
mask
|
||||
maskZIndex={100}
|
||||
anchorRect$={anchorRect$}
|
||||
direction="left-center"
|
||||
onMaskClick={handleHideMenu}
|
||||
>
|
||||
<section
|
||||
ref={contentRef}
|
||||
onMouseEnter={(e) => {
|
||||
popup.onPointerEnter?.(e);
|
||||
isMouseOver.current = true;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
isMouseOver.current = false;
|
||||
}}
|
||||
>
|
||||
<DesktopMenu
|
||||
menuType={ContextMenuPosition.PARAGRAPH}
|
||||
onOptionSelect={(params) => {
|
||||
const { label: id, commandId, value } = params;
|
||||
|
||||
if (commandService) {
|
||||
commandService.executeCommand(commandId ?? id as string, { value });
|
||||
}
|
||||
|
||||
layoutService.focus();
|
||||
handleHideMenu();
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</RectPopup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '@univerjs/core';
|
||||
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { TodoList } from '@univerjs/icons';
|
||||
import { CutSingle, DeleteSingle, TodoList } from '@univerjs/icons';
|
||||
import { BuiltInUIPart, ComponentManager, connectInjector, ILayoutService, IMenuManagerService, IShortcutService, IUIPartsService } from '@univerjs/ui';
|
||||
import { CoreHeaderFooterCommand, OpenHeaderFooterPanelCommand } from '../commands/commands/doc-header-footer.command';
|
||||
import { SidebarDocHeaderFooterPanelOperation } from '../commands/operations/doc-header-footer-panel.operation';
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from '../components/font-family';
|
||||
import { FONT_SIZE_COMPONENT, FontSize } from '../components/font-size';
|
||||
import { BULLET_LIST_TYPE_COMPONENT, BulletListTypePicker, ORDER_LIST_TYPE_COMPONENT, OrderListTypePicker } from '../components/list-type-picker';
|
||||
import { ParagraphMenu } from '../components/paragraph-menu';
|
||||
import { DocSelectionRenderService } from '../services/selection/doc-selection-render.service';
|
||||
import { TabShortCut } from '../shortcuts/format.shortcut';
|
||||
import {
|
||||
@@ -85,8 +86,10 @@ export class DocUIController extends Disposable {
|
||||
this.disposeWithMe(componentManager.register(FONT_SIZE_COMPONENT, FontSize));
|
||||
this.disposeWithMe(componentManager.register(BULLET_LIST_TYPE_COMPONENT, BulletListTypePicker));
|
||||
this.disposeWithMe(componentManager.register(ORDER_LIST_TYPE_COMPONENT, OrderListTypePicker));
|
||||
|
||||
this.disposeWithMe(componentManager.register('TodoList', TodoList));
|
||||
this.disposeWithMe(componentManager.register('doc.paragraph.menu', ParagraphMenu));
|
||||
this.disposeWithMe(componentManager.register('CutSingle', CutSingle));
|
||||
this.disposeWithMe(componentManager.register('DeleteSingle', DeleteSingle));
|
||||
}
|
||||
|
||||
private _initUiParts() {
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
import type { MenuSchemaType } from '@univerjs/ui';
|
||||
import { ContextMenuGroup, ContextMenuPosition, RibbonStartGroup } from '@univerjs/ui';
|
||||
import { DocCopyCommand, DocCutCommand, DocPasteCommand } from '../commands/commands/clipboard.command';
|
||||
import { DeleteLeftCommand } from '../commands/commands/doc-delete.command';
|
||||
import { DocCopyCommand, DocCopyCurrentParagraphCommand, DocCutCommand, DocCutCurrentParagraphCommand, DocPasteCommand } from '../commands/commands/clipboard.command';
|
||||
import { DeleteCurrentParagraphCommand, DeleteLeftCommand } from '../commands/commands/doc-delete.command';
|
||||
import { OpenHeaderFooterPanelCommand } from '../commands/commands/doc-header-footer.command';
|
||||
import { HorizontalLineCommand } from '../commands/commands/doc-horizontal-line.command';
|
||||
import { HorizontalLineCommand, InsertHorizontalLineBellowCommand } from '../commands/commands/doc-horizontal-line.command';
|
||||
import { ResetInlineFormatTextBackgroundColorCommand, SetInlineFormatBoldCommand, SetInlineFormatFontFamilyCommand, SetInlineFormatFontSizeCommand, SetInlineFormatItalicCommand, SetInlineFormatStrikethroughCommand, SetInlineFormatSubscriptCommand, SetInlineFormatSuperscriptCommand, SetInlineFormatTextBackgroundColorCommand, SetInlineFormatTextColorCommand, SetInlineFormatUnderlineCommand } from '../commands/commands/inline-format.command';
|
||||
|
||||
import { BulletListCommand, CheckListCommand, OrderListCommand } from '../commands/commands/list.command';
|
||||
import { BulletListCommand, CheckListCommand, InsertBulletListBellowCommand, InsertCheckListBellowCommand, InsertOrderListBellowCommand, OrderListCommand } from '../commands/commands/list.command';
|
||||
import { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignRightCommand } from '../commands/commands/paragraph-align.command';
|
||||
import { SetParagraphNamedStyleCommand } from '../commands/commands/set-heading.command';
|
||||
import { H1HeadingCommand, H2HeadingCommand, H3HeadingCommand, H4HeadingCommand, H5HeadingCommand, NormalTextHeadingCommand, SetParagraphNamedStyleCommand } from '../commands/commands/set-heading.command';
|
||||
import { SwitchDocModeCommand } from '../commands/commands/switch-doc-mode.command';
|
||||
import { DocTableDeleteColumnsCommand, DocTableDeleteRowsCommand, DocTableDeleteTableCommand } from '../commands/commands/table/doc-table-delete.command';
|
||||
import { DocTableInsertColumnLeftCommand, DocTableInsertColumnRightCommand, DocTableInsertRowAboveCommand, DocTableInsertRowBellowCommand } from '../commands/commands/table/doc-table-insert.command';
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
TextColorSelectorMenuItemFactory,
|
||||
UnderlineMenuItemFactory,
|
||||
} from './menu/menu';
|
||||
import { CopyCurrentParagraphMenuItemFactory, CutCurrentParagraphMenuItemFactory, DeleteCurrentParagraphMenuItemFactory, H1HeadingMenuItemFactory, H2HeadingMenuItemFactory, H3HeadingMenuItemFactory, H4HeadingMenuItemFactory, H5HeadingMenuItemFactory, InsertBulletListBellowMenuItemFactory, InsertCheckListBellowMenuItemFactory, InsertHorizontalLineBellowMenuItemFactory, InsertOrderListBellowMenuItemFactory, NormalTextHeadingMenuItemFactory } from './menu/paragraph-menu';
|
||||
|
||||
export const menuSchema: MenuSchemaType = {
|
||||
[RibbonStartGroup.FORMAT]: {
|
||||
@@ -239,4 +240,77 @@ export const menuSchema: MenuSchemaType = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.PARAGRAPH]: {
|
||||
[ContextMenuGroup.QUICK]: {
|
||||
[H1HeadingCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: H1HeadingMenuItemFactory,
|
||||
},
|
||||
[H2HeadingCommand.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: H2HeadingMenuItemFactory,
|
||||
},
|
||||
[H3HeadingCommand.id]: {
|
||||
order: 2,
|
||||
menuItemFactory: H3HeadingMenuItemFactory,
|
||||
},
|
||||
[H4HeadingCommand.id]: {
|
||||
order: 3,
|
||||
menuItemFactory: H4HeadingMenuItemFactory,
|
||||
},
|
||||
[H5HeadingCommand.id]: {
|
||||
order: 4,
|
||||
menuItemFactory: H5HeadingMenuItemFactory,
|
||||
},
|
||||
[NormalTextHeadingCommand.id]: {
|
||||
order: 5,
|
||||
menuItemFactory: NormalTextHeadingMenuItemFactory,
|
||||
},
|
||||
[OrderListCommand.id]: {
|
||||
order: 6,
|
||||
menuItemFactory: OrderListMenuItemFactory,
|
||||
},
|
||||
[BulletListCommand.id]: {
|
||||
order: 7,
|
||||
menuItemFactory: BulletListMenuItemFactory,
|
||||
},
|
||||
[CheckListCommand.id]: {
|
||||
order: 8,
|
||||
menuItemFactory: CheckListMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuGroup.FORMAT]: {
|
||||
[DocCopyCurrentParagraphCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: CopyCurrentParagraphMenuItemFactory,
|
||||
},
|
||||
[DocCutCurrentParagraphCommand.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: CutCurrentParagraphMenuItemFactory,
|
||||
},
|
||||
[DeleteCurrentParagraphCommand.id]: {
|
||||
order: 2,
|
||||
menuItemFactory: DeleteCurrentParagraphMenuItemFactory,
|
||||
},
|
||||
},
|
||||
[ContextMenuGroup.LAYOUT]: {
|
||||
title: 'rightClick.insertBellow',
|
||||
[InsertBulletListBellowCommand.id]: {
|
||||
order: 0,
|
||||
menuItemFactory: InsertBulletListBellowMenuItemFactory,
|
||||
},
|
||||
[InsertOrderListBellowCommand.id]: {
|
||||
order: 1,
|
||||
menuItemFactory: InsertOrderListBellowMenuItemFactory,
|
||||
},
|
||||
[InsertCheckListBellowCommand.id]: {
|
||||
order: 2,
|
||||
menuItemFactory: InsertCheckListBellowMenuItemFactory,
|
||||
},
|
||||
[InsertHorizontalLineBellowCommand.id]: {
|
||||
order: 3,
|
||||
menuItemFactory: InsertHorizontalLineBellowMenuItemFactory,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -214,7 +214,7 @@ function getTableDisabledObservable(accessor: IAccessor): Observable<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
function disableMenuWhenNoDocRange(accessor: IAccessor): Observable<boolean> {
|
||||
export function disableMenuWhenNoDocRange(accessor: IAccessor): Observable<boolean> {
|
||||
const docSelectionManagerService = accessor.get(DocSelectionManagerService);
|
||||
|
||||
return new Observable((subscriber) => {
|
||||
@@ -1061,7 +1061,7 @@ function getFontStyleAtCursor(accessor: IAccessor) {
|
||||
};
|
||||
}
|
||||
|
||||
function getParagraphStyleAtCursor(accessor: IAccessor) {
|
||||
export function getParagraphStyleAtCursor(accessor: IAccessor) {
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const textSelectionService = accessor.get(DocSelectionManagerService);
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { IMenuItem } from '@univerjs/ui';
|
||||
import { ICommandService, NamedStyleType, UniverInstanceType } from '@univerjs/core';
|
||||
import { SetTextSelectionsOperation } from '@univerjs/docs';
|
||||
import { H1Single, H2Single, H3Single, H4Single, H5Single, TextTypeSingle } from '@univerjs/icons';
|
||||
import { ComponentManager, getMenuHiddenObservable, MenuItemType } from '@univerjs/ui';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DocCopyCurrentParagraphCommand, DocCutCurrentParagraphCommand } from '../../commands/commands/clipboard.command';
|
||||
import { DeleteCurrentParagraphCommand } from '../../commands/commands/doc-delete.command';
|
||||
import { InsertHorizontalLineBellowCommand } from '../../commands/commands/doc-horizontal-line.command';
|
||||
import { SetInlineFormatFontSizeCommand } from '../../commands/commands/inline-format.command';
|
||||
import { InsertBulletListBellowCommand, InsertCheckListBellowCommand, InsertOrderListBellowCommand } from '../../commands/commands/list.command';
|
||||
import { H1HeadingCommand, H2HeadingCommand, H3HeadingCommand, H4HeadingCommand, H5HeadingCommand, NormalTextHeadingCommand, SubtitleHeadingCommand, TitleHeadingCommand } from '../../commands/commands/set-heading.command';
|
||||
import { disableMenuWhenNoDocRange, getParagraphStyleAtCursor } from './menu';
|
||||
|
||||
const HEADING_MAP: Record<NamedStyleType, ICommand> = {
|
||||
[NamedStyleType.HEADING_1]: H1HeadingCommand,
|
||||
[NamedStyleType.HEADING_2]: H2HeadingCommand,
|
||||
[NamedStyleType.HEADING_3]: H3HeadingCommand,
|
||||
[NamedStyleType.HEADING_4]: H4HeadingCommand,
|
||||
[NamedStyleType.HEADING_5]: H5HeadingCommand,
|
||||
[NamedStyleType.NORMAL_TEXT]: NormalTextHeadingCommand,
|
||||
[NamedStyleType.TITLE]: TitleHeadingCommand,
|
||||
[NamedStyleType.SUBTITLE]: SubtitleHeadingCommand,
|
||||
[NamedStyleType.NAMED_STYLE_TYPE_UNSPECIFIED]: NormalTextHeadingCommand,
|
||||
};
|
||||
|
||||
export const HEADING_ICON_MAP: Record<NamedStyleType, { key: string; component: React.ComponentType<{ className: string }> }> = {
|
||||
[NamedStyleType.HEADING_1]: { key: 'H1Single', component: H1Single },
|
||||
[NamedStyleType.HEADING_2]: { key: 'H2Single', component: H2Single },
|
||||
[NamedStyleType.HEADING_3]: { key: 'H3Single', component: H3Single },
|
||||
[NamedStyleType.HEADING_4]: { key: 'H4Single', component: H4Single },
|
||||
[NamedStyleType.HEADING_5]: { key: 'H5Single', component: H5Single },
|
||||
[NamedStyleType.NORMAL_TEXT]: { key: 'TextTypeSingle', component: TextTypeSingle },
|
||||
[NamedStyleType.TITLE]: { key: 'TextTypeSingle', component: TextTypeSingle },
|
||||
[NamedStyleType.SUBTITLE]: { key: 'TextTypeSingle', component: TextTypeSingle },
|
||||
[NamedStyleType.NAMED_STYLE_TYPE_UNSPECIFIED]: { key: 'TextTypeSingle', component: TextTypeSingle },
|
||||
};
|
||||
|
||||
const createHeadingSelectorMenuItemFactory = (headingType: NamedStyleType) => (accessor: IAccessor): IMenuItem => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const componentManager = accessor.get(ComponentManager);
|
||||
const icon = HEADING_ICON_MAP[headingType];
|
||||
if (!componentManager.get(icon.key)) {
|
||||
componentManager.register(icon.key, icon.component);
|
||||
}
|
||||
|
||||
return {
|
||||
id: HEADING_MAP[headingType]!.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: icon.key,
|
||||
tooltip: 'toolbar.heading.tooltip',
|
||||
disabled$: disableMenuWhenNoDocRange(accessor),
|
||||
hidden$: getMenuHiddenObservable(accessor, UniverInstanceType.UNIVER_DOC),
|
||||
activated$: new Observable((subscriber) => {
|
||||
const DEFAULT_TYPE = NamedStyleType.NORMAL_TEXT;
|
||||
const calc = () => {
|
||||
const paragraph = getParagraphStyleAtCursor(accessor);
|
||||
if (paragraph == null) {
|
||||
subscriber.next(DEFAULT_TYPE === headingType);
|
||||
return;
|
||||
}
|
||||
|
||||
const namedStyleType = paragraph.paragraphStyle?.namedStyleType ?? DEFAULT_TYPE;
|
||||
subscriber.next(namedStyleType === headingType);
|
||||
};
|
||||
|
||||
const disposable = commandService.onCommandExecuted((c) => {
|
||||
const id = c.id;
|
||||
|
||||
if (id === SetTextSelectionsOperation.id || id === SetInlineFormatFontSizeCommand.id) {
|
||||
calc();
|
||||
}
|
||||
});
|
||||
|
||||
calc();
|
||||
return disposable.dispose;
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const H1HeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.HEADING_1);
|
||||
export const H2HeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.HEADING_2);
|
||||
export const H3HeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.HEADING_3);
|
||||
export const H4HeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.HEADING_4);
|
||||
export const H5HeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.HEADING_5);
|
||||
export const NormalTextHeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.NORMAL_TEXT);
|
||||
export const TitleHeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.TITLE);
|
||||
export const SubtitleHeadingMenuItemFactory = createHeadingSelectorMenuItemFactory(NamedStyleType.SUBTITLE);
|
||||
|
||||
export const CopyCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocCopyCurrentParagraphCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'Copy',
|
||||
title: 'rightClick.copy',
|
||||
};
|
||||
};
|
||||
|
||||
export const CutCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DocCutCurrentParagraphCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'CutSingle',
|
||||
title: 'rightClick.cut',
|
||||
};
|
||||
};
|
||||
|
||||
export const DeleteCurrentParagraphMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: DeleteCurrentParagraphCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'DeleteSingle',
|
||||
title: 'rightClick.delete',
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertBulletListBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertBulletListBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'UnorderSingle',
|
||||
title: 'rightClick.bulletList',
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertOrderListBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertOrderListBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'OrderSingle',
|
||||
title: 'rightClick.orderList',
|
||||
};
|
||||
};
|
||||
|
||||
export const InsertCheckListBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertCheckListBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'TodoList',
|
||||
title: 'rightClick.checkList',
|
||||
};
|
||||
};
|
||||
|
||||
// export const InsertTableBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
// return {
|
||||
// id: InsertTableBellowCommand.id,
|
||||
// type: MenuItemType.BUTTON,
|
||||
// icon: 'Table',
|
||||
// title: 'rightClick.insertTableBellow',
|
||||
// };
|
||||
// };
|
||||
|
||||
export const InsertHorizontalLineBellowMenuItemFactory = (accessor: IAccessor): IMenuItem => {
|
||||
return {
|
||||
id: InsertHorizontalLineBellowCommand.id,
|
||||
type: MenuItemType.BUTTON,
|
||||
icon: 'ReduceSingle',
|
||||
title: 'toolbar.horizontalLine',
|
||||
};
|
||||
};
|
||||
@@ -36,12 +36,12 @@ import { IShortcutService } from '@univerjs/ui';
|
||||
import { DOC_UI_PLUGIN_NAME } from './basics/const/plugin-name';
|
||||
import { AfterSpaceCommand, EnterCommand, TabCommand } from './commands/commands/auto-format.command';
|
||||
import { BreakLineCommand } from './commands/commands/break-line.command';
|
||||
import { DocCopyCommand, DocCutCommand, DocPasteCommand } from './commands/commands/clipboard.command';
|
||||
import { DocCopyCommand, DocCopyCurrentParagraphCommand, DocCutCommand, DocCutCurrentParagraphCommand, DocPasteCommand } from './commands/commands/clipboard.command';
|
||||
import { CutContentCommand, InnerPasteCommand } from './commands/commands/clipboard.inner.command';
|
||||
import { DeleteCommand, InsertCommand, UpdateCommand } from './commands/commands/core-editing.command';
|
||||
import { DeleteCustomBlockCommand, DeleteLeftCommand, DeleteRightCommand, MergeTwoParagraphCommand, RemoveHorizontalLineCommand } from './commands/commands/doc-delete.command';
|
||||
import { DeleteCurrentParagraphCommand, DeleteCustomBlockCommand, DeleteLeftCommand, DeleteRightCommand, MergeTwoParagraphCommand, RemoveHorizontalLineCommand } from './commands/commands/doc-delete.command';
|
||||
import { CloseHeaderFooterCommand } from './commands/commands/doc-header-footer.command';
|
||||
import { HorizontalLineCommand } from './commands/commands/doc-horizontal-line.command';
|
||||
import { HorizontalLineCommand, InsertHorizontalLineBellowCommand } from './commands/commands/doc-horizontal-line.command';
|
||||
import { DocParagraphSettingCommand } from './commands/commands/doc-paragraph-setting.command';
|
||||
import { DocSelectAllCommand } from './commands/commands/doc-select-all.command';
|
||||
import { IMEInputCommand } from './commands/commands/ime-input.command';
|
||||
@@ -65,6 +65,9 @@ import {
|
||||
ChangeListNestingLevelCommand,
|
||||
ChangeListTypeCommand,
|
||||
CheckListCommand,
|
||||
InsertBulletListBellowCommand,
|
||||
InsertCheckListBellowCommand,
|
||||
InsertOrderListBellowCommand,
|
||||
ListOperationCommand,
|
||||
OrderListCommand,
|
||||
QuickListCommand,
|
||||
@@ -73,7 +76,7 @@ import {
|
||||
import { AlignCenterCommand, AlignJustifyCommand, AlignLeftCommand, AlignOperationCommand, AlignRightCommand } from './commands/commands/paragraph-align.command';
|
||||
import { CoverContentCommand, ReplaceContentCommand, ReplaceSelectionCommand, ReplaceSnapshotCommand, ReplaceTextRunsCommand } from './commands/commands/replace-content.command';
|
||||
import { SetDocZoomRatioCommand } from './commands/commands/set-doc-zoom-ratio.command';
|
||||
import { QuickHeadingCommand, SetParagraphNamedStyleCommand } from './commands/commands/set-heading.command';
|
||||
import { H1HeadingCommand, H2HeadingCommand, H3HeadingCommand, H4HeadingCommand, H5HeadingCommand, NormalTextHeadingCommand, QuickHeadingCommand, SetParagraphNamedStyleCommand, SubtitleHeadingCommand, TitleHeadingCommand } from './commands/commands/set-heading.command';
|
||||
import { SwitchDocModeCommand } from './commands/commands/switch-doc-mode.command';
|
||||
import { CreateDocTableCommand } from './commands/commands/table/doc-table-create.command';
|
||||
import { DocTableDeleteColumnsCommand, DocTableDeleteRowsCommand, DocTableDeleteTableCommand } from './commands/commands/table/doc-table-delete.command';
|
||||
@@ -115,6 +118,7 @@ import { DocEventManagerService } from './services/doc-event-manager.service';
|
||||
import { DocIMEInputManagerService } from './services/doc-ime-input-manager.service';
|
||||
import { DocMenuStyleService } from './services/doc-menu-style.service';
|
||||
import { DocPageLayoutService } from './services/doc-page-layout.service';
|
||||
import { DocParagraphMenuService } from './services/doc-paragraph-menu.service';
|
||||
import { DocCanvasPopManagerService } from './services/doc-popup-manager.service';
|
||||
import { DocStateChangeManagerService } from './services/doc-state-change-manager.service';
|
||||
import { DocsRenderService } from './services/docs-render.service';
|
||||
@@ -194,6 +198,7 @@ export class UniverDocsUIPlugin extends Plugin {
|
||||
]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initCommand() {
|
||||
[
|
||||
DeleteLeftCommand,
|
||||
@@ -265,6 +270,21 @@ export class UniverDocsUIPlugin extends Plugin {
|
||||
InsertCustomRangeCommand,
|
||||
SetParagraphNamedStyleCommand,
|
||||
QuickHeadingCommand,
|
||||
DeleteCurrentParagraphCommand,
|
||||
DocCopyCurrentParagraphCommand,
|
||||
DocCutCurrentParagraphCommand,
|
||||
H1HeadingCommand,
|
||||
H2HeadingCommand,
|
||||
H3HeadingCommand,
|
||||
H4HeadingCommand,
|
||||
H5HeadingCommand,
|
||||
NormalTextHeadingCommand,
|
||||
TitleHeadingCommand,
|
||||
SubtitleHeadingCommand,
|
||||
InsertBulletListBellowCommand,
|
||||
InsertOrderListBellowCommand,
|
||||
InsertCheckListBellowCommand,
|
||||
InsertHorizontalLineBellowCommand,
|
||||
].forEach((e) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(e));
|
||||
});
|
||||
@@ -352,6 +372,7 @@ export class UniverDocsUIPlugin extends Plugin {
|
||||
private _initRenderModules(): void {
|
||||
([
|
||||
[DocEventManagerService],
|
||||
[DocParagraphMenuService],
|
||||
[DocBackScrollRenderController],
|
||||
[DocSelectionRenderController],
|
||||
[DocHeaderFooterController],
|
||||
|
||||
@@ -98,6 +98,12 @@ const locale: typeof zhCN = {
|
||||
fixedValue: 'Fixed Value(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: 'Bullet list',
|
||||
orderList: 'Ordered list',
|
||||
checkList: 'Task list',
|
||||
insertBellow: 'Insert below',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -98,6 +98,12 @@ const locale: typeof zhCN = {
|
||||
fixedValue: 'مقدار ثابت(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: 'لیست بدون شماره',
|
||||
orderList: 'لیست شمارهدار',
|
||||
checkList: 'لیست وظیفه',
|
||||
insertBellow: 'درج در پایین',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -98,6 +98,12 @@ const locale: typeof enUS = {
|
||||
fixedValue: 'Valeur fixe(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: 'Liste non ordonnée',
|
||||
orderList: 'Liste ordonnée',
|
||||
checkList: 'Liste de tâches',
|
||||
insertBellow: 'Insérer dans le bas',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -98,6 +98,12 @@ const locale: typeof zhCN = {
|
||||
fixedValue: 'Fixed Value(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: 'Неупорядоченный список',
|
||||
orderList: 'Упорядоченный список',
|
||||
checkList: 'Список задач',
|
||||
insertBellow: 'Вставить ниже',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -98,6 +98,12 @@ const locale: typeof zhCN = {
|
||||
fixedValue: 'Fixed Value(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: 'Danh sách không thứ tự',
|
||||
orderList: 'Danh sách có thứ tự',
|
||||
checkList: 'Danh sách công việc',
|
||||
insertBellow: 'Chèn dưới',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -96,6 +96,12 @@ const locale = {
|
||||
fixedValue: '固定值(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: '无序列表',
|
||||
orderList: '有序列表',
|
||||
checkList: '任务列表',
|
||||
insertBellow: '在下方插入',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -98,6 +98,12 @@ const locale: typeof zhCN = {
|
||||
fixedValue: 'Fixed Value(px)',
|
||||
},
|
||||
},
|
||||
rightClick: {
|
||||
bulletList: '無序列表',
|
||||
orderList: '有序列表',
|
||||
checkList: '任務列表',
|
||||
insertBellow: '在下方插入',
|
||||
},
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDisposable, IDocumentBody, IDocumentData, Nullable } from '@univerjs/core';
|
||||
import type { IDisposable, IDocumentBody, IDocumentData } from '@univerjs/core';
|
||||
import type { IDocImage } from '@univerjs/docs-drawing';
|
||||
import type { IRectRangeWithStyle, ITextRangeWithStyle } from '@univerjs/engine-render';
|
||||
|
||||
@@ -53,8 +53,8 @@ export interface IDocClipboardHook {
|
||||
}
|
||||
|
||||
export interface IDocClipboardService {
|
||||
copy(sliceType?: SliceBodyType): Promise<boolean>;
|
||||
cut(): Promise<boolean>;
|
||||
copy(sliceType?: SliceBodyType, ranges?: ITextRangeWithStyle[]): Promise<boolean>;
|
||||
cut(ranges?: ITextRangeWithStyle[]): Promise<boolean>;
|
||||
paste(items: ClipboardItem[]): Promise<boolean>;
|
||||
legacyPaste(options: { html?: string; text?: string; files: File[] }): Promise<boolean>;
|
||||
addClipboardHook(hook: IDocClipboardHook): IDisposable;
|
||||
@@ -110,16 +110,15 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
super();
|
||||
}
|
||||
|
||||
async copy(sliceType: SliceBodyType = SliceBodyType.copy): Promise<boolean> {
|
||||
const { newSnapshotList = [], needCache = false, snapshot } = this._getDocumentBodyInRanges(sliceType) ?? {};
|
||||
async copy(sliceType: SliceBodyType = SliceBodyType.copy, ranges?: ITextRangeWithStyle[]): Promise<boolean> {
|
||||
const { newSnapshotList = [], needCache = false, snapshot, ranges: allRanges } = this._getDocumentBodyInRanges(sliceType, ranges) ?? {};
|
||||
|
||||
if (newSnapshotList.length === 0 || snapshot == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeRange = this._docSelectionManagerService.getActiveTextRange();
|
||||
const isCopyInHeaderFooter = !!activeRange?.segmentId;
|
||||
const isCopyInHeaderFooter = !!allRanges?.[0]?.segmentId;
|
||||
|
||||
this._setClipboardData(newSnapshotList, !isCopyInHeaderFooter && needCache);
|
||||
} catch (e) {
|
||||
@@ -130,8 +129,8 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
return true;
|
||||
}
|
||||
|
||||
async cut(): Promise<boolean> {
|
||||
return this._cut();
|
||||
async cut(ranges?: ITextRangeWithStyle[]): Promise<boolean> {
|
||||
return this._cut(ranges);
|
||||
}
|
||||
|
||||
async paste(items: ClipboardItem[]): Promise<boolean> {
|
||||
@@ -164,15 +163,14 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
return this._paste(partDocData);
|
||||
}
|
||||
|
||||
private async _cut(): Promise<boolean> {
|
||||
private async _cut(ranges?: ITextRangeWithStyle[]): Promise<boolean> {
|
||||
const textRanges = ranges?.filter((range) => range.rangeType === DOC_RANGE_TYPE.TEXT) ?? this._docSelectionManagerService.getTextRanges() ?? [];
|
||||
const rectRanges = ranges?.filter((range) => range.rangeType === DOC_RANGE_TYPE.RECT) as IRectRangeWithStyle[] ?? this._docSelectionManagerService.getRectRanges() ?? [];
|
||||
const {
|
||||
segmentId,
|
||||
endOffset: activeEndOffset,
|
||||
style,
|
||||
} = this._docSelectionManagerService.getActiveTextRange() ?? {};
|
||||
const textRanges = this._docSelectionManagerService.getTextRanges() ?? [];
|
||||
const rectRanges = this._docSelectionManagerService.getRectRanges() ?? [];
|
||||
|
||||
} = textRanges[0] ?? {};
|
||||
if (segmentId == null) {
|
||||
this._logService.error('[DocClipboardController] segmentId is not existed');
|
||||
}
|
||||
@@ -182,7 +180,7 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
}
|
||||
|
||||
// Set content to clipboard.
|
||||
this.copy(SliceBodyType.cut);
|
||||
this.copy(SliceBodyType.cut, ranges);
|
||||
|
||||
try {
|
||||
let cursor = 0;
|
||||
@@ -212,7 +210,12 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
},
|
||||
];
|
||||
|
||||
return this._commandService.executeCommand(CutContentCommand.id, { segmentId, textRanges: newTextRanges });
|
||||
return this._commandService.executeCommand(CutContentCommand.id, {
|
||||
segmentId,
|
||||
textRanges: newTextRanges,
|
||||
rectRanges,
|
||||
selections: textRanges,
|
||||
});
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
} catch (_e) {
|
||||
this._logService.error('[DocClipboardController] cut content failed');
|
||||
@@ -353,13 +356,9 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
});
|
||||
}
|
||||
|
||||
private _getDocumentBodyInRanges(sliceType: SliceBodyType): Nullable<{
|
||||
newSnapshotList: IDocumentData[];
|
||||
needCache: boolean;
|
||||
snapshot: IDocumentData;
|
||||
}> {
|
||||
private _getDocumentBodyInRanges(sliceType: SliceBodyType, ranges?: ITextRangeWithStyle[]) {
|
||||
const docDataModel = this._univerInstanceService.getCurrentUniverDocInstance();
|
||||
const allRanges = this._docSelectionManagerService.getDocRanges();
|
||||
const allRanges = ranges ?? this._docSelectionManagerService.getDocRanges();
|
||||
|
||||
const results: IDocumentData['body'][] = [];
|
||||
let needCache = true;
|
||||
@@ -415,6 +414,7 @@ export class DocClipboardService extends Disposable implements IDocClipboardServ
|
||||
newSnapshotList: results.map((e) => ({ ...snapshot, body: e })),
|
||||
needCache,
|
||||
snapshot,
|
||||
ranges: allRanges,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel, ICustomRange, IParagraph, IParagraphRange, ITextRangeParam, Nullable } from '@univerjs/core';
|
||||
import type { Documents, DocumentSkeleton, IBoundRectNoAngle, IDocumentSkeletonGlyph, IDocumentSkeletonPage, IMouseEvent, IPointerEvent, IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { BuildTextUtils, Disposable, fromEventSubject, Inject } from '@univerjs/core';
|
||||
import type { DocumentDataModel, ICustomRange, IParagraph, ITextRangeParam, Nullable } from '@univerjs/core';
|
||||
import type { Documents, DocumentSkeleton, IBoundRectNoAngle, IDocumentSkeletonGlyph, IDocumentSkeletonPage, IDocumentSkeletonSection, IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import { Disposable, fromEventSubject, Inject } from '@univerjs/core';
|
||||
import { DocSkeletonManagerService } from '@univerjs/docs';
|
||||
import { CURSOR_TYPE, TRANSFORM_CHANGE_OBSERVABLE_TYPE } from '@univerjs/engine-render';
|
||||
import { BehaviorSubject, distinctUntilChanged, filter, map, Subject, switchMap, take, throttleTime } from 'rxjs';
|
||||
@@ -39,11 +39,6 @@ export interface IBulletBound {
|
||||
paragraph: IParagraph;
|
||||
}
|
||||
|
||||
export interface IParagraphBound {
|
||||
rect: IBoundRectNoAngle;
|
||||
paragraph: IParagraphRange;
|
||||
}
|
||||
|
||||
const calcDocRangePositions = (range: ITextRangeParam, documents: Documents, skeleton: DocumentSkeleton, pageIndex: number): IBoundRectNoAngle[] | undefined => {
|
||||
const startPosition = skeleton.findNodePositionByCharIndex(range.startOffset, true, range.segmentId, pageIndex);
|
||||
const skeletonData = skeleton.getSkeletonData();
|
||||
@@ -72,52 +67,61 @@ const calcDocRangePositions = (range: ITextRangeParam, documents: Documents, ske
|
||||
}));
|
||||
};
|
||||
|
||||
const calcDocParagraphPositions = (startIndex: number, endIndex: number, documents: Documents, skeleton: DocumentSkeleton): Nullable<IBoundRectNoAngle> => {
|
||||
const startPosition = skeleton.findNodePositionByCharIndex(startIndex, true, undefined, -1);
|
||||
const endPosition = skeleton.findNodePositionByCharIndex(endIndex, true, undefined, -1);
|
||||
if (!endPosition || !startPosition) {
|
||||
return null;
|
||||
}
|
||||
interface ICustomRangeBoundBase {
|
||||
paragraphStart: number;
|
||||
paragraphEnd: number;
|
||||
startIndex: number;
|
||||
rect: IBoundRectNoAngle;
|
||||
fisrtLine: IBoundRectNoAngle;
|
||||
}
|
||||
|
||||
try {
|
||||
const documentOffsetConfig = documents.getOffsetConfig();
|
||||
const convertor = new NodePositionConvertToCursor(documentOffsetConfig, skeleton);
|
||||
const { borderBoxPointGroup } = convertor.getRangePointData(startPosition, endPosition);
|
||||
const bounds = getLineBounding(borderBoxPointGroup);
|
||||
if (!bounds.length) {
|
||||
return null;
|
||||
}
|
||||
const rects = bounds.map((rect) => ({
|
||||
top: rect.top + documentOffsetConfig.docsTop - DOC_VERTICAL_PADDING,
|
||||
bottom: rect.bottom + documentOffsetConfig.docsTop + DOC_VERTICAL_PADDING,
|
||||
left: rect.left + documentOffsetConfig.docsLeft,
|
||||
right: rect.right + documentOffsetConfig.docsLeft,
|
||||
}));
|
||||
const calcDocParagraphPositions = (sections: IDocumentSkeletonSection[], top: number, left: number, pageWidth: number): ICustomRangeBoundBase[] => {
|
||||
const paragraphBounds: ICustomRangeBoundBase[] = [];
|
||||
for (const section of sections) {
|
||||
const sectionTop = section.top;
|
||||
for (const column of section.columns) {
|
||||
const columnLeft = column.left;
|
||||
const width = section.colCount === 1 ? pageWidth : column.width;
|
||||
let currentParagraph: ICustomRangeBoundBase | null = null;
|
||||
for (const line of column.lines) {
|
||||
const startIndex = line.paragraphIndex;
|
||||
if (line.paragraphStart) {
|
||||
if (currentParagraph) {
|
||||
paragraphBounds.push(currentParagraph);
|
||||
currentParagraph = null;
|
||||
}
|
||||
const lineRect = {
|
||||
top: top + sectionTop + line.top,
|
||||
left: left + columnLeft,
|
||||
right: left + columnLeft + width,
|
||||
bottom: top + sectionTop + line.top + line.lineHeight,
|
||||
};
|
||||
currentParagraph = {
|
||||
paragraphStart: line.st,
|
||||
paragraphEnd: startIndex,
|
||||
startIndex,
|
||||
rect: lineRect,
|
||||
fisrtLine: {
|
||||
top: top + sectionTop + line.top + line.marginTop + line.paddingTop,
|
||||
left: left + columnLeft,
|
||||
right: left + columnLeft + width,
|
||||
bottom: top + sectionTop + line.top + line.marginTop + line.paddingTop + line.contentHeight,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (currentParagraph && currentParagraph.startIndex === startIndex) {
|
||||
currentParagraph.rect.bottom = top + sectionTop + line.top + line.lineHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let left;
|
||||
let right;
|
||||
let top;
|
||||
let bottom;
|
||||
|
||||
for (const rect of rects) {
|
||||
if (left === undefined || rect.left < left) {
|
||||
left = rect.left;
|
||||
}
|
||||
if (right === undefined || rect.right > right) {
|
||||
right = rect.right;
|
||||
}
|
||||
if (top === undefined || rect.top < top) {
|
||||
top = rect.top;
|
||||
}
|
||||
if (bottom === undefined || rect.bottom > bottom) {
|
||||
bottom = rect.bottom;
|
||||
if (currentParagraph) {
|
||||
paragraphBounds.push(currentParagraph);
|
||||
}
|
||||
}
|
||||
|
||||
return { left: left!, right: right!, top: top!, bottom: bottom! };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return paragraphBounds;
|
||||
};
|
||||
|
||||
export const calcDocGlyphPosition = (glyph: IDocumentSkeletonGlyph, documents: Documents, skeleton: DocumentSkeleton, pageIndex = -1): IBoundRectNoAngle | undefined => {
|
||||
@@ -155,6 +159,52 @@ interface IBulletActive {
|
||||
rect: IBoundRectNoAngle;
|
||||
}
|
||||
|
||||
export interface IMutiPageParagraphBound {
|
||||
rect: IBoundRectNoAngle;
|
||||
paragraphStart: number;
|
||||
paragraphEnd: number;
|
||||
startIndex: number;
|
||||
rects: IBoundRectNoAngle[];
|
||||
pageIndex: number;
|
||||
segmentId?: string;
|
||||
firstLine: IBoundRectNoAngle;
|
||||
}
|
||||
|
||||
export interface ITableParagraphBound {
|
||||
rect: IBoundRectNoAngle;
|
||||
paragraphStart: number;
|
||||
paragraphEnd: number;
|
||||
startIndex: number;
|
||||
pageIndex: number;
|
||||
segmentId?: string;
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
firstLine: IBoundRectNoAngle;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
export interface ITableBound {
|
||||
rect: IBoundRectNoAngle;
|
||||
pageIndex: number;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
export interface ITableCellBound {
|
||||
rect: IBoundRectNoAngle;
|
||||
pageIndex: number;
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
function isPointInRect(x: number, y: number, rect: IBoundRectNoAngle) {
|
||||
const { left, right, top, bottom } = rect;
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export class DocEventManagerService extends Disposable implements IRenderModule {
|
||||
private readonly _hoverCustomRanges$ = new BehaviorSubject<ICustomRangeActive[]>([]);
|
||||
readonly hoverCustomRanges$ = this._hoverCustomRanges$.pipe(distinctUntilChanged((pre, aft) => pre.length === aft.length && pre.every((item, i) => aft[i].range.rangeId === item.range.rangeId && aft[i].segmentId === item.segmentId && aft[i].segmentPageIndex === item.segmentPageIndex && aft[i].range.startIndex === item.range.startIndex)));
|
||||
@@ -168,6 +218,22 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
private readonly _clickBullet$ = new Subject<IBulletActive>();
|
||||
readonly clickBullets$ = this._clickBullet$.asObservable();
|
||||
|
||||
private readonly _hoverParagraph$ = new Subject<Nullable<IMutiPageParagraphBound>>();
|
||||
readonly hoverParagraph$ = this._hoverParagraph$.pipe(distinctUntilChanged((pre, aft) => pre?.startIndex === aft?.startIndex && pre?.segmentId === aft?.segmentId && pre?.pageIndex === aft?.pageIndex));
|
||||
readonly hoverParagraphRealTime$ = this._hoverParagraph$.asObservable();
|
||||
|
||||
private readonly _hoverParagraphLeft$ = new Subject<Nullable<IMutiPageParagraphBound>>();
|
||||
readonly hoverParagraphLeft$ = this._hoverParagraphLeft$.pipe(distinctUntilChanged((pre, aft) => pre?.startIndex === aft?.startIndex && pre?.segmentId === aft?.segmentId && pre?.pageIndex === aft?.pageIndex));
|
||||
readonly hoverParagraphLeftRealTime$ = this._hoverParagraphLeft$.asObservable();
|
||||
|
||||
private readonly _hoverTableCell$ = new Subject<Nullable<ITableCellBound>>();
|
||||
readonly hoverTableCell$ = this._hoverTableCell$.pipe(distinctUntilChanged((pre, aft) => pre?.rowIndex === aft?.rowIndex && pre?.colIndex === aft?.colIndex && pre?.tableId === aft?.tableId && pre?.pageIndex === aft?.pageIndex));
|
||||
readonly hoverTableCellRealTime$ = this._hoverTableCell$.asObservable();
|
||||
|
||||
private readonly _hoverTable$ = new Subject<Nullable<ITableBound>>();
|
||||
readonly hoverTable$ = this._hoverTable$.pipe(distinctUntilChanged((pre, aft) => pre?.tableId === aft?.tableId && pre?.pageIndex === aft?.pageIndex));
|
||||
readonly hoverTableRealTime$ = this._hoverTable$.asObservable();
|
||||
|
||||
private _customRangeDirty = true;
|
||||
private _bulletDirty = true;
|
||||
private _paragraphDirty = true;
|
||||
@@ -186,7 +252,13 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
* cache the bounding of paragraphs,
|
||||
* it will be updated when the doc-skeleton is recalculated
|
||||
*/
|
||||
private _paragraphBounds: IParagraphBound[] = [];
|
||||
private _paragraphBounds: Map<number, IMutiPageParagraphBound> = new Map();
|
||||
private _paragraphLeftBounds: IMutiPageParagraphBound[] = [];
|
||||
private _tableParagraphBounds: Map<string, ITableParagraphBound[]> = new Map();
|
||||
private _segmentParagraphBounds: Map<string, Map<number, IMutiPageParagraphBound[]>> = new Map();
|
||||
|
||||
private _tableCellBounds: Map<string, ITableCellBound[]> = new Map();
|
||||
private _tableBounds: Map<string, ITableBound> = new Map();
|
||||
|
||||
private get _skeleton() {
|
||||
return this._docSkeletonManagerService.getSkeleton();
|
||||
@@ -245,11 +317,18 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
|
||||
private _initEvents() {
|
||||
this.disposeWithMe(fromEventSubject(this._context.scene.onPointerMove$).pipe(throttleTime(30)).subscribe((evt) => {
|
||||
const { x, y } = transformOffset2Bound(evt.offsetX, evt.offsetY, this._context.scene);
|
||||
this._hoverCustomRanges$.next(
|
||||
this._calcActiveRanges(evt)
|
||||
this._calcActiveRanges({ x, y })
|
||||
);
|
||||
this._hoverParagraph$.next(
|
||||
this._calcActiveParagraph({ x, y })
|
||||
);
|
||||
this._hoverParagraphLeft$.next(
|
||||
this._calcActiveParagraphLeft({ x, y })
|
||||
);
|
||||
this._hoverBullet$.next(
|
||||
this._calcActiveBullet(evt)
|
||||
this._calcActiveBullet({ x, y })
|
||||
);
|
||||
}));
|
||||
this.disposeWithMe(this._context.scene.onPointerEnter$.subscribeEvent(() => {
|
||||
@@ -262,7 +341,6 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
this.disposeWithMe(onPointerDown$.pipe(
|
||||
switchMap((down) => onPointerUp$.pipe(take(1), map((up) => ({ down, up })))),
|
||||
filter(({ down, up }) => down.target === up.target && up.timeStamp - down.timeStamp < 300)
|
||||
// filter(({ down, up }) => down.offsetX === up.offsetX && down.offsetY === up.offsetY)
|
||||
).subscribe(({ down }) => {
|
||||
if (down.button === 2) {
|
||||
return;
|
||||
@@ -327,19 +405,12 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
this._customRangeBounds = customRangeBounds;
|
||||
}
|
||||
|
||||
private _calcActiveRanges(evt: IPointerEvent | IMouseEvent) {
|
||||
private _calcActiveRanges(evt: { x: number; y: number }) {
|
||||
this._buildCustomRangeBounds();
|
||||
|
||||
const { offsetX, offsetY } = evt;
|
||||
const { x, y } = transformOffset2Bound(offsetX, offsetY, this._context.scene);
|
||||
const { x, y } = evt;
|
||||
const matchedRanges = this._customRangeBounds.filter((layout) => {
|
||||
return layout.rects.some((rect) => {
|
||||
const { left, right, top, bottom } = rect;
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return layout.rects.some((rect) => isPointInRect(x, y, rect));
|
||||
});
|
||||
return matchedRanges.map(
|
||||
(range) => ({
|
||||
@@ -433,31 +504,137 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
});
|
||||
}
|
||||
|
||||
private _calcActiveBullet(evt: IPointerEvent | IMouseEvent) {
|
||||
private _calcActiveBullet(evt: { x: number; y: number }) {
|
||||
this._buildBulletBounds();
|
||||
|
||||
const { offsetX, offsetY } = evt;
|
||||
const { x, y } = transformOffset2Bound(offsetX, offsetY, this._context.scene);
|
||||
const bullet = this._bulletBounds.find((layout) => {
|
||||
const { left, right, top, bottom } = layout.rect;
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const { x, y } = evt;
|
||||
const bullet = this._bulletBounds.find((layout) => isPointInRect(x, y, layout.rect));
|
||||
return bullet;
|
||||
}
|
||||
|
||||
private _buildParagraphBoundsBySegment(segmentId?: string, pageIndex = -1) {
|
||||
const paragraphs = BuildTextUtils.paragraph.util.transform(this._context.unit.getBody()?.paragraphs ?? []);
|
||||
return paragraphs.map((paragraph) => {
|
||||
return {
|
||||
paragraph,
|
||||
segmentId,
|
||||
pageIndex,
|
||||
rect: calcDocParagraphPositions(paragraph.paragraphStart, paragraph.paragraphEnd, this._documents, this._skeleton),
|
||||
} as IParagraphBound;
|
||||
}).filter((layout) => layout.rect !== null);
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _buildParagraphBoundsBySegment(segmentId?: string) {
|
||||
const skeletonData = this._skeleton.getSkeletonData();
|
||||
const documents = this._documents;
|
||||
const documentOffsetConfig = documents.getOffsetConfig();
|
||||
if (!skeletonData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
const calc = (pages: IDocumentSkeletonPage[]) => {
|
||||
const paragraphMap: Map<number, IMutiPageParagraphBound> = new Map();
|
||||
const handlePage = (page: IDocumentSkeletonPage, pageIndex: number, top: number, left: number) => {
|
||||
const bounds = calcDocParagraphPositions(page.sections, top, left, page.pageWidth - page.marginLeft - page.marginRight);
|
||||
|
||||
bounds.forEach((bound) => {
|
||||
if (!paragraphMap.has(bound.startIndex)) {
|
||||
paragraphMap.set(bound.startIndex, {
|
||||
rect: bound.rect,
|
||||
paragraphStart: bound.paragraphStart,
|
||||
paragraphEnd: bound.paragraphEnd,
|
||||
startIndex: bound.startIndex,
|
||||
rects: [bound.rect],
|
||||
pageIndex,
|
||||
segmentId,
|
||||
firstLine: bound.fisrtLine,
|
||||
});
|
||||
} else {
|
||||
const current = paragraphMap.get(bound.startIndex);
|
||||
if (current) {
|
||||
current.rect.bottom = bound.rect.bottom;
|
||||
current.rects.push(bound.rect);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const top = ((page.pageHeight === Infinity ? 0 : page.pageHeight) + documentOffsetConfig.pageMarginTop) * i + page.marginTop + documentOffsetConfig.docsTop;
|
||||
const left = page.marginLeft + documentOffsetConfig.docsLeft;
|
||||
if (page.skeTables) {
|
||||
Array.from(page.skeTables.values()).forEach((table) => {
|
||||
const tableLeft = table.left + left;
|
||||
const tableTop = table.top + top;
|
||||
const tableRight = tableLeft + table.width;
|
||||
const tableBottom = tableTop + table.height;
|
||||
const tableId = table.tableId;
|
||||
this._tableBounds.set(tableId, {
|
||||
rect: {
|
||||
left: tableLeft,
|
||||
top: tableTop,
|
||||
right: tableRight,
|
||||
bottom: tableBottom,
|
||||
},
|
||||
pageIndex: i,
|
||||
tableId,
|
||||
});
|
||||
|
||||
table.rows.forEach((row, rowIndex) => {
|
||||
row.cells.forEach((cell, colIndex) => {
|
||||
const top = (((page.pageHeight === Infinity ? 0 : page.pageHeight) + documentOffsetConfig.pageMarginTop) * i) + table.top + documentOffsetConfig.docsTop + page.marginTop + row.top + cell.marginTop;
|
||||
const left = table.left + documentOffsetConfig.docsLeft + page.marginLeft + cell.left + cell.marginLeft;
|
||||
const bounds = calcDocParagraphPositions(cell.sections, top, left, cell.pageWidth - cell.marginLeft - cell.marginRight);
|
||||
let arr = this._tableParagraphBounds.get(tableId);
|
||||
if (!arr) {
|
||||
arr = [];
|
||||
this._tableParagraphBounds.set(tableId, arr);
|
||||
}
|
||||
|
||||
arr.push(...bounds.map((bound) => ({
|
||||
rect: bound.rect,
|
||||
paragraphStart: bound.paragraphStart,
|
||||
paragraphEnd: bound.paragraphEnd,
|
||||
startIndex: bound.startIndex,
|
||||
pageIndex: i,
|
||||
segmentId,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
firstLine: bound.fisrtLine,
|
||||
tableId,
|
||||
}))
|
||||
);
|
||||
|
||||
let cellBounds = this._tableCellBounds.get(tableId);
|
||||
|
||||
if (!cellBounds) {
|
||||
cellBounds = [];
|
||||
this._tableCellBounds.set(tableId, cellBounds);
|
||||
}
|
||||
|
||||
cellBounds.push({
|
||||
rect: {
|
||||
top,
|
||||
left,
|
||||
right: left + cell.pageWidth - cell.marginLeft - cell.marginRight,
|
||||
bottom: top + cell.pageHeight - cell.marginBottom - cell.marginTop,
|
||||
},
|
||||
pageIndex: i,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
tableId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handlePage(page, i, top, left);
|
||||
}
|
||||
|
||||
return paragraphMap;
|
||||
};
|
||||
|
||||
if (segmentId) {
|
||||
const page = skeletonData.skeFooters.get(segmentId)?.values() ?? skeletonData.skeHeaders.get(segmentId)?.values();
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
return calc(Array.from(page));
|
||||
}
|
||||
|
||||
return calc(skeletonData.pages);
|
||||
}
|
||||
|
||||
private _buildParagraphBounds() {
|
||||
@@ -465,21 +642,73 @@ export class DocEventManagerService extends Disposable implements IRenderModule
|
||||
return;
|
||||
}
|
||||
this._paragraphDirty = false;
|
||||
this._tableParagraphBounds = new Map();
|
||||
this._tableCellBounds = new Map();
|
||||
this._tableBounds = new Map();
|
||||
this._paragraphBounds = this._buildParagraphBoundsBySegment() ?? new Map();
|
||||
this._paragraphLeftBounds = Array.from(this._paragraphBounds.values()).map((bound) => ({
|
||||
...bound,
|
||||
rect: {
|
||||
left: bound.rect.left - 60,
|
||||
right: bound.rect.left,
|
||||
top: bound.rect.top,
|
||||
bottom: bound.rect.bottom,
|
||||
},
|
||||
}));
|
||||
const handleSegment = (segmentId: string) => {
|
||||
this._segmentParagraphBounds.set(segmentId, this._buildParagraphBoundsBySegment(segmentId) ?? new Map());
|
||||
};
|
||||
|
||||
this._paragraphBounds = [];
|
||||
this._paragraphBounds.push(...this._buildParagraphBoundsBySegment());
|
||||
|
||||
this._skeleton.getSkeletonData()?.pages.forEach((page, pageIndex) => {
|
||||
this._skeleton.getSkeletonData()?.pages.forEach((page) => {
|
||||
if (page.headerId) {
|
||||
this._paragraphBounds.push(...this._buildParagraphBoundsBySegment(page.headerId, pageIndex));
|
||||
handleSegment(page.headerId);
|
||||
}
|
||||
|
||||
if (page.footerId) {
|
||||
this._paragraphBounds.push(...this._buildParagraphBoundsBySegment(page.footerId, pageIndex));
|
||||
handleSegment(page.footerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _calcActiveParagraph(evt: { x: number; y: number }): Nullable<IMutiPageParagraphBound> {
|
||||
this._buildParagraphBounds();
|
||||
|
||||
const { x, y } = evt;
|
||||
|
||||
const table = Array.from(this._tableBounds.values()).find((bound) => isPointInRect(x, y, bound.rect));
|
||||
this._hoverTable$.next(table);
|
||||
|
||||
if (table) {
|
||||
const tableCell = this._tableCellBounds.get(table.tableId)?.find((bound) => isPointInRect(x, y, bound.rect));
|
||||
this._hoverTableCell$.next(tableCell);
|
||||
if (!tableCell) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const paragraphs = this._tableParagraphBounds.get(tableCell.tableId)
|
||||
?.filter((bound) => bound.colIndex === tableCell.colIndex && bound.rowIndex === tableCell.rowIndex);
|
||||
const paragraph = paragraphs?.find((bound) => isPointInRect(x, y, bound.rect));
|
||||
return paragraph && {
|
||||
...paragraph,
|
||||
rects: [paragraph.rect],
|
||||
};
|
||||
}
|
||||
|
||||
const paragraph = this._paragraphBounds.values().find((bounds) => {
|
||||
return bounds.rects.some((rect) => isPointInRect(x, y, rect));
|
||||
});
|
||||
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
private _calcActiveParagraphLeft(evt: { x: number; y: number }): Nullable<IMutiPageParagraphBound> {
|
||||
this._buildParagraphBounds();
|
||||
const { x, y } = evt;
|
||||
|
||||
const paragraph = this._paragraphLeftBounds.find((bound) => isPointInRect(x, y, bound.rect));
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
get paragraphBounds() {
|
||||
this._buildParagraphBounds();
|
||||
return this._paragraphBounds;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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, IParagraphRange, Nullable } from '@univerjs/core';
|
||||
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
|
||||
import type { IMutiPageParagraphBound } from './doc-event-manager.service';
|
||||
import { Disposable, Inject, isInternalEditorID } from '@univerjs/core';
|
||||
import { DocSelectionManagerService } from '@univerjs/docs';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { combineLatest, first, throttleTime } from 'rxjs';
|
||||
import { VIEWPORT_KEY } from '../basics/docs-view-key';
|
||||
import { DocEventManagerService } from './doc-event-manager.service';
|
||||
import { DocCanvasPopManagerService } from './doc-popup-manager.service';
|
||||
|
||||
export class DocParagraphMenuService extends Disposable implements IRenderModule {
|
||||
private _paragrahMenu: {
|
||||
paragraph: IParagraphRange;
|
||||
disposable: INeedCheckDisposable;
|
||||
active: boolean;
|
||||
} | null = null;
|
||||
|
||||
get activeParagraph() {
|
||||
return this._paragrahMenu?.paragraph;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _context: IRenderContext<DocumentDataModel>,
|
||||
@Inject(DocSelectionManagerService) private _docSelectionManagerService: DocSelectionManagerService,
|
||||
@Inject(DocEventManagerService) private _docEventManagerService: DocEventManagerService,
|
||||
@Inject(DocCanvasPopManagerService) private _docPopupManagerService: DocCanvasPopManagerService,
|
||||
@Inject(ComponentManager) private _componentManager: ComponentManager
|
||||
) {
|
||||
super();
|
||||
|
||||
if (isInternalEditorID(this._context.unitId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _isCursorInActiveParagraph() {
|
||||
if (!this._paragrahMenu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTextRange = this._docSelectionManagerService.getActiveTextRange();
|
||||
if (!activeTextRange?.collapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!activeTextRange?.collapsed ||
|
||||
activeTextRange.startOffset < this._paragrahMenu.paragraph.paragraphStart ||
|
||||
activeTextRange.startOffset > this._paragrahMenu.paragraph.paragraphEnd
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setParagraphMenuActive(active: boolean) {
|
||||
if (this._paragrahMenu) {
|
||||
this._paragrahMenu.active = active;
|
||||
if (!this._isCursorInActiveParagraph()) {
|
||||
this._docSelectionManagerService.replaceDocRanges([{
|
||||
startOffset: this._paragrahMenu.paragraph.paragraphStart,
|
||||
endOffset: this._paragrahMenu.paragraph.paragraphStart,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _init() {
|
||||
const handleHoverParagraph = (paragraph: Nullable<IMutiPageParagraphBound>) => {
|
||||
if (this._paragrahMenu?.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (paragraph) {
|
||||
this.showParagraphMenu(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideParagraphMenu(true);
|
||||
};
|
||||
|
||||
this.disposeWithMe(
|
||||
combineLatest([this._docEventManagerService.hoverParagraphRealTime$, this._docEventManagerService.hoverParagraphLeft$])
|
||||
.pipe(throttleTime(16))
|
||||
.subscribe(([p, left]) => {
|
||||
const paragraph = p ?? left;
|
||||
handleHoverParagraph(paragraph);
|
||||
})
|
||||
);
|
||||
|
||||
let lastScrollY = 0;
|
||||
this.disposeWithMe(this._context.scene.getViewport(VIEWPORT_KEY.VIEW_MAIN)!.onScrollAfter$.subscribeEvent((e) => {
|
||||
if (lastScrollY === e.scrollY) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastScrollY = e.scrollY;
|
||||
this.hideParagraphMenu(true);
|
||||
}));
|
||||
}
|
||||
|
||||
showParagraphMenu(paragraph: IMutiPageParagraphBound) {
|
||||
if (this._paragrahMenu?.paragraph.startIndex === paragraph.startIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideParagraphMenu(true);
|
||||
|
||||
const disposable = this._docPopupManagerService.attachPopupToRect(
|
||||
paragraph.firstLine,
|
||||
{
|
||||
componentKey: 'doc.paragraph.menu',
|
||||
direction: 'left-center',
|
||||
onClickOutside: () => {
|
||||
this._docSelectionManagerService.textSelection$.pipe(first()).subscribe(() => {
|
||||
if (!this._isCursorInActiveParagraph()) {
|
||||
this.hideParagraphMenu(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
this._context.unitId
|
||||
);
|
||||
|
||||
this._paragrahMenu = {
|
||||
paragraph,
|
||||
disposable,
|
||||
active: false,
|
||||
};
|
||||
}
|
||||
|
||||
hideParagraphMenu(force = false) {
|
||||
if (this._paragrahMenu && ((this._paragrahMenu.disposable.canDispose() || force))) {
|
||||
this._paragrahMenu.disposable.dispose();
|
||||
this._paragrahMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IDisposable, INeedCheckDisposable, ITextRangeParam } from '@univerjs/core';
|
||||
import type { INeedCheckDisposable, ITextRangeParam } from '@univerjs/core';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { BaseObject, Documents, IBoundRectNoAngle, IRender, Scene } from '@univerjs/engine-render';
|
||||
import type { IPopup } from '@univerjs/ui';
|
||||
@@ -84,7 +84,7 @@ export function transformOffset2Bound(offsetX: number, offsetY: number, scene: S
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDocCanvasPopup extends Pick<IPopup, 'direction' | 'excludeOutside' | 'componentKey' | 'offset' | 'onClickOutside' | 'hideOnInvisible'> {
|
||||
export interface IDocCanvasPopup extends Pick<IPopup, 'direction' | 'excludeOutside' | 'componentKey' | 'offset' | 'onClickOutside' | 'hideOnInvisible' | 'customActive'> {
|
||||
mask?: boolean;
|
||||
extraProps?: Record<string, any>;
|
||||
multipleDirection?: IPopup['direction'];
|
||||
@@ -134,20 +134,10 @@ export class DocCanvasPopManagerService extends Disposable {
|
||||
super();
|
||||
}
|
||||
|
||||
private _createObjectPositionObserver(
|
||||
targetObject: BaseObject,
|
||||
currentRender: IRender
|
||||
) {
|
||||
private _createRectPositionObserver(rect: IBoundRectNoAngle | (() => IBoundRectNoAngle), currentRender: IRender) {
|
||||
const calc = () => {
|
||||
const { scene, engine } = currentRender;
|
||||
const { left, top, width, height } = targetObject;
|
||||
|
||||
const bound: IBoundRectNoAngle = {
|
||||
left,
|
||||
right: left + width,
|
||||
top,
|
||||
bottom: top + height,
|
||||
};
|
||||
const bound: IBoundRectNoAngle = typeof rect === 'function' ? rect() : rect;
|
||||
const canvasElement = engine.getCanvasElement();
|
||||
const canvasClientRect = canvasElement.getBoundingClientRect();
|
||||
const widthOfCanvas = pxToNum(canvasElement.style.width); // declared width
|
||||
@@ -192,6 +182,24 @@ export class DocCanvasPopManagerService extends Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
private _createObjectPositionObserver(
|
||||
targetObject: BaseObject,
|
||||
currentRender: IRender
|
||||
) {
|
||||
const getBound = () => {
|
||||
const { left, top, width, height } = targetObject;
|
||||
const bound: IBoundRectNoAngle = {
|
||||
left,
|
||||
right: left + width,
|
||||
top,
|
||||
bottom: top + height,
|
||||
};
|
||||
return bound;
|
||||
};
|
||||
|
||||
return this._createRectPositionObserver(getBound, currentRender);
|
||||
}
|
||||
|
||||
private _createRangePositionObserver(range: ITextRangeParam, currentRender: IRender) {
|
||||
const positions = calcDocRangePositions(range, currentRender) ?? [];
|
||||
const positions$ = new BehaviorSubject(positions);
|
||||
@@ -226,6 +234,32 @@ export class DocCanvasPopManagerService extends Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
attachPopupToRect(rect: IBoundRectNoAngle, popup: IDocCanvasPopup, unitId: string): INeedCheckDisposable {
|
||||
const currentRender = this._renderManagerService.getRenderById(unitId);
|
||||
if (!currentRender) {
|
||||
throw new Error(`Current render not found, unitId: ${unitId}`);
|
||||
}
|
||||
|
||||
const { position, position$, disposable } = this._createRectPositionObserver(rect, currentRender);
|
||||
const id = this._globalPopupManagerService.addPopup({
|
||||
...popup,
|
||||
unitId,
|
||||
subUnitId: 'default',
|
||||
anchorRect: position,
|
||||
anchorRect$: position$,
|
||||
canvasElement: currentRender.engine.getCanvasElement(),
|
||||
});
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this._globalPopupManagerService.removePopup(id);
|
||||
position$.complete();
|
||||
disposable.dispose();
|
||||
},
|
||||
canDispose: () => this._globalPopupManagerService.activePopupId !== id,
|
||||
};
|
||||
}
|
||||
|
||||
// #region attach to object
|
||||
/**
|
||||
* attach a popup to canvas object
|
||||
@@ -233,7 +267,7 @@ export class DocCanvasPopManagerService extends Disposable {
|
||||
* @param popup popup item
|
||||
* @returns disposable
|
||||
*/
|
||||
attachPopupToObject(targetObject: BaseObject, popup: IDocCanvasPopup, unitId: string): IDisposable {
|
||||
attachPopupToObject(targetObject: BaseObject, popup: IDocCanvasPopup, unitId: string): INeedCheckDisposable {
|
||||
const currentRender = this._renderManagerService.getRenderById(unitId);
|
||||
if (!currentRender) {
|
||||
throw new Error(`Current render not found, unitId: ${unitId}`);
|
||||
@@ -255,6 +289,7 @@ export class DocCanvasPopManagerService extends Disposable {
|
||||
position$.complete();
|
||||
disposable.dispose();
|
||||
},
|
||||
canDispose: () => this._globalPopupManagerService.activePopupId !== id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,7 +328,7 @@ export class DocCanvasPopManagerService extends Disposable {
|
||||
? bounds.length > 1
|
||||
? multipleDirection ?? direction
|
||||
: direction
|
||||
: 'bottom',
|
||||
: direction,
|
||||
canvasElement: currentRender.engine.getCanvasElement(),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel } from '@univerjs/core';
|
||||
import type { DocumentDataModel, IParagraph } from '@univerjs/core';
|
||||
import type { ISideMenuItem } from '@univerjs/design';
|
||||
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
|
||||
import type { IParagraphBound } from '../../services/doc-event-manager.service';
|
||||
import type { IMutiPageParagraphBound } from '../../services/doc-event-manager.service';
|
||||
import { debounce, fromEventSubject, getPlainText, ICommandService, isInternalEditorID, IUniverInstanceService, NamedStyleType, UniverInstanceType } from '@univerjs/core';
|
||||
import { SideMenu } from '@univerjs/design';
|
||||
import { RichTextEditingMutation } from '@univerjs/docs';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useDependency, useEvent, useObservable } from '@univerjs/ui';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { of, throttleTime } from 'rxjs';
|
||||
import { VIEWPORT_KEY } from '../../basics/docs-view-key';
|
||||
@@ -47,21 +47,27 @@ const transformNamedStyleTypeToLevel = (type: NamedStyleType) => {
|
||||
}
|
||||
};
|
||||
|
||||
function findActiveHeading(paragraphBounds: IParagraphBound[], scrollTop: number, bottom: number) {
|
||||
const paragraphIndex = paragraphBounds.findIndex((p) => p.paragraph.paragraphStart !== p.paragraph.paragraphEnd && p.rect.top < bottom && p.rect.bottom > scrollTop);
|
||||
function findActiveHeading(boundMap: Map<number, IMutiPageParagraphBound> | undefined, paragraphMap: Map<number, IParagraph>, scrollTop: number, bottom: number) {
|
||||
if (!boundMap) {
|
||||
return undefined;
|
||||
}
|
||||
const paragraphBounds = Array.from(boundMap.values());
|
||||
const paragraphIndex = paragraphBounds.findIndex((p) => p.paragraphStart !== p.paragraphEnd && p.rect.top < bottom && p.rect.bottom > scrollTop);
|
||||
if (paragraphIndex === -1) return undefined;
|
||||
const lastParagraphIndex = paragraphBounds?.findLastIndex((p) => p.paragraph.paragraphStart !== p.paragraph.paragraphEnd && p.rect.top < bottom && p.rect.bottom > scrollTop);
|
||||
const lastParagraphIndex = paragraphBounds?.findLastIndex((p) => p.paragraphStart !== p.paragraphEnd && p.rect.top < bottom && p.rect.bottom > scrollTop);
|
||||
for (let i = paragraphIndex; i <= lastParagraphIndex; i++) {
|
||||
const paragraph = paragraphBounds[i];
|
||||
if (paragraph.paragraph.paragraphStyle?.headingId) {
|
||||
return paragraph.paragraph.paragraphStyle.headingId;
|
||||
const bound = paragraphBounds[i];
|
||||
const paragraph = paragraphMap.get(bound.startIndex);
|
||||
if (paragraph?.paragraphStyle?.headingId) {
|
||||
return paragraph.paragraphStyle.headingId;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = paragraphIndex; i >= 0; i--) {
|
||||
const paragraph = paragraphBounds[i];
|
||||
if (paragraph.paragraph.paragraphStyle?.headingId) {
|
||||
return paragraph.paragraph.paragraphStyle.headingId;
|
||||
const bound = paragraphBounds[i];
|
||||
const paragraph = paragraphMap.get(bound.startIndex);
|
||||
if (paragraph?.paragraphStyle?.headingId) {
|
||||
return paragraph.paragraphStyle.headingId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,20 +93,39 @@ export function DocSideMenu() {
|
||||
const canvasHeight = renderer?.engine.height ?? 0;
|
||||
const scaleY = renderer?.scene.scaleY ?? 1;
|
||||
const bottom = scrollTop + (canvasHeight / scaleY);
|
||||
const paragraphs = currentDoc?.getBody()?.paragraphs ?? [];
|
||||
const paragraphMap = useMemo(() => {
|
||||
const map = new Map<number, IParagraph>();
|
||||
paragraphs.forEach((p) => {
|
||||
map.set(p.startIndex, p);
|
||||
});
|
||||
return map;
|
||||
}, [paragraphs]);
|
||||
useObservable(useMemo(() => (renderer?.engine.onTransformChange$ ? fromEventSubject(renderer?.engine.onTransformChange$).pipe(throttleTime(33)) : of(null)), [renderer?.engine.onTransformChange$]));
|
||||
const mode = left < 180 ? 'float' : 'side-bar';
|
||||
let minLevel = Infinity;
|
||||
const paragraphMenus = paragraphBounds?.filter(({ paragraph: p }) => p.paragraphStyle?.namedStyleType !== undefined && p.paragraphStyle!.namedStyleType !== NamedStyleType.SUBTITLE).map(({ paragraph: p }) => {
|
||||
const level = transformNamedStyleTypeToLevel(p.paragraphStyle!.namedStyleType!);
|
||||
minLevel = Math.min(minLevel, level);
|
||||
|
||||
return {
|
||||
id: p.paragraphStyle!.headingId!,
|
||||
text: getPlainText(fullDataStream.slice(p.paragraphStart, p.paragraphEnd)),
|
||||
level,
|
||||
isTitle: p.paragraphStyle?.namedStyleType === NamedStyleType.TITLE,
|
||||
};
|
||||
});
|
||||
const paragraphMenus = paragraphs
|
||||
?.filter((p) =>
|
||||
p.paragraphStyle?.namedStyleType !== undefined &&
|
||||
p.paragraphStyle!.namedStyleType !== NamedStyleType.SUBTITLE &&
|
||||
p.paragraphStyle.namedStyleType !== NamedStyleType.NORMAL_TEXT
|
||||
)
|
||||
.map((p) => {
|
||||
const level = transformNamedStyleTypeToLevel(p.paragraphStyle!.namedStyleType!);
|
||||
minLevel = Math.min(minLevel, level);
|
||||
const bound = paragraphBounds?.get(p.startIndex);
|
||||
if (!bound) return null;
|
||||
const { paragraphStart, paragraphEnd } = bound;
|
||||
|
||||
return {
|
||||
id: p.paragraphStyle!.headingId!,
|
||||
text: getPlainText(fullDataStream.slice(paragraphStart, paragraphEnd)),
|
||||
level,
|
||||
isTitle: p.paragraphStyle?.namedStyleType === NamedStyleType.TITLE,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ISideMenuItem[];
|
||||
|
||||
const menus = paragraphMenus?.find((p) => p.isTitle)
|
||||
? paragraphMenus
|
||||
@@ -114,10 +139,10 @@ export function DocSideMenu() {
|
||||
}]
|
||||
: []),
|
||||
...(paragraphMenus ?? []),
|
||||
].filter(Boolean);
|
||||
].filter(Boolean) as ISideMenuItem[];
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
const activeId = findActiveHeading(paragraphBounds ?? [], scrollTop, bottom);
|
||||
const activeId = findActiveHeading(paragraphBounds, paragraphMap, scrollTop, bottom);
|
||||
|
||||
useEffect(() => {
|
||||
const debounceUpdater = debounce(setUpdateKey, 100);
|
||||
@@ -148,7 +173,7 @@ export function DocSideMenu() {
|
||||
};
|
||||
}, [renderer]);
|
||||
|
||||
const handleClick = (menu: ISideMenuItem) => {
|
||||
const handleClick = useEvent((menu: ISideMenuItem) => {
|
||||
const viewport = renderer?.scene.getViewport(VIEWPORT_KEY.VIEW_MAIN);
|
||||
if (!viewport) {
|
||||
return;
|
||||
@@ -158,13 +183,17 @@ export function DocSideMenu() {
|
||||
viewport.scrollToViewportPos({ viewportScrollY: 0 });
|
||||
return;
|
||||
}
|
||||
const bound = paragraphBounds?.find((p) => p.paragraph.paragraphStyle?.headingId === menu.id);
|
||||
const paragraph = paragraphs.find((p) => p.paragraphStyle?.headingId === menu.id);
|
||||
if (!paragraph) {
|
||||
return;
|
||||
}
|
||||
const bound = paragraphBounds?.get(paragraph.startIndex);
|
||||
if (!bound) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.scrollToViewportPos({ viewportScrollY: bound.rect.top });
|
||||
};
|
||||
});
|
||||
|
||||
if (!currentDoc || isInternalEditorID(unitId) || !menus?.length) {
|
||||
return null;
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
IValueOption,
|
||||
MenuItemDefaultValueType,
|
||||
} from '../../../services/menu/menu';
|
||||
import { isRealNum } from '@univerjs/core';
|
||||
import { isRealNum, LocaleService } from '@univerjs/core';
|
||||
import {
|
||||
clsx,
|
||||
Menu as DesignMenu,
|
||||
@@ -37,10 +37,12 @@ import { combineLatest, isObservable, of } from 'rxjs';
|
||||
import { ILayoutService } from '../../../services/layout/layout.service';
|
||||
import { MenuItemType } from '../../../services/menu/menu';
|
||||
import { IMenuManagerService } from '../../../services/menu/menu-manager.service';
|
||||
import { ContextMenuGroup } from '../../../services/menu/types';
|
||||
import { useDependency, useObservable } from '../../../utils/di';
|
||||
import { CustomLabel } from '../../custom-label/CustomLabel';
|
||||
import { useScrollYOverContainer } from '../../hooks/layout';
|
||||
import styles from './index.module.less';
|
||||
import { UITinyMenuGroup } from './tiny-menu-group';
|
||||
|
||||
// TODO: @jikkai disabled and hidden are not working
|
||||
|
||||
@@ -62,13 +64,12 @@ export interface IBaseMenuProps {
|
||||
/** @deprecated */
|
||||
function MenuWrapper(props: IBaseMenuProps) {
|
||||
const { menuType, onOptionSelect } = props;
|
||||
|
||||
const localeService = useDependency(LocaleService);
|
||||
const menuManagerService = useDependency(IMenuManagerService);
|
||||
|
||||
const menuItems = useMemo(() => menuType ? menuManagerService.getMenuByPositionKey(menuType) : [], [menuType, menuManagerService]);
|
||||
|
||||
const [hiddenStates, setHiddenStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
const filteredMenuItems = useMemo(() => {
|
||||
return menuItems.filter((item) => {
|
||||
if (!item.children) return item;
|
||||
@@ -114,21 +115,23 @@ function MenuWrapper(props: IBaseMenuProps) {
|
||||
/>
|
||||
)
|
||||
: item.children?.length
|
||||
? (
|
||||
<DesignMenuItemGroup key={item.key} eventKey={item.key}>
|
||||
{item.children.map((child) => (
|
||||
child.item && (
|
||||
<MenuItem
|
||||
key={child.key}
|
||||
menuItem={child.item}
|
||||
onClick={(object: Partial<IValueOption>) => {
|
||||
onOptionSelect?.({ value: '', label: child.key, ...object });
|
||||
}}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</DesignMenuItemGroup>
|
||||
)
|
||||
? item.key === ContextMenuGroup.QUICK
|
||||
? <UITinyMenuGroup key={item.key} item={item} onOptionSelect={onOptionSelect} />
|
||||
: (
|
||||
<DesignMenuItemGroup key={item.key} eventKey={item.key} title={item.title ? localeService.t(item.title) : undefined}>
|
||||
{item.children.map((child) => (
|
||||
child.item && (
|
||||
<MenuItem
|
||||
key={child.key}
|
||||
menuItem={child.item}
|
||||
onClick={(object: Partial<IValueOption>) => {
|
||||
onOptionSelect?.({ value: '', label: child.key, ...object });
|
||||
}}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</DesignMenuItemGroup>
|
||||
)
|
||||
: null);
|
||||
}
|
||||
|
||||
@@ -195,7 +198,6 @@ export const Menu = (props: IBaseMenuProps) => {
|
||||
setMenuEl(ref.list);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DesignMenu
|
||||
ref={handleSetMenuEl}
|
||||
@@ -292,16 +294,18 @@ function MenuItem({ menuItem, onClick }: IMenuItemProps) {
|
||||
)}
|
||||
expandIcon={<MoreSingle className={styles.menuItemMoreIcon} />}
|
||||
>
|
||||
{selections.length > 0 && (
|
||||
<MenuOptionsWrapper
|
||||
parentKey={item.id}
|
||||
menuType={item.id}
|
||||
options={selections}
|
||||
onOptionSelect={(v) => {
|
||||
onClick({ value: v.value, id: item.id, commandId: v.commandId });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DesignMenuItemGroup>
|
||||
{selections.length > 0 && (
|
||||
<MenuOptionsWrapper
|
||||
parentKey={item.id}
|
||||
menuType={item.id}
|
||||
options={selections}
|
||||
onOptionSelect={(v) => {
|
||||
onClick({ value: v.value, id: item.id, commandId: v.commandId });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DesignMenuItemGroup>
|
||||
</DesignSubMenu>
|
||||
);
|
||||
}
|
||||
@@ -339,7 +343,9 @@ function MenuItem({ menuItem, onClick }: IMenuItemProps) {
|
||||
)}
|
||||
expandIcon={<MoreSingle className={styles.menuItemMoreIcon} />}
|
||||
>
|
||||
{subMenuItems.length && <MenuWrapper menuType={item.id} parentKey={item.id} onOptionSelect={onClick} />}
|
||||
<DesignMenuItemGroup>
|
||||
{subMenuItems.length && <MenuWrapper menuType={item.id} parentKey={item.id} onOptionSelect={onClick} />}
|
||||
</DesignMenuItemGroup>
|
||||
</DesignSubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 { IValueOption } from '../../../services/menu/menu';
|
||||
import type { IMenuSchema } from '../../../services/menu/menu-manager.service';
|
||||
import { convertObservableToBehaviorSubject, LocaleService } from '@univerjs/core';
|
||||
import {
|
||||
TinyMenuGroup as DesignTinyMenuGroup,
|
||||
} from '@univerjs/design';
|
||||
import { useDependency } from '@wendellhu/redi/react-bindings';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { combineLatest, of } from 'rxjs';
|
||||
import { ComponentManager } from '../../../common';
|
||||
|
||||
interface IUITinyMenuGroupProps {
|
||||
item: IMenuSchema;
|
||||
onOptionSelect?: (option: IValueOption) => void;
|
||||
}
|
||||
|
||||
export function UITinyMenuGroup(props: IUITinyMenuGroupProps) {
|
||||
const { item, onOptionSelect } = props;
|
||||
const [activeItems, setActiveItems] = useState<string[]>([]);
|
||||
const componentManager = useDependency(ComponentManager);
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item.children) return;
|
||||
const observables = item.children.map((child) => convertObservableToBehaviorSubject(child.item?.activated$ ?? of(false), false));
|
||||
const subscription = combineLatest(observables).subscribe((activedArr) => {
|
||||
const actived = activedArr.map((actived, index) => ({ actived, item: item.children![index].item!.id })).filter((actived) => actived.actived);
|
||||
if (actived.length === 0) {
|
||||
setActiveItems([]);
|
||||
} else {
|
||||
setActiveItems(actived.map((actived) => actived.item));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
observables.forEach((observable) => {
|
||||
observable.complete();
|
||||
});
|
||||
};
|
||||
}, [item]);
|
||||
|
||||
if (!item.children) return null;
|
||||
|
||||
return (
|
||||
<DesignTinyMenuGroup
|
||||
items={item.children.map((child) => ({
|
||||
key: child.key,
|
||||
onClick: () => {
|
||||
onOptionSelect?.({
|
||||
value: '',
|
||||
label: child.key,
|
||||
commandId: child.item?.commandId,
|
||||
id: child.item?.id,
|
||||
tooltip: child.item?.tooltip && localeService.t(child.item?.tooltip),
|
||||
});
|
||||
},
|
||||
className: '',
|
||||
Icon: componentManager.get(child.item!.icon as string)!,
|
||||
active: activeItems.includes(child.item?.id ?? ''),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export { UNIVER_UI_PLUGIN_NAME } from './ui-plugin';
|
||||
export * from './utils';
|
||||
export { ComponentContainer, type IComponentContainerProps, useComponentsOfPart } from './views/components/ComponentContainer';
|
||||
export { ZenZone } from './views/components/zen-zone/ZenZone';
|
||||
|
||||
export { Menu as DesktopMenu } from './components/menu/desktop/Menu';
|
||||
// #region - workbench components
|
||||
|
||||
export { type IConfirmPartMethodOptions } from './views/components/confirm-part/interface';
|
||||
|
||||
@@ -27,8 +27,10 @@ export const IMenuManagerService = createIdentifier<IMenuManagerService>('univer
|
||||
export interface IMenuSchema {
|
||||
key: string;
|
||||
order: number;
|
||||
title?: string;
|
||||
item?: IMenuItem;
|
||||
children?: IMenuSchema[];
|
||||
tiny?: boolean;
|
||||
}
|
||||
|
||||
export interface IMenuManagerService {
|
||||
@@ -46,6 +48,7 @@ export interface IMenuManagerService {
|
||||
export type MenuSchemaType = {
|
||||
order?: number;
|
||||
menuItemFactory?: (accessor: IAccessor) => IMenuItem;
|
||||
title?: string;
|
||||
} | {
|
||||
[key: string]: MenuSchemaType;
|
||||
};
|
||||
@@ -113,6 +116,27 @@ export class MenuManagerService extends Disposable implements IMenuManagerServic
|
||||
[MenuManagerPosition.CONTEXT_MENU]: {
|
||||
[ContextMenuPosition.MAIN_AREA]: {
|
||||
order: 0,
|
||||
[ContextMenuGroup.QUICK]: {
|
||||
order: -1,
|
||||
},
|
||||
[ContextMenuGroup.FORMAT]: {
|
||||
order: 0,
|
||||
},
|
||||
[ContextMenuGroup.LAYOUT]: {
|
||||
order: 1,
|
||||
},
|
||||
[ContextMenuGroup.DATA]: {
|
||||
order: 2,
|
||||
},
|
||||
[ContextMenuGroup.OTHERS]: {
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
[ContextMenuPosition.PARAGRAPH]: {
|
||||
order: 0,
|
||||
[ContextMenuGroup.QUICK]: {
|
||||
order: -1,
|
||||
},
|
||||
[ContextMenuGroup.FORMAT]: {
|
||||
order: 0,
|
||||
},
|
||||
@@ -205,7 +229,7 @@ export class MenuManagerService extends Disposable implements IMenuManagerServic
|
||||
_target[_key] = merge({}, _target[_key], source[_key]);
|
||||
|
||||
this.menuChanged$.next();
|
||||
} else {
|
||||
} else if (typeof value === 'object') {
|
||||
this.mergeMenu(source, value);
|
||||
}
|
||||
}
|
||||
@@ -223,6 +247,7 @@ export class MenuManagerService extends Disposable implements IMenuManagerServic
|
||||
const menuItem: Partial<IMenuSchema> = {
|
||||
key,
|
||||
order: value.order,
|
||||
title: value.title,
|
||||
};
|
||||
|
||||
if (value.menuItemFactory) {
|
||||
@@ -239,14 +264,15 @@ export class MenuManagerService extends Disposable implements IMenuManagerServic
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const children = this._buildMenuSchema(value);
|
||||
if (children.length > 0) {
|
||||
menuItem.children = children.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
const children = this._buildMenuSchema(value);
|
||||
if (children.length > 0) {
|
||||
menuItem.children = children.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
if (menuItem.item || menuItem.children) {
|
||||
result.push(menuItem as IMenuSchema); // 使用类型断言补充缺失字段
|
||||
if (menuItem.item || menuItem.children) {
|
||||
result.push(menuItem as IMenuSchema); // 使用类型断言补充缺失字段
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,9 +64,17 @@ export enum ContextMenuPosition {
|
||||
ROW_HEADER = 'contextMenu.rowHeader',
|
||||
FOOTER_TABS = 'contextMenu.footerTabs',
|
||||
FOOTER_MENU = 'contextMenu.footerMenu',
|
||||
/**
|
||||
* paragraph context menu in doc
|
||||
*/
|
||||
PARAGRAPH = 'contextMenu.paragraph',
|
||||
}
|
||||
|
||||
export enum ContextMenuGroup {
|
||||
/**
|
||||
* quick context menu, displayed as icon
|
||||
*/
|
||||
QUICK = 'contextMenu.quick',
|
||||
FORMAT = 'contextMenu.format',
|
||||
LAYOUT = 'contextMenu.layout',
|
||||
DATA = 'contextMenu.data',
|
||||
|
||||
@@ -36,7 +36,8 @@ export interface IPopup extends Omit<IRectPopupProps, 'children' | 'hidden' | 'e
|
||||
hideOnInvisible?: boolean;
|
||||
hiddenType?: 'hide' | 'destroy';
|
||||
hiddenRects$?: Observable<IBoundRectNoAngle[]>;
|
||||
|
||||
customActive?: boolean;
|
||||
onActiveChange?: (active: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ICanvasPopupService {
|
||||
@@ -83,12 +84,13 @@ export class CanvasPopupService extends Disposable implements ICanvasPopupServic
|
||||
const id = Tools.generateRandomId();
|
||||
this._popupMap.set(id, {
|
||||
...item,
|
||||
onPointerEnter: () => {
|
||||
this._activePopupId = id;
|
||||
},
|
||||
onPointerLeave: () => {
|
||||
if (this._activePopupId === id) {
|
||||
this._activePopupId = null;
|
||||
onActiveChange: (active: boolean) => {
|
||||
if (active) {
|
||||
this._activePopupId = id;
|
||||
} else {
|
||||
if (this._activePopupId === id) {
|
||||
this._activePopupId = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -87,8 +87,14 @@ const SingleCanvasPopup = ({ popup, children }: ISingleCanvasPopupProps) => {
|
||||
onClickOutside={popup.onClickOutside}
|
||||
excludeOutside={popup.excludeOutside}
|
||||
excludeRects={excludeRectsRef}
|
||||
onPointerEnter={popup.onPointerEnter}
|
||||
onPointerLeave={popup.onPointerLeave}
|
||||
{
|
||||
...popup.customActive
|
||||
? null
|
||||
: {
|
||||
onPointerEnter: () => popup.onActiveChange?.(true),
|
||||
onPointerLeave: () => popup.onActiveChange?.(false),
|
||||
}
|
||||
}
|
||||
onClick={popup.onClick}
|
||||
onContextMenu={popup.onContextMenu}
|
||||
>
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface IRectPopupProps {
|
||||
*/
|
||||
anchorRect$: Observable<IAbsolutePosition>;
|
||||
excludeRects?: RefObject<Nullable<IAbsolutePosition[]>>;
|
||||
direction?: 'vertical' | 'horizontal' | 'top' | 'right' | 'left' | 'bottom' | 'bottom-center' | 'top-center';
|
||||
direction?: 'vertical' | 'horizontal' | 'top' | 'right' | 'left' | 'right-center' | 'left-center' | 'bottom' | 'bottom-center' | 'top-center';
|
||||
|
||||
hidden?: boolean;
|
||||
// #region closing behavior
|
||||
@@ -50,11 +50,16 @@ export interface IRectPopupProps {
|
||||
excludeOutside?: HTMLElement[];
|
||||
onContextMenu?: () => void;
|
||||
|
||||
onPointerEnter?: (e: React.PointerEvent<HTMLElement>) => void;
|
||||
onPointerLeave?: (e: React.PointerEvent<HTMLElement>) => void;
|
||||
onPointerEnter?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
onPointerLeave?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
// #endregion
|
||||
portal?: boolean;
|
||||
|
||||
mask?: boolean;
|
||||
zIndex?: number;
|
||||
maskZIndex?: number;
|
||||
onMaskClick?: () => void;
|
||||
}
|
||||
|
||||
export interface IPopupLayoutInfo extends Pick<IRectPopupProps, 'direction'> {
|
||||
@@ -105,20 +110,49 @@ function calcPopupPosition(layout: IPopupLayoutInfo): { top: number; left: numbe
|
||||
// In x-axis
|
||||
const { left: startX, top: startY, right: endX, bottom: endY } = position;
|
||||
// const horizontalStyle = ((endX + width) > containerWidth || direction === 'left')
|
||||
const horizontalStyle = direction === 'left'
|
||||
const horizontalStyle = direction.includes('left')
|
||||
? { left: Math.max(startX - width, PUSHING_MINIMUM_GAP) } // on left
|
||||
: { left: Math.min(endX, containerWidth - width - PUSHING_MINIMUM_GAP) }; // on right
|
||||
let verticalStyle;
|
||||
const minTop = PUSHING_MINIMUM_GAP;
|
||||
const maxTop = containerHeight - height - PUSHING_MINIMUM_GAP;
|
||||
|
||||
// If the popup element exceed the visible area. We should "push" it back.
|
||||
const verticalStyle = ((startY + height) > containerHeight)
|
||||
? { top: Math.max(endY - height, PUSHING_MINIMUM_GAP) } // on top
|
||||
: { top: Math.min(startY, containerHeight - height - PUSHING_MINIMUM_GAP) }; // on bottom
|
||||
if (direction.includes('center')) {
|
||||
const rectHeight = endY - startY;
|
||||
const offsetY = (rectHeight - height) / 2;
|
||||
|
||||
verticalStyle = (Math.max(startY + offsetY, PUSHING_MINIMUM_GAP) + height) > containerHeight
|
||||
? { top: Math.max(Math.min(maxTop, endY - height - offsetY), minTop) }
|
||||
: { top: Math.max(minTop, Math.min(startY + offsetY, maxTop)) };
|
||||
} else {
|
||||
// If the popup element exceed the visible area. We should "push" it back.
|
||||
verticalStyle = ((startY + height) > containerHeight)
|
||||
? { top: Math.max(endY - height, PUSHING_MINIMUM_GAP) } // on top
|
||||
: { top: Math.min(startY, containerHeight - height - PUSHING_MINIMUM_GAP) }; // on bottom
|
||||
}
|
||||
|
||||
return { ...verticalStyle, ...horizontalStyle };
|
||||
};
|
||||
|
||||
function RectPopup(props: IRectPopupProps) {
|
||||
const { portal, children, anchorRect$, direction = 'vertical', onClickOutside, excludeOutside, excludeRects, onPointerEnter, onPointerLeave, onClick, hidden, onContextMenu } = props;
|
||||
const {
|
||||
mask,
|
||||
portal,
|
||||
children,
|
||||
anchorRect$,
|
||||
direction = 'vertical',
|
||||
onClickOutside,
|
||||
excludeOutside,
|
||||
excludeRects,
|
||||
onPointerEnter,
|
||||
onPointerLeave,
|
||||
onClick,
|
||||
hidden,
|
||||
onContextMenu,
|
||||
zIndex = 1020,
|
||||
maskZIndex = 100,
|
||||
onMaskClick,
|
||||
} = props;
|
||||
const nodeRef = useRef<HTMLElement>(null);
|
||||
const clickOtherFn = useEvent(onClickOutside ?? (() => { /* empty */ }));
|
||||
const contextMenuFn = useEvent(onContextMenu ?? (() => { /* empty */ }));
|
||||
@@ -227,21 +261,30 @@ function RectPopup(props: IRectPopupProps) {
|
||||
}, [contextMenuFn]);
|
||||
|
||||
const ele = (
|
||||
<section
|
||||
ref={nodeRef}
|
||||
className={`
|
||||
univer-pointer-events-auto univer-fixed univer-left-[-9999px] univer-top-[-9999px] univer-z-[1020]
|
||||
`}
|
||||
style={{ ...positionRef.current, ...hidden ? { display: 'none' } : null }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={onClick}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
>
|
||||
<RectPopupContext.Provider value={anchorRectRef}>
|
||||
{children}
|
||||
</RectPopupContext.Provider>
|
||||
</section>
|
||||
<>
|
||||
{mask && (
|
||||
<div
|
||||
className="univer-fixed univer-bottom-0 univer-left-0 univer-right-0 univer-top-0 univer-z-[100]"
|
||||
style={{ zIndex: maskZIndex }}
|
||||
onClick={onMaskClick}
|
||||
/>
|
||||
)}
|
||||
<section
|
||||
ref={nodeRef}
|
||||
className={`
|
||||
univer-pointer-events-auto univer-fixed univer-left-[-9999px] univer-top-[-9999px] univer-z-[1020]
|
||||
`}
|
||||
style={{ ...positionRef.current, ...hidden ? { display: 'none' } : null, zIndex }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={onClick}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
>
|
||||
<RectPopupContext.Provider value={anchorRectRef}>
|
||||
{children}
|
||||
</RectPopupContext.Provider>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
return !portal ? ele : document.getElementById(popupRootId) ? createPortal(ele, document.getElementById(popupRootId)!) : null;
|
||||
|
||||
Reference in New Issue
Block a user