Tag Archives: javascript
JavaScript || How To Scroll To An Element & Add Automatic Anchor Scroll Using Vanilla JavaScript

The following is a module with functions that demonstrates how to scroll to a specific HTML element, as well as setting anchors to have a smooth scroll effect on button click.
1. Scroll To Element
The example below demonstrates how to scroll to a specific element.
1 2 3 4 5 6 7 |
// Scroll to a specific element <script> (() => { Utils.scrollToElement("#examples"); })(); </script> |
The ‘Utils.scrollToElement‘ function accepts either a JavaScript element or a string selector of the element to scroll to.
2. Add Scroll To Anchor Target
The example below demonstrates how to automatically add a scrolling effect to an anchor target.
1 2 3 4 5 6 7 |
// Scroll to anchor target <script> (() => { Utils.registerAnchorScroll(); })(); </script> |
The ‘Utils.registerAnchorScroll‘ function gets all the anchor links on a page and registers smooth scrolling on button click using ‘Utils.scrollToElement‘.
3. Utils Namespace
The following is the Utils.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 7, 2020 // Taken From: http://programmingnotes.org/ // File: Utils.js // Description: Javascript that handles general utility functions // ============================================================================ /** * NAMESPACE: Utils * USE: Handles general utility functions. */ var Utils = Utils || {}; (function(namespace) { 'use strict'; // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: registerAnchorScroll * USE: Gets all the anchor links on a page and registers smooth scrolling * on button click. * @return: N/A. */ exposed.registerAnchorScroll = () => { let anchorLinks = document.querySelectorAll('a[href^="#"]'); for (let link of anchorLinks) { link.addEventListener('click', (e) => { let hashval = link.getAttribute('href'); let target = document.querySelector(hashval) || document.querySelector(`[name='${hashval.slice(1)}']`); if (target) { exposed.scrollToElement(target); history.pushState(null, null, hashval); e.preventDefault(); } }); } } /** * FUNCTION: scrollToElement * USE: Scrolls the page to the given element. * @param target: A JavaScript element or a string selector of the * element to scroll to. * @param behavior: The scroll behavior. Defaut is 'smooth' * @return: N/A. */ exposed.scrollToElement = (target, behavior = 'smooth') => { if (typeof target === 'string') { target = document.querySelector(target); } target.scrollIntoView({ behavior: behavior, block: 'start' }); } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Utils)); // http://programmingnotes.org/ |
4. More Examples
Below are more examples demonstrating the use of ‘Utils.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Aug 7, 2020 // Taken From: http://programmingnotes.org/ // File: scrollDemo.html // Description: Demonstrates the use of Utils.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Utils.js Demo</title> <style> .space {margin-top: 1000px;} </style> <!-- // Include module --> <script type="text/javascript" src="./Utils.js"></script> </head> <body> <blockquote><code> <a href="#basicUsage">1. Basic Usage</a> <a href="#examples">2. Examples</a> </code></blockquote> <div class="space"></div> <h3 id="basicUsage"> 1. Basic Usage </h3> <div class="space"></div> <h3 id="examples"> 2. Examples </h3> <div class="space"></div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { Utils.registerAnchorScroll(); //Utils.scrollToElement("#examples"); }); </script> </body> </html> <!-- http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript/CSS/HTML || TablePagination.js – Simple Add Pagination To Any Table Using Vanilla JavaScript

The following is a module which allows for simple yet fully customizable table pagination in vanilla JavaScript.
With multiple options, as well as multiple callback functions to modify the pagination buttons and information text, almost all the ways you can think of pagination is supported by this module. It’s look and feel can also be adjusted via CSS.
Contents
1. Basic Usage
2. Available Options
3. Only Show Page Numbers
4. Mini - Only Show Arrow Buttons
5. Show All Pages
6. Only Show Go Input
7. Format Navigation Text
8. Button Click Event
9. Navigation Position
10. Specify Initial Page
11. Navigation Binding Area
12. Remove Pagination
13. TablePagination.js & CSS Source
14. More Examples
Syntax is very straightforward. The following demonstrates adding pagination to a table.
Calling ‘TablePagination.page‘ with no options will add pagination with all of the default options applied to it. It accepts one or more HTMLTableElement.
1 2 3 4 5 6 7 8 |
// Add table pagination. <script> (() => { // Adds table pagination to the table elements with the default options TablePagination.page(document.querySelectorAll('table')); })(); </script> |
2. Available Options
The options supplied to the ‘TablePagination.page‘ function is an object that is made up of the following properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// Available options TablePagination.page({ table: document.querySelector('table'), // One or more Javascript elements of the tables to page. rowsPerPage: 4, // Optional. The number of rows per page. Default is 5 initialPage: null, // Optional. The initial page to display. Possible values: Numeric value or 'first/last'. Default is page 1 navigationPosition: 'bottom', // Optional. The navigation position. Possible values: 'top/bottom'. Default is 'bottom' showFirstPageButton: true, // Optional. Specifies if the first page button is shown. Default is true showLastPageButton: true, // Optional. Specifies if the last page button is shown. Default is true showPreviousPageButton: true, // Optional. Specifies if the previous page button is shown. Default is true showNextPageButton: true, // Optional. Specifies if the next page button is shown. Default is true showPageNumberButtons: true, // Optional. Specifies if the page number buttons are shown. Default is true showNavigationInput: true, // Optional. Specifies if the 'Go' search functionality is shown. Default is true showNavigationInfoText: true, // Optional. Specifies if the navigation info text is shown. Default is true visiblePageNumberButtons: 4, // Optional. The maximum number of visible page number buttons. Default is 3. Set to null to show all buttons onButtonClick: (pageNumber, event) => { // Optional. Function that allows to do something on button click //window.location.href = "#page=" + pageNumber; }, onButtonTextRender: (text, desc) => { // Optional. Function that allows to format the button text //console.log(`Button Text: ${text}`); //console.log(`Button Description: ${desc}`); return text; }, onButtonTitleRender: (title, desc) => { // Optional. Function that allows to format the button title //console.log(`Button Text: ${text}`); //console.log(`Button Description: ${desc}`); return title; }, onNavigationInfoTextRender: (text, rowInfo) => { // Optional. Function that allows to format the navigation info text //console.log(`Navigation Text: ${text}`); //console.log(`Row Info:`, rowInfo); return text; }, navigationBindTo: null, // Optional. Javascript element of the container where the navigation controls are bound to. // If not specified, default destination is above or below the table element, depending on // the 'navigationPosition' value }); |
Supplying different options to ‘TablePagination.page‘ can change its appearance. The following examples below demonstrate this.
3. Only Show Page Numbers
The following example demonstrates pagination with only page numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Pagination with only page numbers. <script> (() => { // Only show page numbers TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, showFirstPageButton: false, showLastPageButton: false, showNavigationInput: false, }); })(); </script> |
4. Mini – Only Show Arrow Buttons
The following example demonstrates pagination with only arrow buttons.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Pagination with only arrow buttons. <script> (() => { // Only show arrow buttons TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, showFirstPageButton: false, showLastPageButton: false, showPageNumberButtons: false, showNavigationInput: false, }); })(); </script> |
5. Show All Pages
The following example demonstrates pagination showing all page numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Pagination showing all page numbers. <script> (() => { // Show all page numbers TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 8, showFirstPageButton: false, showLastPageButton: false, showPreviousPageButton: false, showNextPageButton: false, showNavigationInput: false, visiblePageNumberButtons: null }); })(); </script> |
6. Only Show Go Input
The following example demonstrates pagination with only showing the “Go” user input option.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Pagination with only showing go input. <script> (() => { // Only showing go input TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, showFirstPageButton: false, showLastPageButton: false, showPreviousPageButton: false, showNextPageButton: false, showPageNumberButtons: false, }); })(); </script> |
7. Format Navigation Text
The following example demonstrates pagination formatting the navigation text. This allows to alter the navigation text without having to modify the pagination code!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// Pagination formatting the navigation text. <script> (() => { // Pagination formatting the navigation text TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, onButtonTextRender: (text, desc) => { switch (desc) { case TablePagination.settings.onRenderDescFirstPage: break case TablePagination.settings.onRenderDescPrevPage: break case TablePagination.settings.onRenderDescPageNumber: text += ' - Test'; break; case TablePagination.settings.onRenderDescNextPage: break case TablePagination.settings.onRenderDescLastPage: break case TablePagination.settings.onRenderDescGoInput: break } return text; }, onButtonTitleRender: (title, desc) => { switch (desc) { case TablePagination.settings.onRenderDescFirstPage: break case TablePagination.settings.onRenderDescPrevPage: break case TablePagination.settings.onRenderDescPageNumber: title += ' - Test'; break; case TablePagination.settings.onRenderDescNextPage: break case TablePagination.settings.onRenderDescLastPage: break case TablePagination.settings.onRenderDescGoInput: break } return title; }, onNavigationInfoTextRender: (text, rowInfo) => { text += ' - Test' return text; }, }); })(); </script> |
8. Button Click Event
The following example demonstrates pagination with the button click callback. This allows you to do something on button click
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Pagination onButtonClick callback. <script> (() => { // onButtonClick callback allows you to do something on button click TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, onButtonClick: (pageNumber, event) => { alert(pageNumber); window.location.href = "#page=" + pageNumber; }, }); })(); </script> |
9. Navigation Position
The following example demonstrates formatting the navigation position. The position options can be on top, or bottom. Bottom is the default position.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Pagination navigation position. <script> (() => { // Navigation position - top TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, navigationPosition: 'top' }); })(); </script> |
10. Specify Initial Page
The following example demonstrates pagination setting the initial default page.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Pagination with initial page set. <script> (() => { // Set initial page TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, initialPage: 3 }); })(); </script> |
11. Navigation Binding Area
By default, the navigation controls are placed either above or below the table element, depending on the ‘navigationPosition’ value. You can override this placement by specifying the container where the controls should be bound to.
The following example demonstrates specifying the element where the controls are bound to.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Navigation Binding Area. <script> (() => { // Navigation Binding Area TablePagination.page({ table: document.querySelectorAll('table'), rowsPerPage: 2, navigationPosition: 'bottom', navigationBindTo: document.querySelector('#binding-section') }); })(); </script> |
12. Remove Pagination
The following example demonstrates how to remove pagination from a table.
‘TablePagination.remove‘ will remove pagination. It accepts one or more HTMLTableElement.
1 2 3 4 5 6 7 8 |
// Remove table pagination. <script> (() => { // Removes table pagination from the table elements TablePagination.remove(document.querySelectorAll('table')); })(); </script> |
13. TablePagination.js & CSS Source
The following is the TablePagination.js Namespace & CSS Source. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 |
// ============================================================================ // Author: Kenneth Perkins // Date: Aug 5, 2020 // Taken From: http://programmingnotes.org/ // File: TablePagination.js // Description: Namespace which handles html table pagination // Example: // // Add Pagination to a table // TablePagination.page({ // table: document.querySelector('table'), // rowsPerPage: 10 // }); // ============================================================================ /** * NAMESPACE: TablePagination * USE: Handles html table pagination. */ var TablePagination = TablePagination || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; // Set class names and other shared data const settings = { // Misc settings defaultRowsPerPage: 5, defaultPosition: 'bottom', defaultVisibleButtons: 3, onRenderDescFirstPage: 'first_page', onRenderDescPrevPage: 'previous_page', onRenderDescPageNumber: 'page_number', onRenderDescNextPage: 'next_page', onRenderDescLastPage: 'last_page', onRenderDescGoInput: 'go_input', buttonOptionFirst: 'first', buttonOptionPrevious: 'previous', buttonOptionNext: 'next', buttonOptionLast: 'last', navigationPositionTop: 'top', navigationPositionBottom: 'bottom', onTextRender: (text) => text, // Element class names classNameHide: '.pagination-hide', classNameButton: 'pagination-btn', classNameButtonActive: 'active', classNameButtonFirst: 'first', classNameButtonPrevious: 'previous', classNameButtonPageNumber: 'page-number', classNameButtonNext: 'next', classNameButtonLast: 'last', classNameButtonGo: 'go', classNameButtonHide: 'hide', classNameNavigation: 'pagination-navigation', classNameNavigationTop: 'top', classNameNavigationBottom: 'bottom', classNameNavigationInfoSection: 'pagination-navigation-info-section', classNameNavigationControlSection: 'pagination-navigation-control-section', classNameNavigationButtonSection: 'pagination-navigation-button-section', classNameNavigationInputSection: 'pagination-navigation-input-section', classNameNavigationInput: 'pagination-navigation-input', // Element data names dataNamePageNumber: 'data-pageNumber', dataNameTotalPages: 'data-totalPages', dataNameRowsPerPage: 'data-rowsPerPage', dataNameCurrentPageNumber: 'data-currentPageNumber', dataNameNavigationId: 'data-navigationId', dataNameNavigationInfoId: 'data-navigationInfoId', dataNameNavigationInputId: 'data-navigationInputId', dataNameNavigationButtonsId: 'data-navigationButtonsId', dataNameVisiblePageButtons: 'data-visiblePageButtons', cleanClassName: (str) => { return str ? str.trim().replace('.', '') : ''; }, }; exposed.settings = settings; /** * FUNCTION: page * USE: Initializes & renders pagination for the given table elements * @param options: An object of initialization options. * Its made up of the following properties: * { * table: One or more Javascript elements of the tables to page. * rowsPerPage: Optional. The number of rows per page. Default is page 5 * initialPage: Optional. The initial page to display. Posible values: Numeric value * or 'first/last'. Default is page 1 * navigationPosition: Optional. The navigation position. Posible values: 'top/bottom'. * Default is 'bottom' * showFirstPageButton: Optional. Boolean that indicates if the first page * button should be displayed. Default is true * showLastPageButton: Optional. Boolean that indicates if the last page * button should be displayed. Default is true * showPreviousPageButton: Optional. Boolean that indicates if the previous page * button should be displayed. Default is true * showNextPageButton: Optional. Boolean that indicates if the next page * button should be displayed. Default is true * showPageNumberButtons: Optional. Boolean that indicates if the page number buttons * should be displayed. Default is true * showNavigationInput: Optional. Specifies if the 'Go' search functionality is shown. * Default is true * showNavigationInfoText: Optional. Specifies if the navigation info text is shown. * Default is true * visiblePageNumberButtons: Optional. The maximum number of visible page number buttons. * Default is 3. Set to null to show all buttons * onButtonClick(pageNumber, event): Optional. Function that allows to do something on * button click * onButtonTextRender(text, desc): Optional. Function that allows to format the button text * onButtonTitleRender(title, desc): Optional. Function that allows to format the button title * onNavigationInfoTextRender(text, rowInfo): Optional. Function that allows to format * the navigation info text * navigationBindTo: Optional. Javascript element of the container where the navigation controls * are bound to. If not specified, default destination is above or below the table element, * depending on the 'navigationPosition' value * } * @return: N/A. */ exposed.page = (options) => { // Make sure the required options are valid if (isNull(options)) { // Check to see if there are options throw new TypeError('There are no options specified.'); } else if (typeof options !== 'object' || isElement(options) || isArrayLike(options)) { // Check to see if a table is specified let table = options; options = {}; options.table = table; } // Make sure the table is an array if (!isArrayLike(options.table)) { options.table = [options.table]; } // Make sure additional options are valid if (!isNull(options.rowsPerPage) && !isNumeric(options.rowsPerPage)) { // Check to see if a rowsPerPage is valid throw new TypeError(`Unable to process rowsPerPage of type: ${typeof options.rowsPerPage}. Reason: '${options.rowsPerPage}' is not a numeric value.`); } else if (!isNull(options.navigationBindTo) && !isElement(options.navigationBindTo)) { // Check to see if the navigation bind to element is an HTMLElement throw new TypeError(`Unable to process navigationBindTo of type: ${typeof options.navigationBindTo}. Reason: '${options.navigationBindTo}' is not an HTMLElement.`); } else if (!isNull(options.onButtonTitleRender) && !isFunction(options.onButtonTitleRender)) { // Check to see if callback is a function throw new TypeError(`Unable to call onButtonTitleRender of type: ${typeof options.onButtonTitleRender}. Reason: '${options.onButtonTitleRender}' is not a function.`); } else if (!isNull(options.onButtonTextRender) && !isFunction(options.onButtonTextRender)) { // Check to see if callback is a function throw new TypeError(`Unable to call onButtonTextRender of type: ${typeof options.onButtonTextRender}. Reason: '${options.onButtonTextRender}' is not a function.`); } else if (!isNull(options.onButtonClick) && !isFunction(options.onButtonClick)) { // Check to see if callback is a function throw new TypeError(`Unable to call onButtonClick of type: ${typeof options.onButtonClick}. Reason: '${options.onButtonClick}' is not a function.`); } else if (!isNull(options.visiblePageNumberButtons) && !isNumeric(options.visiblePageNumberButtons)) { // Check to see if a visiblePageNumberButtons is valid throw new TypeError(`Unable to process visiblePageNumberButtons of type: ${typeof options.visiblePageNumberButtons}. Reason: '${options.visiblePageNumberButtons}' is not a numeric value.`); } else if (!isNull(options.onNavigationInfoTextRender) && !isFunction(options.onNavigationInfoTextRender)) { // Check to see if callback is a function throw new TypeError(`Unable to call onNavigationInfoTextRender of type: ${typeof options.onNavigationInfoTextRender}. Reason: '${options.onNavigationInfoTextRender}' is not a function.`); } // Get the tables and remove the property from the object let tables = options.table; delete options.table; // Page the tables for (let index = 0; index < tables.length; ++index) { // Get the table and make sure its valid let table = tables[index]; if (!isTable(table)) { // Check to see if the table is an HTMLTableElement throw new TypeError(`Unable to process ${getTableDisplayName(table)} of type: ${typeof table}. Reason: '${table}' is not an HTMLTableElement.`); } // Build the table navigation controls buildNavigation(table, options) // Add click events for the navigation controls addClickEvents(table, options); // Show the initial page showPage(table, {pageNumber: getInitialPage(table, options.initialPage), pageOptions: options}); } } /** * FUNCTION: remove * USE: Removes the rendered table pagination * @param table: JavaScript elements of the paged tables. * @return: True if table pagination controls were removed from all tables, false otherwise. */ exposed.remove = (tables) => { if (isNull(tables)) { return false; } else if (!isArrayLike(tables)) { tables = [tables]; } let allRemoved = true; for (let index = 0; index < tables.length; ++index) { let table = tables[index]; if (!isTable(table)) { // Check to see if the table is an HTMLTableElement throw new TypeError(`Unable to process ${getTableDisplayName(table)} of type: ${typeof table}. Reason: '${table}' is not an HTMLTableElement.`); } if (!removeNavigation(table)) { allRemoved = false; } } return allRemoved; } // -- Private data -- let getRows = (table) => { let rows = table.rows; let results = []; // Only return data rows for (let indexRow = 0; indexRow < rows.length; ++indexRow) { let row = rows[indexRow]; let isTDRow = true; for (let indexCell = 0; indexCell < row.cells.length; ++indexCell) { let cell = row.cells[indexCell]; if (cell.tagName.toLowerCase() !== 'td') { isTDRow = false; break; } } if (isTDRow) { results.push(row); } } return results; } let buildNavigation = (table, options) => { // Remove the previous navigation removeNavigation(table); // Set the max rows per page let rowsPerPage = options.rowsPerPage || settings.defaultRowsPerPage; setRowsPerPage(table, rowsPerPage); // Calculate the number of pages needed and set its value let totalPages = calculateTotalPagesRequired(table); setTotalPages(table, totalPages); if (totalPages < 1) { throw new Error(`${totalPages} pages calculated in order to page the table. Exiting...`); } // Get the current options let position = (options.navigationPosition || settings.defaultPosition).trim().toLowerCase(); let showNavigationInfoText = !isNull(options.showNavigationInfoText) ? options.showNavigationInfoText : true; let navigationBindTo = options.navigationBindTo || table.parentNode; let isCustomBinding = !isNull(options.navigationBindTo); // Add the navigation controls to the page let navigationContainer = attachNavigation({ table: table, container: navigationBindTo, isCustomBinding: isCustomBinding, position: position, classes: getNavigationClasses(position), data: [{key: 'tabindex', value: 0}] }); // Show page info if (showNavigationInfoText) { attachNavigationInfo({ table: table, container: navigationContainer, classes: [settings.classNameNavigationInfoSection] }); } // Add navigation buttons attatchNavigationButtons({ table: table, pageOptions: options, container: navigationContainer, classes: [settings.classNameNavigationControlSection] }); } let addClickEvents = (table, options) => { let paginationButtons = getNavigationButtons(table); let inputButton = getInputButton(table); let inputTextbox = getInputTextbox(table); let navigation = document.querySelector(`#${getNavigationId(table)}`); // Make sure there are visible navigation buttons let navigationVisible = (!isNull(paginationButtons) && paginationButtons.length > 0) || (!isNull(inputButton) && !isNull(inputTextbox)); // Throw an error if there are no visible navigation buttons if (!navigationVisible) { throw new Error(`The settings chosen on ${getTableDisplayName(table)} do not allow for any visible navigation buttons!`); } // Function to go to a page let navigateToPage = (pageNumber, event) => { pageNumber = translatePageNumber(table, pageNumber); if (!isNumeric(pageNumber)) { return false; } // Show the page showPage(table, {pageNumber: pageNumber, pageOptions: options}); // Call the on click function if specified if (options.onButtonClick) { options.onButtonClick.call(this, pageNumber, event); } return true; } // Add click events for the navigation buttons if (!isNull(paginationButtons)) { paginationButtons.forEach((paginationButton, index) => { // Add button click paginationButton.addEventListener('click', (event) => { event.preventDefault(); let pageNumber = getButtonPageNumber(paginationButton); navigateToPage(pageNumber, event); navigation.focus({preventScroll:true}); }); }); } // Add click event for the input button if (!isNull(inputButton)) { inputButton.addEventListener('click', (event) => { event.preventDefault(); // Get the input textbox let pageNumber = getInputValue(table, inputTextbox); navigateToPage(pageNumber, event); }); } // Add click event for the input textbox if (!isNull(inputTextbox)) { inputTextbox.addEventListener('keyup', (event) => { event = event || window.event; let keyCode = event.key || event.keyCode; // Check to see if the enter button was clicked switch (String(keyCode)) { case 'Enter': case '13': inputButton.click(); break; } }); } // Add click events for the left/right keyboard buttons if (!isNull(navigation)) { navigation.addEventListener('keydown', (event) => { event = event || window.event; let keyCode = event.key || event.keyCode; let pageNumber = getCurrentPageNumber(table); // Check to see if an arrow key was clicked switch (String(keyCode)) { case 'ArrowLeft': case 'Left': case '37': --pageNumber; break; case 'ArrowRight': case 'Right': case '39': ++pageNumber; break; default: return; break; } navigateToPage(pageNumber, event); }); } } let getTableId = (table) => { let tableId = !isNull(table.id) && table.id.length > 0 ? table.id : null; return tableId; } let getTableDisplayName = (table) => { let tableId = getTableId(table); let tableName = 'Table' + (tableId ? ' id: "' + tableId + '"' : ''); return tableName; } let getInitialPage = (table, initialPage) => { let initialActivePage = 1; if (!isNull(initialPage)) { let possiblePageNumber = translatePageNumber(table, initialPage); if (isNumeric(possiblePageNumber)) { initialActivePage = possiblePageNumber; } } return initialActivePage; } let translatePageNumber = (table, pageNumber) => { if (!isNull(pageNumber) && !isNumeric(pageNumber)) { pageNumber = String(pageNumber).trim().toLowerCase(); switch (pageNumber) { case settings.buttonOptionFirst.trim().toLowerCase(): pageNumber = 1; break; case settings.buttonOptionLast.trim().toLowerCase(): pageNumber = getTotalPages(table); break; case settings.buttonOptionPrevious.trim().toLowerCase(): pageNumber = getCurrentPageNumber(table) - 1; break; case settings.buttonOptionNext.trim().toLowerCase(): pageNumber = getCurrentPageNumber(table) + 1; break; } } return pageNumber; } let getNavigationClasses = (position) => { let navigationClasses = [] navigationClasses.push(settings.classNameNavigation); if (position == settings.navigationPositionTop.trim().toLowerCase()) { navigationClasses.push(settings.classNameNavigationTop); } else { navigationClasses.push(settings.classNameNavigationBottom); } return navigationClasses; } let hideAllRows = (table) => { let totalPages = getTotalPages(table); for (let pageNumber = 1; pageNumber <= totalPages; ++pageNumber) { hidePage(table, {pageNumber: pageNumber}); } } let hidePage = (table, options) => { let pageNumber = options.pageNumber; let rowInfo = getRowInfo(table, pageNumber); let rowIndexStart = rowInfo.rowIndexStart; let rowIndexEnd = rowInfo.rowIndexEnd; hideRows(table, rowIndexStart, rowIndexEnd); } let hideRows = (table, rowIndexStart, rowIndexEnd) => { let tableRows = getRows(table); for (let index = rowIndexStart; index <= rowIndexEnd && index < tableRows.length; ++index) { let tableRow = tableRows[index]; hideRow(tableRow); } } let hideRow = (tableRow) => { addClass(tableRow, settings.classNameHide); } let showAllRows = (table) => { let totalPages = getTotalPages(table); for (let pageNumber = 1; pageNumber <= totalPages; ++pageNumber) { showPage(table, { pageNumber: pageNumber, hidePreviousRows: false }); } } let showPage = (table, options) => { if (isNull(options.hidePreviousRows) || options.hidePreviousRows) { hideAllRows(table); } let rowInfo = getRowInfo(table, options.pageNumber); let rowIndexStart = rowInfo.rowIndexStart; let rowIndexEnd = rowInfo.rowIndexEnd; let pageNumber = rowInfo.pageNumber; showRows(table, rowIndexStart, rowIndexEnd); setCurrentPageNumber(table, pageNumber); highlightButton(table, pageNumber); showPageButtons(table, pageNumber); updateInfoText(table, rowInfo, options.pageOptions); clearInputValue(table); } let showRows = (table, rowIndexStart, rowIndexEnd) => { let tableRows = getRows(table); for (let index = rowIndexStart; index <= rowIndexEnd && index < tableRows.length; ++index) { let tableRow = tableRows[index]; showRow(tableRow); } } let showRow = (tableRow) => { removeClass(tableRow, settings.classNameHide); } let clearInputValue = (table, inputTextbox = null) => { inputTextbox = inputTextbox || getInputTextbox(table); if (!isNull(inputTextbox)) { inputTextbox.value = null; } } let getInputValue = (table, inputTextbox = null) => { inputTextbox = inputTextbox || getInputTextbox(table); return !isNull(inputTextbox) ? inputTextbox.value : null; } let updateInfoText = (table, rowInfo, pageOptions) => { let navigationInfo = getNavigationInfo(table); if (isNull(navigationInfo)) { return; } let text = `Showing ${rowInfo.itemCountStart} to ${rowInfo.itemCountEnd} of ${rowInfo.totalItems} entries.`; text += `<br />`; text += `Page ${rowInfo.pageNumber} of ${rowInfo.totalPages}`; let onNavigationInfoTextRender = settings.onTextRender; if (!isNull(pageOptions) && !isNull(pageOptions.onNavigationInfoTextRender)) { onNavigationInfoTextRender = pageOptions.onNavigationInfoTextRender; } navigationInfo.innerHTML = onNavigationInfoTextRender.call(this, text, rowInfo); } let showPageButtons = (table, pageNumber) => { let visibleButtons = getTotalVisiblePageButtons(table); if (!isNumeric(visibleButtons)) { return; } let totalPages = getTotalPages(table); let firstVisiblePage = Math.max(0, getPreviousMultiple(pageNumber, visibleButtons)) + 1; let lastVisiblePage = Math.min(totalPages, getNextMultiple(pageNumber, visibleButtons)); // Make sure there are at least 'visibleButtons' total buttons shown let difference = (visibleButtons - 1) - (lastVisiblePage - firstVisiblePage); if (difference > 0) { firstVisiblePage -= difference; } getNavigationButtons(table).forEach((btn, index) => { let buttonPageNumber = getButtonPageNumber(btn); if (isNumeric(buttonPageNumber)) { buttonPageNumber = Number(buttonPageNumber); addClass(btn, settings.classNameButtonHide); if (buttonPageNumber >= firstVisiblePage && buttonPageNumber <= lastVisiblePage) { removeClass(btn, settings.classNameButtonHide); } } }); } let getRowInfo = (table, pageNumber) => { let totalPages = getTotalPages(table); let rows = getRows(table); // Make sure the page number is within valid range [1 to last page number] pageNumber = Math.max(1, Math.min(pageNumber, totalPages)); let totalItems = rows.length; let rowsPerPage = getRowsPerPage(table); let rowIndexStart = (pageNumber - 1) * rowsPerPage; let rowIndexEnd = Math.min(rowIndexStart + (rowsPerPage - 1), totalItems - 1); let rowInfo = { rowIndexStart: rowIndexStart, rowIndexEnd: rowIndexEnd, pageNumber: pageNumber, totalPages: totalPages, totalItems: totalItems, rowsPerPage: rowsPerPage, itemCountStart: rowIndexStart + 1, itemCountEnd: rowIndexEnd + 1, }; return rowInfo; } let getCurrentPageNumber = (table) => { let pageNumber = getData(table, settings.dataNameCurrentPageNumber); return isNull(pageNumber) ? 0 : Number(pageNumber); } let setCurrentPageNumber = (table, pageNumber) => { addData(table, {key: settings.dataNameCurrentPageNumber, value: pageNumber}); } let highlightButton = (table, pageNumber) => { let paginationButton = null; let paginationButtons = getNavigationButtons(table); if (isNull(paginationButtons)) { return; } // Reset the previous page button colors resetButtonColors(paginationButtons); // Mark the selected button as active paginationButtons.forEach((btn, index) => { let buttonPageNumber = getButtonPageNumber(btn); if (isNumeric(buttonPageNumber) && Number(pageNumber) === Number(buttonPageNumber)) { paginationButton = btn; return false; } }); if (!isNull(paginationButton)) { addClass(paginationButton, settings.classNameButtonActive); } } let resetButtonColors = (paginationButtons) => { if (isNull(paginationButtons)) { return; } paginationButtons.forEach((paginationButton, index) => { let buttonPageNumber = getButtonPageNumber(paginationButton); if (isNumeric(buttonPageNumber)) { removeClass(paginationButton, settings.classNameButtonActive); } }); } let getButtonPageNumber = (btn) => { let pageNumber = getData(btn, settings.dataNamePageNumber); return String(!isNull(pageNumber) ? pageNumber : 0); } let setNavigationId = (table, navigationId) => { addData(table, {key: settings.dataNameNavigationId, value: navigationId}); } let getNavigationId = (table) => { return getData(table, settings.dataNameNavigationId); } let setNavigationInfoId = (table, navigationInfoId) => { addData(table, {key: settings.dataNameNavigationInfoId, value: navigationInfoId}); } let getNavigationInfoId = (table) => { return getData(table, settings.dataNameNavigationInfoId); } let setNavigationInputId = (table, navigationInputId) => { addData(table, {key: settings.dataNameNavigationInputId, value: navigationInputId}); } let getNavigationInputId = (table) => { return getData(table, settings.dataNameNavigationInputId); } let getNavigationButtons = (table) => { let selector = `#${getNavigationButtonsId(table)} a`; return document.querySelectorAll(selector); } let getInputButton = (table) => { let selector = `#${getNavigationInputId(table)} a`; return document.querySelector(selector); } let getInputTextbox = (table) => { let selector = `#${getNavigationInputId(table)} input`; return document.querySelector(selector); } let getNavigationInfo = (table) => { let selector = `#${getNavigationInfoId(table)}`; return document.querySelector(selector); } let setNavigationButtonsId = (table, navigationButtonsId) => { addData(table, {key: settings.dataNameNavigationButtonsId, value: navigationButtonsId}); } let getNavigationButtonsId = (table) => { return getData(table, settings.dataNameNavigationButtonsId); } let getTotalVisiblePageButtons = (table) => { let value = getData(table, settings.dataNameVisiblePageButtons); return !isNull(value) ? Number(value) : value; } let setTotalVisiblePageButtons = (table, visiblePageButtons) => { addData(table, {key: settings.dataNameVisiblePageButtons, value: visiblePageButtons}); } let setRowsPerPage = (table, rowsPerPage) => { addData(table, {key: settings.dataNameRowsPerPage, value: rowsPerPage}); } let getRowsPerPage = (table) => { let value = getData(table, settings.dataNameRowsPerPage); return !isNull(value) ? Number(value) : value; } let setTotalPages = (table, totalPages) => { addData(table, {key: settings.dataNameTotalPages, value: totalPages}); } let getTotalPages = (table) => { let value = getData(table, settings.dataNameTotalPages); return !isNull(value) ? Number(value) : value; } let calculateTotalPagesRequired = (table) => { let rowsPerPage = getRowsPerPage(table); let totalRows = getRows(table).length; let totalPages = totalRows / rowsPerPage; if (totalRows % rowsPerPage !== 0) { totalPages = Math.floor(++totalPages); } return totalPages; } let addClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); let modified = false; if (cssClass.length > 0 && !hasClass(element, cssClass)) { element.classList.add(cssClass) modified = true; } return modified; } let removeClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); let modified = false; if (cssClass.length > 0 && hasClass(element, cssClass)) { element.classList.remove(cssClass); modified = true; } return modified; } let hasClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); return element.classList.contains(cssClass); } let isNull = (item) => { return isUndefined(item) || null === item; } let isUndefined = (item) => { return undefined === item; } let isFunction = (item) => { return 'function' === typeof item } let isTable = (item) => { return isElement(item) && item instanceof HTMLTableElement; } let isElement = (item) => { let value = false; try { value = item instanceof HTMLElement || item instanceof HTMLDocument; } catch (e) { value = (typeof item==="object") && (item.nodeType===1) && (typeof item.style === "object") && (typeof item.ownerDocument ==="object"); } return value; } let randomFromTo = (from, to) => { return Math.floor(Math.random() * (to - from + 1) + from); } let isNumeric = (n) => { return !isNaN(parseFloat(n)) && isFinite(n); } let generateNavigationId = (table) => { let tableId = getTableId(table); return 'tablePaginationNavigation_' + (tableId ? tableId : randomFromTo(1271991, 7281987)); } let removeNavigation = (table) => { let modified = false; let previousNavigationId = getNavigationId(table); // Remove the previous table navigation if (!isNull(previousNavigationId)) { let previousElement = document.querySelector(`#${previousNavigationId}`); if (!isNull(previousElement)) { previousElement.parentNode.removeChild(previousElement); showAllRows(table); modified = true; } } return modified; } let attachNavigation = (options) => { // Create the table navigation control div let navigationContainer = document.createElement('div'); navigationContainer.id = generateNavigationId(options.table); // Determine the navigation position let position = options.position.trim().toLowerCase(); if (!options.isCustomBinding) { if (position === settings.navigationPositionTop) { // Insert navigation before the table options.container.insertBefore(navigationContainer, options.table); } else { // Insert navigation after the table options.container.insertBefore(navigationContainer, options.table.nextSibling); } } else { if (position === settings.navigationPositionTop) { // Insert navigation at the beginning of the container options.container.insertBefore(navigationContainer, options.container.firstChild); } else { // Insert navigation ar end of the container options.container.insertBefore(navigationContainer, options.container.lastChild); } } // Add classes and data addClasses(navigationContainer, options.classes); addData(navigationContainer, options.data); // Set the current table navigation control id setNavigationId(options.table, navigationContainer.id); setNavigationInfoId(options.table, null); return navigationContainer; } let attachButton = (options) => { let btn = document.createElement('a'); btn.href = '#'; addClasses(btn, options.classes); addData(btn, options.data); btn.insertAdjacentHTML('beforeend', options.text); btn.title = options.title; options.container.appendChild(btn); return btn; } let attatchNavigationButtons = (options) => { let pageOptions = options.pageOptions; let table = options.table; // Create the table navigation info control div let navigationButtonSection = document.createElement('div'); navigationButtonSection.id = options.container.id + '_control_section'; options.container.appendChild(navigationButtonSection); // Add classes and data addClasses(navigationButtonSection, options.classes); addData(navigationButtonSection, options.data); let showFirstPageButton = !isNull(pageOptions.showFirstPageButton) ? pageOptions.showFirstPageButton : true; let showLastPageButton = !isNull(pageOptions.showLastPageButton) ? pageOptions.showLastPageButton : true; let showPreviousPageButton = !isNull(pageOptions.showPreviousPageButton) ? pageOptions.showPreviousPageButton : true; let showNextPageButton = !isNull(pageOptions.showNextPageButton) ? pageOptions.showNextPageButton : true; let showPageNumberButtons = !isNull(pageOptions.showPageNumberButtons) ? pageOptions.showPageNumberButtons : true; let showNavigationInput = !isNull(pageOptions.showNavigationInput) ? pageOptions.showNavigationInput : true; let onButtonTitleRender = pageOptions.onButtonTitleRender || settings.onTextRender; let onButtonTextRender = pageOptions.onButtonTextRender || settings.onTextRender; let defaultVisibleButtons = !isUndefined(pageOptions.visiblePageNumberButtons) ? pageOptions.visiblePageNumberButtons : settings.defaultVisibleButtons; if (isNumeric(defaultVisibleButtons) && defaultVisibleButtons < 1) { showPageNumberButtons = false; } // Set the total visible page number buttons setTotalVisiblePageButtons(table, defaultVisibleButtons); // Create the buttons div let navigationButtons = document.createElement('div'); navigationButtons.id = navigationButtonSection.id + '_buttons'; navigationButtonSection.appendChild(navigationButtons); // Set the current table navigation butons id setNavigationButtonsId(table, navigationButtons.id); addClasses(navigationButtons, [settings.classNameNavigationButtonSection]); // Add the first page button if (showFirstPageButton) { attachButton({ container: navigationButtons, classes: [settings.classNameButton, settings.classNameButtonFirst], data: {key: settings.dataNamePageNumber, value: settings.buttonOptionFirst}, text: onButtonTextRender.call(this, '⇠ First', settings.onRenderDescFirstPage), title: onButtonTitleRender.call(this, 'First Page', settings.onRenderDescFirstPage), }); } // Add the previous page button if (showPreviousPageButton) { attachButton({ container: navigationButtons, classes: [settings.classNameButton, settings.classNameButtonPrevious], data: {key: settings.dataNamePageNumber, value: settings.buttonOptionPrevious}, text: onButtonTextRender.call(this, '⮜', settings.onRenderDescPrevPage), title: onButtonTitleRender.call(this, 'Previous Page', settings.onRenderDescPrevPage), }); } // Add the page number buttons if (showPageNumberButtons) { let totalPages = getTotalPages(table); for (let index = 1; index <= totalPages; ++index) { attachButton({ container: navigationButtons, classes: [settings.classNameButton, settings.classNameButtonPageNumber], data: {key: settings.dataNamePageNumber, value: index}, text: onButtonTextRender.call(this, String(index), settings.onRenderDescPageNumber), title: onButtonTitleRender.call(this, 'Page ' + index, settings.onRenderDescPageNumber), }); } } // Add the next page button if (showNextPageButton) { attachButton({ container: navigationButtons, classes: [settings.classNameButton, settings.classNameButtonNext], data: {key: settings.dataNamePageNumber, value: settings.buttonOptionNext}, text: onButtonTextRender.call(this, '⮞', settings.onRenderDescNextPage), title: onButtonTitleRender.call(this, 'Next Page', settings.onRenderDescNextPage), }); } // Add the last page button if (showLastPageButton) { attachButton({ container: navigationButtons, classes: [settings.classNameButton, settings.classNameButtonLast], data: {key: settings.dataNamePageNumber, value: settings.buttonOptionLast}, text: onButtonTextRender.call(this, 'Last ⤑', settings.onRenderDescLastPage), title: onButtonTitleRender.call(this, 'Last Page', settings.onRenderDescLastPage), }); } // Add navigation input if (showNavigationInput) { attachNavigationInput({ table: table, pageOptions: pageOptions, container: navigationButtonSection, classes: [settings.classNameNavigationInputSection] }); } return navigationButtonSection; } let attachNavigationInfo = (options) => { // Create the table navigation info control div let navigationInfo = document.createElement('div'); navigationInfo.id = options.container.id + '_info_section'; options.container.appendChild(navigationInfo); // Add classes and data addClasses(navigationInfo, options.classes); addData(navigationInfo, options.data); // Set the current table navigation info id setNavigationInfoId(options.table, navigationInfo.id); return navigationInfo; } let attachNavigationInput = (options) => { let pageOptions = options.pageOptions; let onButtonTitleRender = pageOptions.onButtonTitleRender || settings.onTextRender; let onButtonTextRender = pageOptions.onButtonTextRender || settings.onTextRender; // Create the table navigation input control div let navigationInputContainer = document.createElement('div'); navigationInputContainer.id = options.container.id + '_input'; options.container.appendChild(navigationInputContainer); // Add classes and data addClasses(navigationInputContainer, options.classes); addData(navigationInputContainer, options.data); // Add the input textbox let navigationGoInput = document.createElement('input'); navigationGoInput.type = 'text'; navigationInputContainer.appendChild(navigationGoInput); // Add input classes addClasses(navigationGoInput, [settings.classNameNavigationInput]); // Add the Go button attachButton({ container: navigationInputContainer, classes: [settings.classNameButton, settings.classNameButtonGo], text: onButtonTextRender.call(this, 'Go', settings.onRenderDescGoInput), title: onButtonTitleRender.call(this, 'Go To Page', settings.onRenderDescGoInput), }); // Set the current table navigation input id setNavigationInputId(options.table, navigationInputContainer.id); return navigationInputContainer; } let addClasses = (element, classes) => { if (isNull(classes)) { return; } else if (!Array.isArray(classes)) { classes = [classes]; } classes.forEach(item => { addClass(element, item); }); } let removeClasses = (element, classes) => { if (isNull(classes)) { return; } else if (!Array.isArray(classes)) { classes = [classes]; } classes.forEach(item => { removeClass(element, item); }); } let addData = (element, data) => { if (isNull(data)) { return; } else if (!Array.isArray(data)) { data = [data]; } data.forEach(item => { if (!isNull(item.value)) { element.setAttribute(item.key, item.value); } else { removeData(element, item); } }); } let removeData = (element, data) => { if (isNull(data)) { return; } else if (!Array.isArray(data)) { data = [data]; } data.forEach(item => { let key = item.key || item; element.removeAttribute(key); }); } let getData = (element, data) => { if (isNull(data)) { return null; } else if (!Array.isArray(data)) { data = [data]; } let results = []; data.forEach(item => { let key = item.key || item; results.push(element.getAttribute(key)); }); return results.length == 1 ? results[0] : results; } let getNextMultiple = (number, multiple, skipAlreadyMultiple = false) => { let retVal = 0; if (multiple !== 0) { let remainder = (number % multiple); if (!skipAlreadyMultiple && remainder === 0) { retVal = number; } else { retVal = number + (multiple - remainder); } } return retVal; } let getPreviousMultiple = (number, multiple, skipAlreadyMultiple = false) => { return getNextMultiple(number, multiple, !skipAlreadyMultiple) - multiple; } // see if it looks and smells like an iterable object, and do accept length === 0 let isArrayLike = (item) => { return ( Array.isArray(item) || (!!item && typeof item === "object" && typeof (item.length) === "number" && (item.length === 0 || (item.length > 0 && (item.length - 1) in item) ) ) ); } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(TablePagination)); // http://programmingnotes.org/ |
The following is TablePagination.css.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
/* // ============================================================================ // Author: Kenneth Perkins // Date: Aug 5, 2020 // Taken From: http://programmingnotes.org/ // File: TablePagination.css // Description: CSS for the table pagination // ============================================================================ */ @import url('https://fonts.googleapis.com/css?family=Roboto'); .pagination-btn { padding: 6px 8px; font-weight: normal; font-size: 15px; border-radius: 4px; display: inline-block; text-align: center; white-space: nowrap; vertical-align: middle; touch-action: manipulation; cursor: pointer; user-select: none; text-decoration: none; transition: background-color 400ms; min-width: 10px; height: 18px; line-height: 18px; font-family: "Roboto",sans-serif, Marmelad,"Lucida Grande",Arial,"Hiragino Sans GB",Georgia,"Helvetica Neue",Helvetica; color: white; border-color: #5a3291; background-color: #064685; } .pagination-btn:focus, .pagination-btn:hover, .pagination-btn:active, .pagination-btn:active:hover, .pagination-btn:active:focus { color: white; border-color: #784cb5; background-color: #376a9d; } .pagination-btn.first { } .pagination-btn.previous { font-size: 12px; } .pagination-btn.next { font-size: 12px; } .pagination-btn.last { } .pagination-btn.page-number { min-width: 17px; } .pagination-btn.go { height: 19px; } .pagination-btn.hide { display: none; } .pagination-btn.active { color: white; background-color: #3eceff; border-color: #3eceff; } .pagination-btn.active:focus, .pagination-btn.active:hover, .pagination-btn.active:active { color: white; background-color: #00b4f0; border-color: #00b4f0; } .pagination-btn:disabled, .pagination-btn[disabled], .pagination-btn.disabled:hover, .pagination-btn[disabled]:hover, fieldset[disabled] .pagination-btn:hover, .pagination-btn.disabled:focus, .pagination-btn[disabled]:focus, fieldset[disabled] .pagination-btn:focus, .pagination-btn.disabled.focus, .pagination-btn[disabled].focus, fieldset[disabled] .pagination-btn.focus, .pagination-btn.active:disabled, .pagination-btn.active[disabled], .pagination-btn.active.disabled:hover, .pagination-btn.active[disabled]:hover, fieldset[disabled] .pagination-btn.active:hover, .pagination-btn.active.disabled:focus, .pagination-btn.active[disabled]:focus, fieldset[disabled] .pagination-btn.active:focus, .pagination-btn.active.disabled.focus, .pagination-btn.active[disabled].focus, fieldset[disabled] .pagination-btn.active.focus { color: #e4e4e4; border-color: #9d80c4; background-color: #6990b5; } .pagination-hide { display: none; } .pagination-navigation { width: 100%; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; outline: none; } .pagination-navigation.top { top: 0; } .pagination-navigation.bottom { bottom: 0; } .pagination-navigation-info-section { text-align: center; padding: 2px; box-sizing: border-box; display: inline-block; font-family: "Roboto",sans-serif, Marmelad,"Lucida Grande",Arial,"Hiragino Sans GB",Georgia,"Helvetica Neue",Helvetica; font-size: 14px; line-height: 1.42857143; color: #333; margin-right: auto; margin-left: 0; } .pagination-navigation-control-section { display: inline-block; margin-left: auto; margin-right: 0; } .pagination-navigation-button-section { display: inline-block; } .pagination-navigation-input-section { display: inline-block; margin-left: 5px; padding-top: 2px; box-sizing: border-box; } .pagination-navigation-input { height: 29px; width: 36px; font-size: 18px; background: #fff; border-radius: 3px; border: 1px solid #aaa; padding: 0; font-size: 14px; text-align: center; vertical-align: baseline; outline: 0; box-shadow: none; margin-right: 5px; } /* // http://programmingnotes.org/ */ |
14. More Examples
Below are more examples demonstrating the use of ‘TablePagination.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Aug 5, 2020 // Taken From: http://programmingnotes.org/ // File: tablePaginationDemo.html // Description: Demonstrates the use of TablePagination.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes TablePagination.js Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } .inline { display:inline-block; } #pagination-demo { border:1px solid #C0C0C0; border-collapse:collapse; padding:5px; margin: auto; width: 100%; } #pagination-demo th { border:1px solid #C0C0C0; padding:5px; background:#F0F0F0; width: 100px; } #pagination-demo td { border:1px solid #C0C0C0; padding:5px; text-align: center; } .tableContainer { min-height: 50px; max-height: 500px; overflow:auto; width: 100%; } #navigationSection { margin-top: 10px; } </style> <!-- // Include module --> <link type="text/css" rel="stylesheet" href="./TablePagination.css"> <script type="text/javascript" src="./TablePagination.js"></script> </head> <body> <div class="main"> My Programming Notes TablePagination.js Demo <div id="navigationSection"> <div class="inline tableContainer"> <table id="pagination-demo"> <tr> <th>First</th> <th>Last</th> <th>Age</th> </tr> <tr> <td>Kenneth</td> <td>Perkins</td> <td>31</td> </tr> <tr> <td>Jennifer</td> <td>Nguyen</td> <td>28</td> </tr> <tr> <td>Reynalda</td> <td>Bahr</td> <td>87</td> </tr> <tr> <td>Marilou</td> <td>Bower</td> <td>91</td> </tr> <tr> <td>Santina</td> <td>Rinaldi</td> <td>65</td> </tr> <tr> <td>Sherita</td> <td>Deskins</td> <td>41</td> </tr> <tr> <td>Flora</td> <td>Galusha</td> <td>52</td> </tr> <tr> <td>Yun</td> <td>Aucoin</td> <td>55</td> </tr> <tr> <td>Booker</td> <td>Freeland</td> <td>12</td> </tr> <tr> <td>Rhonda</td> <td>Fenimore</td> <td>25</td> </tr> <tr> <td>Rebecka</td> <td>Sickler</td> <td>22</td> </tr> <tr> <td>Aurora</td> <td>Coolbaugh</td> <td>24</td> </tr> <tr> <td>Jordon</td> <td>Finck</td> <td>40</td> </tr> <tr> <td>Renate</td> <td>Allred</td> <td>55</td> </tr> <tr> <td>Juana</td> <td>Rickerson</td> <td>16</td> </tr> <tr> <td>Elsie</td> <td>Hebb</td> <td>5</td> </tr> <tr> <td>Solomon</td> <td>Stookey</td> <td>42</td> </tr> <tr> <td>Collin</td> <td>Kuhlman</td> <td>17</td> </tr> <tr> <td>Siobhan</td> <td>Harville</td> <td>46</td> </tr> <tr> <td>Luigi</td> <td>Moates</td> <td>70</td> </tr> <tr> <td>Charlena</td> <td>Sebree</td> <td>24</td> </tr> <tr> <td>Fairy</td> <td>Pompa</td> <td>74</td> </tr> <tr> <td>Nita</td> <td>Yerkes</td> <td>44</td> </tr> <tr> <td>Marisa</td> <td>Caldwell</td> <td>10</td> </tr> <tr> <td>Tyrone</td> <td>Rodi</td> <td>8</td> </tr> <tr> <td>Irmgard</td> <td>Welker</td> <td>4</td> </tr> <tr> <td>Cristen</td> <td>Tank</td> <td>26</td> </tr> <tr> <td>Hildred</td> <td>Lautenschlage</td> <td>45</td> </tr> <tr> <td>Ranae</td> <td>Mccright</td> <td>45</td> </tr> <tr> <td>Myung</td> <td>Hemsley</td> <td>21</td> </tr> <tr> <td>Carlie</td> <td>Heyer</td> <td>18</td> </tr> <tr> <td>Clorinda</td> <td>Couch</td> <td>35</td> </tr> <tr> <td>Marcel</td> <td>Galentine</td> <td>39</td> </tr> <tr> <td>Zenobia</td> <td>Alford</td> <td>12</td> </tr> <tr> <td>Yuko</td> <td>Nolasco</td> <td>2</td> </tr> <tr> <td>Mertie</td> <td>Zimmer</td> <td>66</td> </tr> <tr> <td>Jeanne</td> <td>Odwyer</td> <td>90</td> </tr> <tr> <td>Suzie</td> <td>Gourley</td> <td>29</td> </tr> <tr> <td>Idella</td> <td>Gauvin</td> <td>60</td> </tr> <tr> <td>Sharyl</td> <td>Hydrick</td> <td>6</td> </tr> <tr> <td>Major</td> <td>Lacey</td> <td>1</td> </tr> <tr> <td>Mercedez</td> <td>Claiborne</td> <td>42</td> </tr> <tr> <td>Antonietta</td> <td>Zehnder</td> <td>70</td> </tr> <tr> <td>Normand</td> <td>Hittle</td> <td>63</td> </tr> <tr> <td>Benita</td> <td>Lineberry</td> <td>44</td> </tr> <tr> <td>Carroll</td> <td>Calfee</td> <td>75</td> </tr> <tr> <td>Theda</td> <td>Winters</td> <td>63</td> </tr> <tr> <td>Ella</td> <td>Hegarty</td> <td>85</td> </tr> <tr> <td>Jule</td> <td>Shanahan</td> <td>22</td> </tr> <tr> <td>Catherine</td> <td>Ver</td> <td>55</td> </tr> <tr> <td>Anabel</td> <td>Difranco</td> <td>33</td> </tr> <tr> <td>Virgen</td> <td>Ruffner</td> <td>81</td> </tr> <tr> <td>Crystle</td> <td>Buschman</td> <td>27</td> </tr> <tr> <td>Alyse</td> <td>Happel</td> <td>68</td> </tr> <tr> <td>Sharla</td> <td>Hammes</td> <td>5</td> </tr> <tr> <td>Eliana</td> <td>Sippel</td> <td>33</td> </tr> <tr> <td>Elise</td> <td>Hoxie</td> <td>82</td> </tr> <tr> <td>Keshia</td> <td>Arceo</td> <td>11</td> </tr> <tr> <td>Leslee</td> <td>Jablonski</td> <td>22</td> </tr> <tr> <td>Marcos</td> <td>Chien</td> <td>18</td> </tr> <tr> <td>Rosalyn</td> <td>Dahl</td> <td>37</td> </tr> <tr> <td>Elmo</td> <td>Mcglade</td> <td>46</td> </tr> <tr> <td>Bernita</td> <td>Mcdavis</td> <td>85</td> </tr> <tr> <td>Myung</td> <td>Marquardt</td> <td>77</td> </tr> <tr> <td>Kalyn</td> <td>Lichtenberger</td> <td>88</td> </tr> <tr> <td>Sophia</td> <td>Warrior</td> <td>99</td> </tr> <tr> <td>Carri</td> <td>Basile</td> <td>10</td> </tr> <tr> <td>Daria</td> <td>Patridge</td> <td>44</td> </tr> <tr> <td>Kory</td> <td>Eifert</td> <td>63</td> </tr> <tr> <td>Ahmed</td> <td>Vore</td> <td>71</td> </tr> <tr> <td>Erlinda</td> <td>Dias</td> <td>36</td> </tr> <tr> <td>Magdalene</td> <td>Hokanson</td> <td>56</td> </tr> <tr> <td>Loren</td> <td>Haun</td> <td>84</td> </tr> <tr> <td>Arlie</td> <td>Garren</td> <td>32</td> </tr> <tr> <td>Fernande</td> <td>Styron</td> <td>63</td> </tr> <tr> <td>Lizabeth</td> <td>Richerson</td> <td>75</td> </tr> <tr> <td>Jessica</td> <td>Ferrara</td> <td>52</td> </tr> <tr> <td>Shin</td> <td>Philson</td> <td>64</td> </tr> <tr> <td>Jackelyn</td> <td>Lafayette</td> <td>2</td> </tr> <tr> <td>Tambra</td> <td>Pantano</td> <td>7</td> </tr> <tr> <td>Tomiko</td> <td>Nicols</td> <td>6</td> </tr> <tr> <td>Cordelia</td> <td>Mask</td> <td>55</td> </tr> <tr> <td>Shellie</td> <td>Duffer</td> <td>10</td> </tr> <tr> <td>Melina</td> <td>Fluitt</td> <td>19</td> </tr> </table> </div> </div> </div> <div id="binding-section"> Custom Binding Section </div> <table id="test" cellspacing="0" style="width: 100%; border: 1px solid black; text-align: left;"> <thead> <tr> <th class="th-sm">Name </th> <th class="th-sm">Position </th> <th class="th-sm">Office </th> <th class="th-sm">Age </th> <th class="th-sm">Start date </th> <th class="th-sm">Salary </th> </tr> </thead> <tbody> <tr> <td>Tiger Nixon</td> <td>System Architect</td> <td>Edinburgh</td> <td>61</td> <td>2011/04/25</td> <td>$320,800</td> </tr> <tr> <td>Garrett Winters</td> <td>Accountant</td> <td>Tokyo</td> <td>63</td> <td>2011/07/25</td> <td>$170,750</td> </tr> <tr> <td>Ashton Cox</td> <td>Junior Technical Author</td> <td>San Francisco</td> <td>66</td> <td>2009/01/12</td> <td>$86,000</td> </tr> <tr> <td>Cedric Kelly</td> <td>Senior Javascript Developer</td> <td>Edinburgh</td> <td>22</td> <td>2012/03/29</td> <td>$433,060</td> </tr> <tr> <td>Airi Satou</td> <td>Accountant</td> <td>Tokyo</td> <td>33</td> <td>2008/11/28</td> <td>$162,700</td> </tr> <tr> <td>Brielle Williamson</td> <td>Integration Specialist</td> <td>New York</td> <td>61</td> <td>2012/12/02</td> <td>$372,000</td> </tr> <tr> <td>Herrod Chandler</td> <td>Sales Assistant</td> <td>San Francisco</td> <td>59</td> <td>2012/08/06</td> <td>$137,500</td> </tr> <tr> <td>Rhona Davidson</td> <td>Integration Specialist</td> <td>Tokyo</td> <td>55</td> <td>2010/10/14</td> <td>$327,900</td> </tr> <tr> <td>Colleen Hurst</td> <td>Javascript Developer</td> <td>San Francisco</td> <td>39</td> <td>2009/09/15</td> <td>$205,500</td> </tr> <tr> <td>Sonya Frost</td> <td>Software Engineer</td> <td>Edinburgh</td> <td>23</td> <td>2008/12/13</td> <td>$103,600</td> </tr> <tr> <td>Jena Gaines</td> <td>Office Manager</td> <td>London</td> <td>30</td> <td>2008/12/19</td> <td>$90,560</td> </tr> <tr> <td>Quinn Flynn</td> <td>Support Lead</td> <td>Edinburgh</td> <td>22</td> <td>2013/03/03</td> <td>$342,000</td> </tr> <tr> <td>Charde Marshall</td> <td>Regional Director</td> <td>San Francisco</td> <td>36</td> <td>2008/10/16</td> <td>$470,600</td> </tr> <tr> <td>Haley Kennedy</td> <td>Senior Marketing Designer</td> <td>London</td> <td>43</td> <td>2012/12/18</td> <td>$313,500</td> </tr> <tr> <td>Tatyana Fitzpatrick</td> <td>Regional Director</td> <td>London</td> <td>19</td> <td>2010/03/17</td> <td>$385,750</td> </tr> <tr> <td>Michael Silva</td> <td>Marketing Designer</td> <td>London</td> <td>66</td> <td>2012/11/27</td> <td>$198,500</td> </tr> <tr> <td>Paul Byrd</td> <td>Chief Financial Officer (CFO)</td> <td>New York</td> <td>64</td> <td>2010/06/09</td> <td>$725,000</td> </tr> <tr> <td>Gloria Little</td> <td>Systems Administrator</td> <td>New York</td> <td>59</td> <td>2009/04/10</td> <td>$237,500</td> </tr> <tr> <td>Bradley Greer</td> <td>Software Engineer</td> <td>London</td> <td>41</td> <td>2012/10/13</td> <td>$132,000</td> </tr> <tr> <td>Dai Rios</td> <td>Personnel Lead</td> <td>Edinburgh</td> <td>35</td> <td>2012/09/26</td> <td>$217,500</td> </tr> <tr> <td>Jenette Caldwell</td> <td>Development Lead</td> <td>New York</td> <td>30</td> <td>2011/09/03</td> <td>$345,000</td> </tr> <tr> <td>Yuri Berry</td> <td>Chief Marketing Officer (CMO)</td> <td>New York</td> <td>40</td> <td>2009/06/25</td> <td>$675,000</td> </tr> <tr> <td>Caesar Vance</td> <td>Pre-Sales Support</td> <td>New York</td> <td>21</td> <td>2011/12/12</td> <td>$106,450</td> </tr> <tr> <td>Doris Wilder</td> <td>Sales Assistant</td> <td>Sidney</td> <td>23</td> <td>2010/09/20</td> <td>$85,600</td> </tr> <tr> <td>Angelica Ramos</td> <td>Chief Executive Officer (CEO)</td> <td>London</td> <td>47</td> <td>2009/10/09</td> <td>$1,200,000</td> </tr> <tr> <td>Gavin Joyce</td> <td>Developer</td> <td>Edinburgh</td> <td>42</td> <td>2010/12/22</td> <td>$92,575</td> </tr> <tr> <td>Jennifer Chang</td> <td>Regional Director</td> <td>Singapore</td> <td>28</td> <td>2010/11/14</td> <td>$357,650</td> </tr> <tr> <td>Brenden Wagner</td> <td>Software Engineer</td> <td>San Francisco</td> <td>28</td> <td>2011/06/07</td> <td>$206,850</td> </tr> <tr> <td>Fiona Green</td> <td>Chief Operating Officer (COO)</td> <td>San Francisco</td> <td>48</td> <td>2010/03/11</td> <td>$850,000</td> </tr> <tr> <td>Shou Itou</td> <td>Regional Marketing</td> <td>Tokyo</td> <td>20</td> <td>2011/08/14</td> <td>$163,000</td> </tr> <tr> <td>Michelle House</td> <td>Integration Specialist</td> <td>Sidney</td> <td>37</td> <td>2011/06/02</td> <td>$95,400</td> </tr> <tr> <td>Suki Burks</td> <td>Developer</td> <td>London</td> <td>53</td> <td>2009/10/22</td> <td>$114,500</td> </tr> <tr> <td>Prescott Bartlett</td> <td>Technical Author</td> <td>London</td> <td>27</td> <td>2011/05/07</td> <td>$145,000</td> </tr> <tr> <td>Gavin Cortez</td> <td>Team Leader</td> <td>San Francisco</td> <td>22</td> <td>2008/10/26</td> <td>$235,500</td> </tr> <tr> <td>Martena Mccray</td> <td>Post-Sales support</td> <td>Edinburgh</td> <td>46</td> <td>2011/03/09</td> <td>$324,050</td> </tr> <tr> <td>Unity Butler</td> <td>Marketing Designer</td> <td>San Francisco</td> <td>47</td> <td>2009/12/09</td> <td>$85,675</td> </tr> <tr> <td>Howard Hatfield</td> <td>Office Manager</td> <td>San Francisco</td> <td>51</td> <td>2008/12/16</td> <td>$164,500</td> </tr> <tr> <td>Hope Fuentes</td> <td>Secretary</td> <td>San Francisco</td> <td>41</td> <td>2010/02/12</td> <td>$109,850</td> </tr> <tr> <td>Vivian Harrell</td> <td>Financial Controller</td> <td>San Francisco</td> <td>62</td> <td>2009/02/14</td> <td>$452,500</td> </tr> <tr> <td>Timothy Mooney</td> <td>Office Manager</td> <td>London</td> <td>37</td> <td>2008/12/11</td> <td>$136,200</td> </tr> <tr> <td>Jackson Bradshaw</td> <td>Director</td> <td>New York</td> <td>65</td> <td>2008/09/26</td> <td>$645,750</td> </tr> <tr> <td>Olivia Liang</td> <td>Support Engineer</td> <td>Singapore</td> <td>64</td> <td>2011/02/03</td> <td>$234,500</td> </tr> <tr> <td>Bruno Nash</td> <td>Software Engineer</td> <td>London</td> <td>38</td> <td>2011/05/03</td> <td>$163,500</td> </tr> <tr> <td>Sakura Yamamoto</td> <td>Support Engineer</td> <td>Tokyo</td> <td>37</td> <td>2009/08/19</td> <td>$139,575</td> </tr> <tr> <td>Thor Walton</td> <td>Developer</td> <td>New York</td> <td>61</td> <td>2013/08/11</td> <td>$98,540</td> </tr> <tr> <td>Finn Camacho</td> <td>Support Engineer</td> <td>San Francisco</td> <td>47</td> <td>2009/07/07</td> <td>$87,500</td> </tr> <tr> <td>Serge Baldwin</td> <td>Data Coordinator</td> <td>Singapore</td> <td>64</td> <td>2012/04/09</td> <td>$138,575</td> </tr> <tr> <td>Zenaida Frank</td> <td>Software Engineer</td> <td>New York</td> <td>63</td> <td>2010/01/04</td> <td>$125,250</td> </tr> <tr> <td>Zorita Serrano</td> <td>Software Engineer</td> <td>San Francisco</td> <td>56</td> <td>2012/06/01</td> <td>$115,000</td> </tr> <tr> <td>Jennifer Acosta</td> <td>Junior Javascript Developer</td> <td>Edinburgh</td> <td>43</td> <td>2013/02/01</td> <td>$75,650</td> </tr> <tr> <td>Cara Stevens</td> <td>Sales Assistant</td> <td>New York</td> <td>46</td> <td>2011/12/06</td> <td>$145,600</td> </tr> <tr> <td>Hermione Butler</td> <td>Regional Director</td> <td>London</td> <td>47</td> <td>2011/03/21</td> <td>$356,250</td> </tr> <tr> <td>Lael Greer</td> <td>Systems Administrator</td> <td>London</td> <td>21</td> <td>2009/02/27</td> <td>$103,500</td> </tr> <tr> <td>Jonas Alexander</td> <td>Developer</td> <td>San Francisco</td> <td>30</td> <td>2010/07/14</td> <td>$86,500</td> </tr> <tr> <td>Shad Decker</td> <td>Regional Director</td> <td>Edinburgh</td> <td>51</td> <td>2008/11/13</td> <td>$183,000</td> </tr> <tr> <td>Michael Bruce</td> <td>Javascript Developer</td> <td>Singapore</td> <td>29</td> <td>2011/06/27</td> <td>$183,000</td> </tr> <tr> <td>Donna Snider</td> <td>Customer Support</td> <td>New York</td> <td>27</td> <td>2011/01/25</td> <td>$112,000</td> </tr> </tbody> <tfoot> <tr> <th>Name </th> <th>Position </th> <th>Office </th> <th>Age </th> <th>Start date </th> <th>Salary </th> </tr> </tfoot> </table> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Available options TablePagination.page({ table: document.querySelector('#pagination-demo'), // One or more Javascript elements of the tables to page. rowsPerPage: 4, // Optional. The number of rows per page. Default is 5 initialPage: null, // Optional. The initial page to display. Possible values: Numeric value or 'first/last'. Default is page 1 navigationPosition: 'bottom', // Optional. The navigation position. Possible values: 'top/bottom'. Default is 'bottom' showFirstPageButton: true, // Optional. Specifies if the first page button is shown. Default is true showLastPageButton: true, // Optional. Specifies if the last page button is shown. Default is true showPreviousPageButton: true, // Optional. Specifies if the previous page button is shown. Default is true showNextPageButton: true, // Optional. Specifies if the next page button is shown. Default is true showPageNumberButtons: true, // Optional. Specifies if the page number buttons are shown. Default is true showNavigationInput: true, // Optional. Specifies if the 'Go' search functionality is shown. Default is true showNavigationInfoText: true, // Optional. Specifies if the navigation info text is shown. Default is true visiblePageNumberButtons: 4, // Optional. The maximum number of visible page number buttons. Default is 3. Set to null to show all buttons onButtonClick: (pageNumber, event) => { // Optional. Function that allows to do something on button click //window.location.href = "#page=" + pageNumber; }, onButtonTextRender: (text, desc) => { // Optional. Function that allows to format the button text //console.log(`Button Text: ${text}`); //console.log(`Button Description: ${desc}`); return text; }, onButtonTitleRender: (title, desc) => { // Optional. Function that allows to format the button title //console.log(`Button Text: ${text}`); //console.log(`Button Description: ${desc}`); return title; }, onNavigationInfoTextRender: (text, rowInfo) => { // Optional. Function that allows to format the navigation info text //console.log(`Navigation Text: ${text}`); //console.log(`Row Info:`, rowInfo); return text; }, navigationBindTo: null, // Optional. Javascript element of the container where the navigation controls are bound to. // If not specified, default destination is above or below the table element, depending on // the 'navigationPosition' value }); //TablePagination.remove(document.querySelector('#pagination-demo')); TablePagination.page({table: document.querySelector('#test'), rowsPerPage: 10}); }); </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript || FileIO.js – Open, Save, & Read Local & Remote Files Using Vanilla JavaScript

The following is a module that handles opening, reading, and saving files in JavaScript. This allows for file manipulation on the client side.
Contents
1. Open File Dialog
2. Reading Files
3. Saving Files
4. Reading Remote URL Files
5. Saving Remote URL Files
6. FileIO.js Namespace
7. More Examples
1. Open File Dialog
Syntax is very straightforward. The following demonstrates opening a file dialog to get files from the user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Open a file dialog prompt to select files. <script> (async () => { // An array of File objects is returned. let files = await FileIO.openDialog({ contentTypes: '.json, text/csv, image/*', // Optional. The accepted file types allowMultiple: true, // Optional. Allows multiple file selection. Default is false }); // ... Do something with the selected files files.forEach((file, index) => { console.log(`File #${index + 1}: ${file.name}`); }); })(); </script> |
‘FileIO.openDialog‘ returns an array of File objects that contains the selected files after the user hits submit. You can specify the accepted file types, as well as if selecting multiple files is allowed or not.
2. Reading Files
The following example demonstrates how to read a file. This allows to read file contents.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Read file contents. <script> (async () => { // By default, file data is read as text. // To read a file as a Uint8Array, // set 'readAsTypedArray' to true let contents = await FileIO.read({ data: file, // File/Blob encoding: 'ISO-8859-1', // Optional. If reading as text, set the encoding. Default is UTF-8 readAsTypedArray: false, // Optional. Specifies to read the file as a Uint8Array. Default is false onProgressChange: (progressEvent) => { // Optional. Fires periodically as the file is read let percentLoaded = Number(((progressEvent.loaded / progressEvent.total) * 100).toFixed(2)); console.log(`Percent Loaded: ${percentLoaded}`); } }); // ... Do something with the file contents console.log(contents); })(); </script> |
‘FileIO.read‘ accepts either a File, or a Blob as acceptable data to be read.
By default, ‘FileIO.read‘ will read the file contents as text. To read file contents as an array, set the property ‘readAsTypedArray‘ to true. A Uint8Array is returned when this option is set to true.
3. Saving Files
The following example demonstrates how to create and save a file. This allows to save contents to a file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Save data to a file. <script> (() => { let text = 'My Programming Notes is awesome!'; let filename = 'myProgrammingNotes.txt' // The data to be saved can be a String/Blob/File/Uint8Array let data = text; // Example demonstrating the other datatypes that can be saved //data = new Blob([text], {type: "text/plain;charset=utf-8"}); //data = new File([text], filename, {type: "text/plain;charset=utf-8"}); //data = FileIO.stringToTypedArray(text); // Save the data to a file FileIO.save({ data: data, // String/Blob/File/Uint8Array filename: filename, // Optional if data is a File object decodeBase64Data: false, // Optional. If data is a base64 string, specifies if it should be decoded before saving. Default is false. }); // ... The file is saved to the client })(); </script> |
‘FileIO.save‘ accepts either a String, Blob, File, or a Uint8Array as acceptable data to be saved.
If the data to be saved is a base64 string, set the property ‘decodeBase64Data‘ to true in order to decode the string before saving. If the bease64 string is a file, this will allow the actual file to be decoded and saved to the client. Note: This action is only performed if the data to be saved is a string.
4. Reading Remote URL Files
The following example demonstrates how ‘FileIO.read‘ can be used to read downloaded file contents from a remote URL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Read file contents from a remote URL. <script> (async () => { // Remote file url let url = 'https://www.w3.org/TR/PNG/iso_8859-1.txt'; // Other example files //url = 'http://samplecsvs.s3.amazonaws.com/Sacramentorealestatetransactions.csv'; //url = 'http://dummy.restapiexample.com/api/v1/employees'; // Fetch the URL let response = await fetch(url); // Get the response file data let blob = await response.blob(); // Read the remote file as text let contents = await FileIO.read({ data: blob }); // ... Do something with the file contents console.log(contents); })(); </script> |
The example above uses fetch to retrieve data from the remote URL. Once the data is received, ‘FileIO.read‘ can be used to read its file contents.
5. Saving Remote URL Files
The following example demonstrates how ‘FileIO.save‘ can be used to save downloaded file contents from a remote URL to the client.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Save file contents from a remote URL. <script> (async () => { // Remote file url let url = 'https://www.w3.org/TR/PNG/iso_8859-1.txt'; // Other example files //url = 'https://www.nba.com/lakers/sites/lakers/files/lakers_logo_500.png'; //url = 'http://www.africau.edu/images/default/sample.pdf'; //url = 'https://gahp.net/wp-content/uploads/2017/09/sample.pdf' //url = 'http://samplecsvs.s3.amazonaws.com/Sacramentorealestatetransactions.csv'; //url = 'https://file-examples-com.github.io/uploads/2017/02/file-sample_1MB.docx'; // Fetch the URL let response = await fetch(url); // Get the response file data let blob = await response.blob(); // Save the remote file to the client FileIO.save({ data: blob, filename: FileIO.getFilename(url) }); // ... The file is saved to the client })(); </script> |
The example above uses fetch to retrieve data from the remote URL. Once the data is received, ‘FileIO.save‘ can be used to save its file contents.
6. FileIO.js Namespace
The following is the FileIO.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jul 17, 2020 // Taken From: http://programmingnotes.org/ // File: FileIO.js // Description: Namespace which handles opening, reading and saving files // Example: // // 1. Save data to a file // FileIO.save({ // data: 'My Programming Notes is awesome!', // filename: 'notes.txt', // decodeBase64Data: false, // optional // }); // // // 2. Get files // let files = await FileIO.openDialog({ // contentTypes: '.txt, .json, .doc, .png', // optional // allowMultiple: true, // optional // }); // // // 3. Read file contents // let contents = await FileIO.read({ // data: file, // encoding: 'UTF-8', // optional // readAsTypedArray: false, // optional // }); // ============================================================================ /** * NAMESPACE: FileIO * USE: Handles opening, reading and saving files. */ var FileIO = FileIO || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: save * USE: Exports data to be saved to the client * @param options: An object of file save options. * Its made up of the following properties: * { * data: The data (String/Blob/File/Uint8Array) to be saved. * filename: Optional. The name of the file * decodeBase64Data: Optional. If the data to be saved is not * a Blob type, it will be converted to one. If the data * being converted is a base64 string, this specifies * if the string should be decoded or not when converting * to a Blob. * } * @return: N/A. */ exposed.save = (options) => { if (isNull(options)) { throw new TypeError('There are no options specified'); } else if (isNull(options.data)) { throw new TypeError('There is no data is specified to save'); } let blob = options.data; let decodeBase64Data = options.decodeBase64Data || false; // Check to see if the file data is a blob. // Try to convert it if not if (!isBlob(blob)) { blob = exposed.convertToBlob(blob, decodeBase64Data); } let filename = options.filename || blob.name || 'FileIO_Download'; let urlCreator = window.URL || window.webkitURL; let url = urlCreator.createObjectURL(blob); let a = document.createElement('a'); a.download = filename; a.href = url; a.style.display = 'none'; a.rel = 'noopener'; a.target = '_blank'; document.body.appendChild(a); elementClick(a); setTimeout(() => { document.body.removeChild(a); urlCreator.revokeObjectURL(url) }, 250); } /** * FUNCTION: openDialog * USE: Opens a file dialog box which enables the user to select a file * @param options: An object of file dialog options. * Its made up of the following properties: * { * contentTypes: Optional. A comma separated string of * acceptable content types. * allowMultiple: Optional. Boolean indicating if the user * can select multple files. Default is false. * } * @return: A promise that returns the selected files when the user hits submit. */ exposed.openDialog = (options) => { return new Promise((resolve, reject) => { try { if (isNull(options)) { options = {}; } // Check to see if content types is an array let contentTypes = options.contentTypes || ''; if (!isNull(contentTypes) && !Array.isArray(contentTypes)) { contentTypes = contentTypes.split(','); } if (!isNull(contentTypes)) { contentTypes = contentTypes.join(','); } let input = document.createElement('input'); input.type = 'file'; input.multiple = options.allowMultiple || false; input.accept = contentTypes; input.addEventListener('change', (e) => { // Convert FileList to an array let files = Array.prototype.slice.call(input.files); resolve(files); }); input.style.display = 'none'; document.body.appendChild(input); elementClick(input); setTimeout(() => { document.body.removeChild(input); }, 250); } catch (e) { let message = e.message ? e.message : e; reject(`Failed to open dialog. Reason: ${message}`); } }); } /** * FUNCTION: read * USE: Reads a file and gets its file contents * @param options: An object of file read options. * Its made up of the following properties: * { * data: The data (File/Blob) to be read. * encoding: Optional. String that indicates the file encoding * when opening a file while reading as text. Default is UTF-8. * readAsText is the default behavior. * readAsTypedArray: Optional. Boolean that indicates if the file should * be read as a typed array. If this option is specified, a Uint8Array * object is returned on file read. * onProgressChange(progressEvent): Function that is fired periodically as * the FileReader reads data. * } * @return: A promise that returns the file contents after successful file read. */ exposed.read = (options) => { return new Promise((resolve, reject) => { try { if (!(window && window.File && window.FileList && window.FileReader)) { throw new TypeError('Unable to read. Your environment does not support File API.'); } else if (isNull(options)) { // Check to see if there are options throw new TypeError('There are no options specified.'); } else if (isNull(options.data)) { // Check to see if a file is specified throw new TypeError('Unable to read. There is no data specified.'); } else if (!isBlob(options.data)) { // Check to see if a file is specified throw new TypeError(`Unable to read data of type: ${typeof options.data}. Reason: '${options.data}' is not a Blob/File.`); } else if (!isNull(options.onProgressChange) && !isFunction(options.onProgressChange)) { // Check to see if progress change callback is a function throw new TypeError(`Unable to call onProgressChange of type: ${typeof options.onProgressChange}. Reason: '${options.onProgressChange}' is not a function.`); } let file = options.data; let encoding = options.encoding || 'UTF-8'; let readAsTypedArray = options.readAsTypedArray || false; // Setting up the reader let reader = new FileReader(); // Tell the reader what to do when it's done reading reader.addEventListener('load', (e) => { let content = e.target.result; if (readAsTypedArray) { content = new Uint8Array(content) } resolve(content); }); // Return the error reader.addEventListener('error', (e) => { reject(reader.error); }); // Call the on progress change function if specified if (options.onProgressChange) { let events = ['loadstart', 'loadend', 'progress']; events.forEach((eventName) => { reader.addEventListener(eventName, (e) => { options.onProgressChange.call(this, e); }); }); } // Begin reading the file if (readAsTypedArray) { reader.readAsArrayBuffer(file); } else { reader.readAsText(file, encoding); } } catch (e) { let message = e.message ? e.message : e; reject(`Failed to read data. Reason: ${message}`); } }); } /** * FUNCTION: convertToBlob * USE: Converts data to a Blob * @param data: Data to be converted to a Blob. * @param decodeBase64Data: Indicates if the data should be base64 decoded. * @return: The data converted to a Blob. */ exposed.convertToBlob = (data, decodeBase64Data = false) => { let arry = data; if (!isTypedArray(arry)) { arry = decodeBase64Data ? exposed.base64ToTypedArray(data) : exposed.stringToTypedArray(data); } let blob = new Blob([arry]); return blob; } /** * FUNCTION: stringToTypedArray * USE: Converts a string to a typed array (Uint8Array) * @param data: String to be converted to a typed array. * @return: The data converted to a typed array. */ exposed.stringToTypedArray = (data) => { let arry = new Uint8Array(data.length); for (let index = 0; index < data.length; ++index) { arry[index] = data.charCodeAt(index); } return arry; } /** * FUNCTION: typedArrayToString * USE: Converts a typed array to a string * @param data: Typed array to be converted to a string. * @return: The data converted to a string. */ exposed.typedArrayToString = (data) => { let str = ''; for (let index = 0; index < data.length; ++index) { str += String.fromCharCode(data[index]); } return str; } /** * FUNCTION: base64ToTypedArray * USE: Converts a base64 string to a typed array * @param data: Base64 string to be converted to a typed array. * @return: The data converted to a typed array. */ exposed.base64ToTypedArray = (data) => { let str = atob(data); let arry = exposed.stringToTypedArray(str); return arry; } /** * FUNCTION: typedArrayToBase64 * USE: Converts a typed array to a base64 string * @param data: Typed array to be converted to a base64 string. * @return: The data converted to a base64 string. */ exposed.typedArrayToBase64 = (data) => { let str = exposed.typedArrayToString(data); return btoa(str); } /** * FUNCTION: getFilename * USE: Returns the filename of a url * @param url: The url. * @return: The filename of a url. */ exposed.getFilename = (url) => { let filename = ''; if (url && url.length > 0) { filename = url.split('\\').pop().split('/').pop(); // Remove any querystring if (filename.indexOf('?') > -1) { filename = filename.substr(0, filename.indexOf('?')); } } return filename; } /** * FUNCTION: getExtension * USE: Returns the file extension of the filename of a url * @param url: The url. * @return: The file extension of a url. */ exposed.getExtension = (url) => { let filename = exposed.getFilename(url); let ext = filename.split('.').pop(); return (ext === filename) ? '' : ext; } // -- Private data -- let isTypedArray = (item) => { return item && ArrayBuffer.isView(item) && !(item instanceof DataView); } let isBlob = (item) => { return item instanceof Blob; } let isNull = (item) => { return undefined === item || null === item } let isFunction = (item) => { return 'function' === typeof item } let elementClick = (element) => { try { element.dispatchEvent(new MouseEvent('click')) } catch (e) { let evt = document.createEvent('MouseEvents') evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null) element.dispatchEvent(evt) } } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(FileIO)); // http://programmingnotes.org/ |
7. More Examples
Below are more examples demonstrating the use of ‘FileIO.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jul 17, 2020 // Taken From: http://programmingnotes.org/ // File: fileIODemo.html // Description: Demonstrates the use of FileIO.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes FileIO.js Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } .button { padding: 5px; background-color: #d2d2d2; height:100%; text-align:center; text-decoration:none; color:black; display: flex; justify-content: center; align-items: center; flex-direction: column; border-radius: 15px; cursor: pointer; width: 120px; } .button:hover { background-color:#bdbdbd; } .inline { display:inline-block; } .image-section, .text-section, .video-section { width: 60%; vertical-align: text-top; margin: 15px; } .imageContainer, .textContainer, .videoContainer { text-align: left; min-height: 100px; max-height: 350px; overflow: auto; } .text-section { width: 30%; } .textContainer { border: 1px solid black; padding: 10px; } .video-section { width: 100%; } .videoContainer { text-align: center; } .fileStatus { margin: 10px; display: none; } </style> <!-- // Include module --> <script type="text/javascript" src="./FileIO.js"></script> </head> <body> <div class="main"> My Programming Notes FileIO.js Demo <div style="margin: 10px;"> <div id="btnOpenFile" class="inline button"> Open File </div> <div class="fileStatus"> </div> </div> <hr /> <div class="inline image-section" > Images: <div class="imageContainer"></div> </div> <div class="inline text-section"> Text: <div class="textContainer"></div> <div style="margin: 10px;"> <div id="btnSaveText" class="inline button"> Save Text </div> </div> </div> <div class="video-section"> Video: <div class="videoContainer"></div> </div> </div> <script> // Open a file dialog prompt to select files. (async () => { return; // Opens a file dialog prompt. // An array of File objects is returned. let files = await FileIO.openDialog({ contentTypes: '.json, text/csv, image/*', // Optional. The accepted file types allowMultiple: true, // Optional. Allows multiple file selection. Default is false }); // ... Do something with the selected files files.forEach((file, index) => { console.log(`File #${index + 1}: ${file.name}`); }); })(); </script> <script> // Read file contents. (async () => { return; // By default, file data is read as text. // To read a file as a Uint8Array, // set 'readAsTypedArray' to true let contents = await FileIO.read({ data: file, // File/Blob encoding: 'ISO-8859-1', // Optional. If reading as text, set the encoding. Default is UTF-8 readAsTypedArray: false, // Optional. Specifies to read the file as a Uint8Array. Default is false onProgressChange: (progressEvent) => { // Optional. Fires periodically as the file is read let percentLoaded = Number(((progressEvent.loaded / progressEvent.total) * 100).toFixed(2)); console.log(`Percent Loaded: ${percentLoaded}`); } }); // ... Do something with the file contents console.log(contents); })(); </script> <script> // Save data to a file. (() => { return; let text = 'My Programming Notes is awesome!'; let filename = 'myProgrammingNotes.txt' // The data to be saved can be a String/Blob/File/Uint8Array let data = text; // Example demonstrating the other datatypes that can be saved //data = new Blob([text], {type: "text/plain;charset=utf-8"}); //data = new File([text], filename, {type: "text/plain;charset=utf-8"}); //data = FileIO.stringToTypedArray(text); // Save the data to a file FileIO.save({ data: data, // String/Blob/File/Uint8Array filename: filename, // Optional if data is a File object decodeBase64Data: false, // Optional. If data is a base64 string, specifies if it should be decoded before saving. Default is false. }); // ... The file is saved to the client })(); </script> <script> // Read a remote file (async () => { return; // Remote file url let url = 'https://www.w3.org/TR/PNG/iso_8859-1.txt'; // Other example files //url = 'http://samplecsvs.s3.amazonaws.com/Sacramentorealestatetransactions.csv'; //url = 'http://dummy.restapiexample.com/api/v1/employees'; //url = 'file:///C:/Users/Jr/Desktop/html%20projects/fileIOExample.html'; // Fetch the URL let response = await fetch(url); // Get the response file data let blob = await response.blob(); // Read the remote file as text let contents = await FileIO.read({ data: blob }); // ... Do something with the file contents console.log(contents); })(); </script> <script> // Save a remote file (async () => { return; // Remote file url let url = 'https://www.nba.com/lakers/sites/lakers/files/lakers_logo_500.png'; // Other example files //url = 'https://www.w3.org/TR/PNG/iso_8859-1.txt'; //url = 'http://www.africau.edu/images/default/sample.pdf'; //url = 'https://gahp.net/wp-content/uploads/2017/09/sample.pdf' //url = 'http://samplecsvs.s3.amazonaws.com/Sacramentorealestatetransactions.csv'; //url = 'https://file-examples-com.github.io/uploads/2017/02/file-sample_1MB.docx'; // Fetch the URL let response = await fetch(url); // Get the response file data let blob = await response.blob(); // Save the remote file to the client FileIO.save({ data: blob, filename: FileIO.getFilename(url) }); // ... The file is saved to the client })(); </script> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Open file document.querySelector('#btnOpenFile').addEventListener('click', async (e) => { // Open a file dialog prompt to select a file let files = await FileIO.openDialog({ contentTypes: '.txt, .json, text/plain, text/csv, image/*, video/*', allowMultiple: true, }); // Go through each file and read them for (let index = 0; index < files.length; ++index) { let file = files[index]; console.log(`Reading File: ${file.name}`, `, Type: ${file.type}`); // Read the file as a Uint8Array let contents = await FileIO.read({ data: file, readAsTypedArray: true, onProgressChange: (progressEvent) => { let percentLoaded = Number(((progressEvent.loaded / progressEvent.total) * 100).toFixed(2)); updateProgress(file, percentLoaded); } }); console.log(`Finished Reading File: ${file.name}`, `, Type: ${file.type}`); // Show the file showFile(file, contents); } // Hide the status info after a certain amount of time setTimeout(() => updateProgress(null, 0), 5000); }); // Save file document.querySelector('#btnSaveText').addEventListener('click', (e) => { let data = getTextContainer().innerHTML; let filename = 'FileIO_OutputExample.txt'; // Make sure there is data to save if (data.trim().length < 1) { alert('Please load a text file before saving output!'); return; } // Save the text that is in the output div to a file FileIO.save({ data: data, filename: filename, }); }); }); let showFile = (file, data) => { if (isImage(file)) { showImage(file, data); } else if (isVideo(file)) { showVideo(file, data); } else { showText(file, data); } } let showImage = (file, data) => { let output = getImageContainer(); let url = 'data:image/png;base64,' + FileIO.typedArrayToBase64(data); // You could also do this below to create a virtual url //let blob = FileIO.convertToBlob(data); //let url = URL.createObjectURL(blob); let divElementContainer = createElementContainerDiv(); let img = document.createElement("img"); img.src = url; img.height = 150; img.title = file.name; divElementContainer.appendChild(img); divElementContainer.appendChild(createFilenameDiv(file)); output.appendChild(divElementContainer); } let showText = (file, data) => { let output = getTextContainer(); let content = ''; try { content = new TextDecoder('UTF-8').decode(data); } catch (e) { content = FileIO.typedArrayToString(data); } let currentText = output.innerHTML; let newText = ''; newText += ` <div style='text-align: center; margin-bottom: 20px;'> File Name: <span style='font-style: italic; text-decoration: underline;'> ${file.name} </span> </div>`; newText += content; if (currentText.length > 0) { newText += '<br />=========================================<br />'; } newText += currentText; output.innerHTML = newText; } let showVideo = (file, data) => { let output = getVideoContainer(); let blob = FileIO.convertToBlob(data); let divElementContainer = createElementContainerDiv(); let video = document.createElement('video'); video.width = 400; video.controls = true; divElementContainer.appendChild(video); let source = document.createElement('source'); source.src = URL.createObjectURL(blob); source.type = file.type; video.appendChild(source); divElementContainer.appendChild(createFilenameDiv(file)); output.appendChild(divElementContainer); } let isImage = (file) => { try { return isType(file, 'image.*'); } catch (e) { let ext = FileIO.getExtension(file.name); return ['jpg', 'gif', 'bmp', 'png'].indexOf(ext) > -1; } } let isVideo = (file) => { try { return isType(file, 'video.*'); } catch (e) { let ext = FileIO.getExtension(file.name); return ['m4v', 'avi','mpg','mp4', 'webm'].indexOf(ext) > -1; } } let isType = (file, typeCheck) => { return file.type.match(typeCheck) != null; } let getImageContainer = () => { return document.querySelector('.imageContainer'); } let getTextContainer = () => { return document.querySelector('.textContainer'); } let getVideoContainer = () => { return document.querySelector('.videoContainer'); } let getFileStatusContainer = () => { return document.querySelector('.fileStatus'); } let createElementContainerDiv = () => { let divElementContainer = document.createElement("div"); divElementContainer.style.display = 'inline-block'; divElementContainer.style.textAlign = 'center'; divElementContainer.style.margin = '10px'; return divElementContainer; } let createFilenameDiv = (file) => { let divFileName = document.createElement("div"); divFileName.innerHTML = `${file.name}`; //divFileName.style.fontSize = '12.5px'; divFileName.style.width = '150px'; divFileName.style.margin = 'auto'; divFileName.style.wordWrap = 'break-word'; return divFileName; } let updateProgress = (file, percentLoaded) => { let output = getFileStatusContainer(); if (file) { let status = `File: ${file.name} <br />Percent Loaded: ${percentLoaded}`; output.style.display = 'block'; output.innerHTML = status; console.log(status.replace('<br />', '\n')); } else { output.style.display = null; } } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript || Deserialize & Parse JSON Date To A Date Object & Convert Date String To Date Object Using Vanilla JavaScript

The following is a module which demonstrates how to parse a JSON date string to a date object, as well as converting a date string to a date object.
1. Convert Date String To Date Object
Using ‘Utils.parseDate‘, the example below demonstrates how to convert a date string to a date object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// Convert string to date <script> (() => { // Convert the date strings to a date object let dates = [ '2020-01-28T00:45:08.118Z', '2020-07-29', '/Date(1238540400000)/', '/Date(1594368487704)/', '2020-11-28 03:20:35', `/Date(${Date.now()})/`, '7/14/2020 03:20:35 PM', ]; dates.forEach((dateString, index) => { // Convert the date string to a date object let date = Utils.parseDate(dateString); console.log(`\n${index + 1}. ${dateString}`); console.log('Is Date: ', date instanceof Date); console.log(date.toLocaleString()); }); })(); </script> // expected output: /* 1. 2020-01-28T00:45:08.118Z Is Date: true 1/27/2020, 4:45:08 PM 2. 2020-07-29 Is Date: true 7/28/2020, 5:00:00 PM 3. /Date(1238540400000)/ Is Date: true 3/31/2009, 4:00:00 PM 4. /Date(1594368487704)/ Is Date: true 7/10/2020, 1:08:07 AM 5. 2020-11-28 03:20:35 Is Date: true 11/28/2020, 3:20:35 AM 6. /Date(1594774590773)/ Is Date: true 7/14/2020, 5:56:30 PM 7. 7/14/2020 03:20:35 PM Is Date: true 7/14/2020, 3:20:35 PM */ |
2. Parse JSON Date String To Date Object
Using ‘Utils.parseDate‘, the example below demonstrates how to automatically parse a JSON string to a date object via a reviver function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Parse JSON date string to date <script> (() => { // Convert Json date in object let personJson = ` { "firstName": "Kenneth", "lastName": "P", "time": "2020-07-15T00:28:11.920Z" } `; // Deserialize json and automatically convert date string to date object let deserialized = JSON.parse(personJson, (key, value) => Utils.parseDate(value) ); console.log('Is Date: ', deserialized.time instanceof Date); console.log(deserialized.time.toLocaleString()); })(); </script> // expected output: /* Is Date: true 7/14/2020, 5:28:11 PM */ |
3. Utils.parseDate Namespace
The following is the Utils.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jul 14, 2020 // Taken From: http://programmingnotes.org/ // File: Utils.js // Description: Javascript that handles general utility functions // ============================================================================ /** * NAMESPACE: Utils * USE: Handles general utility functions. */ var Utils = Utils || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: parseDate * USE: Converts a date string to a date object. If the expression to * convert is not a valid date string, no conversion takes place and * the original value is returned. * @param value: The date value to be converted. * @return: The converted value. */ exposed.parseDate = (value) => { if (typeof value === 'string') { // Check ISO-formatted string let reISO = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])([T\s](([01]\d|2[0-3])\:[0-5]\d|24\:00)(\:[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3])\:?([0-5]\d)?)?)?$/; let a = reISO.exec(value); if (a) { // if so, Date() can parse it return new Date(value); } // Check Microsoft-format string let reMsAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/; a = reMsAjax.exec(value); if (a) { // Parse for relevant portions let b = a[1].split(/[-+,.]/); return new Date(b[0] ? +b[0] : 0 - +b[1]); } // Check forward slash date let reDate = /\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}|^\d{1,2}\/\d{1,2}\/\d{4}$/; a = reDate.exec(value); if (a) { // Parse for relevant portions return new Date(value); } } return value; } // -- Private data -- (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Utils)); // http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript || Convert & Deserialize JSON & Simple Object To A Class Type Using Vanilla JavaScript

The following is a module which demonstrates how to convert a simple object to a class type, as well as parsing a JSON serialized object back to its class type.
1. Convert Simple Object To Typed Class
The example below demonstrates how to convert a simple object to its class type. Converting the simple object to its class type allows us to use its class functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// Convert Simple Object To Class <script> class Person { constructor(firstName, lastName, address) { this.firstName = firstName; this.lastName = lastName; this.address = address } getFullName() { return `${this.firstName} ${this.lastName}`; } } (() => { // Convert simple object to class object let personObj = { firstName: 'Kenneth', lastName: 'P' }; // Convert to the class type 'Person' allowing // us to use the functions from that class let personType = Utils.ctype(personObj, Person); console.log(personType.getFullName()); })(); </script> // expected output: /* Kenneth P */ |
The ‘Utils.ctype‘ function converts the object to a class, which allows to use the functions from that class.
2. Deserialize Simple JSON Object To Typed Class
Parsing a simple JSON string and converting it to a typed class can be achieved in a similar way. The example below demonstrates this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Parse Simple JSON Object To Class <script> class Person { constructor(firstName, lastName, address) { this.firstName = firstName; this.lastName = lastName; this.address = address } getFullName() { return `${this.firstName} ${this.lastName}`; } } (() => { // Convert Json to class object let personJson = ` { "firstName": "Kenneth", "lastName": "P" } `; // Convert to the class type 'Person' allowing // us to use the functions from that class let personType = Utils.ctype(JSON.parse(personJson), Person); console.log(personType.getFullName()); })(); </script> // expected output: /* Kenneth P */ |
3. Deserialize Simple Nested JSON Object To Typed Class
Parsing a multi-leveled, nested JSON string and converting it to a typed class is done a little differently. We have to map the JSON structure with the values we want to convert. This is done using ‘Utils.ctypeMap‘.
The example below demonstrates this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// Parse Nested JSON Object To Class <script> class Person { constructor(firstName, lastName, address) { this.firstName = firstName; this.lastName = lastName; this.address = address } getFullName() { return `${this.firstName} ${this.lastName}`; } } class Address { constructor(street, city, zip) { this.street = street; this.city = city; this.zip = zip; } getFullAddress() { return `${this.street} - ${this.city}, ${this.zip}`; } } (() => { // Convert Json to class object let personJson = ` { "firstName": "Kenneth", "lastName": "P", "address": { "street": "Valley Hwy", "city": "Valley Of Fire", "zip": "89040" } } `; // Create a conversion type map let conversionTypeMap = { '': Person, // Convert the entire object to type 'Person' 'address': Address, // Convert the address property to type 'Address' }; // Deserialize the json string and convert the types let personType = Utils.ctypeMap(JSON.parse(personJson), conversionTypeMap); console.log(personType.getFullName()); console.log(personType.address.getFullAddress()); })(); </script> // expected output: /* Kenneth P Valley Hwy - Valley Of Fire, 89040 */ |
4. Deserialize Complex Nested JSON Object To Typed Class
Parsing a complex JSON string and converting it to a typed class can be achieved in a similar way. The example below demonstrates this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
// Parse Nested JSON Object To Class <script> class Address { constructor(street, city, zip) { this.street = street; this.city = city; this.zip = zip; } getFullAddress() { return `${this.street} - ${this.city}, ${this.zip}`; } } class Person { constructor(firstName, lastName, address) { this.firstName = firstName; this.lastName = lastName; this.address = address } getFullName() { return `${this.firstName} ${this.lastName}`; } } class Album { constructor(artist, title, released) { this.artist = artist; this.title = title; this.released = released } getDescription() { return `${this.artist}: ${this.title} - ${this.released}`; } } (() => { // Convert Json object array to class object let personJson = `{ "people": [ { "firstName": "Kenneth", "lastName": "P", "address": { "street": "Valley Hwy", "city": "Valley Of Fire", "zip": "89040" }, "albums": [ { "artist": "System of a Down", "title": "Toxicity", "released": 2001 }, { "artist": "Kanye West", "title": "The College Dropout", "released": 2004 } ] }, { "firstName": "Jennifer", "lastName": "N", "address": { "street": "Boulder Ln", "city": "Yosemite Valley", "zip": "95389" }, "albums": [ { "artist": "Coldplay", "title": "Parachutes", "released": 2000 }, { "artist": "Alicia Keys", "title": "Songs in A Minor", "released": 2001 } ] } ], "nested": { "albums": [ { "artist": "Coldplay", "title": "Parachutes", "released": 2000 }, { "artist": "Alicia Keys", "title": "Songs in A Minor", "released": 2001 }, { "artist": "System of a Down", "title": "Toxicity", "released": 2001 }, { "artist": "Kanye West", "title": "The College Dropout", "released": 2004 } ] }, "nested2": { "album": { "artist": "Coldplay", "title": "Parachutes", "released": 2000 } } }`; // Create a conversion type map let conversionTypeMap = { 'people.albums': Album, // Convert all the albums in the person class array to type 'Album' 'people[0].address': Address, // Convert only the first address object of the people array to type 'Address' 'people.address': Address, // Convert all address objects of the people array to type 'Address' 'people': Person, // Convert all the objects in the people array to type 'Person' 'nested.albums': Album, // Convert all the albums objects in the nested object to type 'Album' 'nested2.album': Album, // Convert all the albums in the nested2 object to type 'Album' }; // Deserialize the json string and convert the types let deserializedData = Utils.ctypeMap(JSON.parse(personJson), conversionTypeMap); // Print the converted addresses using the class function console.log('\nPeoples Albums:'); deserializedData.people.forEach((person, index) => { console.log((index + 1) + '. ' + person.getFullName() + '\'s Albums:'); person.albums.forEach((album, index) => { console.log(`\t${index + 1}. ${album.getDescription()}`); }); }); // Print the first address object console.log('\nFirst address object'); console.log(deserializedData.people[0].address.getFullAddress()); // Print the converted people using the class function console.log('\nPeople:'); deserializedData.people.forEach((person, index) => { console.log(`${index + 1}. ${person.getFullName()}`); }); // Print the converted albums using the class function console.log('\nNested Albums:'); deserializedData.nested.albums.forEach((album, index) => { console.log(`${index + 1}. ${album.getDescription()}`); }); // Print the converted addresses using the class function console.log('\nPeoples Address:'); deserializedData.people.forEach((person, index) => { console.log(`${index + 1}. ${person.address.getFullAddress()}`); }); console.log('\nNested 2'); console.log(deserializedData.nested2.album.getDescription()); })(); </script> // expected output: /* Peoples Albums: 1. Kenneth P's Albums: 1. System of a Down: Toxicity - 2001 2. Kanye West: The College Dropout - 2004 2. Jennifer N's Albums: 1. Coldplay: Parachutes - 2000 2. Alicia Keys: Songs in A Minor - 2001 First address object Valley Hwy - Valley Of Fire, 89040 People: 1. Kenneth P 2. Jennifer N Nested Albums: 1. Coldplay: Parachutes - 2000 2. Alicia Keys: Songs in A Minor - 2001 3. System of a Down: Toxicity - 2001 4. Kanye West: The College Dropout - 2004 Peoples Address: 1. Valley Hwy - Valley Of Fire, 89040 2. Boulder Ln - Yosemite Valley, 95389 Nested 2 Coldplay: Parachutes - 2000 */ |
5. Utils.ctype/ctypeMap Namespace
The following is the Utils.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jul 14, 2020 // Taken From: http://programmingnotes.org/ // File: Utils.js // Description: Javascript that handles general utility functions // ============================================================================ /** * NAMESPACE: Utils * USE: Handles general utility functions. */ var Utils = Utils || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: ctype * USE: Returns the result of explicitly converting an expression * to a specified data type or class * @param expression: The object to be converted. * @param typename: The data type or class to be converted to. * @return: The converted value. */ exposed.ctype = (expression, typename) => { let value = expression; if (!exposed.isType(expression, typename)) { value = Object.create(typename.prototype, Object.getOwnPropertyDescriptors(expression)); } return value; } /** * FUNCTION: ctypeMap * USE: Returns the result of explicitly converting an expression * to a specified data type or class * @param expression: The object to be converted. * @param conversionTypeMap: An object that specifies the data type * or class to be converted to. * @return: The converted value. */ exposed.ctypeMap = (expression, conversionTypeMap) => { // Go through each property and convert // objects to the class specific types for (const prop in conversionTypeMap) { let type = conversionTypeMap[prop]; if (prop.trim().length < 1) { expression = exposed.ctype(expression, type); continue; } let items = getSetDescendantProp(expression, prop); if (!items) { continue; } else if (!Array.isArray(items)) { items = [items]; } items.forEach((obj, index) => { if (exposed.isType(obj, type)) { return; } getSetDescendantProp(expression, prop, obj, exposed.ctype(obj, type)); }); } return expression; } exposed.isType = (expression, typename) => { return (expression instanceof typename); } // -- Private data -- let getSetDescendantProp = (obj, desc, prevValue, newValue) => { let arr = desc ? desc.split('.') : []; // Go through the item properties and try to // find the item that matches 'desc' while (arr.length && obj) { let comp = arr.shift(); // Handle full arrays let target = obj[comp]; if (target && ((target.length && target.forEach) || Array.isArray(target))) { let remainder = arr.join('.'); let results = []; for (let index = 0; index < target.length; ++index){ let x = getSetDescendantProp(target[index], remainder, prevValue, newValue); if (x) { results = results.concat(x); } if (remainder.length < 1 && typeof newValue !== 'undefined') { if (prevValue === target[index]) { target[index] = newValue; } } } return results; } else { // Handle indexed arrays let match = new RegExp('(.+)\\[([0-9]*)\\]').exec(comp); if ((match !== null) && (match.length == 3)) { let arrayData = { arrName: match[1], arrIndex: match[2] }; if (obj[arrayData.arrName] !== undefined) { if (typeof newValue !== 'undefined' && arr.length === 0 && prevValue === obj[arrayData.arrName][arrayData.arrIndex]) { obj[arrayData.arrName][arrayData.arrIndex] = newValue; } // Move to the next item obj = obj[arrayData.arrName][arrayData.arrIndex]; } else { obj = undefined; } } else { // Handle regular items if (typeof newValue !== 'undefined') { if (obj[comp] === undefined) { obj[comp] = {}; } if (arr.length === 0 && prevValue === obj[comp]) { obj[comp] = newValue; } } // Move to the next item obj = obj[comp]; } } } return obj; } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Utils)); // http://programmingnotes.org/ |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript || Promise – Resolve Promises In Order Of Completion Using Vanilla JavaScript

The return values of Promise.all are returned in the order that the Promises were passed in, regardless of completion order. Sometimes, it is nice to know which promise completed first.
The following is sample code which demonstrates how to resolve an array of promises and return the results in order of completion.
Contents
1. Resolve By Completion
2. Fail-Fast Behavior
3. Timeout
4. Utils.resolveByCompletion
5. More Examples
The examples below demonstrate the return values of Promise.all compared to ‘resolve by completion‘.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Promise all example <script> (() => { let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve) => setTimeout(() => resolve('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // Promise all Promise.all(promises).then(values => { console.log(values); }).catch(error => { console.log(error); }); })() </script> // expected output: /* [ "A (slow)","B (slower)","C (fast)" ] */ |
‘Resolve by completion’ returns the completed values from fastest to slowest.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Resolve By Completion <script> (() => { let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve) => setTimeout(() => resolve('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // Promise values are returned in the order of completion Utils.resolveByCompletion(promises).then(values => { console.log(values); }).catch(error => { console.log(error); }); })() </script> // expected output: /* [ "C (fast)","A (slow)","B (slower)" ] */ |
2. Fail-Fast Behavior
Promise.all is rejected if any of the elements are rejected. For example, if you pass in multiple promises that will resolve, and one promise that rejects, then Promise.all will reject immediately.
Similar to Promise.allSettled, ‘Resolve by completion’ allows returning the promises rejected value to the result list.
The following example demonstrates this behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Promise all example <script> (() => { let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve, reject) => setTimeout(() => reject('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // Rejected value is returned to the 'catch' function Promise.all(promises).then(values => { console.log(values); }).catch(error => { console.log('Rejected: ', error); }); })() </script> // expected output: /* Rejected: B (slower) */ |
‘Resolve by completion’ returns the rejected value as apart of the result list, depending on the boolean ‘rejectOnError’ parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Resolve By Completion <script> (() => { let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve, reject) => setTimeout(() => reject('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // Rejected promise values are returned when the parameter 'rejectOnError' is set to false. // Setting the parameter to true will reject the operation like normal. Utils.resolveByCompletion(promises, false).then(values => { console.log(values); }).catch(error => { console.log('Rejected: ', error); }); })() </script> // expected output: /* [ "C (fast)","A (slow)","B (slower)" ] */ |
3. Timeout
By default, ‘resolve by completion’ has no timeout. If setting a timeout is desired, it can be done like so.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Resolve By Completion - Timeout <script> (() => { let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve) => setTimeout(() => resolve('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // The operation will reject when timeout period expires. // The operation will reject on timeout, regardless of the 'rejectOnError' parameter Utils.resolveByCompletion(promises, false, 500).then(values => { console.log(values); }).catch(error => { console.log(error); }); })() </script> // expected output: /* Error: Timeout of 500ms expired */ |
4. Utils.resolveByCompletion
The following is Utils.resolveByCompletion. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jul 8, 2020 // Taken From: http://programmingnotes.org/ // File: Utils.js // Description: Javascript that handles general utility functions // ============================================================================ /** * NAMESPACE: Utils * USE: Handles general utility functions. */ var Utils = Utils || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: resolveByCompletion * USE: Returns a promise that will resolve when all of the input's * promises have resolved, in order of completion. * @param promises: An array of promises. * @param rejectOnError: Optional. Boolean that indicates if the operation * should reject if any of the input promises reject or throw an error. * If set to true, the operation is rejected if any of the input * promises reject or throw an error. * If set to false, the promises rejected value is added to the result list. * @param timeout: Optional. Integer that indicates how long to wait * (in milliseconds) for the promise group to complete. * @return: A promise that will contain the input promises results on completion. */ exposed.resolveByCompletion = (promises, rejectOnError = true, timeout = null) => { return new Promise(async (resolve, reject) => { try { let results = [] let promiseMap = new Map(); Array.prototype.forEach.call(promises, (promise, index) => { let promiseResult = { index: index, value: null, error: null }; let mapValue = null; if (promise instanceof Promise) { mapValue = promise .then(value => {promiseResult.value = value; return promiseResult}) .catch(error => {promiseResult.error = error; return promiseResult}) } else { mapValue = promiseResult; promiseResult.value = promise; } promiseMap.set(index, mapValue); }); let start = Date.now(); let isTimedOut = () => { let result = false; if (timeout) { let elapsed = (Date.now() - start); result = elapsed >= timeout; } return result; } while (promiseMap.size > 0) { let promiseResult = await Promise.race(promiseMap.values()); if (!promiseMap.delete(promiseResult.index)) { throw new Error('Error occurred processing values'); } if (promiseResult.error) { if (rejectOnError) { reject(promiseResult.error); return; } results.push(promiseResult.error); } else { results.push(promiseResult.value); } if (isTimedOut()) { throw new Error(`Timeout of ${timeout}ms expired`); } } resolve(results); } catch (e) { reject(e); } }); } // -- Private data -- (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Utils)); // http://programmingnotes.org/ |
5. More Examples
Below are more examples demonstrating the use of ‘Utils.resolveByCompletion’. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jul 8, 2020 // Taken From: http://programmingnotes.org/ // File: utils.html // Description: Demonstrates the use of functions in Utils.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Utils.resolveByCompletion Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } .output { text-align: left; } pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } .string { color: green; } .number { color: darkorange; } .boolean { color: blue; } .null { color: magenta; } .key { color: red; } </style> <!-- // Include module --> <script type="text/javascript" src="./Utils.js"></script> </head> <body> <div class="main"> My Programming Notes Utils.resolveByCompletion Demo <pre><code><div class="output"></div></code></pre> </div> <script> (() => { return; let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve) => setTimeout(() => resolve('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // Promise values are returned in the order of completion Utils.resolveByCompletion(promises).then(values => { console.log(values); print('Promise values are returned in the order of completion', values); }).catch(error => { console.log(error); }); })() </script> <script> (() => { return; let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve, reject) => setTimeout(() => reject('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // Rejected promise values are returned when the parameter 'rejectOnError' is set to false Utils.resolveByCompletion(promises, false).then(values => { console.log(values); print('Rejected promise values are returned when the parameter "rejectOnError" is set to false', values); }).catch(error => { console.log('Rejected: ', error); }); })() </script> <script> (() => { return; let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve) => setTimeout(() => resolve('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; // The operation will reject when timeout period expires Utils.resolveByCompletion(promises, false, 500).then(values => { console.log(values); print('The operation will reject when timeout period expires', values); }).catch(error => { console.log(error); print('', error.message); }); })() </script> <script> (async () => { //return; // Using promise all let promiseAllResult1 = await Promise.all(example1()); print('1. Resolved promises', promiseAllResult1, 4); // Using resolve by completion let resolveByResult1 = await Utils.resolveByCompletion(example1()); print('1. Resolved promises ordered by completion', resolveByResult1, 4); // Using promise all let promiseAllResult2 = await Promise.all(example2()); print('2. Resolved promises', promiseAllResult2, 4); // Using resolve by completion let resolveByResult2 = await Utils.resolveByCompletion(example2()); print('2. Resolved promises ordered by completion', resolveByResult2, 4); // Using promise all let promiseAllResult3 = await Promise.all(example3()); print('3. Resolved promises', promiseAllResult3, 4); // Using resolve by completion let resolveByResult3 = await Utils.resolveByCompletion(example3()); print('3. Resolved promises ordered by completion', resolveByResult3, 4); // Using promise all let promiseAllResult4 = await Promise.all(example4()); print('4. Resolved promises', promiseAllResult4, 4); // Using resolve by completion let resolveByResult4 = await Utils.resolveByCompletion(example4()); print('4. Resolved promises ordered by completion', resolveByResult4, 4); // Using promise all let promiseAllResult5 = Promise.all(example5()).catch((error) => { print('5. Promise.all rejected item', error, 4); }); // Using resolve by completion let resolveByResult5 = await Utils.resolveByCompletion(example5(), false); print('5. resolveByCompletion including rejected item', resolveByResult5, 4); })() function example1() { let promises = [ new Promise((resolve) => setTimeout(() => resolve('A (slow)'), 1000)), new Promise((resolve) => setTimeout(() => resolve('B (slower)'), 2000)), new Promise((resolve) => setTimeout(() => resolve('C (fast)'), 10)) ]; return promises; } function example2() { let promises = [ new Promise(resolve => {setTimeout(resolve, 200, 'slow');}), 'instant', new Promise(resolve => {setTimeout(resolve, 50, 'quick');}) ]; return promises; } function example3() { let promises = [ Promise.resolve(3), 42, new Promise((resolve, reject) => { setTimeout(resolve, 100, 'foo');}) ]; return promises; } function example4() { let promises = [1,2,3, Promise.resolve(444)]; return promises; } function example5() { let promises = [1,2,3, Promise.reject(555)]; return promises; } </script> <script> function print(desc, obj, indent = 0) { let text = (desc.length > 0 ? '<br />' : '') + desc + (desc.length > 0 ? '<br />' : ''); let objText = obj || ''; if (obj && typeof obj != 'string') { objText = syntaxHighlight(JSON.stringify(obj, null, indent)); } text += objText; let pageText = document.querySelector('.output').innerHTML; pageText += (pageText.length > 0 ? '<br />' : '') + text; document.querySelector('.output').innerHTML = pageText; } function syntaxHighlight(json) { if (typeof json != 'string') { json = JSON.stringify(json, undefined, 2); } json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; }); } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript || Resources.js – JavaScript & CSS Parallel File Loader To Dynamically Import & Load Files To Page Document Using Vanilla JavaScript

The following is a module that handles loading JavaScript and CSS files into a document in parallel, as fast as the browser will allow. This module also allows to ensure proper execution order if you have dependencies between files.
Contents
1. Load Files One By One
2. Load Multiple Files In Parallel
3. Load Multiple Files In Parallel With Dependencies
4. Load A JavaScript Function From Another JS File
5. Resources.js Namespace
6. More Examples
Syntax is very straightforward. The following demonstrates loading single files one by one:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Load JavaScript and CSS files one by one. <script> (async () => { // Load files one by one await Resources.load('/file1.js'); await Resources.load('/file2.js'); await Resources.load('/file3.css'); await Resources.load('/file4.css'); await Resources.load('https://code.jquery.com/jquery-3.5.1.js'); await Resources.load('https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js'); })(); </script> |
The ‘Resources.load‘ function returns an object that contains information about the load status. It can let you know if a file was loaded successfully or not.
To get the load status in order to determine if the file was loaded correctly, it can be done like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Load JavaScript and CSS files one by one and verify. <script> (async () => { // Load files one by one console.log(await Resources.load('/file1.js')); console.log(await Resources.load('/file2.js')); console.log(await Resources.load('/file3.css')); console.log(await Resources.load('/file4.css')); console.log(await Resources.load('https://code.jquery.com/jquery-3.5.1.js')); console.log(await Resources.load('https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js')); })(); </script> // expected output: /* [ { url: "/file1.js", loaded: false, notes: "*** /file1.js failed to load ***" } ] [ { url: "/file2.js", loaded: false, notes: "*** /file2.js failed to load ***" } ] [ { url: "/file3.css", loaded: false, notes: "*** /file3.css failed to load ***" } ] [ { url: "/file4.css", loaded: false, notes: "*** /file4.css failed to load ***" } ] [ { url: "https://code.jquery.com/jquery-3.5.1.js", loaded: true, notes: "https://code.jquery.com/jquery-3.5.1.js loaded" } ] [ { url: "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js", loaded: true, notes: "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js loaded" } ] */ |
In the example above, the load results can let you know which files were loaded into the document, and the reason for failure.
2. Load Multiple Files In Parallel
In the examples above, files were added sequentially, one by one.
Ideally, you would want to load all of your files at once in parallel. This makes it so you are able to add multiple files as fast as the browser will allow. Parallel loading also allows you to set if there are any file dependencies.
Using the files from the examples above, this can be done like so.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Load JavaScript and CSS files in parallel. <script> (async () => { // Load files in parallel let loadResult = await Resources.load({ files: [ { url: '/file1.js', type: 'js', wait: false }, { url: '/file2.js', type: 'js', wait: false }, { url: '/file3.css', type: 'css', wait: false }, { url: '/file4.css', type: 'css', wait: false }, { url: 'https://code.jquery.com/jquery-3.5.1.js', type: 'js', wait: false }, { url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js', type: 'js', wait: false }, ], logResults: true, rejectOnError: false, timeout: null }); console.log(loadResult); })(); </script> // possile output: /* [ { "url": "/file3.css", "loaded": false, "notes": "*** /file3.css failed to load ***" }, { "url": "/file4.css", "loaded": false, "notes": "*** /file4.css failed to load ***" }, { "url": "/file1.js", "loaded": false, "notes": "*** /file1.js failed to load ***" }, { "url": "/file2.js", "loaded": false, "notes": "*** /file2.js failed to load ***" }, { "url": "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js", "loaded": true, "notes": "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js loaded" }, { "url": "https://code.jquery.com/jquery-3.5.1.js", "loaded": true, "notes": "https://code.jquery.com/jquery-3.5.1.js loaded" } ] */ |
For parallel loading, the parameter supplied to the ‘Resources.load‘ function is an object that is made up of the following properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
{ • files: An object array of 'File Info' of files to load (See below). • logResults: Optional. Boolean that indicates if the load status should be displayed to console.log(). Default is false. • rejectOnError: Optional. Boolean that indicates if the load operation should reject if a loading error occurs for any file in the group. Default is false. • timeout: Optional. Integer that indicates how long to wait (in milliseconds) for the file group to load before timeout occurs. Default is null. // ** 'File Info': This is made up of the following properties { • url: The source url to load. • type: Optional. String indicating the file type to load. Options are 'css' or 'js'. Default type is determined by the file extension if not supplied. • wait: Optional. Boolean that indicates if this file should be completely loaded before loading subsequent files in the group. • If set to true, other urls are not loaded until this one finishes loading. After loading this file, other files continue loading in parallel • If set to false, no wait occurs and files load in parallel. This makes it so dependent files dont get loaded until the required files are loaded first. Files with this property set to true are loaded before any other file in the group. Default is false. } } |
3. Load Multiple Files In Parallel With Dependencies
The ‘wait‘ property is a great way to manage file dependencies. When the ‘wait‘ property is set to true, that indicates that the file should be completely loaded before loading subsequent files in the group.
This makes it so loading files in parallel does not cause files to be loaded before its dependent files.
The following examples demonstrates the ‘wait‘ functionality when loading files in parallel.
The example below demonstrates what happens when ‘wait‘ is not used. Load order behavior is undefined because files are loaded as they become available. This could cause an issue if the second file depends on the first file, but loads before it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Load JavaScript and CSS files in parallel. // Load order behavior is undefined because files // are loaded as they become available <script> (async () => { // Load files in parallel let loadResult = await Resources.load({ files: [ { url: 'https://code.jquery.com/jquery-3.5.1.js', type: 'js', wait: false }, { url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js', type: 'js', wait: false }, ], logResults: true, rejectOnError: false, timeout: null }); console.log(loadResult); })(); </script> /* // possible output: [ { "url": "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js", "loaded": true, "notes": "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js loaded" }, { "url": "https://code.jquery.com/jquery-3.5.1.js", "loaded": true, "notes": "https://code.jquery.com/jquery-3.5.1.js loaded" } ] */ |
In the example below, the ‘wait‘ property is set to true for the first file, ensuring that it will always be loaded before any subsequent files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Load JavaScript and CSS files in parallel. // Load behavior is managed using wait <script> (async () => { // Load files in parallel let loadResult = await Resources.load({ files: [ { url: 'https://code.jquery.com/jquery-3.5.1.js', type: 'js', wait: true }, { url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js', type: 'js', wait: false }, ], logResults: true, rejectOnError: false, timeout: null }); console.log(loadResult); })(); </script> /* // expected output: [ { "url": "https://code.jquery.com/jquery-3.5.1.js", "loaded": true, "notes": "https://code.jquery.com/jquery-3.5.1.js loaded" }, { "url": "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js", "loaded": true, "notes": "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js loaded" } ] */ |
4. Load A JavaScript Function From Another JS File
‘Resources.load‘ could be used to call a JavaScript function in another js file. The example below demonstrates this.
In the following example, main.html is the ‘driver’ file. It loads ‘fullName.js‘ to get the functions located in that file. ‘fullName.js‘ then uses ‘Resources.load‘ to load the files ‘display.js‘, ‘log.js‘ and ‘date.js‘, and calls the functions from those files to display information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
<!-- // main.html // This file loads functions from fullName.js --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Resources.js Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } </style> <!-- Include Resources.js --> <script type="text/javascript" src="./Resources.js"></script> </head> <body> <div class="main"> My Programming Notes Resources.js Demo </div> <script> (async () => { // Load fullname.js to get the 'fullname' functions await Resources.load({ files: [ './fullName.js' ], logResults: true }); // Fucntions from fullname.js let fullName = getFullName('Kenneth', 'P'); // This calls a function in display.js via fullname.js with no return value await displayFullName(fullName).catch((error) => { // Log if any errors occur console.log('Failed:', error); }); // This calls a function in log.js via fullname.js with no return value await logFullName(fullName).catch((error) => { // Log if any errors occur console.log('Failed:', error); }); // This calls a function in date.js via fullname.js that rerturns a value try { alert(await getFullNameWithDate(fullName)); } catch (error) { // Log if any errors occur console.log('Failed:', error); } })(); </script> </body> </html> <!-- // expected output: *** Resources.js Load Status *** ./fullName.js loaded *** Resources.js Load Status *** ./display.js loaded ./log.js loaded ./date.js loaded [ALERT: Kenneth P] Kenneth P [ALERT: Name: Kenneth P Date: 7/7/2020, 12:09:44 PM] --> |
Below is fullName.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// fullName.js // Load the 'display', 'log' and 'getDateString' functions // from the files below. let dependencies = Resources.load({ files: [ './display.js', './log.js', './date.js' ], logResults: true }); let getFullName = (firstName, lastName) => { return `${firstName} ${lastName}`; } // Uses the 'display' function from display.js let displayFullName = (fullName) => { return new Promise(async (resolve, reject) => { try { // Make sure the dependencies have loaded. // The load status is logged to the console. let status = await dependencies; //console.log(status); display(fullName); resolve(); } catch (error) { // Log if any errors occur //console.log('Failed:', error); reject(error); } }); } // Uses the 'log' function from log.js let logFullName = (fullName) => { return new Promise(async (resolve, reject) => { try { // Make sure the dependencies have loaded. // The load status is logged to the console. let status = await dependencies; //console.log(status); log(fullName); resolve(); } catch (error) { // Log if any errors occur //console.log('Failed:', error); reject(error); } }); } // Uses the 'getDateString' function from date.js that returns a value let getFullNameWithDate = (fullName) => { return new Promise(async (resolve, reject) => { try { // Make sure the dependencies have loaded. // The load status is logged to the console. let status = await dependencies; //console.log(status); let date = getDateString(new Date()); resolve(`Name: ${fullName}\nDate: ${date}`); } catch (error) { // Log if any errors occur //console.log('Failed:', error); reject(error); } }); } |
Below is display.js
1 2 3 4 5 |
// display.js let display = (data) => { alert(data); } |
Below is log.js
1 2 3 4 5 |
// log.js let log = (data) => { console.log(data); } |
Below is date.js
1 2 3 4 5 |
// date.js let getDateString = (date) => { return date.toLocaleString(); } |
5. Resources.js Namespace
The following is the Resources.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jul 7, 2020 // Taken From: http://programmingnotes.org/ // File: Resources.js // Description: Javascript that loads Javascript and CSS files // into a document in parallel, as fast as the browser will allow. // This allows to ensure proper execution order if you have dependencies // between files. // Example: // // Load JavaScript and CSS files and wait for them to load // const dependencies = await Resources.load({ // files: [ // { url: '/file1.js', type: 'js', wait: false }, // { url: '/file2.js', type: 'js', wait: false }, // { url: '/file3.css', type: 'css', wait: false }, // { url: '/file4.css', type: 'css', wait: false }, // { url: 'https://code.jquery.com/jquery-3.5.1.js', type: 'js', wait: true }, // { url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js', type: 'js', wait: false }, // ], // logResults: true, // rejectOnError: false, // timeout: null // }); // ============================================================================ /** * NAMESPACE: Resources * USE: Handles loading Javascript and CSS files in parallel. */ var Resources = Resources || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: load * USE: Loads Css and/or Javascript files into a document in * parallel, depending on the options * @param options: An object of file loading options. * Its made up of the following properties: * { * files: An object array of File info of files to load (See below). * logResults: Optional. Boolean that indicates if the load * status should be displayed to console.log(). * Default is false. * rejectOnError: Optional. Boolean that indicates if the load * operation should reject if a loading error occurs for * any file in the group. * Default is false. * timeout: Optional. Integer that indicates how long to wait * (in milliseconds) for the file group to load before timeout occurs. * Default is null. * } * * ** File info: This is made up of the following properties * { * url: The source url to load. * type: Optional. String indicating the file type to load. * Options are 'css' or 'js'. * Default type is determined by the file extension. * wait: Optional. Boolean that indicates if this file should be * completely loaded before loading subsequent files in the group. * If set to true, other urls are not loaded until this one * finishes loading. After loading this file, other files * continue loading in parallel. * If set to false, no wait occurs and files load in parallel. * This makes it so dependent files dont get loaded until the * required files are loaded first. * Files with this property set to true are loaded before * any other file in the group. * Default is false. * } * @return: A promise that completes when all resources are loaded, which * contains an array of each files loading status. */ exposed.load = (options) => { return new Promise(async (resolve, reject) => { try { // Load files and wait for the results options = verifyOptions(options); let results = await loadFiles(options); if (options.logResults) { console.log('*** Resources.js Load Status ***') let notes = results.map((x) => x.notes); console.log(notes.join('\n')); } resolve(results); } catch (e) { reject(e); } }); } /** * FUNCTION: loadJs * USE: Loads a Javascript file into a document * @param url: The javascript url to load. * @return: A promise that completes when the url is loaded. */ exposed.loadJs = (url) => { return new Promise((resolve, reject) => { // Load file and attach it to the document let status = createFileStatus(url); try { if (fileExists(url)) { status.loaded = true; status.notes = `${url} already loaded`; resolve(status); return; } let script = document.createElement("script"); script.src = url; script.type = "text/javascript" script.id = getId(url); script.onload = script.onreadystatechange = function() { if ((!this.readyState || this.readyState == "loaded" || this.readyState == "complete") ) { status.loaded = true; status.notes = `${url} loaded`; resolve(status); } } script.onerror = () => { removeFile(url); status.notes = `*** ${url} failed to load ***`; reject(status); } document.getElementsByTagName("head")[0].appendChild(script); addFile(url); } catch (e) { let message = e.message ? e.message : e; status.notes = `*** ${url} failed to load. Reason: ${message} ***`; reject(status); } }); } /** * FUNCTION: loadCss * USE: Loads a Css file into a document * @param url: The css url to load. * @return: A promise that completes when the url is loaded. */ exposed.loadCss = (url) => { return new Promise((resolve, reject) => { // Load file and attach it to the document let status = createFileStatus(url); try { if (fileExists(url)) { status.loaded = true; status.notes = `${url} already loaded`; resolve(status); return; } let link = document.createElement('link'); link.type = 'text/css'; link.rel = 'stylesheet'; link.onload = () => { status.loaded = true; status.notes = `${url} loaded`; resolve(status); } link.onerror = () => { removeFile(url); status.notes = `*** ${url} failed to load ***`; reject(status); } link.href = url; link.id = getId(url); document.getElementsByTagName("head")[0].appendChild(link); addFile(url); } catch (e) { let message = e.message ? e.message : e; status.notes = `*** ${url} failed to load. Reason: ${message} ***`; reject(status); } }); } /** * FUNCTION: getFilename * USE: Returns the filename of a url * @param url: The url. * @return: The filename of a url. */ exposed.getFilename = (url) => { let filename = ''; if (url && url.length > 0) { filename = url.split('\\').pop().split('/').pop(); // Remove any querystring if (filename.indexOf('?') > -1) { filename = filename.substr(0, filename.indexOf('?')); } } return filename; } /** * FUNCTION: getExtension * USE: Returns the file extension of the filename of a url * @param url: The url. * @return: The file extension of a url. */ exposed.getExtension = (url) => { let filename = exposed.getFilename(url); let ext = filename.split('.').pop(); return (ext === filename) ? '' : ext; } // -- Private data -- let loadFiles = (options) => { return new Promise(async (resolve, reject) => { try { if (!options.files) { throw new Error('There are no files specified to load.'); } let files = verifyArray(options.files); // Order the files putting the critical ones first prioritizeFiles(files); // Load files to the document let results = []; let promises = []; for (let file of files) { file = verifyFile(file); let promise = getCallback(file).call(this, file.url); if (file.wait) { let value = await resolveByCompletion([promise], options.rejectOnError, options.timeout); results.push(value[0]); } else { promises.push(promise); } } // Get the results let completed = await resolveByCompletion(promises, options.rejectOnError, options.timeout); results = [...results, ...completed]; resolve(results); } catch (e) { let message = e.message ? e.message : e.notes ? e.notes : e; reject(new Error(`Unable to load resources. Reason: ${message}`)); } }); } let prioritizeFiles = (files) => { files.sort((x, y) => { let conditionX = x.wait || false; let conditionY = y.wait || false; return conditionY - conditionX; }); return files; } let getCallback = (file) => { let types = { css: exposed.loadCss, js: exposed.loadJs } let callback = types[file.type.toLocaleLowerCase()]; if (!callback) { throw new Error(`Unknown file type/file extension (${file.type}) for the following url: ${file.url}`); } return callback; } let getId = (str) => { return `${str}_added_resource` } let fileExists = (url) => { return loadedResources.has(url) || (document.querySelector(`script[src='${url}']`) != null); } let addFile = (url) => { loadedResources.set(url, true); } let removeFile = (url) => { return loadedResources.delete(url); } let createFileStatus = (url) => { let status = { url: url, loaded: false, notes: '', } return status; } let verifyOptions = (options) => { if (!options) { throw new Error('There are no options specified'); } if (typeof options !== 'object' || Array.isArray(options)) { let files = options; options = {}; options.files = files; } options.rejectOnError = options.rejectOnError || false; options.timeout = options.timeout|| null; return options; } let verifyArray = (arry) => { if (!arry) { arry = []; } if (!Array.isArray(arry)) { arry = [arry]; } return arry; } let verifyFile = (file) => { if (typeof file !== 'object') { let url = file; file = {}; file.url = url; } file.url = file.url || ''; file.wait = file.wait || false; file.type = file.type || exposed.getExtension(file.url); return file } /** * FUNCTION: resolveByCompletion * USE: Returns a promise that will resolve when all of the input's * promises have resolved, in order of completion. * @param promises: An array of promises. * @param rejectOnError: Optional. Boolean that indicates if the operation * should reject if any of the input promises reject or throw an error. * If set to true, the operation is rejected if any of the input * promises reject or throw an error. * If set to false, the promises rejected value is added to the result list. * @param timeout: Optional. Integer that indicates how long to wait * (in milliseconds) for the promise group to complete. * @return: A promise that will contain the input promises results on completion. */ let resolveByCompletion = (promises, rejectOnError = true, timeout = null) => { return new Promise(async (resolve, reject) => { try { let results = [] let promiseMap = new Map(); Array.prototype.forEach.call(promises, (promise, index) => { let promiseResult = { index: index, value: null, error: null }; let mapValue = null; if (promise instanceof Promise) { mapValue = promise .then(value => {promiseResult.value = value; return promiseResult}) .catch(error => {promiseResult.error = error; return promiseResult}) } else { mapValue = promiseResult; promiseResult.value = promise; } promiseMap.set(index, mapValue); }); let start = Date.now(); let isTimedOut = () => { let result = false; if (timeout) { let elapsed = (Date.now() - start); result = elapsed >= timeout; } return result; } while (promiseMap.size > 0) { let promiseResult = await Promise.race(promiseMap.values()); if (!promiseMap.delete(promiseResult.index)) { throw new Error('Error occurred processing values'); } if (promiseResult.error) { if (rejectOnError) { reject(promiseResult.error); return; } results.push(promiseResult.error); } else { results.push(promiseResult.value); } if (isTimedOut()) { throw new Error(`Timeout of ${timeout}ms expired`); } } resolve(results); } catch (e) { reject(e); } }); } let loadedResources = new Map(); (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Resources)); // http://programmingnotes.org/ |
6. More Examples
Below are more examples demonstrating the use of ‘Resources.load‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jul 7, 2020 // Taken From: http://programmingnotes.org/ // File: resourcesDemo.html // Description: Demonstrates the use of Resources.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Resources.js Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } .output { text-align: left; } pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } .string { color: green; } .number { color: darkorange; } .boolean { color: blue; } .null { color: magenta; } .key { color: red; } </style> <!-- // Include module --> <script type="text/javascript" src="./Resources.js"></script> </head> <body> <div class="main"> My Programming Notes Resources.js Demo <pre><code><div class="output"></div></code></pre> </div> <script> (async () => { // Load files in parallel with undefined loading order let loadResult = await Resources.load({ files: [ { url: 'https://code.jquery.com/jquery-3.5.1.js', type: 'js' , wait: false }, { url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js', type: 'js' , wait: false }, ], logResults: true, rejectOnError: false, timeout: null }); console.log(loadResult); print('Resources.load Status Result with undefined loading order', loadResult, 4); })(); </script> <script> (async () => { return; // Load files in parallel with defined loading order let loadResult = await Resources.load({ files: [ { url: 'https://code.jquery.com/jquery-3.5.1.js', type: 'js' , wait: true }, { url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js', type: 'js' , wait: false }, ], logResults: true, rejectOnError: false, timeout: null }); console.log(loadResult); print('Resources.load Status Result with defined loading order', loadResult, 4); })(); </script> <script> (async () => { return; // Load fullname.js to get the 'fullname' functions await Resources.load({ files: ['./fullName.js'], logResults: true, }); let fullName = getFullName('Kenneth', 'P'); displayFullName(fullName); logFullName(fullName); })(); </script> <script> (async () => { return; // Load files one by one console.log(await Resources.load('/file1.js')); console.log(await Resources.load('/file2.js')); console.log(await Resources.load('/file3.css')); console.log(await Resources.load('/file4.css')); console.log(await Resources.load('https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js')); })(); </script> <script> function print(desc, obj, indent = 0) { let text = (desc.length > 0 ? '<br />' : '') + desc + (desc.length > 0 ? '<br />' : ''); let objText = obj || ''; if (obj && typeof obj != 'string') { objText = syntaxHighlight(JSON.stringify(obj, null, indent)); } text += objText; let pageText = document.querySelector('.output').innerHTML; pageText += (pageText.length > 0 ? '<br />' : '') + text; document.querySelector('.output').innerHTML = pageText; } function syntaxHighlight(json) { if (typeof json != 'string') { json = JSON.stringify(json, undefined, 2); } json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; }); } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript || Sort.by.js – Simple Array Sorting With Multiple Sorting Conditions Using Vanilla JavaScript

The following is a module that handles complex array sorting. Much like the SQL/LINQ ‘Order By’ operation, this module allows sorting capabilities with multiple sorting conditions for arrays of any type.
Contents
1. Usage
2. Sorting Condition
3. Comparison
4. Null Values
5. Filtering & Selecting Items
6. Sort.by.js Namespace
7. Notes
8. More Examples
Syntax is very straightforward. The following demonstrates sorting a simple array:
1 2 3 4 5 6 7 8 9 |
// Sort array with no sorting conditions. // By default, the array will be sorted in ascending order console.log( Sort.by( [5,4,3,2,1] ) ); // expected output: /* [1,2,3,4,5] */ |
To sort a simple array in descending order, it can be done like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Sort array with one sorting condition. // This array is sorted in descending order console.log( Sort.by( [10,11,12,13,14,15], [ { value:(x) => x, desc: true } ]) ); // expected output: /* [15,14,13,12,11,10] */ |
In the example above, the second parameter of the ‘Sort.by‘ function defines the sorting condition by which the array should be sorted.
2. Sorting Condition
A sorting condition defines how the array should be sorted. It is made up of 3 properties.
1 2 3 4 5 6 7 8 |
[ { value: (x) => x.foo, // Indicates the value to be sorted for this condition desc: true / false, // Optional: Indicates the sorting direction for this condition. true = descending, false = ascending. Default sorting direction is ascending compare: (x, y) => x.localeCompare(y) // Optional: Overrides the default compare function for this condition }, // {...} // Additional sorting conditions ] |
The ‘value‘ property is a function which defines how the value for the sorting condition should be determined. The ‘desc‘ property is a boolean, which defines the sorting direction of the sorting condition. The ‘compare‘ property is a function, which overrides the default compare function for the sorting condition.
More than one sorting condition can be used. The array is sorted in the order in which the conditions were specified (FIFO).
The following example sorts an object array with multiple sorting conditions. In this example, the array is first sorted by the full name length in descending order, followed by the id field sorted in ascending order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// Sort object array with more than one sorting condition. class Person { constructor(id, firstName, lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } getFullName() { return `${this.firstName} ${this.lastName}` } }; let people = [ new Person(31, 'Kenneth', 'P'), new Person(28, 'Jennifer', 'N'), new Person(5, 'Lynn', 'P'), new Person(22, 'Kenneth', 'P'), new Person(19, 'Jennifer', 'N'), ]; // First, sorts by the full name length in descending order // followed by the id field sorted in ascending order console.log( Sort.by(people, [ { value: (x) => x.getFullName().length, desc: true }, { value: (x) => x.id }, ]) ); // expected output: /* [ { "id": 19, "firstName": "Jennifer", "lastName": "N" }, { "id": 28, "firstName": "Jennifer", "lastName": "N" }, { "id": 22, "firstName": "Kenneth", "lastName": "P" }, { "id": 31, "firstName": "Kenneth", "lastName": "P" }, { "id": 5, "firstName": "Lynn", "lastName": "P" } ] */ |
3. Comparison
Overriding the default comparison function is optional. But, when it is used, it takes precedence over the ‘desc’ property. That is because when supplying the custom comparison function, that ultimately defines the sorting direction and behavior.
The following example demonstrates the use of the comparison function to sort a mixed array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// Sort a multi dimensional array with multiple conditions. let mixedArray = [ [0, 'Aluminium', 0, 'Francis'], [1, 'Argon', 1, 'Ada'], [2, 'Brom', 2, 'John'], [3, 'Cadmium', 9, 'Marie'], [4, 'Fluor', 12, 'Marie'], [5, 'Gold', 1, 'Ada'], [6, 'Kupfer', 4, 'Ines'], [7, 'Krypton', 4, 'Joe'], [8, 'Sauerstoff', 0, 'Marie'], [9, 'Zink', 5, 'Max'] ]; // This array is sorted in ascending order at index 3, // ascending order at index 2, // and descending order at index 1 console.log( Sort.by(mixedArray, [ { value: (x) => x[3], compare: (x, y) => x.localeCompare(y) }, { value: (x) => x[2], desc: false }, { value: (x) => x[1], compare: (x, y) => y.localeCompare(x) }, ]) ); // expected output: /* [ [ 5, "Gold", 1, "Ada" ], [ 1, "Argon", 1, "Ada" ], [ 0, "Aluminium", 0, "Francis" ], [ 6, "Kupfer", 4, "Ines" ], [ 7, "Krypton", 4, "Joe" ], [ 2, "Brom", 2, "John" ], [ 8, "Sauerstoff", 0, "Marie" ], [ 3, "Cadmium", 9, "Marie" ], [ 4, "Fluor", 12, "Marie" ], [ 9, "Zink", 5, "Max" ] ] */ |
4. Null Array Values
The default comparison function automatically handles null values by placing them at the end of the array, regardless of the sorting direction. This behavior can be overridden by supplying your own user defined comparison function.
The following example demonstrates sorting an array with null values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Sort an array with one condition. // This demonstrates null value behavior let arrayWithNullValues = ['211', '36', '1.68', '0.254']; arrayWithNullValues[9] = 1991; arrayWithNullValues[10] = 1987; // This array is sorted in ascending order, with // null values placed at the end of the array console.log( Sort.by(arrayWithNullValues, [ { value: (x) => x ? parseFloat(x) : x } ]) ); // expected output: /* ["0.254","1.68","36","211",1987,1991,null,null,null,null,null] */ |
In the example below, the default comparison function is overridden, but null values are un-handled. This leaves it up to the user to define it’s behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Sort an array with one condition. // This demonstrates null value behavior with a custom // comparison function let arrayWithNullValues = ['211', '36', '1.68', '0.254']; arrayWithNullValues[9] = 1991; arrayWithNullValues[10] = 1987; // This array is sorted in ascending order, but // null values are un-handled console.log( Sort.by(arrayWithNullValues, [ // The sorting condition below overrides the // compare function but does not handle null values { value: (x) => x ? parseFloat(x) : x, compare:(x, y) => x - y } ]) ); // expected output: /* ["0.254","1.68","36","211",null,null,null,null,null,1987,1991] */ |
5. Filtering & Selecting Items
Filtering and selecting result items can be achieved by using either the Array.filter() and Array.map() technique, or by using Array.reduce().
The example below demonstrates this. It first sorts an object array with multiple conditions. The array is sorted by score DESC, time ASC, and age ASC. It then filters the result array by a certain score, and returns a new list which contains only the score, time, and age properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// Sort an array with miltiple conditions. // This demonstrates then filtering & selecting values let users = [ {USER:"bob", SCORE:2000, TIME:32, AGE:16, COUNTRY:"US"}, {USER:"jane", SCORE:4000, TIME:35, AGE:16, COUNTRY:"DE"}, {USER:"tim", SCORE:1000, TIME:30, AGE:17, COUNTRY:"UK"}, {USER:"mary", SCORE:1500, TIME:31, AGE:19, COUNTRY:"PL"}, {USER:"joe", SCORE:2500, TIME:33, AGE:18, COUNTRY:"US"}, {USER:"sally", SCORE:2000, TIME:30, AGE:16, COUNTRY:"CA"}, {USER:"yuri", SCORE:3000, TIME:34, AGE:19, COUNTRY:"RU"}, {USER:"anita", SCORE:2500, TIME:32, AGE:17, COUNTRY:"LV"}, {USER:"mark", SCORE:2000, TIME:30, AGE:18, COUNTRY:"DE"}, {USER:"amy", SCORE:1500, TIME:29, AGE:19, COUNTRY:"UK"} ]; // Sort an object with multiple conditions. // The array is sorted by score DESC, time ASC, age ASC. console.log( Sort.by(users, [ { value: (x) => x.SCORE, desc: true }, { value: (x) => x.TIME }, { value: (x) => x.AGE }, ]).reduce((acc, x) => { x.SCORE >= 2000 && acc.push( {SCORE: x.SCORE, TIME: x.TIME, AGE: x.AGE} ); return acc }, []) // You could also achive the same result as above using the filter & map method //.filter( (x) => x.SCORE >= 2000 ).map( (x) => ( {SCORE: x.SCORE, TIME: x.TIME, AGE: x.AGE} ) ) ); // expected output: /* [ { "SCORE": 4000, "TIME": 35, "AGE": 16 }, { "SCORE": 3000, "TIME": 34, "AGE": 19 }, { "SCORE": 2500, "TIME": 32, "AGE": 17 }, { "SCORE": 2500, "TIME": 33, "AGE": 18 }, { "SCORE": 2000, "TIME": 30, "AGE": 16 }, { "SCORE": 2000, "TIME": 30, "AGE": 18 }, { "SCORE": 2000, "TIME": 32, "AGE": 16 } ] */ |
It should be noted that using the Array.filter() and Array.map() technique iterates the array twice. This same effect can be achieved by using Array.reduce(), which only iterates the array once, thus being more efficient.
6. Sort.by.js Namespace
The following is the Sort.by.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jun 27, 2020 // Taken From: http://programmingnotes.org/ // File: Sort.by.js // Description: Namespace which handles complex array sorting. This allows // SQL/LINQ like sorting capabilities with multiple sorting conditions // for arrays of any type. // Example: // // Sort a simple array in descending order // let sorted = Sort.by( [28,91,10,87,19,32], // [ // { value:(x) => x, desc: true } // // Custom compare example: // // { value:(x) => String(x), compare: (x, y) => y.localeCompare(x) } // ]); // ============================================================================ /** * NAMESPACE: Sort * USE: Handles complex array sorting with multiple sorting conditions. */ var Sort = Sort || {}; // Declare the namespace members (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: by * USE: Sorts an array in ascending or descending order depending * on the sorting conditions. This method returns a new, sorted * array. The original array is left untouched * @param arry: The array to be sorted. * @param conditions: The conditions by which the array should be sorted. * Accepts multiple sorting conditions. The array is sorted in the * order in which the conditions were placed (FIFO) * @return: The sorted array according to the sorting conditions. */ exposed.by = (arry, conditions = null) => { // Make sure the array is valid verifyArray(arry); // Make sure the sorting conditions are in the expected format conditions = verifyConditions(conditions); // Create a seperate array that holds only the computed values we want to sort. // This is done to reduce overhead in the .sort function, so item // values are only computed once, as opposed to possibly multiple times // for each comparison. let sortable = mapArray(arry, conditions); // Get sorting info from the sorting conditions let sortingInfo = getSortingInfo(conditions); // Sort the sortable array according to its pre-computed value sortable.sort((x, y) => { // Evaluate each condition based on the order they were supplied in let evaluation = evaluate(sortingInfo, x, y); return evaluation; }); // Using the sorted sortable array, reorder the original // array by its index and save it into a result list let result = sortable.map((item) => { return arry[item.index]; }); return result; } /** * FUNCTION: defaultCompare * USE: Default comparer that defines the sort order. * @param x: The first element for comparison. * @param y: The second element for comparison. * @param desc: Indicates if comparison should be ascending or descending. * @return: The result value of the comparison. */ exposed.defaultCompare = (x, y, desc) => { // Default: both values left unchanged let evaluation = 0; // Check to see if both values are the same if (x === y) { evaluation = 0; } // Null values sort after anything else else if (isNull(x) || isNull(y)) { if (isNull(x)) { evaluation = 1; } else { evaluation = -1; } } else { // x goes before y if (x > y) { evaluation = 1 } // x goes after y else if (x < y) { evaluation = -1; } // Reverse order if descending if (desc) { evaluation *= -1; } } return evaluation; } // -- Private data -- /** * FUNCTION: evaluate * USE: Evaluates each sorting condition and determines the * order the item should be in. * @param sortingInfo: The sorting info from the sorting conditions. * @param x: The first element for comparison. * @param y: The second element for comparison. * @return: The sorting order of this item. */ let evaluate = (sortingInfo, x, y) => { let retValue = 0; // Evaluate each condition based on the order they were supplied in for (let index = 0; index < sortingInfo.length; ++index) { let desc = sortingInfo[index].desc; let compare = sortingInfo[index].compare; let valueX = x.values[index].value; let valueY = y.values[index].value; // Call the comparison function to evaluate the condition. // Exit if we find a valid ordering let evaluation = compare.call(this, valueX, valueY, desc); if (evaluation) { retValue = evaluation; break; } } return retValue; } /** * FUNCTION: mapArray * USE: Creates a seperate array that holds only the computed * values we want to sort. This is done to limit overhead, so * the item values are only computed once in the .sort * function, as opposed to possibly multiple times for each * comparison * @param arry: The original array to be sorted * @param conditions: The sorting conditions * @return: The sortable array holding only the computed values */ let mapArray = (arry, conditions) => { let mapped = []; for (let index = 0; index < arry.length; ++index) { const item = arry[index]; // Create a new object that represents the item to sort let sortableItem = createSortableItem(item, index, conditions); mapped.push(sortableItem); } return mapped; } /** * FUNCTION: createSortableItem * USE: Creates the item that holds the computed values to be sorted, * and saves its array order (index) of the original array * @param item: The array item in the original array * @param index: The index the item resides at in the original array * @param conditions: The sorting conditions * @return: The sortable item holding the computed values */ let createSortableItem = (item, index, conditions) => { let sortableItem = {}; sortableItem.index = index; sortableItem.values = []; // Go through each condition and process its value Array.prototype.forEach.call(conditions, (condition, index) => { // Check to see if there is a custom value format specified let formatValue = condition.value; if (isNull(formatValue)) { if (!isNull(condition.prop) && item.hasOwnProperty(condition.prop)) { formatValue = (item) => item[condition.prop]; } else { formatValue = (item) => item; } } if (!isFunction(formatValue)) { throw new TypeError(`Unable to determine the value on sorting condition #${index + 1}. Reason: '${formatValue}' is not a function`); } // Evaluate the field and save its value sortableItem.values.push({ value: formatValue.call(this, item) }); }); return sortableItem; } /** * FUNCTION: getSortingInfo * USE: Gets sorting information about the sorting condition * @param conditions: Sorting conditions * @return: The sorting direction and comparison * function to be used for each sorting condition. */ let getSortingInfo = (conditions) => { let sortingInfo = []; Array.prototype.forEach.call(conditions, (condition, index) => { // Check to see if this condition should be in descending order. // Default is ascending sort let desc = (!isNull(condition.desc)) ? condition.desc : false; // Get the comparison function let compare = condition.compare || exposed.defaultCompare; if (!isFunction(compare)) { throw new TypeError(`Invalid comparison of type '${typeof compare}' found on sorting condition #${index + 1}. Reason: '${compare}' is not a function`); } // Save the sorting condition info sortingInfo.push({ desc: desc, compare: compare }); }); return sortingInfo; } /** * FUNCTION: verifyArray * USE: Make sure the array is valid for sorting * @param arry: The array to be sorted * @return: N/A. */ let verifyArray = (arry) => { if (isNull(arry)) { throw new TypeError('Unable to sort. Reason: Array is not defined'); } else if (!isArrayLike(arry) && !isString(arry)) { throw new TypeError(`Unable to sort value of type: ${typeof arry}. Reason: '${arry}' is not an array.`); } } /** * FUNCTION: verifyConditions * USE: Make sure the sorting conditions are in the expected format * @param conditions: Sorting conditions * @return: The verified sorting condition. */ let verifyConditions = (conditions) => { if (isNull(conditions)) { conditions = {}; } if (!isArrayLike(conditions)) { conditions = [conditions]; } return conditions; } let isFunction = (item) => { return 'function' === typeof item } let isNull = (item) => { return undefined === item || null === item } let isString = (item) => { return 'string' == typeof item; } // see if it looks and smells like an iterable object, and do accept length === 0 let isArrayLike = (item) => { return ( Array.isArray(item) || (!!item && typeof item === "object" && typeof (item.length) === "number" && (item.length === 0 || (item.length > 0 && (item.length - 1) in item) ) ) ); } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Sort)); // http://programmingnotes.org/ |
7. Notes
This module uses the Array.sort() function to handle array sorting. But instead of altering the original array, it returns a newly sorted array. Internally, a separate ‘sortable’ array is created, which contains only the values to be sorted. This is done to reduce overhead in the .sort().compare() function, so item values are only computed once, as opposed to possibly multiple times for each comparison. So instead of sorting the original array, the lightweight ‘sortable’ array is sorted instead. Once array sorting is complete, the newly sorted ‘sortable’ array is used to reorder the original array, and that result is returned.
8. More Examples
Below are more examples demonstrating the Sort.by use for sorting arrays. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jun 27, 2020 // Taken From: http://programmingnotes.org/ // File: sortByDemo.html // Description: Demonstrates the use of Sort.by.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Sort.by.js Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } .output { text-align: left; } pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } .string { color: green; } .number { color: darkorange; } .boolean { color: blue; } .null { color: magenta; } .key { color: red; } </style> <!-- // Include module --> <script type="text/javascript" src="./Sort.by.js"></script> </head> <body> <div class="main"> My Programming Notes Sort.by.js Demo <pre><code><div class="output"></div></code></pre> </div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { let indent = 4; // Sort a simple array. // Ascending order is the default sort direction let simpleArray1 = [5,4,3,2,1]; print('1. Original:', simpleArray1); print('1. Sorted in ascending order:', Sort.by( simpleArray1 ) ); // Sort a simple array with one condition. // To sort in descending order, specify a sorting condition let simpleArray2 = [10,11,12,13,14,15]; print('2. Original:', simpleArray2); print('2. Sorted in descending order:', Sort.by( simpleArray2, [ { value:(x) => x, desc: true } ]) ); // Sort a simple array with multiple conditions. // This option first, orders the even numbers and then // sorts them in ascending order let simpleArray3 = [28,91,10,87,19,32]; print('3. Original:', simpleArray3); print('3. First, orders the even numbers and then sorts them in ascending order:', Sort.by( simpleArray3, [ { value:(x) => (x % 2 == 0) ? 0 : 1 }, { value:(x) => x } ]) ); // Sort an object with one condition. // This array is sorted by artist converted to uppercase in ascending order let albums = [ { artist: 'Coldplay', title: 'Parachutes', released: 2000 }, { artist: 'Alicia Keys', title: 'Songs in A Minor', released: 2001 }, { artist: 'System of a Down', title: 'Toxicity', released: 2001 }, { artist: 'Kanye West', title: 'The College Dropout', released: 2004 }, ]; print('4. Original:', albums, indent); print('4. Sorted by artist in ascending order:', Sort.by(albums, [ { value:(x) => x.artist.toUpperCase() } ]) , indent); // Sort an object with multiple conditions. // The array is first sorted by the full name length in descending order, // followed by the id field sorted in ascending order let people = [ new Person(31, 'Kenneth', 'P'), new Person(28, 'Jennifer', 'N'), new Person(5, 'Lynn', 'P'), new Person(22, 'Kenneth', 'P'), new Person(19, 'Jennifer', 'N'), ]; print('5. Original:', people, indent); print('5. First, sorts by the full name length in descending order, followed by the id field sorted in ascending order:', Sort.by(people, [ { value: (x) => x.getFullName().length, desc: true }, { value: (x) => x.id }, ]) , indent); // Sort an object with multiple conditions. // The array is sorted by name in ascending order // followed by the speciality in descending order let employees = [ { name: 'Mike', speciality: 'JS', age: 22 }, { name: 'Tom', speciality: 'Java', age: 30 }, { name: 'Mike', speciality: 'PHP', age: 40 }, { name: 'Abby', speciality: 'Design', age: 20 }, ]; print('6. Original:', employees, indent); print('6. The array is sorted by name in ascending order followed by the speciality in descending order:', Sort.by(employees, [ { value: (x) => x.name }, { value: (x) => x.speciality, desc: true }, ]) , indent); // Sort a multi dimensional array with multiple conditions. // This array is sorted by accesing its value by numerical index. // The optional comparison function is used to sort in // using 'localeCompare' let mixedArray = [ [0, 'Aluminium', 0, 'Francis'], [1, 'Argon', 1, 'Ada'], [2, 'Brom', 2, 'John'], [3, 'Cadmium', 9, 'Marie'], [4, 'Fluor', 12, 'Marie'], [5, 'Gold', 1, 'Ada'], [6, 'Kupfer', 4, 'Ines'], [7, 'Krypton', 4, 'Joe'], [8, 'Sauerstoff', 0, 'Marie'], [9, 'Zink', 5, 'Max'] ]; print('7. Original:', mixedArray, indent); print('7. This array is sorted in ascending order at index 3, ascending order at index 2, and descending order at index 1:', Sort.by(mixedArray, [ { value: (x) => x[3], compare: (x, y) => x.localeCompare(y) }, { value: (x) => x[2], desc: false }, { value: (x) => x[1], compare: (x, y) => y.localeCompare(x) }, ]) , indent); let users = [ {USER:"bob", SCORE:2000, TIME:32, AGE:16, COUNTRY:"US"}, {USER:"jane", SCORE:4000, TIME:35, AGE:16, COUNTRY:"DE"}, {USER:"tim", SCORE:1000, TIME:30, AGE:17, COUNTRY:"UK"}, {USER:"mary", SCORE:1500, TIME:31, AGE:19, COUNTRY:"PL"}, {USER:"joe", SCORE:2500, TIME:33, AGE:18, COUNTRY:"US"}, {USER:"sally", SCORE:2000, TIME:30, AGE:16, COUNTRY:"CA"}, {USER:"yuri", SCORE:3000, TIME:34, AGE:19, COUNTRY:"RU"}, {USER:"anita", SCORE:2500, TIME:32, AGE:17, COUNTRY:"LV"}, {USER:"mark", SCORE:2000, TIME:30, AGE:18, COUNTRY:"DE"}, {USER:"amy", SCORE:1500, TIME:29, AGE:19, COUNTRY:"UK"} ]; print('8. Original:', users, indent); // Sort an object with multiple conditions. // The array is sorted by score DESC, time ASC, age ASC. print('8. This array is sorted by score DESC, time ASC, age ASC:', Sort.by(users, [ { value: (x) => x.SCORE, desc: true }, { value: (x) => x.TIME }, { value: (x) => x.AGE }, ]) , indent); // Sort an array with one condition // The sorting method automatically sorts null values to the end of the array let arrayWithNullValues = ['211', '36', '1.68', '0.254']; arrayWithNullValues[9] = 1991; arrayWithNullValues[10] = 1987; print('9. Original:', arrayWithNullValues); print('9. This array is sorted in ascending order, with null values placed at the end of the array:', Sort.by(arrayWithNullValues, [ { value: (x) => x ? parseFloat(x) : x } ]) ); }); function print(desc, obj, indent = 0) { let text = (desc.length > 0 ? '<br />' : '') + desc + (desc.length > 0 ? '<br />' : ''); let objText = obj || ''; if (obj && typeof obj != 'string') { objText = syntaxHighlight(JSON.stringify(obj, null, indent)); } text += objText; let output = document.querySelector('.output'); let pageText = output.innerHTML; pageText += (pageText.length > 0 ? '<br />' : '') + text; output.innerHTML = pageText; } class Person { constructor(id, firstName, lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } getFullName() { return `${this.firstName} ${this.lastName}` } }; function syntaxHighlight(json) { if (typeof json != 'string') { json = JSON.stringify(json, undefined, 2); } json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; }); } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript/CSS/HTML || sliderRadioButton.js – Simple Animated Slider Radio Button Using Vanilla JavaScript

Radio buttons let a user select only one of a limited number of choices. Radio buttons are normally presented in radio groups (a collection of radio buttons describing a set of related options). Only one radio button in a group can be selected at the same time.
Using JavaScript, the following is sample code which demonstrates how to display a simple animated slider radio button group to the page.
This animated slider radio button comes with a few basic features. When a selection is chosen, a sliding animation appears. The speed of the animation can be modified via CSS transitions. The look and orientation can also be modified via CSS, with the button group direction being either vertical or horizontal.
Contents
1. Basic Usage
2. Slider HTML - Default
3. Slider HTML - Column
4. sliderRadioButton.js & CSS Source
5. More Examples
1. Basic Usage
Syntax is very straightforward. The following demonstrates the JavaScript used to setup the radio buttons decorated with the ‘slider’ CSS class.
1 2 3 4 5 6 7 8 |
// Initialize slider. <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Initialize slider Slider.init(); }); </script> |
2. Slider HTML – Default
The following is an example of the HTML used to display the slider. By default, the options are displayed inline.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- // HTML --> <!-- Radio Group --> <div class="slider-radio-group"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoSole" value="Solé" name="rdoGroupName"> <label class="slider-option-label" for="rdoSole">Solé</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoLynn" value="Lynn" name="rdoGroupName"> <label class="slider-option-label" for="rdoLynn">Lynn</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoJennifer" value="Jennifer" name="rdoGroupName"> <label class="slider-option-label" for="rdoJennifer">Jennifer</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoKenneth" value="Kenneth" name="rdoGroupName"> <label class="slider-option-label" for="rdoKenneth">Kenneth</label> </div> |
3. Slider HTML – Column
The following is an example of the HTML used to display the slider as a column.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- // HTML --> <!-- Radio Group --> <div class="slider-radio-group column"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoRed" value="Red" name="rdoGroupColor"> <label class="slider-option-label" for="rdoRed">Red</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoBlue" value="Blue" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlue">Blue</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoBlack" value="Black" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlack">Black</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoYellow" value="Yellow" name="rdoGroupColor"> <label class="slider-option-label" for="rdoYellow">Yellow</label> <!-- Option 5 --> <input type="radio" class="slider-radio-button" id="rdoPurple" value="Purple" name="rdoGroupColor"> <label class="slider-option-label" for="rdoPurple">Purple</label> </div> |
4. sliderRadioButton.js & CSS Source
The following is the sliderRadioButton.js Namespace & CSS Source. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jun 12, 2020 // Taken From: http://programmingnotes.org/ // File: sliderRadioButton.js // Description: Javascript that handles the animation for a slider radio // button. // ============================================================================ /** * NAMESPACE: Slider * USE: Handles the sliding animation for the Slider Radio Button. */ var Slider = Slider || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; // Set class names and other shared data const settings = { classNameRadioGroup: '.slider-radio-group', classNameRadioButton: '.slider-radio-button', classNameRadioButtonLabel: 'slider-option-label', classNameSelectedItem: '.slider-selected-item', classNameRadioColumn: '.column', classNameLabelContent: '.slider-option-label-content', clean: (str) => { return str ? str.trim().replace('.', '') : ''; } }; exposed.settings = settings; /** * FUNCTION: init * USE: Initializes the slider radio groups and button clicks. * @param element: JavaScript element to search for radio groups * @return: N/A. */ exposed.init = (element = document) => { exposed.prepareRadioGroups(element); addClickEvents(element); } /** * FUNCTION: animate * USE: Handles the sliding animation of a radio button. * @param radioButton: JavaScript element (radio button) that the * slider animation should be applied to. * @return: N/A. */ exposed.animate = (radioButton) => { if (!radioButton) { throw new Error('Radio button does not exist!'); } // Get the radio group for this button and make sure it exists let radioGroup = exposed.getGroupForButton(radioButton); if (!radioGroup) { throw new Error('Unable to find radio group!'); } // Get the radio button label and make sure it exists let radioButtonLabel = exposed.getLabelForButton(radioButton, radioGroup); if (!radioButtonLabel) { throw new Error('Unable to find radio option label!'); } // Get the "selected item" slider div that marks the // label as selected and make sure it exists let selectedItem = exposed.getSelectedItem(radioGroup); if (!selectedItem) { throw new Error('Unable to find selected item slider animation element!'); } // Mark the option label as selected markAsSelected(radioButtonLabel, selectedItem); } /** * FUNCTION: prepareRadioGroups * USE: Makes sure radio groups contain the "selected item" slider div * and makes sure radio column groups are set up properly * @param element: JavaScript element to search for radio groups * @return: N/A. */ exposed.prepareRadioGroups = (element = document) => { // Get radio groups and make sure it exists let radioGroups = exposed.getRadioGroups(element); if (!radioGroups) { throw new Error('Unable to find a radio group!'); } // Go through radio groups and make sure they are setup properly for (const radioGroup of radioGroups) { // Make sure the "selected item" slider div exists. Add it in if it does not let selectedItemElement = exposed.getSelectedItem(radioGroup); if (!selectedItemElement) { selectedItemElement = document.createElement('div'); selectedItemElement.classList.add(settings.clean(settings.classNameSelectedItem)); radioGroup.appendChild(selectedItemElement); } // Get radio buttons for this group and make sure it exists let radioButtons = exposed.getRadioButtons(radioGroup); if (!radioButtons) { continue; } let classRadioColumn = settings.clean(settings.classNameRadioColumn); for (const radioButton of radioButtons) { // Check to see if this radio group is marked as a "column". If it is, // also add the associated class to its linked radio button elements if (radioGroup.classList.contains(classRadioColumn) && !radioButton.classList.contains(classRadioColumn)) { radioButton.classList.add(classRadioColumn); } let radioButtonLabel = exposed.getLabelForButton(radioButton, radioGroup); if (!radioButtonLabel) { continue; } let container = document.createElement('div'); container.classList.add(settings.clean(settings.classNameLabelContent)); while (radioButtonLabel.hasChildNodes()) { container.appendChild(radioButtonLabel.firstChild); } radioButtonLabel.appendChild(container); } } } /** * FUNCTION: getRadioGroups * USE: Returns the radio groups that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for radio groups * @return: Radio group descendants found in the given element. */ exposed.getRadioGroups = (element = document) => { return element.querySelectorAll(settings.classNameRadioGroup); } /** * FUNCTION: getRadioButtons * USE: Returns the radio buttons that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for radio buttons * @return: Radio button descendants found in the given element. */ exposed.getRadioButtons = (element = document) => { return element.querySelectorAll(settings.classNameRadioButton); } /** * FUNCTION: getGroupForButton * USE: Returns the radio group that is a parent of * the object on which this method was called. * @param radioButton: JavaScript element representing the radio button * @return: Radio button label descendant found in the given element. */ exposed.getGroupForButton = (radioButton) => { return radioButton.closest(settings.classNameRadioGroup); } /** * FUNCTION: getLabelForButton * USE: Returns the radio button label that is a descendant of * the object on which this method was called. * @param radioButton: JavaScript element representing the radio button * @param element: JavaScript parent element of the radio button * @return: Radio button label descendant found in the given element. */ exposed.getLabelForButton = (radioButton, element = document) => { return element.querySelector('label[for="' + radioButton.id + '"]'); } /** * FUNCTION: getSelectedItem * USE: Returns the "selected item" div for the object on which this * method was called. * @param element: JavaScript element to search for the "selected item" * @return: The "selected item" div found in the given element. */ exposed.getSelectedItem = (element = document) => { return element.querySelector(settings.classNameSelectedItem); } /** * FUNCTION: getSelectedRadioButton * USE: Returns the selected (checked) radio button that is * descendant of the object on which this method was called. * @param element: JavaScript element to search for radio buttons * @return: Selected radio button descendant found in the given element. */ exposed.getSelectedRadioButton = (element = document) => { let selectedButton = null; let buttons = exposed.getRadioButtons(element); for (const button of buttons) { if (button.checked) { selectedButton = button; break; } } return selectedButton; } // -- Private data -- /** * FUNCTION: addClickEvents * USE: Adds slider animation button click events for the radio buttons. * @param element: JavaScript element to search for radio groups * @return: N/A. */ let addClickEvents = (element = document) => { // Go through each radio button to initialize any that are selected/set button clicks exposed.getRadioButtons(element).forEach((radioButton, index) => { // If the radio button is checked, animate the selection if (radioButton.checked) { exposed.animate(radioButton); } // Add click events to update the selected radio button radioButton.addEventListener('click', (eventClick) => { exposed.animate(radioButton); }); }); } /** * FUNCTION: markAsSelected * USE: Marks the radio button label as "selected" and performs * the slider animation by moving the "selected item" div to * the location of the radio button label. * @param radioButtonLabel: JavaScript element (label) linked to the selected radio button * @param selectedItem: JavaScript element (div) for "selected item" * @return: N/A. */ let markAsSelected = (radioButtonLabel, selectedItem) => { let radioButtonLabelCoords = getElementCoords(radioButtonLabel, true); moveElement(selectedItem, radioButtonLabelCoords); selectedItem.style.display = 'block'; } /** * FUNCTION: moveElement * USE: Moves an element to a specific location and resizes it * to a specific size. * @param element: JavaScript element to move * @param coords: Coordinates to move & resize the element to * @return: N/A. */ let moveElement = (element, coords) => { element.style.left = coords.x + 'px'; element.style.top = coords.y + 'px'; element.style.width = coords.width + 'px'; element.style.height = coords.height + 'px'; } /** * FUNCTION: getElementCoords * USE: Gets the coordinates & size of an element. * @param element: JavaScript element in question * @param roundUp: Determines if element coordinates should * be rounded up or not * @return: The coordinates of the element in question. */ let getElementCoords = (element, roundUp = true) => { const boundingRect = element.getBoundingClientRect(); let coords = { x: element.offsetLeft, y: element.offsetTop, width: boundingRect.width, height: boundingRect.height } if (roundUp) { for (const prop in coords) { let item = coords[prop]; if (isNumber(item)) { coords[prop] = Math.ceil(item); } } } return coords; } let isNumber = (n) => { return !isNaN(parseFloat(n)) && !isNaN(n - 0) } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Slider)); // http://programmingnotes.org/ |
The following is sliderRadioButton.css.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/* // ============================================================================ // Author: Kenneth Perkins // Date: Jun 12, 2020 // Taken From: http://programmingnotes.org/ // File: sliderRadioButton.css // Description: CSS that handles the animation for a slider radio // button. // ============================================================================ */ @import url('https://fonts.googleapis.com/css?family=Roboto'); .slider-radio-group { border: solid 1px #bdbdbd; margin: 5px; border-radius: 10px; overflow: hidden; width: 420px; position: relative; font-family: roboto; display: flex; justify-content: center; align-items: center; } .slider-radio-group.column { width: 100px; flex-direction: column; } .slider-radio-button { visibility: hidden; display: none; } .slider-radio-button.column { } input[type=radio][class*="slider-radio-button" i]:checked + label { background-color: rgba(117, 190, 218, .3); } input[type=radio][class*="slider-radio-button" i]:not(:first-child) + label { border-left: solid 1px lightgrey; } input[type=radio][class*="slider-radio-button column" i]:not(:first-child) + label { border-top: solid 1px lightgrey; } .slider-option-label { display: inline-block; cursor: pointer; padding: 5px; width: 110px; text-align:center; background-color: #f3f4f4; overflow: hidden; box-sizing: border-box; } .slider-option-label:hover { background-color: #eef6fa; } .slider-option-label-content { position: relative; z-index: 1; } .slider-selected-item { cursor: pointer; position: absolute; transition: all 400ms ease-in-out; background-color: rgba(117, 190, 218, .25); display:none; } /* // http://programmingnotes.org/ */ |
5. More Examples
Below are more examples demonstrating the use of ‘sliderRadioButton.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jun 12, 2020 // Taken From: http://programmingnotes.org/ // File: sliderRadioButtonDemo.html // Description: Demonstrates how to display a simple animated // slider radio button to the page. // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Simple Animated Slider Radio Button Demo</title> <style> .button { padding: 8px; background-color: #d2d2d2; height:100%; text-align:center; text-decoration:none; color:black; display: flex; justify-content: center; align-items: center; flex-direction: column; border-radius: 15px; cursor: pointer; } .button.medium { height:18px; width:80px; } .section-header { margin-top: 20px; text-align:center; font-size: 15px; font-weight: bold; font-style: italic; font-family: Tahoma,helvetica,arial,sans-serif; } .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } .emoji { font-size:13px } </style> <!-- // Include module --> <link type="text/css" rel="stylesheet" href="./sliderRadioButton.css"> <script type="text/javascript" src="./sliderRadioButton.js"></script> </head> <body> <div class="main"> <div class="inline" style="margin: 5px;"> <div> <div class="section-header"> Rate Our Service </div> <!-- Radio Group 1 --> <div class="slider-radio-group"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoOneStar" value="1" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoOneStar"><span class="emoji">⭐</span></label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoTwoStar" value="2" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoTwoStar"><span class="emoji">⭐⭐</span></label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoThreeStar" value="3" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoThreeStar"><span class="emoji">⭐⭐⭐</span></label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoFourStar" value="4" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoFourStar"><span class="emoji">⭐⭐⭐⭐</span></label> </div> </div> <div> <div class="section-header"> Favorite Name </div> <!-- Radio Group 2 --> <div class="slider-radio-group"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoSole" value="Solé" name="rdoGroupName"> <label class="slider-option-label" for="rdoSole">Solé</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoLynn" value="Lynn" name="rdoGroupName"> <label class="slider-option-label" for="rdoLynn">Lynn</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoJennifer" value="Jennifer" name="rdoGroupName"> <label class="slider-option-label" for="rdoJennifer">Jennifer</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoKenneth" value="Kenneth" name="rdoGroupName"> <label class="slider-option-label" for="rdoKenneth">Kenneth</label> </div> </div> <div class="inline"> <div class="section-header"> Favorite Color </div> <!-- Radio Group 3 --> <div class="slider-radio-group column"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoRed" value="Red" name="rdoGroupColor"> <label class="slider-option-label" for="rdoRed">Red</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoBlue" value="Blue" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlue">Blue</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoBlack" value="Black" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlack">Black</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoYellow" value="Yellow" name="rdoGroupColor"> <label class="slider-option-label" for="rdoYellow">Yellow</label> <!-- Option 5 --> <input type="radio" class="slider-radio-button" id="rdoPurple" value="Purple" name="rdoGroupColor"> <label class="slider-option-label" for="rdoPurple">Purple</label> </div> </div> <div class="button section-header" id="btnSelect"> Select </div> </div> </div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Make sure the radio group is setup with the selected option slider div and other options // And add button clicks to animate the radio button slider Slider.init(); // Select button click document.querySelector('#btnSelect').addEventListener('click', function(eventClick) { let radioButtons = Slider.getRadioButtons(); let optionSelected = false; if (radioButtons) { for (const radioButton of radioButtons) { if (radioButton.checked) { alert(radioButton.id + ' is checked. Its value is: ' + radioButton.value); optionSelected = true; } } } if (!optionSelected) { alert('Please select an option!'); } }); }); </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript/CSS/HTML || Collapsible.js – Simple Collapsible Accordion Panel Using Vanilla JavaScript

Accordion panels are a vertical stack of items. Each item can be “expanded” or “collapsed” to reveal the content associated with that item. There can be zero expanded items, or more than one item expanded at a time, depending on the configuration.
Using JavaScript, the following is sample code which demonstrates how to display a simple collapsible accordion panel to the page.
This panel comes with a few basic features. Using data attributes, you can adjust the slide up and slide down speed, as well as the initial collapsed status. You can also specify either the entire row as selectable, or just the button.
Contents
1. Basic Usage
2. Collapsible HTML
3. Collapsible.js & CSS Source
4. More Examples
1. Basic Usage
Syntax is very straightforward. The following demonstrates the JavaScript used to setup the elements decorated with the ‘collapsible‘ CSS class.
1 2 3 4 5 6 7 8 |
// Initialize collapsible. <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Initialize collapsible Collapsible.init(); }); </script> |
2. Collapsible HTML
The following is an example of the HTML used to display the collapsible.
To make the entire row clickable, only add the ‘collapsible‘ class to the ‘collapsible-header‘ element. To make just the button clickable, only add the ‘collapsible‘ class to the ‘collapsible-button‘ element
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- // HTML --> <!-- Collapsible 1 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > Sample Header! </div> <div class="collapsible-button"></div> </header> <section class="collapsible-body" data-slideup="800" data-slidedown="100" data-expanded="true"> <!-- Specify the slide duration (in milliseconds), and if the row is initially expanded --> <p> You can adjust the slide up and slide down speed, as well as the initial expanded status with data attributes. </p> <p> You can specify either the entire row as selectable, or just the button, depending on where the 'collapsible' class declaration is placed. </p> </section> <!-- ... Additional Collapsibles --> |
3. Collapsible.js & CSS Source
The following is the Collapsible.js Namespace & CSS Source. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jun 8, 2020 // Taken From: http://programmingnotes.org/ // File: Collapsible.js // Description: Demonstrates how to display a simple collapsible // accordion panel to the page. // ============================================================================ /** * NAMESPACE: Collapsible * USE: Collapsible accordion panel. */ var Collapsible = Collapsible || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; // Set class names and other shared data const settings = { classNameExpanded: '.expanded', classNameHeader: '.collapsible-header', classNameHeaderText: '.collapsible-header-text', classNameBody: '.collapsible-body', classNameButton: '.collapsible-button', classNameHideContent: '.hide', classNameCollapsible: '.collapsible', dataNameSlideUp: 'data-slideup', dataNameSlideDown: 'data-slidedown', dataNameExpanded: 'data-expanded', dataNameCurrentlyExpanded: 'data-isCurrentlyExpanded', cleanClassName: function(str) { return str ? str.trim().replace('.', '') : ''; }, }; exposed.settings = settings; /** * FUNCTION: init * USE: Initializes the collapsible accordion panel button clicks. * @param element: JavaScript element to search for collapsible panels * @return: N/A. */ exposed.init = (element = document) => { addEvents(element); } /** * FUNCTION: animate * USE: Handles opening/closing of a collapsible accordion panel. * @param collapsible: JavaScript element that raised the event that * is decorated with the 'collapsible' class tag. * @return: N/A. */ exposed.animate = (collapsible) => { if (!collapsible) { return; } // Find the collapsible header row // and make sure its found let header = collapsible.closest(settings.classNameHeader); if (!header) { throw new Error('Unable to find header row!'); } // Find the associated collapsible body text // and make sure its found let body = header.nextElementSibling; if (!body) { throw new Error('Unable to find content body!'); } // Determine if the content should be expanded or not let status = getPanelStatus(body); // Get the slide up/down speeds let slideUpDuration = status.slideUpDuration; let slideDownDuration = status.slideDownDuration; // Get the current collapsible status let isCurrentlyExpanded = status.isCurrentlyExpanded; // Find the action button so we can change its icon let button = header.querySelector(settings.classNameButton); // Update contents depending on if the row is expanded or not if (toBoolean(isCurrentlyExpanded)) { // Mark the header row as 'active' addClass(header, settings.classNameExpanded); // Change the button icon to '-' addClass(button, settings.classNameExpanded); // Slide down the body slideDown(body, slideDownDuration); } else { // Remove the header row as 'active' removeClass(header, settings.classNameExpanded); // Change the button icon to '+' removeClass(button, settings.classNameExpanded); // Slide up the body slideUp(body, slideUpDuration); } // Save the expanded status body.setAttribute(settings.dataNameCurrentlyExpanded, isCurrentlyExpanded); } /** * FUNCTION: getHeaders * USE: Returns the collapsible headers that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for collapsible headers * @return: Collapsible headers descendants found in the given element. */ exposed.getHeaders = (element = document) => { return element.querySelectorAll(settings.classNameHeader); } /** * FUNCTION: getBodies * USE: Returns the collapsible bodies that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for collapsible bodies * @return: Collapsible body descendants found in the given element. */ exposed.getBodies = (element = document) => { return element.querySelectorAll(settings.classNameBody); } /** * FUNCTION: getPanels * USE: Returns the collapsible panels (header & bodies) that are * descendants of the object on which this method was called. * @param element: JavaScript element to search for collapsible panels * @return: Collapsible panel descendants found in the given element. */ exposed.getPanels = (element = document) => { let panels = []; // Get collapsible headers and bodies const headers = exposed.getHeaders(element); const bodies = exposed.getBodies(element); // Get the maximum item count const maxItems = Math.max(headers.length, bodies.length); // Go through each header and body and create an // object to group them together and save them to a list for(let x = 0; x < maxItems; ++x) { let header = (x < headers.length) ? headers[x] : null; let body = (x < bodies.length) ? bodies[x] : null; panels.push({ header: header, body: body, }); } return panels; } // -- Private data -- /** * FUNCTION: addEvents * USE: Adds events for the collapsibles. * @param element: JavaScript element to search for collapsibles * @return: N/A. */ let addEvents = (element = document) => { // Initialize each row to its initial // opened/closed state, depending on user values element.querySelectorAll(settings.classNameCollapsible).forEach(function(collapsible, index) { exposed.animate(collapsible); // Add button click collapsible.addEventListener('click', function(eventClick) { eventClick.stopPropagation(); exposed.animate(collapsible) }); }); window.addEventListener('resize', (event) => { let bodies = exposed.getBodies(element); for (let body of bodies) { if (toBoolean(body.getAttribute(settings.dataNameCurrentlyExpanded))) { slideUp(body, 0); slideDown(body, 0); } } }); } let addClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); let modified = false; if (cssClass.length > 0 && !hasClass(element, cssClass)) { element.classList.add(cssClass) modified = true; } return modified; } let removeClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); let modified = false; if (cssClass.length > 0 && hasClass(element, cssClass)) { element.classList.remove(cssClass); modified = true; } return modified; } let hasClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); return element.classList.contains(cssClass); } let slideUp = (body, slideUpDuration) => { // Add the class that removes the content body addClass(body, settings.classNameHideContent); // Set the slide up duration body.style.transitionDuration = (null != slideUpDuration) ? slideUpDuration + 'ms' : null; // Remove the content body custom height so the transition can take effect body.style.height = null; } let slideDown = (body, slideDownDuration) => { // Remove the class that hides the content body removeClass(body, settings.classNameHideContent); // Set the slide down duration body.style.transitionDuration = (null != slideDownDuration) ? slideDownDuration + 'ms' : null; // Get the content body height let bodyHeight = body.scrollHeight; // Get the style for the element so we can get its max height let computedStyle = window.getComputedStyle(body); // Check to see if the current content body is greater than the maximum allowed height. // Scrollbars appear when setting a custom height. // We do this check so scroll bars arent visible if they dont need to be if (computedStyle && computedStyle.maxHeight) { let computedMaxHeight = parseInt(computedStyle.maxHeight, 10); if (bodyHeight > computedMaxHeight) { // Body height is bigger than the max height, remove the custom overflow value // and use its computed overflow style body.style.overflow = null; } else { // Override and set overflow to hidden so scroll bars arent visible // if they dont need to be body.style.overflow = 'hidden'; } } // Set the content body custom height so the transition can take effect body.style.height = bodyHeight + 'px'; } let getPanelStatus = (body) => { // Get the slide up/down speeds let slideUpDuration = body.getAttribute(settings.dataNameSlideUp); let slideDownDuration = body.getAttribute(settings.dataNameSlideDown); // Get the current collapsible status let isCurrentlyExpanded = body.getAttribute(settings.dataNameCurrentlyExpanded); // If the current status hasnt been defined yet, use the user defined value if (null == isCurrentlyExpanded) { isCurrentlyExpanded = body.getAttribute(settings.dataNameExpanded) // Assume the row is closed if the user did not define a value if (null == isCurrentlyExpanded) { isCurrentlyExpanded = false; } // Remove the delay so the row is immediately opened/closed slideUpDuration = 0; slideDownDuration = 0; } else { // The status has been defined in the past. Change its state if (toBoolean(isCurrentlyExpanded)) { isCurrentlyExpanded = false; } else { isCurrentlyExpanded = true; } } return { isCurrentlyExpanded: isCurrentlyExpanded, slideUpDuration: slideUpDuration, slideDownDuration: slideDownDuration, } } /** * FUNCTION: toBoolean * USE: Converts a specified value to an equivalent Boolean value. * @param value: Value to convert * @return: true if value evaluates to true; false otherwise. */ let toBoolean = (value) => { if (typeof value === 'string') { value = value.trim().toLowerCase(); } let ret = false; switch (value) { case true: case "true": case "yes": case "1": case 1: ret = true; break; } return ret; } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Collapsible)); // http://programmingnotes.org/ |
The following is Collapsible.css.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
/* // ============================================================================ // Author: Kenneth Perkins // Date: Jun 8, 2020 // Taken From: http://programmingnotes.org/ // File: Collapsible.css // Description: CSS for the collapsible accordian panel // ============================================================================ */ /* Indicates that an element is collapsible and clickable. To make the entire row clickable, only add this class to the 'collapsible-header' element. To make just the button clickable, only add this class to the 'collapsible-button' element. */ .collapsible { cursor: pointer; } .collapsible-header, .collapsible-body { width: 100%; } .collapsible-header { background-color: #d6d6d6; border-top-left-radius: 5px; border-top-right-radius: 5px; padding: 10px; position: relative; text-align: left; border-top: 0.5px solid #bfbfbf; box-sizing: border-box; } .collapsible-header-text { max-width: 90%; max-height: 20px; overflow: hidden; padding-left: 8px; display: inline-block; font-family: helvetica,arial,sans-serif; white-space: nowrap; text-overflow: ellipsis; } .collapsible-button { position: absolute; right: 0; margin-right: 15px; display: inline-block; font-family: Tahoma,helvetica,arial,sans-serif; border: unset; background-color: inherit; font-size: inherit; } .collapsible-button:after { content: '\002B'; /* + */ } .expanded, .collapsible-header:hover { background-color: #a6a6a6; } .collapsible-button.expanded:after { content: "\2212"; /* - */ } .collapsible-button.expanded { background-color: inherit; } .collapsible-body { border: 1px solid #d8d8d8; border-top: unset; max-height: 380px; padding: 15px; padding-top: 0px; padding-bottom: 0px; text-align: left; overflow: auto; box-sizing: border-box; transition: all 400ms ease-in-out; height: 0; } .collapsible-body.hide { height: 0; padding-top: 0px; padding-bottom: 0px; margin-bottom: 0px; } /* http://programmingnotes.org/ */ |
4. More Examples
Below are more examples demonstrating the use of ‘Collapsible.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jun 8, 2020 // Taken From: http://programmingnotes.org/ // File: SimpleCollapsibleAccordionPanel.html // Description: Demonstrates how to display a simple collapsible // accordion panel to the page. // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Simple Collapsible Accordion Panel Demo</title> <style> .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } </style> <!-- // Include module --> <link type="text/css" rel="stylesheet" href="./Collapsible.css"> <script type="text/javascript" src="./Collapsible.js"></script> </head> <body> <div class="main"> <p>My Programming Notes Collapsible.js Demo</p> <div class="inline" style="margin: 5px; width: 600px;"> <!-- Collapsible 1 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > Sample Header! </div> <div class="collapsible-button"> </div> </header> <section class="collapsible-body" data-slideup="800" data-slidedown="100" data-expanded="true"> <!-- Specify the slide duration (in milliseconds), and if the row is initially expanded --> <p> You can adjust the slide up and slide down speed, as well as the initial expanded status with data attributes. </p> <p> You can specify either the entire row as selectable, or just the button, depending on where the 'collapsible' class declaration is placed. </p> </section> <!-- Collapsible 2 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > A Journey To Another World </div> <div class="collapsible-button"> </div> </header> <section class="collapsible-body"> <div style="text-align: center;"> <p> Cultivated who resolution connection motionless did occasional. Journey promise if it colonel. Can all mirth abode nor hills added. Them men does for body pure. Far end not horses remain sister. Mr parish is to he answer roused piqued afford sussex. It abode words began enjoy years no do no. Tried spoil as heart visit blush or. Boy possible blessing sensible set but margaret interest. Off tears are day blind smile alone had. </p> <p> Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education. Discovery incommode earnestly no he commanded if. Put still any about manor heard. </p> <p> Use securing confined his shutters. Delightful as he it acceptance an solicitude discretion reasonably. Carriage we husbands advanced an perceive greatest. Totally dearest expense on demesne ye he. Curiosity excellent commanded in me. Unpleasing impression themselves to at assistance acceptance my or. On consider laughter civility offended oh. </p> <p> Breakfast agreeable incommode departure it an. By ignorant at on wondered relation. Enough at tastes really so cousin am of. Extensive therefore supported by extremity of contented. Is pursuit compact demesne invited elderly be. View him she roof tell her case has sigh. Moreover is possible he admitted sociable concerns. By in cold no less been sent hard hill. </p> </div> </section> <!-- Collapsible 3 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > Tell Us About Yourself </div> <div class="collapsible-button"> </div> </header> <section class="collapsible-body"> <div style="padding: 10px;"> <input type="radio" id="male" name="gender" value="male" checked> <label for="male">Male</label><br /> <input type="radio" id="female" name="gender" value="female"> <label for="female">Female</label><br /> <input type="radio" id="na" name="gender" value="na"> <label for="na">Prefer Not To Say</label> </div> </section> </div> <div style="margin-top: 5px;"> Click a tab to try! </div> </div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Initialize each row to its initial // opened/closed state, depending on user values Collapsible.init(); }); </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.