checklists.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. const { calculateIndexData, capitalize } = Utils;
  2. function initSorting(items) {
  3. items.sortable({
  4. tolerance: 'pointer',
  5. helper: 'clone',
  6. items: '.js-checklist-item:not(.placeholder)',
  7. connectWith: '.js-checklist-items',
  8. appendTo: 'parent',
  9. distance: 7,
  10. placeholder: 'checklist-item placeholder',
  11. scroll: false,
  12. start(evt, ui) {
  13. ui.placeholder.height(ui.helper.height());
  14. EscapeActions.executeUpTo('popup-close');
  15. },
  16. stop(evt, ui) {
  17. const parent = ui.item.parents('.js-checklist-items');
  18. const checklistId = Blaze.getData(parent.get(0)).checklist._id;
  19. let prevItem = ui.item.prev('.js-checklist-item').get(0);
  20. if (prevItem) {
  21. prevItem = Blaze.getData(prevItem).item;
  22. }
  23. let nextItem = ui.item.next('.js-checklist-item').get(0);
  24. if (nextItem) {
  25. nextItem = Blaze.getData(nextItem).item;
  26. }
  27. const nItems = 1;
  28. const sortIndex = calculateIndexData(prevItem, nextItem, nItems);
  29. const checklistDomElement = ui.item.get(0);
  30. const checklistData = Blaze.getData(checklistDomElement);
  31. const checklistItem = checklistData.item;
  32. items.sortable('cancel');
  33. checklistItem.move(checklistId, sortIndex.base);
  34. },
  35. });
  36. }
  37. BlazeComponent.extendComponent({
  38. onRendered() {
  39. const self = this;
  40. self.itemsDom = this.$('.js-checklist-items');
  41. initSorting(self.itemsDom);
  42. self.itemsDom.mousedown(function(evt) {
  43. evt.stopPropagation();
  44. });
  45. function userIsMember() {
  46. return Meteor.user() && Meteor.user().isBoardMember();
  47. }
  48. // Disable sorting if the current user is not a board member or is a miniscreen
  49. self.autorun(() => {
  50. const $itemsDom = $(self.itemsDom);
  51. if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
  52. $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
  53. if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
  54. $(self.itemsDom).sortable({
  55. handle: 'span.fa.checklistitem-handle',
  56. });
  57. }
  58. }
  59. });
  60. },
  61. canModifyCard() {
  62. return (
  63. Meteor.user() &&
  64. Meteor.user().isBoardMember() &&
  65. !Meteor.user().isCommentOnly() &&
  66. !Meteor.user().isWorker()
  67. );
  68. },
  69. }).register('checklistDetail');
  70. BlazeComponent.extendComponent({
  71. addChecklist(event) {
  72. event.preventDefault();
  73. const textarea = this.find('textarea.js-add-checklist-item');
  74. const title = textarea.value.trim();
  75. let cardId = this.currentData().cardId;
  76. const card = Cards.findOne(cardId);
  77. if (card.isLinked()) cardId = card.linkedId;
  78. if (title) {
  79. Checklists.insert({
  80. cardId,
  81. title,
  82. sort: card.checklists().count(),
  83. });
  84. setTimeout(() => {
  85. this.$('.add-checklist-item')
  86. .last()
  87. .click();
  88. }, 100);
  89. }
  90. textarea.value = '';
  91. textarea.focus();
  92. },
  93. addChecklistItem(event) {
  94. event.preventDefault();
  95. const textarea = this.find('textarea.js-add-checklist-item');
  96. const title = textarea.value.trim();
  97. const checklist = this.currentData().checklist;
  98. if (title) {
  99. ChecklistItems.insert({
  100. title,
  101. checklistId: checklist._id,
  102. cardId: checklist.cardId,
  103. sort: checklist.itemCount(),
  104. });
  105. }
  106. // We keep the form opened, empty it.
  107. textarea.value = '';
  108. textarea.focus();
  109. },
  110. canModifyCard() {
  111. return (
  112. Meteor.user() &&
  113. Meteor.user().isBoardMember() &&
  114. !Meteor.user().isCommentOnly() &&
  115. !Meteor.user().isWorker()
  116. );
  117. },
  118. deleteChecklist() {
  119. const checklist = this.currentData().checklist;
  120. if (checklist && checklist._id) {
  121. Checklists.remove(checklist._id);
  122. this.toggleDeleteDialog.set(false);
  123. }
  124. },
  125. deleteItem() {
  126. const checklist = this.currentData().checklist;
  127. const item = this.currentData().item;
  128. if (checklist && item && item._id) {
  129. ChecklistItems.remove(item._id);
  130. }
  131. },
  132. editChecklist(event) {
  133. event.preventDefault();
  134. const textarea = this.find('textarea.js-edit-checklist-item');
  135. const title = textarea.value.trim();
  136. const checklist = this.currentData().checklist;
  137. checklist.setTitle(title);
  138. },
  139. editChecklistItem(event) {
  140. event.preventDefault();
  141. const textarea = this.find('textarea.js-edit-checklist-item');
  142. const title = textarea.value.trim();
  143. const item = this.currentData().item;
  144. item.setTitle(title);
  145. },
  146. onCreated() {
  147. this.toggleDeleteDialog = new ReactiveVar(false);
  148. this.checklistToDelete = null; //Store data context to pass to checklistDeleteDialog template
  149. },
  150. pressKey(event) {
  151. //If user press enter key inside a form, submit it
  152. //Unless the user is also holding down the 'shift' key
  153. if (event.keyCode === 13 && !event.shiftKey) {
  154. event.preventDefault();
  155. const $form = $(event.currentTarget).closest('form');
  156. $form.find('button[type=submit]').click();
  157. }
  158. },
  159. focusChecklistItem(event) {
  160. // If a new checklist is created, pre-fill the title and select it.
  161. const checklist = this.currentData().checklist;
  162. if (!checklist) {
  163. const textarea = event.target;
  164. textarea.value = capitalize(TAPi18n.__('r-checklist'));
  165. textarea.select();
  166. }
  167. },
  168. events() {
  169. const events = {
  170. 'click .toggle-delete-checklist-dialog'(event) {
  171. if ($(event.target).hasClass('js-delete-checklist')) {
  172. this.checklistToDelete = this.currentData().checklist; //Store data context
  173. }
  174. this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
  175. },
  176. 'click #toggleHideCheckedItemsButton'() {
  177. Meteor.call('toggleHideCheckedItems');
  178. },
  179. };
  180. return [
  181. {
  182. ...events,
  183. 'submit .js-add-checklist': this.addChecklist,
  184. 'submit .js-edit-checklist-title': this.editChecklist,
  185. 'submit .js-add-checklist-item': this.addChecklistItem,
  186. 'submit .js-edit-checklist-item': this.editChecklistItem,
  187. 'click .js-delete-checklist-item': this.deleteItem,
  188. 'click .confirm-checklist-delete': this.deleteChecklist,
  189. 'focus .js-add-checklist-item': this.focusChecklistItem,
  190. keydown: this.pressKey,
  191. },
  192. ];
  193. },
  194. }).register('checklists');
  195. Template.checklists.helpers({
  196. hideCheckedItems() {
  197. const currentUser = Meteor.user();
  198. if (currentUser) return currentUser.hasHideCheckedItems();
  199. return false;
  200. },
  201. });
  202. Template.addChecklistItemForm.onRendered(() => {
  203. autosize($('textarea.js-add-checklist-item'));
  204. });
  205. Template.editChecklistItemForm.onRendered(() => {
  206. autosize($('textarea.js-edit-checklist-item'));
  207. });
  208. Template.checklistDeleteDialog.onCreated(() => {
  209. const $cardDetails = this.$('.card-details');
  210. this.scrollState = {
  211. position: $cardDetails.scrollTop(), //save current scroll position
  212. top: false, //required for smooth scroll animation
  213. };
  214. //Callback's purpose is to only prevent scrolling after animation is complete
  215. $cardDetails.animate({ scrollTop: 0 }, 500, () => {
  216. this.scrollState.top = true;
  217. });
  218. //Prevent scrolling while dialog is open
  219. $cardDetails.on('scroll', () => {
  220. if (this.scrollState.top) {
  221. //If it's already in position, keep it there. Otherwise let animation scroll
  222. $cardDetails.scrollTop(0);
  223. }
  224. });
  225. });
  226. Template.checklistDeleteDialog.onDestroyed(() => {
  227. const $cardDetails = this.$('.card-details');
  228. $cardDetails.off('scroll'); //Reactivate scrolling
  229. $cardDetails.animate({ scrollTop: this.scrollState.position });
  230. });
  231. Template.checklistItemDetail.helpers({
  232. canModifyCard() {
  233. return (
  234. Meteor.user() &&
  235. Meteor.user().isBoardMember() &&
  236. !Meteor.user().isCommentOnly() &&
  237. !Meteor.user().isWorker()
  238. );
  239. },
  240. hideCheckedItems() {
  241. const user = Meteor.user();
  242. if (user) return user.hasHideCheckedItems();
  243. return false;
  244. },
  245. });
  246. BlazeComponent.extendComponent({
  247. toggleItem() {
  248. const checklist = this.currentData().checklist;
  249. const item = this.currentData().item;
  250. if (checklist && item && item._id) {
  251. item.toggleItem();
  252. }
  253. },
  254. events() {
  255. return [
  256. {
  257. 'click .js-checklist-item .check-box-container': this.toggleItem,
  258. },
  259. ];
  260. },
  261. }).register('checklistItemDetail');