diff --git a/lang/en/message.php b/lang/en/message.php index 629220d8dbc4d..ec13709665481 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -37,6 +37,7 @@ $string['blockedmessages'] = '{$a} message(s) to/from blocked users'; $string['blockedusers'] = 'Blocked users ({$a})'; $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me'; +$string['collapsenotification'] = 'Collapse notification'; $string['contactlistempty'] = 'Contact list empty'; $string['contacts'] = 'Contacts'; $string['context'] = 'context'; @@ -65,11 +66,13 @@ $string['eventmessagedeleted'] = 'Message deleted'; $string['eventmessageviewed'] = 'Message viewed'; $string['eventmessagesent'] = 'Message sent'; +$string['expandnotification'] = 'Expand notification'; $string['forced'] = 'Forced'; $string['formorethan'] = 'For more than'; $string['guestnoeditmessage'] = 'Guest user can not edit messaging options'; $string['guestnoeditmessageother'] = 'Guest user can not edit other user messaging options'; $string['gotomessages'] = 'Go to messages'; +$string['hidenotificationwindow'] = 'Hide notification window'; $string['includeblockedusers'] = 'Include blocked users'; $string['incomingcontacts'] = 'Incoming contacts ({$a})'; $string['keywords'] = 'Keywords'; @@ -105,6 +108,12 @@ $string['nomessagesfound'] = 'No messages were found'; $string['noreply'] = 'Do not reply to this message'; $string['nosearchresults'] = 'There were no results from your search'; +$string['nonotifications'] = 'You have no notifications'; +$string['nonewnotifications'] = 'You have no new notifications'; +$string['notificationwindow'] = 'Notification window'; +$string['notificationpreferences'] = 'Notification preferences'; +$string['notificationimage'] = 'Notification image'; +$string['notifications'] = 'Notifications'; $string['offline'] = 'Offline'; $string['offlinecontacts'] = 'Offline contacts ({$a})'; $string['online'] = 'Online'; @@ -144,10 +153,15 @@ $string['settings'] = 'Settings'; $string['settingssaved'] = 'Your settings have been saved'; $string['showmessagewindow'] = 'Popup window on new message'; +$string['showallnotifications'] = 'Show all notifications'; +$string['shownewnotifications'] = 'Show new notifications'; +$string['shownotificationwindownonew'] = 'Show notification window with no new notifications'; +$string['shownotificationwindowwithcount'] = 'Show notification window with {$a} new notifications'; $string['strftimedaydatetime'] = '%A, %d %B %Y, %I:%M %p'; $string['thisconversation'] = 'this conversation'; $string['timenosee'] = 'Minutes since I was last seen online'; $string['timesent'] = 'Time sent'; +$string['togglenotificationmenu'] = 'Toggle notification menu'; $string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) that doesn\'t exist'; $string['unabletomessageuser'] = 'You are not permitted to send a message to that user'; $string['unblockcontact'] = 'Unblock contact'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index b7334d7fddfc6..a5a6bcb0d05cf 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -934,6 +934,7 @@ $string['hidesection'] = 'Hide section {$a}'; $string['hidesettings'] = 'Hide settings'; $string['hideshowblocks'] = 'Hide or show blocks'; +$string['hidepopoverwindow'] = 'Hide popover window'; $string['highlight'] = 'Highlight'; $string['highlightoff'] = 'Remove highlight'; $string['hits'] = 'Hits'; @@ -1099,6 +1100,7 @@ $string['managefilters'] = 'Filters'; $string['managemodules'] = 'Modules'; $string['manageroles'] = 'Roles and permissions'; +$string['markallread'] = 'Mark all as read'; $string['markedthistopic'] = 'This topic is highlighted as the current topic'; $string['markthistopic'] = 'Highlight this topic as the current topic'; $string['matchingsearchandrole'] = 'Matching \'{$a->search}\' and {$a->role}'; @@ -1706,6 +1708,7 @@ $string['showmodulecourse'] = 'Show list of courses containing activity'; $string['showonly'] = 'Show only'; $string['showperpage'] = 'Show {$a} per page'; +$string['showpopoverwindow'] = 'Show popover window'; $string['showrecent'] = 'Show recent activity'; $string['showreports'] = 'Show activity reports'; $string['showreports_help'] = 'Activity reports are available for each participant that show their activity in the course. As well as listings of their contributions, such as forum posts or assignment submissions, these reports also include access logs. This setting determines whether a student can view their own activity reports via their profile page.'; diff --git a/lib/amd/src/mdl_popover_controller.js b/lib/amd/src/mdl_popover_controller.js new file mode 100644 index 0000000000000..f8edcd6a7e9da --- /dev/null +++ b/lib/amd/src/mdl_popover_controller.js @@ -0,0 +1,342 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the mdl popover element. + * + * See template: core/mdl_popover + * + * @module core/mdl_popover_controller + * @class mdl_popover_controller + * @package core + * @copyright 2015 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define(['jquery', 'core/str', 'core/custom_interaction_events'], + function($, str, customEvents) { + + var SELECTORS = { + CONTENT: '.mdl-popover-content', + CONTENT_CONTAINER: '.mdl-popover-content-container', + MENU_CONTAINER: '.mdl-popover-container', + MENU_TOGGLE: '.mdl-popover-toggle', + }; + + /** + * Constructor for the MdlPopoverController. + * + * @param element jQuery object root element of the popover + * @return object MdlPopoverController + */ + var MdlPopoverController = function(element) { + this.root = $(element); + this.content = this.root.find(SELECTORS.CONTENT); + this.contentContainer = this.root.find(SELECTORS.CONTENT_CONTAINER); + this.menuContainer = this.root.find(SELECTORS.MENU_CONTAINER); + this.menuToggle = this.root.find(SELECTORS.MENU_TOGGLE); + this.isLoading = false; + }; + + /** + * The collection of events triggered by this controller. + */ + MdlPopoverController.prototype.events = function() { + return { + menuOpened: 'mdlpopover:menuopened', + menuClosed: 'mdlpopover:menuclosed', + startLoading: 'mdlpopover:startLoading', + stopLoading: 'mdlpopover:stopLoading', + }; + }; + + /** + * Return the container element for the content element. + * + * @method getContentContainer + * @return jQuery object + */ + MdlPopoverController.prototype.getContentContainer = function() { + return this.contentContainer; + }; + + /** + * Return the content element. + * + * @method getContent + * @return jQuery object + */ + MdlPopoverController.prototype.getContent = function() { + return this.content; + }; + + /** + * Checks if the popover is displayed. + * + * @method isMenuOpen + * @return bool + */ + MdlPopoverController.prototype.isMenuOpen = function() { + return !this.root.hasClass('collapsed'); + }; + + /** + * Toggle the visibility of the popover. + * + * @method toggleMenu + */ + MdlPopoverController.prototype.toggleMenu = function() { + if (this.isMenuOpen()) { + this.closeMenu(); + } else { + this.openMenu(); + } + }; + + /** + * Hide the popover. + * + * Note: This triggers the menuClosed event. + * + * @method closeMenu + */ + MdlPopoverController.prototype.closeMenu = function() { + // We're already closed. + if (!this.isMenuOpen()) { + return; + } + + this.root.addClass('collapsed'); + this.menuContainer.attr('aria-expanded', 'false'); + this.menuContainer.attr('aria-hidden', 'true'); + this.updateButtonAriaLabel(); + this.root.trigger(this.events().menuClosed); + }; + + /** + * Show the popover. + * + * Note: This triggers the menuOpened event. + * + * @method openMenu + */ + MdlPopoverController.prototype.openMenu = function() { + // We're already open. + if (this.isMenuOpen()) { + return; + } + + this.root.removeClass('collapsed'); + this.menuContainer.attr('aria-expanded', 'true'); + this.menuContainer.attr('aria-hidden', 'false'); + this.updateButtonAriaLabel(); + this.root.trigger(this.events().menuOpened); + }; + + /** + * Set the appropriate aria label on the popover toggle. + * + * @method updateButtonAriaLabel + */ + MdlPopoverController.prototype.updateButtonAriaLabel = function() { + if (this.isMenuOpen()) { + str.get_string('hidepopoverwindow').done(function(string) { + this.menuToggle.attr('aria-label', string); + }.bind(this)); + } else { + str.get_string('showpopoverwindow').done(function(string) { + this.menuToggle.attr('aria-label', string); + }.bind(this)); + } + }; + + /** + * Set the loading state on this popover. + * + * Note: This triggers the startLoading event. + * + * @method startLoading + */ + MdlPopoverController.prototype.startLoading = function() { + this.isLoading = true; + this.getContentContainer().addClass('loading'); + this.getContentContainer().attr('aria-busy', 'true'); + this.root.trigger(this.events().startLoading); + }; + + /** + * Undo the loading state on this popover. + * + * Note: This triggers the stopLoading event. + * + * @method stopLoading + */ + MdlPopoverController.prototype.stopLoading = function() { + this.isLoading = false; + this.getContentContainer().removeClass('loading'); + this.getContentContainer().attr('aria-busy', 'false'); + this.root.trigger(this.events().stopLoading); + }; + + /** + * Sets the focus on the menu toggle. + * + * @method focusMenuToggle + */ + MdlPopoverController.prototype.focusMenuToggle = function() { + this.menuToggle.focus(); + }; + + /** + * Return the currently focused content item. + * + * @method getContentItemWithFocus + * @return jQuery object + */ + MdlPopoverController.prototype.getContentItemWithFocus = function() { + var currentFocus = $(document.activeElement); + var items = this.getContent().children(); + var currentItem = items.filter(currentFocus); + + if (!currentItem.length) { + currentItem = items.has(currentFocus); + } + + return currentItem; + }; + + /** + * Set focus on the first content item in the list. + * + * @method focusFirstContentItem + */ + MdlPopoverController.prototype.focusFirstContentItem = function() { + this.getContent().children().first().focus(); + }; + + /** + * Set focus on the last content item in the list. + * + * @method focusLastContentItem + */ + MdlPopoverController.prototype.focusLastContentItem = function() { + this.getContent().children().last().focus(); + }; + + /** + * Set focus on the content item after the item that currently has focus + * in the list. + * + * @method focusNextContentItem + */ + MdlPopoverController.prototype.focusNextContentItem = function() { + var currentItem = this.getContentItemWithFocus(); + + if (currentItem.length && currentItem.next()) { + currentItem.next().focus(); + } + }; + + /** + * Set focus on the content item preceding the item that currently has focus + * in the list. + * + * @method focusPreviousContentItem + */ + MdlPopoverController.prototype.focusPreviousContentItem = function() { + var currentItem = this.getContentItemWithFocus(); + + if (currentItem.length && currentItem.prev()) { + currentItem.prev().focus(); + } + }; + + /** + * Register the minimal amount of listeners for the popover to function. + * + * @method registerBaseEventListeners + */ + MdlPopoverController.prototype.registerBaseEventListeners = function() { + customEvents.define(this.root, [ + customEvents.events.activate, + customEvents.events.escape, + ]); + + // Toggle the popover visibility on activation (click/enter/space) of the toggle button. + this.root.on(customEvents.events.activate, SELECTORS.MENU_TOGGLE, function() { + this.toggleMenu(); + }.bind(this)); + + // Close the popover if escape is pressed. + this.root.on(customEvents.events.escape, function() { + this.closeMenu(); + this.focusMenuToggle(); + }.bind(this)); + + // Close the popover if any other part of the page is clicked. + $('html').click(function(e) { + var target = $(e.target); + if (!this.root.is(target) && !this.root.has(target).length) { + this.closeMenu(); + } + }.bind(this)); + + customEvents.define(this.getContentContainer(), [ + customEvents.events.scrollBottom + ]); + }; + + /** + * Set up the event listeners for keyboard navigating a list of content items. + * + * @method registerListNavigationEventListeners + */ + MdlPopoverController.prototype.registerListNavigationEventListeners = function() { + customEvents.define(this.root, [ + customEvents.events.down, + customEvents.events.up, + customEvents.events.home, + customEvents.events.end, + ]); + + // If the down arrow is pressed then open the menu and focus the first content + // item or focus the next content item if the menu is open. + this.root.on(customEvents.events.down, function() { + if (!this.isMenuOpen()) { + this.openMenu(); + this.focusFirstContentItem(); + } else { + this.focusNextContentItem(); + } + }.bind(this)); + + // Shift focus to the previous content item if the up key is pressed. + this.root.on(customEvents.events.up, function() { + this.focusPreviousContentItem(); + }.bind(this)); + + // Jump focus to the first content item if the home key is pressed. + this.root.on(customEvents.events.home, function() { + this.focusFirstContentItem(); + }.bind(this)); + + // Jump focus to the last content item if the end key is pressed. + this.root.on(customEvents.events.end, function() { + this.focusLastContentItem(); + }.bind(this)); + }; + + return MdlPopoverController; +}); diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 49de5d34ffc8b..be0a54b4495aa 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -145,6 +145,14 @@ public function render_from_template($templatename, $context) { static $templatecache = array(); $mustache = $this->get_mustache(); + try { + // Grab a copy of the existing helper to be restored later. + $uniqidHelper = $mustache->getHelper('uniqid'); + } catch (Mustache_Exception_UnknownHelperException $e) { + // Helper doesn't exist. + $uniqidHelper = null; + } + // Provide 1 random value that will not change within a template // but will be different from template to template. This is useful for // e.g. aria attributes that only work with id attributes and must be @@ -3184,6 +3192,27 @@ public function search_box($id = false) { return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper', 'id' => $id)); } + /** + * Returns the noticication menu + * + * @return string HTML for the notification menu + */ + public function notification_menu() { + global $USER; + + if (isloggedin()) { + $context = [ + 'userid' => $USER->id, + 'urls' => [ + 'preferences' => (new moodle_url('/message/edit.php', ['id' => $USER->id]))->out(), + ], + ]; + return $this->render_from_template('message/notification_popover', $context); + } else { + return ''; + } + } + /** * Construct a user menu, returning HTML that can be echoed out by a * layout file. diff --git a/lib/templates/mdl_popover.mustache b/lib/templates/mdl_popover.mustache new file mode 100644 index 0000000000000..9115d70d8537f --- /dev/null +++ b/lib/templates/mdl_popover.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/mdl_popover + + This template will render a mdl popover + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * userid the logged in user id + + Example context (json): + { + } + +}} + +{{#js}} +require(['jquery', 'core/mdl_popover_controller'], function($, controller) { + var container = $(".mdl-popover-{{uniqid}}"); + var controller = new controller(container); + controller.registerBaseEventListeners(); +}); +{{/js}} diff --git a/message/amd/src/notification_popover_controller.js b/message/amd/src/notification_popover_controller.js new file mode 100644 index 0000000000000..411e58e32d447 --- /dev/null +++ b/message/amd/src/notification_popover_controller.js @@ -0,0 +1,631 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Controls the notification popover in the nav bar. + * + * See template: message/notification_menu + * + * @module message/notification_popover_controller + * @class notification_popover_controller + * @package message + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define(['jquery', 'theme_bootstrapbase/bootstrap', 'core/ajax', 'core/templates', 'core/str', + 'core/notification', 'core/custom_interaction_events', 'core/mdl_popover_controller', + 'message/notification_repository'], + function($, bootstrap, ajax, templates, str, debugNotification, customEvents, + popoverController, notificationRepo) { + + var SELECTORS = { + MARK_ALL_READ_BUTTON: '#mark-all-read-button', + USER_ID: 'data-userid', + MODE_TOGGLE: '.mdl-popover-header-actions .fancy-toggle', + UNREAD_NOTIFICATIONS_CONTAINER: '.unread-notifications', + ALL_NOTIFICATIONS_CONTAINER: '.all-notifications', + SHOW_BUTTON: '.show-button', + HIDE_BUTTON: '.hide-button', + CONTENT_ITEM_CONTAINER: '.content-item-container', + EMPTY_MESSAGE: '.empty-message', + CONTENT_BODY_SHORT: '.content-body-short', + CONTENT_BODY_FULL: '.content-body-full', + }; + + /** + * Constructor for the NotificationPopoverController. + * Extends MdlPopoverController. + * + * @param element jQuery object root element of the popover + * @return object NotificationPopoverController + */ + var NotificationPopoverController = function(element) { + // Initialise base class. + popoverController.call(this, element); + + this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON); + this.unreadCount = 0; + this.userId = this.root.attr(SELECTORS.USER_ID); + this.modeToggle = this.root.find(SELECTORS.MODE_TOGGLE); + this.state = { + unread: { + container: this.root.find(SELECTORS.UNREAD_NOTIFICATIONS_CONTAINER), + limit: 6, + offset: 0, + loadedAll: false, + initialLoad: false, + }, + all: { + container: this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER), + limit: 20, + offset: 0, + loadedAll: false, + initialLoad: false, + } + }; + + // Let's find out how many unread notifications there are. + this.loadUnreadNotificationCount(); + this.root.find('[data-toggle="tooltip"]').tooltip(); + }; + + /** + * Clone the parent prototype. + */ + NotificationPopoverController.prototype = Object.create(popoverController.prototype); + + /** + * Set the correct aria label on the menu toggle button to be read out by screen + * readers. The message will indicate the state of the unread notifications. + * + * @method updateButtonAriaLabel + */ + NotificationPopoverController.prototype.updateButtonAriaLabel = function() { + if (this.isMenuOpen()) { + str.get_string('hidenotificationwindow', 'message').done(function(string) { + this.menuToggle.attr('aria-label', string); + }.bind(this)); + } else { + if (this.unreadCount) { + str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) { + this.menuToggle.attr('aria-label', string); + }.bind(this)); + } else { + str.get_string('shownotificationwindownonew', 'message').done(function(string) { + this.menuToggle.attr('aria-label', string); + }.bind(this)); + } + } + }; + + /** + * Return the jQuery element with the content. This will return either + * the unread notification container or the all notification container + * depending on which is currently visible. + * + * @method getContent + * @return jQuery object currently visible content contianer + */ + NotificationPopoverController.prototype.getContent = function() { + return this.getState().container; + }; + + /** + * Check whether the notification menu is showing unread notification or + * all notifications. + * + * @method unreadOnlyMode + * @return bool true if only showing unread notifications, false otherwise + */ + NotificationPopoverController.prototype.unreadOnlyMode = function() { + return this.modeToggle.hasClass('on'); + }; + + /** + * Get the current state of the notification menu. Checks whether + * the popover is in unread only mode. + * + * The internal state tracks various properties required for loading + * notifications. + * + * @method getState + * @return object unread state or all state + */ + NotificationPopoverController.prototype.getState = function() { + if (this.unreadOnlyMode()) { + return this.state.unread; + } else { + return this.state.all; + } + }; + + /** + * Get the offset value for the current state of the popover in order + * to sent to the backend to correctly paginate the notifications. + * + * @method getOffset + * @return int current offset + */ + NotificationPopoverController.prototype.getOffset = function() { + return this.getState().offset; + }; + + /** + * Increment the offset for the current state, if required. + * + * @method incrementOffset + */ + NotificationPopoverController.prototype.incrementOffset = function() { + // Only need to increment offset if we're combining read and unread + // because all unread messages are marked as read when we retrieve them + // which acts as the result set increment for us. + if (!this.unreadOnlyMode()) { + this.getState().offset += this.getState().limit; + } + }; + + /** + * Reset the offset to zero for the current state. + * + * @method resetOffset + */ + NotificationPopoverController.prototype.resetOffset = function() { + this.getState().offset = 0; + }; + + /** + * Check if the first load of notification has been triggered for the current + * state of the popover. + * + * @method hasDoneInitialLoad + * @return bool true if first notification loaded, false otherwise + */ + NotificationPopoverController.prototype.hasDoneInitialLoad = function() { + return this.getState().initialLoad; + }; + + /** + * Check if we've loaded all of the notifications for the current popover + * state. + * + * @method hasLoadedAllContent + * @return bool true if all notifications loaded, false otherwise + */ + NotificationPopoverController.prototype.hasLoadedAllContent = function() { + return this.getState().loadedAll; + }; + + /** + * Set the state of the loaded all content property for the current state + * of the popover. + * + * @method setLoadedAllContent + * @param bool true if all content is loaded, false otherwise + */ + NotificationPopoverController.prototype.setLoadedAllContent = function(val) { + this.getState().loadedAll = val; + }; + + /** + * Reset the unread notification state and empty the unread notification content + * element. + * + * @method clearUnreadNotifications + */ + NotificationPopoverController.prototype.clearUnreadNotifications = function() { + this.state.unread.offset = 0; + this.state.unread.loadedAll = false; + this.state.unread.initialLoad = false; + this.state.unread.container.empty(); + }; + + /** + * Show the unread notification count badge on the menu toggle if there + * are unread notifications, otherwise hide it. + * + * @method renderUnreadCount + */ + NotificationPopoverController.prototype.renderUnreadCount = function() { + var element = this.root.find('.count-container'); + + if (this.unreadCount) { + element.text(this.unreadCount); + element.removeClass('hidden'); + } else { + element.addClass('hidden'); + } + }; + + /** + * Hide the unread notification count badge on the menu toggle. + * + * @method hideUnreadCount + */ + NotificationPopoverController.prototype.hideUnreadCount = function() { + this.root.find('.count-container').addClass('hidden'); + }; + + /** + * Ask the server how many unread notifications are left, render the value + * as a badge on the menu toggle and update the aria labels on the menu + * toggle. + * + * @method loadUnreadNotificationCount + */ + NotificationPopoverController.prototype.loadUnreadNotificationCount = function() { + notificationRepo.countUnread({useridto: this.userId}).then(function(count) { + this.unreadCount = count; + this.renderUnreadCount(); + this.updateButtonAriaLabel(); + }.bind(this)); + }; + + /** + * Render the notification data with the appropriate template and add it to the DOM. + * + * @method renderNotifications + * @param notifications array notification data + * @param container jQuery object the container to append the rendered notifications + * @return jQuery promise that is resolved when all notifications have been + * rendered and added to the DOM + */ + NotificationPopoverController.prototype.renderNotifications = function(notifications, container) { + var promises = []; + + if (notifications.length) { + $.each(notifications, function(index, notification) { + var promise = templates.render('message/notification_content_item', notification); + promise.then(function(html, js) { + container.append(html); + templates.runTemplateJS(js); + }.bind(this)); + + promises.push(promise); + }.bind(this)); + } + + return $.when.apply($.when, promises); + }; + + /** + * Send a request for more notifications from the server, if we aren't already + * loading some and haven't already loaded all of them. + * + * Takes into account the current mode of the popover and will request only + * unread notifications if required. + * + * All notifications are marked as read by the server when they are returned. + * + * @method loadMoreNotifications + * @return jQuery promise that is resolved when notifications have been + * retrieved and added to the DOM + */ + 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, + markasread: true, + embeduserto: false, + embeduserfrom: true, + }; + + if (this.unreadOnlyMode()) { + request.status = 'unread'; + } + + var container = this.getContent(); + var promise = notificationRepo.query(request).then(function(result) { + var notifications = result.notifications; + this.unreadCount = result.unreadcount; + this.setLoadedAllContent(!notifications.length || notifications.length < this.limit); + this.getState().initialLoad = true; + this.updateButtonAriaLabel(); + + if (notifications.length) { + this.incrementOffset(); + return this.renderNotifications(notifications, container); + } + }.bind(this)) + .always(function() { this.stopLoading(); }.bind(this)); + + return promise; + }; + + /** + * Send a request to the server to mark all unread notifications as read and update + * the unread count and unread notification elements appropriately. + * + * @method markAllAsRead + */ + NotificationPopoverController.prototype.markAllAsRead = function() { + this.markAllReadButton.addClass('loading'); + + return notificationRepo.markAllAsRead({useridto: this.userId}) + .then(function() { + this.unreadCount = 0; + this.clearUnreadNotifications(); + }.bind(this)) + .always(function() { this.markAllReadButton.removeClass('loading'); }.bind(this)); + }; + + /** + * Shift focus to the next content item in the list if the content item + * list current contains focus, otherwise the first item in the list is + * given focus. + * + * Overrides MdlPopoverController.focusNextContentItem + * @method focusNextContentItem + */ + NotificationPopoverController.prototype.focusNextContentItem = function() { + var currentFocus = $(document.activeElement); + var container = this.getContent(); + + if (container.has(currentFocus).length) { + var currentNotification = currentFocus.closest(SELECTORS.CONTENT_ITEM_CONTAINER); + currentNotification.next().focus(); + } else { + this.focusFirstContentItem(); + } + }; + + /** + * Shift focus to the previous content item in the content item list, if the + * content item list contains focus. + * + * Overrides MdlPopoverController.focusPreviousContentItem + * @method focusPreviousContentItem + */ + NotificationPopoverController.prototype.focusPreviousContentItem = function() { + var currentFocus = $(document.activeElement); + var container = this.getContent(); + + if (container.has(currentFocus).length) { + var currentNotification = currentFocus.closest(SELECTORS.CONTENT_ITEM_CONTAINER); + currentNotification.prev().focus(); + } + }; + + /** + * Give focus to the first item in the list of content items. + * + * Overrides MdlPopoverController.focusFirstContentItem + * @method focusFirstContentItem + */ + NotificationPopoverController.prototype.focusFirstContentItem = function() { + var container = this.getContent(); + var notification = container.children().first(); + + if (!notification.length) { + // If we don't have any notifications then we should focus the empty + // empty message for the user. + notification = container.next(SELECTORS.EMPTY_MESSAGE); + } + + notification.focus(); + }; + + /** + * Give focus to the last item in the list of content items, that is the list + * of notifications that have already been loaded. + * + * Overrides MdlPopoverController.focusLastContentItem + * @method focusLastContentItem + */ + NotificationPopoverController.prototype.focusLastContentItem = function() { + var container = this.getContent(); + var notification = container.children().last(); + + if (!notification.length) { + // If we don't have any notifications then we should focus the empty + // empty message for the user. + notification = container.next(SELECTORS.EMPTY_MESSAGE); + } + + notification.focus(); + }; + + /** + * Expand all the currently rendered notificaitons in the current state + * of the popover (unread or all). + * + * @method expandAllContentItems + */ + NotificationPopoverController.prototype.expandAllContentItems = function() { + this.getContent() + .find(SELECTORS.CONTENT_ITEM_CONTAINER) + .addClass('expanded') + .attr('aria-expanded', 'true'); + }; + + /** + * Expand a single content item. + * + * @method expandContentItem + * @param item jQuery object the content item to be expanded + */ + NotificationPopoverController.prototype.expandContentItem = function(item) { + item.addClass('expanded'); + item.attr('aria-expanded', 'true'); + item.find(SELECTORS.SHOW_BUTTON).attr('aria-hidden', 'true'); + item.find(SELECTORS.CONTENT_BODY_SHORT).attr('aria-hidden', 'true'); + item.find(SELECTORS.CONTENT_BODY_FULL).attr('aria-hidden', 'false'); + item.find(SELECTORS.HIDE_BUTTON).attr('aria-hidden', 'false').focus(); + }; + + /** + * Collapse a single content item. + * + * @method collapseContentItem + * @param item jQuery object the content item to be collapsed. + */ + NotificationPopoverController.prototype.collapseContentItem = function(item) { + item.removeClass('expanded'); + item.attr('aria-expanded', 'false'); + item.find(SELECTORS.HIDE_BUTTON).attr('aria-hidden', 'true'); + item.find(SELECTORS.CONTENT_BODY_FULL).attr('aria-hidden', 'true'); + item.find(SELECTORS.CONTENT_BODY_SHORT).attr('aria-hidden', 'false'); + item.find(SELECTORS.SHOW_BUTTON).attr('aria-hidden', 'false').focus(); + }; + + /** + * Navigate the browser to the content URL for the content item, if it has one. + * + * @method navigateToContextURL + * @param item jQuery object representing the content item + */ + NotificationPopoverController.prototype.navigateToContextURL = function(item) { + var url = item.attr('data-context-url'); + + if (url) { + window.location.assign(url); + } + }; + + /** + * Add all of the required event listeners for this notification popover. + * + * @method registerEventListeners + */ + NotificationPopoverController.prototype.registerEventListeners = function() { + customEvents.define(this.root, [ + customEvents.events.activate, + customEvents.events.next, + customEvents.events.previous, + customEvents.events.asterix, + ]); + + // Expand the content item if the user activates (click/enter/space) the show + // button. + this.root.on(customEvents.events.activate, SELECTORS.SHOW_BUTTON, function(e, data) { + var container = $(e.target).closest(SELECTORS.CONTENT_ITEM_CONTAINER); + this.expandContentItem(container); + + e.stopPropagation(); + data.originalEvent.preventDefault(); + }.bind(this)); + + // Expand the content item if the user triggers the next event (right arrow in LTR). + this.root.on(customEvents.events.next, SELECTORS.CONTENT_ITEM_CONTAINER, function(e) { + var contentItem = $(e.target).closest(SELECTORS.CONTENT_ITEM_CONTAINER); + this.expandContentItem(contentItem); + }.bind(this)); + + // Collapse the content item if the user activates the hide button. + this.root.on(customEvents.events.activate, SELECTORS.HIDE_BUTTON, function(e, data) { + var container = $(e.target).closest(SELECTORS.CONTENT_ITEM_CONTAINER); + this.collapseContentItem(container); + + e.stopPropagation(); + data.originalEvent.preventDefault(); + }.bind(this)); + + // Collapse the content item if the user triggers the previous event (left arrow in LTR). + this.root.on(customEvents.events.previous, SELECTORS.CONTENT_ITEM_CONTAINER, function(e) { + var contentItem = $(e.target).closest(SELECTORS.CONTENT_ITEM_CONTAINER); + this.collapseContentItem(contentItem); + }.bind(this)); + + // Switch between popover states (read/unread) if the user activates the toggle. + this.root.on(customEvents.events.activate, SELECTORS.MODE_TOGGLE, function(e) { + if (this.modeToggle.hasClass('on')) { + this.clearUnreadNotifications(); + this.modeToggle.removeClass('on'); + this.modeToggle.addClass('off'); + this.root.removeClass('unread-only'); + + str.get_string('shownewnotifications', 'message').done(function(string) { + this.modeToggle.attr('aria-label', string); + }.bind(this)); + } else { + this.modeToggle.removeClass('off'); + this.modeToggle.addClass('on'); + this.root.addClass('unread-only'); + + str.get_string('showallnotifications', 'message').done(function(string) { + this.modeToggle.attr('aria-label', string); + }.bind(this)); + } + + if (!this.hasDoneInitialLoad()) { + this.loadMoreNotifications(); + } + + e.stopPropagation(); + }.bind(this)); + + // Follow the context URL if the user activates the content item. + this.root.on(customEvents.events.activate, SELECTORS.CONTENT_ITEM_CONTAINER, function(e) { + var contentItem = $(e.target).closest(SELECTORS.CONTENT_ITEM_CONTAINER); + this.navigateToContextURL(contentItem); + e.stopPropagation(); + }.bind(this)); + + // Mark all notifications read if the user activates the mark all as read button. + this.root.on(customEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e) { + this.markAllAsRead(); + e.stopPropagation(); + }.bind(this)); + + // Expand all the currently visible content items if the user hits the + // asterix key. + this.root.on(customEvents.events.asterix, function() { + this.expandAllContentItems(); + }.bind(this)); + + // Update the notification information when the menu is opened. + this.root.on(this.events().menuOpened, function() { + this.hideUnreadCount(); + this.updateButtonAriaLabel(); + + if (!this.hasDoneInitialLoad()) { + this.loadMoreNotifications(); + } + }.bind(this)); + + // Update the unread notification count when the menu is closed. + this.root.on(this.events().menuClosed, function() { + this.renderUnreadCount(); + this.clearUnreadNotifications(); + this.updateButtonAriaLabel(); + }.bind(this)); + + // Set aria attributes when popover is loading. + this.root.on(this.events().startLoading, function() { + this.getContent().attr('aria-busy', 'true'); + }.bind(this)); + + // Set aria attributes when popover is finished loading. + this.root.on(this.events().stopLoading, function() { + this.getContent().attr('aria-busy', 'false'); + }.bind(this)); + + // Load more notifications if the user has scrolled to the end of content + // item list. + this.getContentContainer().on(customEvents.events.scrollBottom, function() { + if (!this.isLoading && !this.hasLoadedAllContent()) { + this.loadMoreNotifications(); + } + }.bind(this)); + }; + + return NotificationPopoverController; +}); diff --git a/message/amd/src/notification_repository.js b/message/amd/src/notification_repository.js new file mode 100644 index 0000000000000..4e60145754644 --- /dev/null +++ b/message/amd/src/notification_repository.js @@ -0,0 +1,79 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Retrieves notifications from the server. + * + * @module message/notification_repository + * @class notification_repository + * @package message + * @copyright 2015 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define(['core/ajax', 'core/notification'], function(ajax, notification) { + var query = function(args) { + if (typeof args.limit === 'undefined') { + args.limit = 20; + } + + if (typeof args.offset === 'undefined') { + args.offset = 0; + } + + var request = { + methodname: 'core_message_get_notifications', + args: args + }; + + var promise = ajax.call([request])[0]; + + promise.fail(notification.exception); + + return promise; + }; + + var countUnread = function(args) { + var request = { + methodname: 'core_message_get_unread_notification_count', + args: args + }; + + var promise = ajax.call([request])[0]; + + promise.fail(notification.exception); + + return promise; + }; + + var markAllAsRead = function(args) { + var request = { + methodname: 'core_message_mark_all_notifications_as_read', + args: args + }; + + var promise = ajax.call([request])[0]; + + promise.fail(notification.exception); + + return promise; + }; + + return { + query: query, + countUnread: countUnread, + markAllAsRead: markAllAsRead, + }; +}); diff --git a/message/templates/notification_content_item.mustache b/message/templates/notification_content_item.mustache new file mode 100644 index 0000000000000..dd8e55cf3598d --- /dev/null +++ b/message/templates/notification_content_item.mustache @@ -0,0 +1,85 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template message/notification_content_item + + This template will render the notification content item for the + navigation bar notification menu. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * userid the logged in user id + + Example context (json): + { + } + +}} + diff --git a/message/templates/notification_popover.mustache b/message/templates/notification_popover.mustache new file mode 100644 index 0000000000000..42f8139c58382 --- /dev/null +++ b/message/templates/notification_popover.mustache @@ -0,0 +1,86 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template message/notification_popover + + This template will render the notification popover for the navigation bar. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * userid the logged in user id + + Example context (json): + { + } + +}} +{{< core/mdl_popover }} + {{$classes}}mdl-popover-notifications unread-only{{/classes}} + {{$attributes}}id="nav-notification-popover-container" data-userid="{{userid}}"{{/attributes}} + + {{$togglelabel}}{{#str}} shownotificationwindownonew, message {{/str}}{{/togglelabel}} + {{$togglecontent}} + {{#pix}} i/notifications, core, {{#str}} togglenotificationmenu, message {{/str}} {{/pix}} + + {{/togglecontent}} + + {{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}} + + {{$headertext}}{{#str}} notifications, message {{/str}}{{/headertext}} + {{$headeractions}} +
+
+
{{#str}} all {{/str}}
+
{{#str}} new {{/str}}
+
+ + {{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}} + {{#pix}} y/loading, core, {{#str}} loading, mod_assign {{/str}} {{/pix}} + + + {{#pix}} i/settings, core, {{#str}} notificationpreferences, message {{/str}} {{/pix}} + + {{/headeractions}} + + {{$content}} +
+
{{#str}} nonotifications, message {{/str}}
+
+
{{#str}} nonewnotifications, message {{/str}}
+ {{/content}} +{{/ core/mdl_popover }} +{{#js}} +require(['jquery', 'message/notification_popover_controller'], function($, controller) { + var container = $('#nav-notification-popover-container'); + var controller = new controller(container); + controller.registerEventListeners(); + controller.registerListNavigationEventListeners(); +}); +{{/js}} diff --git a/pix/i/notifications.png b/pix/i/notifications.png new file mode 100644 index 0000000000000..cd0f44a04e563 Binary files /dev/null and b/pix/i/notifications.png differ diff --git a/pix/i/notifications.svg b/pix/i/notifications.svg new file mode 100644 index 0000000000000..239d5965cd37c --- /dev/null +++ b/pix/i/notifications.svg @@ -0,0 +1,17 @@ + + + +]> + + + + + diff --git a/theme/bootstrapbase/less/moodle.less b/theme/bootstrapbase/less/moodle.less index d6d076fc909a5..a2cbf02b7de13 100644 --- a/theme/bootstrapbase/less/moodle.less +++ b/theme/bootstrapbase/less/moodle.less @@ -12,6 +12,7 @@ @import "moodle/question"; @import "moodle/user"; @import "moodle/search"; +@import "moodle/popover"; // Upstream Bootstrap. diff --git a/theme/bootstrapbase/less/moodle/popover.less b/theme/bootstrapbase/less/moodle/popover.less new file mode 100644 index 0000000000000..5a298136ff03e --- /dev/null +++ b/theme/bootstrapbase/less/moodle/popover.less @@ -0,0 +1,620 @@ +.mdl-popover { + float: right; + position: relative; + + .mdl-popover-toggle { + cursor: pointer; + + .count-container { + padding: 2px; + border-radius: 2px; + background-color: red; + color: white; + font-size: 10px; + line-height: 10px; + position: absolute; + top: 5px; + right: 0; + } + } + + .mdl-popover-container { + position: absolute; + right: 0; + top: 0; + height: 500px; + width: 380px; + border: 1px solid #ddd; + transition: height 0.25s; + opacity: 1; + visibility: visible; + background-color: #fff; + z-index: 10000; + + .mdl-popover-header-container { + height: 25px; + line-height: 24px; + padding-left: 5px; + padding-right: 5px; + border-bottom: 1px solid #ddd; + box-sizing: border-box; + + .mdl-popover-header-text { + float: left; + margin: 0; + font-size: 14px; + line-height: 25px; + } + + .mdl-popover-header-actions { + float: right; + + > * { + margin-left: 5px; + } + } + } + + .mdl-popover-content-container { + height: ~"calc(100% - 25px)"; + width: 100%; + overflow-y: auto; + + .loading-icon { + display: none; + text-align: center; + padding: 5px; + box-sizing: border-box; + } + .empty-message { + display: none; + text-align: center; + padding: 10px; + } + + &.loading { + .loading-icon { + display: block; + } + } + } + } + + &.collapsed { + .mdl-popover-container { + height: 0; + overflow: hidden; + opacity: 0; + visibility: hidden; + transition: height 0.25s, opacity 101ms 0.25s, visibility 101ms 0.25s; + } + } +} + +.navbar { + .mdl-popover { + float: right; + + .mdl-popover-toggle { + height: 42px; + line-height: 42px; + padding-left: 10px; + padding-right: 10px; + + .count-container { + padding: 2px; + border-radius: 2px; + background-color: red; + color: white; + font-size: 10px; + line-height: 10px; + position: absolute; + top: 5px; + right: 0; + } + } + .mdl-popover-container { + top: 42px; + + &::before { + content: ''; + display: inline-block; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-bottom: 10px solid #ddd; + position: absolute; + top: -10px; + right: 7px; + } + &::after { + content: ''; + display: inline-block; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + border-bottom: 9px solid #fff; + position: absolute; + top: -9px; + right: 8px; + } + } + &.collapsed { + .mdl-popover-container { + height: 0; + overflow: hidden; + opacity: 0; + visibility: hidden; + transition: height 0.25s, opacity 101ms 0.25s, visibility 101ms 0.25s; + } + } + } +} + +.mdl-popover-notifications { + &.mdl-popover { + .mdl-popover-container { + .mdl-popover-header-container { + .mdl-popover-header-actions { + .fancy-toggle { + position: relative; + display: inline-block; + border-radius: 4px; + box-sizing: border-box; + line-height: 25px; + height: 20px; + vertical-align: middle; + cursor: pointer; + overflow: hidden; + + .slider-container { + width: 100%; + height: 100%; + position: absolute; + + .slider { + width: 10px; + height: ~"calc(100% - 2px)"; + background-color: #fff; + border-radius: 4px; + margin: 1px; + display: inline-block; + } + } + .on-text { + padding: 2px 5px 2px 14px; + box-sizing: border-box; + line-height: 16px; + vertical-align: top; + display: inline-block; + } + .off-text { + padding: 2px 14px 2px 5px; + box-sizing: border-box; + line-height: 16px; + vertical-align: top; + display: inline-block; + } + + &.on { + background-color: green; + color: #fff; + + .slider-container { + transform: translateX(0); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } + .on-text { + display: block; + } + .off-text { + display: none; + } + } + &.off { + background-color: red; + color: #fff; + + .slider-container { + transform: translateX(100%); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + + .slider { + transform: translateX(-110%); + } + } + .on-text { + display: none; + } + .off-text { + display: block; + } + } + } + } + + #mark-all-read-button { + .normal-icon { + display: inline-block; + } + .loading-icon { + display: none; + height: 12px; + width: 12px; + } + + &.loading { + .normal-icon { + display: none; + } + .loading-icon { + display: inline-block; + } + } + } + } + } + .mdl-popover-container { + .mdl-popover-content-container { + &.loading { + .mdl-popover-content { + .unread-notifications, + .all-notifications { + &:empty + .empty-message, + & + .empty-message { + display: none; + } + } + } + } + + .mdl-popover-content { + .unread-notifications { + opacity: 0; + visibility: hidden; + height: 0; + overflow: hidden; + transition: opacity 0.25s, visibility 101ms 0.25s, height 101ms 0.25s; + + & + .empty-message { + display: none; + } + } + .all-notifications { + opacity: 1; + visibility: visible; + height: auto; + overflow: hidden; + transition: opacity 0.25s 0.25s, visibility 101ms 0.5s, height 101ms 0.5s; + + &:empty + .empty-message { + display: block; + } + } + .content-item-container { + width: 100%; + border-bottom: 1px solid #ddd; + box-sizing: border-box; + padding: 5px; + + &:hover { + opacity: 0.8; + } + + &[data-context-url] { + cursor: pointer; + } + + .content-item { + width: ~"calc(100% - 45px)"; + display: inline-block; + box-sizing: border-box; + + .content-item-header { + height: 20px; + margin-bottom: 5px; + box-sizing: border-box; + + h3 { + font-size: 14px; + line-height: 14px; + float: left; + margin: 0; + max-width: 80%; + } + } + + .content-item-body { + box-sizing: border-box; + + .content-body-short { + max-height: 1000px; + width: 100%; + height: 30px; + opacity: 1; + visibility: visible; + transition: opacity 101ms 0.5s, visibility 101ms 0.5s, max-height 1s 0.5s; + + .notification-image { + display: inline-block; + width: 10%; + + img { + height: 30px; + } + } + .notification-message { + display: inline-block; + font-size: 12px; + line-height: 15px; + margin: 0; + vertical-align: top; + max-width: 80%; + } + } + .content-body-full { + overflow: hidden; + width: 100%; + max-height: 0; + opacity: 0; + visibility: hidden; + transition: max-height 0.5s, opacity 101ms 0.5s, visibility 101ms 0.5s; + + pre { + word-break: normal; + word-wrap: normal; + font-size: 12px; + margin: 0; + } + } + } + + .content-item-footer { + text-align: left; + margin: 5px 0 0; + box-sizing: border-box; + + p { + font-size: 10px; + line-height: 10px; + margin: 0; + } + } + } + + .content-item-controls { + width: 40px; + height: 100%; + display: inline-block; + vertical-align: top; + box-sizing: border-box; + text-align: right; + + .block-button { + margin-left: 5px; + + img { + height: 15px; + } + } + .mark-read-button { + img { + height: 15px; + } + } + .expand-buttons { + display: inline-block; + height: 30px; + width: 100%; + vertical-align: top; + float: right; + text-align: center; + + img { + vertical-align: middle; + height: 100%; + } + .show-button { + width: 100%; + height: 100%; + display: block; + } + .hide-button { + width: 100%; + height: 100%; + display: none; + } + } + } + + &.expanded { + .content-item { + .content-item-body { + .content-body-short { + opacity: 0; + visibility: hidden; + max-height: 0; + overflow: hidden; + transition: opacity 101ms 0.25s, visibility 101ms 0.25s, max-height 0.25s; + } + .content-body-full { + max-height: 1000px; + opacity: 1; + visibility: visible; + transition: max-height 2s 0.25s, opacity 101ms 0.25s, visibility 101ms 0.25s; + } + } + } + + .content-item-controls { + .expand-buttons { + .show-button { + display: none; + } + .hide-button { + display: block; + } + } + } + } + + &:last-child { + border-bottom: none; + } + } + } + } + } + + &.unread-only { + .mdl-popover-container { + .mdl-popover-content-container { + .mdl-popover-content { + .all-notifications { + opacity: 0; + visibility: hidden; + height: 0; + transition: opacity 0.25s, visibility 101ms 0.25s, height 101ms 0.25s; + + & + .empty-message { + display: none; + } + } + .unread-notifications { + opacity: 1; + visibility: visible; + height: auto; + transition: opacity 0.25s 0.25s, visibility 101ms 0.5s, height 101ms 0.5s; + + &:empty + .empty-message { + display: block; + } + } + } + + &.loading { + .mdl-popover-content { + .unread-notifications, + .all-notifications { + &:empty + .empty-message, + & + .empty-message { + display: none; + } + } + } + } + } + } + } + } +} + +.dir-rtl { + .mdl-popover { + .mdl-popover-container { + left: 0; + right: auto; + + .mdl-popover-header-container { + .mdl-popover-header-text { + float: right; + } + .mdl-popover-header-actions { + float: left; + } + } + + .mdl-popover-content-container { + .mdl-popover-content { + .content-item-container { + .content-item { + .content-item-header { + h3 { + float: right; + } + } + .content-item-footer { + text-align: right; + } + } + .content-item-controls { + text-align: left; + } + } + } + } + } + } + .navbar { + .mdl-popover { + float: left; + + .mdl-popover-container { + &::before { + right: auto; + left: 7px; + } + &::after { + right: auto; + left: 8px; + } + + .mdl-popover-header-container { + .mdl-popover-header-text { + float: right; + } + .mdl-popover-header-actions { + float: left; + } + } + } + } + } + .mdl-popover-notifications { + .mdl-popover { + .mdl-popover-container { + .mdl-popover-header-container { + .mdl-popover-header-actions { + .fancy-toggle { + &.off { + .slider-container { + transform: translateX(0); + + .slider { + transform: translateX(0); + } + } + } + &.on { + .slider-container { + transform: translateX(-100%); + + .slider { + transform: translateX(110%); + } + } + } + } + } + } + .mdl-popover-content-container { + .mdl-popover-content { + .content-item-container { + .content-item { + .content-item-header { + h3 { + float: right; + } + } + .content-item-footer { + text-align: right; + } + } + .content-item-controls { + text-align: left; + } + } + } + } + } + } + } +} diff --git a/theme/bootstrapbase/less/moodle/responsive.less b/theme/bootstrapbase/less/moodle/responsive.less index 0525f9d15da45..354f0e1cd4611 100644 --- a/theme/bootstrapbase/less/moodle/responsive.less +++ b/theme/bootstrapbase/less/moodle/responsive.less @@ -194,6 +194,22 @@ } @media (max-width: 480px) { + .navbar { + .mdl-popover { + .mdl-popover-container { + position: fixed; + width: 100%; + height: ~"calc(100% - 52px)"; + top: 52px; + + &::before, + &::after { + display: none; + } + } + } + } + // make tabs act like nav-stacked // (mostly) copied from bootstrap/navs.less .nav-tabs > li { @@ -537,6 +553,44 @@ } @media (max-width: 767px) { + .usermenu { + .moodle-actionmenu { + .toggle-display { + .userbutton { + .usertext { + display: none; + } + } + } + } + } + .jsenabled { + &:not(.dir-rtl) { + .usermenu { + .moodle-actionmenu { + .toggle-display { + &.textmenu { + margin-left: 0; + padding-left: 0; + } + } + } + } + } + &.dir-rtl { + .usermenu { + .moodle-actionmenu { + .toggle-display { + &.textmenu { + margin-right: 0; + padding-right: 0; + } + } + } + } + } + } + // Resize, reflow file-picker on small devices #filesskin .yui3-panel, #filesskin .file-picker.fp-generallayout { diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index f322a7eeb8c34..b77cf38c25d40 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -6938,6 +6938,463 @@ body.path-question-type .mform fieldset.hidden { .search-areas-actions > div { display: inline-block; } +.mdl-popover { + float: right; + position: relative; +} +.mdl-popover .mdl-popover-toggle { + cursor: pointer; +} +.mdl-popover .mdl-popover-toggle .count-container { + padding: 2px; + border-radius: 2px; + background-color: red; + color: white; + font-size: 10px; + line-height: 10px; + position: absolute; + top: 5px; + right: 0; +} +.mdl-popover .mdl-popover-container { + position: absolute; + right: 0; + top: 0; + height: 500px; + width: 380px; + border: 1px solid #ddd; + transition: height 0.25s; + opacity: 1; + visibility: visible; + background-color: #fff; + z-index: 10000; +} +.mdl-popover .mdl-popover-container .mdl-popover-header-container { + height: 25px; + line-height: 24px; + padding-left: 5px; + padding-right: 5px; + border-bottom: 1px solid #ddd; + box-sizing: border-box; +} +.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-text { + float: left; + margin: 0; + font-size: 14px; + line-height: 25px; +} +.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions { + float: right; +} +.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions > * { + margin-left: 5px; +} +.mdl-popover .mdl-popover-container .mdl-popover-content-container { + height: calc(100% - 25px); + width: 100%; + overflow-y: auto; +} +.mdl-popover .mdl-popover-container .mdl-popover-content-container .loading-icon { + display: none; + text-align: center; + padding: 5px; + box-sizing: border-box; +} +.mdl-popover .mdl-popover-container .mdl-popover-content-container .empty-message { + display: none; + text-align: center; + padding: 10px; +} +.mdl-popover .mdl-popover-container .mdl-popover-content-container.loading .loading-icon { + display: block; +} +.mdl-popover.collapsed .mdl-popover-container { + height: 0; + overflow: hidden; + opacity: 0; + visibility: hidden; + transition: height 0.25s, opacity 101ms 0.25s, visibility 101ms 0.25s; +} +.navbar .mdl-popover { + float: right; +} +.navbar .mdl-popover .mdl-popover-toggle { + height: 42px; + line-height: 42px; + padding-left: 10px; + padding-right: 10px; +} +.navbar .mdl-popover .mdl-popover-toggle .count-container { + padding: 2px; + border-radius: 2px; + background-color: red; + color: white; + font-size: 10px; + line-height: 10px; + position: absolute; + top: 5px; + right: 0; +} +.navbar .mdl-popover .mdl-popover-container { + top: 42px; +} +.navbar .mdl-popover .mdl-popover-container::before { + content: ''; + display: inline-block; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-bottom: 10px solid #ddd; + position: absolute; + top: -10px; + right: 7px; +} +.navbar .mdl-popover .mdl-popover-container::after { + content: ''; + display: inline-block; + border-left: 9px solid transparent; + border-right: 9px solid transparent; + border-bottom: 9px solid #fff; + position: absolute; + top: -9px; + right: 8px; +} +.navbar .mdl-popover.collapsed .mdl-popover-container { + height: 0; + overflow: hidden; + opacity: 0; + visibility: hidden; + transition: height 0.25s, opacity 101ms 0.25s, visibility 101ms 0.25s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle { + position: relative; + display: inline-block; + border-radius: 4px; + box-sizing: border-box; + line-height: 25px; + height: 20px; + vertical-align: middle; + cursor: pointer; + overflow: hidden; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle .slider-container { + width: 100%; + height: 100%; + position: absolute; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle .slider-container .slider { + width: 10px; + height: calc(100% - 2px); + background-color: #fff; + border-radius: 4px; + margin: 1px; + display: inline-block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle .on-text { + padding: 2px 5px 2px 14px; + box-sizing: border-box; + line-height: 16px; + vertical-align: top; + display: inline-block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle .off-text { + padding: 2px 14px 2px 5px; + box-sizing: border-box; + line-height: 16px; + vertical-align: top; + display: inline-block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.on { + background-color: green; + color: #fff; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.on .slider-container { + transform: translateX(0); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.on .on-text { + display: block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.on .off-text { + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off { + background-color: red; + color: #fff; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off .slider-container { + transform: translateX(100%); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off .slider-container .slider { + transform: translateX(-110%); +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off .on-text { + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off .off-text { + display: block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container #mark-all-read-button .normal-icon { + display: inline-block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container #mark-all-read-button .loading-icon { + display: none; + height: 12px; + width: 12px; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container #mark-all-read-button.loading .normal-icon { + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-header-container #mark-all-read-button.loading .loading-icon { + display: inline-block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .unread-notifications:empty + .empty-message, +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .all-notifications:empty + .empty-message, +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .unread-notifications + .empty-message, +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .all-notifications + .empty-message { + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .unread-notifications { + opacity: 0; + visibility: hidden; + height: 0; + overflow: hidden; + transition: opacity 0.25s, visibility 101ms 0.25s, height 101ms 0.25s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .unread-notifications + .empty-message { + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .all-notifications { + opacity: 1; + visibility: visible; + height: auto; + overflow: hidden; + transition: opacity 0.25s 0.25s, visibility 101ms 0.5s, height 101ms 0.5s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .all-notifications:empty + .empty-message { + display: block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container { + width: 100%; + border-bottom: 1px solid #ddd; + box-sizing: border-box; + padding: 5px; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container:hover { + opacity: 0.8; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container[data-context-url] { + cursor: pointer; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item { + width: calc(100% - 45px); + display: inline-block; + box-sizing: border-box; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-header { + height: 20px; + margin-bottom: 5px; + box-sizing: border-box; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-header h3 { + font-size: 14px; + line-height: 14px; + float: left; + margin: 0; + max-width: 80%; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body { + box-sizing: border-box; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body .content-body-short { + max-height: 1000px; + width: 100%; + height: 30px; + opacity: 1; + visibility: visible; + transition: opacity 101ms 0.5s, visibility 101ms 0.5s, max-height 1s 0.5s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body .content-body-short .notification-image { + display: inline-block; + width: 10%; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body .content-body-short .notification-image img { + height: 30px; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body .content-body-short .notification-message { + display: inline-block; + font-size: 12px; + line-height: 15px; + margin: 0; + vertical-align: top; + max-width: 80%; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body .content-body-full { + overflow: hidden; + width: 100%; + max-height: 0; + opacity: 0; + visibility: hidden; + transition: max-height 0.5s, opacity 101ms 0.5s, visibility 101ms 0.5s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-body .content-body-full pre { + word-break: normal; + word-wrap: normal; + font-size: 12px; + margin: 0; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-footer { + text-align: left; + margin: 5px 0 0; + box-sizing: border-box; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-footer p { + font-size: 10px; + line-height: 10px; + margin: 0; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls { + width: 40px; + height: 100%; + display: inline-block; + vertical-align: top; + box-sizing: border-box; + text-align: right; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .block-button { + margin-left: 5px; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .block-button img { + height: 15px; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .mark-read-button img { + height: 15px; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .expand-buttons { + display: inline-block; + height: 30px; + width: 100%; + vertical-align: top; + float: right; + text-align: center; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .expand-buttons img { + vertical-align: middle; + height: 100%; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .expand-buttons .show-button { + width: 100%; + height: 100%; + display: block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls .expand-buttons .hide-button { + width: 100%; + height: 100%; + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container.expanded .content-item .content-item-body .content-body-short { + opacity: 0; + visibility: hidden; + max-height: 0; + overflow: hidden; + transition: opacity 101ms 0.25s, visibility 101ms 0.25s, max-height 0.25s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container.expanded .content-item .content-item-body .content-body-full { + max-height: 1000px; + opacity: 1; + visibility: visible; + transition: max-height 2s 0.25s, opacity 101ms 0.25s, visibility 101ms 0.25s; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container.expanded .content-item-controls .expand-buttons .show-button { + display: none; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container.expanded .content-item-controls .expand-buttons .hide-button { + display: block; +} +.mdl-popover-notifications.mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container:last-child { + border-bottom: none; +} +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .all-notifications { + opacity: 0; + visibility: hidden; + height: 0; + transition: opacity 0.25s, visibility 101ms 0.25s, height 101ms 0.25s; +} +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .all-notifications + .empty-message { + display: none; +} +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .unread-notifications { + opacity: 1; + visibility: visible; + height: auto; + transition: opacity 0.25s 0.25s, visibility 101ms 0.5s, height 101ms 0.5s; +} +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .unread-notifications:empty + .empty-message { + display: block; +} +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .unread-notifications:empty + .empty-message, +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .all-notifications:empty + .empty-message, +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .unread-notifications + .empty-message, +.mdl-popover-notifications.mdl-popover.unread-only .mdl-popover-container .mdl-popover-content-container.loading .mdl-popover-content .all-notifications + .empty-message { + display: none; +} +.dir-rtl .mdl-popover .mdl-popover-container { + left: 0; + right: auto; +} +.dir-rtl .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-text { + float: right; +} +.dir-rtl .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions { + float: left; +} +.dir-rtl .mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-header h3 { + float: right; +} +.dir-rtl .mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-footer { + text-align: right; +} +.dir-rtl .mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls { + text-align: left; +} +.dir-rtl .navbar .mdl-popover { + float: left; +} +.dir-rtl .navbar .mdl-popover .mdl-popover-container::before { + right: auto; + left: 7px; +} +.dir-rtl .navbar .mdl-popover .mdl-popover-container::after { + right: auto; + left: 8px; +} +.dir-rtl .navbar .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-text { + float: right; +} +.dir-rtl .navbar .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions { + float: left; +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off .slider-container { + transform: translateX(0); +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.off .slider-container .slider { + transform: translateX(0); +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.on .slider-container { + transform: translateX(-100%); +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-header-container .mdl-popover-header-actions .fancy-toggle.on .slider-container .slider { + transform: translateX(110%); +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-header h3 { + float: right; +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item .content-item-footer { + text-align: right; +} +.dir-rtl .mdl-popover-notifications .mdl-popover .mdl-popover-container .mdl-popover-content-container .mdl-popover-content .content-item-container .content-item-controls { + text-align: left; +} /*! * Bootstrap v2.3.2 * @@ -16772,6 +17229,16 @@ body.has_dock div#dock { } } @media (max-width: 480px) { + .navbar .mdl-popover .mdl-popover-container { + position: fixed; + width: 100%; + height: calc(100% - 52px); + top: 52px; + } + .navbar .mdl-popover .mdl-popover-container::before, + .navbar .mdl-popover .mdl-popover-container::after { + display: none; + } .nav-tabs > li { float: none; } @@ -17077,6 +17544,17 @@ body.has_dock div#dock { } } @media (max-width: 767px) { + .usermenu .moodle-actionmenu .toggle-display .userbutton .usertext { + display: none; + } + .jsenabled:not(.dir-rtl) .usermenu .moodle-actionmenu .toggle-display.textmenu { + margin-left: 0; + padding-left: 0; + } + .jsenabled.dir-rtl .usermenu .moodle-actionmenu .toggle-display.textmenu { + margin-right: 0; + padding-right: 0; + } #filesskin .yui3-panel, #filesskin .file-picker.fp-generallayout { width: 100%; diff --git a/theme/clean/layout/columns1.php b/theme/clean/layout/columns1.php index fb56007e62359..520b42505b7ff 100644 --- a/theme/clean/layout/columns1.php +++ b/theme/clean/layout/columns1.php @@ -44,6 +44,7 @@ navbar_home(); ?> navbar_button(); ?> user_menu(); ?> + notification_menu(); ?> search_box(); ?>