Memulai
Siklus Hidup
Peramban
Pengembangan
Paket Luar
useClickAway
Menjalankan sesuatu ketika kliknya jatuh di luar sekumpulan elemen.
Menu tertutup.
Ini yang menutup dropdown, popover, dan menu ketika orang menekan tempat lain. Terdengar sepele, tapi ada dua hal yang selalu salah kalau ditulis dadakan dengan document.addEventListener('click', …):
- Target yang belum ada. Elemen yang dipantau baru muncul setelah komponennya terpasang. Pendengar yang dipasang lebih dulu akan menganggap setiap klik jatuh "di luar" — termasuk klik yang membuka menunya sendiri. Di sini targetnya boleh berupa
refyang masih kosong, dan elemen yang belum ada dianggap belum siap, bukan dianggap di luar. - Shadow DOM.
event.targetdi dalam shadow root sudah diretarget saat peristiwanya sampai kedocument, jadicontains()selalu bilang tidak. Kalau semua targetnya tinggal di dalam shadow root yang sama, pendengarnya dipasang di root itu, bukan didocument— sama seperti sumbernya.
Pemasangan
npx shadcn-vue@latest add @kliping/use-click-away Perlu @kliping terdaftar di components.json project Anda. Lihat cara menyiapkannya.
Ikut terpasang sendiri: is-browser, target.
Salin dulu is-browser, target — masing-masing memuat sumbernya di halamannya sendiri — lalu tempel berkas ini.
ts
import { getCurrentScope, onScopeDispose, watch } from 'vue'
import { bacaElemen } from '@/lib/target'
import { isBrowser } from '@/lib/is-browser'
import type { TargetElemen } from '@/lib/target'
export function useClickAway<T extends Event = Event>(
onClickAway: (event: T) => void,
target: TargetElemen | TargetElemen[],
eventName: string | string[] = 'click',
) {
const daftarNama = Array.isArray(eventName) ? eventName : [eventName]
const semuaTarget = () => {
const daftar = Array.isArray(target) ? target : [target]
return daftar.map(t => bacaElemen(t))
}
/**
* Tempat pendengarnya dipasang: shadow root kalau seluruh targetnya tinggal
* di sana, `document` kalau tidak.
*/
const tempatPasang = (elemen: (Element | Node | undefined)[]): Document | Node => {
const ada = elemen.filter(Boolean) as Element[]
if (!ada.length)
return document
const semuaDiShadow = ada.every(el =>
typeof el.getRootNode === 'function' && el.getRootNode() instanceof ShadowRoot,
)
return semuaDiShadow ? ada[0].getRootNode() : document
}
let lepas: (() => void) | undefined
const bongkar = () => {
lepas?.()
lepas = undefined
}
const berhentiPantau = watch(
() => semuaTarget(),
(elemen) => {
bongkar()
if (!isBrowser)
return
const akar = tempatPasang(elemen as Element[])
const tangani = (event: Event) => {
const kini = semuaTarget()
// Elemen yang belum ada berarti belum siap dinilai. Menganggapnya "di
// luar" akan menutup menu tepat pada klik yang membukanya.
const didalam = kini.some((el) => {
if (!el)
return true
return 'contains' in el && el.contains(event.target as Node)
})
if (!didalam)
onClickAway(event as T)
}
for (const nama of daftarNama)
akar.addEventListener(nama, tangani)
lepas = () => {
for (const nama of daftarNama)
akar.removeEventListener(nama, tangani)
}
},
{ immediate: true, flush: 'post' },
)
const berhenti = () => {
bongkar()
berhentiPantau()
}
if (getCurrentScope())
onScopeDispose(berhenti)
return berhenti
}API
ts
export function useClickAway<T extends Event = Event>(
onClickAway: (event: T) => void,
target: TargetElemen | TargetElemen[],
eventName: string | string[] = 'click',
)