1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
17 * Controls the search page of the message drawer.
19 * @module core_message/message_drawer_view_search
20 * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 'core/custom_interaction_events',
31 'core_message/message_repository',
32 'core_message/message_drawer_events',
45 var MESSAGE_SEARCH_LIMIT = 50;
46 var USERS_SEARCH_LIMIT = 50;
47 var USERS_INITIAL_SEARCH_LIMIT = 3;
50 BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]',
51 CANCEL_SEARCH_BUTTON: '[data-action="cancel-search"]',
52 CONTACTS_CONTAINER: '[data-region="contacts-container"]',
53 CONTACTS_LIST: '[data-region="contacts-container"] [data-region="list"]',
54 EMPTY_MESSAGE_CONTAINER: '[data-region="empty-message-container"]',
55 LIST: '[data-region="list"]',
56 LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
57 LOADING_PLACEHOLDER: '[data-region="loading-placeholder"]',
58 MESSAGES_LIST: '[data-region="messages-container"] [data-region="list"]',
59 MESSAGES_CONTAINER: '[data-region="messages-container"]',
60 NON_CONTACTS_CONTAINER: '[data-region="non-contacts-container"]',
61 NON_CONTACTS_LIST: '[data-region="non-contacts-container"] [data-region="list"]',
62 SEARCH_ICON_CONTAINER: '[data-region="search-icon-container"]',
63 SEARCH_ACTION: '[data-action="search"]',
64 SEARCH_INPUT: '[data-region="search-input"]',
65 SEARCH_RESULTS_CONTAINER: '[data-region="search-results-container"]',
66 LOAD_MORE_USERS: '[data-action="load-more-users"]',
67 LOAD_MORE_MESSAGES: '[data-action="load-more-messages"]',
68 BUTTON_TEXT: '[data-region="button-text"]',
69 NO_RESULTS_CONTAINTER: '[data-region="no-results-container"]',
73 CONTACTS_LIST: 'core_message/message_drawer_contacts_list',
74 NON_CONTACTS_LIST: 'core_message/message_drawer_non_contacts_list',
75 MESSAGES_LIST: 'core_message/message_drawer_messages_list'
79 * Get the logged in user id.
81 * @param {Object} body Search body container element.
82 * @return {Number} User id.
84 var getLoggedInUserId = function(body) {
85 return body.attr('data-user-id');
89 * Show the no messages container element.
91 * @param {Object} body Search body container element.
92 * @return {Object} No messages container element.
94 var getEmptyMessageContainer = function(body) {
95 return body.find(SELECTORS.EMPTY_MESSAGE_CONTAINER);
99 * Get the search loading icon.
101 * @param {Object} header Search header container element.
102 * @return {Object} Loading icon element.
104 var getLoadingIconContainer = function(header) {
105 return header.find(SELECTORS.LOADING_ICON_CONTAINER);
109 * Get the loading container element.
111 * @param {Object} body Search body container element.
112 * @return {Object} Loading container element.
114 var getLoadingPlaceholder = function(body) {
115 return body.find(SELECTORS.LOADING_PLACEHOLDER);
119 * Get the search icon container.
121 * @param {Object} header Search header container element.
122 * @return {Object} Search icon container.
124 var getSearchIconContainer = function(header) {
125 return header.find(SELECTORS.SEARCH_ICON_CONTAINER);
129 * Get the search input container.
131 * @param {Object} header Search header container element.
132 * @return {Object} Search input container.
134 var getSearchInput = function(header) {
135 return header.find(SELECTORS.SEARCH_INPUT);
139 * Get the search results container.
141 * @param {Object} body Search body container element.
142 * @return {Object} Search results container.
144 var getSearchResultsContainer = function(body) {
145 return body.find(SELECTORS.SEARCH_RESULTS_CONTAINER);
149 * Get the search contacts container.
151 * @param {Object} body Search body container element.
152 * @return {Object} Search contacts container.
154 var getContactsContainer = function(body) {
155 return body.find(SELECTORS.CONTACTS_CONTAINER);
159 * Get the search non contacts container.
161 * @param {Object} body Search body container element.
162 * @return {Object} Search non contacts container.
164 var getNonContactsContainer = function(body) {
165 return body.find(SELECTORS.NON_CONTACTS_CONTAINER);
169 * Get the search messages container.
171 * @param {Object} body Search body container element.
172 * @return {Object} Search messages container.
174 var getMessagesContainer = function(body) {
175 return body.find(SELECTORS.MESSAGES_CONTAINER);
180 * Show the messages empty container.
182 * @param {Object} body Search body container element.
184 var showEmptyMessage = function(body) {
185 getEmptyMessageContainer(body).removeClass('hidden');
189 * Hide the messages empty container.
191 * @param {Object} body Search body container element.
193 var hideEmptyMessage = function(body) {
194 getEmptyMessageContainer(body).addClass('hidden');
199 * Show the loading icon.
201 * @param {Object} header Search header container element.
203 var showLoadingIcon = function(header) {
204 getLoadingIconContainer(header).removeClass('hidden');
208 * Hide the loading icon.
210 * @param {Object} header Search header container element.
212 var hideLoadingIcon = function(header) {
213 getLoadingIconContainer(header).addClass('hidden');
217 * Show loading placeholder.
219 * @param {Object} body Search body container element.
221 var showLoadingPlaceholder = function(body) {
222 getLoadingPlaceholder(body).removeClass('hidden');
226 * Hide loading placeholder.
228 * @param {Object} body Search body container element.
230 var hideLoadingPlaceholder = function(body) {
231 getLoadingPlaceholder(body).addClass('hidden');
237 * @param {Object} header Search header container element.
239 var showSearchIcon = function(header) {
240 getSearchIconContainer(header).removeClass('hidden');
246 * @param {Object} header Search header container element.
248 var hideSearchIcon = function(header) {
249 getSearchIconContainer(header).addClass('hidden');
253 * Show search results.
255 * @param {Object} body Search body container element.
257 var showSearchResults = function(body) {
258 getSearchResultsContainer(body).removeClass('hidden');
262 * Hide search results.
264 * @param {Object} body Search body container element.
266 var hideSearchResults = function(body) {
267 getSearchResultsContainer(body).addClass('hidden');
271 * Disable the search input.
273 * @param {Object} header Search header container element.
275 var disableSearchInput = function(header) {
276 getSearchInput(header).prop('disabled', true);
280 * Enable the search input.
282 * @param {Object} header Search header container element.
284 var enableSearchInput = function(header) {
285 getSearchInput(header).prop('disabled', false);
289 * Clear the search input.
291 * @param {Object} header Search header container element.
293 var clearSearchInput = function(header) {
294 getSearchInput(header).val('');
298 * Clear all search results
300 * @param {Object} body Search body container element.
302 var clearAllSearchResults = function(body) {
303 body.find(SELECTORS.CONTACTS_LIST).empty();
304 body.find(SELECTORS.NON_CONTACTS_LIST).empty();
305 body.find(SELECTORS.MESSAGES_LIST).empty();
306 body.find(SELECTORS.NO_RESULTS_CONTAINTER).addClass('hidden');
307 showLoadMoreUsersButton(body);
308 showLoadMoreMessagesButton(body);
312 * Update the body and header to indicate the search is loading.
314 * @param {Object} header Search header container element.
315 * @param {Object} body Search body container element.
317 var startLoading = function(header, body) {
318 hideSearchIcon(header);
319 hideEmptyMessage(body);
320 hideSearchResults(body);
321 showLoadingIcon(header);
322 showLoadingPlaceholder(body);
323 disableSearchInput(header);
327 * Update the body and header to indicate the search has stopped loading.
329 * @param {Object} header Search header container element.
330 * @param {Object} body Search body container element.
332 var stopLoading = function(header, body) {
333 showSearchIcon(header);
334 hideEmptyMessage(body);
335 showSearchResults(body);
336 hideLoadingIcon(header);
337 hideLoadingPlaceholder(body);
338 enableSearchInput(header);
342 * Show the more users loading icon.
344 * @param {Object} root The more users container element.
346 var showUsersLoadingIcon = function(root) {
347 var button = root.find(SELECTORS.LOAD_MORE_USERS);
348 button.prop('disabled', true);
349 button.find(SELECTORS.BUTTON_TEXT).addClass('hidden');
350 button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
354 * Hide the more users loading icon.
356 * @param {Object} root The more users container element.
358 var hideUsersLoadingIcon = function(root) {
359 var button = root.find(SELECTORS.LOAD_MORE_USERS);
360 button.prop('disabled', false);
361 button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden');
362 button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
366 * Show the load more users button.
368 * @param {Object} root The users container element.
370 var showLoadMoreUsersButton = function(root) {
371 root.find(SELECTORS.LOAD_MORE_USERS).removeClass('hidden');
375 * Hide the load more users button.
377 * @param {Object} root The users container element.
379 var hideLoadMoreUsersButton = function(root) {
380 root.find(SELECTORS.LOAD_MORE_USERS).addClass('hidden');
384 * Show the messages are loading icon.
386 * @param {Object} root Messages root element.
388 var showMessagesLoadingIcon = function(root) {
389 var button = root.find(SELECTORS.LOAD_MORE_MESSAGES);
390 button.prop('disabled', true);
391 button.find(SELECTORS.BUTTON_TEXT).addClass('hidden');
392 button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
396 * Hide the messages are loading icon.
398 * @param {Object} root Messages root element.
400 var hideMessagesLoadingIcon = function(root) {
401 var button = root.find(SELECTORS.LOAD_MORE_MESSAGES);
402 button.prop('disabled', false);
403 button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden');
404 button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
408 * Show the load more messages button.
410 * @param {Object} root The messages container element.
412 var showLoadMoreMessagesButton = function(root) {
413 root.find(SELECTORS.LOAD_MORE_MESSAGES).removeClass('hidden');
417 * Hide the load more messages button.
419 * @param {Object} root The messages container element.
421 var hideLoadMoreMessagesButton = function(root) {
422 root.find(SELECTORS.LOAD_MORE_MESSAGES).addClass('hidden');
426 * Find a contact in the search results.
428 * @param {Object} root Search results container element.
429 * @param {Number} userId User id.
430 * @return {Object} User container element.
432 var findContact = function(root, userId) {
433 return root.find('[data-contact-user-id="' + userId + '"]');
437 * Add a contact to the search results.
439 * @param {Object} root Search results container.
440 * @param {Object} contact User in contacts list.
442 var addContact = function(root, contact) {
443 var nonContactsContainer = getNonContactsContainer(root);
444 var nonContact = findContact(nonContactsContainer, contact.userid);
446 if (nonContact.length) {
448 var contactsContainer = getContactsContainer(root);
449 contactsContainer.removeClass('hidden');
450 contactsContainer.find(SELECTORS.LIST).append(nonContact);
453 if (!nonContactsContainer.find(SELECTORS.LIST).children().length) {
454 nonContactsContainer.addClass('hidden');
459 * Remove a contact from the contacts results.
461 * @param {Object} root Search results container.
462 * @param {Object} userId Contact user id.
464 var removeContact = function(root, userId) {
465 var contactsContainer = getContactsContainer(root);
466 var contact = findContact(contactsContainer, userId);
468 if (contact.length) {
470 var nonContactsContainer = getNonContactsContainer(root);
471 nonContactsContainer.removeClass('hidden');
472 nonContactsContainer.find(SELECTORS.LIST).append(contact);
475 if (!contactsContainer.find(SELECTORS.LIST).children().length) {
476 contactsContainer.addClass('hidden');
481 * Show the contact is blocked icon.
483 * @param {Object} root Search results container.
484 * @param {Object} userId Contact user id.
486 var blockContact = function(root, userId) {
487 var contact = findContact(root, userId);
488 if (contact.length) {
489 contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden');
494 * Hide the contact is blocked icon.
496 * @param {Object} root Search results container.
497 * @param {Object} userId Contact user id.
499 var unblockContact = function(root, userId) {
500 var contact = findContact(root, userId);
501 if (contact.length) {
502 contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden');
507 * Render contacts in the contacts search results.
509 * @param {Object} root Search results container.
510 * @param {Array} contacts List of contacts.
511 * @return {Promise} Renderer promise.
513 var renderContacts = function(root, contacts) {
514 var container = getContactsContainer(root);
515 var list = container.find(SELECTORS.LIST);
517 if (!contacts.length && !list.children().length) {
518 var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
519 noResultsContainer.removeClass('hidden');
520 return $.Deferred().resolve('').promise();
522 return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts})
523 .then(function(html) {
531 * Render non contacts in the contacts search results.
533 * @param {Object} root Search results container.
534 * @param {Array} nonContacts List of non contacts.
535 * @return {Promise} Renderer promise.
537 var renderNonContacts = function(root, nonContacts) {
538 var container = getNonContactsContainer(root);
539 var list = container.find(SELECTORS.LIST);
541 if (!nonContacts.length && !list.children().length) {
542 var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
543 noResultsContainer.removeClass('hidden');
544 return $.Deferred().resolve('').promise();
546 return Templates.render(TEMPLATES.NON_CONTACTS_LIST, {noncontacts: nonContacts})
547 .then(function(html) {
555 * Render messages in the messages search results.
557 * @param {Object} root Search results container.
558 * @param {Array} messages List of messages.
559 * @return {Promise} Renderer promise.
561 var renderMessages = function(root, messages) {
562 var container = getMessagesContainer(root);
563 var list = container.find(SELECTORS.LIST);
565 if (!messages.length && !list.children().length) {
566 var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
567 noResultsContainer.removeClass('hidden');
568 return $.Deferred().resolve('').promise();
570 return Templates.render(TEMPLATES.MESSAGES_LIST, {messages: messages})
571 .then(function(html) {
579 * Load more users from the repository and render the results into the users search results.
581 * @param {Object} root Search results container.
582 * @param {Number} loggedInUserId Current logged in user.
583 * @param {String} text Search text.
584 * @param {Number} limit Number of users to get.
585 * @param {Number} offset Load users from
586 * @return {Object} jQuery promise
588 var loadMoreUsers = function(root, loggedInUserId, text, limit, offset) {
589 var loadedAll = false;
590 showUsersLoadingIcon(root);
591 return Repository.searchUsers(loggedInUserId, text, limit + 1, offset)
592 .then(function(results) {
593 var contacts = results.contacts;
594 var noncontacts = results.noncontacts;
596 if (contacts.length <= limit && noncontacts.length <= limit) {
600 noncontacts: noncontacts
604 contacts: contacts.slice(0, limit),
605 noncontacts: noncontacts.slice(0, limit)
609 .then(function(results) {
611 renderContacts(root, results.contacts),
612 renderNonContacts(root, results.noncontacts)
616 hideUsersLoadingIcon(root);
619 hideLoadMoreUsersButton(root);
624 .catch(function(error) {
625 hideUsersLoadingIcon(root);
626 // Rethrow error for other handlers.
632 * Load more messages from the repository and render the results into the messages search results.
634 * @param {Object} root Search results container.
635 * @param {Number} loggedInUserId Current logged in user.
636 * @param {String} text Search text.
637 * @param {Number} limit Number of messages to get.
638 * @param {Number} offset Load messages from
639 * @return {Object} jQuery promise
641 var loadMoreMessages = function(root, loggedInUserId, text, limit, offset) {
642 var loadedAll = false;
643 showMessagesLoadingIcon(root);
644 return Repository.searchMessages(loggedInUserId, text, limit + 1, offset)
645 .then(function(results) {
646 var messages = results.contacts;
648 if (messages.length <= limit) {
652 return messages.slice(0, limit);
655 .then(function(messages) {
656 return renderMessages(root, messages);
659 hideMessagesLoadingIcon(root);
662 hideLoadMoreMessagesButton(root);
667 .catch(function(error) {
668 hideMessagesLoadingIcon(root);
669 // Rethrow error for other handlers.
675 * Search for users and messages.
677 * @param {Object} header Search header container element.
678 * @param {Object} body Search body container element.
679 * @param {String} searchText Search text.
680 * @param {Number} usersLimit The users limit.
681 * @param {Number} usersOffset The users offset.
682 * @param {Number} messagesLimit The message limit.
683 * @param {Number} messagesOffset The message offset.
684 * @return {Object} jQuery promise
686 var search = function(header, body, searchText, usersLimit, usersOffset, messagesLimit, messagesOffset) {
687 var loggedInUserId = getLoggedInUserId(body);
688 startLoading(header, body);
689 clearAllSearchResults(body);
692 loadMoreUsers(body, loggedInUserId, searchText, usersLimit, usersOffset),
693 loadMoreMessages(body, loggedInUserId, searchText, messagesLimit, messagesOffset)
696 stopLoading(header, body);
703 * Listen to and handle events for searching.
705 * @param {Object} header Search header container element.
706 * @param {Object} body Search body container element.
708 var registerEventListeners = function(header, body) {
709 var loggedInUserId = getLoggedInUserId(body);
710 var searchInput = getSearchInput(header);
712 var messagesOffset = 0;
715 var searchEventHandler = function(e, data) {
716 searchText = searchInput.val().trim();
718 if (searchText !== '') {
725 USERS_INITIAL_SEARCH_LIMIT,
727 MESSAGE_SEARCH_LIMIT,
732 usersOffset = usersOffset + USERS_INITIAL_SEARCH_LIMIT;
733 messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT;
736 .catch(Notification.exception);
739 data.originalEvent.preventDefault();
742 CustomEvents.define(searchInput, [CustomEvents.events.enter]);
743 CustomEvents.define(header, [CustomEvents.events.activate]);
744 CustomEvents.define(body, [CustomEvents.events.activate]);
746 searchInput.on(CustomEvents.events.enter, searchEventHandler);
748 header.on(CustomEvents.events.activate, SELECTORS.SEARCH_ACTION, searchEventHandler);
750 body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_MESSAGES, function(e, data) {
751 if (searchText !== '') {
752 loadMoreMessages(body, loggedInUserId, searchText, MESSAGE_SEARCH_LIMIT, messagesOffset)
754 messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT;
757 .catch(Notification.exception);
759 data.originalEvent.preventDefault();
762 body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_USERS, function(e, data) {
763 if (searchText !== '') {
764 loadMoreUsers(body, loggedInUserId, searchText, USERS_SEARCH_LIMIT, usersOffset)
766 usersOffset = usersOffset + USERS_SEARCH_LIMIT;
769 .catch(Notification.exception);
771 data.originalEvent.preventDefault();
774 header.on(CustomEvents.events.activate, SELECTORS.CANCEL_SEARCH_BUTTON, function() {
775 clearSearchInput(header);
776 showEmptyMessage(body);
777 showSearchIcon(header);
778 hideSearchResults(body);
779 hideLoadingIcon(header);
780 hideLoadingPlaceholder(body);
785 PubSub.subscribe(Events.CONTACT_ADDED, function(userId) {
786 addContact(body, userId);
789 PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) {
790 removeContact(body, userId);
793 PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) {
794 blockContact(body, userId);
797 PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) {
798 unblockContact(body, userId);
803 * Setup the search page.
805 * @param {Object} header Contacts header container element.
806 * @param {Object} body Contacts body container element.
807 * @return {Object} jQuery promise
809 var show = function(header, body) {
810 if (!body.attr('data-init')) {
811 registerEventListeners(header, body);
812 body.attr('data-init', true);
815 var searchInput = getSearchInput(header);
818 return $.Deferred().resolve().promise();
822 * String describing this page used for aria-labels.
824 * @param {Object} header Contacts header container element.
825 * @return {Object} jQuery promise
827 var description = function(header) {
828 var searchInput = getSearchInput(header);
829 var searchText = searchInput.val().trim();
830 return Str.get_string('messagedrawerviewsearch', 'core_message', searchText);
835 description: description