nextcloud-server/core/src/OC/contactsmenu.js

479 lines
10 KiB
JavaScript

/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0-or-later
*
* 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/>.
*
*/
/* eslint-disable */
import _ from 'underscore'
import $ from 'jquery'
import { Collection, Model, View } from 'backbone'
import OC from './index.js'
/**
* @class Contact
*/
const Contact = 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)
}
const fullName = this.get('fullName')
if (this.get('avatar') && fullName) {
this.set('avatarLabel', t('core', 'Avatar of {fullName}', { fullName }))
}
}
})
/**
* @class ContactCollection
* @private
*/
const ContactCollection = Collection.extend({
model: Contact
})
/**
* @class ContactsListView
* @private
*/
const ContactsListView = View.extend({
/** @type {ContactCollection} */
_collection: undefined,
/** @type {array} */
_subViews: [],
/** @type {string} */
tagName: 'ul',
/**
* @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 ContactsListItemView
* @private
*/
const ContactsListItemView = View.extend({
/** @type {string} */
className: 'contact',
/** @type {string} */
tagName: 'li',
/** @type {undefined|function} */
_template: undefined,
/** @type {Contact} */
_model: undefined,
/** @type {boolean} */
_actionMenuShown: false,
events: {
'click .icon-more': '_onToggleActionsMenu'
},
contactTemplate: require('./contactsmenu/contact.handlebars'),
/**
* @param {object} data
* @returns {undefined}
*/
template: function(data) {
return this.contactTemplate(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
* @private
*/
const ContactsMenuView = 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'
},
templates: {
loading: require('./contactsmenu/loading.handlebars'),
error: require('./contactsmenu/error.handlebars'),
menu: require('./contactsmenu/menu.handlebars'),
list: require('./contactsmenu/list.handlebars')
},
/**
* @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 this.templates.loading(data)
},
/**
* @param {object} data
* @returns {string}
*/
errorTemplate: function(data) {
return this.templates.error(
_.extend({
couldNotLoadText: t('core', 'Could not load your contacts')
}, data)
)
},
/**
* @param {object} data
* @returns {string}
*/
contentTemplate: function(data) {
return this.templates.menu(
_.extend({
searchContactsText: t('core', 'Search contacts …')
}, data)
)
},
/**
* @param {object} data
* @returns {string}
*/
contactsTemplate: function(data) {
return this.templates.list(
_.extend({
noContactsFoundText: t('core', 'No contacts found'),
showAllContactsText: t('core', 'Show all contacts …'),
contactsAppMgmtText: t('core', 'Install the Contacts app')
}, 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'),
canInstallApp: OC.isUserAdmin(),
contactsAppMgmtURL: OC.generateUrl('/settings/apps/social/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 {string} options.el
* @class ContactsMenu
* @memberOf OC
*/
const ContactsMenu = function(options) {
this.initialize(options)
}
ContactsMenu.prototype = {
/** @type {string} */
$el: undefined,
/** @type {ContactsMenuView} */
_view: undefined,
/** @type {Promise} */
_contactsPromise: undefined,
/**
* @param {Object} options
* @param {string} options.el - the selector of the element to render the menu in
* @returns {undefined}
*/
initialize: function(options) {
this.$el = $(options.el)
this._view = new ContactsMenuView({
el: this.$el,
})
this._view.on('search', function(searchTerm) {
this.loadContacts(searchTerm)
}, this)
},
/**
* @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))
}
}
export default ContactsMenu