MDL-85504 mod_feedback: Enhance the question edit form via JS

The drag handle and actions menu should not be within the accessible
labels of the question items. In order to enhance the question edit
form with these, they need to be rendered via JS outside of container
of the questions' accessible name.
This commit is contained in:
Jun Pataleta
2025-07-23 16:16:10 +08:00
parent 2b4057c403
commit 26c41abea9
5 changed files with 86 additions and 12 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
define("mod_feedback/edit",["exports","core/loadingicon","core/notification","core/pending","core/prefetch","core/sortable_list","core/str","core/toast","mod_feedback/local/repository"],(function(_exports,_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,_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",(async event=>{const deleteButton=event.target.closest(Selectors_deleteQuestionButton);if(deleteButton){event.preventDefault();const confirmationStrings=await(0,_str.getStrings)([{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"core"},{key:"no",component:"core"}]);_notification.default.confirm(...confirmationStrings,(()=>{window.location=deleteButton.getAttribute("href")}))}else;}));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)},document.addEventListener(_sortable_list.default.EVENTS.elementDrop,(event=>{if(!event.detail.positionChanged)return;const pendingPromise=new _pending.default("mod_feedback/questions:reorder"),itemOrder=getItemOrder(event.detail.element[0]);(0,_loadingicon.addIconToContainerRemoveOnCompletion)(event.detail.element[0],pendingPromise),(0,_repository.reorderQuestions)(moduleId,itemOrder).then((()=>(0,_str.getString)("questionmoved","mod_feedback"))).then(_toast.add).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})),initialized=!0}}));
define("mod_feedback/edit",["exports","core/loadingicon","core/notification","core/pending","core/prefetch","core/sortable_list","core/str","core/toast","mod_feedback/local/repository","core/templates"],(function(_exports,_loadingicon,_notification,_pending,_prefetch,_sortable_list,_str,_toast,_repository,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates);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"] span',Selectors_questionLabel='[data-region="questions-sortable-list"] .col-form-label',Selectors_actionsMenuData="[data-item-actions-menu]",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=async cmId=>{if(moduleId=cmId,initialized)return;(0,_prefetch.prefetchStrings)("core",["yes","no"]),(0,_prefetch.prefetchStrings)("admin",["confirmation"]),(0,_prefetch.prefetchStrings)("mod_feedback",["confirmdeleteitem","questionmoved","move_item"]),await enhanceEditForm(),document.addEventListener("click",(async event=>{const deleteButton=event.target.closest(Selectors_deleteQuestionButton);if(deleteButton){event.preventDefault();const confirmationStrings=await(0,_str.getStrings)([{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"core"},{key:"no",component:"core"}]);_notification.default.confirm(...confirmationStrings,(()=>{window.location=deleteButton.getAttribute("href")}))}else;}));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)},document.addEventListener(_sortable_list.default.EVENTS.elementDrop,(event=>{if(!event.detail.positionChanged)return;const pendingPromise=new _pending.default("mod_feedback/questions:reorder"),itemOrder=getItemOrder(event.detail.element[0]);(0,_loadingicon.addIconToContainerRemoveOnCompletion)(event.detail.element[0],pendingPromise),(0,_repository.reorderQuestions)(moduleId,itemOrder).then((()=>(0,_str.getString)("questionmoved","mod_feedback"))).then(_toast.add).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})),initialized=!0};const enhanceEditForm=async()=>{const questionLabels=document.querySelectorAll(Selectors_questionLabel),movetitle=await(0,_str.getString)("move_item","mod_feedback"),updates=Array.from(questionLabels).map((async container=>{const label=container.querySelector(Selectors_actionsMenuData);if(label)try{const contextData={movetitle:movetitle,label:label.parentElement.outerHTML,actionsmenu:JSON.parse(label.dataset.itemActionsMenu||"{}")};container.innerHTML=await _templates.default.render("mod_feedback/item_edit_enhanced_title",contextData)}catch(error){await _notification.default.exception(error)}}));await Promise.all(updates)}}));
//# sourceMappingURL=edit.min.js.map
File diff suppressed because one or more lines are too long
+37 -2
View File
@@ -31,12 +31,15 @@ import SortableList from 'core/sortable_list';
import {getString, getStrings} from 'core/str';
import {add as addToast} from 'core/toast';
import {reorderQuestions} from 'mod_feedback/local/repository';
import Templates from 'core/templates';
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"]',
sortableElementTitle: '[data-region="item-title"] span',
questionLabel: '[data-region="questions-sortable-list"] .col-form-label',
actionsMenuData: '[data-item-actions-menu]',
};
/**
@@ -72,7 +75,7 @@ let moduleId = null;
*
* @param {Integer} cmId
*/
export const init = (cmId) => {
export const init = async(cmId) => {
moduleId = cmId;
@@ -91,8 +94,11 @@ export const init = (cmId) => {
prefetchStrings('mod_feedback', [
'confirmdeleteitem',
'questionmoved',
'move_item',
]);
await enhanceEditForm();
// Add event listeners.
document.addEventListener('click', async event => {
@@ -133,3 +139,32 @@ export const init = (cmId) => {
initialized = true;
};
/**
* Enhance the edit form by adding a move item button and an action menu to each question.
*
* @returns {Promise<void>}
*/
const enhanceEditForm = async() => {
const questionLabels = document.querySelectorAll(Selectors.questionLabel);
const movetitle = await getString('move_item', 'mod_feedback');
const updates = Array.from(questionLabels).map(async(container) => {
const label = container.querySelector(Selectors.actionsMenuData);
if (!label) {
return;
}
try {
const contextData = {
movetitle,
label: label.parentElement.outerHTML,
actionsmenu: JSON.parse(label.dataset.itemActionsMenu || '{}'),
};
container.innerHTML = await Templates.render('mod_feedback/item_edit_enhanced_title', contextData);
} catch (error) {
await Notification.exception(error);
}
});
await Promise.all(updates);
};
@@ -472,14 +472,10 @@ class mod_feedback_complete_form extends moodleform {
foreach ($actions as $action) {
$menu->add($action);
}
$editmenu = $OUTPUT->render($menu);
$draghandle = $OUTPUT->render_from_template('core/drag_handle',
['movetitle' => get_string('move_item', 'mod_feedback')]);
$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 mb-3'));
$menudata = $menu->export_for_template($OUTPUT);
$element->setLabel(html_writer::span($element->getLabel(), '', [
'data-item-actions-menu' => json_encode($menudata),
]));
}
/**
@@ -0,0 +1,43 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template mod_feedback/item_edit_enhanced_title
The enhanced item title template when editing questions in the feedback activity.
Context variables required for this template:
* movetitle - The string for the title attribute of the move button.
* label - The question text.
* actionsmenu - Context data for the actions menu.
Example context (json):
{
"movetitle": "Move item",
"label": "Hello world!",
"actionsmenu": {}
}
}}
<div class="itemtitle d-flex mb-3">
<div class="itemhandle" data-drag-type="move">
{{> core/drag_handle}}
</div>
<div class="itemname" data-region="item-title">
{{{label}}}
</div>
{{#actionsmenu}}
<div class="itemactions">
{{> core/action_menu}}
</div>
{{/actionsmenu}}
</div>