Merge pull request #1 from taigaio/backlog-sortable-refactor

Backlog sortable refactor
stable
David Barragán Merino 2014-07-26 18:31:21 +02:00
commit 553096c496
7 changed files with 1205 additions and 1126 deletions

View File

@ -450,7 +450,7 @@ BacklogDirective = ($repo, $rootscope) ->
linkToolbar($scope, $el, $attrs, $ctrl)
linkFilters($scope, $el, $attrs, $ctrl)
linkDoomLine($scope, $el, $attrs, $ctrl)
# linkDoomLine($scope, $el, $attrs, $ctrl)
$el.find(".backlog-table-body").disableSelection()

View File

@ -34,67 +34,51 @@ module = angular.module("taigaBacklog")
## Sortable Directive
#############################################################################
BacklogSortableDirective = ($repo, $rs, $rootscope) ->
deleteElement = (el) ->
el.scope().$destroy()
el.off()
el.remove()
#########################
## Drag & Drop Link
#########################
BacklogSortableDirective = ($repo, $rs, $rootscope) ->
# Notes about jquery bug:
# http://stackoverflow.com/questions/5791886/jquery-draggable-shows-
# helper-in-wrong-place-when-scrolled-down-page
linkSortable = ($scope, $el, $attrs, $ctrl) ->
# State
oldParentScope = null
newParentScope = null
itemEl = null
tdom = $el
deleteElement = (itemEl) ->
# Completelly remove item and its scope from dom
itemEl.scope().$destroy()
itemEl.off()
itemEl.remove()
tdom.sortable({
# handle: ".icon-drag-v",
items: "div.sprint-table > div.row, .backlog-table-body > div.row"
link = ($scope, $el, $attrs) ->
$el.sortable({
connectWith: ".sprint-table"
handle: ".icon-drag-v",
containment: ".wrapper"
dropOnEmpty: true
# With scroll activated, it has strange behavior
# with not full screen browser window.
scroll: false
# A consequence of length of backlog user story item
# the default tolerance ("intersection") not works properly.
tolerance: "pointer"
# Revert on backlog is disabled bacause it works bad. Something
# on the current taiga backlog structure or style makes jquery ui
# works unexpectly (in some circumstances calculates wrong
# position for revert).
revert: false
})
tdom.on "sortstop", (event, ui) ->
# Common state for stop event handler
parentEl = ui.item.parent()
itemEl = ui.item
itemUs = itemEl.scope().us
itemIndex = itemEl.index()
newParentScope = parentEl.scope()
$el.on "sortreceive", (event, ui) ->
itemUs = ui.item.scope().us
itemIndex = ui.item.index()
if itemEl.is(".milestone-us-item-row") and parentEl.is(".backlog-table-body")
deleteElement(itemEl)
$scope.$broadcast("sprint:us:move", itemUs, itemIndex, null)
deleteElement(ui.item)
$scope.$emit("sprint:us:move", itemUs, itemIndex, null)
else if itemEl.is(".us-item-row") and parentEl.is(".sprint-table")
deleteElement(itemEl)
$scope.$broadcast("sprint:us:move", itemUs, itemIndex, newParentScope.sprint.id)
$el.on "sortstop", (event, ui) ->
# When parent not exists, do nothing
if ui.item.parent().length == 0
return
else if parentEl.is(".sprint-table") and newParentScope.sprint.id != oldParentScope.sprint.id
deleteElement(itemEl)
$scope.$broadcast("sprint:us:move", itemUs, itemIndex, newParentScope.sprint.id)
else
$scope.$broadcast("sprint:us:move", itemUs, itemIndex, itemUs.milestone)
tdom.on "sortstart", (event, ui) ->
oldParentScope = ui.item.parent().scope()
tdom.on "sort", (event, ui) ->
ui.helper.css("background-color", "#ddd")
tdom.on "sortbeforestop", (event, ui) ->
ui.helper.css("background-color", "transparent")
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
linkSortable($scope, $el, $attrs, $ctrl)
itemUs = ui.item.scope().us
itemIndex = ui.item.index()
$scope.$emit("sprint:us:move", itemUs, itemIndex, null)
$scope.$on "$destroy", ->
$el.off()
@ -102,9 +86,43 @@ BacklogSortableDirective = ($repo, $rs, $rootscope) ->
return {link: link}
SprintSortableDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) ->
$el.sortable({
dropOnEmpty: true
connectWith: ".sprint-table,.backlog-table-body"
})
$el.on "sortreceive", (event, ui) ->
itemUs = ui.item.scope().us
itemIndex = ui.item.index()
deleteElement(ui.item)
$scope.$emit("sprint:us:move", itemUs, itemIndex, $scope.sprint.id)
$el.on "sortstop", (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)
return {link:link}
module.directive("tgBacklogSortable", [
"$tgRepo",
"$tgResources",
"$rootScope",
BacklogSortableDirective
])
module.directive("tgSprintSortable", [
"$tgRepo",
"$tgResources",
"$rootScope",
SprintSortableDirective
])

View File

@ -1,4 +1,4 @@
/*! jQuery UI - v1.11.1-pre - 2014-07-02
/*! jQuery UI - v1.11.1-pre - 2014-07-24
* http://jqueryui.com
* Includes: core.js, widget.js, mouse.js, draggable.js, droppable.js, resizable.js, selectable.js, sortable.js, effect.js, accordion.js, autocomplete.js, button.js, datepicker.js, dialog.js, effect-blind.js, effect-bounce.js, effect-clip.js, effect-drop.js, effect-explode.js, effect-fade.js, effect-fold.js, effect-highlight.js, effect-puff.js, effect-pulsate.js, effect-scale.js, effect-shake.js, effect-size.js, effect-slide.js, effect-transfer.js, menu.js, position.js, progressbar.js, selectmenu.js, slider.js, spinner.js, tabs.js, tooltip.js
* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
@ -313,9 +313,16 @@ var widget_uuid = 0,
$.cleanData = (function( orig ) {
return function( elems ) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
var events, elem, i;
for ( i = 0; (elem = elems[i]) != null; i++ ) {
try {
// Only trigger remove when necessary to save time
events = $._data( elem, "events" );
if ( events && events.remove ) {
$( elem ).triggerHandler( "remove" );
}
// http://bugs.jquery.com/ticket/8235
} catch( e ) {}
}
@ -1078,6 +1085,7 @@ $.widget("ui.draggable", $.ui.mouse, {
_setOption: function( key, value ) {
this._super( key, value );
if ( key === "handle" ) {
this._removeHandleClassName();
this._setHandleClassName();
}
},
@ -1238,8 +1246,8 @@ $.widget("ui.draggable", $.ui.mouse, {
this.position = ui.position;
}
this.helper[ 0 ].style.left = this.positionAbs.left + "px";
this.helper[ 0 ].style.top = this.positionAbs.top + "px";
this.helper[ 0 ].style.left = this.position.left + "px";
this.helper[ 0 ].style.top = this.position.top + "px";
if ($.ui.ddmanager) {
$.ui.ddmanager.drag(this, event);
@ -1314,14 +1322,13 @@ $.widget("ui.draggable", $.ui.mouse, {
},
_setHandleClassName: function() {
this._removeHandleClassName();
$( this.options.handle || this.element ).addClass( "ui-draggable-handle" );
this.handleElement = this.options.handle ?
this.element.find( this.options.handle ) : this.element;
this.handleElement.addClass( "ui-draggable-handle" );
},
_removeHandleClassName: function() {
this.element.find( ".ui-draggable-handle" )
.addBack()
.removeClass( "ui-draggable-handle" );
this.handleElement.removeClass( "ui-draggable-handle" );
},
_createHelper: function(event) {
@ -2735,22 +2742,14 @@ $.widget("ui.resizable", $.ui.mouse, {
_mouseDrag: function(event) {
var data,
el = this.helper, props = {},
var data, props,
smp = this.originalMousePosition,
a = this.axis,
dx = (event.pageX-smp.left)||0,
dy = (event.pageY-smp.top)||0,
trigger = this._change[a];
this.prevPosition = {
top: this.position.top,
left: this.position.left
};
this.prevSize = {
width: this.size.width,
height: this.size.height
};
this._updatePrevProperties();
if (!trigger) {
return false;
@ -2769,26 +2768,16 @@ $.widget("ui.resizable", $.ui.mouse, {
this._propagate("resize", event);
if ( this.position.top !== this.prevPosition.top ) {
props.top = this.position.top + "px";
}
if ( this.position.left !== this.prevPosition.left ) {
props.left = this.position.left + "px";
}
if ( this.size.width !== this.prevSize.width ) {
props.width = this.size.width + "px";
}
if ( this.size.height !== this.prevSize.height ) {
props.height = this.size.height + "px";
}
el.css( props );
props = this._applyChanges();
if ( !this._helper && this._proportionallyResizeElements.length ) {
this._proportionallyResize();
}
if ( !$.isEmptyObject( props ) ) {
this._updatePrevProperties();
this._trigger( "resize", event, this.ui() );
this._applyChanges();
}
return false;
@ -2837,6 +2826,38 @@ $.widget("ui.resizable", $.ui.mouse, {
},
_updatePrevProperties: function() {
this.prevPosition = {
top: this.position.top,
left: this.position.left
};
this.prevSize = {
width: this.size.width,
height: this.size.height
};
},
_applyChanges: function() {
var props = {};
if ( this.position.top !== this.prevPosition.top ) {
props.top = this.position.top + "px";
}
if ( this.position.left !== this.prevPosition.left ) {
props.left = this.position.left + "px";
}
if ( this.size.width !== this.prevSize.width ) {
props.width = this.size.width + "px";
}
if ( this.size.height !== this.prevSize.height ) {
props.height = this.size.height + "px";
}
this.helper.css( props );
return props;
},
_updateVirtualBoundaries: function(forceAspectRatio) {
var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b,
o = this.options;
@ -3059,9 +3080,7 @@ $.widget("ui.resizable", $.ui.mouse, {
position: this.position,
size: this.size,
originalSize: this.originalSize,
originalPosition: this.originalPosition,
prevSize: this.prevSize,
prevPosition: this.prevPosition
originalPosition: this.originalPosition
};
}
@ -3175,7 +3194,7 @@ $.ui.plugin.add( "resizable", "containment", {
}
},
resize: function( event, ui ) {
resize: function( event ) {
var woset, hoset, isParent, isOffsetRelative,
that = $( this ).resizable( "instance" ),
o = that.options,
@ -3241,10 +3260,10 @@ $.ui.plugin.add( "resizable", "containment", {
}
if ( !continueResize ){
that.position.left = ui.prevPosition.left;
that.position.top = ui.prevPosition.top;
that.size.width = ui.prevSize.width;
that.size.height = ui.prevSize.height;
that.position.left = that.prevPosition.left;
that.position.top = that.prevPosition.top;
that.size.width = that.prevSize.width;
that.size.height = that.prevSize.height;
}
},
@ -4078,8 +4097,9 @@ return $.widget("ui.sortable", $.ui.mouse, {
this.positionAbs = this._convertPositionTo("absolute");
//Set the helper position
// Taiga change: this.position -> this.positionAbs
if(!this.options.axis || this.options.axis !== "y") {
this.helper[0].style.left = this.position.left+"px";
this.helper[0].style.left = this.positionAbs.left+"px";
}
if(!this.options.axis || this.options.axis !== "x") {
this.helper[0].style.top = this.positionAbs.top+"px";
@ -5032,7 +5052,11 @@ return $.widget("ui.sortable", $.ui.mouse, {
}
}(function( $ ) {
var dataSpace = "ui-effects-";
var dataSpace = "ui-effects-",
// Create a local jQuery because jQuery Color relies on it and the
// global may not exist with AMD and a custom build (#10199)
jQuery = $;
$.effects = {
effect: {}
@ -6996,7 +7020,9 @@ $.widget( "ui.autocomplete", {
break;
case keyCode.ESCAPE:
if ( this.menu.element.is( ":visible" ) ) {
if ( !this.isMultiLine ) {
this._value( this.term );
}
this.close( event );
// Different browsers have different default behavior for escape
// Single press can mean undo or clear
@ -8684,12 +8710,16 @@ $.extend(Datepicker.prototype, {
datepicker_instActive = inst; // for delegate hover events
inst.dpDiv.empty().append(this._generateHTML(inst));
this._attachHandlers(inst);
inst.dpDiv.find("." + this._dayOverClass + " a");
var origyearshtml,
numMonths = this._getNumberOfMonths(inst),
cols = numMonths[1],
width = 17;
width = 17,
activeCell = inst.dpDiv.find( "." + this._dayOverClass + " a" );
if ( activeCell.length > 0 ) {
datepicker_handleMouseover.apply( activeCell.get( 0 ) );
}
inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");
if (cols > 1) {
@ -9023,7 +9053,8 @@ $.extend(Datepicker.prototype, {
var isDoubled = lookAhead(match),
size = (match === "@" ? 14 : (match === "!" ? 20 :
(match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))),
digits = new RegExp("^\\d{1," + size + "}"),
minSize = (match === "y" ? size : 1),
digits = new RegExp("^\\d{" + minSize + "," + size + "}"),
num = value.substring(iValue).match(digits);
if (!num) {
throw "Missing number at position " + iValue;
@ -9884,8 +9915,11 @@ function datepicker_bindHover(dpDiv) {
$(this).removeClass("ui-datepicker-next-hover");
}
})
.delegate(selector, "mouseover", function(){
if (!$.datepicker._isDisabledDatepicker( datepicker_instActive.inline ? dpDiv.parent()[0] : datepicker_instActive.input[0])) {
.delegate( selector, "mouseover", datepicker_handleMouseover );
}
function datepicker_handleMouseover() {
if (!$.datepicker._isDisabledDatepicker( datepicker_instActive.inline? datepicker_instActive.dpDiv.parent()[0] : datepicker_instActive.input[0])) {
$(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");
$(this).addClass("ui-state-hover");
if (this.className.indexOf("ui-datepicker-prev") !== -1) {
@ -9895,7 +9929,6 @@ function datepicker_bindHover(dpDiv) {
$(this).addClass("ui-datepicker-next-hover");
}
}
});
}
/* jQuery extend now ignores nulls! */
@ -10209,11 +10242,24 @@ return $.widget( "ui.dialog", {
this._position();
this._createOverlay();
this._moveToTop( null, true );
// Ensure the overlay is moved to the top with the dialog, but only when
// opening. The overlay shouldn't move after the dialog is open so that
// modeless dialogs opened after the modal dialog stack properly.
if ( this.overlay ) {
this.overlay.css( "z-index", this.uiDialog.css( "z-index" ) - 1 );
}
this._show( this.uiDialog, this.options.show, function() {
that._focusTabbable();
that._trigger( "focus" );
});
// Track the dialog immediately upon openening in case a focus event
// somehow occurs outside of the dialog before an element inside the
// dialog is focused (#10152)
this._makeFocusTarget();
this._trigger( "open" );
},
@ -10525,14 +10571,18 @@ return $.widget( "ui.dialog", {
_trackFocus: function() {
this._on( this.widget(), {
"focusin": function( event ) {
this._untrackInstance();
this._trackingInstances().unshift( this );
focusin: function( event ) {
this._makeFocusTarget();
this._focusedElement = $( event.target );
}
});
},
_makeFocusTarget: function() {
this._untrackInstance();
this._trackingInstances().unshift( this );
},
_untrackInstance: function() {
var instances = this._trackingInstances(),
exists = $.inArray( this, instances );
@ -12745,8 +12795,11 @@ $.position = {
offset: withinElement.offset() || { left: 0, top: 0 },
scrollLeft: withinElement.scrollLeft(),
scrollTop: withinElement.scrollTop(),
width: isWindow ? withinElement.width() : withinElement.outerWidth(),
height: isWindow ? withinElement.height() : withinElement.outerHeight()
// support: jQuery 1.6.x
// jQuery 1.6 doesn't support .outerWidth/Height() on documents or windows
width: isWindow || isDocument ? withinElement.width() : withinElement.outerWidth(),
height: isWindow || isDocument ? withinElement.height() : withinElement.outerHeight()
};
}
};
@ -15085,24 +15138,7 @@ return $.widget( "ui.tabs", {
this.element
.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" )
.toggleClass( "ui-tabs-collapsible", options.collapsible )
// Prevent users from focusing disabled tabs via click
.delegate( ".ui-tabs-nav > li", "mousedown" + this.eventNamespace, function( event ) {
if ( $( this ).is( ".ui-state-disabled" ) ) {
event.preventDefault();
}
})
// support: IE <9
// Preventing the default action in mousedown doesn't prevent IE
// from focusing the element, so if the anchor gets focused, blur.
// We don't have to worry about focusing the previously focused
// element since clicking on a non-focusable element should focus
// the body anyway.
.delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() {
if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
this.blur();
}
});
.toggleClass( "ui-tabs-collapsible", options.collapsible );
this._processTabs();
options.active = this._initialActive();
@ -15403,7 +15439,26 @@ return $.widget( "ui.tabs", {
this.tablist = this._getList()
.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
.attr( "role", "tablist" );
.attr( "role", "tablist" )
// Prevent users from focusing disabled tabs via click
.delegate( "> li", "mousedown" + this.eventNamespace, function( event ) {
if ( $( this ).is( ".ui-state-disabled" ) ) {
event.preventDefault();
}
})
// support: IE <9
// Preventing the default action in mousedown doesn't prevent IE
// from focusing the element, so if the anchor gets focused, blur.
// We don't have to worry about focusing the previously focused
// element since clicking on a non-focusable element should focus
// the body anyway.
.delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() {
if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
this.blur();
}
});
this.tabs = this.tablist.find( "> li:has(a[href])" )
.addClass( "ui-state-default ui-corner-top" )
@ -15721,6 +15776,8 @@ return $.widget( "ui.tabs", {
.removeAttr( "tabIndex" )
.removeUniqueId();
this.tablist.unbind( this.eventNamespace );
this.tabs.add( this.panels ).each(function() {
if ( $.data( this, "ui-tabs-destroy" ) ) {
$( this ).remove();

View File

@ -5,7 +5,7 @@ block head
block content
div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl",
ng-init="section='backlog'", tg-backlog-sortable)
ng-init="section='backlog'")
sidebar.menu-secondary.extrabar.filters-bar(tg-backlog-filters)
include views/modules/backlog-filters
section.main.backlog

View File

@ -6,5 +6,5 @@ section.backlog-table-header
span.header-points Points
span.icon.icon-arrow-bottom
section.backlog-table-body
section.backlog-table-body(tg-backlog-sortable)
include ../components/backlog-row

View File

@ -10,7 +10,8 @@ section.sprints
a.button.button-green(ng-click="ctrl.addNewSprint()", href="", title="Add New US")
span.text + New sprint
section.sprint(ng-repeat="sprint in sprints track by sprint.id", tg-backlog-sprint="sprint")
section.sprint(ng-repeat="sprint in sprints track by sprint.id"
tg-backlog-sprint="sprint")
header
div.sprint-name
a.icon.icon-arrow-up(href="", title="compact Sprint")
@ -27,7 +28,7 @@ section.sprints
span.description total
div.sprint-progress-bar
div.current-progress(tg-sprint-progressbar="sprint")
div.sprint-table
div.sprint-table(tg-sprint-sortable)
div.row.milestone-us-item-row(ng-repeat="us in sprint.user_stories|orderBy:order track by us.id")
div.column-us.width-8
a.us-name.clickable(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", title="")

View File

@ -116,6 +116,9 @@
}
.sprint-table {
@include slide(1000px, overflow-y);
&.open {
min-height: 50px;
}
.row {
@include table-flex();
border-bottom: 1px solid $gray-light;