swimlanes.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. const { calculateIndex, enableClickOnTouch } = Utils;
  2. function currentListIsInThisSwimlane(swimlaneId) {
  3. const currentList = Lists.findOne(Session.get('currentList'));
  4. return currentList && (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '');
  5. }
  6. function currentCardIsInThisList(listId, swimlaneId) {
  7. const currentCard = Cards.findOne(Session.get('currentCard'));
  8. const currentUser = Meteor.user();
  9. if (currentUser && currentUser.profile.boardView === 'board-view-swimlanes')
  10. return currentCard && currentCard.listId === listId && currentCard.swimlaneId === swimlaneId;
  11. else // Default view: board-view-lists
  12. return currentCard && currentCard.listId === listId;
  13. // https://github.com/wekan/wekan/issues/1623
  14. // https://github.com/ChronikEwok/wekan/commit/cad9b20451bb6149bfb527a99b5001873b06c3de
  15. // TODO: In public board, if you would like to switch between List/Swimlane view, you could
  16. // 1) If there is no view cookie, save to cookie board-view-lists
  17. // board-view-lists / board-view-swimlanes / board-view-cal
  18. // 2) If public user changes clicks board-view-lists then change view and
  19. // then change view and save cookie with view value
  20. // without using currentuser above, because currentuser is null.
  21. }
  22. function initSortable(boardComponent, $listsDom) {
  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. // ugly touch event hotfix
  71. enableClickOnTouch('.js-list:not(.js-list-composer)');
  72. function userIsMember() {
  73. return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
  74. }
  75. // Disable drag-dropping while in multi-selection mode, or if the current user
  76. // is not a board member
  77. boardComponent.autorun(() => {
  78. const $listDom = $listsDom;
  79. if ($listDom.data('sortable')) {
  80. $listsDom.sortable('option', 'disabled',
  81. MultiSelection.isActive() || !userIsMember());
  82. }
  83. });
  84. }
  85. BlazeComponent.extendComponent({
  86. onRendered() {
  87. const boardComponent = this.parentComponent();
  88. const $listsDom = this.$('.js-lists');
  89. if (!Session.get('currentCard')) {
  90. boardComponent.scrollLeft();
  91. }
  92. initSortable(boardComponent, $listsDom);
  93. },
  94. onCreated() {
  95. this.draggingActive = new ReactiveVar(false);
  96. this._isDragging = false;
  97. this._lastDragPositionX = 0;
  98. },
  99. id() {
  100. return this._id;
  101. },
  102. currentCardIsInThisList(listId, swimlaneId) {
  103. return currentCardIsInThisList(listId, swimlaneId);
  104. },
  105. currentListIsInThisSwimlane(swimlaneId) {
  106. return currentListIsInThisSwimlane(swimlaneId);
  107. },
  108. events() {
  109. return [{
  110. // Click-and-drag action
  111. 'mousedown .board-canvas'(evt) {
  112. // Translating the board canvas using the click-and-drag action can
  113. // conflict with the build-in browser mechanism to select text. We
  114. // define a list of elements in which we disable the dragging because
  115. // the user will legitimately expect to be able to select some text with
  116. // his mouse.
  117. const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
  118. if ($(evt.target).closest(noDragInside.join(',')).length === 0 && this.$('.swimlane').prop('clientHeight') > evt.offsetY) {
  119. this._isDragging = true;
  120. this._lastDragPositionX = evt.clientX;
  121. }
  122. },
  123. 'mouseup'() {
  124. if (this._isDragging) {
  125. this._isDragging = false;
  126. }
  127. },
  128. 'mousemove'(evt) {
  129. if (this._isDragging) {
  130. // Update the canvas position
  131. this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
  132. this._lastDragPositionX = evt.clientX;
  133. // Disable browser text selection while dragging
  134. evt.stopPropagation();
  135. evt.preventDefault();
  136. // Don't close opened card or inlined form at the end of the
  137. // click-and-drag.
  138. EscapeActions.executeUpTo('popup-close');
  139. EscapeActions.preventNextClick();
  140. }
  141. },
  142. }];
  143. },
  144. }).register('swimlane');
  145. BlazeComponent.extendComponent({
  146. onCreated() {
  147. this.currentBoard = Boards.findOne(Session.get('currentBoard'));
  148. this.isListTemplatesSwimlane = this.currentBoard.isTemplatesBoard() && this.currentData().isListTemplatesSwimlane();
  149. this.currentSwimlane = this.currentData();
  150. },
  151. // Proxy
  152. open() {
  153. this.childComponents('inlinedForm')[0].open();
  154. },
  155. events() {
  156. return [{
  157. submit(evt) {
  158. evt.preventDefault();
  159. const titleInput = this.find('.list-name-input');
  160. const title = titleInput.value.trim();
  161. if (title) {
  162. Lists.insert({
  163. title,
  164. boardId: Session.get('currentBoard'),
  165. sort: $('.list').length,
  166. type: (this.isListTemplatesSwimlane)?'template-list':'list',
  167. swimlaneId: (this.currentBoard.isTemplatesBoard())?this.currentSwimlane._id:'',
  168. });
  169. titleInput.value = '';
  170. titleInput.focus();
  171. }
  172. },
  173. 'click .js-list-template': Popup.open('searchElement'),
  174. }];
  175. },
  176. }).register('addListForm');
  177. Template.swimlane.helpers({
  178. canSeeAddList() {
  179. return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
  180. },
  181. });
  182. BlazeComponent.extendComponent({
  183. currentCardIsInThisList(listId, swimlaneId) {
  184. return currentCardIsInThisList(listId, swimlaneId);
  185. },
  186. onRendered() {
  187. const boardComponent = this.parentComponent();
  188. const $listsDom = this.$('.js-lists');
  189. if (!Session.get('currentCard')) {
  190. boardComponent.scrollLeft();
  191. }
  192. initSortable(boardComponent, $listsDom);
  193. },
  194. }).register('listsGroup');