/**
* 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, "")}
${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 = `
`;
else {
if (i && !fields[i - 1].inline) colNum = 1;
fieldElement.outerHTML = `
`;
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 ? "