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.

commentstabview.js 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. /*
  2. * Copyright (c) 2016
  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. /* global Handlebars, escapeHTML */
  11. (function(OC, OCA) {
  12. /**
  13. * @memberof OCA.Comments
  14. */
  15. var CommentsTabView = OCA.Files.DetailTabView.extend(
  16. /** @lends OCA.Comments.CommentsTabView.prototype */ {
  17. id: 'commentsTabView',
  18. className: 'tab commentsTabView',
  19. _autoCompleteData: undefined,
  20. _commentsModifyMenu: undefined,
  21. events: {
  22. 'submit .newCommentForm': '_onSubmitComment',
  23. 'click .showMore': '_onClickShowMore',
  24. 'click .cancel': '_onClickCloseComment',
  25. 'click .comment': '_onClickComment',
  26. 'keyup div.message': '_onTextChange',
  27. 'change div.message': '_onTextChange',
  28. 'input div.message': '_onTextChange',
  29. 'paste div.message': '_onPaste'
  30. },
  31. _commentMaxLength: 1000,
  32. initialize: function() {
  33. OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
  34. this.collection = new OCA.Comments.CommentCollection();
  35. this.collection.on('request', this._onRequest, this);
  36. this.collection.on('sync', this._onEndRequest, this);
  37. this.collection.on('add', this._onAddModel, this);
  38. this.collection.on('change:message', this._onChangeModel, this);
  39. this._commentMaxThreshold = this._commentMaxLength * 0.9;
  40. // TODO: error handling
  41. _.bindAll(this, '_onTypeComment', '_initAutoComplete', '_onAutoComplete');
  42. },
  43. template: function(params) {
  44. var currentUser = OC.getCurrentUser();
  45. return OCA.Comments.Templates['view'](_.extend({
  46. actorId: currentUser.uid,
  47. actorDisplayName: currentUser.displayName
  48. }, params));
  49. },
  50. editCommentTemplate: function(params) {
  51. var currentUser = OC.getCurrentUser();
  52. return OCA.Comments.Templates['edit_comment'](_.extend({
  53. actorId: currentUser.uid,
  54. actorDisplayName: currentUser.displayName,
  55. newMessagePlaceholder: t('comments', 'New comment …'),
  56. submitText: t('comments', 'Post'),
  57. cancelText: t('comments', 'Cancel'),
  58. tag: 'li'
  59. }, params));
  60. },
  61. commentTemplate: function(params) {
  62. params = _.extend({
  63. editTooltip: t('comments', 'Edit comment'),
  64. isUserAuthor: OC.getCurrentUser().uid === params.actorId,
  65. isLong: this._isLong(params.message)
  66. }, params);
  67. if (params.actorType === 'deleted_users') {
  68. // makes the avatar a X
  69. params.actorId = null;
  70. params.actorDisplayName = t('comments', '[Deleted user]');
  71. }
  72. return OCA.Comments.Templates['comment'](params);
  73. },
  74. getLabel: function() {
  75. return t('comments', 'Comments');
  76. },
  77. getIcon: function() {
  78. return 'icon-comment';
  79. },
  80. setFileInfo: function(fileInfo) {
  81. if (fileInfo) {
  82. this.model = fileInfo;
  83. this.render();
  84. this._initAutoComplete($('#commentsTabView').find('.newCommentForm .message'));
  85. this.collection.setObjectId(this.model.id);
  86. // reset to first page
  87. this.collection.reset([], {silent: true});
  88. this.nextPage();
  89. } else {
  90. this.model = null;
  91. this.render();
  92. this.collection.reset();
  93. }
  94. },
  95. render: function() {
  96. this.$el.html(this.template({
  97. emptyResultLabel: t('comments', 'No comments yet, start the conversation!'),
  98. moreLabel: t('comments', 'More comments …')
  99. }));
  100. this.$el.find('.comments').before(this.editCommentTemplate({ tag: 'div'}));
  101. this.$el.find('.has-tooltip').tooltip();
  102. this.$container = this.$el.find('ul.comments');
  103. this.$el.find('.avatar').avatar(OC.getCurrentUser().uid, 32);
  104. this.delegateEvents();
  105. this.$el.find('.message').on('keydown input change', this._onTypeComment);
  106. autosize(this.$el.find('.newCommentRow .message'))
  107. this.$el.find('.newCommentForm .message').focus();
  108. },
  109. _initAutoComplete: function($target) {
  110. var s = this;
  111. var limit = 10;
  112. if(!_.isUndefined(OC.appConfig.comments)) {
  113. limit = OC.appConfig.comments.maxAutoCompleteResults;
  114. }
  115. $target.atwho({
  116. at: '@',
  117. limit: limit,
  118. callbacks: {
  119. remoteFilter: s._onAutoComplete,
  120. highlighter: function (li) {
  121. // misuse the highlighter callback to instead of
  122. // highlighting loads the avatars.
  123. var $li = $(li);
  124. $li.find('.avatar').avatar(undefined, 32);
  125. return $li;
  126. },
  127. sorter: function (q, items) { return items; }
  128. },
  129. displayTpl: function (item) {
  130. return '<li>' +
  131. '<span class="avatar-name-wrapper">' +
  132. '<span class="avatar" ' +
  133. 'data-username="' + escapeHTML(item.id) + '" ' + // for avatars
  134. 'data-user="' + escapeHTML(item.id) + '" ' + // for contactsmenu
  135. 'data-user-display-name="' + escapeHTML(item.label) + '">' +
  136. '</span>' +
  137. '<strong>' + escapeHTML(item.label) + '</strong>' +
  138. '</span></li>';
  139. },
  140. insertTpl: function (item) {
  141. return '' +
  142. '<span class="avatar-name-wrapper">' +
  143. '<span class="avatar" ' +
  144. 'data-username="' + escapeHTML(item.id) + '" ' + // for avatars
  145. 'data-user="' + escapeHTML(item.id) + '" ' + // for contactsmenu
  146. 'data-user-display-name="' + escapeHTML(item.label) + '">' +
  147. '</span>' +
  148. '<strong>' + escapeHTML(item.label) + '</strong>' +
  149. '</span>';
  150. },
  151. searchKey: "label"
  152. });
  153. $target.on('inserted.atwho', function (je, $el) {
  154. var editionMode = true;
  155. s._postRenderItem(
  156. // we need to pass the parent of the inserted element
  157. // passing the whole comments form would re-apply and request
  158. // avatars from the server
  159. $(je.target).find(
  160. 'span[data-username="' + $el.find('[data-username]').data('username') + '"]'
  161. ).parent(),
  162. editionMode
  163. );
  164. });
  165. },
  166. _onAutoComplete: function(query, callback) {
  167. var s = this;
  168. if(!_.isUndefined(this._autoCompleteRequestTimer)) {
  169. clearTimeout(this._autoCompleteRequestTimer);
  170. }
  171. this._autoCompleteRequestTimer = _.delay(function() {
  172. if(!_.isUndefined(this._autoCompleteRequestCall)) {
  173. this._autoCompleteRequestCall.abort();
  174. }
  175. this._autoCompleteRequestCall = $.ajax({
  176. url: OC.linkToOCS('core', 2) + 'autocomplete/get',
  177. data: {
  178. search: query,
  179. itemType: 'files',
  180. itemId: s.model.get('id'),
  181. sorter: 'commenters|share-recipients',
  182. limit: OC.appConfig.comments.maxAutoCompleteResults
  183. },
  184. beforeSend: function (request) {
  185. request.setRequestHeader('Accept', 'application/json');
  186. },
  187. success: function (result) {
  188. callback(result.ocs.data);
  189. }
  190. });
  191. }, 400);
  192. },
  193. _formatItem: function(commentModel) {
  194. var timestamp = new Date(commentModel.get('creationDateTime')).getTime();
  195. var data = _.extend({
  196. timestamp: timestamp,
  197. date: OC.Util.relativeModifiedDate(timestamp),
  198. altDate: OC.Util.formatDate(timestamp),
  199. formattedMessage: this._formatMessage(commentModel.get('message'), commentModel.get('mentions'))
  200. }, commentModel.attributes);
  201. return data;
  202. },
  203. _toggleLoading: function(state) {
  204. this._loading = state;
  205. this.$el.find('.loading').toggleClass('hidden', !state);
  206. },
  207. _onRequest: function(type) {
  208. if (type === 'REPORT') {
  209. this._toggleLoading(true);
  210. this.$el.find('.showMore').addClass('hidden');
  211. }
  212. },
  213. _onEndRequest: function(type) {
  214. var fileInfoModel = this.model;
  215. this._toggleLoading(false);
  216. this.$el.find('.emptycontent').toggleClass('hidden', !!this.collection.length);
  217. this.$el.find('.showMore').toggleClass('hidden', !this.collection.hasMoreResults());
  218. if (type !== 'REPORT') {
  219. return;
  220. }
  221. // find first unread comment
  222. var firstUnreadComment = this.collection.findWhere({isUnread: true});
  223. if (firstUnreadComment) {
  224. // update read marker
  225. this.collection.updateReadMarker(
  226. null,
  227. {
  228. success: function() {
  229. fileInfoModel.set('commentsUnread', 0);
  230. }
  231. }
  232. );
  233. }
  234. this.$el.find('.newCommentForm .message').focus();
  235. },
  236. /**
  237. * takes care of post-rendering after a new comment was added to the
  238. * collection
  239. *
  240. * @param model
  241. * @param collection
  242. * @param options
  243. * @private
  244. */
  245. _onAddModel: function(model, collection, options) {
  246. // we need to render it immediately, to ensure that the right
  247. // order of comments is kept on opening comments tab
  248. var $comment = $(this.commentTemplate(this._formatItem(model)));
  249. if (!_.isUndefined(options.at) && collection.length > 1) {
  250. this.$container.find('li').eq(options.at).before($comment);
  251. } else {
  252. this.$container.append($comment);
  253. }
  254. this._postRenderItem($comment);
  255. $('#commentsTabView').find('.newCommentForm div.message').text('').prop('contenteditable', true);
  256. // we need to update the model, because it consists of client data
  257. // only, but the server might add meta data, e.g. about mentions
  258. var oldMentions = model.get('mentions');
  259. var self = this;
  260. model.fetch({
  261. success: function (model) {
  262. if(_.isEqual(oldMentions, model.get('mentions'))) {
  263. // don't attempt to render if unnecessary, avoids flickering
  264. return;
  265. }
  266. var $updated = $(self.commentTemplate(self._formatItem(model)));
  267. $comment.html($updated.html());
  268. self._postRenderItem($comment);
  269. }
  270. })
  271. },
  272. /**
  273. * takes care of post-rendering after a new comment was edited
  274. *
  275. * @param model
  276. * @private
  277. */
  278. _onChangeModel: function (model) {
  279. if(model.get('message').trim() === model.previous('message').trim()) {
  280. return;
  281. }
  282. var $form = this.$container.find('.comment[data-id="' + model.id + '"] form');
  283. var $row = $form.closest('.comment');
  284. var $target = $row.data('commentEl');
  285. if(_.isUndefined($target)) {
  286. // ignore noise – this is only set after editing a comment and hitting post
  287. return;
  288. }
  289. var self = this;
  290. // we need to update the model, because it consists of client data
  291. // only, but the server might add meta data, e.g. about mentions
  292. model.fetch({
  293. success: function (model) {
  294. $target.removeClass('hidden');
  295. $row.remove();
  296. var $message = $target.find('.message');
  297. $message
  298. .html(self._formatMessage(model.get('message'), model.get('mentions')))
  299. .find('.avatar')
  300. .each(function () { $(this).avatar(); });
  301. self._postRenderItem($message);
  302. }
  303. });
  304. },
  305. _postRenderItem: function($el, editionMode) {
  306. $el.find('.has-tooltip').tooltip();
  307. var inlineAvatars = $el.find('.message .avatar');
  308. if ($($el.context).hasClass('message')) {
  309. inlineAvatars = $el.find('.avatar');
  310. }
  311. inlineAvatars.each(function () {
  312. var $this = $(this);
  313. $this.avatar($this.attr('data-username'), 16);
  314. });
  315. $el.find('.authorRow .avatar').each(function () {
  316. var $this = $(this);
  317. $this.avatar($this.attr('data-username'), 32);
  318. });
  319. var username = $el.find('.avatar').data('username');
  320. if (username !== oc_current_user) {
  321. $el.find('.authorRow .avatar, .authorRow .author').contactsMenu(
  322. username, 0, $el.find('.authorRow'));
  323. }
  324. var $message = $el.find('.message');
  325. if($message.length === 0) {
  326. // it is the case when writing a comment and mentioning a person
  327. $message = $el;
  328. }
  329. if (!editionMode) {
  330. var self = this;
  331. // add the dropdown menu to display the edit and delete option
  332. var modifyCommentMenu = new OCA.Comments.CommentsModifyMenu();
  333. $el.find('.authorRow').append(modifyCommentMenu.$el);
  334. $el.find('.more').on('click', _.bind(modifyCommentMenu.show, modifyCommentMenu));
  335. self.listenTo(modifyCommentMenu, 'select:menu-item-clicked', function(ev, action) {
  336. if (action === 'edit') {
  337. self._onClickEditComment(ev);
  338. } else if (action === 'delete') {
  339. self._onClickDeleteComment(ev);
  340. }
  341. });
  342. }
  343. this._postRenderMessage($message, editionMode);
  344. },
  345. _postRenderMessage: function($el, editionMode) {
  346. if (editionMode) {
  347. return;
  348. }
  349. $el.find('.avatar-name-wrapper').each(function() {
  350. var $this = $(this);
  351. var $avatar = $this.find('.avatar');
  352. var user = $avatar.data('user');
  353. if (user !== OC.getCurrentUser().uid) {
  354. $this.contactsMenu(user, 0, $this);
  355. }
  356. });
  357. },
  358. /**
  359. * Convert a message to be displayed in HTML,
  360. * converts newlines to <br> tags.
  361. */
  362. _formatMessage: function(message, mentions, editMode) {
  363. message = escapeHTML(message).replace(/\n/g, '<br/>');
  364. for(var i in mentions) {
  365. if(!mentions.hasOwnProperty(i)) {
  366. return;
  367. }
  368. var mention = '@' + mentions[i].mentionId;
  369. if (mentions[i].mentionId.indexOf(' ') !== -1) {
  370. mention = _.escape('@"' + mentions[i].mentionId + '"');
  371. }
  372. // escape possible regex characters in the name
  373. mention = mention.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  374. var regex = new RegExp("(^|\\s)(" + mention + ")\\b", 'g');
  375. if (mentions[i].mentionId.indexOf(' ') !== -1) {
  376. regex = new RegExp("(^|\\s)(" + mention + ")", 'g');
  377. }
  378. var displayName = this._composeHTMLMention(mentions[i].mentionId, mentions[i].mentionDisplayName);
  379. // replace every mention either at the start of the input or after a whitespace
  380. // followed by a non-word character.
  381. message = message.replace(regex,
  382. function(match, p1) {
  383. // to get number of whitespaces (0 vs 1) right
  384. return p1+displayName;
  385. }
  386. );
  387. }
  388. if(editMode !== true) {
  389. message = OCP.Comments.plainToRich(message);
  390. }
  391. return message;
  392. },
  393. _composeHTMLMention: function(uid, displayName) {
  394. var avatar = '' +
  395. '<span class="avatar" ' +
  396. 'data-username="' + _.escape(uid) + '" ' +
  397. 'data-user="' + _.escape(uid) + '" ' +
  398. 'data-user-display-name="' + _.escape(displayName) + '">' +
  399. '</span>';
  400. var isCurrentUser = (uid === OC.getCurrentUser().uid);
  401. return '' +
  402. '<span class="atwho-inserted" contenteditable="false">' +
  403. '<span class="avatar-name-wrapper' + (isCurrentUser ? ' currentUser' : '') + '">' +
  404. avatar +
  405. '<strong>' + _.escape(displayName) + '</strong>' +
  406. '</span>' +
  407. '</span>';
  408. },
  409. nextPage: function() {
  410. if (this._loading || !this.collection.hasMoreResults()) {
  411. return;
  412. }
  413. this.collection.fetchNext();
  414. },
  415. _onClickEditComment: function(ev) {
  416. ev.preventDefault();
  417. var $comment = $(ev.target).closest('.comment');
  418. var commentId = $comment.data('id');
  419. var commentToEdit = this.collection.get(commentId);
  420. var $formRow = $(this.editCommentTemplate(_.extend({
  421. isEditMode: true,
  422. submitText: t('comments', 'Save')
  423. }, commentToEdit.attributes)));
  424. $comment.addClass('hidden').removeClass('collapsed');
  425. // spawn form
  426. $comment.after($formRow);
  427. $formRow.data('commentEl', $comment);
  428. $formRow.find('.message').on('keydown input change', this._onTypeComment);
  429. // copy avatar element from original to avoid flickering
  430. $formRow.find('.avatar:first').replaceWith($comment.find('.avatar:first').clone());
  431. $formRow.find('.has-tooltip').tooltip();
  432. var $message = $formRow.find('.message');
  433. $message
  434. .html(this._formatMessage(commentToEdit.get('message'), commentToEdit.get('mentions'), true))
  435. .find('.avatar')
  436. .each(function () { $(this).avatar(); });
  437. var editionMode = true;
  438. this._postRenderItem($message, editionMode);
  439. // Enable autosize
  440. autosize($formRow.find('.message'));
  441. // enable autocomplete
  442. this._initAutoComplete($formRow.find('.message'));
  443. return false;
  444. },
  445. _onTypeComment: function(ev) {
  446. var $field = $(ev.target);
  447. var len = $field.text().length;
  448. var $submitButton = $field.data('submitButtonEl');
  449. if (!$submitButton) {
  450. $submitButton = $field.closest('form').find('.submit');
  451. $field.data('submitButtonEl', $submitButton);
  452. }
  453. $field.tooltip('hide');
  454. if (len > this._commentMaxThreshold) {
  455. $field.attr('data-original-title', t('comments', 'Allowed characters {count} of {max}', {count: len, max: this._commentMaxLength}));
  456. $field.tooltip({trigger: 'manual'});
  457. $field.tooltip('show');
  458. $field.addClass('error');
  459. }
  460. var limitExceeded = (len > this._commentMaxLength);
  461. $field.toggleClass('error', limitExceeded);
  462. $submitButton.prop('disabled', limitExceeded);
  463. // Submits form with Enter, but Shift+Enter is a new line. If the
  464. // autocomplete popover is being shown Enter does not submit the
  465. // form either; it will be handled by At.js which will add the
  466. // currently selected item to the message.
  467. if (ev.keyCode === 13 && !ev.shiftKey && !$field.atwho('isSelecting')) {
  468. $submitButton.click();
  469. ev.preventDefault();
  470. }
  471. },
  472. _onClickComment: function(ev) {
  473. var $row = $(ev.target);
  474. if (!$row.is('.comment')) {
  475. $row = $row.closest('.comment');
  476. }
  477. $row.removeClass('collapsed');
  478. },
  479. _onClickCloseComment: function(ev) {
  480. ev.preventDefault();
  481. var $row = $(ev.target).closest('.comment');
  482. $row.data('commentEl').removeClass('hidden');
  483. $row.remove();
  484. return false;
  485. },
  486. _onClickDeleteComment: function(ev) {
  487. ev.preventDefault();
  488. var $comment = $(ev.target).closest('.comment');
  489. var commentId = $comment.data('id');
  490. var $loading = $comment.find('.deleteLoading');
  491. var $moreIcon = $comment.find('.more');
  492. $comment.addClass('disabled');
  493. $loading.removeClass('hidden');
  494. $moreIcon.addClass('hidden');
  495. $comment.data('commentEl', $comment);
  496. this.collection.get(commentId).destroy({
  497. success: function() {
  498. $comment.data('commentEl').remove();
  499. $comment.remove();
  500. },
  501. error: function() {
  502. $loading.addClass('hidden');
  503. $moreIcon.removeClass('hidden');
  504. $comment.removeClass('disabled');
  505. OC.Notification.showTemporary(t('comments', 'Error occurred while retrieving comment with ID {id}', {id: commentId}));
  506. }
  507. });
  508. return false;
  509. },
  510. _onClickShowMore: function(ev) {
  511. ev.preventDefault();
  512. this.nextPage();
  513. },
  514. /**
  515. * takes care of updating comment element states after submit (either new
  516. * comment or edit).
  517. *
  518. * @param {OC.Backbone.Model} model
  519. * @param {jQuery} $form
  520. * @private
  521. */
  522. _onSubmitSuccess: function(model, $form) {
  523. var $submit = $form.find('.submit');
  524. var $loading = $form.find('.submitLoading');
  525. $submit.removeClass('hidden');
  526. $loading.addClass('hidden');
  527. },
  528. _commentBodyHTML2Plain: function($el) {
  529. var $comment = $el.clone();
  530. $comment.find('.avatar-name-wrapper').each(function () {
  531. var $this = $(this),
  532. $inserted = $this.parent(),
  533. userId = $this.find('.avatar').data('username');
  534. if (userId.indexOf(' ') !== -1) {
  535. $inserted.html('@"' + userId + '"');
  536. } else {
  537. $inserted.html('@' + userId);
  538. }
  539. });
  540. $comment.html(OCP.Comments.richToPlain($comment.html()));
  541. var oldHtml;
  542. var html = $comment.html();
  543. do {
  544. // replace works one by one
  545. oldHtml = html;
  546. html = oldHtml.replace("<br>", "\n"); // preserve line breaks
  547. } while(oldHtml !== html);
  548. $comment.html(html);
  549. return $comment.text();
  550. },
  551. _onSubmitComment: function(e) {
  552. var self = this;
  553. var $form = $(e.target);
  554. var commentId = $form.closest('.comment').data('id');
  555. var currentUser = OC.getCurrentUser();
  556. var $submit = $form.find('.submit');
  557. var $loading = $form.find('.submitLoading');
  558. var $commentField = $form.find('.message');
  559. var message = $commentField.text().trim();
  560. e.preventDefault();
  561. if (!message.length || message.length > this._commentMaxLength) {
  562. return;
  563. }
  564. $commentField.prop('contenteditable', false);
  565. $submit.addClass('hidden');
  566. $loading.removeClass('hidden');
  567. message = this._commentBodyHTML2Plain($commentField);
  568. if (commentId) {
  569. // edit mode
  570. var comment = this.collection.get(commentId);
  571. comment.save({
  572. message: message
  573. }, {
  574. success: function(model) {
  575. self._onSubmitSuccess(model, $form);
  576. if(model.get('message').trim() === model.previous('message').trim()) {
  577. // model change event doesn't trigger, manually remove the row.
  578. var $row = $form.closest('.comment');
  579. $row.data('commentEl').removeClass('hidden');
  580. $row.remove();
  581. }
  582. },
  583. error: function() {
  584. self._onSubmitError($form, commentId);
  585. }
  586. });
  587. } else {
  588. this.collection.create({
  589. actorId: currentUser.uid,
  590. actorDisplayName: currentUser.displayName,
  591. actorType: 'users',
  592. verb: 'comment',
  593. message: message,
  594. creationDateTime: (new Date()).toUTCString()
  595. }, {
  596. at: 0,
  597. // wait for real creation before adding
  598. wait: true,
  599. success: function(model) {
  600. self._onSubmitSuccess(model, $form);
  601. },
  602. error: function() {
  603. self._onSubmitError($form, undefined);
  604. }
  605. });
  606. }
  607. return false;
  608. },
  609. /**
  610. * takes care of updating the UI after an error on submit (either new
  611. * comment or edit).
  612. *
  613. * @param {jQuery} $form
  614. * @param {string|undefined} commentId
  615. * @private
  616. */
  617. _onSubmitError: function($form, commentId) {
  618. $form.find('.submit').removeClass('hidden');
  619. $form.find('.submitLoading').addClass('hidden');
  620. $form.find('.message').prop('contenteditable', true);
  621. if(!_.isUndefined(commentId)) {
  622. OC.Notification.show(t('comments', 'Error occurred while updating comment with id {id}', {id: commentId}), {type: 'error'});
  623. } else {
  624. OC.Notification.show(t('comments', 'Error occurred while posting comment'), {type: 'error'});
  625. }
  626. },
  627. /**
  628. * ensures the contenteditable div is really empty, when user removed
  629. * all input, so that the placeholder will be shown again
  630. *
  631. * @private
  632. */
  633. _onTextChange: function() {
  634. var $message = $('#commentsTabView').find('.newCommentForm div.message');
  635. if(!$message.text().trim().length) {
  636. $message.empty();
  637. }
  638. },
  639. /**
  640. * Limit pasting to plain text
  641. *
  642. * @param e
  643. * @private
  644. */
  645. _onPaste: function (e) {
  646. e.preventDefault();
  647. var text = e.originalEvent.clipboardData.getData("text/plain");
  648. document.execCommand('insertText', false, text);
  649. },
  650. /**
  651. * Returns whether the given message is long and needs
  652. * collapsing
  653. */
  654. _isLong: function(message) {
  655. return message.length > 250 || (message.match(/\n/g) || []).length > 1;
  656. }
  657. });
  658. OCA.Comments.CommentsTabView = CommentsTabView;
  659. })(OC, OCA);