;(function(root, factory) { if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else if (typeof exports === 'object') { module.exports = factory(require('jquery')); } else { root.sortable = factory(root.jQuery); } }(this, function($) { /* * HTML5 Sortable jQuery Plugin * https://github.com/voidberg/html5sortable * * Original code copyright 2012 Ali Farhadi. * This version is mantained by Alexandru Badiu & Lukas Oppermann * * * Released under the MIT license. */ 'use strict'; /* * variables global to the plugin */ var dragging; var draggingHeight; var placeholders = $(); var sortables = []; /* * remove event handlers from items * @param [jquery Collection] items * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event) */ var _removeItemEvents = function(items) { items.off('dragstart.h5s'); items.off('dragend.h5s'); items.off('selectstart.h5s'); items.off('dragover.h5s'); items.off('dragenter.h5s'); items.off('drop.h5s'); }; /* * remove event handlers from sortable * @param [jquery Collection] sortable * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event) */ var _removeSortableEvents = function(sortable) { sortable.off('dragover.h5s'); sortable.off('dragenter.h5s'); sortable.off('drop.h5s'); }; /* * attache ghost to dataTransfer object * @param [event] original event * @param [object] ghost-object with item, x and y coordinates */ var _attachGhost = function(event, ghost) { // this needs to be set for HTML5 drag & drop to work event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text', ''); // check if setDragImage method is available if (event.dataTransfer.setDragImage) { event.dataTransfer.setDragImage(ghost.item, ghost.x, ghost.y); } }; /** * _addGhostPos clones the dragged item and adds it as a Ghost item * @param [object] event - the event fired when dragstart is triggered * @param [object] ghost - .item = node, draggedItem = jQuery collection */ var _addGhostPos = function(e, ghost) { if (!ghost.x) { ghost.x = parseInt(e.pageX - ghost.draggedItem.offset().left); } if (!ghost.y) { ghost.y = parseInt(e.pageY - ghost.draggedItem.offset().top); } return ghost; }; /** * _makeGhost decides which way to make a ghost and passes it to attachGhost * @param [jQuery selection] $draggedItem - the item that the user drags */ var _makeGhost = function($draggedItem) { return { item: $draggedItem[0], draggedItem: $draggedItem }; }; /** * _getGhost constructs ghost and attaches it to dataTransfer * @param [event] event - the original drag event object * @param [jQuery selection] $draggedItem - the item that the user drags * @param [object] ghostOpt - the ghost options */ // TODO: could $draggedItem be replaced by event.target in all instances var _getGhost = function(event, $draggedItem) { // add ghost item & draggedItem to ghost object var ghost = _makeGhost($draggedItem); // attach ghost position ghost = _addGhostPos(event, ghost); // attach ghost to dataTransfer _attachGhost(event, ghost); }; /* * return options if not set on sortable already * @param [object] soptions * @param [object] options */ var _getOptions = function(soptions, options) { if (typeof soptions === 'undefined') { return options; } return soptions; }; /* * remove data from sortable * @param [jquery Collection] a single sortable */ var _removeSortableData = function(sortable) { sortable.removeData('opts'); sortable.removeData('connectWith'); sortable.removeData('items'); sortable.removeAttr('aria-dropeffect'); }; /* * remove data from items * @param [jquery Collection] items */ var _removeItemData = function(items) { items.removeAttr('aria-grabbed'); items.removeAttr('draggable'); items.removeAttr('role'); }; /* * check if two lists are connected * @param [jquery Collection] items */ var _listsConnected = function(curList, destList) { if (curList[0] === destList[0]) { return true; } if (curList.data('connectWith') !== undefined) { return curList.data('connectWith') === destList.data('connectWith'); } return false; }; /* * destroy the sortable * @param [jquery Collection] a single sortable */ var _destroySortable = function(sortable) { var opts = sortable.data('opts') || {}; var items = sortable.children(opts.items); var handles = opts.handle ? items.find(opts.handle) : items; // remove event handlers & data from sortable _removeSortableEvents(sortable); _removeSortableData(sortable); // remove event handlers & data from items handles.off('mousedown.h5s'); _removeItemEvents(items); _removeItemData(items); }; /* * enable the sortable * @param [jquery Collection] a single sortable */ var _enableSortable = function(sortable) { var opts = sortable.data('opts'); var items = sortable.children(opts.items); var handles = opts.handle ? items.find(opts.handle) : items; sortable.attr('aria-dropeffect', 'move'); handles.attr('draggable', 'true'); // IE FIX for ghost // can be disabled as it has the side effect that other events // (e.g. click) will be ignored var spanEl = (document || window.document).createElement('span'); if (typeof spanEl.dragDrop === 'function' && !opts.disableIEFix) { handles.on('mousedown.h5s', function() { if (items.index(this) !== -1) { this.dragDrop(); } else { $(this).parents(opts.items)[0].dragDrop(); } }); } }; /* * disable the sortable * @param [jquery Collection] a single sortable */ var _disableSortable = function(sortable) { var opts = sortable.data('opts'); var items = sortable.children(opts.items); var handles = opts.handle ? items.find(opts.handle) : items; sortable.attr('aria-dropeffect', 'none'); handles.attr('draggable', false); handles.off('mousedown.h5s'); }; /* * reload the sortable * @param [jquery Collection] a single sortable * @description events need to be removed to not be double bound */ var _reloadSortable = function(sortable) { var opts = sortable.data('opts'); var items = sortable.children(opts.items); var handles = opts.handle ? items.find(opts.handle) : items; // remove event handlers from items _removeItemEvents(items); handles.off('mousedown.h5s'); // remove event handlers from sortable _removeSortableEvents(sortable); }; /* * public sortable object * @param [object|string] options|method */ var sortable = function(selector, options) { var $sortables = $(selector); var method = String(options); options = $.extend({ connectWith: false, placeholder: null, // dragImage can be null or a jQuery element dragImage: null, disableIEFix: false, placeholderClass: 'sortable-placeholder', draggingClass: 'sortable-dragging', hoverClass: false }, options); /* TODO: maxstatements should be 25, fix and remove line below */ /*jshint maxstatements:false */ return $sortables.each(function() { var $sortable = $(this); if (/enable|disable|destroy/.test(method)) { sortable[method]($sortable); return; } // get options & set options on sortable options = _getOptions($sortable.data('opts'), options); $sortable.data('opts', options); // reset sortable _reloadSortable($sortable); // initialize var items = $sortable.children(options.items); var index; var startParent; var newParent; var placeholder = (options.placeholder === null) ? $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="' + options.placeholderClass + '"/>') : $(options.placeholder).addClass(options.placeholderClass); // setup sortable ids if (!$sortable.attr('data-sortable-id')) { var id = sortables.length; sortables[id] = $sortable; $sortable.attr('data-sortable-id', id); items.attr('data-item-sortable-id', id); } $sortable.data('items', options.items); placeholders = placeholders.add(placeholder); if (options.connectWith) { $sortable.data('connectWith', options.connectWith); } _enableSortable($sortable); items.attr('role', 'option'); items.attr('aria-grabbed', 'false'); // Mouse over class if (options.hoverClass) { var hoverClass = 'sortable-over'; if (typeof options.hoverClass === 'string') { hoverClass = options.hoverClass; } items.hover(function() { $(this).addClass(hoverClass); }, function() { $(this).removeClass(hoverClass); }); } // Handle drag events on draggable items items.on('dragstart.h5s', function(e) { e.stopImmediatePropagation(); if (options.dragImage) { _attachGhost(e.originalEvent, { item: options.dragImage, x: 0, y: 0 }); console.log('WARNING: dragImage option is deprecated' + ' and will be removed in the future!'); } else { // add transparent clone or other ghost to cursor _getGhost(e.originalEvent, $(this), options.dragImage); } // cache selsection & add attr for dragging dragging = $(this); dragging.addClass(options.draggingClass); dragging.attr('aria-grabbed', 'true'); // grab values index = dragging.index(); draggingHeight = dragging.height(); startParent = $(this).parent(); // trigger sortstar update dragging.parent().triggerHandler('sortstart', { item: dragging, placeholder: placeholder, startparent: startParent }); }); // Handle drag events on draggable items items.on('dragend.h5s', function() { if (!dragging) { return; } // remove dragging attributes and show item dragging.removeClass(options.draggingClass); dragging.attr('aria-grabbed', 'false'); dragging.show(); placeholders.detach(); newParent = $(this).parent(); dragging.parent().triggerHandler('sortstop', { item: dragging, startparent: startParent, }); if (index !== dragging.index() || startParent.get(0) !== newParent.get(0)) { dragging.parent().triggerHandler('sortupdate', { item: dragging, index: newParent.children(newParent.data('items')).index(dragging), oldindex: items.index(dragging), elementIndex: dragging.index(), oldElementIndex: index, startparent: startParent, endparent: newParent }); } dragging = null; draggingHeight = null; }); // Handle drop event on sortable & placeholder // TODO: REMOVE placeholder????? $(this).add([placeholder]).on('drop.h5s', function(e) { if (!_listsConnected($sortable, $(dragging).parent())) { return; } e.stopPropagation(); placeholders.filter(':visible').after(dragging); dragging.trigger('dragend.h5s'); return false; }); // Handle dragover and dragenter events on draggable items items.add([this]).on('dragover.h5s dragenter.h5s', function(e) { if (!_listsConnected($sortable, $(dragging).parent())) { return; } e.preventDefault(); e.originalEvent.dataTransfer.dropEffect = 'move'; if (items.is(this)) { var thisHeight = $(this).height(); if (options.forcePlaceholderSize) { placeholder.height(draggingHeight); } // Check if $(this) is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering if (thisHeight > draggingHeight) { // Dead zone? var deadZone = thisHeight - draggingHeight; var offsetTop = $(this).offset().top; if (placeholder.index() < $(this).index() && e.originalEvent.pageY < offsetTop + deadZone) { return false; } if (placeholder.index() > $(this).index() && e.originalEvent.pageY > offsetTop + thisHeight - deadZone) { return false; } } dragging.hide(); if (placeholder.index() < $(this).index()) { $(this).after(placeholder); } else { $(this).before(placeholder); } placeholders.not(placeholder).detach(); } else { if (!placeholders.is(this) && !$(this).children(options.items).length) { placeholders.detach(); $(this).append(placeholder); } } return false; }); }); }; sortable.destroy = function(sortable) { _destroySortable(sortable); }; sortable.enable = function(sortable) { _enableSortable(sortable); }; sortable.disable = function(sortable) { _disableSortable(sortable); }; $.fn.sortable = function(options) { return sortable(this, options); }; return sortable; }));