diff --git a/src/components/graph/selectionToolbox/MenuOptionItem.vue b/src/components/graph/selectionToolbox/MenuOptionItem.vue index fb1f8c01a5..e6909737dc 100644 --- a/src/components/graph/selectionToolbox/MenuOptionItem.vue +++ b/src/components/graph/selectionToolbox/MenuOptionItem.vue @@ -3,10 +3,21 @@ v-if="option.type === 'divider'" class="my-1 h-px bg-smoke-200 dark-theme:bg-zinc-700" /> +
+ {{ t(`contextMenu.${option.label || ''}`) }} +
@@ -57,6 +68,9 @@ const props = defineProps() const emit = defineEmits() const handleClick = (event: Event) => { + if (props.option.disabled) { + return + } emit('click', props.option, event) } diff --git a/src/components/graph/selectionToolbox/NodeOptions.vue b/src/components/graph/selectionToolbox/NodeOptions.vue index 9965ffa6d4..c43f447ab3 100644 --- a/src/components/graph/selectionToolbox/NodeOptions.vue +++ b/src/components/graph/selectionToolbox/NodeOptions.vue @@ -11,15 +11,35 @@ :pt="pt" @show="onPopoverShow" @hide="onPopoverHide" - @wheel="canvasInteractions.forwardEventToCanvas" >
- + +
+
+ + +
+
+ + +
+ +
@@ -37,6 +57,7 @@ import { useRafFn } from '@vueuse/core' import Popover from 'primevue/popover' import { computed, onMounted, onUnmounted, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' import { forceCloseMoreOptionsSignal, @@ -53,13 +74,19 @@ import type { SubMenuOption } from '@/composables/graph/useMoreOptionsMenu' import { useSubmenuPositioning } from '@/composables/graph/useSubmenuPositioning' -import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' +import { calculateMenuPosition } from '@/composables/graph/useViewportAwareMenuPositioning' + +// import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import MenuOptionItem from './MenuOptionItem.vue' import SubmenuPopover from './SubmenuPopover.vue' +const { t } = useI18n() + const popover = ref>() const targetElement = ref(null) +const searchInput = ref(null) +const searchQuery = ref('') const isTriggeredByToolbox = ref(true) // Track open state ourselves so we can restore after drag/move const isOpen = ref(false) @@ -72,7 +99,46 @@ const currentSubmenu = ref(null) const { menuOptions, menuOptionsWithSubmenu, bump } = useMoreOptionsMenu() const { toggleSubmenu, hideAllSubmenus } = useSubmenuPositioning() -const canvasInteractions = useCanvasInteractions() +// const canvasInteractions = useCanvasInteractions() + +// Filter menu options based on search query +const filteredMenuOptions = computed(() => { + const query = searchQuery.value.toLowerCase().trim() + if (!query) { + return menuOptions.value + } + + const filtered: MenuOption[] = [] + let lastWasDivider = false + + for (const option of menuOptions.value) { + // Skip category labels and dividers during filtering, add them back contextually + if (option.type === 'divider') { + lastWasDivider = true + continue + } + + if (option.type === 'category') { + continue + } + + // Check if option matches search query + const label = option.label?.toLowerCase() || '' + if (label.includes(query)) { + // Add divider before this item if the last item was separated by a divider + if (lastWasDivider && filtered.length > 0) { + const lastItem = filtered[filtered.length - 1] + if (lastItem.type !== 'divider') { + filtered.push({ type: 'divider' }) + } + } + filtered.push(option) + lastWasDivider = false + } + } + + return filtered +}) let lastLogTs = 0 const LOG_INTERVAL = 120 // ms @@ -114,19 +180,31 @@ const repositionPopover = () => { const btn = targetElement.value const overlayEl = resolveOverlayEl() if (!btn || !overlayEl) return + const rect = btn.getBoundingClientRect() - const marginY = 8 // tailwind mt-2 ~ 0.5rem = 8px - const left = isTriggeredByToolbox.value - ? rect.left + rect.width / 2 - : rect.right - rect.width / 4 - const top = isTriggeredByToolbox.value - ? rect.bottom + marginY - : rect.top - marginY - 6 + try { - overlayEl.style.position = 'fixed' - overlayEl.style.left = `${left}px` - overlayEl.style.top = `${top}px` - overlayEl.style.transform = 'translate(-50%, 0)' + // Calculate viewport-aware position + const style = calculateMenuPosition({ + triggerRect: rect, + menuElement: overlayEl, + isTriggeredByToolbox: isTriggeredByToolbox.value, + marginY: 8 + }) + + // Apply positioning styles + overlayEl.style.position = style.position + overlayEl.style.left = style.left + overlayEl.style.transform = style.transform + + // Handle top vs bottom positioning + if (style.top !== undefined) { + overlayEl.style.top = style.top + overlayEl.style.bottom = '' // Clear bottom if using top + } else if (style.bottom !== undefined) { + overlayEl.style.bottom = style.bottom + overlayEl.style.top = '' // Clear top if using bottom + } } catch (e) { console.warn('[NodeOptions] Failed to set overlay style', e) return @@ -253,6 +331,10 @@ const setSubmenuRef = (key: string, el: any) => { } } +const clearSearch = () => { + searchQuery.value = '' +} + const pt = computed(() => ({ root: { class: 'absolute z-50 w-[300px] px-[12]' @@ -269,8 +351,14 @@ const pt = computed(() => ({ // Distinguish outside click (PrimeVue dismiss) from programmatic hides. const onPopoverShow = () => { overlayElCache = resolveOverlayEl() + // Clear search and focus input + searchQuery.value = '' // Delay first reposition slightly to ensure DOM fully painted - requestAnimationFrame(() => repositionPopover()) + requestAnimationFrame(() => { + repositionPopover() + // Focus the search input after popover is shown + searchInput.value?.focus() + }) startSync() } @@ -282,6 +370,8 @@ const onPopoverHide = () => { moreOptionsOpen.value = false moreOptionsRestorePending.value = false } + // Clear search when hiding + searchQuery.value = '' overlayElCache = null stopSync() lastProgrammaticHideReason.value = null diff --git a/src/components/graph/selectionToolbox/SubmenuPopover.vue b/src/components/graph/selectionToolbox/SubmenuPopover.vue index 12c1a8571a..10baa5ee40 100644 --- a/src/components/graph/selectionToolbox/SubmenuPopover.vue +++ b/src/components/graph/selectionToolbox/SubmenuPopover.vue @@ -19,9 +19,15 @@ v-for="subOption in option.submenu" :key="subOption.label" :class=" - isColorSubmenu - ? 'w-7 h-7 flex items-center justify-center hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer' - : 'flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer' + cn( + 'flex items-center rounded', + isColorSubmenu + ? 'w-7 h-7 justify-center' + : 'gap-2 px-3 py-1.5 text-sm', + subOption.disabled + ? 'cursor-not-allowed pointer-events-none text-node-icon-disabled' + : 'hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 cursor-pointer' + ) " :title="subOption.label" @click="handleSubmenuClick(subOption)" @@ -46,13 +52,14 @@