checklists.js 9.7 KB

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