Browse Source

+ Intermediate version before release

pull/141/head
RubaXa 10 years ago
parent
commit
c8363dc711
  1. 39
      Gruntfile.js
  2. 110
      README.md
  3. 481
      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

39
Gruntfile.js

@ -1,6 +1,6 @@
'use strict';
module.exports = function (grunt) {
'use strict';
module.exports = function (grunt){
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
@ -8,6 +8,35 @@ module.exports = function (grunt){
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: {
options: {
banner: '/*! <%= pkg.exportName %> <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n'
@ -31,8 +60,8 @@ module.exports = function (grunt){
});
// These plugins provide necessary tasks.
grunt.loadNpmTasks('grunt-version');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-shell');
@ -44,6 +73,6 @@ module.exports = function (grunt){
// grunt.regsterTask('meteor', ['shell:meteor-test', 'shell:meteor-publish']);
grunt.registerTask('meteor', 'shell:meteor-publish');
// Default task.
grunt.registerTask('default', ['version', 'uglify']);
grunt.registerTask('tests', ['jshint']);
grunt.registerTask('default', ['tests', 'version', 'uglify']);
};

110
README.md

@ -1,17 +1,19 @@
# Sortable
Sortable is a minimalist JavaScript library for reorderable drag-and-drop lists.
Demo: http://rubaxa.github.io/Sortable/
## Features
* Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers
* Built using native HTML5 drag and drop API
* Can drag from one list to another or within the same list
* Supports drag handles *and selectable text* (better than voidberg's html5sortable)
* Simple API
* Lightweight, 2KB gzipped
* No jQuery
* Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers
* Can drag from one list to another or within the same list
* Animation moving items when sorting (css animation)
* Supports drag handles *and selectable text* (better than voidberg's html5sortable)
* Built using native HTML5 drag and drop API
* Support [AngularJS](#ng)
* Simple API
* No jQuery
### Usage
@ -25,7 +27,7 @@ Demo: http://rubaxa.github.io/Sortable/
```js
var el = document.getElementById('items');
new Sortable(el);
var sortable = Sortable.create(el);
```
@ -34,25 +36,39 @@ new Sortable(el);
### Options
```js
new Sortable(el, {
group: "name",
var sortabel = new Sortable(el, {
group: "name", // or { name: "..", pull: [true, false, clone], put: [true, false, array] }
sort: true, // sorting inside list
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
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
ghostClass: "sortable-ghost",
setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent);
},
onStart: function (/**Event*/evt) { /* dragging */ },
onEnd: function (/**Event*/evt) { /* dragging */ },
// Element is added to the list
onAdd: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement
itemEl.from; // previous list
},
// Changed sorting in list
onUpdate: function (/**Event*/evt){
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){
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.
```js
new Sortable(el, {
var sortable = new Sortable(el, {
handle: ".my-handle",
});
```
```html
<ul>
<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 one
<li><span class="my-handle">::</span> list item text two
</ul>
```
@ -86,10 +106,61 @@ new Sortable(el, {
}
```
---
### Methods
### `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 };
});
```
---
### Method
##### option(name:`String`[, value:`*`]):`*`
Get or set the option.
##### 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.
@ -117,6 +188,7 @@ Serializes the sortable's item `data-id`'s into an array of string.
##### sort(order:`String[]`)
Sorts the elements according to the array.
```js
var order = sortable.toArray();
sortable.sort(order.reverse()); // apply
@ -124,6 +196,7 @@ sortable.sort(order.reverse()); // apply
##### destroy()
Removes the sortable functionality completely.
---
@ -141,7 +214,7 @@ Saving and restoring of the sort.
```
```js
new Sortable(el, {
Sortable.create(el, {
group: "localStorage-example",
store: {
/**
@ -185,6 +258,7 @@ new Sortable(el, {
* toggleClass(el`:HTMLElement`, name`:String`, state`:Boolean`) — add or remove one classes from each element
---

481
Sortable.js

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

2
component.json

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

192
index.html

@ -6,9 +6,9 @@
<title>Sortable. No jQuery.</title>
<meta name="keywords" content="sortable, reorder, list, javascript, html5, drag and drop, dnd, rubaxa"/>
<meta name="description" content="Sortable - is a minimalist JavaScript library for modern browsers and touch devices (No jQuery)."/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<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). Support AngularJS."/>
<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="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"/>
@ -108,6 +108,103 @@
</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>
<div class="container" style="margin-top: 100px">
<div style="margin-left: 30px">
@ -128,6 +225,7 @@ new Sortable(bar, { group: "omega" });
// Or
var container = document.getElementById("multi");
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
draggable: ".tile", // Specifies which items inside the element should be sortable
onUpdate: function (evt/**Event*/){
@ -164,6 +262,8 @@ var editableList = new Sortable(editable, {
<script src="Sortable.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>
(function (){
@ -178,6 +278,7 @@ var editableList = new Sortable(editable, {
new Sortable(foo, {
group: "words",
animation: 150,
store: {
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group);
@ -198,6 +299,7 @@ var editableList = new Sortable(editable, {
new Sortable(bar, {
group: "words",
animation: 150,
onAdd: function (evt){ console.log('onAdd.bar:', evt.item); },
onUpdate: function (evt){ console.log('onUpdate.bar:', evt.item); },
onRemove: function (evt){ console.log('onRemove.bar:', evt.item); },
@ -207,12 +309,14 @@ var editableList = new Sortable(editable, {
new Sortable(multi, {
animation: 150,
draggable: '.tile',
handle: '.tile__name'
});
var editableList = new Sortable(editable, {
animation: 150,
filter: '.js-remove',
onFilter: function (evt) {
var el = editableList.closest(evt.item);
@ -234,8 +338,88 @@ var editableList = new Sortable(editable, {
[].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",
"exportName": "Sortable",
"version": "0.5.2",
"version": "0.7.0",
"devDependencies": {
"grunt": "*",
"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.",
"main": "Sortable.js",

20
st/app.css

@ -125,6 +125,12 @@ img {
margin-top: -8px;
margin-left: 5px;
background-color: #fff;
}
.block__list-title {
margin: -20px 0 0;
padding: 10px;
text-align: center;
background: #5F9EDF;
}
.block__list li { cursor: move; }
@ -209,3 +215,17 @@ img {
#filter .block__list {
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