@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.
- package/README.ja.md +203 -0
- package/README.md +205 -0
- package/dist/auto.js +3 -0
- package/dist/auto.min.js +3 -0
- package/dist/index.d.ts +291 -0
- package/dist/index.esm.js +711 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.esm.min.js.map +1 -0
- package/package.json +71 -0
|
@@ -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
|