2 * jQuery UI Sortable 1.7.1
4 * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
5 * Dual licensed under the MIT (MIT-LICENSE.txt)
6 * and GPL (GPL-LICENSE.txt) licenses.
8 * http://docs.jquery.com/UI/Sortables
15 $.widget("ui.sortable", $.extend({}, $.ui
.mouse
, {
19 this.containerCache
= {};
20 this.element
.addClass("ui-sortable");
25 //Let's determine if the items are floating
26 this.floating
= this.items
.length
? (/left|right/).test(this.items
[0].item
.css('float')) : false;
28 //Let's determine the parent's offset
29 this.offset
= this.element
.offset();
31 //Initialize mouse events for interaction
38 .removeClass("ui-sortable ui-sortable-disabled")
39 .removeData("sortable")
43 for ( var i
= this.items
.length
- 1; i
>= 0; i
-- )
44 this.items
[i
].item
.removeData("sortable-item");
47 _mouseCapture: function(event
, overrideHandle
) {
53 if(this.options
.disabled
|| this.options
.type
== 'static') return false;
55 //We have to refresh the items data once first
56 this._refreshItems(event
);
58 //Find out if the clicked node (or one of its parents) is a actual item in this.items
59 var currentItem
= null, self
= this, nodes
= $(event
.target
).parents().each(function() {
60 if($.data(this, 'sortable-item') == self
) {
61 currentItem
= $(this);
65 if($.data(event
.target
, 'sortable-item') == self
) currentItem
= $(event
.target
);
67 if(!currentItem
) return false;
68 if(this.options
.handle
&& !overrideHandle
) {
69 var validHandle
= false;
71 $(this.options
.handle
, currentItem
).find("*").andSelf().each(function() { if(this == event
.target
) validHandle
= true; });
72 if(!validHandle
) return false;
75 this.currentItem
= currentItem
;
76 this._removeCurrentsFromItems();
81 _mouseStart: function(event
, overrideHandle
, noActivation
) {
83 var o
= this.options
, self
= this;
84 this.currentContainer
= this;
86 //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
87 this.refreshPositions();
89 //Create and append the visible helper
90 this.helper
= this._createHelper(event
);
92 //Cache the helper size
93 this._cacheHelperProportions();
96 * - Position generation -
97 * This block generates everything position related - it's the core of draggables.
100 //Cache the margins of the original element
101 this._cacheMargins();
103 //Get the next scrolling parent
104 this.scrollParent
= this.helper
.scrollParent();
106 //The element's absolute position on the page minus margins
107 this.offset
= this.currentItem
.offset();
109 top
: this.offset
.top
- this.margins
.top
,
110 left
: this.offset
.left
- this.margins
.left
113 // Only after we got the offset, we can change the helper's position to absolute
114 // TODO: Still need to figure out a way to make relative sorting possible
115 this.helper
.css("position", "absolute");
116 this.cssPosition
= this.helper
.css("position");
118 $.extend(this.offset
, {
119 click
: { //Where the click happened, relative to the element
120 left
: event
.pageX
- this.offset
.left
,
121 top
: event
.pageY
- this.offset
.top
123 parent
: this._getParentOffset(),
124 relative
: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
127 //Generate the original position
128 this.originalPosition
= this._generatePosition(event
);
129 this.originalPageX
= event
.pageX
;
130 this.originalPageY
= event
.pageY
;
132 //Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
134 this._adjustOffsetFromHelper(o
.cursorAt
);
136 //Cache the former DOM position
137 this.domPosition
= { prev
: this.currentItem
.prev()[0], parent
: this.currentItem
.parent()[0] };
139 //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
140 if(this.helper
[0] != this.currentItem
[0]) {
141 this.currentItem
.hide();
144 //Create the placeholder
145 this._createPlaceholder();
147 //Set a containment if given in the options
149 this._setContainment();
151 if(o
.cursor
) { // cursor option
152 if ($('body').css("cursor")) this._storedCursor
= $('body').css("cursor");
153 $('body').css("cursor", o
.cursor
);
156 if(o
.opacity
) { // opacity option
157 if (this.helper
.css("opacity")) this._storedOpacity
= this.helper
.css("opacity");
158 this.helper
.css("opacity", o
.opacity
);
161 if(o
.zIndex
) { // zIndex option
162 if (this.helper
.css("zIndex")) this._storedZIndex
= this.helper
.css("zIndex");
163 this.helper
.css("zIndex", o
.zIndex
);
167 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML')
168 this.overflowOffset
= this.scrollParent
.offset();
171 this._trigger("start", event
, this._uiHash());
173 //Recache the helper size
174 if(!this._preserveHelperProportions
)
175 this._cacheHelperProportions();
178 //Post 'activate' events to possible containers
180 for (var i
= this.containers
.length
- 1; i
>= 0; i
--) { this.containers
[i
]._trigger("activate", event
, self
._uiHash(this)); }
183 //Prepare possible droppables
185 $.ui
.ddmanager
.current
= this;
187 if ($.ui
.ddmanager
&& !o
.dropBehaviour
)
188 $.ui
.ddmanager
.prepareOffsets(this, event
);
190 this.dragging
= true;
192 this.helper
.addClass("ui-sortable-helper");
193 this._mouseDrag(event
); //Execute the drag once - this causes the helper not to be visible before getting its correct position
198 _mouseDrag: function(event
) {
200 //Compute the helpers position
201 this.position
= this._generatePosition(event
);
202 this.positionAbs
= this._convertPositionTo("absolute");
204 if (!this.lastPositionAbs
) {
205 this.lastPositionAbs
= this.positionAbs
;
209 if(this.options
.scroll
) {
210 var o
= this.options
, scrolled
= false;
211 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML') {
213 if((this.overflowOffset
.top
+ this.scrollParent
[0].offsetHeight
) - event
.pageY
< o
.scrollSensitivity
)
214 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
+ o
.scrollSpeed
;
215 else if(event
.pageY
- this.overflowOffset
.top
< o
.scrollSensitivity
)
216 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
- o
.scrollSpeed
;
218 if((this.overflowOffset
.left
+ this.scrollParent
[0].offsetWidth
) - event
.pageX
< o
.scrollSensitivity
)
219 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
+ o
.scrollSpeed
;
220 else if(event
.pageX
- this.overflowOffset
.left
< o
.scrollSensitivity
)
221 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
- o
.scrollSpeed
;
225 if(event
.pageY
- $(document
).scrollTop() < o
.scrollSensitivity
)
226 scrolled
= $(document
).scrollTop($(document
).scrollTop() - o
.scrollSpeed
);
227 else if($(window
).height() - (event
.pageY
- $(document
).scrollTop()) < o
.scrollSensitivity
)
228 scrolled
= $(document
).scrollTop($(document
).scrollTop() + o
.scrollSpeed
);
230 if(event
.pageX
- $(document
).scrollLeft() < o
.scrollSensitivity
)
231 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() - o
.scrollSpeed
);
232 else if($(window
).width() - (event
.pageX
- $(document
).scrollLeft()) < o
.scrollSensitivity
)
233 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() + o
.scrollSpeed
);
237 if(scrolled
!== false && $.ui
.ddmanager
&& !o
.dropBehaviour
)
238 $.ui
.ddmanager
.prepareOffsets(this, event
);
241 //Regenerate the absolute position used for position checks
242 this.positionAbs
= this._convertPositionTo("absolute");
244 //Set the helper position
245 if(!this.options
.axis
|| this.options
.axis
!= "y") this.helper
[0].style
.left
= this.position
.left
+'px';
246 if(!this.options
.axis
|| this.options
.axis
!= "x") this.helper
[0].style
.top
= this.position
.top
+'px';
249 for (var i
= this.items
.length
- 1; i
>= 0; i
--) {
251 //Cache variables and intersection, continue if no intersection
252 var item
= this.items
[i
], itemElement
= item
.item
[0], intersection
= this._intersectsWithPointer(item
);
253 if (!intersection
) continue;
255 if(itemElement
!= this.currentItem
[0] //cannot intersect with itself
256 && this.placeholder
[intersection
== 1 ? "next" : "prev"]()[0] != itemElement
//no useless actions that have been done before
257 && !$.ui
.contains(this.placeholder
[0], itemElement
) //no action if the item moved is the parent of the item checked
258 && (this.options
.type
== 'semi-dynamic' ? !$.ui
.contains(this.element
[0], itemElement
) : true)
261 this.direction
= intersection
== 1 ? "down" : "up";
263 if (this.options
.tolerance
== "pointer" || this._intersectsWithSides(item
)) {
264 this._rearrange(event
, item
);
269 this._trigger("change", event
, this._uiHash());
274 //Post events to containers
275 this._contactContainers(event
);
277 //Interconnect with droppables
278 if($.ui
.ddmanager
) $.ui
.ddmanager
.drag(this, event
);
281 this._trigger('sort', event
, this._uiHash());
283 this.lastPositionAbs
= this.positionAbs
;
288 _mouseStop: function(event
, noPropagation
) {
292 //If we are using droppables, inform the manager about the drop
293 if ($.ui
.ddmanager
&& !this.options
.dropBehaviour
)
294 $.ui
.ddmanager
.drop(this, event
);
296 if(this.options
.revert
) {
298 var cur
= self
.placeholder
.offset();
300 self
.reverting
= true;
302 $(this.helper
).animate({
303 left
: cur
.left
- this.offset
.parent
.left
- self
.margins
.left
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollLeft
),
304 top
: cur
.top
- this.offset
.parent
.top
- self
.margins
.top
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollTop
)
305 }, parseInt(this.options
.revert
, 10) || 500, function() {
309 this._clear(event
, noPropagation
);
324 if(this.options
.helper
== "original")
325 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
327 this.currentItem
.show();
329 //Post deactivating events to containers
330 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
331 this.containers
[i
]._trigger("deactivate", null, self
._uiHash(this));
332 if(this.containers
[i
].containerCache
.over
) {
333 this.containers
[i
]._trigger("out", null, self
._uiHash(this));
334 this.containers
[i
].containerCache
.over
= 0;
340 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
341 if(this.placeholder
[0].parentNode
) this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
342 if(this.options
.helper
!= "original" && this.helper
&& this.helper
[0].parentNode
) this.helper
.remove();
351 if(this.domPosition
.prev
) {
352 $(this.domPosition
.prev
).after(this.currentItem
);
354 $(this.domPosition
.parent
).prepend(this.currentItem
);
361 serialize: function(o
) {
363 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
364 var str
= []; o
= o
|| {};
366 $(items
).each(function() {
367 var res
= ($(o
.item
|| this).attr(o
.attribute
|| 'id') || '').match(o
.expression
|| (/(.+)[-=_](.+)/));
368 if(res
) str
.push((o
.key
|| res
[1]+'[]')+'='+(o
.key
&& o
.expression
? res
[1] : res
[2]));
371 return str
.join('&');
375 toArray: function(o
) {
377 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
378 var ret
= []; o
= o
|| {};
380 items
.each(function() { ret
.push($(o
.item
|| this).attr(o
.attribute
|| 'id') || ''); });
385 /* Be careful with the following core functions */
386 _intersectsWith: function(item
) {
388 var x1
= this.positionAbs
.left
,
389 x2
= x1
+ this.helperProportions
.width
,
390 y1
= this.positionAbs
.top
,
391 y2
= y1
+ this.helperProportions
.height
;
398 var dyClick
= this.offset
.click
.top
,
399 dxClick
= this.offset
.click
.left
;
401 var isOverElement
= (y1
+ dyClick
) > t
&& (y1
+ dyClick
) < b
&& (x1
+ dxClick
) > l
&& (x1
+ dxClick
) < r
;
403 if( this.options
.tolerance
== "pointer"
404 || this.options
.forcePointerForContainers
405 || (this.options
.tolerance
!= "pointer" && this.helperProportions
[this.floating
? 'width' : 'height'] > item
[this.floating
? 'width' : 'height'])
407 return isOverElement
;
410 return (l
< x1
+ (this.helperProportions
.width
/ 2) // Right Half
411 && x2
- (this.helperProportions
.width
/ 2) < r
// Left Half
412 && t
< y1
+ (this.helperProportions
.height
/ 2) // Bottom Half
413 && y2
- (this.helperProportions
.height
/ 2) < b
); // Top Half
418 _intersectsWithPointer: function(item
) {
420 var isOverElementHeight
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
, item
.height
),
421 isOverElementWidth
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
, item
.width
),
422 isOverElement
= isOverElementHeight
&& isOverElementWidth
,
423 verticalDirection
= this._getDragVerticalDirection(),
424 horizontalDirection
= this._getDragHorizontalDirection();
429 return this.floating
?
430 ( ((horizontalDirection
&& horizontalDirection
== "right") || verticalDirection
== "down") ? 2 : 1 )
431 : ( verticalDirection
&& (verticalDirection
== "down" ? 2 : 1) );
435 _intersectsWithSides: function(item
) {
437 var isOverBottomHalf
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
+ (item
.height
/2), item
.height
),
438 isOverRightHalf
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
+ (item
.width
/2), item
.width
),
439 verticalDirection
= this._getDragVerticalDirection(),
440 horizontalDirection
= this._getDragHorizontalDirection();
442 if (this.floating
&& horizontalDirection
) {
443 return ((horizontalDirection
== "right" && isOverRightHalf
) || (horizontalDirection
== "left" && !isOverRightHalf
));
445 return verticalDirection
&& ((verticalDirection
== "down" && isOverBottomHalf
) || (verticalDirection
== "up" && !isOverBottomHalf
));
450 _getDragVerticalDirection: function() {
451 var delta
= this.positionAbs
.top
- this.lastPositionAbs
.top
;
452 return delta
!= 0 && (delta
> 0 ? "down" : "up");
455 _getDragHorizontalDirection: function() {
456 var delta
= this.positionAbs
.left
- this.lastPositionAbs
.left
;
457 return delta
!= 0 && (delta
> 0 ? "right" : "left");
460 refresh: function(event
) {
461 this._refreshItems(event
);
462 this.refreshPositions();
465 _connectWith: function() {
466 var options
= this.options
;
467 return options
.connectWith
.constructor == String
468 ? [options
.connectWith
]
469 : options
.connectWith
;
472 _getItemsAsjQuery: function(connected
) {
477 var connectWith
= this._connectWith();
479 if(connectWith
&& connected
) {
480 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
481 var cur
= $(connectWith
[i
]);
482 for (var j
= cur
.length
- 1; j
>= 0; j
--){
483 var inst
= $.data(cur
[j
], 'sortable');
484 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
485 queries
.push([$.isFunction(inst
.options
.items
) ? inst
.options
.items
.call(inst
.element
) : $(inst
.options
.items
, inst
.element
).not(".ui-sortable-helper"), inst
]);
491 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"), this]);
493 for (var i
= queries
.length
- 1; i
>= 0; i
--){
494 queries
[i
][0].each(function() {
503 _removeCurrentsFromItems: function() {
505 var list
= this.currentItem
.find(":data(sortable-item)");
507 for (var i
=0; i
< this.items
.length
; i
++) {
509 for (var j
=0; j
< list
.length
; j
++) {
510 if(list
[j
] == this.items
[i
].item
[0])
511 this.items
.splice(i
,1);
518 _refreshItems: function(event
) {
521 this.containers
= [this];
522 var items
= this.items
;
524 var queries
= [[$.isFunction(this.options
.items
) ? this.options
.items
.call(this.element
[0], event
, { item
: this.currentItem
}) : $(this.options
.items
, this.element
), this]];
525 var connectWith
= this._connectWith();
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
[0], event
, { item
: this.currentItem
}) : $(inst
.options
.items
, inst
.element
), inst
]);
534 this.containers
.push(inst
);
540 for (var i
= queries
.length
- 1; i
>= 0; i
--) {
541 var targetData
= queries
[i
][1];
542 var _queries
= queries
[i
][0];
544 for (var j
=0, queriesLength
= _queries
.length
; j
< queriesLength
; j
++) {
545 var item
= $(_queries
[j
]);
547 item
.data('sortable-item', targetData
); // Data for target checking (mouse manager)
551 instance
: targetData
,
560 refreshPositions: function(fast
) {
562 //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
563 if(this.offsetParent
&& this.helper
) {
564 this.offset
.parent
= this._getParentOffset();
567 for (var i
= this.items
.length
- 1; i
>= 0; i
--){
568 var item
= this.items
[i
];
570 //We ignore calculating positions of all connected containers when we're not over them
571 if(item
.instance
!= this.currentContainer
&& this.currentContainer
&& item
.item
[0] != this.currentItem
[0])
574 var t
= this.options
.toleranceElement
? $(this.options
.toleranceElement
, item
.item
) : item
.item
;
577 item
.width
= t
.outerWidth();
578 item
.height
= t
.outerHeight();
586 if(this.options
.custom
&& this.options
.custom
.refreshContainers
) {
587 this.options
.custom
.refreshContainers
.call(this);
589 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
590 var p
= this.containers
[i
].element
.offset();
591 this.containers
[i
].containerCache
.left
= p
.left
;
592 this.containers
[i
].containerCache
.top
= p
.top
;
593 this.containers
[i
].containerCache
.width
= this.containers
[i
].element
.outerWidth();
594 this.containers
[i
].containerCache
.height
= this.containers
[i
].element
.outerHeight();
600 _createPlaceholder: function(that
) {
602 var self
= that
|| this, o
= self
.options
;
604 if(!o
.placeholder
|| o
.placeholder
.constructor == String
) {
605 var className
= o
.placeholder
;
607 element: function() {
609 var el
= $(document
.createElement(self
.currentItem
[0].nodeName
))
610 .addClass(className
|| self
.currentItem
[0].className
+" ui-sortable-placeholder")
611 .removeClass("ui-sortable-helper")[0];
614 el
.style
.visibility
= "hidden";
618 update: function(container
, p
) {
620 // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
621 // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
622 if(className
&& !o
.forcePlaceholderSize
) return;
624 //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
625 if(!p
.height()) { p
.height(self
.currentItem
.innerHeight() - parseInt(self
.currentItem
.css('paddingTop')||0, 10) - parseInt(self
.currentItem
.css('paddingBottom')||0, 10)); };
626 if(!p
.width()) { p
.width(self
.currentItem
.innerWidth() - parseInt(self
.currentItem
.css('paddingLeft')||0, 10) - parseInt(self
.currentItem
.css('paddingRight')||0, 10)); };
631 //Create the placeholder
632 self
.placeholder
= $(o
.placeholder
.element
.call(self
.element
, self
.currentItem
));
634 //Append it after the actual current item
635 self
.currentItem
.after(self
.placeholder
);
637 //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
638 o
.placeholder
.update(self
, self
.placeholder
);
642 _contactContainers: function(event
) {
643 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
645 if(this._intersectsWith(this.containers
[i
].containerCache
)) {
646 if(!this.containers
[i
].containerCache
.over
) {
648 if(this.currentContainer
!= this.containers
[i
]) {
650 //When entering a new container, we will find the item with the least distance and append our item near it
651 var dist
= 10000; var itemWithLeastDistance
= null; var base
= this.positionAbs
[this.containers
[i
].floating
? 'left' : 'top'];
652 for (var j
= this.items
.length
- 1; j
>= 0; j
--) {
653 if(!$.ui
.contains(this.containers
[i
].element
[0], this.items
[j
].item
[0])) continue;
654 var cur
= this.items
[j
][this.containers
[i
].floating
? 'left' : 'top'];
655 if(Math
.abs(cur
- base
) < dist
) {
656 dist
= Math
.abs(cur
- base
); itemWithLeastDistance
= this.items
[j
];
660 if(!itemWithLeastDistance
&& !this.options
.dropOnEmpty
) //Check if dropOnEmpty is enabled
663 this.currentContainer
= this.containers
[i
];
664 itemWithLeastDistance
? this._rearrange(event
, itemWithLeastDistance
, null, true) : this._rearrange(event
, null, this.containers
[i
].element
, true);
665 this._trigger("change", event
, this._uiHash());
666 this.containers
[i
]._trigger("change", event
, this._uiHash(this));
668 //Update the placeholder
669 this.options
.placeholder
.update(this.currentContainer
, this.placeholder
);
673 this.containers
[i
]._trigger("over", event
, this._uiHash(this));
674 this.containers
[i
].containerCache
.over
= 1;
677 if(this.containers
[i
].containerCache
.over
) {
678 this.containers
[i
]._trigger("out", event
, this._uiHash(this));
679 this.containers
[i
].containerCache
.over
= 0;
686 _createHelper: function(event
) {
688 var o
= this.options
;
689 var helper
= $.isFunction(o
.helper
) ? $(o
.helper
.apply(this.element
[0], [event
, this.currentItem
])) : (o
.helper
== 'clone' ? this.currentItem
.clone() : this.currentItem
);
691 if(!helper
.parents('body').length
) //Add the helper to the DOM if that didn't happen already
692 $(o
.appendTo
!= 'parent' ? o
.appendTo
: this.currentItem
[0].parentNode
)[0].appendChild(helper
[0]);
694 if(helper
[0] == this.currentItem
[0])
695 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") };
697 if(helper
[0].style
.width
== '' || o
.forceHelperSize
) helper
.width(this.currentItem
.width());
698 if(helper
[0].style
.height
== '' || o
.forceHelperSize
) helper
.height(this.currentItem
.height());
704 _adjustOffsetFromHelper: function(obj
) {
705 if(obj
.left
!= undefined) this.offset
.click
.left
= obj
.left
+ this.margins
.left
;
706 if(obj
.right
!= undefined) this.offset
.click
.left
= this.helperProportions
.width
- obj
.right
+ this.margins
.left
;
707 if(obj
.top
!= undefined) this.offset
.click
.top
= obj
.top
+ this.margins
.top
;
708 if(obj
.bottom
!= undefined) this.offset
.click
.top
= this.helperProportions
.height
- obj
.bottom
+ this.margins
.top
;
711 _getParentOffset: function() {
714 //Get the offsetParent and cache its position
715 this.offsetParent
= this.helper
.offsetParent();
716 var po
= this.offsetParent
.offset();
718 // This is a special case where we need to modify a offset calculated on start, since the following happened:
719 // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
720 // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
721 // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
722 if(this.cssPosition
== 'absolute' && this.scrollParent
[0] != document
&& $.ui
.contains(this.scrollParent
[0], this.offsetParent
[0])) {
723 po
.left
+= this.scrollParent
.scrollLeft();
724 po
.top
+= this.scrollParent
.scrollTop();
727 if((this.offsetParent
[0] == document
.body
) //This needs to be actually done for all browsers, since pageX/pageY includes this information
728 || (this.offsetParent
[0].tagName
&& this.offsetParent
[0].tagName
.toLowerCase() == 'html' && $.browser
.msie
)) //Ugly IE fix
729 po
= { top
: 0, left
: 0 };
732 top
: po
.top
+ (parseInt(this.offsetParent
.css("borderTopWidth"),10) || 0),
733 left
: po
.left
+ (parseInt(this.offsetParent
.css("borderLeftWidth"),10) || 0)
738 _getRelativeOffset: function() {
740 if(this.cssPosition
== "relative") {
741 var p
= this.currentItem
.position();
743 top
: p
.top
- (parseInt(this.helper
.css("top"),10) || 0) + this.scrollParent
.scrollTop(),
744 left
: p
.left
- (parseInt(this.helper
.css("left"),10) || 0) + this.scrollParent
.scrollLeft()
747 return { top
: 0, left
: 0 };
752 _cacheMargins: function() {
754 left
: (parseInt(this.currentItem
.css("marginLeft"),10) || 0),
755 top
: (parseInt(this.currentItem
.css("marginTop"),10) || 0)
759 _cacheHelperProportions: function() {
760 this.helperProportions
= {
761 width
: this.helper
.outerWidth(),
762 height
: this.helper
.outerHeight()
766 _setContainment: function() {
768 var o
= this.options
;
769 if(o
.containment
== 'parent') o
.containment
= this.helper
[0].parentNode
;
770 if(o
.containment
== 'document' || o
.containment
== 'window') this.containment
= [
771 0 - this.offset
.relative
.left
- this.offset
.parent
.left
,
772 0 - this.offset
.relative
.top
- this.offset
.parent
.top
,
773 $(o
.containment
== 'document' ? document
: window
).width() - this.helperProportions
.width
- this.margins
.left
,
774 ($(o
.containment
== 'document' ? document
: window
).height() || document
.body
.parentNode
.scrollHeight
) - this.helperProportions
.height
- this.margins
.top
777 if(!(/^(document|window|parent)$/).test(o
.containment
)) {
778 var ce
= $(o
.containment
)[0];
779 var co
= $(o
.containment
).offset();
780 var over
= ($(ce
).css("overflow") != 'hidden');
783 co
.left
+ (parseInt($(ce
).css("borderLeftWidth"),10) || 0) + (parseInt($(ce
).css("paddingLeft"),10) || 0) - this.margins
.left
,
784 co
.top
+ (parseInt($(ce
).css("borderTopWidth"),10) || 0) + (parseInt($(ce
).css("paddingTop"),10) || 0) - this.margins
.top
,
785 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
,
786 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
792 _convertPositionTo: function(d
, pos
) {
794 if(!pos
) pos
= this.position
;
795 var mod
= d
== "absolute" ? 1 : -1;
796 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
);
800 pos
.top
// The absolute mouse position
801 + this.offset
.relative
.top
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
802 + this.offset
.parent
.top
* mod
// The offsetParent's offset without borders (offset + border)
803 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ) * mod
)
806 pos
.left
// The absolute mouse position
807 + this.offset
.relative
.left
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
808 + this.offset
.parent
.left
* mod
// The offsetParent's offset without borders (offset + border)
809 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ) * mod
)
815 _generatePosition: function(event
) {
817 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
);
819 // This is another very weird special case that only happens for relative elements:
820 // 1. If the css position is relative
821 // 2. and the scroll parent is the document or similar to the offset parent
822 // we have to refresh the relative offset during the scroll so there are no jumps
823 if(this.cssPosition
== 'relative' && !(this.scrollParent
[0] != document
&& this.scrollParent
[0] != this.offsetParent
[0])) {
824 this.offset
.relative
= this._getRelativeOffset();
827 var pageX
= event
.pageX
;
828 var pageY
= event
.pageY
;
831 * - Position constraining -
832 * Constrain the position to a mix of grid, containment.
835 if(this.originalPosition
) { //If we are not dragging yet, we won't check for options
837 if(this.containment
) {
838 if(event
.pageX
- this.offset
.click
.left
< this.containment
[0]) pageX
= this.containment
[0] + this.offset
.click
.left
;
839 if(event
.pageY
- this.offset
.click
.top
< this.containment
[1]) pageY
= this.containment
[1] + this.offset
.click
.top
;
840 if(event
.pageX
- this.offset
.click
.left
> this.containment
[2]) pageX
= this.containment
[2] + this.offset
.click
.left
;
841 if(event
.pageY
- this.offset
.click
.top
> this.containment
[3]) pageY
= this.containment
[3] + this.offset
.click
.top
;
845 var top
= this.originalPageY
+ Math
.round((pageY
- this.originalPageY
) / o
.grid
[1]) * o
.grid
[1];
846 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
;
848 var left
= this.originalPageX
+ Math
.round((pageX
- this.originalPageX
) / o
.grid
[0]) * o
.grid
[0];
849 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
;
856 pageY
// The absolute mouse position
857 - this.offset
.click
.top
// Click offset (relative to the element)
858 - this.offset
.relative
.top
// Only for relative positioned nodes: Relative offset from element to offset parent
859 - this.offset
.parent
.top
// The offsetParent's offset without borders (offset + border)
860 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ))
863 pageX
// The absolute mouse position
864 - this.offset
.click
.left
// Click offset (relative to the element)
865 - this.offset
.relative
.left
// Only for relative positioned nodes: Relative offset from element to offset parent
866 - this.offset
.parent
.left
// The offsetParent's offset without borders (offset + border)
867 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ))
873 _rearrange: function(event
, i
, a
, hardRefresh
) {
875 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
));
877 //Various things done here to improve the performance:
878 // 1. we create a setTimeout, that calls refreshPositions
879 // 2. on the instance, we have a counter variable, that get's higher after every append
880 // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
881 // 4. this lets only the last addition to the timeout stack through
882 this.counter
= this.counter
? ++this.counter
: 1;
883 var self
= this, counter
= this.counter
;
885 window
.setTimeout(function() {
886 if(counter
== self
.counter
) self
.refreshPositions(!hardRefresh
); //Precompute after each DOM insertion, NOT on mousemove
891 _clear: function(event
, noPropagation
) {
893 this.reverting
= false;
894 // We delay all events that have to be triggered to after the point where the placeholder has been removed and
895 // everything else normalized again
896 var delayedTriggers
= [], self
= this;
898 // We first have to update the dom position of the actual currentItem
899 // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
900 if(!this._noFinalSort
&& this.currentItem
[0].parentNode
) this.placeholder
.before(this.currentItem
);
901 this._noFinalSort
= null;
903 if(this.helper
[0] == this.currentItem
[0]) {
904 for(var i
in this._storedCSS
) {
905 if(this._storedCSS
[i
] == 'auto' || this._storedCSS
[i
] == 'static') this._storedCSS
[i
] = '';
907 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
909 this.currentItem
.show();
912 if(this.fromOutside
&& !noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("receive", event
, this._uiHash(this.fromOutside
)); });
913 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
914 if(!$.ui
.contains(this.element
[0], this.currentItem
[0])) { //Node was moved out of the current element
915 if(!noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("remove", event
, this._uiHash()); });
916 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
917 if($.ui
.contains(this.containers
[i
].element
[0], this.currentItem
[0]) && !noPropagation
) {
918 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("receive", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
919 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("update", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
924 //Post events to containers
925 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
926 if(!noPropagation
) delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("deactivate", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
927 if(this.containers
[i
].containerCache
.over
) {
928 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("out", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
929 this.containers
[i
].containerCache
.over
= 0;
933 //Do what was originally in plugins
934 if(this._storedCursor
) $('body').css("cursor", this._storedCursor
); //Reset cursor
935 if(this._storedOpacity
) this.helper
.css("opacity", this._storedOpacity
); //Reset cursor
936 if(this._storedZIndex
) this.helper
.css("zIndex", this._storedZIndex
== 'auto' ? '' : this._storedZIndex
); //Reset z-index
938 this.dragging
= false;
939 if(this.cancelHelperRemoval
) {
941 this._trigger("beforeStop", event
, this._uiHash());
942 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
943 this._trigger("stop", event
, this._uiHash());
948 if(!noPropagation
) this._trigger("beforeStop", event
, this._uiHash());
950 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
951 this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
953 if(this.helper
[0] != this.currentItem
[0]) this.helper
.remove(); this.helper
= null;
956 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
957 this._trigger("stop", event
, this._uiHash());
960 this.fromOutside
= false;
965 _trigger: function() {
966 if ($.widget
.prototype._trigger
.apply(this, arguments
) === false) {
971 _uiHash: function(inst
) {
972 var self
= inst
|| this;
975 placeholder
: self
.placeholder
|| $([]),
976 position
: self
.position
,
977 absolutePosition
: self
.positionAbs
, //deprecated
978 offset
: self
.positionAbs
,
979 item
: self
.currentItem
,
980 sender
: inst
? inst
.element
: null
986 $.extend($.ui
.sortable
, {
987 getter
: "serialize toArray",
993 cancel
: ":input,option",
1001 forcePlaceholderSize
: false,
1002 forceHelperSize
: false,
1011 scrollSensitivity
: 20,
1014 tolerance
: "intersect",