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