kliping.pro
  • Docs
  • Directory
  • Colors
  • Typeset
Hooks
  • State7
  • Advanced6
  • Lifecycle16
  • Browser21
    • useActiveElement
    • useBattery
    • useClickAnyWhere
    • useClickAway
    • useClipboard
    • useDocumentVisibility
    • useElementSize
    • useEventListener
    • useFps
    • useFullscreen
    • useHash
    • useHover
    • useInViewport
    • useIsMatchMedia
    • useIsOnline
    • useMouse
    • useNetwork
    • useOs
    • useScrollLock
    • useTextSelection
    • useTitle
  • Development1
  • Third-Party4
  • Libraries4

useTextSelection

PreviousNext

Teks yang sedang disorot, beserta letaknya di layar.

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.

Installation ​

pnpm dlx 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 { getTargetElement } from '@/lib/target'
import type { BasicTarget } from '@/lib/target'

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

const EMPTY_RECT: SelectionRect = {
  top: Number.NaN,
  left: Number.NaN,
  bottom: Number.NaN,
  right: Number.NaN,
  height: Number.NaN,
  width: Number.NaN,
}

function rectFromSelection(selection: Selection | null): SelectionRect {
  if (!selection || selection.rangeCount < 1) return EMPTY_RECT

  const { top, left, bottom, right, height, width } = selection
    .getRangeAt(0)
    .getBoundingClientRect()
  return { top, left, bottom, right, height, width }
}

export function useTextSelection(target?: BasicTarget) {
  const text = ref('')
  const rect = ref<SelectionRect>({ ...EMPTY_RECT })

  let startedInside = false
  let detach: (() => void) | undefined

  const teardown = () => {
    detach?.()
    detach = undefined
  }

  const stopWatch = watch(
    () => getTargetElement(target),
    (element) => {
      teardown()
      if (!isBrowser) return

      const el = (element ?? document) as Document | Element

      const onMouseUp = () => {
        if (typeof window.getSelection !== 'function') return

        const selection = window.getSelection()
        const content = selection ? selection.toString() : ''

        if (content && startedInside) {
          text.value = content
          rect.value = rectFromSelection(selection)
        }
      }

      const onMouseDown = (event: MouseEvent) => {
        if (event.button === 2) return
        if (typeof window.getSelection !== 'function') return

        if (text.value) {
          text.value = ''
          rect.value = { ...EMPTY_RECT }
        }

        startedInside = false
        const selection = window.getSelection()
        if (!selection) return

        selection.removeAllRanges()
        startedInside = 'contains' in el ? el.contains(event.target as Node) : true
      }

      el.addEventListener('mouseup', onMouseUp as EventListener)
      document.addEventListener('mousedown', onMouseDown as EventListener)

      detach = () => {
        el.removeEventListener('mouseup', onMouseUp as EventListener)
        document.removeEventListener('mousedown', onMouseDown as EventListener)
      }
    },
    { immediate: true, flush: 'post' },
  )

  const stop = () => {
    teardown()
    stopWatch()
  }

  if (getCurrentScope()) onScopeDispose(stop)

  return { text, rect }
}

API ​

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

export function useTextSelection(target?: BasicTarget)
PrevioususeScrollLockNext useTitle

On This Page

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