MDL-69878 core_message: always show message drawer close icon
[moodle.git] / message / amd / src / message_drawer.js
CommitLineData
5005d8cf
RW
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15
16/**
17 * Controls the message drawer.
18 *
19 * @module core_message/message_drawer
20 * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23define(
24[
25 'jquery',
26 'core/custom_interaction_events',
27 'core/pubsub',
28 'core_message/message_drawer_view_contact',
29 'core_message/message_drawer_view_contacts',
30 'core_message/message_drawer_view_conversation',
31 'core_message/message_drawer_view_group_info',
32 'core_message/message_drawer_view_overview',
33 'core_message/message_drawer_view_search',
34 'core_message/message_drawer_view_settings',
35 'core_message/message_drawer_router',
36 'core_message/message_drawer_routes',
11970d0a
AN
37 'core_message/message_drawer_events',
38 'core/pending',
8aca1807 39 'core/drawer',
5005d8cf
RW
40],
41function(
42 $,
43 CustomEvents,
44 PubSub,
45 ViewContact,
46 ViewContacts,
47 ViewConversation,
48 ViewGroupInfo,
49 ViewOverview,
50 ViewSearch,
51 ViewSettings,
52 Router,
53 Routes,
11970d0a 54 Events,
8aca1807
JP
55 Pending,
56 Drawer
5005d8cf
RW
57) {
58
59 var SELECTORS = {
45decc81
BB
60 DRAWER: '[data-region="right-hand-drawer"]',
61 JUMPTO: '.popover-region [data-region="jumpto"]',
7cbea160
BB
62 PANEL_BODY_CONTAINER: '[data-region="panel-body-container"]',
63 PANEL_HEADER_CONTAINER: '[data-region="panel-header-container"]',
5005d8cf
RW
64 VIEW_CONTACT: '[data-region="view-contact"]',
65 VIEW_CONTACTS: '[data-region="view-contacts"]',
66 VIEW_CONVERSATION: '[data-region="view-conversation"]',
67 VIEW_GROUP_INFO: '[data-region="view-group-info"]',
68 VIEW_OVERVIEW: '[data-region="view-overview"]',
69 VIEW_SEARCH: '[data-region="view-search"]',
70 VIEW_SETTINGS: '[data-region="view-settings"]',
71 ROUTES: '[data-route]',
72 ROUTES_BACK: '[data-route-back]',
73 HEADER_CONTAINER: '[data-region="header-container"]',
74 BODY_CONTAINER: '[data-region="body-container"]',
75 FOOTER_CONTAINER: '[data-region="footer-container"]',
92bc86e4 76 CLOSE_BUTTON: '[data-action="closedrawer"]'
5005d8cf
RW
77 };
78
79 /**
80 * Get elements for route.
81 *
cb01c45c 82 * @param {String} namespace Unique identifier for the Routes
5005d8cf
RW
83 * @param {Object} root The message drawer container.
84 * @param {string} selector The route container.
85 *
86 * @return {array} elements Found route container objects.
87 */
cb01c45c 88 var getParametersForRoute = function(namespace, root, selector) {
7cbea160
BB
89
90 var header = root.find(SELECTORS.HEADER_CONTAINER).find(selector);
91 if (!header.length) {
92 header = root.find(SELECTORS.PANEL_HEADER_CONTAINER).find(selector);
93 }
94 var body = root.find(SELECTORS.BODY_CONTAINER).find(selector);
95 if (!body.length) {
96 body = root.find(SELECTORS.PANEL_BODY_CONTAINER).find(selector);
97 }
98 var footer = root.find(SELECTORS.FOOTER_CONTAINER).find(selector);
5005d8cf 99
cb01c45c
MN
100 return [
101 namespace,
102 header.length ? header : null,
103 body.length ? body : null,
104 footer.length ? footer : null
105 ];
5005d8cf
RW
106 };
107
108 var routes = [
109 [Routes.VIEW_CONTACT, SELECTORS.VIEW_CONTACT, ViewContact.show, ViewContact.description],
110 [Routes.VIEW_CONTACTS, SELECTORS.VIEW_CONTACTS, ViewContacts.show, ViewContacts.description],
111 [Routes.VIEW_CONVERSATION, SELECTORS.VIEW_CONVERSATION, ViewConversation.show, ViewConversation.description],
112 [Routes.VIEW_GROUP_INFO, SELECTORS.VIEW_GROUP_INFO, ViewGroupInfo.show, ViewGroupInfo.description],
113 [Routes.VIEW_OVERVIEW, SELECTORS.VIEW_OVERVIEW, ViewOverview.show, ViewOverview.description],
114 [Routes.VIEW_SEARCH, SELECTORS.VIEW_SEARCH, ViewSearch.show, ViewSearch.description],
cb01c45c 115 [Routes.VIEW_SETTINGS, SELECTORS.VIEW_SETTINGS, ViewSettings.show, ViewSettings.description]
5005d8cf
RW
116 ];
117
118 /**
119 * Create routes.
120 *
cb01c45c 121 * @param {String} namespace Unique identifier for the Routes
5005d8cf
RW
122 * @param {Object} root The message drawer container.
123 */
cb01c45c 124 var createRoutes = function(namespace, root) {
5005d8cf 125 routes.forEach(function(route) {
cb01c45c 126 Router.add(namespace, route[0], getParametersForRoute(namespace, root, route[1]), route[2], route[3]);
5005d8cf
RW
127 });
128 };
129
130 /**
131 * Show the message drawer.
132 *
cb01c45c 133 * @param {string} namespace The route namespace.
5005d8cf
RW
134 * @param {Object} root The message drawer container.
135 */
cb01c45c 136 var show = function(namespace, root) {
5005d8cf 137 if (!root.attr('data-shown')) {
cb01c45c 138 Router.go(namespace, Routes.VIEW_OVERVIEW);
5005d8cf
RW
139 root.attr('data-shown', true);
140 }
141
8aca1807 142 var drawerRoot = Drawer.getDrawerRoot(root);
6d9c9356 143 if (drawerRoot.length) {
8aca1807
JP
144 Drawer.show(drawerRoot);
145 }
5005d8cf
RW
146 };
147
148 /**
149 * Hide the message drawer.
150 *
151 * @param {Object} root The message drawer container.
152 */
153 var hide = function(root) {
8aca1807 154 var drawerRoot = Drawer.getDrawerRoot(root);
6d9c9356 155 if (drawerRoot.length) {
8aca1807
JP
156 Drawer.hide(drawerRoot);
157 }
5005d8cf
RW
158 };
159
160 /**
161 * Check if the drawer is visible.
162 *
163 * @param {Object} root The message drawer container.
8aca1807 164 * @return {boolean}
5005d8cf
RW
165 */
166 var isVisible = function(root) {
8aca1807 167 var drawerRoot = Drawer.getDrawerRoot(root);
6d9c9356 168 if (drawerRoot.length) {
8aca1807
JP
169 return Drawer.isVisible(drawerRoot);
170 }
171 return true;
5005d8cf
RW
172 };
173
45decc81
BB
174 /**
175 * Set Jump from button
176 *
177 * @param {String} buttonid The originating button id
178 */
179 var setJumpFrom = function(buttonid) {
180 $(SELECTORS.DRAWER).attr('data-origin', buttonid);
181 };
182
5005d8cf
RW
183 /**
184 * Listen to and handle events for routing, showing and hiding the message drawer.
185 *
cb01c45c 186 * @param {string} namespace The route namespace.
5005d8cf 187 * @param {Object} root The message drawer container.
fd998fc6 188 * @param {bool} alwaysVisible Is this messaging app always shown?
5005d8cf 189 */
fd998fc6 190 var registerEventListeners = function(namespace, root, alwaysVisible) {
5005d8cf
RW
191 CustomEvents.define(root, [CustomEvents.events.activate]);
192 var paramRegex = /^data-route-param-?(\d*)$/;
193
194 root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) {
195 var element = $(e.target).closest(SELECTORS.ROUTES);
196 var route = element.attr('data-route');
197 var attributes = [];
198
199 for (var i = 0; i < element[0].attributes.length; i++) {
200 attributes.push(element[0].attributes[i]);
201 }
202
203 var paramAttributes = attributes.filter(function(attribute) {
204 var name = attribute.nodeName;
205 var match = paramRegex.test(name);
206 return match;
207 });
208 paramAttributes.sort(function(a, b) {
209 var aParts = paramRegex.exec(a.nodeName);
210 var bParts = paramRegex.exec(b.nodeName);
211 var aIndex = aParts.length > 1 ? aParts[1] : 0;
212 var bIndex = bParts.length > 1 ? bParts[1] : 0;
213
214 if (aIndex < bIndex) {
215 return -1;
216 } else if (bIndex < aIndex) {
217 return 1;
218 } else {
219 return 0;
220 }
221 });
222
223 var params = paramAttributes.map(function(attribute) {
224 return attribute.nodeValue;
225 });
7cbea160 226
cb01c45c 227 var routeParams = [namespace, route].concat(params);
5005d8cf
RW
228
229 Router.go.apply(null, routeParams);
230
231 data.originalEvent.preventDefault();
232 });
233
234 root.on(CustomEvents.events.activate, SELECTORS.ROUTES_BACK, function(e, data) {
cb01c45c 235 Router.back(namespace);
5005d8cf
RW
236
237 data.originalEvent.preventDefault();
238 });
239
11970d0a
AN
240 // These are theme-specific to help us fix random behat fails.
241 // These events target those events defined in BS3 and BS4 onwards.
242 root.on('hide.bs.collapse', '.collapse', function(e) {
243 var pendingPromise = new Pending();
244 $(e.target).one('hidden.bs.collapse', function() {
245 pendingPromise.resolve();
246 });
247 });
248
249 root.on('show.bs.collapse', '.collapse', function(e) {
250 var pendingPromise = new Pending();
251 $(e.target).one('shown.bs.collapse', function() {
252 pendingPromise.resolve();
253 });
254 });
255
45decc81 256 $(SELECTORS.JUMPTO).focus(function() {
35bd8622 257 var firstInput = root.find(SELECTORS.CLOSE_BUTTON);
45decc81
BB
258 if (firstInput.length) {
259 firstInput.focus();
260 } else {
261 $(SELECTORS.HEADER_CONTAINER).find(SELECTORS.ROUTES_BACK).focus();
262 }
263 });
264
265 $(SELECTORS.DRAWER).focus(function() {
266 var button = $(this).attr('data-origin');
267 if (button) {
268 $('#' + button).focus();
269 }
270 });
271
fd998fc6
MN
272 if (!alwaysVisible) {
273 PubSub.subscribe(Events.SHOW, function() {
274 show(namespace, root);
275 });
5005d8cf 276
fd998fc6 277 PubSub.subscribe(Events.HIDE, function() {
5005d8cf 278 hide(root);
fd998fc6
MN
279 });
280
45decc81 281 PubSub.subscribe(Events.TOGGLE_VISIBILITY, function(buttonid) {
fd998fc6
MN
282 if (isVisible(root)) {
283 hide(root);
45decc81 284 $(SELECTORS.JUMPTO).attr('tabindex', -1);
fd998fc6
MN
285 } else {
286 show(namespace, root);
45decc81
BB
287 setJumpFrom(buttonid);
288 $(SELECTORS.JUMPTO).attr('tabindex', 0);
fd998fc6
MN
289 }
290 });
291 }
5005d8cf 292
45decc81
BB
293 PubSub.subscribe(Events.SHOW_CONVERSATION, function(args) {
294 setJumpFrom(args.buttonid);
cb01c45c 295 show(namespace, root);
45decc81 296 Router.go(namespace, Routes.VIEW_CONVERSATION, args.conversationid);
5005d8cf
RW
297 });
298
92bc86e4
BB
299 var closebutton = root.find(SELECTORS.CLOSE_BUTTON);
300 closebutton.on(CustomEvents.events.activate, function() {
35bd8622
BB
301 var button = $(SELECTORS.DRAWER).attr('data-origin');
302 if (button) {
303 $('#' + button).focus();
304 }
92bc86e4
BB
305 PubSub.publish(Events.TOGGLE_VISIBILITY);
306 });
307
45decc81
BB
308 PubSub.subscribe(Events.CREATE_CONVERSATION_WITH_USER, function(args) {
309 setJumpFrom(args.buttonid);
cb01c45c 310 show(namespace, root);
45decc81 311 Router.go(namespace, Routes.VIEW_CONVERSATION, null, 'create', args.userid);
1d3535f9
RW
312 });
313
5005d8cf 314 PubSub.subscribe(Events.SHOW_SETTINGS, function() {
cb01c45c
MN
315 show(namespace, root);
316 Router.go(namespace, Routes.VIEW_SETTINGS);
5005d8cf 317 });
663ccd58
RW
318
319 PubSub.subscribe(Events.PREFERENCES_UPDATED, function(preferences) {
320 var filteredPreferences = preferences.filter(function(preference) {
321 return preference.type == 'message_entertosend';
322 });
323 var enterToSendPreference = filteredPreferences.length ? filteredPreferences[0] : null;
324
325 if (enterToSendPreference) {
326 var viewConversationFooter = root.find(SELECTORS.FOOTER_CONTAINER).find(SELECTORS.VIEW_CONVERSATION);
327 viewConversationFooter.attr('data-enter-to-send', enterToSendPreference.value);
328 }
329 });
5005d8cf
RW
330 };
331
332 /**
333 * Initialise the message drawer.
334 *
335 * @param {Object} root The message drawer container.
cb01c45c 336 * @param {String} uniqueId Unique identifier for the Routes
fd998fc6 337 * @param {bool} alwaysVisible Should we show the app now, or wait for the user?
11970d0a 338 * @param {Object} route
5005d8cf 339 */
6cb33ce7 340 var init = function(root, uniqueId, alwaysVisible, route) {
5005d8cf 341 root = $(root);
cb01c45c 342 createRoutes(uniqueId, root);
fd998fc6 343 registerEventListeners(uniqueId, root, alwaysVisible);
6cb33ce7 344
fd998fc6
MN
345 if (alwaysVisible) {
346 show(uniqueId, root);
6cb33ce7
RW
347
348 if (route) {
349 var routeParams = route.params || [];
350 routeParams = [uniqueId, route.path].concat(routeParams);
351 Router.go.apply(null, routeParams);
fd998fc6
MN
352 }
353 }
5005d8cf
RW
354 };
355
356 return {
357 init: init,
358 };
359});