Skip to content

Commit 8397ef6

Browse files
committed
1 parent e52fc5d commit 8397ef6

File tree

1 file changed

+254
-34
lines changed

1 file changed

+254
-34
lines changed

clipboard-viewer.html

Lines changed: 254 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,107 +27,327 @@
2727
border: 2px dashed #aaa;
2828
border-radius: 5px;
2929
resize: vertical;
30+
background-color: #fff;
3031
}
3132
#output {
3233
background-color: white;
3334
border: 1px solid #ddd;
3435
border-radius: 5px;
3536
padding: 20px;
36-
max-height: 70vh;
37+
max-height: 75vh; /* Increased max-height slightly */
3738
overflow-y: auto;
3839
}
3940
.format {
4041
margin-bottom: 20px;
4142
border-bottom: 1px solid #eee;
4243
padding-bottom: 10px;
4344
}
45+
.format:last-child {
46+
border-bottom: none; /* Remove border for the last item */
47+
margin-bottom: 0;
48+
padding-bottom: 0;
49+
}
4450
.format h2 {
4551
color: #0066cc;
52+
margin-top: 0;
4653
margin-bottom: 10px;
54+
font-size: 1.1em;
4755
}
4856
.format-content {
49-
max-height: 200px;
57+
max-height: 300px; /* Increased max-height for text content */
5058
overflow-y: auto;
51-
background-color: #f5f5f5;
52-
border: 1px solid #ddd;
59+
background-color: #f9f9f9; /* Slightly adjusted background */
60+
border: 1px solid #e0e0e0; /* Slightly adjusted border */
5361
border-radius: 3px;
5462
padding: 10px;
63+
font-size: 0.9em; /* Slightly smaller font for content */
64+
}
65+
/* Style for the container holding file info + image */
66+
.file-entry {
67+
margin-bottom: 15px;
68+
padding-bottom: 10px;
69+
border-bottom: 1px dotted #ddd;
70+
}
71+
.file-entry:last-child {
72+
margin-bottom: 0;
73+
padding-bottom: 0;
74+
border-bottom: none;
75+
}
76+
/* Style for displayed images */
77+
.pasted-image {
78+
max-width: 100%;
79+
height: auto; /* Maintain aspect ratio */
80+
display: block; /* Ensure it takes its own line */
81+
margin-top: 10px; /* Space below the text info */
82+
border: 1px solid #ccc; /* Add a light border around images */
5583
}
5684
pre {
5785
margin: 0;
5886
white-space: pre-wrap;
5987
word-wrap: break-word;
88+
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; /* Use monospace font */
89+
}
90+
.empty-format-note {
91+
font-style: italic;
92+
color: #888;
93+
padding: 10px;
6094
}
6195
</style>
6296
</head>
6397
<body>
6498
<h1>Clipboard Format Viewer</h1>
65-
<textarea id="paste-area" placeholder="Paste here or anywhere on the page"></textarea>
66-
<div id="output"></div>
99+
<textarea id="paste-area" placeholder="Paste anywhere on the page (Ctrl+V or Cmd+V)"></textarea>
100+
<div id="output"><p>Paste content to see the available clipboard formats and data.</p></div>
67101

68102
<script>
69103
const pasteArea = document.getElementById('paste-area');
70104
const output = document.getElementById('output');
71105

72106
function handlePaste(event) {
107+
// Prevent the default paste behavior (e.g., pasting into the textarea)
73108
event.preventDefault();
74109
const clipboardData = event.clipboardData || window.clipboardData;
75-
output.innerHTML = '';
110+
output.innerHTML = ''; // Clear previous output
111+
112+
if (!clipboardData) {
113+
output.innerHTML = '<p>Could not access clipboard data. Your browser might not support this feature or requires user permission.</p>';
114+
return;
115+
}
76116

77-
// Get available formats
78117
const formats = clipboardData.types;
79-
118+
const files = clipboardData.files; // Get files list once (for Files format)
119+
const items = clipboardData.items; // Get items list once (for modern API access, especially images)
120+
121+
console.log("Clipboard Data:", clipboardData);
122+
console.log("Available Formats:", formats);
123+
console.log("Number of Files:", files.length);
124+
console.log("Number of Items:", items ? items.length : 'N/A');
125+
126+
if (formats.length === 0 && files.length === 0 && (!items || items.length === 0)) {
127+
output.innerHTML = '<p>Paste detected, but no standard formats, files, or items were found in the clipboard data.</p>';
128+
return;
129+
}
130+
131+
// --- Iterate through detected formats ---
80132
formats.forEach(format => {
81133
const formatDiv = document.createElement('div');
82134
formatDiv.className = 'format';
83-
135+
84136
const formatTitle = document.createElement('h2');
85137
formatTitle.textContent = format;
86138
formatDiv.appendChild(formatTitle);
87139

140+
// Wrapper for content (text or image)
88141
const formatContentWrapper = document.createElement('div');
89-
formatContentWrapper.className = 'format-content';
90-
91-
const formatContent = document.createElement('pre');
92-
let content = clipboardData.getData(format);
93-
94-
// Special handling for certain formats
95-
if (format === 'text/html') {
96-
const tempElement = document.createElement('div');
97-
tempElement.innerHTML = content;
98-
content = tempElement.innerHTML;
99-
} else if (format === 'Files') {
100-
const files = clipboardData.files;
101-
content = Array.from(files).map(file => `${file.name} (${file.type}, ${file.size} bytes)`).join('\n');
142+
let contentDisplayed = false; // Flag to check if content was handled by special cases
143+
144+
// --- 1. Special Handling for "Files" format ---
145+
// This is common when pasting files from the OS.
146+
if (format === 'Files' && files.length > 0) {
147+
const fileListContent = document.createElement('div');
148+
fileListContent.className = 'format-content'; // Apply styling
149+
150+
Array.from(files).forEach(file => {
151+
const fileContainer = document.createElement('div');
152+
fileContainer.className = 'file-entry'; // Container for each file's info + image
153+
154+
// Display file information text
155+
const fileInfo = document.createElement('div');
156+
fileInfo.textContent = `${file.name} (${file.type || 'unknown type'}, ${file.size} bytes)`;
157+
fileContainer.appendChild(fileInfo);
158+
159+
// Check if the file is an image and display it below the text
160+
if (file.type.startsWith('image/')) {
161+
try {
162+
const img = document.createElement('img');
163+
const objectURL = URL.createObjectURL(file);
164+
165+
img.src = objectURL;
166+
img.alt = `${file.name} (Pasted Image)`;
167+
img.className = 'pasted-image'; // Apply image styles
168+
169+
// IMPORTANT: Revoke the object URL once the image is loaded (or fails)
170+
// to prevent memory leaks.
171+
img.onload = () => {
172+
console.log(`Image ${file.name} loaded, revoking ${objectURL}`);
173+
URL.revokeObjectURL(objectURL);
174+
};
175+
img.onerror = () => {
176+
console.error("Error loading image:", file.name);
177+
URL.revokeObjectURL(objectURL); // Revoke even on error
178+
img.alt += ' (Error loading)'; // Update alt text
179+
// Optionally add an error message element
180+
const errorMsg = document.createElement('div');
181+
errorMsg.textContent = 'Could not load image preview.';
182+
errorMsg.style.color = 'red';
183+
fileContainer.appendChild(errorMsg);
184+
};
185+
186+
fileContainer.appendChild(img); // Append the image below the file info
187+
} catch (err) {
188+
console.error(`Error creating object URL for ${file.name}:`, err);
189+
const errorMsg = document.createElement('div');
190+
errorMsg.textContent = 'Error creating image preview.';
191+
errorMsg.style.color = 'red';
192+
fileContainer.appendChild(errorMsg);
193+
}
194+
}
195+
fileListContent.appendChild(fileContainer); // Append container for this file
196+
});
197+
formatContentWrapper.appendChild(fileListContent);
198+
contentDisplayed = true;
102199
}
200+
// --- 2. Special Handling for "image/*" types (if not already handled by Files) ---
201+
// This is common when pasting image data directly (e.g., screenshots, copy image)
202+
// We use clipboardData.items for this, which is more reliable than getData for blobs.
203+
else if (format.startsWith('image/') && files.length === 0 && items) {
204+
// Check items only if Files format wasn't present or empty
205+
let imageFromItemsDisplayed = false;
206+
for (let i = 0; i < items.length; i++) {
207+
const item = items[i];
208+
// Find the first item matching the current format that is a file/blob
209+
if (item.kind === 'file' && item.type === format) {
210+
const file = item.getAsFile(); // This gets the image data as a Blob
211+
if (file) {
212+
try {
213+
// Display image from item
214+
const img = document.createElement('img');
215+
const objectURL = URL.createObjectURL(file);
103216

104-
formatContent.textContent = content;
105-
formatContentWrapper.appendChild(formatContent);
106-
formatDiv.appendChild(formatContentWrapper);
217+
img.src = objectURL;
218+
img.alt = `Pasted image (${format}, ${file.size} bytes)`;
219+
img.className = 'pasted-image'; // Apply image styles
220+
221+
img.onload = () => {
222+
console.log(`Image from item ${format} loaded, revoking ${objectURL}`);
223+
URL.revokeObjectURL(objectURL);
224+
};
225+
img.onerror = () => {
226+
console.error("Error loading image from item:", format);
227+
URL.revokeObjectURL(objectURL);
228+
img.alt += ' (Error loading)';
229+
const errorMsg = document.createElement('div');
230+
errorMsg.textContent = 'Could not load image preview.';
231+
errorMsg.style.color = 'red';
232+
formatContentWrapper.appendChild(errorMsg);
233+
};
234+
235+
// Add a descriptive text above the image
236+
const imgInfo = document.createElement('div');
237+
// Use file.size if available, otherwise indicate unknown size
238+
const sizeText = file.size ? `, ${file.size} bytes` : '';
239+
imgInfo.textContent = `Image data (${format}${sizeText})`;
240+
// No 'format-content' class needed here, just simple text
241+
formatContentWrapper.appendChild(imgInfo);
242+
243+
formatContentWrapper.appendChild(img); // Append image
244+
imageFromItemsDisplayed = true;
245+
contentDisplayed = true;
246+
break; // Handle only the first matching item for this format
247+
} catch (err) {
248+
console.error(`Error creating object URL for image item ${format}:`, err);
249+
const errorMsg = document.createElement('div');
250+
errorMsg.textContent = 'Error creating image preview.';
251+
errorMsg.style.color = 'red';
252+
formatContentWrapper.appendChild(errorMsg);
253+
}
254+
}
255+
}
256+
}
257+
// If image wasn't found via items, fall through to default text handling below
258+
}
259+
260+
// --- 3. Default Handling (Text, HTML, Fallbacks) ---
261+
// Handles text/*, application/*, and serves as fallback if image handling failed
262+
if (!contentDisplayed) {
263+
const formatContent = document.createElement('pre');
264+
let content = '';
265+
let retrievalError = false;
107266

108-
output.appendChild(formatDiv);
267+
try {
268+
content = clipboardData.getData(format);
269+
270+
if (format === 'Files' && files.length === 0) {
271+
// Explicitly handle Files format reported but with no actual files
272+
content = '(No files found in clipboard)';
273+
} else if (format.startsWith('image/') && files.length > 0) {
274+
// If it's an image format but Files were present, assume handled there
275+
content = '(Image data likely shown in the "Files" section above)';
276+
} else if (content === '' && !format.startsWith('text/')) {
277+
// If getData returns empty for non-text, it might be intentional or unavailable
278+
content = '(Empty or non-retrievable data for this format)';
279+
}
280+
// Note: text/html is displayed as raw source by default in <pre>
281+
formatContent.textContent = content;
282+
283+
} catch (e) {
284+
retrievalError = true;
285+
console.warn(`Could not getData for format: ${format}`, e);
286+
// Provide more context based on format type
287+
if (format.startsWith('image/')) {
288+
content = '(Could not retrieve image data directly. It might be available via "Files" or Items API if present.)';
289+
} else if (format === 'Files'){
290+
content = '(Error accessing file list details.)';
291+
} else {
292+
content = `(Unable to retrieve data directly: ${e.message})`;
293+
}
294+
formatContent.textContent = content;
295+
}
296+
297+
// Only add the <pre> element if there's content or an error message
298+
if (content || retrievalError) {
299+
formatContentWrapper.appendChild(formatContent);
300+
formatContentWrapper.className = 'format-content'; // Add class for styling text content
301+
} else {
302+
// If formatContentWrapper is still empty, it means getData returned empty string
303+
// and it wasn't an explicit Files/Image fallback case. Add a note.
304+
const emptyNote = document.createElement('div');
305+
emptyNote.textContent = '(No data retrieved or data is empty string)';
306+
emptyNote.className = 'empty-format-note';
307+
formatContentWrapper.appendChild(emptyNote);
308+
}
309+
}
310+
311+
// Append the content wrapper (containing text or image) to the format div
312+
formatDiv.appendChild(formatContentWrapper);
313+
output.appendChild(formatDiv); // Append the whole section for this format
109314
});
110315

111-
// Additional information about the clipboard event
316+
// --- Additional Clipboard Event Information ---
112317
const eventInfo = document.createElement('div');
113-
eventInfo.className = 'format';
318+
eventInfo.className = 'format'; // Reuse format styling
114319
eventInfo.innerHTML = `
115320
<h2>Clipboard Event Information</h2>
116321
<div class="format-content">
117322
<pre>
118323
Event type: ${event.type}
119-
Formats available: ${formats.join(', ')}
324+
Formats available (${formats.length}): ${formats.join(', ') || 'None'}
325+
Number of files reported: ${files.length}
326+
Number of clipboard items: ${items ? items.length : 'N/A (Items API not supported/available)'}
120327
</pre>
121328
</div>
122329
`;
123-
output.appendChild(eventInfo);
330+
// Append event info only if there was some content pasted initially
331+
if (output.hasChildNodes()){
332+
output.appendChild(eventInfo);
333+
} else if (!output.innerHTML) { // Double check if output is truly empty
334+
output.innerHTML = '<p>Paste detected, but no data could be read or displayed.</p>';
335+
}
124336
}
125337

126-
// Listen for paste events on the textarea
338+
// Listen for paste events on the textarea (useful if focused)
127339
pasteArea.addEventListener('paste', handlePaste);
128340

129-
// Listen for paste events on the entire document
341+
// Listen for paste events on the entire document (allows pasting anywhere)
130342
document.addEventListener('paste', handlePaste);
343+
344+
// Initial message update
345+
if (navigator.clipboard && typeof navigator.clipboard.read !== 'function') {
346+
const warning = document.createElement('p');
347+
warning.style.color = 'orange';
348+
warning.textContent = 'Warning: Your browser might not fully support the modern Clipboard API (read permission). Pasting might rely on the legacy event method.';
349+
output.appendChild(warning);
350+
}
131351
</script>
132352
</body>
133-
</html>
353+
</html>

0 commit comments

Comments
 (0)