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 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  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. ' <span class="shareWithConfirm icon icon-confirm"></span>' +
  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. /**
  30. * @class OCA.Share.ShareDialogView
  31. * @member {OC.Share.ShareItemModel} model
  32. * @member {jQuery} $el
  33. * @memberof OCA.Sharing
  34. * @classdesc
  35. *
  36. * Represents the GUI of the share dialogue
  37. *
  38. */
  39. var ShareDialogView = OC.Backbone.View.extend({
  40. /** @type {Object} **/
  41. _templates: {},
  42. /** @type {boolean} **/
  43. _showLink: true,
  44. /** @type {string} **/
  45. tagName: 'div',
  46. /** @type {OC.Share.ShareConfigModel} **/
  47. configModel: undefined,
  48. /** @type {object} **/
  49. resharerInfoView: undefined,
  50. /** @type {object} **/
  51. linkShareView: undefined,
  52. /** @type {object} **/
  53. expirationView: undefined,
  54. /** @type {object} **/
  55. shareeListView: undefined,
  56. /** @type {object} **/
  57. _lastSuggestions: undefined,
  58. /** @type {int} **/
  59. _pendingOperationsCount: 0,
  60. events: {
  61. 'focus .shareWithField': 'onShareWithFieldFocus',
  62. 'input .shareWithField': 'onShareWithFieldChanged',
  63. 'click .shareWithConfirm': '_confirmShare'
  64. },
  65. initialize: function(options) {
  66. var view = this;
  67. this.model.on('fetchError', function() {
  68. OC.Notification.showTemporary(t('core', 'Share details could not be loaded for this item.'));
  69. });
  70. if(!_.isUndefined(options.configModel)) {
  71. this.configModel = options.configModel;
  72. } else {
  73. throw 'missing OC.Share.ShareConfigModel';
  74. }
  75. this.configModel.on('change:isRemoteShareAllowed', function() {
  76. view.render();
  77. });
  78. this.model.on('change:permissions', function() {
  79. view.render();
  80. });
  81. this.model.on('request', this._onRequest, this);
  82. this.model.on('sync', this._onEndRequest, this);
  83. var subViewOptions = {
  84. model: this.model,
  85. configModel: this.configModel
  86. };
  87. var subViews = {
  88. resharerInfoView: 'ShareDialogResharerInfoView',
  89. linkShareView: 'ShareDialogLinkShareView',
  90. expirationView: 'ShareDialogExpirationView',
  91. shareeListView: 'ShareDialogShareeListView'
  92. };
  93. for(var name in subViews) {
  94. var className = subViews[name];
  95. this[name] = _.isUndefined(options[name])
  96. ? new OC.Share[className](subViewOptions)
  97. : options[name];
  98. }
  99. _.bindAll(this,
  100. 'autocompleteHandler',
  101. '_onSelectRecipient',
  102. 'onShareWithFieldChanged',
  103. 'onShareWithFieldFocus'
  104. );
  105. OC.Plugins.attach('OC.Share.ShareDialogView', this);
  106. },
  107. onShareWithFieldChanged: function() {
  108. var $el = this.$el.find('.shareWithField');
  109. if ($el.val().length < 2) {
  110. $el.removeClass('error').tooltip('hide');
  111. }
  112. },
  113. /* trigger search after the field was re-selected */
  114. onShareWithFieldFocus: function() {
  115. this.$el.find('.shareWithField').autocomplete("search");
  116. },
  117. _getSuggestions: function(searchTerm, perPage, model) {
  118. if (this._lastSuggestions &&
  119. this._lastSuggestions.searchTerm === searchTerm &&
  120. this._lastSuggestions.perPage === perPage &&
  121. this._lastSuggestions.model === model) {
  122. return this._lastSuggestions.promise;
  123. }
  124. var deferred = $.Deferred();
  125. $.get(
  126. OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
  127. {
  128. format: 'json',
  129. search: searchTerm,
  130. perPage: perPage,
  131. itemType: model.get('itemType')
  132. },
  133. function (result) {
  134. if (result.ocs.meta.statuscode === 100) {
  135. var filter = function(users, groups, remotes, emails, circles) {
  136. if (typeof(emails) === 'undefined') {
  137. emails = [];
  138. }
  139. if (typeof(circles) === 'undefined') {
  140. circles = [];
  141. }
  142. var usersLength;
  143. var groupsLength;
  144. var remotesLength;
  145. var emailsLength;
  146. var circlesLength;
  147. var i, j;
  148. //Filter out the current user
  149. usersLength = users.length;
  150. for (i = 0; i < usersLength; i++) {
  151. if (users[i].value.shareWith === OC.currentUser) {
  152. users.splice(i, 1);
  153. break;
  154. }
  155. }
  156. // Filter out the owner of the share
  157. if (model.hasReshare()) {
  158. usersLength = users.length;
  159. for (i = 0 ; i < usersLength; i++) {
  160. if (users[i].value.shareWith === model.getReshareOwner()) {
  161. users.splice(i, 1);
  162. break;
  163. }
  164. }
  165. }
  166. var shares = model.get('shares');
  167. var sharesLength = shares.length;
  168. // Now filter out all sharees that are already shared with
  169. for (i = 0; i < sharesLength; i++) {
  170. var share = shares[i];
  171. if (share.share_type === OC.Share.SHARE_TYPE_USER) {
  172. usersLength = users.length;
  173. for (j = 0; j < usersLength; j++) {
  174. if (users[j].value.shareWith === share.share_with) {
  175. users.splice(j, 1);
  176. break;
  177. }
  178. }
  179. } else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) {
  180. groupsLength = groups.length;
  181. for (j = 0; j < groupsLength; j++) {
  182. if (groups[j].value.shareWith === share.share_with) {
  183. groups.splice(j, 1);
  184. break;
  185. }
  186. }
  187. } else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) {
  188. remotesLength = remotes.length;
  189. for (j = 0; j < remotesLength; j++) {
  190. if (remotes[j].value.shareWith === share.share_with) {
  191. remotes.splice(j, 1);
  192. break;
  193. }
  194. }
  195. } else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
  196. emailsLength = emails.length;
  197. for (j = 0; j < emailsLength; j++) {
  198. if (emails[j].value.shareWith === share.share_with) {
  199. emails.splice(j, 1);
  200. break;
  201. }
  202. }
  203. } else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) {
  204. circlesLength = circles.length;
  205. for (j = 0; j < circlesLength; j++) {
  206. if (circles[j].value.shareWith === share.share_with) {
  207. circles.splice(j, 1);
  208. break;
  209. }
  210. }
  211. }
  212. }
  213. };
  214. filter(
  215. result.ocs.data.exact.users,
  216. result.ocs.data.exact.groups,
  217. result.ocs.data.exact.remotes,
  218. result.ocs.data.exact.emails,
  219. result.ocs.data.exact.circles
  220. );
  221. var exactUsers = result.ocs.data.exact.users;
  222. var exactGroups = result.ocs.data.exact.groups;
  223. var exactRemotes = result.ocs.data.exact.remotes;
  224. var exactEmails = [];
  225. if (typeof(result.ocs.data.emails) !== 'undefined') {
  226. exactEmails = result.ocs.data.exact.emails;
  227. }
  228. var exactCircles = [];
  229. if (typeof(result.ocs.data.circles) !== 'undefined') {
  230. exactCircles = result.ocs.data.exact.circles;
  231. }
  232. var exactMatches = exactUsers.concat(exactGroups).concat(exactRemotes).concat(exactEmails).concat(exactCircles);
  233. filter(
  234. result.ocs.data.users,
  235. result.ocs.data.groups,
  236. result.ocs.data.remotes,
  237. result.ocs.data.emails,
  238. result.ocs.data.circles
  239. );
  240. var users = result.ocs.data.users;
  241. var groups = result.ocs.data.groups;
  242. var remotes = result.ocs.data.remotes;
  243. var lookup = result.ocs.data.lookup;
  244. var emails = [];
  245. if (typeof(result.ocs.data.emails) !== 'undefined') {
  246. emails = result.ocs.data.emails;
  247. }
  248. var circles = [];
  249. if (typeof(result.ocs.data.circles) !== 'undefined') {
  250. circles = result.ocs.data.circles;
  251. }
  252. var suggestions = exactMatches.concat(users).concat(groups).concat(remotes).concat(emails).concat(circles).concat(lookup);
  253. deferred.resolve(suggestions, exactMatches);
  254. } else {
  255. deferred.reject(result.ocs.meta.message);
  256. }
  257. }
  258. ).fail(function() {
  259. deferred.reject();
  260. });
  261. this._lastSuggestions = {
  262. searchTerm: searchTerm,
  263. perPage: perPage,
  264. model: model,
  265. promise: deferred.promise()
  266. };
  267. return this._lastSuggestions.promise;
  268. },
  269. autocompleteHandler: function (search, response) {
  270. var $shareWithField = $('.shareWithField'),
  271. view = this,
  272. $loading = this.$el.find('.shareWithLoading'),
  273. $confirm = this.$el.find('.shareWithConfirm');
  274. var count = oc_config['sharing.minSearchStringLength'];
  275. if (search.term.trim().length < count) {
  276. var title = n('core',
  277. 'At least {count} character is needed for autocompletion',
  278. 'At least {count} characters are needed for autocompletion',
  279. count,
  280. { count: count }
  281. );
  282. $shareWithField.addClass('error')
  283. .attr('data-original-title', title)
  284. .tooltip('hide')
  285. .tooltip({
  286. placement: 'bottom',
  287. trigger: 'manual'
  288. })
  289. .tooltip('fixTitle')
  290. .tooltip('show');
  291. response();
  292. return;
  293. }
  294. $loading.removeClass('hidden');
  295. $loading.addClass('inlineblock');
  296. $confirm.addClass('hidden');
  297. this._pendingOperationsCount++;
  298. $shareWithField.removeClass('error')
  299. .tooltip('hide');
  300. var perPage = 200;
  301. this._getSuggestions(
  302. search.term.trim(),
  303. perPage,
  304. view.model
  305. ).done(function(suggestions) {
  306. view._pendingOperationsCount--;
  307. if (view._pendingOperationsCount === 0) {
  308. $loading.addClass('hidden');
  309. $loading.removeClass('inlineblock');
  310. $confirm.removeClass('hidden');
  311. }
  312. if (suggestions.length > 0) {
  313. $shareWithField
  314. .autocomplete("option", "autoFocus", true);
  315. response(suggestions);
  316. // show a notice that the list is truncated
  317. // this is the case if one of the search results is at least as long as the max result config option
  318. if(oc_config['sharing.maxAutocompleteResults'] > 0 &&
  319. Math.min(perPage, oc_config['sharing.maxAutocompleteResults'])
  320. <= Math.max(users.length, groups.length, remotes.length, emails.length, lookup.length)) {
  321. var message = t('core', 'This list is maybe truncated - please refine your search term to see more results.');
  322. $('.ui-autocomplete').append('<li class="autocomplete-note">' + message + '</li>');
  323. }
  324. } else {
  325. var title = t('core', 'No users or groups found for {search}', {search: $shareWithField.val()});
  326. if (!view.configModel.get('allowGroupSharing')) {
  327. title = t('core', 'No users found for {search}', {search: $('.shareWithField').val()});
  328. }
  329. $shareWithField.addClass('error')
  330. .attr('data-original-title', title)
  331. .tooltip('hide')
  332. .tooltip({
  333. placement: 'bottom',
  334. trigger: 'manual'
  335. })
  336. .tooltip('fixTitle')
  337. .tooltip('show');
  338. response();
  339. }
  340. }).fail(function(message) {
  341. view._pendingOperationsCount--;
  342. if (view._pendingOperationsCount === 0) {
  343. $loading.addClass('hidden');
  344. $loading.removeClass('inlineblock');
  345. $confirm.removeClass('hidden');
  346. }
  347. if (message) {
  348. OC.Notification.showTemporary(t('core', 'An error occurred ("{message}"). Please try again', { message: message }));
  349. } else {
  350. OC.Notification.showTemporary(t('core', 'An error occurred. Please try again'));
  351. }
  352. });
  353. },
  354. autocompleteRenderItem: function(ul, item) {
  355. var text = item.label;
  356. if (item.value.shareType === OC.Share.SHARE_TYPE_GROUP) {
  357. text = t('core', '{sharee} (group)', { sharee: text }, undefined, { escape: false });
  358. } else if (item.value.shareType === OC.Share.SHARE_TYPE_REMOTE) {
  359. text = t('core', '{sharee} (remote)', { sharee: text }, undefined, { escape: false });
  360. } else if (item.value.shareType === OC.Share.SHARE_TYPE_EMAIL) {
  361. text = t('core', '{sharee} (email)', { sharee: text }, undefined, { escape: false });
  362. } else if (item.value.shareType === OC.Share.SHARE_TYPE_CIRCLE) {
  363. text = t('core', '{sharee} ({type}, {owner})', {sharee: text, type: item.value.circleInfo, owner: item.value.circleOwner}, undefined, {escape: false});
  364. }
  365. var insert = $("<div class='share-autocomplete-item'/>");
  366. var avatar = $("<div class='avatardiv'></div>").appendTo(insert);
  367. if (item.value.shareType === OC.Share.SHARE_TYPE_USER || item.value.shareType === OC.Share.SHARE_TYPE_CIRCLE) {
  368. avatar.avatar(item.value.shareWith, 32, undefined, undefined, undefined, item.label);
  369. } else {
  370. avatar.imageplaceholder(text, undefined, 32);
  371. }
  372. $("<div class='autocomplete-item-text'></div>")
  373. .text(text)
  374. .appendTo(insert);
  375. insert.attr('title', item.value.shareWith);
  376. insert = $("<a>")
  377. .append(insert);
  378. return $("<li>")
  379. .addClass((item.value.shareType === OC.Share.SHARE_TYPE_GROUP) ? 'group' : 'user')
  380. .append(insert)
  381. .appendTo(ul);
  382. },
  383. _onSelectRecipient: function(e, s) {
  384. var self = this;
  385. e.preventDefault();
  386. // Ensure that the keydown handler for the input field is not
  387. // called; otherwise it would try to add the recipient again, which
  388. // would fail.
  389. e.stopImmediatePropagation();
  390. $(e.target).attr('disabled', true)
  391. .val(s.item.label);
  392. var $loading = this.$el.find('.shareWithLoading');
  393. var $confirm = this.$el.find('.shareWithConfirm');
  394. $loading.removeClass('hidden');
  395. $loading.addClass('inlineblock');
  396. $confirm.addClass('hidden');
  397. this._pendingOperationsCount++;
  398. this.model.addShare(s.item.value, {success: function() {
  399. // Adding a share changes the suggestions.
  400. self._lastSuggestions = undefined;
  401. $(e.target).val('')
  402. .attr('disabled', false);
  403. self._pendingOperationsCount--;
  404. if (self._pendingOperationsCount === 0) {
  405. $loading.addClass('hidden');
  406. $loading.removeClass('inlineblock');
  407. $confirm.removeClass('hidden');
  408. }
  409. }, error: function(obj, msg) {
  410. OC.Notification.showTemporary(msg);
  411. $(e.target).attr('disabled', false)
  412. .autocomplete('search', $(e.target).val());
  413. self._pendingOperationsCount--;
  414. if (self._pendingOperationsCount === 0) {
  415. $loading.addClass('hidden');
  416. $loading.removeClass('inlineblock');
  417. $confirm.removeClass('hidden');
  418. }
  419. }});
  420. },
  421. _confirmShare: function() {
  422. var self = this;
  423. var $shareWithField = $('.shareWithField');
  424. var $loading = this.$el.find('.shareWithLoading');
  425. var $confirm = this.$el.find('.shareWithConfirm');
  426. $loading.removeClass('hidden');
  427. $loading.addClass('inlineblock');
  428. $confirm.addClass('hidden');
  429. this._pendingOperationsCount++;
  430. $shareWithField.prop('disabled', true);
  431. // Disabling the autocompletion does not clear its search timeout;
  432. // removing the focus from the input field does, but only if the
  433. // autocompletion is not disabled when the field loses the focus.
  434. // Thus, the field has to be disabled before disabling the
  435. // autocompletion to prevent an old pending search result from
  436. // appearing once the field is enabled again.
  437. $shareWithField.autocomplete('close');
  438. $shareWithField.autocomplete('disable');
  439. var restoreUI = function() {
  440. self._pendingOperationsCount--;
  441. if (self._pendingOperationsCount === 0) {
  442. $loading.addClass('hidden');
  443. $loading.removeClass('inlineblock');
  444. $confirm.removeClass('hidden');
  445. }
  446. $shareWithField.prop('disabled', false);
  447. $shareWithField.focus();
  448. };
  449. var perPage = 200;
  450. var onlyExactMatches = true;
  451. this._getSuggestions(
  452. $shareWithField.val(),
  453. perPage,
  454. this.model,
  455. onlyExactMatches
  456. ).done(function(suggestions, exactMatches) {
  457. if (suggestions.length === 0) {
  458. restoreUI();
  459. $shareWithField.autocomplete('enable');
  460. // There is no need to show an error message here; it will
  461. // be automatically shown when the autocomplete is activated
  462. // again (due to the focus on the field) and it finds no
  463. // matches.
  464. return;
  465. }
  466. if (exactMatches.length !== 1) {
  467. restoreUI();
  468. $shareWithField.autocomplete('enable');
  469. return;
  470. }
  471. var actionSuccess = function() {
  472. // Adding a share changes the suggestions.
  473. self._lastSuggestions = undefined;
  474. $shareWithField.val('');
  475. restoreUI();
  476. $shareWithField.autocomplete('enable');
  477. };
  478. var actionError = function(obj, msg) {
  479. restoreUI();
  480. $shareWithField.autocomplete('enable');
  481. OC.Notification.showTemporary(msg);
  482. };
  483. self.model.addShare(exactMatches[0].value, {
  484. success: actionSuccess,
  485. error: actionError
  486. });
  487. }).fail(function(message) {
  488. restoreUI();
  489. $shareWithField.autocomplete('enable');
  490. // There is no need to show an error message here; it will be
  491. // automatically shown when the autocomplete is activated again
  492. // (due to the focus on the field) and getting the suggestions
  493. // fail.
  494. });
  495. },
  496. _toggleLoading: function(state) {
  497. this._loading = state;
  498. this.$el.find('.subView').toggleClass('hidden', state);
  499. this.$el.find('.loading').toggleClass('hidden', !state);
  500. },
  501. _onRequest: function() {
  502. // only show the loading spinner for the first request (for now)
  503. if (!this._loadingOnce) {
  504. this._toggleLoading(true);
  505. }
  506. },
  507. _onEndRequest: function() {
  508. var self = this;
  509. this._toggleLoading(false);
  510. if (!this._loadingOnce) {
  511. this._loadingOnce = true;
  512. // the first time, focus on the share field after the spinner disappeared
  513. if (!OC.Util.isIE()) {
  514. _.defer(function () {
  515. self.$('.shareWithField').focus();
  516. });
  517. }
  518. }
  519. },
  520. render: function() {
  521. var self = this;
  522. var baseTemplate = this._getTemplate('base', TEMPLATE_BASE);
  523. this.$el.html(baseTemplate({
  524. cid: this.cid,
  525. shareLabel: t('core', 'Share'),
  526. sharePlaceholder: this._renderSharePlaceholderPart(),
  527. isSharingAllowed: this.model.sharePermissionPossible()
  528. }));
  529. var $shareField = this.$el.find('.shareWithField');
  530. if ($shareField.length) {
  531. var shareFieldKeydownHandler = function(event) {
  532. if (event.keyCode !== 13) {
  533. return true;
  534. }
  535. self._confirmShare();
  536. return false;
  537. };
  538. $shareField.autocomplete({
  539. minLength: 1,
  540. delay: 750,
  541. focus: function(event) {
  542. event.preventDefault();
  543. },
  544. source: this.autocompleteHandler,
  545. select: this._onSelectRecipient
  546. }).data('ui-autocomplete')._renderItem = this.autocompleteRenderItem;
  547. $shareField.on('keydown', null, shareFieldKeydownHandler);
  548. }
  549. this.resharerInfoView.$el = this.$el.find('.resharerInfoView');
  550. this.resharerInfoView.render();
  551. this.linkShareView.$el = this.$el.find('.linkShareView');
  552. this.linkShareView.render();
  553. this.expirationView.$el = this.$el.find('.expirationView');
  554. this.expirationView.render();
  555. this.shareeListView.$el = this.$el.find('.shareeListView');
  556. this.shareeListView.render();
  557. this.$el.find('.hasTooltip').tooltip();
  558. return this;
  559. },
  560. /**
  561. * sets whether share by link should be displayed or not. Default is
  562. * true.
  563. *
  564. * @param {bool} showLink
  565. */
  566. setShowLink: function(showLink) {
  567. this._showLink = (typeof showLink === 'boolean') ? showLink : true;
  568. this.linkShareView.showLink = this._showLink;
  569. },
  570. _renderSharePlaceholderPart: function () {
  571. var allowRemoteSharing = this.configModel.get('isRemoteShareAllowed');
  572. var allowMailSharing = this.configModel.get('isMailShareAllowed');
  573. if (!allowRemoteSharing && allowMailSharing) {
  574. return t('core', 'Name or email address...');
  575. }
  576. if (allowRemoteSharing && !allowMailSharing) {
  577. return t('core', 'Name or federated cloud ID...');
  578. }
  579. if (allowRemoteSharing && allowMailSharing) {
  580. return t('core', 'Name, federated cloud ID or email address...');
  581. }
  582. return t('core', 'Name...');
  583. },
  584. /**
  585. *
  586. * @param {string} key - an identifier for the template
  587. * @param {string} template - the HTML to be compiled by Handlebars
  588. * @returns {Function} from Handlebars
  589. * @private
  590. */
  591. _getTemplate: function (key, template) {
  592. if (!this._templates[key]) {
  593. this._templates[key] = Handlebars.compile(template);
  594. }
  595. return this._templates[key];
  596. },
  597. });
  598. OC.Share.ShareDialogView = ShareDialogView;
  599. })();