//
// 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 = `
`;
// 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`;
}