Merge branch 'MDL-81713-main' of https://github.com/junpataleta/moodle
[moodle.git] / lib / amd / src / tag.js
blobe8c6d98c6d0ebed470e3861bd0cbbcad52c84bfc
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.
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/>.
16 /**
17  * AJAX helper for the tag management page.
18  *
19  * @module     core/tag
20  * @copyright  2015 Marina Glancy
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  * @since      3.0
23  */
25 import $ from 'jquery';
26 import {call as fetchMany} from 'core/ajax';
27 import * as Notification from 'core/notification';
28 import * as Templates from 'core/templates';
29 import {getString} from 'core/str';
30 import * as ModalEvents from 'core/modal_events';
31 import Pending from 'core/pending';
32 import SaveCancelModal from 'core/modal_save_cancel';
33 import Config from 'core/config';
34 import * as reportSelectors from 'core_reportbuilder/local/selectors';
36 const getTagIndex = (tagindex) => fetchMany([{
37     methodname: 'core_tag_get_tagindex',
38     args: {tagindex}
39 }])[0];
41 const getCheckedTags = (root) => root.querySelectorAll('[data-togglegroup="report-select-all"][data-toggle="slave"]:checked');
43 const handleCombineRequest = async(tagManagementCombine) => {
44     const pendingPromise = new Pending('core/tag:tag-management-combine');
45     const form = tagManagementCombine.closest('form');
47     const reportElement = document.querySelector(reportSelectors.regions.report);
48     const checkedTags = getCheckedTags(reportElement);
50     if (checkedTags.length <= 1) {
51         // We need at least 2 tags to combine them.
52         Notification.alert(
53             getString('combineselected', 'tag'),
54             getString('selectmultipletags', 'tag'),
55             getString('ok'),
56         );
58         return;
59     }
61     const tags = Array.from(checkedTags.values()).map((tag) => {
62         const namedElement = document.querySelector(`.inplaceeditable[data-itemtype=tagname][data-itemid="${tag.value}"]`);
63         return {
64             id: tag.value,
65             name: namedElement.dataset.value,
66         };
67     });
69     const modal = await SaveCancelModal.create({
70         title: getString('combineselected', 'tag'),
71         buttons: {
72             save: getString('continue', 'core'),
73         },
74         body: Templates.render('core_tag/combine_tags', {tags}),
75         show: true,
76         removeOnClose: true,
77     });
79     // Handle save event.
80     modal.getRoot().on(ModalEvents.save, (e) => {
81         e.preventDefault();
83         // Append this temp element in the form in the tags list, not the form in the modal. Confusing, right?!?
84         const tempElement = document.createElement('input');
85         tempElement.hidden = true;
86         tempElement.name = tagManagementCombine.name;
87         form.append(tempElement);
89         // Append selected tags element.
90         const tagsElement = document.createElement('input');
91         tagsElement.hidden = true;
92         tagsElement.name = 'tagschecked';
93         tagsElement.value = [...checkedTags].map(check => check.value).join(',');
94         form.append(tagsElement);
96         // Get the selected tag from the modal.
97         var maintag = $('input[name=maintag]:checked', '#combinetags_form').val();
98         // Append this in the tags list form.
99         $("<input type='hidden'/>").attr('name', 'maintag').attr('value', maintag).appendTo(form);
100         // Submit the tags list form.
101         form.submit();
102     });
104     await modal.getBodyPromise();
105     // Tick the first option.
106     const firstOption = document.querySelector('#combinetags_form input[type=radio]');
107     firstOption.focus();
108     firstOption.checked = true;
110     pendingPromise.resolve();
112     return;
115 const addStandardTags = async() => {
116     var pendingPromise = new Pending('core/tag:addstandardtag');
118     const modal = await SaveCancelModal.create({
119         title: getString('addotags', 'tag'),
120         body: Templates.render('core_tag/add_tags', {
121             actionurl: window.location.href,
122             sesskey: M.cfg.sesskey,
123         }),
124         buttons: {
125             save: getString('continue', 'core'),
126         },
127         removeOnClose: true,
128         show: true,
129     });
131     // Handle save event.
132     modal.getRoot().on(ModalEvents.save, (e) => {
133         var tagsInput = $(e.currentTarget).find('#id_tagslist');
134         var name = tagsInput.val().trim();
136         // Set the text field's value to the trimmed value.
137         tagsInput.val(name);
139         // Add submit event listener to the form.
140         var tagsForm = $('#addtags_form');
141         tagsForm.on('submit', function(e) {
142             // Validate the form.
143             var form = $('#addtags_form');
144             if (form[0].checkValidity() === false) {
145                 e.preventDefault();
146                 e.stopPropagation();
147             }
148             form.addClass('was-validated');
150             // BS2 compatibility.
151             $('[data-region="tagslistinput"]').addClass('error');
152             var errorMessage = $('#id_tagslist_error_message');
153             errorMessage.removeAttr('hidden');
154             errorMessage.addClass('help-block');
155         });
157         // Try to submit the form.
158         tagsForm.submit();
160         return false;
161     });
163     await modal.getBodyPromise();
164     pendingPromise.resolve();
167 const deleteSelectedTags = async(bulkActionDeleteButton) => {
168     const form = bulkActionDeleteButton.closest('form');
170     const reportElement = document.querySelector(reportSelectors.regions.report);
171     const checkedTags = getCheckedTags(reportElement);
173     if (!checkedTags.length) {
174         return;
175     }
177     try {
178         await Notification.saveCancelPromise(
179             getString('delete'),
180             getString('confirmdeletetags', 'tag'),
181             getString('yes'),
182             getString('no'),
183         );
185         // Append this temp element in the form in the tags list, not the form in the modal. Confusing, right?!?
186         const tempElement = document.createElement('input');
187         tempElement.hidden = true;
188         tempElement.name = bulkActionDeleteButton.name;
189         form.append(tempElement);
191         // Append selected tags element.
192         const tagsElement = document.createElement('input');
193         tagsElement.hidden = true;
194         tagsElement.name = 'tagschecked';
195         tagsElement.value = [...checkedTags].map(check => check.value).join(',');
196         form.append(tagsElement);
198         form.submit();
199     } catch {
200         return;
201     }
204 const deleteSelectedTag = async(button) => {
205     try {
206         await Notification.saveCancelPromise(
207             getString('delete'),
208             getString('confirmdeletetag', 'tag'),
209             getString('yes'),
210             getString('no'),
211         );
213         window.location.href = button.href;
214     } catch {
215         return;
216     }
219 const deleteSelectedCollection = async(button) => {
220     try {
221         await Notification.saveCancelPromise(
222             getString('delete'),
223             getString('suredeletecoll', 'tag', button.dataset.collname),
224             getString('yes'),
225             getString('no'),
226         );
228         const redirectTarget = new URL(button.dataset.url);
229         redirectTarget.searchParams.set('sesskey', Config.sesskey);
230         window.location.href = redirectTarget;
231     } catch {
232         return;
233     }
236 const addTagCollection = async(link) => {
237     const pendingPromise = new Pending('core/tag:initManageCollectionsPage-addtagcoll');
238     const href = link.dataset.url;
240     const modal = await SaveCancelModal.create({
241         title: getString('addtagcoll', 'tag'),
242         buttons: {
243             save: getString('create', 'core'),
244         },
245         body: Templates.render('core_tag/add_tag_collection', {
246             actionurl: href,
247             sesskey: M.cfg.sesskey,
248         }),
249         removeOnClose: true,
250         show: true,
251     });
253     // Handle save event.
254     modal.getRoot().on(ModalEvents.save, (e) => {
255         const collectionInput = $(e.currentTarget).find('#addtagcoll_name');
256         const name = collectionInput.val().trim();
257         // Set the text field's value to the trimmed value.
258         collectionInput.val(name);
260         // Add submit event listener to the form.
261         const form = $('#addtagcoll_form');
262         form.on('submit', function(e) {
263             // Validate the form.
264             if (form[0].checkValidity() === false) {
265                 e.preventDefault();
266                 e.stopPropagation();
267             }
268             form.addClass('was-validated');
270             // BS2 compatibility.
271             $('[data-region="addtagcoll_nameinput"]').addClass('error');
272             const errorMessage = $('#id_addtagcoll_name_error_message');
273             errorMessage.removeAttr('hidden');
274             errorMessage.addClass('help-block');
275         });
277         // Try to submit the form.
278         form.submit();
280         return false;
281     });
283     pendingPromise.resolve();
287  * Initialises tag index page.
289  * @method initTagindexPage
290  */
291 export const initTagindexPage = async() => {
292     document.addEventListener('click', async(e) => {
293         const targetArea = e.target.closest('a[data-quickload="1"]');
294         if (!targetArea) {
295             return;
296         }
297         const tagArea = targetArea.closest('.tagarea[data-ta]');
298         if (!tagArea) {
299             return;
300         }
302         e.preventDefault();
303         const pendingPromise = new Pending('core/tag:initTagindexPage');
305         const query = targetArea.search.replace(/^\?/, '');
306         const params = Object.fromEntries((new URLSearchParams(query)).entries());
308         try {
309             const data = await getTagIndex(params);
310             const {html, js} = await Templates.renderForPromise('core_tag/index', data);
311             Templates.replaceNode(tagArea, html, js);
312         } catch (error) {
313             Notification.exception(error);
314         }
315         pendingPromise.resolve();
316     });
320  * Initialises tag management page.
322  * @method initManagePage
323  */
324 export const initManagePage = () => {
325     // Toggle row class when updating flag.
326     $('body').on('updated', '[data-inplaceeditable][data-itemtype=tagflag]', function(e) {
327         var row = $(e.target).closest('tr');
328         row.toggleClass('table-warning', e.ajaxreturn.value === '1');
329     });
331     // Confirmation for bulk tag combine button.
332     document.addEventListener('click', async(e) => {
333         const tagManagementCombine = e.target.closest('#tag-management-combine');
334         if (tagManagementCombine) {
335             e.preventDefault();
336             handleCombineRequest(tagManagementCombine);
337         }
339         if (e.target.closest('[data-action="addstandardtag"]')) {
340             e.preventDefault();
341             addStandardTags();
342         }
344         const bulkActionDeleteButton = e.target.closest('#tag-management-delete');
345         if (bulkActionDeleteButton) {
346             e.preventDefault();
347             deleteSelectedTags(bulkActionDeleteButton);
348         }
350         const rowDeleteButton = e.target.closest('.tagdelete');
351         if (rowDeleteButton) {
352             e.preventDefault();
353             deleteSelectedTag(rowDeleteButton);
354         }
355     });
357     // When user changes tag name to some name that already exists suggest to combine the tags.
358     $('body').on('updatefailed', '[data-inplaceeditable][data-itemtype=tagname]', async(e) => {
359         var exception = e.exception; // The exception object returned by the callback.
360         var newvalue = e.newvalue; // The value that user tried to udpated the element to.
361         var tagid = $(e.target).attr('data-itemid');
362         if (exception.errorcode !== 'namesalreadybeeingused') {
363             return;
364         }
365         e.preventDefault(); // This will prevent default error dialogue.
367         try {
368             await Notification.saveCancelPromise(
369                 getString('confirm'),
370                 getString('nameuseddocombine', 'tag'),
371                 getString('yes'),
372                 getString('cancel'),
373             );
375             // The Promise will resolve on 'Yes' button, and reject on 'Cancel' button.
376             const redirectTarget = new URL(window.location);
377             redirectTarget.searchParams.set('newname', newvalue);
378             redirectTarget.searchParams.set('tagid', tagid);
379             redirectTarget.searchParams.set('action', 'renamecombine');
380             redirectTarget.searchParams.set('sesskey', Config.sesskey);
382             window.location.href = redirectTarget;
383         } catch {
384             return;
385         }
386     });
390  * Initialises tag collection management page.
392  * @method initManageCollectionsPage
393  */
394 export const initManageCollectionsPage = () => {
395     $('body').on('updated', '[data-inplaceeditable]', function(e) {
396         var pendingPromise = new Pending('core/tag:initManageCollectionsPage-updated');
398         var ajaxreturn = e.ajaxreturn,
399             areaid, collid, isenabled;
400         if (ajaxreturn.component === 'core_tag' && ajaxreturn.itemtype === 'tagareaenable') {
401             areaid = $(this).attr('data-itemid');
402             $(".tag-collections-table ul[data-collectionid] li[data-areaid=" + areaid + "]").hide();
403             isenabled = ajaxreturn.value;
404             if (isenabled === '1') {
405                 $(this).closest('tr').removeClass('dimmed_text');
406                 collid = $(this).closest('tr').find('[data-itemtype="tagareacollection"]').attr("data-value");
407                 $(".tag-collections-table ul[data-collectionid=" + collid + "] li[data-areaid=" + areaid + "]").show();
408             } else {
409                 $(this).closest('tr').addClass('dimmed_text');
410             }
411         }
412         if (ajaxreturn.component === 'core_tag' && ajaxreturn.itemtype === 'tagareacollection') {
413             areaid = $(this).attr('data-itemid');
414             $(".tag-collections-table ul[data-collectionid] li[data-areaid=" + areaid + "]").hide();
415             collid = $(this).attr('data-value');
416             isenabled = $(this).closest('tr').find('[data-itemtype="tagareaenable"]').attr("data-value");
417             if (isenabled === "1") {
418                 $(".tag-collections-table ul[data-collectionid=" + collid + "] li[data-areaid=" + areaid + "]").show();
419             }
420         }
422         pendingPromise.resolve();
423     });
425     document.addEventListener('click', async(e) => {
426         const addTagCollectionNode = e.target.closest('.addtagcoll > a');
427         if (addTagCollectionNode) {
428             e.preventDefault();
429             addTagCollection(addTagCollectionNode);
430             return;
431         }
433         const deleteCollectionButton = e.target.closest('.tag-collections-table .action_delete');
434         if (deleteCollectionButton) {
435             e.preventDefault();
436             deleteSelectedCollection(deleteCollectionButton);
437         }
438     });