AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** 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 categorycategorymanager.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<Array>}*/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<Element>}*/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<void>}*/async updatePosition({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;}// Move to a new position within the parent.let previousSibling;let nextSibling;if (newParent.firstElementChild && parseInt(element.sortorder) <= parseInt(newParent.firstElementChild.dataset.sortorder)) {// Move to the top of the list.nextSibling = newParent.firstElementChild;} else {// Move later in the list.previousSibling = newParent.querySelector(this.selectors.PREVIOUS_SIBLING(element.sortorder - 1));nextSibling = previousSibling?.nextElementSibling;}// Check if this has actually moved, or if it's just having its sortorder updated due to another element moving.const moved = (newParent !== this.getElement().parentElement || nextSibling !== this.getElement());if (moved) {if (nextSibling) {// Move to the specified position in the list.newParent.insertBefore(this.getElement(), nextSibling);} else {// Move to the end of the list (may also be the top of the list is empty).newParent.appendChild(this.getElement());}}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<Object>}*/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;});if (precedingSibling) {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.closest(this.selectors.MOVE_BUTTON);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<Element>}*/async checkChildList({element}) {if (element.parent !== 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,]});}}