ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ.// 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 . /** * Display a form in a modal dialogue * * Example: * import ModalForm from 'core_form/modalform'; * * const modalForm = new ModalForm({ * formClass: 'pluginname\\form\\formname', * modalConfig: {title: 'Here comes the title'}, * args: {categoryid: 123}, * returnFocus: e.target, * }); * modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, (c) => window.console.log(c.detail)); * modalForm.show(); * * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms * * @module core_form/modalform * @copyright 2018 Mitxel Moriana * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; import * as FormChangeChecker from 'core_form/changechecker'; import * as FormEvents from 'core_form/events'; import Fragment from 'core/fragment'; import ModalEvents from 'core/modal_events'; import Notification from 'core/notification'; import Pending from 'core/pending'; import {serialize} from './util'; export default class ModalForm { /** * Various events that can be observed. * * @type {Object} */ events = { // Form was successfully submitted - the response is passed to the event listener. // Cancellable (but it's hardly ever needed to cancel this event). FORM_SUBMITTED: 'core_form_modalform_formsubmitted', // Cancel button was pressed. // Cancellable (but it's hardly ever needed to cancel this event). FORM_CANCELLED: 'core_form_modalform_formcancelled', // User attempted to submit the form but there was client-side validation error. CLIENT_VALIDATION_ERROR: 'core_form_modalform_clientvalidationerror', // User attempted to submit the form but server returned validation error. SERVER_VALIDATION_ERROR: 'core_form_modalform_validationerror', // Error occurred while performing request to the server. // Cancellable (by default calls Notification.exception). ERROR: 'core_form_modalform_error', // Right after user pressed no-submit button, // listen to this event if you want to add JS validation or processing for no-submit button. // Cancellable. NOSUBMIT_BUTTON_PRESSED: 'core_form_modalform_nosubmitbutton', // Right after user pressed submit button, // listen to this event if you want to add additional JS validation or confirmation dialog. // Cancellable. SUBMIT_BUTTON_PRESSED: 'core_form_modalform_submitbutton', // Right after user pressed cancel button, // listen to this event if you want to add confirmation dialog. // Cancellable. CANCEL_BUTTON_PRESSED: 'core_form_modalform_cancelbutton', // Modal was loaded and this.modal is available (but the form content may not be loaded yet). LOADED: 'core_form_modalform_loaded', }; /** * Constructor * * Shows the required form inside a modal dialogue * * @param {Object} config parameters for the form and modal dialogue: * @paramy {String} config.formClass PHP class name that handles the form (should extend \core_form\modal ) * @paramy {String} config.moduleName module name to use if different to core/modal_save_cancel (optional) * @paramy {Object} config.modalConfig modal config - title, header, footer, etc. * Default: {removeOnClose: true, large: true} * @paramy {Object} config.args Arguments for the initial form rendering (for example, id of the edited entity) * @paramy {String} config.saveButtonText the text to display on the Modal "Save" button (optional) * @paramy {String} config.saveButtonClasses additional CSS classes for the Modal "Save" button * @paramy {HTMLElement} config.returnFocus element to return focus to after the dialogue is closed */ constructor(config) { this.modal = null; this.config = config; this.config.modalConfig = { removeOnClose: true, large: true, ...(this.config.modalConfig || {}), }; this.config.args = this.config.args || {}; this.futureListeners = []; } /** * Loads the modal module and creates an instance * * @returns {Promise} */ getModalModule() { if (!this.config.moduleName && this.config.modalConfig.type && this.config.modalConfig.type !== 'SAVE_CANCEL') { // Legacy loader for plugins that were not updated with Moodle 4.3 changes. window.console.warn( 'Passing config.modalConfig.type to ModalForm has been deprecated since Moodle 4.3. ' + 'Please pass config.modalName instead with the full module name.', ); return import('core/modal_factory') .then((ModalFactory) => ModalFactory.create(this.config.modalConfig)); } else { // New loader for Moodle 4.3 and above. const moduleName = this.config.moduleName ?? 'core/modal_save_cancel'; return import(moduleName) .then((module) => module.create(this.config.modalConfig)); } } /** * Initialise the modal and shows it * * @return {Promise} */ show() { const pendingPromise = new Pending('core_form/modalform:init'); return this.getModalModule() .then((modal) => { this.modal = modal; // Retrieve the form and set the modal body. We can not set the body in the modalConfig, // we need to make sure that the modal already exists when we render the form. Some form elements // such as date_selector inspect the existing elements on the page to find the highest z-index. const formParams = serialize(this.config.args || {}); const bodyContent = this.getBody(formParams); this.modal.setBodyContent(bodyContent); bodyContent.catch(Notification.exception); // After successfull submit, when we press "Cancel" or close the dialogue by clicking on X in the top right corner. this.modal.getRoot().on(ModalEvents.hidden, () => { this.notifyResetFormChanges(); this.modal.destroy(); // Focus on the element that actually launched the modal. if (this.config.returnFocus) { this.config.returnFocus.focus(); } }); // Add the class to the modal dialogue. this.modal.getModal().addClass('modal-form-dialogue'); // We catch the press on submit buttons in the forms. this.modal.getRoot().on('click', 'form input[type=submit][data-no-submit]', (e) => { e.preventDefault(); const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target); if (!event.defaultPrevented) { this.processNoSubmitButton(e.target); } }); // We catch the form submit event and use it to submit the form with ajax. this.modal.getRoot().on('submit', 'form', (e) => { e.preventDefault(); const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED); if (!event.defaultPrevented) { this.submitFormAjax(); } }); // Change the text for the save button. if (typeof this.config.saveButtonText !== 'undefined' && typeof this.modal.setSaveButtonText !== 'undefined') { this.modal.setSaveButtonText(this.config.saveButtonText); } // Set classes for the save button. if (typeof this.config.saveButtonClasses !== 'undefined') { this.setSaveButtonClasses(this.config.saveButtonClasses); } // When Save button is pressed - submit the form. this.modal.getRoot().on(ModalEvents.save, (e) => { e.preventDefault(); this.modal.getRoot().find('form').submit(); }); // When Cancel button is pressed - allow to intercept. this.modal.getRoot().on(ModalEvents.cancel, (e) => { const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED); if (event.defaultPrevented) { e.preventDefault(); } }); this.futureListeners.forEach(args => this.modal.getRoot()[0].addEventListener(...args)); this.futureListeners = []; this.trigger(this.events.LOADED, null, false); return this.modal.show(); }) .then(pendingPromise.resolve); } /** * Triggers a custom event * * @private * @param {String} eventName * @param {*} detail * @param {Boolean} cancelable * @return {CustomEvent} */ trigger(eventName, detail = null, cancelable = true) { const e = new CustomEvent(eventName, {detail, cancelable}); this.modal.getRoot()[0].dispatchEvent(e); return e; } /** * Add listener for an event * * @param {array} args * @example: * const modalForm = new ModalForm(...); * dynamicForm.addEventListener(modalForm.events.FORM_SUBMITTED, e => { * window.console.log(e.detail); * }); */ addEventListener(...args) { if (!this.modal) { this.futureListeners.push(args); } else { this.modal.getRoot()[0].addEventListener(...args); } } /** * Get form contents (to be used in ModalForm.setBodyContent()) * * @param {String} formDataString form data in format of a query string * @method getBody * @private * @return {Promise} */ getBody(formDataString) { const params = { formdata: formDataString, form: this.config.formClass }; const pendingPromise = new Pending('core_form/modalform:form_body'); return Ajax.call([{ methodname: 'core_form_dynamic_form', args: params }])[0] .then(response => { pendingPromise.resolve(); return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}; }); } /** * On exception during form processing. Caller may override * * @param {Object} exception */ onSubmitError(exception) { const event = this.trigger(this.events.ERROR, exception); if (event.defaultPrevented) { return; } Notification.exception(exception); } /** * Notifies listeners that form dirty state should be reset. * * @fires event:formSubmittedByJavascript */ notifyResetFormChanges() { const form = this.getFormNode(); if (!form) { return; } FormEvents.notifyFormSubmittedByJavascript(form, true); FormChangeChecker.resetFormDirtyState(form); } /** * Get the form node from the Dialogue. * * @returns {HTMLFormElement} */ getFormNode() { return this.modal.getRoot().find('form')[0]; } /** * Click on a "submit" button that is marked in the form as registerNoSubmitButton() * * @param {Element} button button that was pressed * @fires event:formSubmittedByJavascript */ processNoSubmitButton(button) { const form = this.getFormNode(); if (!form) { return; } FormEvents.notifyFormSubmittedByJavascript(form, true); // Add the button name to the form data and submit it. let formData = this.modal.getRoot().find('form').serialize(); formData = formData + '&' + encodeURIComponent(button.getAttribute('name')) + '=' + encodeURIComponent(button.getAttribute('value')); const bodyContent = this.getBody(formData); this.modal.setBodyContent(bodyContent); bodyContent.catch(Notification.exception); } /** * Validate form elements * @return {Boolean} Whether client-side validation has passed, false if there are errors * @fires event:formSubmittedByJavascript */ validateElements() { FormEvents.notifyFormSubmittedByJavascript(this.getFormNode()); // Now the change events have run, see if there are any "invalid" form fields. /** @var {jQuery} list of elements with errors */ const invalid = this.modal.getRoot().find('[aria-invalid="true"], .error'); // If we found invalid fields, focus on the first one and do not submit via ajax. if (invalid.length) { invalid.first().focus(); return false; } return true; } /** * Disable buttons during form submission */ disableButtons() { this.modal.getFooter().find('[data-action]').attr('disabled', true); } /** * Enable buttons after form submission (on validation error) */ enableButtons() { this.modal.getFooter().find('[data-action]').removeAttr('disabled'); } /** * Submit the form via AJAX call to the core_form_dynamic_form WS */ async submitFormAjax() { // If we found invalid fields, focus on the first one and do not submit via ajax. if (!this.validateElements()) { this.trigger(this.events.CLIENT_VALIDATION_ERROR, null, false); return; } this.disableButtons(); // Convert all the form elements values to a serialised string. const form = this.modal.getRoot().find('form'); const formData = form.serialize(); // Now we can continue... Ajax.call([{ methodname: 'core_form_dynamic_form', args: { formdata: formData, form: this.config.formClass } }])[0] .then((response) => { if (!response.submitted) { // Form was not submitted because validation failed. const promise = new Promise( resolve => resolve({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)})); this.modal.setBodyContent(promise); this.enableButtons(); this.trigger(this.events.SERVER_VALIDATION_ERROR); } else { // Form was submitted properly. Hide the modal and execute callback. const data = JSON.parse(response.data); FormChangeChecker.markFormSubmitted(form[0]); const event = this.trigger(this.events.FORM_SUBMITTED, data); if (!event.defaultPrevented) { this.modal.hide(); } } return null; }) .catch(exception => this.onSubmitError(exception)); } /** * Set the classes for the 'save' button. * * @method setSaveButtonClasses * @param {(String)} value The 'save' button classes. */ setSaveButtonClasses(value) { const button = this.modal.getFooter().find("[data-action='save']"); if (!button) { throw new Error("Unable to find the 'save' button"); } button.removeClass().addClass(value); } }