checklists.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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 newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
  99. const title = textarea.value.trim();
  100. const checklist = this.currentData().checklist;
  101. if (title) {
  102. let checklistItems = [title];
  103. if (newlineBecomesNewChecklistItem.checked) {
  104. checklistItems = title.split('\n').map(_value => _value.trim());
  105. }
  106. for (let checklistItem of checklistItems) {
  107. ChecklistItems.insert({
  108. title: checklistItem,
  109. checklistId: checklist._id,
  110. cardId: checklist.cardId,
  111. sort: Utils.calculateIndexData(checklist.lastItem()).base,
  112. });
  113. }
  114. }
  115. // We keep the form opened, empty it.
  116. textarea.value = '';
  117. textarea.focus();
  118. },
  119. canModifyCard() {
  120. return (
  121. Meteor.user() &&
  122. Meteor.user().isBoardMember() &&
  123. !Meteor.user().isCommentOnly() &&
  124. !Meteor.user().isWorker()
  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. pressKey(event) {
  149. //If user press enter key inside a form, submit it
  150. //Unless the user is also holding down the 'shift' key
  151. if (event.keyCode === 13 && !event.shiftKey) {
  152. event.preventDefault();
  153. const $form = $(event.currentTarget).closest('form');
  154. $form.find('button[type=submit]').click();
  155. }
  156. },
  157. focusChecklistItem(event) {
  158. // If a new checklist is created, pre-fill the title and select it.
  159. const checklist = this.currentData().checklist;
  160. if (!checklist) {
  161. const textarea = event.target;
  162. textarea.value = capitalize(TAPi18n.__('r-checklist'));
  163. textarea.select();
  164. }
  165. },
  166. /** closes all inlined forms (checklist and checklist-item input fields) */
  167. closeAllInlinedForms() {
  168. this.$('.js-close-inlined-form').click();
  169. },
  170. events() {
  171. const events = {
  172. 'click #toggleHideCheckedItemsButton'() {
  173. Meteor.call('toggleHideCheckedItems');
  174. },
  175. };
  176. return [
  177. {
  178. ...events,
  179. 'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
  180. 'submit .js-add-checklist': this.addChecklist,
  181. 'submit .js-edit-checklist-title': this.editChecklist,
  182. 'submit .js-add-checklist-item': this.addChecklistItem,
  183. 'submit .js-edit-checklist-item': this.editChecklistItem,
  184. 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
  185. 'click .js-delete-checklist-item': this.deleteItem,
  186. 'focus .js-add-checklist-item': this.focusChecklistItem,
  187. // add and delete checklist / checklist-item
  188. 'click .js-open-inlined-form': this.closeAllInlinedForms,
  189. keydown: this.pressKey,
  190. },
  191. ];
  192. },
  193. }).register('checklists');
  194. BlazeComponent.extendComponent({
  195. onCreated() {
  196. subManager.subscribe('board', Session.get('currentBoard'), false);
  197. this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
  198. },
  199. boards() {
  200. return Boards.find(
  201. {
  202. archived: false,
  203. 'members.userId': Meteor.userId(),
  204. _id: { $ne: Meteor.user().getTemplatesBoardId() },
  205. },
  206. {
  207. sort: { sort: 1 /* boards default sorting */ },
  208. },
  209. );
  210. },
  211. swimlanes() {
  212. const board = Boards.findOne(this.selectedBoardId.get());
  213. return board.swimlanes();
  214. },
  215. aBoardLists() {
  216. const board = Boards.findOne(this.selectedBoardId.get());
  217. return board.lists();
  218. },
  219. events() {
  220. return [
  221. {
  222. 'change .js-select-boards'(event) {
  223. this.selectedBoardId.set($(event.currentTarget).val());
  224. subManager.subscribe('board', this.selectedBoardId.get(), false);
  225. },
  226. },
  227. ];
  228. },
  229. }).register('boardsSwimlanesAndLists');
  230. Template.checklists.helpers({
  231. checklists() {
  232. const card = Cards.findOne(this.cardId);
  233. const ret = card.checklists();
  234. return ret;
  235. },
  236. hideCheckedItems() {
  237. const currentUser = Meteor.user();
  238. if (currentUser) return currentUser.hasHideCheckedItems();
  239. return false;
  240. },
  241. });
  242. BlazeComponent.extendComponent({
  243. onRendered() {
  244. autosize(this.$('textarea.js-add-checklist-item'));
  245. },
  246. canModifyCard() {
  247. return (
  248. Meteor.user() &&
  249. Meteor.user().isBoardMember() &&
  250. !Meteor.user().isCommentOnly() &&
  251. !Meteor.user().isWorker()
  252. );
  253. },
  254. events() {
  255. return [
  256. {
  257. 'click a.fa.fa-copy'(event) {
  258. const $editor = this.$('textarea');
  259. const promise = Utils.copyTextToClipboard($editor[0].value);
  260. const $tooltip = this.$('.copied-tooltip');
  261. Utils.showCopied(promise, $tooltip);
  262. },
  263. }
  264. ];
  265. }
  266. }).register('addChecklistItemForm');
  267. BlazeComponent.extendComponent({
  268. events() {
  269. return [
  270. {
  271. 'click .js-delete-checklist' : Popup.afterConfirm('checklistDelete', function () {
  272. Popup.back(2);
  273. const checklist = this.checklist;
  274. if (checklist && checklist._id) {
  275. Checklists.remove(checklist._id);
  276. }
  277. }),
  278. 'click .js-move-checklist' : Popup.open('moveChecklist'),
  279. }
  280. ]
  281. }
  282. }).register('checklistActionsPopup');
  283. BlazeComponent.extendComponent({
  284. onRendered() {
  285. autosize(this.$('textarea.js-edit-checklist-item'));
  286. },
  287. canModifyCard() {
  288. return (
  289. Meteor.user() &&
  290. Meteor.user().isBoardMember() &&
  291. !Meteor.user().isCommentOnly() &&
  292. !Meteor.user().isWorker()
  293. );
  294. },
  295. events() {
  296. return [
  297. {
  298. 'click a.fa.fa-copy'(event) {
  299. const $editor = this.$('textarea');
  300. const promise = Utils.copyTextToClipboard($editor[0].value);
  301. const $tooltip = this.$('.copied-tooltip');
  302. Utils.showCopied(promise, $tooltip);
  303. },
  304. }
  305. ];
  306. }
  307. }).register('editChecklistItemForm');
  308. Template.checklistItemDetail.helpers({
  309. canModifyCard() {
  310. return (
  311. Meteor.user() &&
  312. Meteor.user().isBoardMember() &&
  313. !Meteor.user().isCommentOnly() &&
  314. !Meteor.user().isWorker()
  315. );
  316. },
  317. hideCheckedItems() {
  318. const user = Meteor.user();
  319. if (user) return user.hasHideCheckedItems();
  320. return false;
  321. },
  322. });
  323. BlazeComponent.extendComponent({
  324. toggleItem() {
  325. const checklist = this.currentData().checklist;
  326. const item = this.currentData().item;
  327. if (checklist && item && item._id) {
  328. item.toggleItem();
  329. }
  330. },
  331. events() {
  332. return [
  333. {
  334. 'click .js-checklist-item .check-box-container': this.toggleItem,
  335. },
  336. ];
  337. },
  338. }).register('checklistItemDetail');
  339. BlazeComponent.extendComponent({
  340. onCreated() {
  341. const boardId = Utils.getCurrentBoardId();
  342. subManager.subscribe('board', boardId, false);
  343. // subManager.subscribe('swimlane', swimlaneId, false);
  344. // subManager.subscribe('list', listId, false);
  345. // subManager.subscribe('card', cardId, false);
  346. this.selectedBoardId = new ReactiveVar(boardId);
  347. this.selectedSwimlaneId = new ReactiveVar('');
  348. this.selectedListId = new ReactiveVar('');
  349. this.selectedCardId = new ReactiveVar('');
  350. this.setMoveChecklistDialogOption(boardId);
  351. },
  352. /** set the last confirmed dialog field values
  353. * @param boardId the current board id
  354. */
  355. setMoveChecklistDialogOption(boardId) {
  356. this.moveChecklistDialogOption = {
  357. 'boardId' : "",
  358. 'swimlaneId' : "",
  359. 'listId' : "",
  360. 'cardId': "",
  361. }
  362. let currentOptions = Meteor.user().getMoveChecklistDialogOptions();
  363. if (currentOptions && boardId && currentOptions[boardId]) {
  364. this.moveChecklistDialogOption = currentOptions[boardId];
  365. }
  366. const board = Boards.findOne(boardId);
  367. try {
  368. const swimlaneId = board.swimlanes().fetch()[0]._id;
  369. this.selectedSwimlaneId.set(swimlaneId);
  370. } catch (e) {}
  371. try {
  372. const listId = board.lists().fetch()[0];
  373. this.selectedListId.set(listId);
  374. } catch (e) {}
  375. const cardId = Utils.getCurrentCardId();
  376. this.selectedCardId.set(cardId);
  377. },
  378. /** returns if the board id was the last confirmed one
  379. * @param boardId check this board id
  380. * @return if the board id was the last confirmed one
  381. */
  382. isMoveChecklistDialogOptionBoardId(boardId) {
  383. let ret = this.moveChecklistDialogOption.boardId == boardId;
  384. return ret;
  385. },
  386. /** returns if the swimlane id was the last confirmed one
  387. * @param swimlaneId check this swimlane id
  388. * @return if the swimlane id was the last confirmed one
  389. */
  390. isMoveChecklistDialogOptionSwimlaneId(swimlaneId) {
  391. let ret = this.moveChecklistDialogOption.swimlaneId == swimlaneId;
  392. return ret;
  393. },
  394. /** returns if the list id was the last confirmed one
  395. * @param listId check this list id
  396. * @return if the list id was the last confirmed one
  397. */
  398. isMoveChecklistDialogOptionListId(listId) {
  399. let ret = this.moveChecklistDialogOption.listId == listId;
  400. return ret;
  401. },
  402. /** returns if the card id was the last confirmed one
  403. * @param cardId check this card id
  404. * @return if the card id was the last confirmed one
  405. */
  406. isMoveChecklistDialogOptionCardId(cardId) {
  407. let ret = this.moveChecklistDialogOption.cardId == cardId;
  408. return ret;
  409. },
  410. boards() {
  411. return Boards.find(
  412. {
  413. archived: false,
  414. 'members.userId': Meteor.userId(),
  415. _id: { $ne: Meteor.user().getTemplatesBoardId() },
  416. },
  417. {
  418. sort: { sort: 1 },
  419. },
  420. );
  421. },
  422. swimlanes() {
  423. const board = Boards.findOne(this.selectedBoardId.get());
  424. return board.swimlanes();
  425. },
  426. lists() {
  427. const board = Boards.findOne(this.selectedBoardId.get());
  428. return board.lists();
  429. },
  430. cards() {
  431. const list = Lists.findOne(this.selectedListId.get());
  432. const ret = list.cards(this.selectedSwimlaneId.get());
  433. return ret;
  434. },
  435. events() {
  436. return [
  437. {
  438. 'click .js-done'() {
  439. const boardSelect = this.$('.js-select-boards')[0];
  440. const boardId = boardSelect.options[boardSelect.selectedIndex].value;
  441. const listSelect = this.$('.js-select-lists')[0];
  442. const listId = listSelect.options[listSelect.selectedIndex].value;
  443. const swimlaneSelect = this.$('.js-select-swimlanes')[0];
  444. const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
  445. const cardSelect = this.$('.js-select-cards')[0];
  446. const cardId = cardSelect.options[cardSelect.selectedIndex].value;
  447. const options = {
  448. 'boardId' : boardId,
  449. 'swimlaneId' : swimlaneId,
  450. 'listId' : listId,
  451. 'cardId': cardId,
  452. }
  453. Meteor.user().setMoveChecklistDialogOption(boardId, options);
  454. this.data().checklist.move(cardId);
  455. Popup.back(2);
  456. },
  457. 'change .js-select-boards'(event) {
  458. const boardId = $(event.currentTarget).val();
  459. subManager.subscribe('board', boardId, false);
  460. this.setMoveChecklistDialogOption(boardId);
  461. this.selectedBoardId.set(boardId);
  462. },
  463. 'change .js-select-swimlanes'(event) {
  464. this.selectedSwimlaneId.set($(event.currentTarget).val());
  465. },
  466. 'change .js-select-lists'(event) {
  467. this.selectedListId.set($(event.currentTarget).val());
  468. },
  469. },
  470. ];
  471. },
  472. }).register('moveChecklistPopup');