/* global OC.Backbone, Handlebars, Promise, _ */ /** * @copyright 2017 Christoph Wurst * * @author 2017 Christoph Wurst * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ (function(OC, $, _, Handlebars) { 'use strict'; /** * @class Contact */ var Contact = OC.Backbone.Model.extend({ defaults: { fullName: '', lastMessage: '', actions: [], hasOneAction: false, hasTwoActions: false, hasManyActions: false }, /** * @returns {undefined} */ initialize: function() { // Add needed property for easier template rendering if (this.get('actions').length === 0) { this.set('hasOneAction', true); } else if (this.get('actions').length === 1) { this.set('hasTwoActions', true); this.set('secondAction', this.get('actions')[0]); } else { this.set('hasManyActions', true); } } }); /** * @class ContactCollection */ var ContactCollection = OC.Backbone.Collection.extend({ model: Contact }); /** * @class ContactsListView */ var ContactsListView = OC.Backbone.View.extend({ /** @type {ContactsCollection} */ _collection: undefined, /** @type {array} */ _subViews: [], /** * @param {object} options * @returns {undefined} */ initialize: function(options) { this._collection = options.collection; }, /** * @returns {self} */ render: function() { var self = this; self.$el.html(''); self._subViews = []; self._collection.forEach(function(contact) { var item = new ContactsListItemView({ model: contact }); item.render(); self.$el.append(item.$el); item.on('toggle:actionmenu', self._onChildActionMenuToggle, self); self._subViews.push(item); }); return self; }, /** * Event callback to propagate opening (another) entry's action menu * * @param {type} $src * @returns {undefined} */ _onChildActionMenuToggle: function($src) { this._subViews.forEach(function(view) { view.trigger('parent:toggle:actionmenu', $src); }); } }); /** * @class CotnactsListItemView */ var ContactsListItemView = OC.Backbone.View.extend({ /** @type {string} */ className: 'contact', /** @type {undefined|function} */ _template: undefined, /** @type {Contact} */ _model: undefined, /** @type {boolean} */ _actionMenuShown: false, events: { 'click .icon-more': '_onToggleActionsMenu' }, /** * @param {object} data * @returns {undefined} */ template: function(data) { return OC.ContactsMenu.Templates['contact'](data); }, /** * @param {object} options * @returns {undefined} */ initialize: function(options) { this._model = options.model; this.on('parent:toggle:actionmenu', this._onOtherActionMenuOpened, this); }, /** * @returns {self} */ render: function() { this.$el.html(this.template({ contact: this._model.toJSON() })); this.delegateEvents(); // Show placeholder if no avatar is available (avatar is rendered as img, not div) this.$('div.avatar').imageplaceholder(this._model.get('fullName')); // Show tooltip for top action this.$('.top-action').tooltip({placement: 'left'}); // Show tooltip for second action this.$('.second-action').tooltip({placement: 'left'}); return this; }, /** * Toggle the visibility of the action popover menu * * @private * @returns {undefined} */ _onToggleActionsMenu: function() { this._actionMenuShown = !this._actionMenuShown; if (this._actionMenuShown) { this.$('.menu').show(); } else { this.$('.menu').hide(); } this.trigger('toggle:actionmenu', this.$el); }, /** * @private * @argument {jQuery} $src * @returns {undefined} */ _onOtherActionMenuOpened: function($src) { if (this.$el.is($src)) { // Ignore return; } this._actionMenuShown = false; this.$('.menu').hide(); } }); /** * @class ContactsMenuView */ var ContactsMenuView = OC.Backbone.View.extend({ /** @type {undefined|function} */ _loadingTemplate: undefined, /** @type {undefined|function} */ _errorTemplate: undefined, /** @type {undefined|function} */ _contentTemplate: undefined, /** @type {undefined|function} */ _contactsTemplate: undefined, /** @type {undefined|ContactCollection} */ _contacts: undefined, /** @type {string} */ _searchTerm: '', events: { 'input #contactsmenu-search': '_onSearch' }, /** * @returns {undefined} */ _onSearch: _.debounce(function(e) { var searchTerm = this.$('#contactsmenu-search').val(); // IE11 triggers an 'input' event after the view has been rendered // resulting in an endless loading loop. To prevent this, we remember // the last search term to savely ignore some events // See https://github.com/nextcloud/server/issues/5281 if (searchTerm !== this._searchTerm) { this.trigger('search', this.$('#contactsmenu-search').val()); this._searchTerm = searchTerm; } }, 700), /** * @param {object} data * @returns {string} */ loadingTemplate: function(data) { return OC.ContactsMenu.Templates['loading'](data); }, /** * @param {object} data * @returns {string} */ errorTemplate: function(data) { return OC.ContactsMenu.Templates['error']( _.extend({ couldNotLoadText: t('core', 'Could not load your contacts') }, data) ); }, /** * @param {object} data * @returns {string} */ contentTemplate: function(data) { return OC.ContactsMenu.Templates['menu']( _.extend({ searchContactsText: t('core', 'Search contacts …') }, data) ); }, /** * @param {object} data * @returns {string} */ contactsTemplate: function(data) { return OC.ContactsMenu.Templates['list']( _.extend({ noContactsFoundText: t('core', 'No contacts found'), showAllContactsText: t('core', 'Show all contacts …') }, data) ); }, /** * @param {object} options * @returns {undefined} */ initialize: function(options) { this.options = options; }, /** * @param {string} text * @returns {undefined} */ showLoading: function(text) { this.render(); this._contacts = undefined; this.$('.content').html(this.loadingTemplate({ loadingText: text })); }, /** * @returns {undefined} */ showError: function() { this.render(); this._contacts = undefined; this.$('.content').html(this.errorTemplate()); }, /** * @param {object} viewData * @param {string} searchTerm * @returns {undefined} */ showContacts: function(viewData, searchTerm) { this._contacts = viewData.contacts; this.render({ contacts: viewData.contacts }); var list = new ContactsListView({ collection: viewData.contacts }); list.render(); this.$('.content').html(this.contactsTemplate({ contacts: viewData.contacts, searchTerm: searchTerm, contactsAppEnabled: viewData.contactsAppEnabled, contactsAppURL: OC.generateUrl('/apps/contacts') })); this.$('#contactsmenu-contacts').html(list.$el); }, /** * @param {object} data * @returns {self} */ render: function(data) { var searchVal = this.$('#contactsmenu-search').val(); this.$el.html(this.contentTemplate(data)); // Focus search this.$('#contactsmenu-search').val(searchVal); this.$('#contactsmenu-search').focus(); return this; } }); /** * @param {Object} options * @param {jQuery} options.el * @param {jQuery} options.trigger * @class ContactsMenu */ var ContactsMenu = function(options) { this.initialize(options); }; ContactsMenu.prototype = { /** @type {jQuery} */ $el: undefined, /** @type {jQuery} */ _$trigger: undefined, /** @type {ContactsMenuView} */ _view: undefined, /** @type {Promise} */ _contactsPromise: undefined, /** * @param {Object} options * @param {jQuery} options.el - the element to render the menu in * @param {jQuery} options.trigger - the element to click on to open the menu * @returns {undefined} */ initialize: function(options) { this.$el = options.el; this._$trigger = options.trigger; this._view = new ContactsMenuView({ el: this.$el }); this._view.on('search', function(searchTerm) { this._loadContacts(searchTerm); }, this); OC.registerMenu(this._$trigger, this.$el, function() { this._toggleVisibility(true); }.bind(this), true); this.$el.on('beforeHide', function() { this._toggleVisibility(false); }.bind(this)); }, /** * @private * @param {boolean} show * @returns {Promise} */ _toggleVisibility: function(show) { if (show) { return this._loadContacts(); } else { this.$el.html(''); return Promise.resolve(); } }, /** * @private * @param {string|undefined} searchTerm * @returns {Promise} */ _getContacts: function(searchTerm) { var url = OC.generateUrl('/contactsmenu/contacts'); return Promise.resolve($.ajax(url, { method: 'POST', data: { filter: searchTerm } })); }, /** * @param {string|undefined} searchTerm * @returns {undefined} */ _loadContacts: function(searchTerm) { var self = this; if (!self._contactsPromise) { self._contactsPromise = self._getContacts(searchTerm); } if (_.isUndefined(searchTerm) || searchTerm === '') { self._view.showLoading(t('core', 'Loading your contacts …')); } else { self._view.showLoading(t('core', 'Looking for {term} …', { term: searchTerm })); } return self._contactsPromise.then(function(data) { // Convert contact entries to Backbone collection data.contacts = new ContactCollection(data.contacts); self._view.showContacts(data, searchTerm); }, function(e) { self._view.showError(); console.error('There was an error loading your contacts', e); }).then(function() { // Delete promise, so that contacts are fetched again when the // menu is opened the next time. delete self._contactsPromise; }).catch(console.error.bind(this)); } }; OC.ContactsMenu = ContactsMenu; })(OC, $, _, Handlebars);