You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sharedialogview.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. /*
  2. * Copyright (c) 2015
  3. *
  4. * This file is licensed under the Affero General Public License version 3
  5. * or later.
  6. *
  7. * See the COPYING-README file.
  8. *
  9. */
  10. /* globals Handlebars */
  11. (function() {
  12. if(!OC.Share) {
  13. OC.Share = {};
  14. }
  15. var TEMPLATE_BASE =
  16. '<div class="resharerInfoView subView"></div>' +
  17. '{{#if isSharingAllowed}}' +
  18. '<label for="shareWith-{{cid}}" class="hidden-visually">{{shareLabel}}</label>' +
  19. '<div class="oneline">' +
  20. ' <input id="shareWith-{{cid}}" class="shareWithField" type="text" placeholder="{{sharePlaceholder}}" />' +
  21. ' <span class="shareWithLoading icon-loading-small hidden"></span>'+
  22. '{{{shareInfo}}}' +
  23. '</div>' +
  24. '{{/if}}' +
  25. '<div class="shareeListView subView"></div>' +
  26. '<div class="linkShareView subView"></div>' +
  27. '<div class="expirationView subView"></div>' +
  28. '<div class="loading hidden" style="height: 50px"></div>';
  29. var TEMPLATE_SHARE_INFO =
  30. '<span class="icon icon-info shareWithRemoteInfo hasTooltip" ' +
  31. 'title="{{tooltip}}"></span>';
  32. /**
  33. * @class OCA.Share.ShareDialogView
  34. * @member {OC.Share.ShareItemModel} model
  35. * @member {jQuery} $el
  36. * @memberof OCA.Sharing
  37. * @classdesc
  38. *
  39. * Represents the GUI of the share dialogue
  40. *
  41. */
  42. var ShareDialogView = OC.Backbone.View.extend({
  43. /** @type {Object} **/
  44. _templates: {},
  45. /** @type {boolean} **/
  46. _showLink: true,
  47. /** @type {string} **/
  48. tagName: 'div',
  49. /** @type {OC.Share.ShareConfigModel} **/
  50. configModel: undefined,
  51. /** @type {object} **/
  52. resharerInfoView: undefined,
  53. /** @type {object} **/
  54. linkShareView: undefined,
  55. /** @type {object} **/
  56. expirationView: undefined,
  57. /** @type {object} **/
  58. shareeListView: undefined,
  59. events: {
  60. 'focus .shareWithField': 'onShareWithFieldFocus',
  61. 'input .shareWithField': 'onShareWithFieldChanged'
  62. },
  63. initialize: function(options) {
  64. var view = this;
  65. this.model.on('fetchError', function() {
  66. OC.Notification.showTemporary(t('core', 'Share details could not be loaded for this item.'));
  67. });
  68. if(!_.isUndefined(options.configModel)) {
  69. this.configModel = options.configModel;
  70. } else {
  71. throw 'missing OC.Share.ShareConfigModel';
  72. }
  73. this.configModel.on('change:isRemoteShareAllowed', function() {
  74. view.render();
  75. });
  76. this.model.on('change:permissions', function() {
  77. view.render();
  78. });
  79. this.model.on('request', this._onRequest, this);
  80. this.model.on('sync', this._onEndRequest, this);
  81. var subViewOptions = {
  82. model: this.model,
  83. configModel: this.configModel
  84. };
  85. var subViews = {
  86. resharerInfoView: 'ShareDialogResharerInfoView',
  87. linkShareView: 'ShareDialogLinkShareView',
  88. expirationView: 'ShareDialogExpirationView',
  89. shareeListView: 'ShareDialogShareeListView'
  90. };
  91. for(var name in subViews) {
  92. var className = subViews[name];
  93. this[name] = _.isUndefined(options[name])
  94. ? new OC.Share[className](subViewOptions)
  95. : options[name];
  96. }
  97. _.bindAll(this,
  98. 'autocompleteHandler',
  99. '_onSelectRecipient',
  100. 'onShareWithFieldChanged',
  101. 'onShareWithFieldFocus'
  102. );
  103. OC.Plugins.attach('OC.Share.ShareDialogView', this);
  104. },
  105. onShareWithFieldChanged: function() {
  106. var $el = this.$el.find('.shareWithField');
  107. if ($el.val().length < 2) {
  108. $el.removeClass('error').tooltip('hide');
  109. }
  110. },
  111. /* trigger search after the field was re-selected */
  112. onShareWithFieldFocus: function() {
  113. this.$el.find('.shareWithField').autocomplete("search");
  114. },
  115. autocompleteHandler: function (search, response) {
  116. var $shareWithField = $('.shareWithField'),
  117. view = this,
  118. $loading = this.$el.find('.shareWithLoading'),
  119. $shareInfo = this.$el.find('.shareWithRemoteInfo');
  120. var count = oc_config['sharing.minSearchStringLength'];
  121. if (search.term.trim().length < count) {
  122. var title = n('core',
  123. 'At least {count} character is needed for autocompletion',
  124. 'At least {count} characters are needed for autocompletion',
  125. count,
  126. { count: count }
  127. );
  128. $shareWithField.addClass('error')
  129. .attr('data-original-title', title)
  130. .tooltip('hide')
  131. .tooltip({
  132. placement: 'bottom',
  133. trigger: 'manual'
  134. })
  135. .tooltip('fixTitle')
  136. .tooltip('show');
  137. response();
  138. return;
  139. }
  140. $loading.removeClass('hidden');
  141. $loading.addClass('inlineblock');
  142. $shareInfo.addClass('hidden');
  143. $shareWithField.removeClass('error')
  144. .tooltip('hide');
  145. var perPage = 200;
  146. $.get(
  147. OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
  148. {
  149. format: 'json',
  150. search: search.term.trim(),
  151. perPage: perPage,
  152. itemType: view.model.get('itemType')
  153. },
  154. function (result) {
  155. $loading.addClass('hidden');
  156. $loading.removeClass('inlineblock');
  157. $shareInfo.removeClass('hidden');
  158. if (result.ocs.meta.statuscode === 100) {
  159. var users = result.ocs.data.exact.users.concat(result.ocs.data.users);
  160. var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups);
  161. var remotes = result.ocs.data.exact.remotes.concat(result.ocs.data.remotes);
  162. var lookup = result.ocs.data.lookup;
  163. var emails = [],
  164. circles = [];
  165. if (typeof(result.ocs.data.emails) !== 'undefined') {
  166. emails = result.ocs.data.exact.emails.concat(result.ocs.data.emails);
  167. }
  168. if (typeof(result.ocs.data.circles) !== 'undefined') {
  169. circles = result.ocs.data.exact.circles.concat(result.ocs.data.circles);
  170. }
  171. var usersLength;
  172. var groupsLength;
  173. var remotesLength;
  174. var emailsLength;
  175. var circlesLength;
  176. var i, j;
  177. //Filter out the current user
  178. usersLength = users.length;
  179. for (i = 0; i < usersLength; i++) {
  180. if (users[i].value.shareWith === OC.currentUser) {
  181. users.splice(i, 1);
  182. break;
  183. }
  184. }
  185. // Filter out the owner of the share
  186. if (view.model.hasReshare()) {
  187. usersLength = users.length;
  188. for (i = 0 ; i < usersLength; i++) {
  189. if (users[i].value.shareWith === view.model.getReshareOwner()) {
  190. users.splice(i, 1);
  191. break;
  192. }
  193. }
  194. }
  195. var shares = view.model.get('shares');
  196. var sharesLength = shares.length;
  197. // Now filter out all sharees that are already shared with
  198. for (i = 0; i < sharesLength; i++) {
  199. var share = shares[i];
  200. if (share.share_type === OC.Share.SHARE_TYPE_USER) {
  201. usersLength = users.length;
  202. for (j = 0; j < usersLength; j++) {
  203. if (users[j].value.shareWith === share.share_with) {
  204. users.splice(j, 1);
  205. break;
  206. }
  207. }
  208. } else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) {
  209. groupsLength = groups.length;
  210. for (j = 0; j < groupsLength; j++) {
  211. if (groups[j].value.shareWith === share.share_with) {
  212. groups.splice(j, 1);
  213. break;
  214. }
  215. }
  216. } else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) {
  217. remotesLength = remotes.length;
  218. for (j = 0; j < remotesLength; j++) {
  219. if (remotes[j].value.shareWith === share.share_with) {
  220. remotes.splice(j, 1);
  221. break;
  222. }
  223. }
  224. } else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
  225. emailsLength = emails.length;
  226. for (j = 0; j < emailsLength; j++) {
  227. if (emails[j].value.shareWith === share.share_with) {
  228. emails.splice(j, 1);
  229. break;
  230. }
  231. }
  232. } else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) {
  233. circlesLength = circles.length;
  234. for (j = 0; j < circlesLength; j++) {
  235. if (circles[j].value.shareWith === share.share_with) {
  236. circles.splice(j, 1);
  237. break;
  238. }
  239. }
  240. }
  241. }
  242. var suggestions = users.concat(groups).concat(remotes).concat(emails).concat(circles).concat(lookup);
  243. if (suggestions.length > 0) {
  244. $shareWithField
  245. .autocomplete("option", "autoFocus", true);
  246. response(suggestions);
  247. // show a notice that the list is truncated
  248. // this is the case if one of the search results is at least as long as the max result config option
  249. if(oc_config['sharing.maxAutocompleteResults'] > 0 &&
  250. Math.min(perPage, oc_config['sharing.maxAutocompleteResults'])
  251. <= Math.max(users.length, groups.length, remotes.length, emails.length, lookup.length)) {
  252. var message = t('core', 'This list is maybe truncated - please refine your search term to see more results.');
  253. $('.ui-autocomplete').append('<li class="autocomplete-note">' + message + '</li>');
  254. }
  255. } else {
  256. var title = t('core', 'No users or groups found for {search}', {search: $shareWithField.val()});
  257. if (!view.configModel.get('allowGroupSharing')) {
  258. title = t('core', 'No users found for {search}', {search: $('.shareWithField').val()});
  259. }
  260. $shareWithField.addClass('error')
  261. .attr('data-original-title', title)
  262. .tooltip('hide')
  263. .tooltip({
  264. placement: 'bottom',
  265. trigger: 'manual'
  266. })
  267. .tooltip('fixTitle')
  268. .tooltip('show');
  269. response();
  270. }
  271. } else {
  272. response();
  273. }
  274. }
  275. ).fail(function() {
  276. $loading.addClass('hidden');
  277. $loading.removeClass('inlineblock');
  278. $shareInfo.removeClass('hidden');
  279. OC.Notification.show(t('core', 'An error occurred. Please try again'));
  280. window.setTimeout(OC.Notification.hide, 5000);
  281. });
  282. },
  283. autocompleteRenderItem: function(ul, item) {
  284. var text = item.label;
  285. if (item.value.shareType === OC.Share.SHARE_TYPE_GROUP) {
  286. text = t('core', '{sharee} (group)', { sharee: text }, undefined, { escape: false });
  287. } else if (item.value.shareType === OC.Share.SHARE_TYPE_REMOTE) {
  288. text = t('core', '{sharee} (remote)', { sharee: text }, undefined, { escape: false });
  289. } else if (item.value.shareType === OC.Share.SHARE_TYPE_EMAIL) {
  290. text = t('core', '{sharee} (email)', { sharee: text }, undefined, { escape: false });
  291. } else if (item.value.shareType === OC.Share.SHARE_TYPE_CIRCLE) {
  292. text = t('core', '{sharee} ({type}, {owner})', {sharee: text, type: item.value.circleInfo, owner: item.value.circleOwner}, undefined, {escape: false});
  293. }
  294. var insert = $("<div class='share-autocomplete-item'/>");
  295. var avatar = $("<div class='avatardiv'></div>").appendTo(insert);
  296. if (item.value.shareType === OC.Share.SHARE_TYPE_USER) {
  297. avatar.avatar(item.value.shareWith, 32, undefined, undefined, undefined, item.label);
  298. } else {
  299. avatar.imageplaceholder(text, undefined, 32);
  300. }
  301. $("<div class='autocomplete-item-text'></div>")
  302. .text(text)
  303. .appendTo(insert);
  304. insert.attr('title', item.value.shareWith);
  305. insert = $("<a>")
  306. .append(insert);
  307. return $("<li>")
  308. .addClass((item.value.shareType === OC.Share.SHARE_TYPE_GROUP) ? 'group' : 'user')
  309. .append(insert)
  310. .appendTo(ul);
  311. },
  312. _onSelectRecipient: function(e, s) {
  313. e.preventDefault();
  314. $(e.target).attr('disabled', true)
  315. .val(s.item.label);
  316. var $loading = this.$el.find('.shareWithLoading');
  317. $loading.removeClass('hidden')
  318. .addClass('inlineblock');
  319. var $shareInfo = this.$el.find('.shareWithRemoteInfo');
  320. $shareInfo.addClass('hidden');
  321. this.model.addShare(s.item.value, {success: function() {
  322. $(e.target).val('')
  323. .attr('disabled', false);
  324. $loading.addClass('hidden')
  325. .removeClass('inlineblock');
  326. $shareInfo.removeClass('hidden');
  327. }, error: function(obj, msg) {
  328. OC.Notification.showTemporary(msg);
  329. $(e.target).attr('disabled', false)
  330. .autocomplete('search', $(e.target).val());
  331. $loading.addClass('hidden')
  332. .removeClass('inlineblock');
  333. $shareInfo.removeClass('hidden');
  334. }});
  335. },
  336. _toggleLoading: function(state) {
  337. this._loading = state;
  338. this.$el.find('.subView').toggleClass('hidden', state);
  339. this.$el.find('.loading').toggleClass('hidden', !state);
  340. },
  341. _onRequest: function() {
  342. // only show the loading spinner for the first request (for now)
  343. if (!this._loadingOnce) {
  344. this._toggleLoading(true);
  345. }
  346. },
  347. _onEndRequest: function() {
  348. var self = this;
  349. this._toggleLoading(false);
  350. if (!this._loadingOnce) {
  351. this._loadingOnce = true;
  352. // the first time, focus on the share field after the spinner disappeared
  353. if (!OC.Util.isIE()) {
  354. _.defer(function () {
  355. self.$('.shareWithField').focus();
  356. });
  357. }
  358. }
  359. },
  360. render: function() {
  361. var baseTemplate = this._getTemplate('base', TEMPLATE_BASE);
  362. this.$el.html(baseTemplate({
  363. cid: this.cid,
  364. shareLabel: t('core', 'Share'),
  365. sharePlaceholder: this._renderSharePlaceholderPart(),
  366. shareInfo: this._renderShareInfoPart(),
  367. isSharingAllowed: this.model.sharePermissionPossible()
  368. }));
  369. var $shareField = this.$el.find('.shareWithField');
  370. if ($shareField.length) {
  371. $shareField.autocomplete({
  372. minLength: 1,
  373. delay: 750,
  374. focus: function(event) {
  375. event.preventDefault();
  376. },
  377. source: this.autocompleteHandler,
  378. select: this._onSelectRecipient
  379. }).data('ui-autocomplete')._renderItem = this.autocompleteRenderItem;
  380. }
  381. this.resharerInfoView.$el = this.$el.find('.resharerInfoView');
  382. this.resharerInfoView.render();
  383. this.linkShareView.$el = this.$el.find('.linkShareView');
  384. this.linkShareView.render();
  385. this.expirationView.$el = this.$el.find('.expirationView');
  386. this.expirationView.render();
  387. this.shareeListView.$el = this.$el.find('.shareeListView');
  388. this.shareeListView.render();
  389. this.$el.find('.hasTooltip').tooltip();
  390. return this;
  391. },
  392. /**
  393. * sets whether share by link should be displayed or not. Default is
  394. * true.
  395. *
  396. * @param {bool} showLink
  397. */
  398. setShowLink: function(showLink) {
  399. this._showLink = (typeof showLink === 'boolean') ? showLink : true;
  400. this.linkShareView.showLink = this._showLink;
  401. },
  402. _renderShareInfoPart: function() {
  403. var shareInfo = '';
  404. var infoTemplate = this._getShareInfoTemplate();
  405. if(this.configModel.get('isMailShareAllowed') && this.configModel.get('isRemoteShareAllowed')) {
  406. shareInfo = infoTemplate({
  407. tooltip: t('core', 'Share with other people by entering a user or group, a federated cloud ID or an email address.')
  408. });
  409. } else if(this.configModel.get('isRemoteShareAllowed')) {
  410. shareInfo = infoTemplate({
  411. tooltip: t('core', 'Share with other people by entering a user or group or a federated cloud ID.')
  412. });
  413. } else if(this.configModel.get('isMailShareAllowed')) {
  414. shareInfo = infoTemplate({
  415. tooltip: t('core', 'Share with other people by entering a user or group or an email address.')
  416. });
  417. }
  418. return shareInfo;
  419. },
  420. _renderSharePlaceholderPart: function () {
  421. var allowRemoteSharing = this.configModel.get('isRemoteShareAllowed');
  422. var allowMailSharing = this.configModel.get('isMailShareAllowed');
  423. if (!allowRemoteSharing && allowMailSharing) {
  424. return t('core', 'Name or email address...');
  425. }
  426. if (allowRemoteSharing && !allowMailSharing) {
  427. return t('core', 'Name or federated cloud ID...');
  428. }
  429. if (allowRemoteSharing && allowMailSharing) {
  430. return t('core', 'Name, federated cloud ID or email address...');
  431. }
  432. return t('core', 'Name...');
  433. },
  434. /**
  435. *
  436. * @param {string} key - an identifier for the template
  437. * @param {string} template - the HTML to be compiled by Handlebars
  438. * @returns {Function} from Handlebars
  439. * @private
  440. */
  441. _getTemplate: function (key, template) {
  442. if (!this._templates[key]) {
  443. this._templates[key] = Handlebars.compile(template);
  444. }
  445. return this._templates[key];
  446. },
  447. /**
  448. * returns the info template for remote sharing
  449. *
  450. * @returns {Function}
  451. * @private
  452. */
  453. _getShareInfoTemplate: function() {
  454. return this._getTemplate('shareInfo', TEMPLATE_SHARE_INFO);
  455. }
  456. });
  457. OC.Share.ShareDialogView = ShareDialogView;
  458. })();