1/**
2 * Browser clipboard integration for Effect programs.
3 *
4 * This module defines the `Clipboard` service, the `ClipboardError` raised by
5 * failed browser operations, a `make` constructor for custom implementations,
6 * and a browser-backed `layer` that uses `navigator.clipboard`. The service
7 * supports reading and writing text, reading and writing `ClipboardItem`
8 * payloads, writing one `Blob`, and clearing the clipboard.
9 *
10 * @since 4.0.0
11 */
12import * as Context from "effect/Context"
13import * as Data from "effect/Data"
14import * as Effect from "effect/Effect"
15import * as Layer from "effect/Layer"
16
17const TypeId = "~@effect/platform-browser/Clipboard"
18const ErrorTypeId = "~@effect/platform-browser/Clipboard/ClipboardError"
19
20/**
21 * Defines the service interface for reading from, writing to, and clearing the browser clipboard.
22 *
23 * **When to use**
24 *
25 * Use when an application needs clipboard operations through an Effect service
26 * so browser failures stay in the error channel.
27 *
28 * **Details**
29 *
30 * `read` and `write` work with `ClipboardItem` arrays. `readString` and
31 * `writeString` use text, `writeBlob` writes one `Blob`, and `clear` writes an
32 * empty string.
33 *
34 * **Gotchas**
35 *
36 * Clipboard access generally requires a secure context and may require user
37 * activation, permissions, or a focused document. `ClipboardItem` and non-text
38 * MIME type support varies by browser. Failed browser operations are surfaced
39 * as `ClipboardError`.
40 *
41 * @category services
42 * @since 4.0.0
43 */
44export interface Clipboard {
45 readonly [TypeId]: typeof TypeId
46 readonly read: Effect.Effect<ClipboardItems, ClipboardError>
47 readonly readString: Effect.Effect<string, ClipboardError>
48 readonly write: (items: ClipboardItems) => Effect.Effect<void, ClipboardError>
49 readonly writeString: (text: string) => Effect.Effect<void, ClipboardError>
50 readonly writeBlob: (blob: Blob) => Effect.Effect<void, ClipboardError>
51 readonly clear: Effect.Effect<void, ClipboardError>
52}
53
54/**
55 * Tagged error raised when a browser clipboard operation fails.
56 *
57 * @category errors
58 * @since 4.0.0
59 */
60export class ClipboardError extends Data.TaggedError("ClipboardError")<{
61 readonly message: string
62 readonly cause: unknown
63}> {
64 readonly [ErrorTypeId] = ErrorTypeId
65}
66
67/**
68 * Service tag for browser clipboard capabilities.
69 *
70 * **When to use**
71 *
72 * Use when you need to require or provide clipboard capabilities through
73 * Effect's context.
74 *
75 * @see {@link make} for building a custom clipboard service
76 * @see {@link layer} for providing the browser-backed clipboard service
77 *
78 * @category services
79 * @since 4.0.0
80 */
81export const Clipboard: Context.Service<Clipboard, Clipboard> = Context.Service<Clipboard>(TypeId)
82
83/**
84 * Builds a `Clipboard` service from primitive read and write operations, deriving `clear` and `writeBlob` helpers.
85 *
86 * @category constructors
87 * @since 4.0.0
88 */
89export const make = (
90 impl: Omit<Clipboard, "clear" | "writeBlob" | typeof TypeId>
91): Clipboard =>
92 Clipboard.of({
93 ...impl,
94 [TypeId]: TypeId,
95 clear: impl.writeString(""),
96 writeBlob: (blob: Blob) => impl.write([new ClipboardItem({ [blob.type]: blob })])
97 })
98
99/**
100 * Layer that directly interfaces with the browser Clipboard API.
101 *
102 * @category layers
103 * @since 4.0.0
104 */
105export const layer: Layer.Layer<Clipboard> = Layer.succeed(
106 Clipboard,
107 make({
108 read: Effect.tryPromise({
109 try: () => navigator.clipboard.read(),
110 catch: (cause) =>
111 new ClipboardError({
112 cause,
113 "message": "Unable to read from clipboard"
114 })
115 }),
116 write: (s: Array<ClipboardItem>) =>
117 Effect.tryPromise({
118 try: () => navigator.clipboard.write(s),
119 catch: (cause) =>
120 new ClipboardError({
121 cause,
122 "message": "Unable to write to clipboard"
123 })
124 }),
125 readString: Effect.tryPromise({
126 try: () => navigator.clipboard.readText(),
127 catch: (cause) =>
128 new ClipboardError({
129 cause,
130 "message": "Unable to read a string from clipboard"
131 })
132 }),
133 writeString: (text: string) =>
134 Effect.tryPromise({
135 try: () => navigator.clipboard.writeText(text),
136 catch: (cause) =>
137 new ClipboardError({
138 cause,
139 "message": "Unable to write a string to clipboard"
140 })
141 })
142 })
143)