diff --git a/.upgradenotes/MDL-87264-2025112115054754.yml b/.upgradenotes/MDL-87264-2025112115054754.yml new file mode 100644 index 00000000000..b4716068a46 --- /dev/null +++ b/.upgradenotes/MDL-87264-2025112115054754.yml @@ -0,0 +1,26 @@ +issueNumber: MDL-87264 +notes: + mod_quiz: + - message: > + The `mod_quiz_output_fragment_switch_question_bank()` Fragment API + callback is deprecated in favour of + `core_question\route\api\bank::banks()`, available via the route + `/api/rest/v2/core/question/banks?courseid=X`. + type: deprecated + - message: > + The "gobacktoquiz" and "selectquestionbank" lang strings have been deprecated. + These are only used by the question bank switching UI, so have been replaced + with the "switchergoback" and "switcherselectbank" strings in the core_question + component. + type: deprecated + core_question: + - message: > + The `\core_question\output\switch_question_bank` renderable is now deprecated, + as rendering of the switch_question_bank template is now all handled client-side + by the `core_question/bank_switcher` Javascript module. + type: deprecated + - message: > + The UI for switching question banks is now encapsulated in the + `core_question/bank_switcher` Javascript module. This takes an existing modal + and replaces the content with the switcher. It will then emit a custom + `bankSwitched` event on the modal's DOM element when a new bank is selected. diff --git a/.upgradenotes/MDL-87264-2025112116010726.yml b/.upgradenotes/MDL-87264-2025112116010726.yml new file mode 100644 index 00000000000..a0fbca6577c --- /dev/null +++ b/.upgradenotes/MDL-87264-2025112116010726.yml @@ -0,0 +1,11 @@ +issueNumber: MDL-87264 +notes: + qbank_managecategories: + - message: >- + Support for displaying multiple contexts has been removed from the + question category management UI. As such, the `editlists` property, + `contexts`, `courseid` and `thiscontext` constructor parameters in + `qbank_managecategories\question_categories` are deprecated. The + constructor now just uses `cmid` to determine the current context + parent course. + type: deprecated diff --git a/.upgradenotes/MDL-87264-2026012116260358.yml b/.upgradenotes/MDL-87264-2026012116260358.yml new file mode 100644 index 00000000000..8522cc73563 --- /dev/null +++ b/.upgradenotes/MDL-87264-2026012116260358.yml @@ -0,0 +1,10 @@ +issueNumber: MDL-87264 +notes: + core: + - message: > + Added new `core\router\parameter\query_course` and + `core\router\parameter\query_coursemodule` parameter types, to allow + course IDs and course module IDs to be accepted as query string + parameters for routes, and automatically converted into the + corresponding objects and contexts. + type: improved diff --git a/.upgradenotes/MDL-87264-2026012116303057.yml b/.upgradenotes/MDL-87264-2026012116303057.yml new file mode 100644 index 00000000000..75e6faadd91 --- /dev/null +++ b/.upgradenotes/MDL-87264-2026012116303057.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-87264 +notes: + core_question: + - message: >- + Added a new route at `/api/rest/v2/question/categories` for returning a + list of question categories in a particular course module. + type: improved diff --git a/public/course/classes/route/controller/course_navigation.php b/public/course/classes/route/controller/course_navigation.php index 5227cd3f1ea..3a0e1056339 100644 --- a/public/course/classes/route/controller/course_navigation.php +++ b/public/course/classes/route/controller/course_navigation.php @@ -40,13 +40,13 @@ class course_navigation { * * @param ServerRequestInterface $request * @param ResponseInterface $response - * @param \stdClass $cm + * @param \stdClass $cmdata * @return ResponseInterface */ #[route( path: '/cms/{cm}/next', pathtypes: [ - new \core\router\parameters\path_module(), + new \core\router\parameters\path_coursemodule(name: 'cm'), ], requirelogin: new require_login( requirelogin: true, @@ -56,11 +56,11 @@ class course_navigation { public function cm_next_element( ServerRequestInterface $request, ResponseInterface $response, - \stdClass $cm, + \stdClass $cmdata, ): ResponseInterface { // The pathinfo module returns a stdClass and not a cm_info, so we need to // get the cm_info instance from the course modinfo. - $cm = cm_info::create($cm); + $cm = cm_info::create($cmdata); $modinfo = $cm->get_modinfo(); $section = $this->get_section($cm); $allsectioncms = $this->get_all_section_cms($modinfo, $section); @@ -88,13 +88,13 @@ class course_navigation { * * @param ServerRequestInterface $request * @param ResponseInterface $response - * @param \stdClass $cm + * @param \stdClass $cmdata * @return ResponseInterface */ #[route( path: '/cms/{cm}/previous', pathtypes: [ - new \core\router\parameters\path_module(), + new \core\router\parameters\path_coursemodule(name: 'cm'), ], requirelogin: new require_login( requirelogin: true, @@ -104,11 +104,11 @@ class course_navigation { public function cm_previous_element( ServerRequestInterface $request, ResponseInterface $response, - \stdClass $cm, + \stdClass $cmdata, ): ResponseInterface { // The pathinfo module returns a stdClass and not a cm_info, so we need to // get the cm_info instance from the course modinfo. - $cm = cm_info::create($cm); + $cm = cm_info::create($cmdata); $modinfo = $cm->get_modinfo(); $section = $this->get_section($cm); $allsectioncms = $this->get_all_section_cms($modinfo, $section); diff --git a/public/course/classes/route/controller/restricted_module.php b/public/course/classes/route/controller/restricted_module.php index bd36eb4838b..4ebeaa9ee39 100644 --- a/public/course/classes/route/controller/restricted_module.php +++ b/public/course/classes/route/controller/restricted_module.php @@ -36,13 +36,13 @@ class restricted_module { * Restricted module. * * @param ResponseInterface $response - * @param modinfo $cm + * @param \stdClass $cmdata * @return ResponseInterface */ #[route( path: '/cms/{cm}/restricted', pathtypes: [ - new \core\router\parameters\path_module(), + new \core\router\parameters\path_coursemodule(name: 'cm'), ], requirelogin: new require_login( requirelogin: true, @@ -52,28 +52,28 @@ class restricted_module { public function restricted_module_page( ServerRequestInterface $request, ResponseInterface $response, - \stdClass $cm, + \stdClass $cmdata, ): ResponseInterface { global $OUTPUT, $PAGE; - $context = \context_module::instance($cm->id); + $context = \context_module::instance($cmdata->id); - $course = get_course($cm->course); + $course = get_course($cmdata->course); $modinfo = get_fast_modinfo($course); - $cminfo = $modinfo->get_cm($cm->id); - $sectioninfo = $modinfo->get_section_info_by_id($cm->section); + $cminfo = $modinfo->get_cm($cmdata->id); + $sectioninfo = $modinfo->get_section_info_by_id($cmdata->section); $format = course_get_format($course); $course->format = $format->get_format(); $url = \core\router\util::get_path_for_callable( [self::class, 'restricted_module_page'], - ['cm' => $cm->id], + ['cm' => $cmdata->id], ); - $PAGE->set_url($url, ['cm' => $cm->id]); + $PAGE->set_url($url, ['cm' => $cmdata->id]); $PAGE->add_body_class('limitedwidth'); $PAGE->set_context($context); - $PAGE->set_pagetype('mod-' . $cm->modname . '-restricted'); + $PAGE->set_pagetype('mod-' . $cminfo->modname . '-restricted'); $strtitle = get_string('restrictedtitle', 'course', $cminfo->get_name()); $PAGE->set_title($strtitle . \moodle_page::TITLE_SEPARATOR . $course->shortname); $PAGE->set_heading($course->fullname); @@ -94,7 +94,7 @@ class restricted_module { $response->getBody()->write($OUTPUT->footer()); $eventdata = [ - 'objectid' => $cm->id, + 'objectid' => $cmdata->id, 'context' => $context, ]; $event = \core\event\course_restricted_module_viewed::create($eventdata); diff --git a/public/lang/en/question.php b/public/lang/en/question.php index 40bbd6d0e1e..ea4adaa9c66 100644 --- a/public/lang/en/question.php +++ b/public/lang/en/question.php @@ -82,6 +82,7 @@ $string['clicktounflag'] = 'Remove flag'; $string['clickunflag'] = 'Remove flag'; $string['contexterror'] = 'You shouldn\'t have got here if you\'re not moving a category to another context.'; $string['copy'] = 'Copy from {$a} and change links.'; +$string['coursenamebankname'] = '{$a->coursename} - {$a->bankname}'; $string['created'] = 'Created'; $string['createdefault'] = 'Create default question bank'; $string['createdby'] = 'Created by'; @@ -327,6 +328,9 @@ $string['shareincontext'] = 'Share in context for {$a}'; $string['stoponerror'] = 'Stop on error'; $string['stoponerror_help'] = 'This setting determines whether the import process stops when an error is detected, resulting in no questions being imported, or whether any questions containing errors are ignored and any valid questions are imported.'; $string['switchbank'] = 'Switch bank'; +$string['switchergoback'] = 'Go back'; +$string['switchersearchbyname'] = 'Search by name...'; +$string['switcherselectbank'] = 'Select question bank'; $string['tofilecategory'] = 'Write category to file'; $string['tofilecontext'] = 'Write context to file'; $string['topfor'] = 'Top for {$a}'; diff --git a/public/lib/classes/router/parameters/course_parameter_trait.php b/public/lib/classes/router/parameters/course_parameter_trait.php new file mode 100644 index 00000000000..bc386b4f696 --- /dev/null +++ b/public/lib/classes/router/parameters/course_parameter_trait.php @@ -0,0 +1,143 @@ +. + +namespace core\router\parameters; + +use core\exception\not_found_exception; +use core\param; +use core\router\schema\example; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Trait for route parameters that pass a course identifier, to attach the course data and context to the request. + * + * This holds common code used in {@see path_course} and {@see query_course}. + * + * @package core + * @copyright 2026 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait course_parameter_trait { + /** + * Create a new course parameter. + * + * @param string $name The name of the parameter to use for the course identifier + * @param mixed ...$extra Additional arguments + */ + public function __construct( + string $name = 'course', + ...$extra, + ) { + $extra['name'] = $name; + $extra['type'] = param::RAW; + $extra['description'] = <<get_record('course', [ + 'id' => $value, + ]); + } else if (str_starts_with($value, 'idnumber:')) { + $data = $DB->get_record('course', [ + 'idnumber' => substr($value, strlen('idnumber:')), + ]); + } else if (str_starts_with($value, 'name:')) { + $data = $DB->get_record('course', [ + 'shortname' => substr($value, strlen('name:')), + ]); + } + + if ($data) { + return $data; + } + + throw new not_found_exception('course', $value); + } + + /** + * Add course and course context parameters to the request. + * + * @param ServerRequestInterface $request + * @param string $value + * @return ServerRequestInterface + */ + public function add_attributes_for_parameter_value( + ServerRequestInterface $request, + string $value, + ): ServerRequestInterface { + $course = $this->get_course_for_value($value); + + return $request + ->withAttribute($this->name, $course) + ->withAttribute("{$this->name}context", \core\context\course::instance($course->id)); + } + + /** + * Get the schema for the parameter type, with valid patterns. + * + * @param param $type + * @return \stdClass + */ + public function get_schema_from_type(param $type): \stdClass { + $schema = parent::get_schema_from_type($type); + + $schema->pattern = "^("; + $schema->pattern .= implode("|", [ + '\d+', + 'idnumber:.+', + 'name:.+', + ]); + $schema->pattern .= ")$"; + + return $schema; + } +} diff --git a/public/lib/classes/router/parameters/coursemodule_parameter_trait.php b/public/lib/classes/router/parameters/coursemodule_parameter_trait.php new file mode 100644 index 00000000000..309f6e01d56 --- /dev/null +++ b/public/lib/classes/router/parameters/coursemodule_parameter_trait.php @@ -0,0 +1,142 @@ +. + +namespace core\router\parameters; + +use core\exception\not_found_exception; +use core\param; +use core\router\schema\example; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Trait for route parameters that pass a course module identifier, to attach the course module data and context to the request. + * + * This code used in {@see query_coursemodule} that can be reused for other parameter types referencing a course module. + * + * @package core + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait coursemodule_parameter_trait { + /** + * Create a new coursemodule parameter. + * + * @param string $name The name of the parameter to use for the course identifier + * @param mixed ...$extra Additional arguments + */ + public function __construct( + string $name = 'coursemodule', + ...$extra, + ) { + $extra['name'] = $name; + $extra['type'] = param::RAW; + $extra['description'] = <<get_record('course_modules', [ + 'id' => $value, + ]); + } else if (str_starts_with($value, 'idnumber:')) { + $data = $DB->get_record('course_modules', [ + 'idnumber' => substr($value, strlen('idnumber:')), + ]); + } + + if ($data) { + return $data; + } + + throw new not_found_exception('coursemodule', $value); + } + + /** + * Add course module data and course module context parameters to the request. + * + * @param ServerRequestInterface $request + * @param string $value + * @return ServerRequestInterface + */ + public function add_attributes_for_parameter_value( + ServerRequestInterface $request, + string $value, + ): ServerRequestInterface { + $data = $this->get_data_for_value($value); + + return $request + ->withAttribute($this->get_value_name('data'), $data) + ->withAttribute($this->get_value_name('context'), \core\context\module::instance($data->id)); + } + + /** + * Get the schema for the parameter type, with valid patterns. + * + * @param param $type + * @return \stdClass + */ + public function get_schema_from_type(param $type): \stdClass { + $schema = parent::get_schema_from_type($type); + + $schema->pattern = "^("; + $schema->pattern .= implode("|", [ + '\d+', + 'idnumber:.+', + ]); + $schema->pattern .= ")$"; + + return $schema; + } + + /** + * Get the name of the parameter with the given suffix. + * + * @param string $suffix + * @return string + */ + protected function get_value_name(string $suffix): string { + return $this->name . $suffix; + } +} diff --git a/public/lib/classes/router/parameters/path_course.php b/public/lib/classes/router/parameters/path_course.php index 8db8f9d1f83..bf4e1784a29 100644 --- a/public/lib/classes/router/parameters/path_course.php +++ b/public/lib/classes/router/parameters/path_course.php @@ -16,120 +16,16 @@ namespace core\router\parameters; -use core\exception\not_found_exception; -use core\param; -use core\router\schema\example; -use core\router\schema\parameters\mapped_property_parameter; -use core\router\schema\referenced_object; -use Psr\Http\Message\ServerRequestInterface; - /** * A Moodle parameter referenced in the path. * * @package core - * @copyright 2023 Andrew Lyons + * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class path_course extends \core\router\schema\parameters\path_parameter implements - mapped_property_parameter, - referenced_object + \core\router\schema\parameters\mapped_property_parameter, + \core\router\schema\referenced_object { - /** - * Create a new path_course parameter. - * - * @param string $name The name of the parameter to use for the course identifier - * @param mixed ...$extra Additional arguments - */ - public function __construct( - string $name = 'course', - ...$extra, - ) { - $extra['name'] = $name; - $extra['type'] = param::RAW; - $extra['description'] = <<get_record('course', [ - 'id' => $value, - ]); - } else if (str_starts_with($value, 'idnumber:')) { - $data = $DB->get_record('course', [ - 'idnumber' => substr($value, strlen('idnumber:')), - ]); - } else if (str_starts_with($value, 'name:')) { - $data = $DB->get_record('course', [ - 'shortname' => substr($value, strlen('name:')), - ]); - } - - if ($data) { - return $data; - } - - throw new not_found_exception('course', $value); - } - - #[\Override] - public function add_attributes_for_parameter_value( - ServerRequestInterface $request, - string $value, - ): ServerRequestInterface { - $course = $this->get_course_for_value($value); - - return $request - ->withAttribute($this->name, $course) - ->withAttribute("{$this->name}context", \core\context\course::instance($course->id)); - } - - #[\Override] - public function get_schema_from_type(param $type): \stdClass { - $schema = parent::get_schema_from_type($type); - - $schema->pattern = "^("; - $schema->pattern .= implode("|", [ - '\d+', - 'idnumber:.+', - 'name:.+', - ]); - $schema->pattern .= ")$"; - - return $schema; - } + use course_parameter_trait; } diff --git a/public/lib/classes/router/parameters/path_coursemodule.php b/public/lib/classes/router/parameters/path_coursemodule.php new file mode 100644 index 00000000000..e057a02ae49 --- /dev/null +++ b/public/lib/classes/router/parameters/path_coursemodule.php @@ -0,0 +1,32 @@ +. + +namespace core\router\parameters; + +/** + * A path parameter referencing a course module ID. This will add the course module data object and context to the request. + * + * @package core + * @copyright Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class path_coursemodule extends \core\router\schema\parameters\path_parameter implements + \core\router\schema\parameters\mapped_property_parameter, + \core\router\schema\referenced_object +{ + use coursemodule_parameter_trait; +} diff --git a/public/lib/classes/router/parameters/path_module.php b/public/lib/classes/router/parameters/path_module.php deleted file mode 100644 index 42c4c636578..00000000000 --- a/public/lib/classes/router/parameters/path_module.php +++ /dev/null @@ -1,77 +0,0 @@ -. - -namespace core\router\parameters; - -use core\exception\not_found_exception; -use core\param; -use core\router\schema\example; -use core\router\schema\parameters\mapped_property_parameter; -use core\router\schema\referenced_object; -use Psr\Http\Message\ServerRequestInterface; - -/** - * A Moodle parameter referenced in the path. - * - * @package core - * @copyright 2026 Amaia Anabitarte - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class path_module extends \core\router\schema\parameters\path_parameter implements - mapped_property_parameter, - referenced_object -{ - /** - * Create a new path_module parameter. - * - * @param string $name The name of the parameter to use for the module identifier - * @param mixed ...$extra Additional arguments - */ - public function __construct( - string $name = 'cm', - ...$extra, - ) { - $extra['name'] = $name; - $extra['type'] = param::RAW; - $extra['description'] = <<withAttribute($this->name, $cm) - ->withAttribute("{$this->name}context", \core\context\module::instance($cm->id)); - } -} diff --git a/public/lib/classes/router/parameters/query_course.php b/public/lib/classes/router/parameters/query_course.php new file mode 100644 index 00000000000..7321015b0ed --- /dev/null +++ b/public/lib/classes/router/parameters/query_course.php @@ -0,0 +1,31 @@ +. + +namespace core\router\parameters; + +/** + * A query parameter referencing a course. + * + * @package core + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class query_course extends \core\router\schema\parameters\query_parameter implements + \core\router\schema\parameters\mapped_property_parameter, + \core\router\schema\referenced_object +{ + use course_parameter_trait; +} diff --git a/public/lib/classes/router/parameters/query_coursemodule.php b/public/lib/classes/router/parameters/query_coursemodule.php new file mode 100644 index 00000000000..02adf2e55d7 --- /dev/null +++ b/public/lib/classes/router/parameters/query_coursemodule.php @@ -0,0 +1,31 @@ +. + +namespace core\router\parameters; + +/** + * A query parameter referencing a course module. + * + * @package core + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class query_coursemodule extends \core\router\schema\parameters\query_parameter implements + \core\router\schema\parameters\mapped_property_parameter, + \core\router\schema\referenced_object +{ + use coursemodule_parameter_trait; +} diff --git a/public/lib/classes/router/schema/parameters/query_parameter.php b/public/lib/classes/router/schema/parameters/query_parameter.php index e3f7c402206..1581e76fae9 100644 --- a/public/lib/classes/router/schema/parameters/query_parameter.php +++ b/public/lib/classes/router/schema/parameters/query_parameter.php @@ -74,7 +74,13 @@ class query_parameter extends parameter { default => throw new \ValueError('Invalid boolean value.'), }; } - $this->type->validate_param($params[$this->name]); + $value = $params[$this->name]; + + $this->type->validate_param($value); + + if ($value !== null && is_a($this, mapped_property_parameter::class)) { + $request = $this->add_attributes_for_parameter_value($request, $value); + } return $this->update_request_params( $request, diff --git a/public/lib/tests/router/parameters/path_module_test.php b/public/lib/tests/router/parameters/path_coursemodule_test.php similarity index 86% rename from public/lib/tests/router/parameters/path_module_test.php rename to public/lib/tests/router/parameters/path_coursemodule_test.php index 95b8f3b4d53..170ee35eef5 100644 --- a/public/lib/tests/router/parameters/path_module_test.php +++ b/public/lib/tests/router/parameters/path_coursemodule_test.php @@ -17,7 +17,6 @@ namespace core\router\parameters; use core\exception\not_found_exception; -use core\router\parameters\path_module; use core\tests\router\route_testcase; use GuzzleHttp\Psr7\ServerRequest; use stdClass; @@ -28,9 +27,9 @@ use stdClass; * @package core * @copyright Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @covers \core\router\parameters\path_module + * @covers \core\router\parameters\path_coursemodule */ -final class path_module_test extends route_testcase { +final class path_coursemodule_test extends route_testcase { public function test_module_id(): void { $this->resetAfterTest(); @@ -38,15 +37,15 @@ final class path_module_test extends route_testcase { $mod = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); $modcontext = \context_module::instance($mod->cmid); - $param = new path_module(); + $param = new path_coursemodule(); $request = new ServerRequest('GET', '/course/cms/' . $mod->cmid . '/restricted'); $newrequest = $param->add_attributes_for_parameter_value($request, $mod->cmid); - $this->assertInstanceOf(stdClass::class, $newrequest->getAttribute('cm')); - $this->assertInstanceOf(\core\context\module::class, $newrequest->getAttribute('cmcontext')); + $this->assertInstanceOf(stdClass::class, $newrequest->getAttribute('coursemoduledata')); + $this->assertInstanceOf(\core\context\module::class, $newrequest->getAttribute('coursemodulecontext')); - $this->assertEquals($mod->cmid, $newrequest->getAttribute('cm')->id); - $this->assertEquals($modcontext->id, $newrequest->getAttribute('cmcontext')->id); + $this->assertEquals($mod->cmid, $newrequest->getAttribute('coursemoduledata')->id); + $this->assertEquals($modcontext->id, $newrequest->getAttribute('coursemodulecontext')->id); } /** @@ -55,7 +54,7 @@ final class path_module_test extends route_testcase { public function test_module_not_found(): void { $this->resetAfterTest(); - $param = new path_module(); + $param = new path_coursemodule(); $course = $this->getDataGenerator()->create_course(); $modid = 9999; diff --git a/public/mod/quiz/amd/build/add_question_modal.min.js b/public/mod/quiz/amd/build/add_question_modal.min.js index 8c2fee6443c..9216d5c872a 100644 --- a/public/mod/quiz/amd/build/add_question_modal.min.js +++ b/public/mod/quiz/amd/build/add_question_modal.min.js @@ -1,10 +1,10 @@ -define("mod_quiz/add_question_modal",["exports","core/modal","core/fragment","core/str","core/form-autocomplete"],(function(_exports,_modal,Fragment,_str,_formAutocomplete){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("mod_quiz/add_question_modal",["exports","core/modal"],(function(_exports,_modal){var obj; /** * Contain the logic for the add random question modal. * * @module mod_quiz/add_question_modal * @copyright 2023 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),Fragment=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete);class AddQuestionModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.quizCmId=modalConfig.quizCmId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,super.configure(modalConfig)}constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}async handleSwitchBankContentReload(Selector){var _document$querySelect;this.setTitle((0,_str.getString)("selectquestionbank","mod_quiz"));const el=document.createElement("button");el.classList.add("btn","btn-primary"),el.textContent=await(0,_str.getString)("gobacktoquiz","mod_quiz"),el.setAttribute("data-action","go-back"),el.setAttribute("value",this.bankCmId),this.setFooter(el),this.setBody(Fragment.loadFragment("mod_quiz","switch_question_bank",this.getContextId(),{quizcmid:this.quizCmId,bankcmid:this.bankCmId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");return await this.getBodyPromise(),await _formAutocomplete.default.enhance(Selector,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),null===(_document$querySelect=document.querySelector(".search-banks .form-autocomplete-selection"))||void 0===_document$querySelect||_document$querySelect.classList.add("d-none"),this}}return _exports.default=AddQuestionModal,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class AddQuestionModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.quizCmId=modalConfig.quizCmId,this.bankCmId=modalConfig.bankCmId,this.courseId=modalConfig.courseId,this.originalTitle=modalConfig.title,super.configure(modalConfig)}constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}}return _exports.default=AddQuestionModal,_exports.default})); //# sourceMappingURL=add_question_modal.min.js.map \ No newline at end of file diff --git a/public/mod/quiz/amd/build/add_question_modal.min.js.map b/public/mod/quiz/amd/build/add_question_modal.min.js.map index fc4b9eebcf0..1953fcab333 100644 --- a/public/mod/quiz/amd/build/add_question_modal.min.js.map +++ b/public/mod/quiz/amd/build/add_question_modal.min.js.map @@ -1 +1 @@ -{"version":3,"file":"add_question_modal.min.js","sources":["../src/add_question_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/add_question_modal\n * @copyright 2023 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\n\nexport default class AddQuestionModal extends Modal {\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n\n // Store the quiz module id for when we need to POST to the quiz.\n // This is because the URL cmid param will change during filter operations as we will be in another bank context.\n this.quizCmId = modalConfig.quizCmId;\n this.bankCmId = modalConfig.bankCmId;\n\n // Store the original title of the modal, so we can revert back to it once we have switched to another bank.\n this.originalTitle = modalConfig.title;\n\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n constructor(root) {\n super(root);\n\n this.contextId = null;\n this.addOnPageId = null;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {Number} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {Number}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {Number} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n }\n\n /**\n * Returns the saved page id for the question to be added to.\n *\n * @method getAddOnPageId\n * @return {Number}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n\n /**\n * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields.\n *\n * @param {String} Selector for the original select element.\n * @return {Promise} Modal.\n */\n async handleSwitchBankContentReload(Selector) {\n this.setTitle(getString('selectquestionbank', 'mod_quiz'));\n\n // Create a 'Go back' button and set it in the footer.\n const el = document.createElement('button');\n el.classList.add('btn', 'btn-primary');\n el.textContent = await getString('gobacktoquiz', 'mod_quiz');\n el.setAttribute('data-action', 'go-back');\n el.setAttribute('value', this.bankCmId);\n this.setFooter(el);\n\n this.setBody(\n Fragment.loadFragment(\n 'mod_quiz',\n 'switch_question_bank',\n this.getContextId(),\n {\n 'quizcmid': this.quizCmId,\n 'bankcmid': this.bankCmId,\n })\n );\n const placeholder = await getString('searchbyname', 'mod_quiz');\n await this.getBodyPromise();\n await AutoComplete.enhance(\n Selector,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true\n );\n\n // Hide the selection element as we don't need it.\n document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none');\n\n return this;\n }\n}\n"],"names":["AddQuestionModal","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","quizCmId","bankCmId","originalTitle","title","constructor","root","addOnPageId","id","getContextId","this","getAddOnPageId","Selector","setTitle","el","document","createElement","classList","add","textContent","setAttribute","setFooter","setBody","Fragment","loadFragment","placeholder","getBodyPromise","AutoComplete","enhance","querySelector"],"mappings":";;;;;;;61BA4BqBA,yBAAyBC,eAC1CC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,gBAI3BC,SAAWR,YAAYQ,cACvBC,SAAWT,YAAYS,cAGvBC,cAAgBV,YAAYW,YAG3BZ,UAAUC,aAGpBY,YAAYC,YACFA,WAEDR,UAAY,UACZS,YAAc,KAUvBV,aAAaW,SACJV,UAAYU,GASrBC,sBACWC,KAAKZ,UAUhBC,eAAeS,SACND,YAAcC,GASvBG,wBACWD,KAAKH,gDASoBK,yCAC3BC,UAAS,kBAAU,qBAAsB,mBAGxCC,GAAKC,SAASC,cAAc,UAClCF,GAAGG,UAAUC,IAAI,MAAO,eACxBJ,GAAGK,kBAAoB,kBAAU,eAAgB,YACjDL,GAAGM,aAAa,cAAe,WAC/BN,GAAGM,aAAa,QAASV,KAAKR,eACzBmB,UAAUP,SAEVQ,QACDC,SAASC,aACL,WACA,uBACAd,KAAKD,eACL,UACgBC,KAAKT,kBACLS,KAAKR,kBAGvBuB,kBAAoB,kBAAU,eAAgB,yBAC9Cf,KAAKgB,uBACLC,0BAAaC,QACfhB,UACA,EACA,0CACAa,aACA,GACA,EACA,IACA,iCAIJV,SAASc,cAAc,sGAA+CZ,UAAUC,IAAI,UAE7ER"} \ No newline at end of file +{"version":3,"file":"add_question_modal.min.js","sources":["../src/add_question_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/add_question_modal\n * @copyright 2023 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\n\nexport default class AddQuestionModal extends Modal {\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n\n // Store the quiz module id for when we need to POST to the quiz.\n // This is because the URL cmid param will change during filter operations as we will be in another bank context.\n this.quizCmId = modalConfig.quizCmId;\n this.bankCmId = modalConfig.bankCmId;\n this.courseId = modalConfig.courseId;\n\n // Store the original title of the modal, so we can revert back to it once we have switched to another bank.\n this.originalTitle = modalConfig.title;\n\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n constructor(root) {\n super(root);\n\n this.contextId = null;\n this.addOnPageId = null;\n }\n\n /**\n * Save the Moodle context id that the question bank is being\n * rendered in.\n *\n * @method setContextId\n * @param {Number} id\n */\n setContextId(id) {\n this.contextId = id;\n }\n\n /**\n * Retrieve the saved Moodle context id.\n *\n * @method getContextId\n * @return {Number}\n */\n getContextId() {\n return this.contextId;\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {Number} id\n */\n setAddOnPageId(id) {\n this.addOnPageId = id;\n }\n\n /**\n * Returns the saved page id for the question to be added to.\n *\n * @method getAddOnPageId\n * @return {Number}\n */\n getAddOnPageId() {\n return this.addOnPageId;\n }\n}\n"],"names":["AddQuestionModal","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","quizCmId","bankCmId","courseId","originalTitle","title","constructor","root","addOnPageId","id","getContextId","this","getAddOnPageId"],"mappings":";;;;;;;iJAyBqBA,yBAAyBC,eAC1CC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,gBAI3BC,SAAWR,YAAYQ,cACvBC,SAAWT,YAAYS,cACvBC,SAAWV,YAAYU,cAGvBC,cAAgBX,YAAYY,YAG3Bb,UAAUC,aAGpBa,YAAYC,YACFA,WAEDT,UAAY,UACZU,YAAc,KAUvBX,aAAaY,SACJX,UAAYW,GASrBC,sBACWC,KAAKb,UAUhBC,eAAeU,SACND,YAAcC,GASvBG,wBACWD,KAAKH"} \ No newline at end of file diff --git a/public/mod/quiz/amd/build/modal_add_random_question.min.js b/public/mod/quiz/amd/build/modal_add_random_question.min.js index 8186390a1fc..efed2b7de91 100644 --- a/public/mod/quiz/amd/build/modal_add_random_question.min.js +++ b/public/mod/quiz/amd/build/modal_add_random_question.min.js @@ -1,3 +1,3 @@ -define("mod_quiz/modal_add_random_question",["exports","jquery","./add_question_modal","core/notification","core/fragment","core/templates","core_form/changechecker","core/ajax","core/pending"],(function(_exports,_jquery,_add_question_modal,Notification,Fragment,Templates,FormChangeChecker,_ajax,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_add_question_modal=_interopRequireDefault(_add_question_modal),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Templates=_interopRequireWildcard(Templates),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),_pending=_interopRequireDefault(_pending);const SELECTORS={ANCHOR:"a[href]",EXISTING_CATEGORY_CONTAINER:'[data-region="existing-category-container"]',EXISTING_CATEGORY_TAB:"#id_existingcategoryheader",NEW_CATEGORY_CONTAINER:'[data-region="new-category-container"]',NEW_CATEGORY_TAB:"#id_newcategoryheader",TAB_CONTENT:'[data-region="tab-content"]',ADD_ON_PAGE_FORM_ELEMENT:'[name="addonpage"]',ADD_RANDOM_BUTTON:'input[type="submit"][name="addrandom"]',ADD_NEW_CATEGORY_BUTTON:'input[type="submit"][name="newcategory"]',SUBMIT_BUTTON_ELEMENT:'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"], input[type="submit"][name="update"]',FORM_HEADER:"legend",SELECT_NUMBER_TO_ADD:"#menurandomcount",NEW_CATEGORY_ELEMENT:"#categoryname",PARENT_CATEGORY_ELEMENT:"#parentcategory",FILTER_CONDITION_ELEMENT:"[data-filtercondition]",FORM_ELEMENT:"#add_random_question_form",MESSAGE_INPUT:'[name="message"]',SWITCH_TO_OTHER_BANK:'button[data-action="switch-question-bank"]',NEW_BANKMOD_ID:"data-newmodid",BANK_SEARCH:"#searchbanks",GO_BACK_BUTTON:'button[data-action="go-back"]',UPDATE_FILTER_BUTTON:'input[type="submit"][name="update"]'};class ModalAddRandomQuestion extends _add_question_modal.default{static init(contextId,bankCmId,category,returnUrl,quizCmId){let showNewCategory=!(arguments.length>5&&void 0!==arguments[5])||arguments[5];document.addEventListener("click",(e=>{const trigger=e.target.closest('.menu [data-action="addarandomquestion"], [data-action="editrandomquestion"]');trigger&&(e.preventDefault(),trigger.dataset.slotid&&(showNewCategory=!1),ModalAddRandomQuestion.create({contextId:contextId,bankCmId:bankCmId,category:category,returnUrl:returnUrl,quizCmId:quizCmId,showNewCategory:showNewCategory,title:trigger.dataset.header,addOnPage:trigger.dataset.addonpage,slotId:trigger.dataset.slotid,templateContext:{hidden:showNewCategory}}))}))}constructor(root){super(root),this.category=null,this.returnUrl=null,this.quizCmId=null,this.loadedForm=!1,this.slotId=0,this.savedFilterCondition=null}configure(modalConfig){var _modalConfig$slotId,_modalConfig$savedFil;modalConfig.removeOnClose=!0,this.setCategory(modalConfig.category),this.setReturnUrl(modalConfig.returnUrl),this.showNewCategory=modalConfig.showNewCategory,this.setSlotId(null!==(_modalConfig$slotId=modalConfig.slotId)&&void 0!==_modalConfig$slotId?_modalConfig$slotId:0),this.setSavedFilterCondition(null!==(_modalConfig$savedFil=modalConfig.savedFilterCondition)&&void 0!==_modalConfig$savedFil?_modalConfig$savedFil:null),super.configure(modalConfig)}setAddOnPageId(id){super.setAddOnPageId(id),this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id)}setCategory(category){this.category=category}getCategory(){return this.category}setReturnUrl(url){this.returnUrl=url}getReturnUrl(){return this.returnUrl}setSlotId(slotId){this.slotId=slotId}getSlotId(){return this.slotId}setSavedFilterCondition(filterCondition){this.savedFilterCondition=filterCondition}getSavedFilterCondition(){return this.savedFilterCondition}moveContentIntoTab(tabContent,tabElement){tabContent.find(SELECTORS.FORM_HEADER).addClass("hidden"),tabContent.wrap(tabElement)}moveTabsIntoTabContent(form){const tabContent=this.getBody().find(SELECTORS.TAB_CONTENT).empty();form.find('[role="tabpanel"]').wrapAll(tabContent)}moveCancelButtonToTabs(form){const cancelButton=form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass("ms-1"),tabFooters=form.find('[data-region="footer"]');cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove(),cancelButton.clone().appendTo(tabFooters)}loadForm(){const addonpage=this.getAddOnPageId(),returnurl=this.getReturnUrl(),quizcmid=this.quizCmId,bankcmid=this.bankCmId,savedfiltercondition=this.getSavedFilterCondition();return this.setSavedFilterCondition(null),Fragment.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:null!=addonpage?addonpage:null,returnurl:returnurl,quizcmid:quizcmid,bankcmid:bankcmid,slotid:this.getSlotId(),savedfiltercondition:savedfiltercondition}).then(((html,js)=>{const form=(0,_jquery.default)(html);if(!this.getSlotId()){const existingCategoryTabContent=form.find(SELECTORS.EXISTING_CATEGORY_TAB),existingCategoryTab=this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER),newCategoryTabContent=form.find(SELECTORS.NEW_CATEGORY_TAB),newCategoryTab=this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);this.moveContentIntoTab(existingCategoryTabContent,existingCategoryTab),this.moveContentIntoTab(newCategoryTabContent,newCategoryTab),this.moveTabsIntoTabContent(form)}Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT),form,js)})).then((()=>{FormChangeChecker.disableAllChecks(),this.getBody()[0].addEventListener("click",(e=>{if(!e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT))return;e.preventDefault(),document.querySelector('#questionscontainer input[name="cmid"]').setAttribute("name",this.quizCmId);if(e.target.closest(SELECTORS.ADD_RANDOM_BUTTON)){var _document$querySelect;const randomcount=document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value,filtercondition=null===(_document$querySelect=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect?void 0:_document$querySelect.filtercondition;return void this.addQuestions(quizcmid,addonpage,randomcount,filtercondition,"","")}if(e.target.closest(SELECTORS.UPDATE_FILTER_BUTTON)){var _document$querySelect2;const filtercondition=null===(_document$querySelect2=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.filtercondition;return void this.updateFilterCondition(quizcmid,this.getSlotId(),filtercondition)}e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON)&&this.addQuestions(quizcmid,addonpage,1,"",document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value)})),this.getModal().on("click",SELECTORS.SWITCH_TO_OTHER_BANK,(()=>{var _document$querySelect3;this.setSavedFilterCondition(null===(_document$querySelect3=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect3?void 0:_document$querySelect3.filtercondition),this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH).then((function(ModalQuizQuestionBank){var _$;return null===(_$=(0,_jquery.default)(SELECTORS.BANK_SEARCH))||void 0===_$||_$.on("change",(e=>{const bankCmId=(0,_jquery.default)(e.currentTarget).val();bankCmId>0&&ModalAddRandomQuestion.create({contextId:ModalQuizQuestionBank.getContextId(),bankCmId:bankCmId,category:ModalQuizQuestionBank.getCategory(),returnUrl:ModalQuizQuestionBank.getReturnUrl(),quizCmId:ModalQuizQuestionBank.quizCmId,title:ModalQuizQuestionBank.originalTitle,addOnPage:ModalQuizQuestionBank.getAddOnPageId(),templateContext:{hidden:ModalQuizQuestionBank.showNewCategory},showNewCategory:ModalQuizQuestionBank.showNewCategory,slotId:ModalQuizQuestionBank.getSlotId()}).then(ModalQuizQuestionBank.destroy()).catch(Notification.exception)})),ModalQuizQuestionBank}))})),this.getModal().on("click",SELECTORS.GO_BACK_BUTTON,(e=>{const anchorElement=(0,_jquery.default)(e.currentTarget);ModalAddRandomQuestion.create({contextId:this.getContextId(),bankCmId:anchorElement.attr("value"),category:this.getCategory(),returnUrl:this.getReturnUrl(),quizCmId:this.quizCmId,title:this.originalTitle,addOnPage:this.getAddOnPageId(),templateContext:{hidden:this.showNewCategory},showNewCategory:this.showNewCategory,savedFilterCondition:this.getSavedFilterCondition(),slotId:this.getSlotId()}).then(this.destroy()).catch(Notification.exception)})),this.getModal().on("click",SELECTORS.ANCHOR,(e=>{const anchorElement=(0,_jquery.default)(e.currentTarget);anchorElement.closest("a["+SELECTORS.NEW_BANKMOD_ID+"]").length&&ModalAddRandomQuestion.create({contextId:this.getContextId(),bankCmId:anchorElement.attr(SELECTORS.NEW_BANKMOD_ID),category:this.getCategory(),returnUrl:this.getReturnUrl(),quizCmId:this.quizCmId,title:this.originalTitle,addOnPage:this.getAddOnPageId(),templateContext:{hidden:this.showNewCategory},showNewCategory:this.showNewCategory,slotId:this.getSlotId()}).then(this.destroy()).catch(Notification.exception)}))})).catch(Notification.exception)}async addQuestions(quizcmid,addonpage,randomcount,filtercondition,newcategory,parentcategory){new _pending.default("mod-quiz/modal_add_random_questions");const call={methodname:"mod_quiz_add_random_questions",args:{cmid:quizcmid,addonpage:addonpage,randomcount:randomcount,filtercondition:filtercondition,newcategory:newcategory,parentcategory:parentcategory}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}async updateFilterCondition(quizcmid,slotid,filtercondition){new _pending.default("mod-quiz/modal_add_random_questions");const call={methodname:"mod_quiz_update_filter_condition",args:{cmid:quizcmid,slotid:slotid,filtercondition:filtercondition}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}show(){super.show(this),this.loadedForm||(this.tabHtml=this.getBody(),this.loadForm(window.location.search),this.loadedForm=!0)}}return _exports.default=ModalAddRandomQuestion,_defineProperty(ModalAddRandomQuestion,"TYPE","mod_quiz-quiz-add-random-question"),_defineProperty(ModalAddRandomQuestion,"TEMPLATE","mod_quiz/modal_add_random_question"),ModalAddRandomQuestion.registerModalType(),_exports.default})); +define("mod_quiz/modal_add_random_question",["exports","jquery","./add_question_modal","core/notification","core/fragment","core/templates","core_form/changechecker","core/ajax","core/pending","core_question/bank_switcher"],(function(_exports,_jquery,_add_question_modal,Notification,Fragment,Templates,FormChangeChecker,_ajax,_pending,_bank_switcher){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_add_question_modal=_interopRequireDefault(_add_question_modal),Notification=_interopRequireWildcard(Notification),Fragment=_interopRequireWildcard(Fragment),Templates=_interopRequireWildcard(Templates),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),_pending=_interopRequireDefault(_pending),_bank_switcher=_interopRequireDefault(_bank_switcher);const SELECTORS={ANCHOR:"a[href]",EXISTING_CATEGORY_CONTAINER:'[data-region="existing-category-container"]',EXISTING_CATEGORY_TAB:"#id_existingcategoryheader",NEW_CATEGORY_CONTAINER:'[data-region="new-category-container"]',NEW_CATEGORY_TAB:"#id_newcategoryheader",TAB_CONTENT:'[data-region="tab-content"]',ADD_ON_PAGE_FORM_ELEMENT:'[name="addonpage"]',ADD_RANDOM_BUTTON:'input[type="submit"][name="addrandom"]',ADD_NEW_CATEGORY_BUTTON:'input[type="submit"][name="newcategory"]',SUBMIT_BUTTON_ELEMENT:'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"], input[type="submit"][name="update"]',FORM_HEADER:"legend",SELECT_NUMBER_TO_ADD:"#menurandomcount",NEW_CATEGORY_ELEMENT:"#categoryname",PARENT_CATEGORY_ELEMENT:"#parentcategory",FILTER_CONDITION_ELEMENT:"[data-filtercondition]",FORM_ELEMENT:"#add_random_question_form",MESSAGE_INPUT:'[name="message"]',SWITCH_TO_OTHER_BANK:'button[data-action="switch-question-bank"]',NEW_BANKMOD_ID:"data-newmodid",BANK_SEARCH:"#searchbanks",GO_BACK_BUTTON:'button[data-action="go-back"]',UPDATE_FILTER_BUTTON:'input[type="submit"][name="update"]'};class ModalAddRandomQuestion extends _add_question_modal.default{static init(contextId,bankCmId,category,returnUrl,quizCmId){let showNewCategory=!(arguments.length>5&&void 0!==arguments[5])||arguments[5],courseId=arguments.length>6?arguments[6]:void 0;document.addEventListener("click",(e=>{const trigger=e.target.closest('.menu [data-action="addarandomquestion"], [data-action="editrandomquestion"]');trigger&&(e.preventDefault(),trigger.dataset.slotid&&(showNewCategory=!1),ModalAddRandomQuestion.create({contextId:contextId,bankCmId:bankCmId,category:category,returnUrl:returnUrl,quizCmId:quizCmId,showNewCategory:showNewCategory,title:trigger.dataset.header,addOnPage:trigger.dataset.addonpage,slotId:trigger.dataset.slotid,templateContext:{hidden:showNewCategory},courseId:courseId}))}))}constructor(root){super(root),this.category=null,this.returnUrl=null,this.quizCmId=null,this.loadedForm=!1,this.slotId=0,this.savedFilterCondition=null}configure(modalConfig){var _modalConfig$slotId,_modalConfig$savedFil;modalConfig.removeOnClose=!0,this.setCategory(modalConfig.category),this.setReturnUrl(modalConfig.returnUrl),this.showNewCategory=modalConfig.showNewCategory,this.setSlotId(null!==(_modalConfig$slotId=modalConfig.slotId)&&void 0!==_modalConfig$slotId?_modalConfig$slotId:0),this.setSavedFilterCondition(null!==(_modalConfig$savedFil=modalConfig.savedFilterCondition)&&void 0!==_modalConfig$savedFil?_modalConfig$savedFil:null),this.setCourseId(modalConfig.courseId),super.configure(modalConfig)}setAddOnPageId(id){super.setAddOnPageId(id),this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id)}setCategory(category){this.category=category}getCategory(){return this.category}setReturnUrl(url){this.returnUrl=url}getReturnUrl(){return this.returnUrl}setSlotId(slotId){this.slotId=slotId}getSlotId(){return this.slotId}setCourseId(courseId){this.courseId=courseId}getCourseId(){return this.courseId}setSavedFilterCondition(filterCondition){this.savedFilterCondition=filterCondition}getSavedFilterCondition(){return this.savedFilterCondition}moveContentIntoTab(tabContent,tabElement){tabContent.find(SELECTORS.FORM_HEADER).addClass("hidden"),tabContent.wrap(tabElement)}moveTabsIntoTabContent(form){const tabContent=this.getBody().find(SELECTORS.TAB_CONTENT).empty();form.find('[role="tabpanel"]').wrapAll(tabContent)}moveCancelButtonToTabs(form){const cancelButton=form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass("ms-1"),tabFooters=form.find('[data-region="footer"]');cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove(),cancelButton.clone().appendTo(tabFooters)}loadForm(){const addonpage=this.getAddOnPageId(),returnurl=this.getReturnUrl(),quizcmid=this.quizCmId,bankcmid=this.bankCmId,savedfiltercondition=this.getSavedFilterCondition();return this.setSavedFilterCondition(null),Fragment.loadFragment("mod_quiz","add_random_question_form",this.getContextId(),{addonpage:null!=addonpage?addonpage:null,returnurl:returnurl,quizcmid:quizcmid,bankcmid:bankcmid,slotid:this.getSlotId(),savedfiltercondition:savedfiltercondition}).then(((html,js)=>{const form=(0,_jquery.default)(html);if(!this.getSlotId()){const existingCategoryTabContent=form.find(SELECTORS.EXISTING_CATEGORY_TAB),existingCategoryTab=this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER),newCategoryTabContent=form.find(SELECTORS.NEW_CATEGORY_TAB),newCategoryTab=this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);this.moveContentIntoTab(existingCategoryTabContent,existingCategoryTab),this.moveContentIntoTab(newCategoryTabContent,newCategoryTab),this.moveTabsIntoTabContent(form)}Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT),form,js)})).then((()=>{FormChangeChecker.disableAllChecks(),this.getBody()[0].addEventListener("click",(e=>{if(!e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT))return;e.preventDefault(),document.querySelector('#questionscontainer input[name="cmid"]').setAttribute("name",this.quizCmId);if(e.target.closest(SELECTORS.ADD_RANDOM_BUTTON)){var _document$querySelect;const randomcount=document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value,filtercondition=null===(_document$querySelect=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect?void 0:_document$querySelect.filtercondition;return void this.addQuestions(quizcmid,addonpage,randomcount,filtercondition,"","")}if(e.target.closest(SELECTORS.UPDATE_FILTER_BUTTON)){var _document$querySelect2;const filtercondition=null===(_document$querySelect2=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.filtercondition;return void this.updateFilterCondition(quizcmid,this.getSlotId(),filtercondition)}e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON)&&this.addQuestions(quizcmid,addonpage,1,"",document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value)}));const switcher=new _bank_switcher.default;this.getModal().on("click",SELECTORS.SWITCH_TO_OTHER_BANK,(()=>{var _document$querySelect3;this.setSavedFilterCondition(null===(_document$querySelect3=document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset)||void 0===_document$querySelect3?void 0:_document$querySelect3.filtercondition);try{switcher.show(this,this.courseId,this.getContextId(),this.bankCmId,this.quizCmId)}catch(ex){Notification.exception(ex)}})),this.getModal().get(0).addEventListener("bankSwitched",(async e=>{if(e.detail.cmid>0){const modalConfig={contextId:this.getContextId(),bankCmId:e.detail.cmid,category:this.getCategory(),returnUrl:this.getReturnUrl(),quizCmId:this.quizCmId,title:this.originalTitle,addOnPage:this.getAddOnPageId(),templateContext:{hidden:this.showNewCategory},showNewCategory:this.showNewCategory,slotId:this.getSlotId(),courseId:this.getCourseId()};e.detail.cmid===this.bankCmId&&(modalConfig.savedFilterCondition=this.getSavedFilterCondition());try{await ModalAddRandomQuestion.create(modalConfig),this.destroy()}catch(ex){await Notification.exception(ex)}}}))})).catch(Notification.exception)}async addQuestions(quizcmid,addonpage,randomcount,filtercondition,newcategory,parentcategory){new _pending.default("mod-quiz/modal_add_random_questions");const call={methodname:"mod_quiz_add_random_questions",args:{cmid:quizcmid,addonpage:addonpage,randomcount:randomcount,filtercondition:filtercondition,newcategory:newcategory,parentcategory:parentcategory}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}async updateFilterCondition(quizcmid,slotid,filtercondition){new _pending.default("mod-quiz/modal_add_random_questions");const call={methodname:"mod_quiz_update_filter_condition",args:{cmid:quizcmid,slotid:slotid,filtercondition:filtercondition}};try{const response=await(0,_ajax.call)([call])[0],form=document.querySelector(SELECTORS.FORM_ELEMENT);form.querySelector(SELECTORS.MESSAGE_INPUT).value=response.message,form.submit()}catch(e){Notification.exception(e)}}show(){super.show(this),this.loadedForm||(this.tabHtml=this.getBody(),this.loadForm(window.location.search),this.loadedForm=!0)}}return _exports.default=ModalAddRandomQuestion,_defineProperty(ModalAddRandomQuestion,"TYPE","mod_quiz-quiz-add-random-question"),_defineProperty(ModalAddRandomQuestion,"TEMPLATE","mod_quiz/modal_add_random_question"),ModalAddRandomQuestion.registerModalType(),_exports.default})); //# sourceMappingURL=modal_add_random_question.min.js.map \ No newline at end of file diff --git a/public/mod/quiz/amd/build/modal_add_random_question.min.js.map b/public/mod/quiz/amd/build/modal_add_random_question.min.js.map index cc6cab4e700..871f87a40b4 100644 --- a/public/mod/quiz/amd/build/modal_add_random_question.min.js.map +++ b/public/mod/quiz/amd/build/modal_add_random_question.min.js.map @@ -1 +1 @@ -{"version":3,"file":"modal_add_random_question.min.js","sources":["../src/modal_add_random_question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/modal_add_random_question\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from './add_question_modal';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Templates from 'core/templates';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\n\nconst SELECTORS = {\n ANCHOR: 'a[href]',\n EXISTING_CATEGORY_CONTAINER: '[data-region=\"existing-category-container\"]',\n EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',\n NEW_CATEGORY_CONTAINER: '[data-region=\"new-category-container\"]',\n NEW_CATEGORY_TAB: '#id_newcategoryheader',\n TAB_CONTENT: '[data-region=\"tab-content\"]',\n ADD_ON_PAGE_FORM_ELEMENT: '[name=\"addonpage\"]',\n ADD_RANDOM_BUTTON: 'input[type=\"submit\"][name=\"addrandom\"]',\n ADD_NEW_CATEGORY_BUTTON: 'input[type=\"submit\"][name=\"newcategory\"]',\n SUBMIT_BUTTON_ELEMENT: 'input[type=\"submit\"][name=\"addrandom\"], '\n + 'input[type=\"submit\"][name=\"newcategory\"], '\n + 'input[type=\"submit\"][name=\"update\"]',\n FORM_HEADER: 'legend',\n SELECT_NUMBER_TO_ADD: '#menurandomcount',\n NEW_CATEGORY_ELEMENT: '#categoryname',\n PARENT_CATEGORY_ELEMENT: '#parentcategory',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n FORM_ELEMENT: '#add_random_question_form',\n MESSAGE_INPUT: '[name=\"message\"]',\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n NEW_BANKMOD_ID: 'data-newmodid',\n BANK_SEARCH: '#searchbanks',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n UPDATE_FILTER_BUTTON: 'input[type=\"submit\"][name=\"update\"]',\n};\n\nexport default class ModalAddRandomQuestion extends Modal {\n static TYPE = 'mod_quiz-quiz-add-random-question';\n static TEMPLATE = 'mod_quiz/modal_add_random_question';\n\n /**\n * Create the add random question modal.\n *\n * @param {Number} contextId Current context id.\n * @param {Number} bankCmId Current question bank course module id.\n * @param {string} category Category id and category context id comma separated.\n * @param {string} returnUrl URL to return to after form submission.\n * @param {Number} quizCmId Current quiz course module id.\n * @param {boolean} showNewCategory Display the New category tab when selecting random questions.\n */\n static init(\n contextId,\n bankCmId,\n category,\n returnUrl,\n quizCmId,\n showNewCategory = true\n ) {\n const selector = '.menu [data-action=\"addarandomquestion\"], [data-action=\"editrandomquestion\"]';\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest(selector);\n if (!trigger) {\n return;\n }\n e.preventDefault();\n\n if (trigger.dataset.slotid) {\n showNewCategory = false;\n }\n\n ModalAddRandomQuestion.create({\n contextId,\n bankCmId,\n category,\n returnUrl,\n quizCmId,\n showNewCategory,\n title: trigger.dataset.header,\n addOnPage: trigger.dataset.addonpage,\n slotId: trigger.dataset.slotid,\n templateContext: {\n hidden: showNewCategory,\n },\n });\n });\n }\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n this.category = null;\n this.returnUrl = null;\n this.quizCmId = null;\n this.loadedForm = false;\n this.slotId = 0;\n this.savedFilterCondition = null;\n }\n\n configure(modalConfig) {\n modalConfig.removeOnClose = true;\n\n this.setCategory(modalConfig.category);\n this.setReturnUrl(modalConfig.returnUrl);\n this.showNewCategory = modalConfig.showNewCategory;\n this.setSlotId(modalConfig.slotId ?? 0);\n this.setSavedFilterCondition(modalConfig.savedFilterCondition ?? null);\n\n super.configure(modalConfig);\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n super.setAddOnPageId(id);\n this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);\n }\n\n /**\n * Set the category for this form. The category is a comma separated\n * category id and category context id.\n *\n * @method setCategory\n * @param {string} category\n */\n setCategory(category) {\n this.category = category;\n }\n\n /**\n * Returns the saved category.\n *\n * @method getCategory\n * @return {string}\n */\n getCategory() {\n return this.category;\n }\n\n /**\n * Set the return URL for the form.\n *\n * @method setReturnUrl\n * @param {string} url\n */\n setReturnUrl(url) {\n this.returnUrl = url;\n }\n\n /**\n * Returns the return URL for the form.\n *\n * @method getReturnUrl\n * @return {string}\n */\n getReturnUrl() {\n return this.returnUrl;\n }\n\n /**\n * Set the ID of the quiz slot, if we are editing an existing random question.\n *\n * @param {Number} slotId\n */\n setSlotId(slotId) {\n this.slotId = slotId;\n }\n\n /**\n * Get the current slot ID.\n *\n * @return {Number}\n */\n getSlotId() {\n return this.slotId;\n }\n\n /**\n * Store the current filterCondition JSON string.\n *\n * @param {String} filterCondition\n */\n setSavedFilterCondition(filterCondition) {\n this.savedFilterCondition = filterCondition;\n }\n\n /**\n * Return the saved filterCondition JSON string.\n *\n * @return {String}\n */\n getSavedFilterCondition() {\n return this.savedFilterCondition;\n }\n\n /**\n * Moves a given form element inside (a child of) a given tab element.\n *\n * Hides the 'legend' (e.g. header) element of the form element because the\n * tab has the name.\n *\n * Moves the submit button into a footer element at the bottom of the form\n * element for styling purposes.\n *\n * @method moveContentIntoTab\n * @param {jquery} tabContent The form element to move into the tab.\n * @param {jquey} tabElement The tab element for the form element to move into.\n */\n moveContentIntoTab(tabContent, tabElement) {\n // Hide the header because the tabs show us which part of the form we're\n // looking at.\n tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');\n // Move the element inside a tab.\n tabContent.wrap(tabElement);\n }\n\n /**\n * Empty the tab content container and move all tabs from the form into the\n * tab container element.\n *\n * @method moveTabsIntoTabContent\n * @param {jquery} form The form element.\n */\n moveTabsIntoTabContent(form) {\n // Empty it to remove the loading icon.\n const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();\n // Make sure all tabs are inside the tab content element.\n form.find('[role=\"tabpanel\"]').wrapAll(tabContent);\n }\n\n /**\n * Make sure all of the tabs have a cancel button in their fotter to sit along\n * side the submit button.\n *\n * @method moveCancelButtonToTabs\n * @param {jquey} form The form element.\n */\n moveCancelButtonToTabs(form) {\n const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ms-1');\n const tabFooters = form.find('[data-region=\"footer\"]');\n // Remove the buttons container element.\n cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();\n cancelButton.clone().appendTo(tabFooters);\n }\n\n /**\n * Load the add random question form in a fragement and perform some transformation\n * on the HTML to convert it into tabs for rendering in the modal.\n *\n * @method loadForm\n * @return {promise} Resolved with form HTML and JS.\n */\n loadForm() {\n const addonpage = this.getAddOnPageId();\n const returnurl = this.getReturnUrl();\n const quizcmid = this.quizCmId;\n const bankcmid = this.bankCmId;\n const savedfiltercondition = this.getSavedFilterCondition();\n this.setSavedFilterCondition(null);\n\n return Fragment.loadFragment(\n 'mod_quiz',\n 'add_random_question_form',\n this.getContextId(),\n {\n addonpage: addonpage ?? null,\n returnurl,\n quizcmid,\n bankcmid,\n slotid: this.getSlotId(),\n savedfiltercondition,\n }\n )\n .then((html, js) => {\n const form = $(html);\n if (!this.getSlotId()) {\n const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);\n const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);\n const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);\n const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);\n\n // Transform the form into tabs for better rendering in the modal.\n this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);\n this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);\n this.moveTabsIntoTabContent(form);\n }\n\n Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);\n return;\n })\n .then(() => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n\n // Add question to quiz.\n this.getBody()[0].addEventListener('click', (e) => {\n const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);\n if (!button) {\n return;\n }\n e.preventDefault();\n\n // Intercept the submission to adjust the POST params so that the quiz mod id is set and not the bank module id.\n document.querySelector('#questionscontainer input[name=\"cmid\"]').setAttribute('name', this.quizCmId);\n\n // Add Random questions if the add random button was clicked.\n const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);\n if (addRandomButton) {\n const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n\n this.addQuestions(quizcmid, addonpage, randomcount, filtercondition, '', '');\n return;\n }\n // Update the filter condition for the slot if the update button was clicked.\n const updateFilterButton = e.target.closest(SELECTORS.UPDATE_FILTER_BUTTON);\n if (updateFilterButton) {\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n this.updateFilterCondition(quizcmid, this.getSlotId(), filtercondition);\n return;\n }\n // Add new category if the add category button was clicked.\n const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);\n if (addCategoryButton) {\n this.addQuestions(\n quizcmid,\n addonpage,\n 1,\n '',\n document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,\n document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value\n );\n return;\n }\n });\n\n this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => {\n this.setSavedFilterCondition(\n document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition\n );\n this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH)\n .then(function(ModalQuizQuestionBank) {\n $(SELECTORS.BANK_SEARCH)?.on('change', (e) => {\n const bankCmId = $(e.currentTarget).val();\n // Have to recreate the modal as we have already used the body for the switch bank content.\n if (bankCmId > 0) {\n ModalAddRandomQuestion.create({\n 'contextId': ModalQuizQuestionBank.getContextId(),\n 'bankCmId': bankCmId,\n 'category': ModalQuizQuestionBank.getCategory(),\n 'returnUrl': ModalQuizQuestionBank.getReturnUrl(),\n 'quizCmId': ModalQuizQuestionBank.quizCmId,\n 'title': ModalQuizQuestionBank.originalTitle,\n 'addOnPage': ModalQuizQuestionBank.getAddOnPageId(),\n 'templateContext': {hidden: ModalQuizQuestionBank.showNewCategory},\n 'showNewCategory': ModalQuizQuestionBank.showNewCategory,\n 'slotId': ModalQuizQuestionBank.getSlotId(),\n })\n .then(ModalQuizQuestionBank.destroy())\n .catch(Notification.exception);\n }\n });\n return ModalQuizQuestionBank;\n });\n });\n\n this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {\n const anchorElement = $(e.currentTarget);\n // Have to recreate the modal as we have already used the body for the switch bank content.\n ModalAddRandomQuestion.create({\n 'contextId': this.getContextId(),\n 'bankCmId': anchorElement.attr('value'),\n 'category': this.getCategory(),\n 'returnUrl': this.getReturnUrl(),\n 'quizCmId': this.quizCmId,\n 'title': this.originalTitle,\n 'addOnPage': this.getAddOnPageId(),\n 'templateContext': {hidden: this.showNewCategory},\n 'showNewCategory': this.showNewCategory,\n 'savedFilterCondition': this.getSavedFilterCondition(),\n 'slotId': this.getSlotId(),\n }).then(this.destroy()).catch(Notification.exception);\n });\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = $(e.currentTarget);\n // Have to recreate the modal as we have already used the body for the switch bank content.\n if (anchorElement.closest('a[' + SELECTORS.NEW_BANKMOD_ID + ']').length) {\n ModalAddRandomQuestion.create({\n 'contextId': this.getContextId(),\n 'bankCmId': anchorElement.attr(SELECTORS.NEW_BANKMOD_ID),\n 'category': this.getCategory(),\n 'returnUrl': this.getReturnUrl(),\n 'quizCmId': this.quizCmId,\n 'title': this.originalTitle,\n 'addOnPage': this.getAddOnPageId(),\n 'templateContext': {hidden: this.showNewCategory},\n 'showNewCategory': this.showNewCategory,\n 'slotId': this.getSlotId(),\n }).then(this.destroy()).catch(Notification.exception);\n }\n });\n })\n .catch(Notification.exception);\n }\n\n /**\n * Call web service function to add random questions\n *\n * @param {number} quizcmid the course module id of the quiz to add questions to.\n * @param {number} addonpage the page where random questions will be added to\n * @param {number} randomcount Number of random questions\n * @param {string} filtercondition Filter condition\n * @param {string} newcategory add new category\n * @param {string} parentcategory parent category of new category\n */\n async addQuestions(\n quizcmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory\n ) {\n // We do not need to resolve this Pending because the form submission will result in a page redirect.\n new Pending('mod-quiz/modal_add_random_questions');\n const call = {\n methodname: 'mod_quiz_add_random_questions',\n args: {\n cmid: quizcmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Call web service function to update the filter condition for an existing slot.\n *\n * @param {number} quizcmid the course module id of the quiz.\n * @param {number} slotid The slot the random question is in.\n * @param {string} filtercondition The new filter condition.\n */\n async updateFilterCondition(\n quizcmid,\n slotid,\n filtercondition,\n ) {\n // We do not need to resolve this Pending because the form submission will result in a page redirect.\n new Pending('mod-quiz/modal_add_random_questions');\n const call = {\n methodname: 'mod_quiz_update_filter_condition',\n args: {\n cmid: quizcmid,\n slotid,\n filtercondition,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Override the modal show function to load the form when this modal is first\n * shown.\n *\n * @method show\n */\n show() {\n super.show(this);\n\n if (!this.loadedForm) {\n this.tabHtml = this.getBody();\n this.loadForm(window.location.search);\n this.loadedForm = true;\n }\n }\n}\n\nModalAddRandomQuestion.registerModalType();\n"],"names":["SELECTORS","ANCHOR","EXISTING_CATEGORY_CONTAINER","EXISTING_CATEGORY_TAB","NEW_CATEGORY_CONTAINER","NEW_CATEGORY_TAB","TAB_CONTENT","ADD_ON_PAGE_FORM_ELEMENT","ADD_RANDOM_BUTTON","ADD_NEW_CATEGORY_BUTTON","SUBMIT_BUTTON_ELEMENT","FORM_HEADER","SELECT_NUMBER_TO_ADD","NEW_CATEGORY_ELEMENT","PARENT_CATEGORY_ELEMENT","FILTER_CONDITION_ELEMENT","FORM_ELEMENT","MESSAGE_INPUT","SWITCH_TO_OTHER_BANK","NEW_BANKMOD_ID","BANK_SEARCH","GO_BACK_BUTTON","UPDATE_FILTER_BUTTON","ModalAddRandomQuestion","Modal","contextId","bankCmId","category","returnUrl","quizCmId","showNewCategory","document","addEventListener","e","trigger","target","closest","preventDefault","dataset","slotid","create","title","header","addOnPage","addonpage","slotId","templateContext","hidden","constructor","root","loadedForm","savedFilterCondition","configure","modalConfig","removeOnClose","setCategory","setReturnUrl","setSlotId","setSavedFilterCondition","setAddOnPageId","id","getBody","find","val","getCategory","this","url","getReturnUrl","getSlotId","filterCondition","getSavedFilterCondition","moveContentIntoTab","tabContent","tabElement","addClass","wrap","moveTabsIntoTabContent","form","empty","wrapAll","moveCancelButtonToTabs","cancelButton","CANCEL_BUTTON_ELEMENT","tabFooters","BUTTON_CONTAINER","remove","clone","appendTo","loadForm","getAddOnPageId","returnurl","quizcmid","bankcmid","savedfiltercondition","Fragment","loadFragment","getContextId","then","html","js","existingCategoryTabContent","existingCategoryTab","newCategoryTabContent","newCategoryTab","Templates","replaceNode","FormChangeChecker","disableAllChecks","querySelector","setAttribute","randomcount","value","filtercondition","_document$querySelect","addQuestions","_document$querySelect2","updateFilterCondition","getModal","on","_document$querySelect3","handleSwitchBankContentReload","ModalQuizQuestionBank","currentTarget","originalTitle","destroy","catch","Notification","exception","anchorElement","attr","length","newcategory","parentcategory","Pending","call","methodname","args","cmid","response","message","submit","show","tabHtml","window","location","search","registerModalType"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"modal_add_random_question.min.js","sources":["../src/modal_add_random_question.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the add random question modal.\n *\n * @module mod_quiz/modal_add_random_question\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Modal from './add_question_modal';\nimport * as Notification from 'core/notification';\nimport * as Fragment from 'core/fragment';\nimport * as Templates from 'core/templates';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\nimport BankSwitcher from 'core_question/bank_switcher';\n\nconst SELECTORS = {\n ANCHOR: 'a[href]',\n EXISTING_CATEGORY_CONTAINER: '[data-region=\"existing-category-container\"]',\n EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',\n NEW_CATEGORY_CONTAINER: '[data-region=\"new-category-container\"]',\n NEW_CATEGORY_TAB: '#id_newcategoryheader',\n TAB_CONTENT: '[data-region=\"tab-content\"]',\n ADD_ON_PAGE_FORM_ELEMENT: '[name=\"addonpage\"]',\n ADD_RANDOM_BUTTON: 'input[type=\"submit\"][name=\"addrandom\"]',\n ADD_NEW_CATEGORY_BUTTON: 'input[type=\"submit\"][name=\"newcategory\"]',\n SUBMIT_BUTTON_ELEMENT: 'input[type=\"submit\"][name=\"addrandom\"], '\n + 'input[type=\"submit\"][name=\"newcategory\"], '\n + 'input[type=\"submit\"][name=\"update\"]',\n FORM_HEADER: 'legend',\n SELECT_NUMBER_TO_ADD: '#menurandomcount',\n NEW_CATEGORY_ELEMENT: '#categoryname',\n PARENT_CATEGORY_ELEMENT: '#parentcategory',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n FORM_ELEMENT: '#add_random_question_form',\n MESSAGE_INPUT: '[name=\"message\"]',\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n NEW_BANKMOD_ID: 'data-newmodid',\n BANK_SEARCH: '#searchbanks',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n UPDATE_FILTER_BUTTON: 'input[type=\"submit\"][name=\"update\"]',\n};\n\nexport default class ModalAddRandomQuestion extends Modal {\n static TYPE = 'mod_quiz-quiz-add-random-question';\n static TEMPLATE = 'mod_quiz/modal_add_random_question';\n\n /**\n * Create the add random question modal.\n *\n * @param {Number} contextId Current context id.\n * @param {Number} bankCmId Current question bank course module id.\n * @param {string} category Category id and category context id comma separated.\n * @param {string} returnUrl URL to return to after form submission.\n * @param {Number} quizCmId Current quiz course module id.\n * @param {boolean} showNewCategory Display the New category tab when selecting random questions.\n * @param {Number} courseId Current course id, required for switching banks.\n */\n static init(\n contextId,\n bankCmId,\n category,\n returnUrl,\n quizCmId,\n showNewCategory = true,\n courseId,\n ) {\n const selector = '.menu [data-action=\"addarandomquestion\"], [data-action=\"editrandomquestion\"]';\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest(selector);\n if (!trigger) {\n return;\n }\n e.preventDefault();\n\n if (trigger.dataset.slotid) {\n showNewCategory = false;\n }\n\n ModalAddRandomQuestion.create({\n contextId,\n bankCmId,\n category,\n returnUrl,\n quizCmId,\n showNewCategory,\n title: trigger.dataset.header,\n addOnPage: trigger.dataset.addonpage,\n slotId: trigger.dataset.slotid,\n templateContext: {\n hidden: showNewCategory,\n },\n courseId,\n });\n });\n }\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n constructor(root) {\n super(root);\n this.category = null;\n this.returnUrl = null;\n this.quizCmId = null;\n this.loadedForm = false;\n this.slotId = 0;\n this.savedFilterCondition = null;\n }\n\n configure(modalConfig) {\n modalConfig.removeOnClose = true;\n\n this.setCategory(modalConfig.category);\n this.setReturnUrl(modalConfig.returnUrl);\n this.showNewCategory = modalConfig.showNewCategory;\n this.setSlotId(modalConfig.slotId ?? 0);\n this.setSavedFilterCondition(modalConfig.savedFilterCondition ?? null);\n this.setCourseId(modalConfig.courseId);\n\n super.configure(modalConfig);\n }\n\n /**\n * Set the id of the page that the question should be added to\n * when the user clicks the add to quiz link.\n *\n * @method setAddOnPageId\n * @param {int} id\n */\n setAddOnPageId(id) {\n super.setAddOnPageId(id);\n this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);\n }\n\n /**\n * Set the category for this form. The category is a comma separated\n * category id and category context id.\n *\n * @method setCategory\n * @param {string} category\n */\n setCategory(category) {\n this.category = category;\n }\n\n /**\n * Returns the saved category.\n *\n * @method getCategory\n * @return {string}\n */\n getCategory() {\n return this.category;\n }\n\n /**\n * Set the return URL for the form.\n *\n * @method setReturnUrl\n * @param {string} url\n */\n setReturnUrl(url) {\n this.returnUrl = url;\n }\n\n /**\n * Returns the return URL for the form.\n *\n * @method getReturnUrl\n * @return {string}\n */\n getReturnUrl() {\n return this.returnUrl;\n }\n\n /**\n * Set the ID of the quiz slot, if we are editing an existing random question.\n *\n * @param {Number} slotId\n */\n setSlotId(slotId) {\n this.slotId = slotId;\n }\n\n /**\n * Get the current slot ID.\n *\n * @return {Number}\n */\n getSlotId() {\n return this.slotId;\n }\n\n /**\n * Store the ID of the current course\n *\n * @param {Number} courseId\n */\n setCourseId(courseId) {\n this.courseId = courseId;\n }\n\n /**\n * Get the current course ID.\n *\n * @return {Number}\n */\n getCourseId() {\n return this.courseId;\n }\n\n /**\n * Store the current filterCondition JSON string.\n *\n * @param {String} filterCondition\n */\n setSavedFilterCondition(filterCondition) {\n this.savedFilterCondition = filterCondition;\n }\n\n /**\n * Return the saved filterCondition JSON string.\n *\n * @return {String}\n */\n getSavedFilterCondition() {\n return this.savedFilterCondition;\n }\n\n /**\n * Moves a given form element inside (a child of) a given tab element.\n *\n * Hides the 'legend' (e.g. header) element of the form element because the\n * tab has the name.\n *\n * Moves the submit button into a footer element at the bottom of the form\n * element for styling purposes.\n *\n * @method moveContentIntoTab\n * @param {jquery} tabContent The form element to move into the tab.\n * @param {jquey} tabElement The tab element for the form element to move into.\n */\n moveContentIntoTab(tabContent, tabElement) {\n // Hide the header because the tabs show us which part of the form we're\n // looking at.\n tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');\n // Move the element inside a tab.\n tabContent.wrap(tabElement);\n }\n\n /**\n * Empty the tab content container and move all tabs from the form into the\n * tab container element.\n *\n * @method moveTabsIntoTabContent\n * @param {jquery} form The form element.\n */\n moveTabsIntoTabContent(form) {\n // Empty it to remove the loading icon.\n const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();\n // Make sure all tabs are inside the tab content element.\n form.find('[role=\"tabpanel\"]').wrapAll(tabContent);\n }\n\n /**\n * Make sure all of the tabs have a cancel button in their fotter to sit along\n * side the submit button.\n *\n * @method moveCancelButtonToTabs\n * @param {jquey} form The form element.\n */\n moveCancelButtonToTabs(form) {\n const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ms-1');\n const tabFooters = form.find('[data-region=\"footer\"]');\n // Remove the buttons container element.\n cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();\n cancelButton.clone().appendTo(tabFooters);\n }\n\n /**\n * Load the add random question form in a fragement and perform some transformation\n * on the HTML to convert it into tabs for rendering in the modal.\n *\n * @method loadForm\n * @return {promise} Resolved with form HTML and JS.\n */\n loadForm() {\n const addonpage = this.getAddOnPageId();\n const returnurl = this.getReturnUrl();\n const quizcmid = this.quizCmId;\n const bankcmid = this.bankCmId;\n const savedfiltercondition = this.getSavedFilterCondition();\n this.setSavedFilterCondition(null);\n\n return Fragment.loadFragment(\n 'mod_quiz',\n 'add_random_question_form',\n this.getContextId(),\n {\n addonpage: addonpage ?? null,\n returnurl,\n quizcmid,\n bankcmid,\n slotid: this.getSlotId(),\n savedfiltercondition,\n }\n )\n .then((html, js) => {\n const form = $(html);\n if (!this.getSlotId()) {\n const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);\n const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);\n const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);\n const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);\n\n // Transform the form into tabs for better rendering in the modal.\n this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);\n this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);\n this.moveTabsIntoTabContent(form);\n }\n\n Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);\n return;\n })\n .then(() => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n\n // Add question to quiz.\n this.getBody()[0].addEventListener('click', (e) => {\n const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);\n if (!button) {\n return;\n }\n e.preventDefault();\n\n // Intercept the submission to adjust the POST params so that the quiz mod id is set and not the bank module id.\n document.querySelector('#questionscontainer input[name=\"cmid\"]').setAttribute('name', this.quizCmId);\n\n // Add Random questions if the add random button was clicked.\n const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);\n if (addRandomButton) {\n const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n\n this.addQuestions(quizcmid, addonpage, randomcount, filtercondition, '', '');\n return;\n }\n // Update the filter condition for the slot if the update button was clicked.\n const updateFilterButton = e.target.closest(SELECTORS.UPDATE_FILTER_BUTTON);\n if (updateFilterButton) {\n const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;\n this.updateFilterCondition(quizcmid, this.getSlotId(), filtercondition);\n return;\n }\n // Add new category if the add category button was clicked.\n const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);\n if (addCategoryButton) {\n this.addQuestions(\n quizcmid,\n addonpage,\n 1,\n '',\n document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,\n document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value\n );\n return;\n }\n });\n\n const switcher = new BankSwitcher();\n this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => {\n this.setSavedFilterCondition(\n document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition\n );\n try {\n switcher.show(this, this.courseId, this.getContextId(), this.bankCmId, this.quizCmId);\n } catch (ex) {\n Notification.exception(ex);\n }\n });\n this.getModal().get(0).addEventListener('bankSwitched', async(e) => {\n if (e.detail.cmid > 0) {\n const modalConfig = {\n 'contextId': this.getContextId(),\n 'bankCmId': e.detail.cmid,\n 'category': this.getCategory(),\n 'returnUrl': this.getReturnUrl(),\n 'quizCmId': this.quizCmId,\n 'title': this.originalTitle,\n 'addOnPage': this.getAddOnPageId(),\n 'templateContext': {hidden: this.showNewCategory},\n 'showNewCategory': this.showNewCategory,\n 'slotId': this.getSlotId(),\n 'courseId': this.getCourseId(),\n };\n if (e.detail.cmid === this.bankCmId) {\n // We're displaying the same bank as before, keep the current filters.\n modalConfig.savedFilterCondition = this.getSavedFilterCondition();\n }\n try {\n await ModalAddRandomQuestion.create(modalConfig);\n this.destroy();\n } catch (ex) {\n await Notification.exception(ex);\n }\n }\n });\n })\n .catch(Notification.exception);\n }\n\n /**\n * Call web service function to add random questions\n *\n * @param {number} quizcmid the course module id of the quiz to add questions to.\n * @param {number} addonpage the page where random questions will be added to\n * @param {number} randomcount Number of random questions\n * @param {string} filtercondition Filter condition\n * @param {string} newcategory add new category\n * @param {string} parentcategory parent category of new category\n */\n async addQuestions(\n quizcmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory\n ) {\n // We do not need to resolve this Pending because the form submission will result in a page redirect.\n new Pending('mod-quiz/modal_add_random_questions');\n const call = {\n methodname: 'mod_quiz_add_random_questions',\n args: {\n cmid: quizcmid,\n addonpage,\n randomcount,\n filtercondition,\n newcategory,\n parentcategory,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Call web service function to update the filter condition for an existing slot.\n *\n * @param {number} quizcmid the course module id of the quiz.\n * @param {number} slotid The slot the random question is in.\n * @param {string} filtercondition The new filter condition.\n */\n async updateFilterCondition(\n quizcmid,\n slotid,\n filtercondition,\n ) {\n // We do not need to resolve this Pending because the form submission will result in a page redirect.\n new Pending('mod-quiz/modal_add_random_questions');\n const call = {\n methodname: 'mod_quiz_update_filter_condition',\n args: {\n cmid: quizcmid,\n slotid,\n filtercondition,\n }\n };\n try {\n const response = await fetchMany([call])[0];\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n }\n\n /**\n * Override the modal show function to load the form when this modal is first\n * shown.\n *\n * @method show\n */\n show() {\n super.show(this);\n\n if (!this.loadedForm) {\n this.tabHtml = this.getBody();\n this.loadForm(window.location.search);\n this.loadedForm = true;\n }\n }\n}\n\nModalAddRandomQuestion.registerModalType();\n"],"names":["SELECTORS","ANCHOR","EXISTING_CATEGORY_CONTAINER","EXISTING_CATEGORY_TAB","NEW_CATEGORY_CONTAINER","NEW_CATEGORY_TAB","TAB_CONTENT","ADD_ON_PAGE_FORM_ELEMENT","ADD_RANDOM_BUTTON","ADD_NEW_CATEGORY_BUTTON","SUBMIT_BUTTON_ELEMENT","FORM_HEADER","SELECT_NUMBER_TO_ADD","NEW_CATEGORY_ELEMENT","PARENT_CATEGORY_ELEMENT","FILTER_CONDITION_ELEMENT","FORM_ELEMENT","MESSAGE_INPUT","SWITCH_TO_OTHER_BANK","NEW_BANKMOD_ID","BANK_SEARCH","GO_BACK_BUTTON","UPDATE_FILTER_BUTTON","ModalAddRandomQuestion","Modal","contextId","bankCmId","category","returnUrl","quizCmId","showNewCategory","courseId","document","addEventListener","e","trigger","target","closest","preventDefault","dataset","slotid","create","title","header","addOnPage","addonpage","slotId","templateContext","hidden","constructor","root","loadedForm","savedFilterCondition","configure","modalConfig","removeOnClose","setCategory","setReturnUrl","setSlotId","setSavedFilterCondition","setCourseId","setAddOnPageId","id","getBody","find","val","getCategory","this","url","getReturnUrl","getSlotId","getCourseId","filterCondition","getSavedFilterCondition","moveContentIntoTab","tabContent","tabElement","addClass","wrap","moveTabsIntoTabContent","form","empty","wrapAll","moveCancelButtonToTabs","cancelButton","CANCEL_BUTTON_ELEMENT","tabFooters","BUTTON_CONTAINER","remove","clone","appendTo","loadForm","getAddOnPageId","returnurl","quizcmid","bankcmid","savedfiltercondition","Fragment","loadFragment","getContextId","then","html","js","existingCategoryTabContent","existingCategoryTab","newCategoryTabContent","newCategoryTab","Templates","replaceNode","FormChangeChecker","disableAllChecks","querySelector","setAttribute","randomcount","value","filtercondition","_document$querySelect","addQuestions","_document$querySelect2","updateFilterCondition","switcher","BankSwitcher","getModal","on","_document$querySelect3","show","ex","Notification","exception","get","async","detail","cmid","originalTitle","destroy","catch","newcategory","parentcategory","Pending","call","methodname","args","response","message","submit","tabHtml","window","location","search","registerModalType"],"mappings":"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"} \ No newline at end of file diff --git a/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js b/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js index 9afcb6bfa3c..114713c5276 100644 --- a/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js +++ b/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js @@ -1,3 +1,3 @@ -define("mod_quiz/modal_quiz_question_bank",["exports","./add_question_modal","core/fragment","core_form/changechecker","core/modal_events","core/notification"],(function(_exports,_add_question_modal,Fragment,FormChangeChecker,ModalEvents,Notification){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_add_question_modal=(obj=_add_question_modal)&&obj.__esModule?obj:{default:obj},Fragment=_interopRequireWildcard(Fragment),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),ModalEvents=_interopRequireWildcard(ModalEvents),Notification=_interopRequireWildcard(Notification);const SELECTORS_ADD_TO_QUIZ_CONTAINER="td.addtoquizaction",SELECTORS_ANCHOR="a[href]",SELECTORS_PREVIEW_CONTAINER="td.previewquestionaction",SELECTORS_ADD_QUESTIONS_FORM="form#questionsubmit",SELECTORS_SORTERS=".sorters",SELECTORS_SWITCH_TO_OTHER_BANK='button[data-action="switch-question-bank"]',SELECTORS_NEW_BANKMOD_ID="data-newmodid",SELECTORS_BANK_SEARCH="#searchbanks",SELECTORS_GO_BACK_BUTTON='button[data-action="go-back"]',SELECTORS_ADD_ON_PAGE_FORM_ELEMENT='input[name="addonpage"]',SELECTORS_CMID_FORM_ELEMENT='form#questionsubmit input[name="cmid"]';class ModalQuizQuestionBank extends _add_question_modal.default{static init(contextId,bankCmId,quizCmId){document.addEventListener("click",(e=>{const trigger=e.target.closest('.menu [data-action="questionbank"]');trigger&&(e.preventDefault(),ModalQuizQuestionBank.create({contextId:contextId,quizCmId:quizCmId,bankCmId:bankCmId,title:trigger.dataset.header,addOnPage:trigger.dataset.addonpage,templateContext:{hidden:!0},large:!0}))}))}show(){return this.reloadBodyContent(window.location.search),super.show(this)}reloadBodyContent(querystring){this.hideFooter(),this.setTitle(this.originalTitle),this.setBody(Fragment.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:querystring,quizcmid:this.quizCmId,bankcmid:this.bankCmId}))}handleAddToQuizEvent(e,anchorElement){const href=new URL(anchorElement.getAttribute("href"));href.searchParams.set("addonpage",this.getAddOnPageId()),href.searchParams.set("cmid",this.quizCmId),anchorElement.setAttribute("href",href)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("submit",SELECTORS_ADD_QUESTIONS_FORM,(e=>{const formElement=e.currentTarget;document.querySelector(SELECTORS_ADD_ON_PAGE_FORM_ELEMENT).setAttribute("value",this.getAddOnPageId()),document.querySelector(SELECTORS_CMID_FORM_ELEMENT).setAttribute("value",this.quizCmId);const actionUrl=new URL(formElement.getAttribute("action"));actionUrl.searchParams.set("cmid",this.quizCmId),formElement.setAttribute("action",actionUrl.toString())})),this.getModal().on("click",SELECTORS_SWITCH_TO_OTHER_BANK,(()=>{this.handleSwitchBankContentReload(SELECTORS_BANK_SEARCH).then((function(ModalQuizQuestionBank){var _document$querySelect;null===(_document$querySelect=document.querySelector(SELECTORS_BANK_SEARCH))||void 0===_document$querySelect||_document$querySelect.addEventListener("change",(e=>{const bankCmId=e.currentTarget.value;bankCmId>0&&(ModalQuizQuestionBank.bankCmId=bankCmId,ModalQuizQuestionBank.reloadBodyContent(window.location.search))})),document.querySelector(SELECTORS_GO_BACK_BUTTON).addEventListener("click",(e=>{ModalQuizQuestionBank.bankCmId=e.currentTarget.value,ModalQuizQuestionBank.reloadBodyContent(window.location.search)}))})).catch(Notification.exception)})),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=e.currentTarget;if(anchorElement.closest(SELECTORS_ADD_TO_QUIZ_CONTAINER))this.handleAddToQuizEvent(e,anchorElement);else if(!anchorElement.closest(SELECTORS_PREVIEW_CONTAINER)&&!anchorElement.closest(SELECTORS_SORTERS)){if(anchorElement.closest("a["+SELECTORS_NEW_BANKMOD_ID+"]")){this.bankCmId=anchorElement.getAttribute(SELECTORS_NEW_BANKMOD_ID);const url=new URL(location.href);url.searchParams.delete("filter"),history.pushState({},"",url)}e.preventDefault(),this.reloadBodyContent(anchorElement.search)}})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}return _exports.default=ModalQuizQuestionBank,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(ModalQuizQuestionBank,"TYPE","mod_quiz-quiz-question-bank"),ModalQuizQuestionBank.registerModalType(),_exports.default})); +define("mod_quiz/modal_quiz_question_bank",["exports","./add_question_modal","core/fragment","core_form/changechecker","core/modal_events","core/notification","core_question/bank_switcher"],(function(_exports,_add_question_modal,Fragment,FormChangeChecker,ModalEvents,Notification,_bank_switcher){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_add_question_modal=_interopRequireDefault(_add_question_modal),Fragment=_interopRequireWildcard(Fragment),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),ModalEvents=_interopRequireWildcard(ModalEvents),Notification=_interopRequireWildcard(Notification),_bank_switcher=_interopRequireDefault(_bank_switcher);const SELECTORS_ADD_TO_QUIZ_CONTAINER="td.addtoquizaction",SELECTORS_ANCHOR="a[href]",SELECTORS_ADD_QUESTIONS_FORM="form#questionsubmit",SELECTORS_SWITCH_TO_OTHER_BANK='button[data-action="switch-question-bank"]',SELECTORS_ADD_ON_PAGE_FORM_ELEMENT='input[name="addonpage"]',SELECTORS_CMID_FORM_ELEMENT='form#questionsubmit input[name="cmid"]';class ModalQuizQuestionBank extends _add_question_modal.default{static init(contextId,bankCmId,quizCmId,courseId){document.addEventListener("click",(e=>{const trigger=e.target.closest('.menu [data-action="questionbank"]');trigger&&(e.preventDefault(),ModalQuizQuestionBank.create({contextId:contextId,quizCmId:quizCmId,bankCmId:bankCmId,title:trigger.dataset.header,addOnPage:trigger.dataset.addonpage,templateContext:{hidden:!0},large:!0,courseId:courseId}))}))}show(){return this.reloadBodyContent(window.location.search),super.show(this)}reloadBodyContent(querystring){this.hideFooter(),this.setTitle(this.originalTitle),this.setBody(Fragment.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:querystring,quizcmid:this.quizCmId,bankcmid:this.bankCmId}))}handleAddToQuizEvent(e,anchorElement){const href=new URL(anchorElement.getAttribute("href"));href.searchParams.set("addonpage",this.getAddOnPageId()),href.searchParams.set("cmid",this.quizCmId),anchorElement.setAttribute("href",href)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("submit",SELECTORS_ADD_QUESTIONS_FORM,(e=>{const formElement=e.currentTarget;document.querySelector(SELECTORS_ADD_ON_PAGE_FORM_ELEMENT).setAttribute("value",this.getAddOnPageId()),document.querySelector(SELECTORS_CMID_FORM_ELEMENT).setAttribute("value",this.quizCmId);const actionUrl=new URL(formElement.getAttribute("action"));actionUrl.searchParams.set("cmid",this.quizCmId),formElement.setAttribute("action",actionUrl.toString())}));const switcher=new _bank_switcher.default;this.getModal().on("click",SELECTORS_SWITCH_TO_OTHER_BANK,(()=>{try{switcher.show(this,this.courseId,this.getContextId(),this.bankCmId,this.quizCmId)}catch(ex){Notification.exception(ex)}})),this.getModal().get(0).addEventListener("bankSwitched",(e=>{const bankCmId=e.detail.cmid;if(bankCmId>0){const url=new URL(location.href);url.searchParams.delete("filter"),history.pushState({},"",url),this.bankCmId=bankCmId,this.reloadBodyContent(window.location.search)}})),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=e.currentTarget;anchorElement.closest(SELECTORS_ADD_TO_QUIZ_CONTAINER)?this.handleAddToQuizEvent(e,anchorElement):e.preventDefault()})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}var obj,key,value;return _exports.default=ModalQuizQuestionBank,value="mod_quiz-quiz-question-bank",(key="TYPE")in(obj=ModalQuizQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,ModalQuizQuestionBank.registerModalType(),_exports.default})); //# sourceMappingURL=modal_quiz_question_bank.min.js.map \ No newline at end of file diff --git a/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map b/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map index 5d2437e72fa..24e66083914 100644 --- a/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map +++ b/public/mod/quiz/amd/build/modal_quiz_question_bank.min.js.map @@ -1 +1 @@ -{"version":3,"file":"modal_quiz_question_bank.min.js","sources":["../src/modal_quiz_question_bank.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module mod_quiz/modal_quiz_question_bank\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from './add_question_modal';\nimport * as Fragment from 'core/fragment';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as ModalEvents from 'core/modal_events';\nimport * as Notification from 'core/notification';\n\nconst SELECTORS = {\n ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',\n ANCHOR: 'a[href]',\n PREVIEW_CONTAINER: 'td.previewquestionaction',\n ADD_QUESTIONS_FORM: 'form#questionsubmit',\n SORTERS: '.sorters',\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n NEW_BANKMOD_ID: 'data-newmodid',\n BANK_SEARCH: '#searchbanks',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n ADD_ON_PAGE_FORM_ELEMENT: 'input[name=\"addonpage\"]',\n CMID_FORM_ELEMENT: 'form#questionsubmit input[name=\"cmid\"]',\n};\n\nexport default class ModalQuizQuestionBank extends Modal {\n static TYPE = 'mod_quiz-quiz-question-bank';\n\n /**\n * Create the question bank modal.\n *\n * @param {Number} contextId Current module context id.\n * @param {Number} bankCmId Current question bank course module id.\n * @param {Number} quizCmId Current quiz course module id.\n */\n static init(contextId, bankCmId, quizCmId) {\n const selector = '.menu [data-action=\"questionbank\"]';\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest(selector);\n if (!trigger) {\n return;\n }\n e.preventDefault();\n\n ModalQuizQuestionBank.create({\n contextId,\n quizCmId,\n bankCmId,\n title: trigger.dataset.header,\n addOnPage: trigger.dataset.addonpage,\n templateContext: {\n hidden: true,\n },\n large: true,\n });\n });\n }\n\n /**\n * Override the parent show function.\n *\n * Reload the body contents when the modal is shown. The current\n * window URL is used to inform the new content that should be\n * displayed.\n *\n * @method show\n * @return {void}\n */\n show() {\n this.reloadBodyContent(window.location.search);\n return super.show(this);\n }\n\n /**\n * Replaces the current body contents with a new version of the question\n * bank.\n *\n * The contents of the question bank are generated using the provided\n * query string.\n *\n * @method reloadBodyContent\n * @param {string} querystring URL encoded string.\n */\n reloadBodyContent(querystring) {\n // Load the question bank fragment to be displayed in the modal and hide the 'go back' button.\n this.hideFooter();\n this.setTitle(this.originalTitle);\n this.setBody(Fragment.loadFragment(\n 'mod_quiz',\n 'quiz_question_bank',\n this.getContextId(),\n {\n querystring,\n quizcmid: this.quizCmId,\n bankcmid: this.bankCmId,\n }\n ));\n }\n\n /**\n * Update the URL of the anchor element that the user clicked on to make\n * sure that the question is added to the correct page.\n *\n * @method handleAddToQuizEvent\n * @param {event} e A JavaScript event\n * @param {object} anchorElement The anchor element that was triggered\n */\n handleAddToQuizEvent(e, anchorElement) {\n // If the user clicks the plus icon to add the question to the page\n // directly then we need to intercept the click in order to adjust the\n // href and include the correct add on page id and cmid before the page is\n // redirected.\n const href = new URL(anchorElement.getAttribute('href'));\n href.searchParams.set('addonpage', this.getAddOnPageId());\n href.searchParams.set('cmid', this.quizCmId);\n anchorElement.setAttribute('href', href);\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('submit', SELECTORS.ADD_QUESTIONS_FORM, (e) => {\n // If the user clicks on the \"Add selected questions to the quiz\" button to add some questions to the page\n // then we need to intercept the submit in order to include the correct \"add on page id\"\n // and the quizmod id before the form is submitted.\n const formElement = e.currentTarget;\n document.querySelector(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).setAttribute('value', this.getAddOnPageId());\n\n // We also need to set the form cmid & action as the quiz modid as this could be coming from a module that isn't a quiz.\n document.querySelector(SELECTORS.CMID_FORM_ELEMENT).setAttribute('value', this.quizCmId);\n const actionUrl = new URL(formElement.getAttribute('action'));\n actionUrl.searchParams.set('cmid', this.quizCmId);\n formElement.setAttribute('action', actionUrl.toString());\n });\n\n this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => {\n this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH)\n .then(function(ModalQuizQuestionBank) {\n document.querySelector(SELECTORS.BANK_SEARCH)?.addEventListener('change', (e) => {\n const bankCmId = e.currentTarget.value;\n if (bankCmId > 0) {\n ModalQuizQuestionBank.bankCmId = bankCmId;\n ModalQuizQuestionBank.reloadBodyContent(window.location.search);\n }\n });\n document.querySelector(SELECTORS.GO_BACK_BUTTON).addEventListener('click', (e) => {\n ModalQuizQuestionBank.bankCmId = e.currentTarget.value;\n ModalQuizQuestionBank.reloadBodyContent(window.location.search);\n });\n }\n )\n .catch(Notification.exception);\n });\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = e.currentTarget;\n\n // If the anchor element was the add to quiz link.\n if (anchorElement.closest(SELECTORS.ADD_TO_QUIZ_CONTAINER)) {\n this.handleAddToQuizEvent(e, anchorElement);\n return;\n }\n\n // If the anchor element was a preview question link.\n if (anchorElement.closest(SELECTORS.PREVIEW_CONTAINER)) {\n return;\n }\n\n // Sorting links have their own handler.\n if (anchorElement.closest(SELECTORS.SORTERS)) {\n return;\n }\n\n if (anchorElement.closest('a[' + SELECTORS.NEW_BANKMOD_ID + ']')) {\n this.bankCmId = anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID);\n\n // We need to clear the filter as we are about to reload the content.\n const url = new URL(location.href);\n url.searchParams.delete('filter');\n history.pushState({}, '', url);\n }\n\n // Anything else means reload the pop-up contents.\n e.preventDefault();\n this.reloadBodyContent(anchorElement.search);\n });\n\n // Disable the form change checker when the body is rendered.\n this.getRoot().on(ModalEvents.bodyRendered, () => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n });\n }\n}\n\nModalQuizQuestionBank.registerModalType();\n"],"names":["SELECTORS","ModalQuizQuestionBank","Modal","contextId","bankCmId","quizCmId","document","addEventListener","e","trigger","target","closest","preventDefault","create","title","dataset","header","addOnPage","addonpage","templateContext","hidden","large","show","reloadBodyContent","window","location","search","super","this","querystring","hideFooter","setTitle","originalTitle","setBody","Fragment","loadFragment","getContextId","quizcmid","bankcmid","handleAddToQuizEvent","anchorElement","href","URL","getAttribute","searchParams","set","getAddOnPageId","setAttribute","registerEventListeners","getModal","on","formElement","currentTarget","querySelector","actionUrl","toString","handleSwitchBankContentReload","then","value","catch","Notification","exception","url","delete","history","pushState","getRoot","ModalEvents","bodyRendered","FormChangeChecker","disableAllChecks","registerModalType"],"mappings":"6iDA6BMA,gCACqB,qBADrBA,iBAEM,UAFNA,4BAGiB,2BAHjBA,6BAIkB,sBAJlBA,kBAKO,WALPA,+BAMoB,6CANpBA,yBAOc,gBAPdA,sBAQW,eARXA,yBASc,gCATdA,mCAUwB,0BAVxBA,4BAWiB,+CAGFC,8BAA8BC,wCAUnCC,UAAWC,SAAUC,UAE7BC,SAASC,iBAAiB,SAAUC,UAC1BC,QAAUD,EAAEE,OAAOC,QAFZ,sCAGRF,UAGLD,EAAEI,iBAEFX,sBAAsBY,OAAO,CACzBV,UAAAA,UACAE,SAAAA,SACAD,SAAAA,SACAU,MAAOL,QAAQM,QAAQC,OACvBC,UAAWR,QAAQM,QAAQG,UAC3BC,gBAAiB,CACbC,QAAQ,GAEZC,OAAO,QAenBC,mBACSC,kBAAkBC,OAAOC,SAASC,QAChCC,MAAML,KAAKM,MAatBL,kBAAkBM,kBAETC,kBACAC,SAASH,KAAKI,oBACdC,QAAQC,SAASC,aAClB,WACA,qBACAP,KAAKQ,eACL,CACIP,YAAAA,YACAQ,SAAUT,KAAKvB,SACfiC,SAAUV,KAAKxB,YAa3BmC,qBAAqB/B,EAAGgC,qBAKdC,KAAO,IAAIC,IAAIF,cAAcG,aAAa,SAChDF,KAAKG,aAAaC,IAAI,YAAajB,KAAKkB,kBACxCL,KAAKG,aAAaC,IAAI,OAAQjB,KAAKvB,UACnCmC,cAAcO,aAAa,OAAQN,MAQvCO,+BAEUA,uBAAuBpB,WAExBqB,WAAWC,GAAG,SAAUlD,8BAA+BQ,UAIlD2C,YAAc3C,EAAE4C,cACtB9C,SAAS+C,cAAcrD,oCAAoC+C,aAAa,QAASnB,KAAKkB,kBAGtFxC,SAAS+C,cAAcrD,6BAA6B+C,aAAa,QAASnB,KAAKvB,gBACzEiD,UAAY,IAAIZ,IAAIS,YAAYR,aAAa,WACnDW,UAAUV,aAAaC,IAAI,OAAQjB,KAAKvB,UACxC8C,YAAYJ,aAAa,SAAUO,UAAUC,oBAG5CN,WAAWC,GAAG,QAASlD,gCAAgC,UACnDwD,8BAA8BxD,uBAC9ByD,MAAK,SAASxD,+EACPK,SAAS+C,cAAcrD,+EAAwBO,iBAAiB,UAAWC,UACjEJ,SAAWI,EAAE4C,cAAcM,MAC7BtD,SAAW,IACXH,sBAAsBG,SAAWA,SACjCH,sBAAsBsB,kBAAkBC,OAAOC,SAASC,YAGhEpB,SAAS+C,cAAcrD,0BAA0BO,iBAAiB,SAAUC,IACxEP,sBAAsBG,SAAWI,EAAE4C,cAAcM,MACjDzD,sBAAsBsB,kBAAkBC,OAAOC,SAASC,cAInEiC,MAAMC,aAAaC,mBAGvBZ,WAAWC,GAAG,QAASlD,kBAAmBQ,UACrCgC,cAAgBhC,EAAE4C,iBAGpBZ,cAAc7B,QAAQX,sCACjBuC,qBAAqB/B,EAAGgC,wBAK7BA,cAAc7B,QAAQX,+BAKtBwC,cAAc7B,QAAQX,uBAItBwC,cAAc7B,QAAQ,KAAOX,yBAA2B,KAAM,MACzDI,SAAWoC,cAAcG,aAAa3C,gCAGrC8D,IAAM,IAAIpB,IAAIjB,SAASgB,MAC7BqB,IAAIlB,aAAamB,OAAO,UACxBC,QAAQC,UAAU,GAAI,GAAIH,KAI9BtD,EAAEI,sBACGW,kBAAkBiB,cAAcd,iBAIpCwC,UAAUhB,GAAGiB,YAAYC,cAAc,KAGxCC,kBAAkBC,+MA5KTrE,6BACH,+BAgLlBA,sBAAsBsE"} \ No newline at end of file +{"version":3,"file":"modal_quiz_question_bank.min.js","sources":["../src/modal_quiz_question_bank.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module mod_quiz/modal_quiz_question_bank\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from './add_question_modal';\nimport * as Fragment from 'core/fragment';\nimport * as FormChangeChecker from 'core_form/changechecker';\nimport * as ModalEvents from 'core/modal_events';\nimport * as Notification from 'core/notification';\nimport BankSwitcher from 'core_question/bank_switcher';\n\nconst SELECTORS = {\n ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',\n ANCHOR: 'a[href]',\n PREVIEW_CONTAINER: 'td.previewquestionaction',\n ADD_QUESTIONS_FORM: 'form#questionsubmit',\n SORTERS: '.sorters',\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n NEW_BANKMOD_ID: 'data-newmodid',\n BANK_SEARCH: '#searchbanks',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n ADD_ON_PAGE_FORM_ELEMENT: 'input[name=\"addonpage\"]',\n CMID_FORM_ELEMENT: 'form#questionsubmit input[name=\"cmid\"]',\n};\n\nexport default class ModalQuizQuestionBank extends Modal {\n static TYPE = 'mod_quiz-quiz-question-bank';\n\n /**\n * Create the question bank modal.\n *\n * @param {Number} contextId Current module context id.\n * @param {Number} bankCmId Current question bank course module id.\n * @param {Number} quizCmId Current quiz course module id.\n * @param {Number} courseId Current course id, required for switching banks.\n */\n static init(contextId, bankCmId, quizCmId, courseId) {\n const selector = '.menu [data-action=\"questionbank\"]';\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest(selector);\n if (!trigger) {\n return;\n }\n e.preventDefault();\n\n ModalQuizQuestionBank.create({\n contextId,\n quizCmId,\n bankCmId,\n title: trigger.dataset.header,\n addOnPage: trigger.dataset.addonpage,\n templateContext: {\n hidden: true,\n },\n large: true,\n courseId,\n });\n });\n }\n\n /**\n * Override the parent show function.\n *\n * Reload the body contents when the modal is shown. The current\n * window URL is used to inform the new content that should be\n * displayed.\n *\n * @method show\n * @return {void}\n */\n show() {\n this.reloadBodyContent(window.location.search);\n return super.show(this);\n }\n\n /**\n * Replaces the current body contents with a new version of the question\n * bank.\n *\n * The contents of the question bank are generated using the provided\n * query string.\n *\n * @method reloadBodyContent\n * @param {string} querystring URL encoded string.\n */\n reloadBodyContent(querystring) {\n // Load the question bank fragment to be displayed in the modal and hide the 'go back' button.\n this.hideFooter();\n this.setTitle(this.originalTitle);\n this.setBody(Fragment.loadFragment(\n 'mod_quiz',\n 'quiz_question_bank',\n this.getContextId(),\n {\n querystring,\n quizcmid: this.quizCmId,\n bankcmid: this.bankCmId,\n }\n ));\n }\n\n /**\n * Update the URL of the anchor element that the user clicked on to make\n * sure that the question is added to the correct page.\n *\n * @method handleAddToQuizEvent\n * @param {event} e A JavaScript event\n * @param {object} anchorElement The anchor element that was triggered\n */\n handleAddToQuizEvent(e, anchorElement) {\n // If the user clicks the plus icon to add the question to the page\n // directly then we need to intercept the click in order to adjust the\n // href and include the correct add on page id and cmid before the page is\n // redirected.\n const href = new URL(anchorElement.getAttribute('href'));\n href.searchParams.set('addonpage', this.getAddOnPageId());\n href.searchParams.set('cmid', this.quizCmId);\n anchorElement.setAttribute('href', href);\n }\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('submit', SELECTORS.ADD_QUESTIONS_FORM, (e) => {\n // If the user clicks on the \"Add selected questions to the quiz\" button to add some questions to the page\n // then we need to intercept the submit in order to include the correct \"add on page id\"\n // and the quizmod id before the form is submitted.\n const formElement = e.currentTarget;\n document.querySelector(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).setAttribute('value', this.getAddOnPageId());\n\n // We also need to set the form cmid & action as the quiz modid as this could be coming from a module that isn't a quiz.\n document.querySelector(SELECTORS.CMID_FORM_ELEMENT).setAttribute('value', this.quizCmId);\n const actionUrl = new URL(formElement.getAttribute('action'));\n actionUrl.searchParams.set('cmid', this.quizCmId);\n formElement.setAttribute('action', actionUrl.toString());\n });\n\n const switcher = new BankSwitcher();\n\n this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => {\n try {\n switcher.show(this, this.courseId, this.getContextId(), this.bankCmId, this.quizCmId);\n } catch (ex) {\n Notification.exception(ex);\n }\n });\n\n this.getModal().get(0).addEventListener('bankSwitched', (e) => {\n const bankCmId = e.detail.cmid;\n if (bankCmId > 0) {\n // We need to clear the filter as we are about to reload the content.\n const url = new URL(location.href);\n url.searchParams.delete('filter');\n history.pushState({}, '', url);\n\n this.bankCmId = bankCmId;\n this.reloadBodyContent(window.location.search);\n }\n });\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = e.currentTarget;\n\n // If the anchor element was the add to quiz link.\n if (anchorElement.closest(SELECTORS.ADD_TO_QUIZ_CONTAINER)) {\n this.handleAddToQuizEvent(e, anchorElement);\n return;\n }\n\n e.preventDefault();\n });\n\n // Disable the form change checker when the body is rendered.\n this.getRoot().on(ModalEvents.bodyRendered, () => {\n // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the\n // page once the modal is hidden.\n FormChangeChecker.disableAllChecks();\n });\n }\n}\n\nModalQuizQuestionBank.registerModalType();\n"],"names":["SELECTORS","ModalQuizQuestionBank","Modal","contextId","bankCmId","quizCmId","courseId","document","addEventListener","e","trigger","target","closest","preventDefault","create","title","dataset","header","addOnPage","addonpage","templateContext","hidden","large","show","reloadBodyContent","window","location","search","super","this","querystring","hideFooter","setTitle","originalTitle","setBody","Fragment","loadFragment","getContextId","quizcmid","bankcmid","handleAddToQuizEvent","anchorElement","href","URL","getAttribute","searchParams","set","getAddOnPageId","setAttribute","registerEventListeners","getModal","on","formElement","currentTarget","querySelector","actionUrl","toString","switcher","BankSwitcher","ex","Notification","exception","get","detail","cmid","url","delete","history","pushState","getRoot","ModalEvents","bodyRendered","FormChangeChecker","disableAllChecks","registerModalType"],"mappings":"0sDA8BMA,gCACqB,qBADrBA,iBAEM,UAFNA,6BAIkB,sBAJlBA,+BAMoB,6CANpBA,mCAUwB,0BAVxBA,4BAWiB,+CAGFC,8BAA8BC,wCAWnCC,UAAWC,SAAUC,SAAUC,UAEvCC,SAASC,iBAAiB,SAAUC,UAC1BC,QAAUD,EAAEE,OAAOC,QAFZ,sCAGRF,UAGLD,EAAEI,iBAEFZ,sBAAsBa,OAAO,CACzBX,UAAAA,UACAE,SAAAA,SACAD,SAAAA,SACAW,MAAOL,QAAQM,QAAQC,OACvBC,UAAWR,QAAQM,QAAQG,UAC3BC,gBAAiB,CACbC,QAAQ,GAEZC,OAAO,EACPhB,SAAAA,eAeZiB,mBACSC,kBAAkBC,OAAOC,SAASC,QAChCC,MAAML,KAAKM,MAatBL,kBAAkBM,kBAETC,kBACAC,SAASH,KAAKI,oBACdC,QAAQC,SAASC,aAClB,WACA,qBACAP,KAAKQ,eACL,CACIP,YAAAA,YACAQ,SAAUT,KAAKxB,SACfkC,SAAUV,KAAKzB,YAa3BoC,qBAAqB/B,EAAGgC,qBAKdC,KAAO,IAAIC,IAAIF,cAAcG,aAAa,SAChDF,KAAKG,aAAaC,IAAI,YAAajB,KAAKkB,kBACxCL,KAAKG,aAAaC,IAAI,OAAQjB,KAAKxB,UACnCoC,cAAcO,aAAa,OAAQN,MAQvCO,+BAEUA,uBAAuBpB,WAExBqB,WAAWC,GAAG,SAAUnD,8BAA+BS,UAIlD2C,YAAc3C,EAAE4C,cACtB9C,SAAS+C,cAActD,oCAAoCgD,aAAa,QAASnB,KAAKkB,kBAGtFxC,SAAS+C,cAActD,6BAA6BgD,aAAa,QAASnB,KAAKxB,gBACzEkD,UAAY,IAAIZ,IAAIS,YAAYR,aAAa,WACnDW,UAAUV,aAAaC,IAAI,OAAQjB,KAAKxB,UACxC+C,YAAYJ,aAAa,SAAUO,UAAUC,qBAG3CC,SAAW,IAAIC,4BAEhBR,WAAWC,GAAG,QAASnD,gCAAgC,SAEpDyD,SAASlC,KAAKM,KAAMA,KAAKvB,SAAUuB,KAAKQ,eAAgBR,KAAKzB,SAAUyB,KAAKxB,UAC9E,MAAOsD,IACLC,aAAaC,UAAUF,aAI1BT,WAAWY,IAAI,GAAGtD,iBAAiB,gBAAiBC,UAC/CL,SAAWK,EAAEsD,OAAOC,QACtB5D,SAAW,EAAG,OAER6D,IAAM,IAAItB,IAAIjB,SAASgB,MAC7BuB,IAAIpB,aAAaqB,OAAO,UACxBC,QAAQC,UAAU,GAAI,GAAIH,UAErB7D,SAAWA,cACXoB,kBAAkBC,OAAOC,SAASC,iBAI1CuB,WAAWC,GAAG,QAASnD,kBAAmBS,UACrCgC,cAAgBhC,EAAE4C,cAGpBZ,cAAc7B,QAAQZ,sCACjBwC,qBAAqB/B,EAAGgC,eAIjChC,EAAEI,yBAIDwD,UAAUlB,GAAGmB,YAAYC,cAAc,KAGxCC,kBAAkBC,6FA5JZ,iDADGxE,6HAkKrBA,sBAAsByE"} \ No newline at end of file diff --git a/public/mod/quiz/amd/src/add_question_modal.js b/public/mod/quiz/amd/src/add_question_modal.js index e9f9cf44993..569293e8c00 100644 --- a/public/mod/quiz/amd/src/add_question_modal.js +++ b/public/mod/quiz/amd/src/add_question_modal.js @@ -22,9 +22,6 @@ */ import Modal from 'core/modal'; -import * as Fragment from 'core/fragment'; -import {getString} from 'core/str'; -import AutoComplete from 'core/form-autocomplete'; export default class AddQuestionModal extends Modal { configure(modalConfig) { @@ -43,6 +40,7 @@ export default class AddQuestionModal extends Modal { // This is because the URL cmid param will change during filter operations as we will be in another bank context. this.quizCmId = modalConfig.quizCmId; this.bankCmId = modalConfig.bankCmId; + this.courseId = modalConfig.courseId; // Store the original title of the modal, so we can revert back to it once we have switched to another bank. this.originalTitle = modalConfig.title; @@ -99,50 +97,4 @@ export default class AddQuestionModal extends Modal { getAddOnPageId() { return this.addOnPageId; } - - /** - * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields. - * - * @param {String} Selector for the original select element. - * @return {Promise} Modal. - */ - async handleSwitchBankContentReload(Selector) { - this.setTitle(getString('selectquestionbank', 'mod_quiz')); - - // Create a 'Go back' button and set it in the footer. - const el = document.createElement('button'); - el.classList.add('btn', 'btn-primary'); - el.textContent = await getString('gobacktoquiz', 'mod_quiz'); - el.setAttribute('data-action', 'go-back'); - el.setAttribute('value', this.bankCmId); - this.setFooter(el); - - this.setBody( - Fragment.loadFragment( - 'mod_quiz', - 'switch_question_bank', - this.getContextId(), - { - 'quizcmid': this.quizCmId, - 'bankcmid': this.bankCmId, - }) - ); - const placeholder = await getString('searchbyname', 'mod_quiz'); - await this.getBodyPromise(); - await AutoComplete.enhance( - Selector, - false, - 'core_question/question_banks_datasource', - placeholder, - false, - true, - '', - true - ); - - // Hide the selection element as we don't need it. - document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none'); - - return this; - } } diff --git a/public/mod/quiz/amd/src/modal_add_random_question.js b/public/mod/quiz/amd/src/modal_add_random_question.js index 12d05f4fc9b..0aada464d70 100644 --- a/public/mod/quiz/amd/src/modal_add_random_question.js +++ b/public/mod/quiz/amd/src/modal_add_random_question.js @@ -29,6 +29,7 @@ import * as Templates from 'core/templates'; import * as FormChangeChecker from 'core_form/changechecker'; import {call as fetchMany} from 'core/ajax'; import Pending from 'core/pending'; +import BankSwitcher from 'core_question/bank_switcher'; const SELECTORS = { ANCHOR: 'a[href]', @@ -70,6 +71,7 @@ export default class ModalAddRandomQuestion extends Modal { * @param {string} returnUrl URL to return to after form submission. * @param {Number} quizCmId Current quiz course module id. * @param {boolean} showNewCategory Display the New category tab when selecting random questions. + * @param {Number} courseId Current course id, required for switching banks. */ static init( contextId, @@ -77,7 +79,8 @@ export default class ModalAddRandomQuestion extends Modal { category, returnUrl, quizCmId, - showNewCategory = true + showNewCategory = true, + courseId, ) { const selector = '.menu [data-action="addarandomquestion"], [data-action="editrandomquestion"]'; document.addEventListener('click', (e) => { @@ -104,6 +107,7 @@ export default class ModalAddRandomQuestion extends Modal { templateContext: { hidden: showNewCategory, }, + courseId, }); }); } @@ -131,6 +135,7 @@ export default class ModalAddRandomQuestion extends Modal { this.showNewCategory = modalConfig.showNewCategory; this.setSlotId(modalConfig.slotId ?? 0); this.setSavedFilterCondition(modalConfig.savedFilterCondition ?? null); + this.setCourseId(modalConfig.courseId); super.configure(modalConfig); } @@ -206,6 +211,24 @@ export default class ModalAddRandomQuestion extends Modal { return this.slotId; } + /** + * Store the ID of the current course + * + * @param {Number} courseId + */ + setCourseId(courseId) { + this.courseId = courseId; + } + + /** + * Get the current course ID. + * + * @return {Number} + */ + getCourseId() { + return this.courseId; + } + /** * Store the current filterCondition JSON string. * @@ -366,61 +389,22 @@ export default class ModalAddRandomQuestion extends Modal { } }); + const switcher = new BankSwitcher(); this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => { this.setSavedFilterCondition( document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition ); - this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH) - .then(function(ModalQuizQuestionBank) { - $(SELECTORS.BANK_SEARCH)?.on('change', (e) => { - const bankCmId = $(e.currentTarget).val(); - // Have to recreate the modal as we have already used the body for the switch bank content. - if (bankCmId > 0) { - ModalAddRandomQuestion.create({ - 'contextId': ModalQuizQuestionBank.getContextId(), - 'bankCmId': bankCmId, - 'category': ModalQuizQuestionBank.getCategory(), - 'returnUrl': ModalQuizQuestionBank.getReturnUrl(), - 'quizCmId': ModalQuizQuestionBank.quizCmId, - 'title': ModalQuizQuestionBank.originalTitle, - 'addOnPage': ModalQuizQuestionBank.getAddOnPageId(), - 'templateContext': {hidden: ModalQuizQuestionBank.showNewCategory}, - 'showNewCategory': ModalQuizQuestionBank.showNewCategory, - 'slotId': ModalQuizQuestionBank.getSlotId(), - }) - .then(ModalQuizQuestionBank.destroy()) - .catch(Notification.exception); - } - }); - return ModalQuizQuestionBank; - }); + try { + switcher.show(this, this.courseId, this.getContextId(), this.bankCmId, this.quizCmId); + } catch (ex) { + Notification.exception(ex); + } }); - - this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => { - const anchorElement = $(e.currentTarget); - // Have to recreate the modal as we have already used the body for the switch bank content. - ModalAddRandomQuestion.create({ - 'contextId': this.getContextId(), - 'bankCmId': anchorElement.attr('value'), - 'category': this.getCategory(), - 'returnUrl': this.getReturnUrl(), - 'quizCmId': this.quizCmId, - 'title': this.originalTitle, - 'addOnPage': this.getAddOnPageId(), - 'templateContext': {hidden: this.showNewCategory}, - 'showNewCategory': this.showNewCategory, - 'savedFilterCondition': this.getSavedFilterCondition(), - 'slotId': this.getSlotId(), - }).then(this.destroy()).catch(Notification.exception); - }); - - this.getModal().on('click', SELECTORS.ANCHOR, (e) => { - const anchorElement = $(e.currentTarget); - // Have to recreate the modal as we have already used the body for the switch bank content. - if (anchorElement.closest('a[' + SELECTORS.NEW_BANKMOD_ID + ']').length) { - ModalAddRandomQuestion.create({ + this.getModal().get(0).addEventListener('bankSwitched', async(e) => { + if (e.detail.cmid > 0) { + const modalConfig = { 'contextId': this.getContextId(), - 'bankCmId': anchorElement.attr(SELECTORS.NEW_BANKMOD_ID), + 'bankCmId': e.detail.cmid, 'category': this.getCategory(), 'returnUrl': this.getReturnUrl(), 'quizCmId': this.quizCmId, @@ -429,7 +413,18 @@ export default class ModalAddRandomQuestion extends Modal { 'templateContext': {hidden: this.showNewCategory}, 'showNewCategory': this.showNewCategory, 'slotId': this.getSlotId(), - }).then(this.destroy()).catch(Notification.exception); + 'courseId': this.getCourseId(), + }; + if (e.detail.cmid === this.bankCmId) { + // We're displaying the same bank as before, keep the current filters. + modalConfig.savedFilterCondition = this.getSavedFilterCondition(); + } + try { + await ModalAddRandomQuestion.create(modalConfig); + this.destroy(); + } catch (ex) { + await Notification.exception(ex); + } } }); }) diff --git a/public/mod/quiz/amd/src/modal_quiz_question_bank.js b/public/mod/quiz/amd/src/modal_quiz_question_bank.js index 5fac3a76bff..731b6a12b50 100644 --- a/public/mod/quiz/amd/src/modal_quiz_question_bank.js +++ b/public/mod/quiz/amd/src/modal_quiz_question_bank.js @@ -26,6 +26,7 @@ import * as Fragment from 'core/fragment'; import * as FormChangeChecker from 'core_form/changechecker'; import * as ModalEvents from 'core/modal_events'; import * as Notification from 'core/notification'; +import BankSwitcher from 'core_question/bank_switcher'; const SELECTORS = { ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction', @@ -50,8 +51,9 @@ export default class ModalQuizQuestionBank extends Modal { * @param {Number} contextId Current module context id. * @param {Number} bankCmId Current question bank course module id. * @param {Number} quizCmId Current quiz course module id. + * @param {Number} courseId Current course id, required for switching banks. */ - static init(contextId, bankCmId, quizCmId) { + static init(contextId, bankCmId, quizCmId, courseId) { const selector = '.menu [data-action="questionbank"]'; document.addEventListener('click', (e) => { const trigger = e.target.closest(selector); @@ -70,6 +72,7 @@ export default class ModalQuizQuestionBank extends Modal { hidden: true, }, large: true, + courseId, }); }); } @@ -157,23 +160,27 @@ export default class ModalQuizQuestionBank extends Modal { formElement.setAttribute('action', actionUrl.toString()); }); + const switcher = new BankSwitcher(); + this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => { - this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH) - .then(function(ModalQuizQuestionBank) { - document.querySelector(SELECTORS.BANK_SEARCH)?.addEventListener('change', (e) => { - const bankCmId = e.currentTarget.value; - if (bankCmId > 0) { - ModalQuizQuestionBank.bankCmId = bankCmId; - ModalQuizQuestionBank.reloadBodyContent(window.location.search); - } - }); - document.querySelector(SELECTORS.GO_BACK_BUTTON).addEventListener('click', (e) => { - ModalQuizQuestionBank.bankCmId = e.currentTarget.value; - ModalQuizQuestionBank.reloadBodyContent(window.location.search); - }); - } - ) - .catch(Notification.exception); + try { + switcher.show(this, this.courseId, this.getContextId(), this.bankCmId, this.quizCmId); + } catch (ex) { + Notification.exception(ex); + } + }); + + this.getModal().get(0).addEventListener('bankSwitched', (e) => { + const bankCmId = e.detail.cmid; + if (bankCmId > 0) { + // We need to clear the filter as we are about to reload the content. + const url = new URL(location.href); + url.searchParams.delete('filter'); + history.pushState({}, '', url); + + this.bankCmId = bankCmId; + this.reloadBodyContent(window.location.search); + } }); this.getModal().on('click', SELECTORS.ANCHOR, (e) => { @@ -185,28 +192,7 @@ export default class ModalQuizQuestionBank extends Modal { return; } - // If the anchor element was a preview question link. - if (anchorElement.closest(SELECTORS.PREVIEW_CONTAINER)) { - return; - } - - // Sorting links have their own handler. - if (anchorElement.closest(SELECTORS.SORTERS)) { - return; - } - - if (anchorElement.closest('a[' + SELECTORS.NEW_BANKMOD_ID + ']')) { - this.bankCmId = anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID); - - // We need to clear the filter as we are about to reload the content. - const url = new URL(location.href); - url.searchParams.delete('filter'); - history.pushState({}, '', url); - } - - // Anything else means reload the pop-up contents. e.preventDefault(); - this.reloadBodyContent(anchorElement.search); }); // Disable the form change checker when the body is rendered. diff --git a/public/mod/quiz/classes/output/edit_renderer.php b/public/mod/quiz/classes/output/edit_renderer.php index 83176d4a344..a8cdfff6017 100644 --- a/public/mod/quiz/classes/output/edit_renderer.php +++ b/public/mod/quiz/classes/output/edit_renderer.php @@ -122,6 +122,7 @@ class edit_renderer extends \plugin_renderer_base { $thiscontext->id, $quizobj->get_cm()->id, $quizobj->get_cm()->id, + $quizobj->get_courseid(), ]); $this->page->requires->js_call_amd('mod_quiz/modal_add_random_question', 'init', [ @@ -131,6 +132,7 @@ class edit_renderer extends \plugin_renderer_base { $pageurl->out_as_local_url(true), $pageurl->param('cmid'), \core\plugininfo\qbank::is_plugin_enabled(\qbank_managecategories\helper::PLUGINNAME), + $quizobj->get_courseid(), ]); // Include the question chooser. diff --git a/public/mod/quiz/lang/en/deprecated.txt b/public/mod/quiz/lang/en/deprecated.txt index 4e33c9b3234..46dbba18e3b 100644 --- a/public/mod/quiz/lang/en/deprecated.txt +++ b/public/mod/quiz/lang/en/deprecated.txt @@ -5,3 +5,5 @@ groupoverridesdeleted,mod_quiz randomcoursecatwithsubcat,mod_quiz randomcoursewithsubcat,mod_quiz randomsystemwithsubcat,mod_quiz +selectquestionbank,mod_quiz +gobacktoquiz,mod_quiz diff --git a/public/mod/quiz/lang/en/quiz.php b/public/mod/quiz/lang/en/quiz.php index 3ce69a22f26..81de4c729b5 100644 --- a/public/mod/quiz/lang/en/quiz.php +++ b/public/mod/quiz/lang/en/quiz.php @@ -1012,7 +1012,6 @@ $string['selectedattempts'] = 'Selected attempts...'; $string['selectmultipleitems'] = 'Select multiple items'; $string['selectmultipletoolbar'] = 'Select multiple toolbar'; $string['selectnone'] = 'Deselect all'; -$string['selectquestionbank'] = 'Select question bank'; $string['selectquestionslot'] = 'Select question {$a}'; $string['selectquestiontype'] = '-- Select question type --'; $string['sendnotificationopendatesoon'] = 'Notify user of an approaching quiz open date'; @@ -1155,7 +1154,6 @@ $string['yourfinalgradeis'] = 'Your final grade for this quiz is {$a}.'; $string['questionversion'] = 'v{$a}'; $string['questionversionlatest'] = 'v{$a} (latest)'; $string['alwayslatest'] = 'Always latest'; -$string['gobacktoquiz'] = 'Go back'; // Deprecated since Moodle 4.5. $string['attemptsdeleted'] = 'Quiz attempts deleted'; @@ -1167,3 +1165,7 @@ $string['groupoverridesdeleted'] = 'Group overrides deleted'; $string['randomcoursecatwithsubcat'] = 'Any category inside course category {$a}'; $string['randomcoursewithsubcat'] = 'Any category in this course'; $string['randomsystemwithsubcat'] = 'Any system-level category'; + +// Deprecated since Moodle 5.2. +$string['gobacktoquiz'] = 'Go back'; +$string['selectquestionbank'] = 'Select question bank'; diff --git a/public/mod/quiz/lib.php b/public/mod/quiz/lib.php index 74990c16ca9..3e6b9434bef 100644 --- a/public/mod/quiz/lib.php +++ b/public/mod/quiz/lib.php @@ -2490,8 +2490,15 @@ function mod_quiz_output_fragment_quiz_question_bank($args): string { * @param array $args provided by the AJAX request. * @return string html to render to the modal. */ +#[\core\attribute\deprecated( + replacement: 'core_question\route\api\bank::switcher', + since: '5.2', + reason: 'Replaced with a non-quiz-specific API endpoint', + mdl: 'MDL-87264', +)] function mod_quiz_output_fragment_switch_question_bank($args): string { global $USER, $COURSE, $OUTPUT; + \core\deprecation::emit_deprecation(__FUNCTION__); $quizcmid = clean_param($args['quizcmid'], PARAM_INT); diff --git a/public/question/amd/build/bank_switcher.min.js b/public/question/amd/build/bank_switcher.min.js new file mode 100644 index 00000000000..17ffacc2cdc --- /dev/null +++ b/public/question/amd/build/bank_switcher.min.js @@ -0,0 +1,21 @@ +define("core_question/bank_switcher",["exports","core/str","core/fetch","core/templates","core/form-autocomplete"],(function(_exports,_str,_fetch,_templates,_formAutocomplete){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Bank switcher module for switching between question banks. + * + * The BankSwitcher class is used to display the bank switching UI in an existing Modal. This can either be a new Modal + * created for the purpose, or an existing modal, in which case it will replace the existing title, body and footer. + * + * When a new bank is selected, it will emit a custom `bankSwitched` event on the modal's DOM element, + * with the cmid of the selected bank in `event.details.cmid`. For example: + * + * const modal = Modal.create(...); + * const switcher = new BankSwitcher(); + * switcher.show(modal, courseId, cmId); + * modal.getModal().get(0).addEventListener('bankSwitched', (e) => window.console.log(e.details.cmid)); + * + * @module core_question/bank_switcher + * @copyright 2025 Catalyst IT Europe Ltd. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_fetch=_interopRequireDefault(_fetch),_templates=_interopRequireDefault(_templates),_formAutocomplete=_interopRequireDefault(_formAutocomplete);const SELECTORS_ANCHOR="a[data-newmodid]",SELECTORS_AUTOCOMPLETE_SELECTION=".search-banks .form-autocomplete-selection",SELECTORS_BANK_SEARCH="#searchbanks",SELECTORS_BANK_SWITCHER=".bank-switcher",SELECTORS_GO_BACK_BUTTON='button[data-action="go-back"]',SELECTORS_MODAL='div[data-region="modal"]';return _exports.default=class{async show(modal,courseId,contextId){var _modalBody$querySelec;let currentCmId=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,activityCmId=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;if(!courseId)throw new Error("courseId is required by the bank switcher.");if(modal.setTitle((0,_str.get_string)("switcherselectbank","core_question")),currentCmId){const el=document.createElement("button");el.classList.add("btn","btn-primary"),el.textContent=await(0,_str.get_string)("switchergoback","core_question"),el.setAttribute("data-action","go-back"),el.setAttribute("value",currentCmId),el.addEventListener("click",this.clickListener),modal.setFooter(el)}const params={course:courseId,includeshared:!0,includerecent:!0};activityCmId&&(params.currentmodule=activityCmId);const banksResponse=await _fetch.default.performGet("question","banks",{params:params}),{banks:banks}=await banksResponse.json(),templateContext={contextid:contextId,hasactivitybank:!1,hascoursesharedbanks:!1,coursesharedbanks:[],hasrecentlyviewedbanks:!1,recentlyviewedbanks:[]};banks.forEach((bank=>bank.current?(templateContext.hasactivitybank=!0,void(templateContext.activitybank={name:bank.name,cmid:bank.modid})):bank.recent?(templateContext.hasrecentlyviewedbanks=!0,void templateContext.recentlyviewedbanks.push({coursenamebankname:bank.coursenamebankname,modid:bank.modid})):(templateContext.hascoursesharedbanks=!0,void templateContext.coursesharedbanks.push({name:bank.name,modid:bank.modid})))),modal.setBody(_templates.default.render("core_question/switch_question_bank",templateContext));const placeholder=await(0,_str.get_string)("switchersearchbyname","core_question");await modal.getBodyPromise(),await _formAutocomplete.default.enhance(SELECTORS_BANK_SEARCH,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0);const modalBody=modal.getBody()[0];null===(_modalBody$querySelec=modalBody.querySelector(SELECTORS_AUTOCOMPLETE_SELECTION))||void 0===_modalBody$querySelec||_modalBody$querySelec.classList.add("d-none");const switcherElement=modalBody.querySelector(SELECTORS_BANK_SWITCHER);switcherElement.addEventListener("click",this.clickListener),switcherElement.addEventListener("change",this.changeListener)}clickListener(e){const goBack=e.target.closest(SELECTORS_GO_BACK_BUTTON);let cmid;if(goBack)cmid=goBack.value;else{const anchor=e.target.closest(SELECTORS_ANCHOR);if(!anchor)return;cmid=anchor.dataset.newmodid}e.preventDefault();const modalElement=e.target.closest(SELECTORS_MODAL),bankSwitched=new CustomEvent("bankSwitched",{detail:{cmid:cmid}});modalElement.dispatchEvent(bankSwitched)}changeListener(e){const search=e.target.closest(SELECTORS_BANK_SEARCH);if(!search)return;const bankCmId=search.value;if(bankCmId>0){const modalElement=e.target.closest(SELECTORS_MODAL),bankSwitched=new CustomEvent("bankSwitched",{detail:{cmid:bankCmId}});modalElement.dispatchEvent(bankSwitched)}}},_exports.default})); + +//# sourceMappingURL=bank_switcher.min.js.map \ No newline at end of file diff --git a/public/question/amd/build/bank_switcher.min.js.map b/public/question/amd/build/bank_switcher.min.js.map new file mode 100644 index 00000000000..601f358a3c9 --- /dev/null +++ b/public/question/amd/build/bank_switcher.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bank_switcher.min.js","sources":["../src/bank_switcher.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {get_string as getString} from 'core/str';\nimport Fetch from 'core/fetch';\nimport Templates from 'core/templates';\nimport AutoComplete from 'core/form-autocomplete';\n\n/**\n * Bank switcher module for switching between question banks.\n *\n * The BankSwitcher class is used to display the bank switching UI in an existing Modal. This can either be a new Modal\n * created for the purpose, or an existing modal, in which case it will replace the existing title, body and footer.\n *\n * When a new bank is selected, it will emit a custom `bankSwitched` event on the modal's DOM element,\n * with the cmid of the selected bank in `event.details.cmid`. For example:\n *\n * const modal = Modal.create(...);\n * const switcher = new BankSwitcher();\n * switcher.show(modal, courseId, cmId);\n * modal.getModal().get(0).addEventListener('bankSwitched', (e) => window.console.log(e.details.cmid));\n *\n * @module core_question/bank_switcher\n * @copyright 2025 Catalyst IT Europe Ltd.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n ANCHOR: 'a[data-newmodid]',\n AUTOCOMPLETE_SELECTION: '.search-banks .form-autocomplete-selection',\n BANK_SEARCH: '#searchbanks',\n BANK_SWITCHER: '.bank-switcher',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n MODAL: 'div[data-region=\"modal\"]',\n};\n\nexport default class BankSwitcher {\n /**\n * Show the bank switcher.\n *\n * This will replace the content of the given modal with the bank switcher, and emit a bankChanged event on the modal\n * when a bank is selected (either a new bank, or the current one again).\n *\n * @param {Modal} modal The modal to display the switcher within.\n * @param {Number} courseId The course ID to display banks from.\n * @param {Number} contextId The context the bank switcher is being displayed in, for text filters.\n * @param {Number} currentCmId The cmid of the currently selected question bank.\n * @param {Number} activityCmId The cmid of activity we are currently in (if any).\n * @return {Promise} Resolves once the switcher has finished displaying.\n */\n async show(modal, courseId, contextId, currentCmId = null, activityCmId = null) {\n if (!courseId) {\n throw new Error('courseId is required by the bank switcher.');\n }\n modal.setTitle(getString('switcherselectbank', 'core_question'));\n\n if (currentCmId) {\n // Create a 'Go back' button and set it in the footer.\n const el = document.createElement('button');\n el.classList.add('btn', 'btn-primary');\n el.textContent = await getString('switchergoback', 'core_question');\n el.setAttribute('data-action', 'go-back');\n el.setAttribute('value', currentCmId);\n el.addEventListener('click', this.clickListener);\n modal.setFooter(el);\n }\n\n const params = {\n course: courseId,\n includeshared: true,\n includerecent: true,\n };\n if (activityCmId) {\n params.currentmodule = activityCmId;\n }\n const banksResponse = await Fetch.performGet('question', 'banks', {params});\n const {banks} = await banksResponse.json();\n const templateContext = {\n contextid: contextId,\n hasactivitybank: false,\n hascoursesharedbanks: false,\n coursesharedbanks: [],\n hasrecentlyviewedbanks: false,\n recentlyviewedbanks: [],\n };\n banks.forEach((bank) => {\n if (bank.current) {\n templateContext.hasactivitybank = true;\n templateContext.activitybank = {\n name: bank.name,\n cmid: bank.modid,\n };\n return;\n }\n if (bank.recent) {\n templateContext.hasrecentlyviewedbanks = true;\n templateContext.recentlyviewedbanks.push({\n coursenamebankname: bank.coursenamebankname,\n modid: bank.modid,\n });\n return;\n }\n templateContext.hascoursesharedbanks = true;\n templateContext.coursesharedbanks.push({\n name: bank.name,\n modid: bank.modid,\n });\n });\n\n modal.setBody(\n Templates.render('core_question/switch_question_bank', templateContext)\n );\n const placeholder = await getString('switchersearchbyname', 'core_question');\n await modal.getBodyPromise();\n await AutoComplete.enhance(\n SELECTORS.BANK_SEARCH,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true\n );\n\n const modalBody = modal.getBody()[0];\n // Hide the selection element as we don't need it.\n modalBody.querySelector(SELECTORS.AUTOCOMPLETE_SELECTION)?.classList.add('d-none');\n\n const switcherElement = modalBody.querySelector(SELECTORS.BANK_SWITCHER);\n switcherElement.addEventListener('click', this.clickListener);\n switcherElement.addEventListener('change', this.changeListener);\n }\n\n clickListener(e) {\n const goBack = e.target.closest(SELECTORS.GO_BACK_BUTTON);\n let cmid;\n if (goBack) {\n cmid = goBack.value;\n } else {\n const anchor = e.target.closest(SELECTORS.ANCHOR);\n if (!anchor) {\n return;\n }\n cmid = anchor.dataset.newmodid;\n }\n e.preventDefault();\n const modalElement = e.target.closest(SELECTORS.MODAL);\n const bankSwitched = new CustomEvent('bankSwitched', {detail: {cmid}});\n modalElement.dispatchEvent(bankSwitched);\n }\n\n changeListener(e) {\n const search = e.target.closest(SELECTORS.BANK_SEARCH);\n if (!search) {\n return;\n }\n const bankCmId = search.value;\n if (bankCmId > 0) {\n const modalElement = e.target.closest(SELECTORS.MODAL);\n const bankSwitched = new CustomEvent('bankSwitched', {detail: {cmid: bankCmId}});\n modalElement.dispatchEvent(bankSwitched);\n }\n }\n}\n"],"names":["SELECTORS","modal","courseId","contextId","currentCmId","activityCmId","Error","setTitle","el","document","createElement","classList","add","textContent","setAttribute","addEventListener","this","clickListener","setFooter","params","course","includeshared","includerecent","currentmodule","banksResponse","Fetch","performGet","banks","json","templateContext","contextid","hasactivitybank","hascoursesharedbanks","coursesharedbanks","hasrecentlyviewedbanks","recentlyviewedbanks","forEach","bank","current","activitybank","name","cmid","modid","recent","push","coursenamebankname","setBody","Templates","render","placeholder","getBodyPromise","AutoComplete","enhance","modalBody","getBody","querySelector","switcherElement","changeListener","e","goBack","target","closest","value","anchor","dataset","newmodid","preventDefault","modalElement","bankSwitched","CustomEvent","detail","dispatchEvent","search","bankCmId"],"mappings":";;;;;;;;;;;;;;;;;;2OAuCMA,iBACM,mBADNA,iCAEsB,6CAFtBA,sBAGW,eAHXA,wBAIa,iBAJbA,yBAKc,gCALdA,gBAMK,oEAiBIC,MAAOC,SAAUC,yCAAWC,mEAAc,KAAMC,oEAAe,SACjEH,eACK,IAAII,MAAM,iDAEpBL,MAAMM,UAAS,mBAAU,qBAAsB,kBAE3CH,YAAa,OAEPI,GAAKC,SAASC,cAAc,UAClCF,GAAGG,UAAUC,IAAI,MAAO,eACxBJ,GAAGK,kBAAoB,mBAAU,iBAAkB,iBACnDL,GAAGM,aAAa,cAAe,WAC/BN,GAAGM,aAAa,QAASV,aACzBI,GAAGO,iBAAiB,QAASC,KAAKC,eAClChB,MAAMiB,UAAUV,UAGdW,OAAS,CACXC,OAAQlB,SACRmB,eAAe,EACfC,eAAe,GAEfjB,eACAc,OAAOI,cAAgBlB,oBAErBmB,oBAAsBC,eAAMC,WAAW,WAAY,QAAS,CAACP,OAAAA,UAC7DQ,MAACA,aAAeH,cAAcI,OAC9BC,gBAAkB,CACpBC,UAAW3B,UACX4B,iBAAiB,EACjBC,sBAAsB,EACtBC,kBAAmB,GACnBC,wBAAwB,EACxBC,oBAAqB,IAEzBR,MAAMS,SAASC,MACPA,KAAKC,SACLT,gBAAgBE,iBAAkB,OAClCF,gBAAgBU,aAAe,CAC3BC,KAAMH,KAAKG,KACXC,KAAMJ,KAAKK,SAIfL,KAAKM,QACLd,gBAAgBK,wBAAyB,OACzCL,gBAAgBM,oBAAoBS,KAAK,CACrCC,mBAAoBR,KAAKQ,mBACzBH,MAAOL,KAAKK,UAIpBb,gBAAgBG,sBAAuB,OACvCH,gBAAgBI,kBAAkBW,KAAK,CACnCJ,KAAMH,KAAKG,KACXE,MAAOL,KAAKK,WAIpBzC,MAAM6C,QACFC,mBAAUC,OAAO,qCAAsCnB,wBAErDoB,kBAAoB,mBAAU,uBAAwB,uBACtDhD,MAAMiD,uBACNC,0BAAaC,QACfpD,uBACA,EACA,0CACAiD,aACA,GACA,EACA,IACA,SAGEI,UAAYpD,MAAMqD,UAAU,iCAElCD,UAAUE,cAAcvD,0FAAmCW,UAAUC,IAAI,gBAEnE4C,gBAAkBH,UAAUE,cAAcvD,yBAChDwD,gBAAgBzC,iBAAiB,QAASC,KAAKC,eAC/CuC,gBAAgBzC,iBAAiB,SAAUC,KAAKyC,gBAGpDxC,cAAcyC,SACJC,OAASD,EAAEE,OAAOC,QAAQ7D,8BAC5ByC,QACAkB,OACAlB,KAAOkB,OAAOG,UACX,OACGC,OAASL,EAAEE,OAAOC,QAAQ7D,sBAC3B+D,cAGLtB,KAAOsB,OAAOC,QAAQC,SAE1BP,EAAEQ,uBACIC,aAAeT,EAAEE,OAAOC,QAAQ7D,iBAChCoE,aAAe,IAAIC,YAAY,eAAgB,CAACC,OAAQ,CAAC7B,KAAAA,QAC/D0B,aAAaI,cAAcH,cAG/BX,eAAeC,SACLc,OAASd,EAAEE,OAAOC,QAAQ7D,2BAC3BwE,oBAGCC,SAAWD,OAAOV,SACpBW,SAAW,EAAG,OACRN,aAAeT,EAAEE,OAAOC,QAAQ7D,iBAChCoE,aAAe,IAAIC,YAAY,eAAgB,CAACC,OAAQ,CAAC7B,KAAMgC,YACrEN,aAAaI,cAAcH"} \ No newline at end of file diff --git a/public/question/amd/src/bank_switcher.js b/public/question/amd/src/bank_switcher.js new file mode 100644 index 00000000000..de16b31022b --- /dev/null +++ b/public/question/amd/src/bank_switcher.js @@ -0,0 +1,177 @@ +// 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 . + +import {get_string as getString} from 'core/str'; +import Fetch from 'core/fetch'; +import Templates from 'core/templates'; +import AutoComplete from 'core/form-autocomplete'; + +/** + * Bank switcher module for switching between question banks. + * + * The BankSwitcher class is used to display the bank switching UI in an existing Modal. This can either be a new Modal + * created for the purpose, or an existing modal, in which case it will replace the existing title, body and footer. + * + * When a new bank is selected, it will emit a custom `bankSwitched` event on the modal's DOM element, + * with the cmid of the selected bank in `event.details.cmid`. For example: + * + * const modal = Modal.create(...); + * const switcher = new BankSwitcher(); + * switcher.show(modal, courseId, cmId); + * modal.getModal().get(0).addEventListener('bankSwitched', (e) => window.console.log(e.details.cmid)); + * + * @module core_question/bank_switcher + * @copyright 2025 Catalyst IT Europe Ltd. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +const SELECTORS = { + ANCHOR: 'a[data-newmodid]', + AUTOCOMPLETE_SELECTION: '.search-banks .form-autocomplete-selection', + BANK_SEARCH: '#searchbanks', + BANK_SWITCHER: '.bank-switcher', + GO_BACK_BUTTON: 'button[data-action="go-back"]', + MODAL: 'div[data-region="modal"]', +}; + +export default class BankSwitcher { + /** + * Show the bank switcher. + * + * This will replace the content of the given modal with the bank switcher, and emit a bankChanged event on the modal + * when a bank is selected (either a new bank, or the current one again). + * + * @param {Modal} modal The modal to display the switcher within. + * @param {Number} courseId The course ID to display banks from. + * @param {Number} contextId The context the bank switcher is being displayed in, for text filters. + * @param {Number} currentCmId The cmid of the currently selected question bank. + * @param {Number} activityCmId The cmid of activity we are currently in (if any). + * @return {Promise} Resolves once the switcher has finished displaying. + */ + async show(modal, courseId, contextId, currentCmId = null, activityCmId = null) { + if (!courseId) { + throw new Error('courseId is required by the bank switcher.'); + } + modal.setTitle(getString('switcherselectbank', 'core_question')); + + if (currentCmId) { + // Create a 'Go back' button and set it in the footer. + const el = document.createElement('button'); + el.classList.add('btn', 'btn-primary'); + el.textContent = await getString('switchergoback', 'core_question'); + el.setAttribute('data-action', 'go-back'); + el.setAttribute('value', currentCmId); + el.addEventListener('click', this.clickListener); + modal.setFooter(el); + } + + const params = { + course: courseId, + includeshared: true, + includerecent: true, + }; + if (activityCmId) { + params.currentmodule = activityCmId; + } + const banksResponse = await Fetch.performGet('question', 'banks', {params}); + const {banks} = await banksResponse.json(); + const templateContext = { + contextid: contextId, + hasactivitybank: false, + hascoursesharedbanks: false, + coursesharedbanks: [], + hasrecentlyviewedbanks: false, + recentlyviewedbanks: [], + }; + banks.forEach((bank) => { + if (bank.current) { + templateContext.hasactivitybank = true; + templateContext.activitybank = { + name: bank.name, + cmid: bank.modid, + }; + return; + } + if (bank.recent) { + templateContext.hasrecentlyviewedbanks = true; + templateContext.recentlyviewedbanks.push({ + coursenamebankname: bank.coursenamebankname, + modid: bank.modid, + }); + return; + } + templateContext.hascoursesharedbanks = true; + templateContext.coursesharedbanks.push({ + name: bank.name, + modid: bank.modid, + }); + }); + + modal.setBody( + Templates.render('core_question/switch_question_bank', templateContext) + ); + const placeholder = await getString('switchersearchbyname', 'core_question'); + await modal.getBodyPromise(); + await AutoComplete.enhance( + SELECTORS.BANK_SEARCH, + false, + 'core_question/question_banks_datasource', + placeholder, + false, + true, + '', + true + ); + + const modalBody = modal.getBody()[0]; + // Hide the selection element as we don't need it. + modalBody.querySelector(SELECTORS.AUTOCOMPLETE_SELECTION)?.classList.add('d-none'); + + const switcherElement = modalBody.querySelector(SELECTORS.BANK_SWITCHER); + switcherElement.addEventListener('click', this.clickListener); + switcherElement.addEventListener('change', this.changeListener); + } + + clickListener(e) { + const goBack = e.target.closest(SELECTORS.GO_BACK_BUTTON); + let cmid; + if (goBack) { + cmid = goBack.value; + } else { + const anchor = e.target.closest(SELECTORS.ANCHOR); + if (!anchor) { + return; + } + cmid = anchor.dataset.newmodid; + } + e.preventDefault(); + const modalElement = e.target.closest(SELECTORS.MODAL); + const bankSwitched = new CustomEvent('bankSwitched', {detail: {cmid}}); + modalElement.dispatchEvent(bankSwitched); + } + + changeListener(e) { + const search = e.target.closest(SELECTORS.BANK_SEARCH); + if (!search) { + return; + } + const bankCmId = search.value; + if (bankCmId > 0) { + const modalElement = e.target.closest(SELECTORS.MODAL); + const bankSwitched = new CustomEvent('bankSwitched', {detail: {cmid: bankCmId}}); + modalElement.dispatchEvent(bankSwitched); + } + } +} diff --git a/public/question/bank/bulkmove/classes/output/bulk_move.php b/public/question/bank/bulkmove/classes/output/bulk_move.php index f50bfbfb656..036e2857b95 100644 --- a/public/question/bank/bulkmove/classes/output/bulk_move.php +++ b/public/question/bank/bulkmove/classes/output/bulk_move.php @@ -72,14 +72,14 @@ class bulk_move implements \renderable, \templatable { currentbankid: $this->currentbankid, filtercontext: $currentbankcm->context, limit: 1, - )[0]; + )[0]->get_formatted(); } else { $banktorender = question_bank_helper::get_activity_instances_with_private_questions( incourseids: [$currentbankcm->course], havingcap: ['moodle/question:add'], currentbankid: $this->currentbankid, filtercontext: $currentbankcm->context, - )[0]; + )[0]->get_formatted(); } $categoryselector = new question_category_selector( diff --git a/public/question/bank/managecategories/amd/build/category.min.js b/public/question/bank/managecategories/amd/build/category.min.js index 63c870467ac..f35716ce81f 100644 --- a/public/question/bank/managecategories/amd/build/category.min.js +++ b/public/question/bank/managecategories/amd/build/category.min.js @@ -1,3 +1,3 @@ -define("qbank_managecategories/category",["exports","core/reactive","qbank_managecategories/categorymanager","core/templates","core/modal","core/str","core/local/inplace_editable/events"],(function(_exports,_reactive,_categorymanager,_templates,_modal,_str,_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal);class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={CATEGORY_LIST:".qbank_managecategories-categorylist",CATEGORY_ITEM:".qbank_managecategories-item[data-categoryid]",CATEGORY_CONTENTS:".qbank_managecategories-item > .container",EDIT_BUTTON:'[data-action="addeditcategory"]',EDITABLE_CATEGORY_NAME:'.inplaceeditable[data-itemtype="categoryname"]',MOVE_BUTTON:'[role="menuitem"][data-actiontype="move"]',CONTEXT:".qbank_managecategories-categorylist[data-contextid]",MODAL_CATEGORY_ITEM:".modal_category_item[data-movingcategoryid]",CONTENT_AREA:".qbank_managecategories-details",CATEGORY_ID:id=>"#category-".concat(id),CONTENT_CONTAINER:id=>"#category-".concat(id," .qbank_managecategories-childlistcontainer"),CHILD_LIST:id=>'ul[data-categoryid="'.concat(id,'"]'),PREVIOUS_SIBLING:sortorder=>':scope > [data-sortorder="'.concat(sortorder,'"]')},this.classes={NO_BOTTOM_PADDING:"pb-0",DRAGHANDLE:"draghandle",DROPTARGET:"qbank_managecategories-droptarget-before"},this.ids={CATEGORY:id=>"category-".concat(id)}}stateReady(){this.initDragDrop(),this.addEventListener(this.getElement(this.selectors.EDIT_BUTTON),"click",_categorymanager.categorymanager.showEditModal);const moveButton=this.getElement(this.selectors.MOVE_BUTTON);this.addEventListener(moveButton,"click",this.showMoveModal),this.addEventListener(this.getElement(),_events.eventTypes.elementUpdated,(e=>{const editable=e.target.closest(this.selectors.EDITABLE_CATEGORY_NAME);_categorymanager.categorymanager.updateCategoryName(editable.dataset.itemid,editable.dataset.value)}))}destroy(){this.deInitDragDrop()}initDragDrop(){this.deInitDragDrop(),this.element.classList.contains(this.classes.DRAGHANDLE)&&(this.getDraggableData=this._getDraggableData),this.dragdrop=new _reactive.DragDrop(this)}deInitDragDrop(){void 0!==this.dragdrop&&(void 0!==this.getDraggableData&&(this.dragdrop.setDraggable(!1),this.getDraggableData=void 0),this.dragdrop.unregister(),this.dragdrop=void 0)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}_getDraggableData(){return{id:this.getElement().dataset.categoryid}}validateDropData(){return!0}showDropZone(dropData){return!this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))&&(this.getElement().classList.add(this.classes.DROPTARGET),!0)}hideDropZone(){this.getElement().classList.remove(this.classes.DROPTARGET)}drop(dropData,event){var _precedingSibling;const dropTarget=event.target.closest(this.selectors.CATEGORY_ITEM);if(!dropTarget)return;if(dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id)))return;if(!document.getElementById(this.ids.CATEGORY(dropData.id)))return;const targetParentId=dropTarget.dataset.parent,parentList=dropTarget.closest(this.selectors.CATEGORY_LIST);let precedingSibling;precedingSibling=dropTarget===parentList.firstElementChild?null:dropTarget.previousElementSibling,_categorymanager.categorymanager.moveCategory(dropData.id,targetParentId,null===(_precedingSibling=precedingSibling)||void 0===_precedingSibling?void 0:_precedingSibling.dataset.categoryid)}getWatchers(){return[{watch:"categories[".concat(this.element.dataset.categoryid,"]:updated"),handler:this.updatePosition},{watch:"categories[".concat(this.element.dataset.categoryid,"].templatecontext:created"),handler:this.rerender},{watch:"categories[".concat(this.element.dataset.categoryid,"].templatecontext:updated"),handler:this.rerender},{watch:"categories[".concat(this.element.dataset.categoryid,"].name:updated"),handler:this.updateName},{watch:"categories:created",handler:this.checkChildList}]}async rerender(_ref){let{element:element}=_ref;const{html:html,js:js}=await _templates.default.renderForPromise("qbank_managecategories/category_details",element.templatecontext);return _templates.default.replaceNodeContents(this.getElement(this.selectors.CONTENT_AREA),html,js)}async createChildList(context){const{html:html,js:js}=await _templates.default.renderForPromise("qbank_managecategories/childlist",context),parentContainer=document.querySelector(this.selectors.CONTENT_CONTAINER(context.categoryid));await _templates.default.appendNodeContents(parentContainer,html,js);const childList=document.querySelector(this.selectors.CHILD_LIST(context.categoryid));return childList.closest(this.selectors.CATEGORY_CONTENTS).classList.add(this.classes.NO_BOTTOM_PADDING),childList}async updatePosition(_ref2){let newParent,{element:element}=_ref2;const originParent=document.querySelector(this.selectors.CHILD_LIST(this.getElement().dataset.parent));let previousSibling,nextSibling;var _previousSibling;(parseInt(this.getElement().dataset.parent)!==element.parent?(newParent=document.querySelector(this.selectors.CHILD_LIST(element.parent)),newParent||(newParent=await this.createChildList({categoryid:element.parent})),this.getElement().dataset.parent=element.parent):newParent=this.getElement().parentElement,newParent.firstElementChild&&parseInt(element.sortorder)<=parseInt(newParent.firstElementChild.dataset.sortorder))?nextSibling=newParent.firstElementChild:(previousSibling=newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder-1)),nextSibling=null===(_previousSibling=previousSibling)||void 0===_previousSibling?void 0:_previousSibling.nextElementSibling);(newParent!==this.getElement().parentElement||nextSibling!==this.getElement())&&(nextSibling?newParent.insertBefore(this.getElement(),nextSibling):newParent.appendChild(this.getElement())),originParent!==newParent&&this.reactive.stateManager.processUpdates([{name:"categoryLists",action:"put",fields:{id:originParent.dataset.categoryid,childCount:originParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length}},{name:"categoryLists",action:"put",fields:{id:newParent.dataset.categoryid,childCount:newParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length}}]),this.element.dataset.sortorder=element.sortorder;const isDraggable=this.element.classList.contains(this.classes.DRAGHANDLE);isDraggable&&!element.draghandle?(this.element.classList.remove(this.classes.DRAGHANDLE),this.initDragDrop()):!isDraggable&&element.draghandle&&(this.element.classList.add(this.classes.DRAGHANDLE),this.initDragDrop())}createMoveCategoryList(item,movingCategoryId){const categories=[];if(item.children){let precedingSibling=null;if(item.children.forEach((category=>{var _precedingSibling$dat,_precedingSibling2;const categoryId=parseInt(category.dataset.categoryid);if(categoryId===movingCategoryId)return;let child={categoryid:categoryId,movingcategoryid:movingCategoryId,precedingsiblingid:null!==(_precedingSibling$dat=null===(_precedingSibling2=precedingSibling)||void 0===_precedingSibling2?void 0:_precedingSibling2.dataset.categoryid)&&void 0!==_precedingSibling$dat?_precedingSibling$dat:0,parent:category.dataset.parent,categoryname:category.dataset.categoryname,categories:null,current:categoryId===movingCategoryId};const childList=category.querySelector(this.selectors.CATEGORY_LIST);child.categories=childList?this.createMoveCategoryList(childList,movingCategoryId):[{movingcategoryid:movingCategoryId,precedingsiblingid:0,parent:categoryId,categoryname:category.dataset.categoryname,categories:null,newchild:!0}],categories.push(child),precedingSibling=category})),precedingSibling){const precedingId=parseInt(precedingSibling.dataset.categoryid);precedingId!==movingCategoryId&&categories.push({movingcategoryid:movingCategoryId,precedingsiblingid:precedingId,parent:precedingSibling.dataset.parent,categoryname:precedingSibling.dataset.categoryname,categories:null,lastchild:!0})}}return categories}async showMoveModal(e){const item=e.target.closest(this.selectors.MOVE_BUTTON);if(!item)return;if("true"===item.getAttribute("aria-disabled"))return;item.setAttribute("aria-disabled",!0);let moveList={contexts:[]};document.querySelectorAll(this.selectors.CONTEXT).forEach((context=>{const moveContext={contextname:context.dataset.contextname,categories:[],hascategories:!1};moveContext.categories=this.createMoveCategoryList(context,parseInt(item.dataset.categoryid)),moveContext.hascategories=moveContext.categories.length>0,moveList.contexts.push(moveContext)}));const modal=await _modal.default.create({title:(0,_str.get_string)("movecategory","qbank_managecategories",item.dataset.categoryname),body:_templates.default.render("qbank_managecategories/move_context_list",moveList),footer:"",show:!0,large:!0});modal.getBody()[0].addEventListener("click",(e=>{const target=e.target.closest(this.selectors.MODAL_CATEGORY_ITEM);target&&(_categorymanager.categorymanager.moveCategory(target.dataset.movingcategoryid,target.dataset.parent,target.dataset.precedingsiblingid),modal.destroy())})),item.setAttribute("aria-disabled",!1)}async checkChildList(_ref3){let{element:element}=_ref3;return element.parent!==this.getElement().dataset.categoryid||this.getElement(this.selectors.CATEGORY_LIST)?null:this.createChildList({categoryid:element.parent,children:[element.templatecontext]})}updateName(_ref4){let{element:element}=_ref4;this.getElement().dataset.categoryname=element.name}}return _exports.default=_default,_exports.default})); +define("qbank_managecategories/category",["exports","core/reactive","qbank_managecategories/categorymanager","core/templates","core/modal","core/str","core_question/bank_switcher","core/fetch","core/notification","core/url","core/local/inplace_editable/events"],(function(_exports,_reactive,_categorymanager,_templates,_modal,_str,_bank_switcher,_fetch,_notification,CoreUrl,_events){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),_bank_switcher=_interopRequireDefault(_bank_switcher),_fetch=_interopRequireDefault(_fetch),_notification=_interopRequireDefault(_notification),CoreUrl=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CoreUrl);class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={CATEGORY_LIST:".qbank_managecategories-categorylist",CATEGORY_ITEM:".qbank_managecategories-item[data-categoryid]",CATEGORY_CONTENTS:".qbank_managecategories-item > .container",EDIT_BUTTON:'[data-action="addeditcategory"]',EDITABLE_CATEGORY_NAME:'.inplaceeditable[data-itemtype="categoryname"]',MOVE_BUTTON:'[role="menuitem"][data-actiontype="move"]',CONTEXT:".qbank_managecategories-categorylist[data-contextid]",MODAL_CATEGORY_ITEM:".modal_category_item[data-movingcategoryid]",CONTENT_AREA:".qbank_managecategories-details",CATEGORY_ID:id=>"#category-".concat(id),CONTENT_CONTAINER:id=>"#category-".concat(id," .qbank_managecategories-childlistcontainer"),CHILD_LIST:id=>'ul[data-categoryid="'.concat(id,'"]'),PREVIOUS_SIBLING:sortorder=>':scope > [data-sortorder="'.concat(sortorder,'"]'),SWITCH_QUESTION_BANK:'[data-action="switch-question-bank"]',MOVE_BANK_HEADER:".bank-header"},this.classes={NO_BOTTOM_PADDING:"pb-0",DRAGHANDLE:"draghandle",DROPTARGET:"qbank_managecategories-droptarget-before"},this.ids={CATEGORY:id=>"category-".concat(id)}}stateReady(){this.initDragDrop(),this.addEventListener(this.getElement(this.selectors.EDIT_BUTTON),"click",_categorymanager.categorymanager.showEditModal);const moveButton=this.getElement(this.selectors.MOVE_BUTTON);this.addEventListener(moveButton,"click",this.showMoveModal),this.addEventListener(this.getElement(),_events.eventTypes.elementUpdated,(e=>{const editable=e.target.closest(this.selectors.EDITABLE_CATEGORY_NAME);_categorymanager.categorymanager.updateCategoryName(editable.dataset.itemid,editable.dataset.value)}))}destroy(){this.deInitDragDrop()}initDragDrop(){this.deInitDragDrop(),this.element.classList.contains(this.classes.DRAGHANDLE)&&(this.getDraggableData=this._getDraggableData),this.dragdrop=new _reactive.DragDrop(this)}deInitDragDrop(){void 0!==this.dragdrop&&(void 0!==this.getDraggableData&&(this.dragdrop.setDraggable(!1),this.getDraggableData=void 0),this.dragdrop.unregister(),this.dragdrop=void 0)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}_getDraggableData(){return{id:this.getElement().dataset.categoryid}}validateDropData(){return!0}showDropZone(dropData){return!this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))&&(this.getElement().classList.add(this.classes.DROPTARGET),!0)}hideDropZone(){this.getElement().classList.remove(this.classes.DROPTARGET)}drop(dropData,event){var _precedingSibling;const dropTarget=event.target.closest(this.selectors.CATEGORY_ITEM);if(!dropTarget)return;if(dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id)))return;if(!document.getElementById(this.ids.CATEGORY(dropData.id)))return;const targetParentId=dropTarget.dataset.parent,parentList=dropTarget.closest(this.selectors.CATEGORY_LIST);let precedingSibling;precedingSibling=dropTarget===parentList.firstElementChild?null:dropTarget.previousElementSibling,_categorymanager.categorymanager.moveCategory(dropData.id,targetParentId,null===(_precedingSibling=precedingSibling)||void 0===_precedingSibling?void 0:_precedingSibling.dataset.categoryid)}getWatchers(){return[{watch:"categories[".concat(this.element.dataset.categoryid,"]:updated"),handler:this.updatePosition},{watch:"categories[".concat(this.element.dataset.categoryid,"].templatecontext:created"),handler:this.rerender},{watch:"categories[".concat(this.element.dataset.categoryid,"].templatecontext:updated"),handler:this.rerender},{watch:"categories[".concat(this.element.dataset.categoryid,"].name:updated"),handler:this.updateName},{watch:"categories:created",handler:this.checkChildList}]}async rerender(_ref){let{element:element}=_ref;const{html:html,js:js}=await _templates.default.renderForPromise("qbank_managecategories/category_details",element.templatecontext);return _templates.default.replaceNodeContents(this.getElement(this.selectors.CONTENT_AREA),html,js)}async createChildList(context){const{html:html,js:js}=await _templates.default.renderForPromise("qbank_managecategories/childlist",context),parentContainer=document.querySelector(this.selectors.CONTENT_CONTAINER(context.categoryid));await _templates.default.appendNodeContents(parentContainer,html,js);const childList=document.querySelector(this.selectors.CHILD_LIST(context.categoryid));return childList.closest(this.selectors.CATEGORY_CONTENTS).classList.add(this.classes.NO_BOTTOM_PADDING),childList}async updatePosition(_ref2){let newParent,{element:element}=_ref2;const originParent=document.querySelector(this.selectors.CHILD_LIST(this.getElement().dataset.parent));if(element.contextid===_categorymanager.categorymanager.state.page.contextid){let previousSibling,nextSibling;var _previousSibling;if(parseInt(this.getElement().dataset.parent)!==element.parent?(newParent=document.querySelector(this.selectors.CHILD_LIST(element.parent)),newParent||(newParent=await this.createChildList({categoryid:element.parent})),this.getElement().dataset.parent=element.parent):newParent=this.getElement().parentElement,newParent.firstElementChild&&parseInt(element.sortorder)<=parseInt(newParent.firstElementChild.dataset.sortorder))nextSibling=newParent.firstElementChild;else previousSibling=newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder-1)),nextSibling=null===(_previousSibling=previousSibling)||void 0===_previousSibling?void 0:_previousSibling.nextElementSibling;(newParent!==this.getElement().parentElement||nextSibling!==this.getElement())&&(nextSibling?newParent.insertBefore(this.getElement(),nextSibling):newParent.appendChild(this.getElement()))}else this.getElement().remove();originParent!==newParent&&this.reactive.stateManager.processUpdates([{name:"categoryLists",action:"put",fields:{id:originParent.dataset.categoryid,childCount:originParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length}},{name:"categoryLists",action:"put",fields:{id:newParent.dataset.categoryid,childCount:newParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length}}]),this.element.dataset.sortorder=element.sortorder;const isDraggable=this.element.classList.contains(this.classes.DRAGHANDLE);isDraggable&&!element.draghandle?(this.element.classList.remove(this.classes.DRAGHANDLE),this.initDragDrop()):!isDraggable&&element.draghandle&&(this.element.classList.add(this.classes.DRAGHANDLE),this.initDragDrop())}getCategoryDataFromElements(element){const categories=[];return element.children&&element.children.forEach((category=>{let child={categoryId:category.dataset.categoryid,parentId:category.dataset.parent,categoryName:category.dataset.categoryname,children:null};const childList=category.querySelector(this.selectors.CATEGORY_LIST);childList&&(child.children=this.getCategoryDataFromElements(childList)),categories.push(child)})),categories}getCategoryDataFromRecords(record){const categories=[];if(record.children)for(const childId in record.children){const category=record.children[childId];let child={categoryId:parseInt(category.id),parentId:parseInt(category.parent),categoryName:category.name,children:null};category.children&&(child.children=this.getCategoryDataFromRecords(category)),categories.push(child)}return categories}createMoveCategoryList(categoryData,movingCategoryId){const categories=[];if(categoryData){let precedingSibling=null;categoryData.forEach((category=>{var _precedingSibling$cat,_precedingSibling2;if(parseInt(category.categoryId)===movingCategoryId)return;let child={movingcategoryid:movingCategoryId,precedingsiblingid:null!==(_precedingSibling$cat=null===(_precedingSibling2=precedingSibling)||void 0===_precedingSibling2?void 0:_precedingSibling2.categoryId)&&void 0!==_precedingSibling$cat?_precedingSibling$cat:0,parent:category.parentId,categoryname:category.categoryName,categories:null};category.children?child.categories=this.createMoveCategoryList(category.children,movingCategoryId):child.categories=[{movingcategoryid:movingCategoryId,precedingsiblingid:0,parent:category.categoryId,categoryname:category.categoryName,categories:null,newchild:!0}],categories.push(child),precedingSibling=category})),precedingSibling&&precedingSibling.categoryId!==movingCategoryId&&categories.push({movingcategoryid:movingCategoryId,precedingsiblingid:precedingSibling.categoryId,parent:precedingSibling.parentId,categoryname:precedingSibling.categoryName,categories:null,lastchild:!0})}return categories}async showMoveModal(e){const item=e.target.closest(this.selectors.MOVE_BUTTON);if(!item)return;if("true"===item.getAttribute("aria-disabled"))return;item.setAttribute("aria-disabled",!0);const contextElement=document.querySelector(this.selectors.CONTEXT),categoryData=this.getCategoryDataFromElements(contextElement),moveContext={contextname:contextElement.dataset.contextname,contextid:contextElement.dataset.contextid,cmid:_categorymanager.categorymanager.state.page.cmid,categories:[],hascategories:!1},movingCategoryId=parseInt(item.dataset.categoryid);moveContext.categories=this.createMoveCategoryList(categoryData,movingCategoryId),moveContext.hascategories=moveContext.categories.length>0;const moveCategory=(0,_str.get_string)("movecategory","qbank_managecategories",item.dataset.categoryname),modal=await _modal.default.create({title:moveCategory,body:_templates.default.render("qbank_managecategories/move_context_list",moveContext),footer:"",show:!0,large:!0}),switcher=new _bank_switcher.default;modal.getBody()[0].addEventListener("click",(async e=>{const categoryItem=e.target.closest(this.selectors.MODAL_CATEGORY_ITEM),moveHeader=e.currentTarget.querySelector(this.selectors.MOVE_BANK_HEADER);if(categoryItem){if(_categorymanager.categorymanager.moveCategory(categoryItem.dataset.movingcategoryid,categoryItem.dataset.parent,categoryItem.dataset.precedingsiblingid),moveHeader.dataset.cmid!==_categorymanager.categorymanager.state.page.cmid){const url=CoreUrl.relativeUrl("/question/bank/managecategories/category.php",{cmid:moveHeader.dataset.cmid}),message=await(0,_str.get_string)("categorymovedto","qbank_managecategories",{url:url,name:moveHeader.textContent});_notification.default.addNotification({message:message,type:"info"})}return void modal.destroy()}if(e.target.closest(this.selectors.SWITCH_QUESTION_BANK)){const pageState=_categorymanager.categorymanager.state.page;try{const contextId=parseInt(contextElement.dataset.contextid);await switcher.show(modal,pageState.courseid,contextId,parseInt(moveHeader.dataset.cmid),pageState.cmid)}catch(ex){_notification.default.exception(ex)}}})),modal.getModal()[0].addEventListener("bankSwitched",(async e=>{try{const params={coursemodule:e.detail.cmid},categoriesResponse=await _fetch.default.performGet("core_question","categories",{params:params}),{context:context,categories:categories}=await categoriesResponse.json();for(const id in categories){const category=categories[id];if(category.parent>0){const parentitem=categories[category.parent];parentitem.hasOwnProperty("children")||(parentitem.children={}),categories[category.parent].children[category.id]=category}}let topCategory;for(const id in categories)0===parseInt(categories[id].parent)&&(topCategory=categories[id]);const categoryData=this.getCategoryDataFromRecords(topCategory),moveContext={contextname:context.prefixedname,contextid:context.prefixedname,cmid:e.detail.cmid,categories:[],hascategories:!1};moveContext.categories=this.createMoveCategoryList(categoryData,movingCategoryId),moveContext.hascategories=moveContext.categories.length>0,modal.setBody(_templates.default.render("qbank_managecategories/move_context_list",moveContext)),await modal.getBodyPromise(),modal.setTitle(moveCategory),modal.setFooter("")}catch(ex){_notification.default.alert((0,_str.get_string)("error","error"),ex)}})),item.setAttribute("aria-disabled",!1)}async checkChildList(_ref3){let{element:element}=_ref3;return element.parent!==this.getElement().dataset.categoryid||this.getElement(this.selectors.CATEGORY_LIST)?null:this.createChildList({categoryid:element.parent,children:[element.templatecontext]})}updateName(_ref4){let{element:element}=_ref4;this.getElement().dataset.categoryname=element.name}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=category.min.js.map \ No newline at end of file diff --git a/public/question/bank/managecategories/amd/build/category.min.js.map b/public/question/bank/managecategories/amd/build/category.min.js.map index 1219d473151..d272472f08c 100644 --- a/public/question/bank/managecategories/amd/build/category.min.js.map +++ b/public/question/bank/managecategories/amd/build/category.min.js.map @@ -1 +1 @@ -{"version":3,"file":"category.min.js","sources":["../src/category.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The category component.\n *\n * @module qbank_managecategories/category\n * @class qbank_managecategories/category\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\nimport Templates from 'core/templates';\nimport Modal from \"core/modal\";\nimport {get_string as getString} from \"core/str\";\nimport {eventTypes as inplaceEditableEventTypes} from 'core/local/inplace_editable/events';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',\n CATEGORY_CONTENTS: '.qbank_managecategories-item > .container',\n EDIT_BUTTON: '[data-action=\"addeditcategory\"]',\n EDITABLE_CATEGORY_NAME: '.inplaceeditable[data-itemtype=\"categoryname\"]',\n MOVE_BUTTON: '[role=\"menuitem\"][data-actiontype=\"move\"]',\n CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',\n MODAL_CATEGORY_ITEM: '.modal_category_item[data-movingcategoryid]',\n CONTENT_AREA: '.qbank_managecategories-details',\n CATEGORY_ID: id => `#category-${id}`,\n CONTENT_CONTAINER: id => `#category-${id} .qbank_managecategories-childlistcontainer`,\n CHILD_LIST: id => `ul[data-categoryid=\"${id}\"]`,\n PREVIOUS_SIBLING: sortorder => `:scope > [data-sortorder=\"${sortorder}\"]`,\n };\n this.classes = {\n NO_BOTTOM_PADDING: 'pb-0',\n DRAGHANDLE: 'draghandle',\n DROPTARGET: 'qbank_managecategories-droptarget-before',\n };\n this.ids = {\n CATEGORY: id => `category-${id}`,\n };\n }\n\n stateReady() {\n this.initDragDrop();\n this.addEventListener(this.getElement(this.selectors.EDIT_BUTTON), 'click', categorymanager.showEditModal);\n const moveButton = this.getElement(this.selectors.MOVE_BUTTON);\n this.addEventListener(moveButton, 'click', this.showMoveModal);\n this.addEventListener(this.getElement(), inplaceEditableEventTypes.elementUpdated, (e) => {\n const editable = e.target.closest(this.selectors.EDITABLE_CATEGORY_NAME);\n categorymanager.updateCategoryName(editable.dataset.itemid, editable.dataset.value);\n });\n }\n\n destroy() {\n // The draggable element must be unregistered.\n this.deInitDragDrop();\n }\n\n /**\n * Remove any existing DragDrop component, and create a new one.\n */\n initDragDrop() {\n this.deInitDragDrop();\n // If the element is currently draggable, register the getDraggableData method.\n if (this.element.classList.contains(this.classes.DRAGHANDLE)) {\n this.getDraggableData = this._getDraggableData;\n }\n this.dragdrop = new DragDrop(this);\n }\n\n /**\n * If the DragDrop component is currently registered, unregister it.\n */\n deInitDragDrop() {\n if (this.dragdrop !== undefined) {\n if (this.getDraggableData !== undefined) {\n this.dragdrop.setDraggable(false);\n this.getDraggableData = undefined;\n }\n this.dragdrop.unregister();\n this.dragdrop = undefined;\n }\n }\n\n /**\n * Static method to create a component instance.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n selectors,\n reactive: categorymanager,\n });\n }\n\n /**\n * Return the category ID from the component's element.\n *\n * This method is referenced as getDraggableData when the component can be dragged.\n *\n * @return {{id: string}}\n * @private\n */\n _getDraggableData() {\n return {\n id: this.getElement().dataset.categoryid\n };\n }\n\n validateDropData() {\n return true;\n }\n\n /**\n * Highlight the top border of the category item.\n *\n * @param {Object} dropData\n */\n showDropZone(dropData) {\n if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) {\n // Can't drop onto itself or its own child.\n return false;\n }\n this.getElement().classList.add(this.classes.DROPTARGET);\n return true;\n }\n\n /**\n * Remove highlighting.\n */\n hideDropZone() {\n this.getElement().classList.remove(this.classes.DROPTARGET);\n }\n\n /**\n * Find the new position of the dropped category, and trigger the move.\n *\n * @param {Object} dropData The category being moved.\n * @param {Event} event The drop event.\n */\n drop(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.CATEGORY_ITEM);\n\n if (!dropTarget) {\n return;\n }\n\n if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {\n // Can't drop onto your own child.\n return;\n }\n\n const source = document.getElementById(this.ids.CATEGORY(dropData.id));\n\n if (!source) {\n return;\n }\n\n const targetParentId = dropTarget.dataset.parent;\n const parentList = dropTarget.closest(this.selectors.CATEGORY_LIST);\n let precedingSibling;\n\n if (dropTarget === parentList.firstElementChild) {\n // Dropped at the top of the list.\n precedingSibling = null;\n } else {\n precedingSibling = dropTarget.previousElementSibling;\n }\n\n // Insert the category after the target category\n categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);\n }\n\n getWatchers() {\n return [\n // After any update to this category, move it to the new position.\n {watch: `categories[${this.element.dataset.categoryid}]:updated`, handler: this.updatePosition},\n // When the template context is added or updated, re-render the content.\n {watch: `categories[${this.element.dataset.categoryid}].templatecontext:created`, handler: this.rerender},\n {watch: `categories[${this.element.dataset.categoryid}].templatecontext:updated`, handler: this.rerender},\n // When the name is updated, update it in the element data set.\n {watch: `categories[${this.element.dataset.categoryid}].name:updated`, handler: this.updateName},\n // When a new category is created, check whether we need to add a child list to this category.\n {watch: `categories:created`, handler: this.checkChildList},\n ];\n }\n\n /**\n * Re-render the category content.\n *\n * @param {Object} args\n * @param {Element} args.element\n * @return {Promise}\n */\n async rerender({element}) {\n const {html, js} = await Templates.renderForPromise(\n 'qbank_managecategories/category_details',\n element.templatecontext\n );\n return Templates.replaceNodeContents(this.getElement(this.selectors.CONTENT_AREA), html, js);\n }\n\n /**\n * Render and append a new child list.\n *\n * @param {Object} context Template context, must include at least categoryid.\n * @return {Promise}\n */\n async createChildList(context) {\n const {html, js} = await Templates.renderForPromise(\n 'qbank_managecategories/childlist',\n context,\n );\n const parentContainer = document.querySelector(this.selectors.CONTENT_CONTAINER(context.categoryid));\n await Templates.appendNodeContents(parentContainer, html, js);\n const childList = document.querySelector(this.selectors.CHILD_LIST(context.categoryid));\n childList.closest(this.selectors.CATEGORY_CONTENTS).classList.add(this.classes.NO_BOTTOM_PADDING);\n return childList;\n }\n\n /**\n * Move a category to its new position.\n *\n * A category may change its parent, sortorder and draghandle independently or at the same time. This method will resolve those\n * changes and move the element to the new position. If the parent doesn't already have a child list, one will be created.\n *\n * If the parent has changed, this will also update the state with the new child count of the old and new parents.\n *\n * @param {Object} args\n * @param {Object} args.element\n * @return {Promise}\n */\n async updatePosition({element}) {\n // Move to a new parent category.\n let newParent;\n const originParent = document.querySelector(this.selectors.CHILD_LIST(this.getElement().dataset.parent));\n if (parseInt(this.getElement().dataset.parent) !== element.parent) {\n newParent = document.querySelector(this.selectors.CHILD_LIST(element.parent));\n if (!newParent) {\n // The target category doesn't have a child list yet. We'd better create one.\n newParent = await this.createChildList({categoryid: element.parent});\n }\n this.getElement().dataset.parent = element.parent;\n } else {\n newParent = this.getElement().parentElement;\n }\n\n // Move to a new position within the parent.\n let previousSibling;\n let nextSibling;\n if (newParent.firstElementChild && parseInt(element.sortorder) <= parseInt(newParent.firstElementChild.dataset.sortorder)) {\n // Move to the top of the list.\n nextSibling = newParent.firstElementChild;\n } else {\n // Move later in the list.\n previousSibling = newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder - 1));\n nextSibling = previousSibling?.nextElementSibling;\n }\n\n // Check if this has actually moved, or if it's just having its sortorder updated due to another element moving.\n const moved = (newParent !== this.getElement().parentElement || nextSibling !== this.getElement());\n\n if (moved) {\n if (nextSibling) {\n // Move to the specified position in the list.\n newParent.insertBefore(this.getElement(), nextSibling);\n } else {\n // Move to the end of the list (may also be the top of the list is empty).\n newParent.appendChild(this.getElement());\n }\n }\n if (originParent !== newParent) {\n // Update child count of old and new parent.\n this.reactive.stateManager.processUpdates([\n {\n name: 'categoryLists',\n action: 'put',\n fields: {\n id: originParent.dataset.categoryid,\n childCount: originParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length\n }\n },\n {\n name: 'categoryLists',\n action: 'put',\n fields: {\n id: newParent.dataset.categoryid,\n childCount: newParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length\n }\n }\n ]);\n }\n\n this.element.dataset.sortorder = element.sortorder;\n\n // Enable/disable dragging.\n const isDraggable = this.element.classList.contains(this.classes.DRAGHANDLE);\n if (isDraggable && !element.draghandle) {\n this.element.classList.remove(this.classes.DRAGHANDLE);\n this.initDragDrop();\n } else if (!isDraggable && element.draghandle) {\n this.element.classList.add(this.classes.DRAGHANDLE);\n this.initDragDrop();\n }\n }\n\n /**\n * Recursively create a list of all valid destinations for a current category within a parent category.\n *\n * @param {Element} item\n * @param {Number} movingCategoryId\n * @return {Array}\n */\n createMoveCategoryList(item, movingCategoryId) {\n const categories = [];\n if (item.children) {\n let precedingSibling = null;\n item.children.forEach(category => {\n const categoryId = parseInt(category.dataset.categoryid);\n // Don't create a target for the category that's moving.\n if (categoryId === movingCategoryId) {\n return;\n }\n // Create a target to move before this child.\n let child = {\n categoryid: categoryId,\n movingcategoryid: movingCategoryId,\n precedingsiblingid: precedingSibling?.dataset.categoryid ?? 0,\n parent: category.dataset.parent,\n categoryname: category.dataset.categoryname,\n categories: null,\n current: categoryId === movingCategoryId,\n };\n const childList = category.querySelector(this.selectors.CATEGORY_LIST);\n if (childList) {\n // If the child has its own children, recursively make a list of those.\n child.categories = this.createMoveCategoryList(childList, movingCategoryId);\n } else {\n // Otherwise, create a target to move as a new child of this one.\n child.categories = [\n {\n movingcategoryid: movingCategoryId,\n precedingsiblingid: 0,\n parent: categoryId,\n categoryname: category.dataset.categoryname,\n categories: null,\n newchild: true,\n }\n ];\n }\n categories.push(child);\n precedingSibling = category;\n });\n if (precedingSibling) {\n const precedingId = parseInt(precedingSibling.dataset.categoryid);\n if (precedingId !== movingCategoryId) {\n // If this is the last child of its parent, also create a target to move the category after this one.\n categories.push({\n movingcategoryid: movingCategoryId,\n precedingsiblingid: precedingId,\n parent: precedingSibling.dataset.parent,\n categoryname: precedingSibling.dataset.categoryname,\n categories: null,\n lastchild: true,\n });\n }\n }\n }\n return categories;\n }\n\n /**\n * Displays a modal containing links to move the category to a new location.\n *\n * @param {Event} e Button click event.\n */\n async showMoveModal(e) {\n // Return if it is not menu item.\n const item = e.target.closest(this.selectors.MOVE_BUTTON);\n if (!item) {\n return;\n }\n // Return if it is disabled.\n if (item.getAttribute('aria-disabled') === 'true') {\n return;\n }\n\n // Prevent addition click on the item.\n item.setAttribute('aria-disabled', true);\n\n // Build the list of move links.\n let moveList = {contexts: []};\n const contexts = document.querySelectorAll(this.selectors.CONTEXT);\n contexts.forEach(context => {\n const moveContext = {\n contextname: context.dataset.contextname,\n categories: [],\n hascategories: false,\n };\n moveContext.categories = this.createMoveCategoryList(context, parseInt(item.dataset.categoryid));\n moveContext.hascategories = moveContext.categories.length > 0;\n moveList.contexts.push(moveContext);\n });\n\n const modal = await Modal.create({\n title: getString('movecategory', 'qbank_managecategories', item.dataset.categoryname),\n body: Templates.render('qbank_managecategories/move_context_list', moveList),\n footer: '',\n show: true,\n large: true,\n });\n // Show modal and add click event for list items.\n modal.getBody()[0].addEventListener('click', e => {\n const target = e.target.closest(this.selectors.MODAL_CATEGORY_ITEM);\n if (!target) {\n return;\n }\n categorymanager.moveCategory(target.dataset.movingcategoryid, target.dataset.parent, target.dataset.precedingsiblingid);\n modal.destroy();\n });\n item.setAttribute('aria-disabled', false);\n }\n\n /**\n * Check and add a child list if needed.\n *\n * Check whether the category that has just been added has this category as its parent. If it does,\n * check that this category has a child list, and if not, add one.\n *\n * @param {Object} args\n * @param {Element} args.element The new category.\n * @return {Promise}\n */\n async checkChildList({element}) {\n if (element.parent !== this.getElement().dataset.categoryid) {\n return null; // Not for me.\n }\n let childList = this.getElement(this.selectors.CATEGORY_LIST);\n if (childList) {\n return null; // List already exists, it will handle adding the new category.\n }\n // Render and add a new child list containing the new category.\n return this.createChildList({\n categoryid: element.parent,\n children: [\n element.templatecontext,\n ]\n });\n }\n\n /**\n * Update the name in the category item element's data, for building move targets.\n *\n * @param {Object} args\n * @param {Object} args.element\n */\n updateName({element}) {\n this.getElement().dataset.categoryname = element.name;\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","CATEGORY_LIST","CATEGORY_ITEM","CATEGORY_CONTENTS","EDIT_BUTTON","EDITABLE_CATEGORY_NAME","MOVE_BUTTON","CONTEXT","MODAL_CATEGORY_ITEM","CONTENT_AREA","CATEGORY_ID","CONTENT_CONTAINER","CHILD_LIST","PREVIOUS_SIBLING","sortorder","classes","NO_BOTTOM_PADDING","DRAGHANDLE","DROPTARGET","ids","CATEGORY","stateReady","initDragDrop","addEventListener","this","getElement","categorymanager","showEditModal","moveButton","showMoveModal","inplaceEditableEventTypes","elementUpdated","e","editable","target","closest","updateCategoryName","dataset","itemid","value","destroy","deInitDragDrop","classList","contains","getDraggableData","_getDraggableData","dragdrop","DragDrop","undefined","setDraggable","unregister","document","querySelector","reactive","categoryid","validateDropData","showDropZone","dropData","add","hideDropZone","remove","drop","event","dropTarget","getElementById","targetParentId","parent","parentList","precedingSibling","firstElementChild","previousElementSibling","moveCategory","_precedingSibling","getWatchers","watch","handler","updatePosition","rerender","updateName","checkChildList","html","js","Templates","renderForPromise","templatecontext","replaceNodeContents","context","parentContainer","appendNodeContents","childList","newParent","originParent","previousSibling","nextSibling","parseInt","createChildList","parentElement","_previousSibling","nextElementSibling","insertBefore","appendChild","stateManager","processUpdates","action","fields","childCount","querySelectorAll","length","isDraggable","draghandle","createMoveCategoryList","item","movingCategoryId","categories","children","forEach","category","categoryId","child","movingcategoryid","precedingsiblingid","_precedingSibling2","categoryname","current","newchild","push","precedingId","lastchild","getAttribute","setAttribute","moveList","contexts","moveContext","contextname","hascategories","modal","Modal","title","body","render","footer","show","large","getBody"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"category.min.js","sources":["../src/category.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The category component.\n *\n * @module qbank_managecategories/category\n * @class qbank_managecategories/category\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\nimport Templates from 'core/templates';\nimport Modal from \"core/modal\";\nimport {get_string as getString} from \"core/str\";\nimport BankSwitcher from 'core_question/bank_switcher';\nimport Fetch from 'core/fetch';\nimport Notification from 'core/notification';\nimport * as CoreUrl from 'core/url';\nimport {eventTypes as inplaceEditableEventTypes} from 'core/local/inplace_editable/events';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',\n CATEGORY_CONTENTS: '.qbank_managecategories-item > .container',\n EDIT_BUTTON: '[data-action=\"addeditcategory\"]',\n EDITABLE_CATEGORY_NAME: '.inplaceeditable[data-itemtype=\"categoryname\"]',\n MOVE_BUTTON: '[role=\"menuitem\"][data-actiontype=\"move\"]',\n CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',\n MODAL_CATEGORY_ITEM: '.modal_category_item[data-movingcategoryid]',\n CONTENT_AREA: '.qbank_managecategories-details',\n CATEGORY_ID: id => `#category-${id}`,\n CONTENT_CONTAINER: id => `#category-${id} .qbank_managecategories-childlistcontainer`,\n CHILD_LIST: id => `ul[data-categoryid=\"${id}\"]`,\n PREVIOUS_SIBLING: sortorder => `:scope > [data-sortorder=\"${sortorder}\"]`,\n SWITCH_QUESTION_BANK: '[data-action=\"switch-question-bank\"]',\n MOVE_BANK_HEADER: '.bank-header',\n };\n this.classes = {\n NO_BOTTOM_PADDING: 'pb-0',\n DRAGHANDLE: 'draghandle',\n DROPTARGET: 'qbank_managecategories-droptarget-before',\n };\n this.ids = {\n CATEGORY: id => `category-${id}`,\n };\n }\n\n stateReady() {\n this.initDragDrop();\n this.addEventListener(this.getElement(this.selectors.EDIT_BUTTON), 'click', categorymanager.showEditModal);\n const moveButton = this.getElement(this.selectors.MOVE_BUTTON);\n this.addEventListener(moveButton, 'click', this.showMoveModal);\n this.addEventListener(this.getElement(), inplaceEditableEventTypes.elementUpdated, (e) => {\n const editable = e.target.closest(this.selectors.EDITABLE_CATEGORY_NAME);\n categorymanager.updateCategoryName(editable.dataset.itemid, editable.dataset.value);\n });\n }\n\n destroy() {\n // The draggable element must be unregistered.\n this.deInitDragDrop();\n }\n\n /**\n * Remove any existing DragDrop component, and create a new one.\n */\n initDragDrop() {\n this.deInitDragDrop();\n // If the element is currently draggable, register the getDraggableData method.\n if (this.element.classList.contains(this.classes.DRAGHANDLE)) {\n this.getDraggableData = this._getDraggableData;\n }\n this.dragdrop = new DragDrop(this);\n }\n\n /**\n * If the DragDrop component is currently registered, unregister it.\n */\n deInitDragDrop() {\n if (this.dragdrop !== undefined) {\n if (this.getDraggableData !== undefined) {\n this.dragdrop.setDraggable(false);\n this.getDraggableData = undefined;\n }\n this.dragdrop.unregister();\n this.dragdrop = undefined;\n }\n }\n\n /**\n * Static method to create a component instance.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n selectors,\n reactive: categorymanager,\n });\n }\n\n /**\n * Return the category ID from the component's element.\n *\n * This method is referenced as getDraggableData when the component can be dragged.\n *\n * @return {{id: string}}\n * @private\n */\n _getDraggableData() {\n return {\n id: this.getElement().dataset.categoryid\n };\n }\n\n validateDropData() {\n return true;\n }\n\n /**\n * Highlight the top border of the category item.\n *\n * @param {Object} dropData\n */\n showDropZone(dropData) {\n if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) {\n // Can't drop onto itself or its own child.\n return false;\n }\n this.getElement().classList.add(this.classes.DROPTARGET);\n return true;\n }\n\n /**\n * Remove highlighting.\n */\n hideDropZone() {\n this.getElement().classList.remove(this.classes.DROPTARGET);\n }\n\n /**\n * Find the new position of the dropped category, and trigger the move.\n *\n * @param {Object} dropData The category being moved.\n * @param {Event} event The drop event.\n */\n drop(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.CATEGORY_ITEM);\n\n if (!dropTarget) {\n return;\n }\n\n if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {\n // Can't drop onto your own child.\n return;\n }\n\n const source = document.getElementById(this.ids.CATEGORY(dropData.id));\n\n if (!source) {\n return;\n }\n\n const targetParentId = dropTarget.dataset.parent;\n const parentList = dropTarget.closest(this.selectors.CATEGORY_LIST);\n let precedingSibling;\n\n if (dropTarget === parentList.firstElementChild) {\n // Dropped at the top of the list.\n precedingSibling = null;\n } else {\n precedingSibling = dropTarget.previousElementSibling;\n }\n\n // Insert the category after the target category\n categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);\n }\n\n getWatchers() {\n return [\n // After any update to this category, move it to the new position.\n {watch: `categories[${this.element.dataset.categoryid}]:updated`, handler: this.updatePosition},\n // When the template context is added or updated, re-render the content.\n {watch: `categories[${this.element.dataset.categoryid}].templatecontext:created`, handler: this.rerender},\n {watch: `categories[${this.element.dataset.categoryid}].templatecontext:updated`, handler: this.rerender},\n // When the name is updated, update it in the element data set.\n {watch: `categories[${this.element.dataset.categoryid}].name:updated`, handler: this.updateName},\n // When a new category is created, check whether we need to add a child list to this category.\n {watch: `categories:created`, handler: this.checkChildList},\n ];\n }\n\n /**\n * Re-render the category content.\n *\n * @param {Object} args\n * @param {Element} args.element\n * @return {Promise}\n */\n async rerender({element}) {\n const {html, js} = await Templates.renderForPromise(\n 'qbank_managecategories/category_details',\n element.templatecontext\n );\n return Templates.replaceNodeContents(this.getElement(this.selectors.CONTENT_AREA), html, js);\n }\n\n /**\n * Render and append a new child list.\n *\n * @param {Object} context Template context, must include at least categoryid.\n * @return {Promise}\n */\n async createChildList(context) {\n const {html, js} = await Templates.renderForPromise(\n 'qbank_managecategories/childlist',\n context,\n );\n const parentContainer = document.querySelector(this.selectors.CONTENT_CONTAINER(context.categoryid));\n await Templates.appendNodeContents(parentContainer, html, js);\n const childList = document.querySelector(this.selectors.CHILD_LIST(context.categoryid));\n childList.closest(this.selectors.CATEGORY_CONTENTS).classList.add(this.classes.NO_BOTTOM_PADDING);\n return childList;\n }\n\n /**\n * Move a category to its new position.\n *\n * A category may change its parent, sortorder and draghandle independently or at the same time. This method will resolve those\n * changes and move the element to the new position. If the parent doesn't already have a child list, one will be created.\n *\n * If the parent has changed, this will also update the state with the new child count of the old and new parents.\n *\n * @param {Object} args\n * @param {Object} args.element\n * @return {Promise}\n */\n async updatePosition({element}) {\n // Move to a new parent category.\n let newParent;\n const originParent = document.querySelector(this.selectors.CHILD_LIST(this.getElement().dataset.parent));\n if (element.contextid === categorymanager.state.page.contextid) {\n if (parseInt(this.getElement().dataset.parent) !== element.parent) {\n newParent = document.querySelector(this.selectors.CHILD_LIST(element.parent));\n if (!newParent) {\n // The target category doesn't have a child list yet. We'd better create one.\n newParent = await this.createChildList({categoryid: element.parent});\n }\n this.getElement().dataset.parent = element.parent;\n } else {\n newParent = this.getElement().parentElement;\n }\n\n // Move to a new position within the parent.\n let previousSibling;\n let nextSibling;\n if (\n newParent.firstElementChild &&\n parseInt(element.sortorder) <= parseInt(newParent.firstElementChild.dataset.sortorder)\n ) {\n // Move to the top of the list.\n nextSibling = newParent.firstElementChild;\n } else {\n // Move later in the list.\n previousSibling = newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder - 1));\n nextSibling = previousSibling?.nextElementSibling;\n }\n\n // Check if this has actually moved, or if it's just having its sortorder updated due to another element moving.\n const moved = (newParent !== this.getElement().parentElement || nextSibling !== this.getElement());\n\n if (moved) {\n if (nextSibling) {\n // Move to the specified position in the list.\n newParent.insertBefore(this.getElement(), nextSibling);\n } else {\n // Move to the end of the list (may also be the top of the list is empty).\n newParent.appendChild(this.getElement());\n }\n }\n } else {\n // The category was moved to a different context, it should no longer appear on this page.\n this.getElement().remove();\n }\n\n if (originParent !== newParent) {\n // Update child count of old and new parent.\n this.reactive.stateManager.processUpdates([\n {\n name: 'categoryLists',\n action: 'put',\n fields: {\n id: originParent.dataset.categoryid,\n childCount: originParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length\n }\n },\n {\n name: 'categoryLists',\n action: 'put',\n fields: {\n id: newParent.dataset.categoryid,\n childCount: newParent.querySelectorAll(this.selectors.CATEGORY_ITEM).length\n }\n }\n ]);\n }\n\n this.element.dataset.sortorder = element.sortorder;\n\n // Enable/disable dragging.\n const isDraggable = this.element.classList.contains(this.classes.DRAGHANDLE);\n if (isDraggable && !element.draghandle) {\n this.element.classList.remove(this.classes.DRAGHANDLE);\n this.initDragDrop();\n } else if (!isDraggable && element.draghandle) {\n this.element.classList.add(this.classes.DRAGHANDLE);\n this.initDragDrop();\n }\n }\n\n /**\n * Create a list of category data from the elements on the page.\n *\n * This will find the category list item elements on the page and extract the category ID, parent ID, and name from the dataset\n * of each element, with any child categories nested underneath. This list can then be passed to createMoveCategoryList to\n * create a tree of move targets.\n *\n * @param {Element} element The category element from the page.\n * @return {Array} List of categories containing categoryId, parentId, categoryName, and a nested array of children.\n */\n getCategoryDataFromElements(element) {\n const categories = [];\n if (element.children) {\n element.children.forEach(category => {\n // Add this category to the list.\n let child = {\n categoryId: category.dataset.categoryid,\n parentId: category.dataset.parent,\n categoryName: category.dataset.categoryname,\n children: null,\n };\n const childList = category.querySelector(this.selectors.CATEGORY_LIST);\n if (childList) {\n // If the child has its own children, recursively make a list of those.\n child.children = this.getCategoryDataFromElements(childList);\n }\n categories.push(child);\n });\n }\n return categories;\n }\n\n /**\n * Get the category data from the records retrieved from the web service.\n *\n * This will process the list of category records and extract the category ID, parent ID, and name from each,\n * with any child categories nested underneath. This list can then be passed to createMoveCategoryList to\n * create a tree of move targets.\n *\n * @param {Object} record The category record.\n * @return {Array} List of categories containing categoryId, parentId, categoryName, and a nested array of children.\n */\n getCategoryDataFromRecords(record) {\n const categories = [];\n if (record.children) {\n for (const childId in record.children) {\n const category = record.children[childId];\n // Add this category to the list.\n let child = {\n categoryId: parseInt(category.id),\n parentId: parseInt(category.parent),\n categoryName: category.name,\n children: null,\n };\n if (category.children) {\n // If the child has its own children, recursively make a list of those.\n child.children = this.getCategoryDataFromRecords(category);\n }\n categories.push(child);\n }\n }\n return categories;\n }\n\n\n /**\n * Recursively create a list of all valid destinations for a current category within a parent category.\n *\n * Each entry in the list represents moving the category \"before\" another category by default, but may also represent\n * moving \"after\" another category, or \"as a new child of\" a parent category.\n *\n * @param {Object} categoryData A list of category data from getCategoryDataFromElements() or getCategoryDataFromRecords().\n * @param {Number} movingCategoryId The ID of the category currently being moved.\n * @return {Array} A list of objects representing valid move targets for the category. Each object has:\n * movingcategoryid - The ID of the category we are moving.\n * precedingsiblingid - The ID of the previous category under the same parent as this target. 0 if this is the first child.\n * parent - The ID of the target category's parent category. 0 if this is the top category.\n * categoryname - The name of the target category, to display as part of the destination.\n * categories - An array of child category targets. If there are no children, this must be null\n * to prevent infinite recursion in the template.\n * newchild - If true, this destination is \"as a new child of the parent\".\n * lastchild - If true, this destination is after the target category, rather than before.\n */\n createMoveCategoryList(categoryData, movingCategoryId) {\n const categories = [];\n if (categoryData) {\n let precedingSibling = null;\n categoryData.forEach(category => {\n // Don't create a target for the category that's moving.\n if (parseInt(category.categoryId) === movingCategoryId) {\n return;\n }\n // Create a target to move before this child.\n let child = {\n movingcategoryid: movingCategoryId,\n precedingsiblingid: precedingSibling?.categoryId ?? 0,\n parent: category.parentId,\n categoryname: category.categoryName,\n categories: null, // Prevent infinite recursion in the template.\n };\n if (category.children) {\n // If the child has its own children, recursively make a list of those.\n child.categories = this.createMoveCategoryList(category.children, movingCategoryId);\n } else {\n // Otherwise, create a target to move as a new child of this one.\n child.categories = [\n {\n movingcategoryid: movingCategoryId,\n precedingsiblingid: 0,\n parent: category.categoryId,\n categoryname: category.categoryName,\n categories: null, // Prevent infinite recursion in the template.\n newchild: true,\n }\n ];\n }\n categories.push(child);\n precedingSibling = category;\n });\n if (precedingSibling) {\n if (precedingSibling.categoryId !== movingCategoryId) {\n // If this is the last child of its parent, also create a target to move the category after this one.\n categories.push({\n movingcategoryid: movingCategoryId,\n precedingsiblingid: precedingSibling.categoryId,\n parent: precedingSibling.parentId,\n categoryname: precedingSibling.categoryName,\n categories: null, // Prevent infinite recursion in the template.\n lastchild: true,\n });\n }\n }\n }\n return categories;\n }\n\n /**\n * Displays a modal containing links to move the category to a new location.\n *\n * @param {Event} e Button click event.\n */\n async showMoveModal(e) {\n // Return if it is not menu item.\n const item = e.target.closest(this.selectors.MOVE_BUTTON);\n if (!item) {\n return;\n }\n // Return if it is disabled.\n if (item.getAttribute('aria-disabled') === 'true') {\n return;\n }\n\n // Prevent addition click on the item.\n item.setAttribute('aria-disabled', true);\n\n // Build the list of move links.\n const contextElement = document.querySelector(this.selectors.CONTEXT);\n const categoryData = this.getCategoryDataFromElements(contextElement);\n\n const moveContext = {\n contextname: contextElement.dataset.contextname,\n contextid: contextElement.dataset.contextid,\n cmid: categorymanager.state.page.cmid,\n categories: [],\n hascategories: false,\n };\n const movingCategoryId = parseInt(item.dataset.categoryid);\n moveContext.categories = this.createMoveCategoryList(categoryData, movingCategoryId);\n moveContext.hascategories = moveContext.categories.length > 0;\n\n const moveCategory = getString('movecategory', 'qbank_managecategories', item.dataset.categoryname);\n const modal = await Modal.create({\n title: moveCategory,\n body: Templates.render('qbank_managecategories/move_context_list', moveContext),\n footer: '',\n show: true,\n large: true,\n });\n const switcher = new BankSwitcher();\n // Show modal and add click event for list items and bank switcher.\n modal.getBody()[0].addEventListener('click', async(e) => {\n const categoryItem = e.target.closest(this.selectors.MODAL_CATEGORY_ITEM);\n const moveHeader = e.currentTarget.querySelector(this.selectors.MOVE_BANK_HEADER);\n if (categoryItem) {\n categorymanager.moveCategory(\n categoryItem.dataset.movingcategoryid,\n categoryItem.dataset.parent,\n categoryItem.dataset.precedingsiblingid,\n );\n if (moveHeader.dataset.cmid !== categorymanager.state.page.cmid) {\n const url = CoreUrl.relativeUrl(\n '/question/bank/managecategories/category.php',\n {cmid: moveHeader.dataset.cmid}\n );\n const message = await getString(\n 'categorymovedto',\n 'qbank_managecategories',\n {url, name: moveHeader.textContent},\n );\n Notification.addNotification({message: message, type: 'info'});\n }\n modal.destroy();\n return;\n }\n const switchButton = e.target.closest(this.selectors.SWITCH_QUESTION_BANK);\n if (switchButton) {\n const pageState = categorymanager.state.page;\n try {\n const contextId = parseInt(contextElement.dataset.contextid);\n await switcher.show(modal, pageState.courseid, contextId, parseInt(moveHeader.dataset.cmid), pageState.cmid);\n } catch (ex) {\n Notification.exception(ex);\n }\n }\n });\n modal.getModal()[0].addEventListener('bankSwitched', async(e) => {\n try {\n const params = {coursemodule: e.detail.cmid};\n const categoriesResponse = await Fetch.performGet(\n 'core_question',\n 'categories',\n {params},\n );\n const {context, categories} = await categoriesResponse.json();\n // Convert the list of categories into a nested tree.\n for (const id in categories) {\n const category = categories[id];\n if (category.parent > 0) {\n const parentitem = categories[category.parent];\n if (!parentitem.hasOwnProperty('children')) {\n parentitem.children = {};\n }\n categories[category.parent].children[category.id] = category;\n }\n }\n // Get the top category with all the others nested below.\n let topCategory;\n for (const id in categories) {\n if (parseInt(categories[id].parent) === 0) {\n topCategory = categories[id];\n }\n }\n const categoryData = this.getCategoryDataFromRecords(topCategory);\n const moveContext = {\n contextname: context.prefixedname,\n contextid: context.prefixedname,\n cmid: e.detail.cmid,\n categories: [],\n hascategories: false,\n };\n moveContext.categories = this.createMoveCategoryList(categoryData, movingCategoryId);\n moveContext.hascategories = moveContext.categories.length > 0;\n modal.setBody(\n Templates.render('qbank_managecategories/move_context_list', moveContext),\n );\n await modal.getBodyPromise();\n modal.setTitle(moveCategory);\n modal.setFooter('');\n } catch (ex) {\n Notification.alert(getString('error', 'error'), ex);\n }\n });\n item.setAttribute('aria-disabled', false);\n }\n\n /**\n * Check and add a child list if needed.\n *\n * Check whether the category that has just been added has this category as its parent. If it does,\n * check that this category has a child list, and if not, add one.\n *\n * @param {Object} args\n * @param {Element} args.element The new category.\n * @return {Promise}\n */\n async checkChildList({element}) {\n if (element.parent !== this.getElement().dataset.categoryid) {\n return null; // Not for me.\n }\n let childList = this.getElement(this.selectors.CATEGORY_LIST);\n if (childList) {\n return null; // List already exists, it will handle adding the new category.\n }\n // Render and add a new child list containing the new category.\n return this.createChildList({\n categoryid: element.parent,\n children: [\n element.templatecontext,\n ]\n });\n }\n\n /**\n * Update the name in the category item element's data, for building move targets.\n *\n * @param {Object} args\n * @param {Object} args.element\n */\n updateName({element}) {\n this.getElement().dataset.categoryname = element.name;\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","CATEGORY_LIST","CATEGORY_ITEM","CATEGORY_CONTENTS","EDIT_BUTTON","EDITABLE_CATEGORY_NAME","MOVE_BUTTON","CONTEXT","MODAL_CATEGORY_ITEM","CONTENT_AREA","CATEGORY_ID","CONTENT_CONTAINER","CHILD_LIST","PREVIOUS_SIBLING","sortorder","SWITCH_QUESTION_BANK","MOVE_BANK_HEADER","classes","NO_BOTTOM_PADDING","DRAGHANDLE","DROPTARGET","ids","CATEGORY","stateReady","initDragDrop","addEventListener","this","getElement","categorymanager","showEditModal","moveButton","showMoveModal","inplaceEditableEventTypes","elementUpdated","e","editable","target","closest","updateCategoryName","dataset","itemid","value","destroy","deInitDragDrop","classList","contains","getDraggableData","_getDraggableData","dragdrop","DragDrop","undefined","setDraggable","unregister","document","querySelector","reactive","categoryid","validateDropData","showDropZone","dropData","add","hideDropZone","remove","drop","event","dropTarget","getElementById","targetParentId","parent","parentList","precedingSibling","firstElementChild","previousElementSibling","moveCategory","_precedingSibling","getWatchers","watch","handler","updatePosition","rerender","updateName","checkChildList","html","js","Templates","renderForPromise","templatecontext","replaceNodeContents","context","parentContainer","appendNodeContents","childList","newParent","originParent","contextid","state","page","previousSibling","nextSibling","parseInt","createChildList","parentElement","_previousSibling","nextElementSibling","insertBefore","appendChild","stateManager","processUpdates","action","fields","childCount","querySelectorAll","length","isDraggable","draghandle","getCategoryDataFromElements","categories","children","forEach","category","child","categoryId","parentId","categoryName","categoryname","push","getCategoryDataFromRecords","record","childId","createMoveCategoryList","categoryData","movingCategoryId","movingcategoryid","precedingsiblingid","_precedingSibling2","newchild","lastchild","item","getAttribute","setAttribute","contextElement","moveContext","contextname","cmid","hascategories","modal","Modal","title","body","render","footer","show","large","switcher","BankSwitcher","getBody","async","categoryItem","moveHeader","currentTarget","url","CoreUrl","relativeUrl","message","textContent","addNotification","type","pageState","contextId","courseid","ex","exception","getModal","params","coursemodule","detail","categoriesResponse","Fetch","performGet","json","parentitem","hasOwnProperty","topCategory","prefixedname","setBody","getBodyPromise","setTitle","setFooter","alert"],"mappings":"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"} \ No newline at end of file diff --git a/public/question/bank/managecategories/amd/build/categorymanager.min.js b/public/question/bank/managecategories/amd/build/categorymanager.min.js index 8d2609c59f4..f095f31e6e0 100644 --- a/public/question/bank/managecategories/amd/build/categorymanager.min.js +++ b/public/question/bank/managecategories/amd/build/categorymanager.min.js @@ -1,3 +1,3 @@ -define("qbank_managecategories/categorymanager",["exports","core/reactive","core/str","qbank_managecategories/mutations","qbank_managecategories/events","core/ajax","core/notification","core_form/modalform"],(function(_exports,_reactive,_str,_mutations,_events,_ajax,_notification,_modalform){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_modalform=_interopRequireDefault(_modalform);const SELECTORS_CATEGORY_LIST=".qbank_managecategories-categorylist",SELECTORS_CONTEXT=".qbank_managecategories-categorylist[data-contextid]",SELECTORS_CATEGORY_ITEM=".qbank_managecategories-item[data-categoryid]",SELECTORS_CATEGORY_ROOT="#categoryroot",SELECTORS_SHOWDESCRIPTIONS_TOGGLE="#showdescriptions-toggle",SELECTORS_ADD_EDIT_BUTTON='[data-action="addeditcategory"]',CLASSES_DRAGHANDLE="draghandle",CLASSES_DANGER="alert-danger";class CategoryManager extends _reactive.Reactive{moveCategory(categoryId,targetParentId){let precedingSiblingId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const call={methodname:"qbank_managecategories_move_category",args:{pagecontextid:this.state.page.contextid,categoryid:categoryId,targetparentid:targetParentId,precedingsiblingid:precedingSiblingId}};_ajax.default.call([call])[0].then((stateUpdates=>(this.stateManager.processUpdates(stateUpdates),stateUpdates))).catch((error=>{var _document$getElements;_notification.default.addNotification({message:error.message,type:"error"}),null===(_document$getElements=document.getElementsByClassName(CLASSES_DANGER)[0])||void 0===_document$getElements||_document$getElements.scrollIntoView()}))}getTitle(isEdit){return(0,_str.get_string)(isEdit?"editcategory":"addcategory","question")}getSave(isEdit){return isEdit?(0,_str.get_string)("savechanges","core"):(0,_str.get_string)("addcategory","question")}showEditModal(e){const addEditButton=e.target.closest(SELECTORS_ADD_EDIT_BUTTON);if(!addEditButton)return;if(!addEditButton.dataset.actiontype)return;e.preventDefault();const title=categorymanager.getTitle("edit"===addEditButton.dataset.actiontype),save=categorymanager.getSave("edit"===addEditButton.dataset.actiontype),cmid=addEditButton.dataset.cmid,courseid=addEditButton.dataset.courseid,questioncount=addEditButton.dataset.questioncount;let contextid=addEditButton.dataset.contextid,categoryid=null,sortorder=null,parent=null;const categoryItem=e.target.closest(SELECTORS_CATEGORY_ITEM);if(categoryItem){contextid=categoryItem.dataset.contextid,categoryid=categoryItem.dataset.categoryid,sortorder=categoryItem.dataset.sortorder;const parentContext=categoryItem.closest(SELECTORS_CONTEXT);parent=categoryItem.dataset.parent+","+parentContext.dataset.contextid}const modalForm=new _modalform.default({formClass:"qbank_managecategories\\form\\question_category_edit_form",args:{cmid:cmid,courseid:courseid,questioncount:questioncount,contextid:contextid,categoryid:categoryid,sortorder:sortorder,parent:parent},modalConfig:{title:title,large:!0},saveButtonText:save,returnFocus:addEditButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(response=>{categorymanager.stateManager.processUpdates(response.detail)})),modalForm.show()}updateCategoryName(categoryId,newName){categorymanager.stateManager.setReadOnly(!1),categorymanager.stateManager.processUpdate("categories","put",{id:categoryId,name:newName}),categorymanager.stateManager.setReadOnly(!0)}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{(async reactive=>{const stateData={page:{contextid:document.querySelector(SELECTORS_CATEGORY_ROOT).dataset.contextid,showdescriptions:document.querySelector(SELECTORS_SHOWDESCRIPTIONS_TOGGLE).checked},categories:[],categoryLists:[]};document.querySelectorAll(SELECTORS_CATEGORY_ITEM).forEach((item=>{stateData.categories.push({id:item.dataset.categoryid,name:item.dataset.categoryname,parent:item.dataset.parent,contextid:item.dataset.contextid,sortorder:item.dataset.sortorder,draghandle:item.classList.contains(CLASSES_DRAGHANDLE)})})),document.querySelectorAll(SELECTORS_CATEGORY_LIST).forEach((categoryList=>{stateData.categoryLists.push({id:categoryList.dataset.categoryid,childCount:categoryList.querySelectorAll(SELECTORS_CATEGORY_ITEM).length})})),reactive.setInitialState(stateData)})(categorymanager)}})); +define("qbank_managecategories/categorymanager",["exports","core/reactive","core/str","qbank_managecategories/mutations","qbank_managecategories/events","core/ajax","core/notification","core_form/modalform"],(function(_exports,_reactive,_str,_mutations,_events,_ajax,_notification,_modalform){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_modalform=_interopRequireDefault(_modalform);const SELECTORS_CATEGORY_LIST=".qbank_managecategories-categorylist",SELECTORS_CONTEXT=".qbank_managecategories-categorylist[data-contextid]",SELECTORS_CATEGORY_ITEM=".qbank_managecategories-item[data-categoryid]",SELECTORS_CATEGORY_ROOT="#categoryroot",SELECTORS_SHOWDESCRIPTIONS_TOGGLE="#showdescriptions-toggle",SELECTORS_ADD_EDIT_BUTTON='[data-action="addeditcategory"]',CLASSES_DRAGHANDLE="draghandle",CLASSES_DANGER="alert-danger";class CategoryManager extends _reactive.Reactive{moveCategory(categoryId,targetParentId){let precedingSiblingId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const call={methodname:"qbank_managecategories_move_category",args:{pagecontextid:this.state.page.contextid,categoryid:categoryId,targetparentid:targetParentId,precedingsiblingid:precedingSiblingId}};_ajax.default.call([call])[0].then((stateUpdates=>(this.stateManager.processUpdates(stateUpdates),stateUpdates))).catch((error=>{var _document$getElements;_notification.default.addNotification({message:error.message,type:"error"}),null===(_document$getElements=document.getElementsByClassName(CLASSES_DANGER)[0])||void 0===_document$getElements||_document$getElements.scrollIntoView()}))}getTitle(isEdit){return(0,_str.get_string)(isEdit?"editcategory":"addcategory","question")}getSave(isEdit){return isEdit?(0,_str.get_string)("savechanges","core"):(0,_str.get_string)("addcategory","question")}showEditModal(e){const addEditButton=e.target.closest(SELECTORS_ADD_EDIT_BUTTON);if(!addEditButton)return;if(!addEditButton.dataset.actiontype)return;e.preventDefault();const title=categorymanager.getTitle("edit"===addEditButton.dataset.actiontype),save=categorymanager.getSave("edit"===addEditButton.dataset.actiontype),cmid=addEditButton.dataset.cmid,courseid=addEditButton.dataset.courseid,questioncount=addEditButton.dataset.questioncount;let contextid=addEditButton.dataset.contextid,categoryid=null,sortorder=null,parent=null;const categoryItem=e.target.closest(SELECTORS_CATEGORY_ITEM);if(categoryItem){contextid=categoryItem.dataset.contextid,categoryid=categoryItem.dataset.categoryid,sortorder=categoryItem.dataset.sortorder;const parentContext=categoryItem.closest(SELECTORS_CONTEXT);parent=categoryItem.dataset.parent+","+parentContext.dataset.contextid}const modalForm=new _modalform.default({formClass:"qbank_managecategories\\form\\question_category_edit_form",args:{cmid:cmid,courseid:courseid,questioncount:questioncount,contextid:contextid,categoryid:categoryid,sortorder:sortorder,parent:parent},modalConfig:{title:title,large:!0},saveButtonText:save,returnFocus:addEditButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(response=>{categorymanager.stateManager.processUpdates(response.detail)})),modalForm.show()}updateCategoryName(categoryId,newName){categorymanager.stateManager.setReadOnly(!1),categorymanager.stateManager.processUpdate("categories","put",{id:categoryId,name:newName}),categorymanager.stateManager.setReadOnly(!0)}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{(async reactive=>{const rootElement=document.querySelector(SELECTORS_CATEGORY_ROOT),stateData={page:{contextid:rootElement.dataset.contextid,courseid:rootElement.dataset.courseid,cmid:rootElement.dataset.cmid,showdescriptions:document.querySelector(SELECTORS_SHOWDESCRIPTIONS_TOGGLE).checked},categories:[],categoryLists:[]};document.querySelectorAll(SELECTORS_CATEGORY_ITEM).forEach((item=>{stateData.categories.push({id:item.dataset.categoryid,name:item.dataset.categoryname,parent:item.dataset.parent,contextid:item.dataset.contextid,sortorder:item.dataset.sortorder,draghandle:item.classList.contains(CLASSES_DRAGHANDLE)})})),document.querySelectorAll(SELECTORS_CATEGORY_LIST).forEach((categoryList=>{stateData.categoryLists.push({id:categoryList.dataset.categoryid,childCount:categoryList.querySelectorAll(SELECTORS_CATEGORY_ITEM).length})})),reactive.setInitialState(stateData)})(categorymanager)}})); //# sourceMappingURL=categorymanager.min.js.map \ No newline at end of file diff --git a/public/question/bank/managecategories/amd/build/categorymanager.min.js.map b/public/question/bank/managecategories/amd/build/categorymanager.min.js.map index 7ee9e843b87..4b4f0d35e62 100644 --- a/public/question/bank/managecategories/amd/build/categorymanager.min.js.map +++ b/public/question/bank/managecategories/amd/build/categorymanager.min.js.map @@ -1 +1 @@ -{"version":3,"file":"categorymanager.min.js","sources":["../src/categorymanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive module for category manager\n *\n * @module qbank_managecategories/categorymanager\n */\n\nimport {Reactive} from 'core/reactive';\nimport {get_string as getString} from 'core/str';\nimport {mutations} from 'qbank_managecategories/mutations';\nimport {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events';\nimport Ajax from \"core/ajax\";\nimport Notification from \"core/notification\";\nimport ModalForm from 'core_form/modalform';\n\nconst SELECTORS = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',\n CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',\n CATEGORY_ROOT: '#categoryroot',\n SHOWDESCRIPTIONS_TOGGLE: '#showdescriptions-toggle',\n ADD_EDIT_BUTTON: '[data-action=\"addeditcategory\"]',\n};\n\nconst CLASSES = {\n DRAGHANDLE: 'draghandle',\n DANGER: 'alert-danger',\n};\n\n/**\n * Load the initial state.\n *\n * This iterates over the initial tree of category items, and captures the data required for the state from each category.\n * It also captures a count of the number of children in each list.\n *\n * @param {Reactive} reactive\n * @return {Promise}\n */\nconst loadState = async(reactive) => {\n const rootElement = document.querySelector(SELECTORS.CATEGORY_ROOT);\n const stateData = {\n page: {\n contextid: rootElement.dataset.contextid,\n showdescriptions: document.querySelector(SELECTORS.SHOWDESCRIPTIONS_TOGGLE).checked,\n },\n categories: [],\n categoryLists: [],\n };\n const listItems = document.querySelectorAll(SELECTORS.CATEGORY_ITEM);\n listItems.forEach(item => {\n stateData.categories.push({\n id: item.dataset.categoryid,\n name: item.dataset.categoryname,\n parent: item.dataset.parent,\n contextid: item.dataset.contextid,\n sortorder: item.dataset.sortorder,\n draghandle: item.classList.contains(CLASSES.DRAGHANDLE),\n });\n });\n const categoryLists = document.querySelectorAll(SELECTORS.CATEGORY_LIST);\n categoryLists.forEach(categoryList => {\n stateData.categoryLists.push({\n id: categoryList.dataset.categoryid,\n childCount: categoryList.querySelectorAll(SELECTORS.CATEGORY_ITEM).length,\n });\n });\n reactive.setInitialState(stateData);\n};\n\n/**\n * Reactive instance for the category manager.\n */\nclass CategoryManager extends Reactive {\n /**\n * Move a category to a new position within the given parent.\n *\n * This will call the move_category web service function to re-order the categories, then update\n * the state with the returned updates.\n *\n * @param {Number} categoryId The ID of the category being moved.\n * @param {Number} targetParentId The ID of the destination parent category (this may not have changed).\n * @param {Number} precedingSiblingId The ID of the category to put the moved category after.\n * This may be null if moving to the top of a list.\n */\n moveCategory(\n categoryId,\n targetParentId,\n precedingSiblingId = null,\n ) {\n const call = {\n methodname: 'qbank_managecategories_move_category',\n args: {\n pagecontextid: this.state.page.contextid,\n categoryid: categoryId,\n targetparentid: targetParentId,\n precedingsiblingid: precedingSiblingId,\n }\n };\n Ajax.call([call])[0]\n .then((stateUpdates) => {\n this.stateManager.processUpdates(stateUpdates);\n return stateUpdates;\n })\n .catch(error => {\n Notification.addNotification({\n message: error.message,\n type: 'error',\n });\n document.getElementsByClassName(CLASSES.DANGER)[0]?.scrollIntoView();\n });\n }\n\n /**\n * Return title for the add/edit modal.\n *\n * @param {boolean} isEdit is 'add' or 'edit' form\n * @returns {String} title string\n */\n getTitle(isEdit) {\n return getString(isEdit ? 'editcategory' : 'addcategory', 'question');\n }\n\n /**\n * Return save button label for the add/edit modal.\n *\n * @param {boolean} isEdit is 'add' or 'edit' form\n * @returns {String} save string\n */\n getSave(isEdit) {\n return isEdit ? getString('savechanges', 'core') : getString('addcategory', 'question');\n }\n\n /**\n * Function handling display of modal form.\n *\n * @param {Event} e The click event triggering the modal.\n */\n showEditModal(e) {\n const addEditButton = e.target.closest(SELECTORS.ADD_EDIT_BUTTON);\n\n // Return if it is not 'addeditcategory' button.\n if (!addEditButton) {\n return;\n }\n\n // Return if the action type is not specified.\n if (!addEditButton.dataset.actiontype) {\n return;\n }\n\n e.preventDefault();\n // Data for the modal.\n const title = categorymanager.getTitle(addEditButton.dataset.actiontype === 'edit');\n const save = categorymanager.getSave(addEditButton.dataset.actiontype === 'edit');\n const cmid = addEditButton.dataset.cmid;\n const courseid = addEditButton.dataset.courseid;\n const questioncount = addEditButton.dataset.questioncount;\n let contextid = addEditButton.dataset.contextid;\n let categoryid = null;\n let sortorder = null;\n let parent = null;\n const categoryItem = e.target.closest(SELECTORS.CATEGORY_ITEM);\n if (categoryItem) {\n contextid = categoryItem.dataset.contextid;\n categoryid = categoryItem.dataset.categoryid;\n sortorder = categoryItem.dataset.sortorder;\n const parentContext = categoryItem.closest(SELECTORS.CONTEXT);\n parent = categoryItem.dataset.parent + ',' + parentContext.dataset.contextid;\n }\n\n // Call the modal.\n const modalForm = new ModalForm({\n formClass: \"qbank_managecategories\\\\form\\\\question_category_edit_form\",\n args: {\n cmid,\n courseid,\n questioncount,\n contextid,\n categoryid,\n sortorder,\n parent,\n },\n modalConfig: {\n title: title,\n large: true,\n },\n saveButtonText: save,\n returnFocus: addEditButton,\n });\n // Once the form has been submitted via the web service, update the state with the new or updated\n // category based on the web service response.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (response) => {\n categorymanager.stateManager.processUpdates(response.detail);\n });\n // Show the form.\n modalForm.show();\n }\n\n updateCategoryName(categoryId, newName) {\n categorymanager.stateManager.setReadOnly(false);\n categorymanager.stateManager.processUpdate('categories', 'put', {id: categoryId, name: newName});\n categorymanager.stateManager.setReadOnly(true);\n }\n}\n\nexport const categorymanager = new CategoryManager({\n name: 'qtype_managecategories_categorymanager',\n eventName: eventTypes.qbankManagecategoriesStateUpdated,\n eventDispatch: notifyQbankManagecategoriesStateUpdated,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n loadState(categorymanager);\n};\n"],"names":["SELECTORS","CLASSES","CategoryManager","Reactive","moveCategory","categoryId","targetParentId","precedingSiblingId","call","methodname","args","pagecontextid","this","state","page","contextid","categoryid","targetparentid","precedingsiblingid","then","stateUpdates","stateManager","processUpdates","catch","error","addNotification","message","type","document","getElementsByClassName","scrollIntoView","getTitle","isEdit","getSave","showEditModal","e","addEditButton","target","closest","dataset","actiontype","preventDefault","title","categorymanager","save","cmid","courseid","questioncount","sortorder","parent","categoryItem","parentContext","modalForm","ModalForm","formClass","modalConfig","large","saveButtonText","returnFocus","addEventListener","events","FORM_SUBMITTED","response","detail","show","updateCategoryName","newName","setReadOnly","processUpdate","id","name","eventName","eventTypes","qbankManagecategoriesStateUpdated","eventDispatch","notifyQbankManagecategoriesStateUpdated","mutations","async","stateData","querySelector","showdescriptions","checked","categories","categoryLists","querySelectorAll","forEach","item","push","categoryname","draghandle","classList","contains","categoryList","childCount","length","reactive","setInitialState","loadState"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"categorymanager.min.js","sources":["../src/categorymanager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Reactive module for category manager\n *\n * @module qbank_managecategories/categorymanager\n */\n\nimport {Reactive} from 'core/reactive';\nimport {get_string as getString} from 'core/str';\nimport {mutations} from 'qbank_managecategories/mutations';\nimport {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events';\nimport Ajax from \"core/ajax\";\nimport Notification from \"core/notification\";\nimport ModalForm from 'core_form/modalform';\n\nconst SELECTORS = {\n CATEGORY_LIST: '.qbank_managecategories-categorylist',\n CONTEXT: '.qbank_managecategories-categorylist[data-contextid]',\n CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',\n CATEGORY_ROOT: '#categoryroot',\n SHOWDESCRIPTIONS_TOGGLE: '#showdescriptions-toggle',\n ADD_EDIT_BUTTON: '[data-action=\"addeditcategory\"]',\n};\n\nconst CLASSES = {\n DRAGHANDLE: 'draghandle',\n DANGER: 'alert-danger',\n};\n\n/**\n * Load the initial state.\n *\n * This iterates over the initial tree of category items, and captures the data required for the state from each category.\n * It also captures a count of the number of children in each list.\n *\n * @param {Reactive} reactive\n * @return {Promise}\n */\nconst loadState = async(reactive) => {\n const rootElement = document.querySelector(SELECTORS.CATEGORY_ROOT);\n const stateData = {\n page: {\n contextid: rootElement.dataset.contextid,\n courseid: rootElement.dataset.courseid,\n cmid: rootElement.dataset.cmid,\n showdescriptions: document.querySelector(SELECTORS.SHOWDESCRIPTIONS_TOGGLE).checked,\n },\n categories: [],\n categoryLists: [],\n };\n const listItems = document.querySelectorAll(SELECTORS.CATEGORY_ITEM);\n listItems.forEach(item => {\n stateData.categories.push({\n id: item.dataset.categoryid,\n name: item.dataset.categoryname,\n parent: item.dataset.parent,\n contextid: item.dataset.contextid,\n sortorder: item.dataset.sortorder,\n draghandle: item.classList.contains(CLASSES.DRAGHANDLE),\n });\n });\n const categoryLists = document.querySelectorAll(SELECTORS.CATEGORY_LIST);\n categoryLists.forEach(categoryList => {\n stateData.categoryLists.push({\n id: categoryList.dataset.categoryid,\n childCount: categoryList.querySelectorAll(SELECTORS.CATEGORY_ITEM).length,\n });\n });\n reactive.setInitialState(stateData);\n};\n\n/**\n * Reactive instance for the category manager.\n */\nclass CategoryManager extends Reactive {\n /**\n * Move a category to a new position within the given parent.\n *\n * This will call the move_category web service function to re-order the categories, then update\n * the state with the returned updates.\n *\n * @param {Number} categoryId The ID of the category being moved.\n * @param {Number} targetParentId The ID of the destination parent category (this may not have changed).\n * @param {Number} precedingSiblingId The ID of the category to put the moved category after.\n * This may be null if moving to the top of a list.\n */\n moveCategory(\n categoryId,\n targetParentId,\n precedingSiblingId = null,\n ) {\n const call = {\n methodname: 'qbank_managecategories_move_category',\n args: {\n pagecontextid: this.state.page.contextid,\n categoryid: categoryId,\n targetparentid: targetParentId,\n precedingsiblingid: precedingSiblingId,\n }\n };\n Ajax.call([call])[0]\n .then((stateUpdates) => {\n this.stateManager.processUpdates(stateUpdates);\n return stateUpdates;\n })\n .catch(error => {\n Notification.addNotification({\n message: error.message,\n type: 'error',\n });\n document.getElementsByClassName(CLASSES.DANGER)[0]?.scrollIntoView();\n });\n }\n\n /**\n * Return title for the add/edit modal.\n *\n * @param {boolean} isEdit is 'add' or 'edit' form\n * @returns {String} title string\n */\n getTitle(isEdit) {\n return getString(isEdit ? 'editcategory' : 'addcategory', 'question');\n }\n\n /**\n * Return save button label for the add/edit modal.\n *\n * @param {boolean} isEdit is 'add' or 'edit' form\n * @returns {String} save string\n */\n getSave(isEdit) {\n return isEdit ? getString('savechanges', 'core') : getString('addcategory', 'question');\n }\n\n /**\n * Function handling display of modal form.\n *\n * @param {Event} e The click event triggering the modal.\n */\n showEditModal(e) {\n const addEditButton = e.target.closest(SELECTORS.ADD_EDIT_BUTTON);\n\n // Return if it is not 'addeditcategory' button.\n if (!addEditButton) {\n return;\n }\n\n // Return if the action type is not specified.\n if (!addEditButton.dataset.actiontype) {\n return;\n }\n\n e.preventDefault();\n // Data for the modal.\n const title = categorymanager.getTitle(addEditButton.dataset.actiontype === 'edit');\n const save = categorymanager.getSave(addEditButton.dataset.actiontype === 'edit');\n const cmid = addEditButton.dataset.cmid;\n const courseid = addEditButton.dataset.courseid;\n const questioncount = addEditButton.dataset.questioncount;\n let contextid = addEditButton.dataset.contextid;\n let categoryid = null;\n let sortorder = null;\n let parent = null;\n const categoryItem = e.target.closest(SELECTORS.CATEGORY_ITEM);\n if (categoryItem) {\n contextid = categoryItem.dataset.contextid;\n categoryid = categoryItem.dataset.categoryid;\n sortorder = categoryItem.dataset.sortorder;\n const parentContext = categoryItem.closest(SELECTORS.CONTEXT);\n parent = categoryItem.dataset.parent + ',' + parentContext.dataset.contextid;\n }\n\n // Call the modal.\n const modalForm = new ModalForm({\n formClass: \"qbank_managecategories\\\\form\\\\question_category_edit_form\",\n args: {\n cmid,\n courseid,\n questioncount,\n contextid,\n categoryid,\n sortorder,\n parent,\n },\n modalConfig: {\n title: title,\n large: true,\n },\n saveButtonText: save,\n returnFocus: addEditButton,\n });\n // Once the form has been submitted via the web service, update the state with the new or updated\n // category based on the web service response.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (response) => {\n categorymanager.stateManager.processUpdates(response.detail);\n });\n // Show the form.\n modalForm.show();\n }\n\n updateCategoryName(categoryId, newName) {\n categorymanager.stateManager.setReadOnly(false);\n categorymanager.stateManager.processUpdate('categories', 'put', {id: categoryId, name: newName});\n categorymanager.stateManager.setReadOnly(true);\n }\n}\n\nexport const categorymanager = new CategoryManager({\n name: 'qtype_managecategories_categorymanager',\n eventName: eventTypes.qbankManagecategoriesStateUpdated,\n eventDispatch: notifyQbankManagecategoriesStateUpdated,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n loadState(categorymanager);\n};\n"],"names":["SELECTORS","CLASSES","CategoryManager","Reactive","moveCategory","categoryId","targetParentId","precedingSiblingId","call","methodname","args","pagecontextid","this","state","page","contextid","categoryid","targetparentid","precedingsiblingid","then","stateUpdates","stateManager","processUpdates","catch","error","addNotification","message","type","document","getElementsByClassName","scrollIntoView","getTitle","isEdit","getSave","showEditModal","e","addEditButton","target","closest","dataset","actiontype","preventDefault","title","categorymanager","save","cmid","courseid","questioncount","sortorder","parent","categoryItem","parentContext","modalForm","ModalForm","formClass","modalConfig","large","saveButtonText","returnFocus","addEventListener","events","FORM_SUBMITTED","response","detail","show","updateCategoryName","newName","setReadOnly","processUpdate","id","name","eventName","eventTypes","qbankManagecategoriesStateUpdated","eventDispatch","notifyQbankManagecategoriesStateUpdated","mutations","async","rootElement","querySelector","stateData","showdescriptions","checked","categories","categoryLists","querySelectorAll","forEach","item","push","categoryname","draghandle","classList","contains","categoryList","childCount","length","reactive","setInitialState","loadState"],"mappings":"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"} \ No newline at end of file diff --git a/public/question/bank/managecategories/amd/src/category.js b/public/question/bank/managecategories/amd/src/category.js index f46fe1f8b21..1c2cb06f344 100644 --- a/public/question/bank/managecategories/amd/src/category.js +++ b/public/question/bank/managecategories/amd/src/category.js @@ -25,6 +25,10 @@ import {categorymanager} from 'qbank_managecategories/categorymanager'; import Templates from 'core/templates'; import Modal from "core/modal"; import {get_string as getString} from "core/str"; +import BankSwitcher from 'core_question/bank_switcher'; +import Fetch from 'core/fetch'; +import Notification from 'core/notification'; +import * as CoreUrl from 'core/url'; import {eventTypes as inplaceEditableEventTypes} from 'core/local/inplace_editable/events'; export default class extends BaseComponent { @@ -45,6 +49,8 @@ export default class extends BaseComponent { CONTENT_CONTAINER: id => `#category-${id} .qbank_managecategories-childlistcontainer`, CHILD_LIST: id => `ul[data-categoryid="${id}"]`, PREVIOUS_SIBLING: sortorder => `:scope > [data-sortorder="${sortorder}"]`, + SWITCH_QUESTION_BANK: '[data-action="switch-question-bank"]', + MOVE_BANK_HEADER: '.bank-header', }; this.classes = { NO_BOTTOM_PADDING: 'pb-0', @@ -254,41 +260,50 @@ export default class extends BaseComponent { // Move to a new parent category. let newParent; const originParent = document.querySelector(this.selectors.CHILD_LIST(this.getElement().dataset.parent)); - if (parseInt(this.getElement().dataset.parent) !== element.parent) { - newParent = document.querySelector(this.selectors.CHILD_LIST(element.parent)); - if (!newParent) { - // The target category doesn't have a child list yet. We'd better create one. - newParent = await this.createChildList({categoryid: element.parent}); - } - this.getElement().dataset.parent = element.parent; - } else { - newParent = this.getElement().parentElement; - } - - // Move to a new position within the parent. - let previousSibling; - let nextSibling; - if (newParent.firstElementChild && parseInt(element.sortorder) <= parseInt(newParent.firstElementChild.dataset.sortorder)) { - // Move to the top of the list. - nextSibling = newParent.firstElementChild; - } else { - // Move later in the list. - previousSibling = newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder - 1)); - nextSibling = previousSibling?.nextElementSibling; - } - - // Check if this has actually moved, or if it's just having its sortorder updated due to another element moving. - const moved = (newParent !== this.getElement().parentElement || nextSibling !== this.getElement()); - - if (moved) { - if (nextSibling) { - // Move to the specified position in the list. - newParent.insertBefore(this.getElement(), nextSibling); + if (element.contextid === categorymanager.state.page.contextid) { + if (parseInt(this.getElement().dataset.parent) !== element.parent) { + newParent = document.querySelector(this.selectors.CHILD_LIST(element.parent)); + if (!newParent) { + // The target category doesn't have a child list yet. We'd better create one. + newParent = await this.createChildList({categoryid: element.parent}); + } + this.getElement().dataset.parent = element.parent; } else { - // Move to the end of the list (may also be the top of the list is empty). - newParent.appendChild(this.getElement()); + newParent = this.getElement().parentElement; } + + // Move to a new position within the parent. + let previousSibling; + let nextSibling; + if ( + newParent.firstElementChild && + parseInt(element.sortorder) <= parseInt(newParent.firstElementChild.dataset.sortorder) + ) { + // Move to the top of the list. + nextSibling = newParent.firstElementChild; + } else { + // Move later in the list. + previousSibling = newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder - 1)); + nextSibling = previousSibling?.nextElementSibling; + } + + // Check if this has actually moved, or if it's just having its sortorder updated due to another element moving. + const moved = (newParent !== this.getElement().parentElement || nextSibling !== this.getElement()); + + if (moved) { + if (nextSibling) { + // Move to the specified position in the list. + newParent.insertBefore(this.getElement(), nextSibling); + } else { + // Move to the end of the list (may also be the top of the list is empty). + newParent.appendChild(this.getElement()); + } + } + } else { + // The category was moved to a different context, it should no longer appear on this page. + this.getElement().remove(); } + if (originParent !== newParent) { // Update child count of old and new parent. this.reactive.stateManager.processUpdates([ @@ -325,45 +340,117 @@ export default class extends BaseComponent { } /** - * Recursively create a list of all valid destinations for a current category within a parent category. + * Create a list of category data from the elements on the page. * - * @param {Element} item - * @param {Number} movingCategoryId - * @return {Array} + * This will find the category list item elements on the page and extract the category ID, parent ID, and name from the dataset + * of each element, with any child categories nested underneath. This list can then be passed to createMoveCategoryList to + * create a tree of move targets. + * + * @param {Element} element The category element from the page. + * @return {Array} List of categories containing categoryId, parentId, categoryName, and a nested array of children. */ - createMoveCategoryList(item, movingCategoryId) { + getCategoryDataFromElements(element) { const categories = []; - if (item.children) { - let precedingSibling = null; - item.children.forEach(category => { - const categoryId = parseInt(category.dataset.categoryid); - // Don't create a target for the category that's moving. - if (categoryId === movingCategoryId) { - return; - } - // Create a target to move before this child. + if (element.children) { + element.children.forEach(category => { + // Add this category to the list. let child = { - categoryid: categoryId, - movingcategoryid: movingCategoryId, - precedingsiblingid: precedingSibling?.dataset.categoryid ?? 0, - parent: category.dataset.parent, - categoryname: category.dataset.categoryname, - categories: null, - current: categoryId === movingCategoryId, + categoryId: category.dataset.categoryid, + parentId: category.dataset.parent, + categoryName: category.dataset.categoryname, + children: null, }; const childList = category.querySelector(this.selectors.CATEGORY_LIST); if (childList) { // If the child has its own children, recursively make a list of those. - child.categories = this.createMoveCategoryList(childList, movingCategoryId); + child.children = this.getCategoryDataFromElements(childList); + } + categories.push(child); + }); + } + return categories; + } + + /** + * Get the category data from the records retrieved from the web service. + * + * This will process the list of category records and extract the category ID, parent ID, and name from each, + * with any child categories nested underneath. This list can then be passed to createMoveCategoryList to + * create a tree of move targets. + * + * @param {Object} record The category record. + * @return {Array} List of categories containing categoryId, parentId, categoryName, and a nested array of children. + */ + getCategoryDataFromRecords(record) { + const categories = []; + if (record.children) { + for (const childId in record.children) { + const category = record.children[childId]; + // Add this category to the list. + let child = { + categoryId: parseInt(category.id), + parentId: parseInt(category.parent), + categoryName: category.name, + children: null, + }; + if (category.children) { + // If the child has its own children, recursively make a list of those. + child.children = this.getCategoryDataFromRecords(category); + } + categories.push(child); + } + } + return categories; + } + + + /** + * Recursively create a list of all valid destinations for a current category within a parent category. + * + * Each entry in the list represents moving the category "before" another category by default, but may also represent + * moving "after" another category, or "as a new child of" a parent category. + * + * @param {Object} categoryData A list of category data from getCategoryDataFromElements() or getCategoryDataFromRecords(). + * @param {Number} movingCategoryId The ID of the category currently being moved. + * @return {Array} A list of objects representing valid move targets for the category. Each object has: + * movingcategoryid - The ID of the category we are moving. + * precedingsiblingid - The ID of the previous category under the same parent as this target. 0 if this is the first child. + * parent - The ID of the target category's parent category. 0 if this is the top category. + * categoryname - The name of the target category, to display as part of the destination. + * categories - An array of child category targets. If there are no children, this must be null + * to prevent infinite recursion in the template. + * newchild - If true, this destination is "as a new child of the parent". + * lastchild - If true, this destination is after the target category, rather than before. + */ + createMoveCategoryList(categoryData, movingCategoryId) { + const categories = []; + if (categoryData) { + let precedingSibling = null; + categoryData.forEach(category => { + // Don't create a target for the category that's moving. + if (parseInt(category.categoryId) === movingCategoryId) { + return; + } + // Create a target to move before this child. + let child = { + movingcategoryid: movingCategoryId, + precedingsiblingid: precedingSibling?.categoryId ?? 0, + parent: category.parentId, + categoryname: category.categoryName, + categories: null, // Prevent infinite recursion in the template. + }; + if (category.children) { + // If the child has its own children, recursively make a list of those. + child.categories = this.createMoveCategoryList(category.children, movingCategoryId); } else { // Otherwise, create a target to move as a new child of this one. child.categories = [ { movingcategoryid: movingCategoryId, precedingsiblingid: 0, - parent: categoryId, - categoryname: category.dataset.categoryname, - categories: null, + parent: category.categoryId, + categoryname: category.categoryName, + categories: null, // Prevent infinite recursion in the template. newchild: true, } ]; @@ -372,15 +459,14 @@ export default class extends BaseComponent { precedingSibling = category; }); if (precedingSibling) { - const precedingId = parseInt(precedingSibling.dataset.categoryid); - if (precedingId !== movingCategoryId) { + if (precedingSibling.categoryId !== movingCategoryId) { // If this is the last child of its parent, also create a target to move the category after this one. categories.push({ movingcategoryid: movingCategoryId, - precedingsiblingid: precedingId, - parent: precedingSibling.dataset.parent, - categoryname: precedingSibling.dataset.categoryname, - categories: null, + precedingsiblingid: precedingSibling.categoryId, + parent: precedingSibling.parentId, + categoryname: precedingSibling.categoryName, + categories: null, // Prevent infinite recursion in the template. lastchild: true, }); } @@ -409,34 +495,111 @@ export default class extends BaseComponent { item.setAttribute('aria-disabled', true); // Build the list of move links. - let moveList = {contexts: []}; - const contexts = document.querySelectorAll(this.selectors.CONTEXT); - contexts.forEach(context => { - const moveContext = { - contextname: context.dataset.contextname, - categories: [], - hascategories: false, - }; - moveContext.categories = this.createMoveCategoryList(context, parseInt(item.dataset.categoryid)); - moveContext.hascategories = moveContext.categories.length > 0; - moveList.contexts.push(moveContext); - }); + const contextElement = document.querySelector(this.selectors.CONTEXT); + const categoryData = this.getCategoryDataFromElements(contextElement); + const moveContext = { + contextname: contextElement.dataset.contextname, + contextid: contextElement.dataset.contextid, + cmid: categorymanager.state.page.cmid, + categories: [], + hascategories: false, + }; + const movingCategoryId = parseInt(item.dataset.categoryid); + moveContext.categories = this.createMoveCategoryList(categoryData, movingCategoryId); + moveContext.hascategories = moveContext.categories.length > 0; + + const moveCategory = getString('movecategory', 'qbank_managecategories', item.dataset.categoryname); const modal = await Modal.create({ - title: getString('movecategory', 'qbank_managecategories', item.dataset.categoryname), - body: Templates.render('qbank_managecategories/move_context_list', moveList), + title: moveCategory, + body: Templates.render('qbank_managecategories/move_context_list', moveContext), footer: '', show: true, large: true, }); - // Show modal and add click event for list items. - modal.getBody()[0].addEventListener('click', e => { - const target = e.target.closest(this.selectors.MODAL_CATEGORY_ITEM); - if (!target) { + const switcher = new BankSwitcher(); + // Show modal and add click event for list items and bank switcher. + modal.getBody()[0].addEventListener('click', async(e) => { + const categoryItem = e.target.closest(this.selectors.MODAL_CATEGORY_ITEM); + const moveHeader = e.currentTarget.querySelector(this.selectors.MOVE_BANK_HEADER); + if (categoryItem) { + categorymanager.moveCategory( + categoryItem.dataset.movingcategoryid, + categoryItem.dataset.parent, + categoryItem.dataset.precedingsiblingid, + ); + if (moveHeader.dataset.cmid !== categorymanager.state.page.cmid) { + const url = CoreUrl.relativeUrl( + '/question/bank/managecategories/category.php', + {cmid: moveHeader.dataset.cmid} + ); + const message = await getString( + 'categorymovedto', + 'qbank_managecategories', + {url, name: moveHeader.textContent}, + ); + Notification.addNotification({message: message, type: 'info'}); + } + modal.destroy(); return; } - categorymanager.moveCategory(target.dataset.movingcategoryid, target.dataset.parent, target.dataset.precedingsiblingid); - modal.destroy(); + const switchButton = e.target.closest(this.selectors.SWITCH_QUESTION_BANK); + if (switchButton) { + const pageState = categorymanager.state.page; + try { + const contextId = parseInt(contextElement.dataset.contextid); + await switcher.show(modal, pageState.courseid, contextId, parseInt(moveHeader.dataset.cmid), pageState.cmid); + } catch (ex) { + Notification.exception(ex); + } + } + }); + modal.getModal()[0].addEventListener('bankSwitched', async(e) => { + try { + const params = {coursemodule: e.detail.cmid}; + const categoriesResponse = await Fetch.performGet( + 'core_question', + 'categories', + {params}, + ); + const {context, categories} = await categoriesResponse.json(); + // Convert the list of categories into a nested tree. + for (const id in categories) { + const category = categories[id]; + if (category.parent > 0) { + const parentitem = categories[category.parent]; + if (!parentitem.hasOwnProperty('children')) { + parentitem.children = {}; + } + categories[category.parent].children[category.id] = category; + } + } + // Get the top category with all the others nested below. + let topCategory; + for (const id in categories) { + if (parseInt(categories[id].parent) === 0) { + topCategory = categories[id]; + } + } + const categoryData = this.getCategoryDataFromRecords(topCategory); + const moveContext = { + contextname: context.prefixedname, + contextid: context.prefixedname, + cmid: e.detail.cmid, + categories: [], + hascategories: false, + }; + moveContext.categories = this.createMoveCategoryList(categoryData, movingCategoryId); + moveContext.hascategories = moveContext.categories.length > 0; + modal.setBody( + Templates.render('qbank_managecategories/move_context_list', moveContext), + ); + await modal.getBodyPromise(); + modal.setTitle(moveCategory); + modal.setFooter(''); + } catch (ex) { + Notification.alert(getString('error', 'error'), ex); + } }); item.setAttribute('aria-disabled', false); } diff --git a/public/question/bank/managecategories/amd/src/categorymanager.js b/public/question/bank/managecategories/amd/src/categorymanager.js index 5c0f4ad2983..3151238ca4e 100644 --- a/public/question/bank/managecategories/amd/src/categorymanager.js +++ b/public/question/bank/managecategories/amd/src/categorymanager.js @@ -55,6 +55,8 @@ const loadState = async(reactive) => { const stateData = { page: { contextid: rootElement.dataset.contextid, + courseid: rootElement.dataset.courseid, + cmid: rootElement.dataset.cmid, showdescriptions: document.querySelector(SELECTORS.SHOWDESCRIPTIONS_TOGGLE).checked, }, categories: [], diff --git a/public/question/bank/managecategories/category.php b/public/question/bank/managecategories/category.php index 33314dbad4b..cc72c132028 100644 --- a/public/question/bank/managecategories/category.php +++ b/public/question/bank/managecategories/category.php @@ -45,8 +45,6 @@ if ($courseid = optional_param('courseid', 0, PARAM_INT)) { list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) = question_edit_setup('categories', '/question/bank/managecategories/category.php'); -$thiscontext = context_module::instance($cmid)->id; - $todelete = optional_param('delete', 0, PARAM_INT); // The ID of a category to delete. $PAGE->set_url($thispageurl); @@ -110,16 +108,10 @@ if ($questionstomove) { echo $OUTPUT->box(get_string('categorymove', 'question', $vars), 'generalbox boxaligncenter'); $moveform->display(); } else { - // Get module contexts we have capabilities to manage. - $contextswithcaps = $contexts->having_one_edit_tab_cap('categories'); - $modcontexts = array_filter($contextswithcaps, static fn ($context) => $context->contextlevel === CONTEXT_MODULE); // Display the user interface. $questioncategories = new question_categories( $thispageurl, - $modcontexts, - $cmid, - $courseid, - $thiscontext, + cmid: $cmid, ); $PAGE->requires->js_call_amd('qbank_managecategories/categorymanager', 'init'); // Load reactive module. echo $OUTPUT->render(new categories_header($questioncategories)); diff --git a/public/question/bank/managecategories/classes/external/move_category.php b/public/question/bank/managecategories/classes/external/move_category.php index 93f4b15811e..398c088aa28 100644 --- a/public/question/bank/managecategories/classes/external/move_category.php +++ b/public/question/bank/managecategories/classes/external/move_category.php @@ -72,7 +72,7 @@ class move_category extends external_api { 'id' => new external_value(PARAM_INT, 'The ID of the category that was updated.'), 'sortorder' => new external_value(PARAM_INT, 'The new sortorder', VALUE_OPTIONAL), 'parent' => new external_value(PARAM_INT, 'The ID of the new parent category.', VALUE_OPTIONAL), - 'context' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL), + 'contextid' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL), 'draghandle' => new external_value( PARAM_BOOL, 'Should this category have a drag handle?', @@ -151,7 +151,7 @@ class move_category extends external_api { ) { $transaction->rollback(new moodle_exception('idnumberexists', 'qbank_managecategories')); } - $originstateupdate->fields->context = $targetparent->contextid; + $originstateupdate->fields->contextid = $targetparent->contextid; } // Update sort order. @@ -187,7 +187,10 @@ class move_category extends external_api { $toupdatesortorder = $DB->get_records_select('question_categories', $select, $params, $sort); foreach ($toupdatesortorder as $category) { $DB->set_field('question_categories', 'sortorder', ++$sortorder, ['id' => $category->id]); - $stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder); + if ($targetparent->contextid == $pagecontextid) { + // If we're updating other categories in the current context, return state updates for those too. + $stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder); + } } if (isset($originstateupdate->fields->parent)) { diff --git a/public/question/bank/managecategories/classes/form/question_category_edit_form.php b/public/question/bank/managecategories/classes/form/question_category_edit_form.php index c1c6a24ed1a..a46eeef41aa 100644 --- a/public/question/bank/managecategories/classes/form/question_category_edit_form.php +++ b/public/question/bank/managecategories/classes/form/question_category_edit_form.php @@ -68,28 +68,14 @@ class question_category_edit_form extends \core_form\dynamic_form { protected function get_current_data() { // If categoryid is set, we are editing an existing category. $currentcat = isset($this->_ajaxformdata['categoryid']) ? (int)$this->_ajaxformdata['categoryid'] : 0; - // Determine the context based on the provided IDs. + // Determine the context based on the provided ID. $cmid = isset($this->_ajaxformdata['cmid']) ? (int)$this->_ajaxformdata['cmid'] : 0; $courseid = isset($this->_ajaxformdata['courseid']) ? (int)$this->_ajaxformdata['courseid'] : 0; if ($cmid !== 0) { $thiscontext = context_module::instance($cmid); } - if ($courseid !== 0) { - $thiscontext = context_course::instance($courseid); - } - - if ($courseid === 0 && $cmid === 0) { - $parentcontext = (int)explode(',', $this->_ajaxformdata['parent'])[1]; - $contextid = $parentcontext === 0 ? $this->_ajaxformdata['contextid'] : (int)$parentcontext; - $thiscontext = context::instance_by_id($contextid); - } - - if ($thiscontext) { - $contexts = new question_edit_contexts($thiscontext); - $contexts = $contexts->all(); - } - return [$currentcat, $cmid, $courseid, $thiscontext, $contexts]; + return [$currentcat, $cmid, $courseid, $thiscontext, [$thiscontext]]; } /** diff --git a/public/question/bank/managecategories/classes/output/categories.php b/public/question/bank/managecategories/classes/output/categories.php index 66b60f2ddcf..f978389865c 100644 --- a/public/question/bank/managecategories/classes/output/categories.php +++ b/public/question/bank/managecategories/classes/output/categories.php @@ -48,31 +48,27 @@ class categories implements renderable, templatable { #[\Override] public function export_for_template(renderer_base $output): array { $categories = []; - foreach ($this->categories->editlists as $contextid => $list) { - // Get list elements. - $context = context::instance_by_id($contextid); - $itemstab = []; - if (count($list->items)) { - foreach ($list->items as $item) { - $category = new category($item, $context); - $itemstab['items'][] = $category->export_for_template($output); - } + $list = $this->categories->editlist; + // Get list elements. + $itemstab = []; + if (count($list->items)) { + foreach ($list->items as $item) { + $category = new category($item, $list->context); + $itemstab['items'][] = $category->export_for_template($output); } - if (isset($itemstab['items'])) { - $ctxlvl = "contextlevel" . $list->context->contextlevel; - $contextname = $list->context->get_context_name(); - $heading = get_string('questioncatsfor', 'question', $contextname); + } + if (isset($itemstab['items'])) { + $contextname = $list->context->get_context_name(); + $heading = get_string('questioncatsfor', 'question', $contextname); - // Get categories context. - $categories[] = [ - 'ctxlvl' => $ctxlvl, - 'contextid' => $list->context->id, - 'contextname' => $contextname, - 'heading' => $heading, - 'items' => $itemstab['items'], - 'categoryid' => $list->categoryid, - ]; - } + // Get categories context. + $categories[] = [ + 'contextid' => $list->context->id, + 'contextname' => $contextname, + 'heading' => $heading, + 'items' => $itemstab['items'], + 'categoryid' => $list->categoryid, + ]; } $data = [ 'categoriesrendered' => $categories, diff --git a/public/question/bank/managecategories/classes/question_categories.php b/public/question/bank/managecategories/classes/question_categories.php index 55e588818d7..3211b168677 100644 --- a/public/question/bank/managecategories/classes/question_categories.php +++ b/public/question/bank/managecategories/classes/question_categories.php @@ -17,6 +17,8 @@ namespace qbank_managecategories; use context; +use core\context\module; +use core\exception\coding_exception; use moodle_url; /** @@ -51,65 +53,91 @@ class question_categories { /** * @var array An array containing a tree of categories for each context. */ + #[\core\attribute\deprecated( + 'editlist', + 5.2, + 'Multiple contexts are no longer supported.', + 'MDL-87264' + )] public array $editlists; + /** + * @var \stdClass The tree of categories for the current context. + */ + public \stdClass $editlist; + /** * Constructor. * * @param moodle_url $pageurl base URL of the display categories page. Used for redirects. - * @param context[] $contexts contexts where the current user can edit categories. + * @param context[] $contexts Deprecated since Moodle 5.2, do not use anymore. * @param ?int $cmid course module id for the current page. - * @param ?int $courseid course id for the current page. - * @param ?int $thiscontext The context ID of the current page. + * @param ?int $courseid Deprecated since Moodle 5.2, do not use anymore. + * @param ?int $thiscontext Deprecated since Moodle 5.2, do not use anymore. */ public function __construct( moodle_url $pageurl, - array $contexts, + ?array $contexts = null, ?int $cmid = null, ?int $courseid = null, ?int $thiscontext = null, ) { global $DB; + if (!is_null($contexts)) { + debugging( + 'The contexts argument has been deprecated. Multiple contexts are no longer supported. Please remove the ' + . 'argument from your calls.', + DEBUG_DEVELOPER, + ); + } + if (!is_null($courseid)) { + debugging( + 'The courseid argument has been deprecated. The course will be found from the cmid. Please remove the ' + . 'argument from your calls.', + DEBUG_DEVELOPER, + ); + } + if (!is_null($thiscontext)) { + debugging( + 'The thiscontext argument has been deprecated. The context will be found from the cmid. Please remove the ' + . 'argument from your calls.', + DEBUG_DEVELOPER, + ); + } $this->cmid = $cmid; - $this->courseid = $courseid; + $context = module::instance($this->cmid); + + [$course] = get_course_and_cm_from_cmid($cmid); + $this->courseid = $course->id; $this->pageurl = $pageurl; - $this->contextid = $thiscontext; + $this->contextid = $context->id; - $contextids = array_map(fn($context) => $context->id, $contexts); - [$insql, $params] = $DB->get_in_or_equal($contextids); - $topcategories = $DB->get_records_select_menu( + $topcategory = $DB->get_record( 'question_categories', - 'parent = 0 AND contextid ' . $insql, - $params, - fields: 'contextid, id' + ['parent' => 0, 'contextid' => $this->contextid], ); - foreach ($contexts as $context) { - // We can only have categories in module context. - if ($context->contextlevel !== CONTEXT_MODULE) { - continue; + + $items = helper::get_categories_for_contexts($context->id); + // Create an ordered tree with children correctly nested under parents. + foreach ($items as $item) { + if (array_key_exists((int) $item->parent, $items)) { + $item->parentitem = $items[$item->parent]; + $items[$item->parent]->children[$item->id] = $item; } - $items = helper::get_categories_for_contexts($context->id); - // Create an ordered tree with children correctly nested under parents. - foreach ($items as $item) { - if (array_key_exists((int) $item->parent, $items)) { - $item->parentitem = $items[$item->parent]; - $items[$item->parent]->children[$item->id] = $item; - } - } - foreach ($items as $item) { - if (isset($item->children)) { - foreach ($item->children as $children) { - unset($items[$children->id]); - } - } - } - $this->editlists[$context->id] = (object) [ - 'items' => $items, - 'context' => $context, - 'categoryid' => $topcategories[$context->id], - ]; } + foreach ($items as $item) { + if (isset($item->children)) { + foreach ($item->children as $children) { + unset($items[$children->id]); + } + } + } + $this->editlist = (object) [ + 'items' => $items, + 'context' => $context, + 'categoryid' => $topcategory->id, + ]; } } diff --git a/public/question/bank/managecategories/lang/en/qbank_managecategories.php b/public/question/bank/managecategories/lang/en/qbank_managecategories.php index dbc2ba4bd6e..00b849eeab4 100644 --- a/public/question/bank/managecategories/lang/en/qbank_managecategories.php +++ b/public/question/bank/managecategories/lang/en/qbank_managecategories.php @@ -27,6 +27,7 @@ $string['after'] = 'After {$a}'; $string['before'] = 'Before {$a}'; $string['categoryincategory'] = 'Cannot move category in same category.'; $string['categorymoved'] = 'The category has been moved successfully.'; +$string['categorymovedto'] = 'Category moved to {$a->name}.'; $string['confirmdelete'] = 'Delete category "{$a}"?. Any child categories will be moved to this category\'s parent.'; $string['descriptionnotdisplayed'] = 'Categories description not displayed'; $string['displaydescription'] = 'Categories description displayed'; diff --git a/public/question/bank/managecategories/templates/categories.mustache b/public/question/bank/managecategories/templates/categories.mustache index 88ab99e86c0..0365af5e4a2 100644 --- a/public/question/bank/managecategories/templates/categories.mustache +++ b/public/question/bank/managecategories/templates/categories.mustache @@ -21,6 +21,8 @@ Context variables required for this template: * contextid - Context id for js init. + * courseid - The course the current module context sits within. + * cmid - The module the context belongs to. * categoriesrendered - Array representing categories data rendered. * items - Categories data. * heading - Category's heading for each context (ie: Course or Quiz). @@ -29,6 +31,8 @@ Example context (json): { "contextid": "18", + "courseid": "2", + "cmid": "10", "categoriesrendered": [{ "items": [{ "categoryid": "1", @@ -43,17 +47,19 @@ "iconright": "", "children": "" }], - "heading": "Question categories for 'Quiz: qz'", - "ctxlvl": "contextlevel70" + "heading": "Question categories for 'Quiz: qz'" }] } }}
+ data-contextid="{{{contextid}}}" + data-courseid="{{courseid}}" + data-cmid="{{cmid}}" + > {{#categoriesrendered}}

{{heading}}

-
+
    {{#items}} diff --git a/public/question/bank/managecategories/templates/move_category_list.mustache b/public/question/bank/managecategories/templates/move_category_list.mustache index 9004544e1ec..212f7cb34b6 100644 --- a/public/question/bank/managecategories/templates/move_category_list.mustache +++ b/public/question/bank/managecategories/templates/move_category_list.mustache @@ -24,56 +24,94 @@ Context variables required for this template: * categories - Array of category objects. - * categoryname - The name of the category to move to. - * categoryid - The ID of the category. - * firstchild - Is this the first child of its parent? If so, a "before" option will be rendered as well as "after". - * current - Is this the category being moved? If so, skip rendering it as a target. + * categoryid - The ID of the category to move relative to. + * movingcategoryid - The ID of the category we are moving. + * categoryname - The name of the category to move relative to. + * lastchild - Is this the last child of its parent? If true, an "after X" option will be rendered. + * precedingsiblingid - The ID of the preceding category under the same parent. + * parent - The ID of the parent category. + * newchild - If true, render this as a "new child of X" option rather than "before X". * categories - Array containing child categories of this category. - If there are none, this must be set null or [] to avoid infinite recursion. + If there are none, this must be to [] to avoid infinite recursion. Example context (json): { "categories": [ { - "categoryname": "Default category for course 1", - "categoryid": 3, - "firstchild": true, - "current": false, - "hascategories": false, - "categories": [] - }, - { - "categoryname": "Test category 1", - "categoryid": 4, - "firstchild": false, - "current": false, - "hascategories": false, - "categories": [] - }, - { - "categoryname": "Test category 2", - "categoryid": 5, - "firstchild": false, - "current": true, - "hascategories": false, - "categories": [] - }, - { - "categoryname": "Test category 3 x < 1 && y > 2 ", - "categoryid": 6, - "firstchild": false, - "current": false, - "hascategories": true, + "categoryid": "154001", + "movingcategoryid": 154007, + "precedingsiblingid": 0, + "parent": "154000", + "categoryname": "Default for Qbank 1", "categories": [ { - "categoryname": "Test category 4", - "categoryid": 7, - "firstchild": true, - "current": false, - "hascategories": false, - "categories": [] + "movingcategoryid": 154007, + "precedingsiblingid": 0, + "parent": "154001", + "categoryname": "Default for Qbank 1", + "categories": [], + "newchild": true } ] + }, + { + "categoryid": "154004", + "movingcategoryid": 154007, + "precedingsiblingid": "154001", + "parent": "154000", + "categoryname": "Course category 1", + "categories": [ + { + "categoryid": "154006", + "movingcategoryid": 154007, + "precedingsiblingid": 0, + "parent": "154004", + "categoryname": "Course category 3", + "categories": [ + { + "movingcategoryid": 154007, + "precedingsiblingid": 0, + "parent": "154006", + "categoryname": "Course category 3", + "categories": [], + "newchild": true + } + ] + }, + { + "movingcategoryid": 154007, + "precedingsiblingid": "154006", + "parent": "154004", + "categoryname": "Course category 3", + "categories": [], + "lastchild": true + } + ] + }, + { + "categoryid": "154005", + "movingcategoryid": 154007, + "precedingsiblingid": "154004", + "parent": "154000", + "categoryname": "Course category 2", + "categories": [ + { + "movingcategoryid": 154007, + "precedingsiblingid": 0, + "parent": "154005", + "categoryname": "Course category 2", + "categories": [], + "newchild": true + } + ] + }, + { + "movingcategoryid": 154007, + "precedingsiblingid": "154005", + "parent": "154000", + "categoryname": "Course category 2", + "categories": [], + "lastchild": true } ] } diff --git a/public/question/bank/managecategories/templates/move_context_list.mustache b/public/question/bank/managecategories/templates/move_context_list.mustache index 69abdabe2f8..467750fb4be 100644 --- a/public/question/bank/managecategories/templates/move_context_list.mustache +++ b/public/question/bank/managecategories/templates/move_context_list.mustache @@ -17,67 +17,44 @@ {{! @template qbank_managecategories/move_context_list - Modal to move a category. + Modal to move a category within a context. Context variables required for this template: - * contexts - Array of contexts containing question categories - * contextid - The id of the context. - * contextname - The name of the context to display as a heading. - * categories - Array of category objects. See qbank_managecategories/move_category_list partial for details. + * contextid - The id of the context. + * contextname - The name of the context to display as a heading. + * categories - Array of category objects. See qbank_managecategories/move_category_list partial for details. Example context (json): { - "contexts": [ + "contexid": 1, + "cmid": 1, + "contextname": "Question bank: Qbank 1", + "categories": [ { - "contexid": 1, - "contextname": "Course 1", - "categories": [ - { - "categoryname": "Default category for course 1", - "categoryid": 3, - "firstchild": true, - "current": false, - "hascategories": false, - "categories": [] - }, - { - "categoryname": "Default category for course category 1", - "categoryid": 4, - "firstchild": false, - "current": true, - "hascategories": false, - "categories": [] - } - ] + "categoryname": "Default category for Qbank 1", + "categoryid": 3, + "firstchild": true, + "current": false, + "hascategories": false, + "categories": [] }, { - "contexid": 3, - "contextname": "Course 2", - "categories": [ - { - "categoryname": "Default category for course 2", - "categoryid": 5, - "firstchild": true, - "current": false, - "hascategories": false, - "categories": [] - }, - { - "categoryname": "Default category for course category 2", - "categoryid": 6, - "firstchild": false, - "current": false, - "hascategories": false, - "categories": [] - } - ] + "categoryname": "New category", + "categoryid": 4, + "firstchild": false, + "current": true, + "hascategories": false, + "categories": [] } ] } }}
    - {{#contexts}} -

    {{{contextname}}}

    - {{>qbank_managecategories/move_category_list}} - {{/contexts}} +
    +

    {{{contextname}}}

    + +
    + {{>qbank_managecategories/move_category_list}}
    diff --git a/public/question/bank/managecategories/tests/behat/move_question_categories.feature b/public/question/bank/managecategories/tests/behat/move_question_categories.feature index cb340a8289d..08fc8343f25 100644 --- a/public/question/bank/managecategories/tests/behat/move_question_categories.feature +++ b/public/question/bank/managecategories/tests/behat/move_question_categories.feature @@ -104,3 +104,34 @@ Feature: A teacher can reorder question categories And I should not see "+" in the "Course category 2" "list_item" When I drag "Course category 3" "list_item" and I drop it in "Course category 1" "text" Then I should see "+" in the "Course category 1" "list_item" + + Scenario: Teacher can move a category to another bank + Given the following "question categories" exist: + | contextlevel | reference | name | idnumber | questioncategory | + | Activity module | qbank1 | Course category 4 | questioncat4 | Course category 1 | + And I reload the page + And "Default for Qbank 1" "list_item" should exist + And "Course category 1" "list_item" should exist + And "Course category 2" "list_item" should exist + And "Course category 3" "list_item" should exist + And "Course category 4" "list_item" should exist + And I open the action menu in "Course category 1" "list_item" + And I choose "Move" in the open action menu + And I should see "Question bank: Qbank 1" in the "Move Course category 1" "dialogue" + And I should see "Before Course category 3" in the "Move Course category 1" "dialogue" + And I press "Switch bank" + And I click on "Qbank 2" "link" in the "Select question bank" "dialogue" + And I should see "Question bank: Qbank 2" in the "Move Course category 1" "dialogue" + And I should not see "Before Course category 3" in the "Move Course category 1" "dialogue" + And I should see "As new child of Default for Qbank 2" in the "Move Course category 1" "dialogue" + When I follow "As new child of Default for Qbank 2" + Then "Default for Qbank 1" "list_item" should exist + And "Course category 1" "list_item" should not exist + And "Course category 2" "list_item" should exist + And "Course category 3" "list_item" should exist + And "Course category 4" "list_item" should not exist + And I should see "Category moved to Question bank: Qbank 2" + And I follow "Question bank: Qbank 2" + And "Default for Qbank 2" "list_item" should exist + And "Course category 1" "list_item" should exist + And "Course category 4" "list_item" should exist diff --git a/public/question/bank/managecategories/tests/external/move_category_test.php b/public/question/bank/managecategories/tests/external/move_category_test.php index 8fb1964b369..2f2b333b01b 100644 --- a/public/question/bank/managecategories/tests/external/move_category_test.php +++ b/public/question/bank/managecategories/tests/external/move_category_test.php @@ -18,6 +18,7 @@ namespace qbank_managecategories\external; use context; use context_module; +use core\context\module; use moodle_url; use qbank_managecategories\question_categories; @@ -39,12 +40,12 @@ final class move_category_test extends \qbank_managecategories\manage_category_t /** * Return order of categories for a given context. * - * @param context $context The context to get the category order for. + * @param module $context The context to get the category order for. * @return array Nested array, keyed by category IDs. */ - private function get_current_order(context $context): array { - $categories = new question_categories(new moodle_url('/'), [$context]); - return $this->reduce_tree($categories->editlists[$context->id]->items); + private function get_current_order(module $context): array { + $categories = new question_categories(new moodle_url('/'), cmid: $context->instanceid); + return $this->reduce_tree($categories->editlist->items); } /** @@ -127,7 +128,7 @@ final class move_category_test extends \qbank_managecategories\manage_category_t ], ], ]; - $this->assertEquals($stateupdates, $expectedstateupdates); + $this->assertEquals($expectedstateupdates, $stateupdates); } /** diff --git a/public/question/bank/managecategories/tests/question_categories_test.php b/public/question/bank/managecategories/tests/question_categories_test.php index d12971ca47f..795d4c31044 100644 --- a/public/question/bank/managecategories/tests/question_categories_test.php +++ b/public/question/bank/managecategories/tests/question_categories_test.php @@ -49,9 +49,9 @@ final class question_categories_test extends manage_category_test_base { // Create ordered tree. $questioncategories = new question_categories( new \moodle_url('/'), - [$context], + cmid: $qbank->cmid, ); - $items = $questioncategories->editlists[$context->id]->items; + $items = $questioncategories->editlist->items; // Two top categories (1 and 3) in the course. $this->assertCount(2, $items); diff --git a/public/question/classes/external/search_shared_banks.php b/public/question/classes/external/search_shared_banks.php index 9293c2aaa23..531e8dfd386 100644 --- a/public/question/classes/external/search_shared_banks.php +++ b/public/question/classes/external/search_shared_banks.php @@ -22,6 +22,7 @@ use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_multiple_structure; use core_external\external_value; +use core_question\local\bank\formatted_bank; use core_question\local\bank\question_bank_helper; use core\context; @@ -135,10 +136,13 @@ class search_shared_banks extends external_api { ); $suggestions = array_map( - fn($sharedbank) => [ - 'value' => $sharedbank->modid, - 'label' => $sharedbank->coursenamebankname, - ], + function (formatted_bank $sharedbank): array { + $sharedbank = $sharedbank->get_formatted(); + return [ + 'value' => $sharedbank->modid, + 'label' => $sharedbank->coursenamebankname, + ]; + }, $sharedbanks, ); diff --git a/public/question/classes/local/bank/formatted_bank.php b/public/question/classes/local/bank/formatted_bank.php new file mode 100644 index 00000000000..566cd523f1e --- /dev/null +++ b/public/question/classes/local/bank/formatted_bank.php @@ -0,0 +1,109 @@ +. + +namespace core_question\local\bank; + +use core\context; +use core_course\cm_info; +use JsonSerializable; + +/** + * Data class to hold bank info and categories, and return them with formatted names for output. + * + * @package core_question + * @copyright 2026 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class formatted_bank implements JsonSerializable { + /** + * @var bool True if the bank is the module currently being viewed. + */ + public bool $current; + + /** + * Constructor + * + * @param cm_info $cminfo The course module info for the bank's course module + * @param context $filtercontext The context used for applying filters when formatting strings. + * @param array $questioncategories Array of categories belonging to the bank. + * @param bool $shared True if the bank contains shared questions, false if it contains private questions. + * @param bool $recent True if the bank was recently viewed by the user. + */ + public function __construct( + /** @var cm_info $cminfo Course module info. */ + public cm_info $cminfo, + /** @var context $filtercontext The context used for applying filters when formatting strings. */ + public context $filtercontext, + /** @var array $questioncategories Array of categories belonging to the bank. */ + public array $questioncategories, + /** @var bool True if the bank contains shared questions, false if it contains private questions. */ + public bool $shared, + /** @var bool True if the bank was recently viewed by the user. */ + public bool $recent, + ) { + } + + /** + * Return the question bank formatted for output. + * + * @return \stdClass + */ + public function get_formatted(): \stdClass { + $filteroptions = [ + 'escape' => false, + 'context' => $this->filtercontext, + ]; + $formattedname = $this->cminfo->get_formatted_name($filteroptions); + return (object) [ + 'name' => $formattedname, + 'modid' => $this->cminfo->id, + 'contextid' => $this->cminfo->context->id, + 'coursenamebankname' => get_string( + 'coursenamebankname', + 'question', + (object) [ + 'coursename' => format_string($this->cminfo->get_course()->shortname, true, $filteroptions), + 'bankname' => $formattedname, + ], + ), + 'cminfo' => $this->cminfo, + 'questioncategories' => $this->questioncategories, + 'shared' => $this->shared, + 'recent' => $this->recent, + ]; + } + + /** + * Return the formatted object for encoding as JSON, for example by web service routes. + */ + public function jsonSerialize(): \stdClass { + return $this->get_formatted(); + } + + /** + * Format multiple banks. + * + * @param formatted_bank[] $banks The formatted_bank objects to return formatted. + * @return array The formatted bank data. + */ + public static function format_banks(array $banks): array { + return array_map( + fn($bank) => $bank->get_formatted(), + $banks, + ); + } +} diff --git a/public/question/classes/local/bank/question_bank_helper.php b/public/question/classes/local/bank/question_bank_helper.php index 16a82518f22..21274e1d5d0 100644 --- a/public/question/classes/local/bank/question_bank_helper.php +++ b/public/question/classes/local/bank/question_bank_helper.php @@ -19,6 +19,8 @@ namespace core_question\local\bank; use cm_info; use context; use context_course; +use core\context\course; +use core\context\module; use core\context_helper; use core\di; use core\task\manager; @@ -366,7 +368,7 @@ class question_bank_helper { * @param int $notincourseid if supplied don't return any in this course id * @param ?context $filtercontext Optional context to use for all string filtering, useful for performance when calling with * parameters that will get banks across multiple contexts. - * @return cm_info[] + * @return formatted_bank[] */ public static function get_recently_used_open_banks( int $userid, @@ -397,7 +399,7 @@ class question_bank_helper { if (!empty($havingcap) && !(new question_edit_contexts($context))->have_one_cap($havingcap)) { continue; } - $record = self::get_formatted_bank($cm, filtercontext: $filtercontext); + $record = self::get_formatted_bank($cm, filtercontext: $filtercontext, isrecent: true); $banks[] = $record; } @@ -445,9 +447,15 @@ class question_bank_helper { * Used in qbank_bulkmove/bulk_move.mustache * @param ?context $filtercontext Optional context in which to apply filters. * - * @return stdClass + * @return formatted_bank */ - private static function get_formatted_bank(stdClass $cm, int $currentbankid = 0, ?context $filtercontext = null): stdClass { + private static function get_formatted_bank( + stdClass $cm, + int $currentbankid = 0, + ?context $filtercontext = null, + bool $isshared = true, + bool $isrecent = false, + ): formatted_bank { $cminfo = cm_info::create($cm); $concatedcats = !empty($cm->cats) ? explode(self::CATEGORY_SEPARATOR, $cm->cats) : []; @@ -461,21 +469,14 @@ class question_bank_helper { return $cat; }, $concatedcats); - $bank = new stdClass(); - $filteroptions = ['escape' => false]; - if (!is_null($filtercontext)) { - $filteroptions['context'] = $filtercontext; - } - $bank->name = $cminfo->get_formatted_name($filteroptions); - $bank->modid = $cminfo->id; - $bank->contextid = $cminfo->context->id; - if (!isset($filteroptions['context'])) { - $filteroptions['context'] = context_course::instance($cminfo->get_course()->id); - } - $bank->coursenamebankname = format_string($cminfo->get_course()->shortname, true, $filteroptions) . " - {$bank->name}"; - $bank->cminfo = $cminfo; - $bank->questioncategories = $categories; - return $bank; + $filtercontext ??= context_course::instance($cminfo->get_course()->id); + return new formatted_bank( + $cminfo, + $filtercontext, + $categories, + $isshared, + $isrecent, + ); } /** @@ -737,4 +738,51 @@ class question_bank_helper { // As a failsafe, limit the length of the final string in case the lang string is too long. return shorten_text($bankname, self::BANK_NAME_MAX_LENGTH); } + + /** + * Return a list of question banks. + * + * By default, this will just return a list of shared question banks (activities with FEATURE_PUBLISHES_QUESTIONS) in the given + * course context. + * Other options allow you to include private question banks and banks recently accessed by the user + * (which may be in other courses). + * + * @param course $coursecontext The course context to find shared and private banks within. + * @param module|null $modulecontext If set, use this instead of $coursecontext when applying text filters on bank names. + * @param bool $includeshared Include banks with shared questions. + * @param bool $includerecent Include banks recently viewed by the user. + * @return formatted_bank[] + */ + public static function get_banks_for_course( + course $coursecontext, + ?module $modulecontext = null, + bool $includeshared = true, + bool $includerecent = false, + ): array { + global $USER; + if ($modulecontext) { + $context = $modulecontext; + } else { + $context = $coursecontext; + } + $capabilities = ['moodle/question:useall', 'moodle/question:usemine']; + $banks = []; + if ($includeshared) { + $sharedbanks = self::get_activity_instances_with_shareable_questions( + incourseids: [$coursecontext->instanceid], + havingcap: $capabilities, + filtercontext: $context, + ); + $banks = array_merge($banks, $sharedbanks); + } + if ($includerecent) { + $recentbanks = self::get_recently_used_open_banks( + $USER->id, + filtercontext: $context, + havingcap: $capabilities, + ); + $banks = array_merge($banks, $recentbanks); + } + return $banks; + } } diff --git a/public/question/classes/output/question_bank_list.php b/public/question/classes/output/question_bank_list.php index c2acdb66060..c540765dab7 100644 --- a/public/question/classes/output/question_bank_list.php +++ b/public/question/classes/output/question_bank_list.php @@ -18,6 +18,7 @@ namespace core_question\output; use action_link; use core\output\pix_icon; +use core_question\local\bank\formatted_bank; use renderer_base; use core_courseformat\output\local\content\cm\controlmenu; use core_question\local\bank\question_bank_helper; @@ -34,10 +35,10 @@ class question_bank_list implements \renderable, \templatable { /** * Instantiate the output class. * - * @param array $bankinstances {@see question_bank_helper::get_activity_instances_with_shareable_questions()} + * @param formatted_bank[] $bankinstances {@see question_bank_helper::get_activity_instances_with_shareable_questions()} */ public function __construct( - /** @var array $bankinstances */ + /** @var formatted_bank[] $bankinstances */ protected readonly array $bankinstances ) { } @@ -51,6 +52,7 @@ class question_bank_list implements \renderable, \templatable { public function export_for_template(renderer_base $output): array { $banks = []; foreach ($this->bankinstances as $instance) { + $instance = $instance->get_formatted(); if (plugin_supports('mod', $instance->cminfo->modname, FEATURE_PUBLISHES_QUESTIONS)) { $returnurl = question_bank_helper::get_url_for_qbank_list($instance->cminfo->course); $format = course_get_format($instance->cminfo->course); diff --git a/public/question/classes/output/switch_question_bank.php b/public/question/classes/output/switch_question_bank.php index b958249be8b..f427068f569 100644 --- a/public/question/classes/output/switch_question_bank.php +++ b/public/question/classes/output/switch_question_bank.php @@ -26,12 +26,21 @@ namespace core_question\output; use cm_info; +use core\attribute\deprecated; +use core\deprecation; +use core_question\local\bank\formatted_bank; use core_question\local\bank\question_bank_helper; use renderer_base; /** * Get the switch question bank rendered content. Displays lists of shared banks the viewing user has access to. */ +#[deprecated( + replacement: 'core_question/bank_switcher Javascript module', + since: 5.2, + reason: 'Bank switching interface is now rendered client-side.', + mdl: 'MDL-87264', +)] class switch_question_bank implements \renderable, \templatable { /** @@ -41,6 +50,12 @@ class switch_question_bank implements \renderable, \templatable { * @param int $courseid of the current course. * @param int $userid of the user viewing the page. */ + #[deprecated( + replacement: 'core_question/bank_switcher Javascript module', + since: 5.2, + reason: 'Bank switching interface is now rendered client-side.', + mdl: 'MDL-87264', + )] public function __construct( /** @var int quiz course module id */ private readonly int $quizcmid, @@ -49,6 +64,7 @@ class switch_question_bank implements \renderable, \templatable { /** @var int id of the user viewing the page */ private readonly int $userid ) { + deprecation::emit_deprecation([$this, __FUNCTION__]); } /** @@ -57,18 +73,28 @@ class switch_question_bank implements \renderable, \templatable { * @param renderer_base $output * @return array */ + #[deprecated( + replacement: 'core_question/bank_switcher Javascript module', + since: 5.2, + reason: 'Bank switching interface is now rendered client-side.', + mdl: 'MDL-87264', + )] public function export_for_template(renderer_base $output) { - + deprecation::emit_deprecation([$this, __FUNCTION__]); [, $cm] = get_module_from_cmid($this->quizcmid); $cminfo = cm_info::create($cm); $capabilities = ['moodle/question:useall', 'moodle/question:usemine']; - $coursesharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions( - incourseids: [$this->courseid], - havingcap: $capabilities, - filtercontext: $cminfo->context, + $coursesharedbanks = formatted_bank::format_banks( + question_bank_helper::get_activity_instances_with_shareable_questions( + incourseids: [$this->courseid], + havingcap: $capabilities, + filtercontext: $cminfo->context, + ), + ); + $recentlyviewedbanks = formatted_bank::format_banks( + question_bank_helper::get_recently_used_open_banks($this->userid, havingcap: $capabilities), ); - $recentlyviewedbanks = question_bank_helper::get_recently_used_open_banks($this->userid, havingcap: $capabilities); return [ 'quizname' => $cminfo->get_formatted_name(), diff --git a/public/question/classes/output/view_banks.php b/public/question/classes/output/view_banks.php index 944b41422a6..dd4bd57b98c 100644 --- a/public/question/classes/output/view_banks.php +++ b/public/question/classes/output/view_banks.php @@ -17,6 +17,7 @@ namespace core_question\output; use context_course; +use core_question\local\bank\formatted_bank; use core_question\local\bank\question_bank_helper; use renderer_base; use single_button; @@ -35,14 +36,14 @@ class view_banks implements \templatable, \renderable { /** * Create a new view_banks instance. * - * @param array $sharedbanks {@see question_bank_helper::get_activity_instances_with_shareable_questions()} - * @param array $privatebanks {@see question_bank_helper::get_activity_instances_with_private_questions()} + * @param formatted_bank[] $sharedbanks {@see question_bank_helper::get_activity_instances_with_shareable_questions()} + * @param formatted_bank[] $privatebanks {@see question_bank_helper::get_activity_instances_with_private_questions()} * @param stdClass $course the viewing course. */ public function __construct( - /** @var array Banks that can be shared */ + /** @var formatted_bank[] Banks that can be shared */ protected readonly array $sharedbanks, - /** @var array Banks that cannot be shared */ + /** @var formatted_bank[] Banks that cannot be shared */ protected readonly array $privatebanks, /** @var stdClass current course object */ protected readonly stdClass $course, diff --git a/public/question/classes/question_category.php b/public/question/classes/question_category.php new file mode 100644 index 00000000000..6e466b47a30 --- /dev/null +++ b/public/question/classes/question_category.php @@ -0,0 +1,89 @@ +. + +namespace core_question; + +use core\context; +use JsonSerializable; +use stdClass; + +/** + * A simple value object representing a question category. + * + * When serialised to JSON for output via routes, this the name and intro will be formatted. + * + * @package core_question + * @copyright 2026 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_category implements JsonSerializable { + /** + * Set properties and format strings. + * + * @param int $id The category ID + * @param string $name The raw category name, this will be formatted. + * @param int $contextid The module context ID the category belongs to. + * @param string $info The category's info, this with be formatted according to $infoformat. + * @param string $infoformat The format for $info. + * @param string $stamp Generated identifier for this category. + * @param int $parent Parent category ID. + * @param int $sortorder Category sort order within its parent. + * @param int $idnumber ID number. + */ + public function __construct( + /** @var int $id The category ID */ + public int $id, + /** @var string $name The raw category name, this will be formatted. */ + public string $name, + /** @var int $contextid The module context ID the category belongs to. */ + public int $contextid, + /** @var string $info The category's info, this with be formatted according to $infoformat. */ + public string $info, + /** @var string $infoformat The format for $info. */ + public string $infoformat, + /** @var string $stamp Generated identifier for this category. */ + public string $stamp, + /** @var int $parent Parent category ID. */ + public int $parent, + /** @var int $sortorder Category sort order within its parent. */ + public int $sortorder, + /** @var int $idnumber ID number. */ + public int $idnumber, + ) { + } + + /** + * Returns the properties with formatted 'name' and 'info' + * + * This is used when encoding the object for output, for example via web service routes. + * + * @return stdClass + */ + public function jsonSerialize(): stdClass { + $context = context::instance_by_id($this->contextid); + return (object) [ + 'id' => $this->id, + 'name' => format_string($this->name, ['context' => $context]), + 'contextid' => $this->contextid, + 'info' => format_text($this->info, $this->infoformat, ['context' => $context]), + 'stamp' => $this->stamp, + 'parent' => $this->parent, + 'sortorder' => $this->sortorder, + 'idnumber' => $this->idnumber, + ]; + } +} diff --git a/public/question/classes/route/api/bank.php b/public/question/classes/route/api/bank.php index 3c8ee01ae43..f4f92c17246 100644 --- a/public/question/classes/route/api/bank.php +++ b/public/question/classes/route/api/bank.php @@ -20,12 +20,25 @@ use core\context\course; use core\context\module; use core\exception\required_capability_exception; use core\param; +use core\router\parameters\query_course; +use core\router\parameters\query_coursemodule; use core\router\require_login; use core\router\route; +use core\router\schema\example; +use core\router\schema\objects\array_of_strings; +use core\router\schema\objects\array_of_things; +use core\router\schema\objects\schema_object; use core\router\schema\parameters\path_parameter; +use core\router\schema\parameters\query_parameter; +use core\router\schema\response\content\json_media_type; use core\router\schema\response\payload_response; +use core\router\schema\response\response; +use core_question\local\bank\formatted_bank; +use core_question\local\bank\question_bank_helper; use core_question\local\bank\question_edit_contexts; use core_question\local\bank\question_version_status; +use core_question\output\question_category_selector; +use core_question\question_category; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -115,4 +128,113 @@ class bank { response: $response, ); } + + /** + * Return a list of formatted question banks matching the parameters. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param course $coursecontext The course context. + * @param module $currentmodulecontext The module context. + * @param question_bank_helper $helper Injected dependency. + * @return payload_response A list of question banks with formatted names, and whether they are shared and recently used. + */ + #[route( + path: '/banks', // Resolves to /api/rest/v2/question/banks. + queryparams: [ + new query_course(required: true), + new query_coursemodule('currentmodule'), + new query_parameter(name: 'includeshared', type: param::BOOL, default: true), + new query_parameter(name: 'includerecent', type: param::BOOL, default: false), + ], + responses: [ + new response( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new schema_object( + content: [ + 'banks' => new array_of_things(thingtype: formatted_bank::class), + ], + ), + ), + ], + ), + ], + requirelogin: new require_login(true, courseattributename: 'course'), + )] + public function banks( + ServerRequestInterface $request, + ResponseInterface $response, + course $coursecontext, + module $currentmodulecontext, + question_bank_helper $helper, + ): payload_response { + $params = $request->getQueryParams(); + $banks = $helper::get_banks_for_course( + $coursecontext, + $currentmodulecontext, + $params['includeshared'], + $params['includerecent'], + ); + return new payload_response( + request: $request, + response: $response, + payload: ['banks' => $banks], + ); + } + + #[route( + path: '/categories', // Resolves to /api/rest/v2/question/categories. + queryparams: [ + new query_coursemodule(), + ], + responses: [ + new response( + statuscode: 200, + description: 'OK', + content: [ + new json_media_type( + schema: new schema_object( + content: [ + 'context' => new array_of_strings(param::ALPHA, param::TEXT), + 'categories' => new array_of_things(question_category::class), + ], + ), + ), + ], + ), + ], + )] + /** + * Return a list of question categories with names and info formatted for output. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param module $coursemodulecontext The module context. + * @param question_category_selector $categoryselector Injected dependency. + * @return payload_response The course module context id and name, and a list of question categories in that context. + */ + public function categories( + ServerRequestInterface $request, + ResponseInterface $response, + module $coursemodulecontext, + question_category_selector $categoryselector, + ): payload_response { + require_login(); + $categories = $categoryselector->get_categories_for_contexts($coursemodulecontext->id, top: true); + return new payload_response( + request: $request, + response: $response, + payload: [ + 'context' => [ + 'id' => $coursemodulecontext->id, + 'name' => $coursemodulecontext->get_context_name(false), + 'prefixedname' => $coursemodulecontext->get_context_name(), + ], + 'categories' => $categories, + ], + ); + } } diff --git a/public/question/templates/switch_question_bank.mustache b/public/question/templates/switch_question_bank.mustache index 725b28ff155..57592601f91 100644 --- a/public/question/templates/switch_question_bank.mustache +++ b/public/question/templates/switch_question_bank.mustache @@ -19,122 +19,77 @@ Example context (json): { - "quizname": "Quiz 1", - "quizcmid": 1, - "quizcontextid": 1, + "contextid": 1, + "hasactivitybank": true, + "activitybank": { + "name": "Quiz 1", + "cmid": 1 + }, "hascoursesharedbanks": true, "coursesharedbanks": [ { "name": "Question bank 1", - "modid": "2", - "contextid": 2, - "coursenamebankname": "c1 - Question bank 1", - "cminfo": {}, - "questioncategories": [] + "modid": "2" }, { "name": "Question bank 2", - "modid": "3", - "contextid": 3, - "coursenamebankname": "c1 - Question bank 2", - "cminfo": {}, - "questioncategories": [] + "modid": "3" } ], "hasrecentlyviewedbanks": true, "recentlyviewedbanks": [ { - "name": "Question bank 3", "modid": "4", - "contextid": 4, - "coursenamebankname": "c2 - Question bank 4", - "cminfo": {}, - "questioncategories": [] + "coursenamebankname": "c2 - Question bank 4" }, { - "name": "Question bank 4", "modid": "6", - "contextid": 6, - "coursenamebankname": "c3 - Question bank 5", - "cminfo": {}, - "questioncategories": [] - } - ], - "hassharedbanks": true, - "sharedbanks": [ - { - "name": "Question bank 1", - "modid": "2", - "contextid": 2, - "coursenamebankname": "c1 - Question bank 1", - "cminfo": {}, - "questioncategories": [] - }, - { - "name": "Question bank 2", - "modid": "3", - "contextid": 3, - "coursenamebankname": "c1 - Question bank 2", - "cminfo": {}, - "questioncategories": [] - }, - { - "name": "Question bank 3", - "modid": "4", - "contextid": 4, - "coursenamebankname": "c2 - Question bank 4", - "cminfo": {}, - "questioncategories": [] - }, - { - "name": "Question bank 4", - "modid": "6", - "contextid": 6, - "coursenamebankname": "c3 - Question bank 5", - "cminfo": {}, - "questioncategories": [] + "coursenamebankname": "c3 - Question bank 5" } ] } }} -
    -
    {{#str}}quizquestionbank, core_question{{/str}}
    - {{quizname}} -
    +
    + {{#hasactivitybank}} +
    +
    {{#str}}quizquestionbank, core_question{{/str}}
    + {{activitybank.name}} +
    +
    + {{/hasactivitybank}} -
    + {{#hascoursesharedbanks}} +
    +
    {{#str}}banksincourse, core_question{{/str}}
    + {{#coursesharedbanks}} + + {{/coursesharedbanks}} +
    +
    + {{/hascoursesharedbanks}} -{{#hascoursesharedbanks}} -
    -
    {{#str}}banksincourse, core_question{{/str}}
    - {{#coursesharedbanks}} - - {{/coursesharedbanks}} + {{#hasrecentlyviewedbanks}} +
    +
    {{#str}}recentlyviewedquestionbanks, core_question{{/str}}
    + {{#recentlyviewedbanks}} + + {{/recentlyviewedbanks}} +
    +
    + {{/hasrecentlyviewedbanks}} + + +
    +
    {{#str}}otherquestionbank, core_question{{/str}}
    +
    -
    -{{/hascoursesharedbanks}} - -{{#hasrecentlyviewedbanks}} -
    -
    {{#str}}recentlyviewedquestionbanks, core_question{{/str}}
    - {{#recentlyviewedbanks}} - - {{/recentlyviewedbanks}} -
    -
    -{{/hasrecentlyviewedbanks}} - - -
    -
    {{#str}}otherquestionbank, core_question{{/str}}
    -
    diff --git a/public/question/tests/local/bank/question_bank_helper_test.php b/public/question/tests/local/bank/question_bank_helper_test.php index c877de22060..ea53f088b01 100644 --- a/public/question/tests/local/bank/question_bank_helper_test.php +++ b/public/question/tests/local/bank/question_bank_helper_test.php @@ -17,6 +17,7 @@ namespace core_question; use core\exception\coding_exception; +use core_question\local\bank\formatted_bank; use core_question\local\bank\question_bank_helper; /** @@ -127,7 +128,8 @@ final class question_bank_helper_test extends \advanced_testcase { ); $count = 0; - foreach ($sharedbanks as $courseinstance) { + foreach ($sharedbanks as $sharedbank) { + $courseinstance = $sharedbank->get_formatted(); // Must all be mod_qbanks. $this->assertEquals('qbank', $courseinstance->cminfo->modname); // Must have 2 categories each bank. @@ -148,7 +150,8 @@ final class question_bank_helper_test extends \advanced_testcase { ); $count = 0; - foreach ($privatebanks as $courseinstance) { + foreach ($privatebanks as $privatebank) { + $courseinstance = $privatebank->get_formatted(); // Must all be mod_quiz. $this->assertEquals('quiz', $courseinstance->cminfo->modname); // Must have 1 category in each bank. @@ -239,7 +242,9 @@ final class question_bank_helper_test extends \advanced_testcase { $this->assertCount(22, $allsharedbanks); // Searching for "2", we get the 4 banks with "2" in the name. - $twobanks = question_bank_helper::get_activity_instances_with_shareable_questions(search: '2'); + $twobanks = formatted_bank::format_banks( + question_bank_helper::get_activity_instances_with_shareable_questions(search: '2'), + ); $this->assertCount(4, $twobanks); $this->assertEquals( [$sharedmods[2]->cmid, $sharedmods[12]->cmid, $sharedmods[20]->cmid, $sharedmods[21]->cmid], @@ -247,12 +252,16 @@ final class question_bank_helper_test extends \advanced_testcase { ); // Searching for "Shared bank" with no limit, we should get all 21, but not "Another bank". - $sharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(search: 'Shared bank'); + $sharedbanks = formatted_bank::format_banks( + question_bank_helper::get_activity_instances_with_shareable_questions(search: 'Shared bank'), + ); $this->assertCount(21, $sharedbanks); $this->assertEmpty(array_filter($sharedbanks, fn($bank) => in_array($bank->name, ['Another bank']))); // Searching for "Shared bank" with a limit of 20, we should get all except number 21 and "Another bank". - $limitedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(search: 'Shared bank', limit: 20); + $limitedbanks = formatted_bank::format_banks( + question_bank_helper::get_activity_instances_with_shareable_questions(search: 'Shared bank', limit: 20), + ); $this->assertCount(20, $limitedbanks); $this->assertEmpty(array_filter($limitedbanks, fn($bank) => in_array($bank->name, ['Shared bank 21', 'Another bank']))); } @@ -413,14 +422,18 @@ final class question_bank_helper_test extends \advanced_testcase { $viewedorder = array_reverse($banks); // Check that the courseid filter works. - $recentlyviewed = question_bank_helper::get_recently_used_open_banks($user->id, $course1->id); + $recentlyviewed = formatted_bank::format_banks( + question_bank_helper::get_recently_used_open_banks($user->id, $course1->id), + ); $this->assertCount(3, $recentlyviewed); // We should have the viewed banks in course 2. $courseviewed = array_slice($banks, 3, 3); $this->assertEqualsCanonicalizing(array_column($recentlyviewed, 'modid'), array_column($courseviewed, 'cmid')); // Check that the capability filter works. - $recentlyviewed = question_bank_helper::get_recently_used_open_banks($user->id, havingcap: ['moodle/question:useall']); + $recentlyviewed = formatted_bank::format_banks( + question_bank_helper::get_recently_used_open_banks($user->id, havingcap: ['moodle/question:useall']), + ); $this->assertCount(2, $recentlyviewed); // We should have the 2 most recently viewed banks in course 1. $capabilityviewed = array_slice($banks, 1, 2); @@ -431,7 +444,7 @@ final class question_bank_helper_test extends \advanced_testcase { // We only keep a record of 5 maximum. $this->assertCount(5, $recentlyviewed); foreach ($recentlyviewed as $order => $record) { - $this->assertEquals($viewedorder[$order]->cmid, $record->modid); + $this->assertEquals($viewedorder[$order]->cmid, $record->cminfo->id); } // Now if we view one of those again it should get bumped to the front of the list. @@ -444,12 +457,12 @@ final class question_bank_helper_test extends \advanced_testcase { // We should still have 5 maximum. $this->assertCount(5, $recentlyviewed); // The recently viewed on got bumped to the front. - $this->assertEquals($banks[2]->cmid, $recentlyviewed[0]->modid); + $this->assertEquals($banks[2]->cmid, $recentlyviewed[0]->cminfo->id); // The others got sorted accordingly behind it. - $this->assertEquals($banks[5]->cmid, $recentlyviewed[1]->modid); - $this->assertEquals($banks[4]->cmid, $recentlyviewed[2]->modid); - $this->assertEquals($banks[3]->cmid, $recentlyviewed[3]->modid); - $this->assertEquals($banks[1]->cmid, $recentlyviewed[4]->modid); + $this->assertEquals($banks[5]->cmid, $recentlyviewed[1]->cminfo->id); + $this->assertEquals($banks[4]->cmid, $recentlyviewed[2]->cminfo->id); + $this->assertEquals($banks[3]->cmid, $recentlyviewed[3]->cminfo->id); + $this->assertEquals($banks[1]->cmid, $recentlyviewed[4]->cminfo->id); // Now create a quiz and trigger the bank view of it. $quiz = self::getDataGenerator()->get_plugin_generator('mod_quiz')->create_instance(['course' => $course1]); @@ -463,7 +476,7 @@ final class question_bank_helper_test extends \advanced_testcase { // Make sure that we only store bank views for plugins that support FEATURE_PUBLISHES_QUESTIONS. foreach ($recentlyviewed as $record) { - $this->assertNotEquals($quiz->cmid, $record->modid); + $this->assertNotEquals($quiz->cmid, $record->cminfo->id); } // Now delete one of the viewed bank modules and get the records again. @@ -472,10 +485,10 @@ final class question_bank_helper_test extends \advanced_testcase { $this->assertCount(4, $recentlyviewed); // Check the order was retained. - $this->assertEquals($banks[5]->cmid, $recentlyviewed[0]->modid); - $this->assertEquals($banks[4]->cmid, $recentlyviewed[1]->modid); - $this->assertEquals($banks[3]->cmid, $recentlyviewed[2]->modid); - $this->assertEquals($banks[1]->cmid, $recentlyviewed[3]->modid); + $this->assertEquals($banks[5]->cmid, $recentlyviewed[0]->cminfo->id); + $this->assertEquals($banks[4]->cmid, $recentlyviewed[1]->cminfo->id); + $this->assertEquals($banks[3]->cmid, $recentlyviewed[2]->cminfo->id); + $this->assertEquals($banks[1]->cmid, $recentlyviewed[3]->cminfo->id); } /**