snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DocumentDataModel } from '@univerjs/core';
|
||||
import type { IValueOption } from '@univerjs/ui';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { LocaleKey } from '../../locale/types';
|
||||
import {
|
||||
BooleanNumber,
|
||||
ICommandService,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
PositionedObjectLayoutType,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { borderClassName, clsx, Dropdown, DropdownMenu, Separator, Tooltip } from '@univerjs/design';
|
||||
import {
|
||||
AutofillDoubleIcon,
|
||||
ChartIcon,
|
||||
MoreDownIcon,
|
||||
TextWrapShapeIcon,
|
||||
} from '@univerjs/icons';
|
||||
import { IconManager, useDependency } from '@univerjs/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface IImagePopupMenuItem {
|
||||
label: string;
|
||||
index: number;
|
||||
commandId: string;
|
||||
commandParams?: object;
|
||||
disable: boolean;
|
||||
type?: 'button' | 'select';
|
||||
value?: string;
|
||||
options?: Array<{ icon?: string; label: IValueOption['label']; value: string }>;
|
||||
commandParamsFactory?: (value: string) => object;
|
||||
hideOnClick?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IImagePopupMenuExtraProps {
|
||||
menuItems: IImagePopupMenuItem[];
|
||||
variant?: 'doc-floating-toolbar' | 'doc-chart-floating-toolbar';
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
drawingId?: string;
|
||||
}
|
||||
|
||||
export interface IImagePopupMenuProps {
|
||||
popup: {
|
||||
extraProps?: IImagePopupMenuExtraProps;
|
||||
};
|
||||
}
|
||||
|
||||
export function ImagePopupMenu(props: IImagePopupMenuProps) {
|
||||
const { popup } = props;
|
||||
|
||||
const menuItems = popup?.extraProps?.menuItems;
|
||||
const commandService = useDependency(ICommandService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
if (!menuItems) return null;
|
||||
|
||||
if (
|
||||
popup.extraProps?.variant === 'doc-floating-toolbar' &&
|
||||
popup.extraProps.unitId &&
|
||||
popup.extraProps.subUnitId &&
|
||||
popup.extraProps.drawingId
|
||||
) {
|
||||
return (
|
||||
<DocImageFloatingToolbar
|
||||
menuItems={menuItems}
|
||||
unitId={popup.extraProps.unitId}
|
||||
subUnitId={popup.extraProps.subUnitId}
|
||||
drawingId={popup.extraProps.drawingId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (popup.extraProps?.variant === 'doc-chart-floating-toolbar') {
|
||||
return <DocChartFloatingToolbar menuItems={menuItems} />;
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
setVisible(visible);
|
||||
};
|
||||
|
||||
const handleClick = (item: IImagePopupMenuItem) => {
|
||||
commandService.executeCommand(item.commandId, item.commandParams);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const showMore = visible || isHovered;
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<DropdownMenu
|
||||
align="start"
|
||||
items={menuItems.map((item) => ({
|
||||
type: 'item',
|
||||
children: localeService.t(item.label as LocaleKey),
|
||||
disabled: item.disable,
|
||||
onSelect: () => handleClick(item),
|
||||
}))}
|
||||
open={visible}
|
||||
onOpenChange={onVisibleChange}
|
||||
>
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-flex univer-items-center univer-gap-2 univer-rounded univer-p-1 univer-text-gray-900
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-800
|
||||
`, borderClassName, {
|
||||
'univer-bg-gray-100 dark:!univer-bg-gray-800': visible,
|
||||
'univer-bg-gray-0 dark:!univer-bg-gray-900': !visible,
|
||||
})}
|
||||
>
|
||||
<AutofillDoubleIcon
|
||||
className={`
|
||||
univer-fill-primary-600 univer-text-gray-900
|
||||
dark:!univer-text-gray-0
|
||||
`}
|
||||
/>
|
||||
{showMore && <MoreDownIcon className="dark:!univer-text-gray-0" />}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UPDATE_DOC_DRAWING_WRAPPING_STYLE_COMMAND_ID = 'doc.command.update-doc-drawing-wrapping-style';
|
||||
|
||||
enum TextWrappingStyle {
|
||||
INLINE = 'inline',
|
||||
BEHIND_TEXT = 'behindText',
|
||||
IN_FRONT_OF_TEXT = 'inFrontOfText',
|
||||
WRAP_SQUARE = 'wrapSquare',
|
||||
WRAP_TOP_AND_BOTTOM = 'wrapTopAndBottom',
|
||||
}
|
||||
|
||||
interface IDocImageFloatingToolbarProps {
|
||||
menuItems: IImagePopupMenuItem[];
|
||||
unitId: string;
|
||||
subUnitId: string;
|
||||
drawingId: string;
|
||||
}
|
||||
|
||||
interface IToolbarDropdownOption<T extends string | number> {
|
||||
label: string;
|
||||
value: T;
|
||||
icon: ReactNode;
|
||||
}
|
||||
|
||||
function getWrappingStyle(documentDataModel: DocumentDataModel | undefined, drawingId: string): TextWrappingStyle {
|
||||
const drawing = documentDataModel?.getSnapshot().drawings?.[drawingId];
|
||||
if (!drawing) {
|
||||
return TextWrappingStyle.INLINE;
|
||||
}
|
||||
|
||||
if (drawing.layoutType === PositionedObjectLayoutType.WRAP_NONE) {
|
||||
return drawing.behindDoc === BooleanNumber.TRUE ? TextWrappingStyle.BEHIND_TEXT : TextWrappingStyle.IN_FRONT_OF_TEXT;
|
||||
}
|
||||
|
||||
if (drawing.layoutType === PositionedObjectLayoutType.WRAP_SQUARE) {
|
||||
return TextWrappingStyle.WRAP_SQUARE;
|
||||
}
|
||||
|
||||
if (drawing.layoutType === PositionedObjectLayoutType.WRAP_TOP_AND_BOTTOM) {
|
||||
return TextWrappingStyle.WRAP_TOP_AND_BOTTOM;
|
||||
}
|
||||
|
||||
return TextWrappingStyle.INLINE;
|
||||
}
|
||||
|
||||
function ToolbarGroup(props: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="univer-flex univer-h-7 univer-items-center univer-gap-1 univer-px-1">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarButton(props: {
|
||||
title: string;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
children: ReactNode;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip
|
||||
className="univer-fill-mode-backwards univer-delay-100"
|
||||
title={props.title}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={props.disabled}
|
||||
onClick={props.onClick}
|
||||
className={clsx(`
|
||||
univer-flex univer-h-6 univer-w-6 univer-items-center univer-justify-center univer-rounded-md
|
||||
univer-border-none univer-bg-transparent univer-p-0 univer-text-sm univer-text-gray-700
|
||||
univer-transition-colors
|
||||
hover:univer-bg-gray-100
|
||||
disabled:univer-cursor-not-allowed disabled:univer-opacity-40
|
||||
dark:!univer-text-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`, {
|
||||
'univer-bg-gray-100 univer-text-primary-600 dark:!univer-bg-gray-700 dark:!univer-text-primary-300': props.active,
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarDropdownButton<T extends string | number>(props: {
|
||||
title: string;
|
||||
value: T;
|
||||
options: Array<IToolbarDropdownOption<T>>;
|
||||
onChange: (value: T) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const activeOption = props.options.find((option) => option.value === props.value) ?? props.options[0];
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
overlay={(
|
||||
<div
|
||||
className={`
|
||||
univer-min-w-32 univer-rounded-lg univer-border univer-border-solid univer-border-gray-200
|
||||
univer-bg-gray-0 univer-p-1 univer-shadow-lg
|
||||
dark:!univer-border-gray-700 dark:!univer-bg-gray-900
|
||||
`}
|
||||
>
|
||||
{props.options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
props.onChange(option.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={clsx(`
|
||||
univer-flex univer-h-8 univer-w-full univer-items-center univer-gap-2 univer-rounded-md
|
||||
univer-border-none univer-bg-transparent univer-px-2 univer-text-left univer-text-sm
|
||||
univer-text-gray-700 univer-transition-colors
|
||||
hover:univer-bg-gray-100
|
||||
dark:!univer-text-gray-100
|
||||
dark:hover:!univer-bg-gray-800
|
||||
`, {
|
||||
'univer-bg-primary-50 univer-text-primary-600 dark:!univer-bg-gray-800 dark:!univer-text-primary-300': option.value === props.value,
|
||||
})}
|
||||
>
|
||||
<span className="univer-flex univer-size-4 univer-items-center univer-justify-center">{option.icon}</span>
|
||||
<span className="univer-flex-1">{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
<Tooltip
|
||||
className="univer-fill-mode-backwards univer-delay-100"
|
||||
title={props.title}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(`
|
||||
univer-flex univer-h-6 univer-min-w-9 univer-items-center univer-justify-center univer-gap-1
|
||||
univer-rounded-md univer-border-none univer-bg-transparent univer-px-1.5 univer-text-sm
|
||||
univer-text-gray-700 univer-transition-colors
|
||||
hover:univer-bg-gray-100
|
||||
dark:!univer-text-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`, {
|
||||
'univer-bg-gray-100 univer-text-primary-600 dark:!univer-bg-gray-700 dark:!univer-text-primary-300': open,
|
||||
})}
|
||||
>
|
||||
{activeOption.icon}
|
||||
<MoreDownIcon className="univer-text-xs" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function DocImageFloatingToolbar(props: IDocImageFloatingToolbarProps) {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const iconManager = useDependency(IconManager);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const documentDataModel = univerInstanceService.getUnit<DocumentDataModel>(props.unitId, UniverInstanceType.UNIVER_DOC) ?? undefined;
|
||||
const [wrappingStyle, setWrappingStyle] = useState(() => getWrappingStyle(documentDataModel, props.drawingId));
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const actionItems = props.menuItems
|
||||
.filter((item) => item.type !== 'select' && item.icon)
|
||||
.sort((a, b) => a.index - b.index);
|
||||
const wrappingStyleOptions = [
|
||||
{
|
||||
label: localeService.t<LocaleKey>('drawing-ui.image-text-wrap.inline'),
|
||||
value: TextWrappingStyle.INLINE,
|
||||
icon: <TextWrapShapeIcon />,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('drawing-ui.image-text-wrap.square'),
|
||||
value: TextWrappingStyle.WRAP_SQUARE,
|
||||
icon: <TextWrapShapeIcon />,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('drawing-ui.image-text-wrap.topAndBottom'),
|
||||
value: TextWrappingStyle.WRAP_TOP_AND_BOTTOM,
|
||||
icon: <TextWrapShapeIcon />,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('drawing-ui.image-text-wrap.behindText'),
|
||||
value: TextWrappingStyle.BEHIND_TEXT,
|
||||
icon: <TextWrapShapeIcon />,
|
||||
},
|
||||
{
|
||||
label: localeService.t<LocaleKey>('drawing-ui.image-text-wrap.inFrontText'),
|
||||
value: TextWrappingStyle.IN_FRONT_OF_TEXT,
|
||||
icon: <TextWrapShapeIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
const executeMenuItem = (item: IImagePopupMenuItem | undefined) => {
|
||||
if (!item || item.disable) {
|
||||
return;
|
||||
}
|
||||
if (item.hideOnClick) {
|
||||
setHidden(true);
|
||||
}
|
||||
commandService.executeCommand(item.commandId, item.commandParams);
|
||||
};
|
||||
|
||||
const updateWrappingStyle = (value: TextWrappingStyle) => {
|
||||
setWrappingStyle(value);
|
||||
commandService.executeCommand(UPDATE_DOC_DRAWING_WRAPPING_STYLE_COMMAND_ID, {
|
||||
unitId: props.unitId,
|
||||
subUnitId: props.subUnitId,
|
||||
drawings: [{ unitId: props.unitId, subUnitId: props.subUnitId, drawingId: props.drawingId }],
|
||||
wrappingStyle: value,
|
||||
});
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="doc-image-floating-toolbar"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
className={clsx(`
|
||||
univer-box-border univer-flex univer-items-center univer-rounded univer-bg-gray-0 univer-px-1 univer-py-1
|
||||
univer-shadow-sm
|
||||
dark:!univer-border-gray-700 dark:!univer-bg-gray-900
|
||||
`, borderClassName)}
|
||||
>
|
||||
<ToolbarGroup>
|
||||
<ToolbarDropdownButton
|
||||
title={wrappingStyleOptions.find((option) => option.value === wrappingStyle)?.label ?? localeService.t<LocaleKey>('drawing-ui.image-text-wrap.inline')}
|
||||
value={wrappingStyle}
|
||||
options={wrappingStyleOptions}
|
||||
onChange={updateWrappingStyle}
|
||||
/>
|
||||
</ToolbarGroup>
|
||||
{actionItems.length > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<ToolbarGroup>
|
||||
{actionItems.map((item) => {
|
||||
const Icon = iconManager.get(item.icon!);
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={`${item.commandId}-${item.index}`}
|
||||
title={localeService.t(item.label as LocaleKey)}
|
||||
disabled={item.disable}
|
||||
onClick={() => executeMenuItem(item)}
|
||||
>
|
||||
<Icon />
|
||||
</ToolbarButton>
|
||||
);
|
||||
})}
|
||||
</ToolbarGroup>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DocChartFloatingToolbar(props: Pick<IDocImageFloatingToolbarProps, 'menuItems'>) {
|
||||
const commandService = useDependency(ICommandService);
|
||||
const iconManager = useDependency(IconManager);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const chartTypeItem = props.menuItems.find((item) => item.type === 'select');
|
||||
const actionItems = props.menuItems
|
||||
.filter((item) => item.type !== 'select' && item.icon)
|
||||
.sort((a, b) => a.index - b.index);
|
||||
const chartTypeOptions = (chartTypeItem?.options ?? []).map((option) => {
|
||||
const Icon = option.icon ? iconManager.get(option.icon) : ChartIcon;
|
||||
|
||||
return {
|
||||
label: String(option.label),
|
||||
value: option.value,
|
||||
icon: <Icon />,
|
||||
};
|
||||
});
|
||||
|
||||
const executeMenuItem = (item: IImagePopupMenuItem | undefined) => {
|
||||
if (!item || item.disable) {
|
||||
return;
|
||||
}
|
||||
if (item.hideOnClick) {
|
||||
setHidden(true);
|
||||
}
|
||||
commandService.executeCommand(item.commandId, item.commandParams);
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="doc-chart-floating-toolbar"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
className={clsx(`
|
||||
univer-box-border univer-flex univer-items-center univer-rounded univer-bg-gray-0 univer-px-1 univer-py-1
|
||||
univer-shadow-sm
|
||||
dark:!univer-border-gray-700 dark:!univer-bg-gray-900
|
||||
`, borderClassName)}
|
||||
>
|
||||
{chartTypeItem && (
|
||||
<>
|
||||
<ToolbarGroup>
|
||||
<ToolbarDropdownButton
|
||||
title={localeService.t(chartTypeItem.label as LocaleKey)}
|
||||
value={chartTypeItem.value ?? chartTypeOptions[0]?.value ?? ''}
|
||||
options={chartTypeOptions}
|
||||
onChange={(value) => {
|
||||
if (!chartTypeItem.disable) {
|
||||
commandService.executeCommand(chartTypeItem.commandId, chartTypeItem.commandParamsFactory?.(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ToolbarGroup>
|
||||
{actionItems.length > 0 && <Separator orientation="vertical" />}
|
||||
</>
|
||||
)}
|
||||
{actionItems.length > 0 && (
|
||||
<ToolbarGroup>
|
||||
{actionItems.map((item) => {
|
||||
const Icon = iconManager.get(item.icon!);
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={`${item.commandId}-${item.index}`}
|
||||
title={localeService.t(item.label as LocaleKey)}
|
||||
disabled={item.disable}
|
||||
onClick={() => executeMenuItem(item)}
|
||||
>
|
||||
<Icon />
|
||||
</ToolbarButton>
|
||||
);
|
||||
})}
|
||||
</ToolbarGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user