1 // This file is part of Moodle - http://moodle.org/
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.
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/>.
17 * AJAX helper for the tag management page.
20 * @copyright 2015 Marina Glancy
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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',
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.
53 getString('combineselected', 'tag'),
54 getString('selectmultipletags', 'tag'),
61 const tags = Array.from(checkedTags.values()).map((tag) => {
62 const namedElement = document.querySelector(`.inplaceeditable[data-itemtype=tagname][data-itemid="${tag.value}"]`);
65 name: namedElement.dataset.value,
69 const modal = await SaveCancelModal.create({
70 title: getString('combineselected', 'tag'),
72 save: getString('continue', 'core'),
74 body: Templates.render('core_tag/combine_tags', {tags}),
80 modal.getRoot().on(ModalEvents.save, (e) => {
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.
104 await modal.getBodyPromise();
105 // Tick the first option.
106 const firstOption = document.querySelector('#combinetags_form input[type=radio]');
108 firstOption.checked = true;
110 pendingPromise.resolve();
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,
125 save: getString('continue', 'core'),
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.
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) {
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');
157 // Try to submit the form.
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) {
178 await Notification.saveCancelPromise(
180 getString('confirmdeletetags', 'tag'),
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);
204 const deleteSelectedTag = async(button) => {
206 await Notification.saveCancelPromise(
208 getString('confirmdeletetag', 'tag'),
213 window.location.href = button.href;
219 const deleteSelectedCollection = async(button) => {
221 await Notification.saveCancelPromise(
223 getString('suredeletecoll', 'tag', button.dataset.collname),
228 const redirectTarget = new URL(button.dataset.url);
229 redirectTarget.searchParams.set('sesskey', Config.sesskey);
230 window.location.href = redirectTarget;
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'),
243 save: getString('create', 'core'),
245 body: Templates.render('core_tag/add_tag_collection', {
247 sesskey: M.cfg.sesskey,
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) {
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');
277 // Try to submit the form.
283 pendingPromise.resolve();
287 * Initialises tag index page.
289 * @method initTagindexPage
291 export const initTagindexPage = async() => {
292 document.addEventListener('click', async(e) => {
293 const targetArea = e.target.closest('a[data-quickload="1"]');
297 const tagArea = targetArea.closest('.tagarea[data-ta]');
303 const pendingPromise = new Pending('core/tag:initTagindexPage');
305 const query = targetArea.search.replace(/^\?/, '');
306 const params = Object.fromEntries((new URLSearchParams(query)).entries());
309 const data = await getTagIndex(params);
310 const {html, js} = await Templates.renderForPromise('core_tag/index', data);
311 Templates.replaceNode(tagArea, html, js);
313 Notification.exception(error);
315 pendingPromise.resolve();
320 * Initialises tag management page.
322 * @method initManagePage
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');
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) {
336 handleCombineRequest(tagManagementCombine);
339 if (e.target.closest('[data-action="addstandardtag"]')) {
344 const bulkActionDeleteButton = e.target.closest('#tag-management-delete');
345 if (bulkActionDeleteButton) {
347 deleteSelectedTags(bulkActionDeleteButton);
350 const rowDeleteButton = e.target.closest('.tagdelete');
351 if (rowDeleteButton) {
353 deleteSelectedTag(rowDeleteButton);
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') {
365 e.preventDefault(); // This will prevent default error dialogue.
368 await Notification.saveCancelPromise(
369 getString('confirm'),
370 getString('nameuseddocombine', 'tag'),
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;
390 * Initialises tag collection management page.
392 * @method initManageCollectionsPage
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();
409 $(this).closest('tr').addClass('dimmed_text');
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();
422 pendingPromise.resolve();
425 document.addEventListener('click', async(e) => {
426 const addTagCollectionNode = e.target.closest('.addtagcoll > a');
427 if (addTagCollectionNode) {
429 addTagCollection(addTagCollectionNode);
433 const deleteCollectionButton = e.target.closest('.tag-collections-table .action_delete');
434 if (deleteCollectionButton) {
436 deleteSelectedCollection(deleteCollectionButton);