2 * jQuery UI Sortable 1.8.2
4 * Copyright (c) 2010 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
17 $.widget("ui.sortable", $.ui
.mouse
, {
18 widgetEventPrefix
: "sort",
27 forcePlaceholderSize
: false,
28 forceHelperSize
: false,
37 scrollSensitivity
: 20,
40 tolerance
: "intersect",
46 this.containerCache
= {};
47 this.element
.addClass("ui-sortable");
52 //Let's determine if the items are floating
53 this.floating
= this.items
.length
? (/left|right/).test(this.items
[0].item
.css('float')) : false;
55 //Let's determine the parent's offset
56 this.offset
= this.element
.offset();
58 //Initialize mouse events for interaction
65 .removeClass("ui-sortable ui-sortable-disabled")
66 .removeData("sortable")
70 for ( var i
= this.items
.length
- 1; i
>= 0; i
-- )
71 this.items
[i
].item
.removeData("sortable-item");
76 _setOption: function(key
, value
){
77 if ( key
=== "disabled" ) {
78 this.options
[ key
] = value
;
81 [ value
? "addClass" : "removeClass"]( "ui-sortable-disabled" );
83 // Don't call widget base _setOption for disable as it adds ui-state-disabled class
84 $.Widget
.prototype._setOption
.apply(this, arguments
);
88 _mouseCapture: function(event
, overrideHandle
) {
94 if(this.options
.disabled
|| this.options
.type
== 'static') return false;
96 //We have to refresh the items data once first
97 this._refreshItems(event
);
99 //Find out if the clicked node (or one of its parents) is a actual item in this.items
100 var currentItem
= null, self
= this, nodes
= $(event
.target
).parents().each(function() {
101 if($.data(this, 'sortable-item') == self
) {
102 currentItem
= $(this);
106 if($.data(event
.target
, 'sortable-item') == self
) currentItem
= $(event
.target
);
108 if(!currentItem
) return false;
109 if(this.options
.handle
&& !overrideHandle
) {
110 var validHandle
= false;
112 $(this.options
.handle
, currentItem
).find("*").andSelf().each(function() { if(this == event
.target
) validHandle
= true; });
113 if(!validHandle
) return false;
116 this.currentItem
= currentItem
;
117 this._removeCurrentsFromItems();
122 _mouseStart: function(event
, overrideHandle
, noActivation
) {
124 var o
= this.options
, self
= this;
125 this.currentContainer
= this;
127 //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
128 this.refreshPositions();
130 //Create and append the visible helper
131 this.helper
= this._createHelper(event
);
133 //Cache the helper size
134 this._cacheHelperProportions();
137 * - Position generation -
138 * This block generates everything position related - it's the core of draggables.
141 //Cache the margins of the original element
142 this._cacheMargins();
144 //Get the next scrolling parent
145 this.scrollParent
= this.helper
.scrollParent();
147 //The element's absolute position on the page minus margins
148 this.offset
= this.currentItem
.offset();
150 top
: this.offset
.top
- this.margins
.top
,
151 left
: this.offset
.left
- this.margins
.left
154 // Only after we got the offset, we can change the helper's position to absolute
155 // TODO: Still need to figure out a way to make relative sorting possible
156 this.helper
.css("position", "absolute");
157 this.cssPosition
= this.helper
.css("position");
159 $.extend(this.offset
, {
160 click
: { //Where the click happened, relative to the element
161 left
: event
.pageX
- this.offset
.left
,
162 top
: event
.pageY
- this.offset
.top
164 parent
: this._getParentOffset(),
165 relative
: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
168 //Generate the original position
169 this.originalPosition
= this._generatePosition(event
);
170 this.originalPageX
= event
.pageX
;
171 this.originalPageY
= event
.pageY
;
173 //Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
174 (o
.cursorAt
&& this._adjustOffsetFromHelper(o
.cursorAt
));
176 //Cache the former DOM position
177 this.domPosition
= { prev
: this.currentItem
.prev()[0], parent
: this.currentItem
.parent()[0] };
179 //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
180 if(this.helper
[0] != this.currentItem
[0]) {
181 this.currentItem
.hide();
184 //Create the placeholder
185 this._createPlaceholder();
187 //Set a containment if given in the options
189 this._setContainment();
191 if(o
.cursor
) { // cursor option
192 if ($('body').css("cursor")) this._storedCursor
= $('body').css("cursor");
193 $('body').css("cursor", o
.cursor
);
196 if(o
.opacity
) { // opacity option
197 if (this.helper
.css("opacity")) this._storedOpacity
= this.helper
.css("opacity");
198 this.helper
.css("opacity", o
.opacity
);
201 if(o
.zIndex
) { // zIndex option
202 if (this.helper
.css("zIndex")) this._storedZIndex
= this.helper
.css("zIndex");
203 this.helper
.css("zIndex", o
.zIndex
);
207 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML')
208 this.overflowOffset
= this.scrollParent
.offset();
211 this._trigger("start", event
, this._uiHash());
213 //Recache the helper size
214 if(!this._preserveHelperProportions
)
215 this._cacheHelperProportions();
218 //Post 'activate' events to possible containers
220 for (var i
= this.containers
.length
- 1; i
>= 0; i
--) { this.containers
[i
]._trigger("activate", event
, self
._uiHash(this)); }
223 //Prepare possible droppables
225 $.ui
.ddmanager
.current
= this;
227 if ($.ui
.ddmanager
&& !o
.dropBehaviour
)
228 $.ui
.ddmanager
.prepareOffsets(this, event
);
230 this.dragging
= true;
232 this.helper
.addClass("ui-sortable-helper");
233 this._mouseDrag(event
); //Execute the drag once - this causes the helper not to be visible before getting its correct position
238 _mouseDrag: function(event
) {
240 //Compute the helpers position
241 this.position
= this._generatePosition(event
);
242 this.positionAbs
= this._convertPositionTo("absolute");
244 if (!this.lastPositionAbs
) {
245 this.lastPositionAbs
= this.positionAbs
;
249 if(this.options
.scroll
) {
250 var o
= this.options
, scrolled
= false;
251 if(this.scrollParent
[0] != document
&& this.scrollParent
[0].tagName
!= 'HTML') {
253 if((this.overflowOffset
.top
+ this.scrollParent
[0].offsetHeight
) - event
.pageY
< o
.scrollSensitivity
)
254 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
+ o
.scrollSpeed
;
255 else if(event
.pageY
- this.overflowOffset
.top
< o
.scrollSensitivity
)
256 this.scrollParent
[0].scrollTop
= scrolled
= this.scrollParent
[0].scrollTop
- o
.scrollSpeed
;
258 if((this.overflowOffset
.left
+ this.scrollParent
[0].offsetWidth
) - event
.pageX
< o
.scrollSensitivity
)
259 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
+ o
.scrollSpeed
;
260 else if(event
.pageX
- this.overflowOffset
.left
< o
.scrollSensitivity
)
261 this.scrollParent
[0].scrollLeft
= scrolled
= this.scrollParent
[0].scrollLeft
- o
.scrollSpeed
;
265 if(event
.pageY
- $(document
).scrollTop() < o
.scrollSensitivity
)
266 scrolled
= $(document
).scrollTop($(document
).scrollTop() - o
.scrollSpeed
);
267 else if($(window
).height() - (event
.pageY
- $(document
).scrollTop()) < o
.scrollSensitivity
)
268 scrolled
= $(document
).scrollTop($(document
).scrollTop() + o
.scrollSpeed
);
270 if(event
.pageX
- $(document
).scrollLeft() < o
.scrollSensitivity
)
271 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() - o
.scrollSpeed
);
272 else if($(window
).width() - (event
.pageX
- $(document
).scrollLeft()) < o
.scrollSensitivity
)
273 scrolled
= $(document
).scrollLeft($(document
).scrollLeft() + o
.scrollSpeed
);
277 if(scrolled
!== false && $.ui
.ddmanager
&& !o
.dropBehaviour
)
278 $.ui
.ddmanager
.prepareOffsets(this, event
);
281 //Regenerate the absolute position used for position checks
282 this.positionAbs
= this._convertPositionTo("absolute");
284 //Set the helper position
285 if(!this.options
.axis
|| this.options
.axis
!= "y") this.helper
[0].style
.left
= this.position
.left
+'px';
286 if(!this.options
.axis
|| this.options
.axis
!= "x") this.helper
[0].style
.top
= this.position
.top
+'px';
289 for (var i
= this.items
.length
- 1; i
>= 0; i
--) {
291 //Cache variables and intersection, continue if no intersection
292 var item
= this.items
[i
], itemElement
= item
.item
[0], intersection
= this._intersectsWithPointer(item
);
293 if (!intersection
) continue;
295 if(itemElement
!= this.currentItem
[0] //cannot intersect with itself
296 && this.placeholder
[intersection
== 1 ? "next" : "prev"]()[0] != itemElement
//no useless actions that have been done before
297 && !$.ui
.contains(this.placeholder
[0], itemElement
) //no action if the item moved is the parent of the item checked
298 && (this.options
.type
== 'semi-dynamic' ? !$.ui
.contains(this.element
[0], itemElement
) : true)
299 //&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container
302 this.direction
= intersection
== 1 ? "down" : "up";
304 if (this.options
.tolerance
== "pointer" || this._intersectsWithSides(item
)) {
305 this._rearrange(event
, item
);
310 this._trigger("change", event
, this._uiHash());
315 //Post events to containers
316 this._contactContainers(event
);
318 //Interconnect with droppables
319 if($.ui
.ddmanager
) $.ui
.ddmanager
.drag(this, event
);
322 this._trigger('sort', event
, this._uiHash());
324 this.lastPositionAbs
= this.positionAbs
;
329 _mouseStop: function(event
, noPropagation
) {
333 //If we are using droppables, inform the manager about the drop
334 if ($.ui
.ddmanager
&& !this.options
.dropBehaviour
)
335 $.ui
.ddmanager
.drop(this, event
);
337 if(this.options
.revert
) {
339 var cur
= self
.placeholder
.offset();
341 self
.reverting
= true;
343 $(this.helper
).animate({
344 left
: cur
.left
- this.offset
.parent
.left
- self
.margins
.left
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollLeft
),
345 top
: cur
.top
- this.offset
.parent
.top
- self
.margins
.top
+ (this.offsetParent
[0] == document
.body
? 0 : this.offsetParent
[0].scrollTop
)
346 }, parseInt(this.options
.revert
, 10) || 500, function() {
350 this._clear(event
, noPropagation
);
365 if(this.options
.helper
== "original")
366 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
368 this.currentItem
.show();
370 //Post deactivating events to containers
371 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
372 this.containers
[i
]._trigger("deactivate", null, self
._uiHash(this));
373 if(this.containers
[i
].containerCache
.over
) {
374 this.containers
[i
]._trigger("out", null, self
._uiHash(this));
375 this.containers
[i
].containerCache
.over
= 0;
381 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
382 if(this.placeholder
[0].parentNode
) this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
383 if(this.options
.helper
!= "original" && this.helper
&& this.helper
[0].parentNode
) this.helper
.remove();
392 if(this.domPosition
.prev
) {
393 $(this.domPosition
.prev
).after(this.currentItem
);
395 $(this.domPosition
.parent
).prepend(this.currentItem
);
402 serialize: function(o
) {
404 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
405 var str
= []; o
= o
|| {};
407 $(items
).each(function() {
408 var res
= ($(o
.item
|| this).attr(o
.attribute
|| 'id') || '').match(o
.expression
|| (/(.+)[-=_](.+)/));
409 if(res
) str
.push((o
.key
|| res
[1]+'[]')+'='+(o
.key
&& o
.expression
? res
[1] : res
[2]));
412 return str
.join('&');
416 toArray: function(o
) {
418 var items
= this._getItemsAsjQuery(o
&& o
.connected
);
419 var ret
= []; o
= o
|| {};
421 items
.each(function() { ret
.push($(o
.item
|| this).attr(o
.attribute
|| 'id') || ''); });
426 /* Be careful with the following core functions */
427 _intersectsWith: function(item
) {
429 var x1
= this.positionAbs
.left
,
430 x2
= x1
+ this.helperProportions
.width
,
431 y1
= this.positionAbs
.top
,
432 y2
= y1
+ this.helperProportions
.height
;
439 var dyClick
= this.offset
.click
.top
,
440 dxClick
= this.offset
.click
.left
;
442 var isOverElement
= (y1
+ dyClick
) > t
&& (y1
+ dyClick
) < b
&& (x1
+ dxClick
) > l
&& (x1
+ dxClick
) < r
;
444 if( this.options
.tolerance
== "pointer"
445 || this.options
.forcePointerForContainers
446 || (this.options
.tolerance
!= "pointer" && this.helperProportions
[this.floating
? 'width' : 'height'] > item
[this.floating
? 'width' : 'height'])
448 return isOverElement
;
451 return (l
< x1
+ (this.helperProportions
.width
/ 2) // Right Half
452 && x2
- (this.helperProportions
.width
/ 2) < r
// Left Half
453 && t
< y1
+ (this.helperProportions
.height
/ 2) // Bottom Half
454 && y2
- (this.helperProportions
.height
/ 2) < b
); // Top Half
459 _intersectsWithPointer: function(item
) {
461 var isOverElementHeight
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
, item
.height
),
462 isOverElementWidth
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
, item
.width
),
463 isOverElement
= isOverElementHeight
&& isOverElementWidth
,
464 verticalDirection
= this._getDragVerticalDirection(),
465 horizontalDirection
= this._getDragHorizontalDirection();
470 return this.floating
?
471 ( ((horizontalDirection
&& horizontalDirection
== "right") || verticalDirection
== "down") ? 2 : 1 )
472 : ( verticalDirection
&& (verticalDirection
== "down" ? 2 : 1) );
476 _intersectsWithSides: function(item
) {
478 var isOverBottomHalf
= $.ui
.isOverAxis(this.positionAbs
.top
+ this.offset
.click
.top
, item
.top
+ (item
.height
/2), item
.height
),
479 isOverRightHalf
= $.ui
.isOverAxis(this.positionAbs
.left
+ this.offset
.click
.left
, item
.left
+ (item
.width
/2), item
.width
),
480 verticalDirection
= this._getDragVerticalDirection(),
481 horizontalDirection
= this._getDragHorizontalDirection();
483 if (this.floating
&& horizontalDirection
) {
484 return ((horizontalDirection
== "right" && isOverRightHalf
) || (horizontalDirection
== "left" && !isOverRightHalf
));
486 return verticalDirection
&& ((verticalDirection
== "down" && isOverBottomHalf
) || (verticalDirection
== "up" && !isOverBottomHalf
));
491 _getDragVerticalDirection: function() {
492 var delta
= this.positionAbs
.top
- this.lastPositionAbs
.top
;
493 return delta
!= 0 && (delta
> 0 ? "down" : "up");
496 _getDragHorizontalDirection: function() {
497 var delta
= this.positionAbs
.left
- this.lastPositionAbs
.left
;
498 return delta
!= 0 && (delta
> 0 ? "right" : "left");
501 refresh: function(event
) {
502 this._refreshItems(event
);
503 this.refreshPositions();
507 _connectWith: function() {
508 var options
= this.options
;
509 return options
.connectWith
.constructor == String
510 ? [options
.connectWith
]
511 : options
.connectWith
;
514 _getItemsAsjQuery: function(connected
) {
519 var connectWith
= this._connectWith();
521 if(connectWith
&& connected
) {
522 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
523 var cur
= $(connectWith
[i
]);
524 for (var j
= cur
.length
- 1; j
>= 0; j
--){
525 var inst
= $.data(cur
[j
], 'sortable');
526 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
527 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
]);
533 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]);
535 for (var i
= queries
.length
- 1; i
>= 0; i
--){
536 queries
[i
][0].each(function() {
545 _removeCurrentsFromItems: function() {
547 var list
= this.currentItem
.find(":data(sortable-item)");
549 for (var i
=0; i
< this.items
.length
; i
++) {
551 for (var j
=0; j
< list
.length
; j
++) {
552 if(list
[j
] == this.items
[i
].item
[0])
553 this.items
.splice(i
,1);
560 _refreshItems: function(event
) {
563 this.containers
= [this];
564 var items
= this.items
;
566 var queries
= [[$.isFunction(this.options
.items
) ? this.options
.items
.call(this.element
[0], event
, { item
: this.currentItem
}) : $(this.options
.items
, this.element
), this]];
567 var connectWith
= this._connectWith();
570 for (var i
= connectWith
.length
- 1; i
>= 0; i
--){
571 var cur
= $(connectWith
[i
]);
572 for (var j
= cur
.length
- 1; j
>= 0; j
--){
573 var inst
= $.data(cur
[j
], 'sortable');
574 if(inst
&& inst
!= this && !inst
.options
.disabled
) {
575 queries
.push([$.isFunction(inst
.options
.items
) ? inst
.options
.items
.call(inst
.element
[0], event
, { item
: this.currentItem
}) : $(inst
.options
.items
, inst
.element
), inst
]);
576 this.containers
.push(inst
);
582 for (var i
= queries
.length
- 1; i
>= 0; i
--) {
583 var targetData
= queries
[i
][1];
584 var _queries
= queries
[i
][0];
586 for (var j
=0, queriesLength
= _queries
.length
; j
< queriesLength
; j
++) {
587 var item
= $(_queries
[j
]);
589 item
.data('sortable-item', targetData
); // Data for target checking (mouse manager)
593 instance
: targetData
,
602 refreshPositions: function(fast
) {
604 //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
605 if(this.offsetParent
&& this.helper
) {
606 this.offset
.parent
= this._getParentOffset();
609 for (var i
= this.items
.length
- 1; i
>= 0; i
--){
610 var item
= this.items
[i
];
612 var t
= this.options
.toleranceElement
? $(this.options
.toleranceElement
, item
.item
) : item
.item
;
615 item
.width
= t
.outerWidth();
616 item
.height
= t
.outerHeight();
624 if(this.options
.custom
&& this.options
.custom
.refreshContainers
) {
625 this.options
.custom
.refreshContainers
.call(this);
627 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
628 var p
= this.containers
[i
].element
.offset();
629 this.containers
[i
].containerCache
.left
= p
.left
;
630 this.containers
[i
].containerCache
.top
= p
.top
;
631 this.containers
[i
].containerCache
.width
= this.containers
[i
].element
.outerWidth();
632 this.containers
[i
].containerCache
.height
= this.containers
[i
].element
.outerHeight();
639 _createPlaceholder: function(that
) {
641 var self
= that
|| this, o
= self
.options
;
643 if(!o
.placeholder
|| o
.placeholder
.constructor == String
) {
644 var className
= o
.placeholder
;
646 element: function() {
648 var el
= $(document
.createElement(self
.currentItem
[0].nodeName
))
649 .addClass(className
|| self
.currentItem
[0].className
+" ui-sortable-placeholder")
650 .removeClass("ui-sortable-helper")[0];
653 el
.style
.visibility
= "hidden";
657 update: function(container
, p
) {
659 // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
660 // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
661 if(className
&& !o
.forcePlaceholderSize
) return;
663 //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
664 if(!p
.height()) { p
.height(self
.currentItem
.innerHeight() - parseInt(self
.currentItem
.css('paddingTop')||0, 10) - parseInt(self
.currentItem
.css('paddingBottom')||0, 10)); };
665 if(!p
.width()) { p
.width(self
.currentItem
.innerWidth() - parseInt(self
.currentItem
.css('paddingLeft')||0, 10) - parseInt(self
.currentItem
.css('paddingRight')||0, 10)); };
670 //Create the placeholder
671 self
.placeholder
= $(o
.placeholder
.element
.call(self
.element
, self
.currentItem
));
673 //Append it after the actual current item
674 self
.currentItem
.after(self
.placeholder
);
676 //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
677 o
.placeholder
.update(self
, self
.placeholder
);
681 _contactContainers: function(event
) {
683 // get innermost container that intersects with item
684 var innermostContainer
= null, innermostIndex
= null;
687 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
689 // never consider a container that's located within the item itself
690 if($.ui
.contains(this.currentItem
[0], this.containers
[i
].element
[0]))
693 if(this._intersectsWith(this.containers
[i
].containerCache
)) {
695 // if we've already found a container and it's more "inner" than this, then continue
696 if(innermostContainer
&& $.ui
.contains(this.containers
[i
].element
[0], innermostContainer
.element
[0]))
699 innermostContainer
= this.containers
[i
];
703 // container doesn't intersect. trigger "out" event if necessary
704 if(this.containers
[i
].containerCache
.over
) {
705 this.containers
[i
]._trigger("out", event
, this._uiHash(this));
706 this.containers
[i
].containerCache
.over
= 0;
712 // if no intersecting containers found, return
713 if(!innermostContainer
) return;
715 // move the item into the container if it's not there already
716 if(this.containers
.length
=== 1) {
717 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
718 this.containers
[innermostIndex
].containerCache
.over
= 1;
719 } else if(this.currentContainer
!= this.containers
[innermostIndex
]) {
721 //When entering a new container, we will find the item with the least distance and append our item near it
722 var dist
= 10000; var itemWithLeastDistance
= null; var base
= this.positionAbs
[this.containers
[innermostIndex
].floating
? 'left' : 'top'];
723 for (var j
= this.items
.length
- 1; j
>= 0; j
--) {
724 if(!$.ui
.contains(this.containers
[innermostIndex
].element
[0], this.items
[j
].item
[0])) continue;
725 var cur
= this.items
[j
][this.containers
[innermostIndex
].floating
? 'left' : 'top'];
726 if(Math
.abs(cur
- base
) < dist
) {
727 dist
= Math
.abs(cur
- base
); itemWithLeastDistance
= this.items
[j
];
731 if(!itemWithLeastDistance
&& !this.options
.dropOnEmpty
) //Check if dropOnEmpty is enabled
734 this.currentContainer
= this.containers
[innermostIndex
];
735 itemWithLeastDistance
? this._rearrange(event
, itemWithLeastDistance
, null, true) : this._rearrange(event
, null, this.containers
[innermostIndex
].element
, true);
736 this._trigger("change", event
, this._uiHash());
737 this.containers
[innermostIndex
]._trigger("change", event
, this._uiHash(this));
739 //Update the placeholder
740 this.options
.placeholder
.update(this.currentContainer
, this.placeholder
);
742 this.containers
[innermostIndex
]._trigger("over", event
, this._uiHash(this));
743 this.containers
[innermostIndex
].containerCache
.over
= 1;
749 _createHelper: function(event
) {
751 var o
= this.options
;
752 var helper
= $.isFunction(o
.helper
) ? $(o
.helper
.apply(this.element
[0], [event
, this.currentItem
])) : (o
.helper
== 'clone' ? this.currentItem
.clone() : this.currentItem
);
754 if(!helper
.parents('body').length
) //Add the helper to the DOM if that didn't happen already
755 $(o
.appendTo
!= 'parent' ? o
.appendTo
: this.currentItem
[0].parentNode
)[0].appendChild(helper
[0]);
757 if(helper
[0] == this.currentItem
[0])
758 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") };
760 if(helper
[0].style
.width
== '' || o
.forceHelperSize
) helper
.width(this.currentItem
.width());
761 if(helper
[0].style
.height
== '' || o
.forceHelperSize
) helper
.height(this.currentItem
.height());
767 _adjustOffsetFromHelper: function(obj
) {
768 if (typeof obj
== 'string') {
769 obj
= obj
.split(' ');
771 if ($.isArray(obj
)) {
772 obj
= {left
: +obj
[0], top
: +obj
[1] || 0};
775 this.offset
.click
.left
= obj
.left
+ this.margins
.left
;
777 if ('right' in obj
) {
778 this.offset
.click
.left
= this.helperProportions
.width
- obj
.right
+ this.margins
.left
;
781 this.offset
.click
.top
= obj
.top
+ this.margins
.top
;
783 if ('bottom' in obj
) {
784 this.offset
.click
.top
= this.helperProportions
.height
- obj
.bottom
+ this.margins
.top
;
788 _getParentOffset: function() {
791 //Get the offsetParent and cache its position
792 this.offsetParent
= this.helper
.offsetParent();
793 var po
= this.offsetParent
.offset();
795 // This is a special case where we need to modify a offset calculated on start, since the following happened:
796 // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
797 // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
798 // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
799 if(this.cssPosition
== 'absolute' && this.scrollParent
[0] != document
&& $.ui
.contains(this.scrollParent
[0], this.offsetParent
[0])) {
800 po
.left
+= this.scrollParent
.scrollLeft();
801 po
.top
+= this.scrollParent
.scrollTop();
804 if((this.offsetParent
[0] == document
.body
) //This needs to be actually done for all browsers, since pageX/pageY includes this information
805 || (this.offsetParent
[0].tagName
&& this.offsetParent
[0].tagName
.toLowerCase() == 'html' && $.browser
.msie
)) //Ugly IE fix
806 po
= { top
: 0, left
: 0 };
809 top
: po
.top
+ (parseInt(this.offsetParent
.css("borderTopWidth"),10) || 0),
810 left
: po
.left
+ (parseInt(this.offsetParent
.css("borderLeftWidth"),10) || 0)
815 _getRelativeOffset: function() {
817 if(this.cssPosition
== "relative") {
818 var p
= this.currentItem
.position();
820 top
: p
.top
- (parseInt(this.helper
.css("top"),10) || 0) + this.scrollParent
.scrollTop(),
821 left
: p
.left
- (parseInt(this.helper
.css("left"),10) || 0) + this.scrollParent
.scrollLeft()
824 return { top
: 0, left
: 0 };
829 _cacheMargins: function() {
831 left
: (parseInt(this.currentItem
.css("marginLeft"),10) || 0),
832 top
: (parseInt(this.currentItem
.css("marginTop"),10) || 0)
836 _cacheHelperProportions: function() {
837 this.helperProportions
= {
838 width
: this.helper
.outerWidth(),
839 height
: this.helper
.outerHeight()
843 _setContainment: function() {
845 var o
= this.options
;
846 if(o
.containment
== 'parent') o
.containment
= this.helper
[0].parentNode
;
847 if(o
.containment
== 'document' || o
.containment
== 'window') this.containment
= [
848 0 - this.offset
.relative
.left
- this.offset
.parent
.left
,
849 0 - this.offset
.relative
.top
- this.offset
.parent
.top
,
850 $(o
.containment
== 'document' ? document
: window
).width() - this.helperProportions
.width
- this.margins
.left
,
851 ($(o
.containment
== 'document' ? document
: window
).height() || document
.body
.parentNode
.scrollHeight
) - this.helperProportions
.height
- this.margins
.top
854 if(!(/^(document|window|parent)$/).test(o
.containment
)) {
855 var ce
= $(o
.containment
)[0];
856 var co
= $(o
.containment
).offset();
857 var over
= ($(ce
).css("overflow") != 'hidden');
860 co
.left
+ (parseInt($(ce
).css("borderLeftWidth"),10) || 0) + (parseInt($(ce
).css("paddingLeft"),10) || 0) - this.margins
.left
,
861 co
.top
+ (parseInt($(ce
).css("borderTopWidth"),10) || 0) + (parseInt($(ce
).css("paddingTop"),10) || 0) - this.margins
.top
,
862 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
,
863 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
869 _convertPositionTo: function(d
, pos
) {
871 if(!pos
) pos
= this.position
;
872 var mod
= d
== "absolute" ? 1 : -1;
873 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
);
877 pos
.top
// The absolute mouse position
878 + this.offset
.relative
.top
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
879 + this.offset
.parent
.top
* mod
// The offsetParent's offset without borders (offset + border)
880 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ) * mod
)
883 pos
.left
// The absolute mouse position
884 + this.offset
.relative
.left
* mod
// Only for relative positioned nodes: Relative offset from element to offset parent
885 + this.offset
.parent
.left
* mod
// The offsetParent's offset without borders (offset + border)
886 - ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ) * mod
)
892 _generatePosition: function(event
) {
894 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
);
896 // This is another very weird special case that only happens for relative elements:
897 // 1. If the css position is relative
898 // 2. and the scroll parent is the document or similar to the offset parent
899 // we have to refresh the relative offset during the scroll so there are no jumps
900 if(this.cssPosition
== 'relative' && !(this.scrollParent
[0] != document
&& this.scrollParent
[0] != this.offsetParent
[0])) {
901 this.offset
.relative
= this._getRelativeOffset();
904 var pageX
= event
.pageX
;
905 var pageY
= event
.pageY
;
908 * - Position constraining -
909 * Constrain the position to a mix of grid, containment.
912 if(this.originalPosition
) { //If we are not dragging yet, we won't check for options
914 if(this.containment
) {
915 if(event
.pageX
- this.offset
.click
.left
< this.containment
[0]) pageX
= this.containment
[0] + this.offset
.click
.left
;
916 if(event
.pageY
- this.offset
.click
.top
< this.containment
[1]) pageY
= this.containment
[1] + this.offset
.click
.top
;
917 if(event
.pageX
- this.offset
.click
.left
> this.containment
[2]) pageX
= this.containment
[2] + this.offset
.click
.left
;
918 if(event
.pageY
- this.offset
.click
.top
> this.containment
[3]) pageY
= this.containment
[3] + this.offset
.click
.top
;
922 var top
= this.originalPageY
+ Math
.round((pageY
- this.originalPageY
) / o
.grid
[1]) * o
.grid
[1];
923 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
;
925 var left
= this.originalPageX
+ Math
.round((pageX
- this.originalPageX
) / o
.grid
[0]) * o
.grid
[0];
926 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
;
933 pageY
// The absolute mouse position
934 - this.offset
.click
.top
// Click offset (relative to the element)
935 - this.offset
.relative
.top
// Only for relative positioned nodes: Relative offset from element to offset parent
936 - this.offset
.parent
.top
// The offsetParent's offset without borders (offset + border)
937 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollTop() : ( scrollIsRootNode
? 0 : scroll
.scrollTop() ) ))
940 pageX
// The absolute mouse position
941 - this.offset
.click
.left
// Click offset (relative to the element)
942 - this.offset
.relative
.left
// Only for relative positioned nodes: Relative offset from element to offset parent
943 - this.offset
.parent
.left
// The offsetParent's offset without borders (offset + border)
944 + ($.browser
.safari
&& this.cssPosition
== 'fixed' ? 0 : ( this.cssPosition
== 'fixed' ? -this.scrollParent
.scrollLeft() : scrollIsRootNode
? 0 : scroll
.scrollLeft() ))
950 _rearrange: function(event
, i
, a
, hardRefresh
) {
952 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
));
954 //Various things done here to improve the performance:
955 // 1. we create a setTimeout, that calls refreshPositions
956 // 2. on the instance, we have a counter variable, that get's higher after every append
957 // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
958 // 4. this lets only the last addition to the timeout stack through
959 this.counter
= this.counter
? ++this.counter
: 1;
960 var self
= this, counter
= this.counter
;
962 window
.setTimeout(function() {
963 if(counter
== self
.counter
) self
.refreshPositions(!hardRefresh
); //Precompute after each DOM insertion, NOT on mousemove
968 _clear: function(event
, noPropagation
) {
970 this.reverting
= false;
971 // We delay all events that have to be triggered to after the point where the placeholder has been removed and
972 // everything else normalized again
973 var delayedTriggers
= [], self
= this;
975 // We first have to update the dom position of the actual currentItem
976 // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
977 if(!this._noFinalSort
&& this.currentItem
[0].parentNode
) this.placeholder
.before(this.currentItem
);
978 this._noFinalSort
= null;
980 if(this.helper
[0] == this.currentItem
[0]) {
981 for(var i
in this._storedCSS
) {
982 if(this._storedCSS
[i
] == 'auto' || this._storedCSS
[i
] == 'static') this._storedCSS
[i
] = '';
984 this.currentItem
.css(this._storedCSS
).removeClass("ui-sortable-helper");
986 this.currentItem
.show();
989 if(this.fromOutside
&& !noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("receive", event
, this._uiHash(this.fromOutside
)); });
990 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
991 if(!$.ui
.contains(this.element
[0], this.currentItem
[0])) { //Node was moved out of the current element
992 if(!noPropagation
) delayedTriggers
.push(function(event
) { this._trigger("remove", event
, this._uiHash()); });
993 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
994 if($.ui
.contains(this.containers
[i
].element
[0], this.currentItem
[0]) && !noPropagation
) {
995 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("receive", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
996 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("update", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1001 //Post events to containers
1002 for (var i
= this.containers
.length
- 1; i
>= 0; i
--){
1003 if(!noPropagation
) delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("deactivate", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1004 if(this.containers
[i
].containerCache
.over
) {
1005 delayedTriggers
.push((function(c
) { return function(event
) { c
._trigger("out", event
, this._uiHash(this)); }; }).call(this, this.containers
[i
]));
1006 this.containers
[i
].containerCache
.over
= 0;
1010 //Do what was originally in plugins
1011 if(this._storedCursor
) $('body').css("cursor", this._storedCursor
); //Reset cursor
1012 if(this._storedOpacity
) this.helper
.css("opacity", this._storedOpacity
); //Reset opacity
1013 if(this._storedZIndex
) this.helper
.css("zIndex", this._storedZIndex
== 'auto' ? '' : this._storedZIndex
); //Reset z-index
1015 this.dragging
= false;
1016 if(this.cancelHelperRemoval
) {
1017 if(!noPropagation
) {
1018 this._trigger("beforeStop", event
, this._uiHash());
1019 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1020 this._trigger("stop", event
, this._uiHash());
1025 if(!noPropagation
) this._trigger("beforeStop", event
, this._uiHash());
1027 //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
1028 this.placeholder
[0].parentNode
.removeChild(this.placeholder
[0]);
1030 if(this.helper
[0] != this.currentItem
[0]) this.helper
.remove(); this.helper
= null;
1032 if(!noPropagation
) {
1033 for (var i
=0; i
< delayedTriggers
.length
; i
++) { delayedTriggers
[i
].call(this, event
); }; //Trigger all delayed events
1034 this._trigger("stop", event
, this._uiHash());
1037 this.fromOutside
= false;
1042 _trigger: function() {
1043 if ($.Widget
.prototype._trigger
.apply(this, arguments
) === false) {
1048 _uiHash: function(inst
) {
1049 var self
= inst
|| this;
1051 helper
: self
.helper
,
1052 placeholder
: self
.placeholder
|| $([]),
1053 position
: self
.position
,
1054 originalPosition
: self
.originalPosition
,
1055 offset
: self
.positionAbs
,
1056 item
: self
.currentItem
,
1057 sender
: inst
? inst
.element
: null
1063 $.extend($.ui
.sortable
, {