Proyectos de Subversion Moodle

Rev

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 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<void>}
     */
    async addCategory({element}) {
        if (element.parent !== 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);
        }
    }
}