"Drag and drop images with serialize"
Bootstrap 3.3.0 Snippet by mosky7

<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <!------ Include the above in your HEAD tag ----------> <div class="container"> <div class="row"> <div class="col-md-12"> <h1>Drag and drop images...</h1> <div class="drag"> <div class='col-md-3'> <img rel="1" src="http://lorempixel.com/200/200/city/1" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="2" src="http://lorempixel.com/200/200/city/2" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="3" src="http://lorempixel.com/200/200/city/3" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="1" src="http://lorempixel.com/200/200/city/1" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="2" src="http://lorempixel.com/200/200/city/2" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="3" src="http://lorempixel.com/200/200/city/3" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="1" src="http://lorempixel.com/200/200/city/1" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="2" src="http://lorempixel.com/200/200/city/2" class="img-thumbnail"> </div> <div class='col-md-3'> <img rel="3" src="http://lorempixel.com/200/200/city/3" class="img-thumbnail"> </div> </div> <p id="output">1,2,3</p> </div> </div> </div>
body.dragging, body.dragging * { cursor: move !important; } div.drag img{ margin: 5px; -moz-transition:width 0.3s, height 0.3s; -webkit-transition:width 0.3s, height 0.3s; -o-transition:width 0.3s, height 0.3s; transition:width 0.3s, height 0.3s; } .dragged { position: absolute; opacity: 0.5; z-index: 2000; } div.drag .placeholder:before { position: absolute; } div.drag .placeholder{ vertical-align: middle; display: inline-block; background-color: #CCC; width: 200px; height: 200px; position: relative; border: 2px dashed #000; line-height: 1.428571429; background-color: #fff; border-radius: 4px; padding: 4px; }
!function ( $, window, pluginName, undefined){ var eventNames, containerDefaults = { // If true, items can be dragged from this container drag: true, // If true, items can be droped onto this container drop: true, // Exclude items from being draggable, if the // selector matches the item exclude: "", // If true, search for nested containers within an item nested: true, // If true, the items are assumed to be arranged vertically vertical: true }, // end container defaults groupDefaults = { // This is executed after the placeholder has been moved. // $closestItemOrContainer contains the closest item, the placeholder // has been put at or the closest empty Container, the placeholder has // been appended to. afterMove: function ($placeholder, container, $closestItemOrContainer) { }, // The exact css path between the container and its items, e.g. "> tbody" containerPath: "", // The css selector of the containers containerSelector: "ol, ul", // Distance the mouse has to travel to start dragging distance: 0, // Time in milliseconds after mousedown until dragging should start. // This option can be used to prevent unwanted drags when clicking on an element. delay: 0, // The css selector of the drag handle handle: "", // The exact css path between the item and its subcontainers itemPath: "", // The css selector of the items itemSelector: "li", // Check if the dragged item may be inside the container. // Use with care, since the search for a valid container entails a depth first search // and may be quite expensive. isValidTarget: function ($item, container) { return true }, // Executed before onDrop if placeholder is detached. // This happens if pullPlaceholder is set to false and the drop occurs outside a container. onCancel: function ($item, container, _super, event) { }, // Executed at the beginning of a mouse move event. // The Placeholder has not been moved yet. onDrag: function ($item, position, _super, event) { $item.css(position) }, // Called after the drag has been started, // that is the mouse button is beeing held down and // the mouse is moving. // The container is the closest initialized container. // Therefore it might not be the container, that actually contains the item. onDragStart: function ($item, container, _super, event) { $item.css({ height: $item.height(), width: $item.width() }) $item.addClass("dragged") $("body").addClass("dragging") }, // Called when the mouse button is beeing released onDrop: function ($item, container, _super, event) { $item.removeClass("dragged").removeAttr("style") $("body").removeClass("dragging") }, // Called on mousedown. If falsy value is returned, the dragging will not start. // If clicked on input element, ignore onMousedown: function ($item, _super, event) { if (!event.target.nodeName.match(/^(input|select)$/i)) { event.preventDefault() return true } }, // Template for the placeholder. Can be any valid jQuery input // e.g. a string, a DOM element. // The placeholder must have the class "placeholder" placeholder: '<li class="placeholder"/>', // If true, the position of the placeholder is calculated on every mousemove. // If false, it is only calculated when the mouse is above a container. pullPlaceholder: true, // Specifies serialization of the container group. // The pair $parent/$children is either container/items or item/subcontainers. serialize: function ($parent, $children, parentIsContainer) { var result = $.extend({}, $parent.data()) if(parentIsContainer) return [$children] else if ($children[0]){ result.children = $children } delete result.subContainers delete result.sortable return result }, // Set tolerance while dragging. Positive values decrease sensitivity, // negative values increase it. tolerance: 0 }, // end group defaults containerGroups = {}, groupCounter = 0, emptyBox = { left: 0, top: 0, bottom: 0, right:0 }, eventNames = { start: "touchstart.sortable mousedown.sortable", drop: "touchend.sortable touchcancel.sortable mouseup.sortable", drag: "touchmove.sortable mousemove.sortable", scroll: "scroll.sortable" }, subContainerKey = "subContainers" /* * a is Array [left, right, top, bottom] * b is array [left, top] */ function d(a,b) { var x = Math.max(0, a[0] - b[0], b[0] - a[1]), y = Math.max(0, a[2] - b[1], b[1] - a[3]) return x+y; } function setDimensions(array, dimensions, tolerance, useOffset) { var i = array.length, offsetMethod = useOffset ? "offset" : "position" tolerance = tolerance || 0 while(i--){ var el = array[i].el ? array[i].el : $(array[i]), // use fitting method pos = el[offsetMethod]() pos.left += parseInt(el.css('margin-left'), 10) pos.top += parseInt(el.css('margin-top'),10) dimensions[i] = [ pos.left - tolerance, pos.left + el.outerWidth() + tolerance, pos.top - tolerance, pos.top + el.outerHeight() + tolerance ] } } function getRelativePosition(pointer, element) { var offset = element.offset() return { left: pointer.left - offset.left, top: pointer.top - offset.top } } function sortByDistanceDesc(dimensions, pointer, lastPointer) { pointer = [pointer.left, pointer.top] lastPointer = lastPointer && [lastPointer.left, lastPointer.top] var dim, i = dimensions.length, distances = [] while(i--){ dim = dimensions[i] distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)] } distances = distances.sort(function (a,b) { return b[1] - a[1] || b[2] - a[2] || b[0] - a[0] }) // last entry is the closest return distances } function ContainerGroup(options) { this.options = $.extend({}, groupDefaults, options) this.containers = [] if(!this.options.rootGroup){ this.scrollProxy = $.proxy(this.scroll, this) this.dragProxy = $.proxy(this.drag, this) this.dropProxy = $.proxy(this.drop, this) this.placeholder = $(this.options.placeholder) if(!options.isValidTarget) this.options.isValidTarget = undefined } } ContainerGroup.get = function (options) { if(!containerGroups[options.group]) { if(options.group === undefined) options.group = groupCounter ++ containerGroups[options.group] = new ContainerGroup(options) } return containerGroups[options.group] } ContainerGroup.prototype = { dragInit: function (e, itemContainer) { this.$document = $(itemContainer.el[0].ownerDocument) // get item to drag this.item = $(e.target).closest(this.options.itemSelector) this.itemContainer = itemContainer if(this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)){ return } this.setPointer(e) this.toggleListeners('on') this.setupDelayTimer() this.dragInitDone = true }, drag: function (e) { if(!this.dragging){ if(!this.distanceMet(e) || !this.delayMet) return this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e) this.item.before(this.placeholder) this.dragging = true } this.setPointer(e) // place item under the cursor this.options.onDrag(this.item, getRelativePosition(this.pointer, this.item.offsetParent()), groupDefaults.onDrag, e) var x = e.pageX || e.originalEvent.pageX, y = e.pageY || e.originalEvent.pageY, box = this.sameResultBox, t = this.options.tolerance if(!box || box.top - t > y || box.bottom + t < y || box.left - t > x || box.right + t < x) if(!this.searchValidTarget()) this.placeholder.detach() }, drop: function (e) { this.toggleListeners('off') this.dragInitDone = false if(this.dragging){ // processing Drop, check if placeholder is detached if(this.placeholder.closest("html")[0]) this.placeholder.before(this.item).detach() else this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e) this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e) // cleanup this.clearDimensions() this.clearOffsetParent() this.lastAppendedItem = this.sameResultBox = undefined this.dragging = false } }, searchValidTarget: function (pointer, lastPointer) { if(!pointer){ pointer = this.relativePointer || this.pointer lastPointer = this.lastRelativePointer || this.lastPointer } var distances = sortByDistanceDesc(this.getContainerDimensions(), pointer, lastPointer), i = distances.length while(i--){ var index = distances[i][0], distance = distances[i][1] if(!distance || this.options.pullPlaceholder){ var container = this.containers[index] if(!container.disabled){ if(!this.$getOffsetParent()){ var offsetParent = container.getItemOffsetParent() pointer = getRelativePosition(pointer, offsetParent) lastPointer = getRelativePosition(lastPointer, offsetParent) } if(container.searchValidTarget(pointer, lastPointer)) return true } } } if(this.sameResultBox) this.sameResultBox = undefined }, movePlaceholder: function (container, item, method, sameResultBox) { var lastAppendedItem = this.lastAppendedItem if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0]) return; item[method](this.placeholder) this.lastAppendedItem = item this.sameResultBox = sameResultBox this.options.afterMove(this.placeholder, container, item) }, getContainerDimensions: function () { if(!this.containerDimensions) setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent()) return this.containerDimensions }, getContainer: function (element) { return element.closest(this.options.containerSelector).data(pluginName) }, $getOffsetParent: function () { if(this.offsetParent === undefined){ var i = this.containers.length - 1, offsetParent = this.containers[i].getItemOffsetParent() if(!this.options.rootGroup){ while(i--){ if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){ // If every container has the same offset parent, // use position() which is relative to this parent, // otherwise use offset() // compare #setDimensions offsetParent = false break; } } } this.offsetParent = offsetParent } return this.offsetParent }, setPointer: function (e) { var pointer = this.getPointer(e) if(this.$getOffsetParent()){ var relativePointer = getRelativePosition(pointer, this.$getOffsetParent()) this.lastRelativePointer = this.relativePointer this.relativePointer = relativePointer } this.lastPointer = this.pointer this.pointer = pointer }, distanceMet: function (e) { var currentPointer = this.getPointer(e) return (Math.max( Math.abs(this.pointer.left - currentPointer.left), Math.abs(this.pointer.top - currentPointer.top) ) >= this.options.distance) }, getPointer: function(e) { return { left: e.pageX || e.originalEvent.pageX, top: e.pageY || e.originalEvent.pageY } }, setupDelayTimer: function () { var that = this this.delayMet = !this.options.delay // init delay timer if needed if (!this.delayMet) { clearTimeout(this._mouseDelayTimer); this._mouseDelayTimer = setTimeout(function() { that.delayMet = true }, this.options.delay) } }, scroll: function (e) { this.clearDimensions() this.clearOffsetParent() // TODO is this needed? }, toggleListeners: function (method) { var that = this, events = ['drag','drop','scroll'] $.each(events,function (i,event) { that.$document[method](eventNames[event], that[event + 'Proxy']) }) }, clearOffsetParent: function () { this.offsetParent = undefined }, // Recursively clear container and item dimensions clearDimensions: function () { this.traverse(function(object){ object._clearDimensions() }) }, traverse: function(callback) { callback(this) var i = this.containers.length while(i--){ this.containers[i].traverse(callback) } }, _clearDimensions: function(){ this.containerDimensions = undefined }, _destroy: function () { containerGroups[this.options.group] = undefined } } function Container(element, options) { this.el = element this.options = $.extend( {}, containerDefaults, options) this.group = ContainerGroup.get(this.options) this.rootGroup = this.options.rootGroup || this.group this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector var itemPath = this.rootGroup.options.itemPath this.target = itemPath ? this.el.find(itemPath) : this.el this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this)) if(this.options.drop) this.group.containers.push(this) } Container.prototype = { dragInit: function (e) { var rootGroup = this.rootGroup if( !this.disabled && !rootGroup.dragInitDone && this.options.drag && this.isValidDrag(e)) { rootGroup.dragInit(e, this) } }, isValidDrag: function(e) { return e.which == 1 || e.type == "touchstart" && e.originalEvent.touches.length == 1 }, searchValidTarget: function (pointer, lastPointer) { var distances = sortByDistanceDesc(this.getItemDimensions(), pointer, lastPointer), i = distances.length, rootGroup = this.rootGroup, validTarget = !rootGroup.options.isValidTarget || rootGroup.options.isValidTarget(rootGroup.item, this) if(!i && validTarget){ rootGroup.movePlaceholder(this, this.target, "append") return true } else while(i--){ var index = distances[i][0], distance = distances[i][1] if(!distance && this.hasChildGroup(index)){ var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer) if(found) return true } else if(validTarget){ this.movePlaceholder(index, pointer) return true } } }, movePlaceholder: function (index, pointer) { var item = $(this.items[index]), dim = this.itemDimensions[index], method = "after", width = item.outerWidth(), height = item.outerHeight(), offset = item.offset(), sameResultBox = { left: offset.left, right: offset.left + width, top: offset.top, bottom: offset.top + height } if(this.options.vertical){ var yCenter = (dim[2] + dim[3]) / 2, inUpperHalf = pointer.top <= yCenter if(inUpperHalf){ method = "before" sameResultBox.bottom -= height / 2 } else sameResultBox.top += height / 2 } else { var xCenter = (dim[0] + dim[1]) / 2, inLeftHalf = pointer.left <= xCenter if(inLeftHalf){ method = "before" sameResultBox.right -= width / 2 } else sameResultBox.left += width / 2 } if(this.hasChildGroup(index)) sameResultBox = emptyBox this.rootGroup.movePlaceholder(this, item, method, sameResultBox) }, getItemDimensions: function () { if(!this.itemDimensions){ this.items = this.$getChildren(this.el, "item").filter(":not(.placeholder, .dragged)").get() setDimensions(this.items, this.itemDimensions = [], this.options.tolerance) } return this.itemDimensions }, getItemOffsetParent: function () { var offsetParent, el = this.el // Since el might be empty we have to check el itself and // can not do something like el.children().first().offsetParent() if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed") offsetParent = el else offsetParent = el.offsetParent() return offsetParent }, hasChildGroup: function (index) { return this.options.nested && this.getContainerGroup(index) }, getContainerGroup: function (index) { var childGroup = $.data(this.items[index], subContainerKey) if( childGroup === undefined){ var childContainers = this.$getChildren(this.items[index], "container") childGroup = false if(childContainers[0]){ var options = $.extend({}, this.options, { rootGroup: this.rootGroup, group: groupCounter ++ }) childGroup = childContainers[pluginName](options).data(pluginName).group } $.data(this.items[index], subContainerKey, childGroup) } return childGroup }, $getChildren: function (parent, type) { var options = this.rootGroup.options, path = options[type + "Path"], selector = options[type + "Selector"] parent = $(parent) if(path) parent = parent.find(path) return parent.children(selector) }, _serialize: function (parent, isContainer) { var that = this, childType = isContainer ? "item" : "container", children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () { return that._serialize($(this), !isContainer) }).get() return this.rootGroup.options.serialize(parent, children, isContainer) }, traverse: function(callback) { $.each(this.items || [], function(item){ var group = $.data(this, subContainerKey) if(group) group.traverse(callback) }); callback(this) }, _clearDimensions: function () { this.itemDimensions = undefined }, _destroy: function() { var that = this; this.target.off(eventNames.start, this.handle); this.el.removeData(pluginName) if(this.options.drop) this.group.containers = $.grep(this.group.containers, function(val){ return val != that }) $.each(this.items || [], function(){ $.removeData(this, subContainerKey) }) } } var API = { enable: function() { this.traverse(function(object){ object.disabled = false }) }, disable: function (){ this.traverse(function(object){ object.disabled = true }) }, serialize: function () { return this._serialize(this.el, true) }, refresh: function() { this.traverse(function(object){ object._clearDimensions() }) }, destroy: function () { this.traverse(function(object){ object._destroy(); }) } } $.extend(Container.prototype, API) /** * jQuery API * * Parameters are * either options on init * or a method name followed by arguments to pass to the method */ $.fn[pluginName] = function(methodOrOptions) { var args = Array.prototype.slice.call(arguments, 1) return this.map(function(){ var $t = $(this), object = $t.data(pluginName) if(object && API[methodOrOptions]) return API[methodOrOptions].apply(object, args) || this else if(!object && (methodOrOptions === undefined || typeof methodOrOptions === "object")) $t.data(pluginName, new Container($t, methodOrOptions)) return this }); }; }(jQuery, window, 'sortable'); $(document).ready(function() { var adjustment var group = $("div.drag").sortable({ group: 'drag', itemSelector: 'img', containerSelector: 'div', vertical: false, placeholder: '<div class="placeholder" />', pullPlaceholder: false, // set item relative to cursor position onDragStart: function ($item, container, _super) { var offset = $item.offset(), pointer = container.rootGroup.pointer adjustment = { left: pointer.left - offset.left, top: pointer.top - offset.top } _super($item, container) }, onDrop: function (item, container, _super) { console.log(group.sortable("serialize").get()); $('#output').text(group.sortable("serialize").get().join("\n")) _super(item, container) }, onDrag: function ($item, position) { $item.css({ width: 180, height: 180, left: position.left - adjustment.left, top: position.top - adjustment.top }) }, serialize: function (parent, children, isContainer) { return isContainer ? children.join() : parent.attr('rel') } }) });

Related: See More


Questions / Comments: