ÿØÿà 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 an embedded form, it is only loaded and reloaded inside its container * * * @module core_form/dynamicform * @copyright 2019 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms * * @example * import DynamicForm from 'core_form/dynamicform'; * * const dynamicForm = new DynamicForm(document.querySelector('#mycontainer', 'pluginname\\form\\formname'); * dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => { * e.preventDefault(); * window.console.log(e.detail); * dynamicForm.container.innerHTML = 'Thank you, your form is submitted!'; * }); * dynamicForm.load(); * */ import * as FormChangeChecker from 'core_form/changechecker'; import * as FormEvents from 'core_form/events'; import Ajax from 'core/ajax'; import Fragment from 'core/fragment'; import Notification from 'core/notification'; import Pending from 'core/pending'; import Templates from 'core/templates'; import {getStrings} from 'core/str'; import {serialize} from './util'; /** * @class core_form/dynamicform */ export default class DynamicForm { /** * Various events that can be observed. * * @type {Object} */ events = { // Form was successfully submitted - the response is passed to the event listener. // Cancellable (in order to prevent default behavior to clear the container). FORM_SUBMITTED: 'core_form_dynamicform_formsubmitted', // Cancel button was pressed. // Cancellable (in order to prevent default behavior to clear the container). FORM_CANCELLED: 'core_form_dynamicform_formcancelled', // User attempted to submit the form but there was client-side validation error. CLIENT_VALIDATION_ERROR: 'core_form_dynamicform_clientvalidationerror', // User attempted to submit the form but server returned validation error. SERVER_VALIDATION_ERROR: 'core_form_dynamicform_validationerror', // Error occurred while performing request to the server. // Cancellable (by default calls Notification.exception). ERROR: 'core_form_dynamicform_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_dynamicform_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_dynamicform_submitbutton', // Right after user pressed cancel button, // listen to this event if you want to add confirmation dialog. // Cancellable. CANCEL_BUTTON_PRESSED: 'core_form_dynamicform_cancelbutton', }; /** * Constructor * * Creates an instance * * @param {Element} container - the parent element for the form * @param {string} formClass full name of the php class that extends \core_form\modal , must be in autoloaded location */ constructor(container, formClass) { this.formClass = formClass; this.container = container; // Ensure strings required for shortforms are always available. getStrings([ {key: 'collapseall', component: 'moodle'}, {key: 'expandall', component: 'moodle'} ]).catch(Notification.exception); // Register delegated events handlers in vanilla JS. this.container.addEventListener('click', e => { if (e.target.matches('form input[type=submit][data-cancel]')) { e.preventDefault(); const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED, e.target); if (!event.defaultPrevented) { this.processCancelButton(); } } else if (e.target.matches('form input[type=submit][data-no-submit="1"]')) { e.preventDefault(); const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target); if (!event.defaultPrevented) { this.processNoSubmitButton(e.target); } } }); this.container.addEventListener('submit', e => { if (e.target.matches('form')) { e.preventDefault(); const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED); if (!event.defaultPrevented) { this.submitFormAjax(); } } }); } /** * Loads the form via AJAX and shows it inside a given container * * @param {Object} args * @return {Promise} * @public */ load(args = null) { const formData = serialize(args || {}); const pendingPromise = new Pending('core_form/dynamicform:load'); return this.getBody(formData) .then((resp) => this.updateForm(resp)) .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.container.dispatchEvent(e); return e; } /** * Add listener for an event * * @param {array} args * @example: * const dynamicForm = new DynamicForm(...); * dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => { * e.preventDefault(); * window.console.log(e.detail); * dynamicForm.container.innerHTML = 'Thank you, your form is submitted!'; * }); */ addEventListener(...args) { this.container.addEventListener(...args); } /** * Get form body * * @param {String} formDataString form data in format of a query string * @private * @return {Promise} */ getBody(formDataString) { return Ajax.call([{ methodname: 'core_form_dynamic_form', args: { formdata: formDataString, form: this.formClass, } }])[0] .then(response => { return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}; }); } /** * On form submit * * @param {*} response Response received from the form's "process" method */ onSubmitSuccess(response) { const event = this.trigger(this.events.FORM_SUBMITTED, response); if (event.defaultPrevented) { return; } // Default implementation is to remove the form. Event listener should either remove or reload the form // since its contents is no longer correct. For example, if an element was created as a result of // form submission, the "id" in the form would be still zero. Also the server-side validation // errors from the previous submission may still be present. this.container.innerHTML = ''; } /** * On exception during form processing * * @private * @param {Object} exception */ onSubmitError(exception) { const event = this.trigger(this.events.ERROR, exception); if (event.defaultPrevented) { return; } Notification.exception(exception); } /** * Click on a "submit" button that is marked in the form as registerNoSubmitButton() * * @method submitButtonPressed * @param {Element} button that was pressed * @fires event:formSubmittedByJavascript */ processNoSubmitButton(button) { const pendingPromise = new Pending('core_form/dynamicform:nosubmit'); const form = this.getFormNode(); const formData = new URLSearchParams([...(new FormData(form)).entries()]); formData.append(button.getAttribute('name'), button.getAttribute('value')); FormEvents.notifyFormSubmittedByJavascript(form, true); // Add the button name to the form data and submit it. this.disableButtons(); this.getBody(formData.toString()) .then(resp => this.updateForm(resp)) .then(pendingPromise.resolve) .catch(exception => this.onSubmitError(exception)); } /** * Get the form node from the Dialogue. * * @returns {HTMLFormElement} */ getFormNode() { return this.container.querySelector('form'); } /** * Notifies listeners that form dirty state should be reset. * * @fires event:formSubmittedByJavascript */ notifyResetFormChanges() { FormEvents.notifyFormSubmittedByJavascript(this.getFormNode(), true); FormChangeChecker.resetFormDirtyState(this.getFormNode()); } /** * Click on a "cancel" button */ processCancelButton() { // Notify listeners that the form is about to be submitted (this will reset atto autosave). this.notifyResetFormChanges(); const event = this.trigger(this.events.FORM_CANCELLED); if (!event.defaultPrevented) { // By default removes the form from the DOM. this.container.innerHTML = ''; } } /** * Update form contents * * @param {object} param * @param {string} param.html * @param {string} param.js * @returns {Promise} */ updateForm({html, js}) { return Templates.replaceNodeContents(this.container, html, js); } /** * Validate form elements * @return {Boolean} Whether client-side validation has passed, false if there are errors * @fires event:formSubmittedByJavascript */ validateElements() { // Notify listeners that the form is about to be submitted (this will reset atto autosave). FormEvents.notifyFormSubmittedByJavascript(this.getFormNode()); // Now the change events have run, see if there are any "invalid" form fields. const invalid = [...this.container.querySelectorAll('[aria-invalid="true"], .error')]; // If we found invalid fields, focus on the first one and do not submit via ajax. if (invalid.length) { invalid[0].focus(); return false; } return true; } /** * Disable buttons during form submission */ disableButtons() { this.container.querySelectorAll('form input[type="submit"]') .forEach(el => el.setAttribute('disabled', true)); } /** * Enable buttons after form submission (on validation error) */ enableButtons() { this.container.querySelectorAll('form input[type="submit"]') .forEach(el => el.removeAttribute('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 (!(await 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.container.querySelector('form'); const formData = new URLSearchParams([...(new FormData(form)).entries()]); // Now we can continue... Ajax.call([{ methodname: 'core_form_dynamic_form', args: { formdata: formData.toString(), form: this.formClass } }])[0] .then((response) => { if (!response.submitted) { // Form was not submitted, it could be either because validation failed or because no-submit button was pressed. this.updateForm({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}); this.enableButtons(); this.trigger(this.events.SERVER_VALIDATION_ERROR, null, false); } else { // Form was submitted properly. const data = JSON.parse(response.data); this.enableButtons(); this.notifyResetFormChanges(); this.onSubmitSuccess(data); } return null; }) .catch(exception => this.onSubmitError(exception)); } }