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:
Roeland Jago Douma 2019-01-29 09:30:28 +01:00 committed by GitHub
commit ef94996fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 879 additions and 876 deletions

View File

@ -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);

View File

@ -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",

38
core/js/dist/main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,6 @@
"oc-requesttoken.js",
"mimetype.js",
"mimetypelist.js",
"oc-backbone.js",
"select2-toggleselect.js",
"placeholder.js",
"jquery.avatar.js",

View File

@ -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);

View File

@ -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();
}

View File

@ -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;
}

480
core/src/OC/contactsmenu.js Normal file
View File

@ -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;

View File

@ -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,
};