diff options
author | Christoph Wurst <christoph@winzerhof-wurst.at> | 2017-01-24 07:47:14 +0100 |
---|---|---|
committer | Christoph Wurst <christoph@winzerhof-wurst.at> | 2017-04-25 20:47:17 +0200 |
commit | d091793ceb1ab2a60133608e844e1d83a5de19f2 (patch) | |
tree | 12c4c5863c5ede094a78c534e03d6718823abb04 /core/js/contactsmenu.js | |
parent | db94b5d4af711f6e18aac0c9d4b0357a3b9123d1 (diff) | |
download | nextcloud-server-d091793ceb1ab2a60133608e844e1d83a5de19f2.tar.gz nextcloud-server-d091793ceb1ab2a60133608e844e1d83a5de19f2.zip |
Contacts menu
* load list of contacts from the server
* show last message of each contact
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Diffstat (limited to 'core/js/contactsmenu.js')
-rw-r--r-- | core/js/contactsmenu.js | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/core/js/contactsmenu.js b/core/js/contactsmenu.js new file mode 100644 index 00000000000..2991a0e84d1 --- /dev/null +++ b/core/js/contactsmenu.js @@ -0,0 +1,515 @@ +/* global OC.Backbone, Handlebars, Promise, _ */ + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +(function(OC, $, _, Handlebars) { + 'use strict'; + + var MENU_TEMPLATE = '' + + '<input id="contactsmenu-search" type="search" placeholder="Search contacts …" value="{{searchTerm}}">' + + '<div class="content">' + + '</div>'; + var CONTACTS_LIST_TEMPLATE = '' + + '{{#unless contacts.length}}' + + '<div class="emptycontent">' + + ' <a class="icon-search"></a>' + + ' <h2>' + t('core', 'No contacts found') + '</h2>' + + '</div>' + + '{{/unless}}' + + '<div id="contactsmenu-contacts"></div>' + + '{{#if contactsAppEnabled}}<div class="footer"><a href="{{contactsAppURL}}">' + t('core', 'Show all contacts …') + '</a></div>{{/if}}'; + var LOADING_TEMPLATE = '' + + '<div class="emptycontent">' + + ' <a class="icon-loading"></a>' + + ' <h2>{{loadingText}}</h2>' + + '</div>'; + var ERROR_TEMPLATE = '' + + '<div class="emptycontent">' + + ' <h2>' + t('core', 'Could not load your contacts.') + '</h2>' + + '</div>'; + var CONTACT_TEMPLATE = '' + + '{{#if contact.avatar}}' + + '<img src="{{contact.avatar}}" class="avatar">' + + '{{else}}' + + '<div class="avatar"></div>' + + '{{/if}}' + + '<div class="body">' + + ' <div class="full-name">{{contact.fullName}}</div>' + + ' <div class="last-message">{{contact.lastMessage}}</div>' + + '</div>' + + '{{#if contact.topAction}}' + + '<a class="top-action {{contact.topAction.icon}}" href="{{contact.topAction.hyperlink}}"></a>' + + '{{/if}}' + + '{{#if contact.hasManyActions}}' + + ' <span class="other-actions icon-more"></span>' + + ' <div class="menu popovermenu">' + + ' <ul>' + + ' {{#each contact.actions}}' + + ' <li>' + + ' <a href="{{hyperlink}}">' + + ' <span class="{{icon}}"></span>' + + ' <span>{{title}}</span>' + + ' </a>' + + ' </li>' + + ' {{/each}}' + + ' </ul>' + + ' </div>' + + '{{/if}}' + + '{{#if contact.hasTwoActions}}' + + '<a class="second-action {{contact.secondAction.icon}}" href="{{contact.secondAction.hyperlink}}"></a>' + + '{{/if}}'; + + /** + * @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) { + if (!this._template) { + this._template = Handlebars.compile(CONTACT_TEMPLATE); + } + return this._template(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 iff no avatar is available (avatar is rendered as img, not div) + this.$('div.avatar').imageplaceholder(this._model.get('fullName')); + + 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, + + events: { + 'input #contactsmenu-search': '_onSearch' + }, + + /** + * @returns {undefined} + */ + _onSearch: _.debounce(function() { + this.trigger('search', this.$('#contactsmenu-search').val()); + }, 700), + + /** + * @param {object} data + * @returns {string} + */ + loadingTemplate: function(data) { + if (!this._loadingTemplate) { + this._loadingTemplate = Handlebars.compile(LOADING_TEMPLATE); + } + return this._loadingTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + errorTemplate: function(data) { + if (!this._errorTemplate) { + this._errorTemplate = Handlebars.compile(ERROR_TEMPLATE); + } + return this._errorTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + contentTemplate: function(data) { + if (!this._contentTemplate) { + this._contentTemplate = Handlebars.compile(MENU_TEMPLATE); + } + return this._contentTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + contactsTemplate: function(data) { + if (!this._contactsTemplate) { + this._contactsTemplate = Handlebars.compile(CONTACTS_LIST_TEMPLATE); + } + return this._contactsTemplate(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)); + 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: 'GET', + 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('could not load 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); |