MDL-56431 messaging: Added aria label to filter.
[moodle.git] / message / amd / src / message_area_search.js
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/>.
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  */
24 define(['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) {
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']",
33         DELETESEARCHFILTER: "[data-region='search-filter-area']",
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     };
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     }
53     /** @type {Messagearea} The messaging area object. */
54     Search.prototype.messageArea = null;
56     /** @type {String} The area we are searching in. */
57     Search.prototype._searchArea = null;
59     /** @type {String} The id of the course we are searching in (if any). */
60     Search.prototype._courseid = null;
62     /** @type {Boolean} checks if we are currently loading  */
63     Search.prototype._isLoading = false;
65     /** @type {String} The number of messages displayed. */
66     Search.prototype._numMessagesDisplayed = 0;
68     /** @type {String} The number of messages to retrieve. */
69     Search.prototype._numMessagesToRetrieve = 20;
71     /** @type {String} The number of users displayed. */
72     Search.prototype._numUsersDisplayed = 0;
74     /** @type {String} The number of users to retrieve. */
75     Search.prototype._numUsersToRetrieve = 20;
77     /** @type {Array} The type of available search areas. **/
78     Search.prototype._searchAreas = {
79         MESSAGES: 'messages',
80         USERS: 'users',
81         USERSINCOURSE: 'usersincourse'
82     };
84     /** @type {int} The timeout before performing an ajax search */
85     Search.prototype._requestTimeout = null;
87     /**
88      * Initialise the event listeners.
89      *
90      * @private
91      */
92     Search.prototype._init = function() {
93         // Handle searching for text.
94         this.messageArea.find(SELECTORS.SEARCHTEXTAREA).on('input', this._searchRequest.bind(this));
96         // Handle clicking on a course in the list of users.
97         this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.SEARCHUSERSINCOURSE, function(e) {
98             this._setFilter($(e.currentTarget).html());
99             this._setPlaceholderText('searchforuser');
100             this._clearSearchArea();
101             this._searchArea = this._searchAreas.USERSINCOURSE;
102             this._courseid = $(e.currentTarget).data('courseid');
103             this._searchUsersInCourse();
104             this.messageArea.find(SELECTORS.SEARCHBOX).focus();
105         }.bind(this));
107         // Handle deleting the search filter.
108         this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.DELETESEARCHFILTER, function() {
109             this._hideSearchResults();
110             // Filter has been removed, so we don't want to be searching in a course anymore.
111             this._searchArea = this._searchAreas.USERS;
112             this._setPlaceholderText('searchforuserorcourse');
113             // Go back the contacts.
114             this.messageArea.trigger(Events.USERSSEARCHCANCELED);
115             this.messageArea.find(SELECTORS.SEARCHBOX).focus();
116         }.bind(this));
118         // Handle events that occur outside this module.
119         this.messageArea.onCustomEvent(Events.CONVERSATIONSSELECTED, function() {
120             this._hideSearchResults();
121             this._searchArea = this._searchAreas.MESSAGES;
122             this._setPlaceholderText('searchmessages');
123         }.bind(this));
124         this.messageArea.onCustomEvent(Events.CONTACTSSELECTED, function() {
125             this._hideSearchResults();
126             this._searchArea = this._searchAreas.USERS;
127             this._setPlaceholderText('searchforuserorcourse');
128         }.bind(this));
129         this.messageArea.onCustomEvent(Events.MESSAGESENT, function() {
130             this._hideSearchResults();
131             this._searchArea = this._searchAreas.MESSAGES;
132             this._setPlaceholderText('searchmessages');
133         }.bind(this));
135         // Event listeners for scrolling through messages and users in courses.
136         CustomEvents.define(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), [
137             CustomEvents.events.scrollBottom
138         ]);
139         this.messageArea.onDelegateEvent(CustomEvents.events.scrollBottom, SELECTORS.SEARCHRESULTSAREA,
140             function() {
141                 if (this._searchArea == this._searchAreas.MESSAGES) {
142                     this._searchMessages();
143                 } else if (this._searchArea == this._searchAreas.USERSINCOURSE) {
144                     this._searchUsersInCourse();
145                 }
146             }.bind(this)
147         );
149         // Set the initial search area.
150         this._searchArea = (this.messageArea.showContactsFirst()) ? this._searchAreas.USERS : this._searchAreas.MESSAGES;
151     };
153     /**
154      * Handles when search requests are sent.
155      *
156      * @private
157      */
158     Search.prototype._searchRequest = function() {
159         var str = this.messageArea.find(SELECTORS.SEARCHTEXTAREA + ' input').val();
161         if (this._requestTimeout) {
162             clearTimeout(this._requestTimeout);
163         }
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();
169                 this.messageArea.trigger(Events.MESSAGESEARCHCANCELED);
170             } else if (this._searchArea == this._searchAreas.USERS) {
171                 this._hideSearchResults();
172                 this.messageArea.trigger(Events.USERSSEARCHCANCELED);
173             } else if (this._searchArea == this._searchAreas.USERSINCOURSE) {
174                 // We are still searching in a course, so need to list all the users again.
175                 this._clearSearchArea();
176                 this._searchUsersInCourse();
177             }
178             return;
179         }
181         this.messageArea.find(SELECTORS.CONVERSATIONS).hide();
182         this.messageArea.find(SELECTORS.CONTACTS).hide();
183         this.messageArea.find(SELECTORS.SEARCHRESULTSAREA).show();
185         if (this._searchArea == this._searchAreas.MESSAGES) {
186             this._requestTimeout = setTimeout(function() {
187                 this._clearSearchArea();
188                 this._numMessagesDisplayed = 0;
189                 this._searchMessages();
190             }.bind(this), 300);
191         } else if (this._searchArea == this._searchAreas.USERSINCOURSE) {
192             this._requestTimeout = setTimeout(function() {
193                 this._clearSearchArea();
194                 this._numUsersDisplayed = 0;
195                 this._searchUsersInCourse();
196             }.bind(this), 300);
197         } else { // Must be searching for users and courses
198             this._requestTimeout = setTimeout(function() {
199                 this._clearSearchArea();
200                 this._numUsersDisplayed = 0;
201                 this._searchUsers();
202             }.bind(this), 300);
203         }
204     };
206     /**
207      * Handles searching for messages.
208      *
209      * @private
210      * @return {Promise|boolean} The promise resolved when the search area has been rendered
211      */
212     Search.prototype._searchMessages = function() {
213         if (this._isLoading) {
214             return false;
215         }
217         var str = this.messageArea.find(SELECTORS.SEARCHBOX).val();
219         // Tell the user we are loading items.
220         this._isLoading = true;
222         // Call the web service to get our data.
223         var promises = Ajax.call([{
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         }]);
233         // Keep track of the number of messages
234         var numberreceived = 0;
235         // Add loading icon to the end of the list.
236         return Templates.render('core/loading', {}).then(function(html, js) {
237             Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA),
238                 "<div style='text-align:center'>" + html + "</div>", js);
239             return promises[0];
240         }.bind(this)).then(function(data) {
241             numberreceived = data.contacts.length;
242             return Templates.render('core_message/message_area_message_search_results', data);
243         }).then(function(html, js) {
244             // Remove the loading icon.
245             this.messageArea.find(SELECTORS.SEARCHRESULTSAREA + " " +
246                 SELECTORS.LOADINGICON).remove();
247             // Only append data if we got data back.
248             if (numberreceived > 0) {
249                 // Show the new content.
250                 Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
251                 // Increment the number of contacts displayed.
252                 this._numMessagesDisplayed += numberreceived;
253             } else if (this._numMessagesDisplayed == 0) { // Must have nothing to begin with.
254                 // Replace the new content.
255                 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
256             }
257             // Mark that we are no longer busy loading data.
258             this._isLoading = false;
259         }.bind(this)).fail(Notification.exception);
260     };
262     /**
263      * Handles searching for users.
264      *
265      * @private
266      * @return {Promise} The promise resolved when the search area has been rendered
267      */
268     Search.prototype._searchUsers = function() {
269         var str = this.messageArea.find(SELECTORS.SEARCHBOX).val();
271         // Call the web service to get our data.
272         var promises = Ajax.call([{
273             methodname: 'core_message_data_for_messagearea_search_users',
274             args: {
275                 userid: this.messageArea.getCurrentUserId(),
276                 search: str,
277                 limitnum: this._numUsersToRetrieve
278             }
279         }]);
281         // Perform the search and replace the content.
282         return Templates.render('core/loading', {}).then(function(html, js) {
283             Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA),
284                 "<div style='text-align:center'>" + html + "</div>", js);
285             return promises[0];
286         }.bind(this)).then(function(data) {
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);
297         }).then(function(html, js) {
298             Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
299         }.bind(this)).fail(Notification.exception);
300     };
302     /**
303      * Handles searching for users in a course.
304      *
305      * @private
306      * @return {Promise|boolean} The promise resolved when the search area has been rendered
307      */
308     Search.prototype._searchUsersInCourse = function() {
309         if (this._isLoading) {
310             return false;
311         }
313         var str = this.messageArea.find(SELECTORS.SEARCHBOX).val();
315         // Tell the user we are loading items.
316         this._isLoading = true;
318         // Call the web service to get our data.
319         var promises = Ajax.call([{
320             methodname: 'core_message_data_for_messagearea_search_users_in_course',
321             args: {
322                 userid: this.messageArea.getCurrentUserId(),
323                 courseid: this._courseid,
324                 search: str,
325                 limitfrom: this._numUsersDisplayed,
326                 limitnum: this._numUsersToRetrieve
327             }
328         }]);
330         // Keep track of the number of contacts
331         var numberreceived = 0;
332         // Add loading icon to the end of the list.
333         return Templates.render('core/loading', {}).then(function(html, js) {
334             Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA),
335                 "<div style='text-align:center'>" + html + "</div>", js);
336             return promises[0];
337         }.bind(this)).then(function(data) {
338             numberreceived = data.contacts.length;
339             if (numberreceived > 0) {
340                 data.hascontacts = true;
341             }
342             return Templates.render('core_message/message_area_user_search_results', data);
343         }).then(function(html, js) {
344             // Remove the loading icon.
345             this.messageArea.find(SELECTORS.SEARCHRESULTSAREA + " " +
346                 SELECTORS.LOADINGICON).remove();
347             // Only append data if we got data back.
348             if (numberreceived > 0) {
349                 // Show the new content.
350                 Templates.appendNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
351                 // Increment the number of contacts displayed.
352                 this._numUsersDisplayed += numberreceived;
353             } else if (this._numUsersDisplayed == 0) { // Must have nothing to begin with.
354                 // Replace the new content.
355                 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.SEARCHRESULTSAREA), html, js);
356             }
357             // Mark that we are no longer busy loading data.
358             this._isLoading = false;
359         }.bind(this)).fail(Notification.exception);
360     };
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) {
370         return Str.get_string(text, 'message').then(function(s) {
371             this.messageArea.find(SELECTORS.SEARCHTEXTAREA + ' input').attr('placeholder', s);
372         }.bind(this));
373     };
375     /**
376      * Sets filter for search input.
377      *
378      * @private
379      * @param {String} text The filter text
380      */
381     Search.prototype._setFilter = function(text) {
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);
386         Str.get_string('removecoursefilter', 'message', text).then(function(languagestring) {
387             this.messageArea.find(SELECTORS.SEARCHFILTERAREA).attr('aria-label', languagestring);
388         }.bind(this));
389     };
391     /**
392      * Hides filter for search input.
393      *
394      * @private
395      */
396     Search.prototype._clearFilters = function() {
397         this.messageArea.find(SELECTORS.CONTACTSAREA).removeClass('searchfilter');
398         this.messageArea.find(SELECTORS.SEARCHFILTER).empty();
399         this.messageArea.find(SELECTORS.SEARCHFILTERAREA).hide();
400         this.messageArea.find(SELECTORS.SEARCHFILTERAREA).removeAttr('aria-label');
401     };
403     /**
404      * Handles clearing the search area.
405      *
406      * @private
407      */
408     Search.prototype._clearSearchArea = function() {
409         this.messageArea.find(SELECTORS.SEARCHRESULTSAREA).empty();
410     };
412     /**
413      * Handles hiding the search area.
414      *
415      * @private
416      */
417     Search.prototype._hideSearchResults = function() {
418         this._clearFilters();
419         this.messageArea.find(SELECTORS.SEARCHTEXTAREA + ' input').val('');
420         this._clearSearchArea();
421         this.messageArea.find(SELECTORS.SEARCHRESULTSAREA).hide();
422     };
424     return Search;
425 });