@wcstack/clipboard 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,711 @@
1
+ const _config = {
2
+ autoTrigger: true,
3
+ triggerAttribute: "data-clipboardtarget",
4
+ tagNames: {
5
+ clipboard: "wcs-clipboard",
6
+ },
7
+ };
8
+ function deepFreeze(obj) {
9
+ if (obj === null || typeof obj !== "object")
10
+ return obj;
11
+ Object.freeze(obj);
12
+ for (const key of Object.keys(obj)) {
13
+ deepFreeze(obj[key]);
14
+ }
15
+ return obj;
16
+ }
17
+ function deepClone(obj) {
18
+ if (obj === null || typeof obj !== "object")
19
+ return obj;
20
+ const clone = {};
21
+ for (const key of Object.keys(obj)) {
22
+ clone[key] = deepClone(obj[key]);
23
+ }
24
+ return clone;
25
+ }
26
+ let frozenConfig = null;
27
+ const config = _config;
28
+ function getConfig() {
29
+ if (!frozenConfig) {
30
+ frozenConfig = deepFreeze(deepClone(_config));
31
+ }
32
+ return frozenConfig;
33
+ }
34
+ function setConfig(partialConfig) {
35
+ if (typeof partialConfig.autoTrigger === "boolean") {
36
+ _config.autoTrigger = partialConfig.autoTrigger;
37
+ }
38
+ if (typeof partialConfig.triggerAttribute === "string") {
39
+ _config.triggerAttribute = partialConfig.triggerAttribute;
40
+ }
41
+ if (partialConfig.tagNames) {
42
+ Object.assign(_config.tagNames, partialConfig.tagNames);
43
+ }
44
+ frozenConfig = null;
45
+ }
46
+
47
+ /**
48
+ * Headless clipboard primitive. A thin, framework-agnostic wrapper around the
49
+ * Clipboard API exposed through the wc-bindable protocol.
50
+ *
51
+ * It has two surfaces, mirroring the two distinct shapes of clipboard access:
52
+ * - **commands** — `writeText()` / `write()` push to the clipboard;
53
+ * `readText()` / `read()` pull from it. These are the `state → element`
54
+ * (command-token) and `element → state` (read result) paths. All four are
55
+ * async and never reject: failures surface through the `error` property so
56
+ * they flow into the declarative state, symmetrical with FetchCore /
57
+ * GeolocationCore.
58
+ * - **monitor** — `startMonitor()` / `stopMonitor()` subscribe to the document's
59
+ * `copy` / `cut` / `paste` events and republish them as the `copied` / `cut` /
60
+ * `pasted` properties (like TimerCore's continuous `start()` / `stop()`),
61
+ * toggling the `monitoring` flag. This is the event-token showcase: a user
62
+ * paste flows element → state declaratively.
63
+ *
64
+ * Clipboard also has permission gates, like GeolocationCore but doubled: read
65
+ * and write are separate permissions (`clipboard-read` / `clipboard-write`).
66
+ * `readPermission` / `writePermission` reflect `navigator.permissions.query`
67
+ * (`prompt` / `granted` / `denied`, or `unsupported`) and track their live
68
+ * `change` events.
69
+ */
70
+ class ClipboardCore extends EventTarget {
71
+ static wcBindable = {
72
+ protocol: "wc-bindable",
73
+ version: 1,
74
+ properties: [
75
+ { name: "text", event: "wcs-clipboard:read", getter: (e) => e.detail.text },
76
+ { name: "items", event: "wcs-clipboard:read", getter: (e) => e.detail.items },
77
+ { name: "loading", event: "wcs-clipboard:loading-changed" },
78
+ { name: "error", event: "wcs-clipboard:error" },
79
+ { name: "readPermission", event: "wcs-clipboard:read-permission-changed" },
80
+ { name: "writePermission", event: "wcs-clipboard:write-permission-changed" },
81
+ { name: "monitoring", event: "wcs-clipboard:monitoring-changed" },
82
+ { name: "copied", event: "wcs-clipboard:copied", getter: (e) => e.detail },
83
+ { name: "cut", event: "wcs-clipboard:cut", getter: (e) => e.detail },
84
+ { name: "pasted", event: "wcs-clipboard:pasted", getter: (e) => e.detail },
85
+ ],
86
+ commands: [
87
+ { name: "writeText", async: true },
88
+ { name: "write", async: true },
89
+ { name: "readText", async: true },
90
+ { name: "read", async: true },
91
+ { name: "startMonitor" },
92
+ { name: "stopMonitor" },
93
+ ],
94
+ };
95
+ _target;
96
+ _text = null;
97
+ _items = null;
98
+ _loading = false;
99
+ _error = null;
100
+ _readPermission = "prompt";
101
+ _writePermission = "prompt";
102
+ _monitoring = false;
103
+ _copied = null;
104
+ _cut = null;
105
+ _pasted = null;
106
+ // Live PermissionStatus handles (when the Permissions API is available), kept
107
+ // so the `change` listeners can be removed on dispose(). Read and write are
108
+ // separate permissions, hence two handles.
109
+ _readStatus = null;
110
+ _writeStatus = null;
111
+ // True once a permission subscription has been (or is being) established, and
112
+ // reset by dispose(). Guards reinitPermission() so the first connect after
113
+ // construction does not double-subscribe, while a reconnect after dispose()
114
+ // does re-subscribe. (Mirrors GeolocationCore.)
115
+ _permissionSubscribed = false;
116
+ // Monotonic id of the current permission query round. Bumped by every
117
+ // _initPermissions() and by dispose(). Each in-flight query captures it and,
118
+ // on resolve, bails unless it is still current — so a query superseded by a
119
+ // rapid (synchronous) disconnect→reconnect, or one that resolves after
120
+ // dispose(), never attaches a listener.
121
+ _permGen = 0;
122
+ // Monotonic id of the current async acquisition lifecycle (read/write),
123
+ // bumped only by dispose(). Each command captures it at start; the resolution
124
+ // bails (no setters) if it is stale, so an op that settles after the element
125
+ // was disconnected does not dispatch wcs-clipboard:* on a torn-down element.
126
+ // The Clipboard API has no AbortController, so a generation guard is the only
127
+ // way to neutralize an in-flight op.
128
+ _acqGen = 0;
129
+ constructor(target) {
130
+ super();
131
+ this._target = target ?? this;
132
+ // Probe the permission states up front so observers see the real values
133
+ // before the first read, then keep them live.
134
+ this._initPermissions();
135
+ }
136
+ get text() {
137
+ return this._text;
138
+ }
139
+ get items() {
140
+ return this._items;
141
+ }
142
+ get loading() {
143
+ return this._loading;
144
+ }
145
+ get error() {
146
+ return this._error;
147
+ }
148
+ get readPermission() {
149
+ return this._readPermission;
150
+ }
151
+ get writePermission() {
152
+ return this._writePermission;
153
+ }
154
+ get monitoring() {
155
+ return this._monitoring;
156
+ }
157
+ get copied() {
158
+ return this._copied;
159
+ }
160
+ get cut() {
161
+ return this._cut;
162
+ }
163
+ get pasted() {
164
+ return this._pasted;
165
+ }
166
+ // --- State setters with event dispatch ---
167
+ // Deliberately NO same-value guard (unlike error/loading/permission/monitoring).
168
+ // A read is a result event, not idempotent state: reading the same text twice is
169
+ // two distinct user/command actions and must re-fire wcs-clipboard:read each time
170
+ // so a `text:`/`items:` binding and command-result consumers see every read.
171
+ _setRead(detail) {
172
+ this._text = detail.text;
173
+ this._items = detail.items;
174
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:read", {
175
+ detail,
176
+ bubbles: true,
177
+ }));
178
+ }
179
+ _setLoading(loading) {
180
+ if (this._loading === loading)
181
+ return;
182
+ this._loading = loading;
183
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:loading-changed", {
184
+ detail: loading,
185
+ bubbles: true,
186
+ }));
187
+ }
188
+ _setError(error) {
189
+ // Same-value guard. `error` has no derived state, so suppressing redundant
190
+ // null→null dispatches (e.g. a successful op clearing an already-null error)
191
+ // avoids spurious events. Reference identity is sufficient: each failure
192
+ // builds a fresh object, and the clear path always passes the literal null.
193
+ if (this._error === error)
194
+ return;
195
+ this._error = error;
196
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:error", {
197
+ detail: error,
198
+ bubbles: true,
199
+ }));
200
+ }
201
+ _setReadPermission(permission) {
202
+ if (this._readPermission === permission)
203
+ return;
204
+ this._readPermission = permission;
205
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:read-permission-changed", {
206
+ detail: permission,
207
+ bubbles: true,
208
+ }));
209
+ }
210
+ _setWritePermission(permission) {
211
+ if (this._writePermission === permission)
212
+ return;
213
+ this._writePermission = permission;
214
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:write-permission-changed", {
215
+ detail: permission,
216
+ bubbles: true,
217
+ }));
218
+ }
219
+ _setMonitoring(monitoring) {
220
+ if (this._monitoring === monitoring)
221
+ return;
222
+ this._monitoring = monitoring;
223
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:monitoring-changed", {
224
+ detail: monitoring,
225
+ bubbles: true,
226
+ }));
227
+ }
228
+ // Deliberately NO same-value guard on the copied/cut/pasted setters (unlike
229
+ // error/loading/permission/monitoring above). These are events, not state:
230
+ // copying the same text twice is two distinct user actions and must re-fire
231
+ // both times so an event-token subscriber (`eventToken.pasted: ...`) sees each
232
+ // occurrence. Do not add a `===` guard here.
233
+ _setCopied(text) {
234
+ this._copied = text;
235
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:copied", {
236
+ detail: text,
237
+ bubbles: true,
238
+ }));
239
+ }
240
+ _setCut(text) {
241
+ this._cut = text;
242
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:cut", {
243
+ detail: text,
244
+ bubbles: true,
245
+ }));
246
+ }
247
+ _setPasted(text) {
248
+ this._pasted = text;
249
+ this._target.dispatchEvent(new CustomEvent("wcs-clipboard:pasted", {
250
+ detail: text,
251
+ bubbles: true,
252
+ }));
253
+ }
254
+ // --- Public API: write ---
255
+ /**
256
+ * Write plain text to the clipboard. Resolves once the write settles or fails
257
+ * — never rejects: failures surface through `error`. Requires transient
258
+ * activation (a user gesture), so call from a click handler / command-token.
259
+ */
260
+ writeText(text) {
261
+ return this._runWrite(() => navigator.clipboard.writeText(text));
262
+ }
263
+ /**
264
+ * Write rich `ClipboardItem`s (images, HTML, multiple MIME types) to the
265
+ * clipboard. Resolves once the write settles or fails — never rejects.
266
+ */
267
+ write(items) {
268
+ return this._runWrite(() => navigator.clipboard.write(items));
269
+ }
270
+ // --- Public API: read ---
271
+ /**
272
+ * Read plain text from the clipboard, publishing it via `text` and the
273
+ * `wcs-clipboard:read` event. Resolves once the read settles or fails — never
274
+ * rejects. Requires focus + read permission.
275
+ */
276
+ readText() {
277
+ return this._runRead(async () => {
278
+ const text = await navigator.clipboard.readText();
279
+ return { text, items: null };
280
+ });
281
+ }
282
+ /**
283
+ * Read rich `ClipboardItem`s from the clipboard, eagerly resolving every
284
+ * representation to a `Blob`. A `text/plain` representation is also surfaced
285
+ * via `text`. Resolves once the read settles or fails — never rejects.
286
+ */
287
+ read() {
288
+ return this._runRead(async () => {
289
+ const items = await navigator.clipboard.read();
290
+ return this._normalizeItems(items);
291
+ });
292
+ }
293
+ // --- Public API: monitor ---
294
+ /**
295
+ * Begin monitoring document `copy` / `cut` / `paste` events, republishing
296
+ * them as the `copied` / `cut` / `pasted` properties. Idempotent while already
297
+ * monitoring (mirrors GeolocationCore.watch()).
298
+ */
299
+ startMonitor() {
300
+ if (this._monitoring)
301
+ return;
302
+ this._setMonitoring(true);
303
+ document.addEventListener("copy", this._onCopy);
304
+ document.addEventListener("cut", this._onCut);
305
+ document.addEventListener("paste", this._onPaste);
306
+ }
307
+ stopMonitor() {
308
+ this._removeMonitorListeners();
309
+ this._setMonitoring(false);
310
+ }
311
+ // --- Permission lifecycle ---
312
+ /**
313
+ * Re-establish the permission `change` subscriptions after a dispose() — e.g.
314
+ * the Shell element was disconnected and then reconnected (reparented). No-op
315
+ * while a subscription is already live, so the first connect after
316
+ * construction does not double-subscribe.
317
+ */
318
+ reinitPermission() {
319
+ if (!this._permissionSubscribed) {
320
+ this._initPermissions();
321
+ }
322
+ }
323
+ /**
324
+ * Detach the live permission `change` listeners and any monitor listeners, and
325
+ * neutralize in-flight async ops. Call from the Shell's `disconnectedCallback`
326
+ * so a removed element does not leak subscriptions or dispatch on a torn-down
327
+ * element. A later reconnect can re-subscribe via reinitPermission().
328
+ */
329
+ dispose() {
330
+ this._permissionSubscribed = false;
331
+ // Invalidate any in-flight permission query so its .then() bails instead of
332
+ // attaching a listener after teardown.
333
+ this._permGen++;
334
+ // Invalidate any in-flight read/write so its resolution bails instead of
335
+ // dispatching on a disconnected element.
336
+ this._acqGen++;
337
+ // Reset the loading shadow silently (no dispatch on a disposed element). The
338
+ // bailed resolution will not clear it, and leaving it true would let the
339
+ // same-value guard swallow the loading=true edge of the next op after a
340
+ // reconnect.
341
+ this._loading = false;
342
+ if (this._readStatus) {
343
+ this._readStatus.removeEventListener("change", this._onReadChange);
344
+ this._readStatus = null;
345
+ }
346
+ if (this._writeStatus) {
347
+ this._writeStatus.removeEventListener("change", this._onWriteChange);
348
+ this._writeStatus = null;
349
+ }
350
+ // Remove monitor listeners silently. The Shell calls stopMonitor() before
351
+ // dispose(), but a direct headless dispose() still tears them down.
352
+ this._removeMonitorListeners();
353
+ this._monitoring = false;
354
+ }
355
+ // --- Internal: write/read runners ---
356
+ _runWrite(op) {
357
+ return this._runOp(async () => {
358
+ await op();
359
+ return null;
360
+ });
361
+ }
362
+ _runRead(op) {
363
+ return this._runOp(op);
364
+ }
365
+ /**
366
+ * Shared async-op lifecycle for read/write: capability check, loading toggle,
367
+ * generation guard, never-reject error handling. When `op` returns a read
368
+ * detail it is published; when it returns null (a write) nothing is published.
369
+ */
370
+ async _runOp(op) {
371
+ if (!this._hasClipboard()) {
372
+ this._setError(this._unsupportedError());
373
+ return;
374
+ }
375
+ const gen = this._acqGen;
376
+ this._setLoading(true);
377
+ this._setError(null);
378
+ try {
379
+ const detail = await op();
380
+ // Stale: the element was disposed (disconnected) while this op was in
381
+ // flight. Drop it so a torn-down element never dispatches wcs-clipboard:*.
382
+ if (gen !== this._acqGen)
383
+ return;
384
+ this._setLoading(false);
385
+ if (detail)
386
+ this._setRead(detail);
387
+ }
388
+ catch (err) {
389
+ if (gen !== this._acqGen)
390
+ return;
391
+ this._setLoading(false);
392
+ this._setError(this._normalizeError(err));
393
+ }
394
+ }
395
+ // --- Internal: monitor handlers ---
396
+ // During a `copy` / `cut` event the clipboard payload is not yet readable —
397
+ // the browser returns an empty string for security reasons — so we report the
398
+ // user's selected text (`document.getSelection().toString()`) instead. A page
399
+ // that overrides the payload with a custom handler via clipboardData.setData()
400
+ // is therefore NOT reflected here. (See README "copy / cut text comes from the
401
+ // selection".) `paste` differs: clipboardData is readable, so _onPaste reads it.
402
+ _onCopy = () => {
403
+ this._setCopied(this._selectionText());
404
+ };
405
+ _onCut = () => {
406
+ this._setCut(this._selectionText());
407
+ };
408
+ _onPaste = (event) => {
409
+ const data = event.clipboardData;
410
+ const text = data ? data.getData("text/plain") : "";
411
+ this._setPasted(text);
412
+ };
413
+ _removeMonitorListeners() {
414
+ document.removeEventListener("copy", this._onCopy);
415
+ document.removeEventListener("cut", this._onCut);
416
+ document.removeEventListener("paste", this._onPaste);
417
+ }
418
+ _selectionText() {
419
+ const selection = document.getSelection();
420
+ return selection ? selection.toString() : "";
421
+ }
422
+ // --- Internal: permission ---
423
+ _initPermissions() {
424
+ // The Permissions API is optional. When absent (or it rejects, e.g. Firefox
425
+ // does not expose the clipboard permission names), report "unsupported" and
426
+ // leave reads/writes to fail loudly via the error property if attempted.
427
+ // Note: we deliberately do NOT set _permissionSubscribed here — there is no
428
+ // live subscription to tear down, so reinitPermission() re-runs this branch
429
+ // on every reconnect. That is harmless: the same-value guard in
430
+ // _setReadPermission/_setWritePermission swallows the redundant
431
+ // unsupported→unsupported dispatch. (Mirrors GeolocationCore.)
432
+ if (typeof navigator === "undefined" || !navigator.permissions || typeof navigator.permissions.query !== "function") {
433
+ this._setReadPermission("unsupported");
434
+ this._setWritePermission("unsupported");
435
+ return;
436
+ }
437
+ this._permissionSubscribed = true;
438
+ const gen = ++this._permGen;
439
+ this._queryPermission("clipboard-read", gen, (s) => { this._readStatus = s; }, (state) => this._setReadPermission(state), this._onReadChange);
440
+ this._queryPermission("clipboard-write", gen, (s) => { this._writeStatus = s; }, (state) => this._setWritePermission(state), this._onWriteChange);
441
+ }
442
+ _queryPermission(name, gen, assignStatus, setState, onChange) {
443
+ navigator.permissions.query({ name: name }).then((status) => {
444
+ // Stale resolution: this query was superseded (rapid reconnect) or the
445
+ // element was disposed while it was in flight. Drop it so only the
446
+ // current subscription attaches a listener.
447
+ if (gen !== this._permGen)
448
+ return;
449
+ assignStatus(status);
450
+ setState(status.state);
451
+ status.addEventListener("change", onChange);
452
+ }, () => {
453
+ if (gen !== this._permGen)
454
+ return;
455
+ setState("unsupported");
456
+ });
457
+ }
458
+ _onReadChange = (event) => {
459
+ const status = event.target;
460
+ this._setReadPermission(status.state);
461
+ };
462
+ _onWriteChange = (event) => {
463
+ const status = event.target;
464
+ this._setWritePermission(status.state);
465
+ };
466
+ // --- Internal: normalization ---
467
+ _hasClipboard() {
468
+ return typeof navigator !== "undefined" && !!navigator.clipboard;
469
+ }
470
+ async _normalizeItems(items) {
471
+ // Resolve every representation of every item in parallel. getType() calls are
472
+ // independent, so awaiting them serially only adds latency. The trade-off is
473
+ // intentional and unchanged from the serial version: if any getType() rejects
474
+ // the whole read errors (no partial success), consistent with the never-reject
475
+ // design where a failed op surfaces a single `error` rather than a half-filled
476
+ // snapshot. Order is preserved so the `text` pick below stays deterministic.
477
+ const resolved = await Promise.all(items.map((item) => Promise.all(item.types.map((type) => item.getType(type))).then((blobs) => ({ item, blobs }))));
478
+ const normalized = [];
479
+ let text = null;
480
+ for (const { item, blobs } of resolved) {
481
+ const data = {};
482
+ item.types.forEach((type, i) => {
483
+ data[type] = blobs[i];
484
+ });
485
+ // Surface the first text/plain representation through `text` for the
486
+ // common "read whatever text is there" case (first item, first match).
487
+ if (text === null) {
488
+ const i = item.types.indexOf("text/plain");
489
+ if (i !== -1) {
490
+ text = await blobs[i].text();
491
+ }
492
+ }
493
+ normalized.push({ types: [...item.types], data });
494
+ }
495
+ return { text, items: normalized };
496
+ }
497
+ _normalizeError(err) {
498
+ if (err instanceof Error) {
499
+ // DOMException is an Error subclass; its `name` (NotAllowedError, etc.) is
500
+ // the meaningful discriminator for consumers switching on failure kind.
501
+ return { name: err.name, message: err.message };
502
+ }
503
+ return { name: "Error", message: String(err) };
504
+ }
505
+ _unsupportedError() {
506
+ return {
507
+ name: "NotSupportedError",
508
+ message: "Clipboard API is not available in this environment.",
509
+ };
510
+ }
511
+ }
512
+
513
+ let registered = false;
514
+ // Attribute names for the optional copy-on-click DOM trigger (clipboard.js-style
515
+ // DX). The element carrying `data-clipboardtarget` points at a <wcs-clipboard>
516
+ // by id; the text to copy comes from either a literal `data-clipboard-text` or
517
+ // a `data-clipboard-from` CSS selector resolving to a source element.
518
+ const TEXT_ATTRIBUTE = "data-clipboard-text";
519
+ const FROM_ATTRIBUTE = "data-clipboard-from";
520
+ function resolveText(triggerElement) {
521
+ // Literal text wins when present (including an empty string — copying "" is a
522
+ // legitimate request).
523
+ if (triggerElement.hasAttribute(TEXT_ATTRIBUTE)) {
524
+ return triggerElement.getAttribute(TEXT_ATTRIBUTE) ?? "";
525
+ }
526
+ const selector = triggerElement.getAttribute(FROM_ATTRIBUTE);
527
+ if (!selector)
528
+ return null;
529
+ const source = document.querySelector(selector);
530
+ if (!source)
531
+ return null;
532
+ // Read a form control's `value`; fall back to text content. A bare
533
+ // `"value" in source` check is too broad — it also matches <button>,
534
+ // <li value>, <progress>, etc. (which carry an unrelated `value`), copying
535
+ // the wrong thing. Narrow to the text-bearing controls a user actually points
536
+ // `data-clipboard-from` at; everything else falls through to textContent.
537
+ if (source instanceof HTMLInputElement ||
538
+ source instanceof HTMLTextAreaElement ||
539
+ source instanceof HTMLSelectElement) {
540
+ return source.value;
541
+ }
542
+ return source.textContent ?? "";
543
+ }
544
+ function handleClick(event) {
545
+ const target = event.target;
546
+ if (!(target instanceof Element))
547
+ return;
548
+ const triggerElement = target.closest(`[${config.triggerAttribute}]`);
549
+ if (!triggerElement)
550
+ return;
551
+ const clipboardId = triggerElement.getAttribute(config.triggerAttribute);
552
+ if (!clipboardId)
553
+ return;
554
+ // Resolve the registered constructor at call time instead of importing
555
+ // WcsClipboard as a value (avoids a components ⇄ autoTrigger import cycle:
556
+ // Clipboard.connectedCallback() calls registerAutoTrigger()). instanceof
557
+ // against the customElements registry keeps the same identity guarantee.
558
+ const ClipboardCtor = customElements.get(config.tagNames.clipboard);
559
+ const clipboardElement = document.getElementById(clipboardId);
560
+ if (!ClipboardCtor || !(clipboardElement instanceof ClipboardCtor))
561
+ return;
562
+ const text = resolveText(triggerElement);
563
+ // No resolvable source: leave the click alone (do not preventDefault) so the
564
+ // element's default action is unaffected.
565
+ if (text === null)
566
+ return;
567
+ // Suppress the default action so a copy can run without navigating. Intentional:
568
+ // do not attach data-clipboardtarget to an element whose default action you
569
+ // also want (real <a href> link). See README "Optional DOM Triggering".
570
+ event.preventDefault();
571
+ clipboardElement.writeText(text);
572
+ }
573
+ function registerAutoTrigger() {
574
+ if (registered)
575
+ return;
576
+ registered = true;
577
+ document.addEventListener("click", handleClick);
578
+ }
579
+
580
+ // Named WcsClipboard (not `Clipboard`) so the class does not shadow the global
581
+ // DOM `Clipboard` interface (the type of `navigator.clipboard`), matching the
582
+ // <wcs-geo> WcsGeolocation / <wcs-ws> WcsWebSocket convention.
583
+ class WcsClipboard extends HTMLElement {
584
+ static wcBindable = {
585
+ ...ClipboardCore.wcBindable,
586
+ // Shell-level settable surface. `monitor` mirrors its boolean attribute
587
+ // (reflects idempotently), following the <wcs-ws> / <wcs-geo> convention.
588
+ // There is no momentary `trigger` property: writes need an argument (the
589
+ // text/items), so element actions are driven via command-token
590
+ // (`command.writeText: $command.copy`) or the DOM autoTrigger, not a
591
+ // false→true boolean pulse.
592
+ inputs: [
593
+ { name: "monitor", attribute: "monitor" },
594
+ ],
595
+ // Commands are identical to the Core's — no rename is needed because the
596
+ // `monitor` boolean attribute accessor does not collide with the
597
+ // `startMonitor` / `stopMonitor` command names (unlike <wcs-geo>, whose
598
+ // `watch` attribute forced the Core's `watch` command to `watchPosition`).
599
+ commands: ClipboardCore.wcBindable.commands,
600
+ };
601
+ _core;
602
+ constructor() {
603
+ super();
604
+ this._core = new ClipboardCore(this);
605
+ }
606
+ // --- Attribute accessors ---
607
+ get monitor() {
608
+ return this.hasAttribute("monitor");
609
+ }
610
+ /**
611
+ * Reflects the `monitor` boolean attribute only — it does NOT start or stop
612
+ * monitoring by itself. The attribute is read at connect time (see
613
+ * connectedCallback); toggling `el.monitor` after connect just flips the
614
+ * attribute. To start/stop monitoring imperatively, call `startMonitor()` /
615
+ * `stopMonitor()`.
616
+ */
617
+ set monitor(value) {
618
+ if (value) {
619
+ this.setAttribute("monitor", "");
620
+ }
621
+ else {
622
+ this.removeAttribute("monitor");
623
+ }
624
+ }
625
+ // --- Core delegated getters ---
626
+ get text() {
627
+ return this._core.text;
628
+ }
629
+ get items() {
630
+ return this._core.items;
631
+ }
632
+ get loading() {
633
+ return this._core.loading;
634
+ }
635
+ get error() {
636
+ return this._core.error;
637
+ }
638
+ get readPermission() {
639
+ return this._core.readPermission;
640
+ }
641
+ get writePermission() {
642
+ return this._core.writePermission;
643
+ }
644
+ get monitoring() {
645
+ return this._core.monitoring;
646
+ }
647
+ get copied() {
648
+ return this._core.copied;
649
+ }
650
+ get cut() {
651
+ return this._core.cut;
652
+ }
653
+ get pasted() {
654
+ return this._core.pasted;
655
+ }
656
+ // --- Commands ---
657
+ writeText(text) {
658
+ return this._core.writeText(text);
659
+ }
660
+ write(items) {
661
+ return this._core.write(items);
662
+ }
663
+ readText() {
664
+ return this._core.readText();
665
+ }
666
+ read() {
667
+ return this._core.read();
668
+ }
669
+ startMonitor() {
670
+ this._core.startMonitor();
671
+ }
672
+ stopMonitor() {
673
+ this._core.stopMonitor();
674
+ }
675
+ // --- Lifecycle ---
676
+ connectedCallback() {
677
+ this.style.display = "none";
678
+ if (config.autoTrigger) {
679
+ registerAutoTrigger();
680
+ }
681
+ // Revive permission tracking after a reconnect (reparenting). No-op on the
682
+ // first connect since the constructor already subscribed; only re-subscribes
683
+ // when disconnectedCallback's dispose() tore the subscription down.
684
+ this._core.reinitPermission();
685
+ // Unlike <wcs-geo>, there is no connect-time acquisition: reads require a
686
+ // user gesture, so the only connect-time action is optional monitoring.
687
+ if (this.monitor) {
688
+ this._core.startMonitor();
689
+ }
690
+ }
691
+ disconnectedCallback() {
692
+ this._core.stopMonitor();
693
+ this._core.dispose();
694
+ }
695
+ }
696
+
697
+ function registerComponents() {
698
+ if (!customElements.get(config.tagNames.clipboard)) {
699
+ customElements.define(config.tagNames.clipboard, WcsClipboard);
700
+ }
701
+ }
702
+
703
+ function bootstrapClipboard(userConfig) {
704
+ if (userConfig) {
705
+ setConfig(userConfig);
706
+ }
707
+ registerComponents();
708
+ }
709
+
710
+ export { ClipboardCore, WcsClipboard, bootstrapClipboard, getConfig };
711
+ //# sourceMappingURL=index.esm.js.map