checklists.js 12 KB

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