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