├── .gitignore ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── lib └── PaginationEmbed.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .DS_Store 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | .env.test 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # vuepress build output 72 | .vuepress/dist 73 | 74 | # Serverless directories 75 | .serverless/ 76 | 77 | # FuseBox cache 78 | .fusebox/ 79 | 80 | # DynamoDB Local files 81 | .dynamodb/ 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Riya (riyacchi) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eris Pagination 2 | Eris Pagination is a super simple to use **Embed Paginator** for the Node.js Discord library [Eris.](https://github.com/abalabahaha/eris) 3 | 4 | The API is *very* straight forward but also offers a big amount of customizability as well! 5 | 6 | # Getting Started 7 | Simply install Eris Pagination via NPM by typing `npm install eris-pagination` into your existing project and require the module wherever you need. That's it! 8 | 9 | # API 10 | There's only a single **asynchronous** method needed for creating paginated Embeds with Eris: 11 | ```js 12 | EmbedPaginator.createPaginationEmbed(message, embeds, options); 13 | ``` 14 | **Returns:** `Promise` - The `createPaginationEmbed` method will return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which resolves with an [Eris.Message](https://abal.moe/Eris/docs/Message) object of the paginated Embed. (E.g. to manually tinker with the paginator message). 15 | 16 | **Parameters:** 17 | - **Eris.Message** `message` - An Eris message emitted from the `messageCreate` event. 18 | - **Object[]** `embeds` - An array containing all Embed objects you want to use with the paginator. 19 | - **Object** `options` - An object containing optional settings to overwrite the default behavior of Eris Paginator. 20 | - **Boolean** `options.showPageNumbers` - Whether to show *"Page **n** of **m**"* over the Embed. 21 | - Optional: **Yes** 22 | - Default: **True** 23 | - **Boolean** `options.extendedButtons` - Whether to show advanced pagination buttons (Jump to First & Last page) 24 | - Optional: **Yes** 25 | - Default: **False** 26 | - **Boolean** `options.cycling` - Cycle through all embeds jumping from the first page to the last page on going back and from the last page to the first page going forth. 27 | - Optional: **Yes** 28 | - Default: **False** 29 | - **Number** `options.startPage` - Which page (element) of the provided array to show first initially. 30 | - Optional: **Yes** 31 | - Default: **1** 32 | - **Number** `options.maxMatches` - Maximum amount of reaction button clicks to listen for. 33 | - Optional: **Yes** 34 | - Default: **50** 35 | - Maximum: **100** 36 | - **Number** `options.timeout` - Duration for how long the pagination embed should work for. 37 | - Optional: **Yes** 38 | - Default: **300000** *(5 minutes)* 39 | - Maximum: **900000** *(15 minutes)* 40 | - **String** `options.firstButton` - Emoji used as the first page button. **Must be Unicode!** 41 | - Optional: **Yes** 42 | - Default: **⏮** 43 | - **String** `options.backButton` - Emoji used as the back button. **Must be Unicode!** 44 | - Optional: **Yes** 45 | - Default: **⬅** 46 | - **String** `options.forthButton` - Emoji used as the forth button. **Must be Unicode!** 47 | - Optional: **Yes** 48 | - Default: **➡** 49 | - **String** `options.lastButton` - Emoji used as the last page button. **Must be Unicode!** 50 | - Optional: **Yes** 51 | - Default: **⏭** 52 | - **String** `options.deleteButton` - Emoji used as the delete button. **Must be Unicode!** 53 | - Optional: **Yes** 54 | - Default: **🗑** 55 | 56 | **Notice:** The Delete button does *not* delete the whole embed. It removes every reaction and resolves with an empty Promise! 57 | 58 | # Examples 59 | **Simple paginator without additional options:** 60 | ```js 61 | const Eris = require('eris'); 62 | const EmbedPaginator = require('eris-pagination'); 63 | const bot = new Eris('BOT_TOKEN'); 64 | 65 | bot.on('ready', () => { 66 | console.log('Ready!'); 67 | }); 68 | 69 | bot.on('messageCreate', async (message) => { 70 | if (message.content === '!test') { 71 | const myEmbeds = [ 72 | { title: 'Test Embed 1', color: 0x2ECC71 }, 73 | { title: 'Test Embed 2', color: 0xE67E22 }, 74 | { title: 'Test Embed 3', color: 0xE74C3C } 75 | ]; 76 | 77 | const paginatedEmbed = await EmbedPaginator.createPaginationEmbed(message, myEmbeds); 78 | /* paginatedEmbed ⇨ Eris.Message */ 79 | } 80 | }); 81 | 82 | bot.connect(); 83 | ``` 84 |
85 | 86 | ![](https://img.kirameki.one/qlrgKF98.gif) 87 | 88 |
89 | 90 |
91 | 92 | **Advanced paginator with overwriting options:** 93 | ```js 94 | const Eris = require('eris'); 95 | const EmbedPaginator = require('eris-pagination'); 96 | const bot = new Eris('BOT_TOKEN'); 97 | 98 | bot.on('ready', () => { 99 | console.log('Ready!'); 100 | }); 101 | 102 | bot.on('messageCreate', async (message) => { 103 | if (message.content === '!test') { 104 | const myEmbeds = [ 105 | { title: 'Test Embed 1', color: 0x2ECC71 }, 106 | { title: 'Test Embed 2', color: 0xE67E22 }, 107 | { title: 'Test Embed 3', color: 0xE74C3C } 108 | ]; 109 | 110 | const paginatedEmbed = await EmbedPaginator.createPaginationEmbed( 111 | message, 112 | myEmbeds, 113 | { 114 | showPageNumbers: false, 115 | extendedButtons: true, 116 | maxMatches: 10, 117 | timeout: 150000, 118 | backButton: '◀', 119 | forthButton: '▶', 120 | deleteButton: '💩', 121 | startPage: 2 122 | } 123 | ); 124 | /* paginatedEmbed ⇨ Eris.Message */ 125 | } 126 | }); 127 | 128 | bot.connect(); 129 | ``` 130 |
131 | 132 | ![](https://img.kirameki.one/lOJVBJ5q.png) 133 | 134 |
135 | 136 | # License 137 | This repository makes use of the [MIT License](https://opensource.org/licenses/MIT) and all of its correlating traits. 138 | 139 | While it isn't mandatory, a small credit if this repository was to be reused would be highly appreciated! 140 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Message, EmbedOptions } from 'eris'; 2 | 3 | declare module 'eris-pagination' { 4 | 5 | /** 6 | * An optional options object for overwriting defaults 7 | */ 8 | interface PaginationOptions { 9 | /** Whether or not to show the current page index over the embed. Defaults to: true */ 10 | showPageNumbers?: boolean; 11 | /** Whether or not to show extended control buttons besides standard pagination (First & Last page, deleting) */ 12 | extendedButtons?: boolean; 13 | /** Cycle through all embeds jumping from the first page to the last page on going back and from the last page to the first page going forth. Defaults to: false */ 14 | cycling?: boolean; 15 | /** How often the reaction handler should listen for a reaction (How often the paginator can be used). Defaults to: 50. Maximum: 100 */ 16 | maxMatches?: number; 17 | /** How long the paginator should work before the reaction listener times out. Defaults to: 300000ms (5 minutes). Maximum: 900000ms (15 minutes) */ 18 | timeout?: number; 19 | /** Emoji which should be used as the delete button. This MUST be a unicode emoji! Defaults to: 🗑 */ 20 | deleteButton?: string; 21 | /** Emoji which should be used as the first page button. This MUST be a unicode emoji! Defaults to: ⏮ */ 22 | firstButton?: string; 23 | /** Emoji which should be used as the last page button. This MUST be a unicode emoji! Defaults to: ⏭ */ 24 | lastButton?: string; 25 | /** Emoji which should be used as the back button. This MUST be a unicode emoji! Defaults to: ⬅ */ 26 | backButton?: string; 27 | /** Emoji which should be used as the forth button. This MUST be a unicode emoji! Defaults to: ➡ */ 28 | forthButton?: string; 29 | /** Which page of the submitted embed array should be shown first. Defaults to: 1 (The 1st page / element in the array) */ 30 | startPage?: number; 31 | } 32 | 33 | /** 34 | * Create an Embed Paginator 35 | * @param message A message object emitted from a messageCreate event coming from Eris, used as an invoker. If sent by the client, the message will be edited. 36 | * @param pages An array containing all embed objects 37 | * @param options An optional options object for overwriting defaults 38 | */ 39 | function createPaginationEmbed(message: Message, pages: EmbedOptions[], options?: PaginationOptions): Promise; 40 | 41 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/PaginationEmbed'); -------------------------------------------------------------------------------- /lib/PaginationEmbed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ReactionHandler = require('eris-reactions'); 4 | 5 | /** 6 | * @typedef {import('eris').EmbedBase} EmbedBase 7 | * @typedef {import('eris').Message} Message 8 | * @typedef {Object} PaginationOptions An optional options object for overwriting defaults 9 | * @property {Boolean} [showPageNumbers] Whether or not to show the current page index over the embed. Defaults to: true 10 | * @property {Boolean} [extendedButtons] Whether or not to show extended control buttons besides standard pagination (First & Last page, deleting) 11 | * @property {Boolean} [cycling] Cycle through all embeds jumping from the first page to the last page on going back and from the last page to the first page going forth. Defaults to: false 12 | * @property {Number} [maxMatches] How often the reaction handler should listen for a reaction (How often the paginator can be used). Defaults to: 50. Maximum: 100 13 | * @property {Number} [timeout] How long the paginator should work before the reaction listener times out. Defaults to: 300000ms (5 minutes). Maximum: 900000ms (15 minutes) 14 | * @property {String} [deleteButton] Emoji which should be used as the delete button. This MUST be a unicode emoji! Defaults to: 🗑 15 | * @property {String} [firstButton] Emoji which should be used as the first page button. This MUST be a unicode emoji! Defaults to: ⏮ 16 | * @property {String} [lastButton] Emoji which should be used as the last page button. This MUST be a unicode emoji! Defaults to: ⏭ 17 | * @property {String} [backButton] Emoji which should be used as the back button. This MUST be a unicode emoji! Defaults to: ⬅ 18 | * @property {String} [forthButton] Emoji which should be used as the forth button. This MUST be a unicode emoji! Defaults to: ➡ 19 | * @property {Number} [startPage] Which page of the submitted embed array should be shown first. Defaults to: 1 (The 1st page / element in the array) 20 | */ 21 | /** 22 | * Embed Pagination class 23 | * @class PaginationEmbed 24 | * @classdesc Handles the creation, listening and updating of paginated Rich Embeds 25 | */ 26 | class PaginationEmbed { 27 | /** 28 | * Constructor for the Embed Paginator 29 | * @param {Message} message A message object emitted from a messageCreate event coming from Eris, used as an invoker. If sent by the client, the message will be edited. 30 | * @param {EmbedBase[]} pages An array containing all embed objects 31 | * @param {PaginationOptions} [options] An optional options object for overwriting defaults 32 | */ 33 | constructor(message, pages = [], options = {}) { 34 | this.pages = pages; 35 | this.invoker = message; 36 | this.options = options; 37 | this.delete = options.deleteButton || '🗑'; 38 | this.firstPage = options.firstButton || '⏮'; 39 | this.lastPage = options.lastButton || '⏭'; 40 | this.back = options.backButton || '⬅'; 41 | this.forth = options.forthButton || '➡'; 42 | this.page = options.startPage || 1; 43 | this.maxMatches = options.maxMatches || 50; 44 | this.timeout = options.timeout || 300000; 45 | this.cycling = options.cycling || false 46 | this.showPages = (typeof options.showPageNumbers !== 'undefined') ? options.showPageNumbers : true; 47 | this.advanced = (typeof options.extendedButtons !== 'undefined') ? options.extendedButtons : false; 48 | } 49 | 50 | /** 51 | * Runs a set of initialization checks, sets up the reaction listener for continuous listening and displays the initial Embed 52 | * @async 53 | */ 54 | async initialize() { 55 | if (this.pages.length < 2) { 56 | return Promise.reject(new Error('A Pagination Embed must contain at least 2 pages!')); 57 | } 58 | 59 | if (this.page < 1 || this.page > this.pages.length) { 60 | return Promise.reject(new Error(`Invalid start page! Must be between 1 (first) and ${this.pages.length} (last)`)); 61 | } 62 | 63 | if (this.maxMatches > 100) { 64 | return Promise.reject(new Error('Maximum amount of page changes exceeded! Must be under 100!')); 65 | } 66 | 67 | if (this.timeout > 900000) { 68 | return Promise.reject(new Error('Embed Timeout too high! Maximum pagination lifespan allowed is 15 minutes (900000 ms)!')); 69 | } 70 | 71 | const messageContent = { 72 | content: (this.showPages) ? `Page **${this.page}** of **${this.pages.length}**` : undefined, 73 | embed: this.pages[this.page - 1] 74 | } 75 | 76 | if (this.invoker.author.id === this.invoker._client.user.id) { 77 | this.message = await this.invoker.edit(messageContent); 78 | } else { 79 | this.message = await this.invoker.channel.createMessage(messageContent) 80 | } 81 | 82 | this.handler = new ReactionHandler.continuousReactionStream(this.message, (userID) => userID === this.invoker.author.id, false, { maxMatches: this.maxMatches, time: this.timeout }); 83 | 84 | if (this.advanced) { 85 | await this.message.addReaction(this.firstPage); 86 | await this.message.addReaction(this.back); 87 | await this.message.addReaction(this.forth); 88 | await this.message.addReaction(this.lastPage); 89 | await this.message.addReaction(this.delete); 90 | } else { 91 | await this.message.addReaction(this.back); 92 | await this.message.addReaction(this.forth); 93 | } 94 | } 95 | 96 | /** 97 | * Updates the embed's content with the new page 98 | */ 99 | update() { 100 | this.message.edit({ 101 | content: (this.showPages) ? `Page **${this.page}** of **${this.pages.length}**` : undefined, 102 | embed: this.pages[this.page - 1] 103 | }); 104 | } 105 | 106 | /** 107 | * Check if the client can remove the reaction 108 | * @returns {Boolean} 109 | */ 110 | checkPerms() { 111 | return this.message.channel.guild && this.message.channel.permissionsOf(this.message._client.user.id).has('manageMessages'); 112 | } 113 | 114 | /** 115 | * Main method handling the reaction listening and content updating 116 | */ 117 | run() { 118 | this.handler.on('reacted', async (event) => { 119 | switch (event.emoji.name) { 120 | case this.firstPage: { 121 | if (this.advanced) { 122 | if (this.checkPerms()) { 123 | await this.message.removeReaction(this.firstPage, this.invoker.author.id); 124 | } 125 | 126 | if (this.page > 1) { 127 | this.page = 1; 128 | this.update(); 129 | } 130 | 131 | break; 132 | } 133 | } 134 | 135 | case this.back: { 136 | if (this.checkPerms()) { 137 | await this.message.removeReaction(this.back, this.invoker.author.id); 138 | } 139 | 140 | if (this.page > 1) { 141 | this.page--; 142 | this.update(); 143 | } else if (this.page === 1 && this.cycling === true) { 144 | this.page = this.pages.length; 145 | this.update(); 146 | } 147 | 148 | break; 149 | } 150 | 151 | case this.forth: { 152 | if (this.checkPerms()) { 153 | await this.message.removeReaction(this.forth, this.invoker.author.id); 154 | } 155 | 156 | if (this.page < this.pages.length) { 157 | this.page++; 158 | this.update(); 159 | } else if (this.page === this.pages.length && this.cycling === true) { 160 | this.page = 1; 161 | this.update(); 162 | } 163 | 164 | break; 165 | } 166 | 167 | case this.lastPage: { 168 | if (this.advanced) { 169 | if (this.checkPerms()) { 170 | await this.message.removeReaction(this.lastPage, this.invoker.author.id); 171 | } 172 | 173 | if (this.page < this.pages.length) { 174 | this.page = this.pages.length; 175 | this.update(); 176 | } 177 | 178 | break; 179 | } 180 | 181 | break; 182 | } 183 | 184 | case this.delete: { 185 | if (this.advanced) { 186 | return new Promise((resolve, reject) => { 187 | if (this.checkPerms()) { 188 | this.message.removeReactions().then(() => { 189 | resolve(); 190 | }); 191 | } else { 192 | reject(new Error('Insufficient permissions to remove reactions')) 193 | } 194 | }); 195 | } 196 | 197 | break; 198 | } 199 | } 200 | }); 201 | } 202 | } 203 | 204 | module.exports = { 205 | /** 206 | * Create an Embed Paginator 207 | * 208 | * @param {Message} message A message object emitted from a messageCreate event coming from Eris, used as an invoker. If sent by the client, the message will be edited. 209 | * @param {EmbedBase[]} pages An array containing all embed objects 210 | * @param {PaginationOptions} [options] An optional options object for overwriting defaults 211 | */ 212 | createPaginationEmbed: async (message, pages, options) => { 213 | const paginationEmbed = new PaginationEmbed(message, pages, options); 214 | await paginationEmbed.initialize(); 215 | paginationEmbed.run(); 216 | 217 | return Promise.resolve(paginationEmbed.message); 218 | } 219 | }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eris-pagination", 3 | "version": "0.5.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "eris-reactions": { 8 | "version": "0.1.4", 9 | "resolved": "https://registry.npmjs.org/eris-reactions/-/eris-reactions-0.1.4.tgz", 10 | "integrity": "sha512-U2rMtfywnhh4TmqFcF7uo/ciyQN8vb9ptwSJMQs/uaY1a401f+t3sei1R3IR2gYqaFejJzgjTmzBMT6jIBPYYQ==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eris-pagination", 3 | "version": "0.5.2", 4 | "description": "An extremely easy to use Embed Paginator for the Eris Discord Library", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/riyacchi/eris-pagination.git" 12 | }, 13 | "keywords": [ 14 | "eris", 15 | "pagination", 16 | "embed", 17 | "paginator", 18 | "embed-pagination", 19 | "discord" 20 | ], 21 | "author": "Riya", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/riyacchi/eris-pagination/issues" 25 | }, 26 | "homepage": "https://github.com/riyacchi/eris-pagination#readme", 27 | "dependencies": { 28 | "eris-reactions": "^0.1.4" 29 | }, 30 | "peerDependencies": { 31 | "eris": "^0.13.1" 32 | } 33 | } 34 | --------------------------------------------------------------------------------