您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

contactsmenu.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. /**
  2. * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
  3. *
  4. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  5. * @author John Molakvoæ <skjnldsv@protonmail.com>
  6. * @author Roeland Jago Douma <roeland@famdouma.nl>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. /* eslint-disable */
  25. import _ from 'underscore'
  26. import $ from 'jquery'
  27. import { Collection, Model, View } from 'backbone'
  28. import OC from './index'
  29. /**
  30. * @class Contact
  31. */
  32. const Contact = 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. const ContactCollection = Collection.extend({
  61. model: Contact
  62. })
  63. /**
  64. * @class ContactsListView
  65. * @private
  66. */
  67. const ContactsListView = 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. const ContactsListItemView = 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. contactTemplate: require('./contactsmenu/contact.handlebars'),
  126. /**
  127. * @param {object} data
  128. * @returns {undefined}
  129. */
  130. template: function(data) {
  131. return this.contactTemplate(data)
  132. },
  133. /**
  134. * @param {object} options
  135. * @returns {undefined}
  136. */
  137. initialize: function(options) {
  138. this._model = options.model
  139. this.on('parent:toggle:actionmenu', this._onOtherActionMenuOpened, this)
  140. },
  141. /**
  142. * @returns {self}
  143. */
  144. render: function() {
  145. this.$el.html(this.template({
  146. contact: this._model.toJSON()
  147. }))
  148. this.delegateEvents()
  149. // Show placeholder if no avatar is available (avatar is rendered as img, not div)
  150. this.$('div.avatar').imageplaceholder(this._model.get('fullName'))
  151. // Show tooltip for top action
  152. this.$('.top-action').tooltip({ placement: 'left' })
  153. // Show tooltip for second action
  154. this.$('.second-action').tooltip({ placement: 'left' })
  155. return this
  156. },
  157. /**
  158. * Toggle the visibility of the action popover menu
  159. *
  160. * @private
  161. * @returns {undefined}
  162. */
  163. _onToggleActionsMenu: function() {
  164. this._actionMenuShown = !this._actionMenuShown
  165. if (this._actionMenuShown) {
  166. this.$('.menu').show()
  167. } else {
  168. this.$('.menu').hide()
  169. }
  170. this.trigger('toggle:actionmenu', this.$el)
  171. },
  172. /**
  173. * @private
  174. * @argument {jQuery} $src
  175. * @returns {undefined}
  176. */
  177. _onOtherActionMenuOpened: function($src) {
  178. if (this.$el.is($src)) {
  179. // Ignore
  180. return
  181. }
  182. this._actionMenuShown = false
  183. this.$('.menu').hide()
  184. }
  185. })
  186. /**
  187. * @class ContactsMenuView
  188. * @private
  189. */
  190. const ContactsMenuView = View.extend({
  191. /** @type {undefined|function} */
  192. _loadingTemplate: undefined,
  193. /** @type {undefined|function} */
  194. _errorTemplate: undefined,
  195. /** @type {undefined|function} */
  196. _contentTemplate: undefined,
  197. /** @type {undefined|function} */
  198. _contactsTemplate: undefined,
  199. /** @type {undefined|ContactCollection} */
  200. _contacts: undefined,
  201. /** @type {string} */
  202. _searchTerm: '',
  203. events: {
  204. 'input #contactsmenu-search': '_onSearch'
  205. },
  206. templates: {
  207. loading: require('./contactsmenu/loading.handlebars'),
  208. error: require('./contactsmenu/error.handlebars'),
  209. menu: require('./contactsmenu/menu.handlebars'),
  210. list: require('./contactsmenu/list.handlebars')
  211. },
  212. /**
  213. * @returns {undefined}
  214. */
  215. _onSearch: _.debounce(function(e) {
  216. var searchTerm = this.$('#contactsmenu-search').val()
  217. // IE11 triggers an 'input' event after the view has been rendered
  218. // resulting in an endless loading loop. To prevent this, we remember
  219. // the last search term to savely ignore some events
  220. // See https://github.com/nextcloud/server/issues/5281
  221. if (searchTerm !== this._searchTerm) {
  222. this.trigger('search', this.$('#contactsmenu-search').val())
  223. this._searchTerm = searchTerm
  224. }
  225. }, 700),
  226. /**
  227. * @param {object} data
  228. * @returns {string}
  229. */
  230. loadingTemplate: function(data) {
  231. return this.templates.loading(data)
  232. },
  233. /**
  234. * @param {object} data
  235. * @returns {string}
  236. */
  237. errorTemplate: function(data) {
  238. return this.templates.error(
  239. _.extend({
  240. couldNotLoadText: t('core', 'Could not load your contacts')
  241. }, data)
  242. )
  243. },
  244. /**
  245. * @param {object} data
  246. * @returns {string}
  247. */
  248. contentTemplate: function(data) {
  249. return this.templates.menu(
  250. _.extend({
  251. searchContactsText: t('core', 'Search contacts …')
  252. }, data)
  253. )
  254. },
  255. /**
  256. * @param {object} data
  257. * @returns {string}
  258. */
  259. contactsTemplate: function(data) {
  260. return this.templates.list(
  261. _.extend({
  262. noContactsFoundText: t('core', 'No contacts found'),
  263. showAllContactsText: t('core', 'Show all contacts …'),
  264. contactsAppMgmtText: t('core', 'Install the Contacts app')
  265. }, data)
  266. )
  267. },
  268. /**
  269. * @param {object} options
  270. * @returns {undefined}
  271. */
  272. initialize: function(options) {
  273. this.options = options
  274. },
  275. /**
  276. * @param {string} text
  277. * @returns {undefined}
  278. */
  279. showLoading: function(text) {
  280. this.render()
  281. this._contacts = undefined
  282. this.$('.content').html(this.loadingTemplate({
  283. loadingText: text
  284. }))
  285. },
  286. /**
  287. * @returns {undefined}
  288. */
  289. showError: function() {
  290. this.render()
  291. this._contacts = undefined
  292. this.$('.content').html(this.errorTemplate())
  293. },
  294. /**
  295. * @param {object} viewData
  296. * @param {string} searchTerm
  297. * @returns {undefined}
  298. */
  299. showContacts: function(viewData, searchTerm) {
  300. this._contacts = viewData.contacts
  301. this.render({
  302. contacts: viewData.contacts
  303. })
  304. var list = new ContactsListView({
  305. collection: viewData.contacts
  306. })
  307. list.render()
  308. this.$('.content').html(this.contactsTemplate({
  309. contacts: viewData.contacts,
  310. searchTerm: searchTerm,
  311. contactsAppEnabled: viewData.contactsAppEnabled,
  312. contactsAppURL: OC.generateUrl('/apps/contacts'),
  313. canInstallApp: OC.isUserAdmin(),
  314. contactsAppMgmtURL: OC.generateUrl('/settings/apps/social/contacts')
  315. }))
  316. this.$('#contactsmenu-contacts').html(list.$el)
  317. },
  318. /**
  319. * @param {object} data
  320. * @returns {self}
  321. */
  322. render: function(data) {
  323. var searchVal = this.$('#contactsmenu-search').val()
  324. this.$el.html(this.contentTemplate(data))
  325. // Focus search
  326. this.$('#contactsmenu-search').val(searchVal)
  327. this.$('#contactsmenu-search').focus()
  328. return this
  329. }
  330. })
  331. /**
  332. * @param {Object} options
  333. * @param {jQuery} options.el
  334. * @param {jQuery} options.trigger
  335. * @class ContactsMenu
  336. * @memberOf OC
  337. */
  338. const ContactsMenu = function(options) {
  339. this.initialize(options)
  340. }
  341. ContactsMenu.prototype = {
  342. /** @type {jQuery} */
  343. $el: undefined,
  344. /** @type {jQuery} */
  345. _$trigger: undefined,
  346. /** @type {ContactsMenuView} */
  347. _view: undefined,
  348. /** @type {Promise} */
  349. _contactsPromise: undefined,
  350. /**
  351. * @param {Object} options
  352. * @param {jQuery} options.el - the element to render the menu in
  353. * @param {jQuery} options.trigger - the element to click on to open the menu
  354. * @returns {undefined}
  355. */
  356. initialize: function(options) {
  357. this.$el = options.el
  358. this._$trigger = options.trigger
  359. this._view = new ContactsMenuView({
  360. el: this.$el
  361. })
  362. this._view.on('search', function(searchTerm) {
  363. this._loadContacts(searchTerm)
  364. }, this)
  365. OC.registerMenu(this._$trigger, this.$el, function() {
  366. this._toggleVisibility(true)
  367. }.bind(this), true)
  368. this.$el.on('beforeHide', function() {
  369. this._toggleVisibility(false)
  370. }.bind(this))
  371. },
  372. /**
  373. * @private
  374. * @param {boolean} show
  375. * @returns {Promise}
  376. */
  377. _toggleVisibility: function(show) {
  378. if (show) {
  379. return this._loadContacts()
  380. } else {
  381. this.$el.html('')
  382. return Promise.resolve()
  383. }
  384. },
  385. /**
  386. * @private
  387. * @param {string|undefined} searchTerm
  388. * @returns {Promise}
  389. */
  390. _getContacts: function(searchTerm) {
  391. var url = OC.generateUrl('/contactsmenu/contacts')
  392. return Promise.resolve($.ajax(url, {
  393. method: 'POST',
  394. data: {
  395. filter: searchTerm
  396. }
  397. }))
  398. },
  399. /**
  400. * @param {string|undefined} searchTerm
  401. * @returns {undefined}
  402. */
  403. _loadContacts: function(searchTerm) {
  404. var self = this
  405. if (!self._contactsPromise) {
  406. self._contactsPromise = self._getContacts(searchTerm)
  407. }
  408. if (_.isUndefined(searchTerm) || searchTerm === '') {
  409. self._view.showLoading(t('core', 'Loading your contacts …'))
  410. } else {
  411. self._view.showLoading(t('core', 'Looking for {term} …', {
  412. term: searchTerm
  413. }))
  414. }
  415. return self._contactsPromise.then(function(data) {
  416. // Convert contact entries to Backbone collection
  417. data.contacts = new ContactCollection(data.contacts)
  418. self._view.showContacts(data, searchTerm)
  419. }, function(e) {
  420. self._view.showError()
  421. console.error('There was an error loading your contacts', e)
  422. }).then(function() {
  423. // Delete promise, so that contacts are fetched again when the
  424. // menu is opened the next time.
  425. delete self._contactsPromise
  426. }).catch(console.error.bind(this))
  427. }
  428. }
  429. export default ContactsMenu