From c080b4bf58d020a8a74939a98a292a5696fed4c3 Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Tue, 24 Jun 2025 09:07:18 +0700 Subject: [PATCH] MDL-85769 AI: Trap focus within AI drawer on smaller screens Co-authored-by: Laurent David --- .../courseassist/amd/build/placement.min.js | 2 +- .../amd/build/placement.min.js.map | 2 +- .../courseassist/amd/src/placement.js | 24 +++++++++++++++++++ .../lang/en/aiplacement_courseassist.php | 1 + .../courseassist/templates/drawer.mustache | 2 +- 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/ai/placement/courseassist/amd/build/placement.min.js b/ai/placement/courseassist/amd/build/placement.min.js index eff1a85cca5..1d59d313fa7 100644 --- a/ai/placement/courseassist/amd/build/placement.min.js +++ b/ai/placement/courseassist/amd/build/placement.min.js @@ -1,3 +1,3 @@ -define("aiplacement_courseassist/placement",["exports","core/templates","core/ajax","core/copy_to_clipboard","core/notification","aiplacement_courseassist/selectors","core_ai/policy","core_ai/helper","core/drawer_events","core/pubsub","core_message/message_drawer_helper"],(function(_exports,_templates,_ajax,_copy_to_clipboard,_notification,_selectors,_policy,_helper,_drawer_events,_pubsub,MessageDrawerHelper){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 _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_policy=_interopRequireDefault(_policy),_helper=_interopRequireDefault(_helper),_drawer_events=_interopRequireDefault(_drawer_events),MessageDrawerHelper=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(MessageDrawerHelper);var _default=class{constructor(userId,contextId){_defineProperty(this,"userId",void 0),_defineProperty(this,"contextId",void 0),this.userId=userId,this.contextId=contextId,this.aiDrawerElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER),this.aiDrawerBodyElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER_BODY),this.pageElement=document.querySelector(_selectors.default.ELEMENTS.PAGE),this.jumpToElement=document.querySelector(_selectors.default.ELEMENTS.JUMPTO),this.summaryActionElement=document.querySelector(_selectors.default.ACTIONS.SUMMARY),this.aiDrawerCloseElement=this.aiDrawerElement.querySelector(_selectors.default.ELEMENTS.AIDRAWER_CLOSE),this.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(async e=>{if(e.target.closest(_selectors.default.ACTIONS.SUMMARY)){e.preventDefault(),this.toggleAIDrawer(),this.summaryActionElement.focus();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displaySummary()}})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(()=>{this.isAIDrawerOpen()&&this.closeAIDrawer()})),this.jumpToElement.addEventListener("focus",(()=>{this.aiDrawerCloseElement.focus()})),this.aiDrawerElement.addEventListener("focus",(()=>{this.summaryActionElement.focus()}))}registerPolicyEventListeners(){const acceptAction=document.querySelector(_selectors.default.ACTIONS.ACCEPT),declineAction=document.querySelector(_selectors.default.ACTIONS.DECLINE);acceptAction&&acceptAction.addEventListener("click",(e=>{e.preventDefault(),this.acceptPolicy().then((()=>this.displaySummary())).catch(_notification.default.exception)})),declineAction&&declineAction.addEventListener("click",(e=>{e.preventDefault(),this.closeAIDrawer()}))}registerErrorEventListeners(){const retryAction=document.querySelector(_selectors.default.ACTIONS.RETRY);retryAction&&retryAction.addEventListener("click",(e=>{e.preventDefault(),this.aiDrawerBodyElement.dataset.hasdata="0",this.displaySummary()}))}registerResponseEventListeners(){const regenerateAction=document.querySelector(_selectors.default.ACTIONS.REGENERATE);regenerateAction&®enerateAction.addEventListener("click",(e=>{e.preventDefault(),this.aiDrawerBodyElement.dataset.hasdata="0",this.displaySummary()}))}registerLoadingEventListeners(){const cancelAction=document.querySelector(_selectors.default.ACTIONS.CANCEL);cancelAction&&cancelAction.addEventListener("click",(e=>{e.preventDefault(),this.setRequestCancelled(),this.toggleAIDrawer()}))}isAIDrawerOpen(){return this.aiDrawerElement.classList.contains("show")}isRequestCancelled(){return"1"===this.aiDrawerBodyElement.dataset.cancelled}setRequestCancelled(){this.aiDrawerBodyElement.dataset.cancelled="1"}openAIDrawer(){MessageDrawerHelper.hide(),this.aiDrawerElement.classList.add("show"),this.aiDrawerElement.setAttribute("tabindex","0"),this.aiDrawerBodyElement.setAttribute("aria-live","polite"),this.pageElement.classList.contains("show-drawer-right")||this.addPadding(),this.jumpToElement.setAttribute("tabindex",0),this.jumpToElement.focus()}closeAIDrawer(){this.aiDrawerElement.classList.remove("show"),this.aiDrawerElement.setAttribute("tabindex","-1"),this.aiDrawerBodyElement.removeAttribute("aria-live"),this.pageElement.classList.contains("show-drawer-right")&&"1"===this.aiDrawerBodyElement.dataset.removepadding&&this.removePadding(),this.jumpToElement.setAttribute("tabindex",-1),this.summaryActionElement.focus()}toggleAIDrawer(){this.isAIDrawerOpen()?this.closeAIDrawer():this.openAIDrawer()}addPadding(){this.pageElement.classList.add("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="1"}removePadding(){this.pageElement.classList.remove("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="0"}async isPolicyAccepted(){return await _policy.default.getPolicyStatus(this.userId)}acceptPolicy(){return _policy.default.acceptPolicy()}hasGeneratedContent(){return"1"===this.aiDrawerBodyElement.dataset.hasdata}displayPolicy(){_templates.default.render("core_ai/policyblock",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerPolicyEventListeners()})).catch(_notification.default.exception)}displayLoading(){_templates.default.render("aiplacement_courseassist/loading",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerLoadingEventListeners()})).catch(_notification.default.exception)}async displaySummary(){if(!this.hasGeneratedContent()){this.displayLoading(),this.aiDrawerBodyElement.innerHTML="";const request={methodname:"aiplacement_courseassist_summarise_text",args:{contextid:this.contextId,prompttext:this.getTextContent()}};try{const responseObj=await _ajax.default.call([request])[0];if(responseObj.error)return void this.displayError();if(!this.isRequestCancelled()){const generatedContent=_helper.default.replaceLineBreaks(responseObj.generatedcontent);return void this.displayResponse(generatedContent)}this.aiDrawerBodyElement.dataset.cancelled="0"}catch(error){window.console.log(error),this.displayError()}}}displayResponse(content){_templates.default.render("aiplacement_courseassist/response",{content:content}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.aiDrawerBodyElement.dataset.hasdata="1",this.registerResponseEventListeners()})).catch(_notification.default.exception)}displayError(){_templates.default.render("aiplacement_courseassist/error",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerErrorEventListeners()})).catch(_notification.default.exception)}getTextContent(){const mainRegion=document.querySelector(_selectors.default.ELEMENTS.MAIN_REGION);return mainRegion.innerText||mainRegion.textContent}};return _exports.default=_default,_exports.default})); +define("aiplacement_courseassist/placement",["exports","core/templates","core/ajax","core/copy_to_clipboard","core/notification","aiplacement_courseassist/selectors","core_ai/policy","core_ai/helper","core/drawer_events","core/pubsub","core_message/message_drawer_helper","core/local/aria/focuslock","core/pagehelpers"],(function(_exports,_templates,_ajax,_copy_to_clipboard,_notification,_selectors,_policy,_helper,_drawer_events,_pubsub,MessageDrawerHelper,FocusLock,_pagehelpers){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}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_selectors=_interopRequireDefault(_selectors),_policy=_interopRequireDefault(_policy),_helper=_interopRequireDefault(_helper),_drawer_events=_interopRequireDefault(_drawer_events),MessageDrawerHelper=_interopRequireWildcard(MessageDrawerHelper),FocusLock=_interopRequireWildcard(FocusLock);var _default=class{constructor(userId,contextId){_defineProperty(this,"userId",void 0),_defineProperty(this,"contextId",void 0),this.userId=userId,this.contextId=contextId,this.aiDrawerElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER),this.aiDrawerBodyElement=document.querySelector(_selectors.default.ELEMENTS.AIDRAWER_BODY),this.pageElement=document.querySelector(_selectors.default.ELEMENTS.PAGE),this.jumpToElement=document.querySelector(_selectors.default.ELEMENTS.JUMPTO),this.summaryActionElement=document.querySelector(_selectors.default.ACTIONS.SUMMARY),this.aiDrawerCloseElement=this.aiDrawerElement.querySelector(_selectors.default.ELEMENTS.AIDRAWER_CLOSE),this.isDrawerFocusLocked=!1,this.registerEventListeners()}registerEventListeners(){document.addEventListener("click",(async e=>{if(e.target.closest(_selectors.default.ACTIONS.SUMMARY)){e.preventDefault(),this.toggleAIDrawer(),this.summaryActionElement.focus();if(!await this.isPolicyAccepted())return void this.displayPolicy();this.displaySummary()}})),document.addEventListener("keydown",(e=>{this.isAIDrawerOpen()&&"Escape"===e.key&&this.closeAIDrawer()})),(0,_pubsub.subscribe)(_drawer_events.default.DRAWER_SHOWN,(()=>{this.isAIDrawerOpen()&&this.closeAIDrawer()})),this.jumpToElement.addEventListener("focus",(()=>{this.aiDrawerCloseElement.focus()})),this.aiDrawerElement.addEventListener("focus",(()=>{this.summaryActionElement.focus()}))}registerPolicyEventListeners(){const acceptAction=document.querySelector(_selectors.default.ACTIONS.ACCEPT),declineAction=document.querySelector(_selectors.default.ACTIONS.DECLINE);acceptAction&&acceptAction.addEventListener("click",(e=>{e.preventDefault(),this.acceptPolicy().then((()=>this.displaySummary())).catch(_notification.default.exception)})),declineAction&&declineAction.addEventListener("click",(e=>{e.preventDefault(),this.closeAIDrawer()}))}registerErrorEventListeners(){const retryAction=document.querySelector(_selectors.default.ACTIONS.RETRY);retryAction&&retryAction.addEventListener("click",(e=>{e.preventDefault(),this.aiDrawerBodyElement.dataset.hasdata="0",this.displaySummary()}))}registerResponseEventListeners(){const regenerateAction=document.querySelector(_selectors.default.ACTIONS.REGENERATE);regenerateAction&®enerateAction.addEventListener("click",(e=>{e.preventDefault(),this.aiDrawerBodyElement.dataset.hasdata="0",this.displaySummary()}))}registerLoadingEventListeners(){const cancelAction=document.querySelector(_selectors.default.ACTIONS.CANCEL);cancelAction&&cancelAction.addEventListener("click",(e=>{e.preventDefault(),this.setRequestCancelled(),this.toggleAIDrawer()}))}isAIDrawerOpen(){return this.aiDrawerElement.classList.contains("show")}isRequestCancelled(){return"1"===this.aiDrawerBodyElement.dataset.cancelled}setRequestCancelled(){this.aiDrawerBodyElement.dataset.cancelled="1"}openAIDrawer(){MessageDrawerHelper.hide(),this.aiDrawerElement.classList.add("show"),this.aiDrawerElement.setAttribute("tabindex","0"),this.aiDrawerBodyElement.setAttribute("aria-live","polite"),this.pageElement.classList.contains("show-drawer-right")||this.addPadding(),this.jumpToElement.setAttribute("tabindex",0),this.jumpToElement.focus(),(0,_pagehelpers.isSmall)()&&(FocusLock.trapFocus(this.aiDrawerElement),this.aiDrawerElement.setAttribute("aria-modal","true"),this.aiDrawerElement.setAttribute("role","dialog"),this.isDrawerFocusLocked=!0)}closeAIDrawer(){this.isDrawerFocusLocked&&(FocusLock.untrapFocus(),this.aiDrawerElement.removeAttribute("aria-modal"),this.aiDrawerElement.setAttribute("role","region")),this.aiDrawerElement.classList.remove("show"),this.aiDrawerElement.setAttribute("tabindex","-1"),this.aiDrawerBodyElement.removeAttribute("aria-live"),this.pageElement.classList.contains("show-drawer-right")&&"1"===this.aiDrawerBodyElement.dataset.removepadding&&this.removePadding(),this.jumpToElement.setAttribute("tabindex",-1),this.summaryActionElement.focus()}toggleAIDrawer(){this.isAIDrawerOpen()?this.closeAIDrawer():this.openAIDrawer()}addPadding(){this.pageElement.classList.add("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="1"}removePadding(){this.pageElement.classList.remove("show-drawer-right"),this.aiDrawerBodyElement.dataset.removepadding="0"}async isPolicyAccepted(){return await _policy.default.getPolicyStatus(this.userId)}acceptPolicy(){return _policy.default.acceptPolicy()}hasGeneratedContent(){return"1"===this.aiDrawerBodyElement.dataset.hasdata}displayPolicy(){_templates.default.render("core_ai/policyblock",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerPolicyEventListeners()})).catch(_notification.default.exception)}displayLoading(){_templates.default.render("aiplacement_courseassist/loading",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerLoadingEventListeners()})).catch(_notification.default.exception)}async displaySummary(){if(!this.hasGeneratedContent()){this.displayLoading(),this.aiDrawerBodyElement.innerHTML="";const request={methodname:"aiplacement_courseassist_summarise_text",args:{contextid:this.contextId,prompttext:this.getTextContent()}};try{const responseObj=await _ajax.default.call([request])[0];if(responseObj.error)return void this.displayError();if(!this.isRequestCancelled()){const generatedContent=_helper.default.replaceLineBreaks(responseObj.generatedcontent);return void this.displayResponse(generatedContent)}this.aiDrawerBodyElement.dataset.cancelled="0"}catch(error){window.console.log(error),this.displayError()}}}displayResponse(content){_templates.default.render("aiplacement_courseassist/response",{content:content}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.aiDrawerBodyElement.dataset.hasdata="1",this.registerResponseEventListeners()})).catch(_notification.default.exception)}displayError(){_templates.default.render("aiplacement_courseassist/error",{}).then((html=>{this.aiDrawerBodyElement.innerHTML=html,this.registerErrorEventListeners()})).catch(_notification.default.exception)}getTextContent(){const mainRegion=document.querySelector(_selectors.default.ELEMENTS.MAIN_REGION);return mainRegion.innerText||mainRegion.textContent}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=placement.min.js.map \ No newline at end of file diff --git a/ai/placement/courseassist/amd/build/placement.min.js.map b/ai/placement/courseassist/amd/build/placement.min.js.map index f00f6969e90..bced4f3e7e9 100644 --- a/ai/placement/courseassist/amd/build/placement.min.js.map +++ b/ai/placement/courseassist/amd/build/placement.min.js.map @@ -1 +1 @@ -{"version":3,"file":"placement.min.js","sources":["../src/placement.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 load and render the tools for the AI assist plugin.\n *\n * @module aiplacement_courseassist/placement\n * @copyright 2024 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Ajax from 'core/ajax';\nimport 'core/copy_to_clipboard';\nimport Notification from 'core/notification';\nimport Selectors from 'aiplacement_courseassist/selectors';\nimport Policy from 'core_ai/policy';\nimport AIHelper from 'core_ai/helper';\nimport DrawerEvents from 'core/drawer_events';\nimport {subscribe} from 'core/pubsub';\nimport * as MessageDrawerHelper from 'core_message/message_drawer_helper';\n\nconst AICourseAssist = class {\n\n /**\n * The user ID.\n * @type {Integer}\n */\n userId;\n /**\n * The context ID.\n * @type {Integer}\n */\n contextId;\n\n /**\n * Constructor.\n * @param {Integer} userId The user ID.\n * @param {Integer} contextId The context ID.\n */\n constructor(userId, contextId) {\n this.userId = userId;\n this.contextId = contextId;\n\n this.aiDrawerElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER);\n this.aiDrawerBodyElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER_BODY);\n this.pageElement = document.querySelector(Selectors.ELEMENTS.PAGE);\n this.jumpToElement = document.querySelector(Selectors.ELEMENTS.JUMPTO);\n this.summaryActionElement = document.querySelector(Selectors.ACTIONS.SUMMARY);\n this.aiDrawerCloseElement = this.aiDrawerElement.querySelector(Selectors.ELEMENTS.AIDRAWER_CLOSE);\n\n this.registerEventListeners();\n }\n\n /**\n * Register event listeners.\n */\n registerEventListeners() {\n document.addEventListener('click', async(e) => {\n const summariseAction = e.target.closest(Selectors.ACTIONS.SUMMARY);\n if (summariseAction) {\n e.preventDefault();\n this.toggleAIDrawer();\n this.summaryActionElement.focus();\n const isPolicyAccepted = await this.isPolicyAccepted();\n if (!isPolicyAccepted) {\n // Display policy.\n this.displayPolicy();\n return;\n }\n // Display summary.\n this.displaySummary();\n }\n });\n\n // Close AI drawer if message drawer is shown.\n subscribe(DrawerEvents.DRAWER_SHOWN, () => {\n if (this.isAIDrawerOpen()) {\n this.closeAIDrawer();\n }\n });\n\n // Focus on the AI drawer's close button when the jump-to element is focused.\n this.jumpToElement.addEventListener('focus', () => {\n this.aiDrawerCloseElement.focus();\n });\n\n // Focus on the summary action element when the AI drawer is focused.\n this.aiDrawerElement.addEventListener('focus', () => {\n this.summaryActionElement.focus();\n });\n }\n\n /**\n * Register event listeners for the policy.\n */\n registerPolicyEventListeners() {\n const acceptAction = document.querySelector(Selectors.ACTIONS.ACCEPT);\n const declineAction = document.querySelector(Selectors.ACTIONS.DECLINE);\n if (acceptAction) {\n acceptAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.acceptPolicy().then(() => {\n return this.displaySummary();\n }).catch(Notification.exception);\n });\n }\n if (declineAction) {\n declineAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.closeAIDrawer();\n });\n }\n }\n\n /**\n * Register event listeners for the error.\n */\n registerErrorEventListeners() {\n const retryAction = document.querySelector(Selectors.ACTIONS.RETRY);\n if (retryAction) {\n retryAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.aiDrawerBodyElement.dataset.hasdata = '0';\n this.displaySummary();\n });\n }\n }\n\n /**\n * Register event listeners for the response.\n */\n registerResponseEventListeners() {\n const regenerateAction = document.querySelector(Selectors.ACTIONS.REGENERATE);\n if (regenerateAction) {\n regenerateAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.aiDrawerBodyElement.dataset.hasdata = '0';\n this.displaySummary();\n });\n }\n }\n\n registerLoadingEventListeners() {\n const cancelAction = document.querySelector(Selectors.ACTIONS.CANCEL);\n if (cancelAction) {\n cancelAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.setRequestCancelled();\n this.toggleAIDrawer();\n });\n }\n }\n\n /**\n * Check if the AI drawer is open.\n * @return {boolean} True if the AI drawer is open, false otherwise.\n */\n isAIDrawerOpen() {\n return this.aiDrawerElement.classList.contains('show');\n }\n\n /**\n * Check if the request is cancelled.\n * @return {boolean} True if the request is cancelled, false otherwise.\n */\n isRequestCancelled() {\n return this.aiDrawerBodyElement.dataset.cancelled === '1';\n }\n\n setRequestCancelled() {\n this.aiDrawerBodyElement.dataset.cancelled = '1';\n }\n\n /**\n * Open the AI drawer.\n */\n openAIDrawer() {\n // Close message drawer if it is shown.\n MessageDrawerHelper.hide();\n this.aiDrawerElement.classList.add('show');\n this.aiDrawerElement.setAttribute('tabindex', '0');\n this.aiDrawerBodyElement.setAttribute('aria-live', 'polite');\n if (!this.pageElement.classList.contains('show-drawer-right')) {\n this.addPadding();\n }\n this.jumpToElement.setAttribute('tabindex', 0);\n this.jumpToElement.focus();\n }\n\n /**\n * Close the AI drawer.\n */\n closeAIDrawer() {\n this.aiDrawerElement.classList.remove('show');\n this.aiDrawerElement.setAttribute('tabindex', '-1');\n this.aiDrawerBodyElement.removeAttribute('aria-live');\n if (this.pageElement.classList.contains('show-drawer-right') && this.aiDrawerBodyElement.dataset.removepadding === '1') {\n this.removePadding();\n }\n this.jumpToElement.setAttribute('tabindex', -1);\n this.summaryActionElement.focus();\n }\n\n /**\n * Toggle the AI drawer.\n */\n toggleAIDrawer() {\n if (this.isAIDrawerOpen()) {\n this.closeAIDrawer();\n } else {\n this.openAIDrawer();\n }\n }\n\n /**\n * Add padding to the page to make space for the AI drawer.\n */\n addPadding() {\n this.pageElement.classList.add('show-drawer-right');\n this.aiDrawerBodyElement.dataset.removepadding = '1';\n }\n\n /**\n * Remove padding from the page.\n */\n removePadding() {\n this.pageElement.classList.remove('show-drawer-right');\n this.aiDrawerBodyElement.dataset.removepadding = '0';\n }\n\n /**\n * Check if the policy is accepted.\n * @return {bool} True if the policy is accepted, false otherwise.\n */\n async isPolicyAccepted() {\n return await Policy.getPolicyStatus(this.userId);\n }\n\n /**\n * Accept the policy.\n * @return {Promise}\n */\n acceptPolicy() {\n return Policy.acceptPolicy();\n }\n\n /**\n * Check if the AI drawer has generated content or not.\n * @return {boolean} True if the AI drawer has generated content, false otherwise.\n */\n hasGeneratedContent() {\n return this.aiDrawerBodyElement.dataset.hasdata === '1';\n }\n\n /**\n * Display the policy.\n */\n displayPolicy() {\n Templates.render('core_ai/policyblock', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerPolicyEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the loading spinner.\n */\n displayLoading() {\n Templates.render('aiplacement_courseassist/loading', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerLoadingEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the summary.\n */\n async displaySummary() {\n if (!this.hasGeneratedContent()) {\n // Display loading spinner.\n this.displayLoading();\n // Clear the drawer content to prevent sending some unnecessary content.\n this.aiDrawerBodyElement.innerHTML = '';\n const request = {\n methodname: 'aiplacement_courseassist_summarise_text',\n args: {\n contextid: this.contextId,\n prompttext: this.getTextContent(),\n }\n };\n try {\n const responseObj = await Ajax.call([request])[0];\n if (responseObj.error) {\n this.displayError();\n return;\n } else {\n if (!this.isRequestCancelled()) {\n // Replace double line breaks with
and with

for paragraphs.\n const generatedContent = AIHelper.replaceLineBreaks(responseObj.generatedcontent);\n this.displayResponse(generatedContent);\n return;\n } else {\n this.aiDrawerBodyElement.dataset.cancelled = '0';\n }\n }\n } catch (error) {\n window.console.log(error);\n this.displayError();\n }\n }\n }\n\n /**\n * Display the response.\n * @param {String} content The content to display.\n */\n displayResponse(content) {\n Templates.render('aiplacement_courseassist/response', {content: content}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.aiDrawerBodyElement.dataset.hasdata = '1';\n this.registerResponseEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the error.\n */\n displayError() {\n Templates.render('aiplacement_courseassist/error', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerErrorEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Get the text content of the main region.\n * @return {String} The text content.\n */\n getTextContent() {\n const mainRegion = document.querySelector(Selectors.ELEMENTS.MAIN_REGION);\n return mainRegion.innerText || mainRegion.textContent;\n }\n};\n\nexport default AICourseAssist;\n"],"names":["constructor","userId","contextId","aiDrawerElement","document","querySelector","Selectors","ELEMENTS","AIDRAWER","aiDrawerBodyElement","AIDRAWER_BODY","pageElement","PAGE","jumpToElement","JUMPTO","summaryActionElement","ACTIONS","SUMMARY","aiDrawerCloseElement","this","AIDRAWER_CLOSE","registerEventListeners","addEventListener","async","e","target","closest","preventDefault","toggleAIDrawer","focus","isPolicyAccepted","displayPolicy","displaySummary","DrawerEvents","DRAWER_SHOWN","isAIDrawerOpen","closeAIDrawer","registerPolicyEventListeners","acceptAction","ACCEPT","declineAction","DECLINE","acceptPolicy","then","catch","Notification","exception","registerErrorEventListeners","retryAction","RETRY","dataset","hasdata","registerResponseEventListeners","regenerateAction","REGENERATE","registerLoadingEventListeners","cancelAction","CANCEL","setRequestCancelled","classList","contains","isRequestCancelled","cancelled","openAIDrawer","MessageDrawerHelper","hide","add","setAttribute","addPadding","remove","removeAttribute","removepadding","removePadding","Policy","getPolicyStatus","hasGeneratedContent","render","html","innerHTML","displayLoading","request","methodname","args","contextid","prompttext","getTextContent","responseObj","Ajax","call","error","displayError","generatedContent","AIHelper","replaceLineBreaks","generatedcontent","displayResponse","window","console","log","content","mainRegion","MAIN_REGION","innerText","textContent"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"placement.min.js","sources":["../src/placement.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 load and render the tools for the AI assist plugin.\n *\n * @module aiplacement_courseassist/placement\n * @copyright 2024 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Ajax from 'core/ajax';\nimport 'core/copy_to_clipboard';\nimport Notification from 'core/notification';\nimport Selectors from 'aiplacement_courseassist/selectors';\nimport Policy from 'core_ai/policy';\nimport AIHelper from 'core_ai/helper';\nimport DrawerEvents from 'core/drawer_events';\nimport {subscribe} from 'core/pubsub';\nimport * as MessageDrawerHelper from 'core_message/message_drawer_helper';\nimport * as FocusLock from 'core/local/aria/focuslock';\nimport {isSmall} from \"core/pagehelpers\";\n\nconst AICourseAssist = class {\n\n /**\n * The user ID.\n * @type {Integer}\n */\n userId;\n /**\n * The context ID.\n * @type {Integer}\n */\n contextId;\n\n /**\n * Constructor.\n * @param {Integer} userId The user ID.\n * @param {Integer} contextId The context ID.\n */\n constructor(userId, contextId) {\n this.userId = userId;\n this.contextId = contextId;\n\n this.aiDrawerElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER);\n this.aiDrawerBodyElement = document.querySelector(Selectors.ELEMENTS.AIDRAWER_BODY);\n this.pageElement = document.querySelector(Selectors.ELEMENTS.PAGE);\n this.jumpToElement = document.querySelector(Selectors.ELEMENTS.JUMPTO);\n this.summaryActionElement = document.querySelector(Selectors.ACTIONS.SUMMARY);\n this.aiDrawerCloseElement = this.aiDrawerElement.querySelector(Selectors.ELEMENTS.AIDRAWER_CLOSE);\n this.isDrawerFocusLocked = false;\n\n this.registerEventListeners();\n }\n\n /**\n * Register event listeners.\n */\n registerEventListeners() {\n document.addEventListener('click', async(e) => {\n const summariseAction = e.target.closest(Selectors.ACTIONS.SUMMARY);\n if (summariseAction) {\n e.preventDefault();\n this.toggleAIDrawer();\n this.summaryActionElement.focus();\n const isPolicyAccepted = await this.isPolicyAccepted();\n if (!isPolicyAccepted) {\n // Display policy.\n this.displayPolicy();\n return;\n }\n // Display summary.\n this.displaySummary();\n }\n });\n\n document.addEventListener('keydown', e => {\n if (this.isAIDrawerOpen() && e.key === 'Escape') {\n this.closeAIDrawer();\n }\n });\n\n // Close AI drawer if message drawer is shown.\n subscribe(DrawerEvents.DRAWER_SHOWN, () => {\n if (this.isAIDrawerOpen()) {\n this.closeAIDrawer();\n }\n });\n\n // Focus on the AI drawer's close button when the jump-to element is focused.\n this.jumpToElement.addEventListener('focus', () => {\n this.aiDrawerCloseElement.focus();\n });\n\n // Focus on the summary action element when the AI drawer is focused.\n this.aiDrawerElement.addEventListener('focus', () => {\n this.summaryActionElement.focus();\n });\n }\n\n /**\n * Register event listeners for the policy.\n */\n registerPolicyEventListeners() {\n const acceptAction = document.querySelector(Selectors.ACTIONS.ACCEPT);\n const declineAction = document.querySelector(Selectors.ACTIONS.DECLINE);\n if (acceptAction) {\n acceptAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.acceptPolicy().then(() => {\n return this.displaySummary();\n }).catch(Notification.exception);\n });\n }\n if (declineAction) {\n declineAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.closeAIDrawer();\n });\n }\n }\n\n /**\n * Register event listeners for the error.\n */\n registerErrorEventListeners() {\n const retryAction = document.querySelector(Selectors.ACTIONS.RETRY);\n if (retryAction) {\n retryAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.aiDrawerBodyElement.dataset.hasdata = '0';\n this.displaySummary();\n });\n }\n }\n\n /**\n * Register event listeners for the response.\n */\n registerResponseEventListeners() {\n const regenerateAction = document.querySelector(Selectors.ACTIONS.REGENERATE);\n if (regenerateAction) {\n regenerateAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.aiDrawerBodyElement.dataset.hasdata = '0';\n this.displaySummary();\n });\n }\n }\n\n registerLoadingEventListeners() {\n const cancelAction = document.querySelector(Selectors.ACTIONS.CANCEL);\n if (cancelAction) {\n cancelAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.setRequestCancelled();\n this.toggleAIDrawer();\n });\n }\n }\n\n /**\n * Check if the AI drawer is open.\n * @return {boolean} True if the AI drawer is open, false otherwise.\n */\n isAIDrawerOpen() {\n return this.aiDrawerElement.classList.contains('show');\n }\n\n /**\n * Check if the request is cancelled.\n * @return {boolean} True if the request is cancelled, false otherwise.\n */\n isRequestCancelled() {\n return this.aiDrawerBodyElement.dataset.cancelled === '1';\n }\n\n setRequestCancelled() {\n this.aiDrawerBodyElement.dataset.cancelled = '1';\n }\n\n /**\n * Open the AI drawer.\n */\n openAIDrawer() {\n // Close message drawer if it is shown.\n MessageDrawerHelper.hide();\n this.aiDrawerElement.classList.add('show');\n this.aiDrawerElement.setAttribute('tabindex', '0');\n this.aiDrawerBodyElement.setAttribute('aria-live', 'polite');\n if (!this.pageElement.classList.contains('show-drawer-right')) {\n this.addPadding();\n }\n this.jumpToElement.setAttribute('tabindex', 0);\n this.jumpToElement.focus();\n\n // If the AI drawer is opened on a small screen, we need to trap the focus tab within the AI drawer.\n if (isSmall()) {\n FocusLock.trapFocus(this.aiDrawerElement);\n this.aiDrawerElement.setAttribute('aria-modal', 'true');\n this.aiDrawerElement.setAttribute('role', 'dialog');\n this.isDrawerFocusLocked = true;\n }\n }\n\n /**\n * Close the AI drawer.\n */\n closeAIDrawer() {\n // Untrap focus if it was locked.\n if (this.isDrawerFocusLocked) {\n FocusLock.untrapFocus();\n this.aiDrawerElement.removeAttribute('aria-modal');\n this.aiDrawerElement.setAttribute('role', 'region');\n }\n\n this.aiDrawerElement.classList.remove('show');\n this.aiDrawerElement.setAttribute('tabindex', '-1');\n this.aiDrawerBodyElement.removeAttribute('aria-live');\n if (this.pageElement.classList.contains('show-drawer-right') && this.aiDrawerBodyElement.dataset.removepadding === '1') {\n this.removePadding();\n }\n this.jumpToElement.setAttribute('tabindex', -1);\n this.summaryActionElement.focus();\n }\n\n /**\n * Toggle the AI drawer.\n */\n toggleAIDrawer() {\n if (this.isAIDrawerOpen()) {\n this.closeAIDrawer();\n } else {\n this.openAIDrawer();\n }\n }\n\n /**\n * Add padding to the page to make space for the AI drawer.\n */\n addPadding() {\n this.pageElement.classList.add('show-drawer-right');\n this.aiDrawerBodyElement.dataset.removepadding = '1';\n }\n\n /**\n * Remove padding from the page.\n */\n removePadding() {\n this.pageElement.classList.remove('show-drawer-right');\n this.aiDrawerBodyElement.dataset.removepadding = '0';\n }\n\n /**\n * Check if the policy is accepted.\n * @return {bool} True if the policy is accepted, false otherwise.\n */\n async isPolicyAccepted() {\n return await Policy.getPolicyStatus(this.userId);\n }\n\n /**\n * Accept the policy.\n * @return {Promise}\n */\n acceptPolicy() {\n return Policy.acceptPolicy();\n }\n\n /**\n * Check if the AI drawer has generated content or not.\n * @return {boolean} True if the AI drawer has generated content, false otherwise.\n */\n hasGeneratedContent() {\n return this.aiDrawerBodyElement.dataset.hasdata === '1';\n }\n\n /**\n * Display the policy.\n */\n displayPolicy() {\n Templates.render('core_ai/policyblock', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerPolicyEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the loading spinner.\n */\n displayLoading() {\n Templates.render('aiplacement_courseassist/loading', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerLoadingEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the summary.\n */\n async displaySummary() {\n if (!this.hasGeneratedContent()) {\n // Display loading spinner.\n this.displayLoading();\n // Clear the drawer content to prevent sending some unnecessary content.\n this.aiDrawerBodyElement.innerHTML = '';\n const request = {\n methodname: 'aiplacement_courseassist_summarise_text',\n args: {\n contextid: this.contextId,\n prompttext: this.getTextContent(),\n }\n };\n try {\n const responseObj = await Ajax.call([request])[0];\n if (responseObj.error) {\n this.displayError();\n return;\n } else {\n if (!this.isRequestCancelled()) {\n // Replace double line breaks with
and with

for paragraphs.\n const generatedContent = AIHelper.replaceLineBreaks(responseObj.generatedcontent);\n this.displayResponse(generatedContent);\n return;\n } else {\n this.aiDrawerBodyElement.dataset.cancelled = '0';\n }\n }\n } catch (error) {\n window.console.log(error);\n this.displayError();\n }\n }\n }\n\n /**\n * Display the response.\n * @param {String} content The content to display.\n */\n displayResponse(content) {\n Templates.render('aiplacement_courseassist/response', {content: content}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.aiDrawerBodyElement.dataset.hasdata = '1';\n this.registerResponseEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Display the error.\n */\n displayError() {\n Templates.render('aiplacement_courseassist/error', {}).then((html) => {\n this.aiDrawerBodyElement.innerHTML = html;\n this.registerErrorEventListeners();\n return;\n }).catch(Notification.exception);\n }\n\n /**\n * Get the text content of the main region.\n * @return {String} The text content.\n */\n getTextContent() {\n const mainRegion = document.querySelector(Selectors.ELEMENTS.MAIN_REGION);\n return mainRegion.innerText || mainRegion.textContent;\n }\n};\n\nexport default AICourseAssist;\n"],"names":["constructor","userId","contextId","aiDrawerElement","document","querySelector","Selectors","ELEMENTS","AIDRAWER","aiDrawerBodyElement","AIDRAWER_BODY","pageElement","PAGE","jumpToElement","JUMPTO","summaryActionElement","ACTIONS","SUMMARY","aiDrawerCloseElement","this","AIDRAWER_CLOSE","isDrawerFocusLocked","registerEventListeners","addEventListener","async","e","target","closest","preventDefault","toggleAIDrawer","focus","isPolicyAccepted","displayPolicy","displaySummary","isAIDrawerOpen","key","closeAIDrawer","DrawerEvents","DRAWER_SHOWN","registerPolicyEventListeners","acceptAction","ACCEPT","declineAction","DECLINE","acceptPolicy","then","catch","Notification","exception","registerErrorEventListeners","retryAction","RETRY","dataset","hasdata","registerResponseEventListeners","regenerateAction","REGENERATE","registerLoadingEventListeners","cancelAction","CANCEL","setRequestCancelled","classList","contains","isRequestCancelled","cancelled","openAIDrawer","MessageDrawerHelper","hide","add","setAttribute","addPadding","FocusLock","trapFocus","untrapFocus","removeAttribute","remove","removepadding","removePadding","Policy","getPolicyStatus","hasGeneratedContent","render","html","innerHTML","displayLoading","request","methodname","args","contextid","prompttext","getTextContent","responseObj","Ajax","call","error","displayError","generatedContent","AIHelper","replaceLineBreaks","generatedcontent","displayResponse","window","console","log","content","mainRegion","MAIN_REGION","innerText","textContent"],"mappings":"qpEAoCuB,MAkBnBA,YAAYC,OAAQC,+FACXD,OAASA,YACTC,UAAYA,eAEZC,gBAAkBC,SAASC,cAAcC,mBAAUC,SAASC,eAC5DC,oBAAsBL,SAASC,cAAcC,mBAAUC,SAASG,oBAChEC,YAAcP,SAASC,cAAcC,mBAAUC,SAASK,WACxDC,cAAgBT,SAASC,cAAcC,mBAAUC,SAASO,aAC1DC,qBAAuBX,SAASC,cAAcC,mBAAUU,QAAQC,cAChEC,qBAAuBC,KAAKhB,gBAAgBE,cAAcC,mBAAUC,SAASa,qBAC7EC,qBAAsB,OAEtBC,yBAMTA,yBACIlB,SAASmB,iBAAiB,SAASC,MAAAA,OACPC,EAAEC,OAAOC,QAAQrB,mBAAUU,QAAQC,SACtC,CACjBQ,EAAEG,sBACGC,sBACAd,qBAAqBe,kBACKX,KAAKY,oCAG3BC,qBAIJC,qBAIb7B,SAASmB,iBAAiB,WAAWE,IAC7BN,KAAKe,kBAA8B,WAAVT,EAAEU,UACtBC,yCAKHC,uBAAaC,cAAc,KAC7BnB,KAAKe,uBACAE,wBAKRvB,cAAcU,iBAAiB,SAAS,UACpCL,qBAAqBY,gBAIzB3B,gBAAgBoB,iBAAiB,SAAS,UACtCR,qBAAqBe,WAOlCS,qCACUC,aAAepC,SAASC,cAAcC,mBAAUU,QAAQyB,QACxDC,cAAgBtC,SAASC,cAAcC,mBAAUU,QAAQ2B,SAC3DH,cACAA,aAAajB,iBAAiB,SAAUE,IACpCA,EAAEG,sBACGgB,eAAeC,MAAK,IACd1B,KAAKc,mBACba,MAAMC,sBAAaC,cAG1BN,eACAA,cAAcnB,iBAAiB,SAAUE,IACrCA,EAAEG,sBACGQ,mBAQjBa,oCACUC,YAAc9C,SAASC,cAAcC,mBAAUU,QAAQmC,OACzDD,aACAA,YAAY3B,iBAAiB,SAAUE,IACnCA,EAAEG,sBACGnB,oBAAoB2C,QAAQC,QAAU,SACtCpB,oBAQjBqB,uCACUC,iBAAmBnD,SAASC,cAAcC,mBAAUU,QAAQwC,YAC9DD,kBACAA,iBAAiBhC,iBAAiB,SAAUE,IACxCA,EAAEG,sBACGnB,oBAAoB2C,QAAQC,QAAU,SACtCpB,oBAKjBwB,sCACUC,aAAetD,SAASC,cAAcC,mBAAUU,QAAQ2C,QAC1DD,cACAA,aAAanC,iBAAiB,SAAUE,IACpCA,EAAEG,sBACGgC,2BACA/B,oBASjBK,wBACWf,KAAKhB,gBAAgB0D,UAAUC,SAAS,QAOnDC,2BAC0D,MAA/C5C,KAAKV,oBAAoB2C,QAAQY,UAG5CJ,2BACSnD,oBAAoB2C,QAAQY,UAAY,IAMjDC,eAEIC,oBAAoBC,YACfhE,gBAAgB0D,UAAUO,IAAI,aAC9BjE,gBAAgBkE,aAAa,WAAY,UACzC5D,oBAAoB4D,aAAa,YAAa,UAC9ClD,KAAKR,YAAYkD,UAAUC,SAAS,2BAChCQ,kBAEJzD,cAAcwD,aAAa,WAAY,QACvCxD,cAAciB,SAGf,4BACAyC,UAAUC,UAAUrD,KAAKhB,sBACpBA,gBAAgBkE,aAAa,aAAc,aAC3ClE,gBAAgBkE,aAAa,OAAQ,eACrChD,qBAAsB,GAOnCe,gBAEQjB,KAAKE,sBACLkD,UAAUE,mBACLtE,gBAAgBuE,gBAAgB,mBAChCvE,gBAAgBkE,aAAa,OAAQ,gBAGzClE,gBAAgB0D,UAAUc,OAAO,aACjCxE,gBAAgBkE,aAAa,WAAY,WACzC5D,oBAAoBiE,gBAAgB,aACrCvD,KAAKR,YAAYkD,UAAUC,SAAS,sBAA2E,MAAnD3C,KAAKV,oBAAoB2C,QAAQwB,oBACxFC,qBAEJhE,cAAcwD,aAAa,YAAa,QACxCtD,qBAAqBe,QAM9BD,iBACQV,KAAKe,sBACAE,qBAEA6B,eAObK,kBACS3D,YAAYkD,UAAUO,IAAI,0BAC1B3D,oBAAoB2C,QAAQwB,cAAgB,IAMrDC,qBACSlE,YAAYkD,UAAUc,OAAO,0BAC7BlE,oBAAoB2C,QAAQwB,cAAgB,0CAQpCE,gBAAOC,gBAAgB5D,KAAKlB,QAO7C2C,sBACWkC,gBAAOlC,eAOlBoC,4BACwD,MAA7C7D,KAAKV,oBAAoB2C,QAAQC,QAM5CrB,mCACciD,OAAO,sBAAuB,IAAIpC,MAAMqC,YACzCzE,oBAAoB0E,UAAYD,UAChC3C,kCAENO,MAAMC,sBAAaC,WAM1BoC,oCACcH,OAAO,mCAAoC,IAAIpC,MAAMqC,YACtDzE,oBAAoB0E,UAAYD,UAChCzB,mCAENX,MAAMC,sBAAaC,sCAOjB7B,KAAK6D,sBAAuB,MAExBI,sBAEA3E,oBAAoB0E,UAAY,SAC/BE,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFC,UAAWrE,KAAKjB,UAChBuF,WAAYtE,KAAKuE,6BAIfC,kBAAoBC,cAAKC,KAAK,CAACR,UAAU,MAC3CM,YAAYG,uBACPC,mBAGA5E,KAAK4C,qBAAsB,OAEtBiC,iBAAmBC,gBAASC,kBAAkBP,YAAYQ,mCAC3DC,gBAAgBJ,uBAGhBvF,oBAAoB2C,QAAQY,UAAY,IAGvD,MAAO8B,OACLO,OAAOC,QAAQC,IAAIT,YACdC,iBASjBK,gBAAgBI,4BACFvB,OAAO,oCAAqC,CAACuB,QAASA,UAAU3D,MAAMqC,YACvEzE,oBAAoB0E,UAAYD,UAChCzE,oBAAoB2C,QAAQC,QAAU,SACtCC,oCAENR,MAAMC,sBAAaC,WAM1B+C,kCACcd,OAAO,iCAAkC,IAAIpC,MAAMqC,YACpDzE,oBAAoB0E,UAAYD,UAChCjC,iCAENH,MAAMC,sBAAaC,WAO1B0C,uBACUe,WAAarG,SAASC,cAAcC,mBAAUC,SAASmG,oBACtDD,WAAWE,WAAaF,WAAWG"} \ No newline at end of file diff --git a/ai/placement/courseassist/amd/src/placement.js b/ai/placement/courseassist/amd/src/placement.js index d78bbb5f639..199e09adce2 100644 --- a/ai/placement/courseassist/amd/src/placement.js +++ b/ai/placement/courseassist/amd/src/placement.js @@ -31,6 +31,8 @@ import AIHelper from 'core_ai/helper'; import DrawerEvents from 'core/drawer_events'; import {subscribe} from 'core/pubsub'; import * as MessageDrawerHelper from 'core_message/message_drawer_helper'; +import * as FocusLock from 'core/local/aria/focuslock'; +import {isSmall} from "core/pagehelpers"; const AICourseAssist = class { @@ -60,6 +62,7 @@ const AICourseAssist = class { this.jumpToElement = document.querySelector(Selectors.ELEMENTS.JUMPTO); this.summaryActionElement = document.querySelector(Selectors.ACTIONS.SUMMARY); this.aiDrawerCloseElement = this.aiDrawerElement.querySelector(Selectors.ELEMENTS.AIDRAWER_CLOSE); + this.isDrawerFocusLocked = false; this.registerEventListeners(); } @@ -85,6 +88,12 @@ const AICourseAssist = class { } }); + document.addEventListener('keydown', e => { + if (this.isAIDrawerOpen() && e.key === 'Escape') { + this.closeAIDrawer(); + } + }); + // Close AI drawer if message drawer is shown. subscribe(DrawerEvents.DRAWER_SHOWN, () => { if (this.isAIDrawerOpen()) { @@ -198,12 +207,27 @@ const AICourseAssist = class { } this.jumpToElement.setAttribute('tabindex', 0); this.jumpToElement.focus(); + + // If the AI drawer is opened on a small screen, we need to trap the focus tab within the AI drawer. + if (isSmall()) { + FocusLock.trapFocus(this.aiDrawerElement); + this.aiDrawerElement.setAttribute('aria-modal', 'true'); + this.aiDrawerElement.setAttribute('role', 'dialog'); + this.isDrawerFocusLocked = true; + } } /** * Close the AI drawer. */ closeAIDrawer() { + // Untrap focus if it was locked. + if (this.isDrawerFocusLocked) { + FocusLock.untrapFocus(); + this.aiDrawerElement.removeAttribute('aria-modal'); + this.aiDrawerElement.setAttribute('role', 'region'); + } + this.aiDrawerElement.classList.remove('show'); this.aiDrawerElement.setAttribute('tabindex', '-1'); this.aiDrawerBodyElement.removeAttribute('aria-live'); diff --git a/ai/placement/courseassist/lang/en/aiplacement_courseassist.php b/ai/placement/courseassist/lang/en/aiplacement_courseassist.php index de4c69873b6..cfa727b3cae 100644 --- a/ai/placement/courseassist/lang/en/aiplacement_courseassist.php +++ b/ai/placement/courseassist/lang/en/aiplacement_courseassist.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['aidrawerlabel'] = 'AI-generated content drawer'; $string['aisummary'] = 'AI summary'; $string['courseassist:summarise_text'] = 'Summarise text'; $string['copy'] = 'Copy'; diff --git a/ai/placement/courseassist/templates/drawer.mustache b/ai/placement/courseassist/templates/drawer.mustache index 44bbf1599f0..d5f5ff06a00 100644 --- a/ai/placement/courseassist/templates/drawer.mustache +++ b/ai/placement/courseassist/templates/drawer.mustache @@ -31,7 +31,7 @@ "content": "

Content to display

" } }} -
+