openpencil/src/components/ZoomDropdown.vue
2026-03-29 19:46:21 +03:00

170 lines
5.2 KiB
Vue

<script setup lang="ts">
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRoot,
DropdownMenuSeparator,
DropdownMenuTrigger
} from 'reka-ui'
import { nextTick, ref, watch } from 'vue'
import { useEditorCommands, useI18n } from '@open-pencil/vue'
import { menuItem, useMenuUI } from '@/components/ui/menu'
import { useEditorStore } from '@/stores/editor'
const store = useEditorStore()
const { getCommand } = useEditorCommands()
const { menu: menuText, commands, panels } = useI18n()
const open = ref(false)
const editing = ref(false)
const inputRef = ref<HTMLInputElement | null>()
const inputValue = ref('')
const menuCls = useMenuUI({ content: 'min-w-52' })
const itemCls = menuItem({ justify: 'start', class: 'relative pl-7' })
function zoomPercent() {
return Math.round(store.state.zoom * 100)
}
function startEditing() {
editing.value = true
inputValue.value = String(zoomPercent())
void nextTick(() => inputRef.value?.select())
}
function commitInput() {
const parsed = Number.parseInt(inputValue.value, 10)
if (!Number.isNaN(parsed) && parsed > 0) {
store.zoomToLevel(parsed / 100)
}
editing.value = false
}
function cancelInput() {
editing.value = false
}
function toggleRulers() {
store.state.showRulers = !store.state.showRulers
store.requestRepaint()
}
function toggleRemoteCursors() {
store.state.showRemoteCursors = !store.state.showRemoteCursors
store.requestRepaint()
}
function zoomIn() {
const center = store.viewportScreenCenter()
store.applyZoom(-100, center.x, center.y)
}
function zoomOut() {
const center = store.viewportScreenCenter()
store.applyZoom(100, center.x, center.y)
}
const ZOOM_PRESETS: ReadonlyArray<{ label: string; level: number; shortcut?: string }> = [
{ label: '50%', level: 0.5 },
{ label: '100%', level: 1, shortcut: '⌘0' },
{ label: '200%', level: 2 }
]
function isActivePreset(level: number) {
return Math.abs(store.state.zoom - level) < 0.005
}
watch(open, (v) => {
if (!v) editing.value = false
})
</script>
<template>
<DropdownMenuRoot v-model:open="open">
<DropdownMenuTrigger as-child>
<button
data-test-id="zoom-dropdown-trigger"
class="ml-auto cursor-pointer rounded px-1.5 py-0.5 text-[11px] text-muted hover:bg-hover"
>
{{ zoomPercent() }}%
</button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
side="bottom"
:side-offset="4"
align="end"
:class="menuCls.content"
@escape-key-down="cancelInput"
>
<div class="px-1 py-1">
<input
v-if="editing"
ref="inputRef"
v-model="inputValue"
data-test-id="zoom-input"
class="w-full rounded border border-accent bg-input px-2 py-1 text-xs text-surface outline-none"
@blur="commitInput"
@keydown.enter="commitInput"
@keydown.escape.stop="cancelInput"
/>
<button
v-else
data-test-id="zoom-input-trigger"
class="w-full cursor-pointer rounded border border-border bg-input px-2 py-1 text-left text-xs text-surface hover:border-muted"
@click="startEditing"
>
{{ zoomPercent() }}%
</button>
</div>
<DropdownMenuSeparator :class="menuCls.separator" />
<DropdownMenuItem :class="itemCls" @select="zoomIn">
<span class="flex-1">{{ menuText.zoomIn }}</span>
<span class="text-[11px] text-muted">⌘+</span>
</DropdownMenuItem>
<DropdownMenuItem :class="itemCls" @select="zoomOut">
<span class="flex-1">{{ menuText.zoomOut }}</span>
<span class="text-[11px] text-muted">⌘−</span>
</DropdownMenuItem>
<DropdownMenuItem :class="itemCls" @select="getCommand('view.zoomFit').run()">
<span class="flex-1">{{ commands.zoomToFit }}</span>
<span class="text-[11px] text-muted">⇧1</span>
</DropdownMenuItem>
<DropdownMenuItem
v-for="preset in ZOOM_PRESETS"
:key="preset.level"
:class="itemCls"
@select="store.zoomToLevel(preset.level)"
>
<icon-lucide-check
v-if="isActivePreset(preset.level)"
class="absolute left-2 size-3.5"
/>
<span class="flex-1">{{ preset.label }}</span>
<span v-if="preset.shortcut" class="text-[11px] text-muted">{{ preset.shortcut }}</span>
</DropdownMenuItem>
<DropdownMenuSeparator :class="menuCls.separator" />
<DropdownMenuItem :class="itemCls" @select.prevent="toggleRulers">
<icon-lucide-check v-if="store.state.showRulers" class="absolute left-2 size-3.5" />
<span class="flex-1">{{ panels.rulers }}</span>
</DropdownMenuItem>
<DropdownMenuItem :class="itemCls" @select.prevent="toggleRemoteCursors">
<icon-lucide-check
v-if="store.state.showRemoteCursors"
class="absolute left-2 size-3.5"
/>
<span class="flex-1">{{ panels.multiplayerCursors }}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</template>