2 * jQuery UI Sortable 1.9.2
5 * Copyright 2012 jQuery Foundation and other contributors
6 * Released under the MIT license.
7 * http://jquery.org/license
9 * http://api.jqueryui.com/sortable/
16 (function( $, undefined ) {
18 $.widget("ui.sortable", $.ui
.mouse
, {
20 widgetEventPrefix
: "sort",
30 forcePlaceholderSize
: false,
31 forceHelperSize
: false,
40 scrollSensitivity
: 20,
43 tolerance
: "intersect",
49 this.containerCache
= {};
50 this.element
.addClass("ui-sortable");
55 //Let's determine if the items are being displayed horizontally
56 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;
58 //Let's determine the parent's offset
59 this.offset
= this.element
.offset();
61 //Initialize mouse events for interaction
69 _destroy: function() {
71 .removeClass("ui-sortable ui-sortable-disabled");
74 for ( var i
= this.items
.length
- 1; i
>= 0; i
-- )
75 this.items
[i
].item
.removeData(this.widgetName
+ "-item");
80 _setOption: function(key
, value
){
81 if ( key
=== "disabled" ) {
82 this.options
[ key
] = value
;
84 this.widget().toggleClass( "ui-sortable-disabled", !!value
);
86 // Don't call widget base _setOption for disable as it adds ui-state-disabled class
87 $.Widget
.prototype._setOption
.apply(this, arguments
);
91 _mouseCapture: function(event
, overrideHandle
) {
98 if(this.options
.disabled
|| this.options
.type
== 'static') return false;
100 //We have to refresh the items data once first
101 this._refreshItems(event
);
103 //Find out if the clicked node (or one of its parents) is a actual item in this.items
104 var currentItem
= null, nodes
= $(event
.target
).parents().each(function() {
105 if($.data(this, that
.widgetName
+ '-item') == that
) {
106 currentItem
= $(this);
110 if($.data(event
.target
, that
.widgetName
+ '-item') == that
) currentItem
= $(event
.target
);
112 if(!currentItem
) return false;
113 if(this.options
.handle
&& !overrideHandle
) {
114 var validHandle
= false;
116 $(this.options
.handle
, currentItem
).find("*").andSelf().each(function() { if(this == event
.target
) validHandle
= true; });
117 if(!validHandle
) return false;
120 this.currentItem
= currentItem
;
121 this._removeCurrentsFromItems();
126 _mouseStart: function(event
, overrideHandle
, noActivation
) {
128 var o
= this.options
;
129 this.currentContainer
= this;
131 //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
132 this.refreshPositions();
134 //Create and append the visible helper
135 this.helper
= this._createHelper(event
);
137 //Cache the helper size
138 this._cacheHelperProportions();
141 * - Position generation -
142 * This block generates everything position related - it's the core of draggables.
145 //Cache the margins of the original element
146 this._cacheMargins();
148 //Get the next scrolling parent
149 this.scrollParent
= this.helper
.scrollParent();
151 //The element's absolute position on the page minus margins
152 this.offset
= this.currentItem
.offset();
154 top
: this.offset
.top
- this.margins
.top
,
155 left
: this.offset
.left
- this.margins
.left
158 $.extend(this.offset
, {
159 click
: { //Where the click happened, relative to the element
160 left
: event
.pageX
- this.offset
.left
,
161 top
: event
.pageY
- this.offset
.top
163 parent
: this._getParentOffset(),
164 relative
: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
167 // Only after we got the offset, we can change the helper's position to absolute
168 // TODO: Still need to figure out a way to make relative sorting possible
169 this.helper
.css("position", "absolute");
170 this.cssPosition
= this.helper
.css("position");
172 //Generate the original position
173 this.originalPosition
= this._generatePosition(event
);
174 this.originalPageX
= event
.pageX
;
175 this.originalPageY
= event
.pageY
;
177 //Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
178 (o
.cursorAt
&& this._adjustOffsetFromHelper(o
.cursorAt
));
180 //Cache the former DOM position
181 this.domPosition
= { prev
: this.currentItem
.prev()[0], parent
: this.currentItem
.parent()[0] };
183 //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
184 if(this.helper
[0] != this.currentItem
[0]) {
185 this.currentItem
.hide();
188 //Create the placeholder
189 this._createPlaceholder();
191 //Set a containment if given in the options
193 this._setContainment();
195 if(o
.cursor
) { // cursor option
196 if ($('body').css("cursor")) this._storedCursor
= $('body').css("cursor");
197 $('body').css("cursor", o
.cursor
);
200 if(o
.opacity
) { // opacity option
201 if (this.helper
.css("opacity")) this._storedOpacity
= this.helper
.css("opacity");
202 this.helper
.css("opacity", o
.opacity
);
205 if(o
.zIndex
) { // zIndex option
206 if (this.helper
.css("zIndex")) this._storedZIndex
= this.helper
.css("zIndex");
207 this.helper
.css("zIndex", o
.zIndex
);
211 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML')
212 this.overflowOffset
= this.scrollParent
.offset();
215 this._trigger("start", event
, this._uiHash());
217 //Recache the helper size
218 if(!this._preserveHelperProportions
)
219 this._cacheHelperProportions();
222 //Post 'activate' events to possible containers
224 for (var i
= this.containers
.length
- 1; i
>= 0; i
--) { this.containers
[i
]._trigger("activate", event
, this._uiHash(this)); }
227 //Prepare possible droppables
229 $.ui
.ddmanager
.current
= this;
231 if ($.ui
.ddmanager
&& !o
.dropBehaviour
)
232 $.ui
.ddmanager
.prepareOffsets(this, event
);
234 this.dragging
= true;
236 this.helper
.addClass("ui-sortable-helper");
237 this._mouseDrag(event
); //Execute the drag once - this causes the helper not to be visible before getting its correct position
242 _mouseDrag: function(event
) {
244 //Compute the helpers position
245 this.position
= this._generatePosition(event
);
246 this.positionAbs
= this._convertPositionTo("absolute");
248 if (!this.lastPositionAbs
) {
249 this.lastPositionAbs
= this.positionAbs
;
253 if(this.options
.scroll
) {
254 var o
= this.options
, scrolled
= false;
255 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML') {
257 if((this.overflowOffset
.top
+ this.scrollParent
[0].offsetHeight
) - event
.pageY
< o
.scrollSensitivity
)
258 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
+ o
.scrollSpeed
;
259 else if(event
.pageY
- this.overflowOffset
.top
< o
.scrollSensitivity
)
260 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
- o
.scrollSpeed
;
262 if((this.overflowOffset
.left
+ this.scrollParent
[0].offsetWidth
) - event
.pageX
< o
.scrollSensitivity
)
263 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
+ o
.scrollSpeed
;
264 else if(event
.pageX
- this.overflowOffset
.left
< o
.scrollSensitivity
)
265 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
- o
.scrollSpeed
;
269 if(event
.pageY
- $(document
).scrollTop() < o
.scrollSensitivity
)
270 scrolled
= $(document
).scrollTop($(document
).scrollTop() - o
.scrollSpeed
);
271 else if($(window
).height() - (event
.pageY
- $(document
).scrollTop()) < o
.scrollSensitivity
)
272 scrolled
= $(document
).scrollTop($(document
).scrollTop() + o
.scrollSpeed
);
274 if(event
.pageX
- $(document
).scrollLeft() < o
.scrollSensitivity
)
275 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() - o
.scrollSpeed
);
276 else if($(window
).width() - (event
.pageX
- $(document
).scrollLeft()) < o
.scrollSensitivity
)
277 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() + o
.scrollSpeed
);
281 if(scrolled
!== false && $.ui
.ddmanager
&& !o
.dropBehaviour
)
282 $.ui
.ddmanager
.prepareOffsets(this, event
);
285 //Regenerate the absolute position used for position checks
286 this.positionAbs
= this._convertPositionTo("absolute");
288 //Set the helper position
289 if(!this.options
.axis
|| this.options
.axis
!= "y") this.helper
[0].style
.left
= this.position
.left
+'px';
290 if(!this.options
.axis
|| this.options
.axis
!= "x") this.helper
[0].style
.top
= this.position
.top
+'px';
293 for (var i
= this.items
.length
- 1; i
>= 0; i
--) {
295 //Cache variables and intersection, continue if no intersection
296 var item
= this.items
[i
], itemElement
= item
.item
[0], intersection
= this._intersectsWithPointer(item
);
297 if (!intersection
) continue;
299 // Only put the placeholder inside the current Container, skip all
300 // items form other containers. This works because when moving
301 // an item from one container to another the
302 // currentContainer is switched before the placeholder is moved.
304 // Without this moving items in "sub-sortables" can cause the placeholder to jitter
305 // beetween the outer and inner container.
306 if (item
.instance
!== this.currentContainer
) continue;
308 if (itemElement
!= this.currentItem
[0] //cannot intersect with itself
309 && this.placeholder
[intersection
== 1 ? "next" : "prev"]()[0] != itemElement
//no useless actions that have been done before
310 && !$.contains(this.placeholder
[0], itemElement
) //no action if the item moved is the parent of the item checked
311 && (this.options
.type
== 'semi-dynamic' ? !$.contains(this.element
[0], itemElement
) : true)
312 //&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container
315 this.direction
= intersection
== 1 ? "down" : "up";
317 if (this.options
.tolerance
== "pointer" || this._intersectsWithSides(item
)) {
318 this._rearrange(event
, item
);
323 this._trigger("change", event
, this._uiHash());
328 //Post events to containers
329 this._contactContainers(event
);
331 //Interconnect with droppables
332 if($.ui
.ddmanager
) $.ui
.ddmanager
.drag(this, event
);
335 this._trigger('sort', event
, this._uiHash());
337 this.lastPositionAbs
= this.positionAbs
;
342 _mouseStop: function(event
, noPropagation
) {
346 //If we are using droppables, inform the manager about the drop
347 if ($.ui
.ddmanager
&& !this.options
.dropBehaviour
)
348 $.ui
.ddmanager
.drop(this, event
);
350 if(this.options
.revert
) {
352 var cur
= this.placeholder
.offset();
354 this.reverting
= true;
356 $(this.helper
).animate({
357 left
: cur
.left
- this.offset
.parent
.left
- this.margins
.left
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollLeft
),
358 top
: cur
.top
- this.offset
.parent
.top
- this.margins
.top
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollTop
)
359 }, parseInt(this.options
.revert
, 10) || 500, function() {
363 this._clear(event
, noPropagation
);
374 this._mouseUp({ target
: null });
376 if(this.options
.helper
== "original")
377 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
379 this.currentItem
.show();
381 //Post deactivating events to containers
382 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
383 this.containers
[i
]._trigger("deactivate", null, this._uiHash(this));
384 if(this.containers
[i
].containerCache
.over
) {
385 this.containers
[i
]._trigger("out", null, this._uiHash(this));
386 this.containers
[i
].containerCache
.over
= 0;
392 if (this.placeholder
) {
393 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
394 if(this.placeholder
[0].parentNode
) this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
395 if(this.options
.helper
!= "original" && this.helper
&& this.helper
[0].parentNode
) this.helper
.remove();
404 if(this.domPosition
.prev
) {
405 $(this.domPosition
.prev
).after(this.currentItem
);
407 $(this.domPosition
.parent
).prepend(this.currentItem
);
415 serialize: function(o
) {
417 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
418 var str
= []; o
= o
|| {};
420 $(items
).each(function() {
421 var res
= ($(o
.item
|| this).attr(o
.attribute
|| 'id') || '').match(o
.expression
|| (/(.+)[-=_](.+)/));
422 if(res
) str
.push((o
.key
|| res
[1]+'[]')+'='+(o
.key
&& o
.expression
? res
[1] : res
[2]));
425 if(!str
.length
&& o
.key
) {
426 str
.push(o
.key
+ '=');
429 return str
.join('&');
433 toArray: function(o
) {
435 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
436 var ret
= []; o
= o
|| {};
438 items
.each(function() { ret
.push($(o
.item
|| this).attr(o
.attribute
|| 'id') || ''); });
443 /* Be careful with the following core functions */
444 _intersectsWith: function(item
) {
446 var x1
= this.positionAbs
.left
,
447 x2
= x1
+ this.helperProportions
.width
,
448 y1
= this.positionAbs
.top
,
449 y2
= y1
+ this.helperProportions
.height
;
456 var dyClick
= this.offset
.click
.top
,
457 dxClick
= this.offset
.click
.left
;
459 var isOverElement
= (y1
+ dyClick
) > t
&& (y1
+ dyClick
) < b
&& (x1
+ dxClick
) > l
&& (x1
+ dxClick
) < r
;
461 if( this.options
.tolerance
== "pointer"
462 || this.options
.forcePointerForContainers
463 || (this.options
.tolerance
!= "pointer" && this.helperProportions
[this.floating
? 'width' : 'height'] > item
[this.floating
? 'width' : 'height'])
465 return isOverElement
;
468 return (l
< x1
+ (this.helperProportions
.width
/ 2) // Right Half
469 && x2
- (this.helperProportions
.width
/ 2) < r
// Left Half
470 && t
< y1
+ (this.helperProportions
.height
/ 2) // Bottom Half
471 && y2
- (this.helperProportions
.height
/ 2) < b
); // Top Half
476 _intersectsWithPointer: function(item
) {
478 var isOverElementHeight
= (this.options
.axis
=== 'x') || $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
, item
.height
),
479 isOverElementWidth
= (this.options
.axis
=== 'y') || $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
, item
.width
),
480 isOverElement
= isOverElementHeight
&& isOverElementWidth
,
481 verticalDirection
= this._getDragVerticalDirection(),
482 horizontalDirection
= this._getDragHorizontalDirection();
487 return this.floating
?
488 ( ((horizontalDirection
&& horizontalDirection
== "right") || verticalDirection
== "down") ? 2 : 1 )
489 : ( verticalDirection
&& (verticalDirection
== "down" ? 2 : 1) );
493 _intersectsWithSides: function(item
) {
495 var isOverBottomHalf
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
+ (item
.height
/2), item
.height
),
496 isOverRightHalf
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
+ (item
.width
/2), item
.width
),
497 verticalDirection
= this._getDragVerticalDirection(),
498 horizontalDirection
= this._getDragHorizontalDirection();
500 if (this.floating
&& horizontalDirection
) {
501 return ((horizontalDirection
== "right" && isOverRightHalf
) || (horizontalDirection
== "left" && !isOverRightHalf
));
503 return verticalDirection
&& ((verticalDirection
== "down" && isOverBottomHalf
) || (verticalDirection
== "up" && !isOverBottomHalf
));
508 _getDragVerticalDirection: function() {
509 var delta
= this.positionAbs
.top
- this.lastPositionAbs
.top
;
510 return delta
!= 0 && (delta
> 0 ? "down" : "up");
513 _getDragHorizontalDirection: function() {
514 var delta
= this.positionAbs
.left
- this.lastPositionAbs
.left
;
515 return delta
!= 0 && (delta
> 0 ? "right" : "left");
518 refresh: function(event
) {
519 this._refreshItems(event
);
520 this.refreshPositions();
524 _connectWith: function() {
525 var options
= this.options
;
526 return options
.connectWith
.constructor == String
527 ? [options
.connectWith
]
528 : options
.connectWith
;
531 _getItemsAsjQuery: function(connected
) {
535 var connectWith
= this._connectWith();
537 if(connectWith
&& connected
) {
538 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
539 var cur
= $(connectWith
[i
]);
540 for (var j
= cur
.length
- 1; j
>= 0; j
--){
541 var inst
= $.data(cur
[j
], this.widgetName
);
542 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
543 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
]);
549 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]);
551 for (var i
= queries
.length
- 1; i
>= 0; i
--){
552 queries
[i
][0].each(function() {
561 _removeCurrentsFromItems: function() {
563 var list
= this.currentItem
.find(":data(" + this.widgetName
+ "-item)");
565 this.items
= $.grep(this.items
, function (item
) {
566 for (var j
=0; j
< list
.length
; j
++) {
567 if(list
[j
] == item
.item
[0])
575 _refreshItems: function(event
) {
578 this.containers
= [this];
579 var items
= this.items
;
580 var queries
= [[$.isFunction(this.options
.items
) ? this.options
.items
.call(this.element
[0], event
, { item
: this.currentItem
}) : $(this.options
.items
, this.element
), this]];
581 var connectWith
= this._connectWith();
583 if(connectWith
&& this.ready
) { //Shouldn't be run the first time through due to massive slow-down
584 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
585 var cur
= $(connectWith
[i
]);
586 for (var j
= cur
.length
- 1; j
>= 0; j
--){
587 var inst
= $.data(cur
[j
], this.widgetName
);
588 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
589 queries
.push([$.isFunction(inst
.options
.items
) ? inst
.options
.items
.call(inst
.element
[0], event
, { item
: this.currentItem
}) : $(inst
.options
.items
, inst
.element
), inst
]);
590 this.containers
.push(inst
);
596 for (var i
= queries
.length
- 1; i
>= 0; i
--) {
597 var targetData
= queries
[i
][1];
598 var _queries
= queries
[i
][0];
600 for (var j
=0, queriesLength
= _queries
.length
; j
< queriesLength
; j
++) {
601 var item
= $(_queries
[j
]);
603 item
.data(this.widgetName
+ '-item', targetData
); // Data for target checking (mouse manager)
607 instance
: targetData
,
616 refreshPositions: function(fast
) {
618 //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
619 if(this.offsetParent
&& this.helper
) {
620 this.offset
.parent
= this._getParentOffset();
623 for (var i
= this.items
.length
- 1; i
>= 0; i
--){
624 var item
= this.items
[i
];
626 //We ignore calculating positions of all connected containers when we're not over them
627 if(item
.instance
!= this.currentContainer
&& this.currentContainer
&& item
.item
[0] != this.currentItem
[0])
630 var t
= this.options
.toleranceElement
? $(this.options
.toleranceElement
, item
.item
) : item
.item
;
633 item
.width
= t
.outerWidth();
634 item
.height
= t
.outerHeight();
642 if(this.options
.custom
&& this.options
.custom
.refreshContainers
) {
643 this.options
.custom
.refreshContainers
.call(this);
645 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
646 var p
= this.containers
[i
].element
.offset();
647 this.containers
[i
].containerCache
.left
= p
.left
;
648 this.containers
[i
].containerCache
.top
= p
.top
;
649 this.containers
[i
].containerCache
.width
= this.containers
[i
].element
.outerWidth();
650 this.containers
[i
].containerCache
.height
= this.containers
[i
].element
.outerHeight();
657 _createPlaceholder: function(that
) {
659 var o
= that
.options
;
661 if(!o
.placeholder
|| o
.placeholder
.constructor == String
) {
662 var className
= o
.placeholder
;
664 element: function() {
666 var el
= $(document
.createElement(that
.currentItem
[0].nodeName
))
667 .addClass(className
|| that
.currentItem
[0].className
+" ui-sortable-placeholder")
668 .removeClass("ui-sortable-helper")[0];
671 el
.style
.visibility
= "hidden";
675 update: function(container
, p
) {
677 // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
678 // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
679 if(className
&& !o
.forcePlaceholderSize
) return;
681 //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
682 if(!p
.height()) { p
.height(that
.currentItem
.innerHeight() - parseInt(that
.currentItem
.css('paddingTop')||0, 10) - parseInt(that
.currentItem
.css('paddingBottom')||0, 10)); };
683 if(!p
.width()) { p
.width(that
.currentItem
.innerWidth() - parseInt(that
.currentItem
.css('paddingLeft')||0, 10) - parseInt(that
.currentItem
.css('paddingRight')||0, 10)); };
688 //Create the placeholder
689 that
.placeholder
= $(o
.placeholder
.element
.call(that
.element
, that
.currentItem
));
691 //Append it after the actual current item
692 that
.currentItem
.after(that
.placeholder
);
694 //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
695 o
.placeholder
.update(that
, that
.placeholder
);
699 _contactContainers: function(event
) {
701 // get innermost container that intersects with item
702 var innermostContainer
= null, innermostIndex
= null;
705 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
707 // never consider a container that's located within the item itself
708 if($.contains(this.currentItem
[0], this.containers
[i
].element
[0]))
711 if(this._intersectsWith(this.containers
[i
].containerCache
)) {
713 // if we've already found a container and it's more "inner" than this, then continue
714 if(innermostContainer
&& $.contains(this.containers
[i
].element
[0], innermostContainer
.element
[0]))
717 innermostContainer
= this.containers
[i
];
721 // container doesn't intersect. trigger "out" event if necessary
722 if(this.containers
[i
].containerCache
.over
) {
723 this.containers
[i
]._trigger("out", event
, this._uiHash(this));
724 this.containers
[i
].containerCache
.over
= 0;
730 // if no intersecting containers found, return
731 if(!innermostContainer
) return;
733 // move the item into the container if it's not there already
734 if(this.containers
.length
=== 1) {
735 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
736 this.containers
[innermostIndex
].containerCache
.over
= 1;
739 //When entering a new container, we will find the item with the least distance and append our item near it
740 var dist
= 10000; var itemWithLeastDistance
= null;
741 var posProperty
= this.containers
[innermostIndex
].floating
? 'left' : 'top';
742 var sizeProperty
= this.containers
[innermostIndex
].floating
? 'width' : 'height';
743 var base
= this.positionAbs
[posProperty
] + this.offset
.click
[posProperty
];
744 for (var j
= this.items
.length
- 1; j
>= 0; j
--) {
745 if(!$.contains(this.containers
[innermostIndex
].element
[0], this.items
[j
].item
[0])) continue;
746 if(this.items
[j
].item
[0] == this.currentItem
[0]) continue;
747 var cur
= this.items
[j
].item
.offset()[posProperty
];
748 var nearBottom
= false;
749 if(Math
.abs(cur
- base
) > Math
.abs(cur
+ this.items
[j
][sizeProperty
] - base
)){
751 cur
+= this.items
[j
][sizeProperty
];
754 if(Math
.abs(cur
- base
) < dist
) {
755 dist
= Math
.abs(cur
- base
); itemWithLeastDistance
= this.items
[j
];
756 this.direction
= nearBottom
? "up": "down";
760 if(!itemWithLeastDistance
&& !this.options
.dropOnEmpty
) //Check if dropOnEmpty is enabled
763 this.currentContainer
= this.containers
[innermostIndex
];
764 itemWithLeastDistance
? this._rearrange(event
, itemWithLeastDistance
, null, true) : this._rearrange(event
, null, this.containers
[innermostIndex
].element
, true);
765 this._trigger("change", event
, this._uiHash());
766 this.containers
[innermostIndex
]._trigger("change", event
, this._uiHash(this));
768 //Update the placeholder
769 this.options
.placeholder
.update(this.currentContainer
, this.placeholder
);
771 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
772 this.containers
[innermostIndex
].containerCache
.over
= 1;
778 _createHelper: function(event
) {
780 var o
= this.options
;
781 var helper
= $.isFunction(o
.helper
) ? $(o
.helper
.apply(this.element
[0], [event
, this.currentItem
])) : (o
.helper
== 'clone' ? this.currentItem
.clone() : this.currentItem
);
783 if(!helper
.parents('body').length
) //Add the helper to the DOM if that didn't happen already
784 $(o
.appendTo
!= 'parent' ? o
.appendTo
: this.currentItem
[0].parentNode
)[0].appendChild(helper
[0]);
786 if(helper
[0] == this.currentItem
[0])
787 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") };
789 if(helper
[0].style
.width
== '' || o
.forceHelperSize
) helper
.width(this.currentItem
.width());
790 if(helper
[0].style
.height
== '' || o
.forceHelperSize
) helper
.height(this.currentItem
.height());
796 _adjustOffsetFromHelper: function(obj
) {
797 if (typeof obj
== 'string') {
798 obj
= obj
.split(' ');
800 if ($.isArray(obj
)) {
801 obj
= {left
: +obj
[0], top
: +obj
[1] || 0};
804 this.offset
.click
.left
= obj
.left
+ this.margins
.left
;
806 if ('right' in obj
) {
807 this.offset
.click
.left
= this.helperProportions
.width
- obj
.right
+ this.margins
.left
;
810 this.offset
.click
.top
= obj
.top
+ this.margins
.top
;
812 if ('bottom' in obj
) {
813 this.offset
.click
.top
= this.helperProportions
.height
- obj
.bottom
+ this.margins
.top
;
817 _getParentOffset: function() {
820 //Get the offsetParent and cache its position
821 this.offsetParent
= this.helper
.offsetParent();
822 var po
= this.offsetParent
.offset();
824 // This is a special case where we need to modify a offset calculated on start, since the following happened:
825 // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
826 // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
827 // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
828 if(this.cssPosition
== 'absolute' && this.scrollParent
[0] != document
&& $.contains(this.scrollParent
[0], this.offsetParent
[0])) {
829 po
.left
+= this.scrollParent
.scrollLeft();
830 po
.top
+= this.scrollParent
.scrollTop();
833 if((this.offsetParent
[0] == document
.body
) //This needs to be actually done for all browsers, since pageX/pageY includes this information
834 || (this.offsetParent
[0].tagName
&& this.offsetParent
[0].tagName
.toLowerCase() == 'html' && $.ui
.ie
)) //Ugly IE fix
835 po
= { top
: 0, left
: 0 };
838 top
: po
.top
+ (parseInt(this.offsetParent
.css("borderTopWidth"),10) || 0),
839 left
: po
.left
+ (parseInt(this.offsetParent
.css("borderLeftWidth"),10) || 0)
844 _getRelativeOffset: function() {
846 if(this.cssPosition
== "relative") {
847 var p
= this.currentItem
.position();
849 top
: p
.top
- (parseInt(this.helper
.css("top"),10) || 0) + this.scrollParent
.scrollTop(),
850 left
: p
.left
- (parseInt(this.helper
.css("left"),10) || 0) + this.scrollParent
.scrollLeft()
853 return { top
: 0, left
: 0 };
858 _cacheMargins: function() {
860 left
: (parseInt(this.currentItem
.css("marginLeft"),10) || 0),
861 top
: (parseInt(this.currentItem
.css("marginTop"),10) || 0)
865 _cacheHelperProportions: function() {
866 this.helperProportions
= {
867 width
: this.helper
.outerWidth(),
868 height
: this.helper
.outerHeight()
872 _setContainment: function() {
874 var o
= this.options
;
875 if(o
.containment
== 'parent') o
.containment
= this.helper
[0].parentNode
;
876 if(o
.containment
== 'document' || o
.containment
== 'window') this.containment
= [
877 0 - this.offset
.relative
.left
- this.offset
.parent
.left
,
878 0 - this.offset
.relative
.top
- this.offset
.parent
.top
,
879 $(o
.containment
== 'document' ? document
: window
).width() - this.helperProportions
.width
- this.margins
.left
,
880 ($(o
.containment
== 'document' ? document
: window
).height() || document
.body
.parentNode
.scrollHeight
) - this.helperProportions
.height
- this.margins
.top
883 if(!(/^(document|window|parent)$/).test(o
.containment
)) {
884 var ce
= $(o
.containment
)[0];
885 var co
= $(o
.containment
).offset();
886 var over
= ($(ce
).css("overflow") != 'hidden');
889 co
.left
+ (parseInt($(ce
).css("borderLeftWidth"),10) || 0) + (parseInt($(ce
).css("paddingLeft"),10) || 0) - this.margins
.left
,
890 co
.top
+ (parseInt($(ce
).css("borderTopWidth"),10) || 0) + (parseInt($(ce
).css("paddingTop"),10) || 0) - this.margins
.top
,
891 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
,
892 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
898 _convertPositionTo: function(d
, pos
) {
900 if(!pos
) pos
= this.position
;
901 var mod
= d
== "absolute" ? 1 : -1;
902 var o
= this.options
, scroll
= this.cssPosition
== 'absolute' && !(this.scrollParent
[0] != document
&& $.contains(this.scrollParent
[0], this.offsetParent
[0])) ? this.offsetParent
: this.scrollParent
, scrollIsRootNode
= (/(html|body)/i).test(scroll
[0].tagName
);
906 pos
.top
// The absolute mouse position
907 + this.offset
.relative
.top
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
908 + this.offset
.parent
.top
* mod
// The offsetParent's offset without borders (offset + border)
909 - ( ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ) * mod
)
912 pos
.left
// The absolute mouse position
913 + this.offset
.relative
.left
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
914 + this.offset
.parent
.left
* mod
// The offsetParent's offset without borders (offset + border)
915 - ( ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ) * mod
)
921 _generatePosition: function(event
) {
923 var o
= this.options
, scroll
= this.cssPosition
== 'absolute' && !(this.scrollParent
[0] != document
&& $.contains(this.scrollParent
[0], this.offsetParent
[0])) ? this.offsetParent
: this.scrollParent
, scrollIsRootNode
= (/(html|body)/i).test(scroll
[0].tagName
);
925 // This is another very weird special case that only happens for relative elements:
926 // 1. If the css position is relative
927 // 2. and the scroll parent is the document or similar to the offset parent
928 // we have to refresh the relative offset during the scroll so there are no jumps
929 if(this.cssPosition
== 'relative' && !(this.scrollParent
[0] != document
&& this.scrollParent
[0] != this.offsetParent
[0])) {
930 this.offset
.relative
= this._getRelativeOffset();
933 var pageX
= event
.pageX
;
934 var pageY
= event
.pageY
;
937 * - Position constraining -
938 * Constrain the position to a mix of grid, containment.
941 if(this.originalPosition
) { //If we are not dragging yet, we won't check for options
943 if(this.containment
) {
944 if(event
.pageX
- this.offset
.click
.left
< this.containment
[0]) pageX
= this.containment
[0] + this.offset
.click
.left
;
945 if(event
.pageY
- this.offset
.click
.top
< this.containment
[1]) pageY
= this.containment
[1] + this.offset
.click
.top
;
946 if(event
.pageX
- this.offset
.click
.left
> this.containment
[2]) pageX
= this.containment
[2] + this.offset
.click
.left
;
947 if(event
.pageY
- this.offset
.click
.top
> this.containment
[3]) pageY
= this.containment
[3] + this.offset
.click
.top
;
951 var top
= this.originalPageY
+ Math
.round((pageY
- this.originalPageY
) / o
.grid
[1]) * o
.grid
[1];
952 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
;
954 var left
= this.originalPageX
+ Math
.round((pageX
- this.originalPageX
) / o
.grid
[0]) * o
.grid
[0];
955 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
;
962 pageY
// The absolute mouse position
963 - this.offset
.click
.top
// Click offset (relative to the element)
964 - this.offset
.relative
.top
// Only for relative positioned nodes: Relative offset from element to offset parent
965 - this.offset
.parent
.top
// The offsetParent's offset without borders (offset + border)
966 + ( ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ))
969 pageX
// The absolute mouse position
970 - this.offset
.click
.left
// Click offset (relative to the element)
971 - this.offset
.relative
.left
// Only for relative positioned nodes: Relative offset from element to offset parent
972 - this.offset
.parent
.left
// The offsetParent's offset without borders (offset + border)
973 + ( ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ))
979 _rearrange: function(event
, i
, a
, hardRefresh
) {
981 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
));
983 //Various things done here to improve the performance:
984 // 1. we create a setTimeout, that calls refreshPositions
985 // 2. on the instance, we have a counter variable, that get's higher after every append
986 // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
987 // 4. this lets only the last addition to the timeout stack through
988 this.counter
= this.counter
? ++this.counter
: 1;
989 var counter
= this.counter
;
991 this._delay(function() {
992 if(counter
== this.counter
) this.refreshPositions(!hardRefresh
); //Precompute after each DOM insertion, NOT on mousemove
997 _clear: function(event
, noPropagation
) {
999 this.reverting
= false;
1000 // We delay all events that have to be triggered to after the point where the placeholder has been removed and
1001 // everything else normalized again
1002 var delayedTriggers
= [];
1004 // We first have to update the dom position of the actual currentItem
1005 // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
1006 if(!this._noFinalSort
&& this.currentItem
.parent().length
) this.placeholder
.before(this.currentItem
);
1007 this._noFinalSort
= null;
1009 if(this.helper
[0] == this.currentItem
[0]) {
1010 for(var i
in this._storedCSS
) {
1011 if(this._storedCSS
[i
] == 'auto' || this._storedCSS
[i
] == 'static') this._storedCSS
[i
] = '';
1013 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
1015 this.currentItem
.show();
1018 if(this.fromOutside
&& !noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("receive", event
, this._uiHash(this.fromOutside
)); });
1019 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
1021 // Check if the items Container has Changed and trigger appropriate
1023 if (this !== this.currentContainer
) {
1024 if(!noPropagation
) {
1025 delayedTriggers
.push(function(event
) { this._trigger("remove", event
, this._uiHash()); });
1026 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("receive", event
, this._uiHash(this)); }; }).call(this, this.currentContainer
));
1027 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("update", event
, this._uiHash(this)); }; }).call(this, this.currentContainer
));
1032 //Post events to containers
1033 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
1034 if(!noPropagation
) delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("deactivate", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1035 if(this.containers
[i
].containerCache
.over
) {
1036 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("out", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1037 this.containers
[i
].containerCache
.over
= 0;
1041 //Do what was originally in plugins
1042 if(this._storedCursor
) $('body').css("cursor", this._storedCursor
); //Reset cursor
1043 if(this._storedOpacity
) this.helper
.css("opacity", this._storedOpacity
); //Reset opacity
1044 if(this._storedZIndex
) this.helper
.css("zIndex", this._storedZIndex
== 'auto' ? '' : this._storedZIndex
); //Reset z-index
1046 this.dragging
= false;
1047 if(this.cancelHelperRemoval
) {
1048 if(!noPropagation
) {
1049 this._trigger("beforeStop", event
, this._uiHash());
1050 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1051 this._trigger("stop", event
, this._uiHash());
1054 this.fromOutside
= false;
1058 if(!noPropagation
) this._trigger("beforeStop", event
, this._uiHash());
1060 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
1061 this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
1063 if(this.helper
[0] != this.currentItem
[0]) this.helper
.remove(); this.helper
= null;
1065 if(!noPropagation
) {
1066 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1067 this._trigger("stop", event
, this._uiHash());
1070 this.fromOutside
= false;
1075 _trigger: function() {
1076 if ($.Widget
.prototype._trigger
.apply(this, arguments
) === false) {
1081 _uiHash: function(_inst
) {
1082 var inst
= _inst
|| this;
1084 helper
: inst
.helper
,
1085 placeholder
: inst
.placeholder
|| $([]),
1086 position
: inst
.position
,
1087 originalPosition
: inst
.originalPosition
,
1088 offset
: inst
.positionAbs
,
1089 item
: inst
.currentItem
,
1090 sender
: _inst
? _inst
.element
: null