145 lines
4.4 KiB
TypeScript
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,
|
|
}
|
|
}
|