Created
October 20, 2015 08:49
-
-
Save Kimserey/2c01402fedd574c7d90a to your computer and use it in GitHub Desktop.
Drag and drop sortable with html5 sortable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace DragnDropUInNext | |
open WebSharper | |
open WebSharper.JavaScript | |
open WebSharper.JQuery | |
open WebSharper.UI.Next | |
open WebSharper.UI.Next.Client | |
open WebSharper.UI.Next.Html | |
open WebSharper.JQueryUI | |
[<JavaScript>] | |
module Client = | |
[<Direct "createSortable($listId, $onUpdate)">] | |
let sortable listId onUpdate = WebSharper.JavaScript.Pervasives.X<unit> | |
let list = ListModel.Create id [] | |
let Main = | |
let rvPre = Var.Create "" | |
[ | |
divAttr [attr.``class`` "container"] [ | |
ulAttr [attr.id "list"; attr.``class`` "list-group"] [ | |
liAttr [attr.``class`` "list-group-item"; Attr.Create "data-id" "Hello 1"] [text "Hello 1"] | |
liAttr [attr.``class`` "list-group-item"; Attr.Create "data-id" "Hello 2"] [text "Hello 2"] | |
liAttr [attr.``class`` "list-group-item"; Attr.Create "data-id" "Hello 3"] [text "Hello 3"] | |
liAttr [attr.``class`` "list-group-item"; Attr.Create "data-id" "Hello 4"] [text "Hello 4"] | |
] | |
] :> Doc | |
list.View | |
|> View.Map (Seq.map (fun d -> div [text d] :> Doc) >> Doc.Concat) | |
|> Doc.EmbedView | |
] | |
|> Doc.Concat | |
|> Doc.RunById "main" | |
sortable "#list" list.Set | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* HTML5 Sortable jQuery Plugin | |
* https://github.com/voidberg/html5sortable | |
* | |
* Original code copyright 2012 Ali Farhadi. | |
* This version is mantained by Alexandru Badiu <[email protected]> & Lukas Oppermann <[email protected]> | |
* | |
* | |
* 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); | |
}; | |
/* start-testing */ | |
sortable.__testing = { | |
// add internal methods here for testing purposes | |
_removeSortableEvents: _removeSortableEvents, | |
_removeItemEvents: _removeItemEvents, | |
_removeItemData: _removeItemData, | |
_removeSortableData: _removeSortableData, | |
_listsConnected: _listsConnected, | |
_getOptions: _getOptions, | |
_attachGhost: _attachGhost, | |
_addGhostPos: _addGhostPos, | |
_getGhost: _getGhost, | |
_makeGhost: _makeGhost | |
}; | |
module.exports = sortable; | |
/* end-testing */ | |
function createSortable(listId, onUpdate) { | |
$(listId) | |
.sortable({ | |
placeholderClass: "fade", | |
forcePlaceholderSize: true | |
}) | |
.bind('sortupdate', function (e, ui) { | |
/* Will retrieve value in data-id attr */ | |
var newList = | |
$(listId).children().map(function () { | |
return $(this).data("id") | |
}).get(); | |
onUpdate(newList); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment