Merge branch 'MDL-61138-master-require-context-id' of git://github.com/ryanwyllie/moodle
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/key_codes"],function(a,b){var c={activate:"cie:activate",keyboardActivate:"cie:keyboardactivate",escape:"cie:escape",down:"cie:down",up:"cie:up",home:"cie:home",end:"cie:end",next:"cie:next",previous:"cie:previous",asterix:"cie:asterix",scrollLock:"cie:scrollLock",scrollTop:"cie:scrollTop",scrollBottom:"cie:scrollBottom",ctrlPageUp:"cie:ctrlPageUp",ctrlPageDown:"cie:ctrlPageDown",enter:"cie:enter"},d=function(a,b){return b=b||[],!(!b.length||b.indexOf(a)===-1)},e=function(a){return a.shiftKey||a.metaKey||a.altKey||a.ctrlKey},f=function(b,c,d){b.off("keydown."+c).on("keydown."+c,function(b){e(b)||b.keyCode==d&&a(b.target).trigger(c,[{originalEvent:b}])})},g=function(d){d.off("click.cie.activate").on("click.cie.activate",function(b){a(b.target).trigger(c.activate,[{originalEvent:b}])}),d.off("keydown.cie.activate").on("keydown.cie.activate",function(d){e(d)||d.keyCode!=b.enter&&d.keyCode!=b.space||a(d.target).trigger(c.activate,[{originalEvent:d}])})},h=function(d){d.off("keydown.cie.keyboardactivate").on("keydown.cie.keyboardactivate",function(d){e(d)||d.keyCode!=b.enter&&d.keyCode!=b.space||a(d.target).trigger(c.keyboardActivate,[{originalEvent:d}])})},i=function(a){f(a,c.escape,b.escape)},j=function(a){f(a,c.down,b.arrowDown)},k=function(a){f(a,c.up,b.arrowUp)},l=function(a){f(a,c.home,b.home)},m=function(a){f(a,c.end,b.end)},n=function(d){var e="rtl"==a("html").attr("dir")?b.arrowLeft:b.arrowRight;f(d,c.next,e)},o=function(d){var e="rtl"==a("html").attr("dir")?b.arrowRight:b.arrowLeft;f(d,c.previous,e)},p=function(a){f(a,c.asterix,b.asterix)},q=function(a){a.off("scroll.cie.scrollTop").on("scroll.cie.scrollTop",function(b){var d=a.scrollTop();0===d&&a.trigger(c.scrollTop,[{originalEvent:b}])})},r=function(a){a.off("scroll.cie.scrollBottom").on("scroll.cie.scrollBottom",function(b){var d=a.scrollTop(),e=a.innerHeight(),f=a[0].scrollHeight;d+e>=f&&a.trigger(c.scrollBottom,[{originalEvent:b}])})},s=function(a){a.off("DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock").on("DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock",function(b){var d=a.scrollTop(),e=a[0].scrollHeight,f=a.height(),g="DOMMouseScroll"==b.type?b.originalEvent.detail*-40:b.originalEvent.wheelDelta,h=g>0;return!h&&-g>e-f-d?(a.scrollTop(e),b.stopPropagation(),b.preventDefault(),b.returnValue=!1,a.trigger(c.scrollLock,[{originalEvent:b}]),!1):!(h&&g>d)||(a.scrollTop(0),b.stopPropagation(),b.preventDefault(),b.returnValue=!1,a.trigger(c.scrollLock,[{originalEvent:b}]),!1)})},t=function(d){d.off("keydown.cie.ctrlpageup").on("keydown.cie.ctrlpageup",function(d){d.ctrlKey&&d.keyCode==b.pageUp&&a(d.target).trigger(c.ctrlPageUp,[{originalEvent:d}])})},u=function(d){d.off("keydown.cie.ctrlpagedown").on("keydown.cie.ctrlpagedown",function(d){d.ctrlKey&&d.keyCode==b.pageDown&&a(d.target).trigger(c.ctrlPageDown,[{originalEvent:d}])})},v=function(a){f(a,c.enter,b.enter)},w=function(){var a={};return a[c.activate]=g,a[c.keyboardActivate]=h,a[c.escape]=i,a[c.down]=j,a[c.up]=k,a[c.home]=l,a[c.end]=m,a[c.next]=n,a[c.previous]=o,a[c.asterix]=p,a[c.scrollLock]=s,a[c.scrollTop]=q,a[c.scrollBottom]=r,a[c.ctrlPageUp]=t,a[c.ctrlPageDown]=u,a[c.enter]=v,a},x=function(b,c){b=a(b),c=c||[],b.length&&c.length&&a.each(w(),function(a,e){d(a,c)&&e(b)})};return{define:x,events:c}});
|
||||
define(["jquery","core/key_codes"],function(a,b){var c={activate:"cie:activate",keyboardActivate:"cie:keyboardactivate",escape:"cie:escape",down:"cie:down",up:"cie:up",home:"cie:home",end:"cie:end",next:"cie:next",previous:"cie:previous",asterix:"cie:asterix",scrollLock:"cie:scrollLock",scrollTop:"cie:scrollTop",scrollBottom:"cie:scrollBottom",ctrlPageUp:"cie:ctrlPageUp",ctrlPageDown:"cie:ctrlPageDown",enter:"cie:enter"},d={},e=function(a,b){return b=b||[],!(!b.length||b.indexOf(a)===-1)},f=function(a){return a.shiftKey||a.metaKey||a.altKey||a.ctrlKey},g=function(b,c){var e="";return c.hasOwnProperty("originalEvent")?(e="triggeredCustom_"+b,c.originalEvent.hasOwnProperty(e)?void 0:(c.originalEvent[e]=!0,void a(c.target).trigger(b,[{originalEvent:c}]))):(e=""+b+c.type+c.timeStamp,void(d.hasOwnProperty(e)||(d[e]=!0,a(c.target).trigger(b,[{originalEvent:c}]))))},h=function(a,b,c){a.off("keydown."+b).on("keydown."+b,function(a){f(a)||a.keyCode==c&&g(b,a)})},i=function(a){a.off("click.cie.activate").on("click.cie.activate",function(a){g(c.activate,a)}),a.off("keydown.cie.activate").on("keydown.cie.activate",function(a){f(a)||a.keyCode!=b.enter&&a.keyCode!=b.space||g(c.activate,a)})},j=function(a){a.off("keydown.cie.keyboardactivate").on("keydown.cie.keyboardactivate",function(a){f(a)||a.keyCode!=b.enter&&a.keyCode!=b.space||g(c.keyboardActivate,a)})},k=function(a){h(a,c.escape,b.escape)},l=function(a){h(a,c.down,b.arrowDown)},m=function(a){h(a,c.up,b.arrowUp)},n=function(a){h(a,c.home,b.home)},o=function(a){h(a,c.end,b.end)},p=function(d){var e="rtl"==a("html").attr("dir")?b.arrowLeft:b.arrowRight;h(d,c.next,e)},q=function(d){var e="rtl"==a("html").attr("dir")?b.arrowRight:b.arrowLeft;h(d,c.previous,e)},r=function(a){h(a,c.asterix,b.asterix)},s=function(a){a.off("scroll.cie.scrollTop").on("scroll.cie.scrollTop",function(b){var d=a.scrollTop();0===d&&g(c.scrollTop,b)})},t=function(a){a.off("scroll.cie.scrollBottom").on("scroll.cie.scrollBottom",function(b){var d=a.scrollTop(),e=a.innerHeight(),f=a[0].scrollHeight;d+e>=f&&g(c.scrollBottom,b)})},u=function(a){a.off("DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock").on("DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock",function(b){var d=a.scrollTop(),e=a[0].scrollHeight,f=a.height(),h="DOMMouseScroll"==b.type?b.originalEvent.detail*-40:b.originalEvent.wheelDelta,i=h>0;return!i&&-h>e-f-d?(a.scrollTop(e),b.stopPropagation(),b.preventDefault(),b.returnValue=!1,g(c.scrollLock,b),!1):!(i&&h>d)||(a.scrollTop(0),b.stopPropagation(),b.preventDefault(),b.returnValue=!1,g(c.scrollLock,b),!1)})},v=function(a){a.off("keydown.cie.ctrlpageup").on("keydown.cie.ctrlpageup",function(a){a.ctrlKey&&a.keyCode==b.pageUp&&g(c.ctrlPageUp,a)})},w=function(a){a.off("keydown.cie.ctrlpagedown").on("keydown.cie.ctrlpagedown",function(a){a.ctrlKey&&a.keyCode==b.pageDown&&g(c.ctrlPageDown,a)})},x=function(a){h(a,c.enter,b.enter)},y=function(){var a={};return a[c.activate]=i,a[c.keyboardActivate]=j,a[c.escape]=k,a[c.down]=l,a[c.up]=m,a[c.home]=n,a[c.end]=o,a[c.next]=p,a[c.previous]=q,a[c.asterix]=r,a[c.scrollLock]=u,a[c.scrollTop]=s,a[c.scrollBottom]=t,a[c.ctrlPageUp]=v,a[c.ctrlPageDown]=w,a[c.enter]=x,a},z=function(b,c){b=a(b),c=c||[],b.length&&c.length&&a.each(y(),function(a,d){e(a,c)&&d(b)})};return{define:z,events:c}});
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/modal_events","core/modal_registry","core/modal","core/modal_save_cancel","core/modal_confirm","core/modal_cancel","core/templates","core/notification","core/custom_interaction_events"],function(a,b,c,d,e,f,g,h,i,j){var k={DEFAULT:"core/modal",SAVE_CANCEL:"core/modal_save_cancel",CONFIRM:"core/modal_save_cancel",CANCEL:"core/modal_cancel"},l={DEFAULT:"DEFAULT",SAVE_CANCEL:"SAVE_CANCEL",CONFIRM:"CONFIRM",CANCEL:"CANCEL"};c.register(l.DEFAULT,d,k.DEFAULT),c.register(l.SAVE_CANCEL,e,k.SAVE_CANCEL),c.register(l.CONFIRM,f,k.CONFIRM),c.register(l.CANCEL,g,k.CANCEL);var m=function(a,c){if("undefined"!=typeof c){var d=null;if(Array.isArray(c)){var e=c[1];c=c[0],j.define(c,[j.events.activate]),c.on(j.events.activate,e,function(b,c){d=b.currentTarget,a.then(function(a){return a.show(),a}),c.originalEvent.preventDefault()})}else j.define(c,[j.events.activate]),c.on(j.events.activate,function(b,c){d=b.currentTarget,a.then(function(a){return a.show(),a}),c.originalEvent.preventDefault()});a.then(function(a){return a.getRoot().on(b.hidden,function(){null!==d&&d.focus()}),a})}},n=function(b,c){c=a(c);var d=b.module,e=new d(c);return e},o=function(b,c,d){var e=b.template,f=h.render(e,c).then(function(c){var d=a(c);return n(b,d)}).fail(i.exception);return m(f,d),f},p=function(a,b){var d=a.type||l.DEFAULT,e=!!a.large,f=null,g={};return f=c.get(d),f||i.exception({message:"Unable to find modal of type: "+d}),"undefined"!=typeof a.templateContext&&(g=a.templateContext),o(f,g,b).then(function(b){return"undefined"!=typeof a.title&&b.setTitle(a.title),"undefined"!=typeof a.body&&b.setBody(a.body),"undefined"!=typeof a.footer&&b.setFooter(a.footer),e&&b.setLarge(),b})};return{create:p,types:l}});
|
||||
define(["jquery","core/modal_events","core/modal_registry","core/modal","core/modal_save_cancel","core/modal_confirm","core/modal_cancel","core/templates","core/notification","core/custom_interaction_events"],function(a,b,c,d,e,f,g,h,i,j){var k={DEFAULT:"core/modal",SAVE_CANCEL:"core/modal_save_cancel",CONFIRM:"core/modal_save_cancel",CANCEL:"core/modal_cancel"},l={DEFAULT:"DEFAULT",SAVE_CANCEL:"SAVE_CANCEL",CONFIRM:"CONFIRM",CANCEL:"CANCEL"};c.register(l.DEFAULT,d,k.DEFAULT),c.register(l.SAVE_CANCEL,e,k.SAVE_CANCEL),c.register(l.CONFIRM,f,k.CONFIRM),c.register(l.CANCEL,g,k.CANCEL);var m=function(c,d,e){var f=null,g="function"==typeof e.preShowCallback,h=function(b,d){f=a(b.currentTarget),c.then(function(a){return g&&e.preShowCallback(f,a),a.show(),a}),d.originalEvent.preventDefault()};if(Array.isArray(d)){var i=d[1];d=d[0],j.define(d,[j.events.activate]),d.on(j.events.activate,i,h)}else j.define(d,[j.events.activate]),d.on(j.events.activate,h);c.then(function(a){return a.getRoot().on(b.hidden,function(){null!==f&&f.focus()}),a})},n=function(b,c){c=a(c);var d=b.module,e=new d(c);return e},o=function(b,c){var d=b.template,e=h.render(d,c).then(function(c){var d=a(c);return n(b,d)}).fail(i.exception);return e},p=function(a,b){var d=a.type||l.DEFAULT,e=!!a.large,f=null,g={};f=c.get(d),f||i.exception({message:"Unable to find modal of type: "+d}),"undefined"!=typeof a.templateContext&&(g=a.templateContext);var h=o(f,g).then(function(b){return"undefined"!=typeof a.title&&b.setTitle(a.title),"undefined"!=typeof a.body&&b.setBody(a.body),"undefined"!=typeof a.footer&&b.setFooter(a.footer),e&&b.setLarge(),b});return"undefined"!=typeof b&&m(h,b,a),h};return{create:p,types:l}});
|
||||
+1
@@ -0,0 +1 @@
|
||||
define([],function(){return{SHOW_PAGES:"core-paged-content-show-pages"}});
|
||||
+1
@@ -0,0 +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}});
|
||||
+1
@@ -0,0 +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}});
|
||||
@@ -0,0 +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&&d<e?d+1:e;break;case"previous":d=g(a),c=d&&d>1?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}});
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/custom_interaction_events","core/paged_content_events"],function(a,b,c){var d={ROOT:'[data-region="paging-dropdown-container"]',DROPDOWN_ITEM:'[data-region="dropdown-item"]',DROPDOWN_TOGGLE:'[data-region="dropdown-toggle"]',ACTIVE_DROPDOWN_ITEM:'[data-region="dropdown-item"].active',CARET:'[data-region="caret"]'},e=function(a){return parseInt(a.attr("data-page-number"),10)},f=function(a){return a.find(d.DROPDOWN_ITEM)},g=function(b,c){var d=e(c);return f(b).filter(function(b,c){return e(a(c))<d})},h=function(a){return parseInt(a.attr("data-item-count"),10)},i=function(b,c){if(void 0!=c.attr("data-offset"))return parseInt(c.attr("data-offset"),10);var d=0;return g(b,c).each(function(b,c){c=a(c),d+=h(c)}),c.attr("data-offset",d),d},j=function(a){return a.find(d.ACTIVE_DROPDOWN_ITEM)},k=function(b,c){return c.map(function(c,d){return d=a(d),{pageNumber:e(d),limit:h(d),offset:i(b,d)}}).get()},l=function(b){b.each(function(b,c){c=a(c),c.attr("data-page-number",b+1)})},m=function(a,b){var e=g(a,b),f=e.add(b),h=k(a,f),i=a.find(d.DROPDOWN_TOGGLE),l=i.find(d.CARET);j(a).removeClass("active"),b.addClass("active"),i.html(b.text()),i.append(l),a.trigger(c.SHOW_PAGES,[h])},n=function(c){c=a(c);var e=f(c);l(e);var g=j(c);g.length&&m(c,g),b.define(c,[b.events.activate]),c.on(b.events.activate,d.DROPDOWN_ITEM,function(b,e){var f=a(b.target).closest(d.DROPDOWN_ITEM);m(c,f),e.originalEvent.preventDefault()})};return{init:n,rootSelector:d.ROOT}});
|
||||
@@ -44,6 +44,10 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
ctrlPageDown: 'cie:ctrlPageDown',
|
||||
enter: 'cie:enter',
|
||||
};
|
||||
// Static cache of jQuery events that have been handled. This should
|
||||
// only be populated by JavaScript generated events (which will keep it
|
||||
// fairly small).
|
||||
var triggeredEvents = {};
|
||||
|
||||
/**
|
||||
* Check if the caller has asked for the given event type to be
|
||||
@@ -77,6 +81,48 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
return (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger the custom event for the given jQuery event.
|
||||
*
|
||||
* This function will only fire the custom event if one hasn't already been
|
||||
* fired for the jQuery event.
|
||||
*
|
||||
* This is to prevent multiple custom event handlers triggering multiple
|
||||
* custom events for a single jQuery event as it bubbles up the stack.
|
||||
*
|
||||
* @param {string} eventName The name of the custom event
|
||||
* @param {event} e The jQuery event
|
||||
* @return {void}
|
||||
*/
|
||||
var triggerEvent = function(eventName, e) {
|
||||
var eventTypeKey = "";
|
||||
|
||||
if (!e.hasOwnProperty('originalEvent')) {
|
||||
// This is a jQuery event generated from JavaScript not a browser event so
|
||||
// we need to build the cache key for the event.
|
||||
eventTypeKey = "" + eventName + e.type + e.timeStamp;
|
||||
|
||||
if (!triggeredEvents.hasOwnProperty(eventTypeKey)) {
|
||||
// If we haven't seen this jQuery event before then fire a custom
|
||||
// event for it and remember the event for later.
|
||||
triggeredEvents[eventTypeKey] = true;
|
||||
$(e.target).trigger(eventName, [{originalEvent: e}]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
eventTypeKey = "triggeredCustom_" + eventName;
|
||||
if (!e.originalEvent.hasOwnProperty(eventTypeKey)) {
|
||||
// If this is a jQuery event generated by the browser then set a
|
||||
// property on the original event to track that we've seen it before.
|
||||
// The property is set on the original event because it's the only part
|
||||
// of the jQuery event that is maintained through multiple event handlers.
|
||||
e.originalEvent[eventTypeKey] = true;
|
||||
$(e.target).trigger(eventName, [{originalEvent: e}]);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a keyboard event that ignores modifier keys.
|
||||
*
|
||||
@@ -90,7 +136,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
element.off('keydown.' + event).on('keydown.' + event, function(e) {
|
||||
if (!isModifierPressed(e)) {
|
||||
if (e.keyCode == keyCode) {
|
||||
$(e.target).trigger(event, [{originalEvent: e}]);
|
||||
triggerEvent(event, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -106,12 +152,12 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
*/
|
||||
var addActivateListener = function(element) {
|
||||
element.off('click.cie.activate').on('click.cie.activate', function(e) {
|
||||
$(e.target).trigger(events.activate, [{originalEvent: e}]);
|
||||
triggerEvent(events.activate, e);
|
||||
});
|
||||
element.off('keydown.cie.activate').on('keydown.cie.activate', function(e) {
|
||||
if (!isModifierPressed(e)) {
|
||||
if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
|
||||
$(e.target).trigger(events.activate, [{originalEvent: e}]);
|
||||
triggerEvent(events.activate, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -129,7 +175,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
element.off('keydown.cie.keyboardactivate').on('keydown.cie.keyboardactivate', function(e) {
|
||||
if (!isModifierPressed(e)) {
|
||||
if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
|
||||
$(e.target).trigger(events.keyboardActivate, [{originalEvent: e}]);
|
||||
triggerEvent(events.keyboardActivate, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -250,7 +296,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function(e) {
|
||||
var scrollTop = element.scrollTop();
|
||||
if (scrollTop === 0) {
|
||||
element.trigger(events.scrollTop, [{originalEvent: e}]);
|
||||
triggerEvent(events.scrollTop, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -270,7 +316,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
var scrollHeight = element[0].scrollHeight;
|
||||
|
||||
if (scrollTop + innerHeight >= scrollHeight) {
|
||||
element.trigger(events.scrollBottom, [{originalEvent: e}]);
|
||||
triggerEvent(events.scrollBottom, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -302,7 +348,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
e.preventDefault();
|
||||
e.returnValue = false;
|
||||
// Fire the scroll lock event.
|
||||
element.trigger(events.scrollLock, [{originalEvent: e}]);
|
||||
triggerEvent(events.scrollLock, e);
|
||||
|
||||
return false;
|
||||
} else if (up && delta > scrollTop) {
|
||||
@@ -312,7 +358,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
e.preventDefault();
|
||||
e.returnValue = false;
|
||||
// Fire the scroll lock event.
|
||||
element.trigger(events.scrollLock, [{originalEvent: e}]);
|
||||
triggerEvent(events.scrollLock, e);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -333,7 +379,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
element.off('keydown.cie.ctrlpageup').on('keydown.cie.ctrlpageup', function(e) {
|
||||
if (e.ctrlKey) {
|
||||
if (e.keyCode == keyCodes.pageUp) {
|
||||
$(e.target).trigger(events.ctrlPageUp, [{originalEvent: e}]);
|
||||
triggerEvent(events.ctrlPageUp, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -351,7 +397,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
|
||||
element.off('keydown.cie.ctrlpagedown').on('keydown.cie.ctrlpagedown', function(e) {
|
||||
if (e.ctrlKey) {
|
||||
if (e.keyCode == keyCodes.pageDown) {
|
||||
$(e.target).trigger(events.ctrlPageDown, [{originalEvent: e}]);
|
||||
triggerEvent(events.ctrlPageDown, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,49 +60,55 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
|
||||
* @method setUpTrigger
|
||||
* @param {Promise} modalPromise The modal instance
|
||||
* @param {object} triggerElement The jQuery element to open the modal
|
||||
* @param {object} modalConfig The modal configuration given to the factory
|
||||
*/
|
||||
var setUpTrigger = function(modalPromise, triggerElement) {
|
||||
if (typeof triggerElement != 'undefined') {
|
||||
// The element that actually shows the modal.
|
||||
var actualTriggerElement = null;
|
||||
if (Array.isArray(triggerElement)) {
|
||||
var selector = triggerElement[1];
|
||||
triggerElement = triggerElement[0];
|
||||
|
||||
CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
|
||||
triggerElement.on(CustomEvents.events.activate, selector, function(e, data) {
|
||||
actualTriggerElement = e.currentTarget;
|
||||
modalPromise.then(function(modal) {
|
||||
modal.show();
|
||||
|
||||
return modal;
|
||||
});
|
||||
data.originalEvent.preventDefault();
|
||||
});
|
||||
} else {
|
||||
CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
|
||||
triggerElement.on(CustomEvents.events.activate, function(e, data) {
|
||||
actualTriggerElement = e.currentTarget;
|
||||
modalPromise.then(function(modal) {
|
||||
modal.show();
|
||||
|
||||
return modal;
|
||||
});
|
||||
data.originalEvent.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
var setUpTrigger = function(modalPromise, triggerElement, modalConfig) {
|
||||
// The element that actually shows the modal.
|
||||
var actualTriggerElement = null;
|
||||
// Check if the client has provided a callback function to be called
|
||||
// before the modal is displayed.
|
||||
var hasPreShowCallback = (typeof modalConfig.preShowCallback == 'function');
|
||||
// Function to handle the trigger element being activated.
|
||||
var triggeredCallback = function(e, data) {
|
||||
actualTriggerElement = $(e.currentTarget);
|
||||
modalPromise.then(function(modal) {
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Focus on the trigger element that actually launched the modal.
|
||||
if (actualTriggerElement !== null) {
|
||||
actualTriggerElement.focus();
|
||||
}
|
||||
});
|
||||
if (hasPreShowCallback) {
|
||||
// If the client provided a pre-show callback then execute
|
||||
// it now before showing the modal.
|
||||
modalConfig.preShowCallback(actualTriggerElement, modal);
|
||||
}
|
||||
|
||||
modal.show();
|
||||
|
||||
return modal;
|
||||
});
|
||||
data.originalEvent.preventDefault();
|
||||
};
|
||||
|
||||
// The trigger element can either be a single element or it can be an
|
||||
// element + selector pair to create a delegated event handler to trigger
|
||||
// the modal.
|
||||
if (Array.isArray(triggerElement)) {
|
||||
var selector = triggerElement[1];
|
||||
triggerElement = triggerElement[0];
|
||||
|
||||
CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
|
||||
triggerElement.on(CustomEvents.events.activate, selector, triggeredCallback);
|
||||
} else {
|
||||
CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
|
||||
triggerElement.on(CustomEvents.events.activate, triggeredCallback);
|
||||
}
|
||||
|
||||
modalPromise.then(function(modal) {
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
// Focus on the trigger element that actually launched the modal.
|
||||
if (actualTriggerElement !== null) {
|
||||
actualTriggerElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return modal;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -112,7 +118,6 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
|
||||
* @method createFromElement
|
||||
* @param {object} registryConf A config from the ModalRegistry
|
||||
* @param {object} modalElement The modal HTML jQuery object
|
||||
* @param {object} triggerElement The trigger HTML jQuery object
|
||||
* @return {object} Modal instance
|
||||
*/
|
||||
var createFromElement = function(registryConf, modalElement) {
|
||||
@@ -125,15 +130,14 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
|
||||
|
||||
/**
|
||||
* Create the correct modal instance for the given type, including loading
|
||||
* the correct template and setting up the trigger relationship with the
|
||||
* trigger element.
|
||||
* the correct template.
|
||||
*
|
||||
* @method createFromType
|
||||
* @param {object} registryConf A config from the ModalRegistry
|
||||
* @param {object} triggerElement The trigger HTML jQuery object
|
||||
* @param {object} templateContext The context to render the template with
|
||||
* @return {promise} Resolved with a Modal instance
|
||||
*/
|
||||
var createFromType = function(registryConf, templateContext, triggerElement) {
|
||||
var createFromType = function(registryConf, templateContext) {
|
||||
var templateName = registryConf.template;
|
||||
|
||||
var modalPromise = Templates.render(templateName, templateContext)
|
||||
@@ -143,8 +147,6 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
setUpTrigger(modalPromise, triggerElement);
|
||||
|
||||
return modalPromise;
|
||||
};
|
||||
|
||||
@@ -172,7 +174,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
|
||||
templateContext = modalConfig.templateContext;
|
||||
}
|
||||
|
||||
return createFromType(registryConf, templateContext, triggerElement)
|
||||
var modalPromise = createFromType(registryConf, templateContext)
|
||||
.then(function(modal) {
|
||||
if (typeof modalConfig.title != 'undefined') {
|
||||
modal.setTitle(modalConfig.title);
|
||||
@@ -192,6 +194,12 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
|
||||
|
||||
return modal;
|
||||
});
|
||||
|
||||
if (typeof triggerElement != 'undefined') {
|
||||
setUpTrigger(modalPromise, triggerElement, modalConfig);
|
||||
}
|
||||
|
||||
return modalPromise;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 the paging bar.
|
||||
*
|
||||
* @module core/paging_bar
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define([], function() {
|
||||
return {
|
||||
SHOW_PAGES: 'core-paged-content-show-pages',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Factory to create a paged content widget.
|
||||
*
|
||||
* @module core/paged_content_factory
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/templates',
|
||||
'core/notification',
|
||||
'core/paged_content_pages'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Templates,
|
||||
Notification,
|
||||
PagedContent
|
||||
) {
|
||||
var TEMPLATES = {
|
||||
PAGED_CONTENT: 'core/paged_content'
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paging bar template with based on the number
|
||||
* of pages to show.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
var buildPagingBarTemplateContext = function(numberOfPages, itemsPerPage) {
|
||||
var context = {
|
||||
"itemsperpage": itemsPerPage,
|
||||
"previous": {},
|
||||
"next": {},
|
||||
"pages": []
|
||||
};
|
||||
|
||||
for (var i = 1; i <= numberOfPages; i++) {
|
||||
var page = {
|
||||
number: i,
|
||||
page: "" + i,
|
||||
};
|
||||
|
||||
// Make the first page active by default.
|
||||
if (i === 1) {
|
||||
page.active = true;
|
||||
}
|
||||
|
||||
context.pages.push(page);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paging dropdown template with 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).
|
||||
*
|
||||
* For example:
|
||||
* Number of pages = 3
|
||||
* Items per page = 25
|
||||
* Would render a dropdown will 4 options:
|
||||
* 25
|
||||
* 50
|
||||
* 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 {object} config Configuration options provided by the client.
|
||||
* @return {object} The template context.
|
||||
*/
|
||||
var buildPagingDropdownTemplateContext = function(numberOfPages, itemsPerPage, config) {
|
||||
var context = {
|
||||
options: []
|
||||
};
|
||||
|
||||
var totalItems = 0;
|
||||
var lastIncrease = 0;
|
||||
var maxPages = numberOfPages;
|
||||
|
||||
if (config.hasOwnProperty('maxPages')) {
|
||||
maxPages = config.maxPages;
|
||||
}
|
||||
|
||||
for (var i = 1; i <= maxPages; i++) {
|
||||
var itemCount = 0;
|
||||
|
||||
if (i <= 2) {
|
||||
itemCount = itemsPerPage;
|
||||
lastIncrease = itemsPerPage;
|
||||
} else {
|
||||
lastIncrease = lastIncrease * 2;
|
||||
itemCount = lastIncrease;
|
||||
}
|
||||
|
||||
totalItems += itemCount;
|
||||
var option = {
|
||||
itemcount: itemCount,
|
||||
content: totalItems
|
||||
};
|
||||
|
||||
// Make the first option active by default.
|
||||
if (i === 1) {
|
||||
option.active = true;
|
||||
}
|
||||
|
||||
context.options.push(option);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paged content template with based on the number
|
||||
* of pages to show, items per page, and configuration option.
|
||||
*
|
||||
* 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 {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
|
||||
};
|
||||
|
||||
if (config.hasOwnProperty('dropdown') && config.dropdown) {
|
||||
context.pagingdropdown = buildPagingDropdownTemplateContext(numberOfPages, itemsPerPage, config);
|
||||
} else {
|
||||
context.pagingbar = buildPagingBarTemplateContext(numberOfPages, 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).
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* @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 deferred = $.Deferred();
|
||||
var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
|
||||
var templateContext = buildTemplateContext(numberOfPages, 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);
|
||||
|
||||
deferred.resolve(html, js);
|
||||
return;
|
||||
})
|
||||
.fail(function(exception) {
|
||||
deferred.reject(exception);
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a paged content widget where the complete list of items is loaded
|
||||
* up front.
|
||||
*
|
||||
* The client code must provide a callback function which renders the
|
||||
* items for each page. The callback will be provided with an array where each
|
||||
* value in the array is a the list of items to render for the page.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* @param {array} contentItems The list of items to paginate.
|
||||
* @param {int} 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.
|
||||
*/
|
||||
var createFromStaticList = function(contentItems, itemsPerPage, renderContentCallback, config) {
|
||||
if (typeof config == 'undefined') {
|
||||
config = {};
|
||||
}
|
||||
|
||||
var numberOfItems = contentItems.length;
|
||||
return createFromAjax(numberOfItems, itemsPerPage, function(pagesData) {
|
||||
var contentToRender = [];
|
||||
pagesData.forEach(function(pageData) {
|
||||
var begin = pageData.offset;
|
||||
var end = pageData.limit ? begin + pageData.limit : numberOfItems;
|
||||
var items = contentItems.slice(begin, end);
|
||||
contentToRender.push(items);
|
||||
});
|
||||
|
||||
return renderContentCallback(contentToRender);
|
||||
}, config);
|
||||
};
|
||||
|
||||
return {
|
||||
createFromAjax: createFromAjax,
|
||||
createFromStaticList: createFromStaticList
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
// 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 showing/hiding pages of content.
|
||||
*
|
||||
* @module core/paged_content_pages
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/templates',
|
||||
'core/notification',
|
||||
'core/paged_content_events'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Templates,
|
||||
Notification,
|
||||
PagedContentEvents
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ROOT: '[data-region="page-container"]',
|
||||
PAGE_REGION: '[data-region="paged-content-page"]',
|
||||
ACTIVE_PAGE_REGION: '[data-region="paged-content-page"].active'
|
||||
};
|
||||
|
||||
var TEMPLATES = {
|
||||
PAGING_CONTENT_ITEM: 'core/paged_content_page',
|
||||
LOADING: 'core/overlay_loading'
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a page by the number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {Number} pageNumber The number of the page to be found.
|
||||
* @returns {jQuery} The page.
|
||||
*/
|
||||
var findPage = function(root, pageNumber) {
|
||||
return root.find('[data-page="' + pageNumber + '"]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the loading spinner until the returned deferred is resolved by the
|
||||
* calling code.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {promise} The page.
|
||||
*/
|
||||
var startLoading = function(root) {
|
||||
var deferred = $.Deferred();
|
||||
|
||||
Templates.render(TEMPLATES.LOADING, {visible: true})
|
||||
.then(function(html) {
|
||||
var loadingSpinner = $(html);
|
||||
// Put this in a timer to give the calling code 100 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);
|
||||
|
||||
deferred.always(function() {
|
||||
clearTimeout(timerId);
|
||||
// Remove the loading spinner when our deferred is resolved
|
||||
// by the calling code.
|
||||
loadingSpinner.remove();
|
||||
root.css('position', '');
|
||||
return;
|
||||
});
|
||||
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the result of the page promise in a paged content page.
|
||||
*
|
||||
* This function returns a promise that is resolved with the new paged content
|
||||
* page.
|
||||
*
|
||||
* @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.
|
||||
* @returns {promise} The page.
|
||||
*/
|
||||
var renderPagePromise = function(root, pagePromise, pageNumber) {
|
||||
var deferred = $.Deferred();
|
||||
pagePromise.then(function(html, 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, {
|
||||
page: pageNumber,
|
||||
content: html
|
||||
})
|
||||
.then(function(html) {
|
||||
// Make sure the JS we got from the page promise is being added
|
||||
// to the page when we render the page.
|
||||
Templates.appendNodeContents(root, html, pageJS);
|
||||
var page = findPage(root, pageNumber);
|
||||
deferred.resolve(page);
|
||||
return;
|
||||
})
|
||||
.fail(function(exception) {
|
||||
deferred.reject(exception);
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
return;
|
||||
})
|
||||
.fail(function(exception) {
|
||||
deferred.reject(exception);
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make one or more pages visible based on the SHOW_PAGES event. The show
|
||||
* pages event provides data containing which pages should be shown as well
|
||||
* as the limit and offset values for loading the items for each of those pages.
|
||||
*
|
||||
* The renderPagesContentCallback is provided this list of data to know which
|
||||
* pages to load. E.g. the data to load 2 pages might look like:
|
||||
* [
|
||||
* {
|
||||
* pageNumber: 1,
|
||||
* limit: 5,
|
||||
* offset: 0
|
||||
* },
|
||||
* {
|
||||
* pageNumber: 2,
|
||||
* limit: 5,
|
||||
* offset: 5
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* The renderPagesContentCallback should return an array of promises, one for
|
||||
* each page in the pages data, that is resolved with the HTML and JS for that page.
|
||||
*
|
||||
* If the renderPagesContentCallback is not provided then it is assumed that
|
||||
* all pages have been rendered prior to initialising this module.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {Number} pagesData The data for which pages need to be visible.
|
||||
* @param {function} renderPagesContentCallback Render pages content.
|
||||
*/
|
||||
var showPages = function(root, pagesData, renderPagesContentCallback) {
|
||||
var existingPages = [];
|
||||
var newPageData = [];
|
||||
var newPagesPromise = $.Deferred();
|
||||
|
||||
// Check which of the pages being requests have previously been rendered
|
||||
// so that we only ask for new pages to be rendered by the callback.
|
||||
pagesData.forEach(function(pageData) {
|
||||
var pageNumber = pageData.pageNumber;
|
||||
var existingPage = findPage(root, pageNumber);
|
||||
if (existingPage.length) {
|
||||
existingPages.push(existingPage);
|
||||
} else {
|
||||
newPageData.push(pageData);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
// 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.
|
||||
var renderPagePromises = promises.map(function(promise, index) {
|
||||
// Create our promise for when our rendering will be completed.
|
||||
return renderPagePromise(root, promise, newPageData[index].pageNumber);
|
||||
});
|
||||
// After each of our rendering promises have been completed then we can
|
||||
// give all of the new pages to the next bit of code for handling.
|
||||
$.when.apply($, renderPagePromises)
|
||||
.then(function() {
|
||||
var newPages = Array.prototype.slice.call(arguments);
|
||||
// Resolve the promise with the list of newly rendered pages.
|
||||
newPagesPromise.resolve(newPages);
|
||||
return;
|
||||
})
|
||||
.fail(function(exception) {
|
||||
newPagesPromise.reject(exception);
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
} else {
|
||||
// If there aren't any pages to load then immediately resolve the promise.
|
||||
newPagesPromise.resolve([]);
|
||||
}
|
||||
|
||||
var loadingPromise = startLoading(root);
|
||||
newPagesPromise.then(function(newPages) {
|
||||
// Once all of the new pages have been created then add them to any
|
||||
// existing pages we have.
|
||||
var pagesToShow = existingPages.concat(newPages);
|
||||
// Hide all existing pages.
|
||||
root.find(SELECTORS.PAGE_REGION).addClass('hidden');
|
||||
// Show each of the pages that were requested.
|
||||
pagesToShow.forEach(function(page) {
|
||||
page.removeClass('hidden');
|
||||
});
|
||||
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception)
|
||||
.always(function() {
|
||||
loadingPromise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the module to listen for SHOW_PAGES events and render the
|
||||
* appropriate pages using the provided renderPagesContentCallback function.
|
||||
*
|
||||
* The renderPagesContentCallback is provided a list of data to know which
|
||||
* pages to load.
|
||||
* E.g. the data to load 2 pages might look like:
|
||||
* [
|
||||
* {
|
||||
* pageNumber: 1,
|
||||
* limit: 5,
|
||||
* offset: 0
|
||||
* },
|
||||
* {
|
||||
* pageNumber: 2,
|
||||
* limit: 5,
|
||||
* offset: 5
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* The renderPagesContentCallback should return an array of promises, one for
|
||||
* each page in the pages data, that is resolved with the HTML and JS for that page.
|
||||
*
|
||||
* If the renderPagesContentCallback is not provided then it is assumed that
|
||||
* all pages have been rendered prior to initialising this module.
|
||||
*
|
||||
* 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 {function} renderPagesContentCallback Render pages content.
|
||||
*/
|
||||
var init = function(root, eventElement, renderPagesContentCallback) {
|
||||
root = $(root);
|
||||
eventElement = $(eventElement);
|
||||
|
||||
eventElement.on(PagedContentEvents.SHOW_PAGES, function(e, pagesData) {
|
||||
showPages(root, pagesData, renderPagesContentCallback);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
rootSelector: SELECTORS.ROOT,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
// 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 enhance the paged content paging bar.
|
||||
*
|
||||
* @module core/paging_bar
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'core/paged_content_events'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
PagedContentEvents
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ROOT: '[data-region="paging-bar"]',
|
||||
PAGE: '[data-page]',
|
||||
PAGE_ITEM: '[data-region="page-item"]',
|
||||
ACTIVE_PAGE_ITEM: '[data-region="page-item"].active'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the page element by number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {Number} pageNumber The page number.
|
||||
* @return {jQuery}
|
||||
*/
|
||||
var getPageByNumber = function(root, pageNumber) {
|
||||
return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the last page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @return {int}
|
||||
*/
|
||||
var getLastPageNumber = function(root) {
|
||||
var lastPage = root.find(SELECTORS.PAGE).last();
|
||||
if (lastPage) {
|
||||
return parseInt(lastPage.attr('data-page-number'), 10);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the active page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {int} 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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {object} page The page.
|
||||
* @returns {int} The page number
|
||||
*/
|
||||
var getPageNumber = function(root, page) {
|
||||
if (page.attr('data-page') != undefined) {
|
||||
// If it's an actual page then we can just use the page number
|
||||
// attribute.
|
||||
return parseInt(page.attr('data-page-number'), 10);
|
||||
}
|
||||
|
||||
var pageNumber = 1;
|
||||
var activePageNumber = null;
|
||||
|
||||
switch (page.attr('data-control')) {
|
||||
case 'first':
|
||||
pageNumber = 1;
|
||||
break;
|
||||
|
||||
case 'last':
|
||||
pageNumber = getLastPageNumber(root);
|
||||
break;
|
||||
|
||||
case 'next':
|
||||
activePageNumber = getActivePageNumber(root);
|
||||
var lastPage = getLastPageNumber(root);
|
||||
if (activePageNumber && activePageNumber < lastPage) {
|
||||
pageNumber = activePageNumber + 1;
|
||||
} else {
|
||||
pageNumber = lastPage;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'previous':
|
||||
activePageNumber = getActivePageNumber(root);
|
||||
if (activePageNumber && activePageNumber > 1) {
|
||||
pageNumber = activePageNumber - 1;
|
||||
} else {
|
||||
pageNumber = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
pageNumber = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure we return an int not a string.
|
||||
return parseInt(pageNumber, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the limit of items for each page.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {int}
|
||||
*/
|
||||
var getLimit = function(root) {
|
||||
return parseInt(root.attr('data-items-per-page'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set page numbers on each of the given items. Page numbers are set
|
||||
* from 1..n (where n is the number of items).
|
||||
*
|
||||
* @param {jQuery} items A jQuery list of items.
|
||||
*/
|
||||
var generatePageNumbers = function(items) {
|
||||
items.each(function(index, item) {
|
||||
item = $(item);
|
||||
item.attr('data-page-number', index + 1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the paging bar item for the given page number visible and fire
|
||||
* the SHOW_PAGES paged content event to tell any listening content to
|
||||
* update.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {int} pageNumber The number for the page to show.
|
||||
* @param {object} page The page.
|
||||
*/
|
||||
var showPage = function(root, pageNumber) {
|
||||
var isSamePage = pageNumber == getActivePageNumber(root);
|
||||
var limit = getLimit(root);
|
||||
var offset = (pageNumber - 1) * limit;
|
||||
|
||||
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');
|
||||
var page = getPageByNumber(root, pageNumber);
|
||||
page.addClass('active');
|
||||
}
|
||||
|
||||
// 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, [[{
|
||||
pageNumber: pageNumber,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}]]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the paging bar.
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
var pages = root.find(SELECTORS.PAGE);
|
||||
generatePageNumbers(pages);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
CustomEvents.define(root, [
|
||||
CustomEvents.events.activate
|
||||
]);
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
rootSelector: SELECTORS.ROOT,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
// 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 manage the paging dropdown control.
|
||||
*
|
||||
* @module core/paged_content_paging_dropdown
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'core/paged_content_events'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
PagedContentEvents
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ROOT: '[data-region="paging-dropdown-container"]',
|
||||
DROPDOWN_ITEM: '[data-region="dropdown-item"]',
|
||||
DROPDOWN_TOGGLE: '[data-region="dropdown-toggle"]',
|
||||
ACTIVE_DROPDOWN_ITEM: '[data-region="dropdown-item"].active',
|
||||
CARET: '[data-region="caret"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the page number.
|
||||
*
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {int}
|
||||
*/
|
||||
var getPageNumber = function(item) {
|
||||
return parseInt(item.attr('data-page-number'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all paging dropdown items.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @returns {jquery} A jquery object with all items.
|
||||
*/
|
||||
var getAllItems = function(root) {
|
||||
return root.find(SELECTORS.DROPDOWN_ITEM);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all paging dropdown items with lower page numbers than the given
|
||||
* dropdown item.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {jquery} A jquery object with all items.
|
||||
*/
|
||||
var getPreviousItems = function(root, item) {
|
||||
var pageNumber = getPageNumber(item);
|
||||
return getAllItems(root).filter(function(index, element) {
|
||||
return getPageNumber($(element)) < pageNumber;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the number of items to be loaded for the dropdown item.
|
||||
*
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {int}
|
||||
*/
|
||||
var getLimit = function(item) {
|
||||
return parseInt(item.attr('data-item-count'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the offset of items from the start of the itemset for the given
|
||||
* dropdown item.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {int}
|
||||
*/
|
||||
var getOffset = function(root, item) {
|
||||
if (item.attr('data-offset') != undefined) {
|
||||
return parseInt(item.attr('data-offset'), 10);
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
|
||||
getPreviousItems(root, item).each(function(index, prevItem) {
|
||||
prevItem = $(prevItem);
|
||||
offset += getLimit(prevItem);
|
||||
});
|
||||
|
||||
item.attr('data-offset', offset);
|
||||
return offset;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the active dropdown item.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @returns {jquery} The active dropdown item.
|
||||
*/
|
||||
var getActiveItem = function(root) {
|
||||
return root.find(SELECTORS.ACTIVE_DROPDOWN_ITEM);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the event payload for the list of dropdown items. The event payload
|
||||
* is an array of objects with one object per dropdown item.
|
||||
*
|
||||
* Each payload object contains the page number, limit, and offset for the
|
||||
* corresponding dropdown item.
|
||||
*
|
||||
* For example: If we had 3 dropdown items with incrementing page numbers loading
|
||||
* 25 items per page then the generated payload would look like:
|
||||
* [
|
||||
* {
|
||||
* pageNumber: 1,
|
||||
* limit: 25,
|
||||
* offset: 0
|
||||
* },
|
||||
* {
|
||||
* pageNumber: 2,
|
||||
* limit: 25,
|
||||
* offset: 25
|
||||
* },
|
||||
* {
|
||||
* pageNumber: 3,
|
||||
* limit: 25,
|
||||
* offset: 50
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {jquery} items The dropdown items.
|
||||
* @returns {object[]} The payload for the event.
|
||||
*/
|
||||
var generateEventPayload = function(root, items) {
|
||||
return items.map(function(index, item) {
|
||||
item = $(item);
|
||||
return {
|
||||
pageNumber: getPageNumber(item),
|
||||
limit: getLimit(item),
|
||||
offset: getOffset(root, item),
|
||||
};
|
||||
}).get();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add page number attributes to each of the given items. The page numbers
|
||||
* start at 1 and increment by 1 for each item, e.g. 1, 2, 3 etc.
|
||||
*
|
||||
* @param {jquery} items The dropdown items.
|
||||
*/
|
||||
var generatePageNumbers = function(items) {
|
||||
items.each(function(index, item) {
|
||||
item = $(item);
|
||||
item.attr('data-page-number', index + 1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the given item active by setting the active class on it and firing
|
||||
* the SHOW_PAGES event for the paged content to show the appropriate
|
||||
* pages.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {jquery} item The dropdown item.
|
||||
*/
|
||||
var setActiveItem = function(root, item) {
|
||||
var prevItems = getPreviousItems(root, item);
|
||||
var allItems = prevItems.add(item);
|
||||
var eventPayload = generateEventPayload(root, allItems);
|
||||
var toggle = root.find(SELECTORS.DROPDOWN_TOGGLE);
|
||||
var caret = toggle.find(SELECTORS.CARET);
|
||||
|
||||
getActiveItem(root).removeClass('active');
|
||||
item.addClass('active');
|
||||
|
||||
// Update the dropdown toggle to show which item is selected.
|
||||
toggle.html(item.text());
|
||||
// Bootstrap 2 compatibility.
|
||||
toggle.append(caret);
|
||||
// Fire the event to tell the content to update.
|
||||
root.trigger(PagedContentEvents.SHOW_PAGES, [eventPayload]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the module by firing the SHOW_PAGES event for an existing
|
||||
* active page found and setting up the event listener for the user to select
|
||||
* new pages.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
var items = getAllItems(root);
|
||||
generatePageNumbers(items);
|
||||
|
||||
var activeItem = getActiveItem(root);
|
||||
if (activeItem.length) {
|
||||
// Fire the first event for the content to make sure it's visible.
|
||||
setActiveItem(root, activeItem);
|
||||
}
|
||||
|
||||
CustomEvents.define(root, [
|
||||
CustomEvents.events.activate
|
||||
]);
|
||||
|
||||
root.on(CustomEvents.events.activate, SELECTORS.DROPDOWN_ITEM, function(e, data) {
|
||||
var item = $(e.target).closest(SELECTORS.DROPDOWN_ITEM);
|
||||
setActiveItem(root, item);
|
||||
|
||||
data.originalEvent.preventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
rootSelector: SELECTORS.ROOT,
|
||||
};
|
||||
});
|
||||
@@ -1146,6 +1146,13 @@ $functions = array(
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
),
|
||||
'core_question_get_random_question_summaries' => array(
|
||||
'classname' => 'core_question_external',
|
||||
'methodname' => 'get_random_question_summaries',
|
||||
'description' => 'Get the random question set for a criteria',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
),
|
||||
'core_rating_get_item_ratings' => array(
|
||||
'classname' => 'core_rating_external',
|
||||
'methodname' => 'get_item_ratings',
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content
|
||||
|
||||
This template renders each of the content regions for a paginated
|
||||
content section.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"pagingbar": {
|
||||
"itemsperpage": 1,
|
||||
"previous": true,
|
||||
"next": true,
|
||||
"first": true,
|
||||
"last": true,
|
||||
"pages": [
|
||||
{
|
||||
"page": "1",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": "#",
|
||||
"page": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"active": true,
|
||||
"page": 1,
|
||||
"content": "<p>Some page 1 content</p>"
|
||||
},
|
||||
{
|
||||
"page": 2,
|
||||
"content": "<p>Some page 2 content</p>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<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}}
|
||||
{{> core/paged_content_pages }}
|
||||
</div>
|
||||
{{^skipjs}}
|
||||
{{#js}}
|
||||
require(
|
||||
[
|
||||
'jquery',
|
||||
'core/paged_content_pages'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
PagedContent
|
||||
) {
|
||||
var container = $("#paged-content-container-{{uniqid}}");
|
||||
var pagingContent = container.find(PagedContent.rootSelector);
|
||||
|
||||
PagedContent.init(pagingContent, container);
|
||||
});
|
||||
{{/js}}
|
||||
{{/skipjs}}
|
||||
@@ -0,0 +1,36 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_page
|
||||
|
||||
This template renders the content of a page. It is to be used with
|
||||
the paging bar or paging dropdown to toggle visibility of the content items.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"active": true,
|
||||
"page": 1,
|
||||
"content": "<p>Some page content</p>"
|
||||
}
|
||||
}}
|
||||
<div data-region="paged-content-page"
|
||||
data-page="{{page}}"
|
||||
class="{{^active}}hidden{{/active}} {{$classes}}{{/classes}}">
|
||||
{{$content}}
|
||||
{{{content}}}
|
||||
{{/content}}
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_pages
|
||||
|
||||
This template renders each of the content regions for a paginated
|
||||
content section.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"active": true,
|
||||
"page": 1,
|
||||
"content": "<p>Some page content</p>"
|
||||
},
|
||||
{
|
||||
"page": 2,
|
||||
"content": "<p>Some page content</p>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div id="{{$pagingcontentid}}page-container-{{uniqid}}{{/pagingcontentid}}" data-region="page-container">
|
||||
{{#pages}}
|
||||
{{$paged-content-page}}
|
||||
{{> core/paged_content_page }}
|
||||
{{/paged-content-page}}
|
||||
{{/pages}}
|
||||
</div>
|
||||
@@ -0,0 +1,97 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_bar
|
||||
|
||||
This template renders the bootstrap style paging bar to control a paged
|
||||
content section.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"itemsperpage": 2,
|
||||
"previous": true,
|
||||
"next": true,
|
||||
"first": true,
|
||||
"last": true,
|
||||
"pages": [
|
||||
{
|
||||
"url": "#",
|
||||
"page": "1",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": "#",
|
||||
"page": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<nav aria-label="{{label}}"
|
||||
id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
|
||||
data-region="paging-bar"
|
||||
data-items-per-page="{{itemsperpage}}">
|
||||
|
||||
<ul class="pagination">
|
||||
{{#previous}}
|
||||
{{< core/paged_content_paging_bar_item }}
|
||||
{{$item-content}}
|
||||
<span aria-hidden="true">«</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">»</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}}
|
||||
@@ -0,0 +1,41 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_bar_item
|
||||
|
||||
This template renders a single item in the paging bar.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"url": "#",
|
||||
"number": 1,
|
||||
"page": "1",
|
||||
"active": true
|
||||
}
|
||||
}}
|
||||
<li class="page-item {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}"
|
||||
data-region="page-item"
|
||||
{{$attributes}}{{/attributes}}>
|
||||
|
||||
<a href="{{#url}}{{.}}{{/url}}{{^url}}#{{/url}}"
|
||||
class="page-link"
|
||||
data-region="page-link">
|
||||
{{$item-content}}
|
||||
{{{page}}}
|
||||
{{/item-content}}
|
||||
</a>
|
||||
</li>
|
||||
@@ -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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_dropdown
|
||||
|
||||
This template renders the bootstrap style dropdown menu to control
|
||||
paged content.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"itemcount": 25,
|
||||
"content": "25",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"itemcount": 25,
|
||||
"content": "50"
|
||||
},
|
||||
{
|
||||
"itemcount": 50,
|
||||
"content": "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="dropdown m-b-1"
|
||||
id="paging-dropdown-{{uniqid}}"
|
||||
data-region="paging-dropdown-container">
|
||||
|
||||
<button class="btn btn-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdown-menu-button-{{uniqid}}"
|
||||
data-region="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
|
||||
{{#options}}
|
||||
{{#active}}
|
||||
{{{content}}}
|
||||
{{/active}}
|
||||
{{/options}}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdown-menu-button-{{uniqid}}">
|
||||
{{#options}}
|
||||
{{> core/paged_content_paging_dropdown_item }}
|
||||
{{/options}}
|
||||
{{< core/paged_content_paging_dropdown_item }}
|
||||
{{$itemcount}}0{{/itemcount}}
|
||||
{{$content}}{{#str}} all {{/str}}{{/content}}
|
||||
{{/ 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}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_dropdown_item
|
||||
|
||||
This template renders a single item in the paging dropdown menu.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"itemcount": 25,
|
||||
"content": "25",
|
||||
"active": true
|
||||
}
|
||||
}}
|
||||
<a class="dropdown-item {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}} {{$classes}}{{/classes}}"
|
||||
data-region="dropdown-item"
|
||||
data-item-count="{{$itemcount}}{{itemcount}}{{/itemcount}}"
|
||||
href="#">
|
||||
|
||||
{{$content}}{{{content}}}{{/content}}
|
||||
</a>
|
||||
@@ -37,6 +37,8 @@ require_once($CFG->libdir.'/formslib.php');
|
||||
class quiz_add_random_form extends moodleform {
|
||||
|
||||
protected function definition() {
|
||||
global $OUTPUT, $PAGE;
|
||||
|
||||
$mform =& $this->_form;
|
||||
$mform->setDisableShortforms();
|
||||
|
||||
@@ -44,7 +46,7 @@ class quiz_add_random_form extends moodleform {
|
||||
$usablecontexts = $contexts->having_cap('moodle/question:useall');
|
||||
|
||||
// Random from existing category section.
|
||||
$mform->addElement('header', 'categoryheader',
|
||||
$mform->addElement('header', 'existingcategoryheader',
|
||||
get_string('randomfromexistingcategory', 'quiz'));
|
||||
|
||||
$mform->addElement('questioncategory', 'category', get_string('category'),
|
||||
@@ -71,10 +73,13 @@ class quiz_add_random_form extends moodleform {
|
||||
$mform->addElement('select', 'numbertoadd', get_string('randomnumber', 'quiz'),
|
||||
$this->get_number_of_questions_to_add_choices());
|
||||
|
||||
$previewhtml = $OUTPUT->render_from_template('mod_quiz/random_question_form_preview', []);
|
||||
$mform->addElement('html', $previewhtml);
|
||||
|
||||
$mform->addElement('submit', 'existingcategory', get_string('addrandomquestion', 'quiz'));
|
||||
|
||||
// Random from a new category section.
|
||||
$mform->addElement('header', 'categoryheader',
|
||||
$mform->addElement('header', 'newcategoryheader',
|
||||
get_string('randomquestionusinganewcategory', 'quiz'));
|
||||
|
||||
$mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"');
|
||||
@@ -97,6 +102,12 @@ class quiz_add_random_form extends moodleform {
|
||||
$mform->setType('cmid', PARAM_INT);
|
||||
$mform->addElement('hidden', 'returnurl', 0);
|
||||
$mform->setType('returnurl', PARAM_LOCALURL);
|
||||
|
||||
// Add the javascript required to enhance this mform.
|
||||
$PAGE->requires->js_call_amd('mod_quiz/add_random_form', 'init', [
|
||||
$mform->getAttribute('id'),
|
||||
$contexts->lowest()->id
|
||||
]);
|
||||
}
|
||||
|
||||
public function validation($fromform, $files) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/notification","core/modal_factory"],function(a,b,c){return{init:function(d,e,f,g){var h=a("body");return c.create({type:d,large:!0,preShowCallback:function(b,c){b=a(b),c.setContextId(f),c.setAddOnPageId(b.attr("data-addonpage")),c.setTitle(b.attr("data-header")),g&&g(b,c)}},[h,e]).fail(b.exception)}}});
|
||||
+1
@@ -0,0 +1 @@
|
||||
define(["jquery","mod_quiz/random_question_form_preview"],function(a,b){var c=2e3,d={PREVIEW_CONTAINER:'[data-region="random-question-preview-container"]',CATEGORY_FORM_ELEMENT:'[name="category"]',SUBCATEGORY_FORM_ELEMENT:'[name="includesubcategories"]',TAG_IDS_FORM_ELEMENT:'[name="fromtags[]"]'},e=function(a){var b=a.find(d.CATEGORY_FORM_ELEMENT).val(),c=b.split(",");return c[0]},f=function(a){return a.find(d.SUBCATEGORY_FORM_ELEMENT).is(":checked")},g=function(a){var b=a.find(d.TAG_IDS_FORM_ELEMENT).val();return b.map(function(a){var b=a.split(",");return b[0]})},h=function(a,c){var h=a.find(d.PREVIEW_CONTAINER);b.reload(h,e(a),f(a),g(a),c)},i=function(a){return a.closest(d.CATEGORY_FORM_ELEMENT).length>0||(a.closest(d.SUBCATEGORY_FORM_ELEMENT).length>0||a.closest(d.TAG_IDS_FORM_ELEMENT).length>0)},j=function(d,e){var f=null;d.on("change",function(g){i(a(g.target))&&(b.showLoadingIcon(d),f&&clearTimeout(f),f=setTimeout(function(){h(d,e)},c))})},k=function(b,c){var d=a("#"+b);h(d,c),j(d,c)};return{init:k}});
|
||||
@@ -0,0 +1 @@
|
||||
define(["mod_quiz/add_question_modal_launcher","mod_quiz/modal_add_random_question"],function(a,b){return{init:function(c,d,e,f){a.init(b.TYPE,'.menu [data-action="addarandomquestion"]',c,function(a,b){b.setCategory(d),b.setReturnUrl(e),b.setCMID(f)})}}});
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/yui","core/notification","core/modal","core/modal_events","core/modal_registry","core/fragment","core/templates"],function(a,b,c,d,e,f,g,h){var i=!1,j={EXISTING_CATEGORY_CONTAINER:'[data-region="existing-category-container"]',EXISTING_CATEGORY_FORM_ELEMENT:"#id_existingcategoryheader",NEW_CATEGORY_CONTAINER:'[data-region="new-category-container"]',NEW_CATEGORY_FORM_ELEMENT:"#id_newcategoryheader",TAB_CONTENT:'[data-region="tab-content"]',ADD_ON_PAGE_FORM_ELEMENT:'[name="addonpage"]',SUBMIT_BUTTON_ELEMENT:'input[type="submit"]',CANCEL_BUTTON_ELEMENT:'input[type="submit"][name="cancel"]',FORM_HEADER:"legend",BUTTON_CONTAINER:".fitem"},k=function(a){d.call(this,a),this.contextId=null,this.addOnPageId=null,this.category=null,this.returnUrl=null,this.cmid=null,this.loadedForm=!1};return k.TYPE="mod_quiz-quiz-add-random-question",k.prototype=Object.create(d.prototype),k.prototype.constructor=k,k.prototype.setContextId=function(a){this.contextId=a},k.prototype.getContextId=function(){return this.contextId},k.prototype.setAddOnPageId=function(a){this.addOnPageId=a,this.getBody().find(j.ADD_ON_PAGE_FORM_ELEMENT).val(a)},k.prototype.getAddOnPageId=function(){return this.addOnPageId},k.prototype.setCategory=function(a){this.category=a},k.prototype.getCategory=function(){return this.category},k.prototype.setReturnUrl=function(a){this.returnUrl=a},k.prototype.getReturnUrl=function(){return this.returnUrl},k.prototype.setCMID=function(a){this.cmid=a},k.prototype.getCMID=function(){return this.cmid},k.prototype.moveFormElementIntoTab=function(b,c){var d=b.find(j.SUBMIT_BUTTON_ELEMENT),e=a('<div class="modal-footer m-t-1" data-region="footer"></div>');b.find(j.FORM_HEADER).addClass("hidden"),b.wrap(c),d.closest(j.BUTTON_CONTAINER).remove(),d.appendTo(e),e.appendTo(b)},k.prototype.moveTabsIntoTabContent=function(a){var b=this.getBody().find(j.TAB_CONTENT).empty();a.find('[role="tabpanel"]').wrapAll(b)},k.prototype.moveCancelButtonToTabs=function(a){var b=a.find(j.CANCEL_BUTTON_ELEMENT).addClass("m-l-1"),c=a.find('[data-region="footer"]');b.closest(j.BUTTON_CONTAINER).remove(),b.clone().appendTo(c)},k.prototype.loadForm=function(){return g.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:this.getAddOnPageId(),cat:this.getCategory(),returnurl:this.getReturnUrl(),cmid:this.getCMID()}).then(function(b,c){var d=a(b),e=d.find(j.EXISTING_CATEGORY_FORM_ELEMENT),f=this.getBody().find(j.EXISTING_CATEGORY_CONTAINER),g=d.find(j.NEW_CATEGORY_FORM_ELEMENT),i=this.getBody().find(j.NEW_CATEGORY_CONTAINER);this.moveFormElementIntoTab(e,f),this.moveFormElementIntoTab(g,i),this.moveTabsIntoTabContent(d),this.moveCancelButtonToTabs(d),h.replaceNode(this.getBody().find(j.TAB_CONTENT),d,c)}.bind(this)).then(function(){b.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()})}).fail(c.exception)},k.prototype.show=function(){d.prototype.show.call(this),this.loadedForm||(this.loadForm(),this.loadedForm=!0)},i||(f.register(k.TYPE,k,"mod_quiz/modal_add_random_question"),i=!0),k});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/notification","core/custom_interaction_events","core/modal_factory","mod_quiz/modal_quiz_question_bank"],function(a,b,c,d,e){var f={ADD_QUESTION_LINKS:'.menu [data-action="questionbank"]'};return{init:function(g){var h=a("body");d.create({type:e.TYPE,large:!0},[h,f.ADD_QUESTION_LINKS]).then(function(b){return b.setContextId(g),h.on(c.events.activate,f.ADD_QUESTION_LINKS,function(c){var d=a(c.target).closest(f.ADD_QUESTION_LINKS);b.setAddOnPageId(d.attr("data-addonpage")),b.setTitle(d.attr("data-header"))}),b}).fail(b.exception)}}});
|
||||
define(["mod_quiz/add_question_modal_launcher","mod_quiz/modal_quiz_question_bank"],function(a,b){return{init:function(c){a.init(b.TYPE,'.menu [data-action="questionbank"]',c)}}});
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/ajax","core/str","core/notification","core/templates","core/paged_content_factory"],function(a,b,c,d,e,f){var g=5,h="mod_quiz/random_question_form_preview_question_list",i={LOADING_ICON_CONTAINER:'[data-region="overlay-icon-container"]',QUESTION_COUNT_CONTAINER:'[data-region="question-count-container"]',QUESTION_LIST_CONTAINER:'[data-region="question-list-container"]'},j=function(a){a.find(i.LOADING_ICON_CONTAINER).removeClass("hidden")},k=function(a){a.find(i.LOADING_ICON_CONTAINER).addClass("hidden")},l=function(a,b){c.get_string("questionsmatchingfilter","mod_quiz",b).then(function(b){a.find(i.QUESTION_COUNT_CONTAINER).html(b)}).fail(d.exception)},m=function(a,c,d,e,f,g){var h={methodname:"core_question_get_random_question_summaries",args:{categoryid:a,includesubcategories:c,tagids:d,contextid:e,limit:f,offset:g}};return b.call([h])[0]},n=function(a,b,c,i,j,k){return f.createFromAjax(j,g,function(f){return f.map(function(f){var g=f.limit,j=f.offset;return 0==j?e.render(h,{questions:k}):m(a,b,c,i,g,j).then(function(a){var b=a.questions;return e.render(h,{questions:b})}).fail(d.exception)})})},o=function(b,c,f,h,o){return j(b),m(c,f,h,o,g,0).then(function(a){var c=a.totalcount;return l(b,c),a}).then(function(b){var d=b.totalcount,e=b.questions;return e.length?n(c,f,h,o,d,e):a.Deferred().resolve("","")}).then(function(a,c){var d=b.find(i.QUESTION_LIST_CONTAINER);e.replaceNodeContents(d,a,c)}).always(function(){k(b)}).fail(d.exception)};return{reload:o,showLoadingIcon:j,hideLoadingIcon:k}});
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Initialise the an add question modal on the quiz page.
|
||||
*
|
||||
* @module mod_quiz/add_question_modal_launcher
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/notification',
|
||||
'core/modal_factory',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Notification,
|
||||
ModalFactory
|
||||
) {
|
||||
|
||||
return {
|
||||
/**
|
||||
* Create a modal using the modal factory and add listeners to launch the
|
||||
* modal when clicked.
|
||||
*
|
||||
* @param {string} modalType Which modal to create
|
||||
* @param {string} selector The selectors for the elements that trigger the modal
|
||||
* @param {int} contextId The current context id
|
||||
* @param {function} preShowCallback A callback to execute before the modal is shown
|
||||
* @return {promise} Resolved with the modal
|
||||
*/
|
||||
init: function(modalType, selector, contextId, preShowCallback) {
|
||||
var body = $('body');
|
||||
|
||||
// Create a question bank modal using the factory.
|
||||
// The same modal will be used by all of the add question
|
||||
// links that match "selector" on the page. The content
|
||||
// of the modal will be changed depending on which link is
|
||||
// clicked.
|
||||
return ModalFactory.create(
|
||||
{
|
||||
type: modalType,
|
||||
large: true,
|
||||
// This callback executes before the modal is shown when the
|
||||
// trigger element is clicked.
|
||||
preShowCallback: function(triggerElement, modal) {
|
||||
triggerElement = $(triggerElement);
|
||||
modal.setContextId(contextId);
|
||||
modal.setAddOnPageId(triggerElement.attr('data-addonpage'));
|
||||
modal.setTitle(triggerElement.attr('data-header'));
|
||||
|
||||
if (preShowCallback) {
|
||||
preShowCallback(triggerElement, modal);
|
||||
}
|
||||
}
|
||||
},
|
||||
// Created a deligated listener rather than a single
|
||||
// trigger element.
|
||||
[body, selector]
|
||||
).fail(Notification.exception);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
// 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 the add_random_form class.
|
||||
*
|
||||
* @module mod_quiz/add_random_form
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'mod_quiz/random_question_form_preview'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
RandomQuestionFormPreview
|
||||
) {
|
||||
|
||||
// Wait 2 seconds before reloading the question set just in case
|
||||
// the user is still changing the criteria.
|
||||
var RELOAD_DELAY = 2000;
|
||||
var SELECTORS = {
|
||||
PREVIEW_CONTAINER: '[data-region="random-question-preview-container"]',
|
||||
CATEGORY_FORM_ELEMENT: '[name="category"]',
|
||||
SUBCATEGORY_FORM_ELEMENT: '[name="includesubcategories"]',
|
||||
TAG_IDS_FORM_ELEMENT: '[name="fromtags[]"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the category id from the form.
|
||||
*
|
||||
* @param {jquery} form The form element.
|
||||
* @return {string} The category id.
|
||||
*/
|
||||
var getCategoryId = function(form) {
|
||||
// The value string is the category id and category context id joined
|
||||
// by a comma.
|
||||
var valueString = form.find(SELECTORS.CATEGORY_FORM_ELEMENT).val();
|
||||
// Split the two ids.
|
||||
var values = valueString.split(',');
|
||||
// Return just the category id.
|
||||
return values[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the form indicates we should include include subcategories in
|
||||
* the filter.
|
||||
*
|
||||
* @param {jquery} form The form element.
|
||||
* @return {bool}
|
||||
*/
|
||||
var shouldIncludeSubcategories = function(form) {
|
||||
return form.find(SELECTORS.SUBCATEGORY_FORM_ELEMENT).is(':checked');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the tag ids for the selected tags in the form.
|
||||
*
|
||||
* @param {jquery} form The form element.
|
||||
* @return {string[]} The tag ids.
|
||||
*/
|
||||
var getTagIds = function(form) {
|
||||
var values = form.find(SELECTORS.TAG_IDS_FORM_ELEMENT).val();
|
||||
return values.map(function(value) {
|
||||
// The tag element value is the tag id and tag name joined
|
||||
// by a comma. So we need to split them to get the tag id.
|
||||
var parts = value.split(',');
|
||||
return parts[0];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload the preview section with a new set of filters.
|
||||
*
|
||||
* @param {jquery} form The form element.
|
||||
* @param {int} contextId The current context id.
|
||||
*/
|
||||
var reloadQuestionPreview = function(form, contextId) {
|
||||
var previewContainer = form.find(SELECTORS.PREVIEW_CONTAINER);
|
||||
RandomQuestionFormPreview.reload(
|
||||
previewContainer,
|
||||
getCategoryId(form),
|
||||
shouldIncludeSubcategories(form),
|
||||
getTagIds(form),
|
||||
contextId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Is this an element we're interested in listening to changes on.
|
||||
*
|
||||
* @param {jquery} element The element to check.
|
||||
* @return {bool}
|
||||
*/
|
||||
var isInterestingElement = function(element) {
|
||||
if (element.closest(SELECTORS.CATEGORY_FORM_ELEMENT).length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (element.closest(SELECTORS.SUBCATEGORY_FORM_ELEMENT).length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (element.closest(SELECTORS.TAG_IDS_FORM_ELEMENT).length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for changes to any of the interesting elements and reload the form
|
||||
* preview with the new filter values if they are changed.
|
||||
*
|
||||
* The reload is delayed for a small amount of time (see RELOAD_DELAY) in case
|
||||
* the user is actively editing the form. This allows us to avoid having to
|
||||
* send multiple requests to the server on each change.
|
||||
*
|
||||
* Instead we can just send a single request when the user appears to have
|
||||
* finished editing the form.
|
||||
*
|
||||
* @param {jquery} form The form element.
|
||||
* @param {int} contextId The current context id.
|
||||
*/
|
||||
var addEventListeners = function(form, contextId) {
|
||||
var reloadTimerId = null;
|
||||
|
||||
form.on('change', function(e) {
|
||||
// Only reload the preview when elements that will change the result
|
||||
// are modified.
|
||||
if (!isInterestingElement($(e.target))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the loading icon to let the user know that the preview
|
||||
// will be updated after their actions.
|
||||
RandomQuestionFormPreview.showLoadingIcon(form);
|
||||
|
||||
if (reloadTimerId) {
|
||||
// Reset the timer each time the form is modified.
|
||||
clearTimeout(reloadTimerId);
|
||||
}
|
||||
|
||||
// Don't immediately reload the question preview section just
|
||||
// in case the user is still modifying the form. We don't want to
|
||||
// spam reload requests.
|
||||
reloadTimerId = setTimeout(function() {
|
||||
reloadQuestionPreview(form, contextId);
|
||||
}, RELOAD_DELAY);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger the first load of the preview section and then listen for modifications
|
||||
* to the form to reload the preview with new filter values.
|
||||
*
|
||||
* @param {jquery} formId The form element id.
|
||||
* @param {int} contextId The current context id.
|
||||
*/
|
||||
var init = function(formId, contextId) {
|
||||
var form = $('#' + formId);
|
||||
|
||||
reloadQuestionPreview(form, contextId);
|
||||
addEventListeners(form, contextId);
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Initialise the add random question modal on the quiz page.
|
||||
*
|
||||
* @module mod_quiz/add_random_question
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'mod_quiz/add_question_modal_launcher',
|
||||
'mod_quiz/modal_add_random_question'
|
||||
],
|
||||
function(
|
||||
AddQuestionModalLauncher,
|
||||
ModalAddRandomQuestion
|
||||
) {
|
||||
|
||||
return {
|
||||
/**
|
||||
* Create the add random question modal.
|
||||
*
|
||||
* @param {int} contextId Current context id.
|
||||
* @param {string} category Category id and category context id comma separated.
|
||||
* @param {string} returnUrl URL to return to after form submission.
|
||||
* @param {int} cmid Current course module id.
|
||||
*/
|
||||
init: function(contextId, category, returnUrl, cmid) {
|
||||
AddQuestionModalLauncher.init(
|
||||
ModalAddRandomQuestion.TYPE,
|
||||
'.menu [data-action="addarandomquestion"]',
|
||||
contextId,
|
||||
// Additional values that should be set before the modal is shown.
|
||||
function(triggerElement, modal) {
|
||||
modal.setCategory(category);
|
||||
modal.setReturnUrl(returnUrl);
|
||||
modal.setCMID(cmid);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Contain the logic for the add random question modal.
|
||||
*
|
||||
* @module mod_quiz/modal_add_random_question
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define([
|
||||
'jquery',
|
||||
'core/yui',
|
||||
'core/notification',
|
||||
'core/modal',
|
||||
'core/modal_events',
|
||||
'core/modal_registry',
|
||||
'core/fragment',
|
||||
'core/templates',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Y,
|
||||
Notification,
|
||||
Modal,
|
||||
ModalEvents,
|
||||
ModalRegistry,
|
||||
Fragment,
|
||||
Templates
|
||||
) {
|
||||
|
||||
var registered = false;
|
||||
var SELECTORS = {
|
||||
EXISTING_CATEGORY_CONTAINER: '[data-region="existing-category-container"]',
|
||||
EXISTING_CATEGORY_FORM_ELEMENT: '#id_existingcategoryheader',
|
||||
NEW_CATEGORY_CONTAINER: '[data-region="new-category-container"]',
|
||||
NEW_CATEGORY_FORM_ELEMENT: '#id_newcategoryheader',
|
||||
TAB_CONTENT: '[data-region="tab-content"]',
|
||||
ADD_ON_PAGE_FORM_ELEMENT: '[name="addonpage"]',
|
||||
SUBMIT_BUTTON_ELEMENT: 'input[type="submit"]',
|
||||
CANCEL_BUTTON_ELEMENT: 'input[type="submit"][name="cancel"]',
|
||||
FORM_HEADER: 'legend',
|
||||
BUTTON_CONTAINER: '.fitem'
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for the Modal.
|
||||
*
|
||||
* @param {object} root The root jQuery element for the modal
|
||||
*/
|
||||
var ModalAddRandomQuestion = function(root) {
|
||||
Modal.call(this, root);
|
||||
this.contextId = null;
|
||||
this.addOnPageId = null;
|
||||
this.category = null;
|
||||
this.returnUrl = null;
|
||||
this.cmid = null;
|
||||
this.loadedForm = false;
|
||||
};
|
||||
|
||||
ModalAddRandomQuestion.TYPE = 'mod_quiz-quiz-add-random-question';
|
||||
ModalAddRandomQuestion.prototype = Object.create(Modal.prototype);
|
||||
ModalAddRandomQuestion.prototype.constructor = ModalAddRandomQuestion;
|
||||
|
||||
/**
|
||||
* Save the Moodle context id that the question bank is being
|
||||
* rendered in.
|
||||
*
|
||||
* @method setContextId
|
||||
* @param {int} id
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.setContextId = function(id) {
|
||||
this.contextId = id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the saved Moodle context id.
|
||||
*
|
||||
* @method getContextId
|
||||
* @return {int}
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.getContextId = function() {
|
||||
return this.contextId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the id of the page that the question should be added to
|
||||
* when the user clicks the add to quiz link.
|
||||
*
|
||||
* @method setAddOnPageId
|
||||
* @param {int} id
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.setAddOnPageId = function(id) {
|
||||
this.addOnPageId = id;
|
||||
this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the saved page id for the question to be added to.
|
||||
*
|
||||
* @method getAddOnPageId
|
||||
* @return {int}
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.getAddOnPageId = function() {
|
||||
return this.addOnPageId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the category for this form. The category is a comma separated
|
||||
* category id and category context id.
|
||||
*
|
||||
* @method setCategory
|
||||
* @param {string} category
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.setCategory = function(category) {
|
||||
this.category = category;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the saved category.
|
||||
*
|
||||
* @method getCategory
|
||||
* @return {string}
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.getCategory = function() {
|
||||
return this.category;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the return URL for the form.
|
||||
*
|
||||
* @method setReturnUrl
|
||||
* @param {string} url
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.setReturnUrl = function(url) {
|
||||
this.returnUrl = url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the return URL for the form.
|
||||
*
|
||||
* @method getReturnUrl
|
||||
* @return {string}
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.getReturnUrl = function() {
|
||||
return this.returnUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the course module id for the form.
|
||||
*
|
||||
* @method setCMID
|
||||
* @param {int} id
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.setCMID = function(id) {
|
||||
this.cmid = id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the course module id for the form.
|
||||
*
|
||||
* @method getCMID
|
||||
* @return {int}
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.getCMID = function() {
|
||||
return this.cmid;
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves a given form element inside (a child of) a given tab element.
|
||||
*
|
||||
* Hides the 'legend' (e.g. header) element of the form element because the
|
||||
* tab has the name.
|
||||
*
|
||||
* Moves the submit button into a footer element at the bottom of the form
|
||||
* element for styling purposes.
|
||||
*
|
||||
* @method moveFormElementIntoTab
|
||||
* @param {jquery} formElement The form element to move into the tab.
|
||||
* @param {jquey} tabElement The tab element for the form element to move into.
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.moveFormElementIntoTab = function(formElement, tabElement) {
|
||||
var submitButtons = formElement.find(SELECTORS.SUBMIT_BUTTON_ELEMENT);
|
||||
var footer = $('<div class="modal-footer m-t-1" data-region="footer"></div>');
|
||||
// Hide the header because the tabs show us which part of the form we're
|
||||
// looking at.
|
||||
formElement.find(SELECTORS.FORM_HEADER).addClass('hidden');
|
||||
// Move the element inside a tab.
|
||||
formElement.wrap(tabElement);
|
||||
// Remove the buttons container element.
|
||||
submitButtons.closest(SELECTORS.BUTTON_CONTAINER).remove();
|
||||
// Put the button inside a footer.
|
||||
submitButtons.appendTo(footer);
|
||||
// Add the footer to the end of the category form element.
|
||||
footer.appendTo(formElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* Empty the tab content container and move all tabs from the form into the
|
||||
* tab container element.
|
||||
*
|
||||
* @method moveTabsIntoTabContent
|
||||
* @param {jquery} form The form element.
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.moveTabsIntoTabContent = function(form) {
|
||||
// Empty it to remove the loading icon.
|
||||
var tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();
|
||||
// Make sure all tabs are inside the tab content element.
|
||||
form.find('[role="tabpanel"]').wrapAll(tabContent);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make sure all of the tabs have a cancel button in their fotter to sit along
|
||||
* side the submit button.
|
||||
*
|
||||
* @method moveCancelButtonToTabs
|
||||
* @param {jquey} form The form element.
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.moveCancelButtonToTabs = function(form) {
|
||||
var cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('m-l-1');
|
||||
var tabFooters = form.find('[data-region="footer"]');
|
||||
// Remove the buttons container element.
|
||||
cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();
|
||||
cancelButton.clone().appendTo(tabFooters);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the add random question form in a fragement and perform some transformation
|
||||
* on the HTML to convert it into tabs for rendering in the modal.
|
||||
*
|
||||
* @method loadForm
|
||||
* @return {promise} Resolved with form HTML and JS.
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.loadForm = function() {
|
||||
return Fragment.loadFragment(
|
||||
'mod_quiz',
|
||||
'add_random_question_form',
|
||||
this.getContextId(),
|
||||
{
|
||||
addonpage: this.getAddOnPageId(),
|
||||
cat: this.getCategory(),
|
||||
returnurl: this.getReturnUrl(),
|
||||
cmid: this.getCMID()
|
||||
}
|
||||
)
|
||||
.then(function(html, js) {
|
||||
var form = $(html);
|
||||
var existingCategoryFormElement = form.find(SELECTORS.EXISTING_CATEGORY_FORM_ELEMENT);
|
||||
var existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);
|
||||
var newCategoryFormElement = form.find(SELECTORS.NEW_CATEGORY_FORM_ELEMENT);
|
||||
var newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);
|
||||
|
||||
// Transform the form into tabs for better rendering in the modal.
|
||||
this.moveFormElementIntoTab(existingCategoryFormElement, existingCategoryTab);
|
||||
this.moveFormElementIntoTab(newCategoryFormElement, newCategoryTab);
|
||||
this.moveTabsIntoTabContent(form);
|
||||
this.moveCancelButtonToTabs(form);
|
||||
|
||||
Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);
|
||||
return;
|
||||
}.bind(this))
|
||||
.then(function() {
|
||||
// Make sure the form change checker is disabled otherwise it'll
|
||||
// stop the user from navigating away from the page once the modal
|
||||
// is hidden.
|
||||
Y.use('moodle-core-formchangechecker', function() {
|
||||
M.core_formchangechecker.reset_form_dirty_state();
|
||||
});
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the modal show function to load the form when this modal is first
|
||||
* shown.
|
||||
*
|
||||
* @method show
|
||||
*/
|
||||
ModalAddRandomQuestion.prototype.show = function() {
|
||||
Modal.prototype.show.call(this);
|
||||
|
||||
if (!this.loadedForm) {
|
||||
this.loadForm();
|
||||
this.loadedForm = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Automatically register with the modal registry the first time this module is
|
||||
// imported so that you can create modals of this type using the modal factory.
|
||||
if (!registered) {
|
||||
ModalRegistry.register(
|
||||
ModalAddRandomQuestion.TYPE,
|
||||
ModalAddRandomQuestion,
|
||||
'mod_quiz/modal_add_random_question'
|
||||
);
|
||||
|
||||
registered = true;
|
||||
}
|
||||
|
||||
return ModalAddRandomQuestion;
|
||||
});
|
||||
@@ -23,55 +23,26 @@
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/notification',
|
||||
'core/custom_interaction_events',
|
||||
'core/modal_factory',
|
||||
'mod_quiz/add_question_modal_launcher',
|
||||
'mod_quiz/modal_quiz_question_bank'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Notification,
|
||||
CustomEvents,
|
||||
ModalFactory,
|
||||
AddQuestionModalLauncher,
|
||||
ModalQuizQuestionBank
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ADD_QUESTION_LINKS: '.menu [data-action="questionbank"]',
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Create the question bank modal.
|
||||
*
|
||||
* @param {int} contextId Current context id.
|
||||
*/
|
||||
init: function(contextId) {
|
||||
var body = $('body');
|
||||
|
||||
// Create a question bank modal using the factory.
|
||||
// The same modal will be used by all of the add question
|
||||
// links on the page. The content of the modal will be
|
||||
// changed depending on which link is clicked.
|
||||
ModalFactory.create(
|
||||
{
|
||||
type: ModalQuizQuestionBank.TYPE,
|
||||
large: true
|
||||
},
|
||||
// Created a deligated listener rather than a single
|
||||
// trigger element.
|
||||
[body, SELECTORS.ADD_QUESTION_LINKS]
|
||||
).then(function(modal) {
|
||||
// Save the Moodle context id that the modal is being rendered in.
|
||||
modal.setContextId(contextId);
|
||||
|
||||
body.on(CustomEvents.events.activate, SELECTORS.ADD_QUESTION_LINKS, function(e) {
|
||||
// We need to listen for activations on the trigger elements because there are
|
||||
// several on the page and we need to know which one was activated in order to
|
||||
// set some relevant data on the modal.
|
||||
var triggerElement = $(e.target).closest(SELECTORS.ADD_QUESTION_LINKS);
|
||||
modal.setAddOnPageId(triggerElement.attr('data-addonpage'));
|
||||
modal.setTitle(triggerElement.attr('data-header'));
|
||||
});
|
||||
|
||||
return modal;
|
||||
}).fail(Notification.exception);
|
||||
AddQuestionModalLauncher.init(
|
||||
ModalQuizQuestionBank.TYPE,
|
||||
'.menu [data-action="questionbank"]',
|
||||
contextId
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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 the random_question_form_preview of the
|
||||
* add_random_form class.
|
||||
*
|
||||
* @module mod_quiz/random_question_form_preview
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/ajax',
|
||||
'core/str',
|
||||
'core/notification',
|
||||
'core/templates',
|
||||
'core/paged_content_factory'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Ajax,
|
||||
Str,
|
||||
Notification,
|
||||
Templates,
|
||||
PagedContentFactory
|
||||
) {
|
||||
|
||||
var ITEMS_PER_PAGE = 5;
|
||||
var TEMPLATE_NAME = 'mod_quiz/random_question_form_preview_question_list';
|
||||
var SELECTORS = {
|
||||
LOADING_ICON_CONTAINER: '[data-region="overlay-icon-container"]',
|
||||
QUESTION_COUNT_CONTAINER: '[data-region="question-count-container"]',
|
||||
QUESTION_LIST_CONTAINER: '[data-region="question-list-container"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the loading spinner over the preview section.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
*/
|
||||
var showLoadingIcon = function(root) {
|
||||
root.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the loading spinner.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
*/
|
||||
var hideLoadingIcon = function(root) {
|
||||
root.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the section of text to show the question count.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {int} questionCount The number of questions.
|
||||
*/
|
||||
var renderQuestionCount = function(root, questionCount) {
|
||||
Str.get_string('questionsmatchingfilter', 'mod_quiz', questionCount)
|
||||
.then(function(string) {
|
||||
root.find(SELECTORS.QUESTION_COUNT_CONTAINER).html(string);
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a request to the server for more questions.
|
||||
*
|
||||
* @param {int} categoryId A question category id.
|
||||
* @param {bool} includeSubcategories If the results should include subcategory questions
|
||||
* @param {int[]} tagIds The list of tag ids that each question must have.
|
||||
* @param {int} contextId The context where the questions will be added.
|
||||
* @param {int} limit How many questions to retrieve.
|
||||
* @param {int} offset How many questions to skip from the start of the result set.
|
||||
* @return {promise} Resolved when the preview section has rendered.
|
||||
*/
|
||||
var requestQuestions = function(
|
||||
categoryId,
|
||||
includeSubcategories,
|
||||
tagIds,
|
||||
contextId,
|
||||
limit,
|
||||
offset
|
||||
) {
|
||||
var request = {
|
||||
methodname: 'core_question_get_random_question_summaries',
|
||||
args: {
|
||||
categoryid: categoryId,
|
||||
includesubcategories: includeSubcategories,
|
||||
tagids: tagIds,
|
||||
contextid: contextId,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}
|
||||
};
|
||||
|
||||
return Ajax.call([request])[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a paged content widget for questions with the given criteria. The
|
||||
* criteria is used to fetch more questions from the server as the user
|
||||
* requests new pages.
|
||||
*
|
||||
* @param {int} categoryId A question category id.
|
||||
* @param {bool} includeSubcategories If the results should include subcategory questions
|
||||
* @param {int[]} tagIds The list of tag ids that each question must have.
|
||||
* @param {int} contextId The context where the questions will be added.
|
||||
* @param {int} totalQuestionCount How many questions match the criteria above.
|
||||
* @param {object[]} firstPageQuestions List of questions for the first page.
|
||||
* @return {promise} A promise resolved with the HTML and JS for the paged content.
|
||||
*/
|
||||
var renderQuestionsAsPagedContent = function(
|
||||
categoryId,
|
||||
includeSubcategories,
|
||||
tagIds,
|
||||
contextId,
|
||||
totalQuestionCount,
|
||||
firstPageQuestions
|
||||
) {
|
||||
// Provide a callback, renderQuestionsPages,
|
||||
// to control how the questions on each page are rendered.
|
||||
return PagedContentFactory.createFromAjax(
|
||||
totalQuestionCount,
|
||||
ITEMS_PER_PAGE,
|
||||
// Callback function to render the requested pages.
|
||||
function(pagesData) {
|
||||
return pagesData.map(function(pageData) {
|
||||
var limit = pageData.limit;
|
||||
var offset = pageData.offset;
|
||||
|
||||
if (offset == 0) {
|
||||
// The first page is being requested and we've already got
|
||||
// that data so we can just render it immediately.
|
||||
return Templates.render(TEMPLATE_NAME, {questions: firstPageQuestions});
|
||||
} else {
|
||||
// Otherwise we need to ask the server for the data.
|
||||
return requestQuestions(
|
||||
categoryId,
|
||||
includeSubcategories,
|
||||
tagIds,
|
||||
contextId,
|
||||
limit,
|
||||
offset
|
||||
)
|
||||
.then(function(response) {
|
||||
var questions = response.questions;
|
||||
return Templates.render(TEMPLATE_NAME, {questions: questions});
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-render the preview section based on the provided filter criteria.
|
||||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {int} categoryId A question category id.
|
||||
* @param {bool} includeSubcategories If the results should include subcategory questions
|
||||
* @param {int[]} tagIds The list of tag ids that each question must have.
|
||||
* @param {int} contextId The context where the questions will be added.
|
||||
* @return {promise} Resolved when the preview section has rendered.
|
||||
*/
|
||||
var reload = function(root, categoryId, includeSubcategories, tagIds, contextId) {
|
||||
// Show the loading spinner to tell the user that something is happening.
|
||||
showLoadingIcon(root);
|
||||
// Load the first set of questions.
|
||||
return requestQuestions(categoryId, includeSubcategories, tagIds, contextId, ITEMS_PER_PAGE, 0)
|
||||
.then(function(response) {
|
||||
var totalCount = response.totalcount;
|
||||
// Show the help message for the user to indicate how many questions
|
||||
// match their filter criteria.
|
||||
renderQuestionCount(root, totalCount);
|
||||
return response;
|
||||
})
|
||||
.then(function(response) {
|
||||
var totalQuestionCount = response.totalcount;
|
||||
var questions = response.questions;
|
||||
|
||||
if (questions.length) {
|
||||
// We received some questions so render them as paged content
|
||||
// with a paging bar.
|
||||
return renderQuestionsAsPagedContent(
|
||||
categoryId,
|
||||
includeSubcategories,
|
||||
tagIds,
|
||||
contextId,
|
||||
totalQuestionCount,
|
||||
questions
|
||||
);
|
||||
} else {
|
||||
// If we didn't receive any questions then we can return empty
|
||||
// HTML and JS to clear the preview section.
|
||||
return $.Deferred().resolve('', '');
|
||||
}
|
||||
})
|
||||
.then(function(html, js) {
|
||||
// Show the user the question set.
|
||||
var container = root.find(SELECTORS.QUESTION_LIST_CONTAINER);
|
||||
Templates.replaceNodeContents(container, html, js);
|
||||
return;
|
||||
})
|
||||
.always(function() {
|
||||
hideLoadingIcon(root);
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
};
|
||||
|
||||
return {
|
||||
reload: reload,
|
||||
showLoadingIcon: showLoadingIcon,
|
||||
hideLoadingIcon: hideLoadingIcon
|
||||
};
|
||||
});
|
||||
@@ -98,17 +98,17 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
|
||||
// Include the contents of any other popups required.
|
||||
if ($structure->can_be_edited()) {
|
||||
$popups = '';
|
||||
|
||||
$thiscontext = $contexts->lowest();
|
||||
$this->page->requires->js_call_amd('mod_quiz/quizquestionbank', 'init', [
|
||||
$contexts->lowest()->id
|
||||
$thiscontext->id
|
||||
]);
|
||||
|
||||
$popups .= $this->random_question_form($pageurl, $contexts, $pagevars);
|
||||
$this->page->requires->yui_module('moodle-mod_quiz-randomquestion',
|
||||
'M.mod_quiz.randomquestion.init');
|
||||
|
||||
$output .= html_writer::div($popups, 'mod_quiz_edit_forms');
|
||||
$this->page->requires->js_call_amd('mod_quiz/add_random_question', 'init', [
|
||||
$thiscontext->id,
|
||||
$pagevars['cat'],
|
||||
$pageurl->out_as_local_url(true),
|
||||
$pageurl->param('cmid')
|
||||
]);
|
||||
|
||||
// Include the question chooser.
|
||||
$output .= $this->question_chooser();
|
||||
@@ -1059,29 +1059,6 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
return html_writer::div($this->pix_icon('i/loading', get_string('loading')), 'questionbankloading');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return random question form.
|
||||
* @param \moodle_url $thispageurl the canonical URL of this page.
|
||||
* @param \question_edit_contexts $contexts the relevant question bank contexts.
|
||||
* @param array $pagevars the variables from {@link \question_edit_setup()}.
|
||||
* @return string HTML to output.
|
||||
*/
|
||||
protected function random_question_form(\moodle_url $thispageurl, \question_edit_contexts $contexts, array $pagevars) {
|
||||
|
||||
if (!$contexts->have_cap('moodle/question:useall')) {
|
||||
return '';
|
||||
}
|
||||
$randomform = new \quiz_add_random_form(new \moodle_url('/mod/quiz/addrandom.php'),
|
||||
array('contexts' => $contexts, 'cat' => $pagevars['cat']));
|
||||
$randomform->set_data(array(
|
||||
'category' => $pagevars['cat'],
|
||||
'returnurl' => $thispageurl->out_as_local_url(true),
|
||||
'randomnumber' => 1,
|
||||
'cmid' => $thispageurl->param('cmid'),
|
||||
));
|
||||
return html_writer::div($randomform->render(), 'randomquestionformforpopup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the JavaScript for the general editing. (JavaScript for popups
|
||||
* is handled with the specific code for those.)
|
||||
|
||||
@@ -341,6 +341,7 @@ $string['eventreportviewed'] = 'Quiz report viewed';
|
||||
$string['everynquestions'] = 'Every {$a} questions';
|
||||
$string['everyquestion'] = 'Every question';
|
||||
$string['everythingon'] = 'Everything on';
|
||||
$string['existingcategory'] = 'Existing category';
|
||||
$string['exportcategory'] = 'export category';
|
||||
$string['exporterror'] = 'An error occurred during export processing';
|
||||
$string['exportingquestions'] = 'Questions are being exported to file';
|
||||
@@ -509,6 +510,7 @@ $string['navmethod_seq'] = 'Sequential';
|
||||
$string['navnojswarning'] = 'Warning: these links will not save your answers. Use the next button at the bottom of the page.';
|
||||
$string['neverallononepage'] = 'Never, all questions on one page';
|
||||
$string['newattemptfail'] = 'Error: Could not start a new attempt at the quiz';
|
||||
$string['newcategory'] = 'New category';
|
||||
$string['newpage'] = 'New page';
|
||||
$string['newpage_help'] = 'For longer quizzes it makes sense to stretch the quiz over several pages by limiting the number of questions per page. When adding questions to the quiz, page breaks will automatically be inserted according to this setting. However page breaks may later be moved manually on the editing page.';
|
||||
$string['newpageevery'] = 'Automatically start a new page';
|
||||
@@ -649,8 +651,10 @@ $string['questionnotloaded'] = 'Question {$a} has not been loaded from the datab
|
||||
$string['questionorder'] = 'Question order';
|
||||
$string['questionposition'] = 'New position in order for question {$a}';
|
||||
$string['questions'] = 'Questions';
|
||||
$string['questionsetpreview'] = 'Question set preview';
|
||||
$string['questionsinclhidden'] = 'Questions (including hidden)';
|
||||
$string['questionsinthisquiz'] = 'Questions in this quiz';
|
||||
$string['questionsmatchingfilter'] = 'Questions matching this filter: {$a}';
|
||||
$string['questionsperpage'] = 'Questions per page';
|
||||
$string['questionsperpageselected'] = 'Questions per page has been set so the paging is currently fixed. As a result, the paging controls have been disabled. You can change this in {$a}.';
|
||||
$string['questionsperpagex'] = 'Questions per page: {$a}';
|
||||
|
||||
+44
-1
@@ -2406,7 +2406,7 @@ function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the question bank in a fargment output. This allows
|
||||
* Generates the question bank in a fragment output. This allows
|
||||
* the question bank to be displayed in a modal.
|
||||
*
|
||||
* The only expected argument provided in the $args array is
|
||||
@@ -2445,3 +2445,46 @@ function mod_quiz_output_fragment_quiz_question_bank($args) {
|
||||
$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
|
||||
return $renderer->question_bank_contents($questionbank, $pagevars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the add random question in a fragment output. This allows the
|
||||
* form to be rendered in javascript, for example inside a modal.
|
||||
*
|
||||
* The required arguments as keys in the $args array are:
|
||||
* cat {string} The category and category context ids comma separated.
|
||||
* addonpage {int} The page id to add this question to.
|
||||
* returnurl {string} URL to return to after form submission.
|
||||
* cmid {int} The course module id the questions are being added to.
|
||||
*
|
||||
* @param array $args The fragment arguments.
|
||||
* @return string The rendered mform fragment.
|
||||
*/
|
||||
function mod_quiz_output_fragment_add_random_question_form($args) {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
|
||||
|
||||
$contexts = new \question_edit_contexts($args['context']);
|
||||
$formoptions = [
|
||||
'contexts' => $contexts,
|
||||
'cat' => $args['cat']
|
||||
];
|
||||
$formdata = [
|
||||
'category' => $args['cat'],
|
||||
'addonpage' => $args['addonpage'],
|
||||
'returnurl' => $args['returnurl'],
|
||||
'cmid' => $args['cmid']
|
||||
];
|
||||
|
||||
$form = new quiz_add_random_form(
|
||||
new \moodle_url('/mod/quiz/addrandom.php'),
|
||||
$formoptions,
|
||||
'post',
|
||||
'',
|
||||
null,
|
||||
true,
|
||||
$formdata
|
||||
);
|
||||
$form->set_data($formdata);
|
||||
|
||||
return $form->render();
|
||||
}
|
||||
|
||||
@@ -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/>.
|
||||
}}
|
||||
{{!
|
||||
@template mod_quiz/modal_add_random_question
|
||||
|
||||
Modal for adding a random question to a quiz activity.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* title A cleaned string (use clean_text()) to display.
|
||||
* body HTML content for the boday
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"title": "Example modal",
|
||||
"body": "Some example content for the body"
|
||||
}
|
||||
}}
|
||||
|
||||
{{< core/modal }}
|
||||
{{$body}}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active"
|
||||
data-toggle="tab"
|
||||
href="#existing-category-{{uniqid}}"
|
||||
role="tab">
|
||||
{{#str}} existingcategory, mod_quiz {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
data-toggle="tab"
|
||||
href="#new-category-{{uniqid}}"
|
||||
role="tab">
|
||||
{{#str}} newcategory, mod_quiz {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" data-region="tab-content">
|
||||
<div class="text-sm-center p-t-2" data-region="loading-container">
|
||||
{{> core/loading }}
|
||||
</div>
|
||||
<div class="tab-pane active p-t-1"
|
||||
id="existing-category-{{uniqid}}"
|
||||
role="tabpanel"
|
||||
data-region="existing-category-container">
|
||||
</div>
|
||||
<div class="tab-pane p-t-1"
|
||||
id="new-category-{{uniqid}}"
|
||||
role="tabpanel"
|
||||
data-region="new-category-container">
|
||||
</div>
|
||||
</div>
|
||||
{{/body}}
|
||||
{{/ core/modal }}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template mod_quiz/random_question_form_preview
|
||||
|
||||
A preview section for the list of questions that match the random
|
||||
question criteria in the random question form.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* title A cleaned string (use clean_text()) to display.
|
||||
* body HTML content for the boday
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div class="m-t-2 position-relative" data-region="random-question-preview-container">
|
||||
<div data-region="question-count-container"></div>
|
||||
<div data-region="question-list-container"></div>
|
||||
{{< core/overlay_loading }}{{$hiddenclass}}{{/hiddenclass}}{{/ core/overlay_loading }}
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template mod_quiz/random_question_form_preview_question_list
|
||||
|
||||
A preview section for the list of questions that match the random
|
||||
question criteria in the random question form.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* title A cleaned string (use clean_text()) to display.
|
||||
* body HTML content for the boday
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"name":"Example question 1"
|
||||
},
|
||||
{
|
||||
"name":"Example question 2"
|
||||
},
|
||||
{
|
||||
"name":"Example question 3"
|
||||
},
|
||||
{
|
||||
"name":"Example question 4"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<ul class="list-group" data-region="question-list">
|
||||
{{#questions}}
|
||||
<li class="list-group-item text-truncate">
|
||||
{{#icon}}{{#pix}} {{key}}, {{component}}, {{{alttext}}} {{/pix}}{{/icon}}
|
||||
{{name}}
|
||||
</li>
|
||||
{{/questions}}
|
||||
</ul>
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
YUI.add('moodle-mod_quiz-randomquestion', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Add a random question functionality for a popup in quiz editing page.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
var CSS = {
|
||||
RANDOMQUESTIONFORM: 'div.randomquestionformforpopup',
|
||||
PAGEHIDDENINPUT: 'input#rform_qpage',
|
||||
RANDOMQUESTIONLINKS: '.menu [data-action="addarandomquestion"]'
|
||||
};
|
||||
|
||||
var PARAMS = {
|
||||
PAGE: 'addonpage',
|
||||
HEADER: 'header',
|
||||
FORM: 'form'
|
||||
};
|
||||
|
||||
var POPUP = function() {
|
||||
POPUP.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(POPUP, Y.Base, {
|
||||
|
||||
dialogue: function(header) {
|
||||
// Create a dialogue on the page and hide it.
|
||||
var config = {
|
||||
headerContent: header,
|
||||
bodyContent: Y.one(CSS.RANDOMQUESTIONFORM),
|
||||
draggable: true,
|
||||
modal: true,
|
||||
zIndex: 1000,
|
||||
centered: false,
|
||||
width: 'auto',
|
||||
visible: false,
|
||||
postmethod: 'form',
|
||||
footerContent: null
|
||||
};
|
||||
var popup = {dialog: null};
|
||||
popup.dialog = new M.core.dialogue(config);
|
||||
popup.dialog.show();
|
||||
},
|
||||
|
||||
initializer: function() {
|
||||
Y.one('body').delegate('click', this.display_dialogue, CSS.RANDOMQUESTIONLINKS, this);
|
||||
},
|
||||
|
||||
display_dialogue: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
Y.one(CSS.RANDOMQUESTIONFORM + ' ' + CSS.PAGEHIDDENINPUT).set('value',
|
||||
e.currentTarget.getData(PARAMS.PAGE));
|
||||
|
||||
this.dialogue(e.currentTarget.getData(PARAMS.HEADER));
|
||||
}
|
||||
});
|
||||
|
||||
M.mod_quiz = M.mod_quiz || {};
|
||||
M.mod_quiz.randomquestion = M.mod_quiz.randomquestion || {};
|
||||
M.mod_quiz.randomquestion.init = function() {
|
||||
return new POPUP();
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "moodle-core-notification-dialogue"]});
|
||||
-1
@@ -1 +0,0 @@
|
||||
YUI.add("moodle-mod_quiz-randomquestion",function(e,t){var n={RANDOMQUESTIONFORM:"div.randomquestionformforpopup",PAGEHIDDENINPUT:"input#rform_qpage",RANDOMQUESTIONLINKS:'.menu [data-action="addarandomquestion"]'},r={PAGE:"addonpage",HEADER:"header",FORM:"form"},i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{dialogue:function(t){var r={headerContent:t,bodyContent:e.one(n.RANDOMQUESTIONFORM),draggable:!0,modal:!0,zIndex:1e3,centered:!1,width:"auto",visible:!1,postmethod:"form",footerContent:null},i={dialog:null};i.dialog=new M.core.dialogue(r),i.dialog.show()},initializer:function(){e.one("body").delegate("click",this.display_dialogue,n.RANDOMQUESTIONLINKS,this)},display_dialogue:function(t){t.preventDefault(),e.one(n.RANDOMQUESTIONFORM+" "+n.PAGEHIDDENINPUT).set("value",t.currentTarget.getData(r.PAGE)),this.dialogue(t.currentTarget.getData(r.HEADER))}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.randomquestion=M.mod_quiz.randomquestion||{},M.mod_quiz.randomquestion.init=function(){return new i}},"@VERSION@",{requires:["base","event","node","io","moodle-core-notification-dialogue"]});
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
YUI.add('moodle-mod_quiz-randomquestion', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Add a random question functionality for a popup in quiz editing page.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
var CSS = {
|
||||
RANDOMQUESTIONFORM: 'div.randomquestionformforpopup',
|
||||
PAGEHIDDENINPUT: 'input#rform_qpage',
|
||||
RANDOMQUESTIONLINKS: '.menu [data-action="addarandomquestion"]'
|
||||
};
|
||||
|
||||
var PARAMS = {
|
||||
PAGE: 'addonpage',
|
||||
HEADER: 'header',
|
||||
FORM: 'form'
|
||||
};
|
||||
|
||||
var POPUP = function() {
|
||||
POPUP.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(POPUP, Y.Base, {
|
||||
|
||||
dialogue: function(header) {
|
||||
// Create a dialogue on the page and hide it.
|
||||
var config = {
|
||||
headerContent: header,
|
||||
bodyContent: Y.one(CSS.RANDOMQUESTIONFORM),
|
||||
draggable: true,
|
||||
modal: true,
|
||||
zIndex: 1000,
|
||||
centered: false,
|
||||
width: 'auto',
|
||||
visible: false,
|
||||
postmethod: 'form',
|
||||
footerContent: null
|
||||
};
|
||||
var popup = {dialog: null};
|
||||
popup.dialog = new M.core.dialogue(config);
|
||||
popup.dialog.show();
|
||||
},
|
||||
|
||||
initializer: function() {
|
||||
Y.one('body').delegate('click', this.display_dialogue, CSS.RANDOMQUESTIONLINKS, this);
|
||||
},
|
||||
|
||||
display_dialogue: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
Y.one(CSS.RANDOMQUESTIONFORM + ' ' + CSS.PAGEHIDDENINPUT).set('value',
|
||||
e.currentTarget.getData(PARAMS.PAGE));
|
||||
|
||||
this.dialogue(e.currentTarget.getData(PARAMS.HEADER));
|
||||
}
|
||||
});
|
||||
|
||||
M.mod_quiz = M.mod_quiz || {};
|
||||
M.mod_quiz.randomquestion = M.mod_quiz.randomquestion || {};
|
||||
M.mod_quiz.randomquestion.init = function() {
|
||||
return new POPUP();
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "moodle-core-notification-dialogue"]});
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "moodle-mod_quiz-randomquestion",
|
||||
"builds": {
|
||||
"moodle-mod_quiz-randomquestion": {
|
||||
"jsfiles": [
|
||||
"randomquestion.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Add a random question functionality for a popup in quiz editing page.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
var CSS = {
|
||||
RANDOMQUESTIONFORM: 'div.randomquestionformforpopup',
|
||||
PAGEHIDDENINPUT: 'input#rform_qpage',
|
||||
RANDOMQUESTIONLINKS: '.menu [data-action="addarandomquestion"]'
|
||||
};
|
||||
|
||||
var PARAMS = {
|
||||
PAGE: 'addonpage',
|
||||
HEADER: 'header',
|
||||
FORM: 'form'
|
||||
};
|
||||
|
||||
var POPUP = function() {
|
||||
POPUP.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(POPUP, Y.Base, {
|
||||
|
||||
dialogue: function(header) {
|
||||
// Create a dialogue on the page and hide it.
|
||||
var config = {
|
||||
headerContent: header,
|
||||
bodyContent: Y.one(CSS.RANDOMQUESTIONFORM),
|
||||
draggable: true,
|
||||
modal: true,
|
||||
zIndex: 1000,
|
||||
centered: false,
|
||||
width: 'auto',
|
||||
visible: false,
|
||||
postmethod: 'form',
|
||||
footerContent: null
|
||||
};
|
||||
var popup = {dialog: null};
|
||||
popup.dialog = new M.core.dialogue(config);
|
||||
popup.dialog.show();
|
||||
},
|
||||
|
||||
initializer: function() {
|
||||
Y.one('body').delegate('click', this.display_dialogue, CSS.RANDOMQUESTIONLINKS, this);
|
||||
},
|
||||
|
||||
display_dialogue: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
Y.one(CSS.RANDOMQUESTIONFORM + ' ' + CSS.PAGEHIDDENINPUT).set('value',
|
||||
e.currentTarget.getData(PARAMS.PAGE));
|
||||
|
||||
this.dialogue(e.currentTarget.getData(PARAMS.HEADER));
|
||||
}
|
||||
});
|
||||
|
||||
M.mod_quiz = M.mod_quiz || {};
|
||||
M.mod_quiz.randomquestion = M.mod_quiz.randomquestion || {};
|
||||
M.mod_quiz.randomquestion.init = function() {
|
||||
return new POPUP();
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"moodle-mod_quiz-randomquestion": {
|
||||
"requires": [
|
||||
"base",
|
||||
"event",
|
||||
"node",
|
||||
"io",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,29 @@ class random_question_loader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of available question ids for the given criteria.
|
||||
*
|
||||
* @param int $categoryid The id of a category in the question bank.
|
||||
* @param bool $includesubcategories Whether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @param array $tagids An array of tag ids. If an array is provided, then
|
||||
* only the questions that are tagged with ALL the provided tagids will be loaded.
|
||||
* @return int[] The list of question ids
|
||||
*/
|
||||
protected function get_question_ids($categoryid, $includesubcategories, $tagids = []) {
|
||||
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
|
||||
$cachedvalues = $this->availablequestionscache[$categorykey];
|
||||
$questionids = [];
|
||||
|
||||
foreach ($cachedvalues as $usecount => $ids) {
|
||||
$questionids = array_merge($questionids, array_keys($ids));
|
||||
}
|
||||
|
||||
return $questionids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given question is available in a given category. If so, mark it used.
|
||||
* If an optional list of tag ids are provided, then the question must be tagged with
|
||||
@@ -235,4 +258,65 @@ class random_question_loader {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of available questions for the given criteria.
|
||||
*
|
||||
* @param int $categoryid The id of a category in the question bank.
|
||||
* @param bool $includesubcategories Whether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @param array $tagids An array of tag ids. If an array is provided, then
|
||||
* only the questions that are tagged with ALL the provided tagids will be loaded.
|
||||
* @param int $limit Maximum number of results to return.
|
||||
* @param int $offset Number of items to skip from the begging of the result set.
|
||||
* @param string[] $fields The fields to return for each question.
|
||||
* @return \stdClass[] The list of question records
|
||||
*/
|
||||
public function get_questions(
|
||||
$categoryid,
|
||||
$includesubcategories,
|
||||
$tagids = [],
|
||||
$limit = 100,
|
||||
$offset = 0,
|
||||
$fields = []
|
||||
) {
|
||||
global $DB;
|
||||
|
||||
$questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids);
|
||||
if (empty($questionids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (empty($fields)) {
|
||||
// Return all fields.
|
||||
$fieldsstring = '*';
|
||||
} else {
|
||||
$fieldsstring = implode(',', $fields);
|
||||
}
|
||||
|
||||
return $DB->get_records_list(
|
||||
'question',
|
||||
'id',
|
||||
$questionids,
|
||||
'id',
|
||||
$fieldsstring,
|
||||
$offset,
|
||||
$limit
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of available questions for the given criteria.
|
||||
*
|
||||
* @param int $categoryid The id of a category in the question bank.
|
||||
* @param bool $includesubcategories Whether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @param array $tagids An array of tag ids. If an array is provided, then
|
||||
* only the questions that are tagged with ALL the provided tagids will be loaded.
|
||||
* @return int The number of questions matching the criteria.
|
||||
*/
|
||||
public function count_questions($categoryid, $includesubcategories, $tagids = []) {
|
||||
$questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids);
|
||||
return count($questionids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once("$CFG->libdir/externallib.php");
|
||||
require_once($CFG->dirroot . '/question/engine/lib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/datalib.php');
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
/**
|
||||
@@ -211,4 +212,106 @@ class core_question_external extends external_api {
|
||||
'status' => new external_value(PARAM_BOOL, 'status: true if success')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method parameters.
|
||||
*
|
||||
* @return external_function_parameters.
|
||||
*/
|
||||
public static function get_random_question_summaries_parameters() {
|
||||
return new external_function_parameters([
|
||||
'categoryid' => new external_value(PARAM_INT, 'Category id to find random questions'),
|
||||
'includesubcategories' => new external_value(PARAM_BOOL, 'Include the subcategories in the search'),
|
||||
'tagids' => new external_multiple_structure(
|
||||
new external_value(PARAM_INT, 'Tag id')
|
||||
),
|
||||
'contextid' => new external_value(PARAM_INT,
|
||||
'Context id that the questions will be rendered in (used for exporting)'),
|
||||
'limit' => new external_value(PARAM_INT, 'Maximum number of results to return',
|
||||
VALUE_DEFAULT, 0),
|
||||
'offset' => new external_value(PARAM_INT, 'Number of items to skip from the begging of the result set',
|
||||
VALUE_DEFAULT, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of random questions for the given criteria. The questions
|
||||
* will be exported in a summaries format and won't include all of the
|
||||
* question data.
|
||||
*
|
||||
* @param int $categoryid Category id to find random questions
|
||||
* @param bool $includesubcategories Include the subcategories in the search
|
||||
* @param int[] $tagids Only include questions with these tags
|
||||
* @param int $contextid The context id where the questions will be rendered
|
||||
* @param int $limit Maximum number of results to return
|
||||
* @param int $offset Number of items to skip from the beginning of the result set.
|
||||
* @return array The list of questions and total question count.
|
||||
*/
|
||||
public static function get_random_question_summaries(
|
||||
$categoryid,
|
||||
$includesubcategories,
|
||||
$tagids,
|
||||
$contextid,
|
||||
$limit = 0,
|
||||
$offset = 0
|
||||
) {
|
||||
global $DB, $PAGE;
|
||||
|
||||
// Parameter validation.
|
||||
$params = self::validate_parameters(
|
||||
self::get_random_question_summaries_parameters(),
|
||||
[
|
||||
'categoryid' => $categoryid,
|
||||
'includesubcategories' => $includesubcategories,
|
||||
'tagids' => $tagids,
|
||||
'contextid' => $contextid,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset
|
||||
]
|
||||
);
|
||||
$categoryid = $params['categoryid'];
|
||||
$includesubcategories = $params['includesubcategories'];
|
||||
$tagids = $params['tagids'];
|
||||
$contextid = $params['contextid'];
|
||||
$limit = $params['limit'];
|
||||
$offset = $params['offset'];
|
||||
|
||||
$context = \context::instance_by_id($contextid);
|
||||
self::validate_context($context);
|
||||
|
||||
$categorycontextid = $DB->get_field('question_categories', 'contextid', ['id' => $categoryid], MUST_EXIST);
|
||||
$categorycontext = \context::instance_by_id($categorycontextid);
|
||||
$editcontexts = new \question_edit_contexts($categorycontext);
|
||||
// The user must be able to view all questions in the category that they are requesting.
|
||||
$editcontexts->require_cap('moodle/question:viewall');
|
||||
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list([]));
|
||||
// Only load the properties we require from the DB.
|
||||
$properties = \core_question\external\question_summary_exporter::get_mandatory_properties();
|
||||
$questions = $loader->get_questions($categoryid, $includesubcategories, $tagids, $limit, $offset, $properties);
|
||||
$totalcount = $loader->count_questions($categoryid, $includesubcategories, $tagids);
|
||||
$renderer = $PAGE->get_renderer('core');
|
||||
|
||||
$formattedquestions = array_map(function($question) use ($context, $renderer) {
|
||||
$exporter = new \core_question\external\question_summary_exporter($question, ['context' => $context]);
|
||||
return $exporter->export($renderer);
|
||||
}, $questions);
|
||||
|
||||
return [
|
||||
'totalcount' => $totalcount,
|
||||
'questions' => $formattedquestions
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method result value.
|
||||
*/
|
||||
public static function get_random_question_summaries_returns() {
|
||||
return new external_single_structure([
|
||||
'totalcount' => new external_value(PARAM_INT, 'total number of questions in result set'),
|
||||
'questions' => new external_multiple_structure(
|
||||
\core_question\external\question_summary_exporter::get_read_structure()
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Class for exporting a question icon from an stdClass.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_question\external;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/engine/bank.php');
|
||||
|
||||
/**
|
||||
* Class for exporting a question from an stdClass.
|
||||
*
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_icon_exporter extends \core\external\exporter {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \stdClass $question
|
||||
* @param array $related The related data.
|
||||
*/
|
||||
public function __construct(\stdClass $question, $related = []) {
|
||||
$qtype = \question_bank::get_qtype($question->qtype, false);
|
||||
|
||||
parent::__construct((object) [
|
||||
'key' => 'icon',
|
||||
'component' => $qtype->plugin_name(),
|
||||
'alttext' => $qtype->local_name()
|
||||
], $related);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the moodle context as a required related object.
|
||||
*
|
||||
* @return array Required related objects.
|
||||
*/
|
||||
protected static function define_related() {
|
||||
return ['context' => '\\context'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function define_properties() {
|
||||
return [
|
||||
'key' => ['type' => PARAM_TEXT],
|
||||
'component' => ['type' => PARAM_COMPONENT],
|
||||
'alttext' => ['type' => PARAM_TEXT],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Class for exporting a question summary from an stdClass.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace core_question\external;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \renderer_base;
|
||||
|
||||
/**
|
||||
* Class for exporting a question summary from an stdClass.
|
||||
*
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_summary_exporter extends \core\external\exporter {
|
||||
|
||||
/**
|
||||
* @var \stdClass $question
|
||||
*/
|
||||
protected $question;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \stdClass $question
|
||||
* @param array $related The related data.
|
||||
*/
|
||||
public function __construct(\stdClass $question, $related = []) {
|
||||
$this->question = $question;
|
||||
return parent::__construct($question, $related);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the moodle context as a required related object.
|
||||
*
|
||||
* @return array Required related objects.
|
||||
*/
|
||||
protected static function define_related() {
|
||||
return ['context' => '\\context'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of mandatory properties required on the question object to
|
||||
* export.
|
||||
*
|
||||
* @return string[] List of properties.
|
||||
*/
|
||||
public static function get_mandatory_properties() {
|
||||
$properties = self::define_properties();
|
||||
$mandatoryproperties = array_filter($properties, function($property) {
|
||||
return empty($property['optional']);
|
||||
});
|
||||
return array_keys($mandatoryproperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of static properties returned.
|
||||
*
|
||||
* @return array List of properties.
|
||||
*/
|
||||
public static function define_properties() {
|
||||
return [
|
||||
'id' => [
|
||||
'type' => PARAM_INT,
|
||||
],
|
||||
'category' => [
|
||||
'type' => PARAM_INT,
|
||||
],
|
||||
'parent' => [
|
||||
'type' => PARAM_INT,
|
||||
],
|
||||
'name' => [
|
||||
'type' => PARAM_TEXT,
|
||||
],
|
||||
'qtype' => [
|
||||
'type' => PARAM_COMPONENT,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the list of calculated properties.
|
||||
*
|
||||
* @return array The list of properties.
|
||||
*/
|
||||
protected static function define_other_properties() {
|
||||
return [
|
||||
'icon' => [
|
||||
'type' => question_icon_exporter::read_properties_definition(),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the values for the properties defined in the define_other_properties
|
||||
* function.
|
||||
*
|
||||
* @param renderer_base $output A renderer.
|
||||
* @return array The list of properties.
|
||||
*/
|
||||
protected function get_other_values(\renderer_base $output) {
|
||||
$iconexporter = new question_icon_exporter($this->question, $this->related);
|
||||
|
||||
return [
|
||||
'icon' => $iconexporter->export($output),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ require_once($CFG->libdir . '/formslib.php');
|
||||
// Read URL parameters telling us which question to edit.
|
||||
$id = optional_param('id', 0, PARAM_INT); // question id
|
||||
$makecopy = optional_param('makecopy', 0, PARAM_BOOL);
|
||||
$qtype = optional_param('qtype', '', PARAM_FILE);
|
||||
$qtype = optional_param('qtype', '', PARAM_COMPONENT);
|
||||
$categoryid = optional_param('category', 0, PARAM_INT);
|
||||
$cmid = optional_param('cmid', 0, PARAM_INT);
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
|
||||
@@ -472,4 +472,367 @@ class core_question_external_testcase extends externallib_advanced_testcase {
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the get_random_question_summaries test.
|
||||
*/
|
||||
public function get_random_question_summaries_test_cases() {
|
||||
return [
|
||||
'empty category' => [
|
||||
'categoryindex' => 'emptycat',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => [],
|
||||
'expectedquestionindexes' => []
|
||||
],
|
||||
'single category' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => [],
|
||||
'expectedquestionindexes' => ['cat1q1', 'cat1q2']
|
||||
],
|
||||
'include sub category' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => [],
|
||||
'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2']
|
||||
],
|
||||
'single category with tags' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => ['cat1'],
|
||||
'expectedquestionindexes' => ['cat1q1']
|
||||
],
|
||||
'include sub category with tag on parent' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['cat1'],
|
||||
'expectedquestionindexes' => ['cat1q1']
|
||||
],
|
||||
'include sub category with tag on sub' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['subcat'],
|
||||
'expectedquestionindexes' => ['subcatq1']
|
||||
],
|
||||
'include sub category with same tag on parent and sub' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['foo'],
|
||||
'expectedquestionindexes' => ['cat1q1', 'subcatq1']
|
||||
],
|
||||
'include sub category with tag not matching' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['cat1', 'cat2'],
|
||||
'expectedquestionindexes' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get_random_question_summaries function with various parameter combinations.
|
||||
*
|
||||
* This function creates a data set as follows:
|
||||
* Category: cat1
|
||||
* Question: cat1q1
|
||||
* Tags: 'cat1', 'foo'
|
||||
* Question: cat1q2
|
||||
* Category: cat2
|
||||
* Question: cat2q1
|
||||
* Tags: 'cat2', 'foo'
|
||||
* Question: cat2q2
|
||||
* Category: subcat
|
||||
* Question: subcatq1
|
||||
* Tags: 'subcat', 'foo'
|
||||
* Question: subcatq2
|
||||
* Parent: cat1
|
||||
* Category: emptycat
|
||||
*
|
||||
* @dataProvider get_random_question_summaries_test_cases()
|
||||
* @param string $categoryindex The named index for the category to use
|
||||
* @param bool $includesubcategories If the search should include subcategories
|
||||
* @param string[] $usetagnames The tag names to include in the search
|
||||
* @param string[] $expectedquestionindexes The questions expected in the result
|
||||
*/
|
||||
public function test_get_random_question_summaries_variations(
|
||||
$categoryindex,
|
||||
$includesubcategories,
|
||||
$usetagnames,
|
||||
$expectedquestionindexes
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$context = context_system::instance();
|
||||
$categories = [];
|
||||
$questions = [];
|
||||
$tagnames = [
|
||||
'cat1',
|
||||
'cat2',
|
||||
'subcat',
|
||||
'foo'
|
||||
];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
// First category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']);
|
||||
$categories['cat1'] = $category;
|
||||
$questions['cat1q1'] = $categoryquestions[0];
|
||||
$questions['cat1q2'] = $categoryquestions[1];
|
||||
// Second category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']);
|
||||
$categories['cat2'] = $category;
|
||||
$questions['cat2q1'] = $categoryquestions[0];
|
||||
$questions['cat2q2'] = $categoryquestions[1];
|
||||
// Sub category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
|
||||
$categories['subcat'] = $category;
|
||||
$questions['subcatq1'] = $categoryquestions[0];
|
||||
$questions['subcatq2'] = $categoryquestions[1];
|
||||
// Empty category.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(0);
|
||||
$categories['emptycat'] = $category;
|
||||
|
||||
// Generate the arguments for the get_questions function.
|
||||
$category = $categories[$categoryindex];
|
||||
$tagids = array_map(function($tagname) use ($tags) {
|
||||
return $tags[$tagname]->id;
|
||||
}, $usetagnames);
|
||||
|
||||
$result = core_question_external::get_random_question_summaries($category->id, $includesubcategories, $tagids, $context->id);
|
||||
$resultquestions = $result['questions'];
|
||||
$resulttotalcount = $result['totalcount'];
|
||||
// Generate the expected question set.
|
||||
$expectedquestions = array_map(function($index) use ($questions) {
|
||||
return $questions[$index];
|
||||
}, $expectedquestionindexes);
|
||||
|
||||
// Ensure the resultquestions matches what was expected.
|
||||
$this->assertCount(count($expectedquestions), $resultquestions);
|
||||
$this->assertEquals(count($expectedquestions), $resulttotalcount);
|
||||
foreach ($expectedquestions as $question) {
|
||||
$this->assertEquals($resultquestions[$question->id]->id, $question->id);
|
||||
$this->assertEquals($resultquestions[$question->id]->category, $question->category);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should throw an invalid_parameter_exception if not
|
||||
* given an integer for the category id.
|
||||
*/
|
||||
public function test_get_random_question_summaries_invalid_category_id_param() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$context = context_system::instance();
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
core_question_external::get_random_question_summaries('invalid value', false, [], $context->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should throw an invalid_parameter_exception if not
|
||||
* given a boolean for the $includesubcategories parameter.
|
||||
*/
|
||||
public function test_get_random_question_summaries_invalid_includesubcategories_param() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$context = context_system::instance();
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
core_question_external::get_random_question_summaries(1, 'invalid value', [], $context->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should throw an invalid_parameter_exception if not
|
||||
* given an array of integers for the tag ids parameter.
|
||||
*/
|
||||
public function test_get_random_question_summaries_invalid_tagids_param() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$context = context_system::instance();
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
core_question_external::get_random_question_summaries(1, false, ['invalid', 'values'], $context->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should throw an invalid_parameter_exception if not
|
||||
* given a context.
|
||||
*/
|
||||
public function test_get_random_question_summaries_invalid_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->expectException('invalid_parameter_exception');
|
||||
core_question_external::get_random_question_summaries(1, false, [1, 2], 'context');
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should throw an restricted_context_exception
|
||||
* if the given context is outside of the set of restricted contexts the user
|
||||
* is allowed to call external functions in.
|
||||
*/
|
||||
public function test_get_random_question_summaries_restricted_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$systemcontext = context_system::instance();
|
||||
// Restrict access to external functions for the logged in user to only
|
||||
// the course we just created. External functions should not be allowed
|
||||
// to execute in any contexts above the course context.
|
||||
core_question_external::set_context_restriction($coursecontext);
|
||||
|
||||
// An exception should be thrown when we try to execute at the system context
|
||||
// since we're restricted to the course context.
|
||||
try {
|
||||
// Do this in a try/catch statement to allow the context restriction
|
||||
// to be reset afterwards.
|
||||
core_question_external::get_random_question_summaries(1, false, [], $systemcontext->id);
|
||||
} catch (Exception $e) {
|
||||
$this->assertInstanceOf('restricted_context_exception', $e);
|
||||
}
|
||||
// Reset the restriction so that other tests don't fail aftwards.
|
||||
core_question_external::set_context_restriction($systemcontext);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should return a question that is formatted correctly.
|
||||
*/
|
||||
public function test_get_random_question_summaries_formats_returned_questions() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
list($category, $questions) = $this->create_category_and_questions(1);
|
||||
$context = context_system::instance();
|
||||
$question = $questions[0];
|
||||
$expected = (object) [
|
||||
'id' => $question->id,
|
||||
'category' => $question->category,
|
||||
'parent' => $question->parent,
|
||||
'name' => $question->name,
|
||||
'qtype' => $question->qtype
|
||||
];
|
||||
|
||||
$result = core_question_external::get_random_question_summaries($category->id, false, [], $context->id);
|
||||
$actual = $result['questions'][$question->id];
|
||||
|
||||
$this->assertEquals($expected->id, $actual->id);
|
||||
$this->assertEquals($expected->category, $actual->category);
|
||||
$this->assertEquals($expected->parent, $actual->parent);
|
||||
$this->assertEquals($expected->name, $actual->name);
|
||||
$this->assertEquals($expected->qtype, $actual->qtype);
|
||||
// These values are added by the formatting. It doesn't matter what the
|
||||
// exact values are just that they are returned.
|
||||
$this->assertObjectHasAttribute('icon', $actual);
|
||||
$this->assertObjectHasAttribute('key', $actual->icon);
|
||||
$this->assertObjectHasAttribute('component', $actual->icon);
|
||||
$this->assertObjectHasAttribute('alttext', $actual->icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should allow limiting and offsetting of the result set.
|
||||
*/
|
||||
public function test_get_random_question_summaries_with_limit_and_offset() {
|
||||
$this->resetAfterTest();
|
||||
$numberofquestions = 5;
|
||||
$includesubcategories = false;
|
||||
$tagids = [];
|
||||
$limit = 1;
|
||||
$offset = 0;
|
||||
$context = context_system::instance();
|
||||
list($category, $questions) = $this->create_category_and_questions($numberofquestions);
|
||||
|
||||
// Sort the questions by id to match the ordering of the result.
|
||||
usort($questions, function($a, $b) {
|
||||
$aid = $a->id;
|
||||
$bid = $b->id;
|
||||
|
||||
if ($aid == $bid) {
|
||||
return 0;
|
||||
}
|
||||
return $aid < $bid ? -1 : 1;
|
||||
});
|
||||
|
||||
for ($i = 0; $i < $numberofquestions; $i++) {
|
||||
$result = core_question_external::get_random_question_summaries(
|
||||
$category->id,
|
||||
$includesubcategories,
|
||||
$tagids,
|
||||
$context->id,
|
||||
$limit,
|
||||
$offset
|
||||
);
|
||||
|
||||
$resultquestions = $result['questions'];
|
||||
$totalcount = $result['totalcount'];
|
||||
|
||||
$this->assertCount($limit, $resultquestions);
|
||||
$this->assertEquals($numberofquestions, $totalcount);
|
||||
$actual = array_shift($resultquestions);
|
||||
$expected = $questions[$i];
|
||||
$this->assertEquals($expected->id, $actual->id);
|
||||
$offset++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get_random_question_summaries should throw an exception if the user doesn't
|
||||
* have the capability to use the questions in the requested category.
|
||||
*/
|
||||
public function test_get_random_question_summaries_without_capability() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$user = $generator->create_user();
|
||||
$roleid = $generator->create_role();
|
||||
$systemcontext = context_system::instance();
|
||||
$numberofquestions = 5;
|
||||
$includesubcategories = false;
|
||||
$tagids = [];
|
||||
$context = context_system::instance();
|
||||
list($category, $questions) = $this->create_category_and_questions($numberofquestions);
|
||||
$categorycontext = context::instance_by_id($category->contextid);
|
||||
|
||||
$generator->role_assign($roleid, $user->id, $systemcontext->id);
|
||||
// Prohibit all of the tag capabilities.
|
||||
assign_capability('moodle/question:viewall', CAP_PROHIBIT, $roleid, $categorycontext->id);
|
||||
|
||||
$this->setUser($user);
|
||||
$this->expectException('moodle_exception');
|
||||
core_question_external::get_random_question_summaries(
|
||||
$category->id,
|
||||
$includesubcategories,
|
||||
$tagids,
|
||||
$context->id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a question category and create questions in that category. Tag
|
||||
* the first question in each category with the given tags.
|
||||
*
|
||||
* @param int $questioncount How many questions to create.
|
||||
* @param string[] $tagnames The list of tags to use.
|
||||
* @param stdClass|null $parentcategory The category to set as the parent of the created category.
|
||||
* @return array The category and questions.
|
||||
*/
|
||||
protected function create_category_and_questions($questioncount, $tagnames = [], $parentcategory = null) {
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
if ($parentcategory) {
|
||||
$catparams = ['parent' => $parentcategory->id];
|
||||
} else {
|
||||
$catparams = [];
|
||||
}
|
||||
|
||||
$category = $generator->create_question_category($catparams);
|
||||
$questions = [];
|
||||
|
||||
for ($i = 0; $i < $questioncount; $i++) {
|
||||
$questions[] = $generator->create_question('shortanswer', null, ['category' => $category->id]);
|
||||
}
|
||||
|
||||
if (!empty($tagnames) && !empty($questions)) {
|
||||
$context = context::instance_by_id($category->contextid);
|
||||
core_tag_tag::set_item_tags('core_question', 'question', $questions[0]->id, $context, $tagnames);
|
||||
}
|
||||
|
||||
return [$category, $questions];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,4 +218,386 @@ class random_question_loader_testcase extends advanced_testcase {
|
||||
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the get_questions test.
|
||||
*/
|
||||
public function get_questions_test_cases() {
|
||||
return [
|
||||
'empty category' => [
|
||||
'categoryindex' => 'emptycat',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => [],
|
||||
'expectedquestionindexes' => []
|
||||
],
|
||||
'single category' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => [],
|
||||
'expectedquestionindexes' => ['cat1q1', 'cat1q2']
|
||||
],
|
||||
'include sub category' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => [],
|
||||
'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2']
|
||||
],
|
||||
'single category with tags' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => ['cat1'],
|
||||
'expectedquestionindexes' => ['cat1q1']
|
||||
],
|
||||
'include sub category with tag on parent' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['cat1'],
|
||||
'expectedquestionindexes' => ['cat1q1']
|
||||
],
|
||||
'include sub category with tag on sub' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['subcat'],
|
||||
'expectedquestionindexes' => ['subcatq1']
|
||||
],
|
||||
'include sub category with same tag on parent and sub' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['foo'],
|
||||
'expectedquestionindexes' => ['cat1q1', 'subcatq1']
|
||||
],
|
||||
'include sub category with tag not matching' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['cat1', 'cat2'],
|
||||
'expectedquestionindexes' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get_questions function with various parameter combinations.
|
||||
*
|
||||
* This function creates a data set as follows:
|
||||
* Category: cat1
|
||||
* Question: cat1q1
|
||||
* Tags: 'cat1', 'foo'
|
||||
* Question: cat1q2
|
||||
* Category: cat2
|
||||
* Question: cat2q1
|
||||
* Tags: 'cat2', 'foo'
|
||||
* Question: cat2q2
|
||||
* Category: subcat
|
||||
* Question: subcatq1
|
||||
* Tags: 'subcat', 'foo'
|
||||
* Question: subcatq2
|
||||
* Parent: cat1
|
||||
* Category: emptycat
|
||||
*
|
||||
* @dataProvider get_questions_test_cases()
|
||||
* @param string $categoryindex The named index for the category to use
|
||||
* @param bool $includesubcategories If the search should include subcategories
|
||||
* @param string[] $usetagnames The tag names to include in the search
|
||||
* @param string[] $expectedquestionindexes The questions expected in the result
|
||||
*/
|
||||
public function test_get_questions_variations(
|
||||
$categoryindex,
|
||||
$includesubcategories,
|
||||
$usetagnames,
|
||||
$expectedquestionindexes
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$categories = [];
|
||||
$questions = [];
|
||||
$tagnames = [
|
||||
'cat1',
|
||||
'cat2',
|
||||
'subcat',
|
||||
'foo'
|
||||
];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
// First category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']);
|
||||
$categories['cat1'] = $category;
|
||||
$questions['cat1q1'] = $categoryquestions[0];
|
||||
$questions['cat1q2'] = $categoryquestions[1];
|
||||
// Second category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']);
|
||||
$categories['cat2'] = $category;
|
||||
$questions['cat2q1'] = $categoryquestions[0];
|
||||
$questions['cat2q2'] = $categoryquestions[1];
|
||||
// Sub category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
|
||||
$categories['subcat'] = $category;
|
||||
$questions['subcatq1'] = $categoryquestions[0];
|
||||
$questions['subcatq2'] = $categoryquestions[1];
|
||||
// Empty category.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(0);
|
||||
$categories['emptycat'] = $category;
|
||||
|
||||
// Generate the arguments for the get_questions function.
|
||||
$category = $categories[$categoryindex];
|
||||
$tagids = array_map(function($tagname) use ($tags) {
|
||||
return $tags[$tagname]->id;
|
||||
}, $usetagnames);
|
||||
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list([]));
|
||||
$result = $loader->get_questions($category->id, $includesubcategories, $tagids);
|
||||
// Generate the expected question set.
|
||||
$expectedquestions = array_map(function($index) use ($questions) {
|
||||
return $questions[$index];
|
||||
}, $expectedquestionindexes);
|
||||
|
||||
// Ensure the result matches what was expected.
|
||||
$this->assertCount(count($expectedquestions), $result);
|
||||
foreach ($expectedquestions as $question) {
|
||||
$this->assertEquals($result[$question->id]->id, $question->id);
|
||||
$this->assertEquals($result[$question->id]->category, $question->category);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get_questions should allow limiting and offsetting of the result set.
|
||||
*/
|
||||
public function test_get_questions_with_limit_and_offset() {
|
||||
$this->resetAfterTest();
|
||||
$numberofquestions = 5;
|
||||
$includesubcategories = false;
|
||||
$tagids = [];
|
||||
$limit = 1;
|
||||
$offset = 0;
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list([]));
|
||||
list($category, $questions) = $this->create_category_and_questions($numberofquestions);
|
||||
|
||||
// Sort the questions by id to match the ordering of the get_questions
|
||||
// function.
|
||||
usort($questions, function($a, $b) {
|
||||
$aid = $a->id;
|
||||
$bid = $b->id;
|
||||
|
||||
if ($aid == $bid) {
|
||||
return 0;
|
||||
}
|
||||
return $aid < $bid ? -1 : 1;
|
||||
});
|
||||
|
||||
for ($i = 0; $i < $numberofquestions; $i++) {
|
||||
$result = $loader->get_questions(
|
||||
$category->id,
|
||||
$includesubcategories,
|
||||
$tagids,
|
||||
$limit,
|
||||
$offset
|
||||
);
|
||||
|
||||
$this->assertCount($limit, $result);
|
||||
$actual = array_shift($result);
|
||||
$expected = $questions[$i];
|
||||
$this->assertEquals($expected->id, $actual->id);
|
||||
$offset++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get_questions should allow retrieving questions with only a subset of
|
||||
* fields populated.
|
||||
*/
|
||||
public function test_get_questions_with_restricted_fields() {
|
||||
$this->resetAfterTest();
|
||||
$includesubcategories = false;
|
||||
$tagids = [];
|
||||
$limit = 10;
|
||||
$offset = 0;
|
||||
$fields = ['id', 'name'];
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list([]));
|
||||
list($category, $questions) = $this->create_category_and_questions(1);
|
||||
|
||||
$result = $loader->get_questions(
|
||||
$category->id,
|
||||
$includesubcategories,
|
||||
$tagids,
|
||||
$limit,
|
||||
$offset,
|
||||
$fields
|
||||
);
|
||||
|
||||
$expectedquestion = array_shift($questions);
|
||||
$actualquestion = array_shift($result);
|
||||
$actualfields = get_object_vars($actualquestion);
|
||||
$actualfields = array_keys($actualfields);
|
||||
sort($actualfields);
|
||||
sort($fields);
|
||||
|
||||
$this->assertEquals($fields, $actualfields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the count_questions test.
|
||||
*/
|
||||
public function count_questions_test_cases() {
|
||||
return [
|
||||
'empty category' => [
|
||||
'categoryindex' => 'emptycat',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => [],
|
||||
'expectedcount' => 0
|
||||
],
|
||||
'single category' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => [],
|
||||
'expectedcount' => 2
|
||||
],
|
||||
'include sub category' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => [],
|
||||
'expectedcount' => 4
|
||||
],
|
||||
'single category with tags' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => false,
|
||||
'usetagnames' => ['cat1'],
|
||||
'expectedcount' => 1
|
||||
],
|
||||
'include sub category with tag on parent' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['cat1'],
|
||||
'expectedcount' => 1
|
||||
],
|
||||
'include sub category with tag on sub' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['subcat'],
|
||||
'expectedcount' => 1
|
||||
],
|
||||
'include sub category with same tag on parent and sub' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['foo'],
|
||||
'expectedcount' => 2
|
||||
],
|
||||
'include sub category with tag not matching' => [
|
||||
'categoryindex' => 'cat1',
|
||||
'includesubcategories' => true,
|
||||
'usetagnames' => ['cat1', 'cat2'],
|
||||
'expectedcount' => 0
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the count_questions function with various parameter combinations.
|
||||
*
|
||||
* This function creates a data set as follows:
|
||||
* Category: cat1
|
||||
* Question: cat1q1
|
||||
* Tags: 'cat1', 'foo'
|
||||
* Question: cat1q2
|
||||
* Category: cat2
|
||||
* Question: cat2q1
|
||||
* Tags: 'cat2', 'foo'
|
||||
* Question: cat2q2
|
||||
* Category: subcat
|
||||
* Question: subcatq1
|
||||
* Tags: 'subcat', 'foo'
|
||||
* Question: subcatq2
|
||||
* Parent: cat1
|
||||
* Category: emptycat
|
||||
*
|
||||
* @dataProvider count_questions_test_cases()
|
||||
* @param string $categoryindex The named index for the category to use
|
||||
* @param bool $includesubcategories If the search should include subcategories
|
||||
* @param string[] $usetagnames The tag names to include in the search
|
||||
* @param int $expectedcount The number of questions expected in the result
|
||||
*/
|
||||
public function test_count_questions_variations(
|
||||
$categoryindex,
|
||||
$includesubcategories,
|
||||
$usetagnames,
|
||||
$expectedcount
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$categories = [];
|
||||
$questions = [];
|
||||
$tagnames = [
|
||||
'cat1',
|
||||
'cat2',
|
||||
'subcat',
|
||||
'foo'
|
||||
];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
// First category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']);
|
||||
$categories['cat1'] = $category;
|
||||
$questions['cat1q1'] = $categoryquestions[0];
|
||||
$questions['cat1q2'] = $categoryquestions[1];
|
||||
// Second category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']);
|
||||
$categories['cat2'] = $category;
|
||||
$questions['cat2q1'] = $categoryquestions[0];
|
||||
$questions['cat2q2'] = $categoryquestions[1];
|
||||
// Sub category and questions.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
|
||||
$categories['subcat'] = $category;
|
||||
$questions['subcatq1'] = $categoryquestions[0];
|
||||
$questions['subcatq2'] = $categoryquestions[1];
|
||||
// Empty category.
|
||||
list($category, $categoryquestions) = $this->create_category_and_questions(0);
|
||||
$categories['emptycat'] = $category;
|
||||
|
||||
// Generate the arguments for the get_questions function.
|
||||
$category = $categories[$categoryindex];
|
||||
$tagids = array_map(function($tagname) use ($tags) {
|
||||
return $tags[$tagname]->id;
|
||||
}, $usetagnames);
|
||||
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list([]));
|
||||
$result = $loader->count_questions($category->id, $includesubcategories, $tagids);
|
||||
|
||||
// Ensure the result matches what was expected.
|
||||
$this->assertEquals($expectedcount, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a question category and create questions in that category. Tag
|
||||
* the first question in each category with the given tags.
|
||||
*
|
||||
* @param int $questioncount How many questions to create.
|
||||
* @param string[] $tagnames The list of tags to use.
|
||||
* @param stdClass|null $parentcategory The category to set as the parent of the created category.
|
||||
* @return array The category and questions.
|
||||
*/
|
||||
protected function create_category_and_questions($questioncount, $tagnames = [], $parentcategory = null) {
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
if ($parentcategory) {
|
||||
$catparams = ['parent' => $parentcategory->id];
|
||||
} else {
|
||||
$catparams = [];
|
||||
}
|
||||
|
||||
$category = $generator->create_question_category($catparams);
|
||||
$questions = [];
|
||||
|
||||
for ($i = 0; $i < $questioncount; $i++) {
|
||||
$questions[] = $generator->create_question('shortanswer', null, ['category' => $category->id]);
|
||||
}
|
||||
|
||||
if (!empty($tagnames) && !empty($questions)) {
|
||||
$context = context::instance_by_id($category->contextid);
|
||||
core_tag_tag::set_item_tags('core_question', 'question', $questions[0]->id, $context, $tagnames);
|
||||
}
|
||||
|
||||
return [$category, $questions];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_bar
|
||||
|
||||
This template renders the bootstrap style paging bar to control a paged
|
||||
content section.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"itemsperpage": 2,
|
||||
"previous": true,
|
||||
"next": true,
|
||||
"first": true,
|
||||
"last": true,
|
||||
"pages": [
|
||||
{
|
||||
"url": "#",
|
||||
"page": "1",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": "#",
|
||||
"page": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div aria-label="{{label}}"
|
||||
class="pagination"
|
||||
id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
|
||||
data-region="paging-bar"
|
||||
data-items-per-page="{{itemsperpage}}">
|
||||
|
||||
<ul class="pagination">
|
||||
{{#previous}}
|
||||
{{< core/paged_content_paging_bar_item }}
|
||||
{{$item-content}}
|
||||
<span aria-hidden="true">«</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">»</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>
|
||||
{{#js}}
|
||||
require(['jquery', 'core/paged_content_paging_bar'], function($, PagingControl) {
|
||||
var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}');
|
||||
PagingControl.init(root);
|
||||
});
|
||||
{{/js}}
|
||||
@@ -0,0 +1,76 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_dropdown
|
||||
|
||||
This template renders the bootstrap style dropdown menu to control
|
||||
paged content.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"itemcount": 25,
|
||||
"content": "25",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"itemcount": 25,
|
||||
"content": "50"
|
||||
},
|
||||
{
|
||||
"itemcount": 50,
|
||||
"content": "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="btn-group m-b-1"
|
||||
id="paging-dropdown-{{uniqid}}"
|
||||
data-region="paging-dropdown-container">
|
||||
|
||||
<button class="btn btn-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdown-menu-button-{{uniqid}}"
|
||||
data-region="dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
|
||||
{{#options}}
|
||||
{{#active}}
|
||||
{{{content}}}
|
||||
{{/active}}
|
||||
{{/options}}
|
||||
<span data-region="caret" class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdown-menu-button-{{uniqid}}">
|
||||
{{#options}}
|
||||
{{> core/paged_content_paging_dropdown_item }}
|
||||
{{/options}}
|
||||
{{< core/paged_content_paging_dropdown_item }}
|
||||
{{$itemcount}}0{{/itemcount}}
|
||||
{{$content}}{{#str}} all {{/str}}{{/content}}
|
||||
{{/ 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}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/paged_content_paging_dropdown_item
|
||||
|
||||
This template renders a single item in the paging dropdown menu.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"itemcount": 25,
|
||||
"content": "25",
|
||||
"active": true
|
||||
}
|
||||
}}
|
||||
<li class="dropdown-item {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}} {{$classes}}{{/classes}}"
|
||||
data-region="dropdown-item"
|
||||
data-item-count="{{$itemcount}}{{itemcount}}{{/itemcount}}">
|
||||
|
||||
<a href="#">{{$content}}{{{content}}}{{/content}}</a>
|
||||
</li>
|
||||
@@ -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/>.
|
||||
}}
|
||||
{{!
|
||||
@template calendar/modal_add_random_question
|
||||
|
||||
Modal for adding a random question to a quiz activity.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* title A cleaned string (use clean_text()) to display.
|
||||
* body HTML content for the boday
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"title": "Example modal",
|
||||
"body": "Some example content for the body"
|
||||
}
|
||||
}}
|
||||
|
||||
{{< core/modal }}
|
||||
{{$body}}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link"
|
||||
data-toggle="tab"
|
||||
href="#existing-category-{{uniqid}}"
|
||||
role="tab">
|
||||
{{#str}} existingcategory, mod_quiz {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
data-toggle="tab"
|
||||
href="#new-category-{{uniqid}}"
|
||||
role="tab">
|
||||
{{#str}} newcategory, mod_quiz {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" data-region="tab-content">
|
||||
<div class="text-sm-center p-t-2" data-region="loading-container">
|
||||
{{> core/loading }}
|
||||
</div>
|
||||
<div class="tab-pane active p-t-1"
|
||||
id="existing-category-{{uniqid}}"
|
||||
role="tabpanel"
|
||||
data-region="existing-category-container">
|
||||
</div>
|
||||
<div class="tab-pane p-t-1"
|
||||
id="new-category-{{uniqid}}"
|
||||
role="tabpanel"
|
||||
data-region="new-category-container">
|
||||
</div>
|
||||
</div>
|
||||
{{/body}}
|
||||
{{/ core/modal }}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template mod_quiz/random_question_form_preview
|
||||
|
||||
A preview section for the list of questions that match the random
|
||||
question criteria in the random question form.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* title A cleaned string (use clean_text()) to display.
|
||||
* body HTML content for the boday
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div class="m-t-2" data-region="random-question-preview-container" style="position:relative">
|
||||
<div data-region="question-count-container"></div>
|
||||
<div data-region="question-list-container"></div>
|
||||
{{< core/overlay_loading }}{{$hiddenclass}}{{/hiddenclass}}{{/ core/overlay_loading }}
|
||||
</div>
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2018041800.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2018041800.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user