diff --git a/.upgradenotes/MDL-72397-2024060512165928.yml b/.upgradenotes/MDL-72397-2024060512165928.yml new file mode 100644 index 0000000000000..bff8278648743 --- /dev/null +++ b/.upgradenotes/MDL-72397-2024060512165928.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-72397 +notes: + qbank_managecategories: + - message: >- + question_category_list and question_category_list_item are no longer + used, and are deprecated. Category lists are now generated by templates. + type: deprecated diff --git a/mod/quiz/tests/behat/editing_add.feature b/mod/quiz/tests/behat/editing_add.feature index b1774b5109a79..2e3442171cb35 100644 --- a/mod/quiz/tests/behat/editing_add.feature +++ b/mod/quiz/tests/behat/editing_add.feature @@ -99,22 +99,12 @@ Feature: Edit quiz page - adding things in various categories and add them to the question bank. # Create a couple of sub categories. - When I am on the "Course 1" "core_question > course question categories" page - Then I should see "Add category" - And I follow "Add category" - Then I set the field "Parent category" to "Default for C1" - And I set the field "Name" to "Subcat 1" - And I set the field "Category info" to "This is sub category 1" - And I press "id_submitbutton" - And I should see "Subcat 1" - - And I follow "Add category" - Then I set the field "Parent category" to "Default for C1" - And I set the field "Name" to "Subcat 2" - And I set the field "Category info" to "This is sub category 2" - And I press "id_submitbutton" - And I should see "Subcat 2" - + Given the following "question categories" exist: + | contextlevel | reference | questioncategory | name | + | Course | C1 | Default for C1 | Subcat 1 | + | Course | C1 | Default for C1 | Subcat 2 | + When I am on "Course 1" course homepage + And I navigate to "Question bank" in current page administration And I select "Questions" from the "Question bank tertiary navigation" singleselect And I should see "Question bank" diff --git a/mod/quiz/tests/behat/random_question.feature b/mod/quiz/tests/behat/random_question.feature index 15a7e047b6896..5e6e7842ec9dc 100644 --- a/mod/quiz/tests/behat/random_question.feature +++ b/mod/quiz/tests/behat/random_question.feature @@ -62,11 +62,13 @@ Feature: Moving a question to another category should not affect random question And I press "Add random question" And I should see "Random question based on filter condition" on quiz page "1" And I am on the "Course 1" "core_question > course question categories" page - And I click on "Edit this category" "link" in the "Used category" "list_item" + And I open the action menu in "Used category" "list_item" + And I choose "Edit" in the open action menu And I set the following fields to these values: | Name | Used category new | | Category info | I was edited | - And I press "Save changes" + And I press "Edit category" + And I set the field "Show descriptions" to "1" Then I should see "Used category new" And I should see "I was edited" in the "Used category new" "list_item" And I am on the "Quiz 1" "mod_quiz > Edit" page diff --git a/question/bank/managecategories/amd/build/addcategory.min.js b/question/bank/managecategories/amd/build/addcategory.min.js new file mode 100644 index 0000000000000..71b2c62b7ea19 --- /dev/null +++ b/question/bank/managecategories/amd/build/addcategory.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/addcategory",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={ADD_BUTTON:'[data-action="addeditcategory"]'}}stateReady(){this.addEventListener(this.getElement(this.selectors.ADD_BUTTON),"click",_categorymanager.categorymanager.showEditModal)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=addcategory.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/addcategory.min.js.map b/question/bank/managecategories/amd/build/addcategory.min.js.map new file mode 100644 index 0000000000000..13e41aa262add --- /dev/null +++ b/question/bank/managecategories/amd/build/addcategory.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"addcategory.min.js","sources":["../src/addcategory.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 * Add category button for displaying the modal form.\n *\n * This just connects up the button to the showEditModal listener.\n *\n * @module qbank_managecategories/addcategory\n * @class qbank_managecategories/addcategory\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n ADD_BUTTON: '[data-action=\"addeditcategory\"]',\n };\n }\n\n stateReady() {\n this.addEventListener(this.getElement(this.selectors.ADD_BUTTON), 'click', categorymanager.showEditModal);\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\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 const targetElement = document.querySelector(target);\n return new this({\n element: targetElement,\n selectors,\n reactive: categorymanager,\n });\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","ADD_BUTTON","stateReady","addEventListener","this","getElement","categorymanager","showEditModal","target","document","querySelector","reactive"],"mappings":"uQA2B6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,WAAY,mCAIpBC,kBACSC,iBAAiBC,KAAKC,WAAWD,KAAKJ,UAAUC,YAAa,QAASK,iCAAgBC,2BAUnFC,OAAQR,kBAET,IAAII,KAAK,CACZN,QAFkBW,SAASC,cAAcF,QAGzCR,UAAAA,UACAW,SAAUL"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/category.min.js b/question/bank/managecategories/amd/build/category.min.js new file mode 100644 index 0000000000000..89bb1cb725d2b --- /dev/null +++ b/question/bank/managecategories/amd/build/category.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/category",["exports","core/reactive","qbank_managecategories/categorymanager","core/templates","core/modal","core/str"],(function(_exports,_reactive,_categorymanager,_templates,_modal,_str){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"]',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)}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: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;window.console.log("updating",this.getElement()),window.console.log("new state",element);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,window.console.log("newParent",newParent),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);window.console.log("previousSibling",previousSibling),window.console.log("nextSibling",nextSibling);const moved=newParent!==this.getElement().parentElement||nextSibling!==this.getElement();window.console.log("moved",moved),moved&&(nextSibling?(window.console.log("insertBefore"),newParent.insertBefore(this.getElement(),nextSibling)):(window.console.log("appendChild"),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;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}));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;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!==parseInt(this.getElement().dataset.categoryid)||this.getElement(this.selectors.CATEGORY_LIST)?null:this.createChildList({categoryid:element.parent,children:[element.templatecontext]})}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=category.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/category.min.js.map b/question/bank/managecategories/amd/build/category.min.js.map new file mode 100644 index 0000000000000..c74b18df9a6b3 --- /dev/null +++ b/question/bank/managecategories/amd/build/category.min.js.map @@ -0,0 +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\";\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 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 }\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 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 window.console.log('updating', this.getElement());\n window.console.log('new state', 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 window.console.log('newParent', newParent);\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 window.console.log('previousSibling', previousSibling);\n window.console.log('nextSibling', nextSibling);\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 window.console.log('moved', moved);\n\n if (moved) {\n if (nextSibling) {\n window.console.log('insertBefore');\n // Move to the specified position in the list.\n newParent.insertBefore(this.getElement(), nextSibling);\n } else {\n window.console.log('appendChild');\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 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 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;\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 !== parseInt(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"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","CATEGORY_LIST","CATEGORY_ITEM","CATEGORY_CONTENTS","EDIT_BUTTON","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","destroy","deInitDragDrop","classList","contains","getDraggableData","_getDraggableData","dragdrop","DragDrop","undefined","setDraggable","unregister","target","document","querySelector","reactive","dataset","categoryid","validateDropData","showDropZone","dropData","closest","add","hideDropZone","remove","drop","event","dropTarget","getElementById","targetParentId","parent","parentList","precedingSibling","firstElementChild","previousElementSibling","moveCategory","_precedingSibling","getWatchers","watch","handler","updatePosition","rerender","checkChildList","html","js","Templates","renderForPromise","templatecontext","replaceNodeContents","context","parentContainer","appendNodeContents","childList","newParent","window","console","log","originParent","previousSibling","nextSibling","parseInt","createChildList","parentElement","_previousSibling","nextElementSibling","moved","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","e","getAttribute","setAttribute","moveList","contexts","moveContext","contextname","hascategories","modal","Modal","title","body","render","footer","show","large","getBody"],"mappings":"0eA4B6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,cAAe,uCACfC,cAAe,gDACfC,kBAAmB,4CACnBC,YAAa,kCACbC,YAAa,4CACbC,QAAS,uDACTC,oBAAqB,8CACrBC,aAAc,kCACdC,YAAaV,wBAAmBA,IAChCW,kBAAmBX,wBAAmBA,kDACtCY,WAAYZ,kCAA6BA,SACzCa,iBAAkBC,+CAA0CA,sBAE3DC,QAAU,CACXC,kBAAmB,OACnBC,WAAY,aACZC,WAAY,iDAEXC,IAAM,CACPC,SAAUpB,uBAAkBA,KAIpCqB,kBACSC,oBACAC,iBAAiBC,KAAKC,WAAWD,KAAKvB,UAAUI,aAAc,QAASqB,iCAAgBC,qBACtFC,WAAaJ,KAAKC,WAAWD,KAAKvB,UAAUK,kBAC7CiB,iBAAiBK,WAAY,QAASJ,KAAKK,eAGpDC,eAESC,iBAMTT,oBACSS,iBAEDP,KAAKzB,QAAQiC,UAAUC,SAAST,KAAKT,QAAQE,mBACxCiB,iBAAmBV,KAAKW,wBAE5BC,SAAW,IAAIC,mBAASb,MAMjCO,sBAC0BO,IAAlBd,KAAKY,gBACyBE,IAA1Bd,KAAKU,wBACAE,SAASG,cAAa,QACtBL,sBAAmBI,QAEvBF,SAASI,kBACTJ,cAAWE,eAWZG,OAAQxC,kBACT,IAAIuB,KAAK,CACZzB,QAAS2C,SAASC,cAAcF,QAChCxC,UAAAA,UACA2C,SAAUlB,mCAYlBS,0BACW,CACHnC,GAAIwB,KAAKC,aAAaoB,QAAQC,YAItCC,0BACW,EAQXC,aAAaC,iBACLzB,KAAKC,aAAayB,QAAQ1B,KAAKvB,UAAUS,YAAYuC,SAASjD,YAI7DyB,aAAaO,UAAUmB,IAAI3B,KAAKT,QAAQG,aACtC,GAMXkC,oBACS3B,aAAaO,UAAUqB,OAAO7B,KAAKT,QAAQG,YASpDoC,KAAKL,SAAUM,mCACLC,WAAaD,MAAMd,OAAOS,QAAQ1B,KAAKvB,UAAUE,mBAElDqD,qBAIDA,WAAWN,QAAQ1B,KAAKvB,UAAUS,YAAYuC,SAASjD,gBAK5C0C,SAASe,eAAejC,KAAKL,IAAIC,SAAS6B,SAASjD,kBAM5D0D,eAAiBF,WAAWX,QAAQc,OACpCC,WAAaJ,WAAWN,QAAQ1B,KAAKvB,UAAUC,mBACjD2D,iBAIAA,iBAFAL,aAAeI,WAAWE,kBAEP,KAEAN,WAAWO,wDAIlBC,aAAaf,SAASjD,GAAI0D,yCAAgBG,qDAAAI,kBAAkBpB,QAAQC,YAGxFoB,oBACW,CAEH,CAACC,2BAAqB3C,KAAKzB,QAAQ8C,QAAQC,wBAAuBsB,QAAS5C,KAAK6C,gBAEhF,CAACF,2BAAqB3C,KAAKzB,QAAQ8C,QAAQC,wCAAuCsB,QAAS5C,KAAK8C,UAChG,CAACH,2BAAqB3C,KAAKzB,QAAQ8C,QAAQC,wCAAuCsB,QAAS5C,KAAK8C,UAEhG,CAACH,2BAA6BC,QAAS5C,KAAK+C,0CAWrCxE,QAACA,oBACNyE,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAC/B,0CACA5E,QAAQ6E,wBAELF,mBAAUG,oBAAoBrD,KAAKC,WAAWD,KAAKvB,UAAUQ,cAAe+D,KAAMC,0BASvEK,eACZN,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAC/B,mCACAG,SAEEC,gBAAkBrC,SAASC,cAAcnB,KAAKvB,UAAUU,kBAAkBmE,QAAQhC,mBAClF4B,mBAAUM,mBAAmBD,gBAAiBP,KAAMC,UACpDQ,UAAYvC,SAASC,cAAcnB,KAAKvB,UAAUW,WAAWkE,QAAQhC,oBAC3EmC,UAAU/B,QAAQ1B,KAAKvB,UAAUG,mBAAmB4B,UAAUmB,IAAI3B,KAAKT,QAAQC,mBACxEiE,0CAmBHC,WAJanF,QAACA,eAClBoF,OAAOC,QAAQC,IAAI,WAAY7D,KAAKC,cACpC0D,OAAOC,QAAQC,IAAI,YAAatF,eAG1BuF,aAAe5C,SAASC,cAAcnB,KAAKvB,UAAUW,WAAWY,KAAKC,aAAaoB,QAAQc,aAc5F4B,gBACAC,kCAdAC,SAASjE,KAAKC,aAAaoB,QAAQc,UAAY5D,QAAQ4D,QACvDuB,UAAYxC,SAASC,cAAcnB,KAAKvB,UAAUW,WAAWb,QAAQ4D,SAChEuB,YAEDA,gBAAkB1D,KAAKkE,gBAAgB,CAAC5C,WAAY/C,QAAQ4D,eAE3DlC,aAAaoB,QAAQc,OAAS5D,QAAQ4D,QAE3CuB,UAAY1D,KAAKC,aAAakE,cAElCR,OAAOC,QAAQC,IAAI,YAAaH,WAK5BA,UAAUpB,mBAAqB2B,SAAS1F,QAAQe,YAAc2E,SAASP,UAAUpB,kBAAkBjB,QAAQ/B,YAE3G0E,YAAcN,UAAUpB,mBAGxByB,gBAAkBL,UAAUvC,cAAcnB,KAAKvB,UAAUY,iBAAiBd,QAAQe,UAAY,IAC9F0E,qCAAcD,mDAAAK,iBAAiBC,oBAEnCV,OAAOC,QAAQC,IAAI,kBAAmBE,iBACtCJ,OAAOC,QAAQC,IAAI,cAAeG,mBAG5BM,MAASZ,YAAc1D,KAAKC,aAAakE,eAAiBH,cAAgBhE,KAAKC,aAErF0D,OAAOC,QAAQC,IAAI,QAASS,OAExBA,QACIN,aACAL,OAAOC,QAAQC,IAAI,gBAEnBH,UAAUa,aAAavE,KAAKC,aAAc+D,eAE1CL,OAAOC,QAAQC,IAAI,eAEnBH,UAAUc,YAAYxE,KAAKC,gBAG/B6D,eAAiBJ,gBAEZtC,SAASqD,aAAaC,eAAe,CACtC,CACIpG,KAAM,gBACNqG,OAAQ,MACRC,OAAQ,CACJpG,GAAIsF,aAAazC,QAAQC,WACzBuD,WAAYf,aAAagB,iBAAiB9E,KAAKvB,UAAUE,eAAeoG,SAGhF,CACIzG,KAAM,gBACNqG,OAAQ,MACRC,OAAQ,CACJpG,GAAIkF,UAAUrC,QAAQC,WACtBuD,WAAYnB,UAAUoB,iBAAiB9E,KAAKvB,UAAUE,eAAeoG,gBAMhFxG,QAAQ8C,QAAQ/B,UAAYf,QAAQe,gBAGnC0F,YAAchF,KAAKzB,QAAQiC,UAAUC,SAAST,KAAKT,QAAQE,YAC7DuF,cAAgBzG,QAAQ0G,iBACnB1G,QAAQiC,UAAUqB,OAAO7B,KAAKT,QAAQE,iBACtCK,iBACGkF,aAAezG,QAAQ0G,kBAC1B1G,QAAQiC,UAAUmB,IAAI3B,KAAKT,QAAQE,iBACnCK,gBAWboF,uBAAuBC,KAAMC,wBACnBC,WAAa,MACfF,KAAKG,SAAU,KACXjD,iBAAmB,KACvB8C,KAAKG,SAASC,SAAQC,8DACZC,WAAaxB,SAASuB,SAASnE,QAAQC,eAEzCmE,aAAeL,4BAIfM,MAAQ,CACRpE,WAAYmE,WACZE,iBAAkBP,iBAClBQ,4EAAoBvD,sDAAAwD,mBAAkBxE,QAAQC,kEAAc,EAC5Da,OAAQqD,SAASnE,QAAQc,OACzB2D,aAAcN,SAASnE,QAAQyE,aAC/BT,WAAY,KACZU,QAASN,aAAeL,wBAEtB3B,UAAY+B,SAASrE,cAAcnB,KAAKvB,UAAUC,eAGpDgH,MAAML,WAFN5B,UAEmBzD,KAAKkF,uBAAuBzB,UAAW2B,kBAGvC,CACf,CACIO,iBAAkBP,iBAClBQ,mBAAoB,EACpBzD,OAAQsD,WACRK,aAAcN,SAASnE,QAAQyE,aAC/BT,WAAY,KACZW,UAAU,IAItBX,WAAWY,KAAKP,OAChBrD,iBAAmBmD,kBAEjBU,YAAcjC,SAAS5B,iBAAiBhB,QAAQC,YAClD4E,cAAgBd,kBAEhBC,WAAWY,KAAK,CACZN,iBAAkBP,iBAClBQ,mBAAoBM,YACpB/D,OAAQE,iBAAiBhB,QAAQc,OACjC2D,aAAczD,iBAAiBhB,QAAQyE,aACvCT,WAAY,KACZc,WAAW,WAIhBd,+BAQSe,SAEVjB,KAAOiB,EAAEnF,WACVkE,eAIsC,SAAvCA,KAAKkB,aAAa,wBAKtBlB,KAAKmB,aAAa,iBAAiB,OAG/BC,SAAW,CAACC,SAAU,IACTtF,SAAS4D,iBAAiB9E,KAAKvB,UAAUM,SACjDwG,SAAQjC,gBACPmD,YAAc,CAChBC,YAAapD,QAAQjC,QAAQqF,YAC7BrB,WAAY,GACZsB,eAAe,GAEnBF,YAAYpB,WAAarF,KAAKkF,uBAAuB5B,QAASW,SAASkB,KAAK9D,QAAQC,aACpFmF,YAAYE,cAAgBF,YAAYpB,WAAWN,OAAS,EAC5DwB,SAASC,SAASP,KAAKQ,sBAGrBG,YAAcC,eAAMzI,OAAO,CAC7B0I,OAAO,mBAAU,eAAgB,yBAA0B3B,KAAK9D,QAAQyE,cACxEiB,KAAM7D,mBAAU8D,OAAO,2CAA4CT,UACnEU,OAAQ,GACRC,MAAM,EACNC,OAAO,IAGXP,MAAMQ,UAAU,GAAGrH,iBAAiB,SAASqG,UACnCnF,OAASmF,EAAEnF,OAAOS,QAAQ1B,KAAKvB,UAAUO,qBAC1CiC,0CAGWuB,aAAavB,OAAOI,QAAQsE,iBAAkB1E,OAAOI,QAAQc,OAAQlB,OAAOI,QAAQuE,oBACpGgB,MAAMtG,cAEV6E,KAAKmB,aAAa,iBAAiB,mCAalB/H,QAACA,sBACdA,QAAQ4D,SAAW8B,SAASjE,KAAKC,aAAaoB,QAAQC,aAG1CtB,KAAKC,WAAWD,KAAKvB,UAAUC,eAFpC,KAOJsB,KAAKkE,gBAAgB,CACxB5C,WAAY/C,QAAQ4D,OACpBmD,SAAU,CACN/G,QAAQ6E"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categorylist.min.js b/question/bank/managecategories/amd/build/categorylist.min.js new file mode 100644 index 0000000000000..543ea985c2906 --- /dev/null +++ b/question/bank/managecategories/amd/build/categorylist.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/categorylist",["exports","core/reactive","core/templates","core/str","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_templates,_str,_categorymanager){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};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",CATEGORY_DETAILS:".qbank_managecategories-details",CATEGORY_NO_DRAGHANDLE:".qbank_managecategories-item[data-categoryid]:not(.draghandle)",CATEGORY_ID:id=>"#category-".concat(id)},this.classes={DROP_TARGET_BEFORE:"qbank_managecategories-droptarget-before",DROP_TARGET:"qbank_managecategories-droptarget",NO_BOTTOM_PADDING:"pb-0"},this.ids={CATEGORY:id=>"category-".concat(id)}}stateReady(){this.dragdrop=new _reactive.DragDrop(this)}destroy(){void 0!==this.dragdrop&&(this.dragdrop.unregister(),this.dragdrop=void 0)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}validateDropData(){return!0}showDropZone(dropData,event){const dropTarget=this.getElement();return!dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))&&(this.getInsertBefore(event,dropTarget)?(dropTarget.classList.add(this.classes.DROP_TARGET_BEFORE),dropTarget.classList.remove(this.classes.DROP_TARGET)):(dropTarget.classList.add(this.classes.DROP_TARGET),dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE)),!0)}hideDropZone(dropData,event){const dropTarget=event.target.closest(this.selectors.CATEGORY_LIST);dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE),dropTarget.classList.remove(this.classes.DROP_TARGET)}getInsertBefore(event,dropTarget){return event.clientY-dropTarget.getBoundingClientRect().top.\n\n/**\n * The category list component.\n *\n * The category list is a drop target, so that a category may be dropped at the top or bottom of the list.\n *\n * @module qbank_managecategories/categorylist\n * @class qbank_managecategories/categorylist\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\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 CATEGORY_DETAILS: '.qbank_managecategories-details',\n CATEGORY_NO_DRAGHANDLE: '.qbank_managecategories-item[data-categoryid]:not(.draghandle)',\n CATEGORY_ID: id => `#category-${id}`,\n };\n this.classes = {\n DROP_TARGET_BEFORE: 'qbank_managecategories-droptarget-before',\n DROP_TARGET: 'qbank_managecategories-droptarget',\n NO_BOTTOM_PADDING: 'pb-0',\n };\n this.ids = {\n CATEGORY: id => `category-${id}`,\n };\n }\n\n stateReady() {\n this.dragdrop = new DragDrop(this);\n }\n\n destroy() {\n // The draggable element must be unregistered.\n if (this.dragdrop !== undefined) {\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 validateDropData() {\n return true;\n }\n\n /**\n * Highlight the border of the list where the category will be moved.\n *\n * If dropping at the top of the list, highlight the top border.\n * If dropping at the bottom, highlight the bottom border.\n *\n * @param {Object} dropData\n * @param {Event} event\n */\n showDropZone(dropData, event) {\n const dropTarget = this.getElement();\n if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {\n // Can't drop onto its own child.\n return false;\n }\n if (this.getInsertBefore(event, dropTarget)) {\n dropTarget.classList.add(this.classes.DROP_TARGET_BEFORE);\n dropTarget.classList.remove(this.classes.DROP_TARGET);\n } else {\n dropTarget.classList.add(this.classes.DROP_TARGET);\n dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE);\n }\n return true;\n }\n\n /**\n * Remove highlighting.\n *\n * @param {Object} dropData\n * @param {Event} event\n */\n hideDropZone(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST);\n dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE);\n dropTarget.classList.remove(this.classes.DROP_TARGET);\n }\n\n /**\n * Determine whether we're dragging over the top or bottom half of the list.\n *\n * @param {Event} event\n * @param {Element} dropTarget\n * @return {boolean}\n */\n getInsertBefore(event, dropTarget) {\n // Get the current mouse position within the drop target\n const mouseY = event.clientY - dropTarget.getBoundingClientRect().top;\n\n // Get the height of the drop target\n const targetHeight = dropTarget.clientHeight;\n\n // Check if the mouse is over the top half of the drop target\n return mouseY < targetHeight / 2;\n }\n\n /**\n * Find the new position of the dropped category, and trigger the move.\n *\n * @param {Object} dropData\n * @param {Event} event\n */\n drop(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST);\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.categoryid;\n let precedingSibling;\n\n if (this.getInsertBefore(event, dropTarget)) {\n // Dropped at the top of the list.\n precedingSibling = null;\n } else {\n // Dropped at the bottom of the list.\n precedingSibling = dropTarget.lastElementChild;\n }\n\n // Insert the category after the target category\n categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);\n }\n\n /**\n * Watch for categories moving to a new parent.\n *\n * @return {Array} A list of watchers.\n */\n getWatchers() {\n return [\n // Watch for this category having its child count updated.\n {watch: `categoryLists[${this.element.dataset.categoryid}].childCount:updated`, handler: this.checkEmptyList},\n // Watch for any new category being created.\n {watch: `categories:created`, handler: this.addCategory},\n ];\n }\n\n /**\n * If this list is now empty, remove it.\n *\n * @param {Object} args\n * @param {Object} args.element The categoryList state element.\n */\n async checkEmptyList({element}) {\n if (element.childCount === 0) {\n // Display a new child drop zone.\n const categoryItem = this.getElement().closest(this.selectors.CATEGORY_ITEM);\n const {html, js} = await Templates.renderForPromise(\n 'qbank_managecategories/newchild',\n {\n categoryid: this.getElement().dataset.categoryid,\n tooltip: getString('newchild', 'qbank_managecategories', categoryItem.dataset.categoryname)\n }\n );\n const activityNameArea = categoryItem.querySelector(this.selectors.CATEGORY_DETAILS);\n await Templates.appendNodeContents(activityNameArea, html, js);\n // Reinstate padding on the parent element.\n this.element.closest(this.selectors.CATEGORY_CONTENTS).classList.remove(this.classes.NO_BOTTOM_PADDING);\n // Remove this list.\n this.remove();\n }\n }\n\n /**\n * If a newly-created category has this list's category as its parent, add it to this list.\n *\n * @param {Object} args\n * @param {Object} args.element\n * @return {Promise}\n */\n async addCategory({element}) {\n if (element.parent !== parseInt(this.getElement().dataset.categoryid)) {\n return; // Not for me.\n }\n const {html, js} = await Templates.renderForPromise('qbank_managecategories/category', element.templatecontext);\n Templates.appendNodeContents(this.getElement(), html, js);\n // If one of the children has no draghandle, it should do now it has a sibling.\n const noDragHandle = this.getElement(this.selectors.CATEGORY_NO_DRAGHANDLE);\n if (noDragHandle) {\n this.reactive.dispatch('showDragHandle', noDragHandle.dataset.categoryid);\n }\n }\n}"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","CATEGORY_LIST","CATEGORY_ITEM","CATEGORY_CONTENTS","CATEGORY_DETAILS","CATEGORY_NO_DRAGHANDLE","CATEGORY_ID","classes","DROP_TARGET_BEFORE","DROP_TARGET","NO_BOTTOM_PADDING","ids","CATEGORY","stateReady","dragdrop","DragDrop","this","destroy","undefined","unregister","target","document","querySelector","reactive","categorymanager","validateDropData","showDropZone","dropData","event","dropTarget","getElement","closest","getInsertBefore","classList","add","remove","hideDropZone","clientY","getBoundingClientRect","top","clientHeight","drop","getElementById","targetParentId","dataset","categoryid","precedingSibling","lastElementChild","moveCategory","_precedingSibling","getWatchers","watch","handler","checkEmptyList","addCategory","childCount","categoryItem","html","js","Templates","renderForPromise","tooltip","categoryname","activityNameArea","appendNodeContents","parent","parseInt","templatecontext","noDragHandle","dispatch"],"mappings":"0XA6B6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,cAAe,uCACfC,cAAe,gDACfC,kBAAmB,4CACnBC,iBAAkB,kCAClBC,uBAAwB,iEACxBC,YAAaP,wBAAmBA,UAE/BQ,QAAU,CACXC,mBAAoB,2CACpBC,YAAa,oCACbC,kBAAmB,aAElBC,IAAM,CACPC,SAAUb,uBAAkBA,KAIpCc,kBACSC,SAAW,IAAIC,mBAASC,MAGjCC,eAE0BC,IAAlBF,KAAKF,gBACAA,SAASK,kBACTL,cAAWI,eAWZE,OAAQpB,kBACT,IAAIgB,KAAK,CACZlB,QAASuB,SAASC,cAAcF,QAChCpB,UAAAA,UACAuB,SAAUC,mCAIlBC,0BACW,EAYXC,aAAaC,SAAUC,aACbC,WAAab,KAAKc,oBACpBD,WAAWE,QAAQf,KAAKhB,UAAUM,YAAYqB,SAAS5B,OAIvDiB,KAAKgB,gBAAgBJ,MAAOC,aAC5BA,WAAWI,UAAUC,IAAIlB,KAAKT,QAAQC,oBACtCqB,WAAWI,UAAUE,OAAOnB,KAAKT,QAAQE,eAEzCoB,WAAWI,UAAUC,IAAIlB,KAAKT,QAAQE,aACtCoB,WAAWI,UAAUE,OAAOnB,KAAKT,QAAQC,sBAEtC,GASX4B,aAAaT,SAAUC,aACbC,WAAaD,MAAMR,OAAOW,QAAQf,KAAKhB,UAAUC,eACvD4B,WAAWI,UAAUE,OAAOnB,KAAKT,QAAQC,oBACzCqB,WAAWI,UAAUE,OAAOnB,KAAKT,QAAQE,aAU7CuB,gBAAgBJ,MAAOC,mBAEJD,MAAMS,QAAUR,WAAWS,wBAAwBC,IAG7CV,WAAWW,aAGD,EASnCC,KAAKd,SAAUC,mCACLC,WAAaD,MAAMR,OAAOW,QAAQf,KAAKhB,UAAUC,mBAElD4B,qBAIDA,WAAWE,QAAQf,KAAKhB,UAAUM,YAAYqB,SAAS5B,gBAK5CsB,SAASqB,eAAe1B,KAAKL,IAAIC,SAASe,SAAS5B,kBAM5D4C,eAAiBd,WAAWe,QAAQC,eACtCC,iBAIAA,iBAFA9B,KAAKgB,gBAAgBJ,MAAOC,YAET,KAGAA,WAAWkB,kDAIlBC,aAAarB,SAAS5B,GAAI4C,yCAAgBG,qDAAAG,kBAAkBL,QAAQC,YAQxFK,oBACW,CAEH,CAACC,8BAAwBnC,KAAKlB,QAAQ8C,QAAQC,mCAAkCO,QAASpC,KAAKqC,gBAE9F,CAACF,2BAA6BC,QAASpC,KAAKsC,6CAU/BxD,QAACA,iBACS,IAAvBA,QAAQyD,WAAkB,OAEpBC,aAAexC,KAAKc,aAAaC,QAAQf,KAAKhB,UAAUE,gBACxDuD,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAC/B,kCACA,CACIf,WAAY7B,KAAKc,aAAac,QAAQC,WACtCgB,SAAS,kBAAU,WAAY,yBAA0BL,aAAaZ,QAAQkB,gBAGhFC,iBAAmBP,aAAalC,cAAcN,KAAKhB,UAAUI,wBAC7DuD,mBAAUK,mBAAmBD,iBAAkBN,KAAMC,SAEtD5D,QAAQiC,QAAQf,KAAKhB,UAAUG,mBAAmB8B,UAAUE,OAAOnB,KAAKT,QAAQG,wBAEhFyB,uCAWKrC,QAACA,kBACXA,QAAQmE,SAAWC,SAASlD,KAAKc,aAAac,QAAQC,yBAGpDY,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,kCAAmC9D,QAAQqE,oCACrFH,mBAAmBhD,KAAKc,aAAc2B,KAAMC,UAEhDU,aAAepD,KAAKc,WAAWd,KAAKhB,UAAUK,wBAChD+D,mBACK7C,SAAS8C,SAAS,iBAAkBD,aAAaxB,QAAQC"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categorymanager.min.js b/question/bank/managecategories/amd/build/categorymanager.min.js new file mode 100644 index 0000000000000..121559599d9a2 --- /dev/null +++ b/question/bank/managecategories/amd/build/categorymanager.min.js @@ -0,0 +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_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;const categoryItem=e.target.closest(SELECTORS_CATEGORY_ITEM);categoryItem&&(contextid=categoryItem.dataset.contextid,categoryid=categoryItem.dataset.categoryid,sortorder=categoryItem.dataset.sortorder);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},modalConfig:{title:title,large:!0},saveButtonText:save,returnFocus:addEditButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(response=>{categorymanager.stateManager.processUpdates(response.detail)})),modalForm.show()}}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)}})); + +//# sourceMappingURL=categorymanager.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categorymanager.min.js.map b/question/bank/managecategories/amd/build/categorymanager.min.js.map new file mode 100644 index 0000000000000..0e0706a35a466 --- /dev/null +++ b/question/bank/managecategories/amd/build/categorymanager.min.js.map @@ -0,0 +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 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 }\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 },\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\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","categoryItem","modalForm","ModalForm","formClass","modalConfig","large","saveButtonText","returnFocus","addEventListener","events","FORM_SUBMITTED","response","detail","show","name","eventName","eventTypes","qbankManagecategoriesStateUpdated","eventDispatch","notifyQbankManagecategoriesStateUpdated","mutations","async","stateData","querySelector","showdescriptions","checked","categories","categoryLists","querySelectorAll","forEach","item","push","id","categoryname","parent","draghandle","classList","contains","categoryList","childCount","length","reactive","setInitialState","loadState"],"mappings":"ymBA6BMA,wBACa,uCADbA,wBAGa,gDAHbA,wBAIa,gBAJbA,kCAKuB,2BALvBA,0BAMe,kCAGfC,mBACU,aADVA,eAEM,qBA8CNC,wBAAwBC,mBAY1BC,aACIC,WACAC,oBACAC,0EAAqB,WAEfC,KAAO,CACTC,WAAY,uCACZC,KAAM,CACFC,cAAeC,KAAKC,MAAMC,KAAKC,UAC/BC,WAAYX,WACZY,eAAgBX,eAChBY,mBAAoBX,mCAGvBC,KAAK,CAACA,OAAO,GACbW,MAAMC,oBACEC,aAAaC,eAAeF,cAC1BA,gBAEVG,OAAMC,wDACUC,gBAAgB,CACzBC,QAASF,MAAME,QACfC,KAAM,wCAEVC,SAASC,uBAAuB5B,gBAAgB,2DAAI6B,oBAUhEC,SAASC,eACE,mBAAUA,OAAS,eAAiB,cAAe,YAS9DC,QAAQD,eACGA,QAAS,mBAAU,cAAe,SAAU,mBAAU,cAAe,YAQhFE,cAAcC,SACJC,cAAgBD,EAAEE,OAAOC,QAAQtC,+BAGlCoC,yBAKAA,cAAcG,QAAQC,kBAI3BL,EAAEM,uBAEIC,MAAQC,gBAAgBZ,SAA8C,SAArCK,cAAcG,QAAQC,YACvDI,KAAOD,gBAAgBV,QAA6C,SAArCG,cAAcG,QAAQC,YACrDK,KAAOT,cAAcG,QAAQM,KAC7BC,SAAWV,cAAcG,QAAQO,SACjCC,cAAgBX,cAAcG,QAAQQ,kBACxChC,UAAYqB,cAAcG,QAAQxB,UAClCC,WAAa,KACbgC,UAAY,WACVC,aAAed,EAAEE,OAAOC,QAAQtC,yBAClCiD,eACAlC,UAAYkC,aAAaV,QAAQxB,UACjCC,WAAaiC,aAAaV,QAAQvB,WAClCgC,UAAYC,aAAaV,QAAQS,iBAI/BE,UAAY,IAAIC,mBAAU,CAC5BC,UAAW,4DACX1C,KAAM,CACFmC,KAAAA,KACAC,SAAAA,SACAC,cAAAA,cACAhC,UAAAA,UACAC,WAAAA,WACAgC,UAAAA,WAEJK,YAAa,CACTX,MAAOA,MACPY,OAAO,GAEXC,eAAgBX,KAChBY,YAAapB,gBAIjBc,UAAUO,iBAAiBP,UAAUQ,OAAOC,gBAAiBC,WACzDjB,gBAAgBtB,aAAaC,eAAesC,SAASC,WAGzDX,UAAUY,cAILnB,gBAAkB,IAAIzC,gBAAgB,CAC/C6D,KAAM,yCACNC,UAAWC,mBAAWC,kCACtBC,cAAeC,gDACfC,UAAAA,8EAMgB,KAvKFC,OAAAA,iBAERC,UAAY,CACdzD,KAAM,CACFC,UAHYa,SAAS4C,cAAcxE,yBAGZuC,QAAQxB,UAC/B0D,iBAAkB7C,SAAS4C,cAAcxE,mCAAmC0E,SAEhFC,WAAY,GACZC,cAAe,IAEDhD,SAASiD,iBAAiB7E,yBAClC8E,SAAQC,OACdR,UAAUI,WAAWK,KAAK,CACtBC,GAAIF,KAAKxC,QAAQvB,WACjB+C,KAAMgB,KAAKxC,QAAQ2C,aACnBC,OAAQJ,KAAKxC,QAAQ4C,OACrBpE,UAAWgE,KAAKxC,QAAQxB,UACxBiC,UAAW+B,KAAKxC,QAAQS,UACxBoC,WAAYL,KAAKM,UAAUC,SAASrF,yBAGtB2B,SAASiD,iBAAiB7E,yBAClC8E,SAAQS,eAClBhB,UAAUK,cAAcI,KAAK,CACzBC,GAAIM,aAAahD,QAAQvB,WACzBwE,WAAYD,aAAaV,iBAAiB7E,yBAAyByF,YAG3EC,SAASC,gBAAgBpB,YA4IzBqB,CAAUjD"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categoryroot.min.js b/question/bank/managecategories/amd/build/categoryroot.min.js new file mode 100644 index 0000000000000..e5813de926047 --- /dev/null +++ b/question/bank/managecategories/amd/build/categoryroot.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/categoryroot",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.classes={SHOWDESCRIPTIONS:"showdescriptions"}}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}getWatchers(){return[{watch:"page.showdescriptions:updated",handler:this.toggleDescriptions}]}toggleDescriptions(_ref){let{element:element}=_ref;element.showdescriptions?this.getElement().classList.add(this.classes.SHOWDESCRIPTIONS):this.getElement().classList.remove(this.classes.SHOWDESCRIPTIONS)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=categoryroot.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/categoryroot.min.js.map b/question/bank/managecategories/amd/build/categoryroot.min.js.map new file mode 100644 index 0000000000000..707491987fc05 --- /dev/null +++ b/question/bank/managecategories/amd/build/categoryroot.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"categoryroot.min.js","sources":["../src/categoryroot.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 root component.\n *\n * @module qbank_managecategories/categoryroot\n * @class qbank_managecategories/categoryroot\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.classes = {\n SHOWDESCRIPTIONS: 'showdescriptions',\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 * Watch for changes to the page state.\n *\n * @return {Array} A list of watchers.\n */\n getWatchers() {\n return [\n // Watch for descriptions being toggled.\n {watch: `page.showdescriptions:updated`, handler: this.toggleDescriptions}\n ];\n }\n\n /**\n * Show or hide descriptions when the flag in the state is changed.\n *\n * @param {Object} args\n * @param {Object} args.element The updated page state.\n */\n toggleDescriptions({element}) {\n if (element.showdescriptions) {\n this.getElement().classList.add(this.classes.SHOWDESCRIPTIONS);\n } else {\n this.getElement().classList.remove(this.classes.SHOWDESCRIPTIONS);\n }\n }\n}"],"names":["BaseComponent","create","descriptor","name","element","id","classes","SHOWDESCRIPTIONS","target","selectors","this","document","querySelector","reactive","categorymanager","getWatchers","watch","handler","toggleDescriptions","showdescriptions","getElement","classList","add","remove"],"mappings":"wQAyB6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,QAAU,CACXC,iBAAkB,gCAWdC,OAAQC,kBACT,IAAIC,KAAK,CACZN,QAASO,SAASC,cAAcJ,QAChCC,UAAAA,UACAI,SAAUC,mCASlBC,oBACW,CAEH,CAACC,sCAAwCC,QAASP,KAAKQ,qBAU/DA,6BAAmBd,QAACA,cACZA,QAAQe,sBACHC,aAAaC,UAAUC,IAAIZ,KAAKJ,QAAQC,uBAExCa,aAAaC,UAAUE,OAAOb,KAAKJ,QAAQC"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/events.min.js b/question/bank/managecategories/amd/build/events.min.js new file mode 100644 index 0000000000000..188d676a04569 --- /dev/null +++ b/question/bank/managecategories/amd/build/events.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyQbankManagecategoriesStateUpdated=_exports.eventTypes=void 0;const eventTypes={qbankManagecategoriesStateUpdated:"qbank_managecategories/stateUpdated"};_exports.eventTypes=eventTypes;_exports.notifyQbankManagecategoriesStateUpdated=(detail,container)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.qbankManagecategoriesStateUpdated,detail,container)})); + +//# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/events.min.js.map b/question/bank/managecategories/amd/build/events.min.js.map new file mode 100644 index 0000000000000..4e0e15b4e7b3a --- /dev/null +++ b/question/bank/managecategories/amd/build/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"events.min.js","sources":["../src/events.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 {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Reactive events for qbank_managecategories\n *\n * @module qbank_managecategories/events\n *\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event qbankManagecategoriesStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qbankManagecategoriesStateUpdated: 'qbank_managecategories/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyQbankManagecategoriesStateUpdated\n * @param {Object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires qbankManagecategoriesStateUpdated\n */\nexport const notifyQbankManagecategoriesStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qbankManagecategoriesStateUpdated, detail, container);\n};\n"],"names":["eventTypes","qbankManagecategoriesStateUpdated","detail","container"],"mappings":"2PAuBaA,WAAa,CAQtBC,kCAAmC,uHAYgB,CAACC,OAAQC,aACrD,mCAAcH,WAAWC,kCAAmCC,OAAQC"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/mutations.min.js b/question/bank/managecategories/amd/build/mutations.min.js new file mode 100644 index 0000000000000..318b51fb7c34d --- /dev/null +++ b/question/bank/managecategories/amd/build/mutations.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/mutations",["exports","core_user/repository"],(function(_exports,_repository){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.mutations=void 0;const mutations=new class{async toggleDescriptions(stateManager,showDescriptions){stateManager.setReadOnly(!1),await(0,_repository.setUserPreference)("qbank_managecategories_showdescriptions",showDescriptions),stateManager.state.page.showdescriptions=showDescriptions,stateManager.setReadOnly(!0)}async showDragHandle(stateManager,categoryId){stateManager.setReadOnly(!1),stateManager.state.categories.get(categoryId).draghandle=!0,stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); + +//# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/mutations.min.js.map b/question/bank/managecategories/amd/build/mutations.min.js.map new file mode 100644 index 0000000000000..803366df6ade8 --- /dev/null +++ b/question/bank/managecategories/amd/build/mutations.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mutations.min.js","sources":["../src/mutations.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 {setUserPreference} from 'core_user/repository';\n\n/**\n * Default mutation manager\n *\n * @module qbank_managecategories/mutations\n */\nclass Mutations {\n /**\n * Set the showdescriptions property to true or false in the page's state.\n *\n * @param {stateManager} stateManager\n * @param {boolean} showDescriptions\n * @return {Promise}\n */\n async toggleDescriptions(stateManager, showDescriptions) {\n stateManager.setReadOnly(false);\n await setUserPreference('qbank_managecategories_showdescriptions', showDescriptions);\n stateManager.state.page.showdescriptions = showDescriptions;\n stateManager.setReadOnly(true);\n }\n\n /**\n * Set the draghandle property to true in a given category's state.\n *\n * @param {stateManager} stateManager\n * @param {Number} categoryId\n * @return {Promise}\n */\n async showDragHandle(stateManager, categoryId) {\n stateManager.setReadOnly(false);\n stateManager.state.categories.get(categoryId).draghandle = true;\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","stateManager","showDescriptions","setReadOnly","state","page","showdescriptions","categoryId","categories","get","draghandle"],"mappings":"qMAmDaA,UAAY,mCArBIC,aAAcC,kBACnCD,aAAaE,aAAY,SACnB,iCAAkB,0CAA2CD,kBACnED,aAAaG,MAAMC,KAAKC,iBAAmBJ,iBAC3CD,aAAaE,aAAY,wBAURF,aAAcM,YAC/BN,aAAaE,aAAY,GACzBF,aAAaG,MAAMI,WAAWC,IAAIF,YAAYG,YAAa,EAC3DT,aAAaE,aAAY"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/newchild.min.js b/question/bank/managecategories/amd/build/newchild.min.js new file mode 100644 index 0000000000000..c62fee98e5ba9 --- /dev/null +++ b/question/bank/managecategories/amd/build/newchild.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/newchild",["exports","core/reactive","jquery","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_jquery,_categorymanager){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={NEW_CHILD:".qbank_managecategories-newchild",CATEGORY_ID:id=>"#category-".concat(id)},this.classes={DROP_TARGET:"qbank_managecategories-droptarget"},this.ids={CATEGORY:id=>"category-".concat(id)}}stateReady(){this.dragdrop=new _reactive.DragDrop(this)}destroy(){void 0!==this.dragdrop&&(this.dragdrop.unregister(),this.dragdrop=void 0)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}validateDropData(dropData){return!this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))}showDropZone(dropData,event){const dropTarget=event.target.closest(this.selectors.NEW_CHILD);dropTarget.classList.add(this.classes.DROP_TARGET),(0,_jquery.default)(dropTarget).tooltip("show")}hideDropZone(dropData,event){const dropTarget=event.target.closest(this.selectors.NEW_CHILD);dropTarget.classList.remove(this.classes.DROP_TARGET),(0,_jquery.default)(dropTarget).tooltip("hide")}drop(dropData,event){const dropTarget=event.target.closest(this.selectors.NEW_CHILD);if(!dropTarget)return;if(!document.getElementById(this.ids.CATEGORY(dropData.id)))return;const targetParentId=dropTarget.dataset.parent;_categorymanager.categorymanager.moveCategory(dropData.id,targetParentId)}getWatchers(){return[{watch:"categories.parent:updated",handler:this.checkNewChild}]}checkNewChild(_ref){let{element:element}=_ref;element.parent===parseInt(this.element.dataset.parent)&&this.remove()}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=newchild.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/newchild.min.js.map b/question/bank/managecategories/amd/build/newchild.min.js.map new file mode 100644 index 0000000000000..7480f17bcfa44 --- /dev/null +++ b/question/bank/managecategories/amd/build/newchild.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"newchild.min.js","sources":["../src/newchild.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 newchild component.\n *\n * This is a drop target for moving a category to an as-yet-nonexistant child list under another category.\n *\n * @module qbank_managecategories/newchild\n * @class qbank_managecategories/newchild\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\nimport $ from 'jquery';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n NEW_CHILD: '.qbank_managecategories-newchild',\n CATEGORY_ID: id => `#category-${id}`\n };\n this.classes = {\n DROP_TARGET: 'qbank_managecategories-droptarget',\n };\n this.ids = {\n CATEGORY: id => `category-${id}`,\n };\n }\n\n stateReady() {\n this.dragdrop = new DragDrop(this);\n }\n\n destroy() {\n // The draggable element must be unregistered.\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n this.dragdrop = undefined;\n }\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\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 const targetElement = document.querySelector(target);\n return new this({\n element: targetElement,\n selectors,\n reactive: categorymanager,\n });\n }\n\n /**\n * Cannot drop a category as a new child of its own descendant.\n *\n * @param {Object} dropData\n * @return {boolean}\n */\n validateDropData(dropData) {\n if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) {\n return false;\n }\n return true;\n }\n\n showDropZone(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.NEW_CHILD);\n dropTarget.classList.add(this.classes.DROP_TARGET);\n $(dropTarget).tooltip('show');\n }\n\n hideDropZone(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.NEW_CHILD);\n dropTarget.classList.remove(this.classes.DROP_TARGET);\n $(dropTarget).tooltip('hide');\n }\n\n drop(dropData, event) {\n const dropTarget = event.target.closest(this.selectors.NEW_CHILD);\n\n if (!dropTarget) {\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\n // Insert the category as the first child of the new parent.\n categorymanager.moveCategory(dropData.id, targetParentId);\n }\n\n /**\n * Watch for categories moving to a new parent.\n *\n * @return {Array} A list of watchers.\n */\n getWatchers() {\n return [\n // Watch for any category having its parent changed.\n {watch: `categories.parent:updated`, handler: this.checkNewChild},\n ];\n }\n\n /**\n * If an element now has this category as the parent, remove this new child target.\n *\n * @param {Object} args\n * @param {Element} args.element\n */\n checkNewChild({element}) {\n if (element.parent === parseInt(this.element.dataset.parent)) {\n this.remove();\n }\n }\n}"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","NEW_CHILD","CATEGORY_ID","classes","DROP_TARGET","ids","CATEGORY","stateReady","dragdrop","DragDrop","this","destroy","undefined","unregister","target","document","querySelector","reactive","categorymanager","validateDropData","dropData","getElement","closest","showDropZone","event","dropTarget","classList","add","tooltip","hideDropZone","remove","drop","getElementById","targetParentId","dataset","parent","moveCategory","getWatchers","watch","handler","checkNewChild","parseInt"],"mappings":"qVA4B6BA,wBACzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,UAAW,mCACXC,YAAaH,wBAAmBA,UAE/BI,QAAU,CACXC,YAAa,0CAEZC,IAAM,CACPC,SAAUP,uBAAkBA,KAIpCQ,kBACSC,SAAW,IAAIC,mBAASC,MAGjCC,eAE0BC,IAAlBF,KAAKF,gBACAA,SAASK,kBACTL,cAAWI,eAWZE,OAAQd,kBAET,IAAIU,KAAK,CACZZ,QAFkBiB,SAASC,cAAcF,QAGzCd,UAAAA,UACAiB,SAAUC,mCAUlBC,iBAAiBC,iBACTV,KAAKW,aAAaC,QAAQZ,KAAKV,UAAUE,YAAYkB,SAASrB,KAMtEwB,aAAaH,SAAUI,aACbC,WAAaD,MAAMV,OAAOQ,QAAQZ,KAAKV,UAAUC,WACvDwB,WAAWC,UAAUC,IAAIjB,KAAKP,QAAQC,iCACpCqB,YAAYG,QAAQ,QAG1BC,aAAaT,SAAUI,aACbC,WAAaD,MAAMV,OAAOQ,QAAQZ,KAAKV,UAAUC,WACvDwB,WAAWC,UAAUI,OAAOpB,KAAKP,QAAQC,iCACvCqB,YAAYG,QAAQ,QAG1BG,KAAKX,SAAUI,aACLC,WAAaD,MAAMV,OAAOQ,QAAQZ,KAAKV,UAAUC,eAElDwB,sBAIUV,SAASiB,eAAetB,KAAKL,IAAIC,SAASc,SAASrB,kBAM5DkC,eAAiBR,WAAWS,QAAQC,wCAG1BC,aAAahB,SAASrB,GAAIkC,gBAQ9CI,oBACW,CAEH,CAACC,kCAAoCC,QAAS7B,KAAK8B,gBAU3DA,wBAAc1C,QAACA,cACPA,QAAQqC,SAAWM,SAAS/B,KAAKZ,QAAQoC,QAAQC,cAC5CL"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/showdescriptions.min.js b/question/bank/managecategories/amd/build/showdescriptions.min.js new file mode 100644 index 0000000000000..1c79c7a045499 --- /dev/null +++ b/question/bank/managecategories/amd/build/showdescriptions.min.js @@ -0,0 +1,3 @@ +define("qbank_managecategories/showdescriptions",["exports","core/reactive","qbank_managecategories/categorymanager"],(function(_exports,_reactive,_categorymanager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;class _default extends _reactive.BaseComponent{create(descriptor){this.name=descriptor.element.id,this.selectors={TOGGLE:"#showdescriptions-toggle"}}stateReady(){this.addEventListener(this.getElement(this.selectors.TOGGLE),"change",this.updateShowDescriptions)}static init(target,selectors){return new this({element:document.querySelector(target),selectors:selectors,reactive:_categorymanager.categorymanager})}async updateShowDescriptions(event){const checked=event.target.checked;this.reactive.dispatch("toggleDescriptions",checked)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=showdescriptions.min.js.map \ No newline at end of file diff --git a/question/bank/managecategories/amd/build/showdescriptions.min.js.map b/question/bank/managecategories/amd/build/showdescriptions.min.js.map new file mode 100644 index 0000000000000..eabc750b7091b --- /dev/null +++ b/question/bank/managecategories/amd/build/showdescriptions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"showdescriptions.min.js","sources":["../src/showdescriptions.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 show descriptions toggle component.\n *\n * @module qbank_managecategories/showdescriptions\n * @class qbank_managecategories/showdescriptions\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {categorymanager} from 'qbank_managecategories/categorymanager';\n\nexport default class extends BaseComponent {\n\n create(descriptor) {\n this.name = descriptor.element.id;\n this.selectors = {\n TOGGLE: '#showdescriptions-toggle',\n };\n }\n\n stateReady() {\n this.addEventListener(this.getElement(this.selectors.TOGGLE), 'change', this.updateShowDescriptions);\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 const targetElement = document.querySelector(target);\n return new this({\n element: targetElement,\n selectors,\n reactive: categorymanager,\n });\n }\n\n /**\n * Dispatch a mutation to toggle the showDescriptions setting.\n *\n * @param {Event} event The toggle change event.\n * @return {Promise}\n */\n async updateShowDescriptions(event) {\n const checked = event.target.checked;\n this.reactive.dispatch('toggleDescriptions', checked);\n }\n}\n"],"names":["BaseComponent","create","descriptor","name","element","id","selectors","TOGGLE","stateReady","addEventListener","this","getElement","updateShowDescriptions","target","document","querySelector","reactive","categorymanager","event","checked","dispatch"],"mappings":"4QAyB6BA,wBAEzBC,OAAOC,iBACEC,KAAOD,WAAWE,QAAQC,QAC1BC,UAAY,CACbC,OAAQ,4BAIhBC,kBACSC,iBAAiBC,KAAKC,WAAWD,KAAKJ,UAAUC,QAAS,SAAUG,KAAKE,oCAUrEC,OAAQP,kBAET,IAAII,KAAK,CACZN,QAFkBU,SAASC,cAAcF,QAGzCP,UAAAA,UACAU,SAAUC,gEAUWC,aACnBC,QAAUD,MAAML,OAAOM,aACxBH,SAASI,SAAS,qBAAsBD"} \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/addcategory.js b/question/bank/managecategories/amd/src/addcategory.js new file mode 100644 index 0000000000000..207f400124ddc --- /dev/null +++ b/question/bank/managecategories/amd/src/addcategory.js @@ -0,0 +1,56 @@ +// 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 . + +/** + * Add category button for displaying the modal form. + * + * This just connects up the button to the showEditModal listener. + * + * @module qbank_managecategories/addcategory + * @class qbank_managecategories/addcategory + */ + +import {BaseComponent} from 'core/reactive'; +import {categorymanager} from 'qbank_managecategories/categorymanager'; + +export default class extends BaseComponent { + + create(descriptor) { + this.name = descriptor.element.id; + this.selectors = { + ADD_BUTTON: '[data-action="addeditcategory"]', + }; + } + + stateReady() { + this.addEventListener(this.getElement(this.selectors.ADD_BUTTON), 'click', categorymanager.showEditModal); + } + + /** + * Static method to create a component instance form the mustahce template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + const targetElement = document.querySelector(target); + return new this({ + element: targetElement, + selectors, + reactive: categorymanager, + }); + } +} diff --git a/question/bank/managecategories/amd/src/category.js b/question/bank/managecategories/amd/src/category.js new file mode 100644 index 0000000000000..70a49dcf4693f --- /dev/null +++ b/question/bank/managecategories/amd/src/category.js @@ -0,0 +1,469 @@ +// 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 . + +/** + * The category component. + * + * @module qbank_managecategories/category + * @class qbank_managecategories/category + */ + +import {BaseComponent, DragDrop} from 'core/reactive'; +import {categorymanager} from 'qbank_managecategories/categorymanager'; +import Templates from 'core/templates'; +import Modal from "core/modal"; +import {get_string as getString} from "core/str"; + +export default class extends 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"]', + 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-${id}`, + CONTENT_CONTAINER: id => `#category-${id} .qbank_managecategories-childlistcontainer`, + CHILD_LIST: id => `ul[data-categoryid="${id}"]`, + PREVIOUS_SIBLING: sortorder => `:scope > [data-sortorder="${sortorder}"]`, + }; + this.classes = { + NO_BOTTOM_PADDING: 'pb-0', + DRAGHANDLE: 'draghandle', + DROPTARGET: 'qbank_managecategories-droptarget-before', + }; + this.ids = { + CATEGORY: id => `category-${id}`, + }; + } + + stateReady() { + this.initDragDrop(); + this.addEventListener(this.getElement(this.selectors.EDIT_BUTTON), 'click', categorymanager.showEditModal); + const moveButton = this.getElement(this.selectors.MOVE_BUTTON); + this.addEventListener(moveButton, 'click', this.showMoveModal); + } + + destroy() { + // The draggable element must be unregistered. + this.deInitDragDrop(); + } + + /** + * Remove any existing DragDrop component, and create a new one. + */ + initDragDrop() { + this.deInitDragDrop(); + // If the element is currently draggable, register the getDraggableData method. + if (this.element.classList.contains(this.classes.DRAGHANDLE)) { + this.getDraggableData = this._getDraggableData; + } + this.dragdrop = new DragDrop(this); + } + + /** + * If the DragDrop component is currently registered, unregister it. + */ + deInitDragDrop() { + if (this.dragdrop !== undefined) { + if (this.getDraggableData !== undefined) { + this.dragdrop.setDraggable(false); + this.getDraggableData = undefined; + } + this.dragdrop.unregister(); + this.dragdrop = undefined; + } + } + + /** + * Static method to create a component instance. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + selectors, + reactive: categorymanager, + }); + } + + /** + * Return the category ID from the component's element. + * + * This method is referenced as getDraggableData when the component can be dragged. + * + * @return {{id: string}} + * @private + */ + _getDraggableData() { + return { + id: this.getElement().dataset.categoryid + }; + } + + validateDropData() { + return true; + } + + /** + * Highlight the top border of the category item. + * + * @param {Object} dropData + */ + showDropZone(dropData) { + if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) { + // Can't drop onto itself or its own child. + return false; + } + this.getElement().classList.add(this.classes.DROPTARGET); + return true; + } + + /** + * Remove highlighting. + */ + hideDropZone() { + this.getElement().classList.remove(this.classes.DROPTARGET); + } + + /** + * Find the new position of the dropped category, and trigger the move. + * + * @param {Object} dropData The category being moved. + * @param {Event} event The drop event. + */ + drop(dropData, event) { + const dropTarget = event.target.closest(this.selectors.CATEGORY_ITEM); + + if (!dropTarget) { + return; + } + + if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) { + // Can't drop onto your own child. + return; + } + + const source = document.getElementById(this.ids.CATEGORY(dropData.id)); + + if (!source) { + return; + } + + const targetParentId = dropTarget.dataset.parent; + const parentList = dropTarget.closest(this.selectors.CATEGORY_LIST); + let precedingSibling; + + if (dropTarget === parentList.firstElementChild) { + // Dropped at the top of the list. + precedingSibling = null; + } else { + precedingSibling = dropTarget.previousElementSibling; + } + + // Insert the category after the target category + categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid); + } + + getWatchers() { + return [ + // After any update to this category, move it to the new position. + {watch: `categories[${this.element.dataset.categoryid}]:updated`, handler: this.updatePosition}, + // When the template context is added or updated, re-render the content. + {watch: `categories[${this.element.dataset.categoryid}].templatecontext:created`, handler: this.rerender}, + {watch: `categories[${this.element.dataset.categoryid}].templatecontext:updated`, handler: this.rerender}, + // When a new category is created, check whether we need to add a child list to this category. + {watch: `categories:created`, handler: this.checkChildList}, + ]; + } + + /** + * Re-render the category content. + * + * @param {Object} args + * @param {Element} args.element + * @return {Promise} + */ + async rerender({element}) { + const {html, js} = await Templates.renderForPromise( + 'qbank_managecategories/category_details', + element.templatecontext + ); + return Templates.replaceNodeContents(this.getElement(this.selectors.CONTENT_AREA), html, js); + } + + /** + * Render and append a new child list. + * + * @param {Object} context Template context, must include at least categoryid. + * @return {Promise} + */ + async createChildList(context) { + const {html, js} = await Templates.renderForPromise( + 'qbank_managecategories/childlist', + context, + ); + const parentContainer = document.querySelector(this.selectors.CONTENT_CONTAINER(context.categoryid)); + await Templates.appendNodeContents(parentContainer, html, js); + const childList = document.querySelector(this.selectors.CHILD_LIST(context.categoryid)); + childList.closest(this.selectors.CATEGORY_CONTENTS).classList.add(this.classes.NO_BOTTOM_PADDING); + return childList; + } + + /** + * Move a category to its new position. + * + * A category may change its parent, sortorder and draghandle independently or at the same time. This method will resolve those + * changes and move the element to the new position. If the parent doesn't already have a child list, one will be created. + * + * If the parent has changed, this will also update the state with the new child count of the old and new parents. + * + * @param {Object} args + * @param {Object} args.element + * @return {Promise} + */ + async updatePosition({element}) { + window.console.log('updating', this.getElement()); + window.console.log('new state', element); + // 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; + } + window.console.log('newParent', newParent); + + // 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; + } + window.console.log('previousSibling', previousSibling); + window.console.log('nextSibling', nextSibling); + + // 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()); + + window.console.log('moved', moved); + + if (moved) { + if (nextSibling) { + window.console.log('insertBefore'); + // Move to the specified position in the list. + newParent.insertBefore(this.getElement(), nextSibling); + } else { + window.console.log('appendChild'); + // Move to the end of the list (may also be the top of the list is empty). + newParent.appendChild(this.getElement()); + } + } + if (originParent !== newParent) { + // Update child count of old and new parent. + 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; + + // Enable/disable dragging. + const isDraggable = this.element.classList.contains(this.classes.DRAGHANDLE); + if (isDraggable && !element.draghandle) { + this.element.classList.remove(this.classes.DRAGHANDLE); + this.initDragDrop(); + } else if (!isDraggable && element.draghandle) { + this.element.classList.add(this.classes.DRAGHANDLE); + this.initDragDrop(); + } + } + + /** + * Recursively create a list of all valid destinations for a current category within a parent category. + * + * @param {Element} item + * @param {Number} movingCategoryId + * @return {Array} + */ + createMoveCategoryList(item, movingCategoryId) { + 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. + let child = { + categoryid: categoryId, + movingcategoryid: movingCategoryId, + precedingsiblingid: precedingSibling?.dataset.categoryid ?? 0, + parent: category.dataset.parent, + categoryname: category.dataset.categoryname, + categories: null, + current: categoryId === movingCategoryId, + }; + 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); + } 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, + newchild: true, + } + ]; + } + categories.push(child); + precedingSibling = category; + }); + const precedingId = parseInt(precedingSibling.dataset.categoryid); + if (precedingId !== 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, + lastchild: true, + }); + } + } + return categories; + } + + /** + * Displays a modal containing links to move the category to a new location. + * + * @param {Event} e Button click event. + */ + async showMoveModal(e) { + // Return if it is not menu item. + const item = e.target; + if (!item) { + return; + } + // Return if it is disabled. + if (item.getAttribute('aria-disabled') === 'true') { + return; + } + + // Prevent addition click on the item. + 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 modal = await Modal.create({ + title: getString('movecategory', 'qbank_managecategories', item.dataset.categoryname), + body: Templates.render('qbank_managecategories/move_context_list', moveList), + 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) { + return; + } + categorymanager.moveCategory(target.dataset.movingcategoryid, target.dataset.parent, target.dataset.precedingsiblingid); + modal.destroy(); + }); + item.setAttribute('aria-disabled', false); + } + + /** + * Check and add a child list if needed. + * + * Check whether the category that has just been added has this category as its parent. If it does, + * check that this category has a child list, and if not, add one. + * + * @param {Object} args + * @param {Element} args.element The new category. + * @return {Promise} + */ + async checkChildList({element}) { + if (element.parent !== parseInt(this.getElement().dataset.categoryid)) { + return null; // Not for me. + } + let childList = this.getElement(this.selectors.CATEGORY_LIST); + if (childList) { + return null; // List already exists, it will handle adding the new category. + } + // Render and add a new child list containing the new category. + return this.createChildList({ + categoryid: element.parent, + children: [ + element.templatecontext, + ] + }); + } +} diff --git a/question/bank/managecategories/amd/src/categorylist.js b/question/bank/managecategories/amd/src/categorylist.js new file mode 100644 index 0000000000000..da97110c317e5 --- /dev/null +++ b/question/bank/managecategories/amd/src/categorylist.js @@ -0,0 +1,236 @@ +// 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 . + +/** + * The category list component. + * + * The category list is a drop target, so that a category may be dropped at the top or bottom of the list. + * + * @module qbank_managecategories/categorylist + * @class qbank_managecategories/categorylist + */ + +import {BaseComponent, DragDrop} from 'core/reactive'; +import Templates from 'core/templates'; +import {getString} from 'core/str'; +import {categorymanager} from 'qbank_managecategories/categorymanager'; + +export default class extends 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', + CATEGORY_DETAILS: '.qbank_managecategories-details', + CATEGORY_NO_DRAGHANDLE: '.qbank_managecategories-item[data-categoryid]:not(.draghandle)', + CATEGORY_ID: id => `#category-${id}`, + }; + this.classes = { + DROP_TARGET_BEFORE: 'qbank_managecategories-droptarget-before', + DROP_TARGET: 'qbank_managecategories-droptarget', + NO_BOTTOM_PADDING: 'pb-0', + }; + this.ids = { + CATEGORY: id => `category-${id}`, + }; + } + + stateReady() { + this.dragdrop = new DragDrop(this); + } + + destroy() { + // The draggable element must be unregistered. + if (this.dragdrop !== undefined) { + this.dragdrop.unregister(); + this.dragdrop = undefined; + } + } + + /** + * Static method to create a component instance. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + selectors, + reactive: categorymanager, + }); + } + + validateDropData() { + return true; + } + + /** + * Highlight the border of the list where the category will be moved. + * + * If dropping at the top of the list, highlight the top border. + * If dropping at the bottom, highlight the bottom border. + * + * @param {Object} dropData + * @param {Event} event + */ + showDropZone(dropData, event) { + const dropTarget = this.getElement(); + if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) { + // Can't drop onto its own child. + return false; + } + if (this.getInsertBefore(event, dropTarget)) { + dropTarget.classList.add(this.classes.DROP_TARGET_BEFORE); + dropTarget.classList.remove(this.classes.DROP_TARGET); + } else { + dropTarget.classList.add(this.classes.DROP_TARGET); + dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE); + } + return true; + } + + /** + * Remove highlighting. + * + * @param {Object} dropData + * @param {Event} event + */ + hideDropZone(dropData, event) { + const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST); + dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE); + dropTarget.classList.remove(this.classes.DROP_TARGET); + } + + /** + * Determine whether we're dragging over the top or bottom half of the list. + * + * @param {Event} event + * @param {Element} dropTarget + * @return {boolean} + */ + getInsertBefore(event, dropTarget) { + // Get the current mouse position within the drop target + const mouseY = event.clientY - dropTarget.getBoundingClientRect().top; + + // Get the height of the drop target + const targetHeight = dropTarget.clientHeight; + + // Check if the mouse is over the top half of the drop target + return mouseY < targetHeight / 2; + } + + /** + * Find the new position of the dropped category, and trigger the move. + * + * @param {Object} dropData + * @param {Event} event + */ + drop(dropData, event) { + const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST); + + if (!dropTarget) { + return; + } + + if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) { + // Can't drop onto your own child. + return; + } + + const source = document.getElementById(this.ids.CATEGORY(dropData.id)); + + if (!source) { + return; + } + + const targetParentId = dropTarget.dataset.categoryid; + let precedingSibling; + + if (this.getInsertBefore(event, dropTarget)) { + // Dropped at the top of the list. + precedingSibling = null; + } else { + // Dropped at the bottom of the list. + precedingSibling = dropTarget.lastElementChild; + } + + // Insert the category after the target category + categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid); + } + + /** + * Watch for categories moving to a new parent. + * + * @return {Array} A list of watchers. + */ + getWatchers() { + return [ + // Watch for this category having its child count updated. + {watch: `categoryLists[${this.element.dataset.categoryid}].childCount:updated`, handler: this.checkEmptyList}, + // Watch for any new category being created. + {watch: `categories:created`, handler: this.addCategory}, + ]; + } + + /** + * If this list is now empty, remove it. + * + * @param {Object} args + * @param {Object} args.element The categoryList state element. + */ + async checkEmptyList({element}) { + if (element.childCount === 0) { + // Display a new child drop zone. + const categoryItem = this.getElement().closest(this.selectors.CATEGORY_ITEM); + const {html, js} = await Templates.renderForPromise( + 'qbank_managecategories/newchild', + { + categoryid: this.getElement().dataset.categoryid, + tooltip: getString('newchild', 'qbank_managecategories', categoryItem.dataset.categoryname) + } + ); + const activityNameArea = categoryItem.querySelector(this.selectors.CATEGORY_DETAILS); + await Templates.appendNodeContents(activityNameArea, html, js); + // Reinstate padding on the parent element. + this.element.closest(this.selectors.CATEGORY_CONTENTS).classList.remove(this.classes.NO_BOTTOM_PADDING); + // Remove this list. + this.remove(); + } + } + + /** + * If a newly-created category has this list's category as its parent, add it to this list. + * + * @param {Object} args + * @param {Object} args.element + * @return {Promise} + */ + async addCategory({element}) { + if (element.parent !== parseInt(this.getElement().dataset.categoryid)) { + return; // Not for me. + } + const {html, js} = await Templates.renderForPromise('qbank_managecategories/category', element.templatecontext); + Templates.appendNodeContents(this.getElement(), html, js); + // If one of the children has no draghandle, it should do now it has a sibling. + const noDragHandle = this.getElement(this.selectors.CATEGORY_NO_DRAGHANDLE); + if (noDragHandle) { + this.reactive.dispatch('showDragHandle', noDragHandle.dataset.categoryid); + } + } +} \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/categorymanager.js b/question/bank/managecategories/amd/src/categorymanager.js new file mode 100644 index 0000000000000..eab2b079bf71b --- /dev/null +++ b/question/bank/managecategories/amd/src/categorymanager.js @@ -0,0 +1,222 @@ +// 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 . + +/** + * Reactive module for category manager + * + * @module qbank_managecategories/categorymanager + */ + +import {Reactive} from 'core/reactive'; +import {get_string as getString} from 'core/str'; +import {mutations} from 'qbank_managecategories/mutations'; +import {eventTypes, notifyQbankManagecategoriesStateUpdated} from 'qbank_managecategories/events'; +import Ajax from "core/ajax"; +import Notification from "core/notification"; +import ModalForm from 'core_form/modalform'; + +const SELECTORS = { + CATEGORY_LIST: '.qbank_managecategories-categorylist', + CONTEXT: '.qbank_managecategories-categorylist[data-contextid]', + CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]', + CATEGORY_ROOT: '#categoryroot', + SHOWDESCRIPTIONS_TOGGLE: '#showdescriptions-toggle', + ADD_EDIT_BUTTON: '[data-action="addeditcategory"]', +}; + +const CLASSES = { + DRAGHANDLE: 'draghandle', + DANGER: 'alert-danger', +}; + +/** + * Load the initial state. + * + * This iterates over the initial tree of category items, and captures the data required for the state from each category. + * It also captures a count of the number of children in each list. + * + * @param {Reactive} reactive + * @return {Promise} + */ +const loadState = async(reactive) => { + const rootElement = document.querySelector(SELECTORS.CATEGORY_ROOT); + const stateData = { + page: { + contextid: rootElement.dataset.contextid, + showdescriptions: document.querySelector(SELECTORS.SHOWDESCRIPTIONS_TOGGLE).checked, + }, + categories: [], + categoryLists: [], + }; + const listItems = document.querySelectorAll(SELECTORS.CATEGORY_ITEM); + listItems.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), + }); + }); + const categoryLists = document.querySelectorAll(SELECTORS.CATEGORY_LIST); + categoryLists.forEach(categoryList => { + stateData.categoryLists.push({ + id: categoryList.dataset.categoryid, + childCount: categoryList.querySelectorAll(SELECTORS.CATEGORY_ITEM).length, + }); + }); + reactive.setInitialState(stateData); +}; + +/** + * Reactive instance for the category manager. + */ +class CategoryManager extends Reactive { + /** + * Move a category to a new position within the given parent. + * + * This will call the move_category web service function to re-order the categories, then update + * the state with the returned updates. + * + * @param {Number} categoryId The ID of the category being moved. + * @param {Number} targetParentId The ID of the destination parent category (this may not have changed). + * @param {Number} precedingSiblingId The ID of the category to put the moved category after. + * This may be null if moving to the top of a list. + */ + moveCategory( + categoryId, + targetParentId, + precedingSiblingId = null, + ) { + const call = { + methodname: 'qbank_managecategories_move_category', + args: { + pagecontextid: this.state.page.contextid, + categoryid: categoryId, + targetparentid: targetParentId, + precedingsiblingid: precedingSiblingId, + } + }; + Ajax.call([call])[0] + .then((stateUpdates) => { + this.stateManager.processUpdates(stateUpdates); + return stateUpdates; + }) + .catch(error => { + Notification.addNotification({ + message: error.message, + type: 'error', + }); + document.getElementsByClassName(CLASSES.DANGER)[0]?.scrollIntoView(); + }); + } + + /** + * Return title for the add/edit modal. + * + * @param {boolean} isEdit is 'add' or 'edit' form + * @returns {String} title string + */ + getTitle(isEdit) { + return getString(isEdit ? 'editcategory' : 'addcategory', 'question'); + } + + /** + * Return save button label for the add/edit modal. + * + * @param {boolean} isEdit is 'add' or 'edit' form + * @returns {String} save string + */ + getSave(isEdit) { + return isEdit ? getString('savechanges', 'core') : getString('addcategory', 'question'); + } + + /** + * Function handling display of modal form. + * + * @param {Event} e The click event triggering the modal. + */ + showEditModal(e) { + const addEditButton = e.target.closest(SELECTORS.ADD_EDIT_BUTTON); + + // Return if it is not 'addeditcategory' button. + if (!addEditButton) { + return; + } + + // Return if the action type is not specified. + if (!addEditButton.dataset.actiontype) { + return; + } + + e.preventDefault(); + // Data for the modal. + const title = categorymanager.getTitle(addEditButton.dataset.actiontype === 'edit'); + const save = categorymanager.getSave(addEditButton.dataset.actiontype === 'edit'); + const cmid = addEditButton.dataset.cmid; + const courseid = addEditButton.dataset.courseid; + const questioncount = addEditButton.dataset.questioncount; + let contextid = addEditButton.dataset.contextid; + let categoryid = null; + let sortorder = null; + const categoryItem = e.target.closest(SELECTORS.CATEGORY_ITEM); + if (categoryItem) { + contextid = categoryItem.dataset.contextid; + categoryid = categoryItem.dataset.categoryid; + sortorder = categoryItem.dataset.sortorder; + } + + // Call the modal. + const modalForm = new ModalForm({ + formClass: "qbank_managecategories\\form\\question_category_edit_form", + args: { + cmid, + courseid, + questioncount, + contextid, + categoryid, + sortorder, + }, + modalConfig: { + title: title, + large: true, + }, + saveButtonText: save, + returnFocus: addEditButton, + }); + // Once the form has been submitted via the web service, update the state with the new or updated + // category based on the web service response. + modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (response) => { + categorymanager.stateManager.processUpdates(response.detail); + }); + // Show the form. + modalForm.show(); + } +} + +export const categorymanager = new CategoryManager({ + name: 'qtype_managecategories_categorymanager', + eventName: eventTypes.qbankManagecategoriesStateUpdated, + eventDispatch: notifyQbankManagecategoriesStateUpdated, + mutations, +}); + +/** + * Load the initial state. + */ +export const init = () => { + loadState(categorymanager); +}; diff --git a/question/bank/managecategories/amd/src/categoryroot.js b/question/bank/managecategories/amd/src/categoryroot.js new file mode 100644 index 0000000000000..488043a7d079d --- /dev/null +++ b/question/bank/managecategories/amd/src/categoryroot.js @@ -0,0 +1,75 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * The category root component. + * + * @module qbank_managecategories/categoryroot + * @class qbank_managecategories/categoryroot + */ + +import {BaseComponent} from 'core/reactive'; +import {categorymanager} from 'qbank_managecategories/categorymanager'; + +export default class extends BaseComponent { + + create(descriptor) { + this.name = descriptor.element.id; + this.classes = { + SHOWDESCRIPTIONS: 'showdescriptions', + }; + } + + /** + * Static method to create a component instance. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + selectors, + reactive: categorymanager, + }); + } + + /** + * Watch for changes to the page state. + * + * @return {Array} A list of watchers. + */ + getWatchers() { + return [ + // Watch for descriptions being toggled. + {watch: `page.showdescriptions:updated`, handler: this.toggleDescriptions} + ]; + } + + /** + * Show or hide descriptions when the flag in the state is changed. + * + * @param {Object} args + * @param {Object} args.element The updated page state. + */ + toggleDescriptions({element}) { + if (element.showdescriptions) { + this.getElement().classList.add(this.classes.SHOWDESCRIPTIONS); + } else { + this.getElement().classList.remove(this.classes.SHOWDESCRIPTIONS); + } + } +} \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/events.js b/question/bank/managecategories/amd/src/events.js new file mode 100644 index 0000000000000..2853d404af6f3 --- /dev/null +++ b/question/bank/managecategories/amd/src/events.js @@ -0,0 +1,46 @@ +// 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 {dispatchEvent} from 'core/event_dispatcher'; + +/** + * Reactive events for qbank_managecategories + * + * @module qbank_managecategories/events + * + */ +export const eventTypes = { + /** + * Event triggered when the activity reactive state is updated. + * + * @event qbankManagecategoriesStateUpdated + * @type {CustomEvent} + * @property {Array} nodes The list of parent nodes which were updated + */ + qbankManagecategoriesStateUpdated: 'qbank_managecategories/stateUpdated', +}; + +/** + * Trigger an event to indicate that the activity state is updated. + * + * @method notifyQbankManagecategoriesStateUpdated + * @param {Object} detail the full state + * @param {HTMLElement} container the custom event target (document if none provided) + * @returns {CustomEvent} + * @fires qbankManagecategoriesStateUpdated + */ +export const notifyQbankManagecategoriesStateUpdated = (detail, container) => { + return dispatchEvent(eventTypes.qbankManagecategoriesStateUpdated, detail, container); +}; diff --git a/question/bank/managecategories/amd/src/mutations.js b/question/bank/managecategories/amd/src/mutations.js new file mode 100644 index 0000000000000..ecc4d432409b2 --- /dev/null +++ b/question/bank/managecategories/amd/src/mutations.js @@ -0,0 +1,52 @@ +// 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 {setUserPreference} from 'core_user/repository'; + +/** + * Default mutation manager + * + * @module qbank_managecategories/mutations + */ +class Mutations { + /** + * Set the showdescriptions property to true or false in the page's state. + * + * @param {stateManager} stateManager + * @param {boolean} showDescriptions + * @return {Promise} + */ + async toggleDescriptions(stateManager, showDescriptions) { + stateManager.setReadOnly(false); + await setUserPreference('qbank_managecategories_showdescriptions', showDescriptions); + stateManager.state.page.showdescriptions = showDescriptions; + stateManager.setReadOnly(true); + } + + /** + * Set the draghandle property to true in a given category's state. + * + * @param {stateManager} stateManager + * @param {Number} categoryId + * @return {Promise} + */ + async showDragHandle(stateManager, categoryId) { + stateManager.setReadOnly(false); + stateManager.state.categories.get(categoryId).draghandle = true; + stateManager.setReadOnly(true); + } +} + +export const mutations = new Mutations(); \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/newchild.js b/question/bank/managecategories/amd/src/newchild.js new file mode 100644 index 0000000000000..5903e2e400aa7 --- /dev/null +++ b/question/bank/managecategories/amd/src/newchild.js @@ -0,0 +1,139 @@ +// 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 . + +/** + * The newchild component. + * + * This is a drop target for moving a category to an as-yet-nonexistant child list under another category. + * + * @module qbank_managecategories/newchild + * @class qbank_managecategories/newchild + */ + +import {BaseComponent, DragDrop} from 'core/reactive'; +import $ from 'jquery'; +import {categorymanager} from 'qbank_managecategories/categorymanager'; + +export default class extends BaseComponent { + create(descriptor) { + this.name = descriptor.element.id; + this.selectors = { + NEW_CHILD: '.qbank_managecategories-newchild', + CATEGORY_ID: id => `#category-${id}` + }; + this.classes = { + DROP_TARGET: 'qbank_managecategories-droptarget', + }; + this.ids = { + CATEGORY: id => `category-${id}`, + }; + } + + stateReady() { + this.dragdrop = new DragDrop(this); + } + + destroy() { + // The draggable element must be unregistered. + if (this.dragdrop !== undefined) { + this.dragdrop.unregister(); + this.dragdrop = undefined; + } + } + + /** + * Static method to create a component instance form the mustahce template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + const targetElement = document.querySelector(target); + return new this({ + element: targetElement, + selectors, + reactive: categorymanager, + }); + } + + /** + * Cannot drop a category as a new child of its own descendant. + * + * @param {Object} dropData + * @return {boolean} + */ + validateDropData(dropData) { + if (this.getElement().closest(this.selectors.CATEGORY_ID(dropData.id))) { + return false; + } + return true; + } + + showDropZone(dropData, event) { + const dropTarget = event.target.closest(this.selectors.NEW_CHILD); + dropTarget.classList.add(this.classes.DROP_TARGET); + $(dropTarget).tooltip('show'); + } + + hideDropZone(dropData, event) { + const dropTarget = event.target.closest(this.selectors.NEW_CHILD); + dropTarget.classList.remove(this.classes.DROP_TARGET); + $(dropTarget).tooltip('hide'); + } + + drop(dropData, event) { + const dropTarget = event.target.closest(this.selectors.NEW_CHILD); + + if (!dropTarget) { + return; + } + + const source = document.getElementById(this.ids.CATEGORY(dropData.id)); + + if (!source) { + return; + } + + const targetParentId = dropTarget.dataset.parent; + + // Insert the category as the first child of the new parent. + categorymanager.moveCategory(dropData.id, targetParentId); + } + + /** + * Watch for categories moving to a new parent. + * + * @return {Array} A list of watchers. + */ + getWatchers() { + return [ + // Watch for any category having its parent changed. + {watch: `categories.parent:updated`, handler: this.checkNewChild}, + ]; + } + + /** + * If an element now has this category as the parent, remove this new child target. + * + * @param {Object} args + * @param {Element} args.element + */ + checkNewChild({element}) { + if (element.parent === parseInt(this.element.dataset.parent)) { + this.remove(); + } + } +} \ No newline at end of file diff --git a/question/bank/managecategories/amd/src/showdescriptions.js b/question/bank/managecategories/amd/src/showdescriptions.js new file mode 100644 index 0000000000000..901cf1e86453b --- /dev/null +++ b/question/bank/managecategories/amd/src/showdescriptions.js @@ -0,0 +1,65 @@ +// 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 . + +/** + * The show descriptions toggle component. + * + * @module qbank_managecategories/showdescriptions + * @class qbank_managecategories/showdescriptions + */ + +import {BaseComponent} from 'core/reactive'; +import {categorymanager} from 'qbank_managecategories/categorymanager'; + +export default class extends BaseComponent { + + create(descriptor) { + this.name = descriptor.element.id; + this.selectors = { + TOGGLE: '#showdescriptions-toggle', + }; + } + + stateReady() { + this.addEventListener(this.getElement(this.selectors.TOGGLE), 'change', this.updateShowDescriptions); + } + + /** + * Static method to create a component instance. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + const targetElement = document.querySelector(target); + return new this({ + element: targetElement, + selectors, + reactive: categorymanager, + }); + } + + /** + * Dispatch a mutation to toggle the showDescriptions setting. + * + * @param {Event} event The toggle change event. + * @return {Promise} + */ + async updateShowDescriptions(event) { + const checked = event.target.checked; + this.reactive.dispatch('toggleDescriptions', checked); + } +} diff --git a/question/bank/managecategories/category.php b/question/bank/managecategories/category.php index af16a2a01c519..0f12841f68c93 100644 --- a/question/bank/managecategories/category.php +++ b/question/bank/managecategories/category.php @@ -24,101 +24,62 @@ */ require_once(__DIR__ . '/../../../config.php'); -require_once($CFG->dirroot."/question/editlib.php"); +require_once($CFG->dirroot . '/question/editlib.php'); +use core_question\output\qbank_actionbar; +use core_question\category_manager; use qbank_managecategories\form\question_move_form; use qbank_managecategories\helper; -use qbank_managecategories\question_category_object; +use qbank_managecategories\output\categories; +use qbank_managecategories\output\categories_header; +use qbank_managecategories\question_categories; require_login(); core_question\local\bank\helper::require_plugin_enabled(helper::PLUGINNAME); list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) = - question_edit_setup('categories', '/question/bank/managecategories/category.php'); - -// Get values from form for actions on this page. -$param = new stdClass(); -$param->moveup = optional_param('moveup', 0, PARAM_INT); -$param->movedown = optional_param('movedown', 0, PARAM_INT); -$param->moveupcontext = optional_param('moveupcontext', 0, PARAM_INT); -$param->movedowncontext = optional_param('movedowncontext', 0, PARAM_INT); -$param->tocontext = optional_param('tocontext', 0, PARAM_INT); -$param->left = optional_param('left', 0, PARAM_INT); -$param->right = optional_param('right', 0, PARAM_INT); -$param->delete = optional_param('delete', 0, PARAM_INT); -$param->confirm = optional_param('confirm', 0, PARAM_INT); -$param->cancel = optional_param('cancel', '', PARAM_ALPHA); -$param->move = optional_param('move', 0, PARAM_INT); -$param->moveto = optional_param('moveto', 0, PARAM_INT); -$param->edit = optional_param('edit', null, PARAM_INT); - -$url = new moodle_url($thispageurl); -foreach ((array)$param as $key => $value) { - if (($key !== 'cancel' && $key !== 'edit' && $value !== 0) || - ($key === 'cancel' && $value !== '') || - ($key === 'edit' && $value !== null)) { - $url->param($key, $value); - } -} -$PAGE->set_url($url); - -$qcobject = new question_category_object($pagevars['cpage'], $thispageurl, - $contexts->having_one_edit_tab_cap('categories'), $param->edit, - $pagevars['cat'], $param->delete, $contexts->having_cap('moodle/question:add')); + question_edit_setup('categories', '/question/bank/managecategories/category.php'); +$courseid = optional_param('courseid', 0, PARAM_INT); -if ($param->left || $param->right || $param->moveup || $param->movedown) { - require_sesskey(); - - foreach ($qcobject->editlists as $list) { - // Processing of these actions is handled in the method where appropriate and page redirects. - $list->process_actions($param->left, $param->right, $param->moveup, $param->movedown); - } +if (!is_null($cmid)) { + $thiscontext = context_module::instance($cmid)->id; +} else { + $course = get_course($courseid); + $thiscontext = context_course::instance($course->id)->id; } -if ($param->moveupcontext || $param->movedowncontext) { - require_sesskey(); +$todelete = optional_param('delete', 0, PARAM_INT); // The ID of a category to delete. - if ($param->moveupcontext) { - $catid = $param->moveupcontext; - } else { - $catid = $param->movedowncontext; - } - $newtopcat = question_get_top_category($param->tocontext); - if (!$newtopcat) { - throw new moodle_exception('invalidcontext'); - } - $oldcat = $DB->get_record('question_categories', ['id' => $catid], '*', MUST_EXIST); - // Log the move to another context. - $category = new stdClass(); - $category->id = explode(',', $pagevars['cat'], -1)[0]; - $category->contextid = $param->tocontext; - $event = \core\event\question_category_moved::create_from_question_category_instance($category); - $event->trigger(); - // Update the set_reference records when moving a category to a different context. - move_question_set_references($catid, $catid, $oldcat->contextid, $category->contextid); - $qcobject->update_category($catid, "{$newtopcat->id},{$param->tocontext}", $oldcat->name, $oldcat->info); - // The previous line does a redirect(). -} +$PAGE->set_url($thispageurl); +$PAGE->add_body_class('limitedwidth'); -if ($param->delete) { - if (!$category = $DB->get_record("question_categories", ["id" => $param->delete])) { - throw new moodle_exception('nocate', 'question', $thispageurl->out(), $param->delete); +$manager = new category_manager($thispageurl); + +if ($todelete) { + if (!$category = $DB->get_record("question_categories", ["id" => $todelete])) { + throw new moodle_exception('nocate', 'question', $thispageurl->out(), $todelete); } - helper::question_remove_stale_questions_from_category($param->delete); + helper::question_remove_stale_questions_from_category($todelete); - $questionstomove = count($qcobject->get_real_question_ids_in_category($param->delete)); + $questionstomove = count($manager->get_real_question_ids_in_category($todelete)); // Second pass, if we still have questions to move, setup the form. if ($questionstomove) { $categorycontext = context::instance_by_id($category->contextid); $moveform = new question_move_form($thispageurl, - ['contexts' => [$categorycontext], 'currentcat' => $param->delete]); + ['contexts' => [$categorycontext], 'currentcat' => $todelete]); if ($moveform->is_cancelled()) { + $thispageurl->remove_all_params(); + if (!is_null($cmid)) { + $thispageurl->param('cmid', $cmid); + } else { + $thispageurl->param('courseid', $courseid); + } redirect($thispageurl); } else if ($formdata = $moveform->get_data()) { list($tocategoryid, $tocontextid) = explode(',', $formdata->category); - $qcobject->move_questions_and_delete_category($formdata->delete, $tocategoryid); + $manager->move_questions_and_delete_category($formdata->delete, $tocategoryid); $thispageurl->remove_params('cat', 'category'); redirect($thispageurl); } @@ -127,54 +88,24 @@ $questionstomove = 0; } -if ($qcobject->catform->is_cancelled()) { - redirect($thispageurl); -} else if ($catformdata = $qcobject->catform->get_data()) { - $catformdata->infoformat = $catformdata->info['format']; - $catformdata->info = $catformdata->info['text']; - if (!$catformdata->id) {// New category. - $qcobject->add_category($catformdata->parent, $catformdata->name, - $catformdata->info, false, $catformdata->infoformat, $catformdata->idnumber); - } else { - $qcobject->update_category($catformdata->id, $catformdata->parent, - $catformdata->name, $catformdata->info, $catformdata->infoformat, $catformdata->idnumber); - } - redirect($thispageurl); -} else if ((!empty($param->delete) and (!$questionstomove) and confirm_sesskey())) { - $qcobject->delete_category($param->delete);// Delete the category now no questions to move. +if ((!empty($todelete) && (!$questionstomove) && confirm_sesskey())) { + $manager->delete_category($todelete);// Delete the category now no questions to move. $thispageurl->remove_params('cat', 'category'); redirect($thispageurl); } -if ($param->edit !== null || $qcobject->catform->is_submitted()) { - // In the is_submitted case, we only get here if it was submitted, - // but not valid, so we need to show the validation error. - $PAGE->navbar->add(get_string('editingcategory', 'question')); -} - $PAGE->set_title(get_string('editcategories', 'question')); $PAGE->set_heading($COURSE->fullname); $PAGE->activityheader->disable(); -echo $OUTPUT->header(); - // Print horizontal nav if needed. $renderer = $PAGE->get_renderer('core_question', 'bank'); -$qbankaction = new \core_question\output\qbank_action_menu($url); -if (is_null($param->edit)) { - $actionurl = new moodle_url($url, ['edit' => 0]); - $qbankaction->set_action_button($actionurl, get_string('addcategory', 'question')); -} +$categoriesrenderer = $PAGE->get_renderer('qbank_managecategories'); +echo $OUTPUT->header(); +$qbankaction = new \core_question\output\qbank_action_menu($thispageurl); echo $renderer->render($qbankaction); - -// Display the UI. -if ($param->edit !== null || $qcobject->catform->is_submitted()) { - // In the is_submitted case, we only get here if it was submitted, - // but not valid, so we need to show the validation error. - // In this case, category id is in the 'id' hidden filed. - $qcobject->edit_single_category($param->edit ?? required_param('id', PARAM_INT)); -} else if ($questionstomove) { +if ($questionstomove) { $vars = new stdClass(); $vars->name = $category->name; $vars->count = $questionstomove; @@ -182,6 +113,16 @@ $moveform->display(); } else { // Display the user interface. - $qcobject->display_user_interface(); + $questioncategories = new question_categories( + $thispageurl, + $contexts->having_one_edit_tab_cap('categories'), + $cmid, + $courseid, + $thiscontext, + ); + $PAGE->requires->js_call_amd('qbank_managecategories/categorymanager', 'init'); // Load reactive module. + echo $OUTPUT->render(new categories_header($questioncategories)); + echo $OUTPUT->render(new categories($questioncategories)); } + echo $OUTPUT->footer(); diff --git a/question/bank/managecategories/classes/external/move_category.php b/question/bank/managecategories/classes/external/move_category.php new file mode 100644 index 0000000000000..23c5a25355f71 --- /dev/null +++ b/question/bank/managecategories/classes/external/move_category.php @@ -0,0 +1,266 @@ +. + +namespace qbank_managecategories\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_question\category_manager; +use moodle_exception; +use context; + +/** + * External class used for category reordering. + * + * @package qbank_managecategories + * @category external + * @copyright 2024 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class move_category extends external_api { + /** + * Generate a category update based on the provided fields. + * + * @param int $id Category ID, required. + * @param int|null $sortorder New sortorder, optional. + * @param int|null $parent Category ID of new parent, optional. + * @param bool|null $draghandle Set display of the drag handle. Optional. + * @return \stdClass The update object. + */ + protected static function add_update( + int $id, + ?int $sortorder = null, + ?int $parent = null, + ?bool $draghandle = null + ): \stdClass { + $update = (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $id, + ], + ]; + if (!is_null($sortorder)) { + $update->fields->sortorder = $sortorder; + } + if (!is_null($parent)) { + $update->fields->parent = $parent; + } + if (!is_null($draghandle)) { + $update->fields->draghandle = $draghandle; + } + return $update; + } + + /** + * Describes the parameters for update_category_order webservice. + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'pagecontextid' => new external_value(PARAM_INT, 'The context of the current page.'), + 'categoryid' => new external_value(PARAM_INT, 'Category being moved'), + 'targetparentid' => new external_value(PARAM_INT, 'The ID of the parent category to move to.'), + 'precedingsiblingid' => new external_value( + PARAM_INT, + 'The ID of the preceding category. Null if this is being moved to top of its parent', + allownull: NULL_ALLOWED, + ), + ]); + } + + /** + * Move category to new location. + * + * @param int $pagecontextid ID of the context of the current page. + * @param int $categoryid ID of the category to move. + * @param int $targetparentid The ID of the parent category to move to. + * @param ?int $precedingsiblingid The ID of the preceding category. Null if this is being moved to top of its parent. + * @return array Reactive state updates representing the changes made to the categories. + */ + public static function execute( + int $pagecontextid, + int $categoryid, + int $targetparentid, + ?int $precedingsiblingid = null + ): array { + // Update category location. + global $DB, $CFG; + + require_once($CFG->libdir . '/questionlib.php'); + + $context = context::instance_by_id($pagecontextid); + self::validate_context($context); + $manager = new category_manager(); + + $origincategory = $DB->get_record('question_categories', ['id' => $categoryid], '*', MUST_EXIST); + $targetparent = $DB->get_record('question_categories', ['id' => $targetparentid], '*', MUST_EXIST); + if ($precedingsiblingid) { + $precedingsibling = $DB->get_record('question_categories', ['id' => $precedingsiblingid], '*', MUST_EXIST); + } + + // Check permission for original and destination contexts. + require_capability('moodle/question:managecategory', context::instance_by_id($origincategory->contextid)); + + if ($origincategory->contextid != $targetparent->contextid) { + require_capability('moodle/question:managecategory', context::instance_by_id($targetparent->contextid)); + } + + $originstateupdate = self::add_update($origincategory->id); + $stateupdates = []; + + $transaction = $DB->start_delegated_transaction(); + + // Set new parent. + if ($origincategory->parent !== $targetparent->id) { + $newsiblings = $DB->get_fieldset('question_categories', 'id', ['parent' => $targetparent->id]); + if ( + count($newsiblings) == 1 + && $manager->is_only_child_of_top_category_in_context(reset($newsiblings)) + ) { + // If we are moving to a top-level parent that only had 1 category before, allow re-ordering of that category. + $stateupdates[] = self::add_update(reset($newsiblings), draghandle: true); + } + $DB->set_field( + 'question_categories', + 'parent', + $targetparent->id, + ['id' => $origincategory->id], + ); + $originstateupdate->fields->parent = $targetparent->id; + } + + // Change to the same context. + if ($origincategory->contextid !== $targetparent->contextid) { + // Check for duplicate idnumber. + if (!is_null($origincategory->idnumber)) { + $duplicateidnumber = $DB->record_exists('question_categories', [ + 'idnumber' => $origincategory->idnumber, + 'contextid' => $targetparent->contextid, + ]); + if ($duplicateidnumber) { + throw new moodle_exception('idnumberexists', 'qbank_managecategories'); + } + } + + $DB->set_field( + 'question_categories', + 'contextid', + $targetparent->contextid, + ['id' => $origincategory->id], + ); + // Make change to sub categories. + \question_move_category_to_context( + $origincategory->id, + $origincategory->contextid, + $targetparent->contextid + ); + $originstateupdate->fields->context = $targetparent->contextid; + } + + // Update sort order. + if ($precedingsiblingid) { + $sortorder = $precedingsibling->sortorder + 1; + } else { + $sortorder = 1; + } + $DB->set_field('question_categories', 'sortorder', $sortorder, ['id' => $origincategory->id]); + $originstateupdate->fields->sortorder = $sortorder; + + // Get other categories which are after the new position, and update their sortorder. + $params = [ + 'parent' => $targetparent->id, + 'sortorder' => $sortorder, + 'origincategoryid' => $origincategory->id, + ]; + $select = " + parent = :parent + AND id <> :origincategoryid + AND sortorder >= :sortorder"; + $sort = "sortorder ASC"; + $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::add_update($category->id, sortorder: $sortorder); + } + + if (isset($originstateupdate->fields->parent)) { + // If the category has moved parent, re-order the original siblings to fill the gap. + $originsortorder = 1; + $params = [ + 'parent' => $origincategory->parent, + ]; + $select = "parent = :parent"; + $sort = "sortorder ASC"; + $originsiblings = $DB->get_records_select('question_categories', $select, $params, $sort); + if ( + count($originsiblings) == 1 + && $manager->is_only_child_of_top_category_in_context(reset($originsiblings)->id) + ) { + // If this is now the only category in the context, don't allow re-ordering. + $stateupdates[] = self::add_update(reset($originsiblings)->id, sortorder: $originsortorder, draghandle: false); + } else { + foreach ($originsiblings as $category) { + if ($category->sortorder !== $originsortorder) { + $DB->set_field('question_categories', 'sortorder', $originsortorder, ['id' => $category->id]); + $stateupdates[] = self::add_update($category->id, sortorder: $originsortorder); + } + $originsortorder++; + } + } + } + + $transaction->allow_commit(); + + array_unshift($stateupdates, $originstateupdate); + + return $stateupdates; + } + + /** + * Returns description of method result value. + * + * @return external_multiple_structure + */ + public static function execute_returns(): external_multiple_structure { + return new external_multiple_structure( + new external_single_structure( + [ + 'name' => new external_value(PARAM_ALPHA, 'State object name'), + 'action' => new external_value(PARAM_ALPHA, 'State update type'), + 'fields' => new external_single_structure( + [ + '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), + 'draghandle' => new external_value( + PARAM_BOOL, + 'Should this category have a drag handle?', + VALUE_OPTIONAL + ), + ] + ), + ] + ), + 'Category state updates', + ); + } +} diff --git a/question/bank/managecategories/classes/form/question_category_edit_form.php b/question/bank/managecategories/classes/form/question_category_edit_form.php index d8e5305321ecb..0ed5683383bac 100644 --- a/question/bank/managecategories/classes/form/question_category_edit_form.php +++ b/question/bank/managecategories/classes/form/question_category_edit_form.php @@ -16,12 +16,20 @@ namespace qbank_managecategories\form; -use moodleform; +use context; +use context_module; +use context_course; +use qbank_managecategories\helper; +use moodle_exception; +use moodle_url; +use core_question\local\bank\question_edit_contexts; +use qbank_managecategories\output\category; use core_question\category_manager; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/questionlib.php'); /** * Defines the form for editing question categories. @@ -32,7 +40,7 @@ * @copyright 2007 Jamie Pratt me@jamiep.org * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class question_category_edit_form extends moodleform { +class question_category_edit_form extends \core_form\dynamic_form { /** @var category_manager $manager */ protected category_manager $manager; @@ -46,13 +54,47 @@ protected function definition() { $mform = $this->_form; $this->manager = new category_manager(); - $contexts = $this->_customdata['contexts']; - $currentcat = $this->_customdata['currentcat']; + // 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. + $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); + } - $mform->addElement('header', 'categoryheader', get_string('addcategory', 'question')); + if ($courseid !== 0) { + $thiscontext = context_course::instance($courseid); + } - $mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'), - ['contexts' => $contexts, 'top' => true, 'currentcat' => $currentcat, 'nochildrenof' => $currentcat]); + 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(); + } + + $mform->addElement('hidden', 'courseid', $courseid); + $mform->setType('courseid', PARAM_INT); + + $mform->addElement('hidden', 'cmid', $cmid); + $mform->setType('cmid', PARAM_INT); + + $mform->addElement( + 'questioncategory', + 'parent', + get_string('parentcategory', 'question'), + [ + 'contexts' => $contexts, + 'top' => true, + 'currentcat' => $currentcat, + 'nochildrenof' => $currentcat, + ], + ); $mform->setType('parent', PARAM_SEQUENCE); if ($this->manager->is_only_child_of_top_category_in_context($currentcat)) { $mform->hardFreeze('parent'); @@ -73,28 +115,14 @@ protected function definition() { $mform->addHelpButton('idnumber', 'idnumber', 'question'); $mform->setType('idnumber', PARAM_RAW); - $this->add_action_buttons(true, get_string('addcategory', 'question')); - $mform->addElement('hidden', 'id', 0); $mform->setType('id', PARAM_INT); - } - /** - * Set data method. - * - * Add additional information to current data. - * @param \stdClass|array $current Object or array of default current data. - */ - public function set_data($current) { - if (is_object($current)) { - $current = (array) $current; - } - if (!empty($current['info'])) { - $current['info'] = ['text' => $current['info'], 'infoformat' => $current['infoformat']]; - } else { - $current['info'] = ['text' => '', 'infoformat' => FORMAT_HTML]; - } - parent::set_data($current); + $mform->addElement('hidden', 'questioncount', $this->_ajaxformdata['questioncount'] ?? 0); + $mform->setType('questioncount', PARAM_INT); + + $mform->addElement('hidden', 'sortorder', $this->_ajaxformdata['sortorder'] ?? 0); + $mform->setType('sortorder', PARAM_INT); } /** @@ -109,10 +137,24 @@ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); - + $currentrec = $DB->get_record('question_categories', ['id' => $data['id']]); // Add field validation check for duplicate idnumber. list($parentid, $contextid) = explode(',', $data['parent']); - if (((string) $data['idnumber'] !== '') && !empty($contextid)) { + if ($currentrec) { + $currentparent = $currentrec->parent . ',' . $currentrec->contextid; + // Cannot move the last category in a context to another parent. + $lastcategoryinthiscontext = helper::question_is_only_child_of_top_category_in_context($data['id']); + if ($lastcategoryinthiscontext && $currentparent !== $data['parent']) { + if ($parentid !== $this->_ajaxformdata['id']) { + $errors['parent'] = get_string('lastcategoryinthiscontext', 'qbank_managecategories'); + } + } + // Cannot move category in same category. + if ($currentrec->id === $parentid && $currentrec->contextid === $contextid) { + $errors['parent'] = get_string('categoryincategory', 'qbank_managecategories'); + } + } + if (((string)$data['idnumber'] !== '') && !empty($contextid)) { $conditions = 'contextid = ? AND idnumber = ?'; $params = [$contextid, $data['idnumber']]; if (!empty($data['id'])) { @@ -126,4 +168,149 @@ public function validation($data, $files) { return $errors; } + + /** + * Get context for submission + * + * @return context the context + */ + protected function get_context_for_dynamic_submission(): context { + $contextid = $this->optional_param('contextid', 0, PARAM_INT); + if ($contextid === 0) { + $contextid = (int)explode(',', $this->_ajaxformdata['parent'])[1]; + } + return context::instance_by_id($contextid); + } + + /** + * Check capability + * + * @return void + */ + protected function check_access_for_dynamic_submission(): void { + require_capability('moodle/question:managecategory', $this->get_context_for_dynamic_submission()); + } + + /** + * Process submission + * + * @return array State updates representing the creation/update of the category. + */ + public function process_dynamic_submission(): array { + global $DB, $OUTPUT; + + $values = $this->get_data(); + + $parentid = (int)explode(',', $values->parent)[0]; + $contextid = (int)explode(',', $values->parent)[1]; + $newcategory = $values->name; + $newinfo = format_text($values->info['text'], (int)$values->info['format'], ['noclean' => false]); + $idnumber = $values->idnumber; + + if ((string)$idnumber === '') { + $idnumber = null; + } + + $record = (object)[ + 'parent' => $parentid, + 'contextid' => $contextid, + 'name' => $newcategory, + 'info' => $newinfo, + 'infoformat' => (int)$values->info['format'], + 'stamp' => make_unique_id_code(), + 'idnumber' => $idnumber, + 'sortorder' => $values->sortorder, + ]; + + // Check if there is any other record having the same idnumber within the context. + if ($idnumber) { + $existingid = $DB->get_field('question_categories', 'id', [ + 'idnumber' => $idnumber, + 'contextid' => $contextid, + ]); + + // There is existing record having the same idnumber. + if ($existingid && $existingid != $values->id) { + throw new moodle_exception('idnumberexists', 'qbank_managecategories'); + } + } + + if ($values->id !== 0) { + $record->id = $values->id; + $DB->update_record('question_categories', $record); + } else { + $manager = new category_manager(); + $record->sortorder = $manager->get_max_sortorder($record->parent) + 1; + $record->id = $DB->insert_record('question_categories', $record); + } + + // The question count will never change, we just need it passed through to re-render the category. + $record->questioncount = $values->questioncount; + + $category = new category( + $record, + context::instance_by_id($record->contextid), + $values->cmid ?? 0, + $values->courseid ?? 0, + ); + + return [ + [ + 'name' => 'categories', + 'action' => 'put', + 'fields' => [ + 'id' => $record->id, + 'name' => $record->name, + 'parent' => $record->parent, + 'sortorder' => $record->sortorder, + 'draghandle' => $category->get_canreorder(), + 'templatecontext' => $category->export_for_template($OUTPUT), + ], + ], + ]; + } + + /** + * Set data + * + * @return void + */ + public function set_data_for_dynamic_submission(): void { + $categoryid = isset($this->_ajaxformdata['categoryid']) ? (int)$this->_ajaxformdata['categoryid'] : 0; + if ($categoryid !== 0) { + global $DB; + $cattoset = $DB->get_record('question_categories', ['id' => $categoryid]); + $this->set_data((object)[ + 'id' => (int)$cattoset->id, + 'name' => $cattoset->name, + 'contextid' => (int)$cattoset->contextid, + 'info' => [ + 'format' => FORMAT_HTML, + 'text' => $cattoset->info, + ], + 'infoformat' => (int)$cattoset->infoformat, + 'parent' => (int)$cattoset->parent . ',' . (int)$cattoset->contextid, + 'idnumber' => $cattoset->idnumber, + ]); + } + } + + /** + * Get page URL + * + * @return moodle_url + */ + protected function get_page_url_for_dynamic_submission(): moodle_url { + $params = []; + $cmid = isset($this->_ajaxformdata['cmid']) ? (int)$this->_ajaxformdata['cmid'] : 0; + $courseid = isset($this->_ajaxformdata['courseid']) ? (int)$this->_ajaxformdata['courseid'] : 0; + if ($cmid !== 0) { + $params['cmid'] = $cmid; + } + + if ($courseid !== 0) { + $params['courseid'] = $courseid; + } + return new moodle_url('/question/bank/managecategories/category.php', $params); + } } diff --git a/question/bank/managecategories/classes/helper.php b/question/bank/managecategories/classes/helper.php index d3f7724364350..312c56631f563 100644 --- a/question/bank/managecategories/classes/helper.php +++ b/question/bank/managecategories/classes/helper.php @@ -16,6 +16,10 @@ namespace qbank_managecategories; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . "/questionlib.php"); + use context; use core_question\category_manager; use core_question\local\bank\question_version_status; @@ -34,7 +38,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { - /** * Name of this plugin. */ @@ -167,7 +170,11 @@ public static function flatten_category_tree(array &$categories, $id, int $depth foreach ($categories[$id]->childids as $childid) { if ($childid != $nochildrenof) { $newcategories = $newcategories + self::flatten_category_tree( - $categories, $childid, $depth + 1, $nochildrenof); + $categories, + $childid, + $depth + 1, + $nochildrenof, + ); } } @@ -198,8 +205,10 @@ public static function add_indented_names(array $categories, int $nochildrenof = // categories from other courses, but not their parents. $toplevelcategoryids = []; foreach (array_keys($categories) as $id) { - if (!empty($categories[$id]->parent) && - array_key_exists($categories[$id]->parent, $categories)) { + if ( + !empty($categories[$id]->parent) && + array_key_exists($categories[$id]->parent, $categories) + ) { $categories[$categories[$id]->parent]->childids[] = $id; } else { $toplevelcategoryids[] = $id; @@ -210,7 +219,11 @@ public static function add_indented_names(array $categories, int $nochildrenof = $newcategories = []; foreach ($toplevelcategoryids as $id) { $newcategories = $newcategories + self::flatten_category_tree( - $categories, $id, 0, $nochildrenof); + $categories, + $id, + 0, + $nochildrenof, + ); } return $newcategories; @@ -229,19 +242,36 @@ public static function add_indented_names(array $categories, int $nochildrenof = * default in the dropdown. * @param int $nochildrenof * @param bool $return to return the string of the select menu or echo that from the method + * @return ?string The HTML, or null if the $return is false. * @throws \coding_exception|\dml_exception */ - public static function question_category_select_menu(array $contexts, bool $top = false, int $currentcat = 0, - string $selected = "", int $nochildrenof = -1, bool $return = false) { - $categoriesarray = self::question_category_options($contexts, $top, $currentcat, - false, $nochildrenof, false); + public static function question_category_select_menu( + array $contexts, + bool $top = false, + int $currentcat = 0, + string $selected = "", + int $nochildrenof = -1, + bool $return = false, + ): ?string { + $categoriesarray = self::question_category_options( + $contexts, + $top, + $currentcat, + false, + $nochildrenof, + false, + ); $choose = ''; $options = []; foreach ($categoriesarray as $group => $opts) { $options[] = [$group => $opts]; } - $outputhtml = html_writer::label(get_string('questioncategory', 'core_question'), - 'id_movetocategory', false, ['class' => 'accesshide']); + $outputhtml = html_writer::label( + get_string('questioncategory', 'core_question'), + 'id_movetocategory', + false, + ['class' => 'accesshide'], + ); $attrs = [ 'id' => 'id_movetocategory', 'class' => 'custom-select', @@ -255,6 +285,7 @@ public static function question_category_select_menu(array $contexts, bool $top return $outputhtml; } else { echo $outputhtml; + return null; } } @@ -262,18 +293,22 @@ public static function question_category_select_menu(array $contexts, bool $top * Get all the category objects, including a count of the number of questions in that category, * for all the categories in the lists $contexts. * - * @param context $contexts + * @param string $contexts * @param string $sortorder used as the ORDER BY clause in the select statement. * @param bool $top Whether to return the top categories or not. * @param int $showallversions 1 to show all versions not only the latest. * @return array of category objects. * @throws \dml_exception */ - public static function get_categories_for_contexts($contexts, string $sortorder = 'parent, sortorder, name ASC', - bool $top = false, int $showallversions = 0): array { + public static function get_categories_for_contexts( + string $contexts, + string $sortorder = 'parent, sortorder, name ASC', + bool $top = false, + int $showallversions = 0, + ): array { global $DB; $topwhere = $top ? '' : 'AND c.parent <> 0'; - $statuscondition = "AND (qv.status = '". question_version_status::QUESTION_STATUS_READY . "' " . + $statuscondition = "AND (qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' " . " OR qv.status = '" . question_version_status::QUESTION_STATUS_DRAFT . "' )"; $sql = "SELECT c.*, @@ -311,9 +346,14 @@ public static function get_categories_for_contexts($contexts, string $sortorder * @return array * @throws \coding_exception|\dml_exception */ - public static function question_category_options(array $contexts, bool $top = false, int $currentcat = 0, - bool $popupform = false, int $nochildrenof = -1, - bool $escapecontextnames = true): array { + public static function question_category_options( + array $contexts, + bool $top = false, + int $currentcat = 0, + bool $popupform = false, + int $nochildrenof = -1, + bool $escapecontextnames = true, + ): array { global $CFG; $pcontexts = []; foreach ($contexts as $context) { @@ -339,9 +379,12 @@ public static function question_category_options(array $contexts, bool $top = fa if ($category->contextid == $contextid) { $cid = $category->id; if ($currentcat != $cid || $currentcat == 0) { - $a = new \stdClass; - $a->name = format_string($category->indentedname, true, - ['context' => $context]); + $a = new \stdClass(); + $a->name = format_string( + $category->indentedname, + true, + ['context' => $context] + ); if ($category->idnumber !== null && $category->idnumber !== '') { $a->idnumber = s($category->idnumber); } @@ -413,4 +456,14 @@ public static function question_fix_top_names(array $categories, bool $escape = return $categories; } + + /** + * Combine id and context id for a question category + * + * @param \stdClass $category a category to extract its id and context id + * @return string the combined string + */ + public static function combine_id_context(\stdClass $category): string { + return $category->id . ',' . $category->contextid; + } } diff --git a/question/bank/managecategories/classes/output/categories.php b/question/bank/managecategories/classes/output/categories.php new file mode 100644 index 0000000000000..549c459d87095 --- /dev/null +++ b/question/bank/managecategories/classes/output/categories.php @@ -0,0 +1,89 @@ +. + +namespace qbank_managecategories\output; + +use context; +use qbank_managecategories\question_categories; +use renderable; +use renderer_base; +use templatable; + +/** + * Output component for categories + * + * @package qbank_managecategories + * @copyright 2024 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 categories implements renderable, templatable { + /** + * Constructor. + * + * @param question_categories $categories Question categories for display. + */ + public function __construct( + /** @var question_categories $categories Question categories for display. */ + protected question_categories $categories, + ) { + } + + /** + * For each context, build an array of the categories. + * + * @param renderer_base $output + * @return array + * @throws \coding_exception + */ + 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); + } + } + if (isset($itemstab['items'])) { + $ctxlvl = "contextlevel" . $list->context->contextlevel; + $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, + ]; + } + } + $data = [ + 'categoriesrendered' => $categories, + 'contextid' => $this->categories->contextid, + 'cmid' => $this->categories->cmid, + 'courseid' => $this->categories->courseid, + 'showdescriptions' => get_user_preferences('qbank_managecategories_showdescriptions'), + ]; + return $data; + } +} diff --git a/question/bank/managecategories/classes/output/categories_header.php b/question/bank/managecategories/classes/output/categories_header.php new file mode 100644 index 0000000000000..9ccd1ed957db8 --- /dev/null +++ b/question/bank/managecategories/classes/output/categories_header.php @@ -0,0 +1,77 @@ +. + +namespace qbank_managecategories\output; + +use context; +use renderable; +use renderer_base; +use templatable; +use qbank_managecategories\question_categories; + +/** + * Output component for category page header. + * + * @package qbank_managecategories + * @copyright 2024 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 categories_header implements renderable, templatable { + /** + * Constructor. + * + * @param question_categories $categories Question categories for display. + */ + public function __construct( + /** @var question_categories $categories Question categories for display. */ + protected question_categories $categories, + ) { + } + + /** + * Output template data for the heading of the category management page. + * + * @param renderer_base $output + * @return array + * @throws \coding_exception + */ + public function export_for_template(renderer_base $output): array { + $helpstringhead = $output->heading_with_help( + get_string('editcategories', 'question'), + 'editcategories', + 'question', + ); + $hascapability = has_capability( + 'moodle/question:managecategory', + context::instance_by_id($this->categories->contextid), + ); + + $data = [ + 'helpstringhead' => $helpstringhead, + 'showdescriptions' => [ + 'id' => 'showdescriptions-toggle', + 'checked' => get_user_preferences('qbank_managecategories_showdescriptions'), + 'label' => get_string('showcategorydescription', 'qbank_managecategories'), + ], + 'hascapability' => $hascapability, + 'contextid' => $this->categories->contextid, + 'cmid' => $this->categories->cmid, + 'courseid' => $this->categories->courseid, + ]; + return $data; + } +} diff --git a/question/bank/managecategories/classes/output/category.php b/question/bank/managecategories/classes/output/category.php new file mode 100644 index 0000000000000..97fa502014d97 --- /dev/null +++ b/question/bank/managecategories/classes/output/category.php @@ -0,0 +1,229 @@ +. + +namespace qbank_managecategories\output; + +use action_menu; +use action_menu_link; +use context; +use core\plugininfo\qbank; +use core_question\category_manager; +use moodle_url; +use pix_icon; +use qbank_managecategories\helper; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Output component for a single category + * + * @package qbank_managecategories + * @copyright 2024 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 category implements renderable, templatable { + /** + * @var bool Can this category be reordered? + */ + protected bool $canreorder; + + /** + * Constructor + * + * @param stdClass $category The record of category we are rendering + * @param context $context The context the category belongs to. + * @param int $cmid The cmid of the course module context the category belongs to (optional). + * @param int $courseid The course ID of the course context the category belongs to (optional). + */ + public function __construct( + /** @var stdClass $category The record of category we are rendering */ + protected stdClass $category, + /** @var context $context The context the category belongs to. */ + protected context $context, + /** @var int $cmid The cmid of the course module context the category belongs to. */ + protected int $cmid = 0, + /** @var int $courseid The course ID of the course context the category belongs to. */ + protected int $courseid = 0, + ) { + $manager = new category_manager(); + $this->canreorder = !$manager->is_only_child_of_top_category_in_context($this->category->id); + } + + /** + * Get the canreorder flag. + * + * @return bool + */ + public function get_canreorder(): bool { + return $this->canreorder; + } + + /** + * Create the template data for a category, and call recursively for child categories. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + global $PAGE; + $canmanagecategory = has_capability('moodle/question:managecategory', $this->context); + $params = $PAGE->url->params(); + $cmid = $params['cmid'] ?? $this->cmid; + $courseid = $params['courseid'] ?? $this->courseid; + + // Each section adds html to be displayed as part of this list item. + $questionbankurl = new moodle_url('/question/edit.php', $params); + $questionbankurl->param('cat', helper::combine_id_context($this->category)); + $categoryname = format_string($this->category->name, true, ['context' => $this->context]); + $idnumber = null; + if ($this->category->idnumber !== null && $this->category->idnumber !== '') { + $idnumber = $this->category->idnumber; + } + $categorydesc = format_text( + $this->category->info, + $this->category->infoformat, + ['context' => $this->context, 'noclean' => true], + ); + $menu = new action_menu(); + $menu->attributes['draggable'] = 'false'; + $menu->set_kebab_trigger(); + $menu->prioritise = true; + + // Don't allow movement if only subcat. + if ($canmanagecategory) { + // This item display a modal for moving a category. + // Move category modal. + $menu->add(new action_menu_link( + new \moodle_url('#'), + new pix_icon( + 't/move', + get_string('move'), + 'moodle', + [ + 'class' => 'iconsmall', + ] + ), + get_string('move'), + false, + [ + 'class' => 'show-when-movable', // Don't allow moving when this is the only category in the context. + 'data-categoryid' => $this->category->id, + 'data-actiontype' => 'move', + 'data-contextid' => (int) $this->category->contextid, + 'data-categoryname' => $categoryname, + 'title' => get_string('movecategory', 'qbank_managecategories', $categoryname), + ] + )); + + $thiscontext = (int) $this->category->contextid; + $editurl = new moodle_url('#'); + $menu->add(new action_menu_link( + $editurl, + new pix_icon('t/edit', 'edit'), + get_string('editsettings'), + false, + [ + 'data-action' => 'addeditcategory', + 'data-actiontype' => 'edit', + 'data-contextid' => $thiscontext, + 'data-categoryid' => $this->category->id, + 'data-cmid' => $cmid, + 'data-courseid' => $courseid, + 'data-questioncount' => $this->category->questioncount, + ] + )); + // Sets up delete link. + $deleteurl = new moodle_url( + '/question/bank/managecategories/category.php', + ['delete' => $this->category->id, 'sesskey' => sesskey()] + ); + if ($courseid !== 0) { + $deleteurl->param('courseid', $courseid); + } else { + $deleteurl->param('cmid', $cmid); + } + $menu->add(new action_menu_link( + $deleteurl, + new pix_icon('t/delete', 'delete'), + get_string('delete'), + false, + [ + 'class' => 'text-danger show-when-movable', // Don't allow deletion when this is the only category in context. + 'data-confirmation' => 'modal', + 'data-confirmation-type' => 'delete', + 'data-confirmation-title-str' => json_encode(['delete', 'core']), + 'data-confirmation-yes-button-str' => json_encode(['delete', 'core']), + 'data-confirmation-content-str' => json_encode([ + 'confirmdelete', + 'qbank_managecategories', + $this->category->name, + ]), + ], + )); + } + + // Sets up export to XML link. + if (qbank::is_plugin_enabled('qbank_exportquestions')) { + $exporturl = new moodle_url( + '/question/bank/exportquestions/export.php', + ['cat' => helper::combine_id_context($this->category)] + ); + if ($courseid !== 0) { + $exporturl->param('courseid', $courseid); + } else { + $exporturl->param('cmid', $cmid); + } + + $menu->add(new action_menu_link( + $exporturl, + new pix_icon('t/download', 'download'), + get_string('exportasxml', 'question'), + false, + )); + } + + $children = []; + if (!empty($this->category->children)) { + foreach ($this->category->children as $child) { + $childcategory = new category($child, $this->context); + $children[] = $childcategory->export_for_template($output); + } + } + $itemdata = [ + 'categoryid' => $this->category->id, + 'contextid' => $this->category->contextid, + 'questionbankurl' => $questionbankurl->out(false), + 'categoryname' => $categoryname, + 'idnumber' => $idnumber, + 'questioncount' => $this->category->questioncount, + 'categorydesc' => $categorydesc, + 'editactionmenu' => $menu->export_for_template($output), + 'draghandle' => $canmanagecategory && $this->canreorder, + 'haschildren' => !empty($children), + 'children' => $children, + 'parent' => $this->category->parent, + 'sortorder' => $this->category->sortorder, + 'newchildtooltip' => get_string('newchild', 'qbank_managecategories', $categoryname), + ]; + return $itemdata; + } + + + +} diff --git a/question/bank/managecategories/classes/privacy/provider.php b/question/bank/managecategories/classes/privacy/provider.php index de587972b668c..70062768e00ae 100644 --- a/question/bank/managecategories/classes/privacy/provider.php +++ b/question/bank/managecategories/classes/privacy/provider.php @@ -16,6 +16,9 @@ namespace qbank_managecategories\privacy; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; + /** * Privacy Subsystem for qbank_managecategories implementing null_provider. * @@ -25,14 +28,43 @@ * @author Guillermo Gomez Arias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements \core_privacy\local\metadata\null_provider { +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\user_preference_provider { + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection): collection { + $collection->add_user_preference('qbank_managecategories_showdescriptions', 'privacy:preference:showdescriptions'); + return $collection; + } + /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. + * Export all user preferences for the plugin. * - * @return string + * @param int $userid The userid of the user whose data is to be exported. */ - public static function get_reason(): string { - return 'privacy:metadata'; + public static function export_user_preferences(int $userid) { + $qbankmanagecategoriesshowdescr = get_user_preferences('qbank_managecategories_showdescriptions', null, $userid); + if ($qbankmanagecategoriesshowdescr !== null) { + switch ($qbankmanagecategoriesshowdescr) { + case 1: + $showdescription = get_string('displaydescription', 'qbank_managecategories'); + break; + case 0: + default: + $showdescription = get_string('descriptionnotdisplayed', 'qbank_managecategories'); + break; + } + writer::export_user_preference( + 'qbank_managecategories', + 'showdescr', + $qbankmanagecategoriesshowdescr, + $showdescription + ); + } } } diff --git a/question/bank/managecategories/classes/question_categories.php b/question/bank/managecategories/classes/question_categories.php index 46462e1392bb7..dfba0c918b195 100644 --- a/question/bank/managecategories/classes/question_categories.php +++ b/question/bank/managecategories/classes/question_categories.php @@ -16,13 +16,6 @@ namespace qbank_managecategories; -/** - * QUESTION_PAGE_LENGTH - Number of categories to display on page. - */ -if (!defined('QUESTION_PAGE_LENGTH')) { - define('QUESTION_PAGE_LENGTH', 25); -} - use context; use moodle_url; diff --git a/question/bank/managecategories/classes/question_category_list.php b/question/bank/managecategories/classes/question_category_list.php index 194633a4bf97f..ced825233b748 100644 --- a/question/bank/managecategories/classes/question_category_list.php +++ b/question/bank/managecategories/classes/question_category_list.php @@ -30,7 +30,10 @@ * @package qbank_managecategories * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated MDL-72397 Since Moodle 4.5 + * @todo Final removal in Moodle 6.0 MDL-80804 */ +#[\core\attribute\deprecated(null, since: '4.5', mdl: 'MDL-72397', reason: 'Output is now generated by templates.')] class question_category_list extends moodle_list { /** @@ -80,18 +83,25 @@ class question_category_list extends moodle_list { * @param string $pageparamname name of url param that is used for passing page no * @param integer $itemsperpage no of top level items. * @param \context $context + * @deprecated MDL-72397 Since Moodle 4.5 + * @todo Final removal in Moodle 6.0 MDL-80804 */ public function __construct($type='ul', $attributes='', $editable = false, $pageurl=null, $page = 0, $pageparamname = 'page', $itemsperpage = DEFAULT_QUESTIONS_PER_PAGE, $context = null) { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); parent::__construct('ul', '', $editable, $pageurl, $page, 'cpage', $itemsperpage); $this->context = $context; } /** * Set the array of records of list items. + * + * @deprecated MDL-72397 Since Moodle 4.5 + * @todo Final removal in Moodle 6.0 MDL-80804 */ public function get_records(): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $this->records = helper::get_categories_for_contexts($this->context->id, $this->sortby); } @@ -101,8 +111,11 @@ public function get_records(): void { * * @param \list_item $item The item which its top level parent is going to be returned. * @return int + * @deprecated MDL-72397 Since Moodle 4.5 + * @todo Final removal in Moodle 6.0 MDL-80804 */ public function get_top_level_parent_id($item): int { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); // Put the item at the highest level it can go. $topcategory = question_get_top_category($item->item->contextid, true); return $topcategory->id; @@ -116,8 +129,11 @@ public function get_top_level_parent_id($item): int { * @param integer $moveup id of item to move up * @param integer $movedown id of item to move down * @return void + * @deprecated MDL-72397 Since Moodle 4.5 + * @todo Final removal in Moodle 6.0 MDL-80804 */ public function process_actions($left, $right, $moveup, $movedown): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $category = new stdClass(); if (!empty($left)) { // Moved Left (In to another category). diff --git a/question/bank/managecategories/classes/question_category_list_item.php b/question/bank/managecategories/classes/question_category_list_item.php index e8f334b64d593..1f288a271a293 100644 --- a/question/bank/managecategories/classes/question_category_list_item.php +++ b/question/bank/managecategories/classes/question_category_list_item.php @@ -24,7 +24,10 @@ * @package qbank_managecategories * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated MDL-72397 Since Moodle 4.5 + * @todo Final removal in Moodle 6.0 MDL-80804 */ +#[\core\attribute\deprecated(null, since: '4.5', mdl: 'MDL-72397', reason: 'Output is now generated by templates.')] class question_category_list_item extends \list_item { /** @@ -33,8 +36,10 @@ class question_category_list_item extends \list_item { * @param bool $first Is the first on the list. * @param bool $last Is the last on the list. * @param \list_item $lastitem Last item. + * @deprecated MDL-72397 Since Moodle 4.5 */ public function set_icon_html($first, $last, $lastitem): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $CFG; $category = $this->item; $url = new moodle_url('/question/bank/managecategories/category.php', @@ -74,8 +79,10 @@ public function set_icon_html($first, $last, $lastitem): void { * @param array $extraargs * @return string Item html. * @throws \moodle_exception + * @deprecated MDL-72397 Since Moodle 4.5 */ public function item_html($extraargs = []): string { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $PAGE, $OUTPUT; $str = $extraargs['str']; $category = $this->item; diff --git a/question/bank/managecategories/classes/question_category_object.php b/question/bank/managecategories/classes/question_category_object.php index 2837486023e02..9bcb2f5893fd3 100644 --- a/question/bank/managecategories/classes/question_category_object.php +++ b/question/bank/managecategories/classes/question_category_object.php @@ -34,6 +34,8 @@ * @package qbank_managecategories * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager or \qbank_managecategories\question_categories. + * @todo Final removal in Moodle 6.0 MDL-80804 */ class question_category_object { @@ -77,9 +79,17 @@ class question_category_object { * @param int|null $defaultcategory id of the current category. null if none. * @param int $todelete id of the category to delete. 0 if none. * @param context[] $addcontexts contexts where the current user can add questions. + * @deprecated Since Moodle 4.5 MDL-72397. Use \qbank_managecategories\question_categories instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\qbank_managecategories\question_categories', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) { - + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $this->tab = str_repeat(' ', $this->tabsize); $this->str = new stdClass(); @@ -118,8 +128,17 @@ public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcat * @param int|null $defaultcategory id of the current category. null if none. * @param int $todelete id of the category to delete. 0 if none. * @param context[] $addcontexts contexts where the current user can add questions. + * @deprecated Since Moodle 4.5 MDL-72397. Use \qbank_managecategories\question_categories instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\qbank_managecategories\question_categories', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $lastlist = null; foreach ($contexts as $context) { $this->editlists[$context->id] = @@ -146,16 +165,35 @@ public function initialize($page, $contexts, $currentcat, $defaultcategory, $tod /** * Displays the user interface. * + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function display_user_interface(): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); // Interface for editing existing categories. $this->output_edit_lists(); } /** * Outputs a table to allow entry of a new category + * + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function output_new_table(): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $this->catform->display(); } @@ -164,8 +202,17 @@ public function output_new_table(): void { * * $this->initialize() must have already been called * + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function output_edit_lists(): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $OUTPUT; echo $OUTPUT->heading_with_help(get_string('questioncategories', 'question'), 'editcategories', 'question'); @@ -189,8 +236,17 @@ public function output_edit_lists(): void { * * @param array $categories contains category objects in a tree representation * @return array courseids flat array in form categoryid=>courseid + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function get_course_ids(array $categories): array { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $courseids = []; foreach ($categories as $key => $cat) { $courseids[$key] = $cat->course; @@ -205,8 +261,17 @@ public function get_course_ids(array $categories): array { * Edit a category, or add a new one if the id is zero. * * @param int $categoryid Category id. + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function edit_single_category(int $categoryid): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); // Interface for adding a new category. global $DB; @@ -235,9 +300,17 @@ public function edit_single_category(int $categoryid): void { * * @param array $parentstrings a list of parentstrings * @param object $category Category object + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function set_viable_parents(array &$parentstrings, object $category): void { - + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); unset($parentstrings[$category->id]); if (isset($category->children)) { foreach ($category->children as $child) { @@ -252,8 +325,17 @@ public function set_viable_parents(array &$parentstrings, object $category): voi * @param int|null $parent - if given, restrict records to those with this parent id. * @param string $sort - [[sortfield [,sortfield]] {ASC|DESC}]. * @return array categories. + * @deprecated Since Moodle 4.5 MDL-72397. No longer used. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'No longer used', + mdl: 'MDL-72397', + )] public function get_question_categories(int $parent = null, string $sort = "sortorder ASC"): array { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $COURSE, $DB; if (is_null($parent)) { $categories = $DB->get_records('question_categories', ['course' => $COURSE->id], $sort); @@ -268,8 +350,17 @@ public function get_question_categories(int $parent = null, string $sort = "sort * Deletes an existing question category. * * @param int $categoryid id of category to delete. + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::delete_category', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function delete_category(int $categoryid): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $CFG, $DB; helper::question_can_delete_cat($categoryid); if (!$category = $DB->get_record("question_categories", ["id" => $categoryid])) { // Security. @@ -293,8 +384,17 @@ public function delete_category(int $categoryid): void { * * @param int $oldcat id of the old category. * @param int $newcat id of the new category. + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::move_questions_and_delete_category', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function move_questions_and_delete_category(int $oldcat, int $newcat): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); helper::question_can_delete_cat($oldcat); $this->move_questions($oldcat, $newcat); $this->delete_category($oldcat); @@ -310,6 +410,7 @@ public function move_questions_and_delete_category(int $oldcat, int $newcat): vo * @deprecated No longer used by internal code and not recommended since Moodle 4.2 MDL-77299. */ public function display_move_form($questionsincategory, $category): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); debugging( 'display_move_form() is deprecated and no longer used by internal code.', DEBUG_DEVELOPER @@ -328,8 +429,17 @@ public function display_move_form($questionsincategory, $category): void { * @param int $oldcat id of the old category. * @param int $newcat id of the new category. * @throws \dml_exception + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::move_questions', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function move_questions(int $oldcat, int $newcat): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $questionids = $this->get_real_question_ids_in_category($oldcat); question_move_questions_to_category($questionids, $newcat); } @@ -348,9 +458,18 @@ public function move_questions(int $oldcat, int $newcat): void { * @param int|string $newinfoformat description format. One of the FORMAT_ constants. * @param null $idnumber the idnumber. '' is converted to null. * @return bool|int New category id if successful, else false. + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::add_category', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function add_category($newparent, $newcategory, $newinfo, $return = false, $newinfoformat = FORMAT_HTML, $idnumber = null): int { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $DB; if (empty($newcategory)) { throw new moodle_exception('categorynamecantbeblank', 'question'); @@ -414,9 +533,18 @@ public function add_category($newparent, $newcategory, $newinfo, $return = false * @param int|string $newinfoformat description format. One of the FORMAT_ constants. * @param int $idnumber the idnumber. '' is converted to null. * @param bool $redirect if true, will redirect once the DB is updated (default). + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::update_category', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function update_category($updateid, $newparent, $newname, $newinfo, $newinfoformat = FORMAT_HTML, $idnumber = null, $redirect = true): void { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $CFG, $DB; if (empty($newname)) { throw new moodle_exception('categorynamecantbeblank', 'question'); @@ -501,8 +629,17 @@ public function update_category($updateid, $newparent, $newname, $newinfo, $newi * * @param int $categoryid id of the category. * @return int[] array of question ids. + * @deprecated Since Moodle 4.5 MDL-72397. Use \core_question\category_manager instead. + * @todo Final removal in Moodle 6.0 MDL-80804 */ + #[\core\attribute\deprecated( + '\core_question\category_manager::get_real_question_ids_in_category', + since: 4.5, + reason: 'API properly divided between qbank_managecategories and core_question', + mdl: 'MDL-72397', + )] public function get_real_question_ids_in_category(int $categoryid): array { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); global $DB; $sql = "SELECT q.id diff --git a/question/bank/managecategories/db/services.php b/question/bank/managecategories/db/services.php new file mode 100644 index 0000000000000..ee7b82c2bb569 --- /dev/null +++ b/question/bank/managecategories/db/services.php @@ -0,0 +1,36 @@ +. + +/** + * qbank_managecategories external functions and service definitions. + * @package qbank_managecategories + * @category webservice + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author 2021, Ghaly Marc-Alexandre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'qbank_managecategories_move_category' => [ + 'classname' => 'qbank_managecategories\external\move_category', + 'description' => 'Move a question category', + 'type' => 'write', + 'capabilities' => 'moodle/question:managecategory', + 'ajax' => true, + ], +]; diff --git a/question/bank/managecategories/lang/en/qbank_managecategories.php b/question/bank/managecategories/lang/en/qbank_managecategories.php index 2c60dcc732ed4..e03f474271ba9 100644 --- a/question/bank/managecategories/lang/en/qbank_managecategories.php +++ b/question/bank/managecategories/lang/en/qbank_managecategories.php @@ -23,5 +23,18 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$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['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'; +$string['idnumberexists'] = 'ID number already in use, please change it to move or update category'; +$string['lastcategoryinthiscontext'] = 'This category is the only present under context, +please add another category if this one is to be moved'; +$string['movecategory'] = 'Move {$a}'; +$string['newchild'] = 'As new child of {$a}'; $string['pluginname'] = 'Manage categories'; -$string['privacy:metadata'] = 'The Manage categories question bank plugin does not store any personal data.'; +$string['privacy:preference:showdescriptions'] = 'A flag to indicate whether or not category descriptions are shown.'; +$string['showcategorydescription'] = 'Show descriptions'; diff --git a/question/bank/managecategories/lib.php b/question/bank/managecategories/lib.php new file mode 100644 index 0000000000000..f1190dccc262d --- /dev/null +++ b/question/bank/managecategories/lib.php @@ -0,0 +1,40 @@ +. + +/** + * Callbacks for qbank_manageacategories + * + * @package qbank_managecategories + * @copyright 2024 Catalyst IT Europe Ltd + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Allow update of user preferences via AJAX. + * + * @return array[] + */ +function qbank_managecategories_user_preferences(): array { + return [ + 'qbank_managecategories_showdescriptions' => [ + 'type' => PARAM_BOOL, + 'null' => NULL_NOT_ALLOWED, + 'default' => false, + 'permissioncallback' => [core_user::class, 'is_current_user'], + ], + ]; +} diff --git a/question/bank/managecategories/styles.css b/question/bank/managecategories/styles.css new file mode 100644 index 0000000000000..1139c6314682f --- /dev/null +++ b/question/bank/managecategories/styles.css @@ -0,0 +1,107 @@ +/* Card-style border on category lists */ +.qbank_managecategories-categorylist { + padding: 1rem; + border: 1px solid #dee2e6; + border-radius: 1rem; +} +/* Category item layout */ +.qbank_managecategories-handlecontainer { + width: 32px; + height: 32px; + display: inline-flex; + justify-content: center; + align-items: center; +} +.qbank_managecategories-item .container { + padding: 0.75rem; +} +/* Separators between each item become drop indicators */ +.qbank_managecategories-item { + width: 100%; + list-style: none; + border-top: 1px solid #dee2e6; +} +.qbank_managecategories-item:first-child { + border-top-color: transparent; +} +.qbank_managecategories-item:last-child { + border-bottom: 1px solid transparent; +} +.qbank_managecategories-item .qbank_managecategories-item:first-child { + border-top-color: #dee2e6; +} +.qbank_managecategories-item.qbank_managecategories-droptarget-before, +.qbank_managecategories-item .qbank_managecategories-item.qbank_managecategories-droptarget-before, +.qbank_managecategories-categorylist.qbank_managecategories-droptarget-before > .qbank_managecategories-item:first-child { + border-top-color: black; +} +.qbank_managecategories-item.qbank_managecategories-droptarget, +.qbank_managecategories-categorylist.qbank_managecategories-droptarget > .qbank_managecategories-item:last-child { + border-bottom-color: black; +} +.qbank_managecategories-details p:last-child { + margin-bottom: 0; +} +.qbank_managecategories-childlistcontainer:empty { + display: none; +} +/* Fade effect on drag handle */ +.qbank_managecategories-draghandle { + display: none; + opacity: 0.45; +} +.qbank_managecategories-item.draghandle .qbank_managecategories-draghandle { + display: inline; +} +/* Drag indicator */ +.qbank_managecategories-item.draghandle > .container { + border: 2px solid transparent; + border-radius: 1rem; +} +.qbank_managecategories-item.draghandle > .container:hover { + cursor: move; + border-color: #0f6cbf; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} +/* No move/delete buttons on non-draggable items */ +.dropdown-item.show-when-movable { + display: none; +} +.qbank_managecategories-item.draghandle .dropdown-item.show-when-movable { + display: block; +} +/* Don't show card border on nested lists. */ +.qbank_managecategories-categorylist .qbank_managecategories-categorylist { + padding: 0 0 .5rem 0; + border: 0; + border-radius: 0; +} +/* Don't show hover indication on parent when a child is hovered */ +.editing .qbank_managecategories-item .container:hover:has(.container:hover) { + border-color: transparent; +} +/* New child drop target */ +.qbank_managecategories-newchild { + display: none; + border: 1px solid #dee2e6; + background-color: #fff; + padding: 0.25rem 1rem; + color: #dee2e6; + font-weight: bold; +} +.qbank_managecategories-newchild.qbank_managecategories-droptarget, +/* Show the drop target if it's directly inside a highlighted category, but not if its inside a child of that category */ +.qbank_managecategories-droptarget-before > .container > .row > .qbank_managecategories-contentcontainer > :not(.qbank_managecategories-childlistcontainer) .qbank_managecategories-newchild { + display: unset; +} +.qbank_managecategories-newchild.qbank_managecategories-droptarget { + border-color: black; + color: black; +} +/* Hidden descriptions */ +.qbank_managecategories-description { + display: none; +} +.showdescriptions .qbank_managecategories-description { + display: unset; +} diff --git a/question/bank/managecategories/templates/addcategory.mustache b/question/bank/managecategories/templates/addcategory.mustache new file mode 100644 index 0000000000000..6ebf0ee60efa5 --- /dev/null +++ b/question/bank/managecategories/templates/addcategory.mustache @@ -0,0 +1,46 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/addcategory + + Add category button + + Context variables required for this template: + * contextid - Context id for js init. + * cmid - Course module id, if in a plugin. + * courseid - The current course ID. + + Example context (json): + { + "contextid": "18", + "cmid": "1234", + "courseid": "1234" + } +}} +
+ +
+ +{{#js}} + require(['qbank_managecategories/addcategory'], (addcategory) => { + addcategory.init('#qbank_managecategories-addcategory'); + }); +{{/js}} diff --git a/question/bank/managecategories/templates/categories.mustache b/question/bank/managecategories/templates/categories.mustache new file mode 100644 index 0000000000000..6ed0a54711a4e --- /dev/null +++ b/question/bank/managecategories/templates/categories.mustache @@ -0,0 +1,79 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/categories + + Template for displaying category view. + + Context variables required for this template: + * contextid - Context id for js init. + * categoriesrendered - Array representing categories data rendered. + * items - Categories data. + * heading - Category's heading for each context (ie: Course or Quiz). + * ctxlvl - Context level for categories section - context. + + Example context (json): + { + "contextid": "18", + "categoriesrendered": [{ + "items": [{ + "categoryid": "1", + "questionbankurl": "question/edit.php?cmid=123", + "categoryname": "Default for Miscellaneous", + "idnumber": "1", + "questioncount": " (1)", + "categorydesc": "The default category for questions shared in context Miscellaneous", + "editactionmenu": "
...
", + "handle": true, + "iconleft": "", + "iconright": "", + "children": "" + }], + "heading": "Question categories for 'Quiz: qz'", + "ctxlvl": "contextlevel70" + }] + } +}} +
+ {{#categoriesrendered}} +

{{heading}}

+
+
    + {{#items}} + {{> qbank_managecategories/category }} + {{/items}} +
+
+ {{/categoriesrendered}} +
+ + +{{#js}} + require(['qbank_managecategories/categoryroot'], (categoryroot) => { + categoryroot.init('#categoryroot'); + }); + require(['qbank_managecategories/categorylist'], (categorylist) => { + // Initialise top-level lists. + const categoryLists = document.querySelectorAll('.qbank_managecategories-categorylist[data-contextid]'); + categoryLists.forEach(element => { + categorylist.init(`.qbank_managecategories-categorylist[data-categoryid="${element.dataset.categoryid}"]`); + }); + }); +{{/js}} diff --git a/question/bank/managecategories/templates/categories_header.mustache b/question/bank/managecategories/templates/categories_header.mustache new file mode 100644 index 0000000000000..48fb1e35f241e --- /dev/null +++ b/question/bank/managecategories/templates/categories_header.mustache @@ -0,0 +1,51 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/categories_header + + Template for displaying category view. + + Context variables required for this template: + * helpstringhead - String header accompanied with it help button. + * hascapability - Boolean, true if user has capability to add categories. + * checkbox - Checkbox displaying descriptions. + * contextid - Context id for js init. + * cmid - Course module id, if in a plugin. + + Example context (json): + { + "helpstringhead": "

Edit categories ...

", + "hascapability": true, + "checkbox": "
...
", + "contextid": "18", + "cmid": "1234", + "courseid": "1234" + } +}} +
+
+ {{{helpstringhead}}} +
+
+
+ {{#hascapability}} + {{>qbank_managecategories/addcategory}} + {{/hascapability}} + {{#showdescriptions}} + {{>qbank_managecategories/showdescriptions}} + {{/showdescriptions}} +
diff --git a/question/bank/managecategories/templates/category.mustache b/question/bank/managecategories/templates/category.mustache new file mode 100644 index 0000000000000..68b2db3a11ddd --- /dev/null +++ b/question/bank/managecategories/templates/category.mustache @@ -0,0 +1,84 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/category + + This template renders the list item for each category. + + Context variables required for this template: + * categoryid - The id of the category this item represents. + * questionbankurl - Url for corresponding question bank. + * categoryname - Category name. + * idnumber - Id number string (optional). + * questioncount - Number of question in question bank. + * categorydesc - Category description. + * editactionmenu - Edit action menu link. + * draghandle - Boolean if handle should be displayed (if category is movable). + * children - Children category item to curent category (same variables as those above). + + Example context (json): + { + "categoryid": "1", + "questionbankurl": "question/edit.php?cmid=123", + "categoryname": "Default for Miscellaneous", + "idnumber": "1", + "questioncount": " 1", + "categorydesc": "The default category for questions shared in context Miscellaneous", + "editactionmenu": "
...
", + "draghandle": true, + "children": "" + } +}} +
  • +
    +
    +
    + + + +
    +
    +
    +
    + {{>qbank_managecategories/category_details}} +
    +
    + {{#editactionmenu}} + {{>core/action_menu}} + {{/editactionmenu}} +
    +
    +
    + {{#haschildren}} + {{> qbank_managecategories/childlist }} + {{/haschildren}} +
    +
    +
    +
    +
  • +{{#js}} + require(['qbank_managecategories/category'], function(component) { + component.init('#category-{{{categoryid}}}'); + }); +{{/js}} diff --git a/question/bank/managecategories/templates/category_details.mustache b/question/bank/managecategories/templates/category_details.mustache new file mode 100644 index 0000000000000..78ffee3b7b3d4 --- /dev/null +++ b/question/bank/managecategories/templates/category_details.mustache @@ -0,0 +1,64 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/category_details + + Partial template for rendering category details. Name, question count, tags, description, etc. + + Context variables required for this template: + * categoryid - The id of the category this item represents. + * questionbankurl - Url for corresponding question bank. + * categoryname - Category name. + * idnumber - Id number string (optional). + * questioncount - Number of question in question bank. + * categorydesc - Category description. + * editactionmenu - Edit action menu link. + * draghandle - Boolean if handle should be displayed (if category is movable). + * children - Children category item to curent category (same variables as those above). + + Example context (json): + { + "categoryid": "1", + "questionbankurl": "question/edit.php?cmid=123", + "categoryname": "Default for Miscellaneous", + "idnumber": "1", + "questioncount": " 1", + "categorydesc": "The default category for questions shared in context Miscellaneous", + "children": "", + "newchildtooltip": "New child of Default for Miscellaneous" + } +}} + + + {{{categoryname}}} + {{#idnumber}} + + + {{#str}}idnumber, question{{/str}} + + {{idnumber}} + + {{/idnumber}} + ({{questioncount}}) + + +{{^children}} + {{> qbank_managecategories/newchild }} +{{/children}} +
    + {{{categorydesc}}} +
    diff --git a/question/bank/managecategories/templates/childlist.mustache b/question/bank/managecategories/templates/childlist.mustache new file mode 100644 index 0000000000000..cf1f5f89eb080 --- /dev/null +++ b/question/bank/managecategories/templates/childlist.mustache @@ -0,0 +1,52 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/childlist + + This template a sub-list of children within a category. + + Context variables required for this template: + * categoryid - The parent category of the child categories. + * children - Children category item to curent category (same variables as qbank_managecategories/category). + + Example context (json): + { + "categoryid": "1", + "children": [ + { + "categoryid": "2", + "questionbankurl": "question/edit.php?cmid=123", + "categoryname": "Default for Miscellaneous", + "idnumber": "1", + "questioncount": " 1", + "categorydesc": "The default category for questions shared in context Miscellaneous", + "editactionmenu": "
    ...
    ", + "draghandle": true + } + ] + } +}} +
      + {{#children}} + {{> qbank_managecategories/category }} + {{/children}} +
    +{{#js}} + require(['qbank_managecategories/categorylist'], function(component) { + component.init('.qbank_managecategories-categorylist[data-categoryid="{{categoryid}}"]'); + }); +{{/js}} diff --git a/question/bank/managecategories/templates/listitem.mustache b/question/bank/managecategories/templates/listitem.mustache index ba2db7834299c..8f23f33519ccb 100644 --- a/question/bank/managecategories/templates/listitem.mustache +++ b/question/bank/managecategories/templates/listitem.mustache @@ -49,7 +49,7 @@ {{#categorydesc}} -
    +
    {{{categorydesc}}}
    {{/categorydesc}} diff --git a/question/bank/managecategories/templates/move_category_list.mustache b/question/bank/managecategories/templates/move_category_list.mustache new file mode 100644 index 0000000000000..9004544e1ec00 --- /dev/null +++ b/question/bank/managecategories/templates/move_category_list.mustache @@ -0,0 +1,117 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/move_category_list + + Partial to render a list of categories that the current category can be moved next to. + + This partial is included recursively to render nested lists. Take care to pass categories = null for bottom-level + categories, to avoid infinite recursion. + + 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. + * categories - Array containing child categories of this category. + If there are none, this must be set null or [] 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, + "categories": [ + { + "categoryname": "Test category 4", + "categoryid": 7, + "firstchild": true, + "current": false, + "hascategories": false, + "categories": [] + } + ] + } + ] + } +}} + + diff --git a/question/bank/managecategories/templates/move_context_list.mustache b/question/bank/managecategories/templates/move_context_list.mustache new file mode 100644 index 0000000000000..69abdabe2f8e2 --- /dev/null +++ b/question/bank/managecategories/templates/move_context_list.mustache @@ -0,0 +1,83 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/move_context_list + + Modal to move a category. + + 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. + + Example context (json): + { + "contexts": [ + { + "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": [] + } + ] + }, + { + "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": [] + } + ] + } + ] + } +}} +
    + {{#contexts}} +

    {{{contextname}}}

    + {{>qbank_managecategories/move_category_list}} + {{/contexts}} +
    diff --git a/question/bank/managecategories/templates/newchild.mustache b/question/bank/managecategories/templates/newchild.mustache new file mode 100644 index 0000000000000..350cda59abb9c --- /dev/null +++ b/question/bank/managecategories/templates/newchild.mustache @@ -0,0 +1,46 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/newchild + + Drop zone for creating a new child category. + + Context variables required for this template: + * categoryid - The id of the parent category. + + Example context (json): + { + "categoryid": "1", + "newchildtooltip": "As new child of Example" + } +}} + + + + + +{{#js}} + require(['qbank_managecategories/newchild'], function(component) { + component.init('.qbank_managecategories-newchild[data-parent="{{categoryid}}"]'); + }); +{{/js}} diff --git a/question/bank/managecategories/templates/showdescriptions.mustache b/question/bank/managecategories/templates/showdescriptions.mustache new file mode 100644 index 0000000000000..b8310270c9a80 --- /dev/null +++ b/question/bank/managecategories/templates/showdescriptions.mustache @@ -0,0 +1,43 @@ +{{! + 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 . +}} +{{! + @template qbank_managecategories/showdescriptions + + Template for displaying the Show descriptions toggle. + + Context variables required for this template: + * helpstringhead - String header accompanied with it help button. + * hascapability - Boolean, true if user has capability to add categories. + * checkbox - Checkbox displaying descriptions. + * contextid - Context id for js init. + * cmid - Course module id, if in a plugin. + + Example context (json): + { + "id": "showdescriptions-toggle", + "label": "Show descriptions", + "checked": true + } +}} +
    + {{>core/toggle}} +
    +{{#js}} + require(['qbank_managecategories/showdescriptions'], function(component) { + component.init('#qbank_managecategories-showdescriptions'); + }); +{{/js}} diff --git a/question/bank/managecategories/tests/behat/move_question_categories.feature b/question/bank/managecategories/tests/behat/move_question_categories.feature index 85953139c70d8..19b39b6fb63b5 100644 --- a/question/bank/managecategories/tests/behat/move_question_categories.feature +++ b/question/bank/managecategories/tests/behat/move_question_categories.feature @@ -1,43 +1,101 @@ -@core @core_question -Feature: A teacher can move question categories in the question bank - In order to organize my questions +@qbank @qbank_managecategories @category_reorder @javascript +Feature: A teacher can reorder question categories + In order to change question category order As a teacher - I create question categories and move them in the question bank + I need to reorder them Background: Given the following "users" exist: | username | firstname | lastname | email | - | teacher1 | T1 | Teacher1 | teacher1@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: - | fullname | shortname | category | - | Course 1 | C1 | 0 | + | fullname | shortname | format | + | Course 1 | C1 | weeks | + And the following "categories" exist: + | name | category | idnumber | + | Category 1 | 0 | CAT1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | - And the following "activities" exist: - | activity | name | course | idnumber | - | quiz | Test quiz | C1 | quiz1 | - - Scenario: A question category can be moved to another context - Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1" - And I select "Categories" from the "Question bank tertiary navigation" singleselect - And I follow "Add category" - And I set the following fields to these values: - | Name | Test category | - | Parent category | Top for Test quiz | - And I press "submitbutton" - And I click on "Share in context for Course: Course 1" "link" in the "Test category" "list_item" - Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel50 ')]" "xpath_element" - - Scenario: A question category can be moved to top level - Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1" - And I select "Categories" from the "Question bank tertiary navigation" singleselect - And I follow "Add category" - And I set the following fields to these values: - | Name | Test category | - | Parent category | Default for Test quiz | - | Category info | Created as a test | - And I press "submitbutton" - And I click on "Move to top level" "link" in the "Test category" "list_item" - Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel70 ')]" "xpath_element" - And "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel70 ')]//li//ul" "xpath_element" should not exist + And the following "system role assigns" exist: + | user | role | contextlevel | + | teacher1 | editingteacher | System | + And the following "question categories" exist: + | contextlevel | reference | name | idnumber | + | Course | C1 | Course category 1 | questioncat1 | + | Course | C1 | Course category 2 | questioncat2 | + | Course | C1 | Course category 3 | questioncat3 | + | Category | CAT1 | Default for Category 1 | | + | System | S1 | System category | | + And I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1" + + Scenario: Teacher cannot move or delete single category under context + When I open the action menu in "Default for Category 1" "list_item" + Then I should not see "Delete" + + Scenario: Teacher can see complete edit menu if multiples categories exist under context + When I open the action menu in "Course category 1" "list_item" + Then I should see "Edit settings" + And I should see "Delete" + And I should see "Export as Moodle XML" + + Scenario: Teacher can move one category after another + Given "Course category 1" "list_item" should appear before "Course category 2" "list_item" + And "Course category 2" "list_item" should appear before "Course category 3" "list_item" + When I open the action menu in "Course category 1" "list_item" + And I choose "Move" in the open action menu + And I click on "After Course category 3" "link" in the "Move Course category 1" "dialogue" + Then "Course category 2" "list_item" should appear before "Course category 3" "list_item" + And "Course category 3" "list_item" should appear before "Course category 1" "list_item" + + Scenario: Teacher can move one category before another + Given "Course category 1" "list_item" should appear before "Course category 2" "list_item" + And "Course category 2" "list_item" should appear before "Course category 3" "list_item" + And I open the action menu in "Course category 3" "list_item" + And I choose "Move" in the open action menu + And I click on "Before Course category 1" "link" in the "Move Course category 3" "dialogue" + Given "Course category 3" "list_item" should appear before "Course category 1" "list_item" + And "Course category 1" "list_item" should appear before "Course category 2" "list_item" + + Scenario: Teacher can make a category a child of an existing category + Given "Course category 1" "list_item" should appear before "Course category 2" "list_item" + And "Course category 2" "list_item" should appear before "Course category 3" "list_item" + And "Course category 3" "list_item" should not exist in the "Course category 1" "list_item" + When I open the action menu in "Course category 3" "list_item" + And I choose "Move" in the open action menu + And I click on "As new child of Course category 1" "link" in the "Move Course category 3" "dialogue" + And "Course category 3" "list_item" should appear before "Course category 2" "list_item" + And "Course category 3" "list_item" should exist in the "Course category 1" "list_item" + + Scenario: Teacher can move a category between contexts + Given "Course: Course 1" "text" should appear before "Course category 1" "list_item" + And "Category: Category 1" "text" should appear after "Course category 1" "list_item" + And "Category: Category 1" "text" should appear before "Default for Category 1" "list_item" + And "Course category 2" "list_item" should appear before "Course category 3" "list_item" + When I open the action menu in "Course category 1" "list_item" + And I choose "Move" in the open action menu + And I click on "After Default for Category 1" "link" in the "Move Course category 1" "dialogue" + Then "Course: Course 1" "text" should appear before "Course category 1" "list_item" + And "Category: Category 1" "text" should appear before "Course category 1" "list_item" + And "Default for Category 1" "list_item" should appear before "Course category 1" "list_item" + + Scenario: Teacher can display and hide category descriptions + When I click on "Show descriptions" "checkbox" + Then I should see "The default category for questions shared in context 'Category 1'." + And I click on "Show descriptions" "checkbox" + And I should not see "The default category for questions shared in context 'Category 1'." + + Scenario: Teacher cannot create a duplicate idnumber within a context by moving a category + Given "Course category 1" "list_item" should appear before "System category" "list_item" + And I open the action menu in "Course category 1" "list_item" + And I choose "Move" in the open action menu + And I click on "After System category" "link" in the "Move Course category 1" "dialogue" + Then "Course category 1" "list_item" should appear after "System category" "list_item" + And I open the action menu in "Course category 2" "list_item" + And I choose "Edit settings" in the open action menu + And I set the field "ID number" to "questioncat1" + And I click on "Save changes" "button" in the "Edit category" "dialogue" + And I open the action menu in "Course category 2" "list_item" + And I choose "Move" in the open action menu + And I click on "After Course category 1" "link" in the "Move Course category 2" "dialogue" + And I should see "ID number already in use, please change it to move or update category" diff --git a/question/bank/managecategories/tests/behat/question_categories.feature b/question/bank/managecategories/tests/behat/question_categories.feature index 593323521ccac..62a79ca9e1bcd 100644 --- a/question/bank/managecategories/tests/behat/question_categories.feature +++ b/question/bank/managecategories/tests/behat/question_categories.feature @@ -1,4 +1,4 @@ -@qbank @qbank_managecategories @javascript +@qbank @qbank_managecategories @question_categories @javascript Feature: A teacher can put questions in categories in the question bank In order to organize my questions As a teacher @@ -6,44 +6,44 @@ Feature: A teacher can put questions in categories in the question bank Background: Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | - | Course 1 | C1 | weeks | + | Course 1 | C1 | weeks | And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | + | user | course | role | + | teacher1 | C1 | editingteacher | And the following "question categories" exist: - | contextlevel | reference | questioncategory | name | - | Course | C1 | Top | top | - | Course | C1 | top | Default for C1 | - | Course | C1 | Default for C1 | Subcategory | - | Course | C1 | Default for C1 | Another subcat | - | Course | C1 | top | Used category | - | Course | C1 | top | Default & testing | + | contextlevel | reference | questioncategory | name | sortorder | desciption | + | Course | C1 | Top | top | 0 | | + | Course | C1 | top | Default for C1 | 0 | Description for default for C1 | + | Course | C1 | Default for C1 | Subcategory | 0 | Description for Subcategory | + | Course | C1 | Default for C1 | Another subcat | 1 | Description for Another subcat | + | Course | C1 | top | Used category | 1 | | + | Course | C1 | top | Default & testing | 2 | | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Used category | essay | Test question to be moved | Write about whatever you want | | Another subcat | essay | Question 1 | Write about whatever you want | And I log in as "teacher1" - And I am on "Course 1" course homepage Scenario: A new question category can be created When I am on the "Course 1" "core_question > course question categories" page - And I follow "Add category" + And I press "Add category" And I set the following fields to these values: | Name | 'Test' & 'display' | | Parent category | Default & testing | | Category info | Created for testing category, HTML entity & its encoding | | ID number | newcatidnumber | - And I press "submitbutton" + And I click on "Add category" "button" in the "Add category" "dialogue" Then I should see "Default & testing" And I should see "ID number" And I should see "newcatidnumber" And I should see "(0)" + And I click on "Show descriptions" "checkbox" And I should see "Created for testing category, HTML entity & its encoding" in the "'Test' & 'display'" "list_item" - And I follow "Add category" + And I press "Add category" And the "Parent category" select box should contain "'Test' & 'display' [newcatidnumber]" Scenario: A question category can be edited @@ -52,29 +52,35 @@ Feature: A teacher can put questions in categories in the question bank And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Subcategory | essay | Test question for renaming category | Write about whatever you want | - And I click on "Edit this category" "link" in the "Subcategory" "list_item" + And I open the action menu in "Subcategory" "list_item" + And I choose "Edit settings" in the open action menu And the field "parent" matches value "   Default for C1" And I set the following fields to these values: | Name | New name | | Category info | I was edited | And I press "Save changes" Then I should see "New name" - And I should see "I was edited" in the "New name" "list_item" + And I click on "Show descriptions" "checkbox" + And I should see "I was edited" Scenario: An empty question category can be deleted When I am on the "Course 1" "core_question > course question categories" page - And I click on "Delete" "link" in the "Subcategory" "list_item" + And I open the action menu in "Subcategory" "list_item" + And I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete" "dialogue" Then I should not see "Subcategory" Scenario: An non-empty question category can be deleted if you move the contents elsewhere When I am on the "Course 1" "core_question > course question categories" page - And I click on "Delete" "link" in the "Used category" "list_item" + And I open the action menu in "Used category" "list_item" + And I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete" "dialogue" And I should see "The category 'Used category' contains 1 questions" And I select "Default for C1" from the "Category" singleselect And I press "Save in category" Then I should not see "Used category" - And I follow "Add category" - And I should see "Default for C1 (1)" + And I should see "Default for C1" + And I should see "(1)" @_file_upload Scenario: Multi answer questions with their child questions can be moved to another category when the current category is deleted @@ -84,12 +90,13 @@ Feature: A teacher can put questions in categories in the question bank And I press "id_submitbutton" And I press "Continue" And I am on the "Course 1" "core_question > course question categories" page - And I click on "Delete" "link" in the "Default for Test images in backup" "list_item" + And I open the action menu in "Default for Test images in backup" "list_item" + And I choose "Delete" in the open action menu And I should see "The category 'Default for Test images in backup' contains 1 questions" And I select "Used category" from the "Category" singleselect And I press "Save in category" Then I should not see "Default for Test images in backup" - And I follow "Add category" + And I press "Add category" And I should see "Used category (2)" Scenario: Filter questions by category and subcategories diff --git a/question/bank/managecategories/tests/behat/question_categories_idnumber.feature b/question/bank/managecategories/tests/behat/question_categories_idnumber.feature index 0c23df2e4791f..2db6176742c0b 100644 --- a/question/bank/managecategories/tests/behat/question_categories_idnumber.feature +++ b/question/bank/managecategories/tests/behat/question_categories_idnumber.feature @@ -1,4 +1,4 @@ -@qbank @qbank_managecategories +@qbank @qbank_managecategories @question_categories_idnumber @javascript Feature: A teacher can put questions with idnumbers in categories with idnumbers in the question bank In order to organize my questions As a teacher @@ -20,34 +20,36 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers Scenario: A new question category can only be created with a unique idnumber for a context # Note need to create the top category each time. When the following "question categories" exist: - | contextlevel | reference | questioncategory | name | idnumber | - | Course | C1 | Top | top | | - | Course | C1 | top | Used category | c1used | + | contextlevel | reference | questioncategory | name | idnumber | + | Course | C1 | Top | top | | + | Course | C1 | top | Used category | c1used | And I am on the "Course 1" "core_question > course question categories" page - And I follow "Add category" + And I press "Add category" And I set the following fields to these values: - | Name | Sub used category | - | Parent category | Used category | + | Name | New cat | + | Parent category | Top for Course 1 | | Category info | Created as a test | | ID number | c1used | - And I press "submitbutton" + And I click on "Add category" "button" in the "Add category" "dialogue" # Standard warning. Then I should see "This ID number is already in use" # Correction to a unique idnumber for the context. And I set the field "ID number" to "c1unused" - And I press "submitbutton" - Then I should see "Sub used category" + And I click on "Add category" "button" in the "Add category" "dialogue" + Then I should see "New cat" And I should see "ID number" And I should see "c1unused" And I should see "(0)" - And I should see "Created as a test" in the "Sub used category" "list_item" + And I click on "Show descriptions" "checkbox" + And I should see "Created as a test" in the "New cat" "list_item" Scenario: A question category can be edited and saved without changing the idnumber When the following "question categories" exist: - | contextlevel | reference | questioncategory | name | idnumber | - | Course | C1 | Top | top | | - | Course | C1 | top | Used category | c1used | + | contextlevel | reference | questioncategory | name | idnumber | + | Course | C1 | Top | top | | + | Course | C1 | top | Used category | c1used | And I am on the "Course 1" "core_question > course question categories" page - And I click on "Edit this category" "link" in the "Used category" "list_item" - And I press "Save changes" + Then I open the action menu in "Used category" "list_item" + And I choose "Edit settings" in the open action menu + And I click on "Save changes" "button" in the "Edit category" "dialogue" Then I should not see "This ID number is already in use" diff --git a/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature b/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature index cf6f5ce93e1ed..457108cf7200b 100644 --- a/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature +++ b/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature @@ -1,4 +1,4 @@ -@qbank @qbank_managecategories @javascript +@qbank @qbank_managecategories @view_manage_categories_plugin @javascript Feature: Use the qbank plugin manager page for managecategories In order to check the plugin behaviour with enable and disable @@ -7,8 +7,8 @@ Feature: Use the qbank plugin manager page for managecategories | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "activities" exist: - | activity | name | course | idnumber | - | quiz | Test quiz | C1 | quiz1 | + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | And the following "question categories" exist: | contextlevel | reference | name | | Course | C1 | Test questions | @@ -26,7 +26,7 @@ Feature: Use the qbank plugin manager page for managecategories And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration And I click on "Enable" "link" in the "Manage categories" "table_row" And I am on the "Test quiz" "mod_quiz > question bank" page - And I should see "Categories" in the "//div[contains(@class, 'urlselect')]//option[contains(text(), 'Categories')]" "xpath_element" + And I should see "Categories" in the "Question bank tertiary navigation" "select" Scenario: Enable/disable the tab New category when trying to add a random question to a quiz Given I log in as "admin" diff --git a/question/bank/managecategories/tests/external/move_category_test.php b/question/bank/managecategories/tests/external/move_category_test.php new file mode 100644 index 0000000000000..afd9525d17303 --- /dev/null +++ b/question/bank/managecategories/tests/external/move_category_test.php @@ -0,0 +1,535 @@ +. + +namespace qbank_managecategories\external; + +use context_course; +use qbank_managecategories\helper; +use qbank_managecategories\question_categories; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../manage_category_test_base.php'); + +/** + * Unit tests for move_category + * + * @package qbank_managecategories + * @copyright 2024 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 + * @covers \qbank_managecategories\external\move_category + */ +final class move_category_test extends \qbank_managecategories\manage_category_test_base { + + /** + * Return order of categories for a given context. + * + * @param int $contextid The context to get the category order for. + * @return array Nested array, keyed by category IDs. + */ + private function get_current_order(int $contextid): array { + return $this->reduce_tree(question_categories::create_ordered_tree(helper::get_categories_for_contexts($contextid))); + } + + /** + * Reduce the ordered tree of categories to a multi-dimensional array of IDs for easier comparison. + * + * @param array $tree Tree of categories from helper::create_ordered_tree + * @return array + */ + private function reduce_tree(array $tree): array { + $result = []; + foreach ($tree as $category) { + $result[$category->id] = []; + if (isset($category->children) && !empty((array)$category->children)) { + $result[$category->id] = $this->reduce_tree($category->children); + } + } + return $result; + } + + /** + * Move a category below another category within the same parent. + * + * @return void + */ + public function test_move_category_down(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $context = context_course::instance($course->id); + $this->create_course_category(); + + // Question categories. + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + $qcat3 = $this->create_question_category_for_a_course($course); + + // Check current order. + $currentorder = $this->get_current_order($context->id); + $expectedorder = [ + $qcat1->id => [], + $qcat2->id => [], + $qcat3->id => [], + ]; + $this->assertEquals($expectedorder, $currentorder); + + // Move category 1 after category 2. + $stateupdates = move_category::execute($context->id, $qcat1->id, $qcat1->parent, $qcat2->id); + + $neworder = $this->get_current_order($context->id); + $newexpectedorder = [ + $qcat2->id => [], + $qcat1->id => [], + $qcat3->id => [], + ]; + $this->assertEquals($newexpectedorder, $neworder); + + // We should have an update to the sortorder of the moved category and following categories. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat1->id, + 'sortorder' => 3, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat3->id, + 'sortorder' => 4, + ], + ], + ]; + $this->assertEquals($stateupdates, $expectedstateupdates); + } + + /** + * Move a category to the top of its parent. + * + * @return void + * @throws \moodle_exception + */ + public function test_move_category_to_top(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $context = context_course::instance($course->id); + $this->create_course_category(); + + // Question categories. + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + $qcat3 = $this->create_question_category_for_a_course($course); + + // Check current order. + $currentorder = $this->get_current_order($context->id); + $expectedorder = [ + $qcat1->id => [], + $qcat2->id => [], + $qcat3->id => [], + ]; + $this->assertEquals($expectedorder, $currentorder); + + // Move category 3 to the top. + $stateupdates = move_category::execute($context->id, $qcat3->id, $qcat3->parent); + + $neworder = $this->get_current_order($context->id); + $newexpectedorder = [ + $qcat3->id => [], + $qcat1->id => [], + $qcat2->id => [], + ]; + $this->assertEquals($newexpectedorder, $neworder); + + // Expecting all categories to have an updated sortorder. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat3->id, + 'sortorder' => 1, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat1->id, + 'sortorder' => 2, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat2->id, + 'sortorder' => 3, + ], + ], + ]; + $this->assertEquals($stateupdates, $expectedstateupdates); + } + + /** + * Move a category to a new parent that doesn't currently have any children. + * + * @return void + */ + public function test_move_category_as_new_child(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $context = context_course::instance($course->id); + $this->create_course_category(); + + // Question categories. + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + $qcat3 = $this->create_question_category_for_a_course($course); + + // Check current order. + $currentorder = $this->get_current_order($context->id); + $expectedorder = [ + $qcat1->id => [], + $qcat2->id => [], + $qcat3->id => [], + ]; + $this->assertEquals($expectedorder, $currentorder); + + // Set Category 2 as the parent of Category 1. + $stateupdates = move_category::execute($context->id, $qcat1->id, $qcat2->id); + + $neworder = $this->get_current_order($context->id); + $newexpectedorder = [ + $qcat2->id => [ + $qcat1->id => [], + ], + $qcat3->id => [], + ]; + $this->assertEquals($newexpectedorder, $neworder); + + // Expecting an update to the parent and sortorder of the moved category, and updated sortorders for the children + // of the original parent. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat1->id, + 'sortorder' => 1, + 'parent' => $qcat2->id, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat2->id, + 'sortorder' => 1, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat3->id, + 'sortorder' => 2, + ], + ], + ]; + $this->assertEquals($stateupdates, $expectedstateupdates); + } + + /** + * Move a category from one parent to another that already has a child. + * + * @return void + * @throws \moodle_exception + */ + public function test_move_category_between_parents(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $context = context_course::instance($course->id); + $this->create_course_category(); + + // Question categories. + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + $qcat3 = $this->create_question_category_for_a_course($course, ['parent' => $qcat1->id]); + $qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat2->id]); + + // Check current order. + $currentorder = $this->get_current_order($context->id); + $expectedorder = [ + $qcat1->id => [ + $qcat3->id => [], + ], + $qcat2->id => [ + $qcat4->id => [], + ], + ]; + $this->assertEquals($expectedorder, $currentorder); + + // Set Category 2 as the parent of Category 1. + $stateupdates = move_category::execute($context->id, $qcat3->id, $qcat2->id, $qcat4->id); + + $neworder = $this->get_current_order($context->id); + $newexpectedorder = [ + $qcat1->id => [], + $qcat2->id => [ + $qcat4->id => [], + $qcat3->id => [], + ], + ]; + $this->assertEquals($newexpectedorder, $neworder); + + // As there are no remaining children of the original parent, and this was moved to the bottom of the new parent, + // just the moved category is updated. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat3->id, + 'sortorder' => 2, + 'parent' => $qcat2->id, + ], + ], + ]; + $this->assertEquals($stateupdates, $expectedstateupdates); + } + + /** + * Move a category from the top category of one context to another. + * + * @return void + */ + public function test_move_category_between_contexts(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $coursecontext = context_course::instance($course->id); + $coursecategory = $this->create_course_category(); + $categorycontext = \context_coursecat::instance($coursecategory->id); + + // Question categories. + $coursecatqcat = question_get_top_category($categorycontext->id, true); + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + + $currentcourseorder = $this->get_current_order($coursecontext->id); + $expectedorder = [ + $qcat1->id => [], + $qcat2->id => [], + ]; + $this->assertEquals($expectedorder, $currentcourseorder); + $currentcoursecatorder = $this->get_current_order($categorycontext->id); + $expectedcoursecatorder = [ + ]; + $this->assertEquals($expectedcoursecatorder, $currentcoursecatorder); + + $stateupdates = move_category::execute($coursecontext->id, $qcat1->id, $coursecatqcat->id); + + $newcourseorder = $this->get_current_order($coursecontext->id); + $newexpectedorder = [ + $qcat2->id => [], + ]; + $this->assertEquals($newcourseorder, $newexpectedorder); + $newcoursecatorder = $this->get_current_order($categorycontext->id); + $newexpectedcoursecatorder = [ + $qcat1->id => [], + ]; + $this->assertEquals($newcoursecatorder, $newexpectedcoursecatorder); + + // Expect an update to the sortorder, parent and context of the moved category. + // Since the original sibling is the only remaining child of a top-level category, it has its draghandle property + // updated as well as its sortorder. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat1->id, + 'sortorder' => 1, + 'parent' => $coursecatqcat->id, + 'context' => $categorycontext->id, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat2->id, + 'sortorder' => 1, + 'draghandle' => false, + ], + ], + ]; + $this->assertEquals($stateupdates, $expectedstateupdates); + } + + /** + * Move a category that has its own children. + * + * The children should move with the parent. + * + * @return void + */ + public function test_move_category_with_children(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $context = context_course::instance($course->id); + $this->create_course_category(); + + // Question categories. + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + $qcat3 = $this->create_question_category_for_a_course($course); + $qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]); + $qcat5 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]); + + // Check current order. + $currentorder = $this->get_current_order($context->id); + $expectedorder = [ + $qcat1->id => [], + $qcat2->id => [], + $qcat3->id => [ + $qcat4->id => [], + $qcat5->id => [], + ], + ]; + $this->assertEquals($expectedorder, $currentorder); + + $stateupdates = move_category::execute($context->id, $qcat3->id, $qcat3->parent, $qcat1->id); + + $neworder = $this->get_current_order($context->id); + $newexpectedorder = [ + $qcat1->id => [], + $qcat3->id => [ + $qcat4->id => [], + $qcat5->id => [], + ], + $qcat2->id => [], + ]; + $this->assertEquals($neworder, $newexpectedorder); + + // Update the sortorder of the moving category and the following sibling. No updates to the children are required. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat3->id, + 'sortorder' => 2, + ], + ], + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat2->id, + 'sortorder' => 3, + ], + ], + ]; + $this->assertEquals($stateupdates, $expectedstateupdates); + } + + /** + * Move a category that has its own children to a new parent. + * + * The children should also move and become descendants of the new parent. + * + * @return void + */ + public function test_change_parent_with_children(): void { + $this->setAdminUser(); + $this->resetAfterTest(); + + // Create context for question categories. + $course = $this->create_course(); + $context = context_course::instance($course->id); + $this->create_course_category(); + + // Question categories. + $qcat1 = $this->create_question_category_for_a_course($course); + $qcat2 = $this->create_question_category_for_a_course($course); + $qcat3 = $this->create_question_category_for_a_course($course); + $qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]); + $qcat5 = $this->create_question_category_for_a_course($course, ['parent' => $qcat4->id]); + + // Check current order. + $currentorder = $this->get_current_order($context->id); + $expectedorder = [ + $qcat1->id => [], + $qcat2->id => [], + $qcat3->id => [ + $qcat4->id => [ + $qcat5->id => [], + ], + ], + ]; + $this->assertEquals($expectedorder, $currentorder); + + $stateupdates = move_category::execute($context->id, $qcat4->id, $qcat2->id); + + $neworder = $this->get_current_order($context->id); + $newexpectedorder = [ + $qcat1->id => [], + $qcat2->id => [ + $qcat4->id => [ + $qcat5->id => [], + ], + ], + $qcat3->id => [], + ]; + $this->assertEquals($newexpectedorder, $neworder); + + // Expecting an update to the sortorder and parent of the moved category. No updates to the children are required. + $expectedstateupdates = [ + (object)[ + 'name' => 'categories', + 'action' => 'put', + 'fields' => (object)[ + 'id' => $qcat4->id, + 'parent' => $qcat2->id, + 'sortorder' => 1, + ], + ], + ]; + $this->assertEquals($expectedstateupdates, $stateupdates); + } +} diff --git a/question/bank/managecategories/tests/manage_category_test_base.php b/question/bank/managecategories/tests/manage_category_test_base.php index d9e85c026053d..360dcfbd3ed7e 100644 --- a/question/bank/managecategories/tests/manage_category_test_base.php +++ b/question/bank/managecategories/tests/manage_category_test_base.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + namespace qbank_managecategories; use core_question\local\bank\question_edit_contexts; diff --git a/question/bank/managecategories/tests/privacy/provider_test.php b/question/bank/managecategories/tests/privacy/provider_test.php new file mode 100644 index 0000000000000..c7c5a2aab11b5 --- /dev/null +++ b/question/bank/managecategories/tests/privacy/provider_test.php @@ -0,0 +1,48 @@ +. + +namespace qbank_managecategories\privacy; + +use advanced_testcase; +use core_privacy\local\request\writer; +use qbank_managecategories\privacy\provider; + +/** + * Unit tests for qbank_managecategories privacy provider. + * + * @package qbank_managecategories + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author 2021, Ghaly Marc-Alexandre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \qbank_managecategories\privacy\provider + */ +final class provider_test extends advanced_testcase { + /** + * Test to check export_user_preferences. + * + * @covers ::export_user_preferences + */ + public function test_export_user_preferences(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + set_user_preference('qbank_managecategories_showdescriptions', 1, $user); + provider::export_user_preferences($user->id); + $writer = writer::with_context(\context_system::instance()); + $prefs = $writer->get_user_preferences('qbank_managecategories'); + $this->assertEquals(1, $prefs->showdescr->value); + $this->assertEquals(get_string('displaydescription', 'qbank_managecategories'), $prefs->showdescr->description); + } +} diff --git a/question/bank/managecategories/tests/question_categories_test.php b/question/bank/managecategories/tests/question_categories_test.php index d0fb344af4e05..8af31f379dacc 100644 --- a/question/bank/managecategories/tests/question_categories_test.php +++ b/question/bank/managecategories/tests/question_categories_test.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + namespace qbank_managecategories; defined('MOODLE_INTERNAL') || die(); @@ -29,7 +30,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \qbank_managecategories\question_categories */ -class question_categories_test extends manage_category_test_base { +final class question_categories_test extends manage_category_test_base { /** * Test get children. * diff --git a/question/bank/managecategories/version.php b/question/bank/managecategories/version.php index 4ac7ad3771b68..71a116f2d0735 100644 --- a/question/bank/managecategories/version.php +++ b/question/bank/managecategories/version.php @@ -26,6 +26,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qbank_managecategories'; -$plugin->version = 2024042200; +$plugin->version = 2024070400; $plugin->requires = 2024041600; $plugin->maturity = MATURITY_STABLE;