first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
define("core_form/changechecker",["exports","core_editor/events","core/str"],(function(_exports,_events,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.watchFormById=_exports.watchForm=_exports.unWatchForm=_exports.startWatching=_exports.resetFormDirtyStateById=_exports.resetFormDirtyState=_exports.resetAllFormDirtyStates=_exports.markFormSubmitted=_exports.markFormChangedFromNode=_exports.markFormAsDirtyById=_exports.markFormAsDirty=_exports.markAllFormsSubmitted=_exports.markAllFormsAsDirty=_exports.isAnyWatchedFormDirty=_exports.disableAllChecks=void 0;
/**
* This module provides change detection to forms, allowing a browser to warn the user before navigating away if changes
* have been made.
*
* Two flags are stored for each form:
* * a 'dirty' flag; and
* * a 'submitted' flag.
*
* When the page is unloaded each watched form is checked. If the 'dirty' flag is set for any form, and the 'submitted'
* flag is not set for any form, then a warning is shown.
*
* The 'dirty' flag is set when any form element is modified within a watched form.
* The flag can also be set programatically. This may be required for custom form elements.
*
* It is not possible to customise the warning message in any modern browser.
*
* Please note that some browsers have controls on when these alerts may or may not be shown.
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} for browser-specific
* notes and references.
*
* @module core_form/changechecker
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @example <caption>Usage where the FormElement is already held</caption>
*
* import {watchForm} from 'core_form/changechecker';
*
* // Fetch the form element somehow.
* watchForm(formElement);
*
* @example <caption>Usage from the child of a form - i.e. an input, button, div, etc.</caption>
*
* import {watchForm} from 'core_form/changechecker';
*
* // Watch the form by using a child of it.
* watchForm(document.querySelector('input[data-foo="bar"]'););
*
* @example <caption>Usage from within a template</caption>
* <form id="mod_example-entry-{{uniqid}}" ...>
* <!--
*
* -->
* </form>
* {{#js}}
* require(['core_form/changechecker'], function(changeChecker) {
* watchFormById('mod_example-entry-{{uniqid}}');
* });
* {{/js}}
*/
let warningString,watchedForms=[],formChangeCheckerDisabled=!1;const getFormFromChild=formChild=>formChild.closest("form"),watchForm=formNode=>{(formNode=getFormFromChild(formNode))&&(isWatchingForm(formNode)||watchedForms.push(formNode))};_exports.watchForm=watchForm;_exports.unWatchForm=formNode=>{watchedForms=watchedForms.filter((watchedForm=>!!watchedForm.contains(formNode)))};const resetAllFormDirtyStates=()=>{watchedForms.forEach((watchedForm=>{watchedForm.dataset.formSubmitted="false",watchedForm.dataset.formDirty="false"}))};_exports.resetAllFormDirtyStates=resetAllFormDirtyStates;const resetFormDirtyState=formNode=>{(formNode=getFormFromChild(formNode))&&(formNode.dataset.formSubmitted="false",formNode.dataset.formDirty="false")};_exports.resetFormDirtyState=resetFormDirtyState;_exports.markAllFormsAsDirty=()=>{watchedForms.forEach((watchedForm=>{watchedForm.dataset.formDirty="true"}))};const markFormAsDirty=formNode=>{(formNode=getFormFromChild(formNode))&&(formNode.dataset.formDirty="true")};_exports.markFormAsDirty=markFormAsDirty;const disableAllChecks=()=>{formChangeCheckerDisabled=!0};_exports.disableAllChecks=disableAllChecks;const isAnyWatchedFormDirty=()=>{if(formChangeCheckerDisabled)return!1;if(watchedForms.some((watchedForm=>"true"===watchedForm.dataset.formSubmitted)))return!1;return!!watchedForms.some((watchedForm=>{if(!watchedForm.isConnected)return!1;if("true"===watchedForm.dataset.formDirty)return!0;if(document.activeElement&&document.activeElement.dataset.propertyIsEnumerable("initialValue")){const isActiveElementWatched=isWatchingForm(document.activeElement)&&!shouldIgnoreChangesForNode(document.activeElement),hasValueChanged=document.activeElement.dataset.initialValue!==document.activeElement.value;if(isActiveElementWatched&&hasValueChanged)return!0}return!1}))||!(void 0===window.tinyMCE||!window.tinyMCE.editors||!window.tinyMCE.editors.some((editor=>editor.isDirty())))};_exports.isAnyWatchedFormDirty=isAnyWatchedFormDirty;const isWatchingForm=target=>watchedForms.some((watchedForm=>watchedForm.contains(target))),shouldIgnoreChangesForNode=target=>!!target.closest(".ignoredirty"),markFormChangedFromNode=changedNode=>{if(changedNode.dataset.formChangeCheckerOverride)return void disableAllChecks();if(!isWatchingForm(changedNode))return;if(shouldIgnoreChangesForNode(changedNode))return;var target;(target=changedNode,watchedForms.find((watchedForm=>watchedForm.contains(target)))).dataset.formDirty="true"};_exports.markFormChangedFromNode=markFormChangedFromNode;const markFormSubmitted=formNode=>{(formNode=getFormFromChild(formNode))&&(formNode.dataset.formSubmitted="true")};_exports.markFormSubmitted=markFormSubmitted;_exports.markAllFormsSubmitted=()=>{watchedForms.forEach((watchedForm=>markFormSubmitted(watchedForm)))};const beforeUnloadHandler=e=>isAnyWatchedFormDirty()&&!M.cfg.behatsiterunning?(e.preventDefault(),e.returnValue=warningString,e.returnValue):(window.removeEventListener("beforeunload",beforeUnloadHandler),null),startWatching=()=>{document.addEventListener("change",(e=>{isWatchingForm(e.target)&&markFormChangedFromNode(e.target)})),document.addEventListener("click",(e=>{if(!e.target.closest("[data-formchangechecker-ignore-submit]"))return;const ownerForm=getFormFromChild(e.target);ownerForm&&(ownerForm.dataset.ignoreSubmission="true")})),document.addEventListener("focusin",(e=>{if(e.target.matches("input, textarea, select")){if(e.target.dataset.propertyIsEnumerable("initialValue"))return;e.target.dataset.initialValue=e.target.value}})),document.addEventListener("submit",(e=>{const formNode=getFormFromChild(e.target);formNode&&(formNode.dataset.ignoreSubmission?formNode.dataset.ignoreSubmission="false":markFormSubmitted(formNode))})),document.addEventListener(_events.eventTypes.editorContentRestored,(e=>{e.target!=document?resetFormDirtyState(e.target):resetAllFormDirtyStates()})),(0,_str.getString)("changesmadereallygoaway","moodle").then((changesMadeString=>{warningString=changesMadeString})).catch(),window.addEventListener("beforeunload",beforeUnloadHandler)};_exports.startWatching=startWatching;_exports.watchFormById=formId=>{watchForm(document.getElementById(formId))};_exports.resetFormDirtyStateById=formId=>{resetFormDirtyState(document.getElementById(formId))};_exports.markFormAsDirtyById=formId=>{markFormAsDirty(document.getElementById(formId))},startWatching()}));
//# sourceMappingURL=changechecker.min.js.map
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
define("core_form/choicedropdown",["exports","core/local/dropdown/status","core_form/changechecker"],(function(_exports,_status,_changechecker){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Field controller for choicedropdown field.
*
* @module core_form/choicedropdown
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Classes_hidden="d-none";
/**
* Internal form element class.
*
* @private
* @class FieldController
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/class FieldController{constructor(elementId){this.elementId=elementId,this.mainSelect=document.getElementById(this.elementId),this.dropdown=(0,_status.getDropdownStatus)('[data-form-controls="'.concat(this.elementId,'"]')),this.dropdown.getElement().classList.remove(Classes_hidden)}addEventListeners(){this.dropdown.getElement().addEventListener("change",this.updateSelect.bind(this)),this.dropdown.getElement().addEventListener("click",(event=>event.preventDefault())),this.mainSelect.addEventListener("change",this.updateDropdown.bind(this));new MutationObserver((mutations=>{mutations.forEach((mutation=>{"attributes"===mutation.type&&"disabled"===mutation.attributeName&&this.updateDropdown()}))})).observe(this.mainSelect,{attributeFilter:["disabled"]})}isDisabled(){var _this$mainSelect;return null===(_this$mainSelect=this.mainSelect)||void 0===_this$mainSelect?void 0:_this$mainSelect.hasAttribute("disabled")}async updateDropdown(){this.dropdown.setButtonDisabled(this.isDisabled()),this.dropdown.getSelectedValue()!=this.mainSelect.value&&this.dropdown.setSelectedValue(this.mainSelect.value)}async updateSelect(){this.dropdown.getSelectedValue()!=this.mainSelect.value&&(this.mainSelect.value=this.dropdown.getSelectedValue(),(0,_changechecker.markFormAsDirty)(this.mainSelect.closest("form")),this.mainSelect.dispatchEvent(new Event("change")))}disableInteractiveDialog(){var _this$mainSelect2;null===(_this$mainSelect2=this.mainSelect)||void 0===_this$mainSelect2||_this$mainSelect2.classList.remove(Classes_hidden);this.dropdown.getElement().classList.add(Classes_hidden)}hasForceDialog(){var _this$mainSelect3;return!(null===(_this$mainSelect3=this.mainSelect)||void 0===_this$mainSelect3||!_this$mainSelect3.dataset.forceDialog)}}_exports.init=elementId=>{const field=new FieldController(elementId);!document.body.classList.contains("behat-site")||field.hasForceDialog()?field.addEventListeners():field.disableInteractiveDialog()}}));
//# sourceMappingURL=choicedropdown.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("core_form/collapsesections",["exports","jquery","core/pending"],(function(_exports,_jquery,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Collapse or expand all form sections on clicking the expand all / collapse al link.
*
* @module core_form/collapsesections
* @copyright 2021 Bas Brands
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);const SELECTORS_FORM=".mform",SELECTORS_FORMHEADER=".fheader",SELECTORS_FORMCONTAINER="fieldset > .fcontainer",CLASSES_SHOW="show",CLASSES_COLLAPSED="collapsed",CLASSES_HIDDEN="d-none";_exports.init=collapsesections=>{const pendingPromise=new _pending.default("core_form/collapsesections"),collapsemenu=document.querySelector(collapsesections),formParent=collapsemenu.closest(SELECTORS_FORM),formContainers=formParent.querySelectorAll(SELECTORS_FORMCONTAINER);collapsemenu.addEventListener("keydown",(e=>{"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),collapsemenu.click())}));let formcontainercount=0,expandedcount=0;formContainers.forEach((container=>{container.parentElement.classList.contains(CLASSES_HIDDEN)||formcontainercount++,container.classList.contains(CLASSES_SHOW)&&expandedcount++})),formcontainercount===expandedcount&&(collapsemenu.classList.remove(CLASSES_COLLAPSED),collapsemenu.setAttribute("aria-expanded",!0)),collapsemenu.addEventListener("click",(()=>{let action="hide";collapsemenu.classList.contains(CLASSES_COLLAPSED)&&(action="show"),formContainers.forEach((container=>(0,_jquery.default)(container).collapse(action)))}));const collapseElementIds=[...formParent.querySelectorAll(SELECTORS_FORMHEADER)].map(((element,index)=>(element.id=element.id||"collapseElement-".concat(index),element.id)));collapsemenu.setAttribute("aria-controls",collapseElementIds.join(" ")),(0,_jquery.default)(SELECTORS_FORMCONTAINER).on("hidden.bs.collapse",(()=>{[...formContainers].every((container=>!container.classList.contains(CLASSES_SHOW)))&&(collapsemenu.classList.add(CLASSES_COLLAPSED),collapsemenu.setAttribute("aria-expanded",!1))})),(0,_jquery.default)(SELECTORS_FORMCONTAINER).on("shown.bs.collapse",(()=>{[...formContainers].every((container=>container.classList.contains(CLASSES_SHOW)))&&(collapsemenu.classList.remove(CLASSES_COLLAPSED),collapsemenu.setAttribute("aria-expanded",!0))})),pendingPromise.resolve()}}));
//# sourceMappingURL=collapsesections.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
define("core_form/configtext_maxlength",["exports","core/str","core/templates","core/notification","core/prefetch"],(function(_exports,_str,_templates,_notification,_prefetch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification);let registered=!1;_exports.init=()=>{registered||((0,_prefetch.prefetchStrings)("core",["maximumchars"]),(0,_prefetch.prefetchTemplates)(["core_form/setting_validation_failure"]),registered=!0,document.addEventListener("input",(e=>{const maxLengthField=e.target.closest("[data-validation-max-length]");if(maxLengthField)if(maxLengthField.value.length>maxLengthField.dataset.validationMaxLength)maxLengthField.form.addEventListener("submit",submissionCheck),(0,_str.get_string)("maximumchars","core",maxLengthField.dataset.validationMaxLength).then((errorMessage=>_templates.default.renderForPromise("core_form/setting_validation_failure",{fieldid:maxLengthField.id,message:errorMessage}))).then((errorTemplate=>{if(!maxLengthField.dataset.validationFailureId){const formWrapper=maxLengthField.closest(".form-text");_templates.default.prependNodeContents(formWrapper,errorTemplate.html,errorTemplate.js),maxLengthField.dataset.validationFailureId="maxlength_error_".concat(maxLengthField.id),updateSubmitButton()}})).then((()=>{maxLengthField.setAttribute("aria-invalid",!0);const errorField=document.getElementById(maxLengthField.dataset.validationFailureId);errorField&&errorField.setAttribute("aria-describedby",maxLengthField.id)})).catch(_notification.default.exception);else{const validationMessage=document.getElementById(maxLengthField.dataset.validationFailureId);validationMessage&&(validationMessage.parentElement.remove(),delete maxLengthField.dataset.validationFailureId,maxLengthField.removeAttribute("aria-invalid"),updateSubmitButton())}})))};const submissionCheck=e=>{const maxLengthFields=e.target.querySelectorAll("[data-validation-max-length]");Array.from(maxLengthFields).some((maxLengthField=>maxLengthField.value.length>maxLengthField.dataset.validationMaxLength&&(e.preventDefault(),maxLengthField.focus(),!0)))},updateSubmitButton=()=>{const shouldDisable=document.querySelector("form#adminsettings .error");document.querySelector('form#adminsettings button[type="submit"]').disabled=!!shouldDisable}}));
//# sourceMappingURL=configtext_maxlength.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
/**
* Functionality for the form element defaultcustom
*
* @module core_form/defaultcustom
* @copyright 2017 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.3
*/
define("core_form/defaultcustom",["jquery"],(function($){$("body").on("change","input[data-defaultcustom=true]",(function(event){var element=$(event.target),defaultvalue=JSON.parse(element.attr("data-defaultvalue")),customvalue=JSON.parse(element.attr("data-customvalue")),type=element.attr("data-type"),form=element.closest("form"),elementName=element.attr("name").replace(/\[customize\]$/,"[value]"),newvalue=element.prop("checked")?customvalue:defaultvalue;"text"===type?form.find('[name="'+elementName+'"]').val(newvalue):"date_selector"===type?(form.find('[name="'+elementName+'[day]"]').val(newvalue.day),form.find('[name="'+elementName+'[month]"]').val(newvalue.month),form.find('[name="'+elementName+'[year]"]').val(newvalue.year)):"date_time_selector"===type&&(form.find('[name="'+elementName+'[day]"]').val(newvalue.day),form.find('[name="'+elementName+'[month]"]').val(newvalue.month),form.find('[name="'+elementName+'[year]"]').val(newvalue.year),form.find('[name="'+elementName+'[hour]"]').val(newvalue.hour),form.find('[name="'+elementName+'[minute]"]').val(newvalue.minute))}))}));
//# sourceMappingURL=defaultcustom.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"defaultcustom.min.js","sources":["../src/defaultcustom.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Functionality for the form element defaultcustom\n *\n * @module core_form/defaultcustom\n * @copyright 2017 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.3\n */\ndefine(['jquery'], function($) {\n var onChangeSelect = function(event) {\n var element = $(event.target),\n defaultvalue = JSON.parse(element.attr('data-defaultvalue')),\n customvalue = JSON.parse(element.attr('data-customvalue')),\n type = element.attr('data-type'),\n form = element.closest('form'),\n elementName = element.attr('name').replace(/\\[customize\\]$/, '[value]'),\n newvalue = element.prop('checked') ? customvalue : defaultvalue;\n\n if (type === 'text') {\n form.find('[name=\"' + elementName + '\"]').val(newvalue);\n } else if (type === 'date_selector') {\n form.find('[name=\"' + elementName + '[day]\"]').val(newvalue.day);\n form.find('[name=\"' + elementName + '[month]\"]').val(newvalue.month);\n form.find('[name=\"' + elementName + '[year]\"]').val(newvalue.year);\n } else if (type === 'date_time_selector') {\n form.find('[name=\"' + elementName + '[day]\"]').val(newvalue.day);\n form.find('[name=\"' + elementName + '[month]\"]').val(newvalue.month);\n form.find('[name=\"' + elementName + '[year]\"]').val(newvalue.year);\n form.find('[name=\"' + elementName + '[hour]\"]').val(newvalue.hour);\n form.find('[name=\"' + elementName + '[minute]\"]').val(newvalue.minute);\n }\n };\n\n var selector = 'input[data-defaultcustom=true]';\n $('body').on('change', selector, onChangeSelect);\n});\n"],"names":["define","$","on","event","element","target","defaultvalue","JSON","parse","attr","customvalue","type","form","closest","elementName","replace","newvalue","prop","find","val","day","month","year","hour","minute"],"mappings":";;;;;;;;AAuBAA,iCAAO,CAAC,WAAW,SAASC,GA0BxBA,EAAE,QAAQC,GAAG,SADE,kCAxBM,SAASC,WACtBC,QAAUH,EAAEE,MAAME,QAClBC,aAAeC,KAAKC,MAAMJ,QAAQK,KAAK,sBACvCC,YAAcH,KAAKC,MAAMJ,QAAQK,KAAK,qBACtCE,KAAOP,QAAQK,KAAK,aACpBG,KAAOR,QAAQS,QAAQ,QACvBC,YAAcV,QAAQK,KAAK,QAAQM,QAAQ,iBAAkB,WAC7DC,SAAWZ,QAAQa,KAAK,WAAaP,YAAcJ,aAE1C,SAATK,KACAC,KAAKM,KAAK,UAAYJ,YAAc,MAAMK,IAAIH,UAC9B,kBAATL,MACPC,KAAKM,KAAK,UAAYJ,YAAc,WAAWK,IAAIH,SAASI,KAC5DR,KAAKM,KAAK,UAAYJ,YAAc,aAAaK,IAAIH,SAASK,OAC9DT,KAAKM,KAAK,UAAYJ,YAAc,YAAYK,IAAIH,SAASM,OAC7C,uBAATX,OACPC,KAAKM,KAAK,UAAYJ,YAAc,WAAWK,IAAIH,SAASI,KAC5DR,KAAKM,KAAK,UAAYJ,YAAc,aAAaK,IAAIH,SAASK,OAC9DT,KAAKM,KAAK,UAAYJ,YAAc,YAAYK,IAAIH,SAASM,MAC7DV,KAAKM,KAAK,UAAYJ,YAAc,YAAYK,IAAIH,SAASO,MAC7DX,KAAKM,KAAK,UAAYJ,YAAc,cAAcK,IAAIH,SAASQ"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("core_form/encryptedpassword",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.EncryptedPassword=void 0;
/**
* Encrypted password functionality.
*
* @module core_form/encryptedpassword
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const EncryptedPassword=function(elementId){const wrapper=document.querySelector('div[data-encryptedpasswordid="'+elementId+'"]');this.spanOrLink=wrapper.querySelector("span, a"),this.input=wrapper.querySelector("input"),this.editButtonOrLink=wrapper.querySelector("button[data-editbutton], a"),this.cancelButton=wrapper.querySelector("button[data-cancelbutton]");var editHandler=e=>{e.stopImmediatePropagation(),e.preventDefault(),this.startEditing(!0)};this.editButtonOrLink.addEventListener("click",editHandler),"A"===this.editButtonOrLink.nodeName&&wrapper.parentElement.previousElementSibling.querySelector("label").addEventListener("click",editHandler),this.cancelButton.addEventListener("click",(e=>{e.stopImmediatePropagation(),e.preventDefault(),this.cancelEditing()})),"y"===wrapper.dataset.novalue&&(this.startEditing(!1),this.cancelButton.style.display="none")};_exports.EncryptedPassword=EncryptedPassword,EncryptedPassword.prototype.startEditing=function(moveFocus){this.input.style.display="inline",this.input.disabled=!1,this.spanOrLink.style.display="none",this.editButtonOrLink.style.display="none",this.cancelButton.style.display="inline";const id=this.editButtonOrLink.id;this.editButtonOrLink.removeAttribute("id"),this.input.id=id,moveFocus&&this.input.focus()},EncryptedPassword.prototype.cancelEditing=function(){this.input.style.display="none",this.input.value="",this.input.disabled=!0,this.spanOrLink.style.display="inline",this.editButtonOrLink.style.display="inline",this.cancelButton.style.display="none";const id=this.input.id;this.input.removeAttribute("id"),this.editButtonOrLink.id=id}}));
//# sourceMappingURL=encryptedpassword.min.js.map
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
define("core_form/events",["exports","core/str","core/event_dispatcher","jquery","core/yui"],(function(_exports,_str,_event_dispatcher,_jquery,_yui){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Javascript events for the `core_form` subsystem.
*
* @module core_form/events
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.10
*
* @example <caption>Example of listening to a form event.</caption>
* import {eventTypes as formEventTypes} from 'core_form/events';
*
* document.addEventListener(formEventTypes.formSubmittedByJavascript, e => {
* window.console.log(e.target); // The form that was submitted.
* window.console.log(e.detail.skipValidation); // Whether form validation was skipped.
* });
*/let changesMadeString;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.types=_exports.triggerUploadStarted=_exports.triggerUploadCompleted=_exports.notifyUploadStarted=_exports.notifyUploadCompleted=_exports.notifyUploadChanged=_exports.notifyFormSubmittedByJavascript=_exports.notifyFormError=_exports.notifyFieldValidationFailure=_exports.notifyFieldStructureChanged=_exports.eventTypes=void 0,_jquery=_interopRequireDefault(_jquery),_yui=_interopRequireDefault(_yui);const changesMadeCheck=e=>{e&&(e.returnValue=changesMadeString)},eventTypes={formError:"core_form/error",formSubmittedByJavascript:"core_form/submittedByJavascript",formFieldValidationFailed:"core_form/fieldValidationFailed",uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted",uploadChanged:"core_form/uploadChanged",fieldStructureChanged:"core_form/fieldStructureChanged"};_exports.eventTypes=eventTypes;_exports.notifyFormError=field=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formError,{},field);_exports.notifyFormSubmittedByJavascript=function(form){let skipValidation=arguments.length>1&&void 0!==arguments[1]&&arguments[1],fallbackHandled=arguments.length>2&&void 0!==arguments[2]&&arguments[2];skipValidation&&(window.skipClientValidation=!0);const customEvent=(0,_event_dispatcher.dispatchEvent)(eventTypes.formSubmittedByJavascript,{skipValidation:skipValidation,fallbackHandled:fallbackHandled},form);return skipValidation&&(window.skipClientValidation=!1),customEvent};_exports.notifyFieldValidationFailure=(field,message)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formFieldValidationFailed,{message:message},field,{cancelable:!0});const notifyUploadStarted=async elementId=>(changesMadeString=await(0,_str.getString)("changesmadereallygoaway","moodle"),window.addEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadStarted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadStarted=notifyUploadStarted;const notifyUploadCompleted=elementId=>(window.removeEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadCompleted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadCompleted=notifyUploadCompleted;const triggerUploadStarted=notifyUploadStarted;_exports.triggerUploadStarted=triggerUploadStarted;const triggerUploadCompleted=notifyUploadCompleted;_exports.triggerUploadCompleted=triggerUploadCompleted;_exports.types={uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted"};let legacyEventsRegistered=!1;legacyEventsRegistered||(_yui.default.use("event","moodle-core-event",(()=>{document.addEventListener(eventTypes.formError,(e=>{const element=_yui.default.one(e.target),formElement=_yui.default.one(e.target.closest("form"));_yui.default.Global.fire(M.core.globalEvents.FORM_ERROR,{formid:formElement.generateID(),elementid:element.generateID()})})),document.addEventListener(eventTypes.formSubmittedByJavascript,(e=>{if(e.detail.fallbackHandled)return;e.skipValidation&&(window.skipClientValidation=!0);const form=_yui.default.one(e.target);form.fire(M.core.event.FORM_SUBMIT_AJAX,{currentTarget:form,fallbackHandled:!0}),e.skipValidation&&(window.skipClientValidation=!1)}))})),document.addEventListener(eventTypes.formFieldValidationFailed,(e=>{const legacyEvent=_jquery.default.Event("core_form-field-validation");(0,_jquery.default)(e.target).trigger(legacyEvent,e.detail.message)})),legacyEventsRegistered=!0);_exports.notifyUploadChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1});_exports.notifyFieldStructureChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.fieldStructureChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1})}));
//# sourceMappingURL=events.min.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
/**
* Password Unmask functionality.
*
* @module core_form/passwordunmask
* @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.2
*/
define("core_form/passwordunmask",["jquery","core/templates"],(function($,Template){var PasswordUnmask=function(elementid){this.wrapperSelector='[data-passwordunmask="wrapper"][data-passwordunmaskid="'+elementid+'"]',this.wrapper=$(this.wrapperSelector),this.editorSpace=this.wrapper.find('[data-passwordunmask="editor"]'),this.editLink=this.wrapper.find('a[data-passwordunmask="edit"]'),this.editInstructions=this.wrapper.find('[data-passwordunmask="instructions"]'),this.displayValue=this.wrapper.find('[data-passwordunmask="displayvalue"]'),this.inputFieldLabel=$('label[for="'+elementid+'"]'),this.inputField=this.editorSpace.find(document.getElementById(elementid)),this.inputField.addClass("d-none"),this.inputField.removeClass("hiddenifjs"),this.editInstructions.attr("id")||this.editInstructions.attr("id",elementid+"_instructions"),this.editInstructions.hide(),this.setDisplayValue(),this.addListeners()};return PasswordUnmask.prototype.addListeners=function(){return this.wrapper.on("click keypress",'[data-passwordunmask="edit"]',$.proxy((function(e){"keypress"===e.type&&13!==e.keyCode||(e.stopImmediatePropagation(),e.preventDefault(),this.isEditing()?"click"===e.type||$(e.relatedTarget).is(":input")?this.turnEditingOff(!1):this.turnEditingOff(!0):this.turnEditingOn())}),this)),this.wrapper.on("click keypress",'[data-passwordunmask="unmask"]',$.proxy((function(e){"keypress"===e.type&&13!==e.keyCode||(e.stopImmediatePropagation(),e.preventDefault(),this.wrapper.data("unmasked",!this.wrapper.data("unmasked")),this.setDisplayValue())}),this)),this.wrapper.on("keydown","input",$.proxy((function(e){"keydown"===e.type&&13!==e.keyCode||(e.stopImmediatePropagation(),e.preventDefault(),this.turnEditingOff(!0))}),this)),this.inputFieldLabel.on("click",$.proxy((function(e){e.preventDefault(),this.turnEditingOn()}),this)),this},PasswordUnmask.prototype.checkFocusOut=function(e){this.isEditing()&&window.setTimeout($.proxy((function(){var relatedTarget=e.relatedTarget||document.activeElement;this.wrapper.has($(relatedTarget)).length||this.turnEditingOff(!$(relatedTarget).is(":input,a"))}),this),100)},PasswordUnmask.prototype.passwordVisible=function(){return!!this.wrapper.data("unmasked")},PasswordUnmask.prototype.isEditing=function(){return this.inputField.hasClass("d-inline-block")},PasswordUnmask.prototype.turnEditingOn=function(){var value=this.getDisplayValue();return this.passwordVisible()?this.inputField.attr("type","text"):this.inputField.attr("type","password"),this.inputField.val(value),this.inputField.attr("size",this.inputField.attr("data-size")),this.inputField.addClass("d-inline-block"),this.editInstructions.length&&(this.inputField.attr("aria-describedby",this.editInstructions.attr("id")),this.editInstructions.show()),this.wrapper.attr("data-passwordunmask-visible",1),this.editLink.hide(),this.inputField.focus().select(),$("body").on("focusout",this.wrapperSelector,$.proxy(this.checkFocusOut,this)),this},PasswordUnmask.prototype.turnEditingOff=function(focusOnEditLink){$("body").off("focusout",this.wrapperSelector,this.checkFocusOut);var value=this.getDisplayValue();return this.inputField.attr("aria-describedby",null),this.inputField.val(value),this.inputField.removeClass("d-inline-block"),this.editInstructions.hide(),this.wrapper.removeAttr("data-passwordunmask-visible"),this.inputField.removeAttr("size"),this.editLink.show(),this.setDisplayValue(),focusOnEditLink&&this.editLink.focus(),this},PasswordUnmask.prototype.getDisplayValue=function(){return this.inputField.val()},PasswordUnmask.prototype.setDisplayValue=function(){var value=this.getDisplayValue();return this.isEditing()&&(this.wrapper.data("unmasked")?this.inputField.attr("type","text"):this.inputField.attr("type","password"),this.inputField.val(value)),value&&this.wrapper.data("unmasked")?this.displayValue.text(value):(value||(value=""),Template.render("core_form/element-passwordunmask-fill",{element:{frozen:this.inputField.is("[readonly]"),value:value,valuechars:value.split("")}}).done($.proxy((function(html,js){this.displayValue.html(html),Template.runTemplateJS(js)}),this))),this},PasswordUnmask}));
//# sourceMappingURL=passwordunmask.min.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
/**
* A class to help show and hide advanced form content.
*
* @module core_form/showadvanced
* @copyright 2016 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("core_form/showadvanced",["jquery","core/log","core/str","core/notification"],(function($,Log,Strings,Notification){var SELECTORS_FIELDSETCONTAINSADVANCED="fieldset.containsadvancedelements",SELECTORS_DIVFITEMADVANCED="div.fitem.advanced",SELECTORS_DIVADVANCEDSECTION="div#form-advanced-div",SELECTORS_MORELESSLINK="fieldset.containsadvancedelements .moreless-toggler",CSS_SHOW="show",CSS_MORELESSACTIONS="moreless-actions",CSS_MORELESSTOGGLER="moreless-toggler",CSS_SHOWLESS="moreless-less",WRAPPERS_FITEM='<div class="fitem"></div>',WRAPPERS_FELEMENT='<div class="felement"></div>',WRAPPERS_ADVANCEDDIV='<div id="form-advanced-div"></div>',uniqIdSeed=0,ShowAdvanced=function(id){this.id=id;var form=$(document.getElementById(id));this.enhanceForm(form)};return ShowAdvanced.prototype.id="",ShowAdvanced.prototype.enhanceForm=function(form){return form.find(SELECTORS_FIELDSETCONTAINSADVANCED).each(function(index,item){this.enhanceFieldset($(item))}.bind(this)),form.on("click",SELECTORS_MORELESSLINK,this.switchState),form.on("keydown",SELECTORS_MORELESSLINK,function(e){return 13!=e.which&&32!=e.which||this.switchState(e)}.bind(this)),this},ShowAdvanced.prototype.generateId=function(node){var id=node.prop("id");return void 0===id&&(id="showadvancedid-"+uniqIdSeed++,node.prop("id",id)),id},ShowAdvanced.prototype.enhanceFieldset=function(fieldset){var statuselement=$("input[name=mform_showmore_"+fieldset.prop("id")+"]");return statuselement.length?(Strings.get_strings([{key:"showmore",component:"core_form"},{key:"showless",component:"core_form"}]).then(function(results){var showmore=results[0],showless=results[1],morelesslink=$('<a href="#"></a>');morelesslink.addClass(CSS_MORELESSTOGGLER),"0"===statuselement.val()?(morelesslink.html(showmore),morelesslink.attr("aria-expanded","false")):(morelesslink.html(showless),morelesslink.attr("aria-expanded","true"),morelesslink.addClass(CSS_SHOWLESS),fieldset.find(SELECTORS_DIVFITEMADVANCED).addClass(CSS_SHOW));var idlist=[];fieldset.find(SELECTORS_DIVFITEMADVANCED).each(function(index,node){idlist[idlist.length]=this.generateId($(node))}.bind(this)),morelesslink.attr("role","button"),morelesslink.attr("aria-controls","form-advanced-div");var formadvancedsection=$(WRAPPERS_ADVANCEDDIV);fieldset.find(SELECTORS_DIVFITEMADVANCED).wrapAll(formadvancedsection);var fitem=$(WRAPPERS_FITEM);fitem.addClass(CSS_MORELESSACTIONS);var felement=$(WRAPPERS_FELEMENT);return felement.append(morelesslink),fitem.append(felement),fieldset.find(SELECTORS_DIVADVANCEDSECTION).before(fitem),!0}.bind(this)).fail(Notification.exception),this):(Log.debug("M.form.showadvanced::processFieldset was called on an fieldset without a status field: '"+fieldset.prop("id")+"'"),this)},ShowAdvanced.prototype.switchState=function(e){return e.preventDefault(),Strings.get_strings([{key:"showmore",component:"core_form"},{key:"showless",component:"core_form"}]).then((function(results){var showmore=results[0],showless=results[1],fieldset=$(e.target).closest(SELECTORS_FIELDSETCONTAINSADVANCED);fieldset.find(SELECTORS_DIVFITEMADVANCED).toggleClass(CSS_SHOW);var statuselement=$("input[name=mform_showmore_"+fieldset.prop("id")+"]");return"0"===statuselement.val()?(statuselement.val(1),$(e.target).addClass(CSS_SHOWLESS),$(e.target).html(showless),$(e.target).attr("aria-expanded","true")):(statuselement.val(0),$(e.target).removeClass(CSS_SHOWLESS),$(e.target).html(showmore),$(e.target).attr("aria-expanded","false")),!0})).fail(Notification.exception),this},{init:function(formid){return new ShowAdvanced(formid)}}}));
//# sourceMappingURL=showadvanced.min.js.map
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
define("core_form/submit",["exports","core_form/events"],(function(_exports,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Submit button JavaScript. All submit buttons will be automatically disabled once the form is
* submitted, unless that submission results in an error/cancelling the submit.
*
* @module core_form/submit
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.8
*/
let cookieListener=0;const cookieListeningButtons=[];let currentUploadCount=0;const uploadListeningButtons=[];let uploadListenersRegistered=!1;const getCookieName=()=>"moodledownload_"+M.cfg.sesskey,clearDownloadCookie=()=>{document.cookie=encodeURIComponent(getCookieName())+"=deleted; expires="+new Date(0).toUTCString()},checkUploadCount=()=>{currentUploadCount?uploadListeningButtons.forEach((button=>{button.disabled=!0})):uploadListeningButtons.forEach((button=>{button.disabled=!1}))};_exports.init=elementId=>{const button=document.getElementById(elementId);null!==button&&(button.disabled||uploadListeningButtons.push(button),uploadListenersRegistered||(document.addEventListener(_events.eventTypes.uploadStarted,(()=>{currentUploadCount++,checkUploadCount()})),document.addEventListener(_events.eventTypes.uploadCompleted,(()=>{currentUploadCount--,checkUploadCount()})),uploadListenersRegistered=!0),"off"!==button.form.dataset.doubleSubmitProtection&&button.form.addEventListener("submit",(function(event){const disableAction=function(){event.defaultPrevented||button.disabled||(button.disabled=!0,clearDownloadCookie(),(button=>{cookieListeningButtons.push(button),cookieListener||(cookieListener=setInterval((()=>{2==document.cookie.split(getCookieName()+"=").length&&(clearDownloadCookie(),clearInterval(cookieListener),cookieListener=0,cookieListeningButtons.forEach((button=>{button.disabled=!1})))}),500))})(button))};window.addEventListener("beforeunload",disableAction),setTimeout((function(){window.removeEventListener("beforeunload",disableAction)}),1)}),!1))}}));
//# sourceMappingURL=submit.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
define("core_form/util",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.serialize=void 0;const serialize=function(data){let prefix=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return[...Object.entries(data).map((_ref=>{let[index,value]=_ref;const key=prefix?"".concat(prefix,"[").concat(index,"]"):index;return null!==value&&"object"==typeof value?serialize(value,key):"".concat(key,"=").concat(encodeURIComponent(value))}))].join("&")};_exports.serialize=serialize}));
//# sourceMappingURL=util.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"util.min.js","sources":["../src/util.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Serialize form values into a string.\n *\n * This must be used instead of URLSearchParams, which does not correctly encode nested values such as arrays.\n *\n * @param {Object} data The form values to serialize\n * @param {string} prefix The prefix to use for key names\n * @returns {string}\n */\nexport const serialize = (data, prefix = '') => [\n ...Object.entries(data).map(([index, value]) => {\n const key = prefix ? `${prefix}[${index}]` : index;\n return (value !== null && typeof value === \"object\") ? serialize(value, key) : `${key}=${encodeURIComponent(value)}`;\n })\n].join(\"&\");\n"],"names":["serialize","data","prefix","Object","entries","map","_ref","index","value","key","encodeURIComponent","join"],"mappings":"gJAwBaA,UAAY,SAACC,UAAMC,8DAAS,SAAO,IACzCC,OAAOC,QAAQH,MAAMI,KAAIC,WAAEC,MAAOC,kBAC3BC,IAAMP,iBAAYA,mBAAUK,WAAWA,aAC3B,OAAVC,OAAmC,iBAAVA,MAAsBR,UAAUQ,MAAOC,eAAUA,gBAAOC,mBAAmBF,YAElHG,KAAK"}
+516
View File
@@ -0,0 +1,516 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* This module provides change detection to forms, allowing a browser to warn the user before navigating away if changes
* have been made.
*
* Two flags are stored for each form:
* * a 'dirty' flag; and
* * a 'submitted' flag.
*
* When the page is unloaded each watched form is checked. If the 'dirty' flag is set for any form, and the 'submitted'
* flag is not set for any form, then a warning is shown.
*
* The 'dirty' flag is set when any form element is modified within a watched form.
* The flag can also be set programatically. This may be required for custom form elements.
*
* It is not possible to customise the warning message in any modern browser.
*
* Please note that some browsers have controls on when these alerts may or may not be shown.
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} for browser-specific
* notes and references.
*
* @module core_form/changechecker
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @example <caption>Usage where the FormElement is already held</caption>
*
* import {watchForm} from 'core_form/changechecker';
*
* // Fetch the form element somehow.
* watchForm(formElement);
*
* @example <caption>Usage from the child of a form - i.e. an input, button, div, etc.</caption>
*
* import {watchForm} from 'core_form/changechecker';
*
* // Watch the form by using a child of it.
* watchForm(document.querySelector('input[data-foo="bar"]'););
*
* @example <caption>Usage from within a template</caption>
* <form id="mod_example-entry-{{uniqid}}" ...>
* <!--
*
* -->
* </form>
* {{#js}}
* require(['core_form/changechecker'], function(changeChecker) {
* watchFormById('mod_example-entry-{{uniqid}}');
* });
* {{/js}}
*/
import {eventTypes} from 'core_editor/events';
import {getString} from 'core/str';
/**
* @property {Bool} initialised Whether the change checker has been initialised
* @private
*/
let initialised = false;
/**
* @property {String} warningString The warning string to show on form change failure
* @private
*/
let warningString;
/**
* @property {Array} watchedForms The list of watched forms
* @private
*/
let watchedForms = [];
/**
* @property {Bool} formChangeCheckerDisabled Whether the form change checker has been actively disabled
* @private
*/
let formChangeCheckerDisabled = false;
/**
* Get the nearest form element from a child element.
*
* @param {HTMLElement} formChild
* @returns {HTMLFormElement|null}
* @private
*/
const getFormFromChild = formChild => formChild.closest('form');
/**
* Watch the specified form for changes.
*
* @method
* @param {HTMLElement} formNode
*/
export const watchForm = formNode => {
// Normalise the formNode.
formNode = getFormFromChild(formNode);
if (!formNode) {
// No form found.
return;
}
if (isWatchingForm(formNode)) {
// This form is already watched.
return;
}
watchedForms.push(formNode);
};
/**
* Stop watching the specified form for changes.
*
* If the form was not watched, then no change is made.
*
* A child of the form may be passed instead.
*
* @method
* @param {HTMLElement} formNode
* @example <caption>Stop watching a form for changes</caption>
* import {unWatchForm} from 'core_form/changechecker';
*
* // ...
* document.addEventListener('click', e => {
* if (e.target.closest('[data-action="changePage"]')) {
* unWatchForm(e.target);
* }
* });
*/
export const unWatchForm = formNode => {
watchedForms = watchedForms.filter(watchedForm => !!watchedForm.contains(formNode));
};
/**
* Reset the 'dirty' flag for all watched forms.
*
* If a form was previously marked as 'dirty', then this flag will be cleared and when the page is unloaded no warning
* will be shown.
*
* @method
*/
export const resetAllFormDirtyStates = () => {
watchedForms.forEach(watchedForm => {
watchedForm.dataset.formSubmitted = "false";
watchedForm.dataset.formDirty = "false";
});
};
/**
* Reset the 'dirty' flag of the specified form.
*
* @method
* @param {HTMLElement} formNode
*/
export const resetFormDirtyState = formNode => {
formNode = getFormFromChild(formNode);
if (!formNode) {
return;
}
formNode.dataset.formSubmitted = "false";
formNode.dataset.formDirty = "false";
};
/**
* Mark all forms as dirty.
*
* This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.
* It will be removed in Moodle 4.4.
*
* @method
*/
export const markAllFormsAsDirty = () => {
watchedForms.forEach(watchedForm => {
watchedForm.dataset.formDirty = "true";
});
};
/**
* Mark a specific form as dirty.
*
* This behaviour may be required for custom form elements which are not caught by the standard change listeners.
*
* @method
* @param {HTMLElement} formNode
*/
export const markFormAsDirty = formNode => {
formNode = getFormFromChild(formNode);
if (!formNode) {
return;
}
// Mark it as dirty.
formNode.dataset.formDirty = "true";
};
/**
* Actively disable the form change checker.
*
* Please note that it cannot be re-enabled once disabled.
*
* @method
*/
export const disableAllChecks = () => {
formChangeCheckerDisabled = true;
};
/**
* Check whether any watched from is dirty.
*
* @method
* @returns {Bool}
*/
export const isAnyWatchedFormDirty = () => {
if (formChangeCheckerDisabled) {
// The form change checker is disabled.
return false;
}
const hasSubmittedForm = watchedForms.some(watchedForm => watchedForm.dataset.formSubmitted === "true");
if (hasSubmittedForm) {
// Do not warn about submitted forms, ever.
return false;
}
const hasDirtyForm = watchedForms.some(watchedForm => {
if (!watchedForm.isConnected) {
// The watched form is not connected to the DOM.
return false;
}
if (watchedForm.dataset.formDirty === "true") {
// The form has been marked as dirty.
return true;
}
// Elements currently holding focus will not have triggered change detection.
// Check whether the value matches the original value upon form load.
if (document.activeElement && document.activeElement.dataset.propertyIsEnumerable('initialValue')) {
const isActiveElementWatched = isWatchingForm(document.activeElement)
&& !shouldIgnoreChangesForNode(document.activeElement);
const hasValueChanged = document.activeElement.dataset.initialValue !== document.activeElement.value;
if (isActiveElementWatched && hasValueChanged) {
return true;
}
}
return false;
});
if (hasDirtyForm) {
// At least one form is dirty.
return true;
}
// Handle TinyMCE editor instances.
// TinyMCE forms may not have been initialised at the time that startWatching is called.
// Check whether any tinyMCE editor is dirty.
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
if (window.tinyMCE.editors.some(editor => editor.isDirty())) {
return true;
}
}
// No dirty forms detected.
return false;
};
/**
* Get the watched form for the specified target.
*
* @method
* @param {HTMLNode} target
* @returns {HTMLFormElement}
* @private
*/
const getFormForNode = target => watchedForms.find(watchedForm => watchedForm.contains(target));
/**
* Whether the specified target is a watched form.
*
* @method
* @param {HTMLNode} target
* @returns {Bool}
* @private
*/
const isWatchingForm = target => watchedForms.some(watchedForm => watchedForm.contains(target));
/**
* Whether the specified target should ignore changes or not.
*
* @method
* @param {HTMLNode} target
* @returns {Bool}
* @private
*/
const shouldIgnoreChangesForNode = target => !!target.closest('.ignoredirty');
/**
* Mark a form as changed.
*
* @method
* @param {HTMLElement} changedNode An element in the form which was changed
*/
export const markFormChangedFromNode = changedNode => {
if (changedNode.dataset.formChangeCheckerOverride) {
// Changes to this form node disable the form change checker entirely.
// This is intended for select fields which cause an immediate redirect.
disableAllChecks();
return;
}
if (!isWatchingForm(changedNode)) {
return;
}
if (shouldIgnoreChangesForNode(changedNode)) {
return;
}
// Mark the form as dirty.
const formNode = getFormForNode(changedNode);
formNode.dataset.formDirty = "true";
};
/**
* Mark a form as submitted.
*
* @method
* @param {HTMLElement} formNode An element in the form to mark as submitted
*/
export const markFormSubmitted = formNode => {
formNode = getFormFromChild(formNode);
if (!formNode) {
return;
}
formNode.dataset.formSubmitted = "true";
};
/**
* Mark all forms as submitted.
*
* This function is only for backwards-compliance with the old YUI module and should not be used in any other situation.
* It will be removed in Moodle 4.4.
*
* @method
*/
export const markAllFormsSubmitted = () => {
watchedForms.forEach(watchedForm => markFormSubmitted(watchedForm));
};
/**
* Handle the beforeunload event.
*
* @method
* @param {Event} e
* @returns {string|null}
* @private
*/
const beforeUnloadHandler = e => {
// Please note: The use of Promises in this function is forbidden.
// This is an event handler and _cannot_ be asynchronous.
let warnBeforeUnload = isAnyWatchedFormDirty() && !M.cfg.behatsiterunning;
if (warnBeforeUnload) {
// According to the specification, to show the confirmation dialog an event handler should call preventDefault()
// on the event.
e.preventDefault();
// However note that not all browsers support this method, and some instead require the event handler to
// implement one of two legacy methods:
// * assigning a string to the event's returnValue property; and
// * returning a string from the event handler.
// Assigning a string to the event's returnValue property.
e.returnValue = warningString;
// Returning a string from the event handler.
return e.returnValue;
}
// Attaching an event handler/listener to window or document's beforeunload event prevents browsers from using
// in-memory page navigation caches, like Firefox's Back-Forward cache or WebKit's Page Cache.
// Remove the handler.
window.removeEventListener('beforeunload', beforeUnloadHandler);
return null;
};
/**
* Start watching for form changes.
*
* This function is called on module load, and should not normally be called.
*
* @method
* @protected
*/
export const startWatching = () => {
if (initialised) {
return;
}
document.addEventListener('change', e => {
if (!isWatchingForm(e.target)) {
return;
}
markFormChangedFromNode(e.target);
});
document.addEventListener('click', e => {
const ignoredButton = e.target.closest('[data-formchangechecker-ignore-submit]');
if (!ignoredButton) {
return;
}
const ownerForm = getFormFromChild(e.target);
if (ownerForm) {
ownerForm.dataset.ignoreSubmission = "true";
}
});
document.addEventListener('focusin', e => {
if (e.target.matches('input, textarea, select')) {
if (e.target.dataset.propertyIsEnumerable('initialValue')) {
// The initial value has already been set.
return;
}
e.target.dataset.initialValue = e.target.value;
}
});
document.addEventListener('submit', e => {
const formNode = getFormFromChild(e.target);
if (!formNode) {
// Weird, but watch for this anyway.
return;
}
if (formNode.dataset.ignoreSubmission) {
// This form was submitted by a button which requested that the form checked should not mark it as submitted.
formNode.dataset.ignoreSubmission = "false";
return;
}
markFormSubmitted(formNode);
});
document.addEventListener(eventTypes.editorContentRestored, e => {
if (e.target != document) {
resetFormDirtyState(e.target);
} else {
resetAllFormDirtyStates();
}
});
getString('changesmadereallygoaway', 'moodle')
.then(changesMadeString => {
warningString = changesMadeString;
return;
})
.catch();
window.addEventListener('beforeunload', beforeUnloadHandler);
};
/**
* Watch the form matching the specified ID for changes.
*
* @method
* @param {String} formId
*/
export const watchFormById = formId => {
watchForm(document.getElementById(formId));
};
/**
* Reset the dirty state of the form matching the specified ID..
*
* @method
* @param {String} formId
*/
export const resetFormDirtyStateById = formId => {
resetFormDirtyState(document.getElementById(formId));
};
/**
* Mark the form matching the specified ID as dirty.
*
* @method
* @param {String} formId
*/
export const markFormAsDirtyById = formId => {
markFormAsDirty(document.getElementById(formId));
};
// Configure all event listeners.
startWatching();
+158
View File
@@ -0,0 +1,158 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Field controller for choicedropdown field.
*
* @module core_form/choicedropdown
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getDropdownStatus} from 'core/local/dropdown/status';
import {markFormAsDirty} from 'core_form/changechecker';
const Classes = {
notClickable: 'not-clickable',
hidden: 'd-none',
};
/**
* Internal form element class.
*
* @private
* @class FieldController
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class FieldController {
/**
* Class constructor.
*
* @param {String} elementId Form element id
*/
constructor(elementId) {
this.elementId = elementId;
this.mainSelect = document.getElementById(this.elementId);
this.dropdown = getDropdownStatus(`[data-form-controls="${this.elementId}"]`);
this.dropdown.getElement().classList.remove(Classes.hidden);
}
/**
* Add form element event listener.
*/
addEventListeners() {
this.dropdown.getElement().addEventListener(
'change',
this.updateSelect.bind(this)
);
// Click on a dropdown link can trigger a wrong dirty form reload warning.
this.dropdown.getElement().addEventListener(
'click',
(event) => event.preventDefault()
);
this.mainSelect.addEventListener(
'change',
this.updateDropdown.bind(this)
);
// Enabling or disabling the select does not trigger any JS event.
const observerCallback = (mutations) => {
mutations.forEach((mutation) => {
if (mutation.type !== 'attributes' || mutation.attributeName !== 'disabled') {
return;
}
this.updateDropdown();
});
};
new MutationObserver(observerCallback).observe(
this.mainSelect,
{attributeFilter: ['disabled']}
);
}
/**
* Check if the field is disabled.
* @returns {Boolean}
*/
isDisabled() {
return this.mainSelect?.hasAttribute('disabled');
}
/**
* Update selected option preview in form.
*/
async updateDropdown() {
this.dropdown.setButtonDisabled(this.isDisabled());
if (this.dropdown.getSelectedValue() == this.mainSelect.value) {
return;
}
this.dropdown.setSelectedValue(this.mainSelect.value);
}
/**
* Update selected option preview in form.
*/
async updateSelect() {
if (this.dropdown.getSelectedValue() == this.mainSelect.value) {
return;
}
this.mainSelect.value = this.dropdown.getSelectedValue();
markFormAsDirty(this.mainSelect.closest('form'));
// Change the select element via JS does not trigger the standard change event.
this.mainSelect.dispatchEvent(new Event('change'));
}
/**
* Disable the choice dialog and convert it into a regular select field.
*/
disableInteractiveDialog() {
this.mainSelect?.classList.remove(Classes.hidden);
const dropdownElement = this.dropdown.getElement();
dropdownElement.classList.add(Classes.hidden);
}
/**
* Check if the field has a force dialog attribute.
// *
* The force dialog is a setting to force the javascript control even in
* behat test.
*
* @returns {Boolean} if the dialog modal should be forced or not
*/
hasForceDialog() {
return !!this.mainSelect?.dataset.forceDialog;
}
}
/**
* Initialises a choice dialog field.
*
* @method init
* @param {String} elementId Form element id
* @listens event:uploadStarted
* @listens event:uploadCompleted
*/
export const init = (elementId) => {
const field = new FieldController(elementId);
// This field is just a select wrapper. To optimize tests, we don't want to keep behat
// waiting for extra loadings in this case. The set field steps are about testing other
// stuff, not to test fancy javascript form fields. However, we keep the possibility of
// testing the javascript part using behat when necessary.
if (document.body.classList.contains('behat-site') && !field.hasForceDialog()) {
field.disableInteractiveDialog();
return;
}
field.addEventListeners();
};
+112
View File
@@ -0,0 +1,112 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Collapse or expand all form sections on clicking the expand all / collapse al link.
*
* @module core_form/collapsesections
* @copyright 2021 Bas Brands
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/
import $ from 'jquery';
import Pending from 'core/pending';
const SELECTORS = {
FORM: '.mform',
FORMHEADER: '.fheader',
FORMCONTAINER: 'fieldset > .fcontainer',
};
const CLASSES = {
SHOW: 'show',
COLLAPSED: 'collapsed',
HIDDEN: 'd-none'
};
/**
* Initialises the form section collapse / expand action.
*
* @param {string} collapsesections the collapse/expand link id.
*/
export const init = collapsesections => {
// All jQuery in this code can be replaced when MDL-71979 is integrated (move to Bootstrap 5).
const pendingPromise = new Pending('core_form/collapsesections');
const collapsemenu = document.querySelector(collapsesections);
const formParent = collapsemenu.closest(SELECTORS.FORM);
const formContainers = formParent.querySelectorAll(SELECTORS.FORMCONTAINER);
collapsemenu.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
collapsemenu.click();
}
});
// Override default collapse class if all visible containers are expanded on page load
let formcontainercount = 0;
let expandedcount = 0;
formContainers.forEach(container => {
const parentFieldset = container.parentElement;
if (!parentFieldset.classList.contains(CLASSES.HIDDEN)) {
formcontainercount++;
}
if (container.classList.contains(CLASSES.SHOW)) {
expandedcount++;
}
});
if (formcontainercount === expandedcount) {
collapsemenu.classList.remove(CLASSES.COLLAPSED);
collapsemenu.setAttribute('aria-expanded', true);
}
// When the collapse menu is toggled, update each form container to match.
collapsemenu.addEventListener('click', () => {
let action = 'hide';
if (collapsemenu.classList.contains(CLASSES.COLLAPSED)) {
action = 'show';
}
formContainers.forEach(container => $(container).collapse(action));
});
// Ensure collapse menu button adds aria-controls attribute referring to each collapsible element.
const collapseElements = formParent.querySelectorAll(SELECTORS.FORMHEADER);
const collapseElementIds = [...collapseElements].map((element, index) => {
element.id = element.id || `collapseElement-${index}`;
return element.id;
});
collapsemenu.setAttribute('aria-controls', collapseElementIds.join(' '));
// When any form container is toggled, re-calculate collapse menu state.
$(SELECTORS.FORMCONTAINER).on('hidden.bs.collapse', () => {
const allCollapsed = [...formContainers].every(container => !container.classList.contains(CLASSES.SHOW));
if (allCollapsed) {
collapsemenu.classList.add(CLASSES.COLLAPSED);
collapsemenu.setAttribute('aria-expanded', false);
}
});
$(SELECTORS.FORMCONTAINER).on('shown.bs.collapse', () => {
const allExpanded = [...formContainers].every(container => container.classList.contains(CLASSES.SHOW));
if (allExpanded) {
collapsemenu.classList.remove(CLASSES.COLLAPSED);
collapsemenu.setAttribute('aria-expanded', true);
}
});
pendingPromise.resolve();
};
+121
View File
@@ -0,0 +1,121 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Validation for configtext_maxlength.
*
* @module core_form/configtext-maxlength
* @copyright 2021 The Open University
*/
import {get_string as getString} from 'core/str';
import Templates from 'core/templates';
import Notification from 'core/notification';
import {prefetchStrings, prefetchTemplates} from 'core/prefetch';
let registered = false;
/**
* Initialisation function.
*/
export const init = () => {
if (registered) {
return;
}
prefetchStrings('core', [
'maximumchars',
]);
prefetchTemplates([
'core_form/setting_validation_failure',
]);
registered = true;
document.addEventListener('input', e => {
const maxLengthField = e.target.closest('[data-validation-max-length]');
if (!maxLengthField) {
return;
}
if (maxLengthField.value.length > maxLengthField.dataset.validationMaxLength) {
// Disable the form for this field.
maxLengthField.form.addEventListener('submit', submissionCheck);
// Display an error.
getString('maximumchars', 'core', maxLengthField.dataset.validationMaxLength)
.then(errorMessage => {
return Templates.renderForPromise('core_form/setting_validation_failure', {
fieldid: maxLengthField.id,
message: errorMessage,
});
})
.then(errorTemplate => {
if (!maxLengthField.dataset.validationFailureId) {
const formWrapper = maxLengthField.closest('.form-text');
Templates.prependNodeContents(formWrapper, errorTemplate.html, errorTemplate.js);
maxLengthField.dataset.validationFailureId = `maxlength_error_${maxLengthField.id}`;
// Disable submit button when the message is displayed.
updateSubmitButton();
}
return;
})
.then(() => {
maxLengthField.setAttribute('aria-invalid', true);
const errorField = document.getElementById(maxLengthField.dataset.validationFailureId);
if (errorField) {
errorField.setAttribute('aria-describedby', maxLengthField.id);
}
return;
})
.catch(Notification.exception);
} else {
// Remove the old message.
const validationMessage = document.getElementById(maxLengthField.dataset.validationFailureId);
if (validationMessage) {
validationMessage.parentElement.remove();
delete maxLengthField.dataset.validationFailureId;
maxLengthField.removeAttribute('aria-invalid');
// Enable submit button when the message was removed.
updateSubmitButton();
}
}
});
};
/**
* Handle form submission.
*
* @param {Event} e The event.
*/
const submissionCheck = e => {
const maxLengthFields = e.target.querySelectorAll('[data-validation-max-length]');
const maxLengthFieldsArray = Array.from(maxLengthFields);
maxLengthFieldsArray.some(maxLengthField => {
// Focus on the first validation failure.
if (maxLengthField.value.length > maxLengthField.dataset.validationMaxLength) {
e.preventDefault();
maxLengthField.focus();
return true;
}
return false;
});
};
/**
* Update submit button.
*/
const updateSubmitButton = () => {
const shouldDisable = document.querySelector('form#adminsettings .error');
document.querySelector('form#adminsettings button[type="submit"]').disabled = !!shouldDisable;
};
+51
View File
@@ -0,0 +1,51 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Functionality for the form element defaultcustom
*
* @module core_form/defaultcustom
* @copyright 2017 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.3
*/
define(['jquery'], function($) {
var onChangeSelect = function(event) {
var element = $(event.target),
defaultvalue = JSON.parse(element.attr('data-defaultvalue')),
customvalue = JSON.parse(element.attr('data-customvalue')),
type = element.attr('data-type'),
form = element.closest('form'),
elementName = element.attr('name').replace(/\[customize\]$/, '[value]'),
newvalue = element.prop('checked') ? customvalue : defaultvalue;
if (type === 'text') {
form.find('[name="' + elementName + '"]').val(newvalue);
} else if (type === 'date_selector') {
form.find('[name="' + elementName + '[day]"]').val(newvalue.day);
form.find('[name="' + elementName + '[month]"]').val(newvalue.month);
form.find('[name="' + elementName + '[year]"]').val(newvalue.year);
} else if (type === 'date_time_selector') {
form.find('[name="' + elementName + '[day]"]').val(newvalue.day);
form.find('[name="' + elementName + '[month]"]').val(newvalue.month);
form.find('[name="' + elementName + '[year]"]').val(newvalue.year);
form.find('[name="' + elementName + '[hour]"]').val(newvalue.hour);
form.find('[name="' + elementName + '[minute]"]').val(newvalue.minute);
}
};
var selector = 'input[data-defaultcustom=true]';
$('body').on('change', selector, onChangeSelect);
});
+377
View File
@@ -0,0 +1,377 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* 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<unknown>}
*/
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));
}
}
+103
View File
@@ -0,0 +1,103 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Encrypted password functionality.
*
* @module core_form/encryptedpassword
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Constructor for EncryptedPassword.
*
* @class core_form/encryptedpassword
* @param {String} elementId The element to apply the encrypted password JS to
*/
export const EncryptedPassword = function(elementId) {
const wrapper = document.querySelector('div[data-encryptedpasswordid="' + elementId + '"]');
this.spanOrLink = wrapper.querySelector('span, a');
this.input = wrapper.querySelector('input');
this.editButtonOrLink = wrapper.querySelector('button[data-editbutton], a');
this.cancelButton = wrapper.querySelector('button[data-cancelbutton]');
// Edit button action.
var editHandler = (e) => {
e.stopImmediatePropagation();
e.preventDefault();
this.startEditing(true);
};
this.editButtonOrLink.addEventListener('click', editHandler);
// When it's a link, do some magic to make the label work as well.
if (this.editButtonOrLink.nodeName === 'A') {
wrapper.parentElement.previousElementSibling.querySelector('label').addEventListener('click', editHandler);
}
// Cancel button action.
this.cancelButton.addEventListener('click', (e) => {
e.stopImmediatePropagation();
e.preventDefault();
this.cancelEditing();
});
// If the value is not set yet, start editing and remove the cancel option - so that
// it saves something in the config table and doesn't keep repeat showing it as a new
// admin setting...
if (wrapper.dataset.novalue === 'y') {
this.startEditing(false);
this.cancelButton.style.display = 'none';
}
};
/**
* Starts editing.
*
* @param {Boolean} moveFocus If true, sets focus to the edit box
*/
EncryptedPassword.prototype.startEditing = function(moveFocus) {
this.input.style.display = 'inline';
this.input.disabled = false;
this.spanOrLink.style.display = 'none';
this.editButtonOrLink.style.display = 'none';
this.cancelButton.style.display = 'inline';
// Move the id around, which changes what happens when you click the label.
const id = this.editButtonOrLink.id;
this.editButtonOrLink.removeAttribute('id');
this.input.id = id;
if (moveFocus) {
this.input.focus();
}
};
/**
* Cancels editing.
*/
EncryptedPassword.prototype.cancelEditing = function() {
this.input.style.display = 'none';
this.input.value = '';
this.input.disabled = true;
this.spanOrLink.style.display = 'inline';
this.editButtonOrLink.style.display = 'inline';
this.cancelButton.style.display = 'none';
// Move the id around, which changes what happens when you click the label.
const id = this.input.id;
this.input.removeAttribute('id');
this.editButtonOrLink.id = id;
};
+367
View File
@@ -0,0 +1,367 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Javascript events for the `core_form` subsystem.
*
* @module core_form/events
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.10
*
* @example <caption>Example of listening to a form event.</caption>
* import {eventTypes as formEventTypes} from 'core_form/events';
*
* document.addEventListener(formEventTypes.formSubmittedByJavascript, e => {
* window.console.log(e.target); // The form that was submitted.
* window.console.log(e.detail.skipValidation); // Whether form validation was skipped.
* });
*/
import {getString} from 'core/str';
import {dispatchEvent} from 'core/event_dispatcher';
let changesMadeString;
/**
* Prevent user navigate away when upload progress still running.
* @param {Event} e The event
*/
const changesMadeCheck = e => {
if (e) {
e.returnValue = changesMadeString;
}
};
/**
* Events for `core_form`.
*
* @constant
* @property {String} formError See {@link event:core_form/error}
* @property {String} formFieldValidationFailed See {@link event:core_form/fieldValidationFailed}
* @property {String} formSubmittedByJavascript See {@link event:core_form/submittedByJavascript}
* @property {String} uploadChanged See {@link event:core_form/uploadChanged}
* @property {String} fieldStructureChanged See {@link event:core_form/fieldStructureChanged}
*/
export const eventTypes = {
/**
* An event triggered when a form contains an error
*
* @event formError
* @type {CustomEvent}
* @property {HTMLElement} target The form field which errored
*/
formError: 'core_form/error',
/**
* An event triggered when an mform is about to be submitted via javascript.
*
* @event core_form/submittedByJavascript
* @type {CustomEvent}
* @property {HTMLElement} target The form that was submitted
* @property {object} detail
* @property {boolean} detail.skipValidation Whether the form was submitted without validation (i.e. via a Cancel button)
* @property {boolean} detail.fallbackHandled Whether the legacy YUI event has been handled
*/
formSubmittedByJavascript: 'core_form/submittedByJavascript',
/**
* An event triggered upon form field validation failure.
*
* @event core_form/fieldValidationFailed
* @type {CustomEvent}
* @property {HTMLElement} target The field that failed validation
* @property {object} detail
* @property {String} detail.message The message displayed upon failure
*/
formFieldValidationFailed: 'core_form/fieldValidationFailed',
/**
* An event triggered when an upload is started
*
* @event core_form/uploadStarted
* @type {CustomEvent}
* @property {HTMLElement} target The location where the upload began
*/
uploadStarted: 'core_form/uploadStarted',
/**
* An event triggered when an upload completes
*
* @event core_form/uploadCompleted
* @type {CustomEvent}
* @property {HTMLElement} target The location where the upload completed
*/
uploadCompleted: 'core_form/uploadCompleted',
/**
* An event triggered when a file upload field has been changed.
*
* @event core_form/uploadChanged
* @type {CustomEvent}
* @property {HTMLElement} target The form field which was changed
*/
uploadChanged: 'core_form/uploadChanged',
/**
* An event triggered when a form field structure has changed.
*
* @event core_form/fieldStructureChanged
* @type {CustomEvent}
* @property {HTMLElement} target The form field that has changed
*/
fieldStructureChanged: 'core_form/fieldStructureChanged',
};
// These are only imported for legacy.
import jQuery from 'jquery';
import Y from 'core/yui';
/**
* Trigger an event to indicate that a form field contained an error.
*
* @method notifyFormError
* @param {HTMLElement} field The form field causing the error
* @returns {CustomEvent}
* @fires formError
*/
export const notifyFormError = field => dispatchEvent(eventTypes.formError, {}, field);
/**
* Trigger an event to indiciate that a form was submitted by Javascript.
*
* @method
* @param {HTMLElement} form The form that was submitted
* @param {Boolean} skipValidation Submit the form without validation. E.g. "Cancel".
* @param {Boolean} fallbackHandled The legacy YUI event has been handled
* @returns {CustomEvent}
* @fires formSubmittedByJavascript
*/
export const notifyFormSubmittedByJavascript = (form, skipValidation = false, fallbackHandled = false) => {
if (skipValidation) {
window.skipClientValidation = true;
}
const customEvent = dispatchEvent(
eventTypes.formSubmittedByJavascript,
{
skipValidation,
fallbackHandled,
},
form
);
if (skipValidation) {
window.skipClientValidation = false;
}
return customEvent;
};
/**
* Trigger an event to indicate that a form field contained an error.
*
* @method notifyFieldValidationFailure
* @param {HTMLElement} field The field which failed validation
* @param {String} message The message displayed
* @returns {CustomEvent}
* @fires formFieldValidationFailed
*/
export const notifyFieldValidationFailure = (field, message) => dispatchEvent(
eventTypes.formFieldValidationFailed,
{
message,
},
field,
{
cancelable: true
}
);
/**
* Trigger an event to indicate that an upload was started.
*
* @method
* @param {String} elementId The element which was uploaded to
* @returns {CustomEvent}
* @fires uploadStarted
*/
export const notifyUploadStarted = async elementId => {
// Add an additional check for changes made.
changesMadeString = await getString('changesmadereallygoaway', 'moodle');
window.addEventListener('beforeunload', changesMadeCheck);
return dispatchEvent(
eventTypes.uploadStarted,
{},
document.getElementById(elementId),
{
bubbles: true,
cancellable: false,
}
);
};
/**
* Trigger an event to indicate that an upload was completed.
*
* @method
* @param {String} elementId The element which was uploaded to
* @returns {CustomEvent}
* @fires uploadCompleted
*/
export const notifyUploadCompleted = elementId => {
// Remove the additional check for changes made.
window.removeEventListener('beforeunload', changesMadeCheck);
return dispatchEvent(
eventTypes.uploadCompleted,
{},
document.getElementById(elementId),
{
bubbles: true,
cancellable: false,
}
);
};
/**
* Trigger upload start event.
*
* @method
* @param {String} elementId
* @returns {CustomEvent}
* @fires uploadStarted
* @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadStarted notifyUploadStarted}
*/
export const triggerUploadStarted = notifyUploadStarted;
/**
* Trigger upload complete event.
*
* @method
* @param {String} elementId
* @returns {CustomEvent}
* @fires uploadCompleted
* @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadCompleted notifyUploadCompleted}
*/
export const triggerUploadCompleted = notifyUploadCompleted;
/**
* List of the events.
*
* @deprecated since Moodle 4.0. See {@link module:core_form/events.eventTypes eventTypes} instead.
**/
export const types = {
uploadStarted: 'core_form/uploadStarted',
uploadCompleted: 'core_form/uploadCompleted',
};
let legacyEventsRegistered = false;
if (!legacyEventsRegistered) {
// The following event triggers are legacy and will be removed in the future.
// The following approach provides a backwards-compatability layer for the new events.
// Code should be updated to make use of native events.
Y.use('event', 'moodle-core-event', () => {
// Watch for the new native formError event, and trigger the legacy YUI event.
document.addEventListener(eventTypes.formError, e => {
const element = Y.one(e.target);
const formElement = Y.one(e.target.closest('form'));
Y.Global.fire(
M.core.globalEvents.FORM_ERROR,
{
formid: formElement.generateID(),
elementid: element.generateID(),
}
);
});
// Watch for the new native formSubmittedByJavascript event, and trigger the legacy YUI event.
document.addEventListener(eventTypes.formSubmittedByJavascript, e => {
if (e.detail.fallbackHandled) {
// This event was originally generated by a YUI event.
// Do not generate another as this will recurse.
return;
}
if (e.skipValidation) {
window.skipClientValidation = true;
}
// Trigger the legacy YUI event.
const form = Y.one(e.target);
form.fire(
M.core.event.FORM_SUBMIT_AJAX,
{
currentTarget: form,
fallbackHandled: true,
}
);
if (e.skipValidation) {
window.skipClientValidation = false;
}
});
});
// Watch for the new native formFieldValidationFailed event, and trigger the legacy jQuery event.
document.addEventListener(eventTypes.formFieldValidationFailed, e => {
// Note: The "core_form-field-validation" event is hard-coded in core/event.
// This is not included to prevent cyclic module dependencies.
const legacyEvent = jQuery.Event("core_form-field-validation");
jQuery(e.target).trigger(legacyEvent, e.detail.message);
});
legacyEventsRegistered = true;
}
/**
* Trigger an event to notify the file upload field has been changed.
*
* @method
* @param {string} elementId The element which was changed
* @returns {CustomEvent}
* @fires uploadChanged
*/
export const notifyUploadChanged = elementId => dispatchEvent(
eventTypes.uploadChanged,
{},
document.getElementById(elementId),
{
bubbles: true,
cancellable: false,
}
);
/**
* Trigger an event to notify the field structure has changed.
*
* @method
* @param {string} elementId The element which was changed
* @returns {CustomEvent}
* @fires fieldStructureChanged
*/
export const notifyFieldStructureChanged = elementId => dispatchEvent(
eventTypes.fieldStructureChanged,
{},
document.getElementById(elementId),
{
bubbles: true,
cancellable: false,
}
);
+304
View File
@@ -0,0 +1,304 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* This module allows to enhance the form elements MoodleQuickForm_filetypes
*
* @module core_form/filetypes
* @copyright 2017 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.3
*/
define(['jquery', 'core/log', 'core/modal_events', 'core/modal_save_cancel', 'core/ajax',
'core/templates', 'core/tree'],
function($, Log, ModalEvents, ModalSaveCancel, Ajax, Templates, Tree) {
"use strict";
/**
* Constructor of the FileTypes instances.
*
* @constructor
* @param {String} elementId The id of the form element to enhance
* @param {String} elementLabel The label of the form element used as the modal selector title
* @param {String} onlyTypes Limit the list of offered types to this
* @param {Bool} allowAll Allow presence of the "All file types" item
*/
var FileTypes = function(elementId, elementLabel, onlyTypes, allowAll) {
this.elementId = elementId;
this.elementLabel = elementLabel;
this.onlyTypes = onlyTypes;
this.allowAll = allowAll;
this.inputField = $('#' + elementId);
this.wrapperBrowserTrigger = $('[data-filetypesbrowser="' + elementId + '"]');
this.wrapperDescriptions = $('[data-filetypesdescriptions="' + elementId + '"]');
if (!this.wrapperBrowserTrigger.length) {
// This is a valid case. Most probably the element is frozen and
// the filetypes browser should not be available.
return;
}
if (!this.inputField.length || !this.wrapperDescriptions.length) {
Log.error('core_form/filetypes: Unexpected DOM structure, unable to enhance filetypes field ' + elementId);
return;
}
this.prepareBrowserTrigger()
.then(function() {
return this.prepareBrowserModal();
}.bind(this))
.then(function() {
return this.prepareBrowserTree();
}.bind(this));
};
/**
* Create and set the browser trigger widget (this.browserTrigger).
*
* @method prepareBrowserTrigger
* @returns {Promise}
*/
FileTypes.prototype.prepareBrowserTrigger = function() {
return Templates.render('core_form/filetypes-trigger', {})
.then(function(html) {
this.wrapperBrowserTrigger.html(html);
this.browserTrigger = this.wrapperBrowserTrigger.find('[data-filetypeswidget="browsertrigger"]');
}.bind(this));
};
/**
* Create and set the modal for displaying the browser (this.browserModal).
*
* @method prepareBrowserModal
* @returns {Promise}
*/
FileTypes.prototype.prepareBrowserModal = function() {
return ModalSaveCancel.create({
title: this.elementLabel,
})
.then(function(modal) {
this.browserModal = modal;
return modal;
}.bind(this))
.then(function() {
// Because we have custom conditional modal trigger, we need to
// handle the focus after closing ourselves, too.
this.browserModal.getRoot().on(ModalEvents.hidden, function() {
this.browserTrigger.focus();
}.bind(this));
this.browserModal.getRoot().on(ModalEvents.save, function() {
this.saveBrowserModal();
}.bind(this));
}.bind(this));
};
/**
* Create and set the tree in the browser modal's body.
*
* @method prepareBrowserTree
* @returns {Promise}
*/
FileTypes.prototype.prepareBrowserTree = function() {
this.browserTrigger.on('click', function(e) {
e.preventDefault();
// We want to display the browser modal only when the associated input
// field is not frozen (disabled).
if (this.inputField.is('[disabled]')) {
return;
}
var bodyContent = this.loadBrowserModalBody();
bodyContent.then(function() {
// Turn the list of groups and extensions into the tree.
this.browserTree = new Tree(this.browserModal.getBody());
// Override the behaviour of the Enter and Space keys to toggle our checkbox,
// rather than toggle the tree node expansion status.
this.browserTree.handleKeyDown = function(item, e) {
if (e.keyCode == this.browserTree.keys.enter || e.keyCode == this.browserTree.keys.space) {
e.preventDefault();
e.stopPropagation();
this.toggleCheckbox(item.attr('data-filetypesbrowserkey'));
} else {
Tree.prototype.handleKeyDown.call(this.browserTree, item, e);
}
}.bind(this);
if (this.allowAll) {
// Hide all other items if "All file types" is enabled.
this.hideOrShowItemsDependingOnAllowAll(this.browserModal.getRoot()
.find('input[type="checkbox"][data-filetypesbrowserkey="*"]').first());
// And do the same whenever we click that checkbox.
this.browserModal.getRoot().on('change', 'input[type="checkbox"][data-filetypesbrowserkey="*"]', function(e) {
this.hideOrShowItemsDependingOnAllowAll($(e.currentTarget));
}.bind(this));
}
// Synchronize checked status if the file extension is present in multiple groups.
this.browserModal.getRoot().on('change', 'input[type="checkbox"][data-filetypesbrowserkey]', function(e) {
var checkbox = $(e.currentTarget);
var key = checkbox.attr('data-filetypesbrowserkey');
this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="' + key + '"]')
.prop('checked', checkbox.prop('checked'));
}.bind(this));
}.bind(this))
.then(function() {
this.browserModal.show();
}.bind(this));
this.browserModal.setBody(bodyContent);
}.bind(this));
// Return a resolved promise.
return $.when();
};
/**
* Load the browser modal body contents.
*
* @returns {Promise}
*/
FileTypes.prototype.loadBrowserModalBody = function() {
var args = {
onlytypes: this.onlyTypes.join(),
allowall: this.allowAll,
current: this.inputField.val()
};
return Ajax.call([{
methodname: 'core_form_get_filetypes_browser_data',
args: args
}])[0].then(function(browserData) {
return Templates.render('core_form/filetypes-browser', {
elementid: this.elementId,
groups: browserData.groups
});
}.bind(this));
};
/**
* Change the checked status of the given file type (group or extension).
*
* @method toggleCheckbox
* @param {String} key
*/
FileTypes.prototype.toggleCheckbox = function(key) {
var checkbox = this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="' + key + '"]').first();
checkbox.prop('checked', !checkbox.prop('checked'));
};
/**
* Update the associated input field with selected file types.
*
* @method saveBrowserModal
*/
FileTypes.prototype.saveBrowserModal = function() {
// Check the "All file types" first.
if (this.allowAll) {
var allcheckbox = this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="*"]');
if (allcheckbox.length && allcheckbox.prop('checked')) {
this.inputField.val('*');
this.updateDescriptions(['*']);
return;
}
}
// Iterate over all checked boxes and populate the list.
var newvalue = [];
this.browserModal.getRoot().find('input[type="checkbox"]').each(/** @this represents the checkbox */ function() {
var checkbox = $(this);
var key = checkbox.attr('data-filetypesbrowserkey');
if (checkbox.prop('checked')) {
newvalue.push(key);
}
});
// Remove duplicates (e.g. file types present in multiple groups).
newvalue = newvalue.filter(function(x, i, a) {
return a.indexOf(x) == i;
});
this.inputField.val(newvalue.join(' '));
this.updateDescriptions(newvalue);
};
/**
* Describe the selected filetypes in the form when saving the browser.
*
* @param {Array} keys List of keys to describe
* @returns {Promise}
*/
FileTypes.prototype.updateDescriptions = function(keys) {
var descriptions = [];
keys.forEach(function(key) {
descriptions.push({
description: this.browserModal.getRoot().find('[data-filetypesname="' + key + '"]').first().text().trim(),
extensions: this.browserModal.getRoot().find('[data-filetypesextensions="' + key + '"]').first().text().trim()
});
}.bind(this));
var templatedata = {
hasdescriptions: (descriptions.length > 0),
descriptions: descriptions
};
return Templates.render('core_form/filetypes-descriptions', templatedata)
.then(function(html) {
this.wrapperDescriptions.html(html);
}.bind(this));
};
/**
* If "All file types" is checked, all other browser items are made hidden, and vice versa.
*
* @param {jQuery} allcheckbox The "All file types" checkbox.
*/
FileTypes.prototype.hideOrShowItemsDependingOnAllowAll = function(allcheckbox) {
var others = this.browserModal.getRoot().find('[role="treeitem"][data-filetypesbrowserkey!="*"]');
if (allcheckbox.prop('checked')) {
others.hide();
} else {
others.show();
}
};
return {
init: function(elementId, elementLabel, onlyTypes, allowAll) {
new FileTypes(elementId, elementLabel, onlyTypes, allowAll);
}
};
});
+432
View File
@@ -0,0 +1,432 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* 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 <mitxel@tresipunt.>
* @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<unknown>}
*/
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)};
})
.catch(exception => this.onSubmitError(exception));
}
/**
* On exception during form processing or initial rendering. 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.enableButtons();
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);
}
}
+299
View File
@@ -0,0 +1,299 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Password Unmask functionality.
*
* @module core_form/passwordunmask
* @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.2
*/
define(['jquery', 'core/templates'], function($, Template) {
/**
* Constructor for PasswordUnmask.
*
* @class core_form/passwordunmask
* @param {String} elementid The element to apply the PasswordUnmask to
*/
var PasswordUnmask = function(elementid) {
// Setup variables.
this.wrapperSelector = '[data-passwordunmask="wrapper"][data-passwordunmaskid="' + elementid + '"]';
this.wrapper = $(this.wrapperSelector);
this.editorSpace = this.wrapper.find('[data-passwordunmask="editor"]');
this.editLink = this.wrapper.find('a[data-passwordunmask="edit"]');
this.editInstructions = this.wrapper.find('[data-passwordunmask="instructions"]');
this.displayValue = this.wrapper.find('[data-passwordunmask="displayvalue"]');
this.inputFieldLabel = $('label[for="' + elementid + '"]');
this.inputField = this.editorSpace.find(document.getElementById(elementid));
// Hide the field.
this.inputField.addClass('d-none');
this.inputField.removeClass('hiddenifjs');
if (!this.editInstructions.attr('id')) {
this.editInstructions.attr('id', elementid + '_instructions');
}
this.editInstructions.hide();
this.setDisplayValue();
// Add the listeners.
this.addListeners();
};
/**
* Add the event listeners required for PasswordUnmask.
*
* @method addListeners
* @return {PasswordUnmask}
* @chainable
*/
PasswordUnmask.prototype.addListeners = function() {
this.wrapper.on('click keypress', '[data-passwordunmask="edit"]', $.proxy(function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
if (this.isEditing()) {
// Only focus on the edit link if the event was not a click, and the new target is not an input field.
if (e.type !== 'click' && !$(e.relatedTarget).is(':input')) {
this.turnEditingOff(true);
} else {
this.turnEditingOff(false);
}
} else {
this.turnEditingOn();
}
}, this));
this.wrapper.on('click keypress', '[data-passwordunmask="unmask"]', $.proxy(function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
// Toggle the data attribute.
this.wrapper.data('unmasked', !this.wrapper.data('unmasked'));
this.setDisplayValue();
}, this));
this.wrapper.on('keydown', 'input', $.proxy(function(e) {
if (e.type === 'keydown' && e.keyCode !== 13) {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
this.turnEditingOff(true);
}, this));
this.inputFieldLabel.on('click', $.proxy(function(e) {
e.preventDefault();
this.turnEditingOn();
}, this));
return this;
};
/**
* Check whether focus was lost from the PasswordUnmask and turn editing off if required.
*
* @method checkFocusOut
* @param {EventFacade} e The EventFacade generating the suspsected Focus Out
*/
PasswordUnmask.prototype.checkFocusOut = function(e) {
if (!this.isEditing()) {
// Ignore - not editing.
return;
}
window.setTimeout($.proxy(function() {
// Firefox does not have the focusout event. Instead jQuery falls back to the 'blur' event.
// The blur event does not have a relatedTarget, so instead we use a timeout and the new activeElement.
var relatedTarget = e.relatedTarget || document.activeElement;
if (this.wrapper.has($(relatedTarget)).length) {
// Ignore, some part of the element is still active.
return;
}
// Only focus on the edit link if the new related target is not an input field or anchor.
this.turnEditingOff(!$(relatedTarget).is(':input,a'));
}, this), 100);
};
/**
* Whether the password is currently visible (unmasked).
*
* @method passwordVisible
* @return {Boolean} True if the password is unmasked
*/
PasswordUnmask.prototype.passwordVisible = function() {
return !!this.wrapper.data('unmasked');
};
/**
* Whether the user is currently editing the field.
*
* @method isEditing
* @return {Boolean} True if edit mode is enabled
*/
PasswordUnmask.prototype.isEditing = function() {
return this.inputField.hasClass('d-inline-block');
};
/**
* Enable the editing functionality.
*
* @method turnEditingOn
* @return {PasswordUnmask}
* @chainable
*/
PasswordUnmask.prototype.turnEditingOn = function() {
var value = this.getDisplayValue();
if (this.passwordVisible()) {
this.inputField.attr('type', 'text');
} else {
this.inputField.attr('type', 'password');
}
this.inputField.val(value);
this.inputField.attr('size', this.inputField.attr('data-size'));
// Show the field.
this.inputField.addClass('d-inline-block');
if (this.editInstructions.length) {
this.inputField.attr('aria-describedby', this.editInstructions.attr('id'));
this.editInstructions.show();
}
this.wrapper.attr('data-passwordunmask-visible', 1);
this.editLink.hide();
this.inputField
.focus()
.select();
// Note, this cannot be added as a delegated listener on init because Firefox does not support the FocusOut
// event (https://bugzilla.mozilla.org/show_bug.cgi?id=687787) and the blur event does not identify the
// relatedTarget.
// The act of focusing the this.inputField means that in Firefox the focusout will be triggered on blur of the edit
// link anchor.
$('body').on('focusout', this.wrapperSelector, $.proxy(this.checkFocusOut, this));
return this;
};
/**
* Disable the editing functionality, optionally focusing on the edit link.
*
* @method turnEditingOff
* @param {Boolean} focusOnEditLink Whether to focus on the edit link after disabling the editor
* @return {PasswordUnmask}
* @chainable
*/
PasswordUnmask.prototype.turnEditingOff = function(focusOnEditLink) {
$('body').off('focusout', this.wrapperSelector, this.checkFocusOut);
var value = this.getDisplayValue();
this.inputField
// Ensure that the aria-describedby is removed.
.attr('aria-describedby', null);
this.inputField.val(value);
// Hide the field again.
this.inputField.removeClass('d-inline-block');
this.editInstructions.hide();
// Remove the visible attr.
this.wrapper.removeAttr('data-passwordunmask-visible');
// Remove the size attr.
this.inputField.removeAttr('size');
this.editLink.show();
this.setDisplayValue();
if (focusOnEditLink) {
this.editLink.focus();
}
return this;
};
/**
* Get the currently value.
*
* @method getDisplayValue
* @return {String}
*/
PasswordUnmask.prototype.getDisplayValue = function() {
return this.inputField.val();
};
/**
* Set the currently value in the display, taking into account the current settings.
*
* @method setDisplayValue
* @return {PasswordUnmask}
* @chainable
*/
PasswordUnmask.prototype.setDisplayValue = function() {
var value = this.getDisplayValue();
if (this.isEditing()) {
if (this.wrapper.data('unmasked')) {
this.inputField.attr('type', 'text');
} else {
this.inputField.attr('type', 'password');
}
this.inputField.val(value);
}
// Update the display value.
// Note: This must always be updated.
// The unmask value can be changed whilst editing and the editing can then be disabled.
if (value && this.wrapper.data('unmasked')) {
// There is a value, and we will show it.
this.displayValue.text(value);
} else {
if (!value) {
value = "";
}
// There is a value, but it will be disguised.
// We use the passwordunmask-fill to allow modification of the fill and to ensure that the display does not
// change as the page loads the JS.
Template.render('core_form/element-passwordunmask-fill', {
element: {
frozen: this.inputField.is('[readonly]'),
value: value,
valuechars: value.split(''),
},
}).done($.proxy(function(html, js) {
this.displayValue.html(html);
Template.runTemplateJS(js);
}, this));
}
return this;
};
return PasswordUnmask;
});
+227
View File
@@ -0,0 +1,227 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* A class to help show and hide advanced form content.
*
* @module core_form/showadvanced
* @copyright 2016 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/log', 'core/str', 'core/notification'], function($, Log, Strings, Notification) {
var SELECTORS = {
FIELDSETCONTAINSADVANCED: 'fieldset.containsadvancedelements',
DIVFITEMADVANCED: 'div.fitem.advanced',
DIVADVANCEDSECTION: 'div#form-advanced-div',
DIVFCONTAINER: 'div.fcontainer',
MORELESSLINK: 'fieldset.containsadvancedelements .moreless-toggler'
},
CSS = {
SHOW: 'show',
MORELESSACTIONS: 'moreless-actions',
MORELESSTOGGLER: 'moreless-toggler',
SHOWLESS: 'moreless-less'
},
WRAPPERS = {
FITEM: '<div class="fitem"></div>',
FELEMENT: '<div class="felement"></div>',
ADVANCEDDIV: '<div id="form-advanced-div"></div>'
},
IDPREFIX = 'showadvancedid-';
/** @property {Integer} uniqIdSeed Auto incrementing number used to generate ids. */
var uniqIdSeed = 0;
/**
* ShowAdvanced behaviour class.
*
* @class core_form/showadvanced
* @param {String} id The id of the form.
*/
var ShowAdvanced = function(id) {
this.id = id;
var form = $(document.getElementById(id));
this.enhanceForm(form);
};
/** @property {String} id The form id to enhance. */
ShowAdvanced.prototype.id = '';
/**
* @method enhanceForm
* @param {JQuery} form JQuery selector representing the form
* @return {ShowAdvanced}
*/
ShowAdvanced.prototype.enhanceForm = function(form) {
var fieldsets = form.find(SELECTORS.FIELDSETCONTAINSADVANCED);
// Enhance each fieldset in the form matching the selector.
fieldsets.each(function(index, item) {
this.enhanceFieldset($(item));
}.bind(this));
// Attach some event listeners.
// Subscribe more/less links to click event.
form.on('click', SELECTORS.MORELESSLINK, this.switchState);
// Subscribe to key events but filter for space or enter.
form.on('keydown', SELECTORS.MORELESSLINK, function(e) {
// Enter or space.
if (e.which == 13 || e.which == 32) {
return this.switchState(e);
}
return true;
}.bind(this));
return this;
};
/**
* Generates a uniq id for the dom element it's called on unless the element already has an id.
* The id is set on the dom node before being returned.
*
* @method generateId
* @param {JQuery} node JQuery selector representing a single DOM Node.
* @return {String}
*/
ShowAdvanced.prototype.generateId = function(node) {
var id = node.prop('id');
if (typeof id === 'undefined') {
id = IDPREFIX + (uniqIdSeed++);
node.prop('id', id);
}
return id;
};
/**
* @method enhanceFieldset
* @param {JQuery} fieldset JQuery selector representing a fieldset
* @return {ShowAdvanced}
*/
ShowAdvanced.prototype.enhanceFieldset = function(fieldset) {
var statuselement = $('input[name=mform_showmore_' + fieldset.prop('id') + ']');
if (!statuselement.length) {
Log.debug("M.form.showadvanced::processFieldset was called on an fieldset without a status field: '" +
fieldset.prop('id') + "'");
return this;
}
// Fetch some strings.
Strings.get_strings([{
key: 'showmore',
component: 'core_form'
}, {
key: 'showless',
component: 'core_form'
}]).then(function(results) {
var showmore = results[0],
showless = results[1];
// Generate more/less links.
var morelesslink = $('<a href="#"></a>');
morelesslink.addClass(CSS.MORELESSTOGGLER);
if (statuselement.val() === '0') {
morelesslink.html(showmore);
morelesslink.attr('aria-expanded', 'false');
} else {
morelesslink.html(showless);
morelesslink.attr('aria-expanded', 'true');
morelesslink.addClass(CSS.SHOWLESS);
fieldset.find(SELECTORS.DIVFITEMADVANCED).addClass(CSS.SHOW);
}
// Build a list of advanced fieldsets.
var idlist = [];
fieldset.find(SELECTORS.DIVFITEMADVANCED).each(function(index, node) {
idlist[idlist.length] = this.generateId($(node));
}.bind(this));
// Set aria attributes.
morelesslink.attr('role', 'button');
morelesslink.attr('aria-controls', 'form-advanced-div');
var formadvancedsection = $(WRAPPERS.ADVANCEDDIV);
fieldset.find(SELECTORS.DIVFITEMADVANCED).wrapAll(formadvancedsection);
// Add elements to the DOM.
var fitem = $(WRAPPERS.FITEM);
fitem.addClass(CSS.MORELESSACTIONS);
var felement = $(WRAPPERS.FELEMENT);
felement.append(morelesslink);
fitem.append(felement);
fieldset.find(SELECTORS.DIVADVANCEDSECTION).before(fitem);
return true;
}.bind(this)).fail(Notification.exception);
return this;
};
/**
* @method switchState
* @param {Event} e Event that triggered this action.
* @return {Boolean}
*/
ShowAdvanced.prototype.switchState = function(e) {
e.preventDefault();
// Fetch some strings.
Strings.get_strings([{
key: 'showmore',
component: 'core_form'
}, {
key: 'showless',
component: 'core_form'
}]).then(function(results) {
var showmore = results[0],
showless = results[1],
fieldset = $(e.target).closest(SELECTORS.FIELDSETCONTAINSADVANCED);
// Toggle collapsed class.
fieldset.find(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.SHOW);
// Get corresponding hidden variable.
var statuselement = $('input[name=mform_showmore_' + fieldset.prop('id') + ']');
// Invert it and change the link text.
if (statuselement.val() === '0') {
statuselement.val(1);
$(e.target).addClass(CSS.SHOWLESS);
$(e.target).html(showless);
$(e.target).attr('aria-expanded', 'true');
} else {
statuselement.val(0);
$(e.target).removeClass(CSS.SHOWLESS);
$(e.target).html(showmore);
$(e.target).attr('aria-expanded', 'false');
}
return true;
}).fail(Notification.exception);
return this;
};
return {
/**
* Initialise this module.
* @method init
* @param {String} formid
* @return {ShowAdvanced}
*/
init: function(formid) {
return new ShowAdvanced(formid);
}
};
});
+165
View File
@@ -0,0 +1,165 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Submit button JavaScript. All submit buttons will be automatically disabled once the form is
* submitted, unless that submission results in an error/cancelling the submit.
*
* @module core_form/submit
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.8
*/
import {eventTypes} from 'core_form/events';
/** @property {number} ID for setInterval used when polling for download cookie */
let cookieListener = 0;
/** @property {Array} Array of buttons that need re-enabling if we get a download cookie */
const cookieListeningButtons = [];
/** @property {number} Number of files uploading. */
let currentUploadCount = 0;
/** @property {Array} Array of buttons that need re-enabling if we get a upload process. */
const uploadListeningButtons = [];
/** @property {Boolean} Is upload listeners registered? */
let uploadListenersRegistered = false;
/**
* Listens in case a download cookie is provided.
*
* This function is used to detect file downloads. If there is a file download then we get a
* beforeunload event, but the page is never unloaded and when the file download completes we
* should re-enable the buttons. We detect this by watching for a specific cookie.
*
* PHP function \core_form\util::form_download_complete() can be used to send this cookie.
*
* @param {HTMLElement} button Button to re-enable
*/
const listenForDownloadCookie = (button) => {
cookieListeningButtons.push(button);
if (!cookieListener) {
cookieListener = setInterval(() => {
// Look for cookie.
const parts = document.cookie.split(getCookieName() + '=');
if (parts.length == 2) {
// We found the cookie, so the file is ready. Expire the cookie and cancel polling.
clearDownloadCookie();
clearInterval(cookieListener);
cookieListener = 0;
// Re-enable all the buttons.
cookieListeningButtons.forEach((button) => {
button.disabled = false;
});
}
}, 500);
}
};
/**
* Gets a unique name for the download cookie.
*
* @returns {string} Cookie name
*/
const getCookieName = () => {
return 'moodledownload_' + M.cfg.sesskey;
};
/**
* Clears the download cookie if there is one.
*/
const clearDownloadCookie = () => {
document.cookie = encodeURIComponent(getCookieName()) + '=deleted; expires=' + new Date(0).toUTCString();
};
/**
* Enable submit buttons when all files are uploaded.
*/
const checkUploadCount = () => {
if (currentUploadCount) {
uploadListeningButtons.forEach(button => {
button.disabled = true;
});
} else {
uploadListeningButtons.forEach(button => {
button.disabled = false;
});
}
};
/**
* Initialises submit buttons.
*
* @param {String} elementId Form button element
* @listens event:uploadStarted
* @listens event:uploadCompleted
*/
export const init = (elementId) => {
const button = document.getElementById(elementId);
if (button === null) {
// Exit early if invalid element id passed.
return;
}
// If buttons are disabled by default, we do not enable them when file upload completed event is fired.
if (!button.disabled) {
uploadListeningButtons.push(button);
}
if (!uploadListenersRegistered) {
// Add event listener for file upload start.
document.addEventListener(eventTypes.uploadStarted, () => {
currentUploadCount++;
checkUploadCount();
});
// Add event listener for file upload complete.
document.addEventListener(eventTypes.uploadCompleted, () => {
currentUploadCount--;
checkUploadCount();
});
uploadListenersRegistered = true;
}
// If the form has double submit protection disabled, do nothing.
if (button.form.dataset.doubleSubmitProtection === 'off') {
return;
}
button.form.addEventListener('submit', function(event) {
// Only disable it if the browser is really going to another page as a result of the
// submit.
const disableAction = function() {
// If the submit was cancelled, or the button is already disabled, don't do anything.
if (event.defaultPrevented || button.disabled) {
return;
}
button.disabled = true;
clearDownloadCookie();
listenForDownloadCookie(button);
};
window.addEventListener('beforeunload', disableAction);
// If there is no beforeunload event as a result of this form submit, then the form
// submit must have been cancelled, so don't disable the button if the page is
// unloaded later.
setTimeout(function() {
window.removeEventListener('beforeunload', disableAction);
}, 1);
}, false);
};
+30
View File
@@ -0,0 +1,30 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Serialize form values into a string.
*
* This must be used instead of URLSearchParams, which does not correctly encode nested values such as arrays.
*
* @param {Object} data The form values to serialize
* @param {string} prefix The prefix to use for key names
* @returns {string}
*/
export const serialize = (data, prefix = '') => [
...Object.entries(data).map(([index, value]) => {
const key = prefix ? `${prefix}[${index}]` : index;
return (value !== null && typeof value === "object") ? serialize(value, key) : `${key}=${encodeURIComponent(value)}`;
})
].join("&");