// // clip-script.js // /************************************************************* * GLOBALS & EVENT LISTENERS *************************************************************/ // Provide a base URL if needed: const BASE_URL = "https://api.is"; document.addEventListener("DOMContentLoaded", async () => { const pasteMainButton = document.getElementById("pasteMainButton"); const pasteDropdownButton = document.getElementById("pasteDropdownButton"); const pasteDropdownMenu = document.getElementById("pasteDropdownMenu"); const saveButton = document.getElementById("saveButton"); pasteMainButton.addEventListener("click", async () => { console.log("Clicked the button!"); let data = await getClipboardContent(); buildAllResults(data); }); pasteDropdownButton.addEventListener("click", async (e) => { e.stopPropagation(); await populatePasteDropdown(); pasteDropdownMenu.style.display = "block"; }); document.addEventListener("click", (event) => { if (!pasteDropdownMenu.contains(event.target) && !pasteDropdownButton.contains(event.target)) { pasteDropdownMenu.style.display = "none"; } }); saveButton.addEventListener("click", () => { saveCurrentClip(); }); // **Event Delegation** for dynamically added
  • elements pasteDropdownMenu.addEventListener("click", async (event) => { const li = event.target.closest("li"); if (li && !li.classList.contains("disabled")) { const option = li.getAttribute("data-opt"); await handlePasteAsOption(option); pasteDropdownMenu.style.display = "none"; } }); let path = window.location.pathname; if (path.startsWith("/clip/")) { let clipId = path.split("/")[2]; if (clipId) { loadClipById(clipId); return; } } else { let clipboardData = await getClipboardContent(); buildAllResults(clipboardData); } // 🚀 Attempt to auto-paste upon page load! try { let result = await navigator.permissions.query({ name: "clipboard-read" }); if (result.state === "granted" || result.state === "prompt") { let clipboardData = await getClipboardContent(); buildAllResults(clipboardData); } else { console.warn("Clipboard access denied. User must trigger paste."); } } catch (error) { console.warn("Clipboard permission check failed:", error); } }); /** * Ensures the dropdown stays visible when needed. */ async function populatePasteDropdown() { const pasteDropdownMenu = document.getElementById("pasteDropdownMenu"); pasteDropdownMenu.innerHTML = ""; try { const clipboardItems = await navigator.clipboard.read(); let availableOptions = new Set(); for (const item of clipboardItems) { for (const type of item.types) { if (type.startsWith("text/plain")) { availableOptions.add({ label: "Plain Text", value: "Plain Text" }); } else if (type.startsWith("text/html")) { availableOptions.add({ label: "Rich Text", value: "Rich Text" }); availableOptions.add({ label: "HTML", value: "HTML raw" }); } else if (type.startsWith("application/pdf")) { availableOptions.add({ label: "PDF", value: "PDF" }); } else if (type.startsWith("image/")) { availableOptions.add({ label: "Image", value: "Image" }); } } } if (availableOptions.size === 0) { pasteDropdownMenu.innerHTML = "
  • No options available
  • "; } else { availableOptions.forEach(option => { pasteDropdownMenu.innerHTML += `
  • Paste as ${option.label}
  • `; }); } } catch (err) { console.error("Failed to read clipboard:", err); pasteDropdownMenu.innerHTML = "
  • Clipboard access denied
  • "; } } /** * Handles paste option selection from the dropdown. */ async function handlePasteAsOption(option) { console.log(`User selected: ${option}`); getClipboardContent(option); } /************************************************************* * PASTE HANDLING *************************************************************/ // Fallback paste event listener (for older browsers) document.addEventListener("paste", async (event) => { event.preventDefault(); event.stopPropagation(); let results = await getClipboardContent().then(buildAllResults); if (!results || results.length === 0) { handleClipboardDataFromEvent(event); } setTimeout(autoResize, 0); }); async function getClipboardContent() { let mimeDataList = []; try { const clipboardItems = await navigator.clipboard.read(); for (const item of clipboardItems) { for (const type of item.types) { const blob = await item.getType(type); if (type.startsWith("image/")) { const fileType = type.split("/").pop().toUpperCase(); const objUrl = URL.createObjectURL(blob); mimeDataList.push({ type: "Image (" + fileType + ")", content: objUrl, isImage: true }); } else if (type === "application/pdf") { const pdfUrl = URL.createObjectURL(blob); mimeDataList.push({ type: "PDF", content: pdfUrl, isPDF: true }); } else if (type.startsWith("text/html")) { const htmlString = await blob.text(); mimeDataList.push({ type: "Rich text", content: htmlString, isHTML: true }); mimeDataList.push({ type: "HTML raw", content: htmlString, isHTMLRaw: true }); } else if (type.startsWith("text/")) { const textVal = await blob.text(); mimeDataList.push({ type: "Plain text", content: textVal, isText: true }); } else { // Possibly binary. Let's guess if it's PNG/JPEG const guessed = await guessImageType(blob); if (guessed) { const imageUrl = URL.createObjectURL(blob); mimeDataList.push({ type: guessed === "image/png" ? "Image (PNG)" : "Image (JPEG)", content: imageUrl, isImage: true }); } else { mimeDataList.push({ type: "Binary file", content: blob, isBinary: true, size: blob.size }); } } } } // Fallback if no items found if (mimeDataList.length === 0) { const fallbackText = await navigator.clipboard.readText(); if (fallbackText) { mimeDataList.push({ type: "Plain text", content: fallbackText, isText: true }); } } } catch (err) { console.warn("navigator.clipboard.read() error:", err); } console.log(`Pasting!!`); return mimeDataList; } function handleClipboardDataFromEvent(event) { let mimeDataList = []; const items = event.clipboardData?.items || []; for (const item of items) { const type = item.type; if (type.startsWith("image/")) { const file = item.getAsFile(); const objUrl = URL.createObjectURL(file); mimeDataList.push({ type: "Image", content: objUrl, isImage: true }); } else if (type === "application/pdf") { const file = item.getAsFile(); const pdfUrl = URL.createObjectURL(file); mimeDataList.push({ type: "PDF", content: pdfUrl, isPDF: true }); } else if (type.startsWith("text/html")) { item.getAsString((htmlVal) => { mimeDataList.push({ type: "Rich text", content: htmlVal, isHTML: true }); mimeDataList.push({ type: "HTML raw", content: htmlVal, isHTMLRaw: true }); buildAllResults(mimeDataList); }); return; } else if (type.startsWith("text/")) { item.getAsString((txtVal) => { mimeDataList.push({ type: "Plain text", content: txtVal, isText: true }); buildAllResults(mimeDataList); }); return; } else { const file = item.getAsFile(); if (file) { guessImageType(file).then((guessed) => { if (guessed) { const imageUrl = URL.createObjectURL(file); mimeDataList.push({ type: guessed === "image/png" ? "Image (PNG)" : "Image (JPEG)", content: imageUrl, isImage: true }); } else { mimeDataList.push({ type: "Binary file", content: file, isBinary: true, size: file.size }); } buildAllResults(mimeDataList); }); return; } } } if (items.length === 0) { console.log("No items found in event.clipboardData."); } else { buildAllResults(mimeDataList); } } /************************************************************* * "Paste as" custom logic *************************************************************/ async function handlePasteAsOption(option) { // Add async here console.log("User chose 'Paste as " + option + "'"); let mimeDataList = await getClipboardContent(); // Now this works // Filter based on user selection if (option === "Plain Text") { mimeDataList = mimeDataList.filter(i => i.isText); } else if (option === "Rich Text") { mimeDataList = mimeDataList.filter(i => i.isHTML); } else if (option === "HTML raw") { mimeDataList = mimeDataList.filter(i => i.isHTMLRaw); } else if (option === "PDF") { mimeDataList = mimeDataList.filter(i => i.isPDF); } else if (option === "Image") { mimeDataList = mimeDataList.filter(i => i.isImage); } // Rebuild results with the filtered selection buildAllResults(mimeDataList); } /************************************************************* * BUILDING THE PRIMARY CARD *************************************************************/ async function buildAllResults(mimeDataList,clipTime = false) { window.lastMimeDataList = mimeDataList; const container = document.getElementById("cards-container"); if (!container) return; container.innerHTML = ""; if (!mimeDataList || mimeDataList.length === 0) { const noDataCard = makeSimpleCard("No clipboard data detected."); container.appendChild(noDataCard); return; } // 1) Get first recognized suggestion from text let textItem = mimeDataList.find(m => m.isText) || { content: "" }; let suggestions = await recognizeText(textItem.content); let defaultView; if (suggestions.length > 0) { // 🚀 Only process the first recognized suggestion let firstSuggestion = suggestions[0]; console.log(firstSuggestion); defaultView = await buildRecognizedSuggestionView(firstSuggestion, textItem.content); } else { // 2) Pick default clipboard item (fallback) let fallbackItem = pickDefaultMimeItem(mimeDataList); defaultView = { label: fallbackItem.type, element: createClipboardCard(fallbackItem,clipTime), mimeItem: fallbackItem }; } // 3) Render the single card renderPrimaryView(defaultView, container); setTimeout(autoResize, 0); } /************************************************************* * RENDER THE PRIMARY CARD *************************************************************/ function renderPrimaryView(viewObj, container) { container.innerHTML = ""; container.appendChild(viewObj.element); } /************************************************************* * PICK THE DEFAULT ITEM *************************************************************/ function pickDefaultMimeItem(mimeDataList) { let maybeImage = mimeDataList.find(item => item.isImage); if (maybeImage) return maybeImage; let maybeHTML = mimeDataList.find(item => item.isHTML); if (maybeHTML) return maybeHTML; let maybeHTMLRaw = mimeDataList.find(item => item.isHTMLRaw); if (maybeHTMLRaw) return maybeHTMLRaw; let maybeText = mimeDataList.find(item => item.isText); if (maybeText) return maybeText; return mimeDataList[0]; // fallback } /************************************************************* * RECOGNIZE TEXT (API) *************************************************************/ async function recognizeText(text) { if (!text.trim()) return []; try { const response = await fetch( `${BASE_URL}/v1/types/recognize/?q=${encodeURIComponent(text)}`, { method: "POST", headers: { "Authorization": "Bearer clipiskey", "Accept": "application/json" } } ); if (!response.ok) throw new Error("API request failed"); const data = await response.json(); return data.suggestions || []; } catch (err) { console.error("Recognition API error:", err); return []; } } async function buildRecognizedSuggestionView(suggestion, originalText) { let card = document.createElement("div"); card.classList.add("card"); // Simple header const truncated = suggestion.normalized.length > 40 ? suggestion.normalized.substring(0, 40) + "..." : suggestion.normalized; card.innerHTML = `
    icon

    ${htmlEscape(truncated)}

    ${htmlEscape(suggestion.data_type)}

    `; // Fill details await processData(originalText, suggestion.data_type, card); return { label: suggestion.data_type, element: card }; } /************************************************************* * PROCESS DATA -> Fetch details=1 first, render, then details=2 *************************************************************/ async function processData(text, dataType, card) { if (!card) return; let cardBody = card.querySelector(".card-body"); // Step 1: Fetch details=1 first let initialData = await fetchDetails(text, dataType, 1); if (initialData) { updateCardWithDetails(initialData, card); console.log("Details level 1 received..."); } // 🚀 **Force UI Update Immediately** Before Fetching details=2 // This is not working for some strange reason! await forceUIUpdate(); // Step 2: Fetch details=2 asynchronously let fullData = await fetchDetails(text, dataType, 2); if (fullData) { updateCardWithDetails(fullData, card); console.log("Details level 2 received..."); } } /************************************************************* * FETCH DETAILS (No caching) *************************************************************/ async function fetchDetails(text, dataType, detailLevel) { console.log(`Fetching details=${detailLevel} for ${dataType}...`); try { const response = await fetch( `${BASE_URL}/v1/types/process/?q=${encodeURIComponent(text)}&data_type=${encodeURIComponent(dataType)}&details=${detailLevel}`, { method: "POST", headers: { "Authorization": "Bearer clipiskey", "Accept": "application/json" } } ); if (!response.ok) throw new Error(`API request failed (details=${detailLevel})`); const data = await response.json(); return data; } catch (err) { console.error(`Error fetching details=${detailLevel}:`, err); return null; } } /************************************************************* * FORCE UI UPDATE (Prevents Batch Rendering Delays) *************************************************************/ async function forceUIUpdate() { return new Promise((resolve) => { requestAnimationFrame(() => { setTimeout(resolve, 0); // Let the browser finish rendering first }); }); } /************************************************************* * UPDATE CARD CONTENT WITH NEW DETAILS *************************************************************/ function updateCardWithDetails(data, card) { if (!card) return; let cardBody = card.querySelector(".card-body"); // Preserve existing loading indicator let loadingIndicator = cardBody.querySelector(".loading-indicator"); // Clear existing content (except loading indicator) cardBody.innerHTML = ""; if (loadingIndicator) { cardBody.appendChild(loadingIndicator); } let titleValue = null, urlValue = null, descriptionValue = null, iconValue = null, socialProfilesValue = null; if (Array.isArray(data.details)) { let otherDetails = []; data.details.forEach(detail => { switch (detail.label.toLowerCase()) { case "title": titleValue = detail.value; break; case "url": urlValue = detail.value; break; case "description": descriptionValue = detail.value; break; case "icon": iconValue = detail.value; break; case "social profiles": socialProfilesValue = detail.value; break; default: otherDetails.push(detail); break; } }); data.details = otherDetails; } // Possibly replace icon if (iconValue) { let headerImg = card.querySelector("img"); if (headerImg) headerImg.src = iconValue; } // Title + link if (titleValue && titleValue.trim() !== "") { let h4 = document.createElement("h4"); h4.classList.add("card-subtitle"); if (isURL(urlValue)) { h4.innerHTML = `${htmlEscape(titleValue)}`; } else { h4.textContent = titleValue; } cardBody.appendChild(h4); } // Description if (descriptionValue && descriptionValue.trim() !== "") { let p = document.createElement("p"); p.classList.add("card-description"); p.textContent = descriptionValue; cardBody.appendChild(p); } // Social profiles if (socialProfilesValue) { let ul = document.createElement("ul"); ul.classList.add("social-list"); socialProfilesValue.forEach(profile => { let li = createActionListItem(profile); if (li) ul.appendChild(li); }); cardBody.appendChild(ul); } // Additional details if (data.details && data.details.length > 0) { let detailTable = document.createElement("table"); detailTable.classList.add("detail-table"); data.details.forEach(detail => { let row = createDetailsRow(detail.label, detail.value); if (row) detailTable.appendChild(row); }); cardBody.appendChild(detailTable); } // Actions if (data.actions && data.actions.length > 0) { let firstAction = data.actions[0]; let headerTitle = card.querySelector("h3"); if (headerTitle && firstAction.url) { // Make the header clickable let link = document.createElement("a"); link.href = firstAction.url; link.target = "_blank"; link.textContent = headerTitle.textContent; link.style.textDecoration = "none"; headerTitle.innerHTML = ""; headerTitle.appendChild(link); } let actionList = document.createElement("ul"); actionList.classList.add("actions-list"); data.actions.forEach(action => { let li = createActionListItem(action); if (li) actionList.appendChild(li); }); cardBody.appendChild(actionList); } } /************************************************************* * CREATE THE CLIPBOARD CARD *************************************************************/ function createClipboardCard(mimeItem,clipTime = false) { let card = document.createElement("div"); card.classList.add("card"); let header = document.createElement("div"); header.classList.add("card-header"); header.innerHTML = `

    ${mimeItem.type || "Unknown"}

    `; if (clipTime) { header.innerHTML += `${timeAgo(clipTime)}`; } console.log(mimeItem); card.appendChild(header); let body = document.createElement("div"); body.classList.add("card-body"); card.appendChild(body); // Render content based on type if (mimeItem.isHTML) { console.log("I'm Rich!") let iframe = document.createElement("iframe"); iframe.style.width = "100%"; iframe.style.border = "none"; iframe.srcdoc = mimeItem.content; body.appendChild(iframe); } else if (mimeItem.isHTMLRaw) { console.log("I'm raw!") let container = createTextareaWithCopy(mimeItem.content); body.appendChild(container); } else if (mimeItem.isImage) { let img = document.createElement("img"); img.style.maxWidth = "100%"; img.style.height = "auto"; img.src = mimeItem.content; body.appendChild(img); } else if (mimeItem.isPDF) { let iframe = document.createElement("iframe"); iframe.style.width = "100%"; iframe.style.height = "500px"; iframe.src = mimeItem.content; body.appendChild(iframe); let link = document.createElement("a"); link.href = mimeItem.content; link.download = "pasted.pdf"; link.textContent = "Download PDF"; body.appendChild(link); } else if (mimeItem.isText) { let container = createTextareaWithCopy(mimeItem.content); body.appendChild(container); } else if (mimeItem.isBinary) { let p = document.createElement("p"); p.textContent = `Binary data (${mimeItem.size || 0} bytes).`; body.appendChild(p); let link = document.createElement("a"); link.href = URL.createObjectURL(mimeItem.content); link.download = "clipboard.bin"; link.textContent = "Download file"; body.appendChild(link); } else { let p = document.createElement("p"); p.textContent = "Unrecognized content."; body.appendChild(p); } return card; } /************************************************************* * SMALL UTILITIES *************************************************************/ function makeSimpleCard(message) { let card = document.createElement("div"); card.classList.add("card"); let header = document.createElement("div"); header.classList.add("card-header"); header.innerHTML = `

    Info

    `; let body = document.createElement("div"); body.classList.add("card-body"); body.textContent = message; card.appendChild(header); card.appendChild(body); return card; } function createTextareaWithCopy(textValue) { let container = document.createElement("div"); container.classList.add("textarea-container"); let textarea = document.createElement("textarea"); textarea.classList.add("raw-textarea"); textarea.value = textValue; let copyButton = document.createElement("button"); copyButton.classList.add("copy-btn"); copyButton.textContent = "⧉"; copyButton.title = "Copy"; copyButton.addEventListener("click", () => { navigator.clipboard.writeText(textarea.value).catch(err => console.error("Failed to copy:", err)); }); container.appendChild(textarea); container.appendChild(copyButton); return container; } async function guessImageType(blob) { try { const arrayBuf = await blob.arrayBuffer(); const bytes = new Uint8Array(arrayBuf.slice(0, 4)); if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) { return "image/png"; } if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) { return "image/jpeg"; } } catch (err) { console.error("guessImageType error:", err); } return null; } function isURL(str) { try { new URL(str); return true; } catch (_) { return false; } } function createDetailsRow(labelText, value) { if (!value || (typeof value === "string" && !value.trim())) { return null; } let row = document.createElement("tr"); let labelCell = document.createElement("td"); labelCell.classList.add("detail-label"); labelCell.textContent = labelText; let valueCell = document.createElement("td"); valueCell.classList.add("detail-value"); if (Array.isArray(value)) { valueCell.innerHTML = value .map(item => (isURL(item) ? `${item}` : item)) .join("
    "); } else if (isURL(value)) { valueCell.innerHTML = `${value}`; } else { valueCell.textContent = value; } row.appendChild(labelCell); row.appendChild(valueCell); return row; } function createActionListItem(action) { if (!action || !action.label || !action.url) { return null; } let li = document.createElement("li"); li.innerHTML = ` `; return li; } function htmlEscape(str) { let div = document.createElement("div"); div.appendChild(document.createTextNode(str)); return div.innerHTML; } /************************************************************* * AUTO-RESIZE *************************************************************/ function autoResize() { document.querySelectorAll("textarea.raw-textarea").forEach(autoResizeTextarea); document.querySelectorAll("iframe").forEach(autoResizeIframe); } function autoResizeTextarea(textarea) { if (!textarea) return; const maxHeight = window.innerHeight * 0.5; textarea.style.height = "auto"; let newHeight = textarea.scrollHeight; if (newHeight > maxHeight) { newHeight = maxHeight; textarea.style.overflowY = "auto"; } else { textarea.style.overflowY = "hidden"; } textarea.style.height = `${newHeight}px`; } function autoResizeIframe(iframe) { if (!iframe) return; iframe.onload = function () { try { const doc = iframe.contentDocument || iframe.contentWindow.document; if (!doc) return; iframe.style.height = "auto"; let newHeight = doc.documentElement.scrollHeight; const maxHeight = window.innerHeight * 0.7; if (newHeight > maxHeight) { newHeight = maxHeight; iframe.style.overflowY = "auto"; } else { iframe.style.overflowY = "hidden"; } iframe.style.height = `${newHeight}px`; } catch (err) { console.warn("Could not auto-resize iframe:", err); } }; } /************************************************************* * LOADING / SAVING CLIPS *************************************************************/ async function saveCurrentClip() { let dataToSave = window.lastMimeDataList || []; if (dataToSave.length === 0) { alert("No clipboard data to save!"); return; } try { const response = await fetch(`${BASE_URL}/v1/clip/save`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: dataToSave }) }); if (!response.ok) throw new Error("Failed to save clip."); const result = await response.json(); if (result.clip_id) { const shareLinkElem = document.getElementById("share-link"); let shareUrl = `${BASE_URL}/clip/${result.clip_id}`; shareLinkElem.innerHTML = `

    Clip saved! Share this link:

    ${shareUrl} `; } } catch (err) { console.error("Save clip error:", err); alert("Failed to save clip."); } } async function loadClipById(clipId) { try { let resp = await fetch(`${BASE_URL}/v1/clip/${clipId}`); if (!resp.ok) throw new Error("Clip not found"); let data = await resp.json(); console.log(timeAgo(data.created_at)); buildAllResults(data.items,data.created_at); } catch (e) { console.error(e); let container = document.getElementById("cards-container"); if (container) { container.innerHTML = ""; container.appendChild(makeSimpleCard("Clip not found.")); } } } /* Misc */ function timeAgo(isoString) { const now = new Date(); const pastDate = new Date(isoString); const diffMs = now - pastDate; // Difference in milliseconds const diffSeconds = Math.floor(diffMs / 1000); const diffMinutes = Math.floor(diffSeconds / 60); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); const diffWeeks = Math.floor(diffDays / 7); const diffMonths = Math.floor(diffDays / 30); const diffYears = Math.floor(diffDays / 365); if (diffSeconds < 10) return "just now"; if (diffSeconds < 60) return `${diffSeconds}s ago`; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; if (diffWeeks < 4) return `${diffWeeks}w ago`; if (diffMonths < 12) return `${diffMonths}mo ago`; return `${diffYears}y ago`; }