Interactive Command based menu manager, simplify the UX process for any djs bot!
Package Exports
@th3ward3n/djs-menu
Readme
Discord.js Menu Manager Addon
This package comes with some useful development tools
List of helpful things here!
TODO
Getting Started
npminstall @th3ward3n/djs-menu
Example spawnCollector()
const exampleDisplay ={
embeds:[newEmbedBuilder({
title:"This is a simple User Choice Embed",
description:"Confirm or Cancel?"})],
components:[spawnUserChoiceRow("Example Text")]};// Example option object, specifying expire timer and collectors to returnconst exampleOptions: ComponentCollectorOptionBase ={
timeLimit:180_000,
sendAs:"Reply",
collectors:{
type:"Button",}};// Collector Spawning Response object/**
* @example Using Destructing
* ```ts
* const { anchorMsg, buttons, strings } = await spawnCollector(i, exampleDisplay, exampleOptions);
* ```
*/const packedResponse =awaitspawnCollector(interaction, exampleDisplay, exampleOptions);// Event Fires for any button interactions that pass filtering// Custom Filters can be passed to the spawner using `collectors: { filterWith: () => boolean }`
packedResponse.buttons.on('collect',(collected)=>{
collected.deferUpdate().then(async()=>{await collected.followUp({
content:`Button Collected with customId: ${collected.customId}`,
flags: MessageFlags.Ephemeral
});}).catch(console.error);});// Event fires on the collector ending, if given a reason through `buttons.stop("reason")` it will fill the `reason` argument on "end"// If the collector ends due to the given timeLimit (default 60_000ms or 60 seconds) the "end" event will fire with `reason: "time"`
packedResponse.buttons.on('end',(collected, reason)=>{if(!reason || reason ==='time')handleCatchDelete(packedResponse.anchorMsg);console.log('Collected Components: ', collected);console.log('Ended with reason: ', reason);});
Example NumberBlockManager()
// Example user object, demonstrating limitersconst user ={ id:"123456789", coins:500};const selectedTotalEmbed =newEmbedBuilder({
title:"Select an Amount",
description:`Amount Selected: 0`});const amountBlock =newNumberBlockManager();const exampleDisplay ={
embeds:[selectedTotalEmbed],
components: amountBlock.rows
};const{
anchorMsg,
buttons
}=awaitspawnCollector(interaction, exampleDisplay, options);
buttons.on("collect",async(c)=>{// Evaluate the collected id
amountBlock.evaluate(c.customId);// This could be any possible condition of your own design!!!if(amountBlock.total > user.coins){
amountBlock.total = user.coins;// Provide feedback for why selected total stops at `500` given exampleawait c.reply({
content:`You cannot increase x more, as it would exceed your total coins ${user.coins}`,
flags: MessageFlags.Ephemeral
});}// Example usage, updating an embed display with value total stored
selectedTotalEmbed.setDescription(`Amount Selected: ${amountBlock.total}`);// For this example, editing the message using the initial display object// This will update the embed message, displaying the changes made to the existing description.await anchorMsg.edit(exampleDisplay);});
buttons.on('end',(_, r)=>{if(!reason || reason ==='time')handleCatchDelete(anchorMsg);});
Example Paginator()
const examplePageData: PagerDataOptionBase ={
embeds:Array(25).fill(0).map<EmbedBuilder>((_, idx)=>newEmbedBuilder({
title:`Page #${idx +1}`,
description:"This is a page, it is one of many"}),),};// Example option object, specifying expire timer and collectors to returnconst exampleOptions: ComponentCollectorOptionBase ={
timeLimit:180_000,
sendAs:"Reply",
collectors:{
type:"Button",}};const pager =newPaginator(examplePageData);const{
anchorMsg,
buttons,}=awaitspawnCollector(i, pager.page, exampleOptions);
buttons.on('collect',(collected)=>{
collected.deferUpdate().then(async()=>{await anchorMsg.edit(pager.changePage(
collected.customId.split('-')[0]));await collected.followUp({
content:`Button Collected with customId: ${collected.customId}`,
flags: MessageFlags.Ephemeral
});}).catch(console.error);});
buttons.on('end',(_, reason)=>{if(!reason || reason ==='time')handleCatchDelete(anchorMsg);});
Example MenuManager() And why its so useful!
const sharedBackRow =spawnBackButtonRow();const frameSize =25;const exampleFrameData: MenuDataContentBase[]=Array(frameSize).fill(0).map<MenuDataContentBase>((_, idx)=>({
embeds:[newEmbedBuilder({
title:`Frame #${idx +1}`,
description:"This is a frame in a menu, it is one of many!"}),],
components:(idx === frameSize -1)?[sharedBackRow]:[newActionRowBuilder<ButtonBuilder>().addComponents(newButtonBuilder({
custom_id:`frame-${idx}-main`,
style: ButtonStyle.Primary,
label:"Do something!"}),newButtonBuilder({
custom_id:`frame-${idx}-alt`,
style: ButtonStyle.Secondary,
label:"Do Something else!"})).toJSON(),
sharedBackRow
],}),);const exampleMenuOptions: MenuManagerOptionBase ={
contents: exampleFrameData[0],
sendAs:"Reply",
timeLimit:300_000};const menu =await MenuManager.createAnchor(interaction, exampleMenuOptions);
menu.buttons.on('collect',(c)=>{
c.deferUpdate().then(async()=>{switch(menu.analyzeAction(c.customId)){case"PAGE":// Not in use for this example!break;case"NEXT":// Move forward one context frameawait menu.frameForward(exampleFrameData[menu.position]);break;case"BACK":case"CANCEL":// Move backwards one context frameawait menu.frameBackward();break;case"UNKNOWN":// Unknown action, refresh current frame!await menu.frameRefresh();break;}await c.followUp({
content:`Collected Button: ${c.customId}`,
flags: MessageFlags.Ephemeral
});}).catch(console.error);});
menu.buttons.on('end',(_, r)=>{if(!r || r ==='time')return menu.destroy();});
Slightly Advanced MenuManager Usage
const sharedBackRow =spawnBackButtonRow();/**
* Main Menu (Frame 0 / Initial Message)
*/const exampleMainMenuDisplay =newEmbedBuilder({
title:"== Select a Help Catagory ==",
description:"> `Fun`\n> `Utility`\n> `Other`"});const exampleMainMenuControls =newActionRowBuilder<ButtonBuilder>().addComponents(newButtonBuilder({
custom_id:"fun",
style: ButtonStyle.Secondary,
label:"Fun Commands"}),newButtonBuilder({
custom_id:"utility",
style: ButtonStyle.Secondary,
label:"Utility Commands"}),newButtonBuilder({
custom_id:"other",
style: ButtonStyle.Secondary,
label:"Other Commands"}),).toJSON();/**
* Injected Placeholder Frame
*
* This method of using placeholders is not desired, it is however currently required.
* Work is being done to solve this concept in the base @th3ward3n/djs-menu package
*/const exampleEmptyDisplay =newEmbedBuilder({
title:"== This will never be seen =="});/**
* Sub Menus (Frame 1 / Injected Per Selected Catagory)
*/const exampleCommandNames ={
fun:["cute-animal","meme","urban-dictonary"],
utility:["command-use-stats","help","profile"],
other:["ping","info"]};constloadHelpCommandPages=(names:string[])=>{return{
embeds:Array(names.length).fill(0).map<EmbedBuilder>((_, idx)=>newEmbedBuilder({
title:`= How to use ${names[idx]} =`,
description:"This is an example help page for a command!"}),),};};const exampleFunCommandPages =loadHelpCommandPages(exampleCommandNames.fun);const exampleUtilityCommandPages =loadHelpCommandPages(exampleCommandNames.utility);const exampleOtherCommandPages =loadHelpCommandPages(exampleCommandNames.other);// Staticly Developer Defined Frame Structure// This is where you can store and manipulate menu "paths" to suit your specific needs// In this example, each "path" follows the same structure, therefore no advanced pathways are usedconst exampleFrameData: MenuDataContentBase[]=[{
embeds:[exampleMainMenuDisplay],
components:[exampleMainMenuControls]},{
embeds:[exampleEmptyDisplay],
components:[sharedBackRow]}];const exampleMenuOptions: MenuManagerOptionBase ={
contents: exampleFrameData[0],
sendAs:"Reply",
timeLimit:300_000};const menu =await MenuManager.createAnchor(interaction, exampleMenuOptions);// Attach internal Paginators using unique ids/**
* @note Given `id`s should be able to exactly match a button/stringSelect `custom_id`
* @example
* ```ts
* menu.spawnPageContainer(pageData, "uniqueid");
*
* // INCORRECT
* const WRONG_ExampleButton = new ButtonBuilder()
* .setCustomId("action-something-uniqueid");
* const WRONG_ExampleButtonTwo = new ButtonBuilder()
* .setCustomId("action-uniqueid-something");
*
* // CORRECT!!
* const CORRECT_ExampleButton = new ButtonBuilder()
* .setCustomId("uniqueid-action-something");
* ```
*
* Refer to `exampleMainMenuControls` for `custom_id` associations
*/
menu.spawnPageContainer(exampleFunCommandPages,"fun");
menu.spawnPageContainer(exampleUtilityCommandPages,"utility");
menu.spawnPageContainer(exampleOtherCommandPages,"other");// Note - Paginator data is persistant, each Paginator will maintain `currentPage` throughout a `MenuManager`s lifetime
menu.buttons?.on('collect',(c)=>{
c.deferUpdate().then(async()=>{switch(menu.analyzeAction(c.customId)){case"PAGE":// Handle paging internallyawait menu.framePageChange(c.customId);break;case"NEXT":// In this example, any button pressed on the first frame will require a paginator injection// Here we are loading the placeholder frame embeds, and specifing the paging `id` to inject with// Refer to the example shown above the paginator attachment step.if(menu.position ===1){await menu.frameForward(
exampleFrameData[menu.position],{ usePager: c.customId.split('-')[0]});}break;case"BACK":case"CANCEL":await menu.frameBackward();break;case"UNKNOWN":await menu.frameRefresh();break;}await c.followUp({
content:`Collected Button: ${c.customId}`,
flags: MessageFlags.Ephemeral
});}).catch(console.error);});
menu.buttons?.on('end',(_, r)=>{if(!r || r ==='time') menu.destroy();});