Skip to content

Commit

Permalink
feat(useClipboard): enhance legacyCopy behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
vikiboss committed Aug 19, 2024
1 parent edd3ff0 commit bccfecb
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 17 deletions.
19 changes: 2 additions & 17 deletions src/use-clipboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useTimeoutFn } from '../use-timeout-fn'
import { useTrackedRefState } from '../use-tracked-ref-state'
import { isDefined } from '../utils/basic'
import { unwrapGettable } from '../utils/unwrap'
import { legacyCopy, legacyRead } from './legacy'

import type { UsePermissionReturns } from '../use-permission'
import type { Gettable } from '../utils/basic'
Expand Down Expand Up @@ -124,7 +125,7 @@ export function useClipboard(
actions.updateRefState('text', value)
actions.updateRefState('copied', true)
startTimeout()
onCopy?.(value)
latest.current.onCopy?.(value)
})

const clear = useStableFn(() => {
Expand All @@ -147,22 +148,6 @@ export function useClipboard(
}
}

function legacyCopy(value: string) {
const textarea = document.createElement('textarea')
textarea.value = value
textarea.style.position = 'absolute'
textarea.style.opacity = '0'
textarea.style.zIndex = '-999999999'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
textarea.remove()
}

function legacyRead() {
return document.getSelection()?.toString() ?? ''
}

function isAllowed(status: UsePermissionReturns<false>) {
return status.current && ['granted', 'prompt'].includes(status.current)
}
103 changes: 103 additions & 0 deletions src/use-clipboard/legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { noop } from '../utils/basic'

export function legacyCopy(value: string) {
const restoreSelection = removeSelection()
const textarea = createHiddenTextarea(value)
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
textarea.remove()
restoreSelection()
}

export function legacyRead() {
return document.getSelection()?.toString() ?? ''
}

// @see https://github.com/sudodoki/toggle-selection/blob/ac73e2b274c10d019d1f13e4da5f8fc93809806a/index.js
export function removeSelection() {
const activeEl = document.activeElement
const selection = document.getSelection()

if (!selection || !selection.rangeCount || !activeEl) return noop

const ranges: Range[] = []

for (let i = 0; i < selection.rangeCount; i++) {
ranges.push(selection.getRangeAt(i))
}

isInputtable(activeEl) && activeEl.blur()

selection.removeAllRanges()

return function restoreSelection() {
selection.type === 'Caret' && selection.removeAllRanges()

if (!selection.rangeCount) {
for (const range of ranges) {
selection.addRange(range)
}
}

isInputtable(activeEl) && activeEl.focus()
}
}

function createHiddenTextarea(value: string = '') {
const textarea = document.createElement('textarea')

// reset user styles for textarea element
textarea.style.all = 'unset'

// Prevent zooming on iOS
textarea.style.fontSize = '12pt'

// Reset box model
textarea.style.border = '0'
textarea.style.padding = '0'
textarea.style.margin = '0'

// Move element out of screen horizontally
textarea.style.position = 'absolute'
const isRTL = document.documentElement.getAttribute('dir') === 'rtl'
textarea.style[isRTL ? 'right' : 'left'] = '-99999px'
textarea.style.opacity = '0'
textarea.style.zIndex = '-999999999'

// Move element to the same position vertically
const yPosition = window.scrollY || window.pageYOffset || document.documentElement.scrollTop
textarea.style.top = `${yPosition}px`

// avoid screen readers from reading out loud the text
textarea.ariaHidden = 'true'

// used to preserve spaces and line breaks
textarea.style.whiteSpace = 'pre'

// do not inherit user-select (it may be `none`)
textarea.style.webkitUserSelect = 'text'
// @ts-expect-error legacy style
textarea.style.MozUserSelect = 'text'
// @ts-expect-error legacy style
textarea.style.msUserSelect = 'text'
textarea.style.userSelect = 'text'

textarea.setAttribute('readonly', '')

textarea.value = value

return textarea
}

function isInputtable(active: Element | null): active is HTMLInputElement | HTMLTextAreaElement {
switch (active?.tagName.toUpperCase()) {
case 'INPUT':
case 'TEXTAREA':
return true
default:
return false
}

// return [HTMLInputElement, HTMLTextAreaElement].some((el) => active instanceof el)
}

0 comments on commit bccfecb

Please sign in to comment.