Kliping
KlipingBerandaDokumentasiKomponenBlocksVariasiTemplateDice UIHooks
Memulai
  • Pengenalan
  • Instalasi
  • Kredit
State
  • useBoolean
  • useControllableValue
  • useCounter
  • useDebounce
  • useResetState
  • useThrottle
  • useToggle
Lanjutan
  • useCreation
  • useCustomCompareEffect
  • useLatest
  • useLockFn
  • useMemoizedFn
  • usePrevious
Siklus Hidup
  • useDebounceEffect
  • useDebounceFn
  • useDeepCompareEffect
  • useDeepCompareLayoutEffect
  • useEffectEvent
  • useEffectWithTarget
  • useInterval
  • useIsHydrated
  • useIsomorphicLayoutEffect
  • useMount
  • useThrottleEffect
  • useThrottleFn
  • useTimeout
  • useUnmount
  • useUpdate
  • useUpdateEffect
Peramban
  • useActiveElement
  • useBattery
  • useClickAnyWhere
  • useClickAway
  • useClipboard
  • useDocumentVisibility
  • useElementSize
  • useEventListener
  • useFps
  • useFullscreen
  • useHash
  • useHover
  • useInViewport
  • useIsMatchMedia
  • useIsOnline
  • useMouse
  • useNetwork
  • useOs
  • useScrollLock
  • useTextSelection
  • useTitle
Pengembangan
  • useWhyDidYouUpdate
Paket Luar
  • useMcp
  • useQuery
  • useStickToBottom
  • useSwr
Pustaka
  • isBrowser
  • createContext
  • bacaElemen
  • saatTerpasang

useTextSelection

SebelumnyaBerikutnya

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.

ts
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 ​

ts
export interface KotakPilihan {
  top: number
  left: number
  bottom: number
  right: number
  height: number
  width: number
}

export function useTextSelection(target?: TargetElemen)
useScrollLockuseTitle

Di halaman ini

PemasanganAPI
Dirilis di bawah lisensi MIT. Ditulis ulang untuk Vue dari shadcn-vue, shadcn-ui-blocks, dan shadcn-hooks. Hak cipta © 2026 Kliping