us 1213 - drag multiple

stable
Juanfran 2014-10-09 16:44:38 +02:00
parent 19da1a1b48
commit 48d9505157
6 changed files with 267 additions and 58 deletions

View File

@ -256,6 +256,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
resortUserStories: (uses, field="backlog_order") -> resortUserStories: (uses, field="backlog_order") ->
items = [] items = []
for item, index in uses for item, index in uses
item[field] = index item[field] = index
if item.isModified() if item.isModified()
@ -263,8 +264,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
return items return items
moveUs: (ctx, us, newUsIndex, newSprintId) -> moveUs: (ctx, usList, newUsIndex, newSprintId) ->
oldSprintId = us.milestone oldSprintId = usList[0].milestone
project = usList[0].project
# In the same sprint or in the backlog # In the same sprint or in the backlog
if newSprintId == oldSprintId if newSprintId == oldSprintId
@ -277,20 +279,25 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
userstories = @scope.sprintsById[newSprintId].user_stories userstories = @scope.sprintsById[newSprintId].user_stories
@scope.$apply -> @scope.$apply ->
r = userstories.indexOf(us) for us, key in usList
userstories.splice(r, 1) r = userstories.indexOf(us)
userstories.splice(newUsIndex, 0, us) userstories.splice(r, 1)
args = [newUsIndex, 0].concat(usList)
Array.prototype.splice.apply(userstories, args)
# If in backlog # If in backlog
if newSprintId == null if newSprintId == null
# Rehash userstories order field # Rehash userstories order field
items = @.resortUserStories(userstories, "backlog_order") items = @.resortUserStories(userstories, "backlog_order")
data = @.prepareBulkUpdateData(items, "backlog_order") data = @.prepareBulkUpdateData(items, "backlog_order")
# Persist in bulk all affected # Persist in bulk all affected
# userstories with order change # userstories with order change
@rs.userstories.bulkUpdateBacklogOrder(us.project, data).then => @rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) for us in usList
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
# For sprint # For sprint
else else
@ -300,27 +307,32 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# Persist in bulk all affected # Persist in bulk all affected
# userstories with order change # userstories with order change
@rs.userstories.bulkUpdateSprintOrder(us.project, data).then => @rs.userstories.bulkUpdateSprintOrder(project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) for us in usList
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
return promise return promise
# From sprint to backlog # From sprint to backlog
if newSprintId == null if newSprintId == null
us.milestone = null us.milestone = null for us in usList
@scope.$apply => @scope.$apply =>
# Add new us to backlog userstories list # Add new us to backlog userstories list
@scope.userstories.splice(newUsIndex, 0, us) # @scope.userstories.splice(newUsIndex, 0, us)
@scope.visibleUserstories.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 # Execute the prefiltering of user stories
@.filterVisibleUserstories() @.filterVisibleUserstories()
# Remove the us from the sprint list. # Remove the us from the sprint list.
sprint = @scope.sprintsById[oldSprintId] sprint = @scope.sprintsById[oldSprintId]
r = sprint.user_stories.indexOf(us) for us, key in usList
sprint.user_stories.splice(r, 1) r = sprint.user_stories.indexOf(us)
sprint.user_stories.splice(r, 1)
# Persist the milestone change of userstory # Persist the milestone change of userstory
promise = @repo.save(us) promise = @repo.save(us)
@ -340,43 +352,55 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# From backlog to sprint # From backlog to sprint
newSprint = @scope.sprintsById[newSprintId] newSprint = @scope.sprintsById[newSprintId]
if us.milestone == null if oldSprintId == null
us.milestone = newSprintId us.milestone = newSprintId for us in usList
@scope.$apply => @scope.$apply =>
args = [newUsIndex, 0].concat(usList)
# Add moving us to sprint user stories list # 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. # Remove moving us from backlog userstories lists.
r = @scope.visibleUserstories.indexOf(us) for us, key in usList
@scope.visibleUserstories.splice(r, 1) r = @scope.visibleUserstories.indexOf(us)
r = @scope.userstories.indexOf(us) @scope.visibleUserstories.splice(r, 1)
@scope.userstories.splice(r, 1)
r = @scope.userstories.indexOf(us)
@scope.userstories.splice(r, 1)
# From sprint to sprint # From sprint to sprint
else else
us.milestone = newSprintId us.milestone = newSprintId for us in usList
@scope.$apply => @scope.$apply =>
args = [newUsIndex, 0].concat(usList)
# Add new us to backlog userstories list # 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. # Remove the us from the sprint list.
oldSprint = @scope.sprintsById[oldSprintId] for us in usList
r = oldSprint.user_stories.indexOf(us) oldSprint = @scope.sprintsById[oldSprintId]
oldSprint.user_stories.splice(r, 1) r = oldSprint.user_stories.indexOf(us)
oldSprint.user_stories.splice(r, 1)
# Persist the milestone change of userstory # Persist the milestone change of userstory
promise = @repo.save(us) promises = _.map usList, (us) => @repo.save(us)
# Rehash userstories order field # Rehash userstories order field
# and persist in bulk all changes. # and persist in bulk all changes.
promise = promise.then => promise = @q.all.apply(null, promises).then =>
items = @.resortUserStories(newSprint.user_stories, "sprint_order") items = @.resortUserStories(newSprint.user_stories, "sprint_order")
data = @.prepareBulkUpdateData(items, "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) @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, -> promise.then null, ->
console.log "FAIL" # TODO console.log "FAIL" # TODO
@ -545,6 +569,7 @@ BacklogDirective = ($repo, $rootscope) ->
# Enable move to current sprint only when there are selected us's # Enable move to current sprint only when there are selected us's
$el.on "change", ".backlog-table-body .user-stories input:checkbox", (event) -> $el.on "change", ".backlog-table-body .user-stories input:checkbox", (event) ->
target = angular.element(event.currentTarget)
moveToCurrentSprintDom = $el.find("#move-to-current-sprint") moveToCurrentSprintDom = $el.find("#move-to-current-sprint")
selectedUsDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") selectedUsDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked")
@ -553,6 +578,8 @@ BacklogDirective = ($repo, $rootscope) ->
else else
moveToCurrentSprintDom.hide() moveToCurrentSprintDom.hide()
target.closest('.us-item-row').toggleClass('ui-multisortable-multiple')
$el.on "click", "#move-to-current-sprint", (event) => $el.on "click", "#move-to-current-sprint", (event) =>
# Calculating the us's to be modified # Calculating the us's to be modified
ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked")

View File

@ -69,23 +69,35 @@ BacklogSortableDirective = ($repo, $rs, $rootscope) ->
cursorAt: {right: 15} cursorAt: {right: 15}
}) })
$el.on "sortreceive", (event, ui) -> $el.on "multiplesortreceive", (event, ui) ->
itemUs = ui.item.scope().us itemUs = ui.item.scope().us
itemIndex = ui.item.index() itemIndex = ui.item.index()
deleteElement(ui.item) 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') ui.item.find('a').removeClass('noclick')
$el.on "sortstop", (event, ui) -> $el.on "multiplesortstop", (event, ui) ->
# When parent not exists, do nothing # When parent not exists, do nothing
if ui.item.parent().length == 0 if $(ui.items[0]).parent().length == 0
return return
itemUs = ui.item.scope().us items = _.sortBy ui.items, (item) ->
itemIndex = ui.item.index() return $(item).index()
$scope.$emit("sprint:us:move", itemUs, itemIndex, null)
ui.item.find('a').removeClass('noclick') 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) -> $el.on "sortstart", (event, ui) ->
ui.item.find('a').addClass('noclick') ui.item.find('a').addClass('noclick')
@ -113,7 +125,7 @@ BacklogEmptySortableDirective = ($repo, $rs, $rootscope) ->
itemIndex = ui.item.index() itemIndex = ui.item.index()
deleteElement(ui.item) 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') ui.item.find('a').removeClass('noclick')
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
@ -132,24 +144,33 @@ SprintSortableDirective = ($repo, $rs, $rootscope) ->
connectWith: ".sprint-table,.backlog-table-body,.empty-backlog" connectWith: ".sprint-table,.backlog-table-body,.empty-backlog"
}) })
$el.on "sortreceive", (event, ui) -> $el.on "multiplesortreceive", (event, ui) ->
itemUs = ui.item.scope().us items = _.sortBy ui.items, (item) ->
itemIndex = ui.item.index() return $(item).index()
deleteElement(ui.item) index = _.min _.map items, (item) ->
$scope.$emit("sprint:us:move", itemUs, itemIndex, $scope.sprint.id) return $(item).index()
ui.item.find('a').removeClass('noclick')
$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 # When parent not exists, do nothing
if ui.item.parent().length == 0 if ui.item.parent().length == 0
return return
itemUs = ui.item.scope().us itemUs = ui.item.scope().us
itemIndex = ui.item.index() itemIndex = ui.item.index()
$scope.$emit("sprint:us:move", itemUs, itemIndex, $scope.sprint.id)
ui.item.find('a').removeClass('noclick') ui.item.find('a').removeClass('noclick')
$scope.$emit("sprint:us:move", [itemUs], itemIndex, $scope.sprint.id)
return {link:link} return {link:link}

View File

@ -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))

View File

@ -789,7 +789,6 @@ $.Widget.prototype = {
} }
} }
} }
this.element.trigger( event, data ); this.element.trigger( event, data );
return !( $.isFunction( callback ) && return !( $.isFunction( callback ) &&
callback.apply( this.element[0], [ event ].concat( data ) ) === false || callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
@ -2368,11 +2367,9 @@ $.ui.ddmanager = {
if ( draggable.options.refreshPositions ) { if ( draggable.options.refreshPositions ) {
$.ui.ddmanager.prepareOffsets( draggable, event ); $.ui.ddmanager.prepareOffsets( draggable, event );
} }
// Run through all droppables and check their positions based on specific tolerance options // Run through all droppables and check their positions based on specific tolerance options
$.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() { $.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() {
if ( this.options.disabled || this.greedyChild || !this.visible || $(this).data('dragMultipleActive')) {
if ( this.options.disabled || this.greedyChild || !this.visible ) {
return; return;
} }
@ -4107,11 +4104,15 @@ return $.widget("ui.sortable", $.ui.mouse, {
//Rearrange //Rearrange
for (i = this.items.length - 1; i >= 0; i--) { for (i = this.items.length - 1; i >= 0; i--) {
//Cache variables and intersection, continue if no intersection //Cache variables and intersection, continue if no intersection
item = this.items[i]; item = this.items[i];
itemElement = item.item[0]; itemElement = item.item[0];
intersection = this._intersectsWithPointer(item); intersection = this._intersectsWithPointer(item);
if (item.item.data('dragMultipleActive')) {
continue;
}
if (!intersection) { if (!intersection) {
continue; continue;
} }
@ -4166,7 +4167,6 @@ return $.widget("ui.sortable", $.ui.mouse, {
}, },
_mouseStop: function(event, noPropagation) { _mouseStop: function(event, noPropagation) {
if(!event) { if(!event) {
return; return;
} }
@ -4471,6 +4471,9 @@ return $.widget("ui.sortable", $.ui.mouse, {
for (i = this.items.length - 1; i >= 0; i--){ for (i = this.items.length - 1; i >= 0; i--){
item = this.items[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 //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]) { 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; return element;
}, },
update: function(container, p) { update: function(container, p) {
// 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that // 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 // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
if(className && !o.forcePlaceholderSize) { if(className && !o.forcePlaceholderSize) {
@ -4661,7 +4663,6 @@ return $.widget("ui.sortable", $.ui.mouse, {
}, },
_createHelper: function(event) { _createHelper: function(event) {
var o = this.options, 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); 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) { _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)); 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: //Various things done here to improve the performance:

View File

@ -82,8 +82,9 @@ paths.js = [
paths.app + "vendor/jquery-textcomplete/jquery.textcomplete.js", paths.app + "vendor/jquery-textcomplete/jquery.textcomplete.js",
paths.app + "vendor/markitup/markitup/jquery.markitup.js", paths.app + "vendor/markitup/markitup/jquery.markitup.js",
paths.app + "vendor/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js", paths.app + "vendor/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js",
paths.app + "js/jquery.ui.git.js", paths.app + "js/jquery.ui.git-custom.js",
paths.app + "js/sha1.js", paths.app + "js/jquery-ui.drag-multiple-custom.js",
paths.app + "js/sha1-custom.js",
paths.app + "plugins/**/*.js" paths.app + "plugins/**/*.js"
] ]