Merge pull request #13870 from nextcloud/refactor/oc-contactsmenu-bundle
Move OC.Contactsmenu and OC.Backbone to the server bundle
This commit is contained in:
commit
ef94996fee
|
@ -1,484 +0,0 @@
|
|||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module OC.ContactsMenu
|
||||
* @private
|
||||
*/
|
||||
(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
|
||||
* @private
|
||||
*/
|
||||
var ContactCollection = OC.Backbone.Collection.extend({
|
||||
model: Contact
|
||||
});
|
||||
|
||||
/**
|
||||
* @class ContactsListView
|
||||
* @private
|
||||
*/
|
||||
var ContactsListView = OC.Backbone.View.extend({
|
||||
|
||||
/** @type {ContactCollection} */
|
||||
_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 ContactsListItemView
|
||||
* @private
|
||||
*/
|
||||
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
|
||||
* @private
|
||||
*/
|
||||
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
|
||||
* @memberOf OC
|
||||
*/
|
||||
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);
|
|
@ -9,8 +9,6 @@
|
|||
"jquery.ocdialog.js",
|
||||
"oc-dialogs.js",
|
||||
"js.js",
|
||||
"oc-backbone.js",
|
||||
"oc-backbone-webdav.js",
|
||||
"l10n.js",
|
||||
"share.js",
|
||||
"sharetemplates.js",
|
||||
|
@ -22,7 +20,6 @@
|
|||
"sharedialogresharerinfoview.js",
|
||||
"sharedialogshareelistview.js",
|
||||
"octemplate.js",
|
||||
"contactsmenu.js",
|
||||
"contactsmenu_templates.js",
|
||||
"eventsource.js",
|
||||
"config.js",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -12,7 +12,6 @@
|
|||
"oc-requesttoken.js",
|
||||
"mimetype.js",
|
||||
"mimetypelist.js",
|
||||
"oc-backbone.js",
|
||||
"select2-toggleselect.js",
|
||||
"placeholder.js",
|
||||
"jquery.avatar.js",
|
||||
|
|
|
@ -1,364 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Webdav transport for Backbone.
|
||||
*
|
||||
* This makes it possible to use Webdav endpoints when
|
||||
* working with Backbone models and collections.
|
||||
*
|
||||
* Requires the davclient.js library.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* var PersonModel = OC.Backbone.Model.extend({
|
||||
* // make it use the DAV transport
|
||||
* sync: OC.Backbone.davSync,
|
||||
*
|
||||
* // DAV properties mapping
|
||||
* davProperties: {
|
||||
* 'id': '{http://example.com/ns}id',
|
||||
* 'firstName': '{http://example.com/ns}first-name',
|
||||
* 'lastName': '{http://example.com/ns}last-name',
|
||||
* 'age': '{http://example.com/ns}age'
|
||||
* },
|
||||
*
|
||||
* // additional parsing, if needed
|
||||
* parse: function(props) {
|
||||
* // additional parsing (DAV property values are always strings)
|
||||
* props.age = parseInt(props.age, 10);
|
||||
* return props;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* var PersonCollection = OC.Backbone.Collection.extend({
|
||||
* // make it use the DAV transport
|
||||
* sync: OC.Backbone.davSync,
|
||||
*
|
||||
* // use person model
|
||||
* // note that davProperties will be inherited
|
||||
* model: PersonModel,
|
||||
*
|
||||
* // DAV collection URL
|
||||
* url: function() {
|
||||
* return OC.linkToRemote('dav') + '/person/';
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
/* global dav */
|
||||
|
||||
(function(Backbone) {
|
||||
var methodMap = {
|
||||
'create': 'POST',
|
||||
'update': 'PROPPATCH',
|
||||
'patch': 'PROPPATCH',
|
||||
'delete': 'DELETE',
|
||||
'read': 'PROPFIND'
|
||||
};
|
||||
|
||||
// Throw an error when a URL is needed, and none is supplied.
|
||||
function urlError() {
|
||||
throw new Error('A "url" property or function must be specified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single propfind result to JSON
|
||||
*
|
||||
* @param {Object} result
|
||||
* @param {Object} davProperties properties mapping
|
||||
*/
|
||||
function parsePropFindResult(result, davProperties) {
|
||||
if (_.isArray(result)) {
|
||||
return _.map(result, function(subResult) {
|
||||
return parsePropFindResult(subResult, davProperties);
|
||||
});
|
||||
}
|
||||
var props = {
|
||||
href: result.href
|
||||
};
|
||||
|
||||
_.each(result.propStat, function(propStat) {
|
||||
if (propStat.status !== 'HTTP/1.1 200 OK') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var key in propStat.properties) {
|
||||
var propKey = key;
|
||||
if (key in davProperties) {
|
||||
propKey = davProperties[key];
|
||||
}
|
||||
props[propKey] = propStat.properties[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (!props.id) {
|
||||
// parse id from href
|
||||
props.id = parseIdFromLocation(props.href);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ID from location
|
||||
*
|
||||
* @param {string} url url
|
||||
* @return {string} id
|
||||
*/
|
||||
function parseIdFromLocation(url) {
|
||||
var queryPos = url.indexOf('?');
|
||||
if (queryPos > 0) {
|
||||
url = url.substr(0, queryPos);
|
||||
}
|
||||
|
||||
var parts = url.split('/');
|
||||
var result;
|
||||
do {
|
||||
result = parts[parts.length - 1];
|
||||
parts.pop();
|
||||
// note: first result can be empty when there is a trailing slash,
|
||||
// so we take the part before that
|
||||
} while (!result && parts.length > 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isSuccessStatus(status) {
|
||||
return status >= 200 && status <= 299;
|
||||
}
|
||||
|
||||
function convertModelAttributesToDavProperties(attrs, davProperties) {
|
||||
var props = {};
|
||||
var key;
|
||||
for (key in attrs) {
|
||||
var changedProp = davProperties[key];
|
||||
var value = attrs[key];
|
||||
if (!changedProp) {
|
||||
console.warn('No matching DAV property for property "' + key);
|
||||
changedProp = key;
|
||||
}
|
||||
if (_.isBoolean(value) || _.isNumber(value)) {
|
||||
// convert to string
|
||||
value = '' + value;
|
||||
}
|
||||
props[changedProp] = value;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
function callPropFind(client, options, model, headers) {
|
||||
return client.propFind(
|
||||
options.url,
|
||||
_.values(options.davProperties) || [],
|
||||
options.depth,
|
||||
headers
|
||||
).then(function(response) {
|
||||
if (isSuccessStatus(response.status)) {
|
||||
if (_.isFunction(options.success)) {
|
||||
var propsMapping = _.invert(options.davProperties);
|
||||
var results = parsePropFindResult(response.body, propsMapping);
|
||||
if (options.depth > 0) {
|
||||
// discard root entry
|
||||
results.shift();
|
||||
}
|
||||
|
||||
options.success(results);
|
||||
return;
|
||||
}
|
||||
} else if (_.isFunction(options.error)) {
|
||||
options.error(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function callPropPatch(client, options, model, headers) {
|
||||
return client.propPatch(
|
||||
options.url,
|
||||
convertModelAttributesToDavProperties(model.changed, options.davProperties),
|
||||
headers
|
||||
).then(function(result) {
|
||||
if (isSuccessStatus(result.status)) {
|
||||
if (_.isFunction(options.success)) {
|
||||
// pass the object's own values because the server
|
||||
// does not return the updated model
|
||||
options.success(model.toJSON());
|
||||
}
|
||||
} else if (_.isFunction(options.error)) {
|
||||
options.error(result);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function callMkCol(client, options, model, headers) {
|
||||
// call MKCOL without data, followed by PROPPATCH
|
||||
return client.request(
|
||||
options.type,
|
||||
options.url,
|
||||
headers,
|
||||
null
|
||||
).then(function(result) {
|
||||
if (!isSuccessStatus(result.status)) {
|
||||
if (_.isFunction(options.error)) {
|
||||
options.error(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
callPropPatch(client, options, model, headers);
|
||||
});
|
||||
}
|
||||
|
||||
function callMethod(client, options, model, headers) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
return client.request(
|
||||
options.type,
|
||||
options.url,
|
||||
headers,
|
||||
options.data
|
||||
).then(function(result) {
|
||||
if (!isSuccessStatus(result.status)) {
|
||||
if (_.isFunction(options.error)) {
|
||||
options.error(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isFunction(options.success)) {
|
||||
if (options.type === 'PUT' || options.type === 'POST' || options.type === 'MKCOL') {
|
||||
// pass the object's own values because the server
|
||||
// does not return anything
|
||||
var responseJson = result.body || model.toJSON();
|
||||
var locationHeader = result.xhr.getResponseHeader('Content-Location');
|
||||
if (options.type === 'POST' && locationHeader) {
|
||||
responseJson.id = parseIdFromLocation(locationHeader);
|
||||
}
|
||||
options.success(responseJson);
|
||||
return;
|
||||
}
|
||||
// if multi-status, parse
|
||||
if (result.status === 207) {
|
||||
var propsMapping = _.invert(options.davProperties);
|
||||
options.success(parsePropFindResult(result.body, propsMapping));
|
||||
} else {
|
||||
options.success(result.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function davCall(options, model) {
|
||||
var client = new dav.Client({
|
||||
baseUrl: options.url,
|
||||
xmlNamespaces: _.extend({
|
||||
'DAV:': 'd',
|
||||
'http://owncloud.org/ns': 'oc'
|
||||
}, options.xmlNamespaces || {})
|
||||
});
|
||||
client.resolveUrl = function() {
|
||||
return options.url;
|
||||
};
|
||||
var headers = _.extend({
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'requesttoken': OC.requestToken
|
||||
}, options.headers);
|
||||
if (options.type === 'PROPFIND') {
|
||||
return callPropFind(client, options, model, headers);
|
||||
} else if (options.type === 'PROPPATCH') {
|
||||
return callPropPatch(client, options, model, headers);
|
||||
} else if (options.type === 'MKCOL') {
|
||||
return callMkCol(client, options, model, headers);
|
||||
} else {
|
||||
return callMethod(client, options, model, headers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DAV transport
|
||||
*/
|
||||
function davSync(method, model, options) {
|
||||
var params = {type: methodMap[method] || method};
|
||||
var isCollection = (model instanceof Backbone.Collection);
|
||||
|
||||
if (method === 'update') {
|
||||
// if a model has an inner collection, it must define an
|
||||
// attribute "hasInnerCollection" that evaluates to true
|
||||
if (model.hasInnerCollection) {
|
||||
// if the model itself is a Webdav collection, use MKCOL
|
||||
params.type = 'MKCOL';
|
||||
} else if (model.usePUT || (model.collection && model.collection.usePUT)) {
|
||||
// use PUT instead of PROPPATCH
|
||||
params.type = 'PUT';
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we have a URL.
|
||||
if (!options.url) {
|
||||
params.url = _.result(model, 'url') || urlError();
|
||||
}
|
||||
|
||||
// Ensure that we have the appropriate request data.
|
||||
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
||||
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
||||
}
|
||||
|
||||
// Don't process data on a non-GET request.
|
||||
if (params.type !== 'PROPFIND') {
|
||||
params.processData = false;
|
||||
}
|
||||
|
||||
if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') {
|
||||
var davProperties = model.davProperties;
|
||||
if (!davProperties && model.model) {
|
||||
// use dav properties from model in case of collection
|
||||
davProperties = model.model.prototype.davProperties;
|
||||
}
|
||||
if (davProperties) {
|
||||
if (_.isFunction(davProperties)) {
|
||||
params.davProperties = davProperties.call(model);
|
||||
} else {
|
||||
params.davProperties = davProperties;
|
||||
}
|
||||
}
|
||||
|
||||
params.davProperties = _.extend(params.davProperties || {}, options.davProperties);
|
||||
|
||||
if (_.isUndefined(options.depth)) {
|
||||
if (isCollection) {
|
||||
options.depth = 1;
|
||||
} else {
|
||||
options.depth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass along `textStatus` and `errorThrown` from jQuery.
|
||||
var error = options.error;
|
||||
options.error = function(xhr, textStatus, errorThrown) {
|
||||
options.textStatus = textStatus;
|
||||
options.errorThrown = errorThrown;
|
||||
if (error) {
|
||||
error.call(options.context, xhr, textStatus, errorThrown);
|
||||
}
|
||||
};
|
||||
|
||||
// Make the request, allowing the user to override any Ajax options.
|
||||
var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model);
|
||||
model.trigger('request', model, xhr, options);
|
||||
return xhr;
|
||||
}
|
||||
|
||||
// exports
|
||||
Backbone.davCall = davCall;
|
||||
Backbone.davSync = davSync;
|
||||
|
||||
})(OC.Backbone);
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
/* global Backbone */
|
||||
if(!_.isUndefined(Backbone)) {
|
||||
OC.Backbone = Backbone.noConflict();
|
||||
}
|
|
@ -0,0 +1,357 @@
|
|||
/*
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Webdav transport for Backbone.
|
||||
*
|
||||
* This makes it possible to use Webdav endpoints when
|
||||
* working with Backbone models and collections.
|
||||
*
|
||||
* Requires the davclient.js library.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* var PersonModel = OC.Backbone.Model.extend({
|
||||
* // make it use the DAV transport
|
||||
* sync: OC.Backbone.davSync,
|
||||
*
|
||||
* // DAV properties mapping
|
||||
* davProperties: {
|
||||
* 'id': '{http://example.com/ns}id',
|
||||
* 'firstName': '{http://example.com/ns}first-name',
|
||||
* 'lastName': '{http://example.com/ns}last-name',
|
||||
* 'age': '{http://example.com/ns}age'
|
||||
* },
|
||||
*
|
||||
* // additional parsing, if needed
|
||||
* parse: function(props) {
|
||||
* // additional parsing (DAV property values are always strings)
|
||||
* props.age = parseInt(props.age, 10);
|
||||
* return props;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* var PersonCollection = OC.Backbone.Collection.extend({
|
||||
* // make it use the DAV transport
|
||||
* sync: OC.Backbone.davSync,
|
||||
*
|
||||
* // use person model
|
||||
* // note that davProperties will be inherited
|
||||
* model: PersonModel,
|
||||
*
|
||||
* // DAV collection URL
|
||||
* url: function() {
|
||||
* return OC.linkToRemote('dav') + '/person/';
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
import _ from 'underscore';
|
||||
import dav from 'davclient.js';
|
||||
|
||||
const methodMap = {
|
||||
create: 'POST',
|
||||
update: 'PROPPATCH',
|
||||
patch: 'PROPPATCH',
|
||||
delete: 'DELETE',
|
||||
read: 'PROPFIND'
|
||||
};
|
||||
|
||||
// Throw an error when a URL is needed, and none is supplied.
|
||||
function urlError () {
|
||||
throw new Error('A "url" property or function must be specified');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single propfind result to JSON
|
||||
*
|
||||
* @param {Object} result
|
||||
* @param {Object} davProperties properties mapping
|
||||
*/
|
||||
function parsePropFindResult (result, davProperties) {
|
||||
if (_.isArray(result)) {
|
||||
return _.map(result, function (subResult) {
|
||||
return parsePropFindResult(subResult, davProperties);
|
||||
});
|
||||
}
|
||||
var props = {
|
||||
href: result.href
|
||||
};
|
||||
|
||||
_.each(result.propStat, function (propStat) {
|
||||
if (propStat.status !== 'HTTP/1.1 200 OK') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var key in propStat.properties) {
|
||||
var propKey = key;
|
||||
if (key in davProperties) {
|
||||
propKey = davProperties[key];
|
||||
}
|
||||
props[propKey] = propStat.properties[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (!props.id) {
|
||||
// parse id from href
|
||||
props.id = parseIdFromLocation(props.href);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ID from location
|
||||
*
|
||||
* @param {string} url url
|
||||
* @return {string} id
|
||||
*/
|
||||
function parseIdFromLocation (url) {
|
||||
var queryPos = url.indexOf('?');
|
||||
if (queryPos > 0) {
|
||||
url = url.substr(0, queryPos);
|
||||
}
|
||||
|
||||
var parts = url.split('/');
|
||||
var result;
|
||||
do {
|
||||
result = parts[parts.length - 1];
|
||||
parts.pop();
|
||||
// note: first result can be empty when there is a trailing slash,
|
||||
// so we take the part before that
|
||||
} while (!result && parts.length > 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isSuccessStatus (status) {
|
||||
return status >= 200 && status <= 299;
|
||||
}
|
||||
|
||||
function convertModelAttributesToDavProperties (attrs, davProperties) {
|
||||
var props = {};
|
||||
var key;
|
||||
for (key in attrs) {
|
||||
var changedProp = davProperties[key];
|
||||
var value = attrs[key];
|
||||
if (!changedProp) {
|
||||
console.warn('No matching DAV property for property "' + key);
|
||||
changedProp = key;
|
||||
}
|
||||
if (_.isBoolean(value) || _.isNumber(value)) {
|
||||
// convert to string
|
||||
value = '' + value;
|
||||
}
|
||||
props[changedProp] = value;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
function callPropFind (client, options, model, headers) {
|
||||
return client.propFind(
|
||||
options.url,
|
||||
_.values(options.davProperties) || [],
|
||||
options.depth,
|
||||
headers
|
||||
).then(function (response) {
|
||||
if (isSuccessStatus(response.status)) {
|
||||
if (_.isFunction(options.success)) {
|
||||
var propsMapping = _.invert(options.davProperties);
|
||||
var results = parsePropFindResult(response.body, propsMapping);
|
||||
if (options.depth > 0) {
|
||||
// discard root entry
|
||||
results.shift();
|
||||
}
|
||||
|
||||
options.success(results);
|
||||
return;
|
||||
}
|
||||
} else if (_.isFunction(options.error)) {
|
||||
options.error(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function callPropPatch (client, options, model, headers) {
|
||||
return client.propPatch(
|
||||
options.url,
|
||||
convertModelAttributesToDavProperties(model.changed, options.davProperties),
|
||||
headers
|
||||
).then(function (result) {
|
||||
if (isSuccessStatus(result.status)) {
|
||||
if (_.isFunction(options.success)) {
|
||||
// pass the object's own values because the server
|
||||
// does not return the updated model
|
||||
options.success(model.toJSON());
|
||||
}
|
||||
} else if (_.isFunction(options.error)) {
|
||||
options.error(result);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function callMkCol (client, options, model, headers) {
|
||||
// call MKCOL without data, followed by PROPPATCH
|
||||
return client.request(
|
||||
options.type,
|
||||
options.url,
|
||||
headers,
|
||||
null
|
||||
).then(function (result) {
|
||||
if (!isSuccessStatus(result.status)) {
|
||||
if (_.isFunction(options.error)) {
|
||||
options.error(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
callPropPatch(client, options, model, headers);
|
||||
});
|
||||
}
|
||||
|
||||
function callMethod (client, options, model, headers) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
return client.request(
|
||||
options.type,
|
||||
options.url,
|
||||
headers,
|
||||
options.data
|
||||
).then(function (result) {
|
||||
if (!isSuccessStatus(result.status)) {
|
||||
if (_.isFunction(options.error)) {
|
||||
options.error(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isFunction(options.success)) {
|
||||
if (options.type === 'PUT' || options.type === 'POST' || options.type === 'MKCOL') {
|
||||
// pass the object's own values because the server
|
||||
// does not return anything
|
||||
var responseJson = result.body || model.toJSON();
|
||||
var locationHeader = result.xhr.getResponseHeader('Content-Location');
|
||||
if (options.type === 'POST' && locationHeader) {
|
||||
responseJson.id = parseIdFromLocation(locationHeader);
|
||||
}
|
||||
options.success(responseJson);
|
||||
return;
|
||||
}
|
||||
// if multi-status, parse
|
||||
if (result.status === 207) {
|
||||
var propsMapping = _.invert(options.davProperties);
|
||||
options.success(parsePropFindResult(result.body, propsMapping));
|
||||
} else {
|
||||
options.success(result.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function davCall (options, model) {
|
||||
var client = new dav.Client({
|
||||
baseUrl: options.url,
|
||||
xmlNamespaces: _.extend({
|
||||
'DAV:': 'd',
|
||||
'http://owncloud.org/ns': 'oc'
|
||||
}, options.xmlNamespaces || {})
|
||||
});
|
||||
client.resolveUrl = function () {
|
||||
return options.url;
|
||||
};
|
||||
var headers = _.extend({
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'requesttoken': OC.requestToken
|
||||
}, options.headers);
|
||||
if (options.type === 'PROPFIND') {
|
||||
return callPropFind(client, options, model, headers);
|
||||
} else if (options.type === 'PROPPATCH') {
|
||||
return callPropPatch(client, options, model, headers);
|
||||
} else if (options.type === 'MKCOL') {
|
||||
return callMkCol(client, options, model, headers);
|
||||
} else {
|
||||
return callMethod(client, options, model, headers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DAV transport
|
||||
*/
|
||||
export function davSync (method, model, options) {
|
||||
var params = {type: methodMap[method] || method};
|
||||
var isCollection = (model instanceof Backbone.Collection);
|
||||
|
||||
if (method === 'update') {
|
||||
// if a model has an inner collection, it must define an
|
||||
// attribute "hasInnerCollection" that evaluates to true
|
||||
if (model.hasInnerCollection) {
|
||||
// if the model itself is a Webdav collection, use MKCOL
|
||||
params.type = 'MKCOL';
|
||||
} else if (model.usePUT || (model.collection && model.collection.usePUT)) {
|
||||
// use PUT instead of PROPPATCH
|
||||
params.type = 'PUT';
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we have a URL.
|
||||
if (!options.url) {
|
||||
params.url = _.result(model, 'url') || urlError();
|
||||
}
|
||||
|
||||
// Ensure that we have the appropriate request data.
|
||||
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
||||
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
||||
}
|
||||
|
||||
// Don't process data on a non-GET request.
|
||||
if (params.type !== 'PROPFIND') {
|
||||
params.processData = false;
|
||||
}
|
||||
|
||||
if (params.type === 'PROPFIND' || params.type === 'PROPPATCH') {
|
||||
var davProperties = model.davProperties;
|
||||
if (!davProperties && model.model) {
|
||||
// use dav properties from model in case of collection
|
||||
davProperties = model.model.prototype.davProperties;
|
||||
}
|
||||
if (davProperties) {
|
||||
if (_.isFunction(davProperties)) {
|
||||
params.davProperties = davProperties.call(model);
|
||||
} else {
|
||||
params.davProperties = davProperties;
|
||||
}
|
||||
}
|
||||
|
||||
params.davProperties = _.extend(params.davProperties || {}, options.davProperties);
|
||||
|
||||
if (_.isUndefined(options.depth)) {
|
||||
if (isCollection) {
|
||||
options.depth = 1;
|
||||
} else {
|
||||
options.depth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass along `textStatus` and `errorThrown` from jQuery.
|
||||
var error = options.error;
|
||||
options.error = function (xhr, textStatus, errorThrown) {
|
||||
options.textStatus = textStatus;
|
||||
options.errorThrown = errorThrown;
|
||||
if (error) {
|
||||
error.call(options.context, xhr, textStatus, errorThrown);
|
||||
}
|
||||
};
|
||||
|
||||
// Make the request, allowing the user to override any Ajax options.
|
||||
var xhr = options.xhr = Backbone.davCall(_.extend(params, options), model);
|
||||
model.trigger('request', model, xhr, options);
|
||||
return xhr;
|
||||
}
|
|
@ -0,0 +1,480 @@
|
|||
/* global 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import {Collection, Model, View} from 'backbone';
|
||||
|
||||
import OC from './index';
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @class ContactCollection
|
||||
* @private
|
||||
*/
|
||||
const ContactCollection = Collection.extend({
|
||||
model: Contact
|
||||
});
|
||||
|
||||
/**
|
||||
* @class ContactsListView
|
||||
* @private
|
||||
*/
|
||||
const ContactsListView = View.extend({
|
||||
|
||||
/** @type {ContactCollection} */
|
||||
_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 ContactsListItemView
|
||||
* @private
|
||||
*/
|
||||
const ContactsListItemView = 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
|
||||
* @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'
|
||||
},
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @memberOf OC
|
||||
*/
|
||||
const 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));
|
||||
}
|
||||
};
|
||||
|
||||
export default ContactsMenu;
|
|
@ -19,9 +19,21 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Backbone from 'backbone';
|
||||
|
||||
import Apps from './apps'
|
||||
import ContactsMenu from './contactsmenu';
|
||||
import {davCall, davSync} from './backbone-webdav';
|
||||
|
||||
// Patch Backbone for DAV
|
||||
Object.assign(Backbone, {
|
||||
davCall,
|
||||
davSync,
|
||||
});
|
||||
|
||||
/** @namespace OC */
|
||||
export default {
|
||||
Apps,
|
||||
Backbone,
|
||||
ContactsMenu,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue