Skip to content

Instantly share code, notes, and snippets.

@Kimserey
Created October 20, 2015 08:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kimserey/2c01402fedd574c7d90a to your computer and use it in GitHub Desktop.
Save Kimserey/2c01402fedd574c7d90a to your computer and use it in GitHub Desktop.
Drag and drop sortable with html5 sortable
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
/*
* HTML5 Sortable jQuery Plugin
* https://github.com/voidberg/html5sortable
*
* Original code copyright 2012 Ali Farhadi.
* This version is mantained by Alexandru Badiu <andu@ctrlz.ro> & Lukas Oppermann <lukas@vea.re>
*
*
* 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