| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- import { useMemo } from 'react';
- import { ElementTypes, type PPTElement } from '@/lib/types/slides';
- import { ImageElement } from '../../components/element/ImageElement';
- import { TextElement } from '../../components/element/TextElement';
- import { LineElement } from '../../components/element/LineElement';
- import { ShapeElement } from '../../components/element/ShapeElement';
- import { ChartElement } from '../../components/element/ChartElement';
- import { LatexElement } from '../../components/element/LatexElement';
- import { TableElement } from '../../components/element/TableElement';
- import { VideoElement } from '../../components/element/VideoElement';
- import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuSeparator,
- ContextMenuShortcut,
- ContextMenuSub,
- ContextMenuSubContent,
- ContextMenuSubTrigger,
- ContextMenuTrigger,
- } from '@/components/ui/context-menu';
- import { ElementOrderCommands, ElementAlignCommands } from '@/lib/types/edit';
- import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations';
- export interface ContextmenuItem {
- text?: string;
- subText?: string;
- divider?: boolean;
- disable?: boolean;
- hide?: boolean;
- children?: ContextmenuItem[];
- handler?: () => void;
- }
- interface EditableElementProps {
- readonly elementInfo: PPTElement;
- readonly elementIndex: number;
- readonly isMultiSelect: boolean;
- readonly selectElement: (
- e: React.MouseEvent | React.TouchEvent,
- element: PPTElement,
- canMove?: boolean,
- ) => void;
- readonly openLinkDialog: () => void;
- }
- export function EditableElement({
- elementInfo,
- elementIndex,
- isMultiSelect,
- selectElement,
- openLinkDialog,
- }: EditableElementProps) {
- const CurrentElementComponent = useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- element components have varying prop signatures
- const elementTypeMap: Record<string, any> = {
- [ElementTypes.IMAGE]: ImageElement,
- [ElementTypes.TEXT]: TextElement,
- [ElementTypes.SHAPE]: ShapeElement,
- [ElementTypes.LINE]: LineElement,
- [ElementTypes.CHART]: ChartElement,
- [ElementTypes.LATEX]: LatexElement,
- [ElementTypes.TABLE]: TableElement,
- [ElementTypes.VIDEO]: VideoElement,
- // TODO: Add other element types
- // [ElementTypes.AUDIO]: AudioElement,
- };
- return elementTypeMap[elementInfo.type] || null;
- }, [elementInfo.type]);
- const {
- copyElement,
- pasteElement,
- cutElement,
- deleteElement,
- lockElement,
- unlockElement,
- selectAllElements,
- alignElementToCanvas,
- orderElement,
- combineElements,
- uncombineElements,
- } = useCanvasOperations();
- const contextmenus = (): ContextmenuItem[] => {
- if (elementInfo.lock) {
- return [
- {
- text: '解锁',
- handler: () => unlockElement(elementInfo),
- },
- ];
- }
- return [
- {
- text: '剪切',
- subText: 'Ctrl + X',
- handler: cutElement,
- },
- {
- text: '复制',
- subText: 'Ctrl + C',
- handler: copyElement,
- },
- {
- text: '粘贴',
- subText: 'Ctrl + V',
- handler: pasteElement,
- },
- { divider: true },
- {
- text: '水平居中',
- handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL),
- children: [
- {
- text: '水平垂直居中',
- handler: () => alignElementToCanvas(ElementAlignCommands.CENTER),
- },
- {
- text: '水平居中',
- handler: () => alignElementToCanvas(ElementAlignCommands.HORIZONTAL),
- },
- {
- text: '左对齐',
- handler: () => alignElementToCanvas(ElementAlignCommands.LEFT),
- },
- {
- text: '右对齐',
- handler: () => alignElementToCanvas(ElementAlignCommands.RIGHT),
- },
- ],
- },
- {
- text: '垂直居中',
- handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL),
- children: [
- {
- text: '水平垂直居中',
- handler: () => alignElementToCanvas(ElementAlignCommands.CENTER),
- },
- {
- text: '垂直居中',
- handler: () => alignElementToCanvas(ElementAlignCommands.VERTICAL),
- },
- {
- text: '顶部对齐',
- handler: () => alignElementToCanvas(ElementAlignCommands.TOP),
- },
- {
- text: '底部对齐',
- handler: () => alignElementToCanvas(ElementAlignCommands.BOTTOM),
- },
- ],
- },
- { divider: true },
- {
- text: '置于顶层',
- disable: isMultiSelect && !elementInfo.groupId,
- handler: () => orderElement(elementInfo, ElementOrderCommands.TOP),
- children: [
- {
- text: '置于顶层',
- handler: () => orderElement(elementInfo, ElementOrderCommands.TOP),
- },
- {
- text: '上移一层',
- handler: () => orderElement(elementInfo, ElementOrderCommands.UP),
- },
- ],
- },
- {
- text: '置于底层',
- disable: isMultiSelect && !elementInfo.groupId,
- handler: () => orderElement(elementInfo, ElementOrderCommands.BOTTOM),
- children: [
- {
- text: '置于底层',
- handler: () => orderElement(elementInfo, ElementOrderCommands.BOTTOM),
- },
- {
- text: '下移一层',
- handler: () => orderElement(elementInfo, ElementOrderCommands.DOWN),
- },
- ],
- },
- { divider: true },
- {
- text: '设置链接',
- handler: openLinkDialog,
- disable: true,
- },
- {
- text: elementInfo.groupId ? '取消组合' : '组合',
- subText: 'Ctrl + G',
- handler: elementInfo.groupId ? uncombineElements : combineElements,
- hide: !isMultiSelect,
- },
- {
- text: '全选',
- subText: 'Ctrl + A',
- handler: selectAllElements,
- },
- {
- text: '锁定',
- subText: 'Ctrl + L',
- handler: lockElement,
- },
- {
- text: '删除',
- subText: 'Delete',
- handler: deleteElement,
- },
- ];
- };
- if (!CurrentElementComponent) {
- return (
- <div
- id={`editable-element-${elementInfo.id}`}
- className="editable-element absolute"
- style={{
- zIndex: elementIndex,
- left: elementInfo.left + 'px',
- top: elementInfo.top + 'px',
- width: elementInfo.width + 'px',
- }}
- >
- <div className="p-2 bg-gray-100 border border-gray-300 text-xs text-gray-500">
- {elementInfo.type} element (not implemented)
- </div>
- </div>
- );
- }
- return (
- <div
- id={`editable-element-${elementInfo.id}`}
- className="editable-element absolute"
- style={{
- zIndex: elementIndex,
- }}
- >
- <ContextMenu>
- <ContextMenuTrigger>
- <CurrentElementComponent elementInfo={elementInfo} selectElement={selectElement} />
- </ContextMenuTrigger>
- <ContextMenuContent>
- {contextmenus().map((item, index) => {
- if (item.divider) {
- return <ContextMenuSeparator key={index} />;
- }
- // If has children, use submenu component
- if (item.children && item.children.length > 0) {
- return (
- <ContextMenuSub key={index}>
- <ContextMenuSubTrigger disabled={item.disable} hidden={item.hide}>
- {item.text}
- {item.subText && <ContextMenuShortcut>{item.subText}</ContextMenuShortcut>}
- </ContextMenuSubTrigger>
- <ContextMenuSubContent>
- {item.children.map((child, childIndex) =>
- child.divider ? (
- <ContextMenuSeparator key={childIndex} />
- ) : (
- <ContextMenuItem
- key={childIndex}
- onClick={(e) => {
- e.stopPropagation();
- child.handler?.();
- }}
- disabled={child.disable}
- hidden={child.hide}
- >
- {child.text}
- {child.subText && (
- <ContextMenuShortcut>{child.subText}</ContextMenuShortcut>
- )}
- </ContextMenuItem>
- ),
- )}
- </ContextMenuSubContent>
- </ContextMenuSub>
- );
- }
- // Regular menu item
- return (
- <ContextMenuItem
- key={index}
- onClick={(e) => {
- e.stopPropagation();
- item.handler?.();
- }}
- disabled={item.disable}
- hidden={item.hide}
- >
- {item.text}
- {item.subText && <ContextMenuShortcut>{item.subText}</ContextMenuShortcut>}
- </ContextMenuItem>
- );
- })}
- </ContextMenuContent>
- </ContextMenu>
- </div>
- );
- }
|