list.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. require('/client/lib/jquery-ui.js')
  2. const { calculateIndex } = Utils;
  3. BlazeComponent.extendComponent({
  4. // Proxy
  5. openForm(options) {
  6. this.childComponents('listBody')[0].openForm(options);
  7. },
  8. onCreated() {
  9. this.newCardFormIsVisible = new ReactiveVar(true);
  10. },
  11. // The jquery UI sortable library is the best solution I've found so far. I
  12. // tried sortable and dragula but they were not powerful enough four our use
  13. // case. I also considered writing/forking a drag-and-drop + sortable library
  14. // but it's probably too much work.
  15. // By calling asking the sortable library to cancel its move on the `stop`
  16. // callback, we basically solve all issues related to reactive updates. A
  17. // comment below provides further details.
  18. onRendered() {
  19. const boardComponent = this.parentComponent().parentComponent();
  20. function userIsMember() {
  21. return (
  22. Meteor.user() &&
  23. Meteor.user().isBoardMember() &&
  24. !Meteor.user().isCommentOnly()
  25. );
  26. }
  27. const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
  28. const $cards = this.$('.js-minicards');
  29. $cards.sortable({
  30. connectWith: '.js-minicards:not(.js-list-full)',
  31. tolerance: 'pointer',
  32. appendTo: '.board-canvas',
  33. helper(evt, item) {
  34. const helper = item.clone();
  35. if (MultiSelection.isActive()) {
  36. const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
  37. if (andNOthers > 0) {
  38. helper.append(
  39. $(
  40. Blaze.toHTML(
  41. HTML.DIV(
  42. { class: 'and-n-other' },
  43. TAPi18n.__('and-n-other-card', { count: andNOthers }),
  44. ),
  45. ),
  46. ),
  47. );
  48. }
  49. }
  50. return helper;
  51. },
  52. distance: 7,
  53. items: itemsSelector,
  54. placeholder: 'minicard-wrapper placeholder',
  55. start(evt, ui) {
  56. ui.helper.css('z-index', 1000);
  57. ui.placeholder.height(ui.helper.height());
  58. EscapeActions.executeUpTo('popup-close');
  59. boardComponent.setIsDragging(true);
  60. },
  61. stop(evt, ui) {
  62. // To attribute the new index number, we need to get the DOM element
  63. // of the previous and the following card -- if any.
  64. const prevCardDom = ui.item.prev('.js-minicard').get(0);
  65. const nextCardDom = ui.item.next('.js-minicard').get(0);
  66. const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
  67. const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
  68. const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
  69. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  70. const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
  71. let targetSwimlaneId = null;
  72. // only set a new swimelane ID if the swimlanes view is active
  73. if (
  74. Utils.boardView() === 'board-view-swimlanes' ||
  75. currentBoard.isTemplatesBoard()
  76. )
  77. targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
  78. ._id;
  79. // Normally the jquery-ui sortable library moves the dragged DOM element
  80. // to its new position, which disrupts Blaze reactive updates mechanism
  81. // (especially when we move the last card of a list, or when multiple
  82. // users move some cards at the same time). To prevent these UX glitches
  83. // we ask sortable to gracefully cancel the move, and to put back the
  84. // DOM in its initial state. The card move is then handled reactively by
  85. // Blaze with the below query.
  86. $cards.sortable('cancel');
  87. if (MultiSelection.isActive()) {
  88. Cards.find(MultiSelection.getMongoSelector(), {sort: ['sort']}).forEach((card, i) => {
  89. const newSwimlaneId = targetSwimlaneId
  90. ? targetSwimlaneId
  91. : card.swimlaneId || defaultSwimlaneId;
  92. card.move(
  93. currentBoard._id,
  94. newSwimlaneId,
  95. listId,
  96. sortIndex.base + i * sortIndex.increment,
  97. );
  98. });
  99. } else {
  100. const cardDomElement = ui.item.get(0);
  101. const card = Blaze.getData(cardDomElement);
  102. const newSwimlaneId = targetSwimlaneId
  103. ? targetSwimlaneId
  104. : card.swimlaneId || defaultSwimlaneId;
  105. card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
  106. }
  107. boardComponent.setIsDragging(false);
  108. },
  109. sort(event, ui) {
  110. const $boardCanvas = $('.board-canvas');
  111. const boardCanvas = $boardCanvas[0];
  112. if (event.pageX < 10)
  113. { // scroll to the left
  114. boardCanvas.scrollLeft -= 15;
  115. ui.helper[0].offsetLeft -= 15;
  116. }
  117. if (
  118. event.pageX > boardCanvas.offsetWidth - 10 &&
  119. boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
  120. )
  121. { // scroll to the right
  122. boardCanvas.scrollLeft += 15;
  123. }
  124. if (
  125. event.pageY > boardCanvas.offsetHeight - 10 &&
  126. boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
  127. )
  128. { // scroll to the bottom
  129. boardCanvas.scrollTop += 15;
  130. }
  131. if (event.pageY < 10)
  132. { // scroll to the top
  133. boardCanvas.scrollTop -= 15;
  134. }
  135. },
  136. activate(event, ui) {
  137. const $boardCanvas = $('.board-canvas');
  138. const boardCanvas = $boardCanvas[0];
  139. // scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
  140. // https://stackoverflow.com/questions/12965296/how-to-get-maximum-document-scrolltop-value/12965383#12965383
  141. $boardCanvas.data('scrollTopMax', $(document).height() - $(window).height());
  142. // https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
  143. $boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
  144. },
  145. });
  146. this.autorun(() => {
  147. if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
  148. $cards.sortable({
  149. handle: '.handle',
  150. });
  151. } else {
  152. $cards.sortable({
  153. handle: '.minicard',
  154. });
  155. }
  156. if ($cards.data('uiSortable') || $cards.data('sortable')) {
  157. $cards.sortable(
  158. 'option',
  159. 'disabled',
  160. // Disable drag-dropping when user is not member
  161. !userIsMember(),
  162. // Not disable drag-dropping while in multi-selection mode
  163. // MultiSelection.isActive() || !userIsMember(),
  164. );
  165. }
  166. });
  167. // We want to re-run this function any time a card is added.
  168. this.autorun(() => {
  169. const currentBoardId = Tracker.nonreactive(() => {
  170. return Session.get('currentBoard');
  171. });
  172. Cards.find({ boardId: currentBoardId }).fetch();
  173. Tracker.afterFlush(() => {
  174. $cards.find(itemsSelector).droppable({
  175. hoverClass: 'draggable-hover-card',
  176. accept: '.js-member,.js-label',
  177. drop(event, ui) {
  178. const cardId = Blaze.getData(this)._id;
  179. const card = Cards.findOne(cardId);
  180. if (ui.draggable.hasClass('js-member')) {
  181. const memberId = Blaze.getData(ui.draggable.get(0)).userId;
  182. card.assignMember(memberId);
  183. } else {
  184. const labelId = Blaze.getData(ui.draggable.get(0))._id;
  185. card.addLabel(labelId);
  186. }
  187. },
  188. });
  189. });
  190. });
  191. },
  192. }).register('list');
  193. Template.miniList.events({
  194. 'click .js-select-list'() {
  195. const listId = this._id;
  196. Session.set('currentList', listId);
  197. },
  198. });