/*! * jQuery UI Sortable @VERSION * http://jqueryui.com * * Copyright 2013 jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/sortable/ * * Depends: * jquery.ui.core.js * jquery.ui.mouse.js * jquery.ui.widget.js */ (function( $, undefined ) { /*jshint loopfunc: true */ function isOverAxis( x, reference, size ) { return ( x > reference ) && ( x < ( reference + size ) ); } function isFloating(item) { return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); } $.widget("ui.sortable", $.ui.mouse, { version: "@VERSION", widgetEventPrefix: "sort", ready: false, options: { appendTo: "parent", axis: false, connectWith: false, containment: false, cursor: "auto", cursorAt: false, dropOnEmpty: true, forcePlaceholderSize: false, forceHelperSize: false, grid: false, handle: false, helper: "original", items: "> *", opacity: false, placeholder: false, revert: false, scroll: true, scrollSensitivity: 20, scrollSpeed: 20, scope: "default", tolerance: "intersect", zIndex: 1000, // callbacks activate: null, beforeStop: null, change: null, deactivate: null, out: null, over: null, receive: null, remove: null, sort: null, start: null, stop: null, update: null }, _create: function() { var o = this.options; this.containerCache = {}; this.element.addClass("ui-sortable"); //Get the items this.refresh(); //Let's determine if the items are being displayed horizontally this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; //Let's determine the parent's offset this.offset = this.element.offset(); //Initialize mouse events for interaction this._mouseInit(); //We're ready to go this.ready = true; }, _destroy: function() { this.element .removeClass("ui-sortable ui-sortable-disabled"); this._mouseDestroy(); for ( var i = this.items.length - 1; i >= 0; i-- ) { this.items[i].item.removeData(this.widgetName + "-item"); } return this; }, _mouseCapture: function(event, overrideHandle) { var currentItem = null, validHandle = false, that = this; if (this.reverting) { return false; } if(this.options.disabled || this.options.type === "static") { return false; } //We have to refresh the items data once first this._refreshItems(event); //Find out if the clicked node (or one of its parents) is a actual item in this.items $(event.target).parents().each(function() { if($.data(this, that.widgetName + "-item") === that) { currentItem = $(this); return false; } }); if($.data(event.target, that.widgetName + "-item") === that) { currentItem = $(event.target); } if(!currentItem) { return false; } if(this.options.handle && !overrideHandle) { $(this.options.handle, currentItem).find("*").addBack().each(function() { if(this === event.target) { validHandle = true; } }); if(!validHandle) { return false; } } this.currentItem = currentItem; this._removeCurrentsFromItems(); return true; }, _mouseStart: function(event, overrideHandle, noActivation) { var i, body, o = this.options; this.currentContainer = this; //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture this.refreshPositions(); //Create and append the visible helper this.helper = this._createHelper(event); //Cache the helper size this._cacheHelperProportions(); /* * - Position generation - * This block generates everything position related - it's the core of draggables. */ //Cache the margins of the original element this._cacheMargins(); //Get the next scrolling parent this.scrollParent = this.helper.scrollParent(); //The element's absolute position on the page minus margins this.offset = this.currentItem.offset(); this.offset = { top: this.offset.top - this.margins.top, left: this.offset.left - this.margins.left }; $.extend(this.offset, { click: { //Where the click happened, relative to the element left: event.pageX - this.offset.left, top: event.pageY - this.offset.top }, parent: this._getParentOffset(), relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper }); // Only after we got the offset, we can change the helper's position to absolute // TODO: Still need to figure out a way to make relative sorting possible this.helper.css("position", "absolute"); this.cssPosition = this.helper.css("position"); //Generate the original position this.originalPosition = this._generatePosition(event); this.originalPageX = event.pageX; this.originalPageY = event.pageY; //Adjust the mouse offset relative to the helper if "cursorAt" is supplied (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); //Cache the former DOM position this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; //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 if(this.helper[0] !== this.currentItem[0]) { this.currentItem.hide(); } //Create the placeholder this._createPlaceholder(); //Set a containment if given in the options if(o.containment) { this._setContainment(); } if( o.cursor && o.cursor !== "auto" ) { // cursor option body = this.document.find( "body" ); // support: IE this.storedCursor = body.css( "cursor" ); body.css( "cursor", o.cursor ); this.storedStylesheet = $( "" ).appendTo( body ); } if(o.opacity) { // opacity option if (this.helper.css("opacity")) { this._storedOpacity = this.helper.css("opacity"); } this.helper.css("opacity", o.opacity); } if(o.zIndex) { // zIndex option if (this.helper.css("zIndex")) { this._storedZIndex = this.helper.css("zIndex"); } this.helper.css("zIndex", o.zIndex); } //Prepare scrolling if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { this.overflowOffset = this.scrollParent.offset(); } //Call callbacks this._trigger("start", event, this._uiHash()); //Recache the helper size if(!this._preserveHelperProportions) { this._cacheHelperProportions(); } //Post "activate" events to possible containers if( !noActivation ) { for ( i = this.containers.length - 1; i >= 0; i-- ) { this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); } } //Prepare possible droppables if($.ui.ddmanager) { $.ui.ddmanager.current = this; } if ($.ui.ddmanager && !o.dropBehaviour) { $.ui.ddmanager.prepareOffsets(this, event); } this.dragging = true; this.helper.addClass("ui-sortable-helper"); this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position return true; }, _mouseDrag: function(event) { var i, item, itemElement, intersection, o = this.options, scrolled = false; //Compute the helpers position this.position = this._generatePosition(event); this.positionAbs = this._convertPositionTo("absolute"); if (!this.lastPositionAbs) { this.lastPositionAbs = this.positionAbs; } //Do scrolling if(this.options.scroll) { if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; } if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; } } else { if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); } if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); } } if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { $.ui.ddmanager.prepareOffsets(this, event); } } //Regenerate the absolute position used for position checks this.positionAbs = this._convertPositionTo("absolute"); //Set the helper position if(!this.options.axis || this.options.axis !== "y") { this.helper[0].style.left = this.position.left+"px"; } if(!this.options.axis || this.options.axis !== "x") { this.helper[0].style.top = this.position.top+"px"; } //Rearrange for (i = this.items.length - 1; i >= 0; i--) { //Cache variables and intersection, continue if no intersection item = this.items[i]; itemElement = item.item[0]; intersection = this._intersectsWithPointer(item); if (!intersection) { continue; } // Only put the placeholder inside the current Container, skip all // items form other containers. This works because when moving // an item from one container to another the // currentContainer is switched before the placeholder is moved. // // Without this moving items in "sub-sortables" can cause the placeholder to jitter // beetween the outer and inner container. if (item.instance !== this.currentContainer) { continue; } // cannot intersect with itself // no useless actions that have been done before // no action if the item moved is the parent of the item checked if (itemElement !== this.currentItem[0] && this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && !$.contains(this.placeholder[0], itemElement) && (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) ) { this.direction = intersection === 1 ? "down" : "up"; if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { this._rearrange(event, item); } else { break; } this._trigger("change", event, this._uiHash()); break; } } //Post events to containers this._contactContainers(event); //Interconnect with droppables if($.ui.ddmanager) { $.ui.ddmanager.drag(this, event); } //Call callbacks this._trigger("sort", event, this._uiHash()); this.lastPositionAbs = this.positionAbs; return false; }, _mouseStop: function(event, noPropagation) { if(!event) { return; } //If we are using droppables, inform the manager about the drop if ($.ui.ddmanager && !this.options.dropBehaviour) { $.ui.ddmanager.drop(this, event); } if(this.options.revert) { var that = this, cur = this.placeholder.offset(), axis = this.options.axis, animation = {}; if ( !axis || axis === "x" ) { animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); } if ( !axis || axis === "y" ) { animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); } this.reverting = true; $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { that._clear(event); }); } else { this._clear(event, noPropagation); } return false; }, cancel: function() { if(this.dragging) { this._mouseUp({ target: null }); if(this.options.helper === "original") { this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); } else { this.currentItem.show(); } //Post deactivating events to containers for (var i = this.containers.length - 1; i >= 0; i--){ this.containers[i]._trigger("deactivate", null, this._uiHash(this)); if(this.containers[i].containerCache.over) { this.containers[i]._trigger("out", null, this._uiHash(this)); this.containers[i].containerCache.over = 0; } } } if (this.placeholder) { //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! if(this.placeholder[0].parentNode) { this.placeholder[0].parentNode.removeChild(this.placeholder[0]); } if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { this.helper.remove(); } $.extend(this, { helper: null, dragging: false, reverting: false, _noFinalSort: null }); if(this.domPosition.prev) { $(this.domPosition.prev).after(this.currentItem); } else { $(this.domPosition.parent).prepend(this.currentItem); } } return this; }, serialize: function(o) { var items = this._getItemsAsjQuery(o && o.connected), str = []; o = o || {}; $(items).each(function() { var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); if (res) { str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); } }); if(!str.length && o.key) { str.push(o.key + "="); } return str.join("&"); }, toArray: function(o) { var items = this._getItemsAsjQuery(o && o.connected), ret = []; o = o || {}; items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); return ret; }, /* Be careful with the following core functions */ _intersectsWith: function(item) { var x1 = this.positionAbs.left, x2 = x1 + this.helperProportions.width, y1 = this.positionAbs.top, y2 = y1 + this.helperProportions.height, l = item.left, r = l + item.width, t = item.top, b = t + item.height, dyClick = this.offset.click.top, dxClick = this.offset.click.left, isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), isOverElement = isOverElementHeight && isOverElementWidth; if ( this.options.tolerance === "pointer" || this.options.forcePointerForContainers || (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) ) { return isOverElement; } else { return (l < x1 + (this.helperProportions.width / 2) && // Right Half x2 - (this.helperProportions.width / 2) < r && // Left Half t < y1 + (this.helperProportions.height / 2) && // Bottom Half y2 - (this.helperProportions.height / 2) < b ); // Top Half } }, _intersectsWithPointer: function(item) { var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), isOverElement = isOverElementHeight && isOverElementWidth, verticalDirection = this._getDragVerticalDirection(), horizontalDirection = this._getDragHorizontalDirection(); if (!isOverElement) { return false; } return this.floating ? ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); }, _intersectsWithSides: function(item) { var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), verticalDirection = this._getDragVerticalDirection(), horizontalDirection = this._getDragHorizontalDirection(); if (this.floating && horizontalDirection) { return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); } else { return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); } }, _getDragVerticalDirection: function() { var delta = this.positionAbs.top - this.lastPositionAbs.top; return delta !== 0 && (delta > 0 ? "down" : "up"); }, _getDragHorizontalDirection: function() { var delta = this.positionAbs.left - this.lastPositionAbs.left; return delta !== 0 && (delta > 0 ? "right" : "left"); }, refresh: function(event) { this._refreshItems(event); this.refreshPositions(); return this; }, _connectWith: function() { var options = this.options; return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; }, _getItemsAsjQuery: function(connected) { var i, j, cur, inst, items = [], queries = [], connectWith = this._connectWith(); if(connectWith && connected) { for (i = connectWith.length - 1; i >= 0; i--){ cur = $(connectWith[i]); for ( j = cur.length - 1; j >= 0; j--){ inst = $.data(cur[j], this.widgetFullName); if(inst && inst !== this && !inst.options.disabled) { 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]); } } } } 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]); for (i = queries.length - 1; i >= 0; i--){ queries[i][0].each(function() { items.push(this); }); } return $(items); }, _removeCurrentsFromItems: function() { var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); this.items = $.grep(this.items, function (item) { for (var j=0; j < list.length; j++) { if(list[j] === item.item[0]) { return false; } } return true; }); }, _refreshItems: function(event) { this.items = []; this.containers = [this]; var i, j, cur, inst, targetData, _queries, item, queriesLength, items = this.items, queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], connectWith = this._connectWith(); if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down for (i = connectWith.length - 1; i >= 0; i--){ cur = $(connectWith[i]); for (j = cur.length - 1; j >= 0; j--){ inst = $.data(cur[j], this.widgetFullName); if(inst && inst !== this && !inst.options.disabled) { queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); this.containers.push(inst); } } } } for (i = queries.length - 1; i >= 0; i--) { targetData = queries[i][1]; _queries = queries[i][0]; for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { item = $(_queries[j]); item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) items.push({ item: item, instance: targetData, width: 0, height: 0, left: 0, top: 0 }); } } }, refreshPositions: function(fast) { //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change if(this.offsetParent && this.helper) { this.offset.parent = this._getParentOffset(); } var i, item, t, p; for (i = this.items.length - 1; i >= 0; i--){ item = this.items[i]; //We ignore calculating positions of all connected containers when we're not over them if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { continue; } t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; if (!fast) { item.width = t.outerWidth(); item.height = t.outerHeight(); } p = t.offset(); item.left = p.left; item.top = p.top; } if(this.options.custom && this.options.custom.refreshContainers) { this.options.custom.refreshContainers.call(this); } else { for (i = this.containers.length - 1; i >= 0; i--){ p = this.containers[i].element.offset(); this.containers[i].containerCache.left = p.left; this.containers[i].containerCache.top = p.top; this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); } } return this; }, _createPlaceholder: function(that) { that = that || this; var className, o = that.options; if(!o.placeholder || o.placeholder.constructor === String) { className = o.placeholder; o.placeholder = { element: function() { var nodeName = that.currentItem[0].nodeName.toLowerCase(), element = $( that.document[0].createElement( nodeName ) ) .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") .removeClass("ui-sortable-helper"); if ( nodeName === "tr" ) { // Use a high colspan to force the td to expand the full // width of the table (browsers are smart enough to // handle this properly) element.append( "