/** * Discord Embed Builder * Contribute or report issues at * https://github.com/Glitchii/embedbuilder */ window.options ??= {}; window.inIframe ??= top !== self; let params = new URLSearchParams(location.search), sendMessage = params.get("message") || null, hasParam = param => params.get(param) !== null, dataSpecified = options.data || params.get("data"), username = params.get("username") || options.username, avatar = params.get("avatar") || options.avatar, guiTabs = params.get("guitabs") || options.guiTabs, useJsonEditor = params.get("editor") === "json" || options.useJsonEditor, verified = hasParam("verified") || options.verified, reverseColumns = hasParam("reverse") || options.reverseColumns, noUser = localStorage.getItem("noUser") || hasParam("nouser") || options.noUser, onlyEmbed = hasParam("embed") || options.onlyEmbed, allowPlaceholders = hasParam("placeholders") || options.allowPlaceholders, autoUpdateURL = localStorage.getItem("autoUpdateURL") || options.autoUpdateURL, noMultiEmbedsOption = localStorage.getItem("noMultiEmbedsOption") || hasParam("nomultiembedsoption") || options.noMultiEmbedsOption, single = noMultiEmbedsOption ? options.single ?? true : (localStorage.getItem("single") || hasParam("single") || options.single) ?? false, multiEmbeds = !single, autoParams = localStorage.getItem("autoParams") || hasParam("autoparams") || options.autoParams, hideEditor = localStorage.getItem("hideeditor") || hasParam("hideeditor") || options.hideEditor, hidePreview = localStorage.getItem("hidepreview") || hasParam("hidepreview") || options.hidePreview, hideMenu = localStorage.getItem("hideMenu") || hasParam("hidemenu") || options.hideMenu, sourceOption = localStorage.getItem("sourceOption") || hasParam("sourceoption") || options.sourceOption, validationError, activeFields, lastActiveGuiEmbedIndex = -1, lastGuiJson, colNum = 1, num = 0; const guiEmbedIndex = guiEl => { const guiEmbed = guiEl?.closest(".guiEmbed"); const gui = guiEmbed?.closest(".gui") return !gui ? -1 : Array.from(gui.querySelectorAll(".guiEmbed")).indexOf(guiEmbed) } const toggleStored = item => { const found = localStorage.getItem(item); if (!found) return localStorage.setItem(item, true); localStorage.removeItem(item); return found; }; const createElement = object => { let element; for (const tag in object) { element = document.createElement(tag); for (const attr in object[tag]) if (attr !== "children") element[attr] = object[tag][attr]; else for (const child of object[tag][attr]) element.appendChild(createElement(child)); } return element; } const encodeJson = (jsonCode, withURL = false, redirect = false) => { let data = btoa(encodeURIComponent((JSON.stringify(typeof jsonCode === "object" ? jsonCode : json)))); let url = new URL(location.href); if (withURL) { url.searchParams.set("data", data); if (redirect) return top.location.href = url; data = url.href // Replace %3D ("=" url encoded) with "=" .replace(/data=\w+(?:%3D)+/g, "data=" + data); } return data; }; const decodeJson = data => { const jsonData = decodeURIComponent(atob(data || dataSpecified)); return typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData; }; const toRGB = (hex, reversed, integer) => { if (reversed) return "#" + hex.match(/\d+/g).map(x => parseInt(x).toString(16).padStart(2, "0")).join(""); if (integer) return parseInt(hex.match(/\d+/g).map(x => parseInt(x).toString(16).padStart(2, "0")).join(""), 16); if (hex.includes(",")) return hex.match(/\d+/g); hex = hex.replace("#", "").match(/.{1,2}/g) return [parseInt(hex[0], 16), parseInt(hex[1], 16), parseInt(hex[2], 16), 1]; }; const reverse = (reversed, callback) => { const side = document.querySelector(reversed ? ".side2" : ".side1"); if (side.nextElementSibling) side.parentElement.insertBefore(side.nextElementSibling, side); else side.parentElement.insertBefore(side, side.parentElement.firstElementChild); const isReversed = document.body.classList.toggle("reversed"); if (autoParams) isReversed ? urlOptions({ set: ["reverse", ""] }) : urlOptions({ remove: "reverse" }); }; const urlOptions = ({ remove, set }) => { const url = new URL(location.href); if (remove) url.searchParams.delete(remove); if (set) url.searchParams.set(set[0], set[1]); try { history.replaceState(null, null, url.href.replace(/(? x === "=" ? "" : "&")); } catch (e) { // "SecurityError" when trying to change the url of a different origin // e.g. when trying to change the url of the parent window from an iframe console.info(e); } }; const animateGuiEmbedNameAt = (i, text) => { const guiEmbedName = document.querySelectorAll(".gui .guiEmbedName")?.[i]; // Shake animation guiEmbedName?.animate( [{ transform: "translate(0, 0)" }, { transform: "translate(10px, 0)" }, { transform: "translate(0, 0)" } ], { duration: 100, iterations: 3 }); text && (guiEmbedName?.style.setProperty("--text", `"${text}"`)); guiEmbedName?.scrollIntoView({ behavior: "smooth", block: "center" }); guiEmbedName?.classList.remove("empty"); setTimeout(() => guiEmbedName?.classList.add("empty"), 10); } const indexOfEmptyGuiEmbed = text => { for (const [i, element] of document.querySelectorAll(".msgEmbed>.container .embed").entries()) if (element.classList.contains("emptyEmbed")) { text !== false && animateGuiEmbedNameAt(i, text); return i; } for (const [i, embedObj] of(json.embeds || []).entries()) if (!(0 in Object.keys(embedObj))) { text !== false && animateGuiEmbedNameAt(i, text); return i; } return -1; } const changeLastActiveGuiEmbed = index => { const pickerEmbedText = document.querySelector(".colors .cTop .embedText>span"); if (index === -1) { lastActiveGuiEmbedIndex = -1; return pickerEmbedText.textContent = ""; } lastActiveGuiEmbedIndex = index; if (pickerEmbedText) { pickerEmbedText.textContent = index + 1; const guiEmbedNames = document.querySelectorAll(".gui .item.guiEmbedName"); pickerEmbedText.onclick = () => { const newIndex = parseInt(prompt("Enter an embed number" + (guiEmbedNames.length > 1 ? `, 1 - ${guiEmbedNames.length}` : ""), index + 1)); if (isNaN(newIndex)) return; if (newIndex < 1 || newIndex > guiEmbedNames.length) return error(guiEmbedNames.length === 1 ? `"${newIndex}" is not a valid embed number` : `"${newIndex}" doesn"t seem like a number between 1 and ${guiEmbedNames.length}`); changeLastActiveGuiEmbed(newIndex - 1); } } } // Called after building embed for extra work. const afterBuilding = () => autoUpdateURL && urlOptions({ set: ["data", encodeJson(json)] }); // Parses emojis to images and adds code highlighting. const externalParsing = ({ noEmojis, element } = {}) => { !noEmojis && twemoji.parse(element || document.querySelector(".msgEmbed"), { base: "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/" }); for (const block of document.querySelectorAll(".markup pre > code")) hljs.highlightBlock(block); const embed = element?.closest(".embed"); if (embed?.innerText.trim()) (multiEmbeds ? embed : document.body).classList.remove("emptyEmbed"); afterBuilding() }; let embedKeys = ["author", "footer", "color", "thumbnail", "image", "fields", "title", "description", "url", "timestamp"]; let mainKeys = ["embed", "embeds", "content"]; let allJsonKeys = [...mainKeys, ...embedKeys]; // "jsonObject" is used internally, do not change it"s value. Assign to "json" instead. // "json" is the object that is used to build the embed. Assigning to it also updates the editor. let jsonObject = window.json || { content: "", embed: { title: "Hello ~~people~~ world :wave:", description: "You can use [links](https://discord.com) or emojis :smile: 😎\n```\nAnd also code blocks\n```", color: 0x41f097, timestamp: new Date().toISOString(), url: "https://discord.com", author: { name: "Author name", url: "https://discord.com", icon_url: "https://cdn.discordapp.com/embed/avatars/0.png" }, thumbnail: { url: "https://cdn.discordapp.com/embed/avatars/0.png" }, image: { url: "https://glitchii.github.io/embedbuilder/assets/media/banner.png" }, footer: { text: "Footer text", icon_url: "https://cdn.discordapp.com/embed/avatars/0.png" }, fields: [{ name: "Field 1, *lorem* **ipsum**, ~~dolor~~", value: "Field value" }, { name: "Field 2", value: "You can use custom emojis <:Kekwlaugh:722088222766923847>. <:GangstaBlob:742256196295065661>", inline: false }, { name: "Inline field", value: "Fields can be inline", inline: true }, { name: "Inline field", value: "*Lorem ipsum*", inline: true }, { name: "Inline field", value: "value", inline: true }, { name: "Another field", value: "> Nope, didn't forget about this", inline: false } ] } } if (dataSpecified) jsonObject = decodeJson(); if (allowPlaceholders) allowPlaceholders = params.get("placeholders") === "errors" ? 1 : 2; // Even if not in multi-embed mode, "jsonObject" should always have an array "embeds" // To get the right json object that includes either "embeds" or "embed" if not in multi-embed mode, // print "json" (global variable) instead of "jsonObject", jsonObject is used internally, you shouldn"t modify it. if (multiEmbeds && !jsonObject.embeds?.length) jsonObject.embeds = jsonObject.embed ? [jsonObject.embed] : []; else if (!multiEmbeds) jsonObject.embeds = jsonObject.embeds?.[0] ? [jsonObject.embeds[0]] : jsonObject.embed ? [jsonObject.embed] : []; delete jsonObject.embed; addEventListener("DOMContentLoaded", () => { if (reverseColumns || localStorage.getItem("reverseColumns")) reverse(); if (autoParams) document.querySelector(".item.auto-params > input").checked = true; if (hideMenu) document.querySelector(".top-btn.menu")?.classList.add("hidden"); if (noMultiEmbedsOption) document.querySelector(".box .item.multi")?.remove(); if (inIframe) // Remove menu options that don"t work in iframe. for (const e of document.querySelectorAll(".no-frame")) e.remove(); if (autoUpdateURL) { document.body.classList.add("autoUpdateURL"); document.querySelector(".item.auto > input").checked = true; } if (single) { document.body.classList.add("single"); if (autoParams) single ? urlOptions({ set: ["single", ""] }) : urlOptions({ remove: "single" }); } if (hideEditor) { document.body.classList.add("no-editor"); document.querySelector(".toggle .toggles .editor input").checked = false; } if (hidePreview) { document.body.classList.add("no-preview"); document.querySelector(".toggle .toggles .preview input").checked = false; } if (onlyEmbed) document.body.classList.add("only-embed"); else { document.querySelector(".side1.noDisplay")?.classList.remove("noDisplay"); if (useJsonEditor) document.body.classList.remove("gui"); } if (noUser) { document.body.classList.add("no-user"); if (autoParams) noUser ? urlOptions({ set: ["nouser", ""] }) : urlOptions({ remove: "nouser" }); } else { if (username) document.querySelector(".username").textContent = username; if (avatar) document.querySelector(".avatar").src = avatar; if (verified) document.querySelector(".msgEmbed > .contents").classList.add("verified"); } for (const e of document.querySelectorAll(".clickable > img")) e.parentElement.addEventListener("mouseup", el => window.open(el.target.src)); const editorHolder = document.querySelector(".editorHolder"), guiParent = document.querySelector(".top"), embedContent = document.querySelector(".messageContent"), embedCont = document.querySelector(".msgEmbed>.container"), gui = guiParent.querySelector(".gui:first-of-type"); editor = CodeMirror(elt => editorHolder.parentNode.replaceChild(elt, editorHolder), { value: JSON.stringify(json, null, 4), gutters: ["CodeMirror-foldgutter", "CodeMirror-lint-markers"], scrollbarStyle: "overlay", mode: "application/json", theme: "material-darker", matchBrackets: true, foldGutter: true, lint: true, extraKeys: { // Fill in indent spaces on a new line when enter (return) key is pressed. Enter: _ => { const cursor = editor.getCursor(); const end = editor.getLine(cursor.line); const leadingSpaces = end.replace(/\S($|.)+/g, "") || " \n"; const nextLine = editor.getLine(cursor.line + 1); if ((nextLine === undefined || !nextLine.trim()) && !end.substr(cursor.ch).trim()) editor.replaceRange("\n", { line: cursor.line, ch: cursor.ch }); else editor.replaceRange(`\n${end.endsWith("{") ? leadingSpaces + " " : leadingSpaces}`, { line: cursor.line, ch: cursor.ch }); }, } }); editor.focus(); const notif = document.querySelector(".notification"); error = (msg, time = "5s") => { notif.innerHTML = msg; notif.style.removeProperty("--startY"); notif.style.removeProperty("--startOpacity"); notif.style.setProperty("--time", time); notif.onanimationend = () => notif.style.display = null; // If notification element is not already visible, (no other message is already displayed), display it. if (!notif.style.display) return notif.style.display = "block", false; // If there"s a message already displayed, update it and delay animating out. notif.style.setProperty("--startY", 0); notif.style.setProperty("--startOpacity", 1); notif.style.display = null; setTimeout(() => notif.style.display = "block", .5); return false; }; const url = (url) => /^(https?:)?\/\//g.exec(url) ? url : "//" + url; const makeShort = (txt, length, mediaWidth) => { if (mediaWidth && matchMedia(`(max-width:${mediaWidth}px)`).matches) return txt.length > (length - 3) ? txt.substring(0, length - 3) + "..." : txt; return txt; } const allGood = embedObj => { let invalid, err; let str = JSON.stringify(embedObj, null, 4) let re = /("(?:icon_)?url": *")((?!\w+?:\/\/).+)"/g.exec(str); if (embedObj.timestamp && new Date(embedObj.timestamp).toString() === "Invalid Date") { if (allowPlaceholders === 2) return true; if (!allowPlaceholders) invalid = true, err = "Timestamp is invalid"; } else if (re) { // If a URL is found without a protocol if (!/\w+:|\/\/|^\//g.exec(re[2]) && re[2].includes(".")) { let activeInput = document.querySelector("input[class$='link' i]:focus") if (activeInput && !allowPlaceholders) { lastPos = activeInput.selectionStart + 7; activeInput.value = `http://${re[2]}`; activeInput.setSelectionRange(lastPos, lastPos) return true; } } if (allowPlaceholders !== 2) invalid = true, err = (`URL should have a protocol. Did you mean http://${makeShort(re[2], 30, 600).replace(" ", "")}?`); } if (invalid) { validationError = true; return error(err); } return true; } const markup = (txt, { replaceEmojis, inlineBlock, inEmbed }) => { if (replaceEmojis) txt = txt.replace(/(?[^>]+)(? p && emojis[p] ? emojis[p] : match); txt = txt /** Markdown */ .replace(/<:\w+:(\d{17,19})>/g, "") .replace(/<a:\w+:(\d{17,20})>/g, "") .replace(/~~(.+?)~~/g, "$1") .replace(/\*\*\*(.+?)\*\*\*/g, "$1") .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/__(.+?)__/g, "$1") .replace(/\*(.+?)\*/g, "$1") .replace(/_(.+?)_/g, "$1") // Replace >>> and > with block-quotes. > is HTML code for > .replace(/^(?: *>>> ([\s\S]*))|(?:^ *>(?!>>) +.+\n)+(?:^ *>(?!>>) .+\n?)+|^(?: *>(?!>>) ([^\n]*))(\n?)/mg, (all, match1, match2, newLine) => { return `
${match1 || match2 || newLine ? match1 || match2 : all.replace(/^ *> /gm, "")}
`; }) /** Mentions */ .replace(/<#\d+>/g, () => `channel`) .replace(/<@(?:&|!)?\d+>|@(?:everyone|here)/g, match => { if (match.startsWith("@")) return `${match}` else return `@${match.includes("&") ? "role" : "user"}` }) if (inlineBlock) // Treat both inline code and code blocks as inline code txt = txt.replace(/`([^`]+?)`|``([^`]+?)``|```((?:\n|.)+?)```/g, (m, x, y, z) => x ? `${x}` : y ? `${y}` : z ? `${z}` : m); else { // Code block txt = txt.replace(/```(?:([a-z0-9_+\-.]+?)\n)?\n*([^\n][^]*?)\n*```/ig, (m, w, x) => { if (w) return `
${x.trim()}
` else return `
${x.trim()}
` }); // Inline code txt = txt.replace(/`([^`]+?)`|``([^`]+?)``/g, (m, x, y, z) => x ? `${x}` : y ? `${y}` : z ? `${z}` : m) } if (inEmbed) txt = txt.replace(/\[([^\[\]]+)\]\((.+?)\)/g, `$1`); return txt; } const createEmbedFields = (fields, embedFields) => { embedFields.innerHTML = ""; let index, gridCol; for (const [i, f] of fields.entries()) { if (f.name && f.value) { const fieldElement = embedFields.insertBefore(document.createElement("div"), null); // Figuring out if there are only two fields on a row to give them more space. // e.fields = json.embeds.fields. // if both the field of index "i" and the next field on it"s right are inline and - if (fields[i].inline && fields[i + 1]?.inline && // it"s the first field in the embed or - ((i === 0 && fields[i + 2] && !fields[i + 2].inline) || (( // it"s not the first field in the embed but the previous field is not inline or - i > 0 && !fields[i - 1].inline || // it has 3 or more fields behind it and 3 of those are inline except the 4th one back if it exists - i >= 3 && fields[i - 1].inline && fields[i - 2].inline && fields[i - 3].inline && (fields[i - 4] ? !fields[i - 4].inline : !fields[i - 4]) // or it"s the first field on the last row or the last field on the last row is not inline or it"s the first field in a row and it"s the last field on the last row. ) && (i == fields.length - 2 || !fields[i + 2].inline))) || i % 3 === 0 && i == fields.length - 2) { // then make the field halfway (and the next field will take the other half of the embed). index = i, gridCol = "1 / 7"; } // The next field. if (index === i - 1) gridCol = "7 / 13"; if (!f.inline) fieldElement.outerHTML = `
${markup(encodeHTML(f.name), { inEmbed: true, replaceEmojis: true, inlineBlock: true })}
${markup(encodeHTML(f.value), { inEmbed: true, replaceEmojis: true })}
`; else { if (i && !fields[i - 1].inline) colNum = 1; fieldElement.outerHTML = `
${markup(encodeHTML(f.name), { inEmbed: true, replaceEmojis: true, inlineBlock: true })}
${markup(encodeHTML(f.value), { inEmbed: true, replaceEmojis: true })}
`; if (index !== i) gridCol = false; } colNum = (colNum === 9 ? 1 : colNum + 4); num++; }; }; for (const e of document.querySelectorAll(".embedField[style=\"grid-column: 1 / 5;\"]")) if (!e.nextElementSibling || e.nextElementSibling.style.gridColumn === "1 / 13") e.style.gridColumn = "1 / 13"; colNum = 1; display(embedFields, undefined, "grid"); } const smallerScreen = matchMedia("(max-width: 1015px)"); const encodeHTML = str => str.replace(/[\u00A0-\u9999<>\&]/g, i => "&#" + i.charCodeAt(0) + ";"); const timestamp = stringISO => { const date = stringISO ? new Date(stringISO) : new Date(), dateArray = date.toLocaleString("en-US", { hour: "numeric", hour12: false, minute: "numeric" }), today = new Date(), yesterday = new Date(new Date().setDate(today.getDate() - 1)), tommorrow = new Date(new Date().setDate(today.getDate() + 1)); return today.toDateString() === date.toDateString() ? `Today at ${dateArray}` : yesterday.toDateString() === date.toDateString() ? `Yesterday at ${dateArray}` : tommorrow.toDateString() === date.toDateString() ? `Tomorrow at ${dateArray}` : `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}/${date.getFullYear()}`; } const display = (el, data, displayType) => { if (data) el.innerHTML = data; el.style.display = displayType || "unset"; } const hide = el => el.style.removeProperty("display"), imgSrc = (elm, src, remove) => remove ? elm.style.removeProperty("content") : elm.style.content = `url(${src})`; const [guiFragment, fieldFragment, embedFragment, guiEmbedAddFragment] = Array.from({ length: 4 }, () => document.createDocumentFragment()); embedFragment.appendChild(document.querySelector(".embed.markup").cloneNode(true)); guiEmbedAddFragment.appendChild(document.querySelector(".guiEmbedAdd").cloneNode(true)); fieldFragment.appendChild(document.querySelector(".edit>.fields>.field").cloneNode(true)); document.querySelector(".embed.markup").remove(); gui.querySelector(".edit>.fields>.field").remove(); for (const child of gui.childNodes) guiFragment.appendChild(child.cloneNode(true)); // Renders the GUI editor with json data from "jsonObject". buildGui = (object = jsonObject, opts) => { gui.innerHTML = ""; gui.appendChild(guiEmbedAddFragment.firstChild.cloneNode(true)) .addEventListener("click", () => { if (indexOfEmptyGuiEmbed("(empty embed)") !== -1) return; jsonObject.embeds.push({}); buildGui(); }); for (const child of Array.from(guiFragment.childNodes)) { if (child.classList?.[1] === "content") gui.insertBefore(gui.appendChild(child.cloneNode(true)), gui.appendChild(child.nextElementSibling.cloneNode(true))).nextElementSibling.firstElementChild.value = object.content || ""; else if (child.classList?.[1] === "guiEmbedName") { for (const [i, embed] of(object.embeds.length ? object.embeds : [{}]).entries()) { const guiEmbedName = gui.appendChild(child.cloneNode(true)) guiEmbedName.querySelector(".text").innerHTML = `Embed ${i + 1}${embed.title ? `: ${embed.title}` : ""}`; guiEmbedName.querySelector(".icon").addEventListener("click", () => { object.embeds.splice(i, 1); buildGui(); buildEmbed(); }); const guiEmbed = gui.appendChild(createElement({ "div": { className: "guiEmbed" } })); const guiEmbedTemplate = child.nextElementSibling; for (const child2 of Array.from(guiEmbedTemplate.children)) { if (!child2?.classList.contains("edit")) { const row = guiEmbed.appendChild(child2.cloneNode(true)); const edit = child2.nextElementSibling?.cloneNode(true); edit?.classList.contains("edit") && guiEmbed.appendChild(edit); switch (child2.classList[1]) { case "author": const authorURL = embed?.author?.icon_url || ""; if (authorURL) edit.querySelector(".imgParent").style.content = "url(" + encodeHTML(authorURL) + ")"; edit.querySelector(".editAuthorLink").value = authorURL; edit.querySelector(".editAuthorName").value = embed?.author?.name || ""; break; case "title": row.querySelector(".editTitle").value = embed?.title || ""; break; case "description": edit.querySelector(".editDescription").value = embed?.description || ""; break; case "thumbnail": const thumbnailURL = embed?.thumbnail?.url || ""; if (thumbnailURL) edit.querySelector(".imgParent").style.content = "url(" + encodeHTML(thumbnailURL) + ")"; edit.querySelector(".editThumbnailLink").value = thumbnailURL; break; case "image": const imageURL = embed?.image?.url || ""; if (imageURL) edit.querySelector(".imgParent").style.content = "url(" + encodeHTML(imageURL) + ")"; edit.querySelector(".editImageLink").value = imageURL; break; case "footer": const footerURL = embed?.footer?.icon_url || ""; if (footerURL) edit.querySelector(".imgParent").style.content = "url(" + encodeHTML(footerURL) + ")"; edit.querySelector(".editFooterLink").value = footerURL; edit.querySelector(".editFooterText").value = embed?.footer?.text || ""; break; case "fields": for (const f of embed?.fields || []) { const fields = edit.querySelector(".fields"); const field = fields.appendChild(createElement({ "div": { className: "field" } })); for (const child of Array.from(fieldFragment.firstChild.children)) { const newChild = field.appendChild(child.cloneNode(true)); if (child.classList.contains("inlineCheck")) newChild.querySelector("input").checked = !!f.inline; else if (f.value && child.classList?.contains("fieldInner")) newChild.querySelector(".designerFieldName input").value = f.name || "", newChild.querySelector(".designerFieldValue textarea").value = f.value || ""; } } } } } } } // Expand last embed in GUI const names = gui.querySelectorAll(".guiEmbedName"); names[names.length - 1]?.classList.add("active"); } for (const e of document.querySelectorAll(".top>.gui .item")) e.addEventListener("click", el => { if (e?.classList.contains("active")) getSelection().anchorNode !== e && e.classList.remove("active"); else if (e) { const inlineField = e.closest(".inlineField"), input = e.nextElementSibling?.querySelector("input[type=\"text\"]"), txt = e.nextElementSibling?.querySelector("textarea"); e.classList.add("active"); if (e.classList.contains("guiEmbedName")) return changeLastActiveGuiEmbed(guiEmbedIndex(e)); else if (inlineField) inlineField.querySelector(".ttle~input").focus(); else if (e.classList.contains("footer")) { const date = new Date(jsonObject.embeds[guiEmbedIndex(e)]?.timestamp || new Date()); const textElement = e.nextElementSibling.querySelector("svg>text"); const dateInput = textElement.closest(".footerDate").querySelector("input"); return ( textElement.textContent = (date.getDate() + "").padStart(2, 0), dateInput.value = date.toISOString().substring(0, 19) ); } else if (input) { !smallerScreen.matches && input.focus(); input.selectionStart = input.selectionEnd = input.value.length; } else if (txt && !smallerScreen.matches) txt.focus(); if (e.classList.contains("fields")) { if (reverseColumns && smallerScreen.matches) // return elm.nextElementSibling.scrollIntoView({ behavior: "smooth", block: "end" }); return e.parentNode.scrollTop = e.offsetTop; e.scrollIntoView({ behavior: "smooth", block: "center" }); } } }) content = gui.querySelector(".editContent"); title = gui.querySelector(".editTitle"); authorName = gui.querySelector(".editAuthorName"); authorLink = gui.querySelector(".editAuthorLink"); desc = gui.querySelector(".editDescription"); thumbLink = gui.querySelector(".editThumbnailLink"); imgLink = gui.querySelector(".editImageLink"); footerText = gui.querySelector(".editFooterText"); footerLink = gui.querySelector(".editFooterLink"); // Scroll into view when tabs are opened in the GUI. const lastTabs = Array.from(document.querySelectorAll(".footer.rows2, .image.largeImg")); const requiresView = matchMedia(`${smallerScreen.media}, (max-height: 845px)`); const addGuiEventListeners = () => { for (const e of document.querySelectorAll(".gui .item:not(.fields)")) e.onclick = () => { if (lastTabs.includes(e) || requiresView.matches) { if (!reverseColumns || !smallerScreen.matches) e.scrollIntoView({ behavior: "smooth", block: "center" }); else if (e.nextElementSibling.classList.contains("edit") && e.classList.contains("active")) // e.nextElementSibling.scrollIntoView({ behavior: "smooth", block: "end" }); e.parentNode.scrollTop = e.offsetTop; } }; for (const e of document.querySelectorAll(".addField")) e.onclick = () => { const guiEmbed = e.closest(".guiEmbed"); const indexOfGuiEmbed = Array.from(gui.querySelectorAll(".guiEmbed")).indexOf(guiEmbed); if (indexOfGuiEmbed === -1) return error("Could not find the embed to add the field to."); const fieldsObj = (jsonObject.embeds[indexOfGuiEmbed] ??= {}).fields ??= []; if (fieldsObj.length >= 25) return error("Cannot have more than 25 fields"); fieldsObj.push({ name: "Field name", value: "Field value", inline: false }); const newField = guiEmbed?.querySelector(".item.fields+.edit>.fields")?.appendChild(fieldFragment.firstChild.cloneNode(true)); buildEmbed(); addGuiEventListeners(); newField.scrollIntoView({ behavior: "smooth", block: "center" }); if (!smallerScreen.matches) { const firstFieldInput = newField.querySelector(".designerFieldName input"); firstFieldInput?.setSelectionRange(firstFieldInput.value.length, firstFieldInput.value.length); firstFieldInput?.focus(); } }; for (const e of document.querySelectorAll(".fields .field .removeBtn")) e.onclick = () => { const embedIndex = guiEmbedIndex(e); const fieldIndex = Array.from(e.closest(".fields").children).indexOf(e.closest(".field")); if (jsonObject.embeds[embedIndex]?.fields[fieldIndex] === -1) return error("Failed to find the index of the field to remove."); jsonObject.embeds[embedIndex].fields.splice(fieldIndex, 1); buildEmbed(); e.closest(".field").remove(); }; for (const e of gui.querySelectorAll("textarea, input")) e.oninput = el => { const value = el.target.value; const index = guiEmbedIndex(el.target); const field = el.target.closest(".field"); const fields = field?.closest(".fields"); const embedObj = jsonObject.embeds[index] ??= {}; if (field) { console.log(field) const fieldIndex = Array.from(fields.children).indexOf(field); const jsonField = embedObj.fields[fieldIndex]; const embedFields = document.querySelectorAll(".container>.embed")[index]?.querySelector(".embedFields"); if (jsonField) { if (el.target.type === "text") jsonField.name = value; else if (el.target.type === "textarea") jsonField.value = value; else jsonField.inline = el.target.checked; createEmbedFields(embedObj.fields, embedFields); } } else { switch (el.target.classList?.[0]) { case "editContent": jsonObject.content = value; buildEmbed({ only: "content" }); break; case "editTitle": embedObj.title = value; const guiEmbedName = el.target.closest(".guiEmbed")?.previousElementSibling; if (guiEmbedName?.classList.contains("guiEmbedName")) guiEmbedName.querySelector(".text").innerHTML = `${guiEmbedName.innerText.split(":")[0]}${value ? `: ${value}` : ""}`; buildEmbed({ only: "embedTitle", index: guiEmbedIndex(el.target) }); break; case "editAuthorName": embedObj.author ??= {}, embedObj.author.name = value; buildEmbed({ only: "embedAuthorName", index: guiEmbedIndex(el.target) }); break; case "editAuthorLink": embedObj.author ??= {}, embedObj.author.icon_url = value; imgSrc(el.target.previousElementSibling, value); buildEmbed({ only: "embedAuthorLink", index: guiEmbedIndex(el.target) }); break; case "editDescription": embedObj.description = value; buildEmbed({ only: "embedDescription", index: guiEmbedIndex(el.target) }); break; case "editThumbnailLink": embedObj.thumbnail ??= {}, embedObj.thumbnail.url = value; imgSrc(el.target.closest(".editIcon").querySelector(".imgParent"), value); buildEmbed({ only: "embedThumbnail", index: guiEmbedIndex(el.target) }); break; case "editImageLink": embedObj.image ??= {}, embedObj.image.url = value; imgSrc(el.target.closest(".editIcon").querySelector(".imgParent"), value); buildEmbed({ only: "embedImageLink", index: guiEmbedIndex(el.target) }); break; case "editFooterText": embedObj.footer ??= {}, embedObj.footer.text = value; buildEmbed({ only: "embedFooterText", index: guiEmbedIndex(el.target) }); break; case "editFooterLink": embedObj.footer ??= {}, embedObj.footer.icon_url = value; imgSrc(el.target.previousElementSibling, value); buildEmbed({ only: "embedFooterLink", index: guiEmbedIndex(el.target) }); break; case "embedFooterTimestamp": const date = new Date(value); if (isNaN(date.getTime())) return error("Invalid date"); embedObj.timestamp = date; el.target.parentElement.querySelector("svg>text").textContent = (date.getDate() + "").padStart(2, 0); buildEmbed({ only: "embedFooterTimestamp", index: guiEmbedIndex(el.target) }); break; } // Find and filter out any empty objects ({}) in the embeds array as Discord doesn"t like them. const nonEmptyEmbedObjects = json.embeds?.filter(o => 0 in Object.keys(o)); if (nonEmptyEmbedObjects?.length) json.embeds = nonEmptyEmbedObjects; } // Display embed elements hidden due to not having content. ".msgEmbed>.container" is embed container. document.querySelectorAll(".msgEmbed>.container")[guiEmbedIndex(el.target)]?.querySelector(".emptyEmbed")?.classList.remove("emptyEmbed"); } const uploadError = (message, browse, sleepTime) => { browse.classList.remove("loading"); browse.classList.add("error"); const p = browse.parentElement.querySelector(".browse.error>p") p.dataset.error = message; setTimeout(() => { browse.classList.remove("error"); delete p.dataset.error; }, sleepTime ?? 7000); } for (const browse of document.querySelectorAll(".browse")) browse.onclick = e => { const formData = new FormData(); const fileInput = createElement({ "input": { type: "file", accept: "image/*" } }); const edit = browse.closest(".edit"); const expiration = 7 * 24 * 60 * 60; fileInput.onchange = el => { if (el.target.files[0].size > 32 * 1024 * 1024) return uploadError("File is too large. Maximum size is 32 MB.", browse, 5000); formData.append("expiration", expiration); // Expire after 7 days. Discord caches files. formData.append("key", options.uploadKey || "93385e22b0619db73a5525140b13491c"); // Add your own key through the uploadKey option. formData.append("image", el.target.files[0]); // formData.append("name", ""); // Uses original file name if no "name" is not specified. browse.classList.add("loading"); fetch("https://api.imgbb.com/1/upload", { method: "POST", body: formData }) .then(res => res.json()) .then(res => { browse.classList.remove("loading"); if (!res.success) { console.log("Upload failed:", res.data?.error || res.error?.message || res); return uploadError(res.data?.error || res.error?.message || "Request failed. (Check dev-console)", browse); } imgSrc(edit.querySelector(".editIcon > .imgParent"), res.data.url); const linkInput = edit.querySelector("input[type=text]"); const textInput = edit.querySelector("input[class$=Name], input[class$=Text]"); linkInput.value = res.data.url; // focus on the next empty input if the field requires a name or text to display eg. footer or author. !textInput?.value && textInput?.focus(); console.info(`${res.data.url} will be deleted in ${expiration / 60 / 60} hours. To delete it now, visit ${res.data.delete_url} and scroll down to find the delete button.`); linkInput.dispatchEvent(new Event("input")); }).catch(err => { browse.classList.remove("loading"); error(`Request failed with error: ${err}`) }) } fileInput.click(); } if (multiEmbeds) { for (const e of document.querySelectorAll(".guiEmbed")) e.onclick = () => { const guiEmbed = e.closest(".guiEmbed"); const indexOfGuiEmbed = Array.from(gui.querySelectorAll(".guiEmbed")).indexOf(guiEmbed); if (indexOfGuiEmbed === -1) return error("Could not find the embed to add the field to."); changeLastActiveGuiEmbed(indexOfGuiEmbed); }; if (!jsonObject.embeds[lastActiveGuiEmbedIndex]) changeLastActiveGuiEmbed( jsonObject.embeds[lastActiveGuiEmbedIndex - 1] ? lastActiveGuiEmbedIndex - 1 : jsonObject.embeds.length ? 0 : -1 ); } else { changeLastActiveGuiEmbed(-1); } } addGuiEventListeners(); let activeGuiEmbed; if (opts?.guiEmbedIndex) { activeGuiEmbed = Array.from(document.querySelectorAll(".gui .item.guiEmbedName"))[opts.guiEmbedIndex]; activeGuiEmbed?.classList.add("active"); activeGuiEmbed = activeGuiEmbed?.nextElementSibling; } if (opts?.activateClassNames) for (const cName of opts.activateClassNames) for (const e of document.getElementsByClassName(cName)) e.classList.add("active"); else if (opts?.guiTabs) { const tabs = opts.guiTabs.split?.(/, */) || opts.guiTabs; const bottomKeys = ["footer", "image"]; const topKeys = ["author", "content"]; // Deactivate the default activated GUI fields for (const e of gui.querySelectorAll(".item:not(.guiEmbedName).active")) e.classList.remove("active"); // Activate wanted GUI fields for (const e of document.querySelectorAll(`.${tabs.join(", .")}`)) e.classList.add("active"); // Autoscroll GUI to the bottom if necessary. if (!tabs.some(item => topKeys.includes(item)) && tabs.some(item => bottomKeys.includes(item))) { const gui2 = document.querySelector(".top .gui"); gui2.scrollTo({ top: gui2.scrollHeight }); } } else if (opts?.activate) for (const clss of Array.from(opts.activate).map(el => el.className).map(clss => "." + clss.split(" ").slice(0, 2).join("."))) for (const e of document.querySelectorAll(clss)) e.classList.add("active"); else for (const clss of document.querySelectorAll(".item.author, .item.description")) clss.classList.add("active"); } buildGui(jsonObject, { guiTabs }); gui.classList.remove("hidden"); fields = gui.querySelector(".fields ~ .edit .fields"); // Renders embed and message content. buildEmbed = ({ jsonData, only, index = 0 } = {}) => { if (jsonData) json = jsonData; if (!jsonObject.embeds?.length) document.body.classList.add("emptyEmbed"); try { // If there"s no message content, hide the message content HTML element. if (!jsonObject.content) document.body.classList.add("emptyContent"); else { // Update embed content in render embedContent.innerHTML = markup(encodeHTML(jsonObject.content), { replaceEmojis: true }); document.body.classList.remove("emptyContent"); } const embed = document.querySelectorAll(".container>.embed")[index]; const embedObj = jsonObject.embeds[index]; if (only && (!embed || !embedObj)) return buildEmbed(); switch (only) { // If only updating the message content and nothing else, return here. case "content": return externalParsing({ element: embedContent }); case "embedTitle": const embedTitle = embed?.querySelector(".embedTitle"); if (!embedTitle) return buildEmbed(); if (!embedObj.title) hide(embedTitle); else display(embedTitle, markup(`${embedObj.url ? "" + encodeHTML(embedObj.title) + "" : encodeHTML(embedObj.title)}`, { replaceEmojis: true, inlineBlock: true })); return externalParsing({ element: embedTitle }); case "embedAuthorName": case "embedAuthorLink": const embedAuthor = embed?.querySelector(".embedAuthor"); if (!embedAuthor) return buildEmbed(); if (!embedObj.author?.name) hide(embedAuthor); else display(embedAuthor, ` ${embedObj.author.icon_url ? "" : ""} ${embedObj.author.url ? "" + encodeHTML(embedObj.author.name) + "" : "" + encodeHTML(embedObj.author.name) + ""}`, "flex"); return externalParsing({ element: embedAuthor }); case "embedDescription": const embedDescription = embed?.querySelector(".embedDescription"); if (!embedDescription) return buildEmbed(); if (!embedObj.description) hide(embedDescription); else display(embedDescription, markup(encodeHTML(embedObj.description), { inEmbed: true, replaceEmojis: true })); return externalParsing({ element: embedDescription }); case "embedThumbnail": const embedThumbnailLink = embed?.querySelector(".embedThumbnailLink"); if (!embedThumbnailLink) return buildEmbed(); const pre = embed.querySelector(".embedGrid .markup pre"); if (embedObj.thumbnail?.url) { embedThumbnailLink.src = embedObj.thumbnail.url; embedThumbnailLink.parentElement.style.display = "block"; if (pre) pre.style.maxWidth = "90%"; } else { hide(embedThumbnailLink.parentElement); pre?.style.removeProperty("max-width"); } return afterBuilding(); case "embedImage": const embedImageLink = embed?.querySelector(".embedImageLink"); if (!embedImageLink) return buildEmbed(); if (!embedObj.image?.url) hide(embedImageLink.parentElement); else embedImageLink.src = embedObj.image.url, embedImageLink.parentElement.style.display = "block"; return afterBuilding(); case "embedFooterText": case "embedFooterLink": case "embedFooterTimestamp": const embedFooter = embed?.querySelector(".embedFooter"); if (!embedFooter) return buildEmbed(); if (!embedObj.footer?.text) hide(embedFooter); else display(embedFooter, ` ${embedObj.footer.icon_url ? "" : ""} ${encodeHTML(embedObj.footer.text)} ${embedObj.timestamp ? "•" + encodeHTML(timestamp(embedObj.timestamp)) : ""}`, "flex"); return externalParsing({ element: embedFooter }); } if (multiEmbeds) embedCont.innerHTML = ""; for (const embedObj of jsonObject.embeds) { if (!allGood(embedObj)) continue; if (!multiEmbeds) embedCont.innerHTML = ""; validationError = false; const embedElement = embedCont.appendChild(embedFragment.firstChild.cloneNode(true)); const embedGrid = embedElement.querySelector(".embedGrid"); const msgEmbed = embedElement.querySelector(".msgEmbed"); const embedTitle = embedElement.querySelector(".embedTitle"); const embedDescription = embedElement.querySelector(".embedDescription"); const embedAuthor = embedElement.querySelector(".embedAuthor"); const embedFooter = embedElement.querySelector(".embedFooter"); const embedImage = embedElement.querySelector(".embedImage > img"); const embedThumbnail = embedElement.querySelector(".embedThumbnail > img"); const embedFields = embedElement.querySelector(".embedFields"); if (embedObj.title) display(embedTitle, markup(`${embedObj.url ? "" + encodeHTML(embedObj.title) + "" : encodeHTML(embedObj.title)}`, { replaceEmojis: true, inlineBlock: true })); else hide(embedTitle); if (embedObj.description) display(embedDescription, markup(encodeHTML(embedObj.description), { inEmbed: true, replaceEmojis: true })); else hide(embedDescription); if (embedObj.color) embedGrid.closest(".embed").style.borderColor = (typeof embedObj.color === "number" ? "#" + embedObj.color.toString(16).padStart(6, "0") : embedObj.color); else embedGrid.closest(".embed").style.removeProperty("border-color"); if (embedObj.author?.name) display(embedAuthor, ` ${embedObj.author.icon_url ? "" : ""} ${embedObj.author.url ? "" + encodeHTML(embedObj.author.name) + "" : "" + encodeHTML(embedObj.author.name) + ""}`, "flex"); else hide(embedAuthor); const pre = embedGrid.querySelector(".markup pre"); if (embedObj.thumbnail?.url) { embedThumbnail.src = embedObj.thumbnail.url; embedThumbnail.parentElement.style.display = "block"; if (pre) pre.style.maxWidth = "90%"; } else { hide(embedThumbnail.parentElement); if (pre) pre.style.removeProperty("max-width"); } if (embedObj.image?.url) embedImage.src = embedObj.image.url, embedImage.parentElement.style.display = "block"; else hide(embedImage.parentElement); if (embedObj.footer?.text) display(embedFooter, ` ${embedObj.footer.icon_url ? "" : ""} ${encodeHTML(embedObj.footer.text)} ${embedObj.timestamp ? "•" + encodeHTML(timestamp(embedObj.timestamp)) : ""}`, "flex"); else if (embedObj.timestamp) display(embedFooter, `${encodeHTML(timestamp(embedObj.timestamp))}`, "flex"); else hide(embedFooter); if (embedObj.fields) createEmbedFields(embedObj.fields, embedFields); else hide(embedFields); document.body.classList.remove("emptyEmbed"); externalParsing(); if (embedElement.innerText.trim() || embedElement.querySelector(".embedGrid > [style*=display] img")) embedElement.classList.remove("emptyEmbed"); else embedElement.classList.add("emptyEmbed"); } // Make sure that the embed has no text or any visible images such as custom emojis before hiding. if (!multiEmbeds && !embedCont.innerText.trim() && !embedCont.querySelector(".embedGrid > [style*=display] img")) document.body.classList.add("emptyEmbed"); afterBuilding() } catch (e) { console.error(e); error(e); } window.parent.postMessage([sendMessage, json], "*"); console.log("UPDATED") } editor.on("change", editor => { // If the editor value is not set by the user, return. if (JSON.stringify(json, null, 4) === editor.getValue()) return; try { // Autofill when " is typed on new line const line = editor.getCursor().line; const text = editor.getLine(line) if (text.trim() === "\"") { editor.replaceRange(text.trim() + ":", { line, ch: line.length }); editor.setCursor(line, text.length) } json = JSON.parse(editor.getValue()); const dataKeys = Object.keys(json); if (dataKeys.length && !allJsonKeys.some(key => dataKeys.includes(key))) { const usedKeys = dataKeys.filter(key => !allJsonKeys.includes(key)); if (usedKeys.length > 2) return error(`'${usedKeys[0] + "', '" + usedKeys.slice(1, usedKeys.length - 1).join("', '")}', and '${usedKeys[usedKeys.length - 1]}' are invalid keys.`); return error(`'${usedKeys.length == 2 ? usedKeys[0] + "' and '" + usedKeys[usedKeys.length - 1] + "' are invalid keys." : usedKeys[0] + "' is an invalid key."}`); } buildEmbed(); } catch (e) { if (editor.getValue()) return; document.body.classList.add("emptyEmbed"); embedContent.innerHTML = ""; } }); const picker = new CP(document.querySelector(".picker"), state = { parent: document.querySelector(".cTop") }); picker.fire?.("change", toRGB("#41f097")); const colors = document.querySelector(".colors"); const hexInput = colors?.querySelector(".hex>div input"); let typingHex = true, exit = false; removePicker = () => { if (exit) return exit = false; if (typingHex) picker.enter(); else { typingHex = false, exit = true; colors.classList.remove("picking"); picker.exit(); } } document.querySelector(".colBack")?.addEventListener("click", () => { picker.self.remove(); typingHex = false; removePicker(); }) picker.on?.("exit", removePicker); picker.on?.("enter", () => { const embedIndex = multiEmbeds && lastActiveGuiEmbedIndex !== -1 ? lastActiveGuiEmbedIndex : 0; if (jsonObject?.embeds[embedIndex]?.color) { hexInput.value = jsonObject.embeds[embedIndex].color.toString(16).padStart(6, "0"); document.querySelector(".hex.incorrect")?.classList.remove("incorrect"); } colors.classList.add("picking") }) document.querySelectorAll(".color").forEach(e => e.addEventListener("click", el => { const embedIndex = multiEmbeds && lastActiveGuiEmbedIndex !== -1 ? lastActiveGuiEmbedIndex : 0; const embed = document.querySelectorAll(".msgEmbed .container>.embed")[embedIndex]; const embedObj = jsonObject.embeds[embedIndex] ??= {}; const color = el.target.closest(".color"); embedObj.color = toRGB(color.style.backgroundColor, false, true); embed && (embed.style.borderColor = color.style.backgroundColor); picker.source.style.removeProperty("background"); })) hexInput?.addEventListener("focus", () => typingHex = true); setTimeout(() => { picker.on?.("change", function (r, g, b, a) { const embedIndex = multiEmbeds && lastActiveGuiEmbedIndex !== -1 ? lastActiveGuiEmbedIndex : 0; const embed = document.querySelectorAll(".msgEmbed .container>.embed")[embedIndex]; const embedObj = jsonObject.embeds[embedIndex]; picker.source.style.background = this.color(r, g, b); embedObj.color = parseInt(this.color(r, g, b).slice(1), 16); embed.style.borderColor = this.color(r, g, b); hexInput.value = embedObj.color.toString(16).padStart(6, "0"); }) }, 1000) document.querySelector(".timeText").innerText = timestamp(); for (const block of document.querySelectorAll(".markup pre > code")) hljs.highlightBlock(block); document.querySelector(".opt.gui").addEventListener("click", () => { if (lastGuiJson && lastGuiJson !== JSON.stringify(json, null, 4)) buildGui(); lastGuiJson = false activeFields = null; document.body.classList.add("gui"); if (pickInGuiMode) { pickInGuiMode = false; togglePicker(); } }) document.querySelector(".opt.json").addEventListener("click", () => { const emptyEmbedIndex = indexOfEmptyGuiEmbed(false); if (emptyEmbedIndex !== -1) // Clicked GUI tab while a blank embed is added from GUI. return error(gui.querySelectorAll(".item.guiEmbedName")[emptyEmbedIndex].innerText.split(":")[0] + " should not be empty.", "3s"); const jsonStr = JSON.stringify(json, null, 4); lastGuiJson = jsonStr; document.body.classList.remove("gui"); editor.setValue(jsonStr === "{}" ? "{\n\t\n}" : jsonStr); editor.refresh(); editor.focus(); activeFields = document.querySelectorAll(".gui > .item.active"); if (document.querySelector("section.side1.low")) togglePicker(true); }) document.querySelector(".clear").addEventListener("click", () => { json = {}; picker.source.style.removeProperty("background"); document.querySelector(".msgEmbed .container>.embed")?.remove(); buildEmbed(); buildGui(); const jsonStr = JSON.stringify(json, null, 4); editor.setValue(jsonStr === "{}" ? "{\n\t\n}" : jsonStr); for (const e of document.querySelectorAll(".gui .item")) e.classList.add("active"); if (!smallerScreen.matches) content.focus(); }) document.querySelector(".top-btn.menu")?.addEventListener("click", e => { if (e.target.closest(".item.dataLink")) { const data = encodeJson(json, true).replace(/(? x === "=" ? "" : "&"); if (!window.chrome) // With long text inside a "prompt" on Chromium based browsers, some text will be trimmed off and replaced with "...". return prompt("Here\"s the current URL with base64 embed data:", data); // So, for the Chromium users, we copy to clipboard instead of showing a prompt. try { // Clipboard API might only work on HTTPS protocol. navigator.clipboard.writeText(data); } catch { const input = document.body.appendChild(document.createElement("input")); input.value = data; input.select(); document.setSelectionRange(0, 50000); document.execCommand("copy"); document.body.removeChild(input); } return alert("Copied to clipboard."); } if (e.target.closest(".item.download")) return createElement({ a: { download: "embed" + ".json", href: "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(json, null, 4)) } }).click(); const input = e.target.closest(".item")?.querySelector("input"); if (input) input.checked = !input.checked; if (e.target.closest(".item.auto")) { autoUpdateURL = document.body.classList.toggle("autoUpdateURL"); if (autoUpdateURL) localStorage.setItem("autoUpdateURL", true); else localStorage.removeItem("autoUpdateURL"); urlOptions({ set: ["data", encodeJson(json)] }); } else if (e.target.closest(".item.reverse")) { reverse(reverseColumns); reverseColumns = !reverseColumns; toggleStored("reverseColumns"); } else if (e.target.closest(".item.noUser")) { if (options.avatar) document.querySelector("img.avatar").src = options.avatar; const noUser = document.body.classList.toggle("no-user"); if (autoParams) noUser ? urlOptions({ set: ["nouser", ""] }) : urlOptions({ remove: "nouser" }); toggleStored("noUser"); } else if (e.target.closest(".item.auto-params")) { if (input.checked) localStorage.setItem("autoParams", true); else localStorage.removeItem("autoParams"); autoParams = input.checked; } else if (e.target.closest(".toggles>.item")) { const win = input.closest(".item").classList[2]; if (input.checked) { document.body.classList.remove(`no-${win}`); localStorage.removeItem(`hide${win}`); } else { document.body.classList.add(`no-${win}`); localStorage.setItem(`hide${win}`, true); } } else if (e.target.closest(".item.multi") && !noMultiEmbedsOption) { multiEmbeds = !document.body.classList.toggle("single"); activeFields = document.querySelectorAll(".gui > .item.active"); if (autoParams) !multiEmbeds ? urlOptions({ set: ["single", ""] }) : urlOptions({ remove: "single" }); if (multiEmbeds) localStorage.setItem("multiEmbeds", true); else { localStorage.removeItem("multiEmbeds"); jsonObject.embeds = [jsonObject.embeds?.[0] || {}]; } buildGui(); buildEmbed(); editor.setValue(JSON.stringify(json, null, 4)); } e.target.closest(".top-btn")?.classList.toggle("active") }) document.querySelectorAll(".img").forEach(e => { if (e.nextElementSibling?.classList.contains("spinner-container")) e.addEventListener("error", el => { el.target.style.removeProperty("display"); el.target.nextElementSibling.style.display = "block"; }) }) let pickInGuiMode = false; togglePicker = pickLater => { colors.classList.toggle("display"); document.querySelector(".side1").classList.toggle("low"); if (pickLater) pickInGuiMode = true; }; document.querySelector(".pickerToggle").addEventListener("click", () => togglePicker()); buildEmbed(); document.body.addEventListener("click", e => { if (e.target.classList.contains("low") || (e.target.classList.contains("top") && colors.classList.contains("display"))) togglePicker(); }) // #0070ff, #5865f2 document.querySelector(".colors .hex>div")?.addEventListener("input", e => { let inputValue = e.target.value; if (inputValue.startsWith("#")) e.target.value = inputValue.slice(1), inputValue = e.target.value; if (inputValue.length !== 6 || !/^[a-zA-Z0-9]{6}$/g.test(inputValue)) return e.target.closest(".hex").classList.add("incorrect"); e.target.closest(".hex").classList.remove("incorrect"); const embedIndex = multiEmbeds && lastActiveGuiEmbedIndex !== -1 ? lastActiveGuiEmbedIndex : 0; jsonObject.embeds[embedIndex].color = parseInt(inputValue, 16); picker.fire?.("change", toRGB(inputValue)); buildEmbed(); }) if (onlyEmbed) document.querySelector(".side1")?.remove(); const menuMore = document.querySelector(".item.section .inner.more"); const menuSource = menuMore?.querySelector(".source"); if (!sourceOption) menuSource.remove(); if (menuMore.childElementCount < 2) menuMore?.classList.add("invisible"); if (menuMore.parentElement.childElementCount < 1) menuMore?.parentElement.classList.add("invisible"); document.querySelector(".top-btn.copy").addEventListener("click", e => { const mark = e.target.closest(".top-btn.copy").querySelector(".mark"), jsonData = JSON.stringify(json, null, 4), next = () => { mark?.classList.remove("hidden"); mark?.previousElementSibling?.classList.add("hidden"); setTimeout(() => { mark?.classList.add("hidden"); mark?.previousElementSibling?.classList.remove("hidden"); }, 1500); } if (!navigator.clipboard?.writeText(jsonData).then(next).catch(err => console.log("Could not copy to clipboard: " + err.message))) { const textarea = document.body.appendChild(document.createElement("textarea")); textarea.value = jsonData; textarea.select(); textarea.setSelectionRange(0, 50000); document.execCommand("copy"); document.body.removeChild(textarea); next(); } }); }); // Don"t assign to "jsonObject", assign to "json" instead. // "jsonObject" is used to store the final json object and used internally. // Below is the getter and setter for "json" which formats the value properly into and out of "jsonObject". Object.defineProperty(window, "json", { configurable: true, // Getter to format "jsonObject" properly depending on options and other factors // eg. using "embeds" or "embed" in output depending on "multiEmbeds" option. get() { const json = {}; if (jsonObject.content) json.content = jsonObject.content; // If "jsonObject.embeds" array is set and has content. Empty braces ({}) will be filtered as not content. if (jsonObject.embeds?.length) if (multiEmbeds) json.embeds = jsonObject.embeds.map(cleanEmbed); else json.embed = cleanEmbed(jsonObject.embeds[0]); return json; }, // Setter for "json" which formats the value properly into "jsonObject". set(val) { // Filter out items which are not objects and not empty objects. const embedObjects = val.embeds?.filter(j => j.constructor === Object && 0 in Object.keys(j)); // Convert "embed" to "embeds" and delete "embed" or validate and use "embeds" if provided. const embeds = val.embed ? [val.embed] : embedObjects?.length ? embedObjects : [] // Convert objects used as values to string and trim whitespace. const content = val.content?.toString().trim(); jsonObject = { ...(content && { content }), embeds: embeds.map(cleanEmbed), }; buildEmbed(); buildGui(); }, }); // Props used to validate embed properties. window.embedObjectsProps ??= { author: ["name", "url", "icon_url", ], thumbnail: ["url", "proxy_url", "height", "width", ], image: ["url", "proxy_url", "height", "width", ], fields: { items: ["name", "value", "inline", ], }, footer: ["text", "icon_url", ], } function cleanEmbed(obj, recursing = false) { if (!recursing) // Remove all invalid properties from embed object. for (const key in obj) if (!embedKeys.includes(key)) delete obj[key]; else if (obj[key].constructor === Object) // Value is an object. eg. "author" // Remove items that are not in the props of the current key. for (const item in obj[key]) !embedObjectsProps[key].includes(item) && delete obj[key][item]; else if (obj[key].constructor === Array) // Value is an array. eg. "fields" // Remove items that are not in the props of the current key. for (const item of obj[key]) for (const i in item) !embedObjectsProps[key].items.includes(i) && delete item[i]; // Remove empty properties from embed object. for (const [key, val] of Object.entries(obj)) if (val === undefined || val.trim?.() === "") // Remove the key if value is empty delete obj[key]; else if (val.constructor === Object) // Remove object (val) if it has no keys or recursively remove empty keys from objects. (!Object.keys(val).length && delete obj[key]) || (obj[key] = cleanEmbed(val, true)); else if (val.constructor === Array) // Remove array (val) if it has no keys or recursively remove empty keys from objects in array. !val.length && delete obj[key] || (obj[key] = val.map(k => cleanEmbed(k, true))); else // If object isn"t a string, boolean, number, array or object, convert it to string. if (!["string", "boolean", "number"].includes(typeof val)) obj[key] = val.toString(); return obj; }