2 * jQuery UI Sortable 1.8.16
4 * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
5 * Dual licensed under the MIT or GPL Version 2 licenses.
6 * http://jquery.org/license
8 * http://docs.jquery.com/UI/Sortables
15 (function( $, undefined ) {
17 $.widget("ui.sortable", $.ui
.mouse
, {
18 widgetEventPrefix
: "sort",
27 forcePlaceholderSize
: false,
28 forceHelperSize
: false,
37 scrollSensitivity
: 20,
40 tolerance
: "intersect",
46 this.containerCache
= {};
47 this.element
.addClass("ui-sortable");
52 //Let's determine if the items are being displayed horizontally
53 this.floating
= this.items
.length
? o
.axis
=== 'x' || (/left|right/).test(this.items
[0].item
.css('float')) || (/inline|table-cell/).test(this.items
[0].item
.css('display')) : false;
55 //Let's determine the parent's offset
56 this.offset
= this.element
.offset();
58 //Initialize mouse events for interaction
65 .removeClass("ui-sortable ui-sortable-disabled")
66 .removeData("sortable")
70 for ( var i
= this.items
.length
- 1; i
>= 0; i
-- )
71 this.items
[i
].item
.removeData("sortable-item");
76 _setOption: function(key
, value
){
77 if ( key
=== "disabled" ) {
78 this.options
[ key
] = value
;
81 [ value
? "addClass" : "removeClass"]( "ui-sortable-disabled" );
83 // Don't call widget base _setOption for disable as it adds ui-state-disabled class
84 $.Widget
.prototype._setOption
.apply(this, arguments
);
88 _mouseCapture: function(event
, overrideHandle
) {
94 if(this.options
.disabled
|| this.options
.type
== 'static') return false;
96 //We have to refresh the items data once first
97 this._refreshItems(event
);
99 //Find out if the clicked node (or one of its parents) is a actual item in this.items
100 var currentItem
= null, self
= this, nodes
= $(event
.target
).parents().each(function() {
101 if($.data(this, 'sortable-item') == self
) {
102 currentItem
= $(this);
106 if($.data(event
.target
, 'sortable-item') == self
) currentItem
= $(event
.target
);
108 if(!currentItem
) return false;
109 if(this.options
.handle
&& !overrideHandle
) {
110 var validHandle
= false;
112 $(this.options
.handle
, currentItem
).find("*").andSelf().each(function() { if(this == event
.target
) validHandle
= true; });
113 if(!validHandle
) return false;
116 this.currentItem
= currentItem
;
117 this._removeCurrentsFromItems();
122 _mouseStart: function(event
, overrideHandle
, noActivation
) {
124 var o
= this.options
, self
= this;
125 this.currentContainer
= this;
127 //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
128 this.refreshPositions();
130 //Create and append the visible helper
131 this.helper
= this._createHelper(event
);
133 //Cache the helper size
134 this._cacheHelperProportions();
137 * - Position generation -
138 * This block generates everything position related - it's the core of draggables.
141 //Cache the margins of the original element
142 this._cacheMargins();
144 //Get the next scrolling parent
145 this.scrollParent
= this.helper
.scrollParent();
147 //The element's absolute position on the page minus margins
148 this.offset
= this.currentItem
.offset();
150 top
: this.offset
.top
- this.margins
.top
,
151 left
: this.offset
.left
- this.margins
.left
154 // Only after we got the offset, we can change the helper's position to absolute
155 // TODO: Still need to figure out a way to make relative sorting possible
156 this.helper
.css("position", "absolute");
157 this.cssPosition
= this.helper
.css("position");
159 $.extend(this.offset
, {
160 click
: { //Where the click happened, relative to the element
161 left
: event
.pageX
- this.offset
.left
,
162 top
: event
.pageY
- this.offset
.top
164 parent
: this._getParentOffset(),
165 relative
: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
168 //Generate the original position
169 this.originalPosition
= this._generatePosition(event
);
170 this.originalPageX
= event
.pageX
;
171 this.originalPageY
= event
.pageY
;
173 //Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
174 (o
.cursorAt
&& this._adjustOffsetFromHelper(o
.cursorAt
));
176 //Cache the former DOM position
177 this.domPosition
= { prev
: this.currentItem
.prev()[0], parent
: this.currentItem
.parent()[0] };
179 //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
180 if(this.helper
[0] != this.currentItem
[0]) {
181 this.currentItem
.hide();
184 //Create the placeholder
185 this._createPlaceholder();
187 //Set a containment if given in the options
189 this._setContainment();
191 if(o
.cursor
) { // cursor option
192 if ($('body').css("cursor")) this._storedCursor
= $('body').css("cursor");
193 $('body').css("cursor", o
.cursor
);
196 if(o
.opacity
) { // opacity option
197 if (this.helper
.css("opacity")) this._storedOpacity
= this.helper
.css("opacity");
198 this.helper
.css("opacity", o
.opacity
);
201 if(o
.zIndex
) { // zIndex option
202 if (this.helper
.css("zIndex")) this._storedZIndex
= this.helper
.css("zIndex");
203 this.helper
.css("zIndex", o
.zIndex
);
207 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML')
208 this.overflowOffset
= this.scrollParent
.offset();
211 this._trigger("start", event
, this._uiHash());
213 //Recache the helper size
214 if(!this._preserveHelperProportions
)
215 this._cacheHelperProportions();
218 //Post 'activate' events to possible containers
220 for (var i
= this.containers
.length
- 1; i
>= 0; i
--) { this.containers
[i
]._trigger("activate", event
, self
._uiHash(this)); }
223 //Prepare possible droppables
225 $.ui
.ddmanager
.current
= this;
227 if ($.ui
.ddmanager
&& !o
.dropBehaviour
)
228 $.ui
.ddmanager
.prepareOffsets(this, event
);
230 this.dragging
= true;
232 this.helper
.addClass("ui-sortable-helper");
233 this._mouseDrag(event
); //Execute the drag once - this causes the helper not to be visible before getting its correct position
238 _mouseDrag: function(event
) {
240 //Compute the helpers position
241 this.position
= this._generatePosition(event
);
242 this.positionAbs
= this._convertPositionTo("absolute");
244 if (!this.lastPositionAbs
) {
245 this.lastPositionAbs
= this.positionAbs
;
249 if(this.options
.scroll
) {
250 var o
= this.options
, scrolled
= false;
251 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML') {
253 if((this.overflowOffset
.top
+ this.scrollParent
[0].offsetHeight
) - event
.pageY
< o
.scrollSensitivity
)
254 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
+ o
.scrollSpeed
;
255 else if(event
.pageY
- this.overflowOffset
.top
< o
.scrollSensitivity
)
256 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
- o
.scrollSpeed
;
258 if((this.overflowOffset
.left
+ this.scrollParent
[0].offsetWidth
) - event
.pageX
< o
.scrollSensitivity
)
259 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
+ o
.scrollSpeed
;
260 else if(event
.pageX
- this.overflowOffset
.left
< o
.scrollSensitivity
)
261 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
- o
.scrollSpeed
;
265 if(event
.pageY
- $(document
).scrollTop() < o
.scrollSensitivity
)
266 scrolled
= $(document
).scrollTop($(document
).scrollTop() - o
.scrollSpeed
);
267 else if($(window
).height() - (event
.pageY
- $(document
).scrollTop()) < o
.scrollSensitivity
)
268 scrolled
= $(document
).scrollTop($(document
).scrollTop() + o
.scrollSpeed
);
270 if(event
.pageX
- $(document
).scrollLeft() < o
.scrollSensitivity
)
271 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() - o
.scrollSpeed
);
272 else if($(window
).width() - (event
.pageX
- $(document
).scrollLeft()) < o
.scrollSensitivity
)
273 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() + o
.scrollSpeed
);
277 if(scrolled
!== false && $.ui
.ddmanager
&& !o
.dropBehaviour
)
278 $.ui
.ddmanager
.prepareOffsets(this, event
);
281 //Regenerate the absolute position used for position checks
282 this.positionAbs
= this._convertPositionTo("absolute");
284 //Set the helper position
285 if(!this.options
.axis
|| this.options
.axis
!= "y") this.helper
[0].style
.left
= this.position
.left
+'px';
286 if(!this.options
.axis
|| this.options
.axis
!= "x") this.helper
[0].style
.top
= this.position
.top
+'px';
289 for (var i
= this.items
.length
- 1; i
>= 0; i
--) {
291 //Cache variables and intersection, continue if no intersection
292 var item
= this.items
[i
], itemElement
= item
.item
[0], intersection
= this._intersectsWithPointer(item
);
293 if (!intersection
) continue;
295 if(itemElement
!= this.currentItem
[0] //cannot intersect with itself
296 && this.placeholder
[intersection
== 1 ? "next" : "prev"]()[0] != itemElement
//no useless actions that have been done before
297 && !$.ui
.contains(this.placeholder
[0], itemElement
) //no action if the item moved is the parent of the item checked
298 && (this.options
.type
== 'semi-dynamic' ? !$.ui
.contains(this.element
[0], itemElement
) : true)
299 //&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container
302 this.direction
= intersection
== 1 ? "down" : "up";
304 if (this.options
.tolerance
== "pointer" || this._intersectsWithSides(item
)) {
305 this._rearrange(event
, item
);
310 this._trigger("change", event
, this._uiHash());
315 //Post events to containers
316 this._contactContainers(event
);
318 //Interconnect with droppables
319 if($.ui
.ddmanager
) $.ui
.ddmanager
.drag(this, event
);
322 this._trigger('sort', event
, this._uiHash());
324 this.lastPositionAbs
= this.positionAbs
;
329 _mouseStop: function(event
, noPropagation
) {
333 //If we are using droppables, inform the manager about the drop
334 if ($.ui
.ddmanager
&& !this.options
.dropBehaviour
)
335 $.ui
.ddmanager
.drop(this, event
);
337 if(this.options
.revert
) {
339 var cur
= self
.placeholder
.offset();
341 self
.reverting
= true;
343 $(this.helper
).animate({
344 left
: cur
.left
- this.offset
.parent
.left
- self
.margins
.left
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollLeft
),
345 top
: cur
.top
- this.offset
.parent
.top
- self
.margins
.top
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollTop
)
346 }, parseInt(this.options
.revert
, 10) || 500, function() {
350 this._clear(event
, noPropagation
);
363 this._mouseUp({ target
: null });
365 if(this.options
.helper
== "original")
366 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
368 this.currentItem
.show();
370 //Post deactivating events to containers
371 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
372 this.containers
[i
]._trigger("deactivate", null, self
._uiHash(this));
373 if(this.containers
[i
].containerCache
.over
) {
374 this.containers
[i
]._trigger("out", null, self
._uiHash(this));
375 this.containers
[i
].containerCache
.over
= 0;
381 if (this.placeholder
) {
382 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
383 if(this.placeholder
[0].parentNode
) this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
384 if(this.options
.helper
!= "original" && this.helper
&& this.helper
[0].parentNode
) this.helper
.remove();
393 if(this.domPosition
.prev
) {
394 $(this.domPosition
.prev
).after(this.currentItem
);
396 $(this.domPosition
.parent
).prepend(this.currentItem
);
404 serialize: function(o
) {
406 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
407 var str
= []; o
= o
|| {};
409 $(items
).each(function() {
410 var res
= ($(o
.item
|| this).attr(o
.attribute
|| 'id') || '').match(o
.expression
|| (/(.+)[-=_](.+)/));
411 if(res
) str
.push((o
.key
|| res
[1]+'[]')+'='+(o
.key
&& o
.expression
? res
[1] : res
[2]));
414 if(!str
.length
&& o
.key
) {
415 str
.push(o
.key
+ '=');
418 return str
.join('&');
422 toArray: function(o
) {
424 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
425 var ret
= []; o
= o
|| {};
427 items
.each(function() { ret
.push($(o
.item
|| this).attr(o
.attribute
|| 'id') || ''); });
432 /* Be careful with the following core functions */
433 _intersectsWith: function(item
) {
435 var x1
= this.positionAbs
.left
,
436 x2
= x1
+ this.helperProportions
.width
,
437 y1
= this.positionAbs
.top
,
438 y2
= y1
+ this.helperProportions
.height
;
445 var dyClick
= this.offset
.click
.top
,
446 dxClick
= this.offset
.click
.left
;
448 var isOverElement
= (y1
+ dyClick
) > t
&& (y1
+ dyClick
) < b
&& (x1
+ dxClick
) > l
&& (x1
+ dxClick
) < r
;
450 if( this.options
.tolerance
== "pointer"
451 || this.options
.forcePointerForContainers
452 || (this.options
.tolerance
!= "pointer" && this.helperProportions
[this.floating
? 'width' : 'height'] > item
[this.floating
? 'width' : 'height'])
454 return isOverElement
;
457 return (l
< x1
+ (this.helperProportions
.width
/ 2) // Right Half
458 && x2
- (this.helperProportions
.width
/ 2) < r
// Left Half
459 && t
< y1
+ (this.helperProportions
.height
/ 2) // Bottom Half
460 && y2
- (this.helperProportions
.height
/ 2) < b
); // Top Half
465 _intersectsWithPointer: function(item
) {
467 var isOverElementHeight
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
, item
.height
),
468 isOverElementWidth
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
, item
.width
),
469 isOverElement
= isOverElementHeight
&& isOverElementWidth
,
470 verticalDirection
= this._getDragVerticalDirection(),
471 horizontalDirection
= this._getDragHorizontalDirection();
476 return this.floating
?
477 ( ((horizontalDirection
&& horizontalDirection
== "right") || verticalDirection
== "down") ? 2 : 1 )
478 : ( verticalDirection
&& (verticalDirection
== "down" ? 2 : 1) );
482 _intersectsWithSides: function(item
) {
484 var isOverBottomHalf
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
+ (item
.height
/2), item
.height
),
485 isOverRightHalf
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
+ (item
.width
/2), item
.width
),
486 verticalDirection
= this._getDragVerticalDirection(),
487 horizontalDirection
= this._getDragHorizontalDirection();
489 if (this.floating
&& horizontalDirection
) {
490 return ((horizontalDirection
== "right" && isOverRightHalf
) || (horizontalDirection
== "left" && !isOverRightHalf
));
492 return verticalDirection
&& ((verticalDirection
== "down" && isOverBottomHalf
) || (verticalDirection
== "up" && !isOverBottomHalf
));
497 _getDragVerticalDirection: function() {
498 var delta
= this.positionAbs
.top
- this.lastPositionAbs
.top
;
499 return delta
!= 0 && (delta
> 0 ? "down" : "up");
502 _getDragHorizontalDirection: function() {
503 var delta
= this.positionAbs
.left
- this.lastPositionAbs
.left
;
504 return delta
!= 0 && (delta
> 0 ? "right" : "left");
507 refresh: function(event
) {
508 this._refreshItems(event
);
509 this.refreshPositions();
513 _connectWith: function() {
514 var options
= this.options
;
515 return options
.connectWith
.constructor == String
516 ? [options
.connectWith
]
517 : options
.connectWith
;
520 _getItemsAsjQuery: function(connected
) {
525 var connectWith
= this._connectWith();
527 if(connectWith
&& connected
) {
528 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
529 var cur
= $(connectWith
[i
]);
530 for (var j
= cur
.length
- 1; j
>= 0; j
--){
531 var inst
= $.data(cur
[j
], 'sortable');
532 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
533 queries
.push([$.isFunction(inst
.options
.items
) ? inst
.options
.items
.call(inst
.element
) : $(inst
.options
.items
, inst
.element
).not(".ui-sortable-helper").not('.ui-sortable-placeholder'), inst
]);
539 queries
.push([$.isFunction(this.options
.items
) ? this.options
.items
.call(this.element
, null, { options
: this.options
, item
: this.currentItem
}) : $(this.options
.items
, this.element
).not(".ui-sortable-helper").not('.ui-sortable-placeholder'), this]);
541 for (var i
= queries
.length
- 1; i
>= 0; i
--){
542 queries
[i
][0].each(function() {
551 _removeCurrentsFromItems: function() {
553 var list
= this.currentItem
.find(":data(sortable-item)");
555 for (var i
=0; i
< this.items
.length
; i
++) {
557 for (var j
=0; j
< list
.length
; j
++) {
558 if(list
[j
] == this.items
[i
].item
[0])
559 this.items
.splice(i
,1);
566 _refreshItems: function(event
) {
569 this.containers
= [this];
570 var items
= this.items
;
572 var queries
= [[$.isFunction(this.options
.items
) ? this.options
.items
.call(this.element
[0], event
, { item
: this.currentItem
}) : $(this.options
.items
, this.element
), this]];
573 var connectWith
= this._connectWith();
576 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
577 var cur
= $(connectWith
[i
]);
578 for (var j
= cur
.length
- 1; j
>= 0; j
--){
579 var inst
= $.data(cur
[j
], 'sortable');
580 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
581 queries
.push([$.isFunction(inst
.options
.items
) ? inst
.options
.items
.call(inst
.element
[0], event
, { item
: this.currentItem
}) : $(inst
.options
.items
, inst
.element
), inst
]);
582 this.containers
.push(inst
);
588 for (var i
= queries
.length
- 1; i
>= 0; i
--) {
589 var targetData
= queries
[i
][1];
590 var _queries
= queries
[i
][0];
592 for (var j
=0, queriesLength
= _queries
.length
; j
< queriesLength
; j
++) {
593 var item
= $(_queries
[j
]);
595 item
.data('sortable-item', targetData
); // Data for target checking (mouse manager)
599 instance
: targetData
,
608 refreshPositions: function(fast
) {
610 //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
611 if(this.offsetParent
&& this.helper
) {
612 this.offset
.parent
= this._getParentOffset();
615 for (var i
= this.items
.length
- 1; i
>= 0; i
--){
616 var item
= this.items
[i
];
618 //We ignore calculating positions of all connected containers when we're not over them
619 if(item
.instance
!= this.currentContainer
&& this.currentContainer
&& item
.item
[0] != this.currentItem
[0])
622 var t
= this.options
.toleranceElement
? $(this.options
.toleranceElement
, item
.item
) : item
.item
;
625 item
.width
= t
.outerWidth();
626 item
.height
= t
.outerHeight();
634 if(this.options
.custom
&& this.options
.custom
.refreshContainers
) {
635 this.options
.custom
.refreshContainers
.call(this);
637 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
638 var p
= this.containers
[i
].element
.offset();
639 this.containers
[i
].containerCache
.left
= p
.left
;
640 this.containers
[i
].containerCache
.top
= p
.top
;
641 this.containers
[i
].containerCache
.width
= this.containers
[i
].element
.outerWidth();
642 this.containers
[i
].containerCache
.height
= this.containers
[i
].element
.outerHeight();
649 _createPlaceholder: function(that
) {
651 var self
= that
|| this, o
= self
.options
;
653 if(!o
.placeholder
|| o
.placeholder
.constructor == String
) {
654 var className
= o
.placeholder
;
656 element: function() {
658 var el
= $(document
.createElement(self
.currentItem
[0].nodeName
))
659 .addClass(className
|| self
.currentItem
[0].className
+" ui-sortable-placeholder")
660 .removeClass("ui-sortable-helper")[0];
663 el
.style
.visibility
= "hidden";
667 update: function(container
, p
) {
669 // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
670 // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
671 if(className
&& !o
.forcePlaceholderSize
) return;
673 //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item
674 if(!p
.height()) { p
.height(self
.currentItem
.innerHeight() - parseInt(self
.currentItem
.css('paddingTop')||0, 10) - parseInt(self
.currentItem
.css('paddingBottom')||0, 10)); };
675 if(!p
.width()) { p
.width(self
.currentItem
.innerWidth() - parseInt(self
.currentItem
.css('paddingLeft')||0, 10) - parseInt(self
.currentItem
.css('paddingRight')||0, 10)); };
680 //Create the placeholder
681 self
.placeholder
= $(o
.placeholder
.element
.call(self
.element
, self
.currentItem
));
683 //Append it after the actual current item
684 self
.currentItem
.after(self
.placeholder
);
686 //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
687 o
.placeholder
.update(self
, self
.placeholder
);
691 _contactContainers: function(event
) {
693 // get innermost container that intersects with item
694 var innermostContainer
= null, innermostIndex
= null;
697 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
699 // never consider a container that's located within the item itself
700 if($.ui
.contains(this.currentItem
[0], this.containers
[i
].element
[0]))
703 if(this._intersectsWith(this.containers
[i
].containerCache
)) {
705 // if we've already found a container and it's more "inner" than this, then continue
706 if(innermostContainer
&& $.ui
.contains(this.containers
[i
].element
[0], innermostContainer
.element
[0]))
709 innermostContainer
= this.containers
[i
];
713 // container doesn't intersect. trigger "out" event if necessary
714 if(this.containers
[i
].containerCache
.over
) {
715 this.containers
[i
]._trigger("out", event
, this._uiHash(this));
716 this.containers
[i
].containerCache
.over
= 0;
722 // if no intersecting containers found, return
723 if(!innermostContainer
) return;
725 // move the item into the container if it's not there already
726 if(this.containers
.length
=== 1) {
727 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
728 this.containers
[innermostIndex
].containerCache
.over
= 1;
729 } else if(this.currentContainer
!= this.containers
[innermostIndex
]) {
731 //When entering a new container, we will find the item with the least distance and append our item near it
732 var dist
= 10000; var itemWithLeastDistance
= null; var base
= this.positionAbs
[this.containers
[innermostIndex
].floating
? 'left' : 'top'];
733 for (var j
= this.items
.length
- 1; j
>= 0; j
--) {
734 if(!$.ui
.contains(this.containers
[innermostIndex
].element
[0], this.items
[j
].item
[0])) continue;
735 var cur
= this.items
[j
][this.containers
[innermostIndex
].floating
? 'left' : 'top'];
736 if(Math
.abs(cur
- base
) < dist
) {
737 dist
= Math
.abs(cur
- base
); itemWithLeastDistance
= this.items
[j
];
741 if(!itemWithLeastDistance
&& !this.options
.dropOnEmpty
) //Check if dropOnEmpty is enabled
744 this.currentContainer
= this.containers
[innermostIndex
];
745 itemWithLeastDistance
? this._rearrange(event
, itemWithLeastDistance
, null, true) : this._rearrange(event
, null, this.containers
[innermostIndex
].element
, true);
746 this._trigger("change", event
, this._uiHash());
747 this.containers
[innermostIndex
]._trigger("change", event
, this._uiHash(this));
749 //Update the placeholder
750 this.options
.placeholder
.update(this.currentContainer
, this.placeholder
);
752 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
753 this.containers
[innermostIndex
].containerCache
.over
= 1;
759 _createHelper: function(event
) {
761 var o
= this.options
;
762 var helper
= $.isFunction(o
.helper
) ? $(o
.helper
.apply(this.element
[0], [event
, this.currentItem
])) : (o
.helper
== 'clone' ? this.currentItem
.clone() : this.currentItem
);
764 if(!helper
.parents('body').length
) //Add the helper to the DOM if that didn't happen already
765 $(o
.appendTo
!= 'parent' ? o
.appendTo
: this.currentItem
[0].parentNode
)[0].appendChild(helper
[0]);
767 if(helper
[0] == this.currentItem
[0])
768 this._storedCSS
= { width
: this.currentItem
[0].style
.width
, height
: this.currentItem
[0].style
.height
, position
: this.currentItem
.css("position"), top
: this.currentItem
.css("top"), left
: this.currentItem
.css("left") };
770 if(helper
[0].style
.width
== '' || o
.forceHelperSize
) helper
.width(this.currentItem
.width());
771 if(helper
[0].style
.height
== '' || o
.forceHelperSize
) helper
.height(this.currentItem
.height());
777 _adjustOffsetFromHelper: function(obj
) {
778 if (typeof obj
== 'string') {
779 obj
= obj
.split(' ');
781 if ($.isArray(obj
)) {
782 obj
= {left
: +obj
[0], top
: +obj
[1] || 0};
785 this.offset
.click
.left
= obj
.left
+ this.margins
.left
;
787 if ('right' in obj
) {
788 this.offset
.click
.left
= this.helperProportions
.width
- obj
.right
+ this.margins
.left
;
791 this.offset
.click
.top
= obj
.top
+ this.margins
.top
;
793 if ('bottom' in obj
) {
794 this.offset
.click
.top
= this.helperProportions
.height
- obj
.bottom
+ this.margins
.top
;
798 _getParentOffset: function() {
801 //Get the offsetParent and cache its position
802 this.offsetParent
= this.helper
.offsetParent();
803 var po
= this.offsetParent
.offset();
805 // This is a special case where we need to modify a offset calculated on start, since the following happened:
806 // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
807 // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
808 // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
809 if(this.cssPosition
== 'absolute' && this.scrollParent
[0] != document
&& $.ui
.contains(this.scrollParent
[0], this.offsetParent
[0])) {
810 po
.left
+= this.scrollParent
.scrollLeft();
811 po
.top
+= this.scrollParent
.scrollTop();
814 if((this.offsetParent
[0] == document
.body
) //This needs to be actually done for all browsers, since pageX/pageY includes this information
815 || (this.offsetParent
[0].tagName
&& this.offsetParent
[0].tagName
.toLowerCase() == 'html' && $.browser
.msie
)) //Ugly IE fix
816 po
= { top
: 0, left
: 0 };
819 top
: po
.top
+ (parseInt(this.offsetParent
.css("borderTopWidth"),10) || 0),
820 left
: po
.left
+ (parseInt(this.offsetParent
.css("borderLeftWidth"),10) || 0)
825 _getRelativeOffset: function() {
827 if(this.cssPosition
== "relative") {
828 var p
= this.currentItem
.position();
830 top
: p
.top
- (parseInt(this.helper
.css("top"),10) || 0) + this.scrollParent
.scrollTop(),
831 left
: p
.left
- (parseInt(this.helper
.css("left"),10) || 0) + this.scrollParent
.scrollLeft()
834 return { top
: 0, left
: 0 };
839 _cacheMargins: function() {
841 left
: (parseInt(this.currentItem
.css("marginLeft"),10) || 0),
842 top
: (parseInt(this.currentItem
.css("marginTop"),10) || 0)
846 _cacheHelperProportions: function() {
847 this.helperProportions
= {
848 width
: this.helper
.outerWidth(),
849 height
: this.helper
.outerHeight()
853 _setContainment: function() {
855 var o
= this.options
;
856 if(o
.containment
== 'parent') o
.containment
= this.helper
[0].parentNode
;
857 if(o
.containment
== 'document' || o
.containment
== 'window') this.containment
= [
858 0 - this.offset
.relative
.left
- this.offset
.parent
.left
,
859 0 - this.offset
.relative
.top
- this.offset
.parent
.top
,
860 $(o
.containment
== 'document' ? document
: window
).width() - this.helperProportions
.width
- this.margins
.left
,
861 ($(o
.containment
== 'document' ? document
: window
).height() || document
.body
.parentNode
.scrollHeight
) - this.helperProportions
.height
- this.margins
.top
864 if(!(/^(document|window|parent)$/).test(o
.containment
)) {
865 var ce
= $(o
.containment
)[0];
866 var co
= $(o
.containment
).offset();
867 var over
= ($(ce
).css("overflow") != 'hidden');
870 co
.left
+ (parseInt($(ce
).css("borderLeftWidth"),10) || 0) + (parseInt($(ce
).css("paddingLeft"),10) || 0) - this.margins
.left
,
871 co
.top
+ (parseInt($(ce
).css("borderTopWidth"),10) || 0) + (parseInt($(ce
).css("paddingTop"),10) || 0) - this.margins
.top
,
872 co
.left
+(over
? Math
.max(ce
.scrollWidth
,ce
.offsetWidth
) : ce
.offsetWidth
) - (parseInt($(ce
).css("borderLeftWidth"),10) || 0) - (parseInt($(ce
).css("paddingRight"),10) || 0) - this.helperProportions
.width
- this.margins
.left
,
873 co
.top
+(over
? Math
.max(ce
.scrollHeight
,ce
.offsetHeight
) : ce
.offsetHeight
) - (parseInt($(ce
).css("borderTopWidth"),10) || 0) - (parseInt($(ce
).css("paddingBottom"),10) || 0) - this.helperProportions
.height
- this.margins
.top
879 _convertPositionTo: function(d
, pos
) {
881 if(!pos
) pos
= this.position
;
882 var mod
= d
== "absolute" ? 1 : -1;
883 var o
= this.options
, scroll
= this.cssPosition
== 'absolute' && !(this.scrollParent
[0] != document
&& $.ui
.contains(this.scrollParent
[0], this.offsetParent
[0])) ? this.offsetParent
: this.scrollParent
, scrollIsRootNode
= (/(html|body)/i).test(scroll
[0].tagName
);
887 pos
.top
// The absolute mouse position
888 + this.offset
.relative
.top
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
889 + this.offset
.parent
.top
* mod
// The offsetParent's offset without borders (offset + border)
890 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ) * mod
)
893 pos
.left
// The absolute mouse position
894 + this.offset
.relative
.left
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
895 + this.offset
.parent
.left
* mod
// The offsetParent's offset without borders (offset + border)
896 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ) * mod
)
902 _generatePosition: function(event
) {
904 var o
= this.options
, scroll
= this.cssPosition
== 'absolute' && !(this.scrollParent
[0] != document
&& $.ui
.contains(this.scrollParent
[0], this.offsetParent
[0])) ? this.offsetParent
: this.scrollParent
, scrollIsRootNode
= (/(html|body)/i).test(scroll
[0].tagName
);
906 // This is another very weird special case that only happens for relative elements:
907 // 1. If the css position is relative
908 // 2. and the scroll parent is the document or similar to the offset parent
909 // we have to refresh the relative offset during the scroll so there are no jumps
910 if(this.cssPosition
== 'relative' && !(this.scrollParent
[0] != document
&& this.scrollParent
[0] != this.offsetParent
[0])) {
911 this.offset
.relative
= this._getRelativeOffset();
914 var pageX
= event
.pageX
;
915 var pageY
= event
.pageY
;
918 * - Position constraining -
919 * Constrain the position to a mix of grid, containment.
922 if(this.originalPosition
) { //If we are not dragging yet, we won't check for options
924 if(this.containment
) {
925 if(event
.pageX
- this.offset
.click
.left
< this.containment
[0]) pageX
= this.containment
[0] + this.offset
.click
.left
;
926 if(event
.pageY
- this.offset
.click
.top
< this.containment
[1]) pageY
= this.containment
[1] + this.offset
.click
.top
;
927 if(event
.pageX
- this.offset
.click
.left
> this.containment
[2]) pageX
= this.containment
[2] + this.offset
.click
.left
;
928 if(event
.pageY
- this.offset
.click
.top
> this.containment
[3]) pageY
= this.containment
[3] + this.offset
.click
.top
;
932 var top
= this.originalPageY
+ Math
.round((pageY
- this.originalPageY
) / o
.grid
[1]) * o
.grid
[1];
933 pageY
= this.containment
? (!(top
- this.offset
.click
.top
< this.containment
[1] || top
- this.offset
.click
.top
> this.containment
[3]) ? top
: (!(top
- this.offset
.click
.top
< this.containment
[1]) ? top
- o
.grid
[1] : top
+ o
.grid
[1])) : top
;
935 var left
= this.originalPageX
+ Math
.round((pageX
- this.originalPageX
) / o
.grid
[0]) * o
.grid
[0];
936 pageX
= this.containment
? (!(left
- this.offset
.click
.left
< this.containment
[0] || left
- this.offset
.click
.left
> this.containment
[2]) ? left
: (!(left
- this.offset
.click
.left
< this.containment
[0]) ? left
- o
.grid
[0] : left
+ o
.grid
[0])) : left
;
943 pageY
// The absolute mouse position
944 - this.offset
.click
.top
// Click offset (relative to the element)
945 - this.offset
.relative
.top
// Only for relative positioned nodes: Relative offset from element to offset parent
946 - this.offset
.parent
.top
// The offsetParent's offset without borders (offset + border)
947 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ))
950 pageX
// The absolute mouse position
951 - this.offset
.click
.left
// Click offset (relative to the element)
952 - this.offset
.relative
.left
// Only for relative positioned nodes: Relative offset from element to offset parent
953 - this.offset
.parent
.left
// The offsetParent's offset without borders (offset + border)
954 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ))
960 _rearrange: function(event
, i
, a
, hardRefresh
) {
962 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
));
964 //Various things done here to improve the performance:
965 // 1. we create a setTimeout, that calls refreshPositions
966 // 2. on the instance, we have a counter variable, that get's higher after every append
967 // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
968 // 4. this lets only the last addition to the timeout stack through
969 this.counter
= this.counter
? ++this.counter
: 1;
970 var self
= this, counter
= this.counter
;
972 window
.setTimeout(function() {
973 if(counter
== self
.counter
) self
.refreshPositions(!hardRefresh
); //Precompute after each DOM insertion, NOT on mousemove
978 _clear: function(event
, noPropagation
) {
980 this.reverting
= false;
981 // We delay all events that have to be triggered to after the point where the placeholder has been removed and
982 // everything else normalized again
983 var delayedTriggers
= [], self
= this;
985 // We first have to update the dom position of the actual currentItem
986 // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
987 if(!this._noFinalSort
&& this.currentItem
.parent().length
) this.placeholder
.before(this.currentItem
);
988 this._noFinalSort
= null;
990 if(this.helper
[0] == this.currentItem
[0]) {
991 for(var i
in this._storedCSS
) {
992 if(this._storedCSS
[i
] == 'auto' || this._storedCSS
[i
] == 'static') this._storedCSS
[i
] = '';
994 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
996 this.currentItem
.show();
999 if(this.fromOutside
&& !noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("receive", event
, this._uiHash(this.fromOutside
)); });
1000 if((this.fromOutside
|| this.domPosition
.prev
!= this.currentItem
.prev().not(".ui-sortable-helper")[0] || this.domPosition
.parent
!= this.currentItem
.parent()[0]) && !noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("update", event
, this._uiHash()); }); //Trigger update callback if the DOM position has changed
1001 if(!$.ui
.contains(this.element
[0], this.currentItem
[0])) { //Node was moved out of the current element
1002 if(!noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("remove", event
, this._uiHash()); });
1003 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
1004 if($.ui
.contains(this.containers
[i
].element
[0], this.currentItem
[0]) && !noPropagation
) {
1005 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("receive", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1006 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("update", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1011 //Post events to containers
1012 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
1013 if(!noPropagation
) delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("deactivate", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1014 if(this.containers
[i
].containerCache
.over
) {
1015 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("out", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1016 this.containers
[i
].containerCache
.over
= 0;
1020 //Do what was originally in plugins
1021 if(this._storedCursor
) $('body').css("cursor", this._storedCursor
); //Reset cursor
1022 if(this._storedOpacity
) this.helper
.css("opacity", this._storedOpacity
); //Reset opacity
1023 if(this._storedZIndex
) this.helper
.css("zIndex", this._storedZIndex
== 'auto' ? '' : this._storedZIndex
); //Reset z-index
1025 this.dragging
= false;
1026 if(this.cancelHelperRemoval
) {
1027 if(!noPropagation
) {
1028 this._trigger("beforeStop", event
, this._uiHash());
1029 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1030 this._trigger("stop", event
, this._uiHash());
1035 if(!noPropagation
) this._trigger("beforeStop", event
, this._uiHash());
1037 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
1038 this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
1040 if(this.helper
[0] != this.currentItem
[0]) this.helper
.remove(); this.helper
= null;
1042 if(!noPropagation
) {
1043 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1044 this._trigger("stop", event
, this._uiHash());
1047 this.fromOutside
= false;
1052 _trigger: function() {
1053 if ($.Widget
.prototype._trigger
.apply(this, arguments
) === false) {
1058 _uiHash: function(inst
) {
1059 var self
= inst
|| this;
1061 helper
: self
.helper
,
1062 placeholder
: self
.placeholder
|| $([]),
1063 position
: self
.position
,
1064 originalPosition
: self
.originalPosition
,
1065 offset
: self
.positionAbs
,
1066 item
: self
.currentItem
,
1067 sender
: inst
? inst
.element
: null
1073 $.extend($.ui
.sortable
, {