Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * The category list component.
18
 *
19
 * The category list is a drop target, so that a category may be dropped at the top or bottom of the list.
20
 *
21
 * @module     qbank_managecategories/categorylist
22
 * @class      qbank_managecategories/categorylist
23
 */
24
 
25
import {BaseComponent, DragDrop} from 'core/reactive';
26
import Templates from 'core/templates';
27
import {getString} from 'core/str';
28
import {categorymanager} from 'qbank_managecategories/categorymanager';
29
 
30
export default class extends BaseComponent {
31
 
32
    create(descriptor) {
33
        this.name = descriptor.element.id;
34
        this.selectors = {
35
            CATEGORY_LIST: '.qbank_managecategories-categorylist',
36
            CATEGORY_ITEM: '.qbank_managecategories-item[data-categoryid]',
37
            CATEGORY_CONTENTS: '.qbank_managecategories-item > .container',
38
            CATEGORY_DETAILS: '.qbank_managecategories-details',
39
            CATEGORY_NO_DRAGHANDLE: '.qbank_managecategories-item[data-categoryid]:not(.draghandle)',
40
            CATEGORY_ID: id => `#category-${id}`,
41
        };
42
        this.classes = {
43
            DROP_TARGET_BEFORE: 'qbank_managecategories-droptarget-before',
44
            DROP_TARGET: 'qbank_managecategories-droptarget',
45
            NO_BOTTOM_PADDING: 'pb-0',
46
        };
47
        this.ids = {
48
            CATEGORY: id => `category-${id}`,
49
        };
50
    }
51
 
52
    stateReady() {
53
        this.dragdrop = new DragDrop(this);
54
    }
55
 
56
    destroy() {
57
        // The draggable element must be unregistered.
58
        if (this.dragdrop !== undefined) {
59
            this.dragdrop.unregister();
60
            this.dragdrop = undefined;
61
        }
62
    }
63
 
64
    /**
65
     * Static method to create a component instance.
66
     *
67
     * @param {string} target the DOM main element or its ID
68
     * @param {object} selectors optional css selector overrides
69
     * @return {Component}
70
     */
71
    static init(target, selectors) {
72
        return new this({
73
            element: document.querySelector(target),
74
            selectors,
75
            reactive: categorymanager,
76
        });
77
    }
78
 
79
    validateDropData() {
80
        return true;
81
    }
82
 
83
    /**
84
     * Highlight the border of the list where the category will be moved.
85
     *
86
     * If dropping at the top of the list, highlight the top border.
87
     * If dropping at the bottom, highlight the bottom border.
88
     *
89
     * @param {Object} dropData
90
     * @param {Event} event
91
     */
92
    showDropZone(dropData, event) {
93
        const dropTarget = this.getElement();
94
        if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {
95
            // Can't drop onto its own child.
96
            return false;
97
        }
98
        if (this.getInsertBefore(event, dropTarget)) {
99
            dropTarget.classList.add(this.classes.DROP_TARGET_BEFORE);
100
            dropTarget.classList.remove(this.classes.DROP_TARGET);
101
        } else {
102
            dropTarget.classList.add(this.classes.DROP_TARGET);
103
            dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE);
104
        }
105
        return true;
106
    }
107
 
108
    /**
109
     * Remove highlighting.
110
     *
111
     * @param {Object} dropData
112
     * @param {Event} event
113
     */
114
    hideDropZone(dropData, event) {
115
        const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST);
116
        dropTarget.classList.remove(this.classes.DROP_TARGET_BEFORE);
117
        dropTarget.classList.remove(this.classes.DROP_TARGET);
118
    }
119
 
120
    /**
121
     * Determine whether we're dragging over the top or bottom half of the list.
122
     *
123
     * @param {Event} event
124
     * @param {Element} dropTarget
125
     * @return {boolean}
126
     */
127
    getInsertBefore(event, dropTarget) {
128
        // Get the current mouse position within the drop target
129
        const mouseY = event.clientY - dropTarget.getBoundingClientRect().top;
130
 
131
        // Get the height of the drop target
132
        const targetHeight = dropTarget.clientHeight;
133
 
134
        // Check if the mouse is over the top half of the drop target
135
        return mouseY < targetHeight / 2;
136
    }
137
 
138
    /**
139
     * Find the new position of the dropped category, and trigger the move.
140
     *
141
     * @param {Object} dropData
142
     * @param {Event} event
143
     */
144
    drop(dropData, event) {
145
        const dropTarget = event.target.closest(this.selectors.CATEGORY_LIST);
146
 
147
        if (!dropTarget) {
148
            return;
149
        }
150
 
151
        if (dropTarget.closest(this.selectors.CATEGORY_ID(dropData.id))) {
152
            // Can't drop onto your own child.
153
            return;
154
        }
155
 
156
        const source = document.getElementById(this.ids.CATEGORY(dropData.id));
157
 
158
        if (!source) {
159
            return;
160
        }
161
 
162
        const targetParentId = dropTarget.dataset.categoryid;
163
        let precedingSibling;
164
 
165
        if (this.getInsertBefore(event, dropTarget)) {
166
            // Dropped at the top of the list.
167
            precedingSibling = null;
168
        } else {
169
            // Dropped at the bottom of the list.
170
            precedingSibling = dropTarget.lastElementChild;
171
        }
172
 
173
        // Insert the category after the target category
174
        categorymanager.moveCategory(dropData.id, targetParentId, precedingSibling?.dataset.categoryid);
175
    }
176
 
177
    /**
178
     * Watch for categories moving to a new parent.
179
     *
180
     * @return {Array} A list of watchers.
181
     */
182
    getWatchers() {
183
        return [
184
            // Watch for this category having its child count updated.
185
            {watch: `categoryLists[${this.element.dataset.categoryid}].childCount:updated`, handler: this.checkEmptyList},
186
            // Watch for any new category being created.
187
            {watch: `categories:created`, handler: this.addCategory},
188
        ];
189
    }
190
 
191
    /**
192
     * If this list is now empty, remove it.
193
     *
194
     * @param {Object} args
195
     * @param {Object} args.element The categoryList state element.
196
     */
197
    async checkEmptyList({element}) {
198
        if (element.childCount === 0) {
199
            // Display a new child drop zone.
200
            const categoryItem = this.getElement().closest(this.selectors.CATEGORY_ITEM);
201
            const {html, js} = await Templates.renderForPromise(
202
                'qbank_managecategories/newchild',
203
                {
204
                    categoryid: this.getElement().dataset.categoryid,
205
                    tooltip: getString('newchild', 'qbank_managecategories', categoryItem.dataset.categoryname)
206
                }
207
            );
208
            const activityNameArea = categoryItem.querySelector(this.selectors.CATEGORY_DETAILS);
209
            await Templates.appendNodeContents(activityNameArea, html, js);
210
            // Reinstate padding on the parent element.
211
            this.element.closest(this.selectors.CATEGORY_CONTENTS).classList.remove(this.classes.NO_BOTTOM_PADDING);
212
            // Remove this list.
213
            this.remove();
214
        }
215
    }
216
 
217
    /**
218
     * If a newly-created category has this list's category as its parent, add it to this list.
219
     *
220
     * @param {Object} args
221
     * @param {Object} args.element
222
     * @return {Promise<void>}
223
     */
224
    async addCategory({element}) {
225
        if (element.parent !== this.getElement().dataset.categoryid) {
226
            return; // Not for me.
227
        }
228
        const {html, js} = await Templates.renderForPromise('qbank_managecategories/category', element.templatecontext);
229
        Templates.appendNodeContents(this.getElement(), html, js);
230
        // If one of the children has no draghandle, it should do now it has a sibling.
231
        const noDragHandle = this.getElement(this.selectors.CATEGORY_NO_DRAGHANDLE);
232
        if (noDragHandle) {
233
            this.reactive.dispatch('showDragHandle', noDragHandle.dataset.categoryid);
234
        }
235
    }
236
}