Browse Source

+ Intermediate version before release

pull/141/head
RubaXa 10 years ago
parent
commit
c8363dc711
  1. 41
      Gruntfile.js
  2. 112
      README.md
  3. 493
      Sortable.js
  4. 4
      Sortable.min.js
  5. 2
      bower.json
  6. 2
      component.json
  7. 192
      index.html
  8. 117
      ng-sortable.js
  9. 7
      package.json
  10. 20
      st/app.css

41
Gruntfile.js

@ -1,6 +1,6 @@
'use strict'; module.exports = function (grunt) {
'use strict';
module.exports = function (grunt){
grunt.initConfig({ grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
@ -8,13 +8,42 @@ module.exports = function (grunt){
src: ['<%= pkg.exportName %>.js', '*.json'] src: ['<%= pkg.exportName %>.js', '*.json']
}, },
jshint: {
all: ['*.js', '!*.min.js'],
options: {
strict: true,
newcap: false, // "Tolerate uncapitalized constructors"
node: true,
expr: true, // - true && call() "Expected an assignment or function call and instead saw an expression."
supernew: true, // - "Missing '()' invoking a constructor."
laxbreak: true,
white: true,
globals: {
define: true,
test: true,
expect: true,
module: true,
asyncTest: true,
start: true,
ok: true,
equal: true,
notEqual: true,
deepEqual: true,
window: true,
document: true,
performance: true
}
}
},
uglify: { uglify: {
options: { options: {
banner: '/*! <%= pkg.exportName %> <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n' banner: '/*! <%= pkg.exportName %> <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n'
}, },
dist: { dist: {
files: { files: {
'<%= pkg.exportName %>.min.js': ['<%= pkg.exportName %>.js'] '<%= pkg.exportName %>.min.js': ['<%= pkg.exportName %>.js']
} }
} }
}, },
@ -31,8 +60,8 @@ module.exports = function (grunt){
}); });
// These plugins provide necessary tasks.
grunt.loadNpmTasks('grunt-version'); grunt.loadNpmTasks('grunt-version');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-shell');
@ -44,6 +73,6 @@ module.exports = function (grunt){
// grunt.regsterTask('meteor', ['shell:meteor-test', 'shell:meteor-publish']); // grunt.regsterTask('meteor', ['shell:meteor-test', 'shell:meteor-publish']);
grunt.registerTask('meteor', 'shell:meteor-publish'); grunt.registerTask('meteor', 'shell:meteor-publish');
// Default task. grunt.registerTask('tests', ['jshint']);
grunt.registerTask('default', ['version', 'uglify']); grunt.registerTask('default', ['tests', 'version', 'uglify']);
}; };

112
README.md

@ -1,17 +1,19 @@
# Sortable # Sortable
Sortable is a minimalist JavaScript library for reorderable drag-and-drop lists. Sortable is a minimalist JavaScript library for reorderable drag-and-drop lists.
Demo: http://rubaxa.github.io/Sortable/ Demo: http://rubaxa.github.io/Sortable/
## Features ## Features
* Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers
* Built using native HTML5 drag and drop API * Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers
* Can drag from one list to another or within the same list * Can drag from one list to another or within the same list
* Supports drag handles *and selectable text* (better than voidberg's html5sortable) * Animation moving items when sorting (css animation)
* Simple API * Supports drag handles *and selectable text* (better than voidberg's html5sortable)
* Lightweight, 2KB gzipped * Built using native HTML5 drag and drop API
* No jQuery * Support [AngularJS](#ng)
* Simple API
* No jQuery
### Usage ### Usage
@ -25,7 +27,7 @@ Demo: http://rubaxa.github.io/Sortable/
```js ```js
var el = document.getElementById('items'); var el = document.getElementById('items');
new Sortable(el); var sortable = Sortable.create(el);
``` ```
@ -34,25 +36,39 @@ new Sortable(el);
### Options ### Options
```js ```js
new Sortable(el, { var sortabel = new Sortable(el, {
group: "name", group: "name", // or { name: "..", pull: [true, false, clone], put: [true, false, array] }
sort: true, // sorting inside list
store: null, // @see Store store: null, // @see Store
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
handle: ".my-handle", // Drag handle selector within list items handle: ".my-handle", // Drag handle selector within list items
filter: ".ignore-elements", // Selectors that do not lead to dragging (String or Function) filter: ".ignor-elements", // Selectors that do not lead to dragging (String or Function)
draggable: ".item", // Specifies which items inside the element should be sortable draggable: ".item", // Specifies which items inside the element should be sortable
ghostClass: "sortable-ghost", ghostClass: "sortable-ghost",
setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent);
},
onStart: function (/**Event*/evt) { /* dragging */ }, onStart: function (/**Event*/evt) { /* dragging */ },
onEnd: function (/**Event*/evt) { /* dragging */ }, onEnd: function (/**Event*/evt) { /* dragging */ },
// Element is added to the list
onAdd: function (/**Event*/evt){ onAdd: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement var itemEl = evt.item; // dragged HTMLElement
itemEl.from; // previous list
}, },
// Changed sorting in list
onUpdate: function (/**Event*/evt){ onUpdate: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement var itemEl = evt.item; // dragged HTMLElement
}, },
// Called by any change to the list (add / update / remove)
onSort: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement
},
// The element is removed from the list
onRemove: function (/**Event*/evt){ onRemove: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement var itemEl = evt.item; // dragged HTMLElement
}, },
@ -63,20 +79,24 @@ new Sortable(el, {
}); });
``` ```
#### handle
---
#### `handle` option
To make list items draggable, Sortable disables text selection by the user. That's not always desirable. To allow text selection, define a drag handler, which is an area of every list element that allows it to be dragged around. To make list items draggable, Sortable disables text selection by the user. That's not always desirable. To allow text selection, define a drag handler, which is an area of every list element that allows it to be dragged around.
```js ```js
new Sortable(el, { var sortable = new Sortable(el, {
handle: ".my-handle", handle: ".my-handle",
}); });
``` ```
```html ```html
<ul> <ul>
<li><span class="my-handle">:: </span> list item text one <li><span class="my-handle">::</span> list item text one
<li><span class="my-handle">:: </span> list item text two <li><span class="my-handle">::</span> list item text two
</ul> </ul>
``` ```
@ -86,10 +106,61 @@ new Sortable(el, {
} }
``` ```
---
### `group` option
* name:`string` — group name
* pull:`true|false|'clone'` — ability to move from the list. `clone` — cloning drag item when moving from the list.
* put:`true|false|["foo", "bar"]` — the possibility of adding an element from the other list, or an array of names groups, which can be taken.
---
<a name="ng"></a>
### Support AngularJS
Include [ng-sortable.js](ng-sortable.js)
```html
<div ng-app"myApp">
<ul ng-sortable>
<li ng-repeat="item in items">{{item}}</li>
</ul>
<ul ng-sortable="{ group: 'foobar' }">
<li ng-repeat="item in foo">{{item}}</li>
</ul>
<ul ng-sortable="barConfig">
<li ng-repeat="item in bar">{{item}}</li>
</ul>
</div>
```
```js
angular.module('myApp', ['ng-sortable'])
.controller(function () {
this.items = ['item 1', 'item 2'];
this.foo = ['foo 1', '..'];
this.bar = ['bar 1', '..'];
this.barConfig = { group: 'foobar', animation: 150 };
});
```
--- ---
### Methods ### Method
##### option(name:`String`[, value:`*`]):`*`
Get or set the option.
##### closest(el:`String`[, selector:`HTMLElement`]):`HTMLElement|null` ##### closest(el:`String`[, selector:`HTMLElement`]):`HTMLElement|null`
For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
@ -117,6 +188,7 @@ Serializes the sortable's item `data-id`'s into an array of string.
##### sort(order:`String[]`) ##### sort(order:`String[]`)
Sorts the elements according to the array. Sorts the elements according to the array.
```js ```js
var order = sortable.toArray(); var order = sortable.toArray();
sortable.sort(order.reverse()); // apply sortable.sort(order.reverse()); // apply
@ -124,6 +196,7 @@ sortable.sort(order.reverse()); // apply
##### destroy() ##### destroy()
Removes the sortable functionality completely.
--- ---
@ -141,7 +214,7 @@ Saving and restoring of the sort.
``` ```
```js ```js
new Sortable(el, { Sortable.create(el, {
group: "localStorage-example", group: "localStorage-example",
store: { store: {
/** /**
@ -185,6 +258,7 @@ new Sortable(el, {
* toggleClass(el`:HTMLElement`, name`:String`, state`:Boolean`) — add or remove one classes from each element * toggleClass(el`:HTMLElement`, name`:String`, state`:Boolean`) — add or remove one classes from each element
--- ---

493
Sortable.js

@ -5,65 +5,64 @@
*/ */
(function (factory){ (function (factory) {
"use strict"; "use strict";
if( typeof define === "function" && define.amd ){ if (typeof define === "function" && define.amd) {
define(factory); define(factory);
} }
else if( typeof module != "undefined" && typeof module.exports != "undefined" ){ else if (typeof module != "undefined" && typeof module.exports != "undefined") {
module.exports = factory(); module.exports = factory();
} }
else if( typeof Package !== "undefined" ){ else if (typeof Package !== "undefined") {
Sortable = factory(); // export for Meteor.js Sortable = factory(); // export for Meteor.js
} }
else { else {
/* jshint sub:true */
window["Sortable"] = factory(); window["Sortable"] = factory();
} }
})(function (){ })(function () {
"use strict"; "use strict";
var var dragEl,
dragEl ghostEl,
, ghostEl cloneEl,
, rootEl rootEl,
, nextEl nextEl,
, lastEl lastEl,
, lastCSS lastCSS,
, lastRect
, activeGroup activeGroup,
, tapEvt tapEvt,
, touchEvt touchEvt,
, expando = 'Sortable' + (new Date).getTime() expando = 'Sortable' + (new Date).getTime(),
, win = window win = window,
, document = win.document document = win.document,
, parseInt = win.parseInt parseInt = win.parseInt,
, supportIEdnd = !!document.createElement('div').dragDrop supportIEdnd = !!document.createElement('div').dragDrop,
, _silent = false _silent = false,
, _createEvent = function (event/**String*/, item/**HTMLElement*/){ _dispatchEvent = function (rootEl, name, targetEl, fromEl) {
var evt = document.createEvent('Event'); var evt = document.createEvent('Event');
evt.initEvent(event, true, true);
evt.item = item;
return evt;
}
, _dispatchEvent = function (rootEl, name, targetEl) { evt.initEvent(name, true, true);
rootEl.dispatchEvent(_createEvent(name, targetEl || rootEl)); evt.item = targetEl || rootEl;
} evt.from = fromEl || rootEl;
, _customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter'.split(' ') rootEl.dispatchEvent(evt);
},
, noop = function (){} _customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' '),
, slice = [].slice
, touchDragOverListeners = [] noop = function () {},
slice = [].slice,
touchDragOverListeners = []
; ;
@ -73,28 +72,46 @@
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {Object} [options] * @param {Object} [options]
*/ */
function Sortable(el, options){ function Sortable(el, options) {
this.el = el; // root element this.el = el; // root element
this.options = options = (options || {}); this.options = options = (options || {});
// Defaults // Default options
var defaults = { var defaults = {
group: Math.random(), group: Math.random(),
sort: true,
store: null, store: null,
handle: null, handle: null,
draggable: el.children[0] && el.children[0].nodeName || (/[uo]l/i.test(el.nodeName) ? 'li' : '*'), draggable: el.children[0] && el.children[0].nodeName || (/[uo]l/i.test(el.nodeName) ? 'li' : '*'),
ghostClass: 'sortable-ghost', ghostClass: 'sortable-ghost',
ignore: 'a, img', ignore: 'a, img',
filter: null filter: null,
animation: 0,
setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent);
}
}; };
// Set default options // Set default options
for (var name in defaults) { for (var name in defaults) {
options[name] = options[name] || defaults[name]; !(name in options) && (options[name] = defaults[name]);
} }
if (!options.group.name) {
options.group = { name: options.group };
}
['pull', 'put'].forEach(function (key) {
if (!(key in options.group)) {
options.group[key] = true;
}
});
// Define events // Define events
_customEvents.forEach(function (name) { _customEvents.forEach(function (name) {
options[name] = _bind(this, options[name] || noop); options[name] = _bind(this, options[name] || noop);
@ -103,12 +120,12 @@
// Export group name // Export group name
el[expando] = options.group; el[expando] = options.group.name;
// Bind all private methods // Bind all private methods
for( var fn in this ){ for (var fn in this) {
if( fn.charAt(0) === '_' ){ if (fn.charAt(0) === '_') {
this[fn] = _bind(this, this[fn]); this[fn] = _bind(this, this[fn]);
} }
} }
@ -133,32 +150,31 @@
constructor: Sortable, constructor: Sortable,
_applyEffects: function (){ _applyEffects: function () {
_toggleClass(dragEl, this.options.ghostClass, true); _toggleClass(dragEl, this.options.ghostClass, true);
}, },
_onTapStart: function (evt/**Event|TouchEvent*/){ _onTapStart: function (/**Event|TouchEvent*/evt) {
var var touch = evt.touches && evt.touches[0],
touch = evt.touches && evt.touches[0] target = (touch || evt).target,
, target = (touch || evt).target options = this.options,
, options = this.options el = this.el,
, el = this.el filter = options.filter;
, filter = options.filter
;
if( evt.type === 'mousedown' && evt.button !== 0 ) {
if (evt.type === 'mousedown' && evt.button !== 0) {
return; // only left button return; // only left button
} }
// Check filter // Check filter
if( typeof filter === 'function' ){ if (typeof filter === 'function') {
if( filter.call(this, target, this) ){ if (filter.call(this, target, this)) {
_dispatchEvent(el, 'filter', target); _dispatchEvent(el, 'filter', target);
return; // cancel dnd return; // cancel dnd
} }
} }
else if( filter ){ else if (filter) {
filter = filter.split(',').filter(function (criteria) { filter = filter.split(',').filter(function (criteria) {
return _closest(target, criteria.trim(), el); return _closest(target, criteria.trim(), el);
}); });
@ -169,20 +185,20 @@
} }
} }
if( options.handle ){ if (options.handle) {
target = _closest(target, options.handle, el); target = _closest(target, options.handle, el);
} }
target = _closest(target, options.draggable, el); target = _closest(target, options.draggable, el);
// IE 9 Support // IE 9 Support
if( target && evt.type == 'selectstart' ){ if (target && evt.type == 'selectstart') {
if( target.tagName != 'A' && target.tagName != 'IMG'){ if (target.tagName != 'A' && target.tagName != 'IMG') {
target.dragDrop(); target.dragDrop();
} }
} }
if( target && !dragEl && (target.parentNode === el) ){ if (target && !dragEl && (target.parentNode === el)) {
tapEvt = evt; tapEvt = evt;
rootEl = this.el; rootEl = this.el;
@ -197,12 +213,12 @@
_find(target, criteria.trim(), _disableDraggable); _find(target, criteria.trim(), _disableDraggable);
}); });
if( touch ){ if (touch) {
// Touch device support // Touch device support
tapEvt = { tapEvt = {
target: target target: target,
, clientX: touch.clientX clientX: touch.clientX,
, clientY: touch.clientY clientY: touch.clientY
}; };
this._onDragStart(tapEvt, true); this._onDragStart(tapEvt, true);
@ -219,33 +235,39 @@
try { try {
if( document.selection ){ if (document.selection) {
document.selection.empty(); document.selection.empty();
} else { } else {
window.getSelection().removeAllRanges() window.getSelection().removeAllRanges();
} }
} catch (err){ } } catch (err) {
}
_dispatchEvent(dragEl, 'start'); _dispatchEvent(dragEl, 'start');
if (activeGroup.pull == 'clone') {
cloneEl = dragEl.cloneNode(true);
_css(cloneEl, 'display', 'none');
rootEl.insertBefore(cloneEl, dragEl);
}
} }
}, },
_emulateDragOver: function (){ _emulateDragOver: function () {
if( touchEvt ){ if (touchEvt) {
_css(ghostEl, 'display', 'none'); _css(ghostEl, 'display', 'none');
var var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY) parent = target,
, parent = target groupName = this.options.group.name,
, group = this.options.group i = touchDragOverListeners.length;
, i = touchDragOverListeners.length
;
if( parent ){ if (parent) {
do { do {
if( parent[expando] === group ){ if (parent[expando] === groupName) {
while( i-- ){ while (i--) {
touchDragOverListeners[i]({ touchDragOverListeners[i]({
clientX: touchEvt.clientX, clientX: touchEvt.clientX,
clientY: touchEvt.clientY, clientY: touchEvt.clientY,
@ -253,12 +275,14 @@
rootEl: parent rootEl: parent
}); });
} }
break; break;
} }
target = parent; // store last element target = parent; // store last element
} }
while( parent = parent.parentNode ); /* jshint boss:true */
while (parent = parent.parentNode);
} }
_css(ghostEl, 'display', ''); _css(ghostEl, 'display', '');
@ -266,14 +290,12 @@
}, },
_onTouchMove: function (evt/**TouchEvent*/){ _onTouchMove: function (/**TouchEvent*/evt) {
if( tapEvt ){ if (tapEvt) {
var var touch = evt.touches[0],
touch = evt.touches[0] dx = touch.clientX - tapEvt.clientX,
, dx = touch.clientX - tapEvt.clientX dy = touch.clientY - tapEvt.clientY,
, dy = touch.clientY - tapEvt.clientY translate3d = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
, translate3d = 'translate3d(' + dx + 'px,' + dy + 'px,0)'
;
touchEvt = touch; touchEvt = touch;
@ -287,17 +309,16 @@
}, },
_onDragStart: function (evt/**Event*/, isTouch/**Boolean*/){ _onDragStart: function (/**Event*/evt, /**boolean*/isTouch) {
var dataTransfer = evt.dataTransfer; var dataTransfer = evt.dataTransfer,
options = this.options;
this._offUpEvents(); this._offUpEvents();
if( isTouch ){ if (isTouch) {
var var rect = dragEl.getBoundingClientRect(),
rect = dragEl.getBoundingClientRect() css = _css(dragEl),
, css = _css(dragEl) ghostRect;
, ghostRect
;
ghostEl = dragEl.cloneNode(true); ghostEl = dragEl.cloneNode(true);
@ -313,8 +334,8 @@
// Fixing dimensions. // Fixing dimensions.
ghostRect = ghostEl.getBoundingClientRect(); ghostRect = ghostEl.getBoundingClientRect();
_css(ghostEl, 'width', rect.width*2 - ghostRect.width); _css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
_css(ghostEl, 'height', rect.height*2 - ghostRect.height); _css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
// Bind touch events // Bind touch events
_on(document, 'touchmove', this._onTouchMove); _on(document, 'touchmove', this._onTouchMove);
@ -325,7 +346,7 @@
} }
else { else {
dataTransfer.effectAllowed = 'move'; dataTransfer.effectAllowed = 'move';
dataTransfer.setData('Text', dragEl.textContent); options.setData && options.setData.call(this, dataTransfer, dragEl);
_on(document, 'drop', this._onDrop); _on(document, 'drop', this._onDrop);
} }
@ -334,54 +355,122 @@
}, },
_onDragOver: function (evt/**Event*/){ _onDragOver: function (/**Event*/evt) {
if( !_silent && (activeGroup === this.options.group) && (evt.rootEl === void 0 || evt.rootEl === this.el) ){ var el = this.el,
var target,
el = this.el dragRect,
, target = _closest(evt.target, this.options.draggable, el) revert,
; options = this.options,
group = options.group,
groupPut = group.put,
isOwner = (activeGroup === group),
canSort = options.sort;
if (!_silent &&
(isOwner
? canSort || (revert = !rootEl.contains(dragEl))
: activeGroup.pull && activeGroup.name === group.name && groupPut && (groupPut.indexOf ? groupPut.indexOf(activeGroup.name) > -1 : groupPut)
) &&
(evt.rootEl === void 0 || evt.rootEl === this.el)
) {
target = _closest(evt.target, options.draggable, el);
dragRect = dragEl.getBoundingClientRect();
if (cloneEl && (cloneEl.state !== isOwner)) {
_css(cloneEl, 'display', isOwner ? 'none' : '');
!isOwner && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
cloneEl.state = isOwner;
}
if (revert) {
if (cloneEl || nextEl) {
rootEl.insertBefore(dragEl, cloneEl || nextEl);
}
else if (!canSort) {
rootEl.appendChild(dragEl);
}
return;
}
if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
(el === evt.target) && (target = _ghostInBottom(el, evt))
) {
if (target) {
if (target.animated) {
return;
}
targetRect = target.getBoundingClientRect();
}
if( el.children.length === 0 || el.children[0] === ghostEl || (el === evt.target) && _ghostInBottom(el, evt) ){
el.appendChild(dragEl); el.appendChild(dragEl);
this._animate(dragRect, dragEl);
target && this._animate(targetRect, target);
} }
else if( target && target !== dragEl && (target.parentNode[expando] !== void 0) ){ else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
if( lastEl !== target ){ if (lastEl !== target) {
lastEl = target; lastEl = target;
lastCSS = _css(target); lastCSS = _css(target);
lastRect = target.getBoundingClientRect();
} }
var var targetRect = target.getBoundingClientRect(),
rect = lastRect width = targetRect.right - targetRect.left,
, width = rect.right - rect.left height = targetRect.bottom - targetRect.top,
, height = rect.bottom - rect.top floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display),
, floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display) isWide = (target.offsetWidth > dragEl.offsetWidth),
, isWide = (target.offsetWidth > dragEl.offsetWidth) isLong = (target.offsetHeight > dragEl.offsetHeight),
, isLong = (target.offsetHeight > dragEl.offsetHeight) halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
, halfway = (floating ? (evt.clientX - rect.left)/width : (evt.clientY - rect.top)/height) > .5 nextSibling = target.nextElementSibling,
, nextSibling = target.nextElementSibling after
, after
; ;
_silent = true; _silent = true;
setTimeout(_unsilent, 30); setTimeout(_unsilent, 30);
if( floating ){ if (floating) {
after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
} else { } else {
after = (nextSibling !== dragEl) && !isLong || halfway && isLong; after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
} }
if( after && !nextSibling ){ if (after && !nextSibling) {
el.appendChild(dragEl); el.appendChild(dragEl);
} else { } else {
target.parentNode.insertBefore(dragEl, after ? nextSibling : target); target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
} }
this._animate(dragRect, dragEl);
this._animate(targetRect, target);
} }
} }
}, },
_animate: function (prevRect, target) {
var ms = this.options.animation;
if (ms) {
var currentRect = target.getBoundingClientRect();
_css(target, 'transition', 'none');
_css(target, 'transform', 'translate3d('
+ (prevRect.left - currentRect.left) + 'px,'
+ (prevRect.top - currentRect.top) + 'px,0)'
);
target.offsetWidth; // repaint
_css(target, 'transition', 'all ' + ms + 'ms');
_css(target, 'transform', 'translate3d(0,0,0)');
clearTimeout(target.animated);
target.animated = setTimeout(function () {
_css(target, 'transition', '');
target.animated = false;
}, ms);
}
},
_offUpEvents: function () { _offUpEvents: function () {
_off(document, 'mouseup', this._onDrop); _off(document, 'mouseup', this._onDrop);
_off(document, 'touchmove', this._onTouchMove); _off(document, 'touchmove', this._onTouchMove);
@ -389,7 +478,7 @@
_off(document, 'touchcancel', this._onDrop); _off(document, 'touchcancel', this._onDrop);
}, },
_onDrop: function (evt/**Event*/){ _onDrop: function (/**Event*/evt) {
clearInterval(this._loopId); clearInterval(this._loopId);
// Unbind events // Unbind events
@ -402,31 +491,35 @@
this._offUpEvents(); this._offUpEvents();
if( evt ){ if (evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
if( ghostEl ){ ghostEl && ghostEl.parentNode.removeChild(ghostEl);
ghostEl.parentNode.removeChild(ghostEl);
}
if( dragEl ){ if (dragEl) {
_disableDraggable(dragEl); _disableDraggable(dragEl);
_toggleClass(dragEl, this.options.ghostClass, false); _toggleClass(dragEl, this.options.ghostClass, false);
if( !rootEl.contains(dragEl) ){ if (!rootEl.contains(dragEl)) {
// Remove event _dispatchEvent(dragEl, 'sort');
_dispatchEvent(rootEl, 'remove', dragEl); _dispatchEvent(rootEl, 'sort');
// Add event // Add event
_dispatchEvent(dragEl, 'add'); _dispatchEvent(dragEl, 'add', dragEl, rootEl);
// Remove event
_dispatchEvent(rootEl, 'remove', dragEl);
} }
else if( dragEl.nextSibling !== nextEl ){ else if (dragEl.nextSibling !== nextEl) {
// Update event // Update event
_dispatchEvent(dragEl, 'update'); _dispatchEvent(dragEl, 'update');
_dispatchEvent(dragEl, 'sort');
cloneEl && cloneEl.parentNode.removeChild(cloneEl);
} }
_dispatchEvent(dragEl, 'end'); _dispatchEvent(rootEl, 'end');
} }
// Set NULL // Set NULL
@ -434,6 +527,7 @@
dragEl = dragEl =
ghostEl = ghostEl =
nextEl = nextEl =
cloneEl =
tapEvt = tapEvt =
touchEvt = touchEvt =
@ -458,8 +552,7 @@
el, el,
children = this.el.children, children = this.el.children,
i = 0, i = 0,
n = children.length n = children.length;
;
for (; i < n; i++) { for (; i < n; i++) {
el = children[i]; el = children[i];
@ -508,6 +601,23 @@
}, },
/**
* Set/get option
* @param {string} name
* @param {*} [value]
* @returns {*}
*/
option: function (name, value) {
var options = this.options;
if (value === void 0) {
return options[name];
} else {
options[name] = value;
}
},
/** /**
* Destroy * Destroy
*/ */
@ -526,7 +636,7 @@
_off(el, 'dragenter', this._onDragOver); _off(el, 'dragenter', this._onDragOver);
//remove draggable attributes //remove draggable attributes
Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function(el) { Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
el.removeAttribute('draggable'); el.removeAttribute('draggable');
}); });
@ -539,115 +649,125 @@
}; };
function _bind(ctx, fn){ function _bind(ctx, fn) {
var args = slice.call(arguments, 2); var args = slice.call(arguments, 2);
return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function (){ return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function () {
return fn.apply(ctx, args.concat(slice.call(arguments))); return fn.apply(ctx, args.concat(slice.call(arguments)));
}; };
} }
function _closest(el, selector, ctx){ function _closest(el, selector, ctx) {
if( selector === '*' ){ if (selector === '*') {
return el; return el;
} }
else if( el ){ else if (el) {
ctx = ctx || document; ctx = ctx || document;
selector = selector.split('.'); selector = selector.split('.');
var var tag = selector.shift().toUpperCase(),
tag = selector.shift().toUpperCase() re = new RegExp('\\s(' + selector.join('|') + ')\\s', 'g');
, re = new RegExp('\\s('+selector.join('|')+')\\s', 'g')
;
do { do {
if( if (
(tag === '' || el.nodeName == tag) (tag === '' || el.nodeName == tag) &&
&& (!selector.length || ((' '+el.className+' ').match(re) || []).length == selector.length) (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
){ ) {
return el; return el;
} }
} }
while( el !== ctx && (el = el.parentNode) ); while (el !== ctx && (el = el.parentNode));
} }
return null; return null;
} }
function _globalDragOver(evt){ function _globalDragOver(evt) {
evt.dataTransfer.dropEffect = 'move'; evt.dataTransfer.dropEffect = 'move';
evt.preventDefault(); evt.preventDefault();
} }
function _on(el, event, fn){ function _on(el, event, fn) {
el.addEventListener(event, fn, false); el.addEventListener(event, fn, false);
} }
function _off(el, event, fn){ function _off(el, event, fn) {
el.removeEventListener(event, fn, false); el.removeEventListener(event, fn, false);
} }
function _toggleClass(el, name, state){ function _toggleClass(el, name, state) {
if( el ){ if (el) {
if( el.classList ){ if (el.classList) {
el.classList[state ? 'add' : 'remove'](name); el.classList[state ? 'add' : 'remove'](name);
} }
else { else {
var className = (' '+el.className+' ').replace(/\s+/g, ' ').replace(' '+name+' ', ''); var className = (' ' + el.className + ' ').replace(/\s+/g, ' ').replace(' ' + name + ' ', '');
el.className = className + (state ? ' '+name : '') el.className = className + (state ? ' ' + name : '');
} }
} }
} }
function _css(el, prop, val){ function _css(el, prop, val) {
if( el && el.style ){ var style = el && el.style;
if( val === void 0 ){
if( document.defaultView && document.defaultView.getComputedStyle ){ if (style) {
if (val === void 0) {
if (document.defaultView && document.defaultView.getComputedStyle) {
val = document.defaultView.getComputedStyle(el, ''); val = document.defaultView.getComputedStyle(el, '');
} }
else if( el.currentStyle ){ else if (el.currentStyle) {
val = el.currentStyle; val = el.currentStyle;
} }
return prop === void 0 ? val : val[prop];
} else { return prop === void 0 ? val : val[prop];
el.style[prop] = val + (typeof val === 'string' ? '' : 'px'); }
else {
if (!(prop in style)) {
prop = '-webkit-' + prop;
}
style[prop] = val + (typeof val === 'string' ? '' : 'px');
} }
} }
} }
function _find(ctx, tagName, iterator){ function _find(ctx, tagName, iterator) {
if( ctx ){ if (ctx) {
var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
if( iterator ){
for( ; i < n; i++ ){ if (iterator) {
for (; i < n; i++) {
iterator(list[i], i); iterator(list[i], i);
} }
} }
return list;
return list;
} }
return [];
return [];
} }
function _disableDraggable(el){ function _disableDraggable(el) {
return el.draggable = false; el.draggable = false;
} }
function _unsilent(){ function _unsilent() {
_silent = false; _silent = false;
} }
function _ghostInBottom(el, evt){ /** @returns {HTMLElement|false} */
var last = el.lastElementChild.getBoundingClientRect(); function _ghostInBottom(el, evt) {
return evt.clientY - (last.top + last.height) > 5; // min delta var lastEl = el.lastElementChild, rect = lastEl.getBoundingClientRect();
return (evt.clientY - (rect.top + rect.height) > 5) && lastEl; // min delta
} }
@ -660,8 +780,7 @@
function _generateId(el) { function _generateId(el) {
var str = el.tagName + el.className + el.src + el.href + el.textContent, var str = el.tagName + el.className + el.src + el.href + el.textContent,
i = str.length, i = str.length,
sum = 0 sum = 0;
;
while (i--) { while (i--) {
sum += str.charCodeAt(i); sum += str.charCodeAt(i);
@ -680,14 +799,22 @@
bind: _bind, bind: _bind,
closest: _closest, closest: _closest,
toggleClass: _toggleClass, toggleClass: _toggleClass,
createEvent: _createEvent,
dispatchEvent: _dispatchEvent dispatchEvent: _dispatchEvent
}; };
Sortable.version = '0.5.2'; Sortable.version = '0.7.0';
/**
* Create sortable instance
* @param {HTMLElement} el
* @param {Object} [options]
*/
Sortable.create = function (el, options) {
return new Sortable(el, options);
};
// Export // Export
return Sortable; return Sortable;
}); });

4
Sortable.min.js vendored

File diff suppressed because one or more lines are too long

2
bower.json

@ -1,7 +1,7 @@
{ {
"name": "Sortable", "name": "Sortable",
"main": "Sortable.js", "main": "Sortable.js",
"version": "0.5.2", "version": "0.7.0",
"homepage": "http://rubaxa.github.io/Sortable/", "homepage": "http://rubaxa.github.io/Sortable/",
"authors": [ "authors": [
"RubaXa <ibnRubaXa@gmail.com>" "RubaXa <ibnRubaXa@gmail.com>"

2
component.json

@ -1,7 +1,7 @@
{ {
"name": "Sortable", "name": "Sortable",
"main": "Sortable.js", "main": "Sortable.js",
"version": "0.5.2", "version": "0.7.0",
"homepage": "http://rubaxa.github.io/Sortable/", "homepage": "http://rubaxa.github.io/Sortable/",
"repo": "RubaXa/Sortable", "repo": "RubaXa/Sortable",
"authors": [ "authors": [

192
index.html

@ -6,9 +6,9 @@
<title>Sortable. No jQuery.</title> <title>Sortable. No jQuery.</title>
<meta name="keywords" content="sortable, reorder, list, javascript, html5, drag and drop, dnd, rubaxa"/> <meta name="keywords" content="sortable, reorder, list, javascript, html5, drag and drop, dnd, animation, groups, angular, ng-sortable, effects, rubaxa"/>
<meta name="description" content="Sortable - is a minimalist JavaScript library for modern browsers and touch devices (No jQuery)."/> <meta name="description" content="Sortable - is a minimalist JavaScript library for modern browsers and touch devices (No jQuery). Support AngularJS."/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=0.5"/>
<link href="//rubaxa.github.io/Ply/ply.css" rel="stylesheet" type="text/css"/> <link href="//rubaxa.github.io/Ply/ply.css" rel="stylesheet" type="text/css"/>
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"/> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"/>
@ -108,6 +108,103 @@
</div> </div>
<a name="ag"></a>
<div class="container" style="margin-top: 100px;">
<div id="advanced" style="margin-left: 30px;">
<div><div data-force="5" class="layer title title_xl">Advanced groups</div></div>
<div style="width: 25%; float: left; margin-top: 15px; margin-left: 10px" class="block__list block__list_words">
<div class="block__list-title">pull & put</div>
<ul id="advanced-1">
<li>Meat</li>
<li>Potato</li>
<li>Tea</li>
</ul>
</div>
<div style="width: 25%; float: left; margin-top: 15px; margin-left: 10px" class="block__list block__list_words">
<div class="block__list-title">only pull (clone), no reordering</div>
<ul id="advanced-2">
<li>Sex</li>
<li>Drugs</li>
<li>Rock'n'roll</li>
</ul>
</div>
<div style="width: 25%; float: left; margin-top: 15px; margin-left: 10px" class="block__list block__list_words">
<div class="block__list-title">only put</div>
<ul id="advanced-3">
<li>Money</li>
<li>Force</li>
<li>Agility</li>
</ul>
</div>
<div style="clear: both"></div>
</div>
</div>
<a name="h"></a>
<div class="container" style="margin-top: 100px;">
<div id="handle" style="margin-left: 30px;">
<div><div data-force="5" class="layer title title_xl">Drag handle and selectable text</div></div>
<div style="width: 30%; float: left; margin-top: 15px; margin-left: 10px" class="block__list_words">
<div class="block__list-title">Drag handles</div>
<ul id="handle-1">
<li><span class="drag-handle">≡</span>Select text freely</li>
<li><span class="drag-handle">≡</span>Drag my handle</li>
<li><span class="drag-handle">≡</span>Best of both worlds</li>
</ul>
</div>
<div style="clear: both"></div>
</div>
</div>
<a name="ng"></a>
<div id="todos" ng-app="todoApp" class="container" style="margin-top: 100px">
<div style="margin-left: 30px">
<div><div data-force="5" class="layer title title_xl">AngluarJS / ng-sortable</div></div>
<div style="width: 30%; margin-top: -8px; margin-left: 10px; float: left;" class="block__list block__list_words">
<div ng-controller="TodoController">
<span style="padding-left: 20px">{{remaining()}} of {{todos.length}} remaining</span>
[ <a href="" ng-click="archive()">archive</a> ]
<ul ng-sortable="{ group: 'todo', animation: 150 }" class="unstyled">
<li ng-repeat="todo in todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
<form ng-submit="addTodo()" style="padding-left: 20px">
<input type="text" ng-model="todoText" size="30"
placeholder="add new todo here">
</form>
</div>
</div>
<div style="width: 30%; margin-top: -8px; margin-left: 10px; float: left;" class="block__list block__list_words">
<div ng-controller="TodoControllerNext">
<span style="padding-left: 20px">{{remaining()}} of {{todos.length}} remaining</span>
<ul ng-sortable="sortableConfig" class="unstyled">
<li ng-repeat="todo in todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
</div>
</div>
<div style="clear: both"></div>
</div>
</div>
<a name="c"></a> <a name="c"></a>
<div class="container" style="margin-top: 100px"> <div class="container" style="margin-top: 100px">
<div style="margin-left: 30px"> <div style="margin-left: 30px">
@ -128,6 +225,7 @@ new Sortable(bar, { group: "omega" });
// Or // Or
var container = document.getElementById("multi"); var container = document.getElementById("multi");
var sort = new Sortable(container, { var sort = new Sortable(container, {
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
handle: ".tile__title", // Restricts sort start click/touch to the specified element handle: ".tile__title", // Restricts sort start click/touch to the specified element
draggable: ".tile", // Specifies which items inside the element should be sortable draggable: ".tile", // Specifies which items inside the element should be sortable
onUpdate: function (evt/**Event*/){ onUpdate: function (evt/**Event*/){
@ -164,6 +262,8 @@ var editableList = new Sortable(editable, {
<script src="Sortable.js"></script> <script src="Sortable.js"></script>
<script src="//rubaxa.github.io/Ply/Ply.min.js"></script> <script src="//rubaxa.github.io/Ply/Ply.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js"></script>
<script src="ng-sortable.js"></script>
<script> <script>
(function (){ (function (){
@ -178,6 +278,7 @@ var editableList = new Sortable(editable, {
new Sortable(foo, { new Sortable(foo, {
group: "words", group: "words",
animation: 150,
store: { store: {
get: function (sortable) { get: function (sortable) {
var order = localStorage.getItem(sortable.options.group); var order = localStorage.getItem(sortable.options.group);
@ -198,6 +299,7 @@ var editableList = new Sortable(editable, {
new Sortable(bar, { new Sortable(bar, {
group: "words", group: "words",
animation: 150,
onAdd: function (evt){ console.log('onAdd.bar:', evt.item); }, onAdd: function (evt){ console.log('onAdd.bar:', evt.item); },
onUpdate: function (evt){ console.log('onUpdate.bar:', evt.item); }, onUpdate: function (evt){ console.log('onUpdate.bar:', evt.item); },
onRemove: function (evt){ console.log('onRemove.bar:', evt.item); }, onRemove: function (evt){ console.log('onRemove.bar:', evt.item); },
@ -207,12 +309,14 @@ var editableList = new Sortable(editable, {
new Sortable(multi, { new Sortable(multi, {
animation: 150,
draggable: '.tile', draggable: '.tile',
handle: '.tile__name' handle: '.tile__name'
}); });
var editableList = new Sortable(editable, { var editableList = new Sortable(editable, {
animation: 150,
filter: '.js-remove', filter: '.js-remove',
onFilter: function (evt) { onFilter: function (evt) {
var el = editableList.closest(evt.item); var el = editableList.closest(evt.item);
@ -234,8 +338,88 @@ var editableList = new Sortable(editable, {
[].forEach.call(multi.getElementsByClassName('tile__list'), function (el){ [].forEach.call(multi.getElementsByClassName('tile__list'), function (el){
new Sortable(el, { group: 'photo' }); new Sortable(el, {
group: 'photo',
animation: 150
});
}); });
[{
name: 'advanced',
pull: true,
put: true
},
{
name: 'advanced',
pull: 'clone',
put: false
}, {
name: 'advanced',
pull: false,
put: true
}].forEach(function (groupOpts, i) {
new Sortable(document.getElementById('advanced-' + (i + 1)), {
sort: (i != 1),
group: groupOpts,
animation: 150
});
});
new Sortable(document.getElementById('handle-1'), {
handle: '.drag-handle',
animation: 150
});
angular.module('todoApp', ['ng-sortable'])
.controller('TodoController', ['$scope', function ($scope) {
$scope.todos = [
{text: 'learn angular', done: true},
{text: 'build an angular app', done: false}
];
$scope.addTodo = function () {
$scope.todos.push({text: $scope.todoText, done: false});
$scope.todoText = '';
};
$scope.remaining = function () {
var count = 0;
angular.forEach($scope.todos, function (todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.archive = function () {
var oldTodos = $scope.todos;
$scope.todos = [];
angular.forEach(oldTodos, function (todo) {
if (!todo.done) $scope.todos.push(todo);
});
};
}])
.controller('TodoControllerNext', ['$scope', function ($scope) {
$scope.todos = [
{text: 'learn Sortable', done: true},
{text: 'use ng-sortable', done: false},
{text: 'Enjoy', done: false}
];
$scope.remaining = function () {
var count = 0;
angular.forEach($scope.todos, function (todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.sortableConfig = { group: 'todo', animation: 150 };
'Start End Add Update Remove Sort'.split(' ').forEach(function (name) {
$scope.sortableConfig['on' + name] = console.log.bind(console, name);
});
}]);
})(); })();

117
ng-sortable.js

@ -0,0 +1,117 @@
/**
* @author RubaXa <trash@rubaxa.org>
* @licence MIT
*/
angular.module('ng-sortable', [])
.constant('$version', '0.1.0')
.directive('ngSortable', ['$parse', '$rootScope', function ($parse, $rootScope) {
"use strict";
var removed;
function getSource(el) {
var scope = angular.element(el).scope();
var ngRepeat = [].filter.call(el.childNodes, function (node) {
return (
(node.nodeType === 8) &&
(node.nodeValue.indexOf("ngRepeat:") !== -1)
);
})[0];
ngRepeat = ngRepeat.nodeValue.match(/ngRepeat:\s*([^\s]+)\s+in\s+([^\s|]+)/);
var item = $parse(ngRepeat[1]);
var items = $parse(ngRepeat[2]);
return {
item: function (el) {
return item(angular.element(el).scope());
},
items: items(scope),
upd: function () {
items.assign(scope, this.items);
}
};
}
return {
restrict: 'AC',
link: function (scope, $el, attrs) {
var el = $el[0];
var options = scope.$eval(attrs.ngSortable) || {};
var _order = [];
var source = getSource(el);
'Start End Add Update Remove Sort'.split(' ').forEach(function (name) {
options['on' + name] = options['on' + name] || function () {};
});
function _sync(evt) {
sortable.toArray().forEach(function (id, i) {
if (_order[i] !== id) {
var idx = _order.indexOf(id);
if (idx === -1) {
var remoteSource = getSource(evt.from);
idx = remoteSource.items.indexOf(remoteSource.item(evt.item));
removed = remoteSource.items.splice(idx, 1)[0];
_order.splice(i, 0, id);
source.items.splice(i, 0, removed);
remoteSource.upd();
evt.from.appendChild(evt.item); // revert element
} else {
_order.splice(i, 0, _order.splice(idx, 1)[0]);
source.items.splice(i, 0, source.items.splice(idx, 1)[0]);
}
}
});
source.upd();
scope.$apply();
}
var sortable = Sortable.create(el, Object.keys(options).reduce(function (opts, name) {
opts[name] = opts[name] || options[name];
return opts;
}, {
onStart: function () {
$rootScope.$broadcast('sortable:start', sortable);
options.onStart();
},
onEnd: function () {
$rootScope.$broadcast('sortable:end', sortable);
options.onEnd();
},
onAdd: function (evt) {
_sync(evt);
options.onAdd(source.items, removed);
},
onUpdate: function (evt) {
_sync(evt);
options.onUpdate(source.items, source.item(evt.item));
},
onRemove: function (evt) {
options.onRemove(source.items, removed);
},
onSort: function () {
options.onSort(source.items);
}
}));
$rootScope.$on('sortable:start', function () {
_order = sortable.toArray();
});
$el.on('$destroy', function () {
el.sortable = null;
sortable.destroy();
});
}
};
}])
;

7
package.json

@ -1,12 +1,13 @@
{ {
"name": "sortable", "name": "sortable",
"exportName": "Sortable", "exportName": "Sortable",
"version": "0.5.2", "version": "0.7.0",
"devDependencies": { "devDependencies": {
"grunt": "*", "grunt": "*",
"grunt-version": "*", "grunt-version": "*",
"grunt-contrib-uglify": "*", "grunt-shell": "*",
"grunt-shell": "*" "grunt-contrib-jshint": "0.9.2",
"grunt-contrib-uglify": "*"
}, },
"description": "Minimalist library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery.", "description": "Minimalist library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery.",
"main": "Sortable.js", "main": "Sortable.js",

20
st/app.css

@ -126,6 +126,12 @@ img {
margin-left: 5px; margin-left: 5px;
background-color: #fff; background-color: #fff;
} }
.block__list-title {
margin: -20px 0 0;
padding: 10px;
text-align: center;
background: #5F9EDF;
}
.block__list li { cursor: move; } .block__list li { cursor: move; }
.block__list_words li { .block__list_words li {
@ -209,3 +215,17 @@ img {
#filter .block__list { #filter .block__list {
padding-bottom: 0; padding-bottom: 0;
} }
.drag-handle {
margin-right: 0.25em;
color: blue;
cursor: move;
cursor: -webkit-grabbing; /* overrides 'move' */
}
#todos input {
padding: 5px;
font-size: 14px;
font-family: 'Roboto', sans-serif;
font-weight: 300;
}

Loading…
Cancel
Save