Files
web-temp/layers/composables/useNavOverflow.ts
2025-09-16 19:44:29 +09:00

145 lines
4.4 KiB
TypeScript

// composables/useHeaderNavOverflow.ts
import { ref, computed, onMounted, nextTick, watch, type Ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'
type NavItem = { id: string | number; label: string; href?: string }
interface Options {
itemSelector?: string // nav 내부 개별 아이템 선택자
headerGap?: number // header 수평 gap(px)
navGap?: number // nav 아이템 간 gap(px)
}
export function useHeaderNavOverflow(
items: Ref<NavItem[]>,
headerRef: Ref<HTMLElement | null>,
logoRef: Ref<HTMLElement | null>,
trailingRefs: Ref<HTMLElement | null>[], // 우측 버튼/아이콘 등
moreButtonRef: Ref<HTMLElement | null>, // "More" 버튼 참조 (실측용)
options: Options = {}
) {
const itemSelector = options.itemSelector ?? '[data-nav-item]'
const headerGap = options.headerGap ?? 0
const navGap = options.navGap ?? 0
const containerWidth = ref(0)
const logoWidth = ref(0)
const trailingWidth = ref(0)
const moreBtnWidth = ref(0) // 오버플로우 발생 시 필요한 "More" 버튼 너비
const itemWidths = ref<number[]>([])
const splitIndex = ref(items.value.length)
const measureStatic = () => {
const header = headerRef.value
if (!header) return
containerWidth.value = header.clientWidth
// 로고/트레일링(우측 버튼들) 폭 합
logoWidth.value = Math.ceil(
logoRef.value?.getBoundingClientRect().width ?? 0
)
trailingWidth.value = trailingRefs.reduce((sum, r) => {
const w = Math.ceil(r.value?.getBoundingClientRect().width ?? 0)
return sum + w
}, 0)
// "More" 버튼 실측 (display:none이면 0이므로, visibility:hidden 상태로 자리 차지 측정 추천)
moreBtnWidth.value = Math.ceil(
moreButtonRef.value?.getBoundingClientRect().width ?? 0
)
}
const measureItems = (navEl: HTMLElement | null) => {
if (!navEl) return
const nodes = Array.from(navEl.querySelectorAll<HTMLElement>(itemSelector))
itemWidths.value = nodes.map(n =>
Math.ceil(n.getBoundingClientRect().width)
)
}
const computeSplit = (navEl: HTMLElement | null) => {
if (!navEl) return
const available =
containerWidth.value - logoWidth.value - trailingWidth.value - headerGap // header 자체 gap 여유분(필요 시)
if (available <= 0) {
splitIndex.value = 0
return
}
// 1차: 모두 넣을 수 있는지 시도
let sum = 0
let idx = items.value.length
for (let i = 0; i < items.value.length; i++) {
const w = itemWidths.value[i] ?? 0
const extra = i === 0 ? 0 : navGap
if (sum + w + extra <= available) {
sum += w + extra
} else {
idx = i
break
}
}
// 오버플로우가 생기면 "More" 버튼 자리도 고려해서 재계산
if (idx < items.value.length && moreBtnWidth.value > 0) {
const available2 = available - moreBtnWidth.value - navGap // more와의 간격
let sum2 = 0
let idx2 = 0
for (let i = 0; i < items.value.length; i++) {
const w = itemWidths.value[i] ?? 0
const extra = i === 0 ? 0 : navGap
if (sum2 + w + extra <= available2) {
sum2 += w + extra
idx2 = i + 1
} else {
break
}
}
splitIndex.value = idx2
} else {
splitIndex.value = idx
}
}
const visibleItems = computed(() => items.value.slice(0, splitIndex.value))
const overflowItems = computed(() => items.value.slice(splitIndex.value))
const measureAll = async (navEl: HTMLElement | null) => {
await nextTick()
measureStatic()
measureItems(navEl)
computeSplit(navEl)
}
const observe = (el: HTMLElement | null, navEl: HTMLElement | null) => {
if (!el) return
useResizeObserver(el, () => {
containerWidth.value = el.clientWidth
computeSplit(navEl)
})
}
const init = (navEl: Ref<HTMLElement | null>) => {
onMounted(() => {
measureAll(navEl.value)
observe(headerRef.value, navEl.value)
// 폰트/i18n 변경 같은 경우에 수동 호출할 수 있도록 measureAll 노출
})
}
// 아이템 배열 변경 시 재측정
watch(items, () => measureAll(currentNav.value), { deep: true })
// nav 엘리먼트를 외부에서 넘기기 위해 보조 ref
const currentNav = ref<HTMLElement | null>(null)
return {
visibleItems,
overflowItems,
splitIndex,
measureAll,
currentNav, // 템플릿에서 ref 바인딩
init,
}
}