MDL-56431 messaging: Added aria label to filter.
[moodle.git] / message / amd / src / message_area_search.js
CommitLineData
cd03b8d7
MN
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 * The module handles searching contacts.
18 *
19 * @module core_message/message_area_search
20 * @package core_message
21 * @copyright 2016 Mark Nelson <markn@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
c476de18
MN
24define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/custom_interaction_events',
25 'core_message/message_area_events'],
26 function($, Ajax, Templates, Notification, Str, CustomEvents, Events) {
27
28 /** @type {Object} The list of selectors for the message area. */
29 var SELECTORS = {
30 CONTACTS: "[data-region='contacts'][data-region-content='contacts']",
31 CONTACTSAREA: "[data-region='contacts-area']",
32 CONVERSATIONS: "[data-region='contacts'][data-region-content='conversations']",
d14207fd 33 DELETESEARCHFILTER: "[data-region='search-filter-area']",
c476de18
MN
34 LOADINGICON: '.loading-icon',
35 SEARCHBOX: "[data-region='search-box']",
36 SEARCHFILTER: "[data-region='search-filter']",
37 SEARCHFILTERAREA: "[data-region='search-filter-area']",
38 SEARCHRESULTSAREA: "[data-region='search-results-area']",
39 SEARCHTEXTAREA: "[data-region='search-text-area']",
40 SEARCHUSERSINCOURSE: "[data-action='search-users-in-course']",
41 };
cd03b8d7
MN
42
43 /**
44 * Search class.
45 *
46 * @param {Messagearea} messageArea The messaging area object.
47 */
48 function Search(messageArea) {
49 this.messageArea = messageArea;
50 this._init();
51 }
52
53 /** @type {Messagearea} The messaging area object. */
54 Search.prototype.messageArea = null;
55
56 /** @type {String} The area we are searching in. */
57 Search.prototype._searchArea = null;
58
59 /** @type {String} The id of the course we are searching in (if any). */
60 Search.prototype._courseid = null;
61
62 /** @type {Boolean} checks if we are currently loading */
63 Search.prototype._isLoading = false;
64
65 /** @type {String} The number of messages displayed. */
66 Search.prototype._numMessagesDisplayed = 0;
67
68 /** @type {String} The number of messages to retrieve. */
69 Search.prototype._numMessagesToRetrieve = 20;
70
48e8bdba
MN
71 /** @type {String} The number of users displayed. */
72 Search.prototype._numUsersDisplayed = 0;
cd03b8d7 73
48e8bdba
MN
74 /** @type {String} The number of users to retrieve. */
75 Search.prototype._numUsersToRetrieve = 20;
cd03b8d7
MN
76
77 /** @type {Array} The type of available search areas. **/
78 Search.prototype._searchAreas = {
79 MESSAGES: 'messages',
48e8bdba
MN
80 USERS: 'users',
81 USERSINCOURSE: 'usersincourse'
cd03b8d7
MN
82 };
83
84 /** @type {int} The timeout before performing an ajax search */
85 Search.prototype._requestTimeout = null;
86
87 /**
88 * Initialise the event listeners.
89 *
90 * @private
91 */
92 Search.prototype._init = function() {
93 // Handle searching for text.
c476de18 94 this.messageArea.find(SELECTORS.SEARCHTEXTAREA).on('input', this._searchRequest.bind(this));
cd03b8d7 95
48e8bdba 96 // Handle clicking on a course in the list of users.
d14207fd 97 this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.SEARCHUSERSINCOURSE, function(e) {
cd03b8d7 98 this._setFilter($(e.currentTarget).html());
48e8bdba 99 this._setPlaceholderText('searchforuser');
cd03b8d7 100 this._clearSearchArea();
48e8bdba 101 this._searchArea = this._searchAreas.USERSINCOURSE;
cd03b8d7 102 this._courseid = $(e.currentTarget).data('courseid');
48e8bdba 103 this._searchUsersInCourse();
d14207fd 104 this.messageArea.find(SELECTORS.SEARCHBOX).focus();
cd03b8d7
MN
105 }.bind(this));
106
107 // Handle deleting the search filter.
d14207fd 108 this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.DELETESEARCHFILTER, function() {
cd03b8d7
MN
109 this._hideSearchResults();
110 // Filter has been removed, so we don't want to be searching in a course anymore.
48e8bdba
MN
111 this._searchArea = this._searchAreas.USERS;
112 this._setPlaceholderText('searchforuserorcourse');
cd03b8d7 113 // Go back the contacts.
c476de18 114 this.messageArea.trigger(Events.USERSSEARCHCANCELED);
d14207fd 115 this.messageArea.find(SELECTORS.SEARCHBOX).focus();
cd03b8d7
MN
116 }.bind(this));
117
118 // Handle events that occur outside this module.
c476de18 119 this.messageArea.onCustomEvent(Events.CONVERSATIONSSELECTED, function() {
cd03b8d7
MN
120 this._hideSearchResults();
121 this._searchArea = this._searchAreas.MESSAGES;
122 this._setPlaceholderText('searchmessages');
123 }.bind(this));
c476de18 124 this.messageArea.onCustomEvent(Events.CONTACTSSELECTED, function() {
cd03b8d7 125 this._hideSearchResults();
48e8bdba
MN
126 this._searchArea = this._searchAreas.USERS;
127 this._setPlaceholderText('searchforuserorcourse');
cd03b8d7 128 }.bind(this));
c476de18 129 this.messageArea.onCustomEvent(Events.MESSAGESENT, function() {
cd03b8d7
MN
130 this._hideSearchResults();
131 this._searchArea = this._searchAreas.MESSAGES;
132 this._setPlaceholderText('searchmessages');
133 }.bind(this));
134
48e8bdba 135 // Event listeners for scrolling through messages and users in courses.
c476de18
MN
136 CustomEvents.define(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), [
137 CustomEvents.events.scrollBottom
cd03b8d7 138 ]);
c476de18 139 this.messageArea.onDelegateEvent(CustomEvents.events.scrollBottom, SELECTORS.SEARCHRESULTSAREA,
cd03b8d7
MN
140 function() {
141 if (this._searchArea == this._searchAreas.MESSAGES) {
142 this._searchMessages();
48e8bdba
MN
143 } else if (this._searchArea == this._searchAreas.USERSINCOURSE) {
144 this._searchUsersInCourse();
cd03b8d7
MN
145 }
146 }.bind(this)
147 );
148
149 // Set the initial search area.
81517613 150 this._searchArea = (this.messageArea.showContactsFirst()) ? this._searchAreas.USERS : this._searchAreas.MESSAGES;
cd03b8d7
MN
151 };
152
153 /**
154 * Handles when search requests are sent.
155 *
156 * @private
157 */
158 Search.prototype._searchRequest = function() {
c476de18 159 var str = this.messageArea.find(SELECTORS.SEARCHTEXTAREA + ' input').val();
cd03b8d7
MN
160
161 if (this._requestTimeout) {
162 clearTimeout(this._requestTimeout);
163 }
164
165 if (str.trim() === '') {
166 // If nothing we being searched then we need to display the usual data.
167 if (this._searchArea == this._searchAreas.MESSAGES) {
168 this._hideSearchResults();
c476de18 169 this.messageArea.trigger(Events.MESSAGESEARCHCANCELED);
48e8bdba 170 } else if (this._searchArea == this._searchAreas.USERS) {
cd03b8d7 171 this._hideSearchResults();
c476de18 172 this.messageArea.trigger(Events.USERSSEARCHCANCELED);
48e8bdba
MN
173 } else if (this._searchArea == this._searchAreas.USERSINCOURSE) {
174 // We are still searching in a course, so need to list all the users again.
cd03b8d7 175 this._clearSearchArea();
48e8bdba 176 this._searchUsersInCourse();
cd03b8d7
MN
177 }
178 return;
179 }
180
c476de18
MN
181 this.messageArea.find(SELECTORS.CONVERSATIONS).hide();
182 this.messageArea.find(SELECTORS.CONTACTS).hide();
183 this.messageArea.find(SELECTORS.SEARCHRESULTSAREA).show();
cd03b8d7
MN
184
185 if (this._searchArea == this._searchAreas.MESSAGES) {
7b55aaa1 186 this._requestTimeout = setTimeout(function() {
cd03b8d7
MN
187 this._clearSearchArea();
188 this._numMessagesDisplayed = 0;
189 this._searchMessages();
190 }.bind(this), 300);
48e8bdba 191 } else if (this._searchArea == this._searchAreas.USERSINCOURSE) {
7b55aaa1 192 this._requestTimeout = setTimeout(function() {
cd03b8d7 193 this._clearSearchArea();
48e8bdba
MN
194 this._numUsersDisplayed = 0;
195 this._searchUsersInCourse();
cd03b8d7 196 }.bind(this), 300);
48e8bdba 197 } else { // Must be searching for users and courses
cd03b8d7
MN
198 this._requestTimeout = setTimeout(function() {
199 this._clearSearchArea();
48e8bdba
MN
200 this._numUsersDisplayed = 0;
201 this._searchUsers();
cd03b8d7
MN
202 }.bind(this), 300);
203 }
204 };
205
206 /**
207 * Handles searching for messages.
208 *
209 * @private
7b55aaa1 210 * @return {Promise|boolean} The promise resolved when the search area has been rendered
cd03b8d7
MN
211 */
212 Search.prototype._searchMessages = function() {
213 if (this._isLoading) {
7b55aaa1 214 return false;
cd03b8d7
MN
215 }
216
c476de18 217 var str = this.messageArea.find(SELECTORS.SEARCHBOX).val();
cd03b8d7
MN
218
219 // Tell the user we are loading items.
220 this._isLoading = true;
221
222 // Call the web service to get our data.
c476de18 223 var promises = Ajax.call([{
cd03b8d7
MN
224 methodname: 'core_message_data_for_messagearea_search_messages',
225 args: {
226 userid: this.messageArea.getCurrentUserId(),
227 search: str,
228 limitfrom: this._numMessagesDisplayed,
229 limitnum: this._numMessagesToRetrieve
230 }
231 }]);
232
233 // Keep track of the number of messages
234 var numberreceived = 0;
235 // Add loading icon to the end of the list.
c476de18
MN
236 return Templates.render('core/loading', {}).then(function(html, js) {
237 Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA),
cd03b8d7
MN
238 "<div style='text-align:center'>" + html + "</div>", js);
239 return promises[0];
240 }.bind(this)).then(function(data) {
241 numberreceived = data.contacts.length;
de55cb1b 242 return Templates.render('core_message/message_area_message_search_results', data);
cd03b8d7
MN
243 }).then(function(html, js) {
244 // Remove the loading icon.
c476de18
MN
245 this.messageArea.find(SELECTORS.SEARCHRESULTSAREA + " " +
246 SELECTORS.LOADINGICON).remove();
cd03b8d7
MN
247 // Only append data if we got data back.
248 if (numberreceived > 0) {
249 // Show the new content.
c476de18 250 Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
cd03b8d7
MN
251 // Increment the number of contacts displayed.
252 this._numMessagesDisplayed += numberreceived;
c9ead36a
MN
253 } else if (this._numMessagesDisplayed == 0) { // Must have nothing to begin with.
254 // Replace the new content.
c476de18 255 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
cd03b8d7
MN
256 }
257 // Mark that we are no longer busy loading data.
258 this._isLoading = false;
c476de18 259 }.bind(this)).fail(Notification.exception);
cd03b8d7
MN
260 };
261
cd03b8d7 262 /**
48e8bdba 263 * Handles searching for users.
cd03b8d7
MN
264 *
265 * @private
7b55aaa1 266 * @return {Promise} The promise resolved when the search area has been rendered
cd03b8d7 267 */
48e8bdba 268 Search.prototype._searchUsers = function() {
c476de18 269 var str = this.messageArea.find(SELECTORS.SEARCHBOX).val();
cd03b8d7
MN
270
271 // Call the web service to get our data.
c476de18 272 var promises = Ajax.call([{
48e8bdba 273 methodname: 'core_message_data_for_messagearea_search_users',
cd03b8d7
MN
274 args: {
275 userid: this.messageArea.getCurrentUserId(),
276 search: str,
48e8bdba 277 limitnum: this._numUsersToRetrieve
cd03b8d7
MN
278 }
279 }]);
280
281 // Perform the search and replace the content.
c476de18
MN
282 return Templates.render('core/loading', {}).then(function(html, js) {
283 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA),
cd03b8d7
MN
284 "<div style='text-align:center'>" + html + "</div>", js);
285 return promises[0];
286 }.bind(this)).then(function(data) {
de55cb1b
MN
287 if (data.contacts.length > 0) {
288 data.hascontacts = true;
289 }
290 if (data.courses.length > 0) {
291 data.hascourses = true;
292 }
293 if (data.noncontacts.length > 0) {
294 data.hasnoncontacts = true;
295 }
296 return Templates.render('core_message/message_area_user_search_results', data);
cd03b8d7 297 }).then(function(html, js) {
c476de18
MN
298 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
299 }.bind(this)).fail(Notification.exception);
cd03b8d7
MN
300 };
301
302 /**
48e8bdba 303 * Handles searching for users in a course.
cd03b8d7
MN
304 *
305 * @private
7b55aaa1 306 * @return {Promise|boolean} The promise resolved when the search area has been rendered
cd03b8d7 307 */
48e8bdba 308 Search.prototype._searchUsersInCourse = function() {
cd03b8d7 309 if (this._isLoading) {
7b55aaa1 310 return false;
cd03b8d7
MN
311 }
312
c476de18 313 var str = this.messageArea.find(SELECTORS.SEARCHBOX).val();
cd03b8d7
MN
314
315 // Tell the user we are loading items.
316 this._isLoading = true;
317
318 // Call the web service to get our data.
c476de18 319 var promises = Ajax.call([{
48e8bdba 320 methodname: 'core_message_data_for_messagearea_search_users_in_course',
cd03b8d7
MN
321 args: {
322 userid: this.messageArea.getCurrentUserId(),
323 courseid: this._courseid,
324 search: str,
48e8bdba
MN
325 limitfrom: this._numUsersDisplayed,
326 limitnum: this._numUsersToRetrieve
cd03b8d7
MN
327 }
328 }]);
329
330 // Keep track of the number of contacts
331 var numberreceived = 0;
332 // Add loading icon to the end of the list.
c476de18
MN
333 return Templates.render('core/loading', {}).then(function(html, js) {
334 Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA),
cd03b8d7
MN
335 "<div style='text-align:center'>" + html + "</div>", js);
336 return promises[0];
337 }.bind(this)).then(function(data) {
338 numberreceived = data.contacts.length;
de55cb1b
MN
339 if (numberreceived > 0) {
340 data.hascontacts = true;
341 }
342 return Templates.render('core_message/message_area_user_search_results', data);
cd03b8d7
MN
343 }).then(function(html, js) {
344 // Remove the loading icon.
c476de18
MN
345 this.messageArea.find(SELECTORS.SEARCHRESULTSAREA + " " +
346 SELECTORS.LOADINGICON).remove();
cd03b8d7
MN
347 // Only append data if we got data back.
348 if (numberreceived > 0) {
349 // Show the new content.
c476de18 350 Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
cd03b8d7 351 // Increment the number of contacts displayed.
48e8bdba
MN
352 this._numUsersDisplayed += numberreceived;
353 } else if (this._numUsersDisplayed == 0) { // Must have nothing to begin with.
c9ead36a 354 // Replace the new content.
c476de18 355 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
cd03b8d7
MN
356 }
357 // Mark that we are no longer busy loading data.
358 this._isLoading = false;
c476de18 359 }.bind(this)).fail(Notification.exception);
cd03b8d7
MN
360 };
361
362 /**
363 * Sets placeholder text for search input.
364 *
365 * @private
366 * @param {String} text The placeholder text
367 * @return {Promise} The promise resolved when the placeholder text has been set
368 */
369 Search.prototype._setPlaceholderText = function(text) {
c476de18
MN
370 return Str.get_string(text, 'message').then(function(s) {
371 this.messageArea.find(SELECTORS.SEARCHTEXTAREA + ' input').attr('placeholder', s);
cd03b8d7
MN
372 }.bind(this));
373 };
374
cd03b8d7
MN
375 /**
376 * Sets filter for search input.
377 *
378 * @private
379 * @param {String} text The filter text
380 */
381 Search.prototype._setFilter = function(text) {
c476de18
MN
382 this.messageArea.find(SELECTORS.SEARCHBOX).val('');
383 this.messageArea.find(SELECTORS.CONTACTSAREA).addClass('searchfilter');
384 this.messageArea.find(SELECTORS.SEARCHFILTERAREA).show();
385 this.messageArea.find(SELECTORS.SEARCHFILTER).html(text);
72a5842b
AG
386 Str.get_string('removecoursefilter', 'message', text).then(function(languagestring) {
387 this.messageArea.find(SELECTORS.SEARCHFILTERAREA).attr('aria-label', languagestring);
388 }.bind(this));
cd03b8d7
MN
389 };
390
391 /**
392 * Hides filter for search input.
393 *
394 * @private
395 */
396 Search.prototype._clearFilters = function() {
c476de18
MN
397 this.messageArea.find(SELECTORS.CONTACTSAREA).removeClass('searchfilter');
398 this.messageArea.find(SELECTORS.SEARCHFILTER).empty();
399 this.messageArea.find(SELECTORS.SEARCHFILTERAREA).hide();
72a5842b 400 this.messageArea.find(SELECTORS.SEARCHFILTERAREA).removeAttr('aria-label');
cd03b8d7
MN
401 };
402
403 /**
404 * Handles clearing the search area.
405 *
406 * @private
407 */
408 Search.prototype._clearSearchArea = function() {
c476de18 409 this.messageArea.find(SELECTORS.SEARCHRESULTSAREA).empty();
cd03b8d7
MN
410 };
411
412 /**
413 * Handles hiding the search area.
414 *
415 * @private
416 */
417 Search.prototype._hideSearchResults = function() {
418 this._clearFilters();
c476de18 419 this.messageArea.find(SELECTORS.SEARCHTEXTAREA + ' input').val('');
cd03b8d7 420 this._clearSearchArea();
c476de18 421 this.messageArea.find(SELECTORS.SEARCHRESULTSAREA).hide();
cd03b8d7
MN
422 };
423
424 return Search;
9d783a8d 425});