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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
define("core_question/question_engine",["exports","core/scroll_manager","core_form/submit"],(function(_exports,scrollManager,formSubmit){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* JavaScript required by the question engine.
*
* @module core_question/question_engine
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.preventRepeatSubmission=_exports.initSubmitButton=_exports.initForm=void 0,scrollManager=_interopRequireWildcard(scrollManager),formSubmit=_interopRequireWildcard(formSubmit);_exports.initSubmitButton=button=>{formSubmit.init(button),scrollManager.watchScrollButtonSaves()};_exports.initForm=formSelector=>{const form=document.querySelector(formSelector);form.setAttribute("autocomplete","off"),form.addEventListener("submit",preventRepeatSubmission),form.addEventListener("key",(event=>{13===event.keyCode&&(event.target.matches("a")||event.target.matches('input[type="submit"]')||event.target.matches("input[type=img]")||event.target.matches("textarea")||event.target.matches("[contenteditable=true]")||event.preventDefault())}));[...form.querySelectorAll(".questionflagsavebutton")].forEach((node=>node.remove())),scrollManager.scrollToSavedPosition()};const preventRepeatSubmission=event=>{const form=event.target.closest("form");"1"!==form.dataset.formSubmitted?(setTimeout((()=>{[...form.querySelectorAll("input[type=submit]")].forEach((input=>input.setAttribute("disabled",!0)))})),form.dataset.formSubmitted="1"):event.preventDefault()};_exports.preventRepeatSubmission=preventRepeatSubmission}));
//# sourceMappingURL=question_engine.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"question_engine.min.js","sources":["../src/question_engine.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 * JavaScript required by the question engine.\n *\n * @module core_question/question_engine\n * @copyright 2021 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as scrollManager from 'core/scroll_manager';\nimport * as formSubmit from 'core_form/submit';\n\n/**\n * Initialise a question submit button. This saves the scroll position and\n * sets the fragment on the form submit URL so the page reloads in the right place.\n *\n * @param {string} button the id of the button in the HTML.\n */\nexport const initSubmitButton = button => {\n formSubmit.init(button);\n scrollManager.watchScrollButtonSaves();\n};\n\n/**\n * Initialise a form that contains questions printed using print_question.\n * This has the effect of:\n * 1. Turning off browser autocomlete.\n * 2. Stopping enter from submitting the form (or toggling the next flag) unless\n * keyboard focus is on the submit button or the flag.\n * 3. Removes any '.questionflagsavebutton's, since we have JavaScript to toggle\n * the flags using ajax.\n * 4. Scroll to the position indicated by scrollpos= in the URL, if it is there.\n * 5. Prevent the user from repeatedly submitting the form.\n *\n * @param {string} formSelector Selector to identify the form.\n */\nexport const initForm = (formSelector) => {\n const form = document.querySelector(formSelector);\n form.setAttribute('autocomplete', 'off');\n\n form.addEventListener('submit', preventRepeatSubmission);\n\n form.addEventListener('key', (event) => {\n if (event.keyCode !== 13) {\n return;\n }\n\n if (event.target.matches('a')) {\n return;\n }\n\n if (event.target.matches('input[type=\"submit\"]')) {\n return;\n }\n\n if (event.target.matches('input[type=img]')) {\n return;\n }\n\n if (event.target.matches('textarea') || event.target.matches('[contenteditable=true]')) {\n return;\n }\n\n event.preventDefault();\n });\n\n const questionFlagSaveButtons = form.querySelectorAll('.questionflagsavebutton');\n [...questionFlagSaveButtons].forEach((node) => node.remove());\n\n // Note: The scrollToSavedPosition function tries to wait until the content has loaded before firing.\n scrollManager.scrollToSavedPosition();\n};\n\n/**\n * Event handler to stop a question form being submitted more than once.\n *\n * @param {object} event the form submit event.\n */\nexport const preventRepeatSubmission = (event) => {\n const form = event.target.closest('form');\n if (form.dataset.formSubmitted === '1') {\n event.preventDefault();\n return;\n }\n\n setTimeout(() => {\n [...form.querySelectorAll('input[type=submit]')].forEach((input) => input.setAttribute('disabled', true));\n });\n form.dataset.formSubmitted = '1';\n};\n"],"names":["button","formSubmit","init","scrollManager","watchScrollButtonSaves","formSelector","form","document","querySelector","setAttribute","addEventListener","preventRepeatSubmission","event","keyCode","target","matches","preventDefault","querySelectorAll","forEach","node","remove","scrollToSavedPosition","closest","dataset","formSubmitted","setTimeout","input"],"mappings":";;;;;;;+QAgCgCA,SAC5BC,WAAWC,KAAKF,QAChBG,cAAcC,4CAgBOC,qBACfC,KAAOC,SAASC,cAAcH,cACpCC,KAAKG,aAAa,eAAgB,OAElCH,KAAKI,iBAAiB,SAAUC,yBAEhCL,KAAKI,iBAAiB,OAAQE,QACJ,KAAlBA,MAAMC,UAIND,MAAME,OAAOC,QAAQ,MAIrBH,MAAME,OAAOC,QAAQ,yBAIrBH,MAAME,OAAOC,QAAQ,oBAIrBH,MAAME,OAAOC,QAAQ,aAAeH,MAAME,OAAOC,QAAQ,2BAI7DH,MAAMI,yBAGsBV,KAAKW,iBAAiB,4BACzBC,SAASC,MAASA,KAAKC,WAGpDjB,cAAckB,+BAQLV,wBAA2BC,cAC9BN,KAAOM,MAAME,OAAOQ,QAAQ,QACC,MAA/BhB,KAAKiB,QAAQC,eAKjBC,YAAW,SACHnB,KAAKW,iBAAiB,uBAAuBC,SAASQ,OAAUA,MAAMjB,aAAa,YAAY,QAEvGH,KAAKiB,QAAQC,cAAgB,KAPzBZ,MAAMI"}
+10
View File
@@ -0,0 +1,10 @@
define("core_question/refresh_ui",["exports","core/fragment","core/templates"],(function(_exports,_fragment,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Question bank UI refresh utility
*
* @module core_question/refresh_ui
* @copyright 2023 Catalyst IT Europe Ltd.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates);var _default={refresh:(uiRoot,returnUrl)=>new Promise(((resolve,reject)=>{const fragmentData=uiRoot.dataset,viewData={},sortData={};returnUrl&&returnUrl.searchParams.forEach(((value,key)=>{const sortItem=key.match(/sortdata\[([^\]]+)\]/);sortItem?sortData[sortItem.pop()]=value:viewData[key]=value})),viewData.sortdata=JSON.stringify(sortData),_fragment.default.loadFragment(fragmentData.component,fragmentData.callback,fragmentData.contextid,viewData).then(((html,js)=>(_templates.default.replaceNode(uiRoot,html,js),resolve(),html))).catch(reject)}))};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=refresh_ui.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"refresh_ui.min.js","sources":["../src/refresh_ui.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 * Question bank UI refresh utility\n *\n * @module core_question/refresh_ui\n * @copyright 2023 Catalyst IT Europe Ltd.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport Templates from 'core/templates';\n\nexport default {\n /**\n * Reload the question bank UI, retaining the current filters and sort data.\n *\n * @param {Element} uiRoot The root element of the UI to be refreshed. Must contain \"component\", \"callback\" and \"contextid\" in\n * its data attributes, to be passed to the Fragment API.\n * @param {URL} returnUrl The url of the current page, containing filter and sort parameters.\n * @return {Promise} Resolved when the refresh is complete.\n */\n refresh: (uiRoot, returnUrl) => {\n return new Promise((resolve, reject) => {\n const fragmentData = uiRoot.dataset;\n const viewData = {};\n const sortData = {};\n if (returnUrl) {\n returnUrl.searchParams.forEach((value, key) => {\n // Match keys like 'sortdata[fieldname]' and convert them to an array,\n // because the fragment API doesn't like non-alphanum argument keys.\n const sortItem = key.match(/sortdata\\[([^\\]]+)\\]/);\n if (sortItem) {\n // The item returned by sortItem.pop() is the contents of the matching group, the field name.\n sortData[sortItem.pop()] = value;\n } else {\n viewData[key] = value;\n }\n });\n }\n viewData.sortdata = JSON.stringify(sortData);\n // We have to use then() there, as loadFragment doesn't appear to work with await.\n Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid, viewData)\n .then((html, js) => {\n Templates.replaceNode(uiRoot, html, js);\n resolve();\n return html;\n })\n .catch(reject);\n });\n }\n};\n"],"names":["refresh","uiRoot","returnUrl","Promise","resolve","reject","fragmentData","dataset","viewData","sortData","searchParams","forEach","value","key","sortItem","match","pop","sortdata","JSON","stringify","loadFragment","component","callback","contextid","then","html","js","replaceNode","catch"],"mappings":";;;;;;;4LA0Be,CASXA,QAAS,CAACC,OAAQC,YACP,IAAIC,SAAQ,CAACC,QAASC,gBACnBC,aAAeL,OAAOM,QACtBC,SAAW,GACXC,SAAW,GACbP,WACAA,UAAUQ,aAAaC,SAAQ,CAACC,MAAOC,aAG7BC,SAAWD,IAAIE,MAAM,wBACvBD,SAEAL,SAASK,SAASE,OAASJ,MAE3BJ,SAASK,KAAOD,SAI5BJ,SAASS,SAAWC,KAAKC,UAAUV,4BAE1BW,aAAad,aAAae,UAAWf,aAAagB,SAAUhB,aAAaiB,UAAWf,UACxFgB,MAAK,CAACC,KAAMC,yBACCC,YAAY1B,OAAQwB,KAAMC,IACpCtB,UACOqB,QAEVG,MAAMvB"}
+259
View File
@@ -0,0 +1,259 @@
// 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/>.
/**
* Question bank filter management.
*
* @module core_question/filter
* @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import CoreFilter from 'core/datafilter';
import Notification from 'core/notification';
import Selectors from 'core/datafilter/selectors';
import Templates from 'core/templates';
import Fragment from 'core/fragment';
/**
* Initialise the question bank filter on the element with the given id.
*
* @param {String} filterRegionId ID of the HTML element containing the filters.
* @param {String} defaultcourseid Course ID for the default course to pass back to the view.
* @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.
* @param {Number} perpage The number of questions to display per page.
* @param {Number} contextId Context ID of the question bank view.
* @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)
* @param {string} callback Name of the callback for the fragment API (e.g question_data)
* @param {string} view The class name of the question bank view class used for this page.
* @param {Number} cmid If we are in an activitiy, the course module ID.
* @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.
* @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.
*/
export const init = (
filterRegionId,
defaultcourseid,
defaultcategoryid,
perpage,
contextId,
component,
callback,
view,
cmid,
pagevars,
extraparams
) => {
const SELECTORS = {
QUESTION_CONTAINER_ID: '#questionscontainer',
QUESTION_TABLE: '#questionscontainer table',
SORT_LINK: '#questionscontainer div.sorters a',
PAGINATION_LINK: '#questionscontainer a[href].page-link',
LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',
BULK_ACTIONS: '#bulkactionsui-container input',
MENU_ACTIONS: '.menu-action',
EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',
EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',
};
const filterSet = document.querySelector(`#${filterRegionId}`);
const viewData = {
extraparams,
cmid,
view,
cat: defaultcategoryid,
courseid: defaultcourseid,
filter: {},
jointype: 0,
qpage: 0,
qperpage: perpage,
sortdata: {},
lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,
};
let sortData = {};
const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
if (defaultSort) {
sortData = JSON.parse(defaultSort);
}
/**
* Retrieve table data.
*
* @param {Object} filterdata data
* @param {Promise} pendingPromise pending promise
*/
const applyFilter = (filterdata, pendingPromise) => {
// Reload the questions based on the specified filters. If no filters are provided,
// use the default category filter condition.
if (filterdata) {
// Main join types.
viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);
delete filterdata.jointype;
// Retrieve filter info.
viewData.filter = filterdata;
if (Object.keys(filterdata).length !== 0) {
if (!isNaN(viewData.jointype)) {
filterdata.jointype = viewData.jointype;
}
updateUrlParams(filterdata);
}
}
// Load questions for first page.
viewData.filter = JSON.stringify(filterdata);
viewData.sortdata = JSON.stringify(sortData);
Fragment.loadFragment(component, callback, contextId, viewData)
// Render questions for first page and pagination.
.then((questionhtml, jsfooter) => {
const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);
if (questionhtml === undefined) {
questionhtml = '';
}
if (jsfooter === undefined) {
jsfooter = '';
}
Templates.replaceNode(questionscontainer, questionhtml, jsfooter);
// Resolve filter promise.
if (pendingPromise) {
pendingPromise.resolve();
}
return {questionhtml, jsfooter};
})
.catch(Notification.exception);
};
// Init core filter processor with apply callback.
const coreFilter = new CoreFilter(filterSet, applyFilter);
coreFilter.activeFilters = {}; // Unset useless courseid filter.
coreFilter.init();
/**
* Update URL Param based upon the current filter.
*
* @param {Object} filters Active filters.
*/
const updateUrlParams = (filters) => {
const url = new URL(location.href);
const filterQuery = JSON.stringify(filters);
url.searchParams.set('filter', filterQuery);
history.pushState(filters, '', url);
const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);
if (editSwitch) {
const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);
const editSwitchUrl = new URL(editSwitchUrlInput.value);
editSwitchUrl.searchParams.set('filter', filterQuery);
editSwitchUrlInput.value = editSwitchUrl;
editSwitch.dataset.pageurl = editSwitchUrl;
}
};
/**
* Cleans URL parameters.
*/
const cleanUrlParams = () => {
const queryString = location.search;
const urlParams = new URLSearchParams(queryString);
if (urlParams.has('cmid')) {
const cleanedUrl = new URL(location.href.replace(location.search, ''));
cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));
history.pushState({}, '', cleanedUrl);
}
if (urlParams.has('courseid')) {
const cleanedUrl = new URL(location.href.replace(location.search, ''));
cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));
history.pushState({}, '', cleanedUrl);
}
};
// Add listeners for the sorting, paging and clear actions.
document.addEventListener('click', e => {
const sortableLink = e.target.closest(SELECTORS.SORT_LINK);
const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);
const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
if (sortableLink) {
e.preventDefault();
const oldSort = sortData;
sortData = {};
sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
for (const sortname in oldSort) {
if (sortname !== sortableLink.dataset.sortname) {
sortData[sortname] = oldSort[sortname];
}
}
viewData.qpage = 0;
coreFilter.updateTableFromFilter();
}
if (paginationLink) {
e.preventDefault();
const paginationURL = new URL(paginationLink.getAttribute("href"));
const qpage = paginationURL.searchParams.get('qpage');
if (paginationURL.search !== null) {
viewData.qpage = qpage;
coreFilter.updateTableFromFilter();
}
}
if (clearLink) {
cleanUrlParams();
}
});
// Run apply filter at page load.
pagevars = JSON.parse(pagevars);
let initialFilters;
let jointype = null;
if (pagevars.filter) {
// Load initial filter based on page vars.
initialFilters = pagevars.filter;
if (pagevars.jointype) {
jointype = pagevars.jointype;
}
}
if (Object.entries(initialFilters).length !== 0) {
// Remove the default empty filter row.
const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);
if (emptyFilterRow) {
emptyFilterRow.remove();
}
// Add filters.
let rowcount = 0;
for (const urlFilter in initialFilters) {
if (urlFilter === 'jointype') {
jointype = initialFilters[urlFilter];
continue;
}
// Add each filter row.
rowcount += 1;
const filterdata = {
filtertype: urlFilter,
values: initialFilters[urlFilter].values,
jointype: initialFilters[urlFilter].jointype,
filteroptions: initialFilters[urlFilter].filteroptions,
rownum: rowcount
};
coreFilter.addFilterRow(filterdata);
}
coreFilter.filterSet.dataset.filterverb = jointype;
// Since we must filter by category, it does not make sense to allow the top-level "match any" or "match none" conditions,
// as this would exclude the category. Remove those options and disable the select.
const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);
join.querySelectorAll(`option:not([value="${jointype}"])`).forEach((option) => option.remove());
join.disabled = true;
}
};
+104
View File
@@ -0,0 +1,104 @@
// 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 required by the question engine.
*
* @module core_question/question_engine
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as scrollManager from 'core/scroll_manager';
import * as formSubmit from 'core_form/submit';
/**
* Initialise a question submit button. This saves the scroll position and
* sets the fragment on the form submit URL so the page reloads in the right place.
*
* @param {string} button the id of the button in the HTML.
*/
export const initSubmitButton = button => {
formSubmit.init(button);
scrollManager.watchScrollButtonSaves();
};
/**
* Initialise a form that contains questions printed using print_question.
* This has the effect of:
* 1. Turning off browser autocomlete.
* 2. Stopping enter from submitting the form (or toggling the next flag) unless
* keyboard focus is on the submit button or the flag.
* 3. Removes any '.questionflagsavebutton's, since we have JavaScript to toggle
* the flags using ajax.
* 4. Scroll to the position indicated by scrollpos= in the URL, if it is there.
* 5. Prevent the user from repeatedly submitting the form.
*
* @param {string} formSelector Selector to identify the form.
*/
export const initForm = (formSelector) => {
const form = document.querySelector(formSelector);
form.setAttribute('autocomplete', 'off');
form.addEventListener('submit', preventRepeatSubmission);
form.addEventListener('key', (event) => {
if (event.keyCode !== 13) {
return;
}
if (event.target.matches('a')) {
return;
}
if (event.target.matches('input[type="submit"]')) {
return;
}
if (event.target.matches('input[type=img]')) {
return;
}
if (event.target.matches('textarea') || event.target.matches('[contenteditable=true]')) {
return;
}
event.preventDefault();
});
const questionFlagSaveButtons = form.querySelectorAll('.questionflagsavebutton');
[...questionFlagSaveButtons].forEach((node) => node.remove());
// Note: The scrollToSavedPosition function tries to wait until the content has loaded before firing.
scrollManager.scrollToSavedPosition();
};
/**
* Event handler to stop a question form being submitted more than once.
*
* @param {object} event the form submit event.
*/
export const preventRepeatSubmission = (event) => {
const form = event.target.closest('form');
if (form.dataset.formSubmitted === '1') {
event.preventDefault();
return;
}
setTimeout(() => {
[...form.querySelectorAll('input[type=submit]')].forEach((input) => input.setAttribute('disabled', true));
});
form.dataset.formSubmitted = '1';
};
+65
View File
@@ -0,0 +1,65 @@
// 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/>.
/**
* Question bank UI refresh utility
*
* @module core_question/refresh_ui
* @copyright 2023 Catalyst IT Europe Ltd.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Fragment from 'core/fragment';
import Templates from 'core/templates';
export default {
/**
* Reload the question bank UI, retaining the current filters and sort data.
*
* @param {Element} uiRoot The root element of the UI to be refreshed. Must contain "component", "callback" and "contextid" in
* its data attributes, to be passed to the Fragment API.
* @param {URL} returnUrl The url of the current page, containing filter and sort parameters.
* @return {Promise} Resolved when the refresh is complete.
*/
refresh: (uiRoot, returnUrl) => {
return new Promise((resolve, reject) => {
const fragmentData = uiRoot.dataset;
const viewData = {};
const sortData = {};
if (returnUrl) {
returnUrl.searchParams.forEach((value, key) => {
// Match keys like 'sortdata[fieldname]' and convert them to an array,
// because the fragment API doesn't like non-alphanum argument keys.
const sortItem = key.match(/sortdata\[([^\]]+)\]/);
if (sortItem) {
// The item returned by sortItem.pop() is the contents of the matching group, the field name.
sortData[sortItem.pop()] = value;
} else {
viewData[key] = value;
}
});
}
viewData.sortdata = JSON.stringify(sortData);
// We have to use then() there, as loadFragment doesn't appear to work with await.
Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid, viewData)
.then((html, js) => {
Templates.replaceNode(uiRoot, html, js);
resolve();
return html;
})
.catch(reject);
});
}
};