추가: 티어표 아이템 우클릭 복제
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Teleport, computed, inject, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Sortable from 'sortablejs'
|
||||
import * as htmlToImage from 'html-to-image'
|
||||
@@ -79,6 +79,12 @@ const poolSearchQuery = ref('')
|
||||
const selectedItemId = ref('')
|
||||
const recentDragFinishedAt = ref(0)
|
||||
const savedEditorSnapshot = ref('')
|
||||
const itemContextMenu = ref({
|
||||
open: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
itemId: '',
|
||||
})
|
||||
let editorLoadToken = 0
|
||||
|
||||
const boardEl = ref(null)
|
||||
@@ -329,6 +335,31 @@ function shouldIgnoreItemClick() {
|
||||
return Date.now() - recentDragFinishedAt.value < 180
|
||||
}
|
||||
|
||||
function closeItemContextMenu() {
|
||||
if (!itemContextMenu.value.open) return
|
||||
itemContextMenu.value = {
|
||||
open: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
itemId: '',
|
||||
}
|
||||
}
|
||||
|
||||
function openItemContextMenu(itemId, event) {
|
||||
if (!canEdit.value || !itemId || !itemsById.value[itemId] || shouldIgnoreItemClick()) return
|
||||
selectedItemId.value = itemId
|
||||
const viewportWidth = typeof window === 'undefined' ? 0 : window.innerWidth
|
||||
const viewportHeight = typeof window === 'undefined' ? 0 : window.innerHeight
|
||||
const menuX = Number(event?.clientX || 0)
|
||||
const menuY = Number(event?.clientY || 0)
|
||||
itemContextMenu.value = {
|
||||
open: true,
|
||||
x: viewportWidth ? Math.max(12, Math.min(menuX, viewportWidth - 180)) : menuX,
|
||||
y: viewportHeight ? Math.max(12, Math.min(menuY, viewportHeight - 72)) : menuY,
|
||||
itemId,
|
||||
}
|
||||
}
|
||||
|
||||
function getItemLocation(itemId) {
|
||||
if (!itemId) return { type: null, groupId: '', columnIndex: -1, index: -1 }
|
||||
|
||||
@@ -405,6 +436,43 @@ function moveSelectedItemToPool() {
|
||||
selectedItemId.value = ''
|
||||
}
|
||||
|
||||
function duplicateItemToPool() {
|
||||
if (!canEdit.value || !itemContextMenu.value.itemId) return
|
||||
|
||||
const sourceItem = itemsById.value[itemContextMenu.value.itemId]
|
||||
if (!sourceItem) {
|
||||
closeItemContextMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const clonedId = createClonedItemId()
|
||||
itemsById.value = {
|
||||
...itemsById.value,
|
||||
[clonedId]: {
|
||||
...sourceItem,
|
||||
id: clonedId,
|
||||
},
|
||||
}
|
||||
pool.value = [clonedId, ...pool.value]
|
||||
selectedItemId.value = clonedId
|
||||
closeItemContextMenu()
|
||||
toast.success('복제본을 미사용 아이템 목록에 추가했어요.')
|
||||
}
|
||||
|
||||
function handleGlobalContextMenu(event) {
|
||||
if (!itemContextMenu.value.open) return
|
||||
const target = event?.target
|
||||
if (target?.closest?.('[data-item-context-menu]') || target?.closest?.('[data-item-id]')) return
|
||||
closeItemContextMenu()
|
||||
}
|
||||
|
||||
function handleGlobalPointerDown(event) {
|
||||
if (!itemContextMenu.value.open) return
|
||||
const target = event?.target
|
||||
if (target?.closest?.('[data-item-context-menu]')) return
|
||||
closeItemContextMenu()
|
||||
}
|
||||
|
||||
function setGroupDropEl(groupId, columnIndex, el) {
|
||||
const key = `${groupId}::${columnIndex}`
|
||||
if (!el) {
|
||||
@@ -544,6 +612,10 @@ function createCustomItemLabel(fileName = '') {
|
||||
return (normalized || 'custom').slice(0, 60)
|
||||
}
|
||||
|
||||
function createClonedItemId() {
|
||||
return `dup-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
groups.value = [
|
||||
...groups.value,
|
||||
@@ -1135,6 +1207,7 @@ function resetEditorStateForRoute() {
|
||||
selectedItemId.value = ''
|
||||
recentDragFinishedAt.value = 0
|
||||
savedEditorSnapshot.value = ''
|
||||
closeItemContextMenu()
|
||||
resetTemplateRequestDrafts()
|
||||
}
|
||||
|
||||
@@ -1240,7 +1313,21 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
window.addEventListener('pointerdown', handleGlobalPointerDown)
|
||||
window.addEventListener('contextmenu', handleGlobalContextMenu)
|
||||
window.addEventListener('blur', closeItemContextMenu)
|
||||
window.addEventListener('scroll', closeItemContextMenu, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('pointerdown', handleGlobalPointerDown)
|
||||
window.removeEventListener('contextmenu', handleGlobalContextMenu)
|
||||
window.removeEventListener('blur', closeItemContextMenu)
|
||||
window.removeEventListener('scroll', closeItemContextMenu, true)
|
||||
}
|
||||
if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
destroySortables()
|
||||
})
|
||||
@@ -1556,6 +1643,7 @@ onUnmounted(() => {
|
||||
:class="{ 'cell--selected': selectedItemId === id }"
|
||||
:data-item-id="id"
|
||||
@click.stop="selectItemByClick(id)"
|
||||
@contextmenu.prevent.stop="openItemContextMenu(id, $event)"
|
||||
>
|
||||
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
||||
<div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
|
||||
@@ -1637,6 +1725,7 @@ onUnmounted(() => {
|
||||
}"
|
||||
:data-item-id="id"
|
||||
@click.stop="selectItemByClick(id)"
|
||||
@contextmenu.prevent.stop="openItemContextMenu(id, $event)"
|
||||
>
|
||||
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
||||
<div class="poolItem__label">{{ itemsById[id]?.label || id }}</div>
|
||||
@@ -1646,6 +1735,19 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="itemContextMenu.open && canEdit"
|
||||
class="itemContextMenu"
|
||||
:style="{ left: `${itemContextMenu.x}px`, top: `${itemContextMenu.y}px` }"
|
||||
data-item-context-menu
|
||||
@click.stop
|
||||
@contextmenu.prevent.stop
|
||||
>
|
||||
<button class="itemContextMenu__action" type="button" @click="duplicateItemToPool">
|
||||
아이템 복제
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -2940,6 +3042,35 @@ onUnmounted(() => {
|
||||
.poolItem--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.itemContextMenu {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
min-width: 150px;
|
||||
padding: 8px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-card-bg);
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.itemContextMenu__action {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 11px 12px;
|
||||
background: transparent;
|
||||
color: var(--theme-text);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.itemContextMenu__action:hover {
|
||||
background: var(--theme-pill-bg);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user