├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .nodemon.json ├── .npmignore ├── .npmrc ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── Gulpfile.js ├── LICENSE ├── README.md ├── UPDATING_V1.md ├── UPDATING_V2.md ├── bin ├── Embeds.js ├── FieldsEmbed.js ├── base │ └── index.js └── index.js ├── demo ├── Embeds.gif ├── FieldsEmbed.png ├── FieldsEmbed2.gif └── Types.png ├── deploy └── travis.sh ├── package.json ├── src ├── .eslintrc ├── Embeds.ts ├── FieldsEmbed.ts ├── base │ └── index.ts └── index.ts ├── test ├── bot.js └── credentials.sample.js ├── tsconfig.json ├── typedoc.json └── typings ├── Embeds.d.ts ├── FieldsEmbed.d.ts ├── base └── index.d.ts └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # EditorConfig is awesome: https://EditorConfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | charset = utf-8 11 | 12 | [*.{js, ts, json, yml}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.md] 17 | indent_style = space 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | typings/* 3 | Gulpfile.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2018, 9 | "sourceType": "module" 10 | }, 11 | "extends": [ "eslint:recommended" ], 12 | "rules": { 13 | "array-bracket-spacing": [ "error", "always" ], 14 | "camelcase": "error", 15 | "comma-dangle": [ 16 | "error", 17 | { 18 | "arrays": "always-multiline", 19 | "exports": "never", 20 | "functions": "never", 21 | "imports": "never", 22 | "objects": "never" 23 | } 24 | ], 25 | "comma-spacing": [ "error", { "after": true, "before": false } ], 26 | "curly": [ "error", "multi" ], 27 | "eqeqeq": [ "error", "smart" ], 28 | "indent": [ "error", 2, { "SwitchCase": 1 } ], 29 | "line-comment-position": [ "error", { "position": "above" } ], 30 | "linebreak-style": [ "error", "unix" ], 31 | "lines-between-class-members": [ "error", "always" ], 32 | "newline-before-return": "error", 33 | "no-duplicate-imports": "error", 34 | "no-eval": "error", 35 | "no-irregular-whitespace": "error", 36 | "no-multi-spaces": "error", 37 | "no-template-curly-in-string": "error", 38 | "no-trailing-spaces": "error", 39 | "no-unsafe-finally": "error", 40 | "no-unused-expressions": "error", 41 | "no-var": "error", 42 | "object-curly-newline": [ "error", { "multiline": true } ], 43 | "object-shorthand": "warn", 44 | "prefer-template": "error", 45 | "quotes": [ "error", "single", { "allowTemplateLiterals": true, "avoidEscape": true } ], 46 | "radix": [ "error", "as-needed" ], 47 | "require-jsdoc": "error", 48 | "semi": [ "error", "always" ], 49 | "sort-keys": "off", 50 | "space-before-blocks": "error", 51 | "space-before-function-paren": "error", 52 | "spaced-comment": [ "error", "always" ], 53 | "max-len": [ "error", { "code": 120 } ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.png binary 4 | *.gif binary 5 | 6 | bin linguist-vendored 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM and Yarn 2 | node_modules/ 3 | yarn-error.log 4 | yarn.lock 5 | 6 | # Tests 7 | *.tsbuildinfo 8 | test/credentials.js 9 | docs 10 | 11 | # Node 12 | report.* 13 | -------------------------------------------------------------------------------- /.nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "watch": [ "test" ], 4 | "env": { "NODE_ENV": "development" }, 5 | "ext": "js" 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | demo 4 | deploy 5 | docs 6 | src 7 | test 8 | 9 | .gitattributes 10 | .gitignore 11 | .nodemon.json 12 | .travis.yml 13 | .eslint* 14 | .editorconfig 15 | Gulpfile.js 16 | report.* 17 | tsconfig.json 18 | typedoc.json 19 | UPDATING_V*.md 20 | yarn.lock 21 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | install: yarn 5 | before_script: 6 | - yarn add discord.js@12 --peer 7 | jobs: 8 | include: 9 | - stage: deploy 10 | script: bash ./deploy/travis.sh 11 | cache: 12 | directories: 13 | - node_modules 14 | sudo: false 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.autoDetect": "on", 3 | 4 | "typescript.tsdk": "node_modules/typescript/lib", 5 | 6 | "eslint.validate": [ "typescript", "javascript" ] 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [2.1.0] - 2020-07-22 9 | 10 | ### Added 11 | - `Embeds` mode: 12 | - Added `toJSON` method. 13 | - `setArray` method now accepts an array of embed data (plain object): 14 | ```js 15 | .setArray([ 16 | { title: 'Yes', description: 'No' }, 17 | { title: 'No', description: 'Yes' }, 18 | ]); 19 | ``` 20 | 21 | ## [2.0.0] - 2020-05-27 22 | 23 | This version involves reflection of official release of Discord.JS 12, code optimisations, and changes that will give the user more control to the instance 24 | such as customisable page indicator, and favourable event emissions. 25 | 26 | ### Added 27 | - (ft. [@cycloptux]) - `setPageIndicator` is now powerful for customisation. 28 | - First parameter has additional option `footer` (makes the indicator replace the embed's footer text instead). 29 | - `footer` option will not *directly* modify embeds from `array` when using `Embeds` mode. 30 | - Second parameter has been added for indicator's formatting function: either use the following pre-made formats `'text' | 'textcompact' | 'circle' | 'hybrid'` — or make your own format. 31 | - In case you don't like the placing of the indicator (i.e. you want it in embed title instead), you can set the first parameter as false but set the second parameter to your preferred format and later on use the format by accessing getter `pageIndicator` then modify the embed via `pageUpdate` event. 32 | - **Example** 33 | - ```js 34 | .setPageIndicator('footer', (page, pages) => `peij ${page} 0f ${pages} - xoxo`) 35 | ``` 36 | - Pre-made formats preview: 37 | - **(default)** text: `Page 1 of 2` 38 | - textcompact: `1/2` 39 | - circle: `● ○` 40 | - hybrid: `[1/2] ● ○` 41 | - `setAuthorizedUsers` now accepts a user ID as well instead of just an array of user IDs. 42 | 43 | ### Changed 44 | - **⚠ BREAKING** - `Embeds` mode's changes to reflect Discord.JS' MessageEmbed methods: 45 | - Removed `addBlankField` 46 | - `spliceField` renamed to `spliceFields` 47 | - Added `addFields` 48 | - **⚠ BREAKING** - Removed `I` prefix on the following TypeScript interfaces: `INavigationEmojis`, `IClientAssets`, `IFunctionEmoji` 49 | - **⚠ BREAKING** - Renamed property `pageIndicator` to `usePageIndicator` 50 | - **⚠ BREAKING** - Navigation emoji identifiers (`BACK`, `JUMP`, `FORWARD`, `DELETE`, `ALL`) has been lowercased due to unnecessary internal transformations. 51 | - `usePageIndicator`'s default is now set to false. 52 | - Event `pageUpdate` has been relocated to emit at initial page as well. 53 | - Events' JSDoc description has been improved. 54 | - ([@cycloptux]) - Peer dependency Discord.JS version updated to `^12.0.0` 55 | - README's example has been updated to reduce complexity. 56 | 57 | ### Fixed 58 | - ([@d-shaun]) - Undesirable placement of `start` event's emission 59 | 60 | ### Internal 61 | - (ft. [@cycloptux]) - Test script `bot.js` updated for new changes 62 | - Migrated linter from TSLint to ESLint 63 | - Upload vscode settings 64 | 65 | [@cycloptux]: https://github.com/cycloptux 66 | [@d-shaun]: https://github.com/d-shaun 67 | 68 | ## [2.0.0-beta.4] - 2020-02-14 69 | Utility has been updated to support the breaking change feature `Managers`. Starting from this version it will no longer support the [Discord.JS](https://github.com/discordjs/discord.js) master commits earlier than [`bbdbc4c`](https://github.com/discordjs/discord.js/commit/bbdbc4cfa789383b7b3dbecf5e6b8401ea2dd998). 70 | 71 | ## [2.0.0-beta.3] - 2020-01-21 72 | 73 | ### Added 74 | - `attachFiles` method for Embeds mode 75 | - `pageUpdate` event (emitted when the page number is updated via reaction) 76 | - Verbose documentation to `navigationEmojis` and `functionEmojis` 77 | 78 | ### Changed 79 | - Test script `bot.js` updated for new features 80 | 81 | ### Fixed 82 | - Incorrect documentation examples for FieldsEmbed mode 83 | - Fix `Unknown Message` error when `deleteOnTimeout` is enabled 84 | 85 | ## [2.0.0-beta.2] - 2019-11-12 86 | 87 | ### Fixed 88 | - Embeds mode's `_loadList` overwriting existing message content 89 | 90 | ## [2.0.0-beta.1] - 2019-11-09 91 | 92 | ### Added 93 | - Documentation example for IFunctionEmoji: stopping the instance from awaiting reacts with function emojis 94 | 95 | ### Changed 96 | - Documentation changes involving `page` and `pages` properties 97 | 98 | ### Fixed 99 | - Incorrect installation note on `UPDATING_V1.md` 100 | - FieldsEmbed mode's `_loadList` overwriting existing message content 101 | 102 | ## [2.0.0-beta.0] - 2019-07-29 103 | Nothing new, but incremented version to make way for updated Discord.JS v11 variant release. 104 | 105 | ## [1.0.0] - 2019-07-29 106 | Latest release for [v11 branch](https://github.com/gazmull/discord-paginationembed/tree/v11). See changes below from 1.0.0-beta.0. 107 | 108 | **Master branch (For Discord.JS v12)** has been incremented to `2.0.0-beta.0` 109 | 110 | ## [1.0.0-beta.4] - 2019-04-26 111 | [**Updating from `v0.8.0`**](UPDATING_V1.md#api-changes) (Updated, please read again) 112 | 113 | ### Added 114 | - Added ability to stop the instance from awaiting emoji reacts with function emojis: 115 | ```js 116 | .addFunctionEmoji('🛑', () => { 117 | // Either 118 | throw 'stopped'; 119 | 120 | // or 121 | return Promise.reject('stopped'); 122 | 123 | // will stop the instance from awaiting reacts. 124 | // Passing an error object will emit the `error` event. 125 | }); 126 | ``` 127 | - Added `PaginationEmbed#setEmojisFunctionAfterNavigation` method. This allows function emojis to either be the first/last set to be deployed before/after navigation emojis. 128 | - Default: `false`. 129 | 130 | ### Changed 131 | - PaginationEmbed no longer emits an event when there is no listener (invoking `.on`/`.once`) 132 | 133 | ### Removed 134 | - Option `prepare` for `clientAssets` has been removed. This affects: 135 | - Setting the `channel` property is a must now. (Used to be either `clientAssets.message` or `channel` must be set) 136 | 137 | ### Fixed 138 | - Fixed possibly unnecessary API call on awaiting emoji reacts timeout. 139 | - `clientMessage#delete` now precedes `clientMessage.reactions#removeAll` 140 | 141 | ## [1.0.0-beta.3] - 2019-03-31 142 | ### Added 143 | - More TypeScript notices 144 | - Master branch (unreleased) installation 145 | 146 | ### Changed 147 | - Beautify bin output 148 | 149 | ### Fixed 150 | - Unnecessary recasting of MessageEmbed on FieldsEmbed#_loadList 151 | - Error handling 152 | - Unnecessary re-setting clientAssets on `_verify()` 153 | 154 | ## [1.0.0-beta.2] - 2019-03-27 155 | [**Updating from `v0.8.0`**](UPDATING_V1.md) (Updated, please read again) 156 | 157 | ### Added 158 | - Examples for README 159 | 160 | ### Changed 161 | - Updated `UPDATING_V1.md` 162 | - File structure: typings imports for TypeScript projects has been changed: 163 | #### Old way 164 | ```ts 165 | import Embeds from 'discord-paginationembed/typings/Embeds'; 166 | import FieldsEmbed from 'discord-paginationembed/typings/FieldsEmbed'; 167 | 168 | // Unlikely 169 | import { IClientAssets } from 'discord-paginationembed/typings/base'; 170 | ``` 171 | 172 | #### New way 173 | ```ts 174 | import { Embeds, FieldsEmbed, IClientAssets } from 'discord-paginationembed'; 175 | ``` 176 | - `showPageIndicator` ➡ `setPageIndicator` 177 | 178 | ### Fixed 179 | - Non-working examples 180 | - Page number that is out of bounds being ignored on `build()` 181 | - Missing message **and** channel objects being ignored 182 | - Undefined array being ignored on `Embeds` mode methods 183 | 184 | ## [1.0.0-beta.1] - 2019-03-26 185 | [**Updating from `v0.8.0`**](UPDATING_V1.md) 186 | 187 | ### Added 188 | - New features testing for the test unit 189 | - `Updating to v1` readme 190 | - Badges for README 191 | - Gulp tasks 192 | 193 | ### Changed 194 | - Files structure 195 | 196 | ### Removed 197 | - Unnecessary NPM package assets being included on publish 198 | - NPM tasks related to package building in favour of Gulp tasks 199 | 200 | ### Fixed 201 | - Typings for events and superclass 202 | - Superclass `EventEmitter`'s methods/properties from docs (docs noises) 203 | - `start` event not being fired at all 204 | 205 | ## [1.0.0-beta.0] - 2019-03-25 206 | [**Updating from `v0.8.0`**](UPDATING_V1.md) 207 | 208 | If the link above is unavailable, you may proceed to the [**documentation**](https://docs.thegzm.space/discord-paginationembed) site instead. 209 | 210 | ### Added 211 | - NPM publish 212 | - Package scripts and configs 213 | - Changelog added 214 | - PaginationEmbed events (from superclass `EventEmitter`) 215 | - Customisable `prompt` message (under `clientAssets.prompt`) 216 | 217 | ### Changed 218 | - `MessageEmbed` superclass changed to `EventEmitter` 219 | - `MessageEmbed` customisations for `FieldsEmbed` mode can only be access via `embed` property. e.g: 220 | ```js 221 | .embed 222 | .setColor('red') 223 | .setTitle('A customised embed') 224 | ``` 225 | - `clientMessage` ➡ `clientAssets`; including its method 226 | - Source code: from `JavaScript` to `TypeScript` 227 | - Tweaked typings and documentation 228 | - `README` revamped (please read) 229 | 230 | ### Removed 231 | - Dependencies-relevant: `yarn.lock` and `eslint`-relevant 232 | - Object parameter to construct an instance. Please use the methods instead 233 | 234 | ### Fixed 235 | - Current instance not being awaited properly on asynchronous functions. As example, your code that looks like this will work as expected now: 236 | ```js 237 | // e.g: A command has a cooldown/ratelimit for using this instance, we should wait for the instance to finish everything (either user deletes or expires the session) before we can let the user use this command again. 238 | await .build(); 239 | 240 | return 'done!'; 241 | ``` 242 | - Better typings and docs references 243 | 244 | ## [0.8.0] - 2019-03-12 245 | ### Added 246 | - Added .catch() for clientMessage deletion (#23) 247 | - Reflected MessageEmbed API (added SpliceFields, and setTimestamp now accepts a specified timestamp/date) 248 | - Added declaration file for TypeScript users 249 | 250 | ### Changed 251 | - README has been cleaned up (removed v11 branch deprecation notice too) 252 | - Documentation site changed from JSDoc to TypeDoc 253 | - Use yarn command on package scripts (due to yarn.lock) 254 | 255 | ### Removed 256 | - Discord.JS stable compatibility (*-v11) 257 | 258 | ### Fixed 259 | - Better array checking 260 | - FieldsEmbed Mode no longer puts its pageIndicator on embed description, puts at the message content instead. 261 | 262 | ## [0.7.7] | [0.7.7-v11] - 2018-07-03 263 | ### Added 264 | - `functionEmoji` - customised emojis with specific function (#8) 265 | - `deleteOnTimeout` - option to delete the message upon `awaiting response` timeout (#11) 266 | - `test` folder for `FieldsEmbed` and `Embeds` test bot 267 | 268 | ### Changed 269 | - Major [documentation](https://gazmull.github.io/discord-paginationembed) changes 270 | - `authorizedUsers` is now an `Array` type (#9) 271 | - `Embeds` will now show `clientMessage.content` after `pageIndicator` 272 | - `emojis` to `navigationEmojis` 273 | - `NavigationButtons` to `NavigationEmojis` 274 | - `setEmojis` to `setNavigationEmojis` 275 | 276 | ### Removed 277 | - `docs` folder; transferred to `gh-pages` branch 278 | 279 | ### Fixed 280 | - Support for DMs (except for message/reaction deletes). 281 | 282 | ## [0.2.3] | [0.2.3-v11] - 2018-06-13 283 | ### Added 284 | - Mention user on jump page prompt 285 | 286 | ### Fixed 287 | - Original content not being kept when page indicator was not set to true 288 | - JSDoc Typos 289 | 290 | ## [0.2.2] | [0.2.2-v11] - 2018-05-28 291 | ### Added 292 | - Discord.JS stable compatibility (*-v11) 293 | ### Fixed 294 | - Buttons still showing up even if the total pages are 1~2 295 | 296 | ## [0.2.1] - 2018-03-29 297 | ### Fixed 298 | - Buttons being 'reacted' after navigation prompt (borderline API spam) 299 | 300 | ## [0.2.0] - 2018-03-09 301 | ### Changed 302 | - `authorisedUser` / `setAuthorisedUser` ➡️ `authorizedUser` / `setAuthorizedUser` 303 | - First parameter on all methods are now required 304 | - Every method that returns the current instance of `FieldsEmbed` / `Embeds` class is now renamed to `PaginationEmbed` to avoid inconsistency 305 | - Typedef tweak for `currentEmbed` and `elementList` 306 | - Tweaked examples for consistency 307 | - Discord.JS is now under `peerDependencies` instead of `dependencies` 308 | 309 | ## [0.1.0] - 2018 310 | ### Added 311 | - Initial release 312 | 313 | [Unreleased]: https://github.com/gazmull/discord-paginationembed/compare/2.1.0...HEAD 314 | [2.1.0]: https://github.com/gazmull/discord-paginationembed/compare/2.0.0...2.1.0 315 | [2.0.0]: https://github.com/gazmull/discord-paginationembed/compare/2.0.0-beta.4...2.0.0 316 | [2.0.0-beta.4]: https://github.com/gazmull/discord-paginationembed/compare/2.0.0-beta.3...2.0.0-beta.4 317 | [2.0.0-beta.3]: https://github.com/gazmull/discord-paginationembed/compare/2.0.0-beta.2...2.0.0-beta.3 318 | [2.0.0-beta.2]: https://github.com/gazmull/discord-paginationembed/compare/2.0.0-beta.1...2.0.0-beta.2 319 | [2.0.0-beta.1]: https://github.com/gazmull/discord-paginationembed/compare/2.0.0-beta.0...2.0.0-beta.1 320 | [2.0.0-beta.0]: https://github.com/gazmull/discord-paginationembed/compare/1.0.0-beta.4...2.0.0-beta.0 321 | [1.0.0]: https://github.com/gazmull/discord-paginationembed/compare/1.0.0-beta.4...1.0.0 322 | [1.0.0-beta.4]: https://github.com/gazmull/discord-paginationembed/compare/1.0.0-beta.3...1.0.0-beta.4 323 | [1.0.0-beta.3]: https://github.com/gazmull/discord-paginationembed/compare/1.0.0-beta.2...1.0.0-beta.3 324 | [1.0.0-beta.2]: https://github.com/gazmull/discord-paginationembed/compare/1.0.0-beta.1...1.0.0-beta.2 325 | [1.0.0-beta.1]: https://github.com/gazmull/discord-paginationembed/compare/1.0.0-beta.0...1.0.0-beta.1 326 | [1.0.0-beta.0]: https://github.com/gazmull/discord-paginationembed/compare/0.8.0...1.0.0-beta.0 327 | [0.8.0]: https://github.com/gazmull/discord-paginationembed/compare/0.7.7...0.8.0 328 | [0.7.7]: https://github.com/gazmull/discord-paginationembed/compare/0.2.3...0.7.7 329 | [0.7.7-v11]: https://github.com/gazmull/discord-paginationembed/compare/0.2.3-v11...0.7.7-v11 330 | [0.2.3]: https://github.com/gazmull/discord-paginationembed/compare/0.2.2...0.2.3 331 | [0.2.3-v11]: https://github.com/gazmull/discord-paginationembed/compare/0.2.2...0.2.3-v11 332 | [0.2.2]: https://github.com/gazmull/discord-paginationembed/compare/0.2.1...0.2.2 333 | [0.2.2-v11]: https://github.com/gazmull/discord-paginationembed/compare/0.2.1...0.2.2-v11 334 | [0.2.1]: https://github.com/gazmull/discord-paginationembed/compare/0.2.0...0.2.1 335 | [0.2.0]: https://github.com/gazmull/discord-paginationembed/compare/b63b22167373ef0f9c0bc2ab45d6ca9554ab83c2...0.2.0 336 | [0.1.0]: https://github.com/gazmull/discord-paginationembed/commit/b63b22167373ef0f9c0bc2ab45d6ca9554ab83c2 337 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const gulp = require('gulp'); 3 | const ts = require('gulp-typescript'); 4 | const eslint = require('gulp-eslint7'); 5 | const terser = require('gulp-terser'); 6 | 7 | const tsProject = ts.createProject('tsconfig.json'); 8 | 9 | const paths = { 10 | bin: 'bin', 11 | src: 'src', 12 | typings: 'typings' 13 | }; 14 | 15 | const terserFiles = { 16 | dest: paths.bin, 17 | src: paths.bin + '/**/*.js' 18 | }; 19 | 20 | gulp.task('clean', () => del([ paths.bin, paths.typings ])); 21 | 22 | gulp.task('lint', () => { 23 | return gulp.src(paths.src + '/**/*.ts') 24 | .pipe(eslint()) 25 | .pipe(eslint.formatEach()) 26 | .pipe(eslint.failOnError()) 27 | }); 28 | 29 | gulp.task('ts', () => { 30 | const res = tsProject.src() 31 | .pipe(tsProject()); 32 | 33 | res.js.pipe(gulp.dest(paths.bin)); 34 | 35 | return res.dts.pipe(gulp.dest(paths.typings)); 36 | }); 37 | 38 | gulp.task('terser', () => { 39 | return gulp.src(terserFiles.src) 40 | .pipe(terser({ 41 | toplevel: true, 42 | output: { 43 | beautify: true, 44 | comments: false, 45 | ecma: 8, 46 | indent_level: 2, 47 | max_line_len: 120 48 | } 49 | })) 50 | .pipe(gulp.dest(terserFiles.dest)); 51 | }); 52 | 53 | const commonTasks = [ 'lint', 'clean', 'ts' ]; 54 | 55 | function watch () { 56 | gulp.watch(paths.src, gulp.series('ts')); 57 | } 58 | 59 | gulp.task('default', gulp.series(...commonTasks, 'terser')); 60 | gulp.task('watch', gulp.series(watch)); 61 | gulp.task('build', gulp.series(...commonTasks)); 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present gazmull 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord.JS - PaginationEmbed 2 | A pagination utility for MessageEmbed in Discord.JS 3 | 4 | [![Discord Server](https://discordapp.com/api/guilds/370614673122263041/embed.png)](https://discord.gg/eDUzT87) 5 | [![Travis (.org) branch](https://img.shields.io/travis/gazmull/discord-paginationembed/master.svg?logo=travis&style=flat-square)](https://travis-ci.org/gazmull/discord-paginationembed) 6 | [![npm peer dependency version](https://img.shields.io/npm/dependency-version/discord-paginationembed/peer/discord.js.svg?logo=npm&style=flat-square)](https://nodei.co/npm/discord-paginationembed/) 7 | [![npm type definitions](https://img.shields.io/npm/types/discord-paginationembed.svg?logo=npm&style=flat-square)](https://nodei.co/npm/discord-paginationembed/) 8 | 9 | [![NPM](https://nodei.co/npm/discord-paginationembed.png?downloads=true&stars=true)](https://nodei.co/npm/discord-paginationembed/) 10 | 11 | ## 📣 Notice Board 12 | - [**Changelog**](https://github.com/gazmull/discord-paginationembed/blob/master/CHANGELOG.md) 13 | - [**Updating from `v1`**](https://github.com/gazmull/discord-paginationembed/blob/master/UPDATING_V2.md) 14 | - Recently updated to `v2`! If **[Documentation]** feels off, please clear site cache! 15 | - This utility no longer provide updates for v1 (Discord.JS **11**) 16 | 17 | ## 🎉 Welcome 18 | - ✔ **Typings** included 19 | - ✔ **[Documentation]** for online references 20 | - ✔ **Asynchronous** workflow 21 | - ✔ Supports [Discord.JS **12**](https://discord.js.org "Go to Discord.JS Documentation") 22 | - ❔ Nothing found within docs or need a nudge? You may visit the [**Discord server**](https://discord.gg/eDUzT87) 23 | 24 | ## 🛠 Installation 25 | - **Requires Discord.JS 12**: `npm install discord.js` 26 | - **PaginationEmbed**: 27 | - **Published**: `npm install discord-paginationembed@beta` 28 | - **Unpublished**: `npm install gazmull/discord-paginationembed` 29 | - ❗ Requires [**Git**](https://git-scm.com/) 30 | 31 | ## 🔰 Examples 32 | > [**Test Unit Example**](https://github.com/gazmull/discord-paginationembed/blob/master/test) 33 | 34 | > [**Preface for TypeScript Projects**](https://github.com/gazmull/discord-paginationembed/blob/master/UPDATING_V1.md#TypeScript) 35 | 36 | ### In-action samples: 37 | - [**Pages of command description**](https://github.com/gazmull/eros-bot/blob/master/src/commands/general/guide.ts#L35) 38 | - [**Toggle between character and its weapon, and toggle image visibility**](https://github.com/gazmull/eros-bot/blob/master/src/commands/kamihime/info.ts#L180) 39 | - [**EXP Leaderboard**](https://github.com/gazmull/eros-bot/blob/master/src/commands/level/leaderboard.ts#L23) 40 | - [**Music Queue**](https://github.com/gazmull/ramiel-bot/blob/master/src/commands/music/queue.ts#L38) 41 | 42 |
43 | 44 | ### ⚠ Warning 45 | - Examples are written under `message` event! 46 | - There are some methods not shown in the examples. If you want to know more methods to fiddle, please visit the **[Documentation]** 47 | 48 | ### There are two modes of pagination 49 | 50 | #### `FieldsEmbed` Mode 51 | - A pagination mode that uses a MessageEmbed with a field(s) containing the elements to paginate. 52 | - Usually used for simple listing such as leaderboards and queues. 53 | 54 | ```js 55 | const Pagination = require('discord-paginationembed'); 56 | 57 | const FieldsEmbed = new Pagination.FieldsEmbed() 58 | // A must: an array to paginate, can be an array of any type 59 |  .setArray([{ word: 'they are' }, { word: 'being treated' }]) 60 | // Set users who can only interact with the instance. Default: `[]` (everyone can interact). 61 | // If there is only 1 user, you may omit the Array literal. 62 | .setAuthorizedUsers([message.author.id]) 63 | // A must: sets the channel where to send the embed 64 | .setChannel(message.channel) 65 | // Elements to show per page. Default: 10 elements per page 66 | .setElementsPerPage(2) 67 | // Have a page indicator (shown on message content). Default: false 68 | .setPageIndicator(false) 69 | // Format based on the array, in this case we're formatting the page based on each object's `word` property 70 | .formatField('Continue...', el => el.word); 71 | 72 | // Customise embed 73 | FieldsEmbed.embed 74 | .setColor(0x00FFFF) 75 | .setTitle('Jesus Yamato Saves the Day by Obliterating a Swarm of Abyssal Bombers!') 76 | .setDescription('Akagi and Kaga give their thanks to their holy saviour today as...') 77 | .setImage('https://lh5.googleusercontent.com/-TIcwCxc7a-A/AAAAAAAAAAI/AAAAAAAAAAA/Hij7_7Qa1j0/s900-c-k-no/photo.jpg'); 78 | 79 | // Deploy embed 80 | FieldsEmbed.build(); 81 | ``` 82 | ![FieldsEmbed](https://github.com/gazmull/discord-paginationembed/blob/master/demo/FieldsEmbed.png?raw=true) 83 | 84 | #### Working with Asynchronous Behaviour 85 | > This assumes this is under an `async` function 86 | 87 | ```js 88 | const Pagination = require('discord-paginationembed'); 89 | 90 | const FieldsEmbed = new Pagination.FieldsEmbed() 91 | .setArray([{ name: 'John Doe' }, { name: 'Jane Doe' }]) 92 | .setAuthorizedUsers([message.author.id]) 93 | .setChannel(message.channel) 94 | .setElementsPerPage(1) 95 | // Initial page on deploy 96 | .setPage(2) 97 | .setPageIndicator(true) 98 | .formatField('Name', i => i.name) 99 | // Deletes the embed upon awaiting timeout 100 | .setDeleteOnTimeout(true) 101 | // Disable built-in navigation emojis, in this case: 🗑 (Delete Embed) 102 | .setDisabledNavigationEmojis(['delete']) 103 | // Set your own customised emojis 104 | .setFunctionEmojis({ 105 | '🔄': (user, instance) => { 106 | const field = instance.embed.fields[0]; 107 | 108 | if (field.name === 'Name') 109 | field.name = user.tag; 110 | else 111 | field.name = 'Name'; 112 | } 113 | }) 114 | // Similar to setFunctionEmojis() but this one takes only one emoji 115 | .addFunctionEmoji('🅱', (_, instance) => { 116 | const field = instance.embed.fields[0]; 117 | 118 | if (field.name.includes('🅱')) 119 | field.name = 'Name'; 120 | else 121 | field.name = 'Na🅱e'; 122 | }) 123 | // Sets whether function emojis should be deployed after navigation emojis 124 | .setEmojisFunctionAfterNavigation(false); 125 | 126 | FieldsEmbed.embed 127 | .setColor(0xFF00AE) 128 | .setDescription('Test Description'); 129 | 130 | await FieldsEmbed.build(); 131 | 132 | // Will not log until the instance finished awaiting user responses 133 | // (or techinically emitted either `expire` or `finish` event) 134 | console.log('done'); 135 | ``` 136 | ![FieldsEmbed2](https://github.com/gazmull/discord-paginationembed/blob/master/demo/FieldsEmbed2.gif?raw=true) 137 | 138 | --- 139 | 140 | ### `Embeds` Mode 141 | - A pagination mode that uses an array of MessageEmbed to paginate. 142 | - Usually used for complex builds such as characters' information. 143 | 144 | ```js 145 | const Discord = require('discord.js'); 146 | const Pagination = require('discord-paginationembed'); 147 | 148 | const embeds = []; 149 | 150 | for (let i = 1; i <= 5; ++i) 151 | embeds.push(new Discord.MessageEmbed().addField('Page', i)); 152 | 153 | const myImage = message.author.displayAvatarURL(); 154 | 155 | new Pagination.Embeds() 156 | .setArray(embeds) 157 | .setAuthorizedUsers([message.author.id]) 158 | .setChannel(message.channel) 159 | .setPageIndicator(true) 160 | .setPage(3) 161 | // Methods below are for customising all embeds 162 | .setImage(myImage) 163 | .setThumbnail(myImage) 164 | .setTitle('Test Title') 165 | .setDescription('Test Description') 166 | .setFooter('Test Footer Text') 167 | .setURL(myImage) 168 | .setColor(0xFF00AE) 169 | .addField('\u200b', '\u200b') 170 | .addField('Test Field 1', 'Test Field 1', true) 171 | .addField('Test Field 2', 'Test Field 2', true) 172 | .build(); 173 | ``` 174 | ![Embeds](https://user-images.githubusercontent.com/32944712/37118454-41116cbe-228f-11e8-9878-f39db26316a1.png) 175 | 176 | > This assumes this is under an `async` function 177 | 178 | ```js 179 | const Discord = require('discord.js'); 180 | const Pagination = require('discord-paginationembed'); 181 | 182 | const embeds = []; 183 | 184 | for (let i = 1; i <= 5; ++i) 185 | embeds.push(new Discord.MessageEmbed().addField('Page', i)); 186 | 187 | const Embeds = new PaginationEmbed.Embeds() 188 | .setArray(embeds) 189 | .setAuthorizedUsers([message.author.id]) 190 | .setChannel(message.channel) 191 | .setPageIndicator(true) 192 | .setTitle('Test Title') 193 | .setDescription('Test Description') 194 | .setFooter('Test Footer Text') 195 | .setURL('https://gazmull.github.io/discord-paginationembed') 196 | .setColor(0xFF00AE) 197 | // Sets the client's assets to utilise. Available options: 198 | // - message: the client's Message object (edits the message instead of sending new one for this instance) 199 | // - prompt: custom content for the message sent when prompted to jump to a page 200 | // {{user}} is the placeholder for the user mention 201 | .setClientAssets({ message, prompt: 'Page plz {{user}}' }) 202 | .setDeleteOnTimeout(true) 203 | .setDisabledNavigationEmojis(['delete']) 204 | .setFunctionEmojis({ 205 | '⬆': (_, instance) => { 206 | for (const embed of instance.array) 207 | embed.fields[0].value++; 208 | }, 209 | '⬇': (_, instance) => { 210 | for (const embed of instance.array) 211 | embed.fields[0].value--; 212 | } 213 | }) 214 | // Listeners for PaginationEmbed's events 215 | // After the initial embed has been sent 216 | // (technically, after the client finished reacting with enabled navigation and function emojis). 217 | .on('start', () => console.log('Started!')) 218 | // When the instance is finished by a user reacting with `delete` navigation emoji 219 | // or a function emoji that throws non-Error type. 220 | .on('finish', (user) => console.log(`Finished! User: ${user.username}`)) 221 | // Upon a user reacting on the instance. 222 | .on('react', (user, emoji) => console.log(`Reacted! User: ${user.username} | Emoji: ${emoji.name} (${emoji.id})`)) 223 | // When the awaiting timeout is reached. 224 | .on('expire', () => console.warn('Expired!')) 225 | // Upon an occurance of error (e.g: Discord API Error). 226 | .on('error', console.error); 227 | 228 | await Embeds.build(); 229 | ``` 230 | ![Embeds2](https://github.com/gazmull/discord-paginationembed/blob/master/demo/Embeds.gif?raw=true) 231 | 232 | ## 💡🐛💻 Contributing 233 | ### Bug Reports 234 | Please provide reproducible steps and results proofs (e.g: images). Also, solutions are welcome! 235 | 236 | ### Suggestions / Discussions 237 | Please be explicit about the feature's description and provide a valid reason (e.g: beneficial to users or development time) why it should be added/changed/removed. 238 | 239 | ### Source Code 240 | - Fork this repository. 241 | - Execute `npm install` 242 | - Code and code and code and code and... code! 243 | - To enable [**incremental compilation**](https://en.wikipedia.org/wiki/Incremental_compiler) to JS: `npm run dev:watch` 244 | - `npm test` to verify if your additions/adjustments are following the project's codebase rules and to verify if the docs are valid. 245 | - Please make sure that you have tested your changes very well. 246 | - There is a test bot script under `test` folder. To get started: 247 | - Copy `credentials.sample.js` to `credentials.js` and fill up your private credentials (token, test channel, etc) 248 | - Execute either: 249 | - One-time test: `npm run test:bot` 250 | - Hot-reloading test (nodemon): `npm run dev:start` 251 | - File a [**Pull Request (PR)**](https://github.com/gazmull/discord-paginationembed/compare)! 252 | - For the PR comment, it goes the same with **`Suggestions / Discussions`**. 253 | 254 | # License 255 | > [**MIT**](https://github.com/gazmull/discord-paginationembed/blob/master/LICENSE) 256 | 257 | © 2018-present [**Euni (gazmull)**](https://github.com/gazmull) 258 | 259 | [Documentation]: https://gazmull.github.io/discord-paginationembed/master 260 | -------------------------------------------------------------------------------- /UPDATING_V1.md: -------------------------------------------------------------------------------- 1 | # Updating From `v0.8.0` 2 | 3 | ## Notice 4 | - This documentation will be changed without prior notice for any changes in v1's source code. In some cases, commit messages will notify everyone that this documentation has been changed. 5 | - If you are from **stable branch (v11)**, then look [**here**](https://github.com/gazmull/discord-paginationembed/blob/stable/UPDATING_V1.md). 6 | 7 | ## Installation 8 | New way to install the utility is from NPM: `npm install discord-paginationembed@beta` 9 | 10 | ## TypeScript 11 | > ***Since v2.0.0***: `I` prefix from interfaces has been removed. 12 | 13 | For importing types to your TypeScript project: 14 | ```ts 15 | import { Embeds, FieldsEmbed, IClientAssets } from 'discord-paginationembed'; 16 | ``` 17 | 18 | Typings also provide type parameter for `FieldsEmbed` mode to help you identify your element's properties: 19 | ```ts 20 | interface LegendaryInterface { 21 | pakaluPapito: string; 22 | hotel?: 'Trivago'; 23 | } 24 | 25 | const fieldsEmbed = new FieldsEmbed(); 26 | ``` 27 | 28 | ![Types](https://github.com/gazmull/discord-paginationembed/blob/master/demo/Types.png?raw=true) 29 | 30 | ## API Changes 31 | ### `showPageIndicator` ➡ `setPageIndicator` 32 | > Since **v1.0.0-beta.2** 33 | 34 | --- 35 | 36 | ### `channel` **must** be set 37 | > Since **v1.0.0-beta.4** 38 | 39 | Before, it's either `clientAssets.message` or `channel` is set, but this makes an unnecessary API call (could lead to ratelimit) via sending/editing a dummy message with `clientAssets.prepare`'s content. By removing `clientAssets.prepare`, setting the channel is now a must while `clientAssets.message` is still optional. 40 | 41 | --- 42 | 43 | ### Passing an object to the constructor removed 44 | Method `object as constructor` no longer exists, instead use the methods provided such as `setArray()`. 45 | 46 | #### Old way 47 | ```js 48 | new FieldsEmbed({ array: [ { name: 'yes' } ] }) 49 | .setAuthorizedUsers([ message.author.id, ...mentionedUsers ]); 50 | ``` 51 | 52 | #### New way 53 | ```js 54 | new FieldsEmbed() 55 | .setArray([ { name: 'yes' } ]) 56 | .setAuthorizedUsers([ message.author.id, ...mentionedUsers ]); 57 | ``` 58 | 59 | --- 60 | 61 | ### `clientMessage` ➡ `clientAssets` 62 | `clientMessage` has been replaced with `clientAssets`: similar API but the latter takes an object as the only parameter. Option `prompt` was added for customising the content for message to send when prompted to jump to a page. 63 | 64 | - [**since v1.0.0-beta.4**] `prepare` option has been removed due to unnecessary API call. 65 | 66 | #### `clientMessage` (Old way) 67 | ```js 68 | new FieldsEmbed() 69 | .setClientMessage(clientMessage, 'yo!'); 70 | ``` 71 | 72 | #### `clientAssets` (New way) 73 | **Note**: `{{user}}` is the placeholder for a user mention. 74 | ```js 75 | new FieldsEmbed() 76 | .setClientAssets({ 77 | message: clientMessage, 78 | prompt: 'Yo {{user}} what page?' // Option for message content when prompted to jump to a page. 79 | }); 80 | ``` 81 | 82 | --- 83 | 84 | ### Customised `FieldsEmbed`'s `MessageEmbed` 85 | All `MessageEmbed` methods for `FieldsEmbed` mode has been moved to `FieldsEmbed#embed`. 86 | 87 | #### Old way 88 | ```js 89 | new FieldsEmbed() 90 | .setColor('RANDOM'); 91 | ``` 92 | 93 | #### New way 94 | ```js 95 | new FieldsEmbed().embed 96 | .setColor('RANDOM'); 97 | ``` 98 | -------------------------------------------------------------------------------- /UPDATING_V2.md: -------------------------------------------------------------------------------- 1 | # Updating From v1 2 | 3 | ## TypeScript 4 | 5 | `I` prefix on interfaces has been removed. 6 | 7 | #### From 8 | ```ts 9 | import { IClientAssets, IFunctionEmoji, INavigationEmojis } from 'discord-paginationembed'; 10 | ``` 11 | 12 | #### To 13 | ```ts 14 | import { ClientAssets, FunctionEmoji, NavigationEmojis } from 'discord-paginationembed'; 15 | ``` 16 | 17 | ## API Changes 18 | 19 | ### Removal of `Embeds`#`addBlankField`() 20 | This method has been removed to reflect Discord.JS' MessageEmbed changes. As a workaround for this, use: 21 | ```js 22 | .addField('\u200b', '\u200b') 23 | ``` 24 | 25 | ### `Embeds`#`spliceField`() => `Embeds`#`spliceFields`() 26 | Same reason above. 27 | 28 | ### `pageIndicator` => `usePageIndicator` 29 | This shouldn't be a big issue unless you're using it this way: `.pageIndicator = true` (which is not really recommended). The change was made to make way for getter `pageIndicator` for the addition of customising indicator: 30 | 31 | From `CHANGELOG.md`: 32 | > - `setPageIndicator` is now powerful for customisation. 33 | > - First parameter has additional option `footer` (makes the indicator replace the embed's footer text instead). 34 | > - `footer` option will not *directly* modify embeds from `array` when using `Embeds` mode. 35 | > - Second parameter has been added for indicator's formatting function: either use the following pre-made formats `'text' | 'textcompact' | 'circle' | 'hybrid'` — or make your own format. 36 | > - In case you don't like the placing of the indicator (i.e. you want it in embed title instead), you can set the first parameter as false but set the second parameter to your preferred format and later on use the format by accessing getter `pageIndicator` then modify the embed via `pageUpdate` event. 37 | > - **Example** 38 | > - ```js 39 | > .setPageIndicator('footer', (page, pages) => `peij ${page} 0f ${pages} - xoxo`) 40 | > ``` 41 | > - Pre-made formats preview: 42 | > - **(default)** text: `Page 1 of 2` 43 | > - textcompact: `1/2` 44 | > - circle: `● ○` 45 | > - hybrid: `[1/2] ● ○` 46 | 47 | ### `usePageIndicator`'s (Formerly `pageIndicator`) Default: `true` => `false` 48 | 49 | ### Navigation Emoji Identfiers (`BACK`, `JUMP`, `FORWARD`, `DELETE`, `ALL`) => Lowercased 50 | They have been lowercased for code optimisation. 51 | 52 | ### Event `pageUpdate` Relocated 53 | Event `pageUpdate` has been relocated to emit at initial page as well. This is to give more control for customised page indicator. 54 | 55 | ## Miscellaneous 56 | 57 | ### `setAuthorizedUsers` Now Accepting Non-Array Parameter 58 | This has been added since this utility is frequently used singularly. 59 | 60 | ```diff 61 | - .setAuthorizedUsers([message.author.id]) 62 | + .setAuthorizedUsers(message.author.id) 63 | ``` 64 | -------------------------------------------------------------------------------- /bin/Embeds.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: !0 5 | }), exports.Embeds = void 0; 6 | 7 | const t = require("discord.js"), r = require("./base"); 8 | 9 | class s extends r.PaginationEmbed { 10 | get currentEmbed() { 11 | return this.array[this.page - 1]; 12 | } 13 | get pages() { 14 | return this.array.length; 15 | } 16 | addField(t, r, s = !1) { 17 | if (!this.array) throw new TypeError("this.array must be set first."); 18 | for (const e of this.array) e.addField(t, r, s); 19 | return this; 20 | } 21 | addFields(...t) { 22 | if (!this.array) throw new TypeError("this.array must be set first."); 23 | for (const r of this.array) r.addFields(...t); 24 | return this; 25 | } 26 | async build() { 27 | return await this._verify(), this._loadList(); 28 | } 29 | setArray(r) { 30 | if (!(Array.isArray(r) && Boolean(r.length))) throw new TypeError("Cannot invoke Embeds class without a valid array to paginate."); 31 | for (const [s, e] of r.entries()) { 32 | if (!Boolean(e) || e.constructor !== Object || !Object.keys(e).length) { 33 | if (e instanceof t.MessageEmbed) continue; 34 | throw new TypeError(`(MessageEmbeds[${s}]) Cannot invoke Embeds class with an invalid MessageEmbed instance.`); 35 | } 36 | r[s] = new t.MessageEmbed(e); 37 | } 38 | return this.array = r, this; 39 | } 40 | setAuthor(t, r, s) { 41 | if (!this.array) throw new TypeError("this.array must be set first."); 42 | for (const e of this.array) e.setAuthor(t, r, s); 43 | return this; 44 | } 45 | setColor(t) { 46 | if (!this.array) throw new TypeError("this.array must be set first."); 47 | for (const r of this.array) r.setColor(t); 48 | return this; 49 | } 50 | setDescription(t) { 51 | if (!this.array) throw new TypeError("this.array must be set first."); 52 | for (const r of this.array) r.setDescription(t); 53 | return this; 54 | } 55 | setFooter(t, r) { 56 | if (!this.array) throw new TypeError("this.array must be set first."); 57 | for (const s of this.array) s.setFooter(t, r); 58 | return this; 59 | } 60 | setImage(t) { 61 | if (!this.array) throw new TypeError("this.array must be set first."); 62 | for (const r of this.array) r.setImage(t); 63 | return this; 64 | } 65 | setThumbnail(t) { 66 | if (!this.array) throw new TypeError("this.array must be set first."); 67 | for (const r of this.array) r.setThumbnail(t); 68 | return this; 69 | } 70 | setTimestamp(t) { 71 | if (!this.array) throw new TypeError("this.array must be set first."); 72 | for (const r of this.array) r.setTimestamp(t); 73 | return this; 74 | } 75 | setTitle(t) { 76 | if (!this.array) throw new TypeError("this.array must be set first."); 77 | for (const r of this.array) r.setTitle(t); 78 | return this; 79 | } 80 | setURL(t) { 81 | if (!this.array) throw new TypeError("this.array must be set first."); 82 | for (const r of this.array) r.setURL(t); 83 | return this; 84 | } 85 | spliceFields(t, r, s, e, i) { 86 | if (!this.array) throw new TypeError("this.array must be set first."); 87 | for (const a of this.array) a.spliceFields(t, r, [ { 88 | name: s, 89 | value: e, 90 | inline: i 91 | } ]); 92 | return this; 93 | } 94 | toJSON() { 95 | if (!this.array) throw new TypeError("this.array must be set first."); 96 | return this.array.map((t => t.toJSON())); 97 | } 98 | async _loadList(r = !0) { 99 | this.listenerCount("pageUpdate") && this.emit("pageUpdate"); 100 | const s = new t.MessageEmbed(this.currentEmbed), e = "footer" === this.usePageIndicator, i = this.usePageIndicator && !e ? 1 === this.pages ? "" : this.pageIndicator : "", {separator: a, text: o} = this.content, n = { 101 | embeds: [ s ], 102 | content: `${o ? `${o}${a}` : ""}${i}` || null 103 | }; 104 | return e && s.setFooter(this.pageIndicator, s.footer.iconURL), this.clientAssets.message ? await this.clientAssets.message.edit(n) : this.clientAssets.message = await this.channel.send(n), 105 | super._loadList(r); 106 | } 107 | } 108 | 109 | exports.Embeds = s; -------------------------------------------------------------------------------- /bin/FieldsEmbed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: !0 5 | }), exports.FieldsEmbed = void 0; 6 | 7 | const e = require("discord.js"), t = require("./base"); 8 | 9 | class s extends t.PaginationEmbed { 10 | constructor() { 11 | super(), this.elementsPerPage = 10, this.embed = new e.MessageEmbed; 12 | } 13 | get elementList() { 14 | const e = (this.page - 1) * this.elementsPerPage, t = e + this.elementsPerPage; 15 | return this.array.slice(e, t); 16 | } 17 | get pages() { 18 | return Math.ceil(this.array.length / this.elementsPerPage); 19 | } 20 | async build() { 21 | await this._verify(); 22 | const e = this.embed.fields; 23 | this.embed.fields = []; 24 | for (const t of e) "function" == typeof t.value ? this.formatField(t.name, t.value, t.inline) : this.embed.addField(t.name, t.value, t.inline); 25 | if (!this.embed.fields.filter((e => "function" == typeof e.value)).length) throw new Error("Cannot invoke FieldsEmbed class without at least one formatted field to paginate."); 26 | return this._loadList(); 27 | } 28 | formatField(e, t, s = !0) { 29 | if ("function" != typeof t) throw new TypeError("formatField() value parameter only takes a function."); 30 | return this.embed.fields.push({ 31 | name: e, 32 | value: t, 33 | inline: s 34 | }), this; 35 | } 36 | setElementsPerPage(e) { 37 | if ("number" != typeof e) throw new TypeError("setElementsPerPage() only accepts number type."); 38 | return this.elementsPerPage = e, this; 39 | } 40 | async _drawList() { 41 | const t = new e.MessageEmbed(this.embed); 42 | t.fields = []; 43 | for (const e of this.embed.fields) t.addField(e.name, "function" == typeof e.value ? this.elementList.map(e.value).join("\n") : e.value, e.inline); 44 | return t; 45 | } 46 | async _loadList(e = !0) { 47 | this.listenerCount("pageUpdate") && this.emit("pageUpdate"); 48 | const t = await this._drawList(), s = "footer" === this.usePageIndicator, i = this.usePageIndicator && !s ? 1 === this.pages ? "" : this.pageIndicator : "", {separator: n, text: a} = this.content, r = { 49 | embeds: [ t ], 50 | content: `${a ? `${a}${n}` : ""}${i}` || null 51 | }; 52 | return s && t.setFooter(this.pageIndicator, t.footer.iconURL), this.clientAssets.message ? await this.clientAssets.message.edit(r) : this.clientAssets.message = await this.channel.send(r), 53 | super._loadList(e); 54 | } 55 | } 56 | 57 | exports.FieldsEmbed = s; -------------------------------------------------------------------------------- /bin/base/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: !0 5 | }), exports.PaginationEmbed = void 0; 6 | 7 | const t = require("events"); 8 | 9 | class e extends t.EventEmitter { 10 | constructor() { 11 | super(), this.authorizedUsers = [], this.channel = null, this.clientAssets = {}, 12 | this.content = { 13 | separator: "\n" 14 | }, this.usePageIndicator = !1, this.deleteOnTimeout = !1, this.page = 1, this.timeout = 3e4, 15 | this.navigationEmojis = { 16 | back: "◀", 17 | jump: "↗", 18 | forward: "▶", 19 | delete: "🗑" 20 | }, this.functionEmojis = {}, this.disabledNavigationEmojis = [], this.emojisFunctionAfterNavigation = !1, 21 | this._disabledNavigationEmojiValues = [], this._defaultNavigationEmojis = { 22 | back: "◀", 23 | jump: "↗", 24 | forward: "▶", 25 | delete: "🗑" 26 | }; 27 | const t = (t, e) => `${"○ ".repeat(t - 1)}● ${"○ ".repeat(e - t)}`.trim(); 28 | this._defaultPageIndicators = { 29 | text: (t, e) => `Page ${t} of ${e}`, 30 | textcompact: (t, e) => `${t}/${e}`, 31 | circle: (e, i) => t(e, i), 32 | hybrid: (e, i) => `[${e}/${i}] ${t(e, i)}` 33 | }, this._pageIndicator = this._defaultPageIndicators.text; 34 | } 35 | get pageIndicator() { 36 | return this._pageIndicator(this.page, this.pages); 37 | } 38 | build() { 39 | throw new Error("Cannot invoke this class. Invoke with [PaginationEmbed.Embeds] or [PaginationEmbed.FieldsEmbed] instead."); 40 | } 41 | addFunctionEmoji(t, e) { 42 | if (!(e instanceof Function)) throw new TypeError(`Callback for ${t} must be a function type.`); 43 | return Object.assign(this.functionEmojis, { 44 | [t]: e 45 | }), this; 46 | } 47 | deleteFunctionEmoji(t) { 48 | if (!(t in this.functionEmojis)) throw new Error(`${t} function emoji does not exist.`); 49 | return delete this.functionEmojis[t], this; 50 | } 51 | resetEmojis() { 52 | for (const t of Object.keys(this.functionEmojis)) delete this.functionEmojis[t]; 53 | return this.navigationEmojis = this._defaultNavigationEmojis, this; 54 | } 55 | setArray(t) { 56 | if (!(Array.isArray(t) && Boolean(t.length))) throw new TypeError("Cannot invoke PaginationEmbed class without a valid array to paginate."); 57 | return this.array = t, this; 58 | } 59 | setAuthorizedUsers(t) { 60 | if (!Array.isArray(t) && "string" != typeof t) throw new TypeError("Cannot invoke PaginationEmbed class without a valid array."); 61 | return this.authorizedUsers = Array.isArray(t) ? t : [ t ], this; 62 | } 63 | setChannel(t) { 64 | return this.channel = t, this; 65 | } 66 | setClientAssets(t) { 67 | const e = typeof t; 68 | if ("object" !== e || null === e) throw new TypeError("setClientAssets() only accepts object type."); 69 | let {prompt: i} = t; 70 | return i || (i = "{{user}}, To what page would you like to jump? Say `cancel` or `0` to cancel the prompt."), 71 | Object.assign(this.clientAssets, Object.assign(Object.assign({}, t), { 72 | prompt: i 73 | })), this; 74 | } 75 | setDisabledNavigationEmojis(t) { 76 | if (!Array.isArray(t)) throw new TypeError("Cannot invoke PaginationEmbed class without a valid array."); 77 | const e = [], i = []; 78 | for (const s of t) { 79 | [ "back", "jump", "forward", "delete", "all" ].includes(s) ? i.push(s) : e.push(s); 80 | } 81 | if (e.length) throw new TypeError(`Cannot invoke PaginationEmbed class with invalid navigation emoji(s): ${e.join(", ")}.`); 82 | return this.disabledNavigationEmojis = i, this; 83 | } 84 | setEmojisFunctionAfterNavigation(t) { 85 | if ("boolean" != typeof t) throw new TypeError("setEmojisFunctionAfterNavigation() only accepts boolean type."); 86 | return this.emojisFunctionAfterNavigation = t, this; 87 | } 88 | setFunctionEmojis(t) { 89 | for (const e of Object.keys(t)) { 90 | const i = t[e]; 91 | this.addFunctionEmoji(e, i); 92 | } 93 | return this; 94 | } 95 | setNavigationEmojis(t) { 96 | return Object.assign(this.navigationEmojis, t), this; 97 | } 98 | setPage(t) { 99 | const e = "string" == typeof t; 100 | if (isNaN(t) && !e) throw new TypeError("setPage() only accepts number/string type."); 101 | const i = "back" === t ? 1 === this.page ? this.page : this.page - 1 : this.page === this.pages ? this.pages : this.page + 1; 102 | return this.page = e ? i : t, this; 103 | } 104 | setTimeout(t) { 105 | if ("number" != typeof t) throw new TypeError("setTimeout() only accepts number type."); 106 | return this.timeout = t, this; 107 | } 108 | setPageIndicator(t, e) { 109 | if ("boolean" != typeof t && ("string" != typeof t || "footer" !== t)) throw new TypeError("setPageIndicator()'s `enabled` parameter only accepts boolean/string type."); 110 | if (this.usePageIndicator = t, e) { 111 | const t = [ "text", "textcompact", "circle", "hybrid" ]; 112 | if ("string" == typeof e && t.includes(e)) this._pageIndicator = this._defaultPageIndicators[e]; else { 113 | if ("function" != typeof e) throw new TypeError("setPageIndicator()'s `fn` parameter only accepts function/string type."); 114 | this._pageIndicator = e; 115 | } 116 | } 117 | return this; 118 | } 119 | setDeleteOnTimeout(t) { 120 | if ("boolean" != typeof t) throw new TypeError("deleteOnTimeout() only accepts boolean type."); 121 | return this.deleteOnTimeout = t, this; 122 | } 123 | setContent(t, e = "\n") { 124 | if ("string" != typeof e) throw new TypeError("setContent()'s `separator` parameter only accepts string type."); 125 | return Object.assign(this.content, { 126 | text: t, 127 | separator: e 128 | }), this; 129 | } 130 | async _verify() { 131 | if (this.setClientAssets(this.clientAssets), !this.channel) throw new Error("Cannot invoke PaginationEmbed class without a channel object set."); 132 | if (!(this.page >= 1 && this.page <= this.pages)) throw new RangeError(`Page number is out of bounds. Max pages: ${this.pages}`); 133 | return this._checkPermissions(); 134 | } 135 | async _checkPermissions() { 136 | const t = this.channel; 137 | if (t.guild) { 138 | const e = t.permissionsFor(t.client.user).missing([ "ADD_REACTIONS", "EMBED_LINKS", "VIEW_CHANNEL", "SEND_MESSAGES" ]); 139 | if (e.length) throw new Error(`Cannot invoke PaginationEmbed class without required permissions: ${e.join(", ")}`); 140 | } 141 | return !0; 142 | } 143 | _enabled(t) { 144 | return !this.disabledNavigationEmojis.includes("all") && !this.disabledNavigationEmojis.includes(t); 145 | } 146 | async _drawEmojis() { 147 | return this.emojisFunctionAfterNavigation ? (await this._drawNavigationEmojis(), 148 | await this._drawFunctionEmojis()) : (await this._drawFunctionEmojis(), await this._drawNavigationEmojis()), 149 | this.listenerCount("start") && this.emit("start"), this._awaitResponse(); 150 | } 151 | async _drawFunctionEmojis() { 152 | if (Object.keys(this.functionEmojis).length) for (const t of Object.keys(this.functionEmojis)) await this.clientAssets.message.react(t); 153 | } 154 | async _drawNavigationEmojis() { 155 | this._enabled("back") && this.pages > 1 && await this.clientAssets.message.react(this.navigationEmojis.back), 156 | this._enabled("jump") && this.pages > 2 && await this.clientAssets.message.react(this.navigationEmojis.jump), 157 | this._enabled("forward") && this.pages > 1 && await this.clientAssets.message.react(this.navigationEmojis.forward), 158 | this._enabled("delete") && await this.clientAssets.message.react(this.navigationEmojis.delete); 159 | } 160 | _loadList(t = !0) { 161 | if (t) return this._drawEmojis(); 162 | } 163 | async _loadPage(t = 1) { 164 | return this.setPage(t), await this._loadList(!1), this._awaitResponse(); 165 | } 166 | async _awaitResponse() { 167 | const t = Object.values(this.navigationEmojis), e = this.clientAssets.message.channel, i = (e, i) => { 168 | const s = !!this._enabled("all") && (!this._disabledNavigationEmojiValues.length || this._disabledNavigationEmojiValues.some((t => ![ e.emoji.name, e.emoji.id ].includes(t)))) && (t.includes(e.emoji.name) || t.includes(e.emoji.id)) || e.emoji.name in this.functionEmojis || e.emoji.id in this.functionEmojis; 169 | return this.authorizedUsers.length ? this.authorizedUsers.includes(i.id) && s : !i.bot && s; 170 | }, s = this.clientAssets.message; 171 | try { 172 | const t = (await s.awaitReactions({ 173 | filter: i, 174 | max: 1, 175 | time: this.timeout, 176 | errors: [ "time" ] 177 | })).first(), n = t.users.cache.last(), a = [ t.emoji.name, t.emoji.id ]; 178 | if (this.listenerCount("react") && this.emit("react", n, t.emoji), s.guild) { 179 | e.permissionsFor(e.client.user).missing([ "MANAGE_MESSAGES" ]).length || await t.users.remove(n); 180 | } 181 | switch (a[0] || a[1]) { 182 | case this.navigationEmojis.back: 183 | return 1 === this.page ? this._awaitResponse() : this._loadPage("back"); 184 | 185 | case this.navigationEmojis.jump: 186 | return this.pages <= 2 ? this._awaitResponse() : this._awaitResponseEx(n); 187 | 188 | case this.navigationEmojis.forward: 189 | return this.page === this.pages ? this._awaitResponse() : this._loadPage("forward"); 190 | 191 | case this.navigationEmojis.delete: 192 | return await s.delete(), void (this.listenerCount("finish") && this.emit("finish", n)); 193 | 194 | default: 195 | { 196 | const t = this.functionEmojis[a[0]] || this.functionEmojis[a[1]]; 197 | try { 198 | await t(n, this); 199 | } catch (t) { 200 | return this._cleanUp(t, s, !1, n); 201 | } 202 | return this._loadPage(this.page); 203 | } 204 | } 205 | } catch (t) { 206 | return this._cleanUp(t, s); 207 | } 208 | } 209 | async _cleanUp(t, e, i = !0, s) { 210 | const n = this.clientAssets.message.channel; 211 | if (this.deleteOnTimeout && e.deletable && (await e.delete(), e.deleted = !0), e.guild && !e.deleted) { 212 | n.permissionsFor(n.client.user).missing([ "MANAGE_MESSAGES" ]).length || await e.reactions.removeAll(); 213 | } 214 | if (t instanceof Error) return void (this.listenerCount("error") && this.emit("error", t)); 215 | const a = i ? "expire" : "finish"; 216 | this.listenerCount(a) && this.emit(a, s); 217 | } 218 | async _awaitResponseEx(t) { 219 | const e = [ "0", "cancel" ], i = i => { 220 | const s = parseInt(i.content); 221 | return i.author.id === t.id && (!isNaN(Number(i.content)) && s !== this.page && s >= 1 && s <= this.pages || e.includes(i.content.toLowerCase())); 222 | }, s = this.clientAssets.message.channel, n = await s.send(this.clientAssets.prompt.replace(/\{\{user\}\}/g, t.toString())); 223 | try { 224 | const t = (await s.awaitMessages({ 225 | filter: i, 226 | max: 1, 227 | time: this.timeout, 228 | errors: [ "time" ] 229 | })).first(), a = t.content, o = s.permissionsFor(s.client.user).missing([ "MANAGE_MESSAGES" ]); 230 | return this.clientAssets.message.deleted ? void (this.listenerCount("error") && this.emit("error", new Error("Client's message was deleted before being processed."))) : (await n.delete(), 231 | t.deletable && (o.length || await t.delete()), e.includes(a) ? this._awaitResponse() : this._loadPage(parseInt(a))); 232 | } catch (t) { 233 | if (n.deletable && await n.delete(), t instanceof Error) return void (this.listenerCount("error") && this.emit("error", t)); 234 | this.listenerCount("expire") && this.emit("expire"); 235 | } 236 | } 237 | } 238 | 239 | exports.PaginationEmbed = e; -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var e = this && this.__createBinding || (Object.create ? function(e, r, t, i) { 4 | void 0 === i && (i = t), Object.defineProperty(e, i, { 5 | enumerable: !0, 6 | get: function() { 7 | return r[t]; 8 | } 9 | }); 10 | } : function(e, r, t, i) { 11 | void 0 === i && (i = t), e[i] = r[t]; 12 | }), r = this && this.__exportStar || function(r, t) { 13 | for (var i in r) "default" === i || Object.prototype.hasOwnProperty.call(t, i) || e(t, r, i); 14 | }; 15 | 16 | Object.defineProperty(exports, "__esModule", { 17 | value: !0 18 | }), exports.version = void 0, r(require("./Embeds"), exports), r(require("./FieldsEmbed"), exports), 19 | r(require("./base"), exports), exports.version = require("../package.json").version; -------------------------------------------------------------------------------- /demo/Embeds.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gazmull/discord-paginationembed/38488a63fb8716cdb8dbe77cfdc644483fbc666d/demo/Embeds.gif -------------------------------------------------------------------------------- /demo/FieldsEmbed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gazmull/discord-paginationembed/38488a63fb8716cdb8dbe77cfdc644483fbc666d/demo/FieldsEmbed.png -------------------------------------------------------------------------------- /demo/FieldsEmbed2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gazmull/discord-paginationembed/38488a63fb8716cdb8dbe77cfdc644483fbc666d/demo/FieldsEmbed2.gif -------------------------------------------------------------------------------- /demo/Types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gazmull/discord-paginationembed/38488a63fb8716cdb8dbe77cfdc644483fbc666d/demo/Types.png -------------------------------------------------------------------------------- /deploy/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Based on https://github.com/hydrabolt/discord.js-site/blob/master/deploy/deploy.sh 3 | 4 | set -e 5 | 6 | if [ "$TRAVIS_BRANCH" != "master" -o -n "$TRAVIS_TAG" -o "$TRAVIS_PULL_REQUEST" != "false" ]; then 7 | echo -e "Not building for a non master branch push - building without deploying." 8 | yarn gh:build 9 | exit 0 10 | fi 11 | 12 | echo -e "Building for a master branch push - building and deploying." 13 | 14 | REPO=$(git config remote.origin.url) 15 | SHA=$(git rev-parse --verify HEAD) 16 | 17 | TARGET_BRANCH="gh-pages" 18 | git clone $REPO dist -b $TARGET_BRANCH 19 | 20 | yarn gh:build 21 | 22 | rsync --delete-before -avh docs/master dist/ 23 | 24 | cd dist 25 | git add --all . 26 | git config user.name "Travis CI" 27 | git config user.email "${COMMIT_EMAIL}" 28 | git commit -m "Build: [MASTER] ${SHA}" || true 29 | git push "https://${GITHUB_TOKEN}@github.com/gazmull/discord-paginationembed.git" $TARGET_BRANCH 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-paginationembed", 3 | "version": "3.0.0-dev.0", 4 | "description": "A pagination utility for MessageEmbed in Discord.JS", 5 | "main": "./bin/index.js", 6 | "types": "./typings", 7 | "engines": { 8 | "node": ">= 14.0.0" 9 | }, 10 | "scripts": { 11 | "test": "gulp", 12 | "test:build": "gulp build", 13 | "test:bot": "node ./test/bot.js", 14 | "dev:start": "nodemon ./test/bot.js --config .nodemon.json", 15 | "dev:watch": "tsc -p . --watch", 16 | "dev:gulp-watch": "gulp watch", 17 | "docs:parse": "npm run gh:build -- --json ./bin/docs.json", 18 | "gh:build": "typedoc", 19 | "prepublishOnly": "npm test", 20 | "pub": "npm publish" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/gazmull/discord-paginationembed.git" 25 | }, 26 | "keywords": [ 27 | "discord", 28 | "discord.js", 29 | "pagination", 30 | "embed", 31 | "paginationembed", 32 | "emoji", 33 | "react", 34 | "leaderboard" 35 | ], 36 | "author": "gazmull", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/gazmull/discord-paginationembed/issues" 40 | }, 41 | "homepage": "https://github.com/gazmull/discord-paginationembed#readme", 42 | "peerDependencies": { 43 | "discord.js": "^13.0.0-dev.4206e35.1626393827" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^14.14.35", 47 | "@typescript-eslint/eslint-plugin": "^4.19.0", 48 | "@typescript-eslint/parser": "^4.19.0", 49 | "del": "^5.0.0", 50 | "gulp": "^4.0.2", 51 | "gulp-eslint7": "^0.3.0", 52 | "gulp-terser": "^2.0.1", 53 | "gulp-typescript": "^6.0.0-alpha.1", 54 | "typedoc": "^0.20.30", 55 | "typedoc-neo-theme": "^1.1.0", 56 | "typedoc-plugin-no-inherit": "^1.2.2", 57 | "typescript": "^4.2.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": false, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ "@typescript-eslint" ], 5 | "extends": [ 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | ], 9 | "rules": { 10 | "@typescript-eslint/ban-ts-comment": "off", 11 | "@typescript-eslint/explicit-module-boundary-types": "off", 12 | "@typescript-eslint/member-ordering": [ 13 | "error", 14 | { 15 | "default": { 16 | "memberTypes": [ 17 | "constructor", 18 | "instance-field", 19 | "instance-method", 20 | "static-field", 21 | "static-method" 22 | ], 23 | "order": "as-written" 24 | } 25 | } 26 | ], 27 | "@typescript-eslint/no-explicit-any": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Embeds.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColorResolvable, 3 | EmbedField, 4 | EmbedFieldData, 5 | Message, 6 | MessageEmbed 7 | } from 'discord.js'; 8 | import { PaginationEmbed } from './base'; 9 | 10 | /** 11 | * A pagination mode that uses an array of MessageEmbed to paginate. 12 | * @extends [[PaginationEmbed]] 13 | * @noInheritDoc 14 | */ 15 | export class Embeds extends PaginationEmbed { 16 | 17 | /** The title of all embeds. */ 18 | public title: string; 19 | 20 | /** The description of all embeds. */ 21 | public description: string; 22 | 23 | /** The URL of all embeds. */ 24 | public url: string; 25 | 26 | /** The color of all embeds. */ 27 | public color: number; 28 | 29 | /** The timestamp of all embeds. */ 30 | public timestamp: number; 31 | 32 | /** The fields of all embeds. */ 33 | public fields: EmbedField[]; 34 | 35 | /** The thumbnail of all embeds. */ 36 | public thumbnail: string; 37 | 38 | /** The image of all embeds. */ 39 | public image: string; 40 | 41 | /** The author of all embeds. */ 42 | public author: { 43 | name: string; 44 | url?: string; 45 | iconURL?: string; 46 | }; 47 | 48 | /** The footer of all embeds. */ 49 | public footer: { 50 | text: string; 51 | iconURL?: string; 52 | }; 53 | 54 | /** Embed in the current page. */ 55 | get currentEmbed (): MessageEmbed { 56 | return this.array[this.page - 1]; 57 | } 58 | 59 | get pages (): number { 60 | return this.array.length; 61 | } 62 | 63 | /** 64 | * Adds a field to the fields of all embeds. 65 | * @param name - The name of the field. 66 | * @param value - The value of the field. 67 | * @param inline - Whether the field is inline to the other fields. 68 | */ 69 | public addField (name: string, value: string, inline = false) { 70 | if (!this.array) throw new TypeError('this.array must be set first.'); 71 | 72 | for (const el of this.array) 73 | el.addField(name, value, inline); 74 | 75 | return this; 76 | } 77 | 78 | public addFields (...fields: EmbedFieldData[] | EmbedFieldData[][]) { 79 | if (!this.array) throw new TypeError('this.array must be set first.'); 80 | 81 | for (const el of this.array) 82 | el.addFields(...fields); 83 | 84 | return this; 85 | } 86 | 87 | /** 88 | * Build the Pagination Embeds. 89 | * 90 | * ### Example 91 | * ```js 92 | * const { Embeds } = require('discord-paginationembed'); 93 | * const { MessageEmbed } = require('discord.js'); 94 | * 95 | * // Under message event. 96 | * const embeds = []; 97 | * 98 | * for (let i = 0; i < 5; ++i) 99 | * embeds.push(new MessageEmbed().addField('Page', i + 1)); 100 | * 101 | * new Embeds() 102 | * .setAuthorizedUsers([message.author.id]) 103 | * .setChannel(message.channel) 104 | * .setClientAssets({ prompt: 'Yo {{user}} wat peige?!?!?' }) 105 | * .setArray(embeds) 106 | * .setPageIndicator(false) 107 | * .setPage(1) 108 | * .setTimeout(69000) 109 | * .setNavigationEmojis({ 110 | * back: '◀', 111 | * jump: '↗', 112 | * forward: '▶', 113 | * delete: '🗑' 114 | * }) 115 | * .setFunctionEmojis({ 116 | * '⬆': (_, instance) => { 117 | * for (const embed of instance.array) 118 | * embed.fields[0].value++; 119 | * }, 120 | * '⬇': (_, instance) => { 121 | * for (const embed of instance.array) 122 | * embed.fields[0].value--; 123 | * } 124 | * }) 125 | * .setDescription('This is one of my embeds with this message!') 126 | * .setColor(0xFF00AE) 127 | * .setTimestamp() 128 | * .build();``` 129 | */ 130 | public async build () { 131 | await this._verify(); 132 | 133 | return this._loadList(); 134 | } 135 | 136 | /** 137 | * Sets the array of MessageEmbed to paginate. 138 | * @param array - An array of MessageEmbed to paginate. 139 | */ 140 | public setArray (array: MessageEmbed[]) { 141 | const isValidArray = Array.isArray(array) && Boolean(array.length); 142 | 143 | if (!isValidArray) throw new TypeError('Cannot invoke Embeds class without a valid array to paginate.'); 144 | 145 | for (const [ i, v ] of array.entries()) 146 | if (Boolean(v) && v.constructor === Object && Object.keys(v).length) 147 | array[i] = new MessageEmbed(v); 148 | else if (v instanceof MessageEmbed) 149 | continue; 150 | else 151 | throw new TypeError(`(MessageEmbeds[${i}]) Cannot invoke Embeds class with an invalid MessageEmbed instance.`); 152 | 153 | this.array = array; 154 | 155 | return this; 156 | } 157 | 158 | /** 159 | * Set the author of all embeds. 160 | * @param name - The name of the author. 161 | * @param iconURL - The icon URL of the author. 162 | * @param url - The URL of the author. 163 | */ 164 | public setAuthor (name: string, iconURL?: string, url?: string) { 165 | if (!this.array) throw new TypeError('this.array must be set first.'); 166 | 167 | for (const el of this.array) 168 | el.setAuthor(name, iconURL, url); 169 | 170 | return this; 171 | } 172 | 173 | /** 174 | * Sets the color of all embeds. 175 | * @param color - The color of all embeds. 176 | */ 177 | public setColor (color: ColorResolvable) { 178 | if (!this.array) throw new TypeError('this.array must be set first.'); 179 | 180 | for (const el of this.array) 181 | el.setColor(color); 182 | 183 | return this; 184 | } 185 | 186 | /** 187 | * Sets the description of all embeds. 188 | * @param description - The description of all embeds. 189 | */ 190 | public setDescription (description: string) { 191 | if (!this.array) throw new TypeError('this.array must be set first.'); 192 | 193 | for (const el of this.array) 194 | el.setDescription(description); 195 | 196 | return this; 197 | } 198 | 199 | /** 200 | * Sets the footer of all embeds. 201 | * @param text - The footer text. 202 | * @param iconURL - URL for the footer's icon. 203 | */ 204 | public setFooter (text: string, iconURL?: string) { 205 | if (!this.array) throw new TypeError('this.array must be set first.'); 206 | 207 | for (const el of this.array) 208 | el.setFooter(text, iconURL); 209 | 210 | return this; 211 | } 212 | 213 | /** 214 | * Sets the image of all embeds. 215 | * @param url - The image of all embeds. 216 | */ 217 | public setImage (url: string) { 218 | if (!this.array) throw new TypeError('this.array must be set first.'); 219 | 220 | for (const el of this.array) 221 | el.setImage(url); 222 | 223 | return this; 224 | } 225 | 226 | /** 227 | * Sets the thumbnail of all embeds. 228 | * @param url - The thumbnail of all embeds. 229 | */ 230 | public setThumbnail (url: string) { 231 | if (!this.array) throw new TypeError('this.array must be set first.'); 232 | 233 | for (const el of this.array) 234 | el.setThumbnail(url); 235 | 236 | return this; 237 | } 238 | 239 | /** 240 | * Sets the timestamp of all embeds. 241 | * @param timestamp - The timestamp or date. 242 | */ 243 | public setTimestamp (timestamp?: Date | number) { 244 | if (!this.array) throw new TypeError('this.array must be set first.'); 245 | 246 | for (const el of this.array) 247 | el.setTimestamp(timestamp); 248 | 249 | return this; 250 | } 251 | 252 | /** 253 | * Sets the title of all embeds. 254 | * @param title - The title of all embeds. 255 | */ 256 | public setTitle (title: string) { 257 | if (!this.array) throw new TypeError('this.array must be set first.'); 258 | 259 | for (const el of this.array) 260 | el.setTitle(title); 261 | 262 | return this; 263 | } 264 | 265 | /** 266 | * Sets the URL of all embeds. 267 | * @param url - The URL of all embeds. 268 | */ 269 | public setURL (url: string) { 270 | if (!this.array) throw new TypeError('this.array must be set first.'); 271 | 272 | for (const el of this.array) 273 | el.setURL(url); 274 | 275 | return this; 276 | } 277 | 278 | /** 279 | * Removes, replaces, and inserts fields in all embeds (max 25). 280 | * @param index - The index to start at. 281 | * @param deleteCount - The number of fields to remove. 282 | * @param name - The name of the field. 283 | * @param value - The value of the field. 284 | * @param inline - Set the field to display inline. 285 | */ 286 | public spliceFields ( 287 | index: number, 288 | deleteCount: number, 289 | name?: string, 290 | value?: string, 291 | inline?: boolean 292 | ) { 293 | if (!this.array) throw new TypeError('this.array must be set first.'); 294 | 295 | for (const el of this.array) 296 | el.spliceFields(index, deleteCount, [ { name, value, inline } ]); 297 | 298 | return this; 299 | } 300 | 301 | /** Transforms all embeds to plain objects. */ 302 | public toJSON () { 303 | if (!this.array) throw new TypeError('this.array must be set first.'); 304 | 305 | return this.array.map(m => m.toJSON()); 306 | } 307 | 308 | /** @ignore */ 309 | public async _loadList (callNavigation = true) { 310 | if (this.listenerCount('pageUpdate')) this.emit('pageUpdate'); 311 | 312 | const embed = new MessageEmbed(this.currentEmbed); 313 | const isFooter = this.usePageIndicator === 'footer'; 314 | const shouldIndicate = this.usePageIndicator && !isFooter 315 | ? this.pages === 1 316 | ? '' 317 | : this.pageIndicator 318 | : ''; 319 | const { separator, text } = this.content; 320 | // Fixes no-argument TS error 321 | const content = `${text ? `${text}${separator}` : ''}${shouldIndicate}`; 322 | const options = { embeds: [ embed ], content: content || null }; 323 | 324 | if (isFooter) 325 | embed.setFooter(this.pageIndicator, embed.footer.iconURL); 326 | if (this.clientAssets.message) 327 | await this.clientAssets.message.edit(options); 328 | else 329 | this.clientAssets.message = await this.channel.send(options) as Message; 330 | 331 | return super._loadList(callNavigation); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/FieldsEmbed.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageEmbed } from 'discord.js'; 2 | import { PaginationEmbed } from './base'; 3 | 4 | /** 5 | * A pagination mode that uses a MessageEmbed with a field(s) containing the elements to paginate. 6 | * @extends [[PaginationEmbed]] 7 | * @noInheritDoc 8 | */ 9 | export class FieldsEmbed extends PaginationEmbed { 10 | 11 | constructor () { 12 | super(); 13 | 14 | this.elementsPerPage = 10; 15 | this.embed = new MessageEmbed(); 16 | } 17 | 18 | /** Maximum number of elements to be displayed per page. */ 19 | public elementsPerPage: number; 20 | 21 | /** 22 | * The MessageEmbed being used for this mode. 23 | * 24 | * ### Notice 25 | * To customise the MessageEmbed for this mode, please access this property. Example: 26 | * ```js 27 | * .embed.setColor('red') 28 | * ``` 29 | */ 30 | public embed: MessageEmbed; 31 | 32 | /** Elements in the current page. */ 33 | get elementList (): Element[] { 34 | const begin = (this.page - 1) * this.elementsPerPage; 35 | const end = begin + this.elementsPerPage; 36 | 37 | return this.array.slice(begin, end); 38 | } 39 | 40 | get pages (): number { 41 | return Math.ceil(this.array.length / this.elementsPerPage); 42 | } 43 | 44 | /** 45 | * Build the Pagination Fields Embed. 46 | * 47 | * ### Example 48 | * ```js 49 | * const { FieldsEmbed } = require('discord-paginationembed'); 50 | * 51 | * // Under message event. 52 | * new FieldsEmbed() 53 | * .setAuthorizedUsers([message.author.id]) 54 | * .setChannel(message.channel) 55 | * .setClientAssets({ prompt: 'Yo {{user}} wat peige?!?!?' }) 56 | * .setArray([{ name: 'John Doe' }, { name: 'Jane Doe' }]) 57 | * .setElementsPerPage(1) 58 | * .setPageIndicator(false) 59 | * .formatField('Name', el => el.name) 60 | * .setPage(1) 61 | * .setTimeout(69000) 62 | * .setNavigationEmojis({ 63 | * back: '◀', 64 | * jump: '↗', 65 | * forward: '▶', 66 | * delete: '🗑' 67 | * }) 68 | * .setFunctionEmojis({ 69 | * '🔄': (user, instance) => { 70 | * const field = instance.embed.fields[0]; 71 | * 72 | * if (field.name === 'Name') 73 | * field.name = user.tag; 74 | * else 75 | * field.name = 'Name'; 76 | * } 77 | * }) 78 | * .build();``` 79 | */ 80 | public async build () { 81 | await this._verify(); 82 | 83 | const fields = this.embed.fields; 84 | this.embed.fields = []; 85 | 86 | for (const field of fields) 87 | if (typeof field.value === 'function') 88 | this.formatField(field.name, field.value, field.inline); 89 | else 90 | this.embed.addField(field.name, field.value, field.inline); 91 | 92 | const hasPaginateField = this.embed.fields.filter(f => typeof f.value === 'function').length; 93 | 94 | if (!hasPaginateField) 95 | throw new Error('Cannot invoke FieldsEmbed class without at least one formatted field to paginate.'); 96 | 97 | return this._loadList(); 98 | } 99 | 100 | /** 101 | * Adds a field to the embed. 102 | * Same as MessageEmbed.addField, but value takes a function instead. 103 | * @param name - Name of the field. 104 | * @param value - Value of the field. Function for `Array.prototype.map().join('\n')`. 105 | * @param inline - Whether the field is inline with other field. Default: `true` 106 | */ 107 | // eslint-disable-next-line max-len 108 | public formatField (name: string, value: (element: Element, index?: number, array?: Element[]) => any, inline = true) { 109 | if (typeof value !== 'function') throw new TypeError('formatField() value parameter only takes a function.'); 110 | 111 | // @ts-ignore 112 | this.embed.fields.push({ name, value, inline }); 113 | 114 | return this; 115 | } 116 | 117 | /** 118 | * Sets the maximum number of elements to be displayed per page. 119 | * @param max - Maximum number of elements to be displayed per page. 120 | */ 121 | public setElementsPerPage (max: number) { 122 | if (typeof max !== 'number') throw new TypeError('setElementsPerPage() only accepts number type.'); 123 | 124 | this.elementsPerPage = max; 125 | 126 | return this; 127 | } 128 | 129 | protected async _drawList () { 130 | const embed = new MessageEmbed(this.embed); 131 | embed.fields = []; 132 | 133 | for (const field of this.embed.fields) 134 | embed.addField( 135 | field.name, 136 | typeof field.value === 'function' 137 | ? this.elementList.map(field.value).join('\n') 138 | : field.value, 139 | field.inline 140 | ); 141 | 142 | return embed; 143 | } 144 | 145 | /** @ignore */ 146 | public async _loadList (callNavigation = true) { 147 | if (this.listenerCount('pageUpdate')) this.emit('pageUpdate'); 148 | 149 | const embed = await this._drawList(); 150 | const isFooter = this.usePageIndicator === 'footer'; 151 | const shouldIndicate = this.usePageIndicator && !isFooter 152 | ? this.pages === 1 153 | ? '' 154 | : this.pageIndicator 155 | : ''; 156 | const { separator, text } = this.content; 157 | // Fixes no-arguemnt TS error 158 | const content = `${text ? `${text}${separator}` : ''}${shouldIndicate}`; 159 | const options = { embeds: [ embed ], content: content || null }; 160 | 161 | if (isFooter) 162 | embed.setFooter(this.pageIndicator, embed.footer.iconURL); 163 | if (this.clientAssets.message) 164 | await this.clientAssets.message.edit(options); 165 | else 166 | this.clientAssets.message = await this.channel.send(options) as Message; 167 | 168 | return super._loadList(callNavigation); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/base/index.ts: -------------------------------------------------------------------------------- 1 | /** @module PaginationEmbed */ 2 | 3 | import { 4 | DMChannel, 5 | Emoji, 6 | Message, 7 | MessageReaction, 8 | NewsChannel, 9 | Snowflake, 10 | TextChannel, 11 | User 12 | } from 'discord.js'; 13 | import { EventEmitter } from 'events'; 14 | import { Embeds } from '../Embeds'; 15 | import { FieldsEmbed } from '../FieldsEmbed'; 16 | 17 | /** @ignore */ 18 | const MESSAGE_DELETED = 'Client\'s message was deleted before being processed.'; 19 | 20 | /** 21 | * The base class for Pagination Modes. **Do not invoke**. 22 | * @extends [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) 23 | * @noInheritDoc 24 | */ 25 | export abstract class PaginationEmbed extends EventEmitter { 26 | 27 | constructor () { 28 | super(); 29 | 30 | this.authorizedUsers = []; 31 | this.channel = null; 32 | this.clientAssets = {}; 33 | this.content = { separator: '\n' }; 34 | this.usePageIndicator = false; 35 | this.deleteOnTimeout = false; 36 | this.page = 1; 37 | this.timeout = 30000; 38 | this.navigationEmojis = { 39 | back: '◀', 40 | jump: '↗', 41 | forward: '▶', 42 | delete: '🗑' 43 | }; 44 | this.functionEmojis = {}; 45 | this.disabledNavigationEmojis = []; 46 | this.emojisFunctionAfterNavigation = false; 47 | 48 | this._disabledNavigationEmojiValues = []; 49 | this._defaultNavigationEmojis = { 50 | back: '◀', 51 | jump: '↗', 52 | forward: '▶', 53 | delete: '🗑' 54 | }; 55 | 56 | const makeCircles = (page: number, pages: number) => 57 | `${'○ '.repeat(page - 1)}● ${'○ '.repeat(pages - page)}`.trim(); 58 | 59 | this._defaultPageIndicators = { 60 | text: (page, pages) => `Page ${page} of ${pages}`, 61 | textcompact: (page, pages) => `${page}/${pages}`, 62 | circle: (page, pages) => makeCircles(page, pages), 63 | hybrid: (page, pages) => `[${page}/${pages}] ${makeCircles(page, pages)}` 64 | }; 65 | this._pageIndicator = this._defaultPageIndicators.text; 66 | } 67 | 68 | /** The authorized users to navigate the pages. Default: `everyone` */ 69 | public authorizedUsers: Snowflake[]; 70 | 71 | /** The channel where to send the embed. */ 72 | public channel: TextChannel | DMChannel | NewsChannel; 73 | 74 | /** Settings for assets for the client. */ 75 | public clientAssets: ClientAssets; 76 | 77 | /** An array of elements to paginate. */ 78 | public array: Element[]; 79 | 80 | /** 81 | * Whether to show page indicator, or put it in embed's footer text (replaces the existing text) instead. 82 | * Default: `false` 83 | */ 84 | public usePageIndicator: boolean | 'footer'; 85 | 86 | /** Whether the client's message will be deleted upon timeout. Default: `false` */ 87 | public deleteOnTimeout: boolean; 88 | 89 | /** The current page. Default: `1` */ 90 | public page: number; 91 | 92 | /** The time for awaiting a user action before timeout in ms. Default: `30000` */ 93 | public timeout: number; 94 | 95 | /** 96 | * The emojis used for navigation emojis. 97 | * Navigation emojis are the default message reactions for navigating through the pagination. 98 | */ 99 | public navigationEmojis: NavigationEmojis; 100 | 101 | /** 102 | * The emojis used for function emojis. 103 | * Function emojis are user-customised message reactions 104 | * for modifying the current instance of the pagination such as modifying embed texts, stopping the pagination, etc. 105 | */ 106 | public functionEmojis: FunctionEmoji; 107 | 108 | /** 109 | * The disabled navigation emojis. 110 | * Available navigation emojis to disable: 111 | * - 'back' 112 | * - 'jump' 113 | * - 'forward' 114 | * - 'delete' 115 | * - 'all' 116 | */ 117 | public disabledNavigationEmojis: ('back' | 'jump' | 'forward' | 'delete' | 'all')[]; 118 | 119 | /** Whether to set function emojis after navigation emojis. Default: `false` */ 120 | public emojisFunctionAfterNavigation: boolean; 121 | 122 | /** Number of pages for this instance. */ 123 | public abstract pages: number; 124 | 125 | /** The client's message content options. */ 126 | public content: ClientMessageContent; 127 | 128 | /** The disabled navigation emojis (in values). */ 129 | protected _disabledNavigationEmojiValues: any[]; 130 | 131 | /** The default navigation emojis. Used for resetting the navigation emojis. */ 132 | protected _defaultNavigationEmojis: { back: string; jump: string; forward: string; delete: string }; 133 | 134 | /** The function for formatting page indicator. */ 135 | protected _pageIndicator: PageIndicatorCaster; 136 | 137 | /** Pre-made functions for formatting page indicator. */ 138 | protected _defaultPageIndicators: { [x: string]: PageIndicatorCaster }; 139 | 140 | /** The formatted page indicator. Default format: `text` */ 141 | public get pageIndicator () { 142 | return this._pageIndicator(this.page, this.pages); 143 | } 144 | 145 | public build () { 146 | throw new Error( 147 | 'Cannot invoke this class. Invoke with [PaginationEmbed.Embeds] or [PaginationEmbed.FieldsEmbed] instead.' 148 | ); 149 | } 150 | 151 | /** 152 | * Adds a function emoji to the embed. 153 | * 154 | * ### Example 155 | * ```js 156 | * .addFunctionEmoji('🅱', (_, instance) => { 157 | * const field = instance.embed.fields[0]; 158 | * 159 | * if (field.name.includes('🅱')) 160 | * field.name = 'Name'; 161 | * else 162 | * field.name = 'Na🅱e'; 163 | * }); 164 | * ``` 165 | * @param emoji - The emoji to use as the function's emoji. 166 | * @param callback - The function to call upon pressing the function emoji. 167 | */ 168 | public addFunctionEmoji (emoji: string, callback: (user: User, instance: Embeds | FieldsEmbed) => any) { 169 | if (!(callback instanceof Function)) 170 | throw new TypeError(`Callback for ${emoji} must be a function type.`); 171 | 172 | Object.assign(this.functionEmojis, { [emoji]: callback }); 173 | 174 | return this; 175 | } 176 | 177 | /** 178 | * Deletes a function emoji. 179 | * @param emoji - The emoji key to delete. 180 | */ 181 | public deleteFunctionEmoji (emoji: string) { 182 | if (!(emoji in this.functionEmojis)) 183 | throw new Error(`${emoji} function emoji does not exist.`); 184 | 185 | delete this.functionEmojis[emoji]; 186 | 187 | return this; 188 | } 189 | 190 | /** Deletes all function emojis, and then re-enables all navigation emojis. */ 191 | public resetEmojis () { 192 | for (const emoji of Object.keys(this.functionEmojis)) 193 | delete this.functionEmojis[emoji]; 194 | 195 | this.navigationEmojis = this._defaultNavigationEmojis; 196 | 197 | return this; 198 | } 199 | 200 | /** 201 | * Sets the array of elements to paginate. This must be called first before any other methods. 202 | * @param array - An array of elements to paginate. 203 | */ 204 | public setArray (array: Element[]) { 205 | const isValidArray = Array.isArray(array) && Boolean(array.length); 206 | 207 | if (!isValidArray) throw new TypeError('Cannot invoke PaginationEmbed class without a valid array to paginate.'); 208 | 209 | this.array = array; 210 | 211 | return this; 212 | } 213 | 214 | /** 215 | * Set the authorized users to navigate the pages. 216 | * @param users - A user ID or an array of user IDs. 217 | */ 218 | public setAuthorizedUsers (users: Snowflake | Snowflake[]) { 219 | if (!(Array.isArray(users) || typeof users === 'string')) 220 | throw new TypeError('Cannot invoke PaginationEmbed class without a valid array.'); 221 | 222 | this.authorizedUsers = Array.isArray(users) ? users : [ users ]; 223 | 224 | return this; 225 | } 226 | 227 | /** 228 | * The channel where to send the embed. 229 | * @param channel - The channel object. 230 | */ 231 | public setChannel (channel: TextChannel | DMChannel | NewsChannel) { 232 | this.channel = channel; 233 | 234 | return this; 235 | } 236 | 237 | /** 238 | * Sets the settings for assets for the client. 239 | * @param assets - The assets for the client. 240 | */ 241 | public setClientAssets (assets: ClientAssets) { 242 | const type = typeof assets; 243 | 244 | if (type !== 'object' || type === null) 245 | throw new TypeError('setClientAssets() only accepts object type.'); 246 | 247 | let { prompt } = assets; 248 | 249 | if (!prompt) 250 | prompt = '{{user}}, To what page would you like to jump? Say `cancel` or `0` to cancel the prompt.'; 251 | 252 | Object.assign(this.clientAssets, { ...assets, prompt }); 253 | 254 | return this; 255 | } 256 | 257 | /** 258 | * Sets the disabled navigation emojis. 259 | * 260 | * ### Example 261 | * ```js 262 | * // Disable specific navigation emojis 263 | * .setDisabledNavigationEmojis(['delete', 'jump']); 264 | * 265 | * // Disable all navigation emojis 266 | * .setDisabledNavigationEmojis(['all']); 267 | * ``` 268 | * 269 | * @param emojis - An array of navigation emojis to disable. 270 | */ 271 | public setDisabledNavigationEmojis (emojis: DisabledNavigationEmojis) { 272 | if (!Array.isArray(emojis)) throw new TypeError('Cannot invoke PaginationEmbed class without a valid array.'); 273 | 274 | const invalid = []; 275 | const verified = []; 276 | 277 | for (const emoji of emojis) { 278 | const validEmojis = [ 'back', 'jump', 'forward', 'delete', 'all' ]; 279 | 280 | if (validEmojis.includes(emoji)) verified.push(emoji); 281 | else invalid.push(emoji); 282 | } 283 | 284 | if (invalid.length) 285 | throw new TypeError( 286 | `Cannot invoke PaginationEmbed class with invalid navigation emoji(s): ${invalid.join(', ')}.` 287 | ); 288 | 289 | this.disabledNavigationEmojis = verified; 290 | 291 | return this; 292 | } 293 | 294 | /** 295 | * Sets whether to set function emojis after navigation emojis. 296 | * @param boolean - Set function emojis after navigation emojis? 297 | */ 298 | public setEmojisFunctionAfterNavigation (boolean: boolean) { 299 | if (typeof boolean !== 'boolean') 300 | throw new TypeError('setEmojisFunctionAfterNavigation() only accepts boolean type.'); 301 | 302 | this.emojisFunctionAfterNavigation = boolean; 303 | 304 | return this; 305 | } 306 | 307 | /** 308 | * Sets the emojis used for function emojis. 309 | * 310 | * ### Example 311 | * ```js 312 | * .setFunctionEmojis({ 313 | * '🔄': (user, instance) => { 314 | * const field = instance.embed.fields[0]; 315 | * 316 | * if (field.name === 'Name') 317 | * field.name = user.tag; 318 | * else 319 | * field.name = 'Name'; 320 | * } 321 | * }); 322 | * ``` 323 | * @param emojis - An object containing customised emojis to use as function emojis. 324 | */ 325 | public setFunctionEmojis (emojis: FunctionEmoji) { 326 | for (const emoji of Object.keys(emojis)) { 327 | const fn = emojis[emoji]; 328 | 329 | this.addFunctionEmoji(emoji, fn); 330 | } 331 | 332 | return this; 333 | } 334 | 335 | /** 336 | * Sets the emojis used for navigation emojis. 337 | * @param emojis - An object containing customised emojis to use as navigation emojis. 338 | */ 339 | public setNavigationEmojis (emojis: NavigationEmojis) { 340 | Object.assign(this.navigationEmojis, emojis); 341 | 342 | return this; 343 | } 344 | 345 | /** 346 | * Sets to jump to a certain page upon calling PaginationEmbed.build(). 347 | * @param page - The page number to jump to. 348 | */ 349 | public setPage (page: number | 'back' | 'forward') { 350 | const isString = typeof page === 'string'; 351 | 352 | if (!(!isNaN(page as number) || isString)) throw new TypeError('setPage() only accepts number/string type.'); 353 | 354 | const navigator = page === 'back' 355 | ? this.page === 1 ? this.page : this.page - 1 356 | : this.page === this.pages ? this.pages : this.page + 1; 357 | 358 | this.page = isString ? navigator : page as number; 359 | 360 | return this; 361 | } 362 | 363 | /** 364 | * Sets the time for awaiting a user action before timeout in ms. 365 | * @param timeout Timeout value in ms. 366 | */ 367 | public setTimeout (timeout: number) { 368 | if (typeof timeout !== 'number') throw new TypeError('setTimeout() only accepts number type.'); 369 | 370 | this.timeout = timeout; 371 | 372 | return this; 373 | } 374 | 375 | /** 376 | * Sets the page indicator formatting function and placement. 377 | * @param enabled - Whether to show page indicator. 378 | * Pass `footer` to display the indicator in embed's footer text (replaces existing text) instead. 379 | * @param fn - Function for indicator formatting. 380 | */ 381 | public setPageIndicator (enabled: boolean | 'footer', fn?: PageIndicatorPreMadeTypes | PageIndicatorCaster) { 382 | if (typeof enabled === 'boolean' || (typeof enabled === 'string' && enabled === 'footer')) 383 | this.usePageIndicator = enabled; 384 | else throw new TypeError('setPageIndicator()\'s `enabled` parameter only accepts boolean/string type.'); 385 | 386 | if (fn) { 387 | const allowedTypes = [ 'text', 'textcompact', 'circle', 'hybrid' ]; 388 | 389 | if (typeof fn === 'string' && allowedTypes.includes(fn)) this._pageIndicator = this._defaultPageIndicators[fn]; 390 | else if (typeof fn === 'function') this._pageIndicator = fn; 391 | else throw new TypeError('setPageIndicator()\'s `fn` parameter only accepts function/string type.'); 392 | } 393 | 394 | return this; 395 | } 396 | 397 | /** 398 | * Sets whether the client's message will be deleted upon timeout. 399 | * @param deleteOnTimeout - Delete client's message upon timeout? 400 | */ 401 | public setDeleteOnTimeout (boolean: boolean) { 402 | if (typeof boolean !== 'boolean') throw new TypeError('deleteOnTimeout() only accepts boolean type.'); 403 | 404 | this.deleteOnTimeout = boolean; 405 | 406 | return this; 407 | } 408 | 409 | /** 410 | * Sets the client's message content. 411 | * @param text - The message content. 412 | * @param separator - The string to separate the content from the page indicator. 413 | */ 414 | public setContent (text: string, separator = '\n') { 415 | if (typeof separator !== 'string') 416 | throw new TypeError('setContent()\'s `separator` parameter only accepts string type.'); 417 | 418 | Object.assign(this.content, { text, separator }); 419 | 420 | return this; 421 | } 422 | 423 | /** 424 | * Evaluates the constructor and the client. 425 | * @ignore 426 | */ 427 | public async _verify () { 428 | this.setClientAssets(this.clientAssets); 429 | 430 | if (!this.channel) 431 | throw new Error('Cannot invoke PaginationEmbed class without a channel object set.'); 432 | 433 | if (!(this.page >= 1 && this.page <= this.pages)) 434 | throw new RangeError(`Page number is out of bounds. Max pages: ${this.pages}`); 435 | 436 | return this._checkPermissions(); 437 | } 438 | 439 | /** 440 | * Checks the permissions of the client before sending the embed. 441 | * @ignore 442 | */ 443 | public async _checkPermissions () { 444 | const channel = this.channel as TextChannel; 445 | 446 | if (channel.guild) { 447 | const missing = channel 448 | .permissionsFor(channel.client.user) 449 | .missing([ 'ADD_REACTIONS', 'EMBED_LINKS', 'VIEW_CHANNEL', 'SEND_MESSAGES' ]); 450 | 451 | if (missing.length) 452 | throw new Error(`Cannot invoke PaginationEmbed class without required permissions: ${missing.join(', ')}`); 453 | } 454 | 455 | return true; 456 | } 457 | 458 | /** 459 | * Returns whether the given navigation emoji is enabled. 460 | * @param emoji The navigation emoji to search. 461 | */ 462 | protected _enabled (emoji: NavigationEmojiIdentifier) { 463 | return this.disabledNavigationEmojis.includes('all') 464 | ? false 465 | : !this.disabledNavigationEmojis.includes(emoji); 466 | } 467 | 468 | /** Deploys emoji reacts for the message sent by the client. */ 469 | protected async _drawEmojis () { 470 | if (this.emojisFunctionAfterNavigation) { 471 | await this._drawNavigationEmojis(); 472 | await this._drawFunctionEmojis(); 473 | } else { 474 | await this._drawFunctionEmojis(); 475 | await this._drawNavigationEmojis(); 476 | } 477 | 478 | if (this.listenerCount('start')) 479 | this.emit('start'); 480 | 481 | return this._awaitResponse(); 482 | } 483 | 484 | /** Deploys function emojis. */ 485 | protected async _drawFunctionEmojis () { 486 | if (Object.keys(this.functionEmojis).length) 487 | for (const emoji of Object.keys(this.functionEmojis)) 488 | await this.clientAssets.message.react(emoji); 489 | } 490 | 491 | /** Deploys navigation emojis. */ 492 | protected async _drawNavigationEmojis () { 493 | if (this._enabled('back') && this.pages > 1) 494 | await this.clientAssets.message.react(this.navigationEmojis.back); 495 | if (this._enabled('jump') && this.pages > 2) 496 | await this.clientAssets.message.react(this.navigationEmojis.jump); 497 | if (this._enabled('forward') && this.pages > 1) 498 | await this.clientAssets.message.react(this.navigationEmojis.forward); 499 | if (this._enabled('delete')) 500 | await this.clientAssets.message.react(this.navigationEmojis.delete); 501 | } 502 | 503 | /** 504 | * Helper for intialising the MessageEmbed. 505 | * [For sub-class] Initialises the MessageEmbed. 506 | * @param callNavigation - Whether to call _drawEmojis(). 507 | * @ignore 508 | */ 509 | public _loadList (callNavigation = true) { 510 | if (callNavigation) return this._drawEmojis(); 511 | } 512 | 513 | /** 514 | * Calls PaginationEmbed.setPage() and then executes `_loadList()` and `_awaitResponse()`. 515 | * @param param - The page number to jump to. 516 | */ 517 | protected async _loadPage (param: number | 'back' | 'forward' = 1) { 518 | this.setPage(param); 519 | 520 | await this._loadList(false); 521 | 522 | return this._awaitResponse(); 523 | } 524 | 525 | /** Awaits the reaction from the user(s). */ 526 | protected async _awaitResponse (): Promise { 527 | const emojis = Object.values(this.navigationEmojis); 528 | const channel = this.clientAssets.message.channel as TextChannel; 529 | const filter = (r: MessageReaction, u: User) => { 530 | const enabledEmoji = this._enabled('all') 531 | ? !this._disabledNavigationEmojiValues.length 532 | || this._disabledNavigationEmojiValues.some(e => ![ r.emoji.name, r.emoji.id ].includes(e)) 533 | : false; 534 | const passedEmoji = 535 | (enabledEmoji && (emojis.includes(r.emoji.name) || emojis.includes(r.emoji.id))) || 536 | r.emoji.name in this.functionEmojis || r.emoji.id in this.functionEmojis; 537 | 538 | if (this.authorizedUsers.length) 539 | return this.authorizedUsers.includes(u.id) && passedEmoji; 540 | 541 | return !u.bot && passedEmoji; 542 | }; 543 | const clientMessage = this.clientAssets.message; 544 | 545 | try { 546 | const responses = await clientMessage.awaitReactions({ filter, max: 1, time: this.timeout, errors: [ 'time' ] }); 547 | const response = responses.first(); 548 | const user = response.users.cache.last(); 549 | const emoji = [ response.emoji.name, response.emoji.id ]; 550 | 551 | if (this.listenerCount('react')) this.emit('react', user, response.emoji); 552 | if (clientMessage.guild) { 553 | const missing = channel 554 | .permissionsFor(channel.client.user) 555 | .missing([ 'MANAGE_MESSAGES' ]); 556 | 557 | if (!missing.length) 558 | await response.users.remove(user); 559 | } 560 | 561 | switch (emoji[0] || emoji[1]) { 562 | case this.navigationEmojis.back: 563 | if (this.page === 1) return this._awaitResponse(); 564 | 565 | return this._loadPage('back'); 566 | 567 | case this.navigationEmojis.jump: 568 | if (this.pages <= 2) return this._awaitResponse(); 569 | 570 | return this._awaitResponseEx(user); 571 | 572 | case this.navigationEmojis.forward: 573 | if (this.page === this.pages) return this._awaitResponse(); 574 | 575 | return this._loadPage('forward'); 576 | 577 | case this.navigationEmojis.delete: 578 | await clientMessage.delete(); 579 | 580 | if (this.listenerCount('finish')) this.emit('finish', user); 581 | 582 | return; 583 | 584 | default: { 585 | const cb = this.functionEmojis[emoji[0]] || this.functionEmojis[emoji[1]]; 586 | 587 | try { 588 | await cb(user, this as unknown as Embeds | FieldsEmbed); 589 | } catch (err) { 590 | return this._cleanUp(err, clientMessage, false, user); 591 | } 592 | 593 | return this._loadPage(this.page); 594 | } 595 | } 596 | } catch (err) { 597 | return this._cleanUp(err, clientMessage); 598 | } 599 | } 600 | 601 | /** 602 | * Only used for `_awaitResponse`: 603 | * Deletes the client's message, and emits either `error` or `finish` event depending on the passed parameters. 604 | * @param err The error object. 605 | * @param clientMessage The client's message. 606 | * @param expired Whether the clean up is for `expired` event. 607 | * @param user The user object (only for `finish` event). 608 | */ 609 | protected async _cleanUp (err: any, clientMessage: Message, expired = true, user?: User) { 610 | const channel = this.clientAssets.message.channel as TextChannel; 611 | 612 | if (this.deleteOnTimeout && clientMessage.deletable) { 613 | await clientMessage.delete(); 614 | 615 | clientMessage.deleted = true; 616 | } 617 | if (clientMessage.guild && !clientMessage.deleted) { 618 | const missing = channel 619 | .permissionsFor(channel.client.user) 620 | .missing([ 'MANAGE_MESSAGES' ]); 621 | 622 | if (!missing.length) await clientMessage.reactions.removeAll(); 623 | } 624 | if (err instanceof Error) { 625 | if (this.listenerCount('error')) this.emit('error', err); 626 | 627 | return; 628 | } 629 | 630 | const eventType = expired ? 'expire' : 'finish'; 631 | 632 | if (this.listenerCount(eventType)) this.emit(eventType, user); 633 | } 634 | 635 | /** 636 | * Awaits the custom page input from the user. 637 | * @param user - The user who reacted to jump on a certain page. 638 | */ 639 | protected async _awaitResponseEx (user: User) { 640 | const cancel = [ '0', 'cancel' ]; 641 | const filter = (m: Message) => { 642 | const supposedPage = parseInt(m.content); 643 | 644 | return ( 645 | m.author.id === user.id && ( 646 | (!isNaN(Number(m.content)) && supposedPage !== this.page && supposedPage >= 1 && supposedPage <= this.pages) 647 | || cancel.includes(m.content.toLowerCase()) 648 | ) 649 | ); 650 | }; 651 | const channel = this.clientAssets.message.channel; 652 | const prompt = await channel 653 | .send(this.clientAssets.prompt.replace(/\{\{user\}\}/g, user.toString())) as Message; 654 | 655 | try { 656 | const responses = await channel.awaitMessages({ filter, max: 1, time: this.timeout, errors: [ 'time' ] }); 657 | const response = responses.first(); 658 | const content = response.content; 659 | const missing = (channel as TextChannel).permissionsFor(channel.client.user) 660 | .missing([ 'MANAGE_MESSAGES' ]); 661 | 662 | if (this.clientAssets.message.deleted) { 663 | if (this.listenerCount('error')) this.emit('error', new Error(MESSAGE_DELETED)); 664 | 665 | return; 666 | } 667 | 668 | await prompt.delete(); 669 | 670 | if (response.deletable) 671 | if (!missing.length) await response.delete(); 672 | 673 | if (cancel.includes(content)) return this._awaitResponse(); 674 | 675 | return this._loadPage(parseInt(content)); 676 | } catch (c) { 677 | if (prompt.deletable) await prompt.delete(); 678 | if (c instanceof Error) { 679 | if (this.listenerCount('error')) this.emit('error', c); 680 | 681 | return; 682 | } 683 | 684 | if (this.listenerCount('expire')) this.emit('expire'); 685 | } 686 | } 687 | 688 | /** 689 | * Emitted after the initial embed has been sent 690 | * (technically, after the client finished reacting with enabled navigation and function emojis). 691 | * @event 692 | */ 693 | public on (event: 'start', listener: () => void): this; 694 | 695 | /** 696 | * Emitted when the instance is finished by a user reacting with `delete` navigation emoji 697 | * or a function emoji that throws non-Error type. 698 | * @event 699 | */ 700 | public on (event: 'finish', listener: ListenerUser): this; 701 | 702 | /** 703 | * Emitted after the page number is updated and before the client sends the embed. 704 | * @event 705 | */ 706 | public on (event: 'pageUpdate', listener: () => void): this; 707 | 708 | /** 709 | * Emitted upon a user reacting on the instance. 710 | * @event 711 | */ 712 | public on (event: 'react', listener: ListenerReact): this; 713 | 714 | /** 715 | * Emitted when the awaiting timeout is reached. 716 | * @event 717 | */ 718 | public on (event: 'expire', listener: () => void): this; 719 | 720 | /** 721 | * Emitted upon an occurance of error. 722 | * @event 723 | */ 724 | // @ts-ignore 725 | public on (event: 'error', listener: ListenerError): this; 726 | 727 | /** @event */ 728 | public once (event: 'finish', listener: ListenerUser): this; 729 | 730 | /** @event */ 731 | public once (event: 'start' | 'expire' | 'pageUpdate', listener: () => void): this; 732 | 733 | /** @event */ 734 | public once (event: 'react', listener: ListenerReact): this; 735 | 736 | /** @event */ 737 | // @ts-ignore 738 | public once (event: 'error', listener: ListenerError): this; 739 | } 740 | 741 | /** @param user The user who responded to the instance. */ 742 | export type ListenerUser = (user: User) => void; 743 | 744 | /** 745 | * @param user The user who responded to the instance. 746 | * @param emoji The emoji that was reacted to the instance. 747 | */ 748 | export type ListenerReact = (user: User, emoji: Emoji) => void; 749 | 750 | /** @param err The error object. */ 751 | export type ListenerError = (err: Error) => void; 752 | 753 | /** Options for [[PaginationEmbed.disabledNavigationEmojis]]. */ 754 | export type DisabledNavigationEmojis = NavigationEmojiIdentifier[]; 755 | 756 | /** An object containing emojis to use as navigation emojis. */ 757 | export interface NavigationEmojis { 758 | back: string | '◀'; 759 | jump: string | '↗'; 760 | forward: string | '▶'; 761 | delete: string | '🗑'; 762 | } 763 | 764 | /** Assets for the client (message). */ 765 | export interface ClientAssets { 766 | /** The message object. */ 767 | message?: Message; 768 | /** 769 | * The text during a prompt for page jump. 770 | * 771 | * To include a user mention in the text, use `{{user}}` as placeholder. 772 | * 773 | * Default: `"{{user}}, To what page would you like to jump? Say 'cancel' or '0' to cancel the prompt."` 774 | */ 775 | prompt?: string; 776 | } 777 | 778 | /** Options for client's message content. */ 779 | export interface ClientMessageContent { 780 | /** The message content. */ 781 | text?: string; 782 | /** 783 | * The string to separate the content from the page indicator. 784 | * 785 | * Default: `\n` 786 | */ 787 | separator?: string; 788 | } 789 | 790 | export type NavigationEmojiIdentifier = 'back' | 'jump' | 'forward' | 'delete' | 'all'; 791 | 792 | /** 793 | * Function for a custom emoji. 794 | * 795 | * Example for stopping the instance from awaiting from further reacts: 796 | * ```js 797 | * (user, instance) => { 798 | * // Either 799 | * throw 'stopped'; 800 | * 801 | * // or 802 | * return Promise.reject('stopped'); 803 | * 804 | * // will stop the instance from awaiting reacts. 805 | * // Passing an error object will emit the `error` event. 806 | * }; 807 | * ``` 808 | */ 809 | export interface FunctionEmoji { 810 | [emojiNameOrID: string]: (user: User, instance: Embeds | FieldsEmbed) => any; 811 | } 812 | 813 | /** 814 | * Function to pass to the instance for page indicator formatting. 815 | * 816 | * Default: 817 | * ```js 818 | * (page, pages) => `Page ${page} of ${pages}` 819 | * ``` 820 | */ 821 | export type PageIndicatorCaster = (page: number, pages: number) => string; 822 | 823 | export type PageIndicatorPreMadeTypes = 'text' | 'textcompact' | 'circle' | 'hybrid'; 824 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Embeds'; 2 | export * from './FieldsEmbed'; 3 | export * from './base'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | export const { version }: { version: string } = require('../package.json'); 7 | -------------------------------------------------------------------------------- /test/bot.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('discord.js'); 2 | const PaginationEmbed = require('../'); 3 | 4 | const credentials = require('./credentials'); 5 | 6 | const bot = new Client({ 7 | intents: [ 8 | 'GUILDS', 9 | 'GUILD_MESSAGES', 10 | 'GUILD_MESSAGE_REACTIONS', 11 | 'DIRECT_MESSAGES', 12 | 'DIRECT_MESSAGE_REACTIONS', 13 | ] 14 | }); 15 | 16 | const error = msg => { 17 | console.error(msg); 18 | 19 | process.exit(1); 20 | }; 21 | 22 | const done = () => { 23 | console.log('Test done!'); 24 | 25 | process.exit(0); 26 | }; 27 | 28 | bot 29 | .on('ready', async () => { 30 | const channel = bot.channels.cache.get(credentials.channel); 31 | 32 | if (!channel) 33 | error('Provided channel is not resolvable by the client.'); 34 | 35 | console.log('Ready to test! Channel name:', channel.name, 'Channel ID:', channel.id); 36 | 37 | const { 38 | test, 39 | users, 40 | disabledNavigationEmojis, 41 | emojisFunctionAfterNavigation, 42 | deleteOnTimeout 43 | } = credentials; 44 | 45 | console.log('Mode:', test); 46 | 47 | if (test === 'embeds') { 48 | const embeds = []; 49 | 50 | for (let i = 1; i <= 3; ++i) 51 | embeds.push({ 52 | fields: [ { name: 'Page', value: i, inline: false } ], 53 | image: { url: 'attachment://1.jpg' } 54 | }); 55 | 56 | const Embeds = new PaginationEmbed.Embeds() 57 | .setArray(embeds) 58 | .setAuthorizedUsers(users) 59 | .setChannel(channel) 60 | .setTitle('Test Title') 61 | .setDescription('Test Description') 62 | .setPageIndicator(true, 'hybrid') 63 | .setContent('benis', '\n\n') 64 | .setFooter(`version: ${PaginationEmbed.version}`) 65 | .setURL('https://gazmull.github.io/discord-paginationembed') 66 | .setColor(0xFF00AE) 67 | .addField('Test Field 1', 'Test Field 1', true) 68 | .addField('Test Field 2', 'Test Field 2', true) 69 | .setDeleteOnTimeout(deleteOnTimeout) 70 | .setEmojisFunctionAfterNavigation(emojisFunctionAfterNavigation) 71 | .setDisabledNavigationEmojis(disabledNavigationEmojis) 72 | .setFunctionEmojis({ 73 | '⬆': (_, instance) => { 74 | for (const embed of instance.array) 75 | embed.fields[0].value++; 76 | }, 77 | '⬇': (_, instance) => { 78 | for (const embed of instance.array) 79 | embed.fields[0].value--; 80 | }, 81 | '⏹': () => Promise.reject('stopped'), 82 | '🔕': () => Promise.reject(new Error('Worst Error Ever')) 83 | }) 84 | .setClientAssets({ prompt: 'yAAAaA— what page {{user}}?' }) 85 | .on('start', () => console.log('Started!')) 86 | .on('finish', (user) => console.log(`Finished! User: ${user.username}`)) 87 | .on('react', (user, emoji) => 88 | console.log(`Reacted! User: ${user.username} | Emoji: ${emoji.name} (${emoji.id})`)) 89 | .on('pageUpdate', () => Embeds.currentEmbed.title = Embeds.pageIndicator) 90 | .on('expire', () => console.warn('Expired!')) 91 | .on('error', console.error); 92 | 93 | await Embeds.build(); 94 | 95 | return done(); 96 | } else if (test === 'fieldsembed') { 97 | const FieldsEmbed = new PaginationEmbed.FieldsEmbed() 98 | .setArray([ { name: 'John Doe' }, { name: 'Jane Doe' } ]) 99 | .setAuthorizedUsers(users) 100 | .setChannel(channel) 101 | .setElementsPerPage(1) 102 | .setPage(2) 103 | .setPageIndicator('footer', (page, pages) => `peij ${page} / ${pages}`) 104 | .formatField('Name', i => i.name) 105 | .setDeleteOnTimeout(deleteOnTimeout) 106 | .setDisabledNavigationEmojis(disabledNavigationEmojis) 107 | .setEmojisFunctionAfterNavigation(emojisFunctionAfterNavigation) 108 | .setFunctionEmojis({ 109 | '🔄': (user, instance) => { 110 | const field = instance.embed.fields[0]; 111 | 112 | if (field.name === 'Name') 113 | field.name = user.tag; 114 | else 115 | field.name = 'Name'; 116 | } 117 | }) 118 | .addFunctionEmoji('🅱', (_, instance) => { 119 | const field = instance.embed.fields[0]; 120 | 121 | if (field.name.includes('🅱')) 122 | field.name = 'Name'; 123 | else 124 | field.name = 'Na🅱e'; 125 | }); 126 | 127 | FieldsEmbed.embed 128 | .setColor(0xFF00AE) 129 | .setDescription('Test Description') 130 | .setFooter(`version: ${PaginationEmbed.version}`) 131 | .addField('Test Static Field', 'and its value'); 132 | 133 | await FieldsEmbed.build(); 134 | 135 | return done(); 136 | } else error('Invalid pagination mode. Either choose \'embeds\' or \'fieldsembed\''); 137 | }) 138 | .login(credentials.token); 139 | -------------------------------------------------------------------------------- /test/credentials.sample.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Your bot's token 3 | token: null, 4 | // The channel where to send the embed test 5 | channel: null, 6 | // Users authorized to interact with the utility 7 | users: [], 8 | // 'embeds' or 'fieldsembed' to test 9 | test: 'embeds', 10 | // 'all' / 'back' / 'jump' / 'foward' / 'delete', 11 | disabledNavigationEmojis: [ 'delete' ], 12 | // Whether function emojis should be deployed after navigation emojis 13 | emojisFunctionAfterNavigation: false, 14 | // Delete PaginationEmbed message after awaiting response timeout? 15 | deleteOnTimeout: true 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noImplicitThis": true, 8 | "noUnusedLocals": true, 9 | "outDir": "bin", 10 | "removeComments": false, 11 | "target": "es2017" 12 | }, 13 | "include": [ 14 | "src" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludeExternals": true, 3 | "excludeProtected": true, 4 | "excludeNotDocumented": true, 5 | "name": "Discord PaginationEmbed - Docs", 6 | "entryPoints": [ 7 | "src/base/index.ts", 8 | "src/Embeds.ts", 9 | "src/FieldsEmbed.ts" 10 | ], 11 | "out": "docs/master", 12 | "readme": "README.md", 13 | "theme": "node_modules/typedoc-neo-theme/bin/default" 14 | } 15 | -------------------------------------------------------------------------------- /typings/Embeds.d.ts: -------------------------------------------------------------------------------- 1 | import { ColorResolvable, EmbedField, EmbedFieldData, MessageEmbed } from 'discord.js'; 2 | import { PaginationEmbed } from './base'; 3 | /** 4 | * A pagination mode that uses an array of MessageEmbed to paginate. 5 | * @extends [[PaginationEmbed]] 6 | * @noInheritDoc 7 | */ 8 | export declare class Embeds extends PaginationEmbed { 9 | /** The title of all embeds. */ 10 | title: string; 11 | /** The description of all embeds. */ 12 | description: string; 13 | /** The URL of all embeds. */ 14 | url: string; 15 | /** The color of all embeds. */ 16 | color: number; 17 | /** The timestamp of all embeds. */ 18 | timestamp: number; 19 | /** The fields of all embeds. */ 20 | fields: EmbedField[]; 21 | /** The thumbnail of all embeds. */ 22 | thumbnail: string; 23 | /** The image of all embeds. */ 24 | image: string; 25 | /** The author of all embeds. */ 26 | author: { 27 | name: string; 28 | url?: string; 29 | iconURL?: string; 30 | }; 31 | /** The footer of all embeds. */ 32 | footer: { 33 | text: string; 34 | iconURL?: string; 35 | }; 36 | /** Embed in the current page. */ 37 | get currentEmbed(): MessageEmbed; 38 | get pages(): number; 39 | /** 40 | * Adds a field to the fields of all embeds. 41 | * @param name - The name of the field. 42 | * @param value - The value of the field. 43 | * @param inline - Whether the field is inline to the other fields. 44 | */ 45 | addField(name: string, value: string, inline?: boolean): this; 46 | addFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; 47 | /** 48 | * Build the Pagination Embeds. 49 | * 50 | * ### Example 51 | * ```js 52 | * const { Embeds } = require('discord-paginationembed'); 53 | * const { MessageEmbed } = require('discord.js'); 54 | * 55 | * // Under message event. 56 | * const embeds = []; 57 | * 58 | * for (let i = 0; i < 5; ++i) 59 | * embeds.push(new MessageEmbed().addField('Page', i + 1)); 60 | * 61 | * new Embeds() 62 | * .setAuthorizedUsers([message.author.id]) 63 | * .setChannel(message.channel) 64 | * .setClientAssets({ prompt: 'Yo {{user}} wat peige?!?!?' }) 65 | * .setArray(embeds) 66 | * .setPageIndicator(false) 67 | * .setPage(1) 68 | * .setTimeout(69000) 69 | * .setNavigationEmojis({ 70 | * back: '◀', 71 | * jump: '↗', 72 | * forward: '▶', 73 | * delete: '🗑' 74 | * }) 75 | * .setFunctionEmojis({ 76 | * '⬆': (_, instance) => { 77 | * for (const embed of instance.array) 78 | * embed.fields[0].value++; 79 | * }, 80 | * '⬇': (_, instance) => { 81 | * for (const embed of instance.array) 82 | * embed.fields[0].value--; 83 | * } 84 | * }) 85 | * .setDescription('This is one of my embeds with this message!') 86 | * .setColor(0xFF00AE) 87 | * .setTimestamp() 88 | * .build();``` 89 | */ 90 | build(): Promise; 91 | /** 92 | * Sets the array of MessageEmbed to paginate. 93 | * @param array - An array of MessageEmbed to paginate. 94 | */ 95 | setArray(array: MessageEmbed[]): this; 96 | /** 97 | * Set the author of all embeds. 98 | * @param name - The name of the author. 99 | * @param iconURL - The icon URL of the author. 100 | * @param url - The URL of the author. 101 | */ 102 | setAuthor(name: string, iconURL?: string, url?: string): this; 103 | /** 104 | * Sets the color of all embeds. 105 | * @param color - The color of all embeds. 106 | */ 107 | setColor(color: ColorResolvable): this; 108 | /** 109 | * Sets the description of all embeds. 110 | * @param description - The description of all embeds. 111 | */ 112 | setDescription(description: string): this; 113 | /** 114 | * Sets the footer of all embeds. 115 | * @param text - The footer text. 116 | * @param iconURL - URL for the footer's icon. 117 | */ 118 | setFooter(text: string, iconURL?: string): this; 119 | /** 120 | * Sets the image of all embeds. 121 | * @param url - The image of all embeds. 122 | */ 123 | setImage(url: string): this; 124 | /** 125 | * Sets the thumbnail of all embeds. 126 | * @param url - The thumbnail of all embeds. 127 | */ 128 | setThumbnail(url: string): this; 129 | /** 130 | * Sets the timestamp of all embeds. 131 | * @param timestamp - The timestamp or date. 132 | */ 133 | setTimestamp(timestamp?: Date | number): this; 134 | /** 135 | * Sets the title of all embeds. 136 | * @param title - The title of all embeds. 137 | */ 138 | setTitle(title: string): this; 139 | /** 140 | * Sets the URL of all embeds. 141 | * @param url - The URL of all embeds. 142 | */ 143 | setURL(url: string): this; 144 | /** 145 | * Removes, replaces, and inserts fields in all embeds (max 25). 146 | * @param index - The index to start at. 147 | * @param deleteCount - The number of fields to remove. 148 | * @param name - The name of the field. 149 | * @param value - The value of the field. 150 | * @param inline - Set the field to display inline. 151 | */ 152 | spliceFields(index: number, deleteCount: number, name?: string, value?: string, inline?: boolean): this; 153 | /** Transforms all embeds to plain objects. */ 154 | toJSON(): unknown[]; 155 | /** @ignore */ 156 | _loadList(callNavigation?: boolean): Promise; 157 | } 158 | -------------------------------------------------------------------------------- /typings/FieldsEmbed.d.ts: -------------------------------------------------------------------------------- 1 | import { MessageEmbed } from 'discord.js'; 2 | import { PaginationEmbed } from './base'; 3 | /** 4 | * A pagination mode that uses a MessageEmbed with a field(s) containing the elements to paginate. 5 | * @extends [[PaginationEmbed]] 6 | * @noInheritDoc 7 | */ 8 | export declare class FieldsEmbed extends PaginationEmbed { 9 | constructor(); 10 | /** Maximum number of elements to be displayed per page. */ 11 | elementsPerPage: number; 12 | /** 13 | * The MessageEmbed being used for this mode. 14 | * 15 | * ### Notice 16 | * To customise the MessageEmbed for this mode, please access this property. Example: 17 | * ```js 18 | * .embed.setColor('red') 19 | * ``` 20 | */ 21 | embed: MessageEmbed; 22 | /** Elements in the current page. */ 23 | get elementList(): Element[]; 24 | get pages(): number; 25 | /** 26 | * Build the Pagination Fields Embed. 27 | * 28 | * ### Example 29 | * ```js 30 | * const { FieldsEmbed } = require('discord-paginationembed'); 31 | * 32 | * // Under message event. 33 | * new FieldsEmbed() 34 | * .setAuthorizedUsers([message.author.id]) 35 | * .setChannel(message.channel) 36 | * .setClientAssets({ prompt: 'Yo {{user}} wat peige?!?!?' }) 37 | * .setArray([{ name: 'John Doe' }, { name: 'Jane Doe' }]) 38 | * .setElementsPerPage(1) 39 | * .setPageIndicator(false) 40 | * .formatField('Name', el => el.name) 41 | * .setPage(1) 42 | * .setTimeout(69000) 43 | * .setNavigationEmojis({ 44 | * back: '◀', 45 | * jump: '↗', 46 | * forward: '▶', 47 | * delete: '🗑' 48 | * }) 49 | * .setFunctionEmojis({ 50 | * '🔄': (user, instance) => { 51 | * const field = instance.embed.fields[0]; 52 | * 53 | * if (field.name === 'Name') 54 | * field.name = user.tag; 55 | * else 56 | * field.name = 'Name'; 57 | * } 58 | * }) 59 | * .build();``` 60 | */ 61 | build(): Promise; 62 | /** 63 | * Adds a field to the embed. 64 | * Same as MessageEmbed.addField, but value takes a function instead. 65 | * @param name - Name of the field. 66 | * @param value - Value of the field. Function for `Array.prototype.map().join('\n')`. 67 | * @param inline - Whether the field is inline with other field. Default: `true` 68 | */ 69 | formatField(name: string, value: (element: Element, index?: number, array?: Element[]) => any, inline?: boolean): this; 70 | /** 71 | * Sets the maximum number of elements to be displayed per page. 72 | * @param max - Maximum number of elements to be displayed per page. 73 | */ 74 | setElementsPerPage(max: number): this; 75 | protected _drawList(): Promise; 76 | /** @ignore */ 77 | _loadList(callNavigation?: boolean): Promise; 78 | } 79 | -------------------------------------------------------------------------------- /typings/base/index.d.ts: -------------------------------------------------------------------------------- 1 | /** @module PaginationEmbed */ 2 | /// 3 | import { DMChannel, Emoji, Message, NewsChannel, Snowflake, TextChannel, User } from 'discord.js'; 4 | import { EventEmitter } from 'events'; 5 | import { Embeds } from '../Embeds'; 6 | import { FieldsEmbed } from '../FieldsEmbed'; 7 | /** 8 | * The base class for Pagination Modes. **Do not invoke**. 9 | * @extends [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) 10 | * @noInheritDoc 11 | */ 12 | export declare abstract class PaginationEmbed extends EventEmitter { 13 | constructor(); 14 | /** The authorized users to navigate the pages. Default: `everyone` */ 15 | authorizedUsers: Snowflake[]; 16 | /** The channel where to send the embed. */ 17 | channel: TextChannel | DMChannel | NewsChannel; 18 | /** Settings for assets for the client. */ 19 | clientAssets: ClientAssets; 20 | /** An array of elements to paginate. */ 21 | array: Element[]; 22 | /** 23 | * Whether to show page indicator, or put it in embed's footer text (replaces the existing text) instead. 24 | * Default: `false` 25 | */ 26 | usePageIndicator: boolean | 'footer'; 27 | /** Whether the client's message will be deleted upon timeout. Default: `false` */ 28 | deleteOnTimeout: boolean; 29 | /** The current page. Default: `1` */ 30 | page: number; 31 | /** The time for awaiting a user action before timeout in ms. Default: `30000` */ 32 | timeout: number; 33 | /** 34 | * The emojis used for navigation emojis. 35 | * Navigation emojis are the default message reactions for navigating through the pagination. 36 | */ 37 | navigationEmojis: NavigationEmojis; 38 | /** 39 | * The emojis used for function emojis. 40 | * Function emojis are user-customised message reactions 41 | * for modifying the current instance of the pagination such as modifying embed texts, stopping the pagination, etc. 42 | */ 43 | functionEmojis: FunctionEmoji; 44 | /** 45 | * The disabled navigation emojis. 46 | * Available navigation emojis to disable: 47 | * - 'back' 48 | * - 'jump' 49 | * - 'forward' 50 | * - 'delete' 51 | * - 'all' 52 | */ 53 | disabledNavigationEmojis: ('back' | 'jump' | 'forward' | 'delete' | 'all')[]; 54 | /** Whether to set function emojis after navigation emojis. Default: `false` */ 55 | emojisFunctionAfterNavigation: boolean; 56 | /** Number of pages for this instance. */ 57 | abstract pages: number; 58 | /** The client's message content options. */ 59 | content: ClientMessageContent; 60 | /** The disabled navigation emojis (in values). */ 61 | protected _disabledNavigationEmojiValues: any[]; 62 | /** The default navigation emojis. Used for resetting the navigation emojis. */ 63 | protected _defaultNavigationEmojis: { 64 | back: string; 65 | jump: string; 66 | forward: string; 67 | delete: string; 68 | }; 69 | /** The function for formatting page indicator. */ 70 | protected _pageIndicator: PageIndicatorCaster; 71 | /** Pre-made functions for formatting page indicator. */ 72 | protected _defaultPageIndicators: { 73 | [x: string]: PageIndicatorCaster; 74 | }; 75 | /** The formatted page indicator. Default format: `text` */ 76 | get pageIndicator(): string; 77 | build(): void; 78 | /** 79 | * Adds a function emoji to the embed. 80 | * 81 | * ### Example 82 | * ```js 83 | * .addFunctionEmoji('🅱', (_, instance) => { 84 | * const field = instance.embed.fields[0]; 85 | * 86 | * if (field.name.includes('🅱')) 87 | * field.name = 'Name'; 88 | * else 89 | * field.name = 'Na🅱e'; 90 | * }); 91 | * ``` 92 | * @param emoji - The emoji to use as the function's emoji. 93 | * @param callback - The function to call upon pressing the function emoji. 94 | */ 95 | addFunctionEmoji(emoji: string, callback: (user: User, instance: Embeds | FieldsEmbed) => any): this; 96 | /** 97 | * Deletes a function emoji. 98 | * @param emoji - The emoji key to delete. 99 | */ 100 | deleteFunctionEmoji(emoji: string): this; 101 | /** Deletes all function emojis, and then re-enables all navigation emojis. */ 102 | resetEmojis(): this; 103 | /** 104 | * Sets the array of elements to paginate. This must be called first before any other methods. 105 | * @param array - An array of elements to paginate. 106 | */ 107 | setArray(array: Element[]): this; 108 | /** 109 | * Set the authorized users to navigate the pages. 110 | * @param users - A user ID or an array of user IDs. 111 | */ 112 | setAuthorizedUsers(users: Snowflake | Snowflake[]): this; 113 | /** 114 | * The channel where to send the embed. 115 | * @param channel - The channel object. 116 | */ 117 | setChannel(channel: TextChannel | DMChannel | NewsChannel): this; 118 | /** 119 | * Sets the settings for assets for the client. 120 | * @param assets - The assets for the client. 121 | */ 122 | setClientAssets(assets: ClientAssets): this; 123 | /** 124 | * Sets the disabled navigation emojis. 125 | * 126 | * ### Example 127 | * ```js 128 | * // Disable specific navigation emojis 129 | * .setDisabledNavigationEmojis(['delete', 'jump']); 130 | * 131 | * // Disable all navigation emojis 132 | * .setDisabledNavigationEmojis(['all']); 133 | * ``` 134 | * 135 | * @param emojis - An array of navigation emojis to disable. 136 | */ 137 | setDisabledNavigationEmojis(emojis: DisabledNavigationEmojis): this; 138 | /** 139 | * Sets whether to set function emojis after navigation emojis. 140 | * @param boolean - Set function emojis after navigation emojis? 141 | */ 142 | setEmojisFunctionAfterNavigation(boolean: boolean): this; 143 | /** 144 | * Sets the emojis used for function emojis. 145 | * 146 | * ### Example 147 | * ```js 148 | * .setFunctionEmojis({ 149 | * '🔄': (user, instance) => { 150 | * const field = instance.embed.fields[0]; 151 | * 152 | * if (field.name === 'Name') 153 | * field.name = user.tag; 154 | * else 155 | * field.name = 'Name'; 156 | * } 157 | * }); 158 | * ``` 159 | * @param emojis - An object containing customised emojis to use as function emojis. 160 | */ 161 | setFunctionEmojis(emojis: FunctionEmoji): this; 162 | /** 163 | * Sets the emojis used for navigation emojis. 164 | * @param emojis - An object containing customised emojis to use as navigation emojis. 165 | */ 166 | setNavigationEmojis(emojis: NavigationEmojis): this; 167 | /** 168 | * Sets to jump to a certain page upon calling PaginationEmbed.build(). 169 | * @param page - The page number to jump to. 170 | */ 171 | setPage(page: number | 'back' | 'forward'): this; 172 | /** 173 | * Sets the time for awaiting a user action before timeout in ms. 174 | * @param timeout Timeout value in ms. 175 | */ 176 | setTimeout(timeout: number): this; 177 | /** 178 | * Sets the page indicator formatting function and placement. 179 | * @param enabled - Whether to show page indicator. 180 | * Pass `footer` to display the indicator in embed's footer text (replaces existing text) instead. 181 | * @param fn - Function for indicator formatting. 182 | */ 183 | setPageIndicator(enabled: boolean | 'footer', fn?: PageIndicatorPreMadeTypes | PageIndicatorCaster): this; 184 | /** 185 | * Sets whether the client's message will be deleted upon timeout. 186 | * @param deleteOnTimeout - Delete client's message upon timeout? 187 | */ 188 | setDeleteOnTimeout(boolean: boolean): this; 189 | /** 190 | * Sets the client's message content. 191 | * @param text - The message content. 192 | * @param separator - The string to separate the content from the page indicator. 193 | */ 194 | setContent(text: string, separator?: string): this; 195 | /** 196 | * Evaluates the constructor and the client. 197 | * @ignore 198 | */ 199 | _verify(): Promise; 200 | /** 201 | * Checks the permissions of the client before sending the embed. 202 | * @ignore 203 | */ 204 | _checkPermissions(): Promise; 205 | /** 206 | * Returns whether the given navigation emoji is enabled. 207 | * @param emoji The navigation emoji to search. 208 | */ 209 | protected _enabled(emoji: NavigationEmojiIdentifier): boolean; 210 | /** Deploys emoji reacts for the message sent by the client. */ 211 | protected _drawEmojis(): Promise; 212 | /** Deploys function emojis. */ 213 | protected _drawFunctionEmojis(): Promise; 214 | /** Deploys navigation emojis. */ 215 | protected _drawNavigationEmojis(): Promise; 216 | /** 217 | * Helper for intialising the MessageEmbed. 218 | * [For sub-class] Initialises the MessageEmbed. 219 | * @param callNavigation - Whether to call _drawEmojis(). 220 | * @ignore 221 | */ 222 | _loadList(callNavigation?: boolean): Promise; 223 | /** 224 | * Calls PaginationEmbed.setPage() and then executes `_loadList()` and `_awaitResponse()`. 225 | * @param param - The page number to jump to. 226 | */ 227 | protected _loadPage(param?: number | 'back' | 'forward'): Promise; 228 | /** Awaits the reaction from the user(s). */ 229 | protected _awaitResponse(): Promise; 230 | /** 231 | * Only used for `_awaitResponse`: 232 | * Deletes the client's message, and emits either `error` or `finish` event depending on the passed parameters. 233 | * @param err The error object. 234 | * @param clientMessage The client's message. 235 | * @param expired Whether the clean up is for `expired` event. 236 | * @param user The user object (only for `finish` event). 237 | */ 238 | protected _cleanUp(err: any, clientMessage: Message, expired?: boolean, user?: User): Promise; 239 | /** 240 | * Awaits the custom page input from the user. 241 | * @param user - The user who reacted to jump on a certain page. 242 | */ 243 | protected _awaitResponseEx(user: User): Promise; 244 | /** 245 | * Emitted after the initial embed has been sent 246 | * (technically, after the client finished reacting with enabled navigation and function emojis). 247 | * @event 248 | */ 249 | on(event: 'start', listener: () => void): this; 250 | /** 251 | * Emitted when the instance is finished by a user reacting with `delete` navigation emoji 252 | * or a function emoji that throws non-Error type. 253 | * @event 254 | */ 255 | on(event: 'finish', listener: ListenerUser): this; 256 | /** 257 | * Emitted after the page number is updated and before the client sends the embed. 258 | * @event 259 | */ 260 | on(event: 'pageUpdate', listener: () => void): this; 261 | /** 262 | * Emitted upon a user reacting on the instance. 263 | * @event 264 | */ 265 | on(event: 'react', listener: ListenerReact): this; 266 | /** 267 | * Emitted when the awaiting timeout is reached. 268 | * @event 269 | */ 270 | on(event: 'expire', listener: () => void): this; 271 | /** 272 | * Emitted upon an occurance of error. 273 | * @event 274 | */ 275 | on(event: 'error', listener: ListenerError): this; 276 | /** @event */ 277 | once(event: 'finish', listener: ListenerUser): this; 278 | /** @event */ 279 | once(event: 'start' | 'expire' | 'pageUpdate', listener: () => void): this; 280 | /** @event */ 281 | once(event: 'react', listener: ListenerReact): this; 282 | /** @event */ 283 | once(event: 'error', listener: ListenerError): this; 284 | } 285 | /** @param user The user who responded to the instance. */ 286 | export declare type ListenerUser = (user: User) => void; 287 | /** 288 | * @param user The user who responded to the instance. 289 | * @param emoji The emoji that was reacted to the instance. 290 | */ 291 | export declare type ListenerReact = (user: User, emoji: Emoji) => void; 292 | /** @param err The error object. */ 293 | export declare type ListenerError = (err: Error) => void; 294 | /** Options for [[PaginationEmbed.disabledNavigationEmojis]]. */ 295 | export declare type DisabledNavigationEmojis = NavigationEmojiIdentifier[]; 296 | /** An object containing emojis to use as navigation emojis. */ 297 | export interface NavigationEmojis { 298 | back: string | '◀'; 299 | jump: string | '↗'; 300 | forward: string | '▶'; 301 | delete: string | '🗑'; 302 | } 303 | /** Assets for the client (message). */ 304 | export interface ClientAssets { 305 | /** The message object. */ 306 | message?: Message; 307 | /** 308 | * The text during a prompt for page jump. 309 | * 310 | * To include a user mention in the text, use `{{user}}` as placeholder. 311 | * 312 | * Default: `"{{user}}, To what page would you like to jump? Say 'cancel' or '0' to cancel the prompt."` 313 | */ 314 | prompt?: string; 315 | } 316 | /** Options for client's message content. */ 317 | export interface ClientMessageContent { 318 | /** The message content. */ 319 | text?: string; 320 | /** 321 | * The string to separate the content from the page indicator. 322 | * 323 | * Default: `\n` 324 | */ 325 | separator?: string; 326 | } 327 | export declare type NavigationEmojiIdentifier = 'back' | 'jump' | 'forward' | 'delete' | 'all'; 328 | /** 329 | * Function for a custom emoji. 330 | * 331 | * Example for stopping the instance from awaiting from further reacts: 332 | * ```js 333 | * (user, instance) => { 334 | * // Either 335 | * throw 'stopped'; 336 | * 337 | * // or 338 | * return Promise.reject('stopped'); 339 | * 340 | * // will stop the instance from awaiting reacts. 341 | * // Passing an error object will emit the `error` event. 342 | * }; 343 | * ``` 344 | */ 345 | export interface FunctionEmoji { 346 | [emojiNameOrID: string]: (user: User, instance: Embeds | FieldsEmbed) => any; 347 | } 348 | /** 349 | * Function to pass to the instance for page indicator formatting. 350 | * 351 | * Default: 352 | * ```js 353 | * (page, pages) => `Page ${page} of ${pages}` 354 | * ``` 355 | */ 356 | export declare type PageIndicatorCaster = (page: number, pages: number) => string; 357 | export declare type PageIndicatorPreMadeTypes = 'text' | 'textcompact' | 'circle' | 'hybrid'; 358 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './Embeds'; 2 | export * from './FieldsEmbed'; 3 | export * from './base'; 4 | export declare const version: string; 5 | --------------------------------------------------------------------------------