1689 lines
No EOL
62 KiB
JavaScript
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(/<:\w+:(\d{17,19})>/g, "<img class=\"emoji\" src=\"https://cdn.discordapp.com/emojis/$1.png\"/>")
|
|
.replace(/<a:\w+:(\d{17,20})>/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. > is HTML code for >
|
|
.replace(/^(?: *>>> ([\s\S]*))|(?:^ *>(?!>>) +.+\n)+(?:^ *>(?!>>) .+\n?)+|^(?: *>(?!>>) ([^\n]*))(\n?)/mg, (all, match1, match2, newLine) => {
|
|
return `<div class="blockquote"><div class="blockquoteDivider"></div><blockquote>${match1 || match2 || newLine ? match1 || match2 : all.replace(/^ *> /gm, "")}</blockquote></div>`;
|
|
})
|
|
|
|
/** Mentions */
|
|
.replace(/<#\d+>/g, () => `<span class="mention channel interactive">channel</span>`)
|
|
.replace(/<@(?:&|!)?\d+>|@(?:everyone|here)/g, match => {
|
|
if (match.startsWith("@")) return `<span class="mention">${match}</span>`
|
|
else return `<span class="mention interactive">@${match.includes("&") ? "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;
|
|
} |