diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 1680fc196bf..eb72e18b41f 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -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 index 00000000000..860f7551723 --- /dev/null +++ b/lib/amd/build/paged_content.min.js @@ -0,0 +1 @@ +define(["jquery","core/paged_content_pages","core/paged_content_paging_bar","core/paged_content_paging_bar_limit_selector","core/paged_content_paging_dropdown"],function(a,b,c,d,e){var f=function(f,g){f=a(f);var h=f.find(b.rootSelector),i=f.find(c.rootSelector),j=f.find(e.rootSelector),k=f.find(d.rootSelector),l=f.attr("id");b.init(h,l,g),i.length&&c.init(i,l),k.length&&d.init(k,l),j.length&&e.init(j,l)};return{init:f,rootSelector:'[data-region="paged-content-container"]'}}); \ No newline at end of file diff --git a/lib/amd/build/paged_content_events.min.js b/lib/amd/build/paged_content_events.min.js index 27e1fa81414..7cd9b320b0c 100644 --- a/lib/amd/build/paged_content_events.min.js +++ b/lib/amd/build/paged_content_events.min.js @@ -1 +1 @@ -define([],function(){return{SHOW_PAGES:"core-paged-content-show-pages"}}); \ No newline at end of file +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"}}); \ No newline at end of file diff --git a/lib/amd/build/paged_content_factory.min.js b/lib/amd/build/paged_content_factory.min.js index c0fd46e8fbc..62c3166e4ce 100644 --- a/lib/amd/build/paged_content_factory.min.js +++ b/lib/amd/build/paged_content_factory.min.js @@ -1 +1 @@ -define(["jquery","core/templates","core/notification","core/paged_content_pages"],function(a,b,c,d){var e={PAGED_CONTENT:"core/paged_content"},f=function(a,b){for(var c={itemsperpage:b,previous:{},next:{},pages:[]},d=1;d<=a;d++){var e={number:d,page:""+d};1===d&&(e.active=!0),c.pages.push(e)}return c},g=function(a,b,c){var d={options:[]},e=0,f=0,g=a;c.hasOwnProperty("maxPages")&&(g=c.maxPages);for(var h=1;h<=g;h++){var i=0;h<=2?(i=b,f=b):(f=2*f,i=f),e+=i;var j={itemcount:i,content:e};1===h&&(j.active=!0),d.options.push(j)}return d},h=function(a,b,c){var d={pagingbar:!1,pagingdropdown:!1,skipjs:!0};return c.hasOwnProperty("dropdown")&&c.dropdown?d.pagingdropdown=g(a,b,c):d.pagingbar=f(a,b),d},i=function(a,b){var c=1;if(a>0){var d=a%b;d?(a-=d,c=a/b+1):c=a/b}return c},j=function(f,g,j,k){"undefined"==typeof k&&(k={});var l=a.Deferred(),m=i(f,g),n=h(m,g,k);return b.render(e.PAGED_CONTENT,n).then(function(b,c){b=a(b);var e=b,f=b.find(d.rootSelector);d.init(f,e,j),l.resolve(b,c)}).fail(function(a){l.reject(a)}).fail(c.exception),l},k=function(a,b,c,d){"undefined"==typeof d&&(d={});var e=a.length;return j(e,b,function(b){var d=[];return b.forEach(function(b){var c=b.offset,f=b.limit?c+b.limit:e,g=a.slice(c,f);d.push(g)}),c(d)},d)};return{createFromAjax:j,createFromStaticList:k}}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core/paged_content"],function(a,b,c,d){var e={PAGED_CONTENT:"core/paged_content"},f={ITEMS_PER_PAGE_SINGLE:25,ITEMS_PER_PAGE_ARRAY:[25,50,100,0],MAX_PAGES:3},g=function(){return{pagingbar:!1,pagingdropdown:!1,skipjs:!0,ignorecontrolwhileloading:!0,controlplacementbottom:!1}},h=function(){return{showitemsperpageselector:!1,itemsperpage:35,previous:!0,next:!0,activepagenumber:1,hidecontrolonsinglepage:!0,pages:[]}},i=function(a,b){var c=1;if(a>0){var d=a%b;d?(a-=d,c=a/b+1):c=a/b}return c},j=function(b,c){null===c&&(c=f.ITEMS_PER_PAGE_SINGLE),a.isArray(c)&&(c=c[0]);var d=h();d.itemsperpage=c;for(var e=i(b,c),g=1;g<=e;g++){var j={number:g,page:""+g};1===g&&(j.active=!0),d.pages.push(j)}return d},k=function(b){if(a.isArray(b)){var c=b.map(function(a){return"number"==typeof a?{value:a,active:!1}:a}),d=c.filter(function(a){return a.active});return d.length||(c[0].active=!0),c}return b},l=function(b){null===b&&(b=f.ITEMS_PER_PAGE_ARRAY);var c=h();return c.itemsperpage=k(b),c.showitemsperpageselector=a.isArray(b),c},m=function(a,b){return a?j(a,b):l(b)},n=function(b,c){if(null===b&&(b=f.ITEMS_PER_PAGE_SINGLE),a.isArray(b))return{options:b};var d={options:[]},e=0,g=0,h=f.MAX_PAGES;c.hasOwnProperty("maxPages")&&(h=c.maxPages);for(var i=1;i<=h;i++){var j=0;i<=2?(j=b,g=b):(g=2*g,j=g),e+=j;var k={itemcount:j,content:e};1===i&&(k.active=!0),d.options.push(k)}return d},o=function(a,b,c){var d=g();return c.hasOwnProperty("ignoreControlWhileLoading")&&(d.ignorecontrolwhileloading=c.ignoreControlWhileLoading),c.hasOwnProperty("controlPlacementBottom")&&(d.controlplacementbottom=c.controlPlacementBottom),c.hasOwnProperty("hideControlOnSinglePage")&&(d.hidecontrolonsinglepage=c.hideControlOnSinglePage),c.hasOwnProperty("ariaLabels")&&(d.arialabels=c.ariaLabels),c.hasOwnProperty("dropdown")&&c.dropdown?d.pagingdropdown=n(b,c):d.pagingbar=m(a,b),d},p=function(a,b){return r(null,null,a,b)},q=function(a,b,c){return r(null,a,b,c)},r=function(f,g,h,i){i=i||{};var j=a.Deferred(),k=o(f,g,i);return b.render(e.PAGED_CONTENT,k).then(function(b,c){b=a(b);var e=b;d.init(e,h),j.resolve(b,c)}).fail(function(a){j.reject(a)}).fail(c.exception),j.promise()},s=function(a,b,c,d){"undefined"==typeof d&&(d={});var e=a.length;return r(e,b,function(b){var d=[];return b.forEach(function(b){var c=b.offset,f=b.limit?c+b.limit:e,g=a.slice(c,f);d.push(g)}),c(d)},d)};return{create:p,createWithLimit:q,createWithTotalAndLimit:r,createFromStaticList:s,createFromAjax:r}}); \ No newline at end of file diff --git a/lib/amd/build/paged_content_pages.min.js b/lib/amd/build/paged_content_pages.min.js index b59f52d4601..ae2015fdea7 100644 --- a/lib/amd/build/paged_content_pages.min.js +++ b/lib/amd/build/paged_content_pages.min.js @@ -1 +1 @@ -define(["jquery","core/templates","core/notification","core/paged_content_events"],function(a,b,c,d){var e={ROOT:'[data-region="page-container"]',PAGE_REGION:'[data-region="paged-content-page"]',ACTIVE_PAGE_REGION:'[data-region="paged-content-page"].active'},f={PAGING_CONTENT_ITEM:"core/paged_content_page",LOADING:"core/overlay_loading"},g=function(a,b){return a.find('[data-page="'+b+'"]')},h=function(d){var e=a.Deferred();return b.render(f.LOADING,{visible:!0}).then(function(b){var c=a(b),f=setTimeout(function(){d.css("position","relative"),c.appendTo(d)},100);e.always(function(){clearTimeout(f),c.remove(),d.css("position","")})}).fail(c.exception),e},i=function(d,e,h){var i=a.Deferred();return e.then(function(a,e){b.render(f.PAGING_CONTENT_ITEM,{page:h,content:a}).then(function(a){b.appendNodeContents(d,a,e);var c=g(d,h);i.resolve(c)}).fail(function(a){i.reject(a)}).fail(c.exception)}).fail(function(a){i.reject(a)}).fail(c.exception),i},j=function(b,d,f){var j=[],k=[],l=a.Deferred();if(d.forEach(function(a){var c=a.pageNumber,d=g(b,c);d.length?j.push(d):k.push(a)}),k.length&&"function"==typeof f){var m=f(k),n=m.map(function(a,c){return i(b,a,k[c].pageNumber)});a.when.apply(a,n).then(function(){var a=Array.prototype.slice.call(arguments);l.resolve(a)}).fail(function(a){l.reject(a)}).fail(c.exception)}else l.resolve([]);var o=h(b);l.then(function(a){var c=j.concat(a);b.find(e.PAGE_REGION).addClass("hidden"),c.forEach(function(a){a.removeClass("hidden")})}).fail(c.exception).always(function(){o.resolve()})},k=function(b,c,e){b=a(b),c=a(c),c.on(d.SHOW_PAGES,function(a,c){j(b,c,e)})};return{init:k,rootSelector:e.ROOT}}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core/pubsub","core/paged_content_events"],function(a,b,c,d,e){var f={ROOT:'[data-region="page-container"]',PAGE_REGION:'[data-region="paged-content-page"]',ACTIVE_PAGE_REGION:'[data-region="paged-content-page"].active'},g={PAGING_CONTENT_ITEM:"core/paged_content_page",LOADING:"core/overlay_loading"},h=300,i=function(a,b){return a.find('[data-page="'+b+'"]')},j=function(d){var e=a.Deferred();return d.attr("aria-busy",!0),b.render(g.LOADING,{visible:!0}).then(function(b){var c=a(b),f=setTimeout(function(){d.css("position","relative"),c.appendTo(d)},h);e.always(function(){clearTimeout(f),c.remove(),d.css("position",""),d.removeAttr("aria-busy")})}).fail(c.exception),e},k=function(d,e,f){var h=a.Deferred();return e.then(function(a,e){e=e||"",b.render(g.PAGING_CONTENT_ITEM,{page:f,content:a}).then(function(a){b.appendNodeContents(d,a,e);var c=i(d,f);h.resolve(c)}).fail(function(a){h.reject(a)}).fail(c.exception)}).fail(function(a){h.reject(a)}).fail(c.exception),h.promise()},l=function(b,g,h,l){var m=[],n=[],o=a.Deferred();if(g.forEach(function(a){var c=a.pageNumber,d=i(b,c);d.length?m.push(d):n.push(a)}),n.length&&"function"==typeof l){var p=l(n,{allItemsLoaded:function(a){d.publish(h+e.ALL_ITEMS_LOADED,a)}}),q=p.map(function(a,c){return k(b,a,n[c].pageNumber)});a.when.apply(a,q).then(function(){var a=Array.prototype.slice.call(arguments);o.resolve(a)}).fail(function(a){o.reject(a)}).fail(c.exception)}else o.resolve([]);var r=j(b);o.then(function(a){var c=m.concat(a);b.find(f.PAGE_REGION).addClass("hidden"),c.forEach(function(a){a.removeClass("hidden")})}).then(function(){d.publish(h+e.PAGES_SHOWN,g)}).fail(c.exception).always(function(){r.resolve()})},m=function(b,c,f){b=a(b),d.subscribe(c+e.SHOW_PAGES,function(a){l(b,a,c,f)}),d.subscribe(c+e.SET_ITEMS_PER_PAGE_LIMIT,function(){b.empty()})};return{init:m,rootSelector:f.ROOT}}); \ No newline at end of file diff --git a/lib/amd/build/paged_content_paging_bar.min.js b/lib/amd/build/paged_content_paging_bar.min.js index fcf975938cc..0c5dc70e8f4 100644 --- a/lib/amd/build/paged_content_paging_bar.min.js +++ b/lib/amd/build/paged_content_paging_bar.min.js @@ -1 +1 @@ -define(["jquery","core/custom_interaction_events","core/paged_content_events"],function(a,b,c){var d={ROOT:'[data-region="paging-bar"]',PAGE:"[data-page]",PAGE_ITEM:'[data-region="page-item"]',ACTIVE_PAGE_ITEM:'[data-region="page-item"].active'},e=function(a,b){return a.find(d.PAGE_ITEM+'[data-page-number="'+b+'"]')},f=function(a){var b=a.find(d.PAGE).last();return b?parseInt(b.attr("data-page-number"),10):null},g=function(a){var b=a.find(d.ACTIVE_PAGE_ITEM);return b.length?h(a,b):null},h=function(a,b){if(void 0!=b.attr("data-page"))return parseInt(b.attr("data-page-number"),10);var c=1,d=null;switch(b.attr("data-control")){case"first":c=1;break;case"last":c=f(a);break;case"next":d=g(a);var e=f(a);c=d&&d1?d-1:1;break;default:c=1}return parseInt(c,10)},i=function(a){return parseInt(a.attr("data-items-per-page"),10)},j=function(b){b.each(function(b,c){c=a(c),c.attr("data-page-number",b+1)})},k=function(a,b){var f=b==g(a),h=i(a),j=(b-1)*h;if(!f){a.find(d.PAGE_ITEM).removeClass("active");var k=e(a,b);k.addClass("active")}a.trigger(c.SHOW_PAGES,[[{pageNumber:b,limit:h,offset:j}]])},l=function(c){c=a(c);var e=c.find(d.PAGE);j(e);var f=g(c);f&&k(c,f),b.define(c,[b.events.activate]),c.on(b.events.activate,d.PAGE_ITEM,function(b,e){var f=a(b.target).closest(d.PAGE_ITEM),g=h(c,f);k(c,g),e.originalEvent.preventDefault(),e.originalEvent.stopPropagation()})};return{init:l,rootSelector:d.ROOT}}); \ No newline at end of file +define(["jquery","core/custom_interaction_events","core/paged_content_events","core/str","core/pubsub"],function(a,b,c,d,e){var f={ROOT:'[data-region="paging-bar"]',PAGE:"[data-page]",PAGE_ITEM:'[data-region="page-item"]',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"]'},g=function(a,b){return a.find(f.PAGE_ITEM+'[data-page-number="'+b+'"]')},h=function(a){return a.find(f.NEXT_BUTTON)},i=function(a,b){a.attr("data-last-page-number",b)},j=function(a){return parseInt(a.attr("data-last-page-number"),10)},k=function(a){return parseInt(a.attr("data-active-page-number"),10)},l=function(a,b){a.attr("data-active-page-number",b)},m=function(a){var b=k(a);return!isNaN(b)&&0!=b},n=function(a,b){if(void 0!=b.attr("data-page"))return parseInt(b.attr("data-page-number"),10);var c=1,d=null;switch(b.attr("data-control")){case"first":c=1;break;case"last":c=j(a);break;case"next":d=k(a);var e=j(a);c=e?d&&d1?d-1:1;break;default:c=1}return parseInt(c,10)},o=function(a){return parseInt(a.attr("data-items-per-page"),10)},p=function(a,b){a.attr("data-items-per-page",b)},q=function(a){a.removeClass("hidden")},r=function(a){a.addClass("hidden")},s=function(a){var b=a.find(f.NEXT_BUTTON),c=a.find(f.LAST_BUTTON);b.addClass("disabled"),b.attr("aria-disabled",!0),c.addClass("disabled"),c.attr("aria-disabled",!0)},t=function(a){var b=a.find(f.NEXT_BUTTON),c=a.find(f.LAST_BUTTON);b.removeClass("disabled"),b.removeAttr("aria-disabled"),c.removeClass("disabled"),c.removeAttr("aria-disabled")},u=function(a){var b=a.find(f.PREVIOUS_BUTTON),c=a.find(f.FIRST_BUTTON);b.addClass("disabled"),b.attr("aria-disabled",!0),c.addClass("disabled"),c.attr("aria-disabled",!0)},v=function(a){var b=a.find(f.PREVIOUS_BUTTON),c=a.find(f.FIRST_BUTTON);b.removeClass("disabled"),b.removeAttr("aria-disabled"),c.removeClass("disabled"),c.removeAttr("aria-disabled")},w=function(a){var b=a.attr("data-aria-label-components-pagination-item"),c=b.split(",").map(function(a){return a.trim()});return c},x=function(a){var b=a.attr("data-aria-label-components-pagination-active-item"),c=b.split(",").map(function(a){return a.trim()});return c},y=function(b,c){var d=0;l(b,0),c.each(function(c,e){var f=c+1;e=a(e),e.attr("data-page-number",f),d++,e.hasClass("active")&&l(b,f)}),i(b,d)},z=function(b){var c=w(b),e=x(b),g=k(b),h=b.find(f.PAGE_ITEM),i=h.map(function(d,f){f=a(f);var h=n(b,f);return h===g?{key:e[0],component:e[1],param:h}:{key:c[0],component:c[1],param:h}});d.get_strings(i).then(function(b){return h.each(function(c,d){d=a(d);var e=b[c];d.attr("aria-label",e),d.find(f.PAGE_LINK).attr("aria-label",e)}),b})["catch"](function(){})},A=function(a,b,d){var h=j(a),i=b==k(a),m=o(a),n=(b-1)*m;if(!i){a.find(f.PAGE_ITEM).removeClass("active").removeAttr("aria-current");var p=g(a,b);p.addClass("active"),p.attr("aria-current",!0),l(a,b)}h&&b>=h?s(a):t(a),b>1?v(a):u(a),z(a),e.publish(d+c.SHOW_PAGES,[{pageNumber:b,limit:m,offset:n}])},B=function(d,g){var h=d.attr("data-ignore-control-while-loading"),k=!1;""==h&&(h=!0),b.define(d,[b.events.activate]),d.on(b.events.activate,f.PAGE_ITEM,function(b,c){if(c.originalEvent.preventDefault(),c.originalEvent.stopPropagation(),!h||!k){var e=a(b.target).closest(f.PAGE_ITEM);if(!e.hasClass("disabled")){var i=n(d,e);A(d,i,g),k=!0}}}),e.subscribe(g+c.ALL_ITEMS_LOADED,function(a){k=!1;var b=j(d);(!b||a. + +/** + * Javascript to load and render a paged content section. + * + * @module core/paged_content + * @copyright 2018 Ryan Wyllie + * @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"]' + }; +}); diff --git a/lib/amd/src/paged_content_events.js b/lib/amd/src/paged_content_events.js index 1181cb1657b..b7a0b1e3475 100644 --- a/lib/amd/src/paged_content_events.js +++ b/lib/amd/src/paged_content_events.js @@ -14,14 +14,17 @@ // along with Moodle. If not, see . /** - * 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 * @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' }; }); diff --git a/lib/amd/src/paged_content_factory.js b/lib/amd/src/paged_content_factory.js index 709dd8ebaa1..696eeeb5180 100644 --- a/lib/amd/src/paged_content_factory.js +++ b/lib/amd/src/paged_content_factory.js @@ -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,52 +308,39 @@ 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. - * - * @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. - */ - 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; - }; - /** * 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). @@ -198,30 +353,78 @@ 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 {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 create = function(renderPagesContentCallback, config) { + return createWithTotalAndLimit(null, null, renderPagesContentCallback, config); + }; + + /** + * 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); + }; + + /** + * 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|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 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 }; }); diff --git a/lib/amd/src/paged_content_pages.js b/lib/amd/src/paged_content_pages.js index 4fbbb104622..e76069ac6e8 100644 --- a/lib/amd/src/paged_content_pages.js +++ b/lib/amd/src/paged_content_pages.js @@ -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(); }); }; diff --git a/lib/amd/src/paged_content_paging_bar.js b/lib/amd/src/paged_content_paging_bar.js index c1d26b4993c..456ee39410d 100644 --- a/lib/amd/src/paged_content_paging_bar.js +++ b/lib/amd/src/paged_content_paging_bar.js @@ -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); - - if (activePage.length) { - return getPageNumber(root, activePage); - } else { - return null; - } + return parseInt(root.attr('data-active-page-number'), 10); }; /** - * Get the page number. + * Set the active page number. * * @param {object} root The root element. - * @param {object} page The page. - * @returns {int} The page number + * @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 for a given page. + * + * @param {object} root The root element. + * @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 index 00000000000..ebc327e767c --- /dev/null +++ b/lib/amd/src/paged_content_paging_bar_limit_selector.js @@ -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 . + +/** + * Javascript for dynamically changing the page limits. + * + * @module core/paged_content_paging_bar_limit_selector + * @copyright 2018 Ryan Wyllie + * @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 + }; +}); diff --git a/lib/amd/src/paged_content_paging_dropdown.js b/lib/amd/src/paged_content_paging_dropdown.js index 59d75b59cb1..5fe58707bc2 100644 --- a/lib/amd/src/paged_content_paging_dropdown.js +++ b/lib/amd/src/paged_content_paging_dropdown.js @@ -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(); }); diff --git a/lib/templates/paged_content.mustache b/lib/templates/paged_content.mustache index 47b5fbb4f5a..953cbccc3e9 100644 --- a/lib/templates/paged_content.mustache +++ b/lib/templates/paged_content.mustache @@ -53,29 +53,41 @@ } }}
- {{#pagingbar}} - {{> core/paged_content_paging_bar }} - {{/pagingbar}} - {{#pagingdropdown}} - {{> core/paged_content_paging_dropdown }} - {{/pagingdropdown}} + {{^controlplacementbottom}} +
+ {{#pagingbar}} + {{> core/paged_content_paging_bar }} + {{/pagingbar}} + {{#pagingdropdown}} + {{> core/paged_content_paging_dropdown }} + {{/pagingdropdown}} +
+ {{/controlplacementbottom}} {{> core/paged_content_pages }} + {{#controlplacementbottom}} +
+ {{#pagingbar}} + {{> core/paged_content_paging_bar }} + {{/pagingbar}} + {{#pagingdropdown}} + {{> core/paged_content_paging_dropdown }} + {{/pagingdropdown}} +
+ {{/controlplacementbottom}}
{{^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}} diff --git a/lib/templates/paged_content_pages.mustache b/lib/templates/paged_content_pages.mustache index a93b33757b0..bd53214d2a8 100644 --- a/lib/templates/paged_content_pages.mustache +++ b/lib/templates/paged_content_pages.mustache @@ -35,7 +35,12 @@ ] } }} -
+
{{#pages}} {{$paged-content-page}} {{> core/paged_content_page }} diff --git a/lib/templates/paged_content_paging_bar.mustache b/lib/templates/paged_content_paging_bar.mustache index 1147f6b691f..15fe2234938 100644 --- a/lib/templates/paged_content_paging_bar.mustache +++ b/lib/templates/paged_content_paging_bar.mustache @@ -27,6 +27,7 @@ "next": true, "first": true, "last": true, + "activepagenumber": 1, "pages": [ { "url": "#", @@ -40,58 +41,148 @@ ] } }} - -{{#js}} -require(['jquery', 'core/paged_content_paging_bar'], function($, PagingControl) { - var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}'); - PagingControl.init(root); -}); -{{/js}} + +
diff --git a/lib/templates/paged_content_paging_dropdown.mustache b/lib/templates/paged_content_paging_dropdown.mustache index d065d39745e..47cb9b3237f 100644 --- a/lib/templates/paged_content_paging_dropdown.mustache +++ b/lib/templates/paged_content_paging_dropdown.mustache @@ -44,8 +44,8 @@ data-region="paging-dropdown-container"> + +
+ + {{/showitemsperpageselector}} -
    - {{#previous}} - {{< core/paged_content_paging_bar_item }} - {{$item-content}} - - {{#str}}previous{{/str}} - {{/item-content}} - {{$attributes}}data-control="previous"{{/attributes}} - {{/ core/paged_content_paging_bar_item }} - {{/previous}} - {{#first}} - {{< core/paged_content_paging_bar_item }} - {{$item-content}} - - {{#str}}first{{/str}} - {{/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}} - - {{#str}}last{{/str}} - {{/item-content}} - {{$attributes}}data-control="last"{{/attributes}} - {{/ core/paged_content_paging_bar_item }} - {{/last}} - {{#next}} - {{< core/paged_content_paging_bar_item }} - {{$item-content}} - - {{#str}}next{{/str}} - {{/item-content}} - {{$attributes}}data-control="next"{{/attributes}} - {{/ core/paged_content_paging_bar_item }} - {{/next}} -
+ -{{#js}} -require(['jquery', 'core/paged_content_paging_bar'], function($, PagingControl) { - var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}'); - PagingControl.init(root); -}); -{{/js}} diff --git a/theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache b/theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache index de8517b248d..c9ef6759a81 100644 --- a/theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache +++ b/theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache @@ -68,9 +68,3 @@ {{/ core/paged_content_paging_dropdown_item }} -{{#js}} -require(['jquery', 'core/paged_content_paging_dropdown'], function($, PagingControl) { - var root = $('#paging-dropdown-{{uniqid}}'); - PagingControl.init(root); -}); -{{/js}}