diff options
Diffstat (limited to 'client/components/swimlanes/swimlanes.js')
-rw-r--r-- | client/components/swimlanes/swimlanes.js | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js new file mode 100644 index 00000000..7965c2bc --- /dev/null +++ b/client/components/swimlanes/swimlanes.js @@ -0,0 +1,235 @@ +const { calculateIndex } = Utils; + +function currentCardIsInThisList(listId, swimlaneId) { + const currentCard = Cards.findOne(Session.get('currentCard')); + const currentUser = Meteor.user(); + if (currentUser.profile.boardView === 'board-view-lists') + return currentCard && currentCard.listId === listId; + else if (currentUser.profile.boardView === 'board-view-swimlanes') + return currentCard && currentCard.listId === listId && currentCard.swimlaneId === swimlaneId; + else + return false; +} + +function initSortable(boardComponent, $listsDom) { + // We want to animate the card details window closing. We rely on CSS + // transition for the actual animation. + $listsDom._uihooks = { + removeElement(node) { + const removeNode = _.once(() => { + node.parentNode.removeChild(node); + }); + if ($(node).hasClass('js-card-details')) { + $(node).css({ + flexBasis: 0, + padding: 0, + }); + $listsDom.one(CSSEvents.transitionend, removeNode); + } else { + removeNode(); + } + }, + }; + + $listsDom.sortable({ + tolerance: 'pointer', + helper: 'clone', + handle: '.js-list-header', + items: '.js-list:not(.js-list-composer)', + placeholder: 'list placeholder', + distance: 7, + start(evt, ui) { + ui.placeholder.height(ui.helper.height()); + EscapeActions.executeUpTo('popup-close'); + boardComponent.setIsDragging(true); + }, + stop(evt, ui) { + // To attribute the new index number, we need to get the DOM element + // of the previous and the following card -- if any. + const prevListDom = ui.item.prev('.js-list').get(0); + const nextListDom = ui.item.next('.js-list').get(0); + const sortIndex = calculateIndex(prevListDom, nextListDom, 1); + + $listsDom.sortable('cancel'); + const listDomElement = ui.item.get(0); + const list = Blaze.getData(listDomElement); + + Lists.update(list._id, { + $set: { + sort: sortIndex.base, + }, + }); + + boardComponent.setIsDragging(false); + }, + }); + + function userIsMember() { + return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); + } + + // Disable drag-dropping while in multi-selection mode, or if the current user + // is not a board member + boardComponent.autorun(() => { + const $listDom = $listsDom; + if ($listDom.data('sortable')) { + $listsDom.sortable('option', 'disabled', + MultiSelection.isActive() || !userIsMember()); + } + }); +} + +BlazeComponent.extendComponent({ + onRendered() { + const boardComponent = this.parentComponent(); + const $listsDom = this.$('.js-lists'); + + if (!Session.get('currentCard')) { + boardComponent.scrollLeft(); + } + + initSortable(boardComponent, $listsDom); + }, + onCreated() { + this.draggingActive = new ReactiveVar(false); + + this._isDragging = false; + this._lastDragPositionX = 0; + }, + + id() { + return this._id; + }, + + currentCardIsInThisList(listId, swimlaneId) { + return currentCardIsInThisList(listId, swimlaneId); + }, + + events() { + return [{ + // Click-and-drag action + 'mousedown .board-canvas'(evt) { + // Translating the board canvas using the click-and-drag action can + // conflict with the build-in browser mechanism to select text. We + // define a list of elements in which we disable the dragging because + // the user will legitimately expect to be able to select some text with + // his mouse. + const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header']; + if ($(evt.target).closest(noDragInside.join(',')).length === 0 && this.$('.swimlane').prop('clientHeight') > evt.offsetY) { + this._isDragging = true; + this._lastDragPositionX = evt.clientX; + } + }, + 'mouseup'() { + if (this._isDragging) { + this._isDragging = false; + } + }, + 'mousemove'(evt) { + if (this._isDragging) { + // Update the canvas position + this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX; + this._lastDragPositionX = evt.clientX; + // Disable browser text selection while dragging + evt.stopPropagation(); + evt.preventDefault(); + // Don't close opened card or inlined form at the end of the + // click-and-drag. + EscapeActions.executeUpTo('popup-close'); + EscapeActions.preventNextClick(); + } + }, + }]; + }, +}).register('swimlane'); + +BlazeComponent.extendComponent({ + // Proxy + open() { + this.childComponents('inlinedForm')[0].open(); + }, + + events() { + return [{ + submit(evt) { + evt.preventDefault(); + const titleInput = this.find('.list-name-input'); + const title = titleInput.value.trim(); + if (title) { + Lists.insert({ + title, + boardId: Session.get('currentBoard'), + sort: $('.list').length, + }); + + titleInput.value = ''; + titleInput.focus(); + } + }, + }]; + }, +}).register('addListForm'); + +BlazeComponent.extendComponent({ + // Proxy + open() { + this.childComponents('inlinedForm')[0].open(); + }, + + events() { + return [{ + submit(evt) { + evt.preventDefault(); + let titleInput = this.find('.list-name-input'); + if (titleInput) { + const title = titleInput.value.trim(); + if (title) { + Lists.insert({ + title, + boardId: Session.get('currentBoard'), + sort: $('.list').length, + }); + + titleInput.value = ''; + titleInput.focus(); + } + } else { + titleInput = this.find('.swimlane-name-input'); + const title = titleInput.value.trim(); + if (title) { + Swimlanes.insert({ + title, + boardId: Session.get('currentBoard'), + sort: $('.swimlane').length, + }); + + titleInput.value = ''; + titleInput.focus(); + } + } + }, + }]; + }, +}).register('addListAndSwimlaneForm'); + +Template.swimlane.helpers({ + canSeeAddList() { + return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); + }, +}); + +BlazeComponent.extendComponent({ + currentCardIsInThisList(listId, swimlaneId) { + return currentCardIsInThisList(listId, swimlaneId); + }, + onRendered() { + const boardComponent = this.parentComponent(); + const $listsDom = this.$('.js-lists'); + + if (!Session.get('currentCard')) { + boardComponent.scrollLeft(); + } + + initSortable(boardComponent, $listsDom); + }, +}).register('listsGroup'); |