From eb11937beca8b38e412d32a53953f9c149f46423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Mon, 29 Apr 2024 15:16:48 +0200 Subject: [PATCH] MDL-81745 mod_feedback: Refactor editor drag and drop - Remove YUI drag and drop. - Add new 'mod_feedback\external\questions\reorder' web service. - Use SortableList JS component to refactor drag and drop, using the new webservice. - Update new SCSS styles - Fix and create Behat scenarios --- .upgradenotes/MDL-81745-2024060507174301.yml | 5 + mod/feedback/ajax.php | 59 ---- mod/feedback/amd/build/edit.min.js | 2 +- mod/feedback/amd/build/edit.min.js.map | 2 +- .../amd/build/local/repository.min.js | 10 + .../amd/build/local/repository.min.js.map | 1 + mod/feedback/amd/src/edit.js | 65 ++++- mod/feedback/amd/src/local/repository.js | 40 +++ mod/feedback/classes/complete_form.php | 25 +- .../classes/external/questions/reorder.php | 89 ++++++ mod/feedback/db/services.php | 7 + mod/feedback/edit.php | 18 +- mod/feedback/lang/en/feedback.php | 1 + mod/feedback/lib.php | 2 +- .../tests/behat/behat_mod_feedback.php | 3 +- mod/feedback/tests/behat/multichoice.feature | 6 +- mod/feedback/tests/behat/questions.feature | 52 ++-- mod/feedback/version.php | 2 +- mod/feedback/yui/dragdrop/dragdrop.js | 265 ------------------ theme/boost/scss/moodle/modules.scss | 47 ++-- theme/boost/style/moodle.css | 43 +-- theme/classic/style/moodle.css | 43 +-- 22 files changed, 342 insertions(+), 445 deletions(-) create mode 100644 .upgradenotes/MDL-81745-2024060507174301.yml delete mode 100644 mod/feedback/ajax.php create mode 100644 mod/feedback/amd/build/local/repository.min.js create mode 100644 mod/feedback/amd/build/local/repository.min.js.map create mode 100644 mod/feedback/amd/src/local/repository.js create mode 100644 mod/feedback/classes/external/questions/reorder.php delete mode 100644 mod/feedback/yui/dragdrop/dragdrop.js diff --git a/.upgradenotes/MDL-81745-2024060507174301.yml b/.upgradenotes/MDL-81745-2024060507174301.yml new file mode 100644 index 0000000000000..905ba95d5f612 --- /dev/null +++ b/.upgradenotes/MDL-81745-2024060507174301.yml @@ -0,0 +1,5 @@ +issueNumber: MDL-81745 +notes: + mod_feedback: + - message: Added new `mod_feedback_questions_reorder` external function + type: improved diff --git a/mod/feedback/ajax.php b/mod/feedback/ajax.php deleted file mode 100644 index cd05d8cdf6e8e..0000000000000 --- a/mod/feedback/ajax.php +++ /dev/null @@ -1,59 +0,0 @@ -. - -/** - * Process ajax requests - * - * @copyright Andreas Grabs - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package mod_feedback - */ - -if (!defined('AJAX_SCRIPT')) { - define('AJAX_SCRIPT', true); -} - -require(__DIR__.'/../../config.php'); -require_once('lib.php'); - -$id = required_param('id', PARAM_INT); -$action = optional_param('action', '', PARAM_ALPHA); -$sesskey = optional_param('sesskey', false, PARAM_TEXT); -$itemorder = optional_param('itemorder', false, PARAM_SEQUENCE); - -$cm = get_coursemodule_from_id('feedback', $id, 0, false, MUST_EXIST); -$course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); -$feedback = $DB->get_record('feedback', array('id'=>$cm->instance), '*', MUST_EXIST); - -require_sesskey(); - -$context = context_module::instance($cm->id); -require_login($course, true, $cm); -require_capability('mod/feedback:edititems', $context); - -$return = false; - -switch ($action) { - case 'saveitemorder': - $itemlist = explode(',', trim($itemorder, ',')); - if (count($itemlist) > 0) { - $return = feedback_ajax_saveitemorder($itemlist, $feedback); - } - break; -} - -echo json_encode($return); -die; diff --git a/mod/feedback/amd/build/edit.min.js b/mod/feedback/amd/build/edit.min.js index 138843c085b62..5af80c0b554cd 100644 --- a/mod/feedback/amd/build/edit.min.js +++ b/mod/feedback/amd/build/edit.min.js @@ -1,3 +1,3 @@ -define("mod_feedback/edit",["exports","core/prefetch","core/str","core/notification"],(function(_exports,_prefetch,_str,_notification){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const Selectors_deleteQuestionButton='[data-action="delete"]';let initialized=!1;_exports.init=()=>{initialized||((0,_prefetch.prefetchStrings)("core",["yes","no"]),(0,_prefetch.prefetchStrings)("admin",["confirmation"]),(0,_prefetch.prefetchStrings)("mod_feedback",["confirmdeleteitem"]),document.addEventListener("click",(event=>{const deleteButton=event.target.closest(Selectors_deleteQuestionButton);if(deleteButton){event.preventDefault();const targetUrl=deleteButton.getAttribute("href"),requiredStrings=[{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"core"},{key:"no",component:"core"}];(0,_str.getStrings)(requiredStrings).then((_ref=>{let[confirmation,confirmdeleteitem,yes,no]=_ref;return _notification.default.confirm(confirmation,confirmdeleteitem,yes,no,(()=>{window.location=targetUrl}))})).catch(_notification.default.exception)}})),initialized=!0)}})); +define("mod_feedback/edit",["exports","jquery","core/loadingicon","core/notification","core/pending","core/prefetch","core/sortable_list","core/str","core/toast","./local/repository"],(function(_exports,_jquery,_loadingicon,_notification,_pending,_prefetch,_sortable_list,_str,_toast,_repository){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list);const Selectors_deleteQuestionButton='[data-action="delete"]',Selectors_sortableListRegion='[data-region="questions-sortable-list"]',Selectors_sortableElement='[data-region="questions-sortable-list"] .feedback_itemlist[id]',Selectors_sortableElementTitle='[data-region="item-title"]',getItemOrder=element=>{const sortableList=element.closest(Selectors_sortableListRegion);let itemOrder=[];return sortableList.querySelectorAll(Selectors_sortableElement).forEach((item=>{var id;itemOrder.push((id=item.id,Number(id.replace(/^.*feedback_item_/i,""))))})),itemOrder.toString()};let initialized=!1,moduleId=null;_exports.init=cmId=>{if(moduleId=cmId,initialized)return;(0,_prefetch.prefetchStrings)("core",["yes","no"]),(0,_prefetch.prefetchStrings)("admin",["confirmation"]),(0,_prefetch.prefetchStrings)("mod_feedback",["confirmdeleteitem","questionmoved"]),document.addEventListener("click",(event=>{const deleteButton=event.target.closest(Selectors_deleteQuestionButton);if(deleteButton){event.preventDefault();const targetUrl=deleteButton.getAttribute("href"),requiredStrings=[{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"core"},{key:"no",component:"core"}];(0,_str.getStrings)(requiredStrings).then((_ref=>{let[confirmation,confirmdeleteitem,yes,no]=_ref;return _notification.default.confirm(confirmation,confirmdeleteitem,yes,no,(()=>{window.location=targetUrl}))})).catch(_notification.default.exception)}}));new _sortable_list.default(document.querySelector(Selectors_sortableListRegion)).getElementName=element=>{var _element$0$querySelec;return Promise.resolve(null===(_element$0$querySelec=element[0].querySelector(Selectors_sortableElementTitle))||void 0===_element$0$querySelec?void 0:_element$0$querySelec.textContent)},(0,_jquery.default)(document).on(_sortable_list.default.EVENTS.DROP,Selectors_sortableElement,((event,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("mod_feedback/questions:reorder"),itemOrder=getItemOrder(info.element[0]);(0,_repository.reorderQuestions)(moduleId,itemOrder).then((0,_loadingicon.addIconToContainerRemoveOnCompletion)(info.element[0],pendingPromise)).then((()=>(0,_str.getString)("questionmoved","mod_feedback"))).then(_toast.add).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)}})),initialized=!0}})); //# sourceMappingURL=edit.min.js.map \ No newline at end of file diff --git a/mod/feedback/amd/build/edit.min.js.map b/mod/feedback/amd/build/edit.min.js.map index 61e8a89d8b054..e90161cf8514a 100644 --- a/mod/feedback/amd/build/edit.min.js.map +++ b/mod/feedback/amd/build/edit.min.js.map @@ -1 +1 @@ -{"version":3,"file":"edit.min.js","sources":["../src/edit.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 * Edit items in feedback module\n *\n * @module mod_feedback/edit\n * @copyright 2016 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport {prefetchStrings} from 'core/prefetch';\nimport {getStrings} from 'core/str';\nimport Notification from 'core/notification';\n\nconst Selectors = {\n deleteQuestionButton: '[data-action=\"delete\"]',\n};\n\nlet initialized = false;\n\n/**\n * Initialise editor and all it's modules\n */\nexport const init = () => {\n // Ensure we only add our listeners once (can be called multiple times).\n if (initialized) {\n return;\n }\n\n prefetchStrings('core', [\n 'yes',\n 'no',\n ]);\n prefetchStrings('admin', [\n 'confirmation',\n ]);\n prefetchStrings('mod_feedback', [\n 'confirmdeleteitem',\n ]);\n\n document.addEventListener('click', event => {\n\n // Delete question.\n const deleteButton = event.target.closest(Selectors.deleteQuestionButton);\n if (deleteButton) {\n event.preventDefault();\n const targetUrl = deleteButton.getAttribute('href');\n\n const requiredStrings = [\n {key: 'confirmation', component: 'admin'},\n {key: 'confirmdeleteitem', component: 'mod_feedback'},\n {key: 'yes', component: 'core'},\n {key: 'no', component: 'core'},\n ];\n\n getStrings(requiredStrings).then(([confirmation, confirmdeleteitem, yes, no]) => {\n return Notification.confirm(confirmation, confirmdeleteitem, yes, no, () => {\n window.location = targetUrl;\n });\n }).catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"names":["Selectors","initialized","document","addEventListener","event","deleteButton","target","closest","preventDefault","targetUrl","getAttribute","requiredStrings","key","component","then","_ref","confirmation","confirmdeleteitem","yes","no","Notification","confirm","window","location","catch","exception"],"mappings":"sSA6BMA,+BACoB,6BAGtBC,aAAc,gBAKE,KAEZA,4CAIY,OAAQ,CACpB,MACA,qCAEY,QAAS,CACrB,+CAEY,eAAgB,CAC5B,sBAGJC,SAASC,iBAAiB,SAASC,cAGzBC,aAAeD,MAAME,OAAOC,QAAQP,mCACtCK,aAAc,CACdD,MAAMI,uBACAC,UAAYJ,aAAaK,aAAa,QAEtCC,gBAAkB,CACpB,CAACC,IAAK,eAAgBC,UAAW,SACjC,CAACD,IAAK,oBAAqBC,UAAW,gBACtC,CAACD,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,KAAMC,UAAW,6BAGhBF,iBAAiBG,MAAKC,WAAEC,aAAcC,kBAAmBC,IAAKC,gBAC9DC,sBAAaC,QAAQL,aAAcC,kBAAmBC,IAAKC,IAAI,KAClEG,OAAOC,SAAWd,gBAEvBe,MAAMJ,sBAAaK,eAI9BxB,aAAc"} \ No newline at end of file +{"version":3,"file":"edit.min.js","sources":["../src/edit.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 * Edit items in feedback module\n *\n * @module mod_feedback/edit\n * @copyright 2016 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport $ from 'jquery';\nimport {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport SortableList from 'core/sortable_list';\nimport {getString, getStrings} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport {reorderQuestions} from './local/repository';\n\nconst Selectors = {\n deleteQuestionButton: '[data-action=\"delete\"]',\n sortableListRegion: '[data-region=\"questions-sortable-list\"]',\n sortableElement: '[data-region=\"questions-sortable-list\"] .feedback_itemlist[id]',\n sortableElementTitle: '[data-region=\"item-title\"]',\n};\n\n/**\n * Returns the Feedback question item id from the DOM id of an item.\n *\n * @param {String} id The dom id, f.g.: feedback_item_22\n * @return int\n */\nconst getItemId = (id) => {\n return Number(id.replace(/^.*feedback_item_/i, ''));\n};\n\n/**\n * Returns the order of the items in the sortable list.\n *\n * @param {Element} element The element to get the order from.\n * @return string\n */\nconst getItemOrder = (element) => {\n const sortableList = element.closest(Selectors.sortableListRegion);\n let itemOrder = [];\n sortableList.querySelectorAll(Selectors.sortableElement).forEach((item) => {\n itemOrder.push(getItemId(item.id));\n });\n return itemOrder.toString();\n};\n\nlet initialized = false;\nlet moduleId = null;\n\n/**\n * Initialise editor and all it's modules\n *\n * @param {Integer} cmId\n */\nexport const init = (cmId) => {\n\n moduleId = cmId;\n\n // Ensure we only add our listeners once (can be called multiple times).\n if (initialized) {\n return;\n }\n\n prefetchStrings('core', [\n 'yes',\n 'no',\n ]);\n prefetchStrings('admin', [\n 'confirmation',\n ]);\n prefetchStrings('mod_feedback', [\n 'confirmdeleteitem',\n 'questionmoved',\n ]);\n\n // Add event listeners.\n document.addEventListener('click', event => {\n\n // Delete question.\n const deleteButton = event.target.closest(Selectors.deleteQuestionButton);\n if (deleteButton) {\n event.preventDefault();\n const targetUrl = deleteButton.getAttribute('href');\n\n const requiredStrings = [\n {key: 'confirmation', component: 'admin'},\n {key: 'confirmdeleteitem', component: 'mod_feedback'},\n {key: 'yes', component: 'core'},\n {key: 'no', component: 'core'},\n ];\n\n getStrings(requiredStrings).then(([confirmation, confirmdeleteitem, yes, no]) => {\n return Notification.confirm(confirmation, confirmdeleteitem, yes, no, () => {\n window.location = targetUrl;\n });\n }).catch(Notification.exception);\n }\n });\n\n // Initialize sortable list to handle active conditions moving (note JQuery dependency, see MDL-72293 for resolution).\n const sortableList = new SortableList(document.querySelector(Selectors.sortableListRegion));\n sortableList.getElementName = element => Promise.resolve(element[0].querySelector(Selectors.sortableElementTitle)?.textContent);\n $(document).on(SortableList.EVENTS.DROP, Selectors.sortableElement, (event, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('mod_feedback/questions:reorder');\n const itemOrder = getItemOrder(info.element[0]);\n\n reorderQuestions(moduleId, itemOrder)\n .then(addIconToContainerRemoveOnCompletion(info.element[0], pendingPromise))\n .then(() => getString('questionmoved', 'mod_feedback'))\n .then(addToast)\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n }\n });\n\n initialized = true;\n};\n"],"names":["Selectors","getItemOrder","element","sortableList","closest","itemOrder","querySelectorAll","forEach","item","id","push","Number","replace","toString","initialized","moduleId","cmId","document","addEventListener","event","deleteButton","target","preventDefault","targetUrl","getAttribute","requiredStrings","key","component","then","_ref","confirmation","confirmdeleteitem","yes","no","Notification","confirm","window","location","catch","exception","SortableList","querySelector","getElementName","Promise","resolve","_element$0$querySelec","textContent","on","EVENTS","DROP","info","positionChanged","pendingPromise","Pending","addToast"],"mappings":"0oBAmCMA,+BACoB,yBADpBA,6BAEkB,0CAFlBA,0BAGe,iEAHfA,+BAIoB,6BAmBpBC,aAAgBC,gBACZC,aAAeD,QAAQE,QAAQJ,kCACjCK,UAAY,UAChBF,aAAaG,iBAAiBN,2BAA2BO,SAASC,OAbnDC,IAAAA,GAcXJ,UAAUK,MAdCD,GAccD,KAAKC,GAb3BE,OAAOF,GAAGG,QAAQ,qBAAsB,UAexCP,UAAUQ,gBAGjBC,aAAc,EACdC,SAAW,mBAOMC,UAEjBD,SAAWC,KAGPF,iDAIY,OAAQ,CACpB,MACA,qCAEY,QAAS,CACrB,+CAEY,eAAgB,CAC5B,oBACA,kBAIJG,SAASC,iBAAiB,SAASC,cAGzBC,aAAeD,MAAME,OAAOjB,QAAQJ,mCACtCoB,aAAc,CACdD,MAAMG,uBACAC,UAAYH,aAAaI,aAAa,QAEtCC,gBAAkB,CACpB,CAACC,IAAK,eAAgBC,UAAW,SACjC,CAACD,IAAK,oBAAqBC,UAAW,gBACtC,CAACD,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,KAAMC,UAAW,6BAGhBF,iBAAiBG,MAAKC,WAAEC,aAAcC,kBAAmBC,IAAKC,gBAC9DC,sBAAaC,QAAQL,aAAcC,kBAAmBC,IAAKC,IAAI,KAClEG,OAAOC,SAAWd,gBAEvBe,MAAMJ,sBAAaK,eAKT,IAAIC,uBAAavB,SAASwB,cAAczC,+BAChD0C,eAAiBxC,2CAAWyC,QAAQC,sCAAQ1C,QAAQ,GAAGuC,cAAczC,wEAAzB6C,sBAA0DC,kCACjH7B,UAAU8B,GAAGP,uBAAaQ,OAAOC,KAAMjD,2BAA2B,CAACmB,MAAO+B,WACpEA,KAAKC,gBAAiB,OAChBC,eAAiB,IAAIC,iBAAQ,kCAC7BhD,UAAYJ,aAAaiD,KAAKhD,QAAQ,qCAE3Ba,SAAUV,WACtBuB,MAAK,qDAAqCsB,KAAKhD,QAAQ,GAAIkD,iBAC3DxB,MAAK,KAAM,kBAAU,gBAAiB,kBACtCA,KAAK0B,YACL1B,MAAK,IAAMwB,eAAeR,YAC1BN,MAAMJ,sBAAaK,eAIhCzB,aAAc"} \ No newline at end of file diff --git a/mod/feedback/amd/build/local/repository.min.js b/mod/feedback/amd/build/local/repository.min.js new file mode 100644 index 0000000000000..118271bf9e564 --- /dev/null +++ b/mod/feedback/amd/build/local/repository.min.js @@ -0,0 +1,10 @@ +define("mod_feedback/local/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; +/** + * Module to handle feedback AJAX requests + * + * @module mod_feedback/local/repository + * @copyright 2024 Mikel Martín + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.reorderQuestions=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.reorderQuestions=(moduleId,itemOrder)=>{const request={methodname:"mod_feedback_questions_reorder",args:{cmid:moduleId,itemorder:itemOrder}};return _ajax.default.call([request])[0]}})); + +//# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/mod/feedback/amd/build/local/repository.min.js.map b/mod/feedback/amd/build/local/repository.min.js.map new file mode 100644 index 0000000000000..4239eadf84944 --- /dev/null +++ b/mod/feedback/amd/build/local/repository.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.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 * Module to handle feedback AJAX requests\n *\n * @module mod_feedback/local/repository\n * @copyright 2024 Mikel Martín \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Reorder questions for a given feedback course module\n *\n * @param {Number} moduleId\n * @param {String} itemOrder\n * @return {Promise}\n */\nexport const reorderQuestions = (moduleId, itemOrder) => {\n const request = {\n methodname: 'mod_feedback_questions_reorder',\n args: {cmid: moduleId, itemorder: itemOrder}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["moduleId","itemOrder","request","methodname","args","cmid","itemorder","Ajax","call"],"mappings":";;;;;;;4KAgCgC,CAACA,SAAUC,mBACjCC,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,KAAML,SAAUM,UAAWL,mBAG/BM,cAAKC,KAAK,CAACN,UAAU"} \ No newline at end of file diff --git a/mod/feedback/amd/src/edit.js b/mod/feedback/amd/src/edit.js index da045d24f2e53..e2e1e6ace49ae 100644 --- a/mod/feedback/amd/src/edit.js +++ b/mod/feedback/amd/src/edit.js @@ -23,20 +23,60 @@ "use strict"; -import {prefetchStrings} from 'core/prefetch'; -import {getStrings} from 'core/str'; +import $ from 'jquery'; +import {addIconToContainerRemoveOnCompletion} from 'core/loadingicon'; import Notification from 'core/notification'; +import Pending from 'core/pending'; +import {prefetchStrings} from 'core/prefetch'; +import SortableList from 'core/sortable_list'; +import {getString, getStrings} from 'core/str'; +import {add as addToast} from 'core/toast'; +import {reorderQuestions} from './local/repository'; const Selectors = { deleteQuestionButton: '[data-action="delete"]', + sortableListRegion: '[data-region="questions-sortable-list"]', + sortableElement: '[data-region="questions-sortable-list"] .feedback_itemlist[id]', + sortableElementTitle: '[data-region="item-title"]', +}; + +/** + * Returns the Feedback question item id from the DOM id of an item. + * + * @param {String} id The dom id, f.g.: feedback_item_22 + * @return int + */ +const getItemId = (id) => { + return Number(id.replace(/^.*feedback_item_/i, '')); +}; + +/** + * Returns the order of the items in the sortable list. + * + * @param {Element} element The element to get the order from. + * @return string + */ +const getItemOrder = (element) => { + const sortableList = element.closest(Selectors.sortableListRegion); + let itemOrder = []; + sortableList.querySelectorAll(Selectors.sortableElement).forEach((item) => { + itemOrder.push(getItemId(item.id)); + }); + return itemOrder.toString(); }; let initialized = false; +let moduleId = null; /** * Initialise editor and all it's modules + * + * @param {Integer} cmId */ -export const init = () => { +export const init = (cmId) => { + + moduleId = cmId; + // Ensure we only add our listeners once (can be called multiple times). if (initialized) { return; @@ -51,8 +91,10 @@ export const init = () => { ]); prefetchStrings('mod_feedback', [ 'confirmdeleteitem', + 'questionmoved', ]); + // Add event listeners. document.addEventListener('click', event => { // Delete question. @@ -76,5 +118,22 @@ export const init = () => { } }); + // Initialize sortable list to handle active conditions moving (note JQuery dependency, see MDL-72293 for resolution). + const sortableList = new SortableList(document.querySelector(Selectors.sortableListRegion)); + sortableList.getElementName = element => Promise.resolve(element[0].querySelector(Selectors.sortableElementTitle)?.textContent); + $(document).on(SortableList.EVENTS.DROP, Selectors.sortableElement, (event, info) => { + if (info.positionChanged) { + const pendingPromise = new Pending('mod_feedback/questions:reorder'); + const itemOrder = getItemOrder(info.element[0]); + + reorderQuestions(moduleId, itemOrder) + .then(addIconToContainerRemoveOnCompletion(info.element[0], pendingPromise)) + .then(() => getString('questionmoved', 'mod_feedback')) + .then(addToast) + .then(() => pendingPromise.resolve()) + .catch(Notification.exception); + } + }); + initialized = true; }; diff --git a/mod/feedback/amd/src/local/repository.js b/mod/feedback/amd/src/local/repository.js new file mode 100644 index 0000000000000..e5cd1b9ae47a7 --- /dev/null +++ b/mod/feedback/amd/src/local/repository.js @@ -0,0 +1,40 @@ +// 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 . + +/** + * Module to handle feedback AJAX requests + * + * @module mod_feedback/local/repository + * @copyright 2024 Mikel Martín + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; + +/** + * Reorder questions for a given feedback course module + * + * @param {Number} moduleId + * @param {String} itemOrder + * @return {Promise} + */ +export const reorderQuestions = (moduleId, itemOrder) => { + const request = { + methodname: 'mod_feedback_questions_reorder', + args: {cmid: moduleId, itemorder: itemOrder} + }; + + return Ajax.call([request])[0]; +}; diff --git a/mod/feedback/classes/complete_form.php b/mod/feedback/classes/complete_form.php index f49ba2cc9b859..40cede415fe9f 100644 --- a/mod/feedback/classes/complete_form.php +++ b/mod/feedback/classes/complete_form.php @@ -68,8 +68,14 @@ public function __construct($mode, mod_feedback_structure $structure, $formid, $ $this->structure = $structure; $this->gopage = isset($customdata['gopage']) ? $customdata['gopage'] : 0; $isanonymous = $this->structure->is_anonymous() ? ' ianonymous' : ''; - parent::__construct(null, $customdata, 'POST', '', - array('id' => $formid, 'class' => 'feedback_form' . $isanonymous), true); + parent::__construct( + null, + $customdata, + 'POST', + '', + ['id' => $formid, 'class' => 'feedback_form' . $isanonymous], + true + ); $this->set_display_vertical(); } @@ -117,11 +123,13 @@ public function definition() { $mform->closeHeaderBefore('buttonar'); } + $mform->addElement('html', html_writer::start_div('', ['data-region' => 'questions-sortable-list'])); if ($this->mode == self::MODE_COMPLETE) { $this->definition_complete(); } else { $this->definition_preview(); } + $mform->addElement('html', html_writer::end_div()); // Set data. $this->set_data(array('gopage' => $this->gopage)); @@ -319,6 +327,7 @@ public function add_form_element($item, $element, $addrequiredrule = true, $setd $attributes = $element->getAttributes(); $class = !empty($attributes['class']) ? ' ' . $attributes['class'] : ''; $attributes['class'] = $this->get_suggested_class($item) . $class; + $element->setAttributes($attributes); // Add required rule. @@ -468,13 +477,13 @@ protected function enhance_name_for_edit($item, $element) { $menu->add($action); } $editmenu = $OUTPUT->render($menu); + $draghandle = $OUTPUT->render_from_template('core/drag_handle', + ['movetitle' => get_string('move_item', 'mod_feedback')]); - $name = $element->getLabel(); - - $name = html_writer::span('', 'itemdd', array('id' => 'feedback_item_box_' . $item->id)) . - html_writer::span($name, 'itemname') . - html_writer::span($editmenu, 'itemactions'); - $element->setLabel(html_writer::span($name, 'itemtitle', ['class' => 'mx-5'])); + $name = html_writer::div($draghandle, 'itemhandle', ['data-drag-type' => 'move']) . + html_writer::div($element->getLabel(), 'itemname', ['data-region' => 'item-title']) . + html_writer::div($editmenu, 'itemactions'); + $element->setLabel(html_writer::div($name, 'itemtitle d-flex')); } /** diff --git a/mod/feedback/classes/external/questions/reorder.php b/mod/feedback/classes/external/questions/reorder.php new file mode 100644 index 0000000000000..84cf48a6875e3 --- /dev/null +++ b/mod/feedback/classes/external/questions/reorder.php @@ -0,0 +1,89 @@ +. + +declare(strict_types=1); + +namespace mod_feedback\external\questions; + +use core_external\external_api; +use core_external\external_value; +use core_external\external_function_parameters; +use context_module; + +/** + * External method for reordering feedback questions. + * + * @package mod_feedback + * @copyright 2024 Mikel Martín + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reorder extends external_api { + + /** + * Describes the parameters for reorder. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'cmid' => new external_value(PARAM_INT, 'Feedback course module id'), + 'itemorder' => new external_value(PARAM_SEQUENCE, 'Feedback question item order'), + ] + ); + } + + /** + * External function to reorder feedback questions. + * + * @param int $cmid + * @param string $itemorder + * @return bool + */ + public static function execute(int $cmid, string $itemorder): bool { + global $DB; + [ + 'cmid' => $cmid, + 'itemorder' => $itemorder, + ] = self::validate_parameters(self::execute_parameters(), [ + 'cmid' => $cmid, + 'itemorder' => $itemorder, + ]); + + $cm = get_coursemodule_from_id('feedback', $cmid, 0, false, MUST_EXIST); + $feedback = $DB->get_record('feedback', ['id' => $cm->instance], '*', MUST_EXIST); + $context = context_module::instance($cm->id); + + self::validate_context($context); + require_capability('mod/feedback:edititems', $context); + + $itemlist = explode(',', trim($itemorder, ',')) ?: []; + if (count($itemlist) > 0) { + return feedback_ajax_saveitemorder($itemlist, $feedback); + } + + return false; + } + + /** + * Describes the data returned from the external function. + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, '', VALUE_REQUIRED); + } +} diff --git a/mod/feedback/db/services.php b/mod/feedback/db/services.php index 6866beab60501..62ef58f7c557d 100644 --- a/mod/feedback/db/services.php +++ b/mod/feedback/db/services.php @@ -141,4 +141,11 @@ 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + 'mod_feedback_questions_reorder' => [ + 'classname' => 'mod_feedback\external\questions\reorder', + 'description' => 'Saves the new order of the questions in the feedback.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/feedback:edititems', + ], ); diff --git a/mod/feedback/edit.php b/mod/feedback/edit.php index 1f0ddfa99c747..a251a936efeb4 100644 --- a/mod/feedback/edit.php +++ b/mod/feedback/edit.php @@ -86,21 +86,7 @@ 'description' => '' ]); $PAGE->add_body_class('limitedwidth'); -$PAGE->requires->js_call_amd('mod_feedback/edit', 'init'); - -//Adding the javascript module for the items dragdrop. -if (count($feedbackitems) > 1) { - $PAGE->requires->strings_for_js([ - 'pluginname', - 'move_item', - 'position', - ], 'feedback'); - $PAGE->requires->yui_module( - 'moodle-mod_feedback-dragdrop', - 'M.mod_feedback.init_dragdrop', - [['cmid' => $cm->id]] - ); -} +$PAGE->requires->js_call_amd('mod_feedback/edit', 'init', [$cm->id]); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('edit_items', 'mod_feedback'), 3); @@ -110,8 +96,6 @@ echo $renderer->main_action_bar($actionbar); $form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_EDIT, $feedbackstructure, 'feedback_edit_form'); -echo '
'; // The container for the dragging area. $form->display(); -echo '
'; echo $OUTPUT->footer(); diff --git a/mod/feedback/lang/en/feedback.php b/mod/feedback/lang/en/feedback.php index e67380f80ea31..946ffbb06b038 100644 --- a/mod/feedback/lang/en/feedback.php +++ b/mod/feedback/lang/en/feedback.php @@ -244,6 +244,7 @@ $string['public'] = 'Public'; $string['question'] = 'Question'; $string['questionandsubmission'] = 'Question and submission settings'; +$string['questionmoved'] = 'Question moved'; $string['questions'] = 'Questions'; $string['questionslimited'] = 'Showing only {$a} first questions, view individual answers or download table data to view all.'; $string['radio'] = 'Multiple choice - single answer'; diff --git a/mod/feedback/lib.php b/mod/feedback/lib.php index f56aae922cadc..fb550838649e8 100644 --- a/mod/feedback/lib.php +++ b/mod/feedback/lib.php @@ -2867,7 +2867,7 @@ function feedback_page_type_list($pagetype, $parentcontext, $currentcontext) { /** * Move save the items of the given $feedback in the order of $itemlist. - * @param string $itemlist a comma separated list with item ids + * @param array $itemlist a list with item ids * @param stdClass $feedback * @return bool true if success */ diff --git a/mod/feedback/tests/behat/behat_mod_feedback.php b/mod/feedback/tests/behat/behat_mod_feedback.php index eb3ecf46d7ca6..9e877fad13398 100644 --- a/mod/feedback/tests/behat/behat_mod_feedback.php +++ b/mod/feedback/tests/behat/behat_mod_feedback.php @@ -237,7 +237,8 @@ protected function compare_exports($expected, $actual) { public static function get_partial_named_selectors(): array { return [ new behat_component_named_selector('Question', [ - ".//*[starts-with(@id, 'fitem_feedback_item_')][.//*[contains(text(), %locator%)]]", + ".//*[starts-with(@id, 'fitem_feedback_item_') or starts-with(@id, 'fgroup_feedback_item_')]" . + "[.//*[contains(text(), %locator%)]]", ]), ]; } diff --git a/mod/feedback/tests/behat/multichoice.feature b/mod/feedback/tests/behat/multichoice.feature index d71d3fb3860c7..418762b4e7b16 100644 --- a/mod/feedback/tests/behat/multichoice.feature +++ b/mod/feedback/tests/behat/multichoice.feature @@ -151,7 +151,7 @@ Feature: Testing multichoice questions in feedback # Change the settings so we don't analyse empty submits And I am on the "Learning experience" "feedback activity" page And I navigate to "Questions" in current page administration - And I open the action menu in "//div[contains(@class, 'feedback_itemlist') and contains(.,'multichoice1')]" "xpath_element" + And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Omit empty submits in analysis" to "Yes" And I press "Save changes to question" @@ -290,7 +290,7 @@ Feature: Testing multichoice questions in feedback # Change the settings so we don't analyse empty submits And I am on the "Learning experience" "feedback activity" page And I navigate to "Questions" in current page administration - And I open the action menu in "//div[contains(@class, 'feedback_itemlist') and contains(.,'multichoice1')]" "xpath_element" + And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Omit empty submits in analysis" to "Yes" And I press "Save changes to question" @@ -408,7 +408,7 @@ Feature: Testing multichoice questions in feedback # Change the settings so we don't analyse empty submits And I am on the "Learning experience" "feedback activity" page And I navigate to "Questions" in current page administration - And I open the action menu in "//div[contains(@class, 'feedback_itemlist') and contains(.,'multichoice1')]" "xpath_element" + And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Omit empty submits in analysis" to "Yes" And I press "Save changes to question" diff --git a/mod/feedback/tests/behat/questions.feature b/mod/feedback/tests/behat/questions.feature index 4ed942a73be37..86b64eabd86a8 100644 --- a/mod/feedback/tests/behat/questions.feature +++ b/mod/feedback/tests/behat/questions.feature @@ -17,23 +17,24 @@ Feature: Managing feedback questions And the following "activities" exist: | activity | name | course | idnumber | | feedback | Learning experience course 1 | C1 | feedback1 | + And the following "mod_feedback > question" exists: + | activity | feedback1 | + | name | Is it me you're looking for? | + | label | q1 | Scenario: Teacher can create a new feedback question Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I add a "Short text answer" question to the feedback with: - | Question | Is it me you're looking for? | - | Label | q1 | - Then I should see "(q1) Is it me you're looking for?" + | Question | I can see it in your eyes | + | Label | q2 | + Then I should see "(q2) I can see it in your eyes" @javascript Scenario: Teacher can edit feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" - And I add a "Short text answer" question to the feedback with: - | Question | Is it me you're looking for? | - | Label | q1 | - When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Question" to "Can you see it in my eyes?" And I press "Save changes to question" @@ -44,10 +45,7 @@ Feature: Managing feedback questions Scenario: Teacher can edit and save as new feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" - And I add a "Short text answer" question to the feedback with: - | Question | Is it me you're looking for? | - | Label | q1 | - When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Question" to "You can se it in my eyes?" And I press "Save as new question" @@ -58,10 +56,7 @@ Feature: Managing feedback questions Scenario: Teacher can delete feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" - And I add a "Short text answer" question to the feedback with: - | Question | Is it me you're looking for? | - | Label | q1 | - When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Delete question" in the open action menu And I click on "Yes" "button" in the "Confirmation" "dialogue" Then I should not see "(q1) Is it me you're looking for?" @@ -70,18 +65,29 @@ Feature: Managing feedback questions Scenario: Teacher can mark as required feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" - And I add a "Short text answer" question to the feedback with: - | Question | Is it me you're looking for? | - | Label | q1 | - | Required | 0 | - When I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Set as required" in the open action menu - And I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu Then the field "Required" matches value "1" And I press "Cancel" - And I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Set as not required" in the open action menu - And I open the action menu in "Is it me you're looking for?" "mod_feedback > Question" + And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu And the field "Required" matches value "0" + + @javascript + Scenario: Teacher can move questions + Given the following "mod_feedback > questions" exist: + | activity | label | name | + | feedback1 | q2 | I can see it in your eyes | + | feedback1 | q3 | I can see it in your smile | + And I am on the "Learning experience course 1" "feedback activity" page logged in as teacher + And I click on "Edit questions" "link" in the "region-main" "region" + When I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question" + Then I should see "After \"(q2) I can see it in your eyes\"" in the "Move this question" "dialogue" + And I should not see "To the top of the list" in the "Move this question" "dialogue" + And I click on "After \"(q3) I can see it in your smile\"" "link" in the "Move this question" "dialogue" + And I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question" + And I click on "To the top of the list" "link" in the "Move this question" "dialogue" diff --git a/mod/feedback/version.php b/mod/feedback/version.php index d4d3e0eb9d19e..74f8191eefea6 100644 --- a/mod/feedback/version.php +++ b/mod/feedback/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024100700; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2024100701; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2024100100; // Requires this Moodle version. $plugin->component = 'mod_feedback'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0; diff --git a/mod/feedback/yui/dragdrop/dragdrop.js b/mod/feedback/yui/dragdrop/dragdrop.js deleted file mode 100644 index cdb194a8150e6..0000000000000 --- a/mod/feedback/yui/dragdrop/dragdrop.js +++ /dev/null @@ -1,265 +0,0 @@ -YUI.add('moodle-mod_feedback-dragdrop', function(Y) { - var DRAGDROPNAME = 'mod_feedback_dragdrop'; - var CSS = { - DRAGAREA: '#feedback_dragarea', - DRAGITEMCLASS: 'feedback_itemlist', - DRAGITEM: '.row.feedback_itemlist', - DRAGLIST: '#feedback_dragarea form', - DRAGHANDLE: 'itemhandle' - }; - - var DRAGDROP = function() { - DRAGDROP.superclass.constructor.apply(this, arguments); - }; - - Y.extend(DRAGDROP, M.core.dragdrop, { - - initializer : function(params) { - //Static Vars - this.cmid = params.cmid; - this.goingUp = false, lastY = 0; - - var groups = ['feedbackitem']; - - var handletitle = M.util.get_string('move_item', 'feedback'); - - //Get the list of li's in the lists and add the drag handle. - basenode = Y.Node.one(CSS.DRAGLIST); - listitems = basenode.all(CSS.DRAGITEM).each(function(v) { - var item_id = this.get_node_id(v.get('id')); //Get the id of the feedback item. - var mydraghandle = this.get_drag_handle(handletitle, CSS.DRAGHANDLE, 'icon'); - v.append(mydraghandle); // Insert the new handle into the item box. - }, this); - - //We use a delegate to make all items draggable - var del = new Y.DD.Delegate({ - container: CSS.DRAGLIST, - nodes: CSS.DRAGITEM, - target: { - padding: '0 0 0 20' - }, - handles: ['.' + CSS.DRAGHANDLE], - dragConfig: {groups: groups} - }); - - //Add plugins to the delegate - del.dd.plug(Y.Plugin.DDProxy, { - // Don't move the node at the end of the drag - moveOnEnd: false, - cloneNode: true - }); - del.dd.plug(Y.Plugin.DDConstrained, { - // Keep it inside the .course-content - constrain: CSS.DRAGAREA - }); - del.dd.plug(Y.Plugin.DDWinScroll); - - //Listen for all drop:over events - del.on('drop:over', this.drop_over_handler, this); - //Listen for all drag:drag events - del.on('drag:drag', this.drag_drag_handler, this); - //Listen for all drag:start events - del.on('drag:start', this.drag_start_handler, this); - //Listen for a drag:end events - del.on('drag:end', this.drag_end_handler, this); - //Listen for all drag:drophit events - del.on('drag:drophit', this.drag_drophit_handler, this); - //Listen for all drag:dropmiss events - del.on('drag:dropmiss', this.drag_dropmiss_handler, this); - - //Create targets for drop. - var droparea = Y.Node.one(CSS.DRAGLIST); - var tar = new Y.DD.Drop({ - groups: groups, - node: droparea - }); - - }, - - /** - * Handles the drop:over event. - * - * @param e the event - * @return void - */ - drop_over_handler : function(e) { - //Get a reference to our drag and drop nodes - var drag = e.drag.get('node'), - drop = e.drop.get('node'); - - //Are we dropping on an li node? - if (drop.hasClass(CSS.DRAGITEMCLASS)) { - //Are we not going up? - if (!this.goingUp) { - drop = drop.get('nextSibling'); - } - //Add the node to this list - e.drop.get('node').get('parentNode').insertBefore(drag, drop); - //Resize this nodes shim, so we can drop on it later. - e.drop.sizeShim(); - } - }, - - /** - * Handles the drag:drag event. - * - * @param e the event - * @return void - */ - drag_drag_handler : function(e) { - //Get the last y point - var y = e.target.lastXY[1]; - //Is it greater than the lastY var? - if (y < this.lastY) { - //We are going up - this.goingUp = true; - } else { - //We are going down. - this.goingUp = false; - } - //Cache for next check - this.lastY = y; - }, - - /** - * Handles the drag:start event. - * - * @param e the event - * @return void - */ - drag_start_handler : function(e) { - //Get our drag object - var drag = e.target; - - //Set some styles here - drag.get('node').addClass('drag_target_active'); - drag.get('dragNode').set('innerHTML', drag.get('node').get('innerHTML')); - drag.get('dragNode').addClass('drag_item_active'); - drag.get('dragNode').setStyles({ - borderColor: drag.get('node').getStyle('borderColor'), - backgroundColor: drag.get('node').getStyle('backgroundColor') - }); - }, - - /** - * Handles the drag:end event. - * - * @param e the event - * @return void - */ - drag_end_handler : function(e) { - var drag = e.target; - //Put our styles back - drag.get('node').removeClass('drag_target_active'); - }, - - /** - * Handles the drag:drophit event. - * - * @param e the event - * @return void - */ - drag_drophit_handler : function(e) { - var drop = e.drop.get('node'), - drag = e.drag.get('node'); - dragnode = Y.one(drag); - if (!drop.hasClass(CSS.DRAGITEMCLASS)) { - if (!drop.contains(drag)) { - drop.appendChild(drag); - } - var childElement; - var elementId; - var elements = []; - drop.all(CSS.DRAGITEM).each(function(v) { - childElement = v.one('.felement')?.one('[id^="feedback_item_"]'); - if (childElement) { - elementId = this.get_node_id(childElement.get('id')); - if (elements.indexOf(elementId) == -1) { - elements.push(elementId); - } - } - }, this); - var spinner = M.util.add_spinner(Y, dragnode); - this.save_item_order(this.cmid, elements.toString(), spinner); - } - }, - - /** - * Save the new item order. - * - * @param cmid the coursemodule id - * @param itemorder A comma separated list with item ids - * @param spinner The spinner icon shown while saving - * @return void - */ - save_item_order : function(cmid, itemorder, spinner) { - - Y.io(M.cfg.wwwroot + '/mod/feedback/ajax.php', { - //The needed paramaters - data: {action: 'saveitemorder', - id: cmid, - itemorder: itemorder, - sesskey: M.cfg.sesskey - }, - - timeout: 5000, //5 seconds for timeout I think it is enough. - - //Define the events. - on: { - start : function(transactionid) { - spinner.show(); - }, - success : function(transactionid, xhr) { - var response = xhr.responseText; - var ergebnis = Y.JSON.parse(response); - window.setTimeout(function(e) { - spinner.hide(); - }, 250); - require(['core/notification', 'core/str', 'core/toast'], function(Notification, Strings, Toast) { - Strings.get_string('changessaved', 'core').then(function(saveString) { - return Toast.add(saveString); - }).catch(Notification.exception); - }); - }, - failure : function(transactionid, xhr) { - var msg = { - name : xhr.status+' '+xhr.statusText, - message : xhr.responseText - }; - return new M.core.exception(msg); - //~ this.ajax_failure(xhr); - spinner.hide(); - } - }, - context:this - }); - }, - - /** - * Returns the numeric id from the dom id of an item. - * - * @param id The dom id, f.g.: feedback_item_22 - * @return int - */ - get_node_id : function(id) { - return Number(id.replace(/^.*feedback_item_/i, '')); - } - - }, { - NAME : DRAGDROPNAME, - ATTRS : { - cmid : { - value : 0 - } - } - - }); - - M.mod_feedback = M.mod_feedback || {}; - M.mod_feedback.init_dragdrop = function(params) { - return new DRAGDROP(params); - } - -}, '@VERSION@', { - requires:['io', 'json-parse', 'dd-constrain', 'dd-proxy', 'dd-drop', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification'] -}); diff --git a/theme/boost/scss/moodle/modules.scss b/theme/boost/scss/moodle/modules.scss index d3f6945027abd..ff68d17f7389d 100644 --- a/theme/boost/scss/moodle/modules.scss +++ b/theme/boost/scss/moodle/modules.scss @@ -1589,12 +1589,6 @@ $popout-header-height: 4rem; border: 0; margin: 0; } - .drag_target_active { - opacity: .25; - } - .drag_item_active { - opacity: .5; - } .feedback_bar_image { height: 10px; } @@ -1611,39 +1605,44 @@ $popout-header-height: 4rem; width: 10%; } } - .feedback_form { - .itemactions { - display: inline-block; - margin: 0 map-get($spacers, 2); - } - } .feedback-item-label { width: 100%; } // Feedback edit form. #feedback_edit_form { - [id*=_feedback_item_].feedback_itemlist { + [id*=_feedback_item_].feedback_itemlist, + .feedback_itemlist.sortable-list-is-dragged { padding: map-get($spacers, 3); border: $border-width solid $border-color; + background-color: $white; + position: relative; @include border-radius(); - .itemhandle { - position: absolute; - width: 32px; - height: 32px; - text-align: center; - align-content: center; + .itemname { + margin-right: map-get($spacers, 5); } - .action-menu { + .itemactions { position: absolute; top: 0; right: 0; + .dropdown-toggle { + @include border-radius(.5rem); + width: $icon-medium-width; + height: $icon-medium-height; + } } - .dropdown-toggle { - border-radius: .5rem; - width: 32px; - height: 32px; + &.sortable-list-current-position { + background-color: $light; } } + .sortable-list-is-dragged { + opacity: .75; + max-width: $course-content-maxwidth; + } + .loading-icon { + position: absolute; + left: 50%; + top: calc(50% - .5rem); + } } // Analysis page. diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index a78947448ab18..e7bb82efe863a 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -35071,12 +35071,6 @@ img.userpicture { border: 0; margin: 0; } -.path-mod-feedback .drag_target_active { - opacity: 0.25; -} -.path-mod-feedback .drag_item_active { - opacity: 0.5; -} .path-mod-feedback .feedback_bar_image { height: 10px; } @@ -35089,35 +35083,46 @@ img.userpicture { .path-mod-feedback .templateslist th.header.action { width: 10%; } -.path-mod-feedback .feedback_form .itemactions { - display: inline-block; - margin: 0 0.5rem; -} .path-mod-feedback .feedback-item-label { width: 100%; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist { +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged { padding: 1rem; border: 1px solid #dee2e6; + background-color: #fff; + position: relative; border-radius: 0.5rem; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemhandle { - position: absolute; - width: 32px; - height: 32px; - text-align: center; - align-content: center; +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemname, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemname { + margin-right: 2rem; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .action-menu { +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions { position: absolute; top: 0; right: 0; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .dropdown-toggle { +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions .dropdown-toggle, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions .dropdown-toggle { border-radius: 0.5rem; width: 32px; height: 32px; } +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist.sortable-list-current-position, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged.sortable-list-current-position { + background-color: #f8f9fa; +} +.path-mod-feedback #feedback_edit_form .sortable-list-is-dragged { + opacity: 0.75; + max-width: 830px; +} +.path-mod-feedback #feedback_edit_form .loading-icon { + position: absolute; + left: 50%; + top: calc(50% - 0.5rem); +} .path-mod-feedback table.analysis { width: 100%; border-top: 1px solid #dee2e6; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 73d9e67395d0c..474a58223d68e 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -35071,12 +35071,6 @@ img.userpicture { border: 0; margin: 0; } -.path-mod-feedback .drag_target_active { - opacity: 0.25; -} -.path-mod-feedback .drag_item_active { - opacity: 0.5; -} .path-mod-feedback .feedback_bar_image { height: 10px; } @@ -35089,35 +35083,46 @@ img.userpicture { .path-mod-feedback .templateslist th.header.action { width: 10%; } -.path-mod-feedback .feedback_form .itemactions { - display: inline-block; - margin: 0 0.5rem; -} .path-mod-feedback .feedback-item-label { width: 100%; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist { +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged { padding: 1rem; border: 1px solid #dee2e6; + background-color: #fff; + position: relative; border-radius: 0.25rem; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemhandle { - position: absolute; - width: 32px; - height: 32px; - text-align: center; - align-content: center; +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemname, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemname { + margin-right: 2rem; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .action-menu { +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions { position: absolute; top: 0; right: 0; } -.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .dropdown-toggle { +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist .itemactions .dropdown-toggle, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged .itemactions .dropdown-toggle { border-radius: 0.5rem; width: 32px; height: 32px; } +.path-mod-feedback #feedback_edit_form [id*=_feedback_item_].feedback_itemlist.sortable-list-current-position, +.path-mod-feedback #feedback_edit_form .feedback_itemlist.sortable-list-is-dragged.sortable-list-current-position { + background-color: #f8f9fa; +} +.path-mod-feedback #feedback_edit_form .sortable-list-is-dragged { + opacity: 0.75; + max-width: 830px; +} +.path-mod-feedback #feedback_edit_form .loading-icon { + position: absolute; + left: 50%; + top: calc(50% - 0.5rem); +} .path-mod-feedback table.analysis { width: 100%; border-top: 1px solid #dee2e6;