From 48d9505157ff67d97e4964d6f125935c1e167040 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Thu, 9 Oct 2014 16:44:38 +0200 Subject: [PATCH] us 1213 - drag multiple --- app/coffee/modules/backlog/main.coffee | 85 ++++++---- app/coffee/modules/backlog/sortable.coffee | 57 +++++-- app/js/jquery-ui.drag-multiple-custom.js | 160 ++++++++++++++++++ ...uery.ui.git.js => jquery.ui.git-custom.js} | 18 +- app/js/{sha1.js => sha1-custom.js} | 0 gulpfile.coffee | 5 +- 6 files changed, 267 insertions(+), 58 deletions(-) create mode 100644 app/js/jquery-ui.drag-multiple-custom.js rename app/js/{jquery.ui.git.js => jquery.ui.git-custom.js} (99%) rename app/js/{sha1.js => sha1-custom.js} (100%) diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee index e15a0005..40a8f2ec 100644 --- a/app/coffee/modules/backlog/main.coffee +++ b/app/coffee/modules/backlog/main.coffee @@ -256,6 +256,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F resortUserStories: (uses, field="backlog_order") -> items = [] + for item, index in uses item[field] = index if item.isModified() @@ -263,8 +264,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F return items - moveUs: (ctx, us, newUsIndex, newSprintId) -> - oldSprintId = us.milestone + moveUs: (ctx, usList, newUsIndex, newSprintId) -> + oldSprintId = usList[0].milestone + project = usList[0].project # In the same sprint or in the backlog if newSprintId == oldSprintId @@ -277,20 +279,25 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F userstories = @scope.sprintsById[newSprintId].user_stories @scope.$apply -> - r = userstories.indexOf(us) - userstories.splice(r, 1) - userstories.splice(newUsIndex, 0, us) + for us, key in usList + r = userstories.indexOf(us) + userstories.splice(r, 1) + + args = [newUsIndex, 0].concat(usList) + Array.prototype.splice.apply(userstories, args) # If in backlog if newSprintId == null # Rehash userstories order field + items = @.resortUserStories(userstories, "backlog_order") data = @.prepareBulkUpdateData(items, "backlog_order") # Persist in bulk all affected # userstories with order change - @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then => - @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) + @rs.userstories.bulkUpdateBacklogOrder(project, data).then => + for us in usList + @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) # For sprint else @@ -300,27 +307,32 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # Persist in bulk all affected # userstories with order change - @rs.userstories.bulkUpdateSprintOrder(us.project, data).then => - @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) + @rs.userstories.bulkUpdateSprintOrder(project, data).then => + for us in usList + @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) return promise # From sprint to backlog if newSprintId == null - us.milestone = null + us.milestone = null for us in usList @scope.$apply => # Add new us to backlog userstories list - @scope.userstories.splice(newUsIndex, 0, us) - @scope.visibleUserstories.splice(newUsIndex, 0, us) + # @scope.userstories.splice(newUsIndex, 0, us) + # @scope.visibleUserstories.splice(newUsIndex, 0, us) + args = [newUsIndex, 0].concat(usList) + Array.prototype.splice.apply(@scope.userstories, args) + Array.prototype.splice.apply(@scope.visibleUserstories, args) # Execute the prefiltering of user stories @.filterVisibleUserstories() # Remove the us from the sprint list. sprint = @scope.sprintsById[oldSprintId] - r = sprint.user_stories.indexOf(us) - sprint.user_stories.splice(r, 1) + for us, key in usList + r = sprint.user_stories.indexOf(us) + sprint.user_stories.splice(r, 1) # Persist the milestone change of userstory promise = @repo.save(us) @@ -340,43 +352,55 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # From backlog to sprint newSprint = @scope.sprintsById[newSprintId] - if us.milestone == null - us.milestone = newSprintId + if oldSprintId == null + us.milestone = newSprintId for us in usList @scope.$apply => + args = [newUsIndex, 0].concat(usList) + # Add moving us to sprint user stories list - newSprint.user_stories.splice(newUsIndex, 0, us) + Array.prototype.splice.apply(newSprint.user_stories, args) # Remove moving us from backlog userstories lists. - r = @scope.visibleUserstories.indexOf(us) - @scope.visibleUserstories.splice(r, 1) - r = @scope.userstories.indexOf(us) - @scope.userstories.splice(r, 1) + for us, key in usList + r = @scope.visibleUserstories.indexOf(us) + @scope.visibleUserstories.splice(r, 1) + + r = @scope.userstories.indexOf(us) + @scope.userstories.splice(r, 1) # From sprint to sprint else - us.milestone = newSprintId + us.milestone = newSprintId for us in usList @scope.$apply => + args = [newUsIndex, 0].concat(usList) + # Add new us to backlog userstories list - newSprint.user_stories.splice(newUsIndex, 0, us) + Array.prototype.splice.apply(newSprint.user_stories, args) # Remove the us from the sprint list. - oldSprint = @scope.sprintsById[oldSprintId] - r = oldSprint.user_stories.indexOf(us) - oldSprint.user_stories.splice(r, 1) + for us in usList + oldSprint = @scope.sprintsById[oldSprintId] + r = oldSprint.user_stories.indexOf(us) + oldSprint.user_stories.splice(r, 1) # Persist the milestone change of userstory - promise = @repo.save(us) + promises = _.map usList, (us) => @repo.save(us) # Rehash userstories order field # and persist in bulk all changes. - promise = promise.then => + promise = @q.all.apply(null, promises).then => items = @.resortUserStories(newSprint.user_stories, "sprint_order") data = @.prepareBulkUpdateData(items, "sprint_order") - return @rs.userstories.bulkUpdateSprintOrder(us.project, data).then => + + return @rs.userstories.bulkUpdateSprintOrder(project, data).then => @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) + return @rs.userstories.bulkUpdateBacklogOrder(project, data).then => + for us in usList + @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) + promise.then null, -> console.log "FAIL" # TODO @@ -545,6 +569,7 @@ BacklogDirective = ($repo, $rootscope) -> # Enable move to current sprint only when there are selected us's $el.on "change", ".backlog-table-body .user-stories input:checkbox", (event) -> + target = angular.element(event.currentTarget) moveToCurrentSprintDom = $el.find("#move-to-current-sprint") selectedUsDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") @@ -553,6 +578,8 @@ BacklogDirective = ($repo, $rootscope) -> else moveToCurrentSprintDom.hide() + target.closest('.us-item-row').toggleClass('ui-multisortable-multiple') + $el.on "click", "#move-to-current-sprint", (event) => # Calculating the us's to be modified ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") diff --git a/app/coffee/modules/backlog/sortable.coffee b/app/coffee/modules/backlog/sortable.coffee index adcf4baa..e568f742 100644 --- a/app/coffee/modules/backlog/sortable.coffee +++ b/app/coffee/modules/backlog/sortable.coffee @@ -69,23 +69,35 @@ BacklogSortableDirective = ($repo, $rs, $rootscope) -> cursorAt: {right: 15} }) - $el.on "sortreceive", (event, ui) -> + $el.on "multiplesortreceive", (event, ui) -> itemUs = ui.item.scope().us itemIndex = ui.item.index() deleteElement(ui.item) - $scope.$emit("sprint:us:move", itemUs, itemIndex, null) + + $scope.$emit("sprint:us:move", [itemUs], itemIndex, null) ui.item.find('a').removeClass('noclick') - $el.on "sortstop", (event, ui) -> + $el.on "multiplesortstop", (event, ui) -> # When parent not exists, do nothing - if ui.item.parent().length == 0 + if $(ui.items[0]).parent().length == 0 return - itemUs = ui.item.scope().us - itemIndex = ui.item.index() - $scope.$emit("sprint:us:move", itemUs, itemIndex, null) - ui.item.find('a').removeClass('noclick') + items = _.sortBy ui.items, (item) -> + return $(item).index() + + index = _.min _.map items, (item) -> + return $(item).index() + + us = _.map items, (item) -> + item = $(item) + itemUs = item.scope().us + + item.find('a').removeClass('noclick') + + return itemUs + + $scope.$emit("sprint:us:move", us, index, null) $el.on "sortstart", (event, ui) -> ui.item.find('a').addClass('noclick') @@ -113,7 +125,7 @@ BacklogEmptySortableDirective = ($repo, $rs, $rootscope) -> itemIndex = ui.item.index() deleteElement(ui.item) - $scope.$emit("sprint:us:move", itemUs, itemIndex, null) + $scope.$emit("sprint:us:move", [itemUs], itemIndex, null) ui.item.find('a').removeClass('noclick') $scope.$on "$destroy", -> @@ -132,24 +144,33 @@ SprintSortableDirective = ($repo, $rs, $rootscope) -> connectWith: ".sprint-table,.backlog-table-body,.empty-backlog" }) - $el.on "sortreceive", (event, ui) -> - itemUs = ui.item.scope().us - itemIndex = ui.item.index() + $el.on "multiplesortreceive", (event, ui) -> + items = _.sortBy ui.items, (item) -> + return $(item).index() - deleteElement(ui.item) - $scope.$emit("sprint:us:move", itemUs, itemIndex, $scope.sprint.id) - ui.item.find('a').removeClass('noclick') + index = _.min _.map items, (item) -> + return $(item).index() - $el.on "sortstop", (event, ui) -> + us = _.map items, (item) -> + item = $(item) + itemUs = item.scope().us + + deleteElement(item) + item.find('a').removeClass('noclick') + + return itemUs + + $scope.$emit("sprint:us:move", us, index, $scope.sprint.id) + + $el.on "multiplesortstop", (event, ui) -> # When parent not exists, do nothing if ui.item.parent().length == 0 return itemUs = ui.item.scope().us itemIndex = ui.item.index() - - $scope.$emit("sprint:us:move", itemUs, itemIndex, $scope.sprint.id) ui.item.find('a').removeClass('noclick') + $scope.$emit("sprint:us:move", [itemUs], itemIndex, $scope.sprint.id) return {link:link} diff --git a/app/js/jquery-ui.drag-multiple-custom.js b/app/js/jquery-ui.drag-multiple-custom.js new file mode 100644 index 00000000..c8d0c0fa --- /dev/null +++ b/app/js/jquery-ui.drag-multiple-custom.js @@ -0,0 +1,160 @@ +(function($) { + var multipleSortableClass = 'ui-multisortable-multiple'; + var dragStarted = false; + + var multiSort = {}; + + multiSort.isBelow = function(elm, compare) { + var elmOriginalPosition = elm.data('dragmultiple:originalPosition'); + var compareOriginalPosition = compare.data('dragmultiple:originalPosition'); + + return elmOriginalPosition.top > compareOriginalPosition.top; + }; + + multiSort.reset = function(elm) { + $(elm) + .removeClass("ui-sortable-helper") + .removeAttr('style') + .data('dragMultipleActive', false); + }; + + multiSort.sort = function(current, positions) { + positions.after.reverse(); + + $.each(positions.after, function () { + multiSort.reset(this); + current.after(this); + }); + + $.each(positions.before, function () { + multiSort.reset(this); + current.before(this); + }); + }; + + multiSort.sortPositions = function(elm, current) { + //saved if the elements are after or before the current + var insertAfter = []; + var insertBefore = []; + + $(elm).find('.' + multipleSortableClass).each(function () { + var elm = $(this); + + if (elm[0] === current[0] || !current.hasClass(multipleSortableClass)) return; + + if (multiSort.isBelow(elm, current)) { + insertAfter.push(elm); + } else { + insertBefore.push(elm); + } + }); + + return {'after': insertAfter, 'before': insertBefore}; + }; + + $.widget( "ui.sortable", $.ui.sortable, { + _mouseStart: function() { + dragStarted = false; + + this._superApply( arguments ); + }, + _createHelper: function () { + var helper = this._superApply( arguments ); + + if ($(helper).hasClass(multipleSortableClass)) { + $(this.element).find('.' + multipleSortableClass).each(function () { + $(this) + .data('dragmultiple:originalPosition', $(this).position()) + .data('dragMultipleActive', true); + }); + } + + return helper; + }, + _mouseStop: function (event, ui) { + var current = this.helper; + var elms = []; + + if (current.hasClass(multipleSortableClass)) { + elms = $(this.element).find('.' + multipleSortableClass); + } + + if (!elms.length) { + elms = [current]; + } + + //save the order of the elements relative to the main + var positions = multiSort.sortPositions(this.element, current); + + this._superApply( arguments ); + + if (this.element !== this.currentContainer.element) { + // change to another sortable list + multiSort.sort(current, positions); + + $(this.currentContainer.element).trigger('multiplesortreceive', { + 'item': current, + 'items': elms + }); + } else if (current.hasClass(multipleSortableClass)) { + // sort in the same list + multiSort.sort(current, positions); + } + + $(this.element).trigger('multiplesortstop', { + 'item': current, + 'items': elms + }); + }, + _mouseDrag: function(key, value) { + this._super(key, value); + + var current = this.helper; + + if (!current.hasClass(multipleSortableClass)) return; + + // following the drag element + var currentLeft = current.position().left; + var currentTop = current.position().top; + var currentZIndex = current.css('z-index'); + var currentPosition = current.css('position'); + + var positions = multiSort.sortPositions(this.element, current); + + positions.before.reverse(); + + [{'positions': positions.after, type: 'after'}, + {'positions': positions.before, type: 'before'}] + .forEach(function (item) { + $.each(item.positions, function (index, elm) { + var top; + + if (item.type === 'after') { + top = currentTop + ((index + 1) * current.outerHeight()); + } else { + top = currentTop - ((index + 1) * current.outerHeight()); + } + + elm + .addClass("ui-sortable-helper") + .css({ + width: elm.outerWidth(), + height: elm.outerHeight(), + position: currentPosition, + zIndex: currentZIndex, + top: top, + left: currentLeft + }); + }); + }); + + // it only refresh position the first time because + // jquery-ui has saved the old positions of the draggable elements + // and with this will remove all elements with dragMultipleActive + if (!dragStarted) { + dragStarted = true; + this.refreshPositions(); + } + } + }); +}(jQuery)) diff --git a/app/js/jquery.ui.git.js b/app/js/jquery.ui.git-custom.js similarity index 99% rename from app/js/jquery.ui.git.js rename to app/js/jquery.ui.git-custom.js index 524b6c72..327fb2a1 100644 --- a/app/js/jquery.ui.git.js +++ b/app/js/jquery.ui.git-custom.js @@ -789,7 +789,6 @@ $.Widget.prototype = { } } } - this.element.trigger( event, data ); return !( $.isFunction( callback ) && callback.apply( this.element[0], [ event ].concat( data ) ) === false || @@ -2368,11 +2367,9 @@ $.ui.ddmanager = { if ( draggable.options.refreshPositions ) { $.ui.ddmanager.prepareOffsets( draggable, event ); } - // Run through all droppables and check their positions based on specific tolerance options $.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() { - - if ( this.options.disabled || this.greedyChild || !this.visible ) { + if ( this.options.disabled || this.greedyChild || !this.visible || $(this).data('dragMultipleActive')) { return; } @@ -4107,11 +4104,15 @@ return $.widget("ui.sortable", $.ui.mouse, { //Rearrange for (i = this.items.length - 1; i >= 0; i--) { - //Cache variables and intersection, continue if no intersection item = this.items[i]; itemElement = item.item[0]; intersection = this._intersectsWithPointer(item); + + if (item.item.data('dragMultipleActive')) { + continue; + } + if (!intersection) { continue; } @@ -4166,7 +4167,6 @@ return $.widget("ui.sortable", $.ui.mouse, { }, _mouseStop: function(event, noPropagation) { - if(!event) { return; } @@ -4471,6 +4471,9 @@ return $.widget("ui.sortable", $.ui.mouse, { for (i = this.items.length - 1; i >= 0; i--){ item = this.items[i]; + if ($(item.item).data('dragMultipleActive')) { + continue; + } //We ignore calculating positions of all connected containers when we're not over them if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { @@ -4536,7 +4539,6 @@ return $.widget("ui.sortable", $.ui.mouse, { return element; }, update: function(container, p) { - // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified if(className && !o.forcePlaceholderSize) { @@ -4661,7 +4663,6 @@ return $.widget("ui.sortable", $.ui.mouse, { }, _createHelper: function(event) { - var o = this.options, helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); @@ -4888,7 +4889,6 @@ return $.widget("ui.sortable", $.ui.mouse, { }, _rearrange: function(event, i, a, hardRefresh) { - a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); //Various things done here to improve the performance: diff --git a/app/js/sha1.js b/app/js/sha1-custom.js similarity index 100% rename from app/js/sha1.js rename to app/js/sha1-custom.js diff --git a/gulpfile.coffee b/gulpfile.coffee index 6ed6b9ef..936b0cdc 100644 --- a/gulpfile.coffee +++ b/gulpfile.coffee @@ -82,8 +82,9 @@ paths.js = [ paths.app + "vendor/jquery-textcomplete/jquery.textcomplete.js", paths.app + "vendor/markitup/markitup/jquery.markitup.js", paths.app + "vendor/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js", - paths.app + "js/jquery.ui.git.js", - paths.app + "js/sha1.js", + paths.app + "js/jquery.ui.git-custom.js", + paths.app + "js/jquery-ui.drag-multiple-custom.js", + paths.app + "js/sha1-custom.js", paths.app + "plugins/**/*.js" ]