embedbuilder/assets/js/script.js
Jonny_Bro (Nikita) 24bd56242f clean up
2023-06-27 12:52:27 +05:00

1689 lines
No EOL
62 KiB
JavaScript

/**
* 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(/(?<!data=[^=]+|=)=(&|$)/g, x => 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 <span class="inline full short">http://${makeShort(re[2], 30, 600).replace(" ", "")}</span>?`);
}
if (invalid) {
validationError = true;
return error(err);
}
return true;
}
const markup = (txt, {
replaceEmojis,
inlineBlock,
inEmbed
}) => {
if (replaceEmojis)
txt = txt.replace(/(?<!code(?: \w+=".+")?>[^>]+)(?<!\/[^\s"]+?):((?!\/)\w+):/g, (match, p) => p && emojis[p] ? emojis[p] : match);
txt = txt
/** Markdown */
.replace(/&#60;:\w+:(\d{17,19})&#62;/g, "<img class=\"emoji\" src=\"https://cdn.discordapp.com/emojis/$1.png\"/>")
.replace(/&#60;a:\w+:(\d{17,20})&#62;/g, "<img class=\"emoji\" src=\"https://cdn.discordapp.com/emojis/$1.gif\"/>")
.replace(/~~(.+?)~~/g, "<s>$1</s>")
.replace(/\*\*\*(.+?)\*\*\*/g, "<em><strong>$1</strong></em>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/__(.+?)__/g, "<u>$1</u>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/_(.+?)_/g, "<em>$1</em>")
// Replace >>> and > with block-quotes. &#62; is HTML code for >
.replace(/^(?: *&#62;&#62;&#62; ([\s\S]*))|(?:^ *&#62;(?!&#62;&#62;) +.+\n)+(?:^ *&#62;(?!&#62;&#62;) .+\n?)+|^(?: *&#62;(?!&#62;&#62;) ([^\n]*))(\n?)/mg, (all, match1, match2, newLine) => {
return `<div class="blockquote"><div class="blockquoteDivider"></div><blockquote>${match1 || match2 || newLine ? match1 || match2 : all.replace(/^ *&#62; /gm, "")}</blockquote></div>`;
})
/** Mentions */
.replace(/&#60;#\d+&#62;/g, () => `<span class="mention channel interactive">channel</span>`)
.replace(/&#60;@(?:&#38;|!)?\d+&#62;|@(?:everyone|here)/g, match => {
if (match.startsWith("@")) return `<span class="mention">${match}</span>`
else return `<span class="mention interactive">@${match.includes("&#38;") ? "role" : "user"}</span>`
})
if (inlineBlock)
// Treat both inline code and code blocks as inline code
txt = txt.replace(/`([^`]+?)`|``([^`]+?)``|```((?:\n|.)+?)```/g, (m, x, y, z) => x ? `<code class="inline">${x}</code>` : y ? `<code class="inline">${y}</code>` : z ? `<code class="inline">${z}</code>` : m);
else {
// Code block
txt = txt.replace(/```(?:([a-z0-9_+\-.]+?)\n)?\n*([^\n][^]*?)\n*```/ig, (m, w, x) => {
if (w) return `<pre><code class="${w}">${x.trim()}</code></pre>`
else return `<pre><code class="hljs nohighlight">${x.trim()}</code></pre>`
});
// Inline code
txt = txt.replace(/`([^`]+?)`|``([^`]+?)``/g, (m, x, y, z) => x ? `<code class="inline">${x}</code>` : y ? `<code class="inline">${y}</code>` : z ? `<code class="inline">${z}</code>` : m)
}
if (inEmbed)
txt = txt.replace(/\[([^\[\]]+)\]\((.+?)\)/g, `<a title="$1" target="_blank" class="anchor" href="$2">$1</a>`);
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 = `
<div class="embedField" style="grid-column: 1 / 13;">
<div class="embedFieldName">${markup(encodeHTML(f.name), { inEmbed: true, replaceEmojis: true, inlineBlock: true })}</div>
<div class="embedFieldValue">${markup(encodeHTML(f.value), { inEmbed: true, replaceEmojis: true })}</div>
</div>`;
else {
if (i && !fields[i - 1].inline) colNum = 1;
fieldElement.outerHTML = `
<div class="embedField ${num}${gridCol ? " colNum-2" : ""}" style="grid-column: ${gridCol || (colNum + " / " + (colNum + 4))};">
<div class="embedFieldName">${markup(encodeHTML(f.name), { inEmbed: true, replaceEmojis: true, inlineBlock: true })}</div>
<div class="embedFieldValue">${markup(encodeHTML(f.value), { inEmbed: true, replaceEmojis: true })}</div>
</div>`;
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 ? `: <span>${embed.title}</span>` : ""}`;
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 ? `: <span>${value}</span>` : ""}`;
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 ? "<a class=\"anchor\" target=\"_blank\" href=\"\" + encodeHTML(url(embedObj.url)) + \"\">" + encodeHTML(embedObj.title) + "</a>" : 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 ? "<img class=\"embedAuthorIcon embedAuthorLink\" src=\"\" + encodeHTML(url(embedObj.author.icon_url)) + \"\">" : ""}
${embedObj.author.url ? "<a class=\"embedAuthorNameLink embedLink embedAuthorName\" href=\"\" + encodeHTML(url(embedObj.author.url)) + \"\" target=\"_blank\">" + encodeHTML(embedObj.author.name) + "</a>" : "<span class=\"embedAuthorName\">" + encodeHTML(embedObj.author.name) + "</span>"}`, "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 ? "<img class=\"embedFooterIcon embedFooterLink\" src=\"\" + encodeHTML(url(embedObj.footer.icon_url)) + \"\">" : ""}<span class="embedFooterText">
${encodeHTML(embedObj.footer.text)}
${embedObj.timestamp ? "<span class=\"embedFooterSeparator\">•</span>" + encodeHTML(timestamp(embedObj.timestamp)) : ""}</span></div>`, "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 ? "<a class=\"anchor\" target=\"_blank\" href=\"\" + encodeHTML(url(embedObj.url)) + \"\">" + encodeHTML(embedObj.title) + "</a>" : 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 ? "<img class=\"embedAuthorIcon embedAuthorLink\" src=\"\" + encodeHTML(url(embedObj.author.icon_url)) + \"\">" : ""}
${embedObj.author.url ? "<a class=\"embedAuthorNameLink embedLink embedAuthorName\" href=\"\" + encodeHTML(url(embedObj.author.url)) + \"\" target=\"_blank\">" + encodeHTML(embedObj.author.name) + "</a>" : "<span class=\"embedAuthorName\">" + encodeHTML(embedObj.author.name) + "</span>"}`, "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 ? "<img class=\"embedFooterIcon embedFooterLink\" src=\"\" + encodeHTML(url(embedObj.footer.icon_url)) + \"\">" : ""}<span class="embedFooterText">
${encodeHTML(embedObj.footer.text)}
${embedObj.timestamp ? "<span class=\"embedFooterSeparator\">•</span>" + encodeHTML(timestamp(embedObj.timestamp)) : ""}</span></div>`, "flex");
else if (embedObj.timestamp) display(embedFooter, `<span class=\"embedFooterText">${encodeHTML(timestamp(embedObj.timestamp))}</span></div>`, "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(/(?<!data=[^=]+|=)=(&|$)/g, x => 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;
}