95 lines
3.4 KiB
Markdown
95 lines
3.4 KiB
Markdown
# `fallback/fallback.ts`
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## Purpose
|
|
|
|
Legacy clipboard fallback paths used when `navigator.clipboard` is
|
|
unavailable or rejects. Provides a hidden-textarea + `execCommand("copy")`
|
|
write path and a paste-event-based read path. Both return the same
|
|
`ClipboardResult` shape as the API path so the manager's call sites do
|
|
not branch on which mechanism actually ran. The paste-event fallback is
|
|
purpose-built for environments — older Safari, embedded WebViews, and
|
|
permission-denied contexts — where `navigator.clipboard.readText`
|
|
rejects but the user can paste manually.
|
|
|
|
## Features
|
|
|
|
- `copyTextFallback(text)` — synchronous textarea + `execCommand` copy.
|
|
- `readTextFallback(timeoutMs)` — focus + paste-event capture with a
|
|
configurable timeout.
|
|
- `dispatchSyntheticPaste(text)` — test helper that fires a synthetic
|
|
`paste` event on the most recent hidden host.
|
|
- SSR-safe — both copy and read paths return
|
|
`CLIPBOARD.DOCUMENT_UNAVAILABLE` when `document` is undefined.
|
|
- Off-screen, non-interactive host element so users never see the
|
|
textarea visibly steal focus.
|
|
|
|
## Usage: Basic
|
|
|
|
```ts
|
|
import { copyTextFallback } from "@suaveplan/clipboard";
|
|
|
|
const result = copyTextFallback("hello");
|
|
if (!result.success) {
|
|
console.warn("legacy copy failed:", result.code);
|
|
}
|
|
```
|
|
|
|
## Usage: Advanced
|
|
|
|
```ts
|
|
import {
|
|
readTextFallback,
|
|
dispatchSyntheticPaste,
|
|
} from "@suaveplan/clipboard";
|
|
|
|
async function readClipboardWithPasteFallback(): Promise<string | undefined> {
|
|
const result = await readTextFallback(2000);
|
|
return result.success ? result.data : undefined;
|
|
}
|
|
|
|
// Inside a test harness:
|
|
const promise = readTextFallback(500);
|
|
queueMicrotask(() => dispatchSyntheticPaste("from-test"));
|
|
const result = await promise;
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### `copyTextFallback(text: string): ClipboardResult<string>`
|
|
|
|
Stages a hidden `<textarea>` carrying `text`, selects it, runs
|
|
`document.execCommand("copy")`, and removes the textarea on cleanup.
|
|
Returns `{ success: true, data, method: "fallback" }` on success and a
|
|
structured failure with `code: CLIPBOARD.FALLBACK_FAILED` (or
|
|
`CLIPBOARD.DOCUMENT_UNAVAILABLE` outside the browser) otherwise.
|
|
|
|
### `readTextFallback(timeoutMs: number): Promise<ClipboardResult<string>>`
|
|
|
|
Creates a hidden textarea, focuses it, listens for the first `paste`
|
|
event, and resolves with the `text/plain` payload carried by the event.
|
|
Resolves with `code: CLIPBOARD.READ_FAILED` if the timeout elapses
|
|
first and `code: CLIPBOARD.DOCUMENT_UNAVAILABLE` outside the browser.
|
|
|
|
### `dispatchSyntheticPaste(text: string): boolean`
|
|
|
|
Test helper. Locates the most recent hidden host (`textarea[aria-hidden="true"]`)
|
|
and dispatches a `paste` ClipboardEvent carrying `text` as `text/plain`.
|
|
Returns `true` when a host was found, `false` otherwise.
|
|
|
|
## Implementation Notes
|
|
|
|
The hidden textarea is positioned at `-9999px` and given `opacity: 0`
|
|
plus `aria-hidden="true"` so assistive technologies skip it. The cleanup
|
|
runs in a `finally` block — even if `execCommand` throws, the textarea is
|
|
removed. The read fallback tracks a `settled` flag so a duplicate or
|
|
post-timeout `paste` event cannot resolve the promise twice. The paste
|
|
event may carry a null `clipboardData` on some browsers; the module
|
|
falls back to an empty string in that case rather than rejecting, since
|
|
"clipboard contained nothing" is a valid outcome. The synthetic-paste
|
|
helper is exported to keep tests deterministic — production code never
|
|
calls it.
|