openpencil/src/components/CanvasMenu.vue
2026-05-19 15:10:32 +03:00

193 lines
6.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import {
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubTrigger,
ContextMenuSubContent,
ContextMenuPortal
} from 'reka-ui'
import IconCombine from '~icons/lucide/combine'
import IconCopyMinus from '~icons/lucide/copy-minus'
import IconCopyX from '~icons/lucide/copy-x'
import IconListCollapse from '~icons/lucide/list-collapse'
import IconSpline from '~icons/lucide/spline'
import IconTypeOutline from '~icons/lucide/type-outline'
import IconSquaresIntersect from '~icons/lucide/squares-intersect'
import {
vTestId,
useEditorCommands,
useI18n,
useMenuModel,
useSelectionState,
editorCommandMetadata,
formatShortcut
} from '@open-pencil/vue'
import type { Component } from 'vue'
import type { EditorCommandId } from '@open-pencil/vue'
import { useEditorStore } from '@/app/editor/active-store'
import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut'
import { createCanvasMenuActions } from '@/app/editor/canvas/menu/actions'
import { useCanvasContextMenu } from '@/app/editor/canvas/menu/context'
import { canvasMenuItemClass, canvasMenuShortcutClass } from '@/app/editor/canvas/menu/model'
import AppShortcutText from '@/components/ui/AppShortcutText.vue'
import { menu, useMenuUI } from '@/components/ui/menu'
const store = useEditorStore()
const { editor, selectedIds, hasSelection } = useSelectionState()
const { getCommand } = useEditorCommands()
const { canvasMenu } = useMenuModel()
const { menu: t } = useI18n()
const canvasMenuActions = createCanvasMenuActions(store, selectedIds)
const { execCommand } = canvasMenuActions
const contextMenu = useCanvasContextMenu(canvasMenu, hasSelection, editor, canvasMenuActions, t)
const menuCls = useMenuUI({
content: 'min-w-56 shadow-[0_8px_30px_rgb(0_0_0/0.4)] animate-in fade-in zoom-in-95',
separator: 'my-1'
})
const componentMenu = menu({ tone: 'component' })
const cls = {
menu: menuCls.content,
submenu: menuCls.content.replace('min-w-56', 'min-w-0 w-max'),
item: menuCls.item,
component: componentMenu.item(),
sep: menuCls.separator
}
const booleanCommandIcons = {
'selection.booleanUnion': IconCombine,
'selection.booleanSubtract': IconCopyMinus,
'selection.booleanIntersect': IconSquaresIntersect,
'selection.booleanExclude': IconCopyX,
'selection.flatten': IconListCollapse,
'selection.outlineText': IconTypeOutline,
'selection.outlineStroke': IconSpline
} satisfies Partial<Record<EditorCommandId, Component>>
function contextCommandTestId(id: EditorCommandId | undefined): string | undefined {
return id ? editorCommandMetadata(id).contextTestId : undefined
}
function contextCommandIcon(id: EditorCommandId | undefined): Component | undefined {
if (!id) return undefined
return (booleanCommandIcons as Partial<Record<EditorCommandId, Component>>)[id]
}
</script>
<template>
<ContextMenuContent :class="cls.menu" :side-offset="2" align="start">
<ContextMenuItem
data-test-id="context-copy"
:class="cls.item"
:disabled="!hasSelection"
@select="execCommand('copy')"
>
<span>{{ t.copy }}</span
><AppShortcutText>{{ appMenuShortcutLabel('copy') }}</AppShortcutText>
</ContextMenuItem>
<ContextMenuItem
data-test-id="context-cut"
:class="cls.item"
:disabled="!hasSelection"
@select="execCommand('cut')"
>
<span>{{ t.cut }}</span
><AppShortcutText>{{ appMenuShortcutLabel('cut') }}</AppShortcutText>
</ContextMenuItem>
<ContextMenuItem data-test-id="context-paste" :class="cls.item" @select="execCommand('paste')">
<span>{{ t.pasteHere }}</span
><AppShortcutText>{{ appMenuShortcutLabel('paste') }}</AppShortcutText>
</ContextMenuItem>
<ContextMenuItem
data-test-id="context-paste-to-replace"
:class="cls.item"
:disabled="!hasSelection"
@select="canvasMenuActions.pasteToReplace"
>
<span>{{ t.pasteToReplace }}</span>
</ContextMenuItem>
<ContextMenuItem
data-test-id="context-duplicate"
:class="cls.item"
:disabled="!hasSelection"
@select="getCommand('selection.duplicate').run()"
>
<span>{{ getCommand('selection.duplicate').label }}</span
><AppShortcutText>{{
formatShortcut(editorCommandMetadata('selection.duplicate').shortcut)
}}</AppShortcutText>
</ContextMenuItem>
<ContextMenuItem
data-test-id="context-delete"
:class="cls.item"
:disabled="!hasSelection"
@select="getCommand('selection.delete').run()"
>
<span>{{ getCommand('selection.delete').label }}</span
><AppShortcutText>{{ editorCommandMetadata('selection.delete').shortcut }}</AppShortcutText>
</ContextMenuItem>
<template v-for="(item, i) in contextMenu" :key="`menu-${i}`">
<ContextMenuSeparator v-if="item.separator" :class="cls.sep" />
<ContextMenuSub v-else-if="item.sub">
<ContextMenuSubTrigger v-test-id="item.testId" :class="cls.item">
<span>{{ item.label }}</span
><span class="text-sm text-muted">›</span>
</ContextMenuSubTrigger>
<ContextMenuPortal>
<ContextMenuSubContent :class="cls.submenu">
<ContextMenuItem
v-for="(sub, j) in item.sub"
:key="j"
:class="cls.item"
v-test-id="sub.separator ? undefined : sub.testId"
:disabled="sub.separator ? true : sub.disabled"
@select="!sub.separator && sub.action?.()"
>
<template v-if="!sub.separator">
<span class="flex min-w-0 flex-1 items-center gap-2">
<component
:is="contextCommandIcon(sub.id)"
v-if="contextCommandIcon(sub.id)"
class="size-3.5 shrink-0 text-muted"
/>
<span class="truncate">{{ sub.label }}</span>
</span>
<AppShortcutText v-if="sub.shortcut">{{ sub.shortcut }}</AppShortcutText>
</template>
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuPortal>
</ContextMenuSub>
<ContextMenuItem
v-else
v-test-id="contextCommandTestId(item.id)"
:class="canvasMenuItemClass(item.label, cls)"
:disabled="item.disabled"
@select="item.action?.()"
>
<span class="flex min-w-0 flex-1 items-center gap-2">
<component
:is="contextCommandIcon(item.id)"
v-if="contextCommandIcon(item.id)"
class="size-3.5 shrink-0 text-muted"
/>
<span class="truncate">{{ item.label }}</span>
</span>
<span
v-if="item.shortcut"
class="text-[11px]"
:class="canvasMenuShortcutClass(item.label)"
>{{ item.shortcut }}</span
>
</ContextMenuItem>
</template>
</ContextMenuContent>
</template>