diff --git a/lib/amd/build/scroll_manager.min.js b/lib/amd/build/scroll_manager.min.js new file mode 100644 index 00000000000..c551ee0806b --- /dev/null +++ b/lib/amd/build/scroll_manager.min.js @@ -0,0 +1,23 @@ +define("core/scroll_manager",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.watchScrollButtonSaves=_exports.scrollToSavedPosition=_exports.saveScrollPositionToForm=_exports.saveScrollPos=_exports.initLinksScrollPos=void 0; +/** + * Scroll manager is a class that help with saving the scroll positing when you + * click on an action icon, and then when the page is reloaded after processing + * the action, it scrolls you to exactly where you were. This is much nicer for + * the user. + * + * To use this in your code, you need to ensure that: + * 1. The button that triggers the action has to have a click event handler that + * calls saveScrollPos() + * 2. After doing the processing, the redirect() function will add 'mdlscrollto' + * parameter into the redirect url automatically. + * 3. Finally, on the page that is reloaded (which should be the same as the one + * the user started on) you need to call scrollToSavedPosition() + * on page load. + * + * @module core/scroll_manager + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +let scrollingElement=null;const getScrollingElement=()=>{if(null===scrollingElement){const page=document.getElementById("page");scrollingElement=(element=>{const hasScrollableContent=element.scrollHeight>element.clientHeight,isOverflowHidden=-1!==window.getComputedStyle(element).overflowY.indexOf("hidden");return hasScrollableContent&&!isOverflowHidden})(page)?page:document.scrollingElement}return scrollingElement},getScrollPos=()=>getScrollingElement().scrollTop;_exports.saveScrollPos=elementId=>{const form=document.getElementById(elementId).closest("form");form&&saveScrollPositionToForm(form)};_exports.watchScrollButtonSaves=()=>{document.addEventListener("click",(e=>{const button=e.target.closest('[data-savescrollposition="true"]');button&&saveScrollPositionToForm(button.form)}))};const saveScrollPositionToForm=form=>{(form=>{const element=form.querySelector("input[name=mdlscrollto]");if(element)return element;const scrollPos=document.createElement("input");return scrollPos.type="hidden",scrollPos.name="mdlscrollto",form.appendChild(scrollPos),scrollPos})(form).value=getScrollPos()};_exports.saveScrollPositionToForm=saveScrollPositionToForm;_exports.initLinksScrollPos=()=>{document.addEventListener("click",(e=>{if(!e.target.closest("a[data-save-scroll=true]"))return;e.preventDefault();const url=new URL(e.target.href);url.searchParams.set("mdlscrollto",getScrollPos()),window.location=url}))};_exports.scrollToSavedPosition=()=>{const url=new URL(window.location.href);if(!url.searchParams.has("mdlscrollto"))return;const scrollPosition=url.searchParams.get("mdlscrollto"),scrollingElement=getScrollingElement();scrollingElement.scrollTo(0,scrollPosition),document.addEventListener("DOMContentLoaded",(()=>{scrollingElement.scrollTo(0,scrollPosition)}))}})); + +//# sourceMappingURL=scroll_manager.min.js.map \ No newline at end of file diff --git a/lib/amd/build/scroll_manager.min.js.map b/lib/amd/build/scroll_manager.min.js.map new file mode 100644 index 00000000000..d3db2066395 --- /dev/null +++ b/lib/amd/build/scroll_manager.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scroll_manager.min.js","sources":["../src/scroll_manager.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 .\n\n/**\n * Scroll manager is a class that help with saving the scroll positing when you\n * click on an action icon, and then when the page is reloaded after processing\n * the action, it scrolls you to exactly where you were. This is much nicer for\n * the user.\n *\n * To use this in your code, you need to ensure that:\n * 1. The button that triggers the action has to have a click event handler that\n * calls saveScrollPos()\n * 2. After doing the processing, the redirect() function will add 'mdlscrollto'\n * parameter into the redirect url automatically.\n * 3. Finally, on the page that is reloaded (which should be the same as the one\n * the user started on) you need to call scrollToSavedPosition()\n * on page load.\n *\n * @module core/scroll_manager\n * @copyright 2021 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/** @property {HTMLElement} scrollingElement the current scrolling element. */\nlet scrollingElement = null;\n\n/**\n * Is the element scrollable?\n *\n * @param {HTMLElement} element Element.\n * @returns {boolean}\n */\nconst isScrollable = (element) => {\n // Check if the element has scrollable content.\n const hasScrollableContent = element.scrollHeight > element.clientHeight;\n\n // If 'overflow-y' is set to hidden, the scroll bar is't show.\n const elementOverflow = window.getComputedStyle(element).overflowY;\n const isOverflowHidden = elementOverflow.indexOf('hidden') !== -1;\n\n return hasScrollableContent && !isOverflowHidden;\n};\n\n/**\n * Get the scrolling element.\n *\n * @returns {HTMLElement}\n */\nconst getScrollingElement = () => {\n if (scrollingElement === null) {\n const page = document.getElementById('page');\n if (isScrollable(page)) {\n scrollingElement = page;\n } else {\n scrollingElement = document.scrollingElement;\n }\n }\n\n return scrollingElement;\n};\n\n/**\n * Get current scroll position.\n *\n * @returns {Number} Scroll position.\n */\nconst getScrollPos = () => {\n const scrollingElement = getScrollingElement();\n\n return scrollingElement.scrollTop;\n};\n\n/**\n * Get the scroll position for this form.\n *\n * @param {HTMLFormElement} form\n * @returns {HTMLInputElement}\n */\nconst getScrollPositionElement = (form) => {\n const element = form.querySelector('input[name=mdlscrollto]');\n if (element) {\n return element;\n }\n\n const scrollPos = document.createElement('input');\n scrollPos.type = 'hidden';\n scrollPos.name = 'mdlscrollto';\n form.appendChild(scrollPos);\n\n return scrollPos;\n};\n\n/**\n * In the form that contains the element, set the value of the form field with\n * name mdlscrollto to the current scroll position. If there is no element with\n * that name, it creates a hidden form field with that name within the form.\n *\n * @param {string} elementId The element in the form.\n */\nexport const saveScrollPos = (elementId) => {\n const element = document.getElementById(elementId);\n const form = element.closest('form');\n if (!form) {\n return;\n }\n\n saveScrollPositionToForm(form);\n};\n\n/**\n * Init event handlers for all links with data-savescrollposition=true.\n * Set the value to the closest form.\n */\nexport const watchScrollButtonSaves = () => {\n document.addEventListener('click', (e) => {\n const button = e.target.closest('[data-savescrollposition=\"true\"]');\n if (button) {\n saveScrollPositionToForm(button.form);\n }\n });\n};\n\n/**\n * Save the position to form.\n *\n * @param {Object} form The form is saved scroll position.\n */\nexport const saveScrollPositionToForm = (form) => {\n getScrollPositionElement(form).value = getScrollPos();\n};\n\n/**\n * Init event handlers for all links with data-save-scroll=true.\n * Handle to add mdlscrollto parameter to link using js when we click on the link.\n *\n */\nexport const initLinksScrollPos = () => {\n document.addEventListener('click', (e) => {\n const link = e.target.closest('a[data-save-scroll=true]');\n if (!link) {\n return;\n }\n\n e.preventDefault();\n const url = new URL(e.target.href);\n url.searchParams.set('mdlscrollto', getScrollPos());\n window.location = url;\n });\n};\n\n/**\n * If there is a parameter like mdlscrollto=123 in the URL, scroll to that saved position.\n */\nexport const scrollToSavedPosition = () => {\n const url = new URL(window.location.href);\n if (!url.searchParams.has('mdlscrollto')) {\n return;\n }\n\n const scrollPosition = url.searchParams.get('mdlscrollto');\n\n // Event onDOMReady is the effective one here. I am leaving the immediate call to\n // window.scrollTo in case it reduces flicker.\n const scrollingElement = getScrollingElement();\n scrollingElement.scrollTo(0, scrollPosition);\n document.addEventListener('DOMContentLoaded', () => {\n scrollingElement.scrollTo(0, scrollPosition);\n });\n};\n"],"names":["scrollingElement","getScrollingElement","page","document","getElementById","element","hasScrollableContent","scrollHeight","clientHeight","isOverflowHidden","window","getComputedStyle","overflowY","indexOf","isScrollable","getScrollPos","scrollTop","elementId","form","closest","saveScrollPositionToForm","addEventListener","e","button","target","querySelector","scrollPos","createElement","type","name","appendChild","getScrollPositionElement","value","preventDefault","url","URL","href","searchParams","set","location","has","scrollPosition","get","scrollTo"],"mappings":";;;;;;;;;;;;;;;;;;;;IAoCIA,iBAAmB,WAwBjBC,oBAAsB,QACC,OAArBD,iBAA2B,OACrBE,KAAOC,SAASC,eAAe,QAEjCJ,iBApBUK,CAAAA,gBAEZC,qBAAuBD,QAAQE,aAAeF,QAAQG,aAItDC,kBAA0D,IADxCC,OAAOC,iBAAiBN,SAASO,UAChBC,QAAQ,iBAE1CP,uBAAyBG,kBAWxBK,CAAaZ,MACMA,KAEAC,SAASH,wBAI7BA,kBAQLe,aAAe,IACQd,sBAEDe,iCA8BEC,kBAEpBC,KADUf,SAASC,eAAea,WACnBE,QAAQ,QACxBD,MAILE,yBAAyBF,uCAOS,KAClCf,SAASkB,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEE,OAAOL,QAAQ,oCAC5BI,QACAH,yBAAyBG,OAAOL,gBAU/BE,yBAA4BF,OAjDPA,CAAAA,aACxBb,QAAUa,KAAKO,cAAc,8BAC/BpB,eACOA,cAGLqB,UAAYvB,SAASwB,cAAc,gBACzCD,UAAUE,KAAO,SACjBF,UAAUG,KAAO,cACjBX,KAAKY,YAAYJ,WAEVA,WAuCPK,CAAyBb,MAAMc,MAAQjB,uGAQT,KAC9BZ,SAASkB,iBAAiB,SAAUC,QACnBA,EAAEE,OAAOL,QAAQ,mCAK9BG,EAAEW,uBACIC,IAAM,IAAIC,IAAIb,EAAEE,OAAOY,MAC7BF,IAAIG,aAAaC,IAAI,cAAevB,gBACpCL,OAAO6B,SAAWL,uCAOW,WAC3BA,IAAM,IAAIC,IAAIzB,OAAO6B,SAASH,UAC/BF,IAAIG,aAAaG,IAAI,4BAIpBC,eAAiBP,IAAIG,aAAaK,IAAI,eAItC1C,iBAAmBC,sBACzBD,iBAAiB2C,SAAS,EAAGF,gBAC7BtC,SAASkB,iBAAiB,oBAAoB,KAC1CrB,iBAAiB2C,SAAS,EAAGF"} \ No newline at end of file diff --git a/lib/amd/src/scroll_manager.js b/lib/amd/src/scroll_manager.js new file mode 100644 index 00000000000..1ede365c934 --- /dev/null +++ b/lib/amd/src/scroll_manager.js @@ -0,0 +1,181 @@ +// 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 . + +/** + * Scroll manager is a class that help with saving the scroll positing when you + * click on an action icon, and then when the page is reloaded after processing + * the action, it scrolls you to exactly where you were. This is much nicer for + * the user. + * + * To use this in your code, you need to ensure that: + * 1. The button that triggers the action has to have a click event handler that + * calls saveScrollPos() + * 2. After doing the processing, the redirect() function will add 'mdlscrollto' + * parameter into the redirect url automatically. + * 3. Finally, on the page that is reloaded (which should be the same as the one + * the user started on) you need to call scrollToSavedPosition() + * on page load. + * + * @module core/scroll_manager + * @copyright 2021 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** @property {HTMLElement} scrollingElement the current scrolling element. */ +let scrollingElement = null; + +/** + * Is the element scrollable? + * + * @param {HTMLElement} element Element. + * @returns {boolean} + */ +const isScrollable = (element) => { + // Check if the element has scrollable content. + const hasScrollableContent = element.scrollHeight > element.clientHeight; + + // If 'overflow-y' is set to hidden, the scroll bar is't show. + const elementOverflow = window.getComputedStyle(element).overflowY; + const isOverflowHidden = elementOverflow.indexOf('hidden') !== -1; + + return hasScrollableContent && !isOverflowHidden; +}; + +/** + * Get the scrolling element. + * + * @returns {HTMLElement} + */ +const getScrollingElement = () => { + if (scrollingElement === null) { + const page = document.getElementById('page'); + if (isScrollable(page)) { + scrollingElement = page; + } else { + scrollingElement = document.scrollingElement; + } + } + + return scrollingElement; +}; + +/** + * Get current scroll position. + * + * @returns {Number} Scroll position. + */ +const getScrollPos = () => { + const scrollingElement = getScrollingElement(); + + return scrollingElement.scrollTop; +}; + +/** + * Get the scroll position for this form. + * + * @param {HTMLFormElement} form + * @returns {HTMLInputElement} + */ +const getScrollPositionElement = (form) => { + const element = form.querySelector('input[name=mdlscrollto]'); + if (element) { + return element; + } + + const scrollPos = document.createElement('input'); + scrollPos.type = 'hidden'; + scrollPos.name = 'mdlscrollto'; + form.appendChild(scrollPos); + + return scrollPos; +}; + +/** + * In the form that contains the element, set the value of the form field with + * name mdlscrollto to the current scroll position. If there is no element with + * that name, it creates a hidden form field with that name within the form. + * + * @param {string} elementId The element in the form. + */ +export const saveScrollPos = (elementId) => { + const element = document.getElementById(elementId); + const form = element.closest('form'); + if (!form) { + return; + } + + saveScrollPositionToForm(form); +}; + +/** + * Init event handlers for all links with data-savescrollposition=true. + * Set the value to the closest form. + */ +export const watchScrollButtonSaves = () => { + document.addEventListener('click', (e) => { + const button = e.target.closest('[data-savescrollposition="true"]'); + if (button) { + saveScrollPositionToForm(button.form); + } + }); +}; + +/** + * Save the position to form. + * + * @param {Object} form The form is saved scroll position. + */ +export const saveScrollPositionToForm = (form) => { + getScrollPositionElement(form).value = getScrollPos(); +}; + +/** + * Init event handlers for all links with data-save-scroll=true. + * Handle to add mdlscrollto parameter to link using js when we click on the link. + * + */ +export const initLinksScrollPos = () => { + document.addEventListener('click', (e) => { + const link = e.target.closest('a[data-save-scroll=true]'); + if (!link) { + return; + } + + e.preventDefault(); + const url = new URL(e.target.href); + url.searchParams.set('mdlscrollto', getScrollPos()); + window.location = url; + }); +}; + +/** + * If there is a parameter like mdlscrollto=123 in the URL, scroll to that saved position. + */ +export const scrollToSavedPosition = () => { + const url = new URL(window.location.href); + if (!url.searchParams.has('mdlscrollto')) { + return; + } + + const scrollPosition = url.searchParams.get('mdlscrollto'); + + // Event onDOMReady is the effective one here. I am leaving the immediate call to + // window.scrollTo in case it reduces flicker. + const scrollingElement = getScrollingElement(); + scrollingElement.scrollTo(0, scrollPosition); + document.addEventListener('DOMContentLoaded', () => { + scrollingElement.scrollTo(0, scrollPosition); + }); +}; diff --git a/mod/quiz/addrandom.php b/mod/quiz/addrandom.php index 8d57c0d6149..d47d14a1892 100644 --- a/mod/quiz/addrandom.php +++ b/mod/quiz/addrandom.php @@ -39,7 +39,7 @@ list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) = $returnurl = optional_param('returnurl', '', PARAM_LOCALURL); $addonpage = optional_param('addonpage', 0, PARAM_INT); $category = optional_param('category', 0, PARAM_INT); -$scrollpos = optional_param('scrollpos', 0, PARAM_INT); +$mdlscrollto = optional_param('mdlscrollto', 0, PARAM_INT); // Get the course object and related bits. if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { @@ -59,8 +59,8 @@ if ($returnurl) { } else { $returnurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cmid)); } -if ($scrollpos) { - $returnurl->param('scrollpos', $scrollpos); +if ($mdlscrollto) { + $returnurl->param('mdlscrollto', $mdlscrollto); } $defaultcategoryobj = question_make_default_categories($contexts->all()); diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php index d5a06c8187d..25abdedfb99 100644 --- a/mod/quiz/classes/output/renderer.php +++ b/mod/quiz/classes/output/renderer.php @@ -563,8 +563,8 @@ class renderer extends plugin_renderer_base { 'value' => '0', 'id' => 'timeup']); $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]); - $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'scrollpos', - 'value' => '', 'id' => 'scrollpos']); + $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'mdlscrollto', + 'value' => '', 'id' => 'mdlscrollto']); // Add a hidden field with questionids. Do this at the end of the form, so // if you navigate before the form has finished loading, it does not wipe all @@ -622,9 +622,14 @@ class renderer extends plugin_renderer_base { public function redo_question_button($slot, $disabled) { $attributes = ['type' => 'submit', 'name' => 'redoslot' . $slot, 'value' => get_string('redoquestion', 'quiz'), - 'class' => 'mod_quiz-redo_question_button btn btn-secondary']; + 'class' => 'mod_quiz-redo_question_button btn btn-secondary', + 'id' => 'redoslot' . $slot . '-submit', + 'data-savescrollposition' => 'true', + ]; if ($disabled) { $attributes['disabled'] = 'disabled'; + } else { + $this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton', [$attributes['id']]); } return html_writer::div(html_writer::empty_tag('input', $attributes)); } diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php index 667289850c3..07d240aacbf 100644 --- a/mod/quiz/edit.php +++ b/mod/quiz/edit.php @@ -46,9 +46,7 @@ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); require_once($CFG->dirroot . '/question/editlib.php'); -// These params are only passed from page request to request while we stay on -// this page otherwise they would go in question_edit_setup. -$scrollpos = optional_param('scrollpos', '', PARAM_INT); +$mdlscrollto = optional_param('mdlscrollto', '', PARAM_INT); list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) = question_edit_setup('editq', '/mod/quiz/edit.php', true); @@ -81,9 +79,6 @@ foreach ($params as $key => $value) { } $afteractionurl = new moodle_url($thispageurl); -if ($scrollpos) { - $afteractionurl->param('scrollpos', $scrollpos); -} if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) { // Re-paginate the quiz. @@ -94,6 +89,10 @@ if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) { redirect($afteractionurl); } +if ($mdlscrollto) { + $afteractionurl->param('mdlscrollto', $mdlscrollto); +} + if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sesskey()) { // Add a single question to the current quiz. $structure->check_can_be_edited(); diff --git a/mod/quiz/module.js b/mod/quiz/module.js index b49d3dcf402..db7d3eda872 100644 --- a/mod/quiz/module.js +++ b/mod/quiz/module.js @@ -26,7 +26,9 @@ M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.init_attempt_form = function(Y) { - M.core_question_engine.init_form(Y, '#responseform'); + require(['core_question/question_engine'], function(qEngine) { + qEngine.initForm('#responseform'); + }); Y.on('submit', M.mod_quiz.timer.stop, '#responseform'); require(['core_form/changechecker'], function(FormChangeChecker) { FormChangeChecker.watchFormById('responseform'); @@ -34,7 +36,9 @@ M.mod_quiz.init_attempt_form = function(Y) { }; M.mod_quiz.init_review_form = function(Y) { - M.core_question_engine.init_form(Y, '.questionflagsaveform'); + require(['core_question/question_engine'], function(qEngine) { + qEngine.initForm('.questionflagsaveform'); + }); Y.on('submit', function(e) { e.halt(); }, '.questionflagsaveform'); }; diff --git a/mod/quiz/processattempt.php b/mod/quiz/processattempt.php index 4a871af17ce..bb2c2d4d933 100644 --- a/mod/quiz/processattempt.php +++ b/mod/quiz/processattempt.php @@ -45,7 +45,7 @@ $previous = optional_param('previous', false, PARAM_BOOL); $next = optional_param('next', false, PARAM_BOOL); $finishattempt = optional_param('finishattempt', false, PARAM_BOOL); $timeup = optional_param('timeup', 0, PARAM_BOOL); // True if form was submitted by timer. -$scrollpos = optional_param('scrollpos', '', PARAM_RAW); +$mdlscrollto = optional_param('mdlscrollto', '', PARAM_RAW); $cmid = optional_param('cmid', null, PARAM_INT); $attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid); @@ -62,8 +62,8 @@ if ($page == -1) { $nexturl = $attemptobj->summary_url(); } else { $nexturl = $attemptobj->attempt_url(null, $page); - if ($scrollpos !== '') { - $nexturl->param('scrollpos', $scrollpos); + if ($mdlscrollto !== '') { + $nexturl->param('mdlscrollto', $mdlscrollto); } } diff --git a/question/amd/build/question_engine.min.js b/question/amd/build/question_engine.min.js new file mode 100644 index 00000000000..95432d57f30 --- /dev/null +++ b/question/amd/build/question_engine.min.js @@ -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 \ No newline at end of file diff --git a/question/amd/build/question_engine.min.js.map b/question/amd/build/question_engine.min.js.map new file mode 100644 index 00000000000..4394c29d910 --- /dev/null +++ b/question/amd/build/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 .\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"} \ No newline at end of file diff --git a/question/amd/src/question_engine.js b/question/amd/src/question_engine.js new file mode 100644 index 00000000000..2e1c584610c --- /dev/null +++ b/question/amd/src/question_engine.js @@ -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 . + +/** + * 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'; +}; diff --git a/question/bank/editquestion/question.php b/question/bank/editquestion/question.php index 72d9d8b27aa..c4b7479dfc5 100644 --- a/question/bank/editquestion/question.php +++ b/question/bank/editquestion/question.php @@ -37,7 +37,7 @@ $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA); $originalreturnurl = optional_param('returnurl', 0, PARAM_LOCALURL); $appendqnumstring = optional_param('appendqnumstring', '', PARAM_ALPHA); $inpopup = optional_param('inpopup', 0, PARAM_BOOL); -$scrollpos = optional_param('scrollpos', 0, PARAM_INT); +$mdlscrollto = optional_param('mdlscrollto', 0, PARAM_INT); \core_question\local\bank\helper::require_plugin_enabled('qbank_editquestion'); @@ -72,8 +72,8 @@ if ($appendqnumstring !== '') { if ($inpopup !== 0) { $url->param('inpopup', $inpopup); } -if ($scrollpos) { - $url->param('scrollpos', $scrollpos); +if ($mdlscrollto) { + $url->param('mdlscrollto', $mdlscrollto); } $PAGE->set_url($url); @@ -92,8 +92,8 @@ if ($originalreturnurl) { } else { $returnurl = $questionbankurl; } -if ($scrollpos) { - $returnurl->param('scrollpos', $scrollpos); +if ($mdlscrollto) { + $returnurl->param('mdlscrollto', $mdlscrollto); } if ($cmid) { @@ -204,7 +204,7 @@ if ($wizardnow !== '') { } $toform = fullclone($question); // Send the question object and a few more parameters to the form. $toform->category = "{$category->id},{$category->contextid}"; -$toform->scrollpos = $scrollpos; +$toform->mdlscrollto = $mdlscrollto; if ($formeditable && $id) { $toform->categorymoveto = $toform->category; } @@ -332,10 +332,11 @@ if ($mform->is_cancelled()) { } } else { - $nexturlparams = array( + $nexturlparams = [ 'returnurl' => $originalreturnurl, 'appendqnumstring' => $appendqnumstring, - 'scrollpos' => $scrollpos); + 'mdlscrollto' => $mdlscrollto, + ]; if (isset($fromform->nextpageparam) && is_array($fromform->nextpageparam)) { // Useful for passing data to the next page which is not saved in the database. $nexturlparams += $fromform->nextpageparam; diff --git a/question/bank/previewquestion/amd/build/preview.min.js b/question/bank/previewquestion/amd/build/preview.min.js index 05a84333044..663f7ba0810 100644 --- a/question/bank/previewquestion/amd/build/preview.min.js +++ b/question/bank/previewquestion/amd/build/preview.min.js @@ -1,11 +1,3 @@ -define("qbank_previewquestion/preview",["exports","jquery"],(function(_exports,_jquery){var obj; -/** - * Javascript for preview. - * - * @module qbank_preview/preview - * @copyright 2021 Catalyst IT Australia Pty Ltd - * @author Safat Shahin - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=(redirect,url)=>{if(!redirect){document.getElementById("close-previewquestion-page").onclick=()=>{null===window.opener?location.href=url:window.close()}}setupQuestionForm("responseform")};const setupQuestionForm=formElement=>{let form=document.getElementById(formElement);form&&(autocompleteOff(form),preventRepeatSubmission(form),removeClass(".questionflagsavebutton",form),scrollToSavedPos(form))},autocompleteOff=form=>{form.setAttribute("autocomplete","off")},preventRepeatSubmission=form=>{form.addEventListener("submit",(function(){return(0,_jquery.default)(this).submit((function(){return!1})),!0}))},removeClass=(classname,form)=>{form.querySelectorAll(classname).forEach((e=>e.remove()))},scrollToSavedPos=form=>{let matches=window.location.href.match(/^.*[?&]scrollpos=(\d*)(?:&|$|#).*$/,"$1");matches&&(window.scrollTo(0,matches[1]),form.addEventListener("DOMContentLoaded",(()=>{window.scrollTo(0,matches[1])})))}})); +define("qbank_previewquestion/preview",["exports","core_question/question_engine"],(function(_exports,_question_engine){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=(redirect,url)=>{if(!redirect){document.getElementById("close-previewquestion-page").addEventListener("click",(e=>{e.preventDefault(),null===window.opener?location.href=url:window.close()}))}(0,_question_engine.initForm)("#responseform")}})); //# sourceMappingURL=preview.min.js.map \ No newline at end of file diff --git a/question/bank/previewquestion/amd/build/preview.min.js.map b/question/bank/previewquestion/amd/build/preview.min.js.map index c5ee224b374..40c460b3e0e 100644 --- a/question/bank/previewquestion/amd/build/preview.min.js.map +++ b/question/bank/previewquestion/amd/build/preview.min.js.map @@ -1 +1 @@ -{"version":3,"file":"preview.min.js","sources":["../src/preview.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 .\n\n/**\n * Javascript for preview.\n *\n * @module qbank_preview/preview\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\n/**\n * Set up the actions.\n *\n * @method init\n * @param {bool} redirect Redirect.\n * @param {string} url url to redirect.\n */\nexport const init = (redirect, url) => {\n if (!redirect) {\n let closeButton = document.getElementById('close-previewquestion-page');\n closeButton.onclick = () => {\n if (window.opener === null) {\n location.href = url;\n } else {\n window.close();\n }\n };\n }\n // Set up the form to be displayed.\n setupQuestionForm('responseform');\n};\n\n/**\n * Set up the form element to be displayed.\n *\n * @method setupQuestionForm\n * @param {string} formElement The form element.\n */\nconst setupQuestionForm = (formElement) => {\n let form = document.getElementById(formElement);\n if (form) {\n // Turning off browser autocomplete.\n autocompleteOff(form);\n // Stop a question form being submitted more than once.\n preventRepeatSubmission(form);\n // Removes any '.questionflagsavebutton's, since we have JavaScript to toggle.\n removeClass('.questionflagsavebutton', form);\n // Scroll to the position indicated by scrollpos= in the URL, if it is there.\n scrollToSavedPos(form);\n }\n};\n\n/**\n * Set the autocomplete off.\n *\n * @method autocompleteOff\n * @param {object} form The form element.\n */\nconst autocompleteOff = (form) => {\n form.setAttribute(\"autocomplete\", \"off\");\n};\n\n/**\n * Event handler to stop a question form being submitted more than once.\n *\n * @method preventRepeatSubmission\n * @param {object} form The form element.\n */\nconst preventRepeatSubmission = (form) => {\n form.addEventListener(\"submit\", function() {\n $(this).submit(function() {\n return false;\n });\n return true;\n });\n};\n\n/**\n * Removes a class inside an element.\n *\n * @method removeClass\n * @param {string} classname Class name.\n * @param {object} form The form element.\n */\nconst removeClass = (classname, form) => {\n form.querySelectorAll(classname).forEach(e => e.remove());\n};\n\n/**\n * If there is a parameter like scrollpos=123 in the URL, scroll to that saved position.\n * (Note: Moodle 4.0 and above do NOT support Internet Explorer 11 and below.)\n *\n * @method scrollToSavedPos\n * @param {object} form The form element.\n */\nconst scrollToSavedPos = (form) => {\n let matches = window.location.href.match(/^.*[?&]scrollpos=(\\d*)(?:&|$|#).*$/, '$1');\n if (matches) {\n // DOMContentLoaded is the effective one here. I am leaving the immediate call to\n // window.scrollTo in case it reduces flicker.\n window.scrollTo(0, matches[1]);\n form.addEventListener(\"DOMContentLoaded\", () => {\n window.scrollTo(0, matches[1]);\n });\n }\n};\n"],"names":["redirect","url","document","getElementById","onclick","window","opener","location","href","close","setupQuestionForm","formElement","form","autocompleteOff","preventRepeatSubmission","removeClass","scrollToSavedPos","setAttribute","addEventListener","this","submit","classname","querySelectorAll","forEach","e","remove","matches","match","scrollTo"],"mappings":";;;;;;;;wJAiCoB,CAACA,SAAUC,WACtBD,SAAU,CACOE,SAASC,eAAe,8BAC9BC,QAAU,KACI,OAAlBC,OAAOC,OACPC,SAASC,KAAOP,IAEhBI,OAAOI,SAKnBC,kBAAkB,uBAShBA,kBAAqBC,kBACnBC,KAAOV,SAASC,eAAeQ,aAC/BC,OAEAC,gBAAgBD,MAEhBE,wBAAwBF,MAExBG,YAAY,0BAA2BH,MAEvCI,iBAAiBJ,QAUnBC,gBAAmBD,OACrBA,KAAKK,aAAa,eAAgB,QAShCH,wBAA2BF,OAC7BA,KAAKM,iBAAiB,UAAU,qCAC1BC,MAAMC,QAAO,kBACJ,MAEJ,MAWTL,YAAc,CAACM,UAAWT,QAC5BA,KAAKU,iBAAiBD,WAAWE,SAAQC,GAAKA,EAAEC,YAU9CT,iBAAoBJ,WAClBc,QAAUrB,OAAOE,SAASC,KAAKmB,MAAM,qCAAsC,MAC3ED,UAGArB,OAAOuB,SAAS,EAAGF,QAAQ,IAC3Bd,KAAKM,iBAAiB,oBAAoB,KACtCb,OAAOuB,SAAS,EAAGF,QAAQ"} \ No newline at end of file +{"version":3,"file":"preview.min.js","sources":["../src/preview.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 .\n\n/**\n * Javascript for preview.\n *\n * @module qbank_preview/preview\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {initForm as initQuestionEngineForm} from 'core_question/question_engine';\n\n/**\n * Set up the actions.\n *\n * @method init\n * @param {bool} redirect Redirect.\n * @param {string} url url to redirect.\n */\nexport const init = (redirect, url) => {\n if (!redirect) {\n const closeButton = document.getElementById('close-previewquestion-page');\n closeButton.addEventListener('click', (e) => {\n e.preventDefault();\n if (window.opener === null) {\n location.href = url;\n } else {\n window.close();\n }\n });\n }\n // Set up the form to be displayed.\n initQuestionEngineForm('#responseform');\n};\n"],"names":["redirect","url","document","getElementById","addEventListener","e","preventDefault","window","opener","location","href","close"],"mappings":"mNAiCoB,CAACA,SAAUC,WACtBD,SAAU,CACSE,SAASC,eAAe,8BAChCC,iBAAiB,SAAUC,IACnCA,EAAEC,iBACoB,OAAlBC,OAAOC,OACPC,SAASC,KAAOT,IAEhBM,OAAOI,yCAKI"} \ No newline at end of file diff --git a/question/bank/previewquestion/amd/src/preview.js b/question/bank/previewquestion/amd/src/preview.js index 446a4ee8d74..d1ef49c4cdd 100644 --- a/question/bank/previewquestion/amd/src/preview.js +++ b/question/bank/previewquestion/amd/src/preview.js @@ -22,7 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import $ from 'jquery'; +import {initForm as initQuestionEngineForm} from 'core_question/question_engine'; /** * Set up the actions. @@ -33,90 +33,16 @@ import $ from 'jquery'; */ export const init = (redirect, url) => { if (!redirect) { - let closeButton = document.getElementById('close-previewquestion-page'); - closeButton.onclick = () => { + const closeButton = document.getElementById('close-previewquestion-page'); + closeButton.addEventListener('click', (e) => { + e.preventDefault(); if (window.opener === null) { location.href = url; } else { window.close(); } - }; + }); } // Set up the form to be displayed. - setupQuestionForm('responseform'); -}; - -/** - * Set up the form element to be displayed. - * - * @method setupQuestionForm - * @param {string} formElement The form element. - */ -const setupQuestionForm = (formElement) => { - let form = document.getElementById(formElement); - if (form) { - // Turning off browser autocomplete. - autocompleteOff(form); - // Stop a question form being submitted more than once. - preventRepeatSubmission(form); - // Removes any '.questionflagsavebutton's, since we have JavaScript to toggle. - removeClass('.questionflagsavebutton', form); - // Scroll to the position indicated by scrollpos= in the URL, if it is there. - scrollToSavedPos(form); - } -}; - -/** - * Set the autocomplete off. - * - * @method autocompleteOff - * @param {object} form The form element. - */ -const autocompleteOff = (form) => { - form.setAttribute("autocomplete", "off"); -}; - -/** - * Event handler to stop a question form being submitted more than once. - * - * @method preventRepeatSubmission - * @param {object} form The form element. - */ -const preventRepeatSubmission = (form) => { - form.addEventListener("submit", function() { - $(this).submit(function() { - return false; - }); - return true; - }); -}; - -/** - * Removes a class inside an element. - * - * @method removeClass - * @param {string} classname Class name. - * @param {object} form The form element. - */ -const removeClass = (classname, form) => { - form.querySelectorAll(classname).forEach(e => e.remove()); -}; - -/** - * If there is a parameter like scrollpos=123 in the URL, scroll to that saved position. - * (Note: Moodle 4.0 and above do NOT support Internet Explorer 11 and below.) - * - * @method scrollToSavedPos - * @param {object} form The form element. - */ -const scrollToSavedPos = (form) => { - let matches = window.location.href.match(/^.*[?&]scrollpos=(\d*)(?:&|$|#).*$/, '$1'); - if (matches) { - // DOMContentLoaded is the effective one here. I am leaving the immediate call to - // window.scrollTo in case it reduces flicker. - window.scrollTo(0, matches[1]); - form.addEventListener("DOMContentLoaded", () => { - window.scrollTo(0, matches[1]); - }); - } + initQuestionEngineForm('#responseform'); }; diff --git a/question/bank/previewquestion/preview.php b/question/bank/previewquestion/preview.php index b89616fdb8c..a21585c5aad 100644 --- a/question/bank/previewquestion/preview.php +++ b/question/bank/previewquestion/preview.php @@ -190,9 +190,9 @@ if (data_submitted() && confirm_sesskey()) { question_engine::save_questions_usage_by_activity($quba); $transaction->allow_commit(); - $scrollpos = optional_param('scrollpos', '', PARAM_RAW); - if ($scrollpos !== '') { - $actionurl->param('scrollpos', (int) $scrollpos); + $mdlscrollto = optional_param('mdlscrollto', '', PARAM_RAW); + if ($mdlscrollto !== '') { + $actionurl->param('mdlscrollto', (int) $mdlscrollto); } redirect($actionurl); } diff --git a/question/behaviour/interactive/renderer.php b/question/behaviour/interactive/renderer.php index 804a9a36c7f..7a12294d559 100644 --- a/question/behaviour/interactive/renderer.php +++ b/question/behaviour/interactive/renderer.php @@ -52,21 +52,21 @@ class qbehaviour_interactive_renderer extends qbehaviour_renderer { return ''; } - $attributes = array( + $attributes = [ 'type' => 'submit', 'id' => $qa->get_behaviour_field_name('tryagain'), 'name' => $qa->get_behaviour_field_name('tryagain'), 'value' => get_string('tryagain', 'qbehaviour_interactive'), 'class' => 'submit btn btn-secondary', - ); + 'data-savescrollposition' => 'true', + ]; if ($options->readonly === qbehaviour_interactive::TRY_AGAIN_VISIBLE_READONLY) { // This means the question really was rendered with read-only option. $attributes['disabled'] = 'disabled'; } $output = html_writer::empty_tag('input', $attributes); if (empty($attributes['disabled'])) { - $this->page->requires->js_init_call('M.core_question_engine.init_submit_button', - array($attributes['id'])); + $this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton', [$attributes['id']]); } return $output; } diff --git a/question/behaviour/rendererbase.php b/question/behaviour/rendererbase.php index db5ff9ebbc6..983b7ce53c6 100644 --- a/question/behaviour/rendererbase.php +++ b/question/behaviour/rendererbase.php @@ -236,14 +236,14 @@ abstract class qbehaviour_renderer extends plugin_renderer_base { 'name' => $qa->get_behaviour_field_name('submit'), 'value' => get_string('check', 'question'), 'class' => 'submit btn btn-secondary', + 'data-savescrollposition' => 'true', ); if ($options->readonly) { $attributes['disabled'] = 'disabled'; } $output = html_writer::empty_tag('input', $attributes); if (!$options->readonly) { - $this->page->requires->js_init_call('M.core_question_engine.init_submit_button', - array($attributes['id'])); + $this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton', [$attributes['id']]); } return $output; } diff --git a/question/qengine.js b/question/qengine.js index 6a89679e730..f5f120f119e 100644 --- a/question/qengine.js +++ b/question/qengine.js @@ -19,105 +19,87 @@ * @package moodlecore * @subpackage questionengine * @copyright 2008 The Open University + * @deprecated since Moodle 4.0 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -/** - * Scroll manager is a class that help with saving the scroll positing when you - * click on an action icon, and then when the page is reloaded after processing - * the action, it scrolls you to exactly where you were. This is much nicer for - * the user. - * - * To use this in your code, you need to ensure that: - * 1. The button that triggers the action has to have a click event handler that - * calls M.core_scroll_manager.save_scroll_pos - * 2. The script that process the action has to grab the scrollpos parameter - * using $scrollpos = optional_param('scrollpos', 0, PARAM_INT); - * 3. After doing the processing, it must add ->param('scrollpos', $scrollpos) - * to the URL that it redirects to. - * 4. Finally, on the page that is reloaded (which should be the same as the one - * the user started on) you need to call M.core_scroll_manager.scroll_to_saved_pos - * on page load. - */ M.core_scroll_manager = M.core_scroll_manager || {}; -/** - * In the form that contains the element, set the value of the form field with - * name scrollpos to the current scroll position. If there is no element with - * that name, it creates a hidden form field wiht that name within the form. - * @param element the element in the form. Should be something that can be - * passed to Y.one. - */ -M.core_scroll_manager.save_scroll_pos = function(Y, element) { - if (typeof(element) == 'string') { - // Have to use getElementById here because element id can contain :. - element = Y.one(document.getElementById(element)); - } - var form = element.ancestor('form'); - if (!form) { - return; - } - var scrollpos = form.one('input[name=scrollpos]'); - if (!scrollpos) { - scrollpos = form.appendChild(form.create('')); - } - scrollpos.set('value', form.get('docScrollY')); -} +// TODO Remove the scroll manager and deprecation layer in 4.6 MDL-76685. +/* eslint-disable */ +var loadedPromise = new Promise(function(resolve) { + require(['core/scroll_manager'], function(ScrollManager) { + var transitionLayer = {}; -/** - * Event handler that can be used on a link. Assumes that the link already - * contains at least one URL parameter. - */ -M.core_scroll_manager.save_scroll_action = function(e) { - var link = e.target.ancestor('a[href]'); - if (!link) { - M.core_scroll_manager.save_scroll_pos({}, e.target); - return; - } - link.set('href', link.get('href') + '&scrollpos=' + link.get('docScrollY')); -} + var deprecatedNotice = function(functionName, newFunctionName) { + window.console.error( + "The " + functionName + " function has been deprecated. " + + "Please use core/scroll_manager::" + newFunctionName + "() instead" + ); + }; -/** - * If there is a parameter like scrollpos=123 in the URL, scroll to that saved position. - * @deprecated since Moodle 4.0 - * @see question\bank\qbank_previewquestion\amd\src - * @todo Final deprecation on Moodle 4.4 MDL-72438 - */ -M.core_scroll_manager.scroll_to_saved_pos = function(Y) { - Y.log("The scroll_to_saved_pos function has been deprecated. " + - "Please use scrollToSavedPos() in qbank_preview/preview.js instead.", 'moodle-core-notification', 'warn'); + transitionLayer.save_scroll_pos = function(Y, element) { + deprecatedNotice('save_scroll_pos', 'saveScrollPos'); + ScrollManager.saveScrollPos(element); + }; - var matches = window.location.href.match(/^.*[?&]scrollpos=(\d*)(?:&|$|#).*$/, '$1'); - if (matches) { - // onDOMReady is the effective one here. I am leaving the immediate call to - // window.scrollTo in case it reduces flicker. - window.scrollTo(0, matches[1]); - Y.on('domready', function() { window.scrollTo(0, matches[1]); }); + transitionLayer.scroll_to_saved_pos = function() { + deprecatedNotice('scroll_to_saved_pos', 'scrollToSavedPosition'); + ScrollManager.scrollToSavedPosition(); + }; - // And the following horror is necessary to make it work in IE 8. - // Note that the class ie8 on body is only there in Moodle 2.0 and OU Moodle. - if (Y.one('body').hasClass('ie')) { - M.core_scroll_manager.force_ie_to_scroll(Y, matches[1]) - } - } -} + M.core_scroll_manager = transitionLayer; -/** - * Beat IE into submission. - * @param targetpos the target scroll position. - */ -M.core_scroll_manager.force_ie_to_scroll = function(Y, targetpos) { - var hackcount = 25; - function do_scroll() { - window.scrollTo(0, targetpos); - hackcount -= 1; - if (hackcount > 0) { - setTimeout(do_scroll, 10); - } - } - Y.on('load', do_scroll, window); + resolve(transitionLayer); + }); +}); + +var callPromisedFunction = function(functionName, args) { + loadedPromise.then(function(transitionLayer) { + transitionLayer[functionName].apply(null, args); + }); +}; + +if (!M.core_scroll_manager.save_scroll_pos) { + // Note: This object is short lived. + // It only lives until the new scroll manager is loaded, at which point it is replaced. + + /** + * In the form that contains the element, set the value of the form field with + * name scrollpos to the current scroll position. If there is no element with + * that name, it creates a hidden form field with that name within the form. + * @deprecated since Moodle 4.0 + * @see core/scroll_manager + * @param element the element in the form. Should be something that can be + * passed to Y.one. + */ + M.core_scroll_manager.save_scroll_pos = function(Y, element) { + callPromisedFunction(M.core_scroll_manager.save_scroll_pos, [Y, element]); + }; + + /** + * Event handler that can be used on a link. Assumes that the link already + * contains at least one URL parameter. + * @deprecated since Moodle 4.0 + * @see core/scroll_manager + */ + M.core_scroll_manager.save_scroll_action = function() { + Y.log("The scroll_to_saved_pos function has been deprecated. " + + "Please use initLinksScrollPos in core/scroll_manager instead.", 'moodle-core-notification', 'warn'); + }; + + /** + * If there is a parameter like scrollpos=123 in the URL, scroll to that saved position. + * @deprecated since Moodle 4.0 + * @see core/scroll_manager + * @todo Final deprecation on Moodle 4.4 MDL-72438 + */ + M.core_scroll_manager.scroll_to_saved_pos = function(Y) { + callPromisedFunction(M.core_scroll_manager.scroll_to_saved_pos, Y); + }; } +/* eslint-enable */ M.core_question_engine = M.core_question_engine || {}; @@ -129,9 +111,14 @@ M.core_question_engine.questionformalreadysubmitted = false; /** * 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. + * @deprecated since Moodle 4.0 + * @see core_question/question_engine * @param button the id of the button in the HTML. */ M.core_question_engine.init_submit_button = function(Y, button) { + Y.log("The core_question_engine.init_submit_button function has been deprecated. " + + "Please use initSubmitButton in core_question/question_engine instead.", 'moodle-core-notification', 'warn'); + require(['core_form/submit'], function(submit) { submit.init(button); }); @@ -160,12 +147,12 @@ M.core_question_engine.init_submit_button = function(Y, button) { * @param Y the Yahoo object. Needs to have the DOM and Event modules loaded. * @param form something that can be passed to Y.one, to find the form element. * @deprecated since Moodle 4.0 - * @see question\bank\qbank_previewquestion\amd\src + * @see core_question/question_engine * @todo Final deprecation on Moodle 4.4 MDL-72438 */ M.core_question_engine.init_form = function(Y, form) { Y.log("The core_question_engine.init_form function has been deprecated. " + - "Please use setupQuestionForm() in qbank_preview/preview.js instead.", 'moodle-core-notification', 'warn'); + "Please use init_form in core_question/question_engine instead.", 'moodle-core-notification', 'warn'); Y.one(form).setAttribute('autocomplete', 'off'); @@ -188,12 +175,12 @@ M.core_question_engine.init_form = function(Y, form) { * @param e the form submit event. * @param form the form element. * @deprecated since Moodle 4.0 - * @see question\bank\qbank_previewquestion\amd\src + * @see core_question/question_engine * @todo Final deprecation on Moodle 4.4 MDL-72438 */ M.core_question_engine.prevent_repeat_submission = function(e, Y) { Y.log("The prevent_repeat_submission function has been deprecated. " + - "Please use preventRepeatSubmission in qbank_preview/preview.js instead.", 'moodle-core-notification', 'warn'); + "Please use preventRepeatSubmission in core_question/question_engine instead.", 'moodle-core-notification', 'warn'); if (M.core_question_engine.questionformalreadysubmitted) { e.halt(); diff --git a/question/type/edit_question_form.php b/question/type/edit_question_form.php index c684030562a..4fe7ed56fac 100644 --- a/question/type/edit_question_form.php +++ b/question/type/edit_question_form.php @@ -52,8 +52,8 @@ abstract class question_wizard_form extends moodleform { $mform->addElement('hidden', 'returnurl'); $mform->setType('returnurl', PARAM_LOCALURL); - $mform->addElement('hidden', 'scrollpos'); - $mform->setType('scrollpos', PARAM_INT); + $mform->addElement('hidden', 'mdlscrollto'); + $mform->setType('mdlscrollto', PARAM_INT); $mform->addElement('hidden', 'appendqnumstring'); $mform->setType('appendqnumstring', PARAM_ALPHA); diff --git a/question/upgrade.txt b/question/upgrade.txt index d477f1a797c..9881c3d79f1 100644 --- a/question/upgrade.txt +++ b/question/upgrade.txt @@ -1,5 +1,10 @@ This files describes API changes for code that uses the question API. +=== 4.2 === + +1) The question/qengine.js has been deprecated. We create core_question/question_engine + and core/scroll_manager to replace it. + === 4.1 === 1) get_bulk_action_key() in core_question\local\bank\bulk_action_base class is deprecated and renamed to get_key().