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({ grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
@ -8,24 +8,50 @@ module.exports = function (grunt){
src: ['<%= pkg.exportName %>.js', '*.json'] src: ['<%= pkg.exportName %>.js', '*.json']
}, },
jshint: {
all: ['*.js', '!*.min.js'],
options: {
jshintrc: 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']
} }
} }
},
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-version');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify'); 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('tests', ['jshint']);
grunt.registerTask('default', ['version', 'uglify']); grunt.registerTask('default', ['tests', 'version', 'uglify']);
}; };

249
README.md

@ -1,13 +1,17 @@
# 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/
## Features ## Features
* Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers * 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
* 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 * 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 * Simple API
* No jQuery * No jQuery
@ -23,76 +27,179 @@ Sortable is a minimalist JavaScript library for reorderable drag-and-drop lists.
```js ```js
var el = document.getElementById('items'); 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 ### Options
```js ```js
var sortabel = new Sortable(el, { var sortable = new Sortable(el, {
group: "name", // or { name: "..", pull: [true, false, clone], put: [true, false, array] } group: "name", // or { name: "...", pull: [true, false, clone], put: [true, false, array] }
sort: true, // sorting inside list sort: true, // sorting inside list
store: null, // @see Store disabled: false, // Disables the sortable if set to true.
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation store: null, // @see Store
handle: ".my-handle", // Restricts sort start click/touch to the specified element animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
filter: ".ignor-elements", // Selectors that do not lead to dragging (String or Function) handle: ".my-handle", // Drag handle selector within list items
draggable: ".item", // Specifies which items inside the element should be sortable filter: ".ignore-elements", // Selectors that do not lead to dragging (String or Function)
ghostClass: "sortable-ghost", 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) { setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent); dataTransfer.setData('Text', dragEl.textContent);
}, },
onStart: function (/**Event*/evt) { /* dragging */ }, // dragging started
onEnd: function (/**Event*/evt) { /* dragging */ }, 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 // Element is dropped into the list from another 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 itemEl.from; // previous list
// + indexes from onEnd
}, },
// Changed sorting in list // Changed sorting within list
onUpdate: function (/**Event*/evt){ onUpdate: function (/**Event*/evt) {
var itemEl = evt.item; // dragged HTMLElement var itemEl = evt.item; // dragged HTMLElement
// + indexes from onEnd
}, },
// Called by any change to the list (add / update / remove) // Called by any change to the list (add / update / remove)
onSort: function (/**Event*/evt){ onSort: function (/**Event*/evt) {
var itemEl = evt.item; // dragged HTMLElement // same properties as onUpdate
}, },
// The element is removed from the list // Element is removed from the list into another list
onRemove: function (/**Event*/evt){ onRemove: function (/**Event*/evt) {
var itemEl = evt.item; // dragged HTMLElement // same properties as onUpdate
}, },
onFilter: function (/**Event*/evt){ // Attempt to drag a filtered element
var itemEl = evt.item; // HTMLElement on which was `mousedown|tapstart` event. 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. ```js
* 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. 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> <a name="ng"></a>
### Support AngularJS ### Support AngularJS
Include [ng-sortable.js](ng-sortable.js) Include [ng-sortable.js](ng-sortable.js)
Demo: http://jsbin.com/naduvo/1/edit?html,js,output
```html ```html
<div ng-app"myApp"> <div ng-app="myApp" ng-controller="demo">
<ul ng-sortable> <ul ng-sortable>
<li ng-repeat="item in items">{{item}}</li> <li ng-repeat="item in items">{{item}}</li>
</ul> </ul>
@ -110,36 +217,28 @@ Include [ng-sortable.js](ng-sortable.js)
```js ```js
angular.module('myApp', ['ng-sortable']) angular.module('myApp', ['ng-sortable'])
.controller(function () { .controller('demo', ['$scope', function ($scope) {
this.items = ['item 1', 'item 2']; $scope.items = ['item 1', 'item 2'];
this.foo = ['foo 1', '..']; $scope.foo = ['foo 1', '..'];
this.bar = ['bar 1', '..']; $scope.bar = ['bar 1', '..'];
this.barConfig = { group: 'foobar', animation: 150 }; $scope.barConfig = { group: 'foobar', animation: 150 };
}); }]);
``` ```
--- ---
### Method ### 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 ##### option(name:`String`[, value:`*`]):`*`
var editableList = new Sortable(list, { Get or set the option.
filter: ".js-remove, .js-edit",
onFilter: function (evt) {
var el = editableList.closest(evt.item); // list item
if (editableList.closest(evt.item, ".js-remove")) { // Click on remove button
el.parentNode.removeChild(el); // remove sortable item
} ##### closest(el:`String`[, selector:`HTMLElement`]):`HTMLElement|null`
else if (editableList.closest(evt.item, ".js-edit")) { // Click on edit link 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[]` ##### toArray():`String[]`
@ -148,6 +247,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
@ -155,6 +255,7 @@ sortable.sort(order.reverse()); // apply
##### destroy() ##### destroy()
Removes the sortable functionality completely.
--- ---
@ -178,7 +279,7 @@ Sortable.create(el, {
/** /**
* Get the order of elements. Called once during initialization. * Get the order of elements. Called once during initialization.
* @param {Sortable} sortable * @param {Sortable} sortable
* @retruns {Array} * @returns {Array}
*/ */
get: function (sortable) { get: function (sortable) {
var order = localStorage.getItem(sortable.options.group); 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 * @param {Sortable} sortable
*/ */
set: function (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 ## MIT LICENSE
Copyright 2013 Lebedev Konstantin <ibnRubaXa@gmail.com> Copyright 2013-2014 Lebedev Konstantin <ibnRubaXa@gmail.com>
http://rubaxa.github.io/Sortable/ http://rubaxa.github.io/Sortable/
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining

439
Sortable.js

@ -5,64 +5,69 @@
*/ */
(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 startIndex,
, ghostEl ghostEl,
, cloneEl cloneEl,
, rootEl rootEl,
, nextEl nextEl,
, lastEl lastEl,
, lastCSS lastCSS,
, 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,
, _dispatchEvent = function (rootEl, name, targetEl, fromEl) { _dispatchEvent = function (rootEl, name, targetEl, fromEl, startIndex, newIndex) {
var evt = document.createEvent('Event'); var evt = document.createEvent('Event');
evt.initEvent(name, true, true); evt.initEvent(name, true, true);
evt.item = targetEl || rootEl; evt.item = targetEl || rootEl;
evt.from = fromEl || rootEl; evt.from = fromEl || rootEl;
evt.oldIndex = startIndex;
evt.newIndex = newIndex;
rootEl.dispatchEvent(evt); rootEl.dispatchEvent(evt);
} },
, _customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' ') _customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' '),
, noop = function (){} noop = function () {},
, slice = [].slice slice = [].slice,
, touchDragOverListeners = [] touchDragOverListeners = []
; ;
@ -72,7 +77,7 @@
* @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 || {});
@ -81,6 +86,7 @@
var defaults = { var defaults = {
group: Math.random(), group: Math.random(),
sort: true, sort: true,
disabled: false,
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' : '*'),
@ -124,8 +130,8 @@
// 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]);
} }
} }
@ -150,56 +156,62 @@
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 originalTarget = 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 || options.disabled) {
return; // only left button 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 // Check filter
if( typeof filter === 'function' ){ if (typeof filter === 'function') {
if( filter.call(this, target, this) ){ if (filter.call(this, evt, target, this)) {
_dispatchEvent(el, 'filter', target); _dispatchEvent(originalTarget, 'filter', target, el, startIndex);
return; // cancel dnd return; // cancel dnd
} }
} }
else if( filter ){ else if (filter) {
filter = filter.split(',').filter(function (criteria) { filter = filter.split(',').some(function (criteria) {
return _closest(target, criteria.trim(), el); criteria = _closest(originalTarget, criteria.trim(), el);
if (criteria) {
_dispatchEvent(criteria, 'filter', target, el, startIndex);
return true;
}
}); });
if (filter.length) { if (filter.length) {
_dispatchEvent(el, 'filter', target);
return; // cancel dnd return; // cancel dnd
} }
} }
if( options.handle ){
target = _closest(target, options.handle, 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;
@ -214,12 +226,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);
@ -236,15 +248,17 @@
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'); // Drag start event
_dispatchEvent(rootEl, 'start', dragEl, rootEl, startIndex);
if (activeGroup.pull == 'clone') { if (activeGroup.pull == 'clone') {
@ -255,21 +269,19 @@
} }
}, },
_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,
, groupName = this.options.group.name i = touchDragOverListeners.length;
, i = touchDragOverListeners.length
;
if( parent ){ if (parent) {
do { do {
if( parent[expando] === groupName ){ if (parent[expando] === groupName) {
while( i-- ){ while (i--) {
touchDragOverListeners[i]({ touchDragOverListeners[i]({
clientX: touchEvt.clientX, clientX: touchEvt.clientX,
clientY: touchEvt.clientY, clientY: touchEvt.clientY,
@ -277,12 +289,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', '');
@ -290,14 +304,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;
@ -311,18 +323,16 @@
}, },
_onDragStart: function (evt/**Event*/, isTouch/**Boolean*/){ _onDragStart: function (/**Event*/evt, /**boolean*/isTouch) {
var dataTransfer = evt.dataTransfer, var dataTransfer = evt.dataTransfer,
options = this.options; 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);
@ -338,8 +348,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);
@ -359,7 +369,7 @@
}, },
_onDragOver: function (evt/**Event*/){ _onDragOver: function (/**Event*/evt) {
var el = this.el, var el = this.el,
target, target,
dragRect, dragRect,
@ -367,14 +377,20 @@
options = this.options, options = this.options,
group = options.group, group = options.group,
groupPut = group.put, groupPut = group.put,
isOwner = (activeGroup === group); isOwner = (activeGroup === group),
canSort = options.sort;
if( !_silent &&
(activeGroup.name === group.name || groupPut && groupPut.indexOf && groupPut.indexOf(activeGroup.name) > -1) && if (!_silent &&
(isOwner && (options.sort || (revert = !rootEl.contains(dragEl))) || groupPut && activeGroup.pull) && (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) (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(); dragRect = dragEl.getBoundingClientRect();
if (cloneEl && (cloneEl.state !== isOwner)) { if (cloneEl && (cloneEl.state !== isOwner)) {
@ -383,53 +399,64 @@
cloneEl.state = isOwner; cloneEl.state = isOwner;
} }
if (revert && cloneEl) { if (revert) {
rootEl.insertBefore(dragEl, cloneEl); if (cloneEl || nextEl) {
rootEl.insertBefore(dragEl, cloneEl || nextEl);
}
else if (!canSort) {
rootEl.appendChild(dragEl);
}
return; return;
} }
if( (el.children.length === 0) || (el.children[0] === ghostEl) || if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
(el === evt.target) && _ghostInBottom(el, evt) (el === evt.target) && (target = _ghostInBottom(el, evt))
){ ) {
target && (targetRect = target.getBoundingClientRect()); if (target) {
if (target.animated) {
return;
}
targetRect = target.getBoundingClientRect();
}
el.appendChild(dragEl); el.appendChild(dragEl);
this._animate(dragRect, dragEl); this._animate(dragRect, dragEl);
target && this._animate(targetRect, target); target && this._animate(targetRect, target);
} }
else if( target && !target.animated && 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);
} }
var targetRect = target.getBoundingClientRect() var targetRect = target.getBoundingClientRect(),
, width = targetRect.right - targetRect.left width = targetRect.right - targetRect.left,
, height = targetRect.bottom - targetRect.top height = targetRect.bottom - targetRect.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) > .5 halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.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(dragRect, dragEl);
this._animate(targetRect, target); this._animate(targetRect, target);
} }
@ -468,7 +495,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
@ -481,35 +508,39 @@
this._offUpEvents(); this._offUpEvents();
if( evt ){ if (evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
ghostEl && ghostEl.parentNode.removeChild(ghostEl); 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); _disableDraggable(dragEl);
_toggleClass(dragEl, this.options.ghostClass, false); _toggleClass(dragEl, this.options.ghostClass, false);
if( !rootEl.contains(dragEl) ){ if (!rootEl.contains(dragEl)) {
_dispatchEvent(dragEl, 'sort'); // drag from one list and drop into another
_dispatchEvent(rootEl, 'sort'); _dispatchEvent(dragEl.parentNode, 'sort', dragEl, rootEl, startIndex, newIndex);
_dispatchEvent(rootEl, 'sort', dragEl, rootEl, startIndex, newIndex);
// Add event // Add event
_dispatchEvent(dragEl, 'add', dragEl, rootEl); _dispatchEvent(dragEl, 'add', dragEl, rootEl, startIndex, newIndex);
// Remove event // Remove event
_dispatchEvent(rootEl, 'remove', dragEl); _dispatchEvent(rootEl, 'remove', dragEl, rootEl, startIndex, newIndex);
} }
else if( dragEl.nextSibling !== nextEl ){ else if (dragEl.nextSibling !== nextEl) {
// Update event // drag & drop within the same list
_dispatchEvent(dragEl, 'update'); _dispatchEvent(rootEl, 'update', dragEl, rootEl, startIndex, newIndex);
_dispatchEvent(dragEl, 'sort'); _dispatchEvent(rootEl, 'sort', dragEl, rootEl, startIndex, newIndex);
cloneEl && cloneEl.parentNode.removeChild(cloneEl); cloneEl && cloneEl.parentNode.removeChild(cloneEl);
} }
_dispatchEvent(rootEl, 'end'); // Drag end event
_dispatchEvent(rootEl, 'end', dragEl, rootEl, startIndex, newIndex);
} }
// Set NULL // Set NULL
@ -542,8 +573,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];
@ -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 * Destroy
*/ */
@ -610,7 +657,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');
}); });
@ -623,81 +670,79 @@
}; };
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(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/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(/**Event*/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) {
var style = el && el.style; var style = el && el.style;
if( style ){ if (style) {
if( val === void 0 ){ if (val === void 0) {
if( document.defaultView && document.defaultView.getComputedStyle ){ 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]; return prop === void 0 ? val : val[prop];
@ -713,33 +758,37 @@
} }
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
} }
@ -752,8 +801,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);
@ -762,6 +810,19 @@
return sum.toString(36); 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 // Export utils
Sortable.utils = { Sortable.utils = {
@ -770,13 +831,17 @@
css: _css, css: _css,
find: _find, find: _find,
bind: _bind, bind: _bind,
is: function (el, selector) {
return !!_closest(el, selector, el);
},
closest: _closest, closest: _closest,
toggleClass: _toggleClass, 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] * @param {Object} [options]
*/ */
Sortable.create = function (el, options) { Sortable.create = function (el, options) {
return new Sortable(el, options) return new Sortable(el, options);
}; };
// Export // 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", "name": "Sortable",
"main": "Sortable.js", "main": "Sortable.js",
"version": "0.6.0", "version": "0.7.2",
"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.6.0", "version": "0.7.2",
"homepage": "http://rubaxa.github.io/Sortable/", "homepage": "http://rubaxa.github.io/Sortable/",
"repo": "RubaXa/Sortable", "repo": "RubaXa/Sortable",
"authors": [ "authors": [

28
index.html

@ -275,7 +275,7 @@ var editableList = new Sortable(editable, {
} }
new Sortable(foo, { Sortable.create(foo, {
group: "words", group: "words",
animation: 150, animation: 150,
store: { store: {
@ -288,15 +288,16 @@ var editableList = new Sortable(editable, {
localStorage.setItem(sortable.options.group, order.join('|')); localStorage.setItem(sortable.options.group, order.join('|'));
} }
}, },
onAdd: function (evt){ console.log('onAdd.foo:', evt.item); }, onAdd: function (evt){ console.log('onAdd.foo:', [evt.item, evt.from]); },
onUpdate: function (evt){ console.log('onUpdate.foo:', evt.item); }, onUpdate: function (evt){ console.log('onUpdate.foo:', [evt.item, evt.from]); },
onRemove: function (evt){ console.log('onRemove.foo:', evt.item); }, onRemove: function (evt){ console.log('onRemove.foo:', [evt.item, evt.from]); },
onStart:function(evt){ console.log('onStart.foo:',evt.item);}, onStart:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onEnd: function(evt){ console.log('onEnd.foo:', evt.item);} 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", group: "words",
animation: 150, animation: 150,
onAdd: function (evt){ console.log('onAdd.bar:', evt.item); }, 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, animation: 150,
draggable: '.tile', draggable: '.tile',
handle: '.tile__name' handle: '.tile__name'
}); });
var editableList = new Sortable(editable, { var editableList = Sortable.create(editable, {
animation: 150, animation: 150,
filter: '.js-remove', filter: '.js-remove',
onFilter: function (evt) { onFilter: function (evt) {
var el = editableList.closest(evt.item); evt.item.parentNode.removeChild(evt.item);
el && el.parentNode.removeChild(el);
} }
}); });
@ -337,7 +337,7 @@ var editableList = new Sortable(editable, {
[].forEach.call(multi.getElementsByClassName('tile__list'), function (el){ [].forEach.call(multi.getElementsByClassName('tile__list'), function (el){
new Sortable(el, { Sortable.create(el, {
group: 'photo', group: 'photo',
animation: 150 animation: 150
}); });
@ -358,14 +358,14 @@ var editableList = new Sortable(editable, {
pull: false, pull: false,
put: true put: true
}].forEach(function (groupOpts, i) { }].forEach(function (groupOpts, i) {
new Sortable(document.getElementById('advanced-' + (i + 1)), { Sortable.create(document.getElementById('advanced-' + (i + 1)), {
sort: (i != 1), sort: (i != 1),
group: groupOpts, group: groupOpts,
animation: 150 animation: 150
}); });
}); });
new Sortable(document.getElementById('handle-1'), { Sortable.create(document.getElementById('handle-1'), {
handle: '.drag-handle', handle: '.drag-handle',
animation: 150 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({ Package.describe({
name: packageName, name: packageName,
summary: 'Sortable (official): minimalist reorderable drag-and-drop lists on modern browsers and touch devices', 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' git: 'https://github.com/RubaXa/Sortable.git'
}); });
Package.onUse(function (api) { Package.onUse(function (api) {
api.versionsFrom('0.9.0'); api.versionsFrom('METEOR@0.9.0');
api.export('Sortable'); api.export('Sortable');
api.addFiles([ api.addFiles([
'Sortable.js' 'Sortable.js'
], 'client' ], where
); );
}); });
Package.onTest(function (api) { Package.onTest(function (api) {
api.use(packageName, 'client'); api.use(packageName, where);
api.use('tinytest', 'client'); 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 # 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. # 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; } 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 # sanity check: make sure we're in the root directory of the checkout
DIR=$( cd "$( dirname "$0" )" && pwd ) cd "$( dirname "$0" )/.."
cd $DIR/..
# 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 function cleanup() {
PACKAGE_NAME=$(grep -i name package.js | head -1 | cut -d "'" -f 2) # we copied the file as package.js, regardless of its original name
PACKAGE_EXISTS=$(meteor search $PACKAGE_NAME 2>/dev/null | wc -l) rm package.js
if [ $PACKAGE_EXISTS -gt 0 ]; then # temporary build files
meteor publish rm -rf ".build.$PACKAGE_NAME" versions.json
else }
meteor publish --create
fi
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. # 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; } 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 # sanity check: make sure we're in the root directory of the checkout
DIR=$( cd "$( dirname "$0" )" && pwd ) cd "$( dirname "$0" )/.."
cd $DIR/..
# Meteor expects package.js to be in the root directory of the checkout, so copy it there temporarily
cp meteor/package.js ./
# run tests and delete the temporary package.js even if Ctrl+C is pressed # run tests and delete the temporary package.js even if Ctrl+C is pressed
int_trap() { int_trap() {
echo echo
echo "Tests interrupted." printf "Tests interrupted. Hopefully you verified in the browser that tests pass?\n\n"
} }
trap int_trap INT 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 package.js
rm -rf ".build.$PACKAGE_NAME"
rm -rf ".build.local-test:$PACKAGE_NAME"
rm versions.json
rm package.js done

108
ng-sortable.js

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

6
package.json

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

Loading…
Cancel
Save