215 lines
6.6 KiB
Vue
215 lines
6.6 KiB
Vue
<script setup>
|
|
import AdminNavPrimaryBranch from './AdminNavPrimaryBranch.vue'
|
|
|
|
const props = defineProps({
|
|
/** buildNavigationEditorTree 결과 */
|
|
wraps: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
/** 들여쓰기 단계 */
|
|
depth: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
/** 루트면 `'root'`, 아니면 부모 항목 id */
|
|
parentKey: {
|
|
type: String,
|
|
default: 'root'
|
|
},
|
|
/** 드래그 중인 항목 id */
|
|
draggingId: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/** 드롭 대상 위에 올린 항목 id */
|
|
dragOverId: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'drag-start',
|
|
'drag-over',
|
|
'drag-end',
|
|
'drop',
|
|
'add-child',
|
|
'remove'
|
|
])
|
|
|
|
/**
|
|
* 입력·버튼 위에서는 행 드래그를 시작하지 않는다.
|
|
* @param {DragEvent} event - 이벤트
|
|
* @returns {boolean} true면 드래그를 막는다
|
|
*/
|
|
const shouldBlockRowDrag = (event) => {
|
|
const el = event.target
|
|
if (!el || typeof el.closest !== 'function') {
|
|
return false
|
|
}
|
|
return Boolean(el.closest('input, button, textarea, select, a'))
|
|
}
|
|
|
|
/**
|
|
* 드래그 시작
|
|
* @param {DragEvent} event - 이벤트
|
|
* @param {string} itemId - 항목 id
|
|
* @returns {void}
|
|
*/
|
|
const onDragStart = (event, itemId) => {
|
|
if (shouldBlockRowDrag(event)) {
|
|
event.preventDefault()
|
|
return
|
|
}
|
|
if (!event.dataTransfer) {
|
|
return
|
|
}
|
|
emit('drag-start', { parentKey: props.parentKey, itemId })
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
}
|
|
|
|
/**
|
|
* 드래그 오버
|
|
* @param {DragEvent} event - 이벤트
|
|
* @param {string} itemId - 항목 id
|
|
* @returns {void}
|
|
*/
|
|
const onDragOver = (event, itemId) => {
|
|
event.preventDefault()
|
|
emit('drag-over', itemId)
|
|
}
|
|
|
|
/**
|
|
* 드롭
|
|
* @param {DragEvent} event - 이벤트
|
|
* @param {string} itemId - 대상 id
|
|
* @returns {void}
|
|
*/
|
|
const onDrop = (event, itemId) => {
|
|
event.preventDefault()
|
|
emit('drop', { parentKey: props.parentKey, targetId: itemId })
|
|
}
|
|
|
|
/**
|
|
* 드래그 종료
|
|
* @returns {void}
|
|
*/
|
|
const onDragEnd = () => {
|
|
emit('drag-end')
|
|
}
|
|
|
|
/**
|
|
* 행 하이라이트 클래스(태그 관리 메인 태그 테이블과 동일 톤)
|
|
* @param {string} itemId - 항목 id
|
|
* @returns {string}
|
|
*/
|
|
const rowStateClass = (itemId) => {
|
|
const id = String(itemId)
|
|
if (props.dragOverId === id) {
|
|
return 'bg-[#f9f9f7]'
|
|
}
|
|
if (props.draggingId === id) {
|
|
return 'opacity-50'
|
|
}
|
|
return ''
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-nav-primary-branch" :class="depth ? 'mt-2 border-l border-line pl-3' : ''">
|
|
<div class="admin-nav-primary-branch__shell overflow-hidden rounded border border-line">
|
|
<table class="admin-nav-primary-branch__table w-full border-collapse text-left text-sm">
|
|
<thead v-if="depth === 0" class="admin-nav-primary-branch__head bg-[#f5f5f2] text-xs uppercase text-muted">
|
|
<tr>
|
|
<th class="admin-nav-primary-branch__cell px-4 py-3">
|
|
#
|
|
</th>
|
|
<th class="admin-nav-primary-branch__cell px-4 py-3">
|
|
라벨
|
|
</th>
|
|
<th class="admin-nav-primary-branch__cell px-4 py-3">
|
|
URL
|
|
</th>
|
|
<th class="admin-nav-primary-branch__cell px-4 py-3">
|
|
관리
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="admin-nav-primary-branch__body divide-y divide-line bg-white">
|
|
<template v-for="(wrap, index) in wraps" :key="wrap.item.id">
|
|
<tr
|
|
class="admin-nav-primary-branch__row cursor-move"
|
|
:class="rowStateClass(wrap.item.id)"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, wrap.item.id)"
|
|
@dragover="onDragOver($event, wrap.item.id)"
|
|
@drop="onDrop($event, wrap.item.id)"
|
|
@dragend="onDragEnd"
|
|
>
|
|
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle text-muted">
|
|
{{ index + 1 }}
|
|
</td>
|
|
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle">
|
|
<input
|
|
v-model="wrap.item.label"
|
|
class="admin-nav-primary-branch__label w-full min-w-[8rem] rounded border border-line px-3 py-2 text-sm outline-none focus:border-[#8e9cac]"
|
|
type="text"
|
|
placeholder="라벨"
|
|
required
|
|
>
|
|
</td>
|
|
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle">
|
|
<input
|
|
v-model="wrap.item.url"
|
|
class="admin-nav-primary-branch__url w-full min-w-[10rem] rounded border border-line px-3 py-2 font-mono text-sm outline-none focus:border-[#8e9cac]"
|
|
type="text"
|
|
placeholder="URL (# 또는 /경로)"
|
|
required
|
|
>
|
|
</td>
|
|
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle">
|
|
<div class="admin-nav-primary-branch__actions flex flex-wrap gap-2">
|
|
<button
|
|
class="admin-nav-primary-branch__add-child rounded border border-line px-3 py-1.5 text-xs font-semibold"
|
|
type="button"
|
|
@click="emit('add-child', wrap.item.id)"
|
|
>
|
|
하위
|
|
</button>
|
|
<button
|
|
class="admin-nav-primary-branch__remove rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700"
|
|
type="button"
|
|
@click="emit('remove', wrap.item.id)"
|
|
>
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="wrap.children.length" class="admin-nav-primary-branch__nest bg-white">
|
|
<td class="p-0" colspan="4">
|
|
<div class="admin-nav-primary-branch__nest-inner border-t border-line bg-[#fafaf8] px-2 py-3">
|
|
<AdminNavPrimaryBranch
|
|
:wraps="wrap.children"
|
|
:depth="depth + 1"
|
|
:parent-key="String(wrap.item.id)"
|
|
:dragging-id="draggingId"
|
|
:drag-over-id="dragOverId"
|
|
@drag-start="emit('drag-start', $event)"
|
|
@drag-over="emit('drag-over', $event)"
|
|
@drag-end="emit('drag-end')"
|
|
@drop="emit('drop', $event)"
|
|
@add-child="emit('add-child', $event)"
|
|
@remove="emit('remove', $event)"
|
|
/>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|