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
 * Contain the logic for the bulkmove questions modal.
18
 *
19
 * @module     qbank_bulkmove/modal_question_bank_bulkmove
20
 * @copyright  2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
21
 * @author     Simon Adams <simon.adams@catalyst-eu.net>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
import Modal from 'core/modal';
26
import * as Fragment from 'core/fragment';
27
import {getString} from 'core/str';
28
import AutoComplete from 'core/form-autocomplete';
29
import {moveQuestions} from 'core_question/repository';
30
import Templates from 'core/templates';
31
import Notification from 'core/notification';
32
import Pending from 'core/pending';
33
 
34
 
35
export default class ModalQuestionBankBulkmove extends Modal {
36
    static TYPE = 'qbank_bulkmove/bulkmove';
37
 
38
    static SELECTORS = {
39
        SAVE_BUTTON: '[data-action="bulkmovesave"]',
40
        SELECTED_QUESTIONS: 'table#categoryquestions input[id^="checkq"]',
41
        SEARCH_BANK: '#searchbanks',
42
        SEARCH_CATEGORY: '.selectcategory',
43
        QUESTION_CATEGORY_SELECTOR: '.question_category_selector',
44
        CATEGORY_OPTIONS: '.selectcategory option',
45
        BANK_OPTIONS: '#searchbanks option',
46
        CATEGORY_ENHANCED_INPUT: '.search-categories input',
47
        ORIGINAL_SELECTS: 'select.bulk-move',
48
        CATEGORY_WARNING: '#searchcatwarning',
49
        CATEGORY_SUGGESTION: '.search-categories span.form-autocomplete-downarrow',
50
        CATEGORY_SELECTION: '.search-categories span[role="option"][data-active-selection="true"]',
51
        CONFIRM_BUTTON: '.bulk-move-footer button[data-action="save"]',
52
        CANCEL_BUTTON: '.bulk-move-footer button[data-action="cancel"]'
53
    };
54
 
55
    /**
56
     * @param {integer} contextId The current bank context id.
57
     * @param {integer} categoryId The current question category id.
58
     */
59
    static init(contextId, categoryId) {
60
        document.addEventListener('click', (e) => {
61
            const trigger = e.target;
62
            if (trigger.className === 'dropdown-item' && trigger.getAttribute('name') === 'move') {
63
                e.preventDefault();
64
                ModalQuestionBankBulkmove.create({
65
                    contextId,
66
                    title: getString('bulkmoveheader', 'qbank_bulkmove'),
67
                    show: true,
68
                    categoryId: categoryId,
69
                });
70
            }
71
        });
72
    }
73
 
74
    /**
75
     * Set the initialised config on the class.
76
     *
77
     * @param {Object} modalConfig
78
     */
79
    configure(modalConfig) {
80
        this.contextId = modalConfig.contextId;
81
        this.targetBankContextId = modalConfig.contextId;
82
        this.initSelectedCategoryId(modalConfig.categoryId);
83
        modalConfig.removeOnClose = true;
84
        super.configure(modalConfig);
85
    }
86
 
87
    /**
88
     * Initialise the category select based on the data passed to the JS or if a filter is applied in the url.
89
     * @param {integer} categoryId
90
     */
91
    initSelectedCategoryId(categoryId) {
92
        const filter = new URLSearchParams(window.location.href).get('filter');
93
        if (filter) {
94
            const filteredCategoryId = JSON.parse(filter)?.category.values[0];
95
            this.currentCategoryId = filteredCategoryId > 0 ? filteredCategoryId : null;
96
            this.targetCategoryId = filteredCategoryId;
97
            return;
98
        }
99
        this.currentCategoryId = categoryId;
100
        this.targetCategoryId = categoryId;
101
    }
102
 
103
    /**
104
     * Render the modal contents.
105
     * @return {Promise}
106
     */
107
    show() {
108
        void this.display(this.contextId, this.currentCategoryId);
109
        return super.show();
110
    }
111
 
112
    /**
113
     * Get the content to display and enhance the selects into auto complete fields.
114
     * @param {integer} currentBankContextId
115
     * @param {integer} currentCategoryId
116
     */
117
    async display(currentBankContextId, currentCategoryId) {
118
        const displayPending = new Pending('qbank_bulkmove/bulk_move_modal');
119
        this.bodyPromise = await Fragment.loadFragment(
120
            'qbank_bulkmove',
121
            'bulk_move',
122
            currentBankContextId,
123
            {
124
                'categoryid': currentCategoryId,
125
            }
126
        );
127
 
128
        await this.setBody(this.bodyPromise);
129
        await this.enhanceSelects();
130
        this.registerEnhancedEventListeners();
131
        this.updateSaveButtonState();
132
        displayPending.resolve();
133
    }
134
 
135
    /**
136
     * Register event listeners on the enhanced selects. Must be done after they have been enhanced.
137
     */
138
    registerEnhancedEventListeners() {
139
        document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY).addEventListener("change", () => {
140
            this.updateSaveButtonState();
141
        });
142
 
143
        document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener("change", async(e) => {
144
            await this.updateCategorySelector(e.currentTarget.value);
145
            this.updateSaveButtonState();
146
        });
147
 
148
        this.getModal().on("click", ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON, (e) => {
149
            e.preventDefault();
150
            void this.displayConfirmMove();
151
        });
152
    }
153
 
154
    /**
155
     * Update the body with a confirmation prompt and set confirm cancel buttons in the footer.
156
     * @return {Promise<void>}
157
     */
158
    async displayConfirmMove() {
159
        this.setTitle(getString('confirm', 'core'));
160
        this.setBody(getString('confirmmove', 'qbank_bulkmove'));
161
        if (!this.hasFooterContent()) {
162
            // We don't have the footer yet so go grab it and register event listeners on the buttons.
163
            this.setFooter(Templates.render('qbank_bulkmove/bulk_move_footer', {}));
164
            await this.getFooterPromise();
165
 
166
            document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CONFIRM_BUTTON).addEventListener("click", (e) => {
167
                e.preventDefault();
168
                this.moveQuestionsAfterConfirm(this.targetBankContextId, this.targetCategoryId);
169
            });
170
 
171
            document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CANCEL_BUTTON).addEventListener("click", (e) => {
172
                e.preventDefault();
173
                this.setTitle(getString('bulkmoveheader', 'qbank_bulkmove'));
174
                this.setBodyContent(Templates.renderForPromise('core/loading', {}));
175
                this.hideFooter();
176
                this.display(this.targetBankContextId, this.targetCategoryId);
177
            });
178
        } else {
179
            // We already have a footer so just show it.
180
            this.showFooter();
181
        }
182
    }
183
 
184
    /**
185
     * Update the category selector based on the selected question bank.
186
     *
187
     * @param {Number} selectedBankCmId
188
     * @return {Promise} Resolved when the update is complete.
189
     */
190
    updateCategorySelector(selectedBankCmId) {
191
        if (!selectedBankCmId) {
192
            this.updateCategorySelectorState(false);
193
            return Promise.resolve();
194
        } else {
195
            return Fragment.loadFragment(
196
                'core_question',
197
                'category_selector',
198
                this.contextId,
199
                {
200
                    'bankcmid': selectedBankCmId,
201
                }
202
            )
203
            .then((html, js) => {
204
                const categorySelector = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.QUESTION_CATEGORY_SELECTOR);
205
                return Templates.replaceNode(categorySelector, html, js);
206
            })
207
            .then(() => {
208
                document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING).classList.add('d-none');
209
                return this.enhanceSelects();
210
            })
211
            .catch(Notification.exception);
212
        }
213
    }
214
 
215
    /**
216
     * Disable/enable the enhanced category selector field.
217
     * @param {boolean} toEnable True to enable, false to disable the field.
218
     */
219
    updateCategorySelectorState(toEnable) {
220
        const warning = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING);
221
        const enhancedInput = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_ENHANCED_INPUT);
222
        const suggestionButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SUGGESTION);
223
        const selection = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SELECTION);
224
 
225
        if (toEnable) {
226
            warning.classList.add('d-none');
227
            enhancedInput.removeAttribute('disabled');
228
            suggestionButton.classList.remove('d-none');
229
        } else {
230
            warning.classList.remove('d-none');
231
            enhancedInput.setAttribute('disabled', 'disabled');
232
            suggestionButton.classList.add('d-none');
233
            selection.click(); // Clear selected category.
234
        }
235
    }
236
 
237
    /**
238
     * Disable the button if the selected category is the same as the one the questions already belong to. Enable it otherwise.
239
     */
240
    updateSaveButtonState() {
241
        const saveButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON);
242
        const categorySelector = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY);
243
        [this.targetCategoryId, this.targetBankContextId] = categorySelector.value.split(',');
244
 
245
        if (this.targetCategoryId && this.targetCategoryId !== this.currentCategoryId) {
246
            saveButton.removeAttribute('disabled');
247
        } else {
248
            saveButton.setAttribute('disabled', 'disabled');
249
        }
250
    }
251
 
252
    /**
253
     * Move the selected questions to their new target category.
254
     * @param {integer} targetContextId the target bank context id.
255
     * @param {integer} targetCategoryId the target question category id.
256
     * @return {Promise<void>}
257
     */
258
    async moveQuestionsAfterConfirm(targetContextId, targetCategoryId) {
259
        await this.setBody(Templates.render('core/loading', {}));
260
        const qelements = document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.SELECTED_QUESTIONS);
261
        const questionids = [];
262
        qelements.forEach((element) => {
263
            if (element.checked) {
264
                const name = element.getAttribute('name');
265
                questionids.push(name.substr(1, name.length));
266
            }
267
        });
268
        if (questionids.length === 0) {
269
            await Notification.exception('No questions selected');
270
        }
271
 
272
        try {
273
            window.location.href = await moveQuestions(
274
                targetContextId,
275
                targetCategoryId,
276
                questionids.join(),
277
                window.location.href
278
            );
279
        } catch (error) {
280
            await Notification.exception(error);
281
        }
282
    }
283
 
284
    /**
285
     * Take the provided select options and enhance them into auto-complete fields.
286
     *
287
     * @return {Promise<Promise[]>}
288
     */
289
    async enhanceSelects() {
290
        const placeholder = await getString('searchbyname', 'mod_quiz');
291
 
292
        await AutoComplete.enhance(
293
            ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK,
294
            false,
295
            'core_question/question_banks_datasource',
296
            placeholder,
297
            false,
298
            true,
299
            '',
300
            true,
301
        );
302
 
303
        await AutoComplete.enhance(
304
            ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY,
305
            false,
306
            null,
307
            placeholder,
308
            false,
309
            true,
310
            '',
311
            true,
312
        );
313
    }
314
}