Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Controls the message drawer.** @module core_message/message_drawer* @copyright 2018 Ryan Wyllie <ryan@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/define(['jquery','core/custom_interaction_events','core/pubsub','core_message/message_drawer_view_contact','core_message/message_drawer_view_contacts','core_message/message_drawer_view_conversation','core_message/message_drawer_view_group_info','core_message/message_drawer_view_overview','core_message/message_drawer_view_search','core_message/message_drawer_view_settings','core_message/message_drawer_router','core_message/message_drawer_routes','core_message/message_drawer_events','core_message/message_drawer_helper','core/pending','core/drawer','core/toast','core/str','core/config','core/ajax','core/local/aria/focuslock','core/modal_backdrop','core/templates','core/local/aria/selectors',],function($,CustomEvents,PubSub,ViewContact,ViewContacts,ViewConversation,ViewGroupInfo,ViewOverview,ViewSearch,ViewSettings,Router,Routes,Events,Helper,Pending,Drawer,Toast,Str,Config,Ajax,FocusLock,ModalBackdrop,Templates,AriaSelectors,) {var SELECTORS = {DRAWER: '[data-region="right-hand-drawer"]',PANEL_BODY_CONTAINER: '[data-region="panel-body-container"]',PANEL_HEADER_CONTAINER: '[data-region="panel-header-container"]',VIEW_CONTACT: '[data-region="view-contact"]',VIEW_CONTACTS: '[data-region="view-contacts"]',VIEW_CONVERSATION: '[data-region="view-conversation"]',VIEW_CONVERSATION_WITH_ID: '[data-region="view-conversation"][data-conversation-id]',VIEW_CONVERSATION_WITH_USER: '[data-region="view-conversation"][data-other-user-id]',VIEW_GROUP_INFO: '[data-region="view-group-info"]',VIEW_OVERVIEW: '[data-region="view-overview"]',VIEW_SEARCH: '[data-region="view-search"]',VIEW_SETTINGS: '[data-region="view-settings"]',ROUTES: '[data-route]',ROUTES_BACK: '[data-route-back]',HEADER_CONTAINER: '[data-region="header-container"]',BODY_CONTAINER: '[data-region="body-container"]',FOOTER_CONTAINER: '[data-region="footer-container"]',CLOSE_BUTTON: '[data-action="closedrawer"]',MESSAGE_INDEX: '[data-region="message-index"]',MESSAGE_TEXT_AREA: '[data-region="send-message-txt"]',};/*** Get elements for route.** @param {String} namespace Unique identifier for the Routes* @param {Object} root The message drawer container.* @param {string} selector The route container.** @return {array} elements Found route container objects.*/var getParametersForRoute = function(namespace, root, selector) {var header = root.find(SELECTORS.HEADER_CONTAINER).find(selector);if (!header.length) {header = root.find(SELECTORS.PANEL_HEADER_CONTAINER).find(selector);}var body = root.find(SELECTORS.BODY_CONTAINER).find(selector);if (!body.length) {body = root.find(SELECTORS.PANEL_BODY_CONTAINER).find(selector);}var footer = root.find(SELECTORS.FOOTER_CONTAINER).find(selector);return [namespace,header.length ? header : null,body.length ? body : null,footer.length ? footer : null];};var routes = [[Routes.VIEW_CONTACT, SELECTORS.VIEW_CONTACT, ViewContact.show, ViewContact.description],[Routes.VIEW_CONTACTS, SELECTORS.VIEW_CONTACTS, ViewContacts.show, ViewContacts.description],[Routes.VIEW_CONVERSATION, SELECTORS.VIEW_CONVERSATION, ViewConversation.show, ViewConversation.description],[Routes.VIEW_GROUP_INFO, SELECTORS.VIEW_GROUP_INFO, ViewGroupInfo.show, ViewGroupInfo.description],[Routes.VIEW_OVERVIEW, SELECTORS.VIEW_OVERVIEW, ViewOverview.show, ViewOverview.description],[Routes.VIEW_SEARCH, SELECTORS.VIEW_SEARCH, ViewSearch.show, ViewSearch.description],[Routes.VIEW_SETTINGS, SELECTORS.VIEW_SETTINGS, ViewSettings.show, ViewSettings.description]];/*** Create routes.** @param {String} namespace Unique identifier for the Routes* @param {Object} root The message drawer container.*/var createRoutes = function(namespace, root) {routes.forEach(function(route) {Router.add(namespace, route[0], getParametersForRoute(namespace, root, route[1]), route[2], route[3]);});};let backdropPromise = null;/*** Set the focus on the drawer.** This method also creates or destroy any necessary backdrop zone and focus trap.** @param {Object} root The message drawer container.* @param {Boolean} hasFocus Whether the drawer has focus or not.*/var setFocus = function(root, hasFocus) {var drawerRoot = Drawer.getDrawerRoot(root);if (!drawerRoot.length) {return;}if (!backdropPromise) {backdropPromise = Templates.render('core/modal_backdrop', {}).then(html => new ModalBackdrop(html));}const backdropWithAdjustments = backdropPromise.then(modalBackdrop => {const messageDrawerZIndex = window.getComputedStyle(drawerRoot[0]).zIndex;if (messageDrawerZIndex) {modalBackdrop.setZIndex(messageDrawerZIndex - 1);}modalBackdrop.getAttachmentPoint().get(0).addEventListener('click', e => {PubSub.publish(Events.HIDE, {});e.preventDefault();});return modalBackdrop;});if (hasFocus) {FocusLock.trapFocus(root[0]);// eslint-disable-next-line promise/catch-or-returnbackdropWithAdjustments.then(modalBackdrop => {if (modalBackdrop) {modalBackdrop.show();const pageWrapper = document.getElementById('page');pageWrapper.style.overflow = 'hidden';// Set the focus on the close button so when we press enter, it closes the drawer as it did beforevar closeButton = root.find(SELECTORS.CLOSE_BUTTON);if (closeButton.length) {closeButton.focus();}}return modalBackdrop;});} else {// eslint-disable-next-line promise/catch-or-returnbackdropWithAdjustments.then(modalBackdrop => {if (modalBackdrop) {FocusLock.untrapFocus();var button = $(SELECTORS.DRAWER).attr('data-origin');if (button) {$('#' + button).focus();}modalBackdrop.hide();const pageWrapper = document.getElementById('page');pageWrapper.style.overflow = 'visible';}return modalBackdrop;});}};/*** Show the message drawer.** @param {string} namespace The route namespace.* @param {Object} root The message drawer container.*/var show = function(namespace, root) {if (!root.attr('data-shown')) {Router.go(namespace, Routes.VIEW_OVERVIEW);root.attr('data-shown', true);}var drawerRoot = Drawer.getDrawerRoot(root);if (drawerRoot.length) {setFocus(root, true);Drawer.show(drawerRoot);}};/*** Hide the message drawer.** @param {Object} root The message drawer container.*/var hide = function(root) {var drawerRoot = Drawer.getDrawerRoot(root);if (drawerRoot.length) {setFocus(root, false);Drawer.hide(drawerRoot);}};/*** Check if the drawer is visible.** @param {Object} root The message drawer container.* @return {boolean}*/var isVisible = function(root) {var drawerRoot = Drawer.getDrawerRoot(root);if (drawerRoot.length) {return Drawer.isVisible(drawerRoot);}return true;};/*** Set Jump from button** @param {String} buttonid The originating button id*/var setJumpFrom = function(buttonid) {$(SELECTORS.DRAWER).attr('data-origin', buttonid);};/*** Store an unsent message.** Don't store this if the user has already seen the unsent message.* This avoids spamming and ensures the user is only reminded once per unsent message.* If the unsent message is sent, this attribute is removed and notification is possible again (see sendMessage).*/const storeUnsentMessage = async() => {const messageTextArea = document.querySelector(SELECTORS.MESSAGE_TEXT_AREA);if (messageTextArea.value.trim().length > 0 && !messageTextArea.hasAttribute('data-unsent-message-viewed')) {let message = messageTextArea.value;let conversationid = 0;let otheruserid = 0;// We don't always have a conversation to link the unsent message to, so let's check for that.const conversationId = document.querySelector(SELECTORS.VIEW_CONVERSATION_WITH_ID);if (conversationId) {const conversationWithId = messageTextArea.closest(SELECTORS.VIEW_CONVERSATION_WITH_ID);conversationid = conversationWithId.getAttribute('data-conversation-id');}// Store the 'other' user id if it is there. This can be used to create conversations.const conversationUser = document.querySelector(SELECTORS.VIEW_CONVERSATION_WITH_USER);if (conversationUser) {const conversationWithUser = messageTextArea.closest(SELECTORS.VIEW_CONVERSATION_WITH_USER);otheruserid = conversationWithUser.getAttribute('data-other-user-id');}setStoredUnsentMessage(message, conversationid, otheruserid);}};/*** Get the stored unsent message from the session via web service.** @returns {Promise}*/const getStoredUnsentMessage = () => Ajax.call([{methodname: 'core_message_get_unsent_message',args: {}}])[0];/*** Set the unsent message value in the session via web service.** SendBeacon is used here because this is called on 'beforeunload'.** @param {string} message The message string.* @param {number} conversationid The conversation id.* @param {number} otheruserid The other user id.* @returns {Promise}*/const setStoredUnsentMessage = (message, conversationid, otheruserid) => {const method = 'core_message_set_unsent_message';const requestUrl = new URL(`${Config.wwwroot}/lib/ajax/service.php`);requestUrl.searchParams.set('sesskey', Config.sesskey);requestUrl.searchParams.set('info', method);navigator.sendBeacon(requestUrl, JSON.stringify([{index: 0,methodname: method,args: {message: message,conversationid: conversationid,otheruserid: otheruserid,}}]));};/*** Check for an unsent message.** @param {String} uniqueId Unique identifier for the Routes.* @param {Object} root The message drawer container.*/const getUnsentMessage = async(uniqueId, root) => {let type;let messageRoot;// We need to check if we are on the message/index page.// This logic is needed to handle the two message widgets here and ensure we are targetting the right one.const messageIndex = document.querySelector(SELECTORS.MESSAGE_INDEX);if (messageIndex !== null) {type = 'index';messageRoot = document.getElementById(`message-index-${uniqueId}`);if (!messageRoot) {// This is not the correct widget.return;}} else {type = 'drawer';messageRoot = document.getElementById(`message-drawer-${uniqueId}`);}const storedMessage = await getStoredUnsentMessage();const messageTextArea = messageRoot.querySelector(SELECTORS.MESSAGE_TEXT_AREA);if (storedMessage.message && messageTextArea !== null) {showUnsentMessage(messageTextArea, storedMessage, type, uniqueId, root);}};/*** Show an unsent message.** There are two message widgets on the message/index page.* Because of that, we need to try and target the correct widget.** @param {String} textArea The textarea element.* @param {Object} stored The stored message content.* @param {String} type Is this from the drawer or index page?* @param {String} uniqueId Unique identifier for the Routes.* @param {Object} root The message drawer container.*/const showUnsentMessage = (textArea, stored, type, uniqueId, root) => {// The user has already been notified.if (textArea.hasAttribute('data-unsent-message-viewed')) {return;}// Depending on the type, show the conversation with the data we have available.// A conversation can be continued if there is a conversationid.// If the user was messaging a new non-contact, we won't have a conversationid yet.// In that case, we use the otheruserid value to start a conversation with them.switch (type) {case 'index':// Show the conversation in the main panel on the message/index page.if (stored.conversationid) {Router.go(uniqueId, Routes.VIEW_CONVERSATION, stored.conversationid, 'frompanel');// There was no conversation id, let's get a conversation going using the user id.} else if (stored.otheruserid) {Router.go(uniqueId, Routes.VIEW_CONVERSATION, null, 'create', stored.otheruserid);}break;case 'drawer':// Open the drawer and show the conversation.if (stored.conversationid) {let args = {conversationid: stored.conversationid};Helper.showConversation(args);// There was no conversation id, let's get a conversation going using the user id.} else if (stored.otheruserid) {show(uniqueId, root);Router.go(uniqueId, Routes.VIEW_CONVERSATION, null, 'create', stored.otheruserid);}break;}// Populate the text area.textArea.value = stored.message;textArea.setAttribute('data-unsent-message-viewed', 1);// Notify the user.Toast.add(Str.get_string('unsentmessagenotification', 'core_message'));};/*** Listen to and handle events for routing, showing and hiding the message drawer.** @param {string} namespace The route namespace.* @param {Object} root The message drawer container.* @param {bool} alwaysVisible Is this messaging app always shown?*/var registerEventListeners = function(namespace, root, alwaysVisible) {CustomEvents.define(root, [CustomEvents.events.activate, CustomEvents.events.escape]);var paramRegex = /^data-route-param-?(\d*)$/;root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) {var element = $(e.target).closest(SELECTORS.ROUTES);var route = element.attr('data-route');var attributes = [];for (var i = 0; i < element[0].attributes.length; i++) {attributes.push(element[0].attributes[i]);}var paramAttributes = attributes.filter(function(attribute) {var name = attribute.nodeName;var match = paramRegex.test(name);return match;});paramAttributes.sort(function(a, b) {var aParts = paramRegex.exec(a.nodeName);var bParts = paramRegex.exec(b.nodeName);var aIndex = aParts.length > 1 ? aParts[1] : 0;var bIndex = bParts.length > 1 ? bParts[1] : 0;if (aIndex < bIndex) {return -1;} else if (bIndex < aIndex) {return 1;} else {return 0;}});var params = paramAttributes.map(function(attribute) {return attribute.nodeValue;});var routeParams = [namespace, route].concat(params);Router.go.apply(null, routeParams);data.originalEvent.preventDefault();});root.on(CustomEvents.events.activate, SELECTORS.ROUTES_BACK, function(e, data) {Router.back(namespace);data.originalEvent.preventDefault();});// Close the message drawer if the drawer is visible and the click happened outside the drawer and the toggle button.$(document).on(CustomEvents.events.activate, e => {var drawer = $(e.target).closest(SELECTORS.DRAWER);var toggleButtonId = $(SELECTORS.DRAWER)?.attr('data-origin');var toggleButton = '';if (toggleButtonId !== undefined && toggleButtonId) {toggleButton = $(e.target).closest("#" + toggleButtonId);}if (!drawer.length && !toggleButton.length && isVisible(root)) {// Determine if the element that was clicked is focusable.var focusableElement = $(e.target).closest(AriaSelectors.elements.focusable);if (focusableElement.length) {// We need to move the focus to the clicked element after the drawer is hidden,// so we need to clear the `data-origin` attribute first.$(SELECTORS.DRAWER).attr('data-origin', '');}// Hide the drawer.hide(root);// Move the focus to the clicked element if it is focusable.if (focusableElement.length) {focusableElement.focus();}}});// These are theme-specific to help us fix random behat fails.// These events target those events defined in BS3 and BS4 onwards.root[0].querySelectorAll('.collapse').forEach((collapse) => {collapse.addEventListener('hide.bs.collapse', (e) => {var pendingPromise = new Pending();e.target.addEventListener('hidden.bs.collapse', function() {pendingPromise.resolve();}, {once: true});});});root[0].querySelectorAll('.collapse').forEach((collapse) => {collapse.addEventListener('show.bs.collapse', (e) => {var pendingPromise = new Pending();e.target.addEventListener('shown.bs.collapse', function() {pendingPromise.resolve();}, {once: true});});});$(SELECTORS.JUMPTO).focus(function() {var firstInput = root.find(SELECTORS.CLOSE_BUTTON);if (firstInput.length) {firstInput.focus();} else {$(SELECTORS.HEADER_CONTAINER).find(SELECTORS.ROUTES_BACK).focus();}});$(SELECTORS.DRAWER).focus(function() {var button = $(this).attr('data-origin');if (button) {$('#' + button).focus();}});if (!alwaysVisible) {PubSub.subscribe(Events.SHOW, function() {show(namespace, root);});PubSub.subscribe(Events.HIDE, function() {hide(root);});PubSub.subscribe(Events.TOGGLE_VISIBILITY, function(buttonid) {const buttonElement = document.getElementById(buttonid);if (isVisible(root)) {hide(root);buttonElement?.setAttribute('aria-expanded', false);$(SELECTORS.JUMPTO).attr('tabindex', -1);} else {show(namespace, root);buttonElement?.setAttribute('aria-expanded', true);setJumpFrom(buttonid);$(SELECTORS.JUMPTO).attr('tabindex', 0);}});root.on(CustomEvents.events.escape, function() {PubSub.publish(Events.HIDE, {});});}PubSub.subscribe(Events.SHOW_CONVERSATION, function(args) {setJumpFrom(args.buttonid);show(namespace, root);Router.go(namespace, Routes.VIEW_CONVERSATION, args.conversationid);});var closebutton = root.find(SELECTORS.CLOSE_BUTTON);closebutton.on(CustomEvents.events.activate, function(e, data) {data.originalEvent.preventDefault();setFocus(root, false);var button = $(SELECTORS.DRAWER).attr('data-origin');if (button) {$('#' + button).focus();}PubSub.publish(Events.TOGGLE_VISIBILITY, button);});PubSub.subscribe(Events.CREATE_CONVERSATION_WITH_USER, function(args) {setJumpFrom(args.buttonid);show(namespace, root);Router.go(namespace, Routes.VIEW_CONVERSATION, null, 'create', args.userid);});PubSub.subscribe(Events.SHOW_SETTINGS, function() {show(namespace, root);Router.go(namespace, Routes.VIEW_SETTINGS);});PubSub.subscribe(Events.PREFERENCES_UPDATED, function(preferences) {var filteredPreferences = preferences.filter(function(preference) {return preference.type == 'message_entertosend';});var enterToSendPreference = filteredPreferences.length ? filteredPreferences[0] : null;if (enterToSendPreference) {var viewConversationFooter = root.find(SELECTORS.FOOTER_CONTAINER).find(SELECTORS.VIEW_CONVERSATION);viewConversationFooter.attr('data-enter-to-send', enterToSendPreference.value);}});// If our textarea is modified, remove the attribute which indicates the user has seen the unsent message notification.// This will allow the user to be notified again.const textArea = document.querySelector(SELECTORS.MESSAGE_TEXT_AREA);if (textArea) {textArea.addEventListener('keyup', function() {textArea.removeAttribute('data-unsent-message-viewed');});}// Catch any unsent messages and store them.window.addEventListener('beforeunload', storeUnsentMessage);};/*** Initialise the message drawer.** @param {Object} root The message drawer container.* @param {String} uniqueId Unique identifier for the Routes* @param {bool} alwaysVisible Should we show the app now, or wait for the user?* @param {Object} route*/var init = function(root, uniqueId, alwaysVisible, route) {root = $(root);createRoutes(uniqueId, root);registerEventListeners(uniqueId, root, alwaysVisible);if (alwaysVisible) {show(uniqueId, root);if (route) {var routeParams = route.params || [];routeParams = [uniqueId, route.path].concat(routeParams);Router.go.apply(null, routeParams);}}// Mark the drawer as ready.Helper.markDrawerReady();// Get and show any unsent message.getUnsentMessage(uniqueId, root);};return {init: init,};});