From 7faa1247e55e6e3165910da348fab2c655db7996 Mon Sep 17 00:00:00 2001 From: Laurent David Date: Mon, 9 Jun 2025 16:34:57 +0200 Subject: [PATCH 1/3] MDL-85231 message_popup: Add focustrap in message popup --- public/lib/templates/popover_region.mustache | 2 +- .../build/notification_popover_controller.min.js | 2 +- .../notification_popover_controller.min.js.map | 2 +- .../amd/src/notification_popover_controller.js | 13 +++++++++++-- .../popup/templates/notification_popover.mustache | 6 +++++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/lib/templates/popover_region.mustache b/public/lib/templates/popover_region.mustache index 70f2709edae..4e12e3d0f4d 100644 --- a/public/lib/templates/popover_region.mustache +++ b/public/lib/templates/popover_region.mustache @@ -53,7 +53,7 @@ data-region="popover-region-container" aria-hidden="true" aria-label="{{$containerlabel}}{{/containerlabel}}" - role="region"> + role="{{$containerrole}}region{{/containerrole}}">

{{$headertext}}{{/headertext}}

{{$headeractions}}{{/headeractions}}
diff --git a/public/message/output/popup/amd/build/notification_popover_controller.min.js b/public/message/output/popup/amd/build/notification_popover_controller.min.js index bcf532b5dfd..2983eccc7f7 100644 --- a/public/message/output/popup/amd/build/notification_popover_controller.min.js +++ b/public/message/output/popup/amd/build/notification_popover_controller.min.js @@ -8,6 +8,6 @@ * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("message_popup/notification_popover_controller",["jquery","core/ajax","core/templates","core/str","core/url","core/notification","core/custom_interaction_events","core/popover_region_controller","message_popup/notification_repository","message_popup/notification_area_events"],(function($,Ajax,Templates,Str,URL,DebugNotification,CustomEvents,PopoverController,NotificationRepo,NotificationAreaEvents){var SELECTORS_MARK_ALL_READ_BUTTON='[data-action="mark-all-read"]',SELECTORS_ALL_NOTIFICATIONS_CONTAINER='[data-region="all-notifications"]',SELECTORS_NOTIFICATION='[data-region="notification-content-item-container"]',SELECTORS_UNREAD_NOTIFICATION='[data-region="notification-content-item-container"].unread',SELECTORS_NOTIFICATION_LINK='[data-action="content-item-link"]',SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',SELECTORS_NOTIFICATION_READ_FEEDBACK='[data-region="notification-read-feedback"]',NotificationPopoverController=function(element){PopoverController.call(this,element),this.markAllReadButton=this.root.find(SELECTORS_MARK_ALL_READ_BUTTON),this.unreadCount=0,this.lastQueried=0,this.userId=this.root.attr("data-userid"),this.container=this.root.find(SELECTORS_ALL_NOTIFICATIONS_CONTAINER),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(SELECTORS_COUNT_CONTAINER).html()};return(NotificationPopoverController.prototype=Object.create(PopoverController.prototype)).constructor=NotificationPopoverController,NotificationPopoverController.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?Str.get_string("hidenotificationwindow","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):this.unreadCount?Str.get_string("shownotificationwindowwithcount","message",this.unreadCount).done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):Str.get_string("shownotificationwindownonew","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this))},NotificationPopoverController.prototype.getContent=function(){return this.container},NotificationPopoverController.prototype.getOffset=function(){return this.offset},NotificationPopoverController.prototype.incrementOffset=function(){this.offset+=this.limit},NotificationPopoverController.prototype.hasDoneInitialLoad=function(){return this.initialLoad},NotificationPopoverController.prototype.hasLoadedAllContent=function(){return this.loadedAll},NotificationPopoverController.prototype.setLoadedAllContent=function(val){this.loadedAll=val},NotificationPopoverController.prototype.renderUnreadCount=function(){var element=this.root.find(SELECTORS_COUNT_CONTAINER);this.unreadCount?(element.text(this.unreadCount),element.removeClass("hidden")):element.addClass("hidden")},NotificationPopoverController.prototype.hideUnreadCount=function(){this.root.find(SELECTORS_COUNT_CONTAINER).addClass("hidden")},NotificationPopoverController.prototype.getNotificationElement=function(id){var element=this.root.find(SELECTORS_NOTIFICATION+'[data-id="'+id+'"]');return 1==element.length?element:null},NotificationPopoverController.prototype.renderNotifications=function(notifications,container){var promises=[];return $.each(notifications,function(index,notification){var offset=this.getOffset()-this.limit;notification.viewmoreurl=URL.relativeUrl("/message/output/popup/notifications.php",{notificationid:notification.id,offset:offset});var notificationurlparams={notificationid:notification.id};notification.contexturl=URL.relativeUrl("message/output/popup/mark_notification_read.php",notificationurlparams);var promise=Templates.render("message_popup/notification_content_item",notification).then((function(html,js){return{html:html,js:js}}));promises.push(promise)}.bind(this)),$.when.apply($,promises).then((function(){$.each(arguments,(function(index,argument){container.append(argument.html),Templates.runTemplateJS(argument.js)}))}))},NotificationPopoverController.prototype.loadMoreNotifications=function(){if(this.isLoading||this.hasLoadedAllContent())return $.Deferred().resolve();this.startLoading();var request={limit:this.limit,offset:this.getOffset(),useridto:this.userId},container=this.getContent();return NotificationRepo.query(request).then(function(result){var notifications=result.notifications;return this.unreadCount=result.unreadcount,this.lastQueried=Math.floor((new Date).getTime()/1e3),this.setLoadedAllContent(!notifications.length||notifications.length{readFeedback.innerHTML=notificationsmarkedasread}))}.bind(this)).always(function(){this.markAllReadButton.removeClass("loading")}.bind(this))},NotificationPopoverController.prototype.registerEventListeners=function(){CustomEvents.define(this.root,[CustomEvents.events.activate]),this.root.on(CustomEvents.events.activate,SELECTORS_MARK_ALL_READ_BUTTON,function(e,data){this.unreadCount>0&&this.markAllAsRead(),e.stopPropagation(),data.originalEvent.preventDefault()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_NOTIFICATION_LINK,function(e){var element=$(e.target).closest(SELECTORS_NOTIFICATION);element.hasClass("unread")&&(this.unreadCount--,element.removeClass("unread")),e.stopPropagation()}.bind(this)),this.root.on(this.events().menuOpened,function(){this.hideUnreadCount(),this.updateButtonAriaLabel(),this.hasDoneInitialLoad()||this.loadMoreNotifications()}.bind(this)),this.root.on(this.events().menuClosed,function(){this.renderUnreadCount(),this.updateButtonAriaLabel()}.bind(this)),this.root.on(this.events().startLoading,function(){this.getContent().attr("aria-busy","true")}.bind(this)),this.root.on(this.events().stopLoading,function(){this.getContent().attr("aria-busy","false")}.bind(this)),this.getContentContainer().on(CustomEvents.events.scrollBottom,function(){this.isLoading||this.hasLoadedAllContent()||this.loadMoreNotifications()}.bind(this)),CustomEvents.define(this.getContentContainer(),[CustomEvents.events.scrollLock]),$(document).on(NotificationAreaEvents.notificationShown,function(e,notification){if(!notification.read){var element=this.getNotificationElement(notification.id);element&&element.removeClass("unread"),this.unreadCount--,this.renderUnreadCount()}}.bind(this))},NotificationPopoverController})); +define("message_popup/notification_popover_controller",["jquery","core/ajax","core/templates","core/str","core/url","core/notification","core/custom_interaction_events","core/popover_region_controller","message_popup/notification_repository","message_popup/notification_area_events","core/local/aria/focuslock"],(function($,Ajax,Templates,Str,URL,DebugNotification,CustomEvents,PopoverController,NotificationRepo,NotificationAreaEvents,FocusLock){var SELECTORS_MARK_ALL_READ_BUTTON='[data-action="mark-all-read"]',SELECTORS_ALL_NOTIFICATIONS_CONTAINER='[data-region="all-notifications"]',SELECTORS_NOTIFICATION='[data-region="notification-content-item-container"]',SELECTORS_UNREAD_NOTIFICATION='[data-region="notification-content-item-container"].unread',SELECTORS_NOTIFICATION_LINK='[data-action="content-item-link"]',SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',SELECTORS_NOTIFICATION_READ_FEEDBACK='[data-region="notification-read-feedback"]',NotificationPopoverController=function(element){PopoverController.call(this,element),this.markAllReadButton=this.root.find(SELECTORS_MARK_ALL_READ_BUTTON),this.unreadCount=0,this.lastQueried=0,this.userId=this.root.attr("data-userid"),this.container=this.root.find(SELECTORS_ALL_NOTIFICATIONS_CONTAINER),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(SELECTORS_COUNT_CONTAINER).html()};return(NotificationPopoverController.prototype=Object.create(PopoverController.prototype)).constructor=NotificationPopoverController,NotificationPopoverController.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?Str.get_string("hidenotificationwindow","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):this.unreadCount?Str.get_string("shownotificationwindowwithcount","message",this.unreadCount).done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):Str.get_string("shownotificationwindownonew","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this))},NotificationPopoverController.prototype.getContent=function(){return this.container},NotificationPopoverController.prototype.getOffset=function(){return this.offset},NotificationPopoverController.prototype.incrementOffset=function(){this.offset+=this.limit},NotificationPopoverController.prototype.hasDoneInitialLoad=function(){return this.initialLoad},NotificationPopoverController.prototype.hasLoadedAllContent=function(){return this.loadedAll},NotificationPopoverController.prototype.setLoadedAllContent=function(val){this.loadedAll=val},NotificationPopoverController.prototype.renderUnreadCount=function(){var element=this.root.find(SELECTORS_COUNT_CONTAINER);this.unreadCount?(element.text(this.unreadCount),element.removeClass("hidden")):element.addClass("hidden")},NotificationPopoverController.prototype.hideUnreadCount=function(){this.root.find(SELECTORS_COUNT_CONTAINER).addClass("hidden")},NotificationPopoverController.prototype.getNotificationElement=function(id){var element=this.root.find(SELECTORS_NOTIFICATION+'[data-id="'+id+'"]');return 1==element.length?element:null},NotificationPopoverController.prototype.renderNotifications=function(notifications,container){var promises=[];return $.each(notifications,function(index,notification){var offset=this.getOffset()-this.limit;notification.viewmoreurl=URL.relativeUrl("/message/output/popup/notifications.php",{notificationid:notification.id,offset:offset});var notificationurlparams={notificationid:notification.id};notification.contexturl=URL.relativeUrl("message/output/popup/mark_notification_read.php",notificationurlparams);var promise=Templates.render("message_popup/notification_content_item",notification).then((function(html,js){return{html:html,js:js}}));promises.push(promise)}.bind(this)),$.when.apply($,promises).then((function(){$.each(arguments,(function(index,argument){container.append(argument.html),Templates.runTemplateJS(argument.js)}))}))},NotificationPopoverController.prototype.loadMoreNotifications=function(){if(this.isLoading||this.hasLoadedAllContent())return $.Deferred().resolve();this.startLoading();var request={limit:this.limit,offset:this.getOffset(),useridto:this.userId},container=this.getContent();return NotificationRepo.query(request).then(function(result){var notifications=result.notifications;return this.unreadCount=result.unreadcount,this.lastQueried=Math.floor((new Date).getTime()/1e3),this.setLoadedAllContent(!notifications.length||notifications.length{readFeedback.innerHTML=notificationsmarkedasread}))}.bind(this)).always(function(){this.markAllReadButton.removeClass("loading")}.bind(this))},NotificationPopoverController.prototype.registerEventListeners=function(){CustomEvents.define(this.root,[CustomEvents.events.activate]),this.root.on(CustomEvents.events.activate,SELECTORS_MARK_ALL_READ_BUTTON,function(e,data){this.unreadCount>0&&this.markAllAsRead(),e.stopPropagation(),data.originalEvent.preventDefault()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_NOTIFICATION_LINK,function(e){var element=$(e.target).closest(SELECTORS_NOTIFICATION);element.hasClass("unread")&&(this.unreadCount--,element.removeClass("unread")),e.stopPropagation()}.bind(this)),this.root.on(this.events().menuOpened,function(){this.hideUnreadCount(),this.updateButtonAriaLabel(),this.hasDoneInitialLoad()||this.loadMoreNotifications();const contentContainer=this.getContentContainer()[0].parentNode;FocusLock.trapFocus(contentContainer)}.bind(this)),this.root.on(this.events().menuClosed,function(){this.renderUnreadCount(),this.updateButtonAriaLabel(),FocusLock.untrapFocus()}.bind(this)),this.root.on(this.events().startLoading,function(){this.getContent().attr("aria-busy","true")}.bind(this)),this.root.on(this.events().stopLoading,function(){this.getContent().attr("aria-busy","false")}.bind(this)),this.getContentContainer().on(CustomEvents.events.scrollBottom,function(){this.isLoading||this.hasLoadedAllContent()||this.loadMoreNotifications()}.bind(this)),CustomEvents.define(this.getContentContainer(),[CustomEvents.events.scrollLock]),$(document).on(NotificationAreaEvents.notificationShown,function(e,notification){if(!notification.read){var element=this.getNotificationElement(notification.id);element&&element.removeClass("unread"),this.unreadCount--,this.renderUnreadCount()}}.bind(this))},NotificationPopoverController})); //# sourceMappingURL=notification_popover_controller.min.js.map \ No newline at end of file diff --git a/public/message/output/popup/amd/build/notification_popover_controller.min.js.map b/public/message/output/popup/amd/build/notification_popover_controller.min.js.map index 92372c17798..9495b7d0520 100644 --- a/public/message/output/popup/amd/build/notification_popover_controller.min.js.map +++ b/public/message/output/popup/amd/build/notification_popover_controller.min.js.map @@ -1 +1 @@ -{"version":3,"file":"notification_popover_controller.min.js","sources":["../src/notification_popover_controller.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 * Controls the notification popover in the nav bar.\n *\n * See template: message_popup/notification_popover\n *\n * @module message_popup/notification_popover_controller\n * @class notification_popover_controller\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',\n 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',\n 'message_popup/notification_repository', 'message_popup/notification_area_events'],\n function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,\n PopoverController, NotificationRepo, NotificationAreaEvents) {\n\n var SELECTORS = {\n MARK_ALL_READ_BUTTON: '[data-action=\"mark-all-read\"]',\n ALL_NOTIFICATIONS_CONTAINER: '[data-region=\"all-notifications\"]',\n NOTIFICATION: '[data-region=\"notification-content-item-container\"]',\n UNREAD_NOTIFICATION: '[data-region=\"notification-content-item-container\"].unread',\n NOTIFICATION_LINK: '[data-action=\"content-item-link\"]',\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n COUNT_CONTAINER: '[data-region=\"count-container\"]',\n NOTIFICATION_READ_FEEDBACK: '[data-region=\"notification-read-feedback\"]',\n };\n\n /**\n * Constructor for the NotificationPopoverController.\n * Extends PopoverRegionController.\n *\n * @param {object} element jQuery object root element of the popover\n */\n var NotificationPopoverController = function(element) {\n // Initialise base class.\n PopoverController.call(this, element);\n\n this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);\n this.unreadCount = 0;\n this.lastQueried = 0;\n this.userId = this.root.attr('data-userid');\n this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);\n this.limit = 20;\n this.offset = 0;\n this.loadedAll = false;\n this.initialLoad = false;\n\n // Let's find out how many unread notifications there are.\n this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();\n };\n\n /**\n * Clone the parent prototype.\n */\n NotificationPopoverController.prototype = Object.create(PopoverController.prototype);\n\n /**\n * Make sure the constructor is set correctly.\n */\n NotificationPopoverController.prototype.constructor = NotificationPopoverController;\n\n /**\n * Set the correct aria label on the menu toggle button to be read out by screen\n * readers. The message will indicate the state of the unread notifications.\n *\n * @method updateButtonAriaLabel\n */\n NotificationPopoverController.prototype.updateButtonAriaLabel = function() {\n if (this.isMenuOpen()) {\n Str.get_string('hidenotificationwindow', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n if (this.unreadCount) {\n Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n Str.get_string('shownotificationwindownonew', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n }\n }\n };\n\n /**\n * Return the jQuery element with the content. This will return either\n * the unread notification container or the all notification container\n * depending on which is currently visible.\n *\n * @method getContent\n * @return {object} jQuery object currently visible content contianer\n */\n NotificationPopoverController.prototype.getContent = function() {\n return this.container;\n };\n\n /**\n * Get the offset value for the current state of the popover in order\n * to sent to the backend to correctly paginate the notifications.\n *\n * @method getOffset\n * @return {int} current offset\n */\n NotificationPopoverController.prototype.getOffset = function() {\n return this.offset;\n };\n\n /**\n * Increment the offset for the current state, if required.\n *\n * @method incrementOffset\n */\n NotificationPopoverController.prototype.incrementOffset = function() {\n this.offset += this.limit;\n };\n\n /**\n * Check if the first load of notification has been triggered for the current\n * state of the popover.\n *\n * @method hasDoneInitialLoad\n * @return {bool} true if first notification loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasDoneInitialLoad = function() {\n return this.initialLoad;\n };\n\n /**\n * Check if we've loaded all of the notifications for the current popover\n * state.\n *\n * @method hasLoadedAllContent\n * @return {bool} true if all notifications loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasLoadedAllContent = function() {\n return this.loadedAll;\n };\n\n /**\n * Set the state of the loaded all content property for the current state\n * of the popover.\n *\n * @method setLoadedAllContent\n * @param {bool} val True if all content is loaded, false otherwise\n */\n NotificationPopoverController.prototype.setLoadedAllContent = function(val) {\n this.loadedAll = val;\n };\n\n /**\n * Show the unread notification count badge on the menu toggle if there\n * are unread notifications, otherwise hide it.\n *\n * @method renderUnreadCount\n */\n NotificationPopoverController.prototype.renderUnreadCount = function() {\n var element = this.root.find(SELECTORS.COUNT_CONTAINER);\n\n if (this.unreadCount) {\n element.text(this.unreadCount);\n element.removeClass('hidden');\n } else {\n element.addClass('hidden');\n }\n };\n\n /**\n * Hide the unread notification count badge on the menu toggle.\n *\n * @method hideUnreadCount\n */\n NotificationPopoverController.prototype.hideUnreadCount = function() {\n this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');\n };\n\n /**\n * Find the notification element for the given id.\n *\n * @param {int} id\n * @method getNotificationElement\n * @return {object|null} The notification element\n */\n NotificationPopoverController.prototype.getNotificationElement = function(id) {\n var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id=\"' + id + '\"]');\n return element.length == 1 ? element : null;\n };\n\n /**\n * Render the notification data with the appropriate template and add it to the DOM.\n *\n * @method renderNotifications\n * @param {array} notifications Notification data\n * @param {object} container jQuery object the container to append the rendered notifications\n * @return {object} jQuery promise that is resolved when all notifications have been\n * rendered and added to the DOM\n */\n NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {\n var promises = [];\n\n $.each(notifications, function(index, notification) {\n // Determine what the offset was when loading this notification.\n var offset = this.getOffset() - this.limit;\n // Update the view more url to contain the offset to allow the notifications\n // page to load to the correct position in the list of notifications.\n notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {\n notificationid: notification.id,\n offset: offset,\n });\n\n // Link to mark read page before loading the actual link.\n var notificationurlparams = {\n notificationid: notification.id\n };\n\n notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);\n\n var promise = Templates.render('message_popup/notification_content_item', notification)\n .then(function(html, js) {\n return {html: html, js: js};\n });\n promises.push(promise);\n }.bind(this));\n\n return $.when.apply($, promises).then(function() {\n // Each of the promises in the when will pass its results as an argument to the function.\n // The order of the arguments will be the order that the promises are passed to when()\n // i.e. the first promise's results will be in the first argument.\n $.each(arguments, function(index, argument) {\n container.append(argument.html);\n Templates.runTemplateJS(argument.js);\n });\n return;\n });\n };\n\n /**\n * Send a request for more notifications from the server, if we aren't already\n * loading some and haven't already loaded all of them.\n *\n * Takes into account the current mode of the popover and will request only\n * unread notifications if required.\n *\n * All notifications are marked as read by the server when they are returned.\n *\n * @method loadMoreNotifications\n * @return {object} jQuery promise that is resolved when notifications have been\n * retrieved and added to the DOM\n */\n NotificationPopoverController.prototype.loadMoreNotifications = function() {\n if (this.isLoading || this.hasLoadedAllContent()) {\n return $.Deferred().resolve();\n }\n\n this.startLoading();\n var request = {\n limit: this.limit,\n offset: this.getOffset(),\n useridto: this.userId,\n };\n\n var container = this.getContent();\n return NotificationRepo.query(request).then(function(result) {\n var notifications = result.notifications;\n this.unreadCount = result.unreadcount;\n this.lastQueried = Math.floor(new Date().getTime() / 1000);\n this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);\n this.initialLoad = true;\n this.updateButtonAriaLabel();\n\n if (notifications.length) {\n this.incrementOffset();\n return this.renderNotifications(notifications, container);\n }\n\n return false;\n }.bind(this))\n .always(function() {\n this.stopLoading();\n }.bind(this));\n };\n\n /**\n * Send a request to the server to mark all unread notifications as read and update\n * the unread count and unread notification elements appropriately.\n *\n * @return {Promise}\n * @method markAllAsRead\n */\n NotificationPopoverController.prototype.markAllAsRead = function() {\n this.markAllReadButton.addClass('loading');\n\n var request = {\n useridto: this.userId,\n timecreatedto: this.lastQueried,\n };\n\n return NotificationRepo.markAllAsRead(request)\n .then(function() {\n this.unreadCount = 0;\n this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');\n\n // Set the ARIA live region's contents with the feedback.\n const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK);\n Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => {\n readFeedback.innerHTML = notificationsmarkedasread;\n });\n }.bind(this))\n .always(function() {\n this.markAllReadButton.removeClass('loading');\n }.bind(this));\n };\n\n /**\n * Add all of the required event listeners for this notification popover.\n *\n * @method registerEventListeners\n */\n NotificationPopoverController.prototype.registerEventListeners = function() {\n CustomEvents.define(this.root, [\n CustomEvents.events.activate,\n ]);\n\n // Mark all notifications read if the user activates the mark all as read button.\n this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {\n if (this.unreadCount > 0) {\n this.markAllAsRead();\n }\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n }.bind(this));\n\n // Mark individual notification read if the user activates it.\n this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {\n var element = $(e.target).closest(SELECTORS.NOTIFICATION);\n\n if (element.hasClass('unread')) {\n this.unreadCount--;\n element.removeClass('unread');\n }\n\n e.stopPropagation();\n }.bind(this));\n\n // Update the notification information when the menu is opened.\n this.root.on(this.events().menuOpened, function() {\n this.hideUnreadCount();\n this.updateButtonAriaLabel();\n\n if (!this.hasDoneInitialLoad()) {\n this.loadMoreNotifications();\n }\n }.bind(this));\n\n // Update the unread notification count when the menu is closed.\n this.root.on(this.events().menuClosed, function() {\n this.renderUnreadCount();\n this.updateButtonAriaLabel();\n }.bind(this));\n\n // Set aria attributes when popover is loading.\n this.root.on(this.events().startLoading, function() {\n this.getContent().attr('aria-busy', 'true');\n }.bind(this));\n\n // Set aria attributes when popover is finished loading.\n this.root.on(this.events().stopLoading, function() {\n this.getContent().attr('aria-busy', 'false');\n }.bind(this));\n\n // Load more notifications if the user has scrolled to the end of content\n // item list.\n this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {\n if (!this.isLoading && !this.hasLoadedAllContent()) {\n this.loadMoreNotifications();\n }\n }.bind(this));\n\n // Stop mouse scroll from propagating to the window element and\n // scrolling the page.\n CustomEvents.define(this.getContentContainer(), [\n CustomEvents.events.scrollLock\n ]);\n\n // Listen for when a notification is shown in the notifications page and mark\n // it as read, if it's unread.\n $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {\n if (!notification.read) {\n var element = this.getNotificationElement(notification.id);\n\n if (element) {\n element.removeClass('unread');\n }\n\n this.unreadCount--;\n this.renderUnreadCount();\n }\n }.bind(this));\n };\n\n return NotificationPopoverController;\n});\n"],"names":["define","$","Ajax","Templates","Str","URL","DebugNotification","CustomEvents","PopoverController","NotificationRepo","NotificationAreaEvents","SELECTORS","NotificationPopoverController","element","call","this","markAllReadButton","root","find","unreadCount","lastQueried","userId","attr","container","limit","offset","loadedAll","initialLoad","html","prototype","Object","create","constructor","updateButtonAriaLabel","isMenuOpen","get_string","done","string","menuToggle","bind","getContent","getOffset","incrementOffset","hasDoneInitialLoad","hasLoadedAllContent","setLoadedAllContent","val","renderUnreadCount","text","removeClass","addClass","hideUnreadCount","getNotificationElement","id","length","renderNotifications","notifications","promises","each","index","notification","viewmoreurl","relativeUrl","notificationid","notificationurlparams","contexturl","promise","render","then","js","push","when","apply","arguments","argument","append","runTemplateJS","loadMoreNotifications","isLoading","Deferred","resolve","startLoading","request","useridto","query","result","unreadcount","Math","floor","Date","getTime","always","stopLoading","markAllAsRead","timecreatedto","readFeedback","get","querySelector","notificationsmarkedasread","innerHTML","registerEventListeners","events","activate","on","e","data","stopPropagation","originalEvent","preventDefault","target","closest","hasClass","menuOpened","menuClosed","getContentContainer","scrollBottom","scrollLock","document","notificationShown","read"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"notification_popover_controller.min.js","sources":["../src/notification_popover_controller.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 * Controls the notification popover in the nav bar.\n *\n * See template: message_popup/notification_popover\n *\n * @module message_popup/notification_popover_controller\n * @class notification_popover_controller\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',\n 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',\n 'message_popup/notification_repository', 'message_popup/notification_area_events',\n 'core/local/aria/focuslock',\n ],\n function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,\n PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) {\n\n var SELECTORS = {\n MARK_ALL_READ_BUTTON: '[data-action=\"mark-all-read\"]',\n ALL_NOTIFICATIONS_CONTAINER: '[data-region=\"all-notifications\"]',\n NOTIFICATION: '[data-region=\"notification-content-item-container\"]',\n UNREAD_NOTIFICATION: '[data-region=\"notification-content-item-container\"].unread',\n NOTIFICATION_LINK: '[data-action=\"content-item-link\"]',\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n COUNT_CONTAINER: '[data-region=\"count-container\"]',\n NOTIFICATION_READ_FEEDBACK: '[data-region=\"notification-read-feedback\"]',\n };\n\n /**\n * Constructor for the NotificationPopoverController.\n * Extends PopoverRegionController.\n *\n * @param {object} element jQuery object root element of the popover\n */\n var NotificationPopoverController = function(element) {\n // Initialise base class.\n PopoverController.call(this, element);\n\n this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);\n this.unreadCount = 0;\n this.lastQueried = 0;\n this.userId = this.root.attr('data-userid');\n this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);\n this.limit = 20;\n this.offset = 0;\n this.loadedAll = false;\n this.initialLoad = false;\n\n // Let's find out how many unread notifications there are.\n this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();\n };\n\n /**\n * Clone the parent prototype.\n */\n NotificationPopoverController.prototype = Object.create(PopoverController.prototype);\n\n /**\n * Make sure the constructor is set correctly.\n */\n NotificationPopoverController.prototype.constructor = NotificationPopoverController;\n\n /**\n * Set the correct aria label on the menu toggle button to be read out by screen\n * readers. The message will indicate the state of the unread notifications.\n *\n * @method updateButtonAriaLabel\n */\n NotificationPopoverController.prototype.updateButtonAriaLabel = function() {\n if (this.isMenuOpen()) {\n Str.get_string('hidenotificationwindow', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n if (this.unreadCount) {\n Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n Str.get_string('shownotificationwindownonew', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n }\n }\n };\n\n /**\n * Return the jQuery element with the content. This will return either\n * the unread notification container or the all notification container\n * depending on which is currently visible.\n *\n * @method getContent\n * @return {object} jQuery object currently visible content contianer\n */\n NotificationPopoverController.prototype.getContent = function() {\n return this.container;\n };\n\n /**\n * Get the offset value for the current state of the popover in order\n * to sent to the backend to correctly paginate the notifications.\n *\n * @method getOffset\n * @return {int} current offset\n */\n NotificationPopoverController.prototype.getOffset = function() {\n return this.offset;\n };\n\n /**\n * Increment the offset for the current state, if required.\n *\n * @method incrementOffset\n */\n NotificationPopoverController.prototype.incrementOffset = function() {\n this.offset += this.limit;\n };\n\n /**\n * Check if the first load of notification has been triggered for the current\n * state of the popover.\n *\n * @method hasDoneInitialLoad\n * @return {bool} true if first notification loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasDoneInitialLoad = function() {\n return this.initialLoad;\n };\n\n /**\n * Check if we've loaded all of the notifications for the current popover\n * state.\n *\n * @method hasLoadedAllContent\n * @return {bool} true if all notifications loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasLoadedAllContent = function() {\n return this.loadedAll;\n };\n\n /**\n * Set the state of the loaded all content property for the current state\n * of the popover.\n *\n * @method setLoadedAllContent\n * @param {bool} val True if all content is loaded, false otherwise\n */\n NotificationPopoverController.prototype.setLoadedAllContent = function(val) {\n this.loadedAll = val;\n };\n\n /**\n * Show the unread notification count badge on the menu toggle if there\n * are unread notifications, otherwise hide it.\n *\n * @method renderUnreadCount\n */\n NotificationPopoverController.prototype.renderUnreadCount = function() {\n var element = this.root.find(SELECTORS.COUNT_CONTAINER);\n\n if (this.unreadCount) {\n element.text(this.unreadCount);\n element.removeClass('hidden');\n } else {\n element.addClass('hidden');\n }\n };\n\n /**\n * Hide the unread notification count badge on the menu toggle.\n *\n * @method hideUnreadCount\n */\n NotificationPopoverController.prototype.hideUnreadCount = function() {\n this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');\n };\n\n /**\n * Find the notification element for the given id.\n *\n * @param {int} id\n * @method getNotificationElement\n * @return {object|null} The notification element\n */\n NotificationPopoverController.prototype.getNotificationElement = function(id) {\n var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id=\"' + id + '\"]');\n return element.length == 1 ? element : null;\n };\n\n /**\n * Render the notification data with the appropriate template and add it to the DOM.\n *\n * @method renderNotifications\n * @param {array} notifications Notification data\n * @param {object} container jQuery object the container to append the rendered notifications\n * @return {object} jQuery promise that is resolved when all notifications have been\n * rendered and added to the DOM\n */\n NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {\n var promises = [];\n\n $.each(notifications, function(index, notification) {\n // Determine what the offset was when loading this notification.\n var offset = this.getOffset() - this.limit;\n // Update the view more url to contain the offset to allow the notifications\n // page to load to the correct position in the list of notifications.\n notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {\n notificationid: notification.id,\n offset: offset,\n });\n\n // Link to mark read page before loading the actual link.\n var notificationurlparams = {\n notificationid: notification.id\n };\n\n notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);\n\n var promise = Templates.render('message_popup/notification_content_item', notification)\n .then(function(html, js) {\n return {html: html, js: js};\n });\n promises.push(promise);\n }.bind(this));\n\n return $.when.apply($, promises).then(function() {\n // Each of the promises in the when will pass its results as an argument to the function.\n // The order of the arguments will be the order that the promises are passed to when()\n // i.e. the first promise's results will be in the first argument.\n $.each(arguments, function(index, argument) {\n container.append(argument.html);\n Templates.runTemplateJS(argument.js);\n });\n return;\n });\n };\n\n /**\n * Send a request for more notifications from the server, if we aren't already\n * loading some and haven't already loaded all of them.\n *\n * Takes into account the current mode of the popover and will request only\n * unread notifications if required.\n *\n * All notifications are marked as read by the server when they are returned.\n *\n * @method loadMoreNotifications\n * @return {object} jQuery promise that is resolved when notifications have been\n * retrieved and added to the DOM\n */\n NotificationPopoverController.prototype.loadMoreNotifications = function() {\n if (this.isLoading || this.hasLoadedAllContent()) {\n return $.Deferred().resolve();\n }\n\n this.startLoading();\n var request = {\n limit: this.limit,\n offset: this.getOffset(),\n useridto: this.userId,\n };\n\n var container = this.getContent();\n return NotificationRepo.query(request).then(function(result) {\n var notifications = result.notifications;\n this.unreadCount = result.unreadcount;\n this.lastQueried = Math.floor(new Date().getTime() / 1000);\n this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);\n this.initialLoad = true;\n this.updateButtonAriaLabel();\n\n if (notifications.length) {\n this.incrementOffset();\n return this.renderNotifications(notifications, container);\n }\n\n return false;\n }.bind(this))\n .always(function() {\n this.stopLoading();\n }.bind(this));\n };\n\n /**\n * Send a request to the server to mark all unread notifications as read and update\n * the unread count and unread notification elements appropriately.\n *\n * @return {Promise}\n * @method markAllAsRead\n */\n NotificationPopoverController.prototype.markAllAsRead = function() {\n this.markAllReadButton.addClass('loading');\n\n var request = {\n useridto: this.userId,\n timecreatedto: this.lastQueried,\n };\n\n return NotificationRepo.markAllAsRead(request)\n .then(function() {\n this.unreadCount = 0;\n this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');\n\n // Set the ARIA live region's contents with the feedback.\n const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK);\n Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => {\n readFeedback.innerHTML = notificationsmarkedasread;\n });\n }.bind(this))\n .always(function() {\n this.markAllReadButton.removeClass('loading');\n }.bind(this));\n };\n\n /**\n * Add all of the required event listeners for this notification popover.\n *\n * @method registerEventListeners\n */\n NotificationPopoverController.prototype.registerEventListeners = function() {\n CustomEvents.define(this.root, [\n CustomEvents.events.activate,\n ]);\n\n // Mark all notifications read if the user activates the mark all as read button.\n this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {\n if (this.unreadCount > 0) {\n this.markAllAsRead();\n }\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n }.bind(this));\n\n // Mark individual notification read if the user activates it.\n this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {\n var element = $(e.target).closest(SELECTORS.NOTIFICATION);\n\n if (element.hasClass('unread')) {\n this.unreadCount--;\n element.removeClass('unread');\n }\n\n e.stopPropagation();\n }.bind(this));\n\n // Update the notification information when the menu is opened.\n this.root.on(this.events().menuOpened, function() {\n this.hideUnreadCount();\n this.updateButtonAriaLabel();\n\n if (!this.hasDoneInitialLoad()) {\n this.loadMoreNotifications();\n }\n\n // Lock focus to the popover when it is opened, it is the parent of the container.\n const contentContainer = this.getContentContainer()[0].parentNode;\n FocusLock.trapFocus(contentContainer);\n\n }.bind(this));\n\n // Update the unread notification count when the menu is closed.\n this.root.on(this.events().menuClosed, function() {\n this.renderUnreadCount();\n this.updateButtonAriaLabel();\n // Lock focus to the popover when it is opened, it is the parent of the container.\n FocusLock.untrapFocus();\n }.bind(this));\n\n // Set aria attributes when popover is loading.\n this.root.on(this.events().startLoading, function() {\n this.getContent().attr('aria-busy', 'true');\n }.bind(this));\n\n // Set aria attributes when popover is finished loading.\n this.root.on(this.events().stopLoading, function() {\n this.getContent().attr('aria-busy', 'false');\n }.bind(this));\n\n // Load more notifications if the user has scrolled to the end of content\n // item list.\n this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {\n if (!this.isLoading && !this.hasLoadedAllContent()) {\n this.loadMoreNotifications();\n }\n }.bind(this));\n\n // Stop mouse scroll from propagating to the window element and\n // scrolling the page.\n CustomEvents.define(this.getContentContainer(), [\n CustomEvents.events.scrollLock\n ]);\n\n // Listen for when a notification is shown in the notifications page and mark\n // it as read, if it's unread.\n $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {\n if (!notification.read) {\n var element = this.getNotificationElement(notification.id);\n\n if (element) {\n element.removeClass('unread');\n }\n\n this.unreadCount--;\n this.renderUnreadCount();\n }\n }.bind(this));\n };\n\n return NotificationPopoverController;\n});\n"],"names":["define","$","Ajax","Templates","Str","URL","DebugNotification","CustomEvents","PopoverController","NotificationRepo","NotificationAreaEvents","FocusLock","SELECTORS","NotificationPopoverController","element","call","this","markAllReadButton","root","find","unreadCount","lastQueried","userId","attr","container","limit","offset","loadedAll","initialLoad","html","prototype","Object","create","constructor","updateButtonAriaLabel","isMenuOpen","get_string","done","string","menuToggle","bind","getContent","getOffset","incrementOffset","hasDoneInitialLoad","hasLoadedAllContent","setLoadedAllContent","val","renderUnreadCount","text","removeClass","addClass","hideUnreadCount","getNotificationElement","id","length","renderNotifications","notifications","promises","each","index","notification","viewmoreurl","relativeUrl","notificationid","notificationurlparams","contexturl","promise","render","then","js","push","when","apply","arguments","argument","append","runTemplateJS","loadMoreNotifications","isLoading","Deferred","resolve","startLoading","request","useridto","query","result","unreadcount","Math","floor","Date","getTime","always","stopLoading","markAllAsRead","timecreatedto","readFeedback","get","querySelector","notificationsmarkedasread","innerHTML","registerEventListeners","events","activate","on","e","data","stopPropagation","originalEvent","preventDefault","target","closest","hasClass","menuOpened","contentContainer","getContentContainer","parentNode","trapFocus","menuClosed","untrapFocus","scrollBottom","scrollLock","document","notificationShown","read"],"mappings":"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"} \ No newline at end of file diff --git a/public/message/output/popup/amd/src/notification_popover_controller.js b/public/message/output/popup/amd/src/notification_popover_controller.js index 3d6aa0ea5e4..b2db9370498 100644 --- a/public/message/output/popup/amd/src/notification_popover_controller.js +++ b/public/message/output/popup/amd/src/notification_popover_controller.js @@ -25,9 +25,11 @@ */ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url', 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller', - 'message_popup/notification_repository', 'message_popup/notification_area_events'], + 'message_popup/notification_repository', 'message_popup/notification_area_events', + 'core/local/aria/focuslock', + ], function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents, - PopoverController, NotificationRepo, NotificationAreaEvents) { + PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) { var SELECTORS = { MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]', @@ -366,12 +368,19 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url', if (!this.hasDoneInitialLoad()) { this.loadMoreNotifications(); } + + // Lock focus to the popover when it is opened, it is the parent of the container. + const contentContainer = this.getContentContainer()[0].parentNode; + FocusLock.trapFocus(contentContainer); + }.bind(this)); // Update the unread notification count when the menu is closed. this.root.on(this.events().menuClosed, function() { this.renderUnreadCount(); this.updateButtonAriaLabel(); + // Lock focus to the popover when it is opened, it is the parent of the container. + FocusLock.untrapFocus(); }.bind(this)); // Set aria attributes when popover is loading. diff --git a/public/message/output/popup/templates/notification_popover.mustache b/public/message/output/popup/templates/notification_popover.mustache index f497b183365..72fc9f2b388 100644 --- a/public/message/output/popup/templates/notification_popover.mustache +++ b/public/message/output/popup/templates/notification_popover.mustache @@ -56,7 +56,11 @@ {{unreadcount}}
{{/togglecontent}} - + {{$containerrole}}dialog{{/containerrole}} + {{$containerattributes}} + aria-modal="true" + tabindex="-1" + {{/containerattributes}} {{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}} {{$headertext}}{{#str}} notifications, message {{/str}}{{/headertext}} From 02c578f2262280beb8a646662eec309e6a791490 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Tue, 29 Jul 2025 17:02:18 +0800 Subject: [PATCH 2/3] MDL-85231 message_popup: Add a close button on the notification popover This follows the recommendation on the ARIA Authoring Practices Guide (APG) for modal dialogues: > It is strongly recommended that the tab sequence of all dialogs > include a visible element with role button that closes the dialog, such as a close icon or cancel button. https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ --- .../popup/amd/build/notification_popover_controller.min.js | 2 +- .../amd/build/notification_popover_controller.min.js.map | 2 +- .../popup/amd/src/notification_popover_controller.js | 7 +++++++ .../output/popup/templates/notification_popover.mustache | 3 +++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/public/message/output/popup/amd/build/notification_popover_controller.min.js b/public/message/output/popup/amd/build/notification_popover_controller.min.js index 2983eccc7f7..2f8a4e53403 100644 --- a/public/message/output/popup/amd/build/notification_popover_controller.min.js +++ b/public/message/output/popup/amd/build/notification_popover_controller.min.js @@ -8,6 +8,6 @@ * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("message_popup/notification_popover_controller",["jquery","core/ajax","core/templates","core/str","core/url","core/notification","core/custom_interaction_events","core/popover_region_controller","message_popup/notification_repository","message_popup/notification_area_events","core/local/aria/focuslock"],(function($,Ajax,Templates,Str,URL,DebugNotification,CustomEvents,PopoverController,NotificationRepo,NotificationAreaEvents,FocusLock){var SELECTORS_MARK_ALL_READ_BUTTON='[data-action="mark-all-read"]',SELECTORS_ALL_NOTIFICATIONS_CONTAINER='[data-region="all-notifications"]',SELECTORS_NOTIFICATION='[data-region="notification-content-item-container"]',SELECTORS_UNREAD_NOTIFICATION='[data-region="notification-content-item-container"].unread',SELECTORS_NOTIFICATION_LINK='[data-action="content-item-link"]',SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',SELECTORS_NOTIFICATION_READ_FEEDBACK='[data-region="notification-read-feedback"]',NotificationPopoverController=function(element){PopoverController.call(this,element),this.markAllReadButton=this.root.find(SELECTORS_MARK_ALL_READ_BUTTON),this.unreadCount=0,this.lastQueried=0,this.userId=this.root.attr("data-userid"),this.container=this.root.find(SELECTORS_ALL_NOTIFICATIONS_CONTAINER),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(SELECTORS_COUNT_CONTAINER).html()};return(NotificationPopoverController.prototype=Object.create(PopoverController.prototype)).constructor=NotificationPopoverController,NotificationPopoverController.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?Str.get_string("hidenotificationwindow","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):this.unreadCount?Str.get_string("shownotificationwindowwithcount","message",this.unreadCount).done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):Str.get_string("shownotificationwindownonew","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this))},NotificationPopoverController.prototype.getContent=function(){return this.container},NotificationPopoverController.prototype.getOffset=function(){return this.offset},NotificationPopoverController.prototype.incrementOffset=function(){this.offset+=this.limit},NotificationPopoverController.prototype.hasDoneInitialLoad=function(){return this.initialLoad},NotificationPopoverController.prototype.hasLoadedAllContent=function(){return this.loadedAll},NotificationPopoverController.prototype.setLoadedAllContent=function(val){this.loadedAll=val},NotificationPopoverController.prototype.renderUnreadCount=function(){var element=this.root.find(SELECTORS_COUNT_CONTAINER);this.unreadCount?(element.text(this.unreadCount),element.removeClass("hidden")):element.addClass("hidden")},NotificationPopoverController.prototype.hideUnreadCount=function(){this.root.find(SELECTORS_COUNT_CONTAINER).addClass("hidden")},NotificationPopoverController.prototype.getNotificationElement=function(id){var element=this.root.find(SELECTORS_NOTIFICATION+'[data-id="'+id+'"]');return 1==element.length?element:null},NotificationPopoverController.prototype.renderNotifications=function(notifications,container){var promises=[];return $.each(notifications,function(index,notification){var offset=this.getOffset()-this.limit;notification.viewmoreurl=URL.relativeUrl("/message/output/popup/notifications.php",{notificationid:notification.id,offset:offset});var notificationurlparams={notificationid:notification.id};notification.contexturl=URL.relativeUrl("message/output/popup/mark_notification_read.php",notificationurlparams);var promise=Templates.render("message_popup/notification_content_item",notification).then((function(html,js){return{html:html,js:js}}));promises.push(promise)}.bind(this)),$.when.apply($,promises).then((function(){$.each(arguments,(function(index,argument){container.append(argument.html),Templates.runTemplateJS(argument.js)}))}))},NotificationPopoverController.prototype.loadMoreNotifications=function(){if(this.isLoading||this.hasLoadedAllContent())return $.Deferred().resolve();this.startLoading();var request={limit:this.limit,offset:this.getOffset(),useridto:this.userId},container=this.getContent();return NotificationRepo.query(request).then(function(result){var notifications=result.notifications;return this.unreadCount=result.unreadcount,this.lastQueried=Math.floor((new Date).getTime()/1e3),this.setLoadedAllContent(!notifications.length||notifications.length{readFeedback.innerHTML=notificationsmarkedasread}))}.bind(this)).always(function(){this.markAllReadButton.removeClass("loading")}.bind(this))},NotificationPopoverController.prototype.registerEventListeners=function(){CustomEvents.define(this.root,[CustomEvents.events.activate]),this.root.on(CustomEvents.events.activate,SELECTORS_MARK_ALL_READ_BUTTON,function(e,data){this.unreadCount>0&&this.markAllAsRead(),e.stopPropagation(),data.originalEvent.preventDefault()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_NOTIFICATION_LINK,function(e){var element=$(e.target).closest(SELECTORS_NOTIFICATION);element.hasClass("unread")&&(this.unreadCount--,element.removeClass("unread")),e.stopPropagation()}.bind(this)),this.root.on(this.events().menuOpened,function(){this.hideUnreadCount(),this.updateButtonAriaLabel(),this.hasDoneInitialLoad()||this.loadMoreNotifications();const contentContainer=this.getContentContainer()[0].parentNode;FocusLock.trapFocus(contentContainer)}.bind(this)),this.root.on(this.events().menuClosed,function(){this.renderUnreadCount(),this.updateButtonAriaLabel(),FocusLock.untrapFocus()}.bind(this)),this.root.on(this.events().startLoading,function(){this.getContent().attr("aria-busy","true")}.bind(this)),this.root.on(this.events().stopLoading,function(){this.getContent().attr("aria-busy","false")}.bind(this)),this.getContentContainer().on(CustomEvents.events.scrollBottom,function(){this.isLoading||this.hasLoadedAllContent()||this.loadMoreNotifications()}.bind(this)),CustomEvents.define(this.getContentContainer(),[CustomEvents.events.scrollLock]),$(document).on(NotificationAreaEvents.notificationShown,function(e,notification){if(!notification.read){var element=this.getNotificationElement(notification.id);element&&element.removeClass("unread"),this.unreadCount--,this.renderUnreadCount()}}.bind(this))},NotificationPopoverController})); +define("message_popup/notification_popover_controller",["jquery","core/ajax","core/templates","core/str","core/url","core/notification","core/custom_interaction_events","core/popover_region_controller","message_popup/notification_repository","message_popup/notification_area_events","core/local/aria/focuslock"],(function($,Ajax,Templates,Str,URL,DebugNotification,CustomEvents,PopoverController,NotificationRepo,NotificationAreaEvents,FocusLock){var SELECTORS_MARK_ALL_READ_BUTTON='[data-action="mark-all-read"]',SELECTORS_ALL_NOTIFICATIONS_CONTAINER='[data-region="all-notifications"]',SELECTORS_NOTIFICATION='[data-region="notification-content-item-container"]',SELECTORS_UNREAD_NOTIFICATION='[data-region="notification-content-item-container"].unread',SELECTORS_NOTIFICATION_LINK='[data-action="content-item-link"]',SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',SELECTORS_NOTIFICATION_READ_FEEDBACK='[data-region="notification-read-feedback"]',SELECTORS_CLOSE_NOTIFICATION_POPOVER='[data-action="close-notification-popover"]',NotificationPopoverController=function(element){PopoverController.call(this,element),this.markAllReadButton=this.root.find(SELECTORS_MARK_ALL_READ_BUTTON),this.unreadCount=0,this.lastQueried=0,this.userId=this.root.attr("data-userid"),this.container=this.root.find(SELECTORS_ALL_NOTIFICATIONS_CONTAINER),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(SELECTORS_COUNT_CONTAINER).html()};return(NotificationPopoverController.prototype=Object.create(PopoverController.prototype)).constructor=NotificationPopoverController,NotificationPopoverController.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?Str.get_string("hidenotificationwindow","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):this.unreadCount?Str.get_string("shownotificationwindowwithcount","message",this.unreadCount).done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):Str.get_string("shownotificationwindownonew","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this))},NotificationPopoverController.prototype.getContent=function(){return this.container},NotificationPopoverController.prototype.getOffset=function(){return this.offset},NotificationPopoverController.prototype.incrementOffset=function(){this.offset+=this.limit},NotificationPopoverController.prototype.hasDoneInitialLoad=function(){return this.initialLoad},NotificationPopoverController.prototype.hasLoadedAllContent=function(){return this.loadedAll},NotificationPopoverController.prototype.setLoadedAllContent=function(val){this.loadedAll=val},NotificationPopoverController.prototype.renderUnreadCount=function(){var element=this.root.find(SELECTORS_COUNT_CONTAINER);this.unreadCount?(element.text(this.unreadCount),element.removeClass("hidden")):element.addClass("hidden")},NotificationPopoverController.prototype.hideUnreadCount=function(){this.root.find(SELECTORS_COUNT_CONTAINER).addClass("hidden")},NotificationPopoverController.prototype.getNotificationElement=function(id){var element=this.root.find(SELECTORS_NOTIFICATION+'[data-id="'+id+'"]');return 1==element.length?element:null},NotificationPopoverController.prototype.renderNotifications=function(notifications,container){var promises=[];return $.each(notifications,function(index,notification){var offset=this.getOffset()-this.limit;notification.viewmoreurl=URL.relativeUrl("/message/output/popup/notifications.php",{notificationid:notification.id,offset:offset});var notificationurlparams={notificationid:notification.id};notification.contexturl=URL.relativeUrl("message/output/popup/mark_notification_read.php",notificationurlparams);var promise=Templates.render("message_popup/notification_content_item",notification).then((function(html,js){return{html:html,js:js}}));promises.push(promise)}.bind(this)),$.when.apply($,promises).then((function(){$.each(arguments,(function(index,argument){container.append(argument.html),Templates.runTemplateJS(argument.js)}))}))},NotificationPopoverController.prototype.loadMoreNotifications=function(){if(this.isLoading||this.hasLoadedAllContent())return $.Deferred().resolve();this.startLoading();var request={limit:this.limit,offset:this.getOffset(),useridto:this.userId},container=this.getContent();return NotificationRepo.query(request).then(function(result){var notifications=result.notifications;return this.unreadCount=result.unreadcount,this.lastQueried=Math.floor((new Date).getTime()/1e3),this.setLoadedAllContent(!notifications.length||notifications.length{readFeedback.innerHTML=notificationsmarkedasread}))}.bind(this)).always(function(){this.markAllReadButton.removeClass("loading")}.bind(this))},NotificationPopoverController.prototype.registerEventListeners=function(){CustomEvents.define(this.root,[CustomEvents.events.activate]),this.root.on(CustomEvents.events.activate,SELECTORS_MARK_ALL_READ_BUTTON,function(e,data){this.unreadCount>0&&this.markAllAsRead(),e.stopPropagation(),data.originalEvent.preventDefault()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_NOTIFICATION_LINK,function(e){var element=$(e.target).closest(SELECTORS_NOTIFICATION);element.hasClass("unread")&&(this.unreadCount--,element.removeClass("unread")),e.stopPropagation()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_CLOSE_NOTIFICATION_POPOVER,function(e){e.preventDefault(),$(this.root).trigger(CustomEvents.events.escape),e.stopPropagation()}.bind(this)),this.root.on(this.events().menuOpened,function(){this.hideUnreadCount(),this.updateButtonAriaLabel(),this.hasDoneInitialLoad()||this.loadMoreNotifications();const contentContainer=this.getContentContainer()[0].parentNode;FocusLock.trapFocus(contentContainer)}.bind(this)),this.root.on(this.events().menuClosed,function(){this.renderUnreadCount(),this.updateButtonAriaLabel(),FocusLock.untrapFocus()}.bind(this)),this.root.on(this.events().startLoading,function(){this.getContent().attr("aria-busy","true")}.bind(this)),this.root.on(this.events().stopLoading,function(){this.getContent().attr("aria-busy","false")}.bind(this)),this.getContentContainer().on(CustomEvents.events.scrollBottom,function(){this.isLoading||this.hasLoadedAllContent()||this.loadMoreNotifications()}.bind(this)),CustomEvents.define(this.getContentContainer(),[CustomEvents.events.scrollLock]),$(document).on(NotificationAreaEvents.notificationShown,function(e,notification){if(!notification.read){var element=this.getNotificationElement(notification.id);element&&element.removeClass("unread"),this.unreadCount--,this.renderUnreadCount()}}.bind(this))},NotificationPopoverController})); //# sourceMappingURL=notification_popover_controller.min.js.map \ No newline at end of file diff --git a/public/message/output/popup/amd/build/notification_popover_controller.min.js.map b/public/message/output/popup/amd/build/notification_popover_controller.min.js.map index 9495b7d0520..9c25a1f0c6c 100644 --- a/public/message/output/popup/amd/build/notification_popover_controller.min.js.map +++ b/public/message/output/popup/amd/build/notification_popover_controller.min.js.map @@ -1 +1 @@ -{"version":3,"file":"notification_popover_controller.min.js","sources":["../src/notification_popover_controller.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 * Controls the notification popover in the nav bar.\n *\n * See template: message_popup/notification_popover\n *\n * @module message_popup/notification_popover_controller\n * @class notification_popover_controller\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',\n 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',\n 'message_popup/notification_repository', 'message_popup/notification_area_events',\n 'core/local/aria/focuslock',\n ],\n function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,\n PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) {\n\n var SELECTORS = {\n MARK_ALL_READ_BUTTON: '[data-action=\"mark-all-read\"]',\n ALL_NOTIFICATIONS_CONTAINER: '[data-region=\"all-notifications\"]',\n NOTIFICATION: '[data-region=\"notification-content-item-container\"]',\n UNREAD_NOTIFICATION: '[data-region=\"notification-content-item-container\"].unread',\n NOTIFICATION_LINK: '[data-action=\"content-item-link\"]',\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n COUNT_CONTAINER: '[data-region=\"count-container\"]',\n NOTIFICATION_READ_FEEDBACK: '[data-region=\"notification-read-feedback\"]',\n };\n\n /**\n * Constructor for the NotificationPopoverController.\n * Extends PopoverRegionController.\n *\n * @param {object} element jQuery object root element of the popover\n */\n var NotificationPopoverController = function(element) {\n // Initialise base class.\n PopoverController.call(this, element);\n\n this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);\n this.unreadCount = 0;\n this.lastQueried = 0;\n this.userId = this.root.attr('data-userid');\n this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);\n this.limit = 20;\n this.offset = 0;\n this.loadedAll = false;\n this.initialLoad = false;\n\n // Let's find out how many unread notifications there are.\n this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();\n };\n\n /**\n * Clone the parent prototype.\n */\n NotificationPopoverController.prototype = Object.create(PopoverController.prototype);\n\n /**\n * Make sure the constructor is set correctly.\n */\n NotificationPopoverController.prototype.constructor = NotificationPopoverController;\n\n /**\n * Set the correct aria label on the menu toggle button to be read out by screen\n * readers. The message will indicate the state of the unread notifications.\n *\n * @method updateButtonAriaLabel\n */\n NotificationPopoverController.prototype.updateButtonAriaLabel = function() {\n if (this.isMenuOpen()) {\n Str.get_string('hidenotificationwindow', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n if (this.unreadCount) {\n Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n Str.get_string('shownotificationwindownonew', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n }\n }\n };\n\n /**\n * Return the jQuery element with the content. This will return either\n * the unread notification container or the all notification container\n * depending on which is currently visible.\n *\n * @method getContent\n * @return {object} jQuery object currently visible content contianer\n */\n NotificationPopoverController.prototype.getContent = function() {\n return this.container;\n };\n\n /**\n * Get the offset value for the current state of the popover in order\n * to sent to the backend to correctly paginate the notifications.\n *\n * @method getOffset\n * @return {int} current offset\n */\n NotificationPopoverController.prototype.getOffset = function() {\n return this.offset;\n };\n\n /**\n * Increment the offset for the current state, if required.\n *\n * @method incrementOffset\n */\n NotificationPopoverController.prototype.incrementOffset = function() {\n this.offset += this.limit;\n };\n\n /**\n * Check if the first load of notification has been triggered for the current\n * state of the popover.\n *\n * @method hasDoneInitialLoad\n * @return {bool} true if first notification loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasDoneInitialLoad = function() {\n return this.initialLoad;\n };\n\n /**\n * Check if we've loaded all of the notifications for the current popover\n * state.\n *\n * @method hasLoadedAllContent\n * @return {bool} true if all notifications loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasLoadedAllContent = function() {\n return this.loadedAll;\n };\n\n /**\n * Set the state of the loaded all content property for the current state\n * of the popover.\n *\n * @method setLoadedAllContent\n * @param {bool} val True if all content is loaded, false otherwise\n */\n NotificationPopoverController.prototype.setLoadedAllContent = function(val) {\n this.loadedAll = val;\n };\n\n /**\n * Show the unread notification count badge on the menu toggle if there\n * are unread notifications, otherwise hide it.\n *\n * @method renderUnreadCount\n */\n NotificationPopoverController.prototype.renderUnreadCount = function() {\n var element = this.root.find(SELECTORS.COUNT_CONTAINER);\n\n if (this.unreadCount) {\n element.text(this.unreadCount);\n element.removeClass('hidden');\n } else {\n element.addClass('hidden');\n }\n };\n\n /**\n * Hide the unread notification count badge on the menu toggle.\n *\n * @method hideUnreadCount\n */\n NotificationPopoverController.prototype.hideUnreadCount = function() {\n this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');\n };\n\n /**\n * Find the notification element for the given id.\n *\n * @param {int} id\n * @method getNotificationElement\n * @return {object|null} The notification element\n */\n NotificationPopoverController.prototype.getNotificationElement = function(id) {\n var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id=\"' + id + '\"]');\n return element.length == 1 ? element : null;\n };\n\n /**\n * Render the notification data with the appropriate template and add it to the DOM.\n *\n * @method renderNotifications\n * @param {array} notifications Notification data\n * @param {object} container jQuery object the container to append the rendered notifications\n * @return {object} jQuery promise that is resolved when all notifications have been\n * rendered and added to the DOM\n */\n NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {\n var promises = [];\n\n $.each(notifications, function(index, notification) {\n // Determine what the offset was when loading this notification.\n var offset = this.getOffset() - this.limit;\n // Update the view more url to contain the offset to allow the notifications\n // page to load to the correct position in the list of notifications.\n notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {\n notificationid: notification.id,\n offset: offset,\n });\n\n // Link to mark read page before loading the actual link.\n var notificationurlparams = {\n notificationid: notification.id\n };\n\n notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);\n\n var promise = Templates.render('message_popup/notification_content_item', notification)\n .then(function(html, js) {\n return {html: html, js: js};\n });\n promises.push(promise);\n }.bind(this));\n\n return $.when.apply($, promises).then(function() {\n // Each of the promises in the when will pass its results as an argument to the function.\n // The order of the arguments will be the order that the promises are passed to when()\n // i.e. the first promise's results will be in the first argument.\n $.each(arguments, function(index, argument) {\n container.append(argument.html);\n Templates.runTemplateJS(argument.js);\n });\n return;\n });\n };\n\n /**\n * Send a request for more notifications from the server, if we aren't already\n * loading some and haven't already loaded all of them.\n *\n * Takes into account the current mode of the popover and will request only\n * unread notifications if required.\n *\n * All notifications are marked as read by the server when they are returned.\n *\n * @method loadMoreNotifications\n * @return {object} jQuery promise that is resolved when notifications have been\n * retrieved and added to the DOM\n */\n NotificationPopoverController.prototype.loadMoreNotifications = function() {\n if (this.isLoading || this.hasLoadedAllContent()) {\n return $.Deferred().resolve();\n }\n\n this.startLoading();\n var request = {\n limit: this.limit,\n offset: this.getOffset(),\n useridto: this.userId,\n };\n\n var container = this.getContent();\n return NotificationRepo.query(request).then(function(result) {\n var notifications = result.notifications;\n this.unreadCount = result.unreadcount;\n this.lastQueried = Math.floor(new Date().getTime() / 1000);\n this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);\n this.initialLoad = true;\n this.updateButtonAriaLabel();\n\n if (notifications.length) {\n this.incrementOffset();\n return this.renderNotifications(notifications, container);\n }\n\n return false;\n }.bind(this))\n .always(function() {\n this.stopLoading();\n }.bind(this));\n };\n\n /**\n * Send a request to the server to mark all unread notifications as read and update\n * the unread count and unread notification elements appropriately.\n *\n * @return {Promise}\n * @method markAllAsRead\n */\n NotificationPopoverController.prototype.markAllAsRead = function() {\n this.markAllReadButton.addClass('loading');\n\n var request = {\n useridto: this.userId,\n timecreatedto: this.lastQueried,\n };\n\n return NotificationRepo.markAllAsRead(request)\n .then(function() {\n this.unreadCount = 0;\n this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');\n\n // Set the ARIA live region's contents with the feedback.\n const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK);\n Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => {\n readFeedback.innerHTML = notificationsmarkedasread;\n });\n }.bind(this))\n .always(function() {\n this.markAllReadButton.removeClass('loading');\n }.bind(this));\n };\n\n /**\n * Add all of the required event listeners for this notification popover.\n *\n * @method registerEventListeners\n */\n NotificationPopoverController.prototype.registerEventListeners = function() {\n CustomEvents.define(this.root, [\n CustomEvents.events.activate,\n ]);\n\n // Mark all notifications read if the user activates the mark all as read button.\n this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {\n if (this.unreadCount > 0) {\n this.markAllAsRead();\n }\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n }.bind(this));\n\n // Mark individual notification read if the user activates it.\n this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {\n var element = $(e.target).closest(SELECTORS.NOTIFICATION);\n\n if (element.hasClass('unread')) {\n this.unreadCount--;\n element.removeClass('unread');\n }\n\n e.stopPropagation();\n }.bind(this));\n\n // Update the notification information when the menu is opened.\n this.root.on(this.events().menuOpened, function() {\n this.hideUnreadCount();\n this.updateButtonAriaLabel();\n\n if (!this.hasDoneInitialLoad()) {\n this.loadMoreNotifications();\n }\n\n // Lock focus to the popover when it is opened, it is the parent of the container.\n const contentContainer = this.getContentContainer()[0].parentNode;\n FocusLock.trapFocus(contentContainer);\n\n }.bind(this));\n\n // Update the unread notification count when the menu is closed.\n this.root.on(this.events().menuClosed, function() {\n this.renderUnreadCount();\n this.updateButtonAriaLabel();\n // Lock focus to the popover when it is opened, it is the parent of the container.\n FocusLock.untrapFocus();\n }.bind(this));\n\n // Set aria attributes when popover is loading.\n this.root.on(this.events().startLoading, function() {\n this.getContent().attr('aria-busy', 'true');\n }.bind(this));\n\n // Set aria attributes when popover is finished loading.\n this.root.on(this.events().stopLoading, function() {\n this.getContent().attr('aria-busy', 'false');\n }.bind(this));\n\n // Load more notifications if the user has scrolled to the end of content\n // item list.\n this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {\n if (!this.isLoading && !this.hasLoadedAllContent()) {\n this.loadMoreNotifications();\n }\n }.bind(this));\n\n // Stop mouse scroll from propagating to the window element and\n // scrolling the page.\n CustomEvents.define(this.getContentContainer(), [\n CustomEvents.events.scrollLock\n ]);\n\n // Listen for when a notification is shown in the notifications page and mark\n // it as read, if it's unread.\n $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {\n if (!notification.read) {\n var element = this.getNotificationElement(notification.id);\n\n if (element) {\n element.removeClass('unread');\n }\n\n this.unreadCount--;\n this.renderUnreadCount();\n }\n }.bind(this));\n };\n\n return NotificationPopoverController;\n});\n"],"names":["define","$","Ajax","Templates","Str","URL","DebugNotification","CustomEvents","PopoverController","NotificationRepo","NotificationAreaEvents","FocusLock","SELECTORS","NotificationPopoverController","element","call","this","markAllReadButton","root","find","unreadCount","lastQueried","userId","attr","container","limit","offset","loadedAll","initialLoad","html","prototype","Object","create","constructor","updateButtonAriaLabel","isMenuOpen","get_string","done","string","menuToggle","bind","getContent","getOffset","incrementOffset","hasDoneInitialLoad","hasLoadedAllContent","setLoadedAllContent","val","renderUnreadCount","text","removeClass","addClass","hideUnreadCount","getNotificationElement","id","length","renderNotifications","notifications","promises","each","index","notification","viewmoreurl","relativeUrl","notificationid","notificationurlparams","contexturl","promise","render","then","js","push","when","apply","arguments","argument","append","runTemplateJS","loadMoreNotifications","isLoading","Deferred","resolve","startLoading","request","useridto","query","result","unreadcount","Math","floor","Date","getTime","always","stopLoading","markAllAsRead","timecreatedto","readFeedback","get","querySelector","notificationsmarkedasread","innerHTML","registerEventListeners","events","activate","on","e","data","stopPropagation","originalEvent","preventDefault","target","closest","hasClass","menuOpened","contentContainer","getContentContainer","parentNode","trapFocus","menuClosed","untrapFocus","scrollBottom","scrollLock","document","notificationShown","read"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"notification_popover_controller.min.js","sources":["../src/notification_popover_controller.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 * Controls the notification popover in the nav bar.\n *\n * See template: message_popup/notification_popover\n *\n * @module message_popup/notification_popover_controller\n * @class notification_popover_controller\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',\n 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',\n 'message_popup/notification_repository', 'message_popup/notification_area_events',\n 'core/local/aria/focuslock',\n ],\n function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,\n PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) {\n\n var SELECTORS = {\n MARK_ALL_READ_BUTTON: '[data-action=\"mark-all-read\"]',\n ALL_NOTIFICATIONS_CONTAINER: '[data-region=\"all-notifications\"]',\n NOTIFICATION: '[data-region=\"notification-content-item-container\"]',\n UNREAD_NOTIFICATION: '[data-region=\"notification-content-item-container\"].unread',\n NOTIFICATION_LINK: '[data-action=\"content-item-link\"]',\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n COUNT_CONTAINER: '[data-region=\"count-container\"]',\n NOTIFICATION_READ_FEEDBACK: '[data-region=\"notification-read-feedback\"]',\n CLOSE_NOTIFICATION_POPOVER: '[data-action=\"close-notification-popover\"]',\n };\n\n /**\n * Constructor for the NotificationPopoverController.\n * Extends PopoverRegionController.\n *\n * @param {object} element jQuery object root element of the popover\n */\n var NotificationPopoverController = function(element) {\n // Initialise base class.\n PopoverController.call(this, element);\n\n this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);\n this.unreadCount = 0;\n this.lastQueried = 0;\n this.userId = this.root.attr('data-userid');\n this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);\n this.limit = 20;\n this.offset = 0;\n this.loadedAll = false;\n this.initialLoad = false;\n\n // Let's find out how many unread notifications there are.\n this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();\n };\n\n /**\n * Clone the parent prototype.\n */\n NotificationPopoverController.prototype = Object.create(PopoverController.prototype);\n\n /**\n * Make sure the constructor is set correctly.\n */\n NotificationPopoverController.prototype.constructor = NotificationPopoverController;\n\n /**\n * Set the correct aria label on the menu toggle button to be read out by screen\n * readers. The message will indicate the state of the unread notifications.\n *\n * @method updateButtonAriaLabel\n */\n NotificationPopoverController.prototype.updateButtonAriaLabel = function() {\n if (this.isMenuOpen()) {\n Str.get_string('hidenotificationwindow', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n if (this.unreadCount) {\n Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n Str.get_string('shownotificationwindownonew', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n }\n }\n };\n\n /**\n * Return the jQuery element with the content. This will return either\n * the unread notification container or the all notification container\n * depending on which is currently visible.\n *\n * @method getContent\n * @return {object} jQuery object currently visible content contianer\n */\n NotificationPopoverController.prototype.getContent = function() {\n return this.container;\n };\n\n /**\n * Get the offset value for the current state of the popover in order\n * to sent to the backend to correctly paginate the notifications.\n *\n * @method getOffset\n * @return {int} current offset\n */\n NotificationPopoverController.prototype.getOffset = function() {\n return this.offset;\n };\n\n /**\n * Increment the offset for the current state, if required.\n *\n * @method incrementOffset\n */\n NotificationPopoverController.prototype.incrementOffset = function() {\n this.offset += this.limit;\n };\n\n /**\n * Check if the first load of notification has been triggered for the current\n * state of the popover.\n *\n * @method hasDoneInitialLoad\n * @return {bool} true if first notification loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasDoneInitialLoad = function() {\n return this.initialLoad;\n };\n\n /**\n * Check if we've loaded all of the notifications for the current popover\n * state.\n *\n * @method hasLoadedAllContent\n * @return {bool} true if all notifications loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasLoadedAllContent = function() {\n return this.loadedAll;\n };\n\n /**\n * Set the state of the loaded all content property for the current state\n * of the popover.\n *\n * @method setLoadedAllContent\n * @param {bool} val True if all content is loaded, false otherwise\n */\n NotificationPopoverController.prototype.setLoadedAllContent = function(val) {\n this.loadedAll = val;\n };\n\n /**\n * Show the unread notification count badge on the menu toggle if there\n * are unread notifications, otherwise hide it.\n *\n * @method renderUnreadCount\n */\n NotificationPopoverController.prototype.renderUnreadCount = function() {\n var element = this.root.find(SELECTORS.COUNT_CONTAINER);\n\n if (this.unreadCount) {\n element.text(this.unreadCount);\n element.removeClass('hidden');\n } else {\n element.addClass('hidden');\n }\n };\n\n /**\n * Hide the unread notification count badge on the menu toggle.\n *\n * @method hideUnreadCount\n */\n NotificationPopoverController.prototype.hideUnreadCount = function() {\n this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');\n };\n\n /**\n * Find the notification element for the given id.\n *\n * @param {int} id\n * @method getNotificationElement\n * @return {object|null} The notification element\n */\n NotificationPopoverController.prototype.getNotificationElement = function(id) {\n var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id=\"' + id + '\"]');\n return element.length == 1 ? element : null;\n };\n\n /**\n * Render the notification data with the appropriate template and add it to the DOM.\n *\n * @method renderNotifications\n * @param {array} notifications Notification data\n * @param {object} container jQuery object the container to append the rendered notifications\n * @return {object} jQuery promise that is resolved when all notifications have been\n * rendered and added to the DOM\n */\n NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {\n var promises = [];\n\n $.each(notifications, function(index, notification) {\n // Determine what the offset was when loading this notification.\n var offset = this.getOffset() - this.limit;\n // Update the view more url to contain the offset to allow the notifications\n // page to load to the correct position in the list of notifications.\n notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {\n notificationid: notification.id,\n offset: offset,\n });\n\n // Link to mark read page before loading the actual link.\n var notificationurlparams = {\n notificationid: notification.id\n };\n\n notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);\n\n var promise = Templates.render('message_popup/notification_content_item', notification)\n .then(function(html, js) {\n return {html: html, js: js};\n });\n promises.push(promise);\n }.bind(this));\n\n return $.when.apply($, promises).then(function() {\n // Each of the promises in the when will pass its results as an argument to the function.\n // The order of the arguments will be the order that the promises are passed to when()\n // i.e. the first promise's results will be in the first argument.\n $.each(arguments, function(index, argument) {\n container.append(argument.html);\n Templates.runTemplateJS(argument.js);\n });\n return;\n });\n };\n\n /**\n * Send a request for more notifications from the server, if we aren't already\n * loading some and haven't already loaded all of them.\n *\n * Takes into account the current mode of the popover and will request only\n * unread notifications if required.\n *\n * All notifications are marked as read by the server when they are returned.\n *\n * @method loadMoreNotifications\n * @return {object} jQuery promise that is resolved when notifications have been\n * retrieved and added to the DOM\n */\n NotificationPopoverController.prototype.loadMoreNotifications = function() {\n if (this.isLoading || this.hasLoadedAllContent()) {\n return $.Deferred().resolve();\n }\n\n this.startLoading();\n var request = {\n limit: this.limit,\n offset: this.getOffset(),\n useridto: this.userId,\n };\n\n var container = this.getContent();\n return NotificationRepo.query(request).then(function(result) {\n var notifications = result.notifications;\n this.unreadCount = result.unreadcount;\n this.lastQueried = Math.floor(new Date().getTime() / 1000);\n this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);\n this.initialLoad = true;\n this.updateButtonAriaLabel();\n\n if (notifications.length) {\n this.incrementOffset();\n return this.renderNotifications(notifications, container);\n }\n\n return false;\n }.bind(this))\n .always(function() {\n this.stopLoading();\n }.bind(this));\n };\n\n /**\n * Send a request to the server to mark all unread notifications as read and update\n * the unread count and unread notification elements appropriately.\n *\n * @return {Promise}\n * @method markAllAsRead\n */\n NotificationPopoverController.prototype.markAllAsRead = function() {\n this.markAllReadButton.addClass('loading');\n\n var request = {\n useridto: this.userId,\n timecreatedto: this.lastQueried,\n };\n\n return NotificationRepo.markAllAsRead(request)\n .then(function() {\n this.unreadCount = 0;\n this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');\n\n // Set the ARIA live region's contents with the feedback.\n const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK);\n Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => {\n readFeedback.innerHTML = notificationsmarkedasread;\n });\n }.bind(this))\n .always(function() {\n this.markAllReadButton.removeClass('loading');\n }.bind(this));\n };\n\n /**\n * Add all of the required event listeners for this notification popover.\n *\n * @method registerEventListeners\n */\n NotificationPopoverController.prototype.registerEventListeners = function() {\n CustomEvents.define(this.root, [\n CustomEvents.events.activate,\n ]);\n\n // Mark all notifications read if the user activates the mark all as read button.\n this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {\n if (this.unreadCount > 0) {\n this.markAllAsRead();\n }\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n }.bind(this));\n\n // Mark individual notification read if the user activates it.\n this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {\n var element = $(e.target).closest(SELECTORS.NOTIFICATION);\n\n if (element.hasClass('unread')) {\n this.unreadCount--;\n element.removeClass('unread');\n }\n\n e.stopPropagation();\n }.bind(this));\n\n this.root.on(CustomEvents.events.activate, SELECTORS.CLOSE_NOTIFICATION_POPOVER, function(e) {\n e.preventDefault();\n $(this.root).trigger(CustomEvents.events.escape);\n e.stopPropagation();\n }.bind(this));\n\n // Update the notification information when the menu is opened.\n this.root.on(this.events().menuOpened, function() {\n this.hideUnreadCount();\n this.updateButtonAriaLabel();\n\n if (!this.hasDoneInitialLoad()) {\n this.loadMoreNotifications();\n }\n\n // Lock focus to the popover when it is opened, it is the parent of the container.\n const contentContainer = this.getContentContainer()[0].parentNode;\n FocusLock.trapFocus(contentContainer);\n\n }.bind(this));\n\n // Update the unread notification count when the menu is closed.\n this.root.on(this.events().menuClosed, function() {\n this.renderUnreadCount();\n this.updateButtonAriaLabel();\n // Lock focus to the popover when it is opened, it is the parent of the container.\n FocusLock.untrapFocus();\n }.bind(this));\n\n // Set aria attributes when popover is loading.\n this.root.on(this.events().startLoading, function() {\n this.getContent().attr('aria-busy', 'true');\n }.bind(this));\n\n // Set aria attributes when popover is finished loading.\n this.root.on(this.events().stopLoading, function() {\n this.getContent().attr('aria-busy', 'false');\n }.bind(this));\n\n // Load more notifications if the user has scrolled to the end of content\n // item list.\n this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {\n if (!this.isLoading && !this.hasLoadedAllContent()) {\n this.loadMoreNotifications();\n }\n }.bind(this));\n\n // Stop mouse scroll from propagating to the window element and\n // scrolling the page.\n CustomEvents.define(this.getContentContainer(), [\n CustomEvents.events.scrollLock\n ]);\n\n // Listen for when a notification is shown in the notifications page and mark\n // it as read, if it's unread.\n $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {\n if (!notification.read) {\n var element = this.getNotificationElement(notification.id);\n\n if (element) {\n element.removeClass('unread');\n }\n\n this.unreadCount--;\n this.renderUnreadCount();\n }\n }.bind(this));\n };\n\n return NotificationPopoverController;\n});\n"],"names":["define","$","Ajax","Templates","Str","URL","DebugNotification","CustomEvents","PopoverController","NotificationRepo","NotificationAreaEvents","FocusLock","SELECTORS","NotificationPopoverController","element","call","this","markAllReadButton","root","find","unreadCount","lastQueried","userId","attr","container","limit","offset","loadedAll","initialLoad","html","prototype","Object","create","constructor","updateButtonAriaLabel","isMenuOpen","get_string","done","string","menuToggle","bind","getContent","getOffset","incrementOffset","hasDoneInitialLoad","hasLoadedAllContent","setLoadedAllContent","val","renderUnreadCount","text","removeClass","addClass","hideUnreadCount","getNotificationElement","id","length","renderNotifications","notifications","promises","each","index","notification","viewmoreurl","relativeUrl","notificationid","notificationurlparams","contexturl","promise","render","then","js","push","when","apply","arguments","argument","append","runTemplateJS","loadMoreNotifications","isLoading","Deferred","resolve","startLoading","request","useridto","query","result","unreadcount","Math","floor","Date","getTime","always","stopLoading","markAllAsRead","timecreatedto","readFeedback","get","querySelector","notificationsmarkedasread","innerHTML","registerEventListeners","events","activate","on","e","data","stopPropagation","originalEvent","preventDefault","target","closest","hasClass","trigger","escape","menuOpened","contentContainer","getContentContainer","parentNode","trapFocus","menuClosed","untrapFocus","scrollBottom","scrollLock","document","notificationShown","read"],"mappings":"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"} \ No newline at end of file diff --git a/public/message/output/popup/amd/src/notification_popover_controller.js b/public/message/output/popup/amd/src/notification_popover_controller.js index b2db9370498..eed763b9e62 100644 --- a/public/message/output/popup/amd/src/notification_popover_controller.js +++ b/public/message/output/popup/amd/src/notification_popover_controller.js @@ -40,6 +40,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url', EMPTY_MESSAGE: '[data-region="empty-message"]', COUNT_CONTAINER: '[data-region="count-container"]', NOTIFICATION_READ_FEEDBACK: '[data-region="notification-read-feedback"]', + CLOSE_NOTIFICATION_POPOVER: '[data-action="close-notification-popover"]', }; /** @@ -360,6 +361,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url', e.stopPropagation(); }.bind(this)); + this.root.on(CustomEvents.events.activate, SELECTORS.CLOSE_NOTIFICATION_POPOVER, function(e) { + e.preventDefault(); + $(this.root).trigger(CustomEvents.events.escape); + e.stopPropagation(); + }.bind(this)); + // Update the notification information when the menu is opened. this.root.on(this.events().menuOpened, function() { this.hideUnreadCount(); diff --git a/public/message/output/popup/templates/notification_popover.mustache b/public/message/output/popup/templates/notification_popover.mustache index 72fc9f2b388..ec931985510 100644 --- a/public/message/output/popup/templates/notification_popover.mustache +++ b/public/message/output/popup/templates/notification_popover.mustache @@ -82,6 +82,9 @@ aria-label="{{#str}} notificationpreferences, message {{/str}}"> {{#pix}} i/settings, core {{/pix}} {{/ urls.preferences }} + {{/headeractions}} {{$content}} From 2bf9750b6908e6446346e60fbe4cc2b80f9a929f Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Fri, 1 Aug 2025 11:40:37 +0800 Subject: [PATCH 3/3] MDL-85231 message: Style the notification buttons consistently --- .../output/popup/templates/notification_popover.mustache | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/message/output/popup/templates/notification_popover.mustache b/public/message/output/popup/templates/notification_popover.mustache index ec931985510..b54eaab7a2a 100644 --- a/public/message/output/popup/templates/notification_popover.mustache +++ b/public/message/output/popup/templates/notification_popover.mustache @@ -65,7 +65,7 @@ {{$headertext}}{{#str}} notifications, message {{/str}}{{/headertext}} {{$headeractions}} - + aria-label="{{#str}} notificationpreferences, message {{/str}}" + class="btn btn-sm btn-link m-0 py-0 icon-no-margin" > {{#pix}} i/settings, core {{/pix}} {{/ urls.preferences }}