MDL-63044 javascript: improve paged content widget
authorRyan Wyllie <ryan@ryanwyllie.com>
Tue, 17 Jul 2018 07:04:37 +0000 (15:04 +0800)
committerRyan Wyllie <ryan@moodle.com>
Thu, 27 Sep 2018 05:10:45 +0000 (13:10 +0800)
21 files changed:
lang/en/moodle.php
lib/amd/build/paged_content.min.js [new file with mode: 0644]
lib/amd/build/paged_content_events.min.js
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_pages.min.js
lib/amd/build/paged_content_paging_bar.min.js
lib/amd/build/paged_content_paging_bar_limit_selector.min.js [new file with mode: 0644]
lib/amd/build/paged_content_paging_dropdown.min.js
lib/amd/src/paged_content.js [new file with mode: 0644]
lib/amd/src/paged_content_events.js
lib/amd/src/paged_content_factory.js
lib/amd/src/paged_content_pages.js
lib/amd/src/paged_content_paging_bar.js
lib/amd/src/paged_content_paging_bar_limit_selector.js [new file with mode: 0644]
lib/amd/src/paged_content_paging_dropdown.js
lib/templates/paged_content.mustache
lib/templates/paged_content_pages.mustache
lib/templates/paged_content_paging_bar.mustache
lib/templates/paged_content_paging_dropdown.mustache
theme/bootstrapbase/templates/core/paged_content_paging_bar.mustache
theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache

index 1680fc1..eb72e18 100644 (file)
@@ -1471,6 +1471,10 @@ $string['outline'] = 'Outline';
 $string['outlinereport'] = 'Outline report';
 $string['page'] = 'Page';
 $string['pagea'] = 'Page {$a}';
+$string['pagedcontentnavigation'] = 'Pagination navigation';
+$string['pagedcontentnavigationitem'] = 'Go to page {$a}';
+$string['pagedcontentnavigationactiveitem'] = 'Current page, page {$a}';
+$string['pagedcontentpagingbaritemsperpage'] = 'Show {$a} items per page';
 $string['pageheaderconfigablock'] = 'Configuring a block in {$a->fullname}';
 $string['pagepath'] = 'Page path';
 $string['pageshouldredirect'] = 'This page should automatically redirect. If nothing is happening please use the continue link below.';
diff --git a/lib/amd/build/paged_content.min.js b/lib/amd/build/paged_content.min.js
new file mode 100644 (file)
index 0000000..860f755
Binary files /dev/null and b/lib/amd/build/paged_content.min.js differ
index 27e1fa8..7cd9b32 100644 (file)
Binary files a/lib/amd/build/paged_content_events.min.js and b/lib/amd/build/paged_content_events.min.js differ
index c0fd46e..62c3166 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js and b/lib/amd/build/paged_content_factory.min.js differ
index b59f52d..ae2015f 100644 (file)
Binary files a/lib/amd/build/paged_content_pages.min.js and b/lib/amd/build/paged_content_pages.min.js differ
index fcf9759..0c5dc70 100644 (file)
Binary files a/lib/amd/build/paged_content_paging_bar.min.js and b/lib/amd/build/paged_content_paging_bar.min.js differ
diff --git a/lib/amd/build/paged_content_paging_bar_limit_selector.min.js b/lib/amd/build/paged_content_paging_bar_limit_selector.min.js
new file mode 100644 (file)
index 0000000..77df6ca
Binary files /dev/null and b/lib/amd/build/paged_content_paging_bar_limit_selector.min.js differ
index 381853b..de7ac54 100644 (file)
Binary files a/lib/amd/build/paged_content_paging_dropdown.min.js and b/lib/amd/build/paged_content_paging_dropdown.min.js differ
diff --git a/lib/amd/src/paged_content.js b/lib/amd/src/paged_content.js
new file mode 100644 (file)
index 0000000..9067ca5
--- /dev/null
@@ -0,0 +1,75 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Javascript to load and render a paged content section.
+ *
+ * @module     core/paged_content
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(
+[
+    'jquery',
+    'core/paged_content_pages',
+    'core/paged_content_paging_bar',
+    'core/paged_content_paging_bar_limit_selector',
+    'core/paged_content_paging_dropdown'
+],
+function(
+    $,
+    Pages,
+    PagingBar,
+    PagingBarLimitSelector,
+    Dropdown
+) {
+
+    /**
+     * Initialise the paged content region by running the pages
+     * module and initialising any paging controls in the DOM.
+     *
+     * @param {object} root The paged content container element
+     * @param {function} renderPagesContentCallback (optional) A callback function to render a
+     *                                              content page. See core/paged_content_pages for
+     *                                              more defails.
+     */
+    var init = function(root, renderPagesContentCallback) {
+        root = $(root);
+        var pagesContainer = root.find(Pages.rootSelector);
+        var pagingBarContainer = root.find(PagingBar.rootSelector);
+        var dropdownContainer = root.find(Dropdown.rootSelector);
+        var pagingBarLimitSelectorContainer = root.find(PagingBarLimitSelector.rootSelector);
+        var id = root.attr('id');
+
+        Pages.init(pagesContainer, id, renderPagesContentCallback);
+
+        if (pagingBarContainer.length) {
+            PagingBar.init(pagingBarContainer, id);
+        }
+
+        if (pagingBarLimitSelectorContainer.length) {
+            PagingBarLimitSelector.init(pagingBarLimitSelectorContainer, id);
+        }
+
+        if (dropdownContainer.length) {
+            Dropdown.init(dropdownContainer, id);
+        }
+    };
+
+    return {
+        init: init,
+        rootSelector: '[data-region="paged-content-container"]'
+    };
+});
index 1181cb1..b7a0b1e 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Javascript to load and render the paging bar.
+ * Events for the paged content element.
  *
- * @module     core/paging_bar
+ * @module     core/paged_content_events
  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define([], function() {
     return {
         SHOW_PAGES: 'core-paged-content-show-pages',
+        PAGES_SHOWN: 'core-paged-content-pages-shown',
+        ALL_ITEMS_LOADED: 'core-paged-content-all-items-loaded',
+        SET_ITEMS_PER_PAGE_LIMIT: 'core-paged-content-set-items-per-page-limit'
     };
 });
index 709dd8e..696eeeb 100644 (file)
@@ -25,7 +25,7 @@ define(
     'jquery',
     'core/templates',
     'core/notification',
-    'core/paged_content_pages'
+    'core/paged_content'
 ],
 function(
     $,
@@ -37,21 +37,92 @@ function(
         PAGED_CONTENT: 'core/paged_content'
     };
 
+    var DEFAULT = {
+        ITEMS_PER_PAGE_SINGLE: 25,
+        ITEMS_PER_PAGE_ARRAY: [25, 50, 100, 0],
+        MAX_PAGES: 3
+    };
+
     /**
-     * Build the context to render the paging bar template with based on the number
-     * of pages to show.
+     * Get the default context to render the paged content mustache
+     * template.
      *
-     * @param  {int} numberOfPages How many pages to have in the paging bar.
-     * @param  {int} itemsPerPage  How many items will be shown per page.
-     * @return {object} The template context.
+     * @return {object}
      */
-    var buildPagingBarTemplateContext = function(numberOfPages, itemsPerPage) {
-        var context = {
-            "itemsperpage": itemsPerPage,
-            "previous": {},
-            "next": {},
-            "pages": []
+    var getDefaultTemplateContext = function() {
+        return {
+            pagingbar: false,
+            pagingdropdown: false,
+            skipjs: true,
+            ignorecontrolwhileloading: true,
+            controlplacementbottom: false
         };
+    };
+
+    /**
+     * Get the default context to render the paging bar mustache template.
+     *
+     * @return {object}
+     */
+    var getDefaultPagingBarTemplateContext = function() {
+        return {
+            showitemsperpageselector: false,
+            itemsperpage: 35,
+            previous: true,
+            next: true,
+            activepagenumber: 1,
+            hidecontrolonsinglepage: true,
+            pages: []
+        };
+    };
+
+    /**
+     * Calculate the number of pages required for the given number of items and
+     * how many of each item should appear on a page.
+     *
+     * @param  {Number} numberOfItems How many items in total.
+     * @param  {Number} itemsPerPage  How many items will be shown per page.
+     * @return {Number} The number of pages required.
+     */
+    var calculateNumberOfPages = function(numberOfItems, itemsPerPage) {
+        var numberOfPages = 1;
+
+        if (numberOfItems > 0) {
+            var partial = numberOfItems % itemsPerPage;
+
+            if (partial) {
+                numberOfItems -= partial;
+                numberOfPages = (numberOfItems / itemsPerPage) + 1;
+            } else {
+                numberOfPages = numberOfItems / itemsPerPage;
+            }
+        }
+
+        return numberOfPages;
+    };
+
+    /**
+     * Build the context for the paging bar template when we have a known number
+     * of items.
+     *
+     * @param {Number} numberOfItems How many items in total.
+     * @param {Number} itemsPerPage  How many items will be shown per page.
+     * @return {object} Mustache template
+     */
+    var buildPagingBarTemplateContextKnownLength = function(numberOfItems, itemsPerPage) {
+        if (itemsPerPage === null) {
+            itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
+        }
+
+        if ($.isArray(itemsPerPage)) {
+            // If we're given a total number of pages then we don't support a variable
+            // set of items per page so just use the first one.
+            itemsPerPage = itemsPerPage[0];
+        }
+
+        var context = getDefaultPagingBarTemplateContext();
+        context.itemsperpage = itemsPerPage;
+        var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
 
         for (var i = 1; i <= numberOfPages; i++) {
             var page = {
@@ -71,15 +142,101 @@ function(
     };
 
     /**
-     * Build the context to render the paging dropdown template with based on the number
+     * Convert the itemsPerPage value into a format applicable for the mustache template.
+     * The given value can be either a single integer or an array of integers / objects.
+     *
+     * E.g.
+     * In: [5, 10]
+     * out: [{value: 5, active: true}, {value: 10, active: false}]
+     *
+     * In: [5, {value: 10, active: true}]
+     * Out: [{value: 5, active: false}, {value: 10, active: true}]
+     *
+     * In: [{value: 5, active: false}, {value: 10, active: true}]
+     * Out: [{value: 5, active: false}, {value: 10, active: true}]
+     *
+     * @param {int|int[]} itemsPerPage Options for number of items per page.
+     * @return {int|array}
+     */
+    var buildItemsPerPagePagingBarContext = function(itemsPerPage) {
+        if ($.isArray(itemsPerPage)) {
+            // Convert the array into a format accepted by the template.
+            var context = itemsPerPage.map(function(num) {
+                if (typeof num === 'number') {
+                    // If the item is just a plain number then convert it into
+                    // an object with value and active keys.
+                    return {
+                        value: num,
+                        active: false
+                    };
+                } else {
+                    // Otherwise we assume the caller has specified things correctly.
+                    return num;
+                }
+            });
+
+            var activeItems = context.filter(function(item) {
+                return item.active;
+            });
+
+            // Default the first item to active if one hasn't been specified.
+            if (!activeItems.length) {
+                context[0].active = true;
+            }
+
+            return context;
+        } else {
+            return itemsPerPage;
+        }
+    };
+
+    /**
+     * Build the context for the paging bar template when we have an unknown
+     * number of items.
+     *
+     * @param {Number} itemsPerPage  How many items will be shown per page.
+     * @return {object} Mustache template
+     */
+    var buildPagingBarTemplateContextUnknownLength = function(itemsPerPage) {
+        if (itemsPerPage === null) {
+            itemsPerPage = DEFAULT.ITEMS_PER_PAGE_ARRAY;
+        }
+
+        var context = getDefaultPagingBarTemplateContext();
+        context.itemsperpage = buildItemsPerPagePagingBarContext(itemsPerPage);
+        context.showitemsperpageselector = $.isArray(itemsPerPage);
+
+        return context;
+    };
+
+    /**
+     * Build the context to render the paging bar template with based on the number
+     * of pages to show.
+     *
+     * @param  {int|null} numberOfItems How many items are there total.
+     * @param  {int|null} itemsPerPage  How many items will be shown per page.
+     * @return {object} The template context.
+     */
+    var buildPagingBarTemplateContext = function(numberOfItems, itemsPerPage) {
+        if (numberOfItems) {
+            return buildPagingBarTemplateContextKnownLength(numberOfItems, itemsPerPage);
+        } else {
+            return buildPagingBarTemplateContextUnknownLength(itemsPerPage);
+        }
+    };
+
+    /**
+     * Build the context to render the paging dropdown template based on the number
      * of pages to show and items per page.
      *
      * This control is rendered with a gradual increase of the items per page to
      * limit the number of pages in the dropdown. Each page will show twice as much
      * as the previous page (except for the first two pages).
      *
+     * By default there will only be 4 pages shown (including the "All" option) unless
+     * a different number of pages is defined using the maxPages config value.
+     *
      * For example:
-     * Number of pages = 3
      * Items per page = 25
      * Would render a dropdown will 4 options:
      * 25
@@ -87,19 +244,30 @@ function(
      * 100
      * All
      *
-     * @param  {int} numberOfPages How many options to have in the dropdown.
-     * @param  {int} itemsPerPage  How many items will be shown per page.
+     * @param  {Number} itemsPerPage  How many items will be shown per page.
      * @param  {object} config  Configuration options provided by the client.
      * @return {object} The template context.
      */
-    var buildPagingDropdownTemplateContext = function(numberOfPages, itemsPerPage, config) {
+    var buildPagingDropdownTemplateContext = function(itemsPerPage, config) {
+        if (itemsPerPage === null) {
+            itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
+        }
+
+        if ($.isArray(itemsPerPage)) {
+            // If we're given an array for the items per page, rather than a number,
+            // then just use that as the options for the dropdown.
+            return {
+                options: itemsPerPage
+            };
+        }
+
         var context = {
             options: []
         };
 
         var totalItems = 0;
         var lastIncrease = 0;
-        var maxPages = numberOfPages;
+        var maxPages = DEFAULT.MAX_PAGES;
 
         if (config.hasOwnProperty('maxPages')) {
             maxPages = config.maxPages;
@@ -140,50 +308,86 @@ function(
      * By default the code will render a paging bar for the paging controls unless
      * otherwise specified in the provided config.
      *
-     * @param  {int} numberOfPages How many pages to have.
-     * @param  {int} itemsPerPage  How many items will be shown per page.
+     * @param  {int|null} numberOfItems Total number of items.
+     * @param  {int|null|array} itemsPerPage  How many items will be shown per page.
      * @param  {object} config  Configuration options provided by the client.
      * @return {object} The template context.
      */
-    var buildTemplateContext = function(numberOfPages, itemsPerPage, config) {
-        var context = {
-            pagingbar: false,
-            pagingdropdown: false,
-            skipjs: true
-        };
+    var buildTemplateContext = function(numberOfItems, itemsPerPage, config) {
+        var context = getDefaultTemplateContext();
+
+        if (config.hasOwnProperty('ignoreControlWhileLoading')) {
+            context.ignorecontrolwhileloading = config.ignoreControlWhileLoading;
+        }
+
+        if (config.hasOwnProperty('controlPlacementBottom')) {
+            context.controlplacementbottom = config.controlPlacementBottom;
+        }
+
+        if (config.hasOwnProperty('hideControlOnSinglePage')) {
+            context.hidecontrolonsinglepage = config.hideControlOnSinglePage;
+        }
+
+        if (config.hasOwnProperty('ariaLabels')) {
+            context.arialabels = config.ariaLabels;
+        }
 
         if (config.hasOwnProperty('dropdown') && config.dropdown) {
-            context.pagingdropdown = buildPagingDropdownTemplateContext(numberOfPages, itemsPerPage, config);
+            context.pagingdropdown = buildPagingDropdownTemplateContext(itemsPerPage, config);
         } else {
-            context.pagingbar = buildPagingBarTemplateContext(numberOfPages, itemsPerPage);
+            context.pagingbar = buildPagingBarTemplateContext(numberOfItems, itemsPerPage);
         }
 
         return context;
     };
 
     /**
-     * Calculate the number of pages required for the given number of items and
-     * how many of each item should appear on a page.
+     * Create a paged content widget where the complete list of items is not loaded
+     * up front but will instead be loaded by an ajax request (or similar).
      *
-     * @param  {int} numberOfItems How many items in total.
-     * @param  {int} itemsPerPage  How many items will be shown per page.
-     * @return {int} The number of pages required.
+     * The client code must provide a callback function which loads and renders the
+     * items for each page. See PagedContent.init for more details.
+     *
+     * The function will return a deferred that is resolved with a jQuery object
+     * for the HTML content and a string for the JavaScript.
+     *
+     * The current list of configuration options available are:
+     *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
+     *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
+     *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
+     *      controlPlacementBottom {bool} Render controls under paged content (default to false)
+     *
+     * @param  {function} renderPagesContentCallback  Callback for loading and rendering the items.
+     * @param  {object} config  Configuration options provided by the client.
+     * @return {promise} Resolved with jQuery HTML and string JS.
      */
-    var calculateNumberOfPages = function(numberOfItems, itemsPerPage) {
-        var numberOfPages = 1;
-
-        if (numberOfItems > 0) {
-            var partial = numberOfItems % itemsPerPage;
-
-            if (partial) {
-                numberOfItems -= partial;
-                numberOfPages = (numberOfItems / itemsPerPage) + 1;
-            } else {
-                numberOfPages = numberOfItems / itemsPerPage;
-            }
-        }
+    var create = function(renderPagesContentCallback, config) {
+        return createWithTotalAndLimit(null, null, renderPagesContentCallback, config);
+    };
 
-        return numberOfPages;
+    /**
+     * Create a paged content widget where the complete list of items is not loaded
+     * up front but will instead be loaded by an ajax request (or similar).
+     *
+     * The client code must provide a callback function which loads and renders the
+     * items for each page. See PagedContent.init for more details.
+     *
+     * The function will return a deferred that is resolved with a jQuery object
+     * for the HTML content and a string for the JavaScript.
+     *
+     * The current list of configuration options available are:
+     *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
+     *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
+     *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
+     *      controlPlacementBottom {bool} Render controls under paged content (default to false)
+     *
+     * @param  {int|array|null} itemsPerPage  How many items will be shown per page.
+     * @param  {function} renderPagesContentCallback  Callback for loading and rendering the items.
+     * @param  {object} config  Configuration options provided by the client.
+     * @return {promise} Resolved with jQuery HTML and string JS.
+     */
+    var createWithLimit = function(itemsPerPage, renderPagesContentCallback, config) {
+        return createWithTotalAndLimit(null, itemsPerPage, renderPagesContentCallback, config);
     };
 
     /**
@@ -198,30 +402,29 @@ function(
      *
      * The current list of configuration options available are:
      *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
+     *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
+     *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
+     *      controlPlacementBottom {bool} Render controls under paged content (default to false)
      *
-     * @param  {int} numberOfItems How many items are there in total.
-     * @param  {int} itemsPerPage  How many items will be shown per page.
+     * @param  {int|null} numberOfItems How many items are there in total.
+     * @param  {int|array|null} itemsPerPage  How many items will be shown per page.
      * @param  {function} renderPagesContentCallback  Callback for loading and rendering the items.
      * @param  {object} config  Configuration options provided by the client.
      * @return {promise} Resolved with jQuery HTML and string JS.
      */
-    var createFromAjax = function(numberOfItems, itemsPerPage, renderPagesContentCallback, config) {
-        if (typeof config == 'undefined') {
-            config = {};
-        }
+    var createWithTotalAndLimit = function(numberOfItems, itemsPerPage, renderPagesContentCallback, config) {
+        config = config || {};
 
         var deferred = $.Deferred();
-        var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
-        var templateContext = buildTemplateContext(numberOfPages, itemsPerPage, config);
+        var templateContext = buildTemplateContext(numberOfItems, itemsPerPage, config);
 
         Templates.render(TEMPLATES.PAGED_CONTENT, templateContext)
             .then(function(html, js) {
                 html = $(html);
 
                 var container = html;
-                var pagedContent = html.find(PagedContent.rootSelector);
 
-                PagedContent.init(pagedContent, container, renderPagesContentCallback);
+                PagedContent.init(container, renderPagesContentCallback);
 
                 deferred.resolve(html, js);
                 return;
@@ -231,7 +434,7 @@ function(
             })
             .fail(Notification.exception);
 
-        return deferred;
+        return deferred.promise();
     };
 
     /**
@@ -247,9 +450,12 @@ function(
      *
      * The current list of configuration options available are:
      *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
+     *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
+     *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
+     *      controlPlacementBottom {bool} Render controls under paged content (default to false)
      *
      * @param  {array} contentItems The list of items to paginate.
-     * @param  {int} itemsPerPage  How many items will be shown per page.
+     * @param  {Number} itemsPerPage  How many items will be shown per page.
      * @param  {function} renderContentCallback  Callback for rendering the items for the page.
      * @param  {object} config  Configuration options provided by the client.
      * @return {promise} Resolved with jQuery HTML and string JS.
@@ -260,7 +466,7 @@ function(
         }
 
         var numberOfItems = contentItems.length;
-        return createFromAjax(numberOfItems, itemsPerPage, function(pagesData) {
+        return createWithTotalAndLimit(numberOfItems, itemsPerPage, function(pagesData) {
             var contentToRender = [];
             pagesData.forEach(function(pageData) {
                 var begin = pageData.offset;
@@ -274,7 +480,11 @@ function(
     };
 
     return {
-        createFromAjax: createFromAjax,
-        createFromStaticList: createFromStaticList
+        create: create,
+        createWithLimit: createWithLimit,
+        createWithTotalAndLimit: createWithTotalAndLimit,
+        createFromStaticList: createFromStaticList,
+        // Backwards compatibility just in case anyone was using this.
+        createFromAjax: createWithTotalAndLimit
     };
 });
index 4fbbb10..e76069a 100644 (file)
@@ -25,12 +25,14 @@ define(
         'jquery',
         'core/templates',
         'core/notification',
+        'core/pubsub',
         'core/paged_content_events'
     ],
     function(
         $,
         Templates,
         Notification,
+        PubSub,
         PagedContentEvents
     ) {
 
@@ -45,6 +47,8 @@ define(
         LOADING: 'core/overlay_loading'
     };
 
+    var PRELOADING_GRACE_PERIOD = 300;
+
     /**
      * Find a page by the number.
      *
@@ -60,23 +64,27 @@ define(
      * Show the loading spinner until the returned deferred is resolved by the
      * calling code.
      *
+     * The loading spinner is only rendered after a short grace period to avoid
+     * having it flash up briefly in the interface.
+     *
      * @param {object} root The root element.
      * @returns {promise} The page.
      */
     var startLoading = function(root) {
         var deferred = $.Deferred();
+        root.attr('aria-busy', true);
 
         Templates.render(TEMPLATES.LOADING, {visible: true})
             .then(function(html) {
                 var loadingSpinner = $(html);
-                // Put this in a timer to give the calling code 100 milliseconds
+                // Put this in a timer to give the calling code 300 milliseconds
                 // to render the content before we show the loading spinner. This
                 // helps prevent a loading icon flicker on close to instant
                 // rendering.
                 var timerId = setTimeout(function() {
                     root.css('position', 'relative');
                     loadingSpinner.appendTo(root);
-                }, 100);
+                }, PRELOADING_GRACE_PERIOD);
 
                 deferred.always(function() {
                     clearTimeout(timerId);
@@ -84,6 +92,7 @@ define(
                     // by the calling code.
                     loadingSpinner.remove();
                     root.css('position', '');
+                    root.removeAttr('aria-busy');
                     return;
                 });
 
@@ -102,12 +111,13 @@ define(
      *
      * @param {object} root The root element.
      * @param {promise} pagePromise The promise resolved with HTML and JS to render in the page.
-     * @param {int} pageNumber The page number.
+     * @param {Number} pageNumber The page number.
      * @returns {promise} The page.
      */
     var renderPagePromise = function(root, pagePromise, pageNumber) {
         var deferred = $.Deferred();
         pagePromise.then(function(html, pageJS) {
+            pageJS = pageJS || '';
             // When we get the contents to be rendered we can pass it in as the
             // content for a new page.
             Templates.render(TEMPLATES.PAGING_CONTENT_ITEM, {
@@ -135,7 +145,7 @@ define(
         })
         .fail(Notification.exception);
 
-        return deferred;
+        return deferred.promise();
     };
 
     /**
@@ -164,11 +174,14 @@ define(
      * If the renderPagesContentCallback is not provided then it is assumed that
      * all pages have been rendered prior to initialising this module.
      *
+     * This function triggers the PAGES_SHOWN event after the pages have been rendered.
+     *
      * @param {object} root The root element.
      * @param {Number} pagesData The data for which pages need to be visible.
+     * @param {string} id A unique id for this instance.
      * @param {function} renderPagesContentCallback Render pages content.
      */
-    var showPages = function(root, pagesData, renderPagesContentCallback) {
+    var showPages = function(root, pagesData, id, renderPagesContentCallback) {
         var existingPages = [];
         var newPageData = [];
         var newPagesPromise = $.Deferred();
@@ -188,7 +201,11 @@ define(
         if (newPageData.length && typeof renderPagesContentCallback === 'function') {
             // If we have pages we haven't previously seen then ask the client code
             // to render them for us by calling the callback.
-            var promises = renderPagesContentCallback(newPageData);
+            var promises = renderPagesContentCallback(newPageData, {
+                allItemsLoaded: function(lastPageNumber) {
+                    PubSub.publish(id + PagedContentEvents.ALL_ITEMS_LOADED, lastPageNumber);
+                }
+            });
             // After the client has finished rendering each of the pages being asked
             // for then begin our rendering process to put that content into paged
             // content pages.
@@ -229,6 +246,11 @@ define(
 
             return;
         })
+        .then(function() {
+            // Let everything else know we've displayed the pages.
+            PubSub.publish(id + PagedContentEvents.PAGES_SHOWN, pagesData);
+            return;
+        })
         .fail(Notification.exception)
         .always(function() {
             loadingPromise.resolve();
@@ -264,15 +286,20 @@ define(
      * The event element is the element to listen for the paged content events on.
      *
      * @param {object} root The root element.
-     * @param {object} eventElement The element to listen for events on.
+     * @param {string} id A unique id for this instance.
      * @param {function} renderPagesContentCallback Render pages content.
      */
-    var init = function(root, eventElement, renderPagesContentCallback) {
+    var init = function(root, id, renderPagesContentCallback) {
         root = $(root);
-        eventElement = $(eventElement);
 
-        eventElement.on(PagedContentEvents.SHOW_PAGES, function(e, pagesData) {
-            showPages(root, pagesData, renderPagesContentCallback);
+        PubSub.subscribe(id + PagedContentEvents.SHOW_PAGES, function(pagesData) {
+            showPages(root, pagesData, id, renderPagesContentCallback);
+        });
+
+        PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function() {
+            // If the items per page limit was changed then we need to clear our content
+            // the load new values based on the new limit.
+            root.empty();
         });
     };
 
index c1d26b4..456ee39 100644 (file)
@@ -24,19 +24,27 @@ define(
     [
         'jquery',
         'core/custom_interaction_events',
-        'core/paged_content_events'
+        'core/paged_content_events',
+        'core/str',
+        'core/pubsub'
     ],
     function(
         $,
         CustomEvents,
-        PagedContentEvents
+        PagedContentEvents,
+        Str,
+        PubSub
     ) {
 
     var SELECTORS = {
         ROOT: '[data-region="paging-bar"]',
         PAGE: '[data-page]',
         PAGE_ITEM: '[data-region="page-item"]',
-        ACTIVE_PAGE_ITEM: '[data-region="page-item"].active'
+        PAGE_LINK: '[data-region="page-link"]',
+        FIRST_BUTTON: '[data-control="first"]',
+        LAST_BUTTON: '[data-control="last"]',
+        NEXT_BUTTON: '[data-control="next"]',
+        PREVIOUS_BUTTON: '[data-control="previous"]'
     };
 
     /**
@@ -50,43 +58,74 @@ define(
         return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]');
     };
 
+    /**
+     * Get the next button element.
+     *
+     * @param {object} root The root element.
+     * @return {jQuery}
+     */
+    var getNextButton = function(root) {
+        return root.find(SELECTORS.NEXT_BUTTON);
+    };
+
+    /**
+     * Set the last page number after which no more pages
+     * should be loaded.
+     *
+     * @param {object} root The root element.
+     * @param {Number} number Page number.
+     */
+    var setLastPageNumber = function(root, number) {
+        root.attr('data-last-page-number', number);
+    };
+
     /**
      * Get the last page number.
      *
      * @param {object} root The root element.
-     * @return {int}
+     * @return {Number}
      */
     var getLastPageNumber = function(root) {
-        var lastPage = root.find(SELECTORS.PAGE).last();
-        if (lastPage) {
-            return parseInt(lastPage.attr('data-page-number'), 10);
-        } else {
-            return null;
-        }
+        return parseInt(root.attr('data-last-page-number'), 10);
     };
 
     /**
      * Get the active page number.
      *
      * @param {object} root The root element.
-     * @returns {int} The page number
+     * @returns {Number} The page number
      */
     var getActivePageNumber = function(root) {
-        var activePage = root.find(SELECTORS.ACTIVE_PAGE_ITEM);
+        return parseInt(root.attr('data-active-page-number'), 10);
+    };
 
-        if (activePage.length) {
-            return getPageNumber(root, activePage);
-        } else {
-            return null;
-        }
+    /**
+     * Set the active page number.
+     *
+     * @param {object} root The root element.
+     * @param {Number} number Page number.
+     */
+    var setActivePageNumber = function(root, number) {
+        root.attr('data-active-page-number', number);
+    };
+
+    /**
+     * Check if there is an active page number.
+     *
+     * @param {object} root The root element.
+     * @returns {bool}
+     */
+    var hasActivePageNumber = function(root) {
+        var number = getActivePageNumber(root);
+        return !isNaN(number) && number != 0;
     };
 
     /**
-     * Get the page number.
+     * Get the page number for a given page.
      *
      * @param {object} root The root element.
-     * @param {object} page The page.
-     * @returns {int} The page number
+     * @param {object} page The page element.
+     * @returns {Number} The page number
      */
     var getPageNumber = function(root, page) {
         if (page.attr('data-page') != undefined) {
@@ -110,7 +149,9 @@ define(
             case 'next':
                 activePageNumber = getActivePageNumber(root);
                 var lastPage = getLastPageNumber(root);
-                if (activePageNumber && activePageNumber < lastPage) {
+                if (!lastPage) {
+                    pageNumber = activePageNumber + 1;
+                } else if (activePageNumber && activePageNumber < lastPage) {
                     pageNumber = activePageNumber + 1;
                 } else {
                     pageNumber = lastPage;
@@ -139,22 +180,207 @@ define(
      * Get the limit of items for each page.
      *
      * @param {object} root The root element.
-     * @returns {int}
+     * @returns {Number}
      */
     var getLimit = function(root) {
         return parseInt(root.attr('data-items-per-page'), 10);
     };
 
+    /**
+     * Set the limit of items for each page.
+     *
+     * @param {object} root The root element.
+     * @param {Number} limit Items per page limit.
+     */
+    var setLimit = function(root, limit) {
+        root.attr('data-items-per-page', limit);
+    };
+
+    /**
+     * Show the paging bar.
+     *
+     * @param {object} root The root element.
+     */
+    var show = function(root) {
+        root.removeClass('hidden');
+    };
+
+    /**
+     * Hide the paging bar.
+     *
+     * @param {object} root The root element.
+     */
+    var hide = function(root) {
+        root.addClass('hidden');
+    };
+
+    /**
+     * Disable the next and last buttons in the paging bar.
+     *
+     * @param {object} root The root element.
+     */
+    var disableNextControlButtons = function(root) {
+        var nextButton = root.find(SELECTORS.NEXT_BUTTON);
+        var lastButton = root.find(SELECTORS.LAST_BUTTON);
+
+        nextButton.addClass('disabled');
+        nextButton.attr('aria-disabled', true);
+        lastButton.addClass('disabled');
+        lastButton.attr('aria-disabled', true);
+    };
+
+    /**
+     * Enable the next and last buttons in the paging bar.
+     *
+     * @param {object} root The root element.
+     */
+    var enableNextControlButtons = function(root) {
+        var nextButton = root.find(SELECTORS.NEXT_BUTTON);
+        var lastButton = root.find(SELECTORS.LAST_BUTTON);
+
+        nextButton.removeClass('disabled');
+        nextButton.removeAttr('aria-disabled');
+        lastButton.removeClass('disabled');
+        lastButton.removeAttr('aria-disabled');
+    };
+
+    /**
+     * Disable the previous and first buttons in the paging bar.
+     *
+     * @param {object} root The root element.
+     */
+    var disablePreviousControlButtons = function(root) {
+        var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);
+        var firstButton = root.find(SELECTORS.FIRST_BUTTON);
+
+        previousButton.addClass('disabled');
+        previousButton.attr('aria-disabled', true);
+        firstButton.addClass('disabled');
+        firstButton.attr('aria-disabled', true);
+    };
+
+    /**
+     * Enable the previous and first buttons in the paging bar.
+     *
+     * @param {object} root The root element.
+     */
+    var enablePreviousControlButtons = function(root) {
+        var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);
+        var firstButton = root.find(SELECTORS.FIRST_BUTTON);
+
+        previousButton.removeClass('disabled');
+        previousButton.removeAttr('aria-disabled');
+        firstButton.removeClass('disabled');
+        firstButton.removeAttr('aria-disabled');
+    };
+
+    /**
+     * Get the components for a get_string request for the aria-label
+     * on a page. The value is a comma separated string of key and
+     * component.
+     *
+     * @param {object} root The root element.
+     * @return {array} First element is the key, second is the component.
+     */
+    var getPageAriaLabelComponents = function(root) {
+        var componentString = root.attr('data-aria-label-components-pagination-item');
+        var components = componentString.split(',').map(function(component) {
+            return component.trim();
+        });
+        return components;
+    };
+
+    /**
+     * Get the components for a get_string request for the aria-label
+     * on an active page. The value is a comma separated string of key and
+     * component.
+     *
+     * @param {object} root The root element.
+     * @return {array} First element is the key, second is the component.
+     */
+    var getActivePageAriaLabelComponents = function(root) {
+        var componentString = root.attr('data-aria-label-components-pagination-active-item');
+        var components = componentString.split(',').map(function(component) {
+            return component.trim();
+        });
+        return components;
+    };
+
     /**
      * Set page numbers on each of the given items. Page numbers are set
      * from 1..n (where n is the number of items).
      *
+     * Sets the active page number to be the last page found with
+     * an "active" class (if any).
+     *
+     * Sets the last page number.
+     *
+     * @param {object} root The root element.
      * @param {jQuery} items A jQuery list of items.
      */
-    var generatePageNumbers = function(items) {
+    var generatePageNumbers = function(root, items) {
+        var lastPageNumber = 0;
+        setActivePageNumber(root, 0);
+
         items.each(function(index, item) {
+            var pageNumber = index + 1;
             item = $(item);
-            item.attr('data-page-number', index + 1);
+            item.attr('data-page-number', pageNumber);
+            lastPageNumber++;
+
+            if (item.hasClass('active')) {
+                setActivePageNumber(root, pageNumber);
+            }
+        });
+
+        setLastPageNumber(root, lastPageNumber);
+    };
+
+    /**
+     * Set the aria-labels on each of the page items in the paging bar.
+     * This includes the next, previous, first, and last items.
+     *
+     * @param {object} root The root element.
+     */
+    var generateAriaLabels = function(root) {
+        var pageAriaLabelComponents = getPageAriaLabelComponents(root);
+        var activePageAriaLabelComponents = getActivePageAriaLabelComponents(root);
+        var activePageNumber = getActivePageNumber(root);
+        var pageItems = root.find(SELECTORS.PAGE_ITEM);
+        // We want to request all of the strings at once rather than
+        // one at a time.
+        var stringRequests = pageItems.map(function(index, page) {
+            page = $(page);
+            var pageNumber = getPageNumber(root, page);
+
+            if (pageNumber === activePageNumber) {
+                return {
+                    key: activePageAriaLabelComponents[0],
+                    component: activePageAriaLabelComponents[1],
+                    param: pageNumber
+                };
+            } else {
+                return {
+                    key: pageAriaLabelComponents[0],
+                    component: pageAriaLabelComponents[1],
+                    param: pageNumber
+                };
+            }
+        });
+
+        Str.get_strings(stringRequests).then(function(strings) {
+            pageItems.each(function(index, page) {
+                page = $(page);
+                var string = strings[index];
+                page.attr('aria-label', string);
+                page.find(SELECTORS.PAGE_LINK).attr('aria-label', string);
+            });
+
+            return strings;
+        })
+        .catch(function() {
+            // No need to interrupt the page if we can't load the aria lang strings.
+            return;
         });
     };
 
@@ -164,10 +390,11 @@ define(
      * update.
      *
      * @param {object} root The root element.
-     * @param {int} pageNumber The number for the page to show.
-     * @param {object} page The page.
+     * @param {Number} pageNumber The number for the page to show.
+     * @param {string} id A uniqie id for this instance.
      */
-    var showPage = function(root, pageNumber) {
+    var showPage = function(root, pageNumber, id) {
+        var lastPageNumber = getLastPageNumber(root);
         var isSamePage = pageNumber == getActivePageNumber(root);
         var limit = getLimit(root);
         var offset = (pageNumber - 1) * limit;
@@ -175,36 +402,56 @@ define(
         if (!isSamePage) {
             // We only need to toggle the active class if the user didn't click
             // on the already active page.
-            root.find(SELECTORS.PAGE_ITEM).removeClass('active');
+            root.find(SELECTORS.PAGE_ITEM).removeClass('active').removeAttr('aria-current');
             var page = getPageByNumber(root, pageNumber);
             page.addClass('active');
+            page.attr('aria-current', true);
+            setActivePageNumber(root, pageNumber);
+        }
+
+        // Make sure the control buttons are disabled as the user navigates
+        // to either end of the limits.
+        if (lastPageNumber && pageNumber >= lastPageNumber) {
+            disableNextControlButtons(root);
+        } else {
+            enableNextControlButtons(root);
         }
 
+        if (pageNumber > 1) {
+            enablePreviousControlButtons(root);
+        } else {
+            disablePreviousControlButtons(root);
+        }
+
+        generateAriaLabels(root);
+
         // This event requires a payload that contains a list of all pages that
         // were activated. In the case of the paging bar we only show one page at
         // a time.
-        root.trigger(PagedContentEvents.SHOW_PAGES, [[{
+        PubSub.publish(id + PagedContentEvents.SHOW_PAGES, [{
             pageNumber: pageNumber,
             limit: limit,
             offset: offset
-        }]]);
+        }]);
     };
 
     /**
-     * Initialise the paging bar.
+     * Add event listeners for interactions with the paging bar as well as listening
+     * for custom paged content events.
+     *
+     * Each event will trigger different logic to update parts of the paging bar's
+     * display.
+     *
      * @param {object} root The root element.
+     * @param {string} id A uniqie id for this instance.
      */
-    var init = function(root) {
-        root = $(root);
-        var pages = root.find(SELECTORS.PAGE);
-        generatePageNumbers(pages);
+    var registerEventListeners = function(root, id) {
+        var ignoreControlWhileLoading = root.attr('data-ignore-control-while-loading');
+        var loading = false;
 
-        var activePageNumber = getActivePageNumber(root);
-        if (activePageNumber) {
-            // If the the paging bar was rendered with an active page selected
-            // then make sure we fired off the event to tell the content page to
-            // show.
-            showPage(root, activePageNumber);
+        if (ignoreControlWhileLoading == "") {
+            // Default to ignoring control while loading if not specified.
+            ignoreControlWhileLoading = true;
         }
 
         CustomEvents.define(root, [
@@ -212,17 +459,98 @@ define(
         ]);
 
         root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) {
-            var page = $(e.target).closest(SELECTORS.PAGE_ITEM);
-            var pageNumber = getPageNumber(root, page);
-            showPage(root, pageNumber);
-
             data.originalEvent.preventDefault();
             data.originalEvent.stopPropagation();
+
+            if (ignoreControlWhileLoading && loading) {
+                // Do nothing if configured to ignore control while loading.
+                return;
+            }
+
+            var page = $(e.target).closest(SELECTORS.PAGE_ITEM);
+
+            if (!page.hasClass('disabled')) {
+                var pageNumber = getPageNumber(root, page);
+                showPage(root, pageNumber, id);
+                loading = true;
+            }
+        });
+
+        // This event is fired when all of the items have been loaded. Typically used
+        // in an "infinite" pages context when we don't know the exact number of pages
+        // ahead of time.
+        PubSub.subscribe(id + PagedContentEvents.ALL_ITEMS_LOADED, function(pageNumber) {
+            loading = false;
+            var currentLastPage = getLastPageNumber(root);
+
+            if (!currentLastPage || pageNumber < currentLastPage) {
+                // Somehow the value we've got saved is higher than the new
+                // value we just received. Perhaps events came out of order.
+                // In any case, save the lowest value.
+                setLastPageNumber(root, pageNumber);
+            }
+
+            if (pageNumber === 1 && root.attr('data-hide-control-on-single-page')) {
+                // If all items were loaded on the first page then we can hide
+                // the paging bar because there are no other pages to load.
+                hide(root);
+                disableNextControlButtons(root);
+                disablePreviousControlButtons(root);
+            } else {
+                show(root);
+                disableNextControlButtons(root);
+            }
+        });
+
+        // This event is fired after all of the requested pages have been rendered.
+        PubSub.subscribe(id + PagedContentEvents.PAGES_SHOWN, function() {
+            // All pages have been shown so turn off the loading flag.
+            loading = false;
+        });
+
+        // This is triggered when the paging limit is modified.
+        PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function(limit) {
+            // Update the limit.
+            setLimit(root, limit);
+            setLastPageNumber(root, 0);
+            setActivePageNumber(root, 0);
+            show(root);
+            // Reload the data from page 1 again.
+            showPage(root, 1, id);
         });
     };
 
+    /**
+     * Initialise the paging bar.
+     * @param {object} root The root element.
+     * @param {string} id A uniqie id for this instance.
+     */
+    var init = function(root, id) {
+        root = $(root);
+        var pages = root.find(SELECTORS.PAGE);
+        generatePageNumbers(root, pages);
+        registerEventListeners(root, id);
+
+        if (hasActivePageNumber(root)) {
+            var activePageNumber = getActivePageNumber(root);
+            // If the the paging bar was rendered with an active page selected
+            // then make sure we fired off the event to tell the content page to
+            // show.
+            getPageByNumber(root, activePageNumber).click();
+            if (activePageNumber == 1) {
+                // If the first page is active then disable the previous buttons.
+                disablePreviousControlButtons(root);
+            }
+        } else {
+            // There was no active page number so load the first page using
+            // the next button. This allows the infinite pagination to work.
+            getNextButton(root).click();
+        }
+    };
+
     return {
         init: init,
+        showPage: showPage,
         rootSelector: SELECTORS.ROOT,
     };
 });
diff --git a/lib/amd/src/paged_content_paging_bar_limit_selector.js b/lib/amd/src/paged_content_paging_bar_limit_selector.js
new file mode 100644 (file)
index 0000000..ebc327e
--- /dev/null
@@ -0,0 +1,77 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Javascript for dynamically changing the page limits.
+ *
+ * @module     core/paged_content_paging_bar_limit_selector
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(
+[
+    'jquery',
+    'core/custom_interaction_events',
+    'core/paged_content_events',
+    'core/pubsub'
+],
+function(
+    $,
+    CustomEvents,
+    PagedContentEvents,
+    PubSub
+) {
+
+    var SELECTORS = {
+        ROOT: '[data-region="paging-control-limit-container"]',
+        LIMIT_OPTION: '[data-limit]',
+        LIMIT_TOGGLE: '[data-action="limit-toggle"]',
+    };
+
+    /**
+     * Trigger the SET_ITEMS_PER_PAGE_LIMIT event when the page limit option
+     * is modified.
+     *
+     * @param {object} root The root element.
+     * @param {string} id A unique id for this instance.
+     */
+    var init = function(root, id) {
+        root = $(root);
+
+        CustomEvents.define(root, [
+            CustomEvents.events.activate
+        ]);
+
+        root.on(CustomEvents.events.activate, SELECTORS.LIMIT_OPTION, function(e, data) {
+            var optionElement = $(e.target).closest(SELECTORS.LIMIT_OPTION);
+
+            if (optionElement.hasClass('active')) {
+                // Don't do anything if it was the active option selected.
+                return;
+            }
+
+            var limit = parseInt(optionElement.attr('data-limit'), 10);
+            // Tell the rest of the pagination components that the limit has changed.
+            PubSub.publish(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, limit);
+
+            data.originalEvent.preventDefault();
+        });
+    };
+
+    return {
+        init: init,
+        rootSelector: SELECTORS.ROOT
+    };
+});
index 59d75b5..5fe5870 100644 (file)
@@ -24,12 +24,14 @@ define(
     [
         'jquery',
         'core/custom_interaction_events',
-        'core/paged_content_events'
+        'core/paged_content_events',
+        'core/pubsub'
     ],
     function(
         $,
         CustomEvents,
-        PagedContentEvents
+        PagedContentEvents,
+        PubSub
     ) {
 
     var SELECTORS = {
@@ -44,7 +46,7 @@ define(
      * Get the page number.
      *
      * @param {jquery} item The dropdown item.
-     * @returns {int}
+     * @returns {Number}
      */
     var getPageNumber = function(item) {
         return parseInt(item.attr('data-page-number'), 10);
@@ -79,7 +81,7 @@ define(
      * Get the number of items to be loaded for the dropdown item.
      *
      * @param {jquery} item The dropdown item.
-     * @returns {int}
+     * @returns {Number}
      */
     var getLimit = function(item) {
         return parseInt(item.attr('data-item-count'), 10);
@@ -91,7 +93,7 @@ define(
      *
      * @param {jquery} root The root element.
      * @param {jquery} item The dropdown item.
-     * @returns {int}
+     * @returns {Number}
      */
     var getOffset = function(root, item) {
         if (item.attr('data-offset') != undefined) {
@@ -181,8 +183,9 @@ define(
      *
      * @param {jquery} root The root element.
      * @param {jquery} item The dropdown item.
+     * @param {string} id A unique id for this instance.
      */
-    var setActiveItem = function(root, item) {
+    var setActiveItem = function(root, item, id) {
         var prevItems = getPreviousItems(root, item);
         var allItems = prevItems.add(item);
         var eventPayload = generateEventPayload(root, allItems);
@@ -197,7 +200,7 @@ define(
         // Bootstrap 2 compatibility.
         toggle.append(caret);
         // Fire the event to tell the content to update.
-        root.trigger(PagedContentEvents.SHOW_PAGES, [eventPayload]);
+        PubSub.publish(id + PagedContentEvents.SHOW_PAGES, eventPayload);
     };
 
     /**
@@ -206,8 +209,9 @@ define(
      * new pages.
      *
      * @param {object} root The root element.
+     * @param {string} id A unique id for this instance.
      */
-    var init = function(root) {
+    var init = function(root, id) {
         root = $(root);
         var items = getAllItems(root);
         generatePageNumbers(items);
@@ -215,7 +219,7 @@ define(
         var activeItem = getActiveItem(root);
         if (activeItem.length) {
             // Fire the first event for the content to make sure it's visible.
-            setActiveItem(root, activeItem);
+            setActiveItem(root, activeItem, id);
         }
 
         CustomEvents.define(root, [
@@ -224,7 +228,7 @@ define(
 
         root.on(CustomEvents.events.activate, SELECTORS.DROPDOWN_ITEM, function(e, data) {
             var item = $(e.target).closest(SELECTORS.DROPDOWN_ITEM);
-            setActiveItem(root, item);
+            setActiveItem(root, item, id);
 
             data.originalEvent.preventDefault();
         });
index 47b5fbb..953cbcc 100644 (file)
     }
 }}
 <div id="paged-content-container-{{uniqid}}" data-region="paged-content-container">
-    {{#pagingbar}}
-        {{> core/paged_content_paging_bar }}
-    {{/pagingbar}}
-    {{#pagingdropdown}}
-        {{> core/paged_content_paging_dropdown }}
-    {{/pagingdropdown}}
+    {{^controlplacementbottom}}
+        <div class="m-b-1">
+            {{#pagingbar}}
+                {{> core/paged_content_paging_bar }}
+            {{/pagingbar}}
+            {{#pagingdropdown}}
+                {{> core/paged_content_paging_dropdown }}
+            {{/pagingdropdown}}
+        </div>
+    {{/controlplacementbottom}}
     {{> core/paged_content_pages }}
+    {{#controlplacementbottom}}
+        <div class="m-t-1">
+            {{#pagingbar}}
+                {{> core/paged_content_paging_bar }}
+            {{/pagingbar}}
+            {{#pagingdropdown}}
+                {{> core/paged_content_paging_dropdown }}
+            {{/pagingdropdown}}
+        </div>
+    {{/controlplacementbottom}}
 </div>
 {{^skipjs}}
 {{#js}}
 require(
 [
     'jquery',
-    'core/paged_content_pages'
+    'core/paged_content'
 ],
 function(
     $,
     PagedContent
 ) {
     var container = $("#paged-content-container-{{uniqid}}");
-    var pagingContent = container.find(PagedContent.rootSelector);
-
-    PagedContent.init(pagingContent, container);
+    PagedContent.init(container);
 });
 {{/js}}
 {{/skipjs}}
index a93b337..bd53214 100644 (file)
         ]
     }
 }}
-<div id="{{$pagingcontentid}}page-container-{{uniqid}}{{/pagingcontentid}}" data-region="page-container">
+<div
+    id="{{$pagingcontentid}}page-container-{{uniqid}}{{/pagingcontentid}}"
+    style="min-height: 50px"
+    data-region="page-container"
+    aria-live="polite"
+>
     {{#pages}}
         {{$paged-content-page}}
             {{> core/paged_content_page }}
index 1147f6b..15fe223 100644 (file)
@@ -27,6 +27,7 @@
         "next": true,
         "first": true,
         "last": true,
+        "activepagenumber": 1,
         "pages": [
             {
                 "url": "#",
         ]
     }
 }}
-<nav aria-label="{{label}}"
-     id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
-     data-region="paging-bar"
-     data-items-per-page="{{itemsperpage}}">
+<div
+    data-region="paging-control-container"
+    class="d-flex"
+>
+    {{#showitemsperpageselector}}
+        <div
+            id="paging-control-limit-container-{{uniqid}}"
+            data-region="paging-control-limit-container"
+            class="d-inline-flex align-items-center"
+        >
+            <span class="mr-1">{{#str}} show {{/str}}</span>
+            <div class="btn-group">
+                <button
+                    type="button"
+                    class="btn btn-outline-secondary dropdown-toggle"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="false"
+                    data-action="limit-toggle"
+                    {{#arialabels.itemsperpage}}
+                        aria-label="{{.}}"
+                    {{/arialabels.itemsperpage}}
+                    {{^arialabels.itemsperpage}}
+                        aria-label="{{#str}} pagedcontentpagingbaritemsperpage, core, {{#itemsperpage}}{{#active}}{{value}}{{/active}}{{/itemsperpage}}{{/str}}"
+                    {{/arialabels.itemsperpage}}
+                >
+                    {{#itemsperpage}}
+                        {{#active}}
+                            {{value}}
+                        {{/active}}
+                    {{/itemsperpage}}
+                </button>
+                <div
+                    role="menu"
+                    class="dropdown-menu"
+                    data-show-active-item
+                    {{#arialabels.itemsperpagecomponents}}
+                        data-active-item-button-aria-label-components="{{.}}"
+                    {{/arialabels.itemsperpagecomponents}}
+                    {{^arialabels.itemsperpagecomponents}}
+                        data-active-item-button-aria-label-components="pagedcontentpagingbaritemsperpage, core"
+                    {{/arialabels.itemsperpagecomponents}}
+                >
+                    {{#itemsperpage}}
+                        <a
+                            class="dropdown-item {{#active}}active{{/active}}"
+                            href="#"
+                            data-limit={{value}}
+                            {{#active}}aria-current="true"{{/active}}
+                        >
+                            {{#value}}{{.}}{{/value}}
+                            {{^value}}{{#str}} all, core {{/str}}{{/value}}
+                        </a>
+                    {{/itemsperpage}}
+                </div>
+            </div>
+        </div>
+    {{/showitemsperpageselector}}
 
-    <ul class="pagination">
-        {{#previous}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&laquo;</span>
-                    <span class="sr-only">{{#str}}previous{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="previous"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/previous}}
-        {{#first}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">{{#str}}first{{/str}}</span>
-                    <span class="sr-only">{{#str}}first{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="first"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/first}}
-        {{#pages}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$attributes}}data-page="true"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/pages}}
-        {{#last}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">{{#str}}last{{/str}}</span>
-                    <span class="sr-only">{{#str}}last{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="last"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/last}}
-        {{#next}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&raquo;</span>
-                    <span class="sr-only">{{#str}}next{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="next"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/next}}
-    </ul>
-</nav>
-{{#js}}
-require(['jquery', 'core/paged_content_paging_bar'], function($, PagingControl) {
-    var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}');
-    PagingControl.init(root);
-});
-{{/js}}
+    <nav
+        role="navigation"
+        id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
+        class="{{#showitemsperpageselector}}ml-auto{{/showitemsperpageselector}}"
+        data-region="paging-bar"
+        data-ignore-control-while-loading="{{ignorecontrolwhileloading}}"
+        data-hide-control-on-single-page="{{hidecontrolonsinglepage}}"
+        {{#activepagenumber}}
+            data-active-page-number="{{.}}"
+        {{/activepagenumber}}
+        {{^activepagenumber}}
+            data-active-page-number="1"
+        {{/activepagenumber}}
+        {{#showitemsperpageselector}}
+            {{#itemsperpage}}
+                {{#active}}
+                    data-items-per-page="{{value}}"
+                {{/active}}
+            {{/itemsperpage}}
+        {{/showitemsperpageselector}}
+        {{^showitemsperpageselector}}
+            data-items-per-page="{{itemsperpage}}"
+        {{/showitemsperpageselector}}
+        {{#arialabels.paginationnav}}
+            aria-label="{{.}}"
+        {{/arialabels.paginationnav}}
+        {{^arialabels.paginationnav}}
+            aria-label="{{#str}} pagedcontentnavigation, core {{/str}}"
+        {{/arialabels.paginationnav}}
+        {{#arialabels.paginationnavitemcomponents}}
+            data-aria-label-components-pagination-item="{{.}}"
+        {{/arialabels.paginationnavitemcomponents}}
+        {{^arialabels.paginationnavitemcomponents}}
+            data-aria-label-components-pagination-item="pagedcontentnavigationitem, core"
+        {{/arialabels.paginationnavitemcomponents}}
+        {{#arialabels.paginationactivenavitemcomponents}}
+            data-aria-label-components-pagination-active-item="{{.}}"
+        {{/arialabels.paginationactivenavitemcomponents}}
+        {{^arialabels.paginationactivenavitemcomponents}}
+            data-aria-label-components-pagination-active-item="pagedcontentnavigationactiveitem, core"
+        {{/arialabels.paginationactivenavitemcomponents}}
+    >
+
+        <ul class="pagination mb-0">
+            {{#previous}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span class="icon-no-margin dir-rtl-hide" aria-hidden="true">{{#pix}} i/previous, core {{/pix}}</span>
+                        <span class="icon-no-margin dir-ltr-hide" aria-hidden="true">{{#pix}} i/next, core {{/pix}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="previous"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/previous}}
+            {{#first}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span aria-hidden="true">{{#str}}first{{/str}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="first"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/first}}
+            {{#pages}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$attributes}}data-page="true"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/pages}}
+            {{#last}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span aria-hidden="true">{{#str}}last{{/str}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="last"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/last}}
+            {{#next}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span class="icon-no-margin dir-rtl-hide" aria-hidden="true">{{#pix}} i/next, core {{/pix}}</span>
+                        <span class="icon-no-margin dir-ltr-hide" aria-hidden="true">{{#pix}} i/previous, core {{/pix}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="next"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/next}}
+        </ul>
+    </nav>
+</div>
index d065d39..47cb9b3 100644 (file)
@@ -44,8 +44,8 @@
     data-region="paging-dropdown-container">
 
     <button class="btn btn-secondary dropdown-toggle"
+        id="paging-dropdown-menu-button-{{uniqid}}"
         type="button"
-        id="dropdown-menu-button-{{uniqid}}"
         data-region="dropdown-toggle"
         data-toggle="dropdown"
         aria-haspopup="true"
@@ -57,7 +57,7 @@
             {{/active}}
         {{/options}}
     </button>
-    <div class="dropdown-menu" aria-labelledby="dropdown-menu-button-{{uniqid}}">
+    <div class="dropdown-menu" aria-labelledby="paging-dropdown-menu-button-{{uniqid}}">
         {{#options}}
             {{> core/paged_content_paging_dropdown_item }}
         {{/options}}
@@ -67,9 +67,3 @@
         {{/ core/paged_content_paging_dropdown_item }}
     </div>
 </div>
-{{#js}}
-require(['jquery', 'core/paged_content_paging_dropdown'], function($, PagingControl) {
-    var root = $('#paging-dropdown-{{uniqid}}');
-    PagingControl.init(root);
-});
-{{/js}}
index b4bfd4f..12c64bb 100644 (file)
@@ -27,6 +27,7 @@
         "next": true,
         "first": true,
         "last": true,
+        "activepagenumber": 1,
         "pages": [
             {
                 "url": "#",
         ]
     }
 }}
-<div aria-label="{{label}}"
-     class="pagination"
-     id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
-     data-region="paging-bar"
-     data-items-per-page="{{itemsperpage}}">
+<div data-region="paging-control-container">
+    {{#showitemsperpageselector}}
+        <div
+            id="paging-control-limit-container-{{uniqid}}"
+            data-region="paging-control-limit-container"
+            class="pull-left"
+        >
+            <span class="mr-1">{{#str}} show {{/str}}</span>
+            <div class="btn-group">
+                <button
+                    type="button"
+                    class="btn btn-outline-secondary dropdown-toggle"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="false"
+                    data-action="limit-toggle"
+                    {{#arialabels.itemsperpage}}
+                        aria-label="{{.}}"
+                    {{/arialabels.itemsperpage}}
+                    {{^arialabels.itemsperpage}}
+                        aria-label="{{#str}} pagedcontentpagingbaritemsperpage, core, {{#itemsperpage}}{{#active}}{{value}}{{/active}}{{/itemsperpage}}{{/str}}"
+                    {{/arialabels.itemsperpage}}
+                >
+                    {{#itemsperpage}}
+                        {{#active}}
+                            <span data-active-item-text>{{value}}</span>
+                        {{/active}}
+                    {{/itemsperpage}}
+                    <span data-region="caret" class="caret"></span>
+                </button>
+                <ul
+                    role="menu"
+                    class="dropdown-menu"
+                    data-show-active-item
+                    {{#arialabels.itemsperpagecomponents}}
+                        data-active-item-button-aria-label-components="{{.}}"
+                    {{/arialabels.itemsperpagecomponents}}
+                    {{^arialabels.itemsperpagecomponents}}
+                        data-active-item-button-aria-label-components="pagedcontentpagingbaritemsperpage, core"
+                    {{/arialabels.itemsperpagecomponents}}
+                >
+                    {{#itemsperpage}}
+                        <li class="dropdown-item {{#active}}active{{/active}}" data-limit={{value}}>
+                            <a href="#">
+                                {{#value}}{{.}}{{/value}}
+                                {{^value}}{{#str}} all, core {{/str}}{{/value}}
+                            </a>
+                        </li>
+                    {{/itemsperpage}}
+                </ul>
+            </div>
+        </div>
+    {{/showitemsperpageselector}}
 
-    <ul class="pagination">
-        {{#previous}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&laquo;</span>
-                    <span class="sr-only">{{#str}}previous{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="previous"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/previous}}
-        {{#first}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">{{#str}}first{{/str}}</span>
-                    <span class="sr-only">{{#str}}first{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="first"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/first}}
-        {{#pages}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$attributes}}data-page="true"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/pages}}
-        {{#last}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">{{#str}}last{{/str}}</span>
-                    <span class="sr-only">{{#str}}last{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="last"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/last}}
-        {{#next}}
-            {{< core/paged_content_paging_bar_item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&raquo;</span>
-                    <span class="sr-only">{{#str}}next{{/str}}</span>
-                {{/item-content}}
-                {{$attributes}}data-control="next"{{/attributes}}
-            {{/ core/paged_content_paging_bar_item }}
-        {{/next}}
-    </ul>
+    <div
+        aria-label="{{label}}"
+        id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
+        class="m-0 pagination {{#showitemsperpageselector}}pagination-right{{/showitemsperpageselector}}"
+        data-region="paging-bar"
+        data-ignore-control-while-loading="{{ignorecontrolwhileloading}}"
+        data-hide-control-on-single-page="{{hidecontrolonsinglepage}}"
+        {{#activepagenumber}}
+            data-active-page-number="{{.}}"
+        {{/activepagenumber}}
+        {{^activepagenumber}}
+            data-active-page-number="1"
+        {{/activepagenumber}}
+        {{#showitemsperpageselector}}
+            {{#itemsperpage}}
+                {{#active}}
+                    data-items-per-page="{{value}}"
+                {{/active}}
+            {{/itemsperpage}}
+        {{/showitemsperpageselector}}
+        {{^showitemsperpageselector}}
+            data-items-per-page="{{itemsperpage}}"
+        {{/showitemsperpageselector}}
+        {{#arialabels.paginationnav}}
+            aria-label="{{.}}"
+        {{/arialabels.paginationnav}}
+        {{^arialabels.paginationnav}}
+            aria-label="{{#str}} pagedcontentnavigation, core {{/str}}"
+        {{/arialabels.paginationnav}}
+        {{#arialabels.paginationnavitemcomponents}}
+            data-aria-label-components-pagination-item="{{.}}"
+        {{/arialabels.paginationnavitemcomponents}}
+        {{^arialabels.paginationnavitemcomponents}}
+            data-aria-label-components-pagination-item="pagedcontentnavigationitem, core"
+        {{/arialabels.paginationnavitemcomponents}}
+        {{#arialabels.paginationactivenavitemcomponents}}
+            data-aria-label-components-pagination-active-item="{{.}}"
+        {{/arialabels.paginationactivenavitemcomponents}}
+        {{^arialabels.paginationactivenavitemcomponents}}
+            data-aria-label-components-pagination-active-item="pagedcontentnavigationactiveitem, core"
+        {{/arialabels.paginationactivenavitemcomponents}}
+    >
+
+        <ul class="mb-0">
+            {{#previous}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span aria-hidden="true">&laquo;</span>
+                        <span class="sr-only">{{#str}}previous{{/str}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="previous"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/previous}}
+            {{#first}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span aria-hidden="true">{{#str}}first{{/str}}</span>
+                        <span class="sr-only">{{#str}}first{{/str}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="first"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/first}}
+            {{#pages}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$attributes}}data-page="true"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/pages}}
+            {{#last}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span aria-hidden="true">{{#str}}last{{/str}}</span>
+                        <span class="sr-only">{{#str}}last{{/str}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="last"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/last}}
+            {{#next}}
+                {{< core/paged_content_paging_bar_item }}
+                    {{$item-content}}
+                        <span aria-hidden="true">&raquo;</span>
+                        <span class="sr-only">{{#str}}next{{/str}}</span>
+                    {{/item-content}}
+                    {{$attributes}}data-control="next"{{/attributes}}
+                {{/ core/paged_content_paging_bar_item }}
+            {{/next}}
+        </ul>
+    </div>
 </div>
-{{#js}}
-require(['jquery', 'core/paged_content_paging_bar'], function($, PagingControl) {
-    var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}');
-    PagingControl.init(root);
-});
-{{/js}}
index de8517b..c9ef675 100644 (file)
@@ -68,9 +68,3 @@
         {{/ core/paged_content_paging_dropdown_item }}
     </ul>
 </div>
-{{#js}}
-require(['jquery', 'core/paged_content_paging_dropdown'], function($, PagingControl) {
-    var root = $('#paging-dropdown-{{uniqid}}');
-    PagingControl.init(root);
-});
-{{/js}}