list.js 8.1 KB

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