Browse Source

Merge branch 'dev' into gh-pages

pull/191/head
RubaXa 10 years ago
parent
commit
d9be889039
  1. 24
      .jshintrc
  2. 38
      Gruntfile.js
  3. 249
      README.md
  4. 439
      Sortable.js
  5. 4
      Sortable.min.js
  6. 2
      bower.json
  7. 2
      component.json
  8. 28
      index.html
  9. 26
      meteor/README.md
  10. 20
      meteor/package.js
  11. 75
      meteor/publish.sh
  12. 35
      meteor/runtests.sh
  13. 108
      ng-sortable.js
  14. 6
      package.json

24
.jshintrc

@ -0,0 +1,24 @@
{
"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
}
}

38
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,24 +8,50 @@ module.exports = function (grunt){
src: ['<%= pkg.exportName %>.js', '*.json']
},
jshint: {
all: ['*.js', '!*.min.js'],
options: {
jshintrc: true
}
},
uglify: {
options: {
banner: '/*! <%= pkg.exportName %> <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n'
},
dist: {
files: {
'<%= pkg.exportName %>.min.js': ['<%= pkg.exportName %>.js']
'<%= pkg.exportName %>.min.js': ['<%= pkg.exportName %>.js']
}
}
},
shell: {
'meteor-test': {
command: 'meteor/runtests.sh'
},
'meteor-publish': {
command: 'meteor/publish.sh'
}
}
});
// These plugins provide necessary tasks.
grunt.loadNpmTasks('grunt-version');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-shell');
// Meteor tasks
grunt.registerTask('meteor-test', 'shell:meteor-test');
grunt.registerTask('meteor-publish', 'shell:meteor-publish');
// ideally we'd run tests before publishing, but the chances of tests breaking (given that
// Meteor is orthogonal to the library) are so small that it's not worth the maintainer's time
// 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']);
};

249
README.md

@ -1,13 +1,17 @@
# 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
* Can drag from one list to another or within the same list
* Animation moving items when sorting (css animation)
* CSS animation when moving items
* Supports drag handles *and selectable text* (better than voidberg's html5sortable)
* Built using native HTML5 drag and drop API
* Support [AngularJS](#ng)
* Supports [AngularJS](#ng) and and any CSS library, e.g. [Bootstrap](#bs)
* Simple API
* No jQuery
@ -23,76 +27,179 @@ Sortable is a minimalist JavaScript library for reorderable drag-and-drop lists.
```js
var el = document.getElementById('items');
Sortable.create(el);
var sortable = Sortable.create(el);
```
You can use any element for the list and its elements, not just `ul`/`li`. Here is an [example with `div`s](http://jsbin.com/luxero/2/edit?html,js,output).
---
### Options
```js
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", // Restricts sort start click/touch to the specified element
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",
var sortable = new Sortable(el, {
group: "name", // or { name: "...", pull: [true, false, clone], put: [true, false, array] }
sort: true, // sorting inside list
disabled: false, // Disables the sortable if set to true.
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)
draggable: ".item", // Specifies which items inside the element should be sortable
ghostClass: "sortable-ghost", // Class name for the drop placeholder - jsbin.com/luxero/3
setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent);
},
onStart: function (/**Event*/evt) { /* dragging */ },
onEnd: function (/**Event*/evt) { /* dragging */ },
// dragging started
onStart: function (/**Event*/evt) {
evt.oldIndex; // element index within parent
},
// dragging ended
onEnd: function (/**Event*/evt) {
evt.oldIndex; // element's old index within parent
evt.newIndex; // element's new index within parent
},
// Element is added to the list
onAdd: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement
itemEl.from; // previous list
// Element is dropped into the list from another list
onAdd: function (/**Event*/evt) {
var itemEl = evt.item; // dragged HTMLElement
itemEl.from; // previous list
// + indexes from onEnd
},
// Changed sorting in list
onUpdate: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement
// Changed sorting within list
onUpdate: function (/**Event*/evt) {
var itemEl = evt.item; // dragged HTMLElement
// + indexes from onEnd
},
// Called by any change to the list (add / update / remove)
onSort: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement
onSort: function (/**Event*/evt) {
// same properties as onUpdate
},
// The element is removed from the list
onRemove: function (/**Event*/evt){
var itemEl = evt.item; // dragged HTMLElement
// Element is removed from the list into another list
onRemove: function (/**Event*/evt) {
// same properties as onUpdate
},
onFilter: function (/**Event*/evt){
var itemEl = evt.item; // HTMLElement on which was `mousedown|tapstart` event.
// Attempt to drag a filtered element
onFilter: function (/**Event*/evt) {
var itemEl = evt.item; // HTMLElement receiving the `mousedown|tapstart` event.
}
});
```
---
#### `group` option
To drag elements from one list into another, both lists must have the same `group` value.
You can also define whether lists can give away, give and keep a copy (`clone`), and receive elements.
* name: `String` — group name
* pull: `true|false|'clone'` — ability to move from the list. `clone` — copy the item, rather than move.
* put: `true|false|["foo", "bar"]` — whether elements can be added from other lists, or an array of group names from which elements can be taken. Demo: http://jsbin.com/naduvo/2/edit?html,js,output
---
#### `sort` option
Sorting inside list
Demo: http://jsbin.com/xizeh/2/edit?html,js,output
---
#### `disabled` options
Disables the sortable if set to `true`.
Demo: http://jsbin.com/xiloqu/1/edit?html,js,output
```js
var sortable = Sortable.create(list);
document.getElementById("switcher").onclick = function () {
var state = sortable.option("disabled"); // get
sortable.option("disabled", !state); // set
};
```
---
#### `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.
Demo: http://jsbin.com/newize/1/edit?html,js,output
```js
Sortable.create(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
</ul>
```
```css
.my-handle {
cursor: move;
cursor: -webkit-grabbing;
}
```
---
### `group` option
#### `filter` 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.
```js
Sortable.create(list, {
filter: ".js-remove, .js-edit",
onFilter: function (evt) {
var item = el.item,
ctrl = evt.target;
if (Sortable.utils.is(ctrl, ".js-remove")) { // Click on remove button
item.parentNode.removeChild(item); // remove sortable item
}
else if (Sortable.utils.is(ctrl, ".js-edit")) { // Click on edit link
// ...
}
}
})
```
---
<a name="ng"></a>
### Support AngularJS
Include [ng-sortable.js](ng-sortable.js)
Demo: http://jsbin.com/naduvo/1/edit?html,js,output
```html
<div ng-app"myApp">
<div ng-app="myApp" ng-controller="demo">
<ul ng-sortable>
<li ng-repeat="item in items">{{item}}</li>
</ul>
@ -110,36 +217,28 @@ Include [ng-sortable.js](ng-sortable.js)
```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 };
});
.controller('demo', ['$scope', function ($scope) {
$scope.items = ['item 1', 'item 2'];
$scope.foo = ['foo 1', '..'];
$scope.bar = ['bar 1', '..'];
$scope.barConfig = { group: 'foobar', animation: 150 };
}]);
```
---
### Method
##### 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.
```js
var editableList = new Sortable(list, {
filter: ".js-remove, .js-edit",
onFilter: function (evt) {
var el = editableList.closest(evt.item); // list item
##### option(name:`String`[, value:`*`]):`*`
Get or set the option.
if (editableList.closest(evt.item, ".js-remove")) { // Click on remove button
el.parentNode.removeChild(el); // remove sortable item
}
else if (editableList.closest(evt.item, ".js-edit")) { // Click on edit link
// ...
}
}
})
```
##### 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.
##### toArray():`String[]`
@ -148,6 +247,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
@ -155,6 +255,7 @@ sortable.sort(order.reverse()); // apply
##### destroy()
Removes the sortable functionality completely.
---
@ -178,7 +279,7 @@ Sortable.create(el, {
/**
* Get the order of elements. Called once during initialization.
* @param {Sortable} sortable
* @retruns {Array}
* @returns {Array}
*/
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group);
@ -186,7 +287,7 @@ Sortable.create(el, {
},
/**
* Save the order of elements. Called every time at the drag end.
* Save the order of elements. Called onEnd (when the item is dropped).
* @param {Sortable} sortable
*/
set: function (sortable) {
@ -198,6 +299,36 @@ Sortable.create(el, {
```
---
<a name="bs"></a>
### Bootstrap
Demo: http://jsbin.com/luxero/2/edit?html,js,output
```html
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"/>
<!-- Latest Sortable -->
<script src="http://rubaxa.github.io/Sortable/Sortable.js"></script>
<!-- Simple List -->
<ul id="simpleList" class="list-group">
<li class="list-group-item">This is <a href="http://rubaxa.github.io/Sortable/">Sortable</a></li>
<li class="list-group-item">It works with Bootstrap...</li>
<li class="list-group-item">...out of the box.</li>
<li class="list-group-item">It has support for touch devices.</li>
<li class="list-group-item">Just drag some elements around.</li>
</ul>
<script>
// Simple list
Sortable.create(simpleList, { /* options */ });
</script>
```
---
@ -222,7 +353,7 @@ Sortable.create(el, {
## MIT LICENSE
Copyright 2013 Lebedev Konstantin <ibnRubaXa@gmail.com>
Copyright 2013-2014 Lebedev Konstantin <ibnRubaXa@gmail.com>
http://rubaxa.github.io/Sortable/
Permission is hereby granted, free of charge, to any person obtaining

439
Sortable.js

@ -5,64 +5,69 @@
*/
(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
, cloneEl
, rootEl
, nextEl
var dragEl,
startIndex,
ghostEl,
cloneEl,
rootEl,
nextEl,
, lastEl
, lastCSS
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,
, _dispatchEvent = function (rootEl, name, targetEl, fromEl) {
_dispatchEvent = function (rootEl, name, targetEl, fromEl, startIndex, newIndex) {
var evt = document.createEvent('Event');
evt.initEvent(name, true, true);
evt.item = targetEl || rootEl;
evt.from = fromEl || rootEl;
evt.oldIndex = startIndex;
evt.newIndex = newIndex;
rootEl.dispatchEvent(evt);
}
},
, _customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' ')
_customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' '),
, noop = function (){}
, slice = [].slice
noop = function () {},
slice = [].slice,
, touchDragOverListeners = []
touchDragOverListeners = []
;
@ -72,7 +77,7 @@
* @param {HTMLElement} el
* @param {Object} [options]
*/
function Sortable(el, options){
function Sortable(el, options) {
this.el = el; // root element
this.options = options = (options || {});
@ -81,6 +86,7 @@
var defaults = {
group: Math.random(),
sort: true,
disabled: false,
store: null,
handle: null,
draggable: el.children[0] && el.children[0].nodeName || (/[uo]l/i.test(el.nodeName) ? 'li' : '*'),
@ -124,8 +130,8 @@
// 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]);
}
}
@ -150,56 +156,62 @@
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,
originalTarget = target,
options = this.options,
el = this.el,
filter = options.filter;
if( evt.type === 'mousedown' && evt.button !== 0 ) {
return; // only left button
if (evt.type === 'mousedown' && evt.button !== 0 || options.disabled) {
return; // only left button or enabled
}
if (options.handle) {
target = _closest(target, options.handle, el);
}
target = _closest(target, options.draggable, el);
// get the index of the dragged element within its parent
startIndex = _index(target);
// Check filter
if( typeof filter === 'function' ){
if( filter.call(this, target, this) ){
_dispatchEvent(el, 'filter', target);
if (typeof filter === 'function') {
if (filter.call(this, evt, target, this)) {
_dispatchEvent(originalTarget, 'filter', target, el, startIndex);
return; // cancel dnd
}
}
else if( filter ){
filter = filter.split(',').filter(function (criteria) {
return _closest(target, criteria.trim(), el);
else if (filter) {
filter = filter.split(',').some(function (criteria) {
criteria = _closest(originalTarget, criteria.trim(), el);
if (criteria) {
_dispatchEvent(criteria, 'filter', target, el, startIndex);
return true;
}
});
if (filter.length) {
_dispatchEvent(el, 'filter', target);
return; // cancel dnd
}
}
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;
@ -214,12 +226,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);
@ -236,15 +248,17 @@
try {
if( document.selection ){
if (document.selection) {
document.selection.empty();
} else {
window.getSelection().removeAllRanges()
window.getSelection().removeAllRanges();
}
} catch (err){ }
} catch (err) {
}
_dispatchEvent(dragEl, 'start');
// Drag start event
_dispatchEvent(rootEl, 'start', dragEl, rootEl, startIndex);
if (activeGroup.pull == 'clone') {
@ -255,21 +269,19 @@
}
},
_emulateDragOver: function (){
if( touchEvt ){
_emulateDragOver: function () {
if (touchEvt) {
_css(ghostEl, 'display', 'none');
var
target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY)
, parent = target
, groupName = this.options.group.name
, 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] === groupName ){
while( i-- ){
if (parent[expando] === groupName) {
while (i--) {
touchDragOverListeners[i]({
clientX: touchEvt.clientX,
clientY: touchEvt.clientY,
@ -277,12 +289,14 @@
rootEl: parent
});
}
break;
}
target = parent; // store last element
}
while( parent = parent.parentNode );
/* jshint boss:true */
while (parent = parent.parentNode);
}
_css(ghostEl, 'display', '');
@ -290,14 +304,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;
@ -311,18 +323,16 @@
},
_onDragStart: function (evt/**Event*/, isTouch/**Boolean*/){
_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);
@ -338,8 +348,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);
@ -359,7 +369,7 @@
},
_onDragOver: function (evt/**Event*/){
_onDragOver: function (/**Event*/evt) {
var el = this.el,
target,
dragRect,
@ -367,14 +377,20 @@
options = this.options,
group = options.group,
groupPut = group.put,
isOwner = (activeGroup === group);
if( !_silent &&
(activeGroup.name === group.name || groupPut && groupPut.indexOf && groupPut.indexOf(activeGroup.name) > -1) &&
(isOwner && (options.sort || (revert = !rootEl.contains(dragEl))) || groupPut && activeGroup.pull) &&
isOwner = (activeGroup === group),
canSort = options.sort;
if (!_silent &&
(isOwner
? canSort || (revert = !rootEl.contains(dragEl))
: activeGroup.pull && groupPut && (
(activeGroup.name === group.name) || // by Name
(groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
)
) &&
(evt.rootEl === void 0 || evt.rootEl === this.el)
){
target = _closest(evt.target, this.options.draggable, el);
) {
target = _closest(evt.target, options.draggable, el);
dragRect = dragEl.getBoundingClientRect();
if (cloneEl && (cloneEl.state !== isOwner)) {
@ -383,53 +399,64 @@
cloneEl.state = isOwner;
}
if (revert && cloneEl) {
rootEl.insertBefore(dragEl, cloneEl);
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) && _ghostInBottom(el, evt)
){
target && (targetRect = target.getBoundingClientRect());
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();
}
el.appendChild(dragEl);
this._animate(dragRect, dragEl);
target && this._animate(targetRect, target);
}
else if( target && !target.animated && 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);
}
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) > .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);
}
@ -468,7 +495,7 @@
_off(document, 'touchcancel', this._onDrop);
},
_onDrop: function (evt/**Event*/){
_onDrop: function (/**Event*/evt) {
clearInterval(this._loopId);
// Unbind events
@ -481,35 +508,39 @@
this._offUpEvents();
if( evt ){
if (evt) {
evt.preventDefault();
evt.stopPropagation();
ghostEl && ghostEl.parentNode.removeChild(ghostEl);
if( dragEl ){
if (dragEl) {
// get the index of the dragged element within its parent
var newIndex = _index(dragEl);
_disableDraggable(dragEl);
_toggleClass(dragEl, this.options.ghostClass, false);
if( !rootEl.contains(dragEl) ){
_dispatchEvent(dragEl, 'sort');
_dispatchEvent(rootEl, 'sort');
if (!rootEl.contains(dragEl)) {
// drag from one list and drop into another
_dispatchEvent(dragEl.parentNode, 'sort', dragEl, rootEl, startIndex, newIndex);
_dispatchEvent(rootEl, 'sort', dragEl, rootEl, startIndex, newIndex);
// Add event
_dispatchEvent(dragEl, 'add', dragEl, rootEl);
_dispatchEvent(dragEl, 'add', dragEl, rootEl, startIndex, newIndex);
// Remove event
_dispatchEvent(rootEl, 'remove', dragEl);
_dispatchEvent(rootEl, 'remove', dragEl, rootEl, startIndex, newIndex);
}
else if( dragEl.nextSibling !== nextEl ){
// Update event
_dispatchEvent(dragEl, 'update');
_dispatchEvent(dragEl, 'sort');
else if (dragEl.nextSibling !== nextEl) {
// drag & drop within the same list
_dispatchEvent(rootEl, 'update', dragEl, rootEl, startIndex, newIndex);
_dispatchEvent(rootEl, 'sort', dragEl, rootEl, startIndex, newIndex);
cloneEl && cloneEl.parentNode.removeChild(cloneEl);
}
_dispatchEvent(rootEl, 'end');
// Drag end event
_dispatchEvent(rootEl, 'end', dragEl, rootEl, startIndex, newIndex);
}
// Set NULL
@ -542,8 +573,7 @@
el,
children = this.el.children,
i = 0,
n = children.length
;
n = children.length;
for (; i < n; i++) {
el = children[i];
@ -592,6 +622,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
*/
@ -610,7 +657,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');
});
@ -623,81 +670,79 @@
};
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(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/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)
){
return el;
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;
return null;
}
function _globalDragOver(evt){
function _globalDragOver(/**Event*/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){
function _css(el, prop, val) {
var style = el && el.style;
if( 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, '');
}
else if( el.currentStyle ){
val = el.currentStyle;
else if (el.currentStyle) {
val = el.currentStyle;
}
return prop === void 0 ? val : val[prop];
@ -713,33 +758,37 @@
}
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 list;
}
return [];
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
}
@ -752,8 +801,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);
@ -762,6 +810,19 @@
return sum.toString(36);
}
/**
* Returns the index of an element within its parent
* @param el
* @returns {number}
* @private
*/
function _index(/**HTMLElement*/el) {
var index = 0;
while (el && (el = el.previousElementSibling)) {
index++;
}
return index;
}
// Export utils
Sortable.utils = {
@ -770,13 +831,17 @@
css: _css,
find: _find,
bind: _bind,
is: function (el, selector) {
return !!_closest(el, selector, el);
},
closest: _closest,
toggleClass: _toggleClass,
dispatchEvent: _dispatchEvent
dispatchEvent: _dispatchEvent,
index: _index
};
Sortable.version = '0.6.0';
Sortable.version = '0.7.2';
/**
@ -785,7 +850,7 @@
* @param {Object} [options]
*/
Sortable.create = function (el, options) {
return new Sortable(el, options)
return new Sortable(el, options);
};
// Export

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.6.0",
"version": "0.7.2",
"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.6.0",
"version": "0.7.2",
"homepage": "http://rubaxa.github.io/Sortable/",
"repo": "RubaXa/Sortable",
"authors": [

28
index.html

@ -275,7 +275,7 @@ var editableList = new Sortable(editable, {
}
new Sortable(foo, {
Sortable.create(foo, {
group: "words",
animation: 150,
store: {
@ -288,15 +288,16 @@ var editableList = new Sortable(editable, {
localStorage.setItem(sortable.options.group, order.join('|'));
}
},
onAdd: function (evt){ console.log('onAdd.foo:', evt.item); },
onUpdate: function (evt){ console.log('onUpdate.foo:', evt.item); },
onRemove: function (evt){ console.log('onRemove.foo:', evt.item); },
onStart:function(evt){ console.log('onStart.foo:',evt.item);},
onEnd: function(evt){ console.log('onEnd.foo:', evt.item);}
onAdd: function (evt){ console.log('onAdd.foo:', [evt.item, evt.from]); },
onUpdate: function (evt){ console.log('onUpdate.foo:', [evt.item, evt.from]); },
onRemove: function (evt){ console.log('onRemove.foo:', [evt.item, evt.from]); },
onStart:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onSort:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onEnd: function(evt){ console.log('onEnd.foo:', [evt.item, evt.from]);}
});
new Sortable(bar, {
Sortable.create(bar, {
group: "words",
animation: 150,
onAdd: function (evt){ console.log('onAdd.bar:', evt.item); },
@ -307,19 +308,18 @@ var editableList = new Sortable(editable, {
});
new Sortable(multi, {
Sortable.create(multi, {
animation: 150,
draggable: '.tile',
handle: '.tile__name'
});
var editableList = new Sortable(editable, {
var editableList = Sortable.create(editable, {
animation: 150,
filter: '.js-remove',
onFilter: function (evt) {
var el = editableList.closest(evt.item);
el && el.parentNode.removeChild(el);
evt.item.parentNode.removeChild(evt.item);
}
});
@ -337,7 +337,7 @@ var editableList = new Sortable(editable, {
[].forEach.call(multi.getElementsByClassName('tile__list'), function (el){
new Sortable(el, {
Sortable.create(el, {
group: 'photo',
animation: 150
});
@ -358,14 +358,14 @@ var editableList = new Sortable(editable, {
pull: false,
put: true
}].forEach(function (groupOpts, i) {
new Sortable(document.getElementById('advanced-' + (i + 1)), {
Sortable.create(document.getElementById('advanced-' + (i + 1)), {
sort: (i != 1),
group: groupOpts,
animation: 150
});
});
new Sortable(document.getElementById('handle-1'), {
Sortable.create(document.getElementById('handle-1'), {
handle: '.drag-handle',
animation: 150
});

26
meteor/README.md

@ -0,0 +1,26 @@
Packaging [Sortable](http://rubaxa.github.io/Sortable/) for [Meteor.js](http://meteor.com).
# Meteor
If you're new to Meteor, here's what the excitement is all about -
[watch the first two minutes](https://www.youtube.com/watch?v=fsi0aJ9yr2o); you'll be hooked by 1:28.
That screencast is from 2012. In the meantime, Meteor has become a mature JavaScript-everywhere web
development framework. Read more at [Why Meteor](http://www.meteorpedia.com/read/Why_Meteor).
# Issues
If you encounter an issue while using this package, please CC @dandv when you file it in this repo.
# DONE
* Instantiation test
# TODO
* Meteor collection backing
* Tests ensuring correct rendering with Meteor dynamic templates

20
meteor/package.js

@ -1,24 +1,30 @@
var packageName = 'rubaxa:sortable';
// package metadata file for Meteor.js
'use strict';
var packageName = 'rubaxa:sortable'; // http://atmospherejs.com/sortable/sortable
var where = 'client'; // where to install: 'client', 'server', or ['client', 'server']
var packageJson = JSON.parse(Npm.require("fs").readFileSync('package.json'));
Package.describe({
name: packageName,
summary: 'Sortable (official): minimalist reorderable drag-and-drop lists on modern browsers and touch devices',
version: '0.5.2',
version: packageJson.version,
git: 'https://github.com/RubaXa/Sortable.git'
});
Package.onUse(function (api) {
api.versionsFrom('0.9.0');
api.versionsFrom('METEOR@0.9.0');
api.export('Sortable');
api.addFiles([
'Sortable.js'
], 'client'
], where
);
});
Package.onTest(function (api) {
api.use(packageName, 'client');
api.use('tinytest', 'client');
api.use(packageName, where);
api.use('tinytest', where);
api.addFiles('meteor/test.js', 'client');
api.addFiles('meteor/test.js', where);
});

75
meteor/publish.sh

@ -1,23 +1,72 @@
#!/bin/bash
# Publish package on Meteor's Atmosphere.js
# Make sure Meteor is installed, per https://www.meteor.com/install. The curl'ed script is totally safe; takes 2 minutes to read its source and check.
type meteor >/dev/null 2>&1 || { curl https://install.meteor.com/ | sh; }
# sanity check: make sure we're in the root directory of the checkout
DIR=$( cd "$( dirname "$0" )" && pwd )
cd $DIR/..
cd "$( dirname "$0" )/.."
# Meteor expects package.js to be in the root directory of the checkout, so copy it there temporarily
cp meteor/package.js ./
# publish package, creating it if it's the first time we're publishing
PACKAGE_NAME=$(grep -i name package.js | head -1 | cut -d "'" -f 2)
PACKAGE_EXISTS=$(meteor search $PACKAGE_NAME 2>/dev/null | wc -l)
function cleanup() {
# we copied the file as package.js, regardless of its original name
rm package.js
if [ $PACKAGE_EXISTS -gt 0 ]; then
meteor publish
else
meteor publish --create
fi
# temporary build files
rm -rf ".build.$PACKAGE_NAME" versions.json
}
rm package.js
# publish separately any package*.js files we have, e.g. package.js, package-compat.js
for PACKAGE_FILE in meteor/package*.js; do
# Meteor expects package.js to be in the root directory of the checkout, so copy there our package file under that name, temporarily
cp $PACKAGE_FILE ./package.js
# publish package, creating it if it's the first time we're publishing
PACKAGE_NAME=$(grep -i name $PACKAGE_FILE | head -1 | cut -d "'" -f 2)
ATMOSPHERE_NAME=${PACKAGE_NAME/://}
echo "Publishing $PACKAGE_NAME..."
# attempt to re-publish the package - the most common operation once the initial release has been made
POTENTIAL_ERROR=$( meteor publish 2>&1 )
if [[ $POTENTIAL_ERROR =~ "There is no package named" ]]; then
# actually this is the first time the package is created, so pass the special --create flag and congratulate the maintainer
echo "Thank you for creating the official Meteor package for this library!"
if meteor publish --create; then
echo "Please post the following to https://github.com/raix/Meteor-community-discussions/issues/14:
--------------------------------------------- 8< --------------------------------------------------------
Happy to announce that I've published the official $PACKAGE_NAME to Atmosphere. Please star!
https://atmospherejs.com/$ATMOSPHERE_NAME
--------------------------------------------- >8 --------------------------------------------------------
"
else
echo "We got an error. Please post it at https://github.com/raix/Meteor-community-discussions/issues/14"
cleanup
exit 1
fi
else
if (( $? > 0 )); then
# the error wasn't that the package didn't exist, so we need to ask for help
echo "We got an error. Please post it at https://github.com/raix/Meteor-community-discussions/issues/14:
--------------------------------------------- 8< --------------------------------------------------------
$POTENTIAL_ERROR
--------------------------------------------- >8 --------------------------------------------------------
"
cleanup
exit 1
else
echo "Thanks for releasing a new version of $PACKAGE_NAME! You can see it at
https://atmospherejs.com/$ATMOSPHERE_NAME"
fi
fi
cleanup
done

35
meteor/runtests.sh

@ -1,28 +1,37 @@
# Test Meteor package before publishing to Atmosphere.js
#!/bin/sh
# Test Meteor package before publishing to Atmospherejs.com
# Make sure Meteor is installed, per https://www.meteor.com/install. The curl'ed script is totally safe; takes 2 minutes to read its source and check.
type meteor >/dev/null 2>&1 || { curl https://install.meteor.com/ | sh; }
# sanity check: make sure we're in the root directory of the checkout
DIR=$( cd "$( dirname "$0" )" && pwd )
cd $DIR/..
# Meteor expects package.js to be in the root directory of the checkout, so copy it there temporarily
cp meteor/package.js ./
cd "$( dirname "$0" )/.."
# run tests and delete the temporary package.js even if Ctrl+C is pressed
int_trap() {
echo
echo "Tests interrupted."
printf "Tests interrupted. Hopefully you verified in the browser that tests pass?\n\n"
}
trap int_trap INT
meteor test-packages ./
# test any package*.js packages we may have, e.g. package.js, package-compat.js
for PACKAGE_FILE in meteor/package*.js; do
PACKAGE_NAME=$(grep -i name $PACKAGE_FILE | head -1 | cut -d "'" -f 2)
echo "Testing $PACKAGE_NAME..."
# Meteor expects package.js to be in the root directory of the checkout, so copy there our package file under that name, temporarily
cp $PACKAGE_FILE ./package.js
# provide an invalid MONGO_URL so Meteor doesn't bog us down with an empty Mongo database
MONGO_URL=mongodb:// meteor test-packages ./
rm -rf ".build.$PACKAGE_NAME"
rm -rf ".build.local-test:$PACKAGE_NAME"
rm versions.json 2>/dev/null
PACKAGE_NAME=$(grep -i name package.js | head -1 | cut -d "'" -f 2)
rm -rf ".build.$PACKAGE_NAME"
rm -rf ".build.local-test:$PACKAGE_NAME"
rm versions.json
rm package.js
rm package.js
done

108
ng-sortable.js

@ -3,8 +3,10 @@
* @licence MIT
*/
angular.module('ng-sortable', [])
.constant('$version', '0.1.0')
.directive('ngSortable', ['$parse', '$rootScope', function ($parse, $rootScope) {
.constant('$version', '0.3.0')
.directive('ngSortable', ['$parse', function ($parse) {
'use strict';
var removed;
function getSource(el) {
@ -12,103 +14,105 @@ angular.module('ng-sortable', [])
var ngRepeat = [].filter.call(el.childNodes, function (node) {
return (
(node.nodeType === 8) &&
(node.nodeValue.indexOf("ngRepeat:") !== -1)
(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]);
var itemExpr = $parse(ngRepeat[1]);
var itemsExpr = $parse(ngRepeat[2]);
return {
item: function (el) {
return item(angular.element(el).scope());
return itemExpr(angular.element(el).scope());
},
items: function () {
return itemsExpr(scope);
},
items: items(scope),
upd: function () {
items.assign(scope, this.items);
itemsExpr.assign(scope, this.items());
}
};
}
// Export
return {
restrict: 'AC',
link: function (scope, $el, attrs) {
var el = $el[0];
var options = scope.$eval(attrs.ngSortable) || {};
var _order = [];
var source = getSource(el);
var el = $el[0],
ngSortable = attrs.ngSortable,
options = scope.$eval(ngSortable) || {},
source = getSource(el),
sortable
;
'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]);
}
}
});
var oldIndex = evt.oldIndex,
newIndex = evt.newIndex,
items = source.items();
if (el !== evt.from) {
var prevSource = getSource(evt.from),
prevItems = prevSource.items();
oldIndex = prevItems.indexOf(prevSource.item(evt.item));
removed = prevItems.splice(oldIndex, 1)[0];
items.splice(newIndex, 0, removed);
prevSource.upd();
evt.from.appendChild(evt.item); // revert element
} else {
items.splice(newIndex, 0, items.splice(oldIndex, 1)[0]);
}
source.upd();
scope.$apply();
}
var sortable = Sortable.create(el, Object.keys(options).reduce(function(opts, name) {
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();
options.onStart(source.items());
},
onEnd:function () {
$rootScope.$broadcast('sortable:end', sortable);
options.onEnd();
onEnd: function () {
options.onEnd(source.items());
},
onAdd: function (evt) {
_sync(evt);
options.onAdd(source.items, removed);
options.onAdd(source.items(), removed);
},
onUpdate: function (evt) {
_sync(evt);
options.onUpdate(source.items, source.item(evt.item));
options.onUpdate(source.items(), source.item(evt.item));
},
onRemove: function (evt) {
options.onRemove(source.items, removed);
onRemove: function () {
options.onRemove(source.items(), removed);
},
onSort: function () {
options.onSort(source.items);
options.onSort(source.items());
}
}));
$rootScope.$on('sortable:start', function () {
_order = sortable.toArray();
});
$el.on('$destroy', function () {
el.sortable = null;
sortable.destroy();
});
if (!/{|}/.test(ngSortable)) { // todo: ugly
angular.forEach(['sort', 'disabled', 'draggable', 'handle', 'animation'], function (name) {
scope.$watch(ngSortable + '.' + name, function (value) {
options[name] = value;
sortable.option(name, value);
});
});
}
}
};
}])

6
package.json

@ -1,10 +1,12 @@
{
"name": "sortable",
"name": "sortablejs",
"exportName": "Sortable",
"version": "0.6.0",
"version": "0.7.2",
"devDependencies": {
"grunt": "*",
"grunt-version": "*",
"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.",

Loading…
Cancel
Save