useTextSelection
Teks yang sedang disorot, beserta letaknya di layar.
Sorot sebagian kalimat ini dengan menyeret penunjuk. Teks yang tersorot akan muncul di bawahnya, lengkap dengan letaknya di layar.
Tersorot: —
Letaknya yang membuat composable ini berguna: dari situ sebuah bilah alat kecil — kutip, salin, sorot — bisa muncul tepat di atas teks yang dipilih.
mousedown mengosongkan pilihan sebelumnya dan mencatat apakah tekanannya bermula di dalam target. Tanpa catatan itu, menyeret dari luar lalu berakhir di dalam akan terbaca sebagai pilihan milik target — padahal bukan. Klik kanan dilewati karena menu konteks tidak menghapus pilihan yang ada.
Pemasangan
npx shadcn-vue@latest add @kliping/use-text-selection 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.
import { getCurrentScope, onScopeDispose, ref, watch } from 'vue'
import { isBrowser } from '@/lib/is-browser'
import { bacaElemen } from '@/lib/target'
import type { TargetElemen } from '@/lib/target'
export interface KotakPilihan {
top: number
left: number
bottom: number
right: number
height: number
width: number
}
const KOTAK_KOSONG: KotakPilihan = {
top: Number.NaN,
left: Number.NaN,
bottom: Number.NaN,
right: Number.NaN,
height: Number.NaN,
width: Number.NaN,
}
function kotakDariPilihan(pilihan: Selection | null): KotakPilihan {
if (!pilihan || pilihan.rangeCount < 1)
return KOTAK_KOSONG
const { top, left, bottom, right, height, width }
= pilihan.getRangeAt(0).getBoundingClientRect()
return { top, left, bottom, right, height, width }
}
export function useTextSelection(target?: TargetElemen) {
const text = ref('')
const rect = ref<KotakPilihan>({ ...KOTAK_KOSONG })
let bermulaDiDalam = false
let lepas: (() => void) | undefined
const bongkar = () => {
lepas?.()
lepas = undefined
}
const berhentiPantau = watch(
() => bacaElemen(target),
(elemen) => {
bongkar()
if (!isBrowser)
return
const el = (elemen ?? document) as Document | Element
const saatLepasTombol = () => {
if (typeof window.getSelection !== 'function')
return
const pilihan = window.getSelection()
const isi = pilihan ? pilihan.toString() : ''
if (isi && bermulaDiDalam) {
text.value = isi
rect.value = kotakDariPilihan(pilihan)
}
}
const saatTekanTombol = (peristiwa: MouseEvent) => {
// Tombol kanan dilewati: menu konteks tidak menghapus pilihan yang ada,
// jadi mengosongkannya di sini justru menghilangkan yang mau disalin.
if (peristiwa.button === 2)
return
if (typeof window.getSelection !== 'function')
return
if (text.value) {
text.value = ''
rect.value = { ...KOTAK_KOSONG }
}
bermulaDiDalam = false
const pilihan = window.getSelection()
if (!pilihan)
return
pilihan.removeAllRanges()
bermulaDiDalam = 'contains' in el
? el.contains(peristiwa.target as Node)
: true
}
el.addEventListener('mouseup', saatLepasTombol as EventListener)
document.addEventListener('mousedown', saatTekanTombol as EventListener)
lepas = () => {
el.removeEventListener('mouseup', saatLepasTombol as EventListener)
document.removeEventListener('mousedown', saatTekanTombol as EventListener)
}
},
{ immediate: true, flush: 'post' },
)
const berhenti = () => {
bongkar()
berhentiPantau()
}
if (getCurrentScope())
onScopeDispose(berhenti)
return { text, rect }
}API
export interface KotakPilihan {
top: number
left: number
bottom: number
right: number
height: number
width: number
}
export function useTextSelection(target?: TargetElemen)