commit 6ace554483b9731b12b09f3c01c3859589c30c52 Author: RubaXa Date: Thu Dec 19 14:11:38 2013 +0400 + v0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..16d24b0 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,31 @@ +'use strict'; + +module.exports = function (grunt){ + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + version: { + src: '<%= pkg.exportName %>.js' + }, + + uglify: { + options: { + banner: '/*! <%= pkg.exportName %> <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n' + }, + dist: { + files: { + '<%= pkg.exportName %>.min.js': ['<%= pkg.exportName %>.js'] + } + } + } + }); + + + // These plugins provide necessary tasks. + grunt.loadNpmTasks('grunt-version'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + + + // Default task. + grunt.registerTask('default', ['version', 'uglify']); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8d8a1d --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Sortable + + +## Features +* Support touch devices +* Built using native HTML5 drag and drop API +* Simple API +* Lightweight, 2KB gzipped +* No jQuery + + +### Usage +```html + +``` + +```js +new Sortable(items); +``` + + +### Options +```js +new Sortable(elem, { + group: "name", + handle: ".my-handle", // Restricts sort start click/touch to the specified element + draggable: ".item", // Specifies which items inside the element should be sortable + ghostClass: "sortable-ghost", + + onAdd: function (evt){ + var itemEl = ui.detail; + }, + + onUpdate: function (evt){ + var itemEl = ui.detail; // the current dragged HTMLElement + }, + + onRemove: function (evt){ + var itemEl = ui.detail; + } +}); +``` diff --git a/Sortable.js b/Sortable.js new file mode 100644 index 0000000..dce64ad --- /dev/null +++ b/Sortable.js @@ -0,0 +1,474 @@ +/**! + * Sortable + * @author RubaXa + * @license MIT + */ + + +(function (factory){ + "use strict"; + + if( typeof define === "function" && define.amd ){ + define("Sortable", [], factory); + } + else { + window["Sortable"] = factory(); + } +})(function (){ + "use strict"; + + var + dragEl + , ghostEl + , rootEl + , nextEl + + , lastEl + , lastCSS + + , activeGroup + + , tapEvt + , touchEvt + + , expando = 'Sortable' + (new Date).getTime() + + , win = window + , document = win.document + , parseInt = win.parseInt + , Event = win.CustomEvent + + , noop = function (){} + , slice = [].slice + + , touchDragOverListeners = [] + ; + + + /** + * @class Sortable + * @param {HTMLElement} el + * @param {Object} [options] + * @constructor + */ + function Sortable(el, options){ + this.el = el; // root element + this.options = options = (options || {}); + + + // Defaults + options.group = options.group || Math.random(); + options.handle = options.handle || null; + options.draggable = options.draggable || el.children[0] && el.children[0].nodeName || 'li'; + options.ghostClass = options.ghostClass || 'sortable-ghost'; + + options.onAdd = _bind(this, options.onAdd || noop); + options.onUpdate = _bind(this, options.onUpdate || noop); + options.onRemove = _bind(this, options.onRemove || noop); + + + el[expando] = options.group; + + + // Bind all prevate methods + for( var fn in this ){ + if( fn.charAt(0) === '_' ){ + this[fn] = _bind(this, this[fn]); + } + } + + + // Bind events + _on(el, 'add', options.onAdd); + _on(el, 'update', options.onUpdate); + _on(el, 'remove', options.onRemove); + + _on(el, 'mousedown', this._onTapStart); + _on(el, 'touchstart', this._onTapStart); + + _on(el, 'dragover', this._onDragOver); + _on(el, 'dragenter', this._onDragOver); + + touchDragOverListeners.push(this._onDragOver); + } + + + Sortable.prototype = { + constructor: Sortable, + + + _applyEffects: function (){ + _toggleClass(dragEl, this.options.ghostClass, true); + }, + + + _onTapStart: function (evt/**TouchEvent*/){ + var + touch = evt.touches && evt.touches[0] + , target = (touch || evt).target + , options = this.options + ; + + if( options.handle ){ + target = _closest(target, options.handle, this.el); + } + + target = _closest(target, options.draggable, this.el); + + if( target && !dragEl ){ + tapEvt = evt; + target.draggable = true; + + // Disable "draggable" + _find(target, 'a', _disableDraggable); + _find(target, 'img', _disableDraggable); + + + if( touch ){ + // Touch device support + tapEvt = { + target: target + , clientX: touch.clientX + , clientY: touch.clientY + }; + this._onDragStart(tapEvt, true); + evt.preventDefault(); + } + + + _on(this.el, 'dragstart', this._onDragStart); + _on(document, 'dragover', _globalDragOver); + + + try { + if( document.selection ){ + document.selection.empty(); + } else { + window.getSelection().removeAllRanges() + } + } catch (err){ } + } + }, + + + _emulateDragOver: function (){ + if( touchEvt ){ + _css(ghostEl, 'display', 'none'); + + var + target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY) + , parent = target + , group = this.options.group + , i = touchDragOverListeners.length + ; + + do { + if( parent[expando] === group ){ + + while( i-- ){ + touchDragOverListeners[i]({ + clientX: touchEvt.clientX, + clientY: touchEvt.clientY, + target: target, + rootEl: parent + }); + } + break; + } + } + while( parent = parent.parentNode ); + + _css(ghostEl, 'display', ''); + } + }, + + + _onTouchMove: function (evt){ + if( tapEvt ){ + var + touch = evt.touches[0] + , dx = touch.clientX - tapEvt.clientX + , dy = touch.clientY - tapEvt.clientY + ; + + touchEvt = touch; + _css(ghostEl, 'webkitTransform', 'translate3d('+dx+'px,'+dy+'px,0)'); + } + }, + + + _onDragStart: function (evt/**Event*/, isTouch){ + var + target = evt.target + , dataTransfer = evt.dataTransfer + ; + + rootEl = this.el; + dragEl = target; + nextEl = target.nextSibling; + activeGroup = this.options.group; + + if( isTouch ){ + var rect = target.getBoundingClientRect(), css = _css(target); + + ghostEl = target.cloneNode(true); + + _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); + _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); + _css(ghostEl, 'width', rect.right - rect.left); + _css(ghostEl, 'height', rect.bottom - rect.top); + _css(ghostEl, 'opacity', '0.8'); + _css(ghostEl, 'position', 'fixed'); + _css(ghostEl, 'zIndex', '100000'); + + target.parentNode.insertBefore(ghostEl, target); + + // Bind touch events + _on(document, 'touchmove', this._onTouchMove); + _on(document, 'touchend', this._onDrop); + + this._loopId = setInterval(this._emulateDragOver, 100); + } + else { + dataTransfer.effectAllowed = 'move'; + dataTransfer.setData('Text', target.textContent); + + _on(document, 'drop', this._onDrop); + } + + setTimeout(this._applyEffects); + }, + + + _onDragOver: function (evt){ + if( activeGroup === this.options.group && (evt.rootEl === void 0 || evt.rootEl === this.el) ){ + var + el = this.el + , target = _closest(evt.target, this.options.draggable, el) + ; + + + if( el.children.length === 0 || target == null ){ + el.appendChild(dragEl); + } + else if( target && (target !== dragEl) ){ + if( lastEl !== target ){ + lastEl = target; + lastCSS = _css(target) + } + + + var + rect = target.getBoundingClientRect() + , width = rect.right - rect.left + , height = rect.bottom - rect.top + , floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display) + , after = !floating && (evt.clientY - rect.top)/height > .5 || floating && (evt.clientX - rect.left)/width > .5 + , nextSibling = target.nextSibling + ; + + + if( after && !nextSibling ){ + el.appendChild(dragEl); + } else { + target.parentNode.insertBefore(dragEl, after ? nextSibling : target); + } + } + } + }, + + + _onDrop: function (evt/**Event*/){ + clearInterval(this._loopId); + + // Unbind events + _off(document, 'drop', this._onDrop); + _off(document, 'dragover', _globalDragOver); + + _off(this.el, 'dragstart', this._onDragStart); + + _off(document, 'touchmove', this._onTouchMove); + _off(document, 'touchend', this._onDrop); + + + if( evt ){ + evt.preventDefault(); + + if( dragEl ){ + var opts = { bubbles: true, cancelable: true, detail: dragEl }; + + _toggleClass(dragEl, this.options.ghostClass, false); + + if( !rootEl.contains(dragEl) ){ + // Remove event + rootEl.dispatchEvent(new Event('remove', opts)); + + // Add event + dragEl.dispatchEvent(new Event('add', opts)); + } + else if( dragEl.nextSibling !== nextEl ){ + // Update event + dragEl.dispatchEvent(new Event('update', opts)); + } + } + + if( ghostEl ){ + ghostEl.parentNode.removeChild(ghostEl); + } + + + // Set NULL + rootEl = + dragEl = + ghostEl = + nextEl = + + tapEvt = + touchEvt = + + lastEl = + lastCSS = + + activeGroup = null; + } + }, + + + destroy: function (){ + var el = this.el, options = this.options; + + _off(el, 'add', options.onAdd); + _off(el, 'update', options.onUpdate); + _off(el, 'remove', options.onRemove); + + _off(el, 'mousedown', this._onTapStart); + _off(el, 'touchstart', this._onTapStart); + + _off(el, 'dragover', this._onDragOver); + _off(el, 'dragenter', this._onDragOver); + + touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); + + this._onDrop(); + + this.el = null; + } + }; + + + function _bind(ctx, fn){ + var args = slice.call(arguments, 2); + return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function (){ + return fn.apply(ctx, args.concat(slice.call(arguments))); + }; + } + + + function _closest(el, selector, ctx){ + if( el && ctx ){ + ctx = ctx || document; + selector = selector.split('.'); + + var + tag = selector.shift().toUpperCase() + , re = new RegExp('\\b('+selector.join('|')+')\\b', 'g') + ; + + do { + if( + (tag === '' || el.nodeName == tag) + && (!selector.length || ((el.className+'').match(re) || []).length == selector.length) + ){ + return el; + } + } + while( el !== ctx && (el = el.parentNode) ); + } + + return null; + } + + + function _globalDragOver(evt){ + evt.dataTransfer.dropEffect = 'move'; + evt.preventDefault(); + } + + + function _on(el, event, fn){ + el.addEventListener(event, fn, false); + } + + + function _off(el, event, fn){ + el.removeEventListener(event, fn, false); + } + + + function _toggleClass(el, name, state){ + if( el ){ + if( el.classList ){ + el.classList[state ? 'add' : 'remove'](name); + } + else { + var className = (' '+el.className+' ').replace(' '+name+' ', '').replace(/\s+/g, ' '); + el.className = className + (state ? ' '+name : '') + } + } + } + + + function _css(el, prop, val){ + if( el && el.style ){ + if( val === void 0 ){ + if( document.defaultView && document.defaultView.getComputedStyle ){ + val = document.defaultView.getComputedStyle(el, ''); + } + else if( el.currentStyle ){ + val = el.currentStyle; + } + return prop === void 0 ? val : val[prop]; + } else { + el.style[prop] = val + (typeof val === 'string' ? '' : 'px'); + } + } + } + + + function _find(ctx, tagName, iterator){ + if( ctx ){ + var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; + if( iterator ){ + for( ; i < n; i++ ){ + iterator(list[i], i); + } + } + return list; + } + return []; + } + + + function _disableDraggable(el){ + return el.draggable = false; + } + + + + // Export utils + Sortable.utils = { + on: _on, + off: _off, + css: _css, + find: _find, + bind: _bind, + closest: _closest, + toggleClass: _toggleClass + }; + + + Sortable.version = '0.1.0'; + + // Export + return Sortable; +}); diff --git a/Sortable.min.js b/Sortable.min.js new file mode 100644 index 0000000..dfd7cb2 --- /dev/null +++ b/Sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 0.1.0 - MIT | git://github.com/rubaxa/Sortable.git */ +!function(a){"use strict";"function"==typeof define&&define.amd?define("Sortable",[],a):window.Sortable=a()}(function(){"use strict";function a(a,c){this.el=a,this.options=c=c||{},c.group=c.group||Math.random(),c.handle=c.handle||null,c.draggable=c.draggable||a.children[0]&&a.children[0].nodeName||"li",c.ghostClass=c.ghostClass||"sortable-ghost",c.onAdd=b(this,c.onAdd||y),c.onUpdate=b(this,c.onUpdate||y),c.onRemove=b(this,c.onRemove||y),a[t]=c.group;for(var d in this)"_"===d.charAt(0)&&(this[d]=b(this,this[d]));e(a,"add",c.onAdd),e(a,"update",c.onUpdate),e(a,"remove",c.onRemove),e(a,"mousedown",this._onTapStart),e(a,"touchstart",this._onTapStart),e(a,"dragover",this._onDragOver),e(a,"dragenter",this._onDragOver),A.push(this._onDragOver)}function b(a,b){var c=z.call(arguments,2);return b.bind?b.bind.apply(b,[a].concat(c)):function(){return b.apply(a,c.concat(z.call(arguments)))}}function c(a,b,c){if(a&&c){c=c||v,b=b.split(".");var d=b.shift().toUpperCase(),e=new RegExp("\\b("+b.join("|")+")\\b","g");do if(!(""!==d&&a.nodeName!=d||b.length&&((a.className+"").match(e)||[]).length!=b.length))return a;while(a!==c&&(a=a.parentNode))}return null}function d(a){a.dataTransfer.dropEffect="move",a.preventDefault()}function e(a,b,c){a.addEventListener(b,c,!1)}function f(a,b,c){a.removeEventListener(b,c,!1)}function g(a,b,c){if(a)if(a.classList)a.classList[c?"add":"remove"](b);else{var d=(" "+a.className+" ").replace(" "+b+" ","").replace(/\s+/g," ");a.className=d+(c?" "+b:"")}}function h(a,b,c){if(a&&a.style){if(void 0===c)return v.defaultView&&v.defaultView.getComputedStyle?c=v.defaultView.getComputedStyle(a,""):a.currentStyle&&(c=a.currentStyle),void 0===b?c:c[b];a.style[b]=c+("string"==typeof c?"":"px")}}function i(a,b,c){if(a){var d=a.getElementsByTagName(b),e=0,f=d.length;if(c)for(;f>e;e++)c(d[e],e);return d}return[]}function j(a){return a.draggable=!1}var k,l,m,n,o,p,q,r,s,t="Sortable"+(new Date).getTime(),u=window,v=u.document,w=u.parseInt,x=u.CustomEvent,y=function(){},z=[].slice,A=[];return a.prototype={constructor:a,_applyEffects:function(){g(k,this.options.ghostClass,!0)},_onTapStart:function(a){var b=a.touches&&a.touches[0],f=(b||a).target,g=this.options;if(g.handle&&(f=c(f,g.handle,this.el)),f=c(f,g.draggable,this.el),f&&!k){r=a,f.draggable=!0,i(f,"a",j),i(f,"img",j),b&&(r={target:f,clientX:b.clientX,clientY:b.clientY},this._onDragStart(r,!0),a.preventDefault()),e(this.el,"dragstart",this._onDragStart),e(v,"dragover",d);try{v.selection?v.selection.empty():window.getSelection().removeAllRanges()}catch(h){}}},_emulateDragOver:function(){if(s){h(l,"display","none");var a=v.elementFromPoint(s.clientX,s.clientY),b=a,c=this.options.group,d=A.length;do if(b[t]===c){for(;d--;)A[d]({clientX:s.clientX,clientY:s.clientY,target:a,rootEl:b});break}while(b=b.parentNode);h(l,"display","")}},_onTouchMove:function(a){if(r){var b=a.touches[0],c=b.clientX-r.clientX,d=b.clientY-r.clientY;s=b,h(l,"webkitTransform","translate3d("+c+"px,"+d+"px,0)")}},_onDragStart:function(a,b){var c=a.target,d=a.dataTransfer;if(m=this.el,k=c,n=c.nextSibling,q=this.options.group,b){var f=c.getBoundingClientRect(),g=h(c);l=c.cloneNode(!0),h(l,"top",f.top-w(g.marginTop,10)),h(l,"left",f.left-w(g.marginLeft,10)),h(l,"width",f.right-f.left),h(l,"height",f.bottom-f.top),h(l,"opacity","0.8"),h(l,"position","fixed"),h(l,"zIndex","100000"),c.parentNode.insertBefore(l,c),e(v,"touchmove",this._onTouchMove),e(v,"touchend",this._onDrop),this._loopId=setInterval(this._emulateDragOver,100)}else d.effectAllowed="move",d.setData("Text",c.textContent),e(v,"drop",this._onDrop);setTimeout(this._applyEffects)},_onDragOver:function(a){if(q===this.options.group&&(void 0===a.rootEl||a.rootEl===this.el)){var b=this.el,d=c(a.target,this.options.draggable,b);if(0===b.children.length||null==d)b.appendChild(k);else if(d&&d!==k){o!==d&&(o=d,p=h(d));var e=d.getBoundingClientRect(),f=e.right-e.left,g=e.bottom-e.top,i=/left|right|inline/.test(p.cssFloat+p.display),j=!i&&(a.clientY-e.top)/g>.5||i&&(a.clientX-e.left)/f>.5,l=d.nextSibling;j&&!l?b.appendChild(k):d.parentNode.insertBefore(k,j?l:d)}}},_onDrop:function(a){if(clearInterval(this._loopId),f(v,"drop",this._onDrop),f(v,"dragover",d),f(this.el,"dragstart",this._onDragStart),f(v,"touchmove",this._onTouchMove),f(v,"touchend",this._onDrop),a){if(a.preventDefault(),k){var b={bubbles:!0,cancelable:!0,detail:k};g(k,this.options.ghostClass,!1),m.contains(k)?k.nextSibling!==n&&k.dispatchEvent(new x("update",b)):(m.dispatchEvent(new x("remove",b)),k.dispatchEvent(new x("add",b)))}l&&l.parentNode.removeChild(l),m=k=l=n=r=s=o=p=q=null}},destroy:function(){var a=this.el,b=this.options;f(a,"add",b.onAdd),f(a,"update",b.onUpdate),f(a,"remove",b.onRemove),f(a,"mousedown",this._onTapStart),f(a,"touchstart",this._onTapStart),f(a,"dragover",this._onDragOver),f(a,"dragenter",this._onDragOver),A.splice(A.indexOf(this._onDragOver),1),this._onDrop(),this.el=null}},a.utils={on:e,off:f,css:h,find:i,bind:b,closest:c,toggleClass:g},a.version="0.1.0",a}); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..6161e84 --- /dev/null +++ b/index.html @@ -0,0 +1,387 @@ + + + + + + + Sortable (No jQuery) + + + + + + + + + + + + + Fork me on GitHub + +
+

+ Sortable + +

+
— is a minimalist JavaScript library for modern browsers and touch devices (No jQuery).
+ + +
+
+

List A

+ +
    +
  • + + Catherine +
  • +
  • + + Polina +
  • +
  • + + Duke +
  • +
  • + + Adnrey +
  • +
  • + + Maxim +
  • +
+
+ +
+

List B

+ +
    +
  • + + Ilya +
  • +
  • + + Anna +
  • +
+
+
+
+ +

 

+

 

+ +
+

Multi

+ +
+
Group A
+
+ +
+
+ +
+
Group B
+
+ +
+
+ +
+
Group C
+
+ +
+
+ +
+

 

+
+ +
+

Code example

+
// Simple list
+var list = document.getElementById("my-ui-list");
+new Sortable(list); // That's all.
+
+
+// Grouping
+var foo = document.getElementById("foo");
+new Sortable(foo, { group: "omega" });
+
+var bar = document.getElementById("bar");
+new Sortable(bar, { group: "omega" });
+
+
+// Or
+var container = document.getElementById("multi");
+var sort = new Sortable(container, {
+  handle: ".tile__title", // Restricts sort start click/touch to the specified element
+  dragabble: ".tile", // Specifies which items inside the element should be sortable
+  onUpdate: function (evt/**Event*/){
+     var item = evt.detail; // the current dragged HTMLElement
+  }
+});
+
+// ..
+sort.destroy();
+
+ +

 

+

 

+

 

+
+ + + + + + + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..492f9c1 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "sortable", + "exportName": "Sortable", + "version": "0.1.0", + "devDependencies": { + "grunt": "*", + "grunt-version": "*", + "grunt-contrib-uglify": "*" + }, + "description": "Sortable - is a minimalist JavaScript library for modern browsers and touch devices (No jQuery).", + "main": "Sortable.js", + "scripts": { + "test": "grunt" + }, + "repository": { + "type": "git", + "url": "git://github.com/rubaxa/Sortable.git" + }, + "keywords": ["sortable", "reorder"], + "author": "Konstantin Lebedev ", + "license": "MIT" +}