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",
|
"jquery.ocdialog.js",
|
||||||
"oc-dialogs.js",
|
"oc-dialogs.js",
|
||||||
"js.js",
|
"js.js",
|
||||||
"oc-backbone.js",
|
|
||||||
"oc-backbone-webdav.js",
|
|
||||||
"l10n.js",
|
"l10n.js",
|
||||||
"share.js",
|
"share.js",
|
||||||
"sharetemplates.js",
|
"sharetemplates.js",
|
||||||
|
@ -22,7 +20,6 @@
|
||||||
"sharedialogresharerinfoview.js",
|
"sharedialogresharerinfoview.js",
|
||||||
"sharedialogshareelistview.js",
|
"sharedialogshareelistview.js",
|
||||||
"octemplate.js",
|
"octemplate.js",
|
||||||
"contactsmenu.js",
|
|
||||||
"contactsmenu_templates.js",
|
"contactsmenu_templates.js",
|
||||||
"eventsource.js",
|
"eventsource.js",
|
||||||
"config.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",
|
"oc-requesttoken.js",
|
||||||
"mimetype.js",
|
"mimetype.js",
|
||||||
"mimetypelist.js",
|
"mimetypelist.js",
|
||||||
"oc-backbone.js",
|
|
||||||
"select2-toggleselect.js",
|
"select2-toggleselect.js",
|
||||||
"placeholder.js",
|
"placeholder.js",
|
||||||
"jquery.avatar.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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Backbone from 'backbone';
|
||||||
|
|
||||||
import Apps from './apps'
|
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 */
|
/** @namespace OC */
|
||||||
export default {
|
export default {
|
||||||
Apps,
|
Apps,
|
||||||
|
Backbone,
|
||||||
|
ContactsMenu,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue