contactsmenu.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /* global OC.Backbone, Handlebars, Promise, _ */
  2. /**
  3. * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
  4. *
  5. * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. /**
  24. * @module OC.ContactsMenu
  25. * @private
  26. */
  27. (function(OC, $, _, Handlebars) {
  28. 'use strict';
  29. /**
  30. * @class Contact
  31. */
  32. var Contact = OC.Backbone.Model.extend({
  33. defaults: {
  34. fullName: '',
  35. lastMessage: '',
  36. actions: [],
  37. hasOneAction: false,
  38. hasTwoActions: false,
  39. hasManyActions: false
  40. },
  41. /**
  42. * @returns {undefined}
  43. */
  44. initialize: function() {
  45. // Add needed property for easier template rendering
  46. if (this.get('actions').length === 0) {
  47. this.set('hasOneAction', true);
  48. } else if (this.get('actions').length === 1) {
  49. this.set('hasTwoActions', true);
  50. this.set('secondAction', this.get('actions')[0]);
  51. } else {
  52. this.set('hasManyActions', true);
  53. }
  54. }
  55. });
  56. /**
  57. * @class ContactCollection
  58. * @private
  59. */
  60. var ContactCollection = OC.Backbone.Collection.extend({
  61. model: Contact
  62. });
  63. /**
  64. * @class ContactsListView
  65. * @private
  66. */
  67. var ContactsListView = OC.Backbone.View.extend({
  68. /** @type {ContactCollection} */
  69. _collection: undefined,
  70. /** @type {array} */
  71. _subViews: [],
  72. /**
  73. * @param {object} options
  74. * @returns {undefined}
  75. */
  76. initialize: function(options) {
  77. this._collection = options.collection;
  78. },
  79. /**
  80. * @returns {self}
  81. */
  82. render: function() {
  83. var self = this;
  84. self.$el.html('');
  85. self._subViews = [];
  86. self._collection.forEach(function(contact) {
  87. var item = new ContactsListItemView({
  88. model: contact
  89. });
  90. item.render();
  91. self.$el.append(item.$el);
  92. item.on('toggle:actionmenu', self._onChildActionMenuToggle, self);
  93. self._subViews.push(item);
  94. });
  95. return self;
  96. },
  97. /**
  98. * Event callback to propagate opening (another) entry's action menu
  99. *
  100. * @param {type} $src
  101. * @returns {undefined}
  102. */
  103. _onChildActionMenuToggle: function($src) {
  104. this._subViews.forEach(function(view) {
  105. view.trigger('parent:toggle:actionmenu', $src);
  106. });
  107. }
  108. });
  109. /**
  110. * @class ContactsListItemView
  111. * @private
  112. */
  113. var ContactsListItemView = OC.Backbone.View.extend({
  114. /** @type {string} */
  115. className: 'contact',
  116. /** @type {undefined|function} */
  117. _template: undefined,
  118. /** @type {Contact} */
  119. _model: undefined,
  120. /** @type {boolean} */
  121. _actionMenuShown: false,
  122. events: {
  123. 'click .icon-more': '_onToggleActionsMenu'
  124. },
  125. /**
  126. * @param {object} data
  127. * @returns {undefined}
  128. */
  129. template: function(data) {
  130. return OC.ContactsMenu.Templates['contact'](data);
  131. },
  132. /**
  133. * @param {object} options
  134. * @returns {undefined}
  135. */
  136. initialize: function(options) {
  137. this._model = options.model;
  138. this.on('parent:toggle:actionmenu', this._onOtherActionMenuOpened, this);
  139. },
  140. /**
  141. * @returns {self}
  142. */
  143. render: function() {
  144. this.$el.html(this.template({
  145. contact: this._model.toJSON()
  146. }));
  147. this.delegateEvents();
  148. // Show placeholder if no avatar is available (avatar is rendered as img, not div)
  149. this.$('div.avatar').imageplaceholder(this._model.get('fullName'));
  150. // Show tooltip for top action
  151. this.$('.top-action').tooltip({placement: 'left'});
  152. // Show tooltip for second action
  153. this.$('.second-action').tooltip({placement: 'left'});
  154. return this;
  155. },
  156. /**
  157. * Toggle the visibility of the action popover menu
  158. *
  159. * @private
  160. * @returns {undefined}
  161. */
  162. _onToggleActionsMenu: function() {
  163. this._actionMenuShown = !this._actionMenuShown;
  164. if (this._actionMenuShown) {
  165. this.$('.menu').show();
  166. } else {
  167. this.$('.menu').hide();
  168. }
  169. this.trigger('toggle:actionmenu', this.$el);
  170. },
  171. /**
  172. * @private
  173. * @argument {jQuery} $src
  174. * @returns {undefined}
  175. */
  176. _onOtherActionMenuOpened: function($src) {
  177. if (this.$el.is($src)) {
  178. // Ignore
  179. return;
  180. }
  181. this._actionMenuShown = false;
  182. this.$('.menu').hide();
  183. }
  184. });
  185. /**
  186. * @class ContactsMenuView
  187. * @private
  188. */
  189. var ContactsMenuView = OC.Backbone.View.extend({
  190. /** @type {undefined|function} */
  191. _loadingTemplate: undefined,
  192. /** @type {undefined|function} */
  193. _errorTemplate: undefined,
  194. /** @type {undefined|function} */
  195. _contentTemplate: undefined,
  196. /** @type {undefined|function} */
  197. _contactsTemplate: undefined,
  198. /** @type {undefined|ContactCollection} */
  199. _contacts: undefined,
  200. /** @type {string} */
  201. _searchTerm: '',
  202. events: {
  203. 'input #contactsmenu-search': '_onSearch'
  204. },
  205. /**
  206. * @returns {undefined}
  207. */
  208. _onSearch: _.debounce(function(e) {
  209. var searchTerm = this.$('#contactsmenu-search').val();
  210. // IE11 triggers an 'input' event after the view has been rendered
  211. // resulting in an endless loading loop. To prevent this, we remember
  212. // the last search term to savely ignore some events
  213. // See https://github.com/nextcloud/server/issues/5281
  214. if (searchTerm !== this._searchTerm) {
  215. this.trigger('search', this.$('#contactsmenu-search').val());
  216. this._searchTerm = searchTerm;
  217. }
  218. }, 700),
  219. /**
  220. * @param {object} data
  221. * @returns {string}
  222. */
  223. loadingTemplate: function(data) {
  224. return OC.ContactsMenu.Templates['loading'](data);
  225. },
  226. /**
  227. * @param {object} data
  228. * @returns {string}
  229. */
  230. errorTemplate: function(data) {
  231. return OC.ContactsMenu.Templates['error'](
  232. _.extend({
  233. couldNotLoadText: t('core', 'Could not load your contacts')
  234. }, data)
  235. );
  236. },
  237. /**
  238. * @param {object} data
  239. * @returns {string}
  240. */
  241. contentTemplate: function(data) {
  242. return OC.ContactsMenu.Templates['menu'](
  243. _.extend({
  244. searchContactsText: t('core', 'Search contacts …')
  245. }, data)
  246. );
  247. },
  248. /**
  249. * @param {object} data
  250. * @returns {string}
  251. */
  252. contactsTemplate: function(data) {
  253. return OC.ContactsMenu.Templates['list'](
  254. _.extend({
  255. noContactsFoundText: t('core', 'No contacts found'),
  256. showAllContactsText: t('core', 'Show all contacts …')
  257. }, data)
  258. );
  259. },
  260. /**
  261. * @param {object} options
  262. * @returns {undefined}
  263. */
  264. initialize: function(options) {
  265. this.options = options;
  266. },
  267. /**
  268. * @param {string} text
  269. * @returns {undefined}
  270. */
  271. showLoading: function(text) {
  272. this.render();
  273. this._contacts = undefined;
  274. this.$('.content').html(this.loadingTemplate({
  275. loadingText: text
  276. }));
  277. },
  278. /**
  279. * @returns {undefined}
  280. */
  281. showError: function() {
  282. this.render();
  283. this._contacts = undefined;
  284. this.$('.content').html(this.errorTemplate());
  285. },
  286. /**
  287. * @param {object} viewData
  288. * @param {string} searchTerm
  289. * @returns {undefined}
  290. */
  291. showContacts: function(viewData, searchTerm) {
  292. this._contacts = viewData.contacts;
  293. this.render({
  294. contacts: viewData.contacts
  295. });
  296. var list = new ContactsListView({
  297. collection: viewData.contacts
  298. });
  299. list.render();
  300. this.$('.content').html(this.contactsTemplate({
  301. contacts: viewData.contacts,
  302. searchTerm: searchTerm,
  303. contactsAppEnabled: viewData.contactsAppEnabled,
  304. contactsAppURL: OC.generateUrl('/apps/contacts')
  305. }));
  306. this.$('#contactsmenu-contacts').html(list.$el);
  307. },
  308. /**
  309. * @param {object} data
  310. * @returns {self}
  311. */
  312. render: function(data) {
  313. var searchVal = this.$('#contactsmenu-search').val();
  314. this.$el.html(this.contentTemplate(data));
  315. // Focus search
  316. this.$('#contactsmenu-search').val(searchVal);
  317. this.$('#contactsmenu-search').focus();
  318. return this;
  319. }
  320. });
  321. /**
  322. * @param {Object} options
  323. * @param {jQuery} options.el
  324. * @param {jQuery} options.trigger
  325. * @class ContactsMenu
  326. * @memberOf OC
  327. */
  328. var ContactsMenu = function(options) {
  329. this.initialize(options);
  330. };
  331. ContactsMenu.prototype = {
  332. /** @type {jQuery} */
  333. $el: undefined,
  334. /** @type {jQuery} */
  335. _$trigger: undefined,
  336. /** @type {ContactsMenuView} */
  337. _view: undefined,
  338. /** @type {Promise} */
  339. _contactsPromise: undefined,
  340. /**
  341. * @param {Object} options
  342. * @param {jQuery} options.el - the element to render the menu in
  343. * @param {jQuery} options.trigger - the element to click on to open the menu
  344. * @returns {undefined}
  345. */
  346. initialize: function(options) {
  347. this.$el = options.el;
  348. this._$trigger = options.trigger;
  349. this._view = new ContactsMenuView({
  350. el: this.$el
  351. });
  352. this._view.on('search', function(searchTerm) {
  353. this._loadContacts(searchTerm);
  354. }, this);
  355. OC.registerMenu(this._$trigger, this.$el, function() {
  356. this._toggleVisibility(true);
  357. }.bind(this), true);
  358. this.$el.on('beforeHide', function() {
  359. this._toggleVisibility(false);
  360. }.bind(this));
  361. },
  362. /**
  363. * @private
  364. * @param {boolean} show
  365. * @returns {Promise}
  366. */
  367. _toggleVisibility: function(show) {
  368. if (show) {
  369. return this._loadContacts();
  370. } else {
  371. this.$el.html('');
  372. return Promise.resolve();
  373. }
  374. },
  375. /**
  376. * @private
  377. * @param {string|undefined} searchTerm
  378. * @returns {Promise}
  379. */
  380. _getContacts: function(searchTerm) {
  381. var url = OC.generateUrl('/contactsmenu/contacts');
  382. return Promise.resolve($.ajax(url, {
  383. method: 'POST',
  384. data: {
  385. filter: searchTerm
  386. }
  387. }));
  388. },
  389. /**
  390. * @param {string|undefined} searchTerm
  391. * @returns {undefined}
  392. */
  393. _loadContacts: function(searchTerm) {
  394. var self = this;
  395. if (!self._contactsPromise) {
  396. self._contactsPromise = self._getContacts(searchTerm);
  397. }
  398. if (_.isUndefined(searchTerm) || searchTerm === '') {
  399. self._view.showLoading(t('core', 'Loading your contacts …'));
  400. } else {
  401. self._view.showLoading(t('core', 'Looking for {term} …', {
  402. term: searchTerm
  403. }));
  404. }
  405. return self._contactsPromise.then(function(data) {
  406. // Convert contact entries to Backbone collection
  407. data.contacts = new ContactCollection(data.contacts);
  408. self._view.showContacts(data, searchTerm);
  409. }, function(e) {
  410. self._view.showError();
  411. console.error('There was an error loading your contacts', e);
  412. }).then(function() {
  413. // Delete promise, so that contacts are fetched again when the
  414. // menu is opened the next time.
  415. delete self._contactsPromise;
  416. }).catch(console.error.bind(this));
  417. }
  418. };
  419. OC.ContactsMenu = ContactsMenu;
  420. })(OC, $, _, Handlebars);