-
Notifications
You must be signed in to change notification settings - Fork 166
Expand file tree
/
Copy pathclipboard-viewer.html
More file actions
353 lines (323 loc) · 17.6 KB
/
clipboard-viewer.html
File metadata and controls
353 lines (323 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clipboard Format Viewer</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
h1 {
text-align: center;
color: #333;
}
#paste-area {
width: 100%;
height: 100px;
margin-bottom: 20px;
padding: 10px;
border: 2px dashed #aaa;
border-radius: 5px;
resize: vertical;
background-color: #fff;
}
#output {
background-color: white;
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
max-height: 75vh; /* Increased max-height slightly */
overflow-y: auto;
}
.format {
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.format:last-child {
border-bottom: none; /* Remove border for the last item */
margin-bottom: 0;
padding-bottom: 0;
}
.format h2 {
color: #0066cc;
margin-top: 0;
margin-bottom: 10px;
font-size: 1.1em;
}
.format-content {
max-height: 300px; /* Increased max-height for text content */
overflow-y: auto;
background-color: #f9f9f9; /* Slightly adjusted background */
border: 1px solid #e0e0e0; /* Slightly adjusted border */
border-radius: 3px;
padding: 10px;
font-size: 0.9em; /* Slightly smaller font for content */
}
/* Style for the container holding file info + image */
.file-entry {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px dotted #ddd;
}
.file-entry:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
/* Style for displayed images */
.pasted-image {
max-width: 100%;
height: auto; /* Maintain aspect ratio */
display: block; /* Ensure it takes its own line */
margin-top: 10px; /* Space below the text info */
border: 1px solid #ccc; /* Add a light border around images */
}
pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; /* Use monospace font */
}
.empty-format-note {
font-style: italic;
color: #888;
padding: 10px;
}
</style>
</head>
<body>
<h1>Clipboard Format Viewer</h1>
<textarea id="paste-area" placeholder="Paste anywhere on the page (Ctrl+V or Cmd+V)"></textarea>
<div id="output"><p>Paste content to see the available clipboard formats and data.</p></div>
<script>
const pasteArea = document.getElementById('paste-area');
const output = document.getElementById('output');
function handlePaste(event) {
// Prevent the default paste behavior (e.g., pasting into the textarea)
event.preventDefault();
const clipboardData = event.clipboardData || window.clipboardData;
output.innerHTML = ''; // Clear previous output
if (!clipboardData) {
output.innerHTML = '<p>Could not access clipboard data. Your browser might not support this feature or requires user permission.</p>';
return;
}
const formats = clipboardData.types;
const files = clipboardData.files; // Get files list once (for Files format)
const items = clipboardData.items; // Get items list once (for modern API access, especially images)
console.log("Clipboard Data:", clipboardData);
console.log("Available Formats:", formats);
console.log("Number of Files:", files.length);
console.log("Number of Items:", items ? items.length : 'N/A');
if (formats.length === 0 && files.length === 0 && (!items || items.length === 0)) {
output.innerHTML = '<p>Paste detected, but no standard formats, files, or items were found in the clipboard data.</p>';
return;
}
// --- Iterate through detected formats ---
formats.forEach(format => {
const formatDiv = document.createElement('div');
formatDiv.className = 'format';
const formatTitle = document.createElement('h2');
formatTitle.textContent = format;
formatDiv.appendChild(formatTitle);
// Wrapper for content (text or image)
const formatContentWrapper = document.createElement('div');
let contentDisplayed = false; // Flag to check if content was handled by special cases
// --- 1. Special Handling for "Files" format ---
// This is common when pasting files from the OS.
if (format === 'Files' && files.length > 0) {
const fileListContent = document.createElement('div');
fileListContent.className = 'format-content'; // Apply styling
Array.from(files).forEach(file => {
const fileContainer = document.createElement('div');
fileContainer.className = 'file-entry'; // Container for each file's info + image
// Display file information text
const fileInfo = document.createElement('div');
fileInfo.textContent = `${file.name} (${file.type || 'unknown type'}, ${file.size} bytes)`;
fileContainer.appendChild(fileInfo);
// Check if the file is an image and display it below the text
if (file.type.startsWith('image/')) {
try {
const img = document.createElement('img');
const objectURL = URL.createObjectURL(file);
img.src = objectURL;
img.alt = `${file.name} (Pasted Image)`;
img.className = 'pasted-image'; // Apply image styles
// IMPORTANT: Revoke the object URL once the image is loaded (or fails)
// to prevent memory leaks.
img.onload = () => {
console.log(`Image ${file.name} loaded, revoking ${objectURL}`);
URL.revokeObjectURL(objectURL);
};
img.onerror = () => {
console.error("Error loading image:", file.name);
URL.revokeObjectURL(objectURL); // Revoke even on error
img.alt += ' (Error loading)'; // Update alt text
// Optionally add an error message element
const errorMsg = document.createElement('div');
errorMsg.textContent = 'Could not load image preview.';
errorMsg.style.color = 'red';
fileContainer.appendChild(errorMsg);
};
fileContainer.appendChild(img); // Append the image below the file info
} catch (err) {
console.error(`Error creating object URL for ${file.name}:`, err);
const errorMsg = document.createElement('div');
errorMsg.textContent = 'Error creating image preview.';
errorMsg.style.color = 'red';
fileContainer.appendChild(errorMsg);
}
}
fileListContent.appendChild(fileContainer); // Append container for this file
});
formatContentWrapper.appendChild(fileListContent);
contentDisplayed = true;
}
// --- 2. Special Handling for "image/*" types (if not already handled by Files) ---
// This is common when pasting image data directly (e.g., screenshots, copy image)
// We use clipboardData.items for this, which is more reliable than getData for blobs.
else if (format.startsWith('image/') && files.length === 0 && items) {
// Check items only if Files format wasn't present or empty
let imageFromItemsDisplayed = false;
for (let i = 0; i < items.length; i++) {
const item = items[i];
// Find the first item matching the current format that is a file/blob
if (item.kind === 'file' && item.type === format) {
const file = item.getAsFile(); // This gets the image data as a Blob
if (file) {
try {
// Display image from item
const img = document.createElement('img');
const objectURL = URL.createObjectURL(file);
img.src = objectURL;
img.alt = `Pasted image (${format}, ${file.size} bytes)`;
img.className = 'pasted-image'; // Apply image styles
img.onload = () => {
console.log(`Image from item ${format} loaded, revoking ${objectURL}`);
URL.revokeObjectURL(objectURL);
};
img.onerror = () => {
console.error("Error loading image from item:", format);
URL.revokeObjectURL(objectURL);
img.alt += ' (Error loading)';
const errorMsg = document.createElement('div');
errorMsg.textContent = 'Could not load image preview.';
errorMsg.style.color = 'red';
formatContentWrapper.appendChild(errorMsg);
};
// Add a descriptive text above the image
const imgInfo = document.createElement('div');
// Use file.size if available, otherwise indicate unknown size
const sizeText = file.size ? `, ${file.size} bytes` : '';
imgInfo.textContent = `Image data (${format}${sizeText})`;
// No 'format-content' class needed here, just simple text
formatContentWrapper.appendChild(imgInfo);
formatContentWrapper.appendChild(img); // Append image
imageFromItemsDisplayed = true;
contentDisplayed = true;
break; // Handle only the first matching item for this format
} catch (err) {
console.error(`Error creating object URL for image item ${format}:`, err);
const errorMsg = document.createElement('div');
errorMsg.textContent = 'Error creating image preview.';
errorMsg.style.color = 'red';
formatContentWrapper.appendChild(errorMsg);
}
}
}
}
// If image wasn't found via items, fall through to default text handling below
}
// --- 3. Default Handling (Text, HTML, Fallbacks) ---
// Handles text/*, application/*, and serves as fallback if image handling failed
if (!contentDisplayed) {
const formatContent = document.createElement('pre');
let content = '';
let retrievalError = false;
try {
content = clipboardData.getData(format);
if (format === 'Files' && files.length === 0) {
// Explicitly handle Files format reported but with no actual files
content = '(No files found in clipboard)';
} else if (format.startsWith('image/') && files.length > 0) {
// If it's an image format but Files were present, assume handled there
content = '(Image data likely shown in the "Files" section above)';
} else if (content === '' && !format.startsWith('text/')) {
// If getData returns empty for non-text, it might be intentional or unavailable
content = '(Empty or non-retrievable data for this format)';
}
// Note: text/html is displayed as raw source by default in <pre>
formatContent.textContent = content;
} catch (e) {
retrievalError = true;
console.warn(`Could not getData for format: ${format}`, e);
// Provide more context based on format type
if (format.startsWith('image/')) {
content = '(Could not retrieve image data directly. It might be available via "Files" or Items API if present.)';
} else if (format === 'Files'){
content = '(Error accessing file list details.)';
} else {
content = `(Unable to retrieve data directly: ${e.message})`;
}
formatContent.textContent = content;
}
// Only add the <pre> element if there's content or an error message
if (content || retrievalError) {
formatContentWrapper.appendChild(formatContent);
formatContentWrapper.className = 'format-content'; // Add class for styling text content
} else {
// If formatContentWrapper is still empty, it means getData returned empty string
// and it wasn't an explicit Files/Image fallback case. Add a note.
const emptyNote = document.createElement('div');
emptyNote.textContent = '(No data retrieved or data is empty string)';
emptyNote.className = 'empty-format-note';
formatContentWrapper.appendChild(emptyNote);
}
}
// Append the content wrapper (containing text or image) to the format div
formatDiv.appendChild(formatContentWrapper);
output.appendChild(formatDiv); // Append the whole section for this format
});
// --- Additional Clipboard Event Information ---
const eventInfo = document.createElement('div');
eventInfo.className = 'format'; // Reuse format styling
eventInfo.innerHTML = `
<h2>Clipboard Event Information</h2>
<div class="format-content">
<pre>
Event type: ${event.type}
Formats available (${formats.length}): ${formats.join(', ') || 'None'}
Number of files reported: ${files.length}
Number of clipboard items: ${items ? items.length : 'N/A (Items API not supported/available)'}
</pre>
</div>
`;
// Append event info only if there was some content pasted initially
if (output.hasChildNodes()){
output.appendChild(eventInfo);
} else if (!output.innerHTML) { // Double check if output is truly empty
output.innerHTML = '<p>Paste detected, but no data could be read or displayed.</p>';
}
}
// Listen for paste events on the textarea (useful if focused)
pasteArea.addEventListener('paste', handlePaste);
// Listen for paste events on the entire document (allows pasting anywhere)
document.addEventListener('paste', handlePaste);
// Initial message update
if (navigator.clipboard && typeof navigator.clipboard.read !== 'function') {
const warning = document.createElement('p');
warning.style.color = 'orange';
warning.textContent = 'Warning: Your browser might not fully support the modern Clipboard API (read permission). Pasting might rely on the legacy event method.';
output.appendChild(warning);
}
</script>
</body>
</html>