checklists.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. import Cards from '/models/cards';
  4. import Boards from '/models/boards';
  5. import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
  6. const subManager = new SubsManager();
  7. const { calculateIndexData, capitalize } = Utils;
  8. function initSorting(items) {
  9. items.sortable({
  10. tolerance: 'pointer',
  11. helper: 'clone',
  12. items: '.js-checklist-item:not(.placeholder)',
  13. connectWith: '.js-checklist-items',
  14. appendTo: 'parent',
  15. distance: 7,
  16. placeholder: 'checklist-item placeholder',
  17. scroll: true,
  18. start(evt, ui) {
  19. ui.placeholder.height(ui.helper.height());
  20. EscapeActions.clickExecute(evt.target, 'inlinedForm');
  21. },
  22. stop(evt, ui) {
  23. const parent = ui.item.parents('.js-checklist-items');
  24. const checklistId = Blaze.getData(parent.get(0)).checklist._id;
  25. let prevItem = ui.item.prev('.js-checklist-item').get(0);
  26. if (prevItem) {
  27. prevItem = Blaze.getData(prevItem).item;
  28. }
  29. let nextItem = ui.item.next('.js-checklist-item').get(0);
  30. if (nextItem) {
  31. nextItem = Blaze.getData(nextItem).item;
  32. }
  33. const nItems = 1;
  34. const sortIndex = calculateIndexData(prevItem, nextItem, nItems);
  35. const checklistDomElement = ui.item.get(0);
  36. const checklistData = Blaze.getData(checklistDomElement);
  37. const checklistItem = checklistData.item;
  38. items.sortable('cancel');
  39. checklistItem.move(checklistId, sortIndex.base);
  40. },
  41. });
  42. }
  43. BlazeComponent.extendComponent({
  44. onRendered() {
  45. const self = this;
  46. self.itemsDom = this.$('.js-checklist-items');
  47. initSorting(self.itemsDom);
  48. self.itemsDom.mousedown(function (evt) {
  49. evt.stopPropagation();
  50. });
  51. function userIsMember() {
  52. return ReactiveCache.getCurrentUser()?.isBoardMember();
  53. }
  54. // Disable sorting if the current user is not a board member
  55. self.autorun(() => {
  56. const $itemsDom = $(self.itemsDom);
  57. if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
  58. $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
  59. if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
  60. $(self.itemsDom).sortable({
  61. handle: 'span.fa.checklistitem-handle',
  62. });
  63. }
  64. }
  65. });
  66. },
  67. /** returns the finished percent of the checklist */
  68. finishedPercent() {
  69. const ret = this.data().checklist.finishedPercent();
  70. return ret;
  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 = ReactiveCache.getCard(cardId);
  80. //if (card.isLinked()) cardId = card.linkedId;
  81. if (card.isLinkedCard()) {
  82. cardId = card.linkedId;
  83. }
  84. let sortIndex;
  85. let checklistItemIndex;
  86. if (this.currentData().position === 'top') {
  87. sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
  88. checklistItemIndex = 0;
  89. } else {
  90. sortIndex = Utils.calculateIndexData(card.lastChecklist(), null).base;
  91. checklistItemIndex = -1;
  92. }
  93. if (title) {
  94. Checklists.insert({
  95. cardId,
  96. title,
  97. sort: sortIndex,
  98. });
  99. this.closeAllInlinedForms();
  100. setTimeout(() => {
  101. this.$('.add-checklist-item')
  102. .eq(checklistItemIndex)
  103. .click();
  104. }, 100);
  105. }
  106. },
  107. addChecklistItem(event) {
  108. event.preventDefault();
  109. const textarea = this.find('textarea.js-add-checklist-item');
  110. const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
  111. const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
  112. const title = textarea.value.trim();
  113. const checklist = this.currentData().checklist;
  114. if (title) {
  115. let checklistItems = [title];
  116. if (newlineBecomesNewChecklistItem.checked) {
  117. checklistItems = title.split('\n').map(_value => _value.trim());
  118. if (this.currentData().position === 'top') {
  119. if (newlineBecomesNewChecklistItemOriginOrder.checked === false) {
  120. checklistItems = checklistItems.reverse();
  121. }
  122. }
  123. }
  124. let addIndex;
  125. let sortIndex;
  126. if (this.currentData().position === 'top') {
  127. sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base;
  128. addIndex = -1;
  129. } else {
  130. sortIndex = Utils.calculateIndexData(checklist.lastItem(), null).base;
  131. addIndex = 1;
  132. }
  133. for (let checklistItem of checklistItems) {
  134. ChecklistItems.insert({
  135. title: checklistItem,
  136. checklistId: checklist._id,
  137. cardId: checklist.cardId,
  138. sort: sortIndex,
  139. });
  140. sortIndex += addIndex;
  141. }
  142. }
  143. // We keep the form opened, empty it.
  144. textarea.value = '';
  145. textarea.focus();
  146. },
  147. deleteItem() {
  148. const checklist = this.currentData().checklist;
  149. const item = this.currentData().item;
  150. if (checklist && item && item._id) {
  151. ChecklistItems.remove(item._id);
  152. }
  153. },
  154. editChecklist(event) {
  155. event.preventDefault();
  156. const textarea = this.find('textarea.js-edit-checklist-item');
  157. const title = textarea.value.trim();
  158. const checklist = this.currentData().checklist;
  159. checklist.setTitle(title);
  160. },
  161. editChecklistItem(event) {
  162. event.preventDefault();
  163. const textarea = this.find('textarea.js-edit-checklist-item');
  164. const title = textarea.value.trim();
  165. const item = this.currentData().item;
  166. item.setTitle(title);
  167. },
  168. pressKey(event) {
  169. //If user press enter key inside a form, submit it
  170. //Unless the user is also holding down the 'shift' key
  171. if (event.keyCode === 13 && !event.shiftKey) {
  172. event.preventDefault();
  173. const $form = $(event.currentTarget).closest('form');
  174. $form.find('button[type=submit]').click();
  175. }
  176. },
  177. focusChecklistItem(event) {
  178. // If a new checklist is created, pre-fill the title and select it.
  179. const checklist = this.currentData().checklist;
  180. if (!checklist) {
  181. const textarea = event.target;
  182. textarea.value = capitalize(TAPi18n.__('r-checklist'));
  183. textarea.select();
  184. }
  185. },
  186. /** closes all inlined forms (checklist and checklist-item input fields) */
  187. closeAllInlinedForms() {
  188. this.$('.js-close-inlined-form').click();
  189. },
  190. events() {
  191. return [
  192. {
  193. 'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
  194. 'submit .js-add-checklist': this.addChecklist,
  195. 'submit .js-edit-checklist-title': this.editChecklist,
  196. 'submit .js-add-checklist-item': this.addChecklistItem,
  197. 'submit .js-edit-checklist-item': this.editChecklistItem,
  198. 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
  199. 'click .js-delete-checklist-item': this.deleteItem,
  200. 'focus .js-add-checklist-item': this.focusChecklistItem,
  201. // add and delete checklist / checklist-item
  202. 'click .js-open-inlined-form': this.closeAllInlinedForms,
  203. 'click #toggleHideFinishedChecklist'(event) {
  204. event.preventDefault();
  205. this.data().card.toggleHideFinishedChecklist();
  206. },
  207. keydown: this.pressKey,
  208. },
  209. ];
  210. },
  211. }).register('checklists');
  212. BlazeComponent.extendComponent({
  213. onCreated() {
  214. subManager.subscribe('board', Session.get('currentBoard'), false);
  215. this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
  216. },
  217. boards() {
  218. const ret = ReactiveCache.getBoards(
  219. {
  220. archived: false,
  221. 'members.userId': Meteor.userId(),
  222. _id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
  223. },
  224. {
  225. sort: { sort: 1 /* boards default sorting */ },
  226. },
  227. );
  228. return ret;
  229. },
  230. swimlanes() {
  231. const board = ReactiveCache.getBoard(this.selectedBoardId.get());
  232. return board.swimlanes();
  233. },
  234. aBoardLists() {
  235. const board = ReactiveCache.getBoard(this.selectedBoardId.get());
  236. return board.lists();
  237. },
  238. events() {
  239. return [
  240. {
  241. 'change .js-select-boards'(event) {
  242. this.selectedBoardId.set($(event.currentTarget).val());
  243. subManager.subscribe('board', this.selectedBoardId.get(), false);
  244. },
  245. },
  246. ];
  247. },
  248. }).register('boardsSwimlanesAndLists');
  249. Template.checklists.helpers({
  250. checklists() {
  251. const card = ReactiveCache.getCard(this.cardId);
  252. const ret = card.checklists();
  253. return ret;
  254. },
  255. });
  256. BlazeComponent.extendComponent({
  257. onRendered() {
  258. autosize(this.$('textarea.js-add-checklist-item'));
  259. },
  260. events() {
  261. return [
  262. {
  263. 'click a.fa.fa-copy'(event) {
  264. const $editor = this.$('textarea');
  265. const promise = Utils.copyTextToClipboard($editor[0].value);
  266. const $tooltip = this.$('.copied-tooltip');
  267. Utils.showCopied(promise, $tooltip);
  268. },
  269. }
  270. ];
  271. }
  272. }).register('addChecklistItemForm');
  273. BlazeComponent.extendComponent({
  274. events() {
  275. return [
  276. {
  277. 'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
  278. Popup.back(2);
  279. const checklist = this.checklist;
  280. if (checklist && checklist._id) {
  281. Checklists.remove(checklist._id);
  282. }
  283. }),
  284. 'click .js-move-checklist': Popup.open('moveChecklist'),
  285. 'click .js-copy-checklist': Popup.open('copyChecklist'),
  286. 'click .js-hide-checked-checklist-items'(event) {
  287. event.preventDefault();
  288. this.data().checklist.toggleHideCheckedChecklistItems();
  289. Popup.back();
  290. },
  291. 'click .js-hide-all-checklist-items'(event) {
  292. event.preventDefault();
  293. this.data().checklist.toggleHideAllChecklistItems();
  294. Popup.back();
  295. },
  296. }
  297. ]
  298. }
  299. }).register('checklistActionsPopup');
  300. BlazeComponent.extendComponent({
  301. onRendered() {
  302. autosize(this.$('textarea.js-edit-checklist-item'));
  303. },
  304. events() {
  305. return [
  306. {
  307. 'click a.fa.fa-copy'(event) {
  308. const $editor = this.$('textarea');
  309. const promise = Utils.copyTextToClipboard($editor[0].value);
  310. const $tooltip = this.$('.copied-tooltip');
  311. Utils.showCopied(promise, $tooltip);
  312. },
  313. }
  314. ];
  315. }
  316. }).register('editChecklistItemForm');
  317. Template.checklistItemDetail.helpers({
  318. });
  319. BlazeComponent.extendComponent({
  320. toggleItem() {
  321. const checklist = this.currentData().checklist;
  322. const item = this.currentData().item;
  323. if (checklist && item && item._id) {
  324. item.toggleItem();
  325. }
  326. },
  327. events() {
  328. return [
  329. {
  330. 'click .js-checklist-item .check-box-container': this.toggleItem,
  331. },
  332. ];
  333. },
  334. }).register('checklistItemDetail');
  335. /** Move Checklist Dialog */
  336. (class extends DialogWithBoardSwimlaneListCard {
  337. getDialogOptions() {
  338. const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
  339. return ret;
  340. }
  341. setDone(cardId, options) {
  342. ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
  343. this.data().checklist.move(cardId);
  344. }
  345. }).register('moveChecklistPopup');
  346. /** Copy Checklist Dialog */
  347. (class extends DialogWithBoardSwimlaneListCard {
  348. getDialogOptions() {
  349. const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
  350. return ret;
  351. }
  352. setDone(cardId, options) {
  353. ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
  354. this.data().checklist.copy(cardId);
  355. }
  356. }).register('copyChecklistPopup');