2 * jQuery UI Sortable 1.8.24
4 * Copyright 2012, 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",
28 forcePlaceholderSize
: false,
29 forceHelperSize
: false,
38 scrollSensitivity
: 20,
41 tolerance
: "intersect",
47 this.containerCache
= {};
48 this.element
.addClass("ui-sortable");
53 //Let's determine if the items are being displayed horizontally
54 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;
56 //Let's determine the parent's offset
57 this.offset
= this.element
.offset();
59 //Initialize mouse events for interaction
68 $.Widget
.prototype.destroy
.call( this );
70 .removeClass("ui-sortable ui-sortable-disabled");
73 for ( var i
= this.items
.length
- 1; i
>= 0; i
-- )
74 this.items
[i
].item
.removeData(this.widgetName
+ "-item");
79 _setOption: function(key
, value
){
80 if ( key
=== "disabled" ) {
81 this.options
[ key
] = value
;
84 [ value
? "addClass" : "removeClass"]( "ui-sortable-disabled" );
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, self
= this, nodes
= $(event
.target
).parents().each(function() {
105 if($.data(this, that
.widgetName
+ '-item') == self
) {
106 currentItem
= $(this);
110 if($.data(event
.target
, that
.widgetName
+ '-item') == self
) 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
, self
= this;
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
, self
._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 && !$.ui
.contains(this.placeholder
[0], itemElement
) //no action if the item moved is the parent of the item checked
311 && (this.options
.type
== 'semi-dynamic' ? !$.ui
.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
= self
.placeholder
.offset();
354 self
.reverting
= true;
356 $(this.helper
).animate({
357 left
: cur
.left
- this.offset
.parent
.left
- self
.margins
.left
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollLeft
),
358 top
: cur
.top
- this.offset
.parent
.top
- self
.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
);
376 this._mouseUp({ target
: null });
378 if(this.options
.helper
== "original")
379 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
381 this.currentItem
.show();
383 //Post deactivating events to containers
384 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
385 this.containers
[i
]._trigger("deactivate", null, self
._uiHash(this));
386 if(this.containers
[i
].containerCache
.over
) {
387 this.containers
[i
]._trigger("out", null, self
._uiHash(this));
388 this.containers
[i
].containerCache
.over
= 0;
394 if (this.placeholder
) {
395 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
396 if(this.placeholder
[0].parentNode
) this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
397 if(this.options
.helper
!= "original" && this.helper
&& this.helper
[0].parentNode
) this.helper
.remove();
406 if(this.domPosition
.prev
) {
407 $(this.domPosition
.prev
).after(this.currentItem
);
409 $(this.domPosition
.parent
).prepend(this.currentItem
);
417 serialize: function(o
) {
419 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
420 var str
= []; o
= o
|| {};
422 $(items
).each(function() {
423 var res
= ($(o
.item
|| this).attr(o
.attribute
|| 'id') || '').match(o
.expression
|| (/(.+)[-=_](.+)/));
424 if(res
) str
.push((o
.key
|| res
[1]+'[]')+'='+(o
.key
&& o
.expression
? res
[1] : res
[2]));
427 if(!str
.length
&& o
.key
) {
428 str
.push(o
.key
+ '=');
431 return str
.join('&');
435 toArray: function(o
) {
437 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
438 var ret
= []; o
= o
|| {};
440 items
.each(function() { ret
.push($(o
.item
|| this).attr(o
.attribute
|| 'id') || ''); });
445 /* Be careful with the following core functions */
446 _intersectsWith: function(item
) {
448 var x1
= this.positionAbs
.left
,
449 x2
= x1
+ this.helperProportions
.width
,
450 y1
= this.positionAbs
.top
,
451 y2
= y1
+ this.helperProportions
.height
;
458 var dyClick
= this.offset
.click
.top
,
459 dxClick
= this.offset
.click
.left
;
461 var isOverElement
= (y1
+ dyClick
) > t
&& (y1
+ dyClick
) < b
&& (x1
+ dxClick
) > l
&& (x1
+ dxClick
) < r
;
463 if( this.options
.tolerance
== "pointer"
464 || this.options
.forcePointerForContainers
465 || (this.options
.tolerance
!= "pointer" && this.helperProportions
[this.floating
? 'width' : 'height'] > item
[this.floating
? 'width' : 'height'])
467 return isOverElement
;
470 return (l
< x1
+ (this.helperProportions
.width
/ 2) // Right Half
471 && x2
- (this.helperProportions
.width
/ 2) < r
// Left Half
472 && t
< y1
+ (this.helperProportions
.height
/ 2) // Bottom Half
473 && y2
- (this.helperProportions
.height
/ 2) < b
); // Top Half
478 _intersectsWithPointer: function(item
) {
480 var isOverElementHeight
= (this.options
.axis
=== 'x') || $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
, item
.height
),
481 isOverElementWidth
= (this.options
.axis
=== 'y') || $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
, item
.width
),
482 isOverElement
= isOverElementHeight
&& isOverElementWidth
,
483 verticalDirection
= this._getDragVerticalDirection(),
484 horizontalDirection
= this._getDragHorizontalDirection();
489 return this.floating
?
490 ( ((horizontalDirection
&& horizontalDirection
== "right") || verticalDirection
== "down") ? 2 : 1 )
491 : ( verticalDirection
&& (verticalDirection
== "down" ? 2 : 1) );
495 _intersectsWithSides: function(item
) {
497 var isOverBottomHalf
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
+ (item
.height
/2), item
.height
),
498 isOverRightHalf
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
+ (item
.width
/2), item
.width
),
499 verticalDirection
= this._getDragVerticalDirection(),
500 horizontalDirection
= this._getDragHorizontalDirection();
502 if (this.floating
&& horizontalDirection
) {
503 return ((horizontalDirection
== "right" && isOverRightHalf
) || (horizontalDirection
== "left" && !isOverRightHalf
));
505 return verticalDirection
&& ((verticalDirection
== "down" && isOverBottomHalf
) || (verticalDirection
== "up" && !isOverBottomHalf
));
510 _getDragVerticalDirection: function() {
511 var delta
= this.positionAbs
.top
- this.lastPositionAbs
.top
;
512 return delta
!= 0 && (delta
> 0 ? "down" : "up");
515 _getDragHorizontalDirection: function() {
516 var delta
= this.positionAbs
.left
- this.lastPositionAbs
.left
;
517 return delta
!= 0 && (delta
> 0 ? "right" : "left");
520 refresh: function(event
) {
521 this._refreshItems(event
);
522 this.refreshPositions();
526 _connectWith: function() {
527 var options
= this.options
;
528 return options
.connectWith
.constructor == String
529 ? [options
.connectWith
]
530 : options
.connectWith
;
533 _getItemsAsjQuery: function(connected
) {
538 var connectWith
= this._connectWith();
540 if(connectWith
&& connected
) {
541 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
542 var cur
= $(connectWith
[i
]);
543 for (var j
= cur
.length
- 1; j
>= 0; j
--){
544 var inst
= $.data(cur
[j
], this.widgetName
);
545 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
546 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
]);
552 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]);
554 for (var i
= queries
.length
- 1; i
>= 0; i
--){
555 queries
[i
][0].each(function() {
564 _removeCurrentsFromItems: function() {
566 var list
= this.currentItem
.find(":data(" + this.widgetName
+ "-item)");
568 for (var i
=0; i
< this.items
.length
; i
++) {
570 for (var j
=0; j
< list
.length
; j
++) {
571 if(list
[j
] == this.items
[i
].item
[0])
572 this.items
.splice(i
,1);
579 _refreshItems: function(event
) {
582 this.containers
= [this];
583 var items
= this.items
;
585 var queries
= [[$.isFunction(this.options
.items
) ? this.options
.items
.call(this.element
[0], event
, { item
: this.currentItem
}) : $(this.options
.items
, this.element
), this]];
586 var connectWith
= this._connectWith();
588 if(connectWith
&& this.ready
) { //Shouldn't be run the first time through due to massive slow-down
589 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
590 var cur
= $(connectWith
[i
]);
591 for (var j
= cur
.length
- 1; j
>= 0; j
--){
592 var inst
= $.data(cur
[j
], this.widgetName
);
593 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
594 queries
.push([$.isFunction(inst
.options
.items
) ? inst
.options
.items
.call(inst
.element
[0], event
, { item
: this.currentItem
}) : $(inst
.options
.items
, inst
.element
), inst
]);
595 this.containers
.push(inst
);
601 for (var i
= queries
.length
- 1; i
>= 0; i
--) {
602 var targetData
= queries
[i
][1];
603 var _queries
= queries
[i
][0];
605 for (var j
=0, queriesLength
= _queries
.length
; j
< queriesLength
; j
++) {
606 var item
= $(_queries
[j
]);
608 item
.data(this.widgetName
+ '-item', targetData
); // Data for target checking (mouse manager)
612 instance
: targetData
,
621 refreshPositions: function(fast
) {
623 //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
624 if(this.offsetParent
&& this.helper
) {
625 this.offset
.parent
= this._getParentOffset();
628 for (var i
= this.items
.length
- 1; i
>= 0; i
--){
629 var item
= this.items
[i
];
631 //We ignore calculating positions of all connected containers when we're not over them
632 if(item
.instance
!= this.currentContainer
&& this.currentContainer
&& item
.item
[0] != this.currentItem
[0])
635 var t
= this.options
.toleranceElement
? $(this.options
.toleranceElement
, item
.item
) : item
.item
;
638 item
.width
= t
.outerWidth();
639 item
.height
= t
.outerHeight();
647 if(this.options
.custom
&& this.options
.custom
.refreshContainers
) {
648 this.options
.custom
.refreshContainers
.call(this);
650 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
651 var p
= this.containers
[i
].element
.offset();
652 this.containers
[i
].containerCache
.left
= p
.left
;
653 this.containers
[i
].containerCache
.top
= p
.top
;
654 this.containers
[i
].containerCache
.width
= this.containers
[i
].element
.outerWidth();
655 this.containers
[i
].containerCache
.height
= this.containers
[i
].element
.outerHeight();
662 _createPlaceholder: function(that
) {
664 var self
= that
|| this, o
= self
.options
;
666 if(!o
.placeholder
|| o
.placeholder
.constructor == String
) {
667 var className
= o
.placeholder
;
669 element: function() {
671 var el
= $(document
.createElement(self
.currentItem
[0].nodeName
))
672 .addClass(className
|| self
.currentItem
[0].className
+" ui-sortable-placeholder")
673 .removeClass("ui-sortable-helper")[0];
676 el
.style
.visibility
= "hidden";
680 update: function(container
, p
) {
682 // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
683 // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
684 if(className
&& !o
.forcePlaceholderSize
) return;
686 //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
687 if(!p
.height()) { p
.height(self
.currentItem
.innerHeight() - parseInt(self
.currentItem
.css('paddingTop')||0, 10) - parseInt(self
.currentItem
.css('paddingBottom')||0, 10)); };
688 if(!p
.width()) { p
.width(self
.currentItem
.innerWidth() - parseInt(self
.currentItem
.css('paddingLeft')||0, 10) - parseInt(self
.currentItem
.css('paddingRight')||0, 10)); };
693 //Create the placeholder
694 self
.placeholder
= $(o
.placeholder
.element
.call(self
.element
, self
.currentItem
));
696 //Append it after the actual current item
697 self
.currentItem
.after(self
.placeholder
);
699 //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
700 o
.placeholder
.update(self
, self
.placeholder
);
704 _contactContainers: function(event
) {
706 // get innermost container that intersects with item
707 var innermostContainer
= null, innermostIndex
= null;
710 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
712 // never consider a container that's located within the item itself
713 if($.ui
.contains(this.currentItem
[0], this.containers
[i
].element
[0]))
716 if(this._intersectsWith(this.containers
[i
].containerCache
)) {
718 // if we've already found a container and it's more "inner" than this, then continue
719 if(innermostContainer
&& $.ui
.contains(this.containers
[i
].element
[0], innermostContainer
.element
[0]))
722 innermostContainer
= this.containers
[i
];
726 // container doesn't intersect. trigger "out" event if necessary
727 if(this.containers
[i
].containerCache
.over
) {
728 this.containers
[i
]._trigger("out", event
, this._uiHash(this));
729 this.containers
[i
].containerCache
.over
= 0;
735 // if no intersecting containers found, return
736 if(!innermostContainer
) return;
738 // move the item into the container if it's not there already
739 if(this.containers
.length
=== 1) {
740 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
741 this.containers
[innermostIndex
].containerCache
.over
= 1;
742 } else if(this.currentContainer
!= this.containers
[innermostIndex
]) {
744 //When entering a new container, we will find the item with the least distance and append our item near it
745 var dist
= 10000; var itemWithLeastDistance
= null; var base
= this.positionAbs
[this.containers
[innermostIndex
].floating
? 'left' : 'top'];
746 for (var j
= this.items
.length
- 1; j
>= 0; j
--) {
747 if(!$.ui
.contains(this.containers
[innermostIndex
].element
[0], this.items
[j
].item
[0])) continue;
748 var cur
= this.containers
[innermostIndex
].floating
? this.items
[j
].item
.offset().left
: this.items
[j
].item
.offset().top
;
749 if(Math
.abs(cur
- base
) < dist
) {
750 dist
= Math
.abs(cur
- base
); itemWithLeastDistance
= this.items
[j
];
751 this.direction
= (cur
- base
> 0) ? 'down' : 'up';
755 if(!itemWithLeastDistance
&& !this.options
.dropOnEmpty
) //Check if dropOnEmpty is enabled
758 this.currentContainer
= this.containers
[innermostIndex
];
759 itemWithLeastDistance
? this._rearrange(event
, itemWithLeastDistance
, null, true) : this._rearrange(event
, null, this.containers
[innermostIndex
].element
, true);
760 this._trigger("change", event
, this._uiHash());
761 this.containers
[innermostIndex
]._trigger("change", event
, this._uiHash(this));
763 //Update the placeholder
764 this.options
.placeholder
.update(this.currentContainer
, this.placeholder
);
766 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
767 this.containers
[innermostIndex
].containerCache
.over
= 1;
773 _createHelper: function(event
) {
775 var o
= this.options
;
776 var helper
= $.isFunction(o
.helper
) ? $(o
.helper
.apply(this.element
[0], [event
, this.currentItem
])) : (o
.helper
== 'clone' ? this.currentItem
.clone() : this.currentItem
);
778 if(!helper
.parents('body').length
) //Add the helper to the DOM if that didn't happen already
779 $(o
.appendTo
!= 'parent' ? o
.appendTo
: this.currentItem
[0].parentNode
)[0].appendChild(helper
[0]);
781 if(helper
[0] == this.currentItem
[0])
782 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") };
784 if(helper
[0].style
.width
== '' || o
.forceHelperSize
) helper
.width(this.currentItem
.width());
785 if(helper
[0].style
.height
== '' || o
.forceHelperSize
) helper
.height(this.currentItem
.height());
791 _adjustOffsetFromHelper: function(obj
) {
792 if (typeof obj
== 'string') {
793 obj
= obj
.split(' ');
795 if ($.isArray(obj
)) {
796 obj
= {left
: +obj
[0], top
: +obj
[1] || 0};
799 this.offset
.click
.left
= obj
.left
+ this.margins
.left
;
801 if ('right' in obj
) {
802 this.offset
.click
.left
= this.helperProportions
.width
- obj
.right
+ this.margins
.left
;
805 this.offset
.click
.top
= obj
.top
+ this.margins
.top
;
807 if ('bottom' in obj
) {
808 this.offset
.click
.top
= this.helperProportions
.height
- obj
.bottom
+ this.margins
.top
;
812 _getParentOffset: function() {
815 //Get the offsetParent and cache its position
816 this.offsetParent
= this.helper
.offsetParent();
817 var po
= this.offsetParent
.offset();
819 // This is a special case where we need to modify a offset calculated on start, since the following happened:
820 // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
821 // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
822 // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
823 if(this.cssPosition
== 'absolute' && this.scrollParent
[0] != document
&& $.ui
.contains(this.scrollParent
[0], this.offsetParent
[0])) {
824 po
.left
+= this.scrollParent
.scrollLeft();
825 po
.top
+= this.scrollParent
.scrollTop();
828 if((this.offsetParent
[0] == document
.body
) //This needs to be actually done for all browsers, since pageX/pageY includes this information
829 || (this.offsetParent
[0].tagName
&& this.offsetParent
[0].tagName
.toLowerCase() == 'html' && $.browser
.msie
)) //Ugly IE fix
830 po
= { top
: 0, left
: 0 };
833 top
: po
.top
+ (parseInt(this.offsetParent
.css("borderTopWidth"),10) || 0),
834 left
: po
.left
+ (parseInt(this.offsetParent
.css("borderLeftWidth"),10) || 0)
839 _getRelativeOffset: function() {
841 if(this.cssPosition
== "relative") {
842 var p
= this.currentItem
.position();
844 top
: p
.top
- (parseInt(this.helper
.css("top"),10) || 0) + this.scrollParent
.scrollTop(),
845 left
: p
.left
- (parseInt(this.helper
.css("left"),10) || 0) + this.scrollParent
.scrollLeft()
848 return { top
: 0, left
: 0 };
853 _cacheMargins: function() {
855 left
: (parseInt(this.currentItem
.css("marginLeft"),10) || 0),
856 top
: (parseInt(this.currentItem
.css("marginTop"),10) || 0)
860 _cacheHelperProportions: function() {
861 this.helperProportions
= {
862 width
: this.helper
.outerWidth(),
863 height
: this.helper
.outerHeight()
867 _setContainment: function() {
869 var o
= this.options
;
870 if(o
.containment
== 'parent') o
.containment
= this.helper
[0].parentNode
;
871 if(o
.containment
== 'document' || o
.containment
== 'window') this.containment
= [
872 0 - this.offset
.relative
.left
- this.offset
.parent
.left
,
873 0 - this.offset
.relative
.top
- this.offset
.parent
.top
,
874 $(o
.containment
== 'document' ? document
: window
).width() - this.helperProportions
.width
- this.margins
.left
,
875 ($(o
.containment
== 'document' ? document
: window
).height() || document
.body
.parentNode
.scrollHeight
) - this.helperProportions
.height
- this.margins
.top
878 if(!(/^(document|window|parent)$/).test(o
.containment
)) {
879 var ce
= $(o
.containment
)[0];
880 var co
= $(o
.containment
).offset();
881 var over
= ($(ce
).css("overflow") != 'hidden');
884 co
.left
+ (parseInt($(ce
).css("borderLeftWidth"),10) || 0) + (parseInt($(ce
).css("paddingLeft"),10) || 0) - this.margins
.left
,
885 co
.top
+ (parseInt($(ce
).css("borderTopWidth"),10) || 0) + (parseInt($(ce
).css("paddingTop"),10) || 0) - this.margins
.top
,
886 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
,
887 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
893 _convertPositionTo: function(d
, pos
) {
895 if(!pos
) pos
= this.position
;
896 var mod
= d
== "absolute" ? 1 : -1;
897 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
);
901 pos
.top
// The absolute mouse position
902 + this.offset
.relative
.top
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
903 + this.offset
.parent
.top
* mod
// The offsetParent's offset without borders (offset + border)
904 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ) * mod
)
907 pos
.left
// The absolute mouse position
908 + this.offset
.relative
.left
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
909 + this.offset
.parent
.left
* mod
// The offsetParent's offset without borders (offset + border)
910 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ) * mod
)
916 _generatePosition: function(event
) {
918 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
);
920 // This is another very weird special case that only happens for relative elements:
921 // 1. If the css position is relative
922 // 2. and the scroll parent is the document or similar to the offset parent
923 // we have to refresh the relative offset during the scroll so there are no jumps
924 if(this.cssPosition
== 'relative' && !(this.scrollParent
[0] != document
&& this.scrollParent
[0] != this.offsetParent
[0])) {
925 this.offset
.relative
= this._getRelativeOffset();
928 var pageX
= event
.pageX
;
929 var pageY
= event
.pageY
;
932 * - Position constraining -
933 * Constrain the position to a mix of grid, containment.
936 if(this.originalPosition
) { //If we are not dragging yet, we won't check for options
938 if(this.containment
) {
939 if(event
.pageX
- this.offset
.click
.left
< this.containment
[0]) pageX
= this.containment
[0] + this.offset
.click
.left
;
940 if(event
.pageY
- this.offset
.click
.top
< this.containment
[1]) pageY
= this.containment
[1] + this.offset
.click
.top
;
941 if(event
.pageX
- this.offset
.click
.left
> this.containment
[2]) pageX
= this.containment
[2] + this.offset
.click
.left
;
942 if(event
.pageY
- this.offset
.click
.top
> this.containment
[3]) pageY
= this.containment
[3] + this.offset
.click
.top
;
946 var top
= this.originalPageY
+ Math
.round((pageY
- this.originalPageY
) / o
.grid
[1]) * o
.grid
[1];
947 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
;
949 var left
= this.originalPageX
+ Math
.round((pageX
- this.originalPageX
) / o
.grid
[0]) * o
.grid
[0];
950 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
;
957 pageY
// The absolute mouse position
958 - this.offset
.click
.top
// Click offset (relative to the element)
959 - this.offset
.relative
.top
// Only for relative positioned nodes: Relative offset from element to offset parent
960 - this.offset
.parent
.top
// The offsetParent's offset without borders (offset + border)
961 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ))
964 pageX
// The absolute mouse position
965 - this.offset
.click
.left
// Click offset (relative to the element)
966 - this.offset
.relative
.left
// Only for relative positioned nodes: Relative offset from element to offset parent
967 - this.offset
.parent
.left
// The offsetParent's offset without borders (offset + border)
968 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ))
974 _rearrange: function(event
, i
, a
, hardRefresh
) {
976 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
));
978 //Various things done here to improve the performance:
979 // 1. we create a setTimeout, that calls refreshPositions
980 // 2. on the instance, we have a counter variable, that get's higher after every append
981 // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
982 // 4. this lets only the last addition to the timeout stack through
983 this.counter
= this.counter
? ++this.counter
: 1;
984 var self
= this, counter
= this.counter
;
986 window
.setTimeout(function() {
987 if(counter
== self
.counter
) self
.refreshPositions(!hardRefresh
); //Precompute after each DOM insertion, NOT on mousemove
992 _clear: function(event
, noPropagation
) {
994 this.reverting
= false;
995 // We delay all events that have to be triggered to after the point where the placeholder has been removed and
996 // everything else normalized again
997 var delayedTriggers
= [], self
= this;
999 // We first have to update the dom position of the actual currentItem
1000 // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
1001 if(!this._noFinalSort
&& this.currentItem
.parent().length
) this.placeholder
.before(this.currentItem
);
1002 this._noFinalSort
= null;
1004 if(this.helper
[0] == this.currentItem
[0]) {
1005 for(var i
in this._storedCSS
) {
1006 if(this._storedCSS
[i
] == 'auto' || this._storedCSS
[i
] == 'static') this._storedCSS
[i
] = '';
1008 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
1010 this.currentItem
.show();
1013 if(this.fromOutside
&& !noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("receive", event
, this._uiHash(this.fromOutside
)); });
1014 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
1016 // Check if the items Container has Changed and trigger appropriate
1018 if (this !== this.currentContainer
) {
1019 if(!noPropagation
) {
1020 delayedTriggers
.push(function(event
) { this._trigger("remove", event
, this._uiHash()); });
1021 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("receive", event
, this._uiHash(this)); }; }).call(this, this.currentContainer
));
1022 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("update", event
, this._uiHash(this)); }; }).call(this, this.currentContainer
));
1026 //Post events to containers
1027 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
1028 if(!noPropagation
) delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("deactivate", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1029 if(this.containers
[i
].containerCache
.over
) {
1030 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("out", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1031 this.containers
[i
].containerCache
.over
= 0;
1035 //Do what was originally in plugins
1036 if(this._storedCursor
) $('body').css("cursor", this._storedCursor
); //Reset cursor
1037 if(this._storedOpacity
) this.helper
.css("opacity", this._storedOpacity
); //Reset opacity
1038 if(this._storedZIndex
) this.helper
.css("zIndex", this._storedZIndex
== 'auto' ? '' : this._storedZIndex
); //Reset z-index
1040 this.dragging
= false;
1041 if(this.cancelHelperRemoval
) {
1042 if(!noPropagation
) {
1043 this._trigger("beforeStop", event
, this._uiHash());
1044 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1045 this._trigger("stop", event
, this._uiHash());
1048 this.fromOutside
= false;
1052 if(!noPropagation
) this._trigger("beforeStop", event
, this._uiHash());
1054 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
1055 this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
1057 if(this.helper
[0] != this.currentItem
[0]) this.helper
.remove(); this.helper
= null;
1059 if(!noPropagation
) {
1060 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1061 this._trigger("stop", event
, this._uiHash());
1064 this.fromOutside
= false;
1069 _trigger: function() {
1070 if ($.Widget
.prototype._trigger
.apply(this, arguments
) === false) {
1075 _uiHash: function(inst
) {
1076 var self
= inst
|| this;
1078 helper
: self
.helper
,
1079 placeholder
: self
.placeholder
|| $([]),
1080 position
: self
.position
,
1081 originalPosition
: self
.originalPosition
,
1082 offset
: self
.positionAbs
,
1083 item
: self
.currentItem
,
1084 sender
: inst
? inst
.element
: null
1090 $.extend($.ui
.sortable
, {