list.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. const { calculateIndex } = Utils;
  2. BlazeComponent.extendComponent({
  3. // Proxy
  4. openForm(options) {
  5. this.childComponents('listBody')[0].openForm(options);
  6. },
  7. onCreated() {
  8. this.newCardFormIsVisible = new ReactiveVar(true);
  9. },
  10. // The jquery UI sortable library is the best solution I've found so far. I
  11. // tried sortable and dragula but they were not powerful enough four our use
  12. // case. I also considered writing/forking a drag-and-drop + sortable library
  13. // but it's probably too much work.
  14. // By calling asking the sortable library to cancel its move on the `stop`
  15. // callback, we basically solve all issues related to reactive updates. A
  16. // comment below provides further details.
  17. onRendered() {
  18. const boardComponent = this.parentComponent().parentComponent();
  19. const $listsDom = boardComponent.$('.js-lists');
  20. if (!Session.get('currentCard')) {
  21. boardComponent.scrollLeft();
  22. }
  23. // We want to animate the card details window closing. We rely on CSS
  24. // transition for the actual animation.
  25. $listsDom._uihooks = {
  26. removeElement(node) {
  27. const removeNode = _.once(() => {
  28. node.parentNode.removeChild(node);
  29. });
  30. if ($(node).hasClass('js-card-details')) {
  31. $(node).css({
  32. flexBasis: 0,
  33. padding: 0,
  34. });
  35. $listsDom.one(CSSEvents.transitionend, removeNode);
  36. } else {
  37. removeNode();
  38. }
  39. },
  40. };
  41. $listsDom.sortable({
  42. tolerance: 'pointer',
  43. helper: 'clone',
  44. handle: '.js-list-header',
  45. items: '.js-list:not(.js-list-composer)',
  46. placeholder: 'list placeholder',
  47. distance: 7,
  48. start(evt, ui) {
  49. ui.placeholder.height(ui.helper.height());
  50. EscapeActions.executeUpTo('popup-close');
  51. boardComponent.setIsDragging(true);
  52. },
  53. stop(evt, ui) {
  54. // To attribute the new index number, we need to get the DOM element
  55. // of the previous and the following card -- if any.
  56. const prevListDom = ui.item.prev('.js-list').get(0);
  57. const nextListDom = ui.item.next('.js-list').get(0);
  58. const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
  59. $listsDom.sortable('cancel');
  60. const listDomElement = ui.item.get(0);
  61. const list = Blaze.getData(listDomElement);
  62. Lists.update(list._id, {
  63. $set: {
  64. sort: sortIndex.base,
  65. },
  66. });
  67. boardComponent.setIsDragging(false);
  68. },
  69. });
  70. function userIsMember() {
  71. return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
  72. }
  73. // Disable drag-dropping while in multi-selection mode, or if the current user
  74. // is not a board member
  75. boardComponent.autorun(() => {
  76. const $listDom = $listsDom;
  77. if ($listDom.data('sortable')) {
  78. $listsDom.sortable('option', 'disabled',
  79. MultiSelection.isActive() || !userIsMember());
  80. }
  81. });
  82. // If there is no data in the board (ie, no lists) we autofocus the list
  83. // creation form by clicking on the corresponding element.
  84. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  85. if (userIsMember() && currentBoard.lists().count() === 0) {
  86. boardComponent.openNewListForm();
  87. }
  88. const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
  89. const $cards = this.$('.js-minicards');
  90. $cards.sortable({
  91. connectWith: '.js-minicards:not(.js-list-full)',
  92. tolerance: 'pointer',
  93. appendTo: '.board-canvas',
  94. helper(evt, item) {
  95. const helper = item.clone();
  96. if (MultiSelection.isActive()) {
  97. const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
  98. if (andNOthers > 0) {
  99. helper.append($(Blaze.toHTML(HTML.DIV(
  100. { 'class': 'and-n-other' },
  101. TAPi18n.__('and-n-other-card', { count: andNOthers })
  102. ))));
  103. }
  104. }
  105. return helper;
  106. },
  107. distance: 7,
  108. items: itemsSelector,
  109. placeholder: 'minicard-wrapper placeholder',
  110. start(evt, ui) {
  111. ui.placeholder.height(ui.helper.height());
  112. EscapeActions.executeUpTo('popup-close');
  113. boardComponent.setIsDragging(true);
  114. },
  115. stop(evt, ui) {
  116. // To attribute the new index number, we need to get the DOM element
  117. // of the previous and the following card -- if any.
  118. const prevCardDom = ui.item.prev('.js-minicard').get(0);
  119. const nextCardDom = ui.item.next('.js-minicard').get(0);
  120. const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
  121. const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
  122. const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
  123. const swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
  124. // Normally the jquery-ui sortable library moves the dragged DOM element
  125. // to its new position, which disrupts Blaze reactive updates mechanism
  126. // (especially when we move the last card of a list, or when multiple
  127. // users move some cards at the same time). To prevent these UX glitches
  128. // we ask sortable to gracefully cancel the move, and to put back the
  129. // DOM in its initial state. The card move is then handled reactively by
  130. // Blaze with the below query.
  131. $cards.sortable('cancel');
  132. if (MultiSelection.isActive()) {
  133. Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
  134. card.move(swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
  135. });
  136. } else {
  137. const cardDomElement = ui.item.get(0);
  138. const card = Blaze.getData(cardDomElement);
  139. card.move(swimlaneId, listId, sortIndex.base);
  140. }
  141. boardComponent.setIsDragging(false);
  142. },
  143. });
  144. // Disable drag-dropping if the current user is not a board member or is comment only
  145. this.autorun(() => {
  146. $cards.sortable('option', 'disabled', !userIsMember());
  147. });
  148. // We want to re-run this function any time a card is added.
  149. this.autorun(() => {
  150. const currentBoardId = Tracker.nonreactive(() => {
  151. return Session.get('currentBoard');
  152. });
  153. Cards.find({ boardId: currentBoardId }).fetch();
  154. Tracker.afterFlush(() => {
  155. $cards.find(itemsSelector).droppable({
  156. hoverClass: 'draggable-hover-card',
  157. accept: '.js-member,.js-label',
  158. drop(event, ui) {
  159. const cardId = Blaze.getData(this)._id;
  160. const card = Cards.findOne(cardId);
  161. if (ui.draggable.hasClass('js-member')) {
  162. const memberId = Blaze.getData(ui.draggable.get(0)).userId;
  163. card.assignMember(memberId);
  164. } else {
  165. const labelId = Blaze.getData(ui.draggable.get(0))._id;
  166. card.addLabel(labelId);
  167. }
  168. },
  169. });
  170. });
  171. });
  172. },
  173. }).register('list');
  174. Template.miniList.events({
  175. 'click .js-select-list'() {
  176. const listId = this._id;
  177. Session.set('currentList', listId);
  178. },
  179. });