Browse Source

Merge branch 'dev'

pull/298/head
RubaXa 10 years ago
parent
commit
81323dedf9
  1. 6
      .jshintrc
  2. 11
      Gruntfile.js
  3. 98
      README.md
  4. 247
      Sortable.js
  5. 2
      Sortable.min.js
  6. 4
      index.html
  7. 62
      ng-sortable.js
  8. 5
      package.json
  9. 147
      react-sortable-mixin.js
  10. 37
      st/app.js

6
.jshintrc

@ -1,9 +1,9 @@
{
"strict": true,
"newcap": false, // "Tolerate uncapitalized constructors"
"newcap": false,
"node": true,
"expr": true, // - true && call() "Expected an assignment or function call and instead saw an expression."
"supernew": true, // - "Missing '()' invoking a constructor."
"expr": true,
"supernew": true,
"laxbreak": true,
"white": true,
"globals": {

11
Gruntfile.js

@ -5,7 +5,16 @@ module.exports = function (grunt) {
pkg: grunt.file.readJSON('package.json'),
version: {
src: ['<%= pkg.exportName %>.js', '*.json']
js: {
src: ['<%= pkg.exportName %>.js', '*.json']
},
cdn: {
options: {
prefix: '(cdnjs\\.cloudflare\\.com\\/ajax\\/libs\\/Sortable|cdn\\.jsdelivr\\.net\\/sortable)\\/',
replace: '[0-9\\.]+'
},
src: ['README.md']
}
},
jshint: {

98
README.md

@ -6,13 +6,13 @@ Demo: http://rubaxa.github.io/Sortable/
## Features
* Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers
* Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers (including IE9)
* Can drag from one list to another or within the same list
* CSS animation when moving items
* Supports drag handles *and selectable text* (better than voidberg's html5sortable)
* Smart auto-scrolling
* Built using native HTML5 drag and drop API
* Supports [Meteor](meteor/README.md) and [AngularJS](#ng)
* Supports [Meteor](meteor/README.md), [AngularJS](#ng) and [React](#react)
* Supports any CSS library, e.g. [Bootstrap](#bs)
* Simple API
* [CDN](#cdn)
@ -286,7 +286,13 @@ angular.module('myApp', ['ng-sortable'])
$scope.items = ['item 1', 'item 2'];
$scope.foo = ['foo 1', '..'];
$scope.bar = ['bar 1', '..'];
$scope.barConfig = { group: 'foobar', animation: 150 };
$scope.barConfig = {
group: 'foobar',
animation: 150,
onSort: function (/** ngSortEvent */evt){
// @see https://github.com/RubaXa/Sortable/blob/master/ng-sortable.js#L18-L24
}
};
}]);
```
@ -294,6 +300,92 @@ angular.module('myApp', ['ng-sortable'])
---
<a name="react"></a>
### Support React
Include [react-sortable-mixin.js](react-sortable-mixin.js).
See [more options](react-sortable-mixin.js#L26).
```jsx
var SortableList = React.createClass({
mixins: [SortableMixin],
getInitialState: function() {
return {
items: ['Mixin', 'Sortable']
};
},
handleSort: function (/** Event */evt) { /*..*/ },
render: function() {
return <ul>{
this.state.items.map(function (text) {
return <li>{text}</li>
})
}</ul>
}
});
React.render(<SortableList />, document.body);
//
// Groups
//
var AllUsers = React.createClass({
mixins: [SortableMixin],
sortableOptions: {
ref: "user",
group: "shared",
model: "users"
},
getInitialState: function() {
return { users: ['Abbi', 'Adela', 'Bud', 'Cate', 'Davis', 'Eric']; };
},
render: function() {
return (
<h1>Users</h1>
<ul ref="users">{
this.state.users.map(function (text) {
return <li>{text}</li>
})
}</ul>
);
}
});
var ApprovedUsers = React.createClass({
mixins: [SortableMixin],
sortableOptions: { group: "shared" },
getInitialState: function() {
return { items: ['Hal', 'Judy']; };
},
render: function() {
return <ul>{
this.state.items.map(function (text) {
return <li>{text}</li>
})
}</ul>
}
});
React.render(<div>
<AllUsers/>
<hr/>
<ApprovedUsers/>
</div>, document.body);
```
---
### Method

247
Sortable.js

@ -28,9 +28,11 @@
ghostEl,
cloneEl,
rootEl,
scrollEl,
nextEl,
scrollEl,
scrollParentEl,
lastEl,
lastCSS,
@ -48,7 +50,9 @@
win = window,
document = win.document,
parseInt = win.parseInt,
supportIEdnd = !!document.createElement('div').dragDrop,
supportDraggable = !!('draggable' in document.createElement('div')),
_silent = false,
@ -74,7 +78,82 @@
abs = Math.abs,
slice = [].slice,
touchDragOverListeners = []
touchDragOverListeners = [],
_autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
if (rootEl && options.scroll) {
var el,
rect,
sens = options.scrollSensitivity,
speed = options.scrollSpeed,
x = evt.clientX,
y = evt.clientY,
winWidth = window.innerWidth,
winHeight = window.innerHeight,
vx,
vy
;
// Delect scrollEl
if (scrollParentEl !== rootEl) {
scrollEl = options.scroll;
scrollParentEl = rootEl;
if (scrollEl === true) {
scrollEl = rootEl;
do {
if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
(scrollEl.offsetHeight < scrollEl.scrollHeight)
) {
break;
}
/* jshint boss:true */
} while (scrollEl = scrollEl.parentNode);
}
}
if (scrollEl) {
el = scrollEl;
rect = scrollEl.getBoundingClientRect();
vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
}
if (!(vx || vy)) {
vx = (winWidth - x <= sens) - (x <= sens);
vy = (winHeight - y <= sens) - (y <= sens);
/* jshint expr:true */
(vx || vy) && (el = win);
}
if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
autoScroll.el = el;
autoScroll.vx = vx;
autoScroll.vy = vy;
clearInterval(autoScroll.pid);
if (el) {
autoScroll.pid = setInterval(function () {
if (el === win) {
win.scrollTo(win.scrollX + vx * speed, win.scrollY + vy * speed);
} else {
vy && (el.scrollTop += vy * speed);
vx && (el.scrollLeft += vx * speed);
}
}, 24);
}
}
}
}, 30)
;
@ -139,8 +218,9 @@
}, this);
// Export group name
el[expando] = group.name + ' ' + (group.put.join ? group.put.join(' ') : '');
// Export options
options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';
el[expando] = options;
// Bind all private methods
@ -154,10 +234,9 @@
// Bind events
_on(el, 'mousedown', this._onTapStart);
_on(el, 'touchstart', this._onTapStart);
supportIEdnd && _on(el, 'selectstart', this._onTapStart);
_on(el, 'dragover', this._onDragOver);
_on(el, 'dragenter', this._onDragOver);
_on(el, 'dragover', this);
_on(el, 'dragenter', this);
touchDragOverListeners.push(this._onDragOver);
@ -171,13 +250,15 @@
_dragStarted: function () {
// Apply effect
_toggleClass(dragEl, this.options.ghostClass, true);
if (rootEl && dragEl) {
// Apply effect
_toggleClass(dragEl, this.options.ghostClass, true);
Sortable.active = this;
Sortable.active = this;
// Drag start event
_dispatchEvent(rootEl, 'start', dragEl, rootEl, oldIndex);
// Drag start event
_dispatchEvent(rootEl, 'start', dragEl, rootEl, oldIndex);
}
},
@ -229,9 +310,6 @@
// Prepare `dragstart`
if (target && !dragEl && (target.parentNode === el)) {
// IE 9 Support
(type === 'selectstart') && target.dragDrop();
tapEvt = evt;
rootEl = this.el;
@ -254,7 +332,7 @@
clientY: touch.clientY
};
this._onDragStart(tapEvt, true);
this._onDragStart(tapEvt, 'touch');
evt.preventDefault();
}
@ -265,8 +343,9 @@
_on(dragEl, 'dragend', this);
_on(rootEl, 'dragstart', this._onDragStart);
_on(document, 'dragover', this);
if (!supportDraggable) {
this._onDragStart(tapEvt, true);
}
try {
if (document.selection) {
@ -285,12 +364,12 @@
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
parent = target,
groupName = this.options.group.name,
groupName = ' ' + this.options.group.name + '',
i = touchDragOverListeners.length;
if (parent) {
do {
if ((' ' + parent[expando] + ' ').indexOf(groupName) > -1) {
if (parent[expando] && parent[expando].groups.indexOf(groupName) > -1) {
while (i--) {
touchDragOverListeners[i]({
clientX: touchEvt.clientX,
@ -316,10 +395,10 @@
_onTouchMove: function (/**TouchEvent*/evt) {
if (tapEvt) {
var touch = evt.touches[0],
var touch = evt.touches ? evt.touches[0] : evt,
dx = touch.clientX - tapEvt.clientX,
dy = touch.clientY - tapEvt.clientY,
translate3d = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
touchEvt = touch;
@ -328,13 +407,12 @@
_css(ghostEl, 'msTransform', translate3d);
_css(ghostEl, 'transform', translate3d);
this._onDrag(touch);
evt.preventDefault();
}
},
_onDragStart: function (/**Event*/evt, /**boolean*/isTouch) {
_onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
var dataTransfer = evt.dataTransfer,
options = this.options;
@ -346,7 +424,7 @@
rootEl.insertBefore(cloneEl, dragEl);
}
if (isTouch) {
if (useFallback) {
var rect = dragEl.getBoundingClientRect(),
css = _css(dragEl),
ghostRect;
@ -368,10 +446,16 @@
_css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
_css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
// Bind touch events
_on(document, 'touchmove', this._onTouchMove);
_on(document, 'touchend', this._onDrop);
_on(document, 'touchcancel', this._onDrop);
if (useFallback === 'touch') {
// Bind touch events
_on(document, 'touchmove', this._onTouchMove);
_on(document, 'touchend', this._onDrop);
_on(document, 'touchcancel', this._onDrop);
} else {
// Old brwoser
_on(document, 'mousemove', this._onTouchMove);
_on(document, 'mouseup', this._onDrop);
}
this._loopId = setInterval(this._emulateDragOver, 150);
}
@ -384,75 +468,9 @@
_on(document, 'drop', this);
}
scrollEl = options.scroll;
if (scrollEl === true) {
scrollEl = rootEl;
do {
if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
(scrollEl.offsetHeight < scrollEl.scrollHeight)
) {
break;
}
/* jshint boss:true */
} while (scrollEl = scrollEl.parentNode);
}
setTimeout(this._dragStarted, 0);
},
_onDrag: _throttle(function (/**Event*/evt) {
// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
if (rootEl && this.options.scroll) {
var el,
rect,
options = this.options,
sens = options.scrollSensitivity,
speed = options.scrollSpeed,
x = evt.clientX,
y = evt.clientY,
winWidth = window.innerWidth,
winHeight = window.innerHeight,
vx = (winWidth - x <= sens) - (x <= sens),
vy = (winHeight - y <= sens) - (y <= sens)
;
if (vx || vy) {
el = win;
}
else if (scrollEl) {
el = scrollEl;
rect = scrollEl.getBoundingClientRect();
vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
}
if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
autoScroll.el = el;
autoScroll.vx = vx;
autoScroll.vy = vy;
clearInterval(autoScroll.pid);
if (el) {
autoScroll.pid = setInterval(function () {
if (el === win) {
win.scrollTo(win.scrollX + vx * speed, win.scrollY + vy * speed);
} else {
vy && (el.scrollTop += vy * speed);
vx && (el.scrollLeft += vx * speed);
}
}, 24);
}
}
}
}, 30),
_onDragOver: function (/**Event*/evt) {
var el = this.el,
target,
@ -464,12 +482,16 @@
isOwner = (activeGroup === group),
canSort = options.sort;
if (evt.dataTransfer && evt.dataTransfer.effectAllowed !== 'move') {
return;
}
if (evt.preventDefault !== void 0) {
evt.preventDefault();
!options.dragoverBubble && evt.stopPropagation();
}
if (!_silent && activeGroup &&
if (activeGroup && !options.disabled &&
(isOwner
? canSort || (revert = !rootEl.contains(dragEl))
: activeGroup.pull && groupPut && (
@ -479,6 +501,13 @@
) &&
(evt.rootEl === void 0 || evt.rootEl === this.el)
) {
// Smart auto-scrolling
_autoScroll(evt, options, this.el);
if (_silent) {
return;
}
target = _closest(evt.target, options.draggable, el);
dragRect = dragEl.getBoundingClientRect();
@ -574,6 +603,7 @@
clearTimeout(target.animated);
target.animated = setTimeout(function () {
_css(target, 'transition', '');
_css(target, 'transform', '');
target.animated = false;
}, ms);
}
@ -595,8 +625,7 @@
// Unbind events
_off(document, 'drop', this);
_off(document, 'dragover', this);
_off(document, 'mousemove', this._onTouchMove);
_off(el, 'dragstart', this._onDragStart);
this._offUpEvents();
@ -644,13 +673,16 @@
Sortable.active && _dispatchEvent(rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
}
// Set NULL
// Nulling
rootEl =
dragEl =
ghostEl =
nextEl =
cloneEl =
scrollEl =
scrollParentEl =
tapEvt =
touchEvt =
@ -669,8 +701,8 @@
handleEvent: function (/**Event*/evt) {
var type = evt.type;
if (type === 'dragover') {
this._onDrag(evt);
if (type === 'dragover' || type === 'dragenter') {
this._onDragOver(evt);
_globalDragOver(evt);
}
else if (type === 'drop' || type === 'dragend') {
@ -774,10 +806,9 @@
_off(el, 'mousedown', this._onTapStart);
_off(el, 'touchstart', this._onTapStart);
_off(el, 'selectstart', this._onTapStart);
_off(el, 'dragover', this._onDragOver);
_off(el, 'dragenter', this._onDragOver);
_off(el, 'dragover', this);
_off(el, 'dragenter', this);
//remove draggable attributes
Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
@ -949,8 +980,10 @@
*/
function _index(/**HTMLElement*/el) {
var index = 0;
while (el && (el = el.previousElementSibling) && (el.nodeName.toUpperCase() !== 'TEMPLATE')) {
index++;
while (el && (el = el.previousElementSibling)) {
if (el.nodeName.toUpperCase() !== 'TEMPLATE') {
index++;
}
}
return index;
}

2
Sortable.min.js vendored

File diff suppressed because one or more lines are too long

4
index.html

@ -7,8 +7,8 @@
<title>Sortable. No jQuery.</title>
<meta name="keywords" content="sortable, reorder, list, javascript, html5, drag and drop, dnd, animation, groups, angular, ng-sortable, effects, rubaxa"/>
<meta name="description" content="Sortable - is a minimalist JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery. Supports Meteor and AngularJS and any CSS library, e.g. Bootstrap."/>
<meta name="keywords" content="sortable, reorder, list, javascript, html5, drag and drop, dnd, animation, groups, angular, ng-sortable, react, mixin, effects, rubaxa"/>
<meta name="description" content="Sortable - is a minimalist JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery. Supports Meteor, AngularJS, React and any CSS library, e.g. Bootstrap."/>
<meta name="viewport" content="width=device-width, initial-scale=0.5"/>
<link href="//rubaxa.github.io/Ply/ply.css" rel="stylesheet" type="text/css"/>

62
ng-sortable.js

@ -9,13 +9,23 @@
factory(angular, Sortable);
}
else if (typeof define === 'function' && define.amd) {
define(['angular', 'sortable'], factory);
define(['angular', './Sortable'], factory);
}
})(function (angular, Sortable) {
'use strict';
/**
* @typedef {Object} ngSortEvent
* @property {*} model List item
* @property {Object|Array} models List of items
* @property {number} oldIndex before sort
* @property {number} newIndex after sort
*/
angular.module('ng-sortable', [])
.constant('$version', '0.3.4')
.constant('$version', '0.3.5')
.directive('ngSortable', ['$parse', function ($parse) {
var removed,
nextSibling;
@ -29,6 +39,11 @@
);
})[0];
if (!ngRepeat) {
// Without ng-repeat
return null;
}
// tests: http://jsbin.com/kosubutilo/1/edit?js,output
ngRepeat = ngRepeat.nodeValue.match(/ngRepeat:\s*(?:\(.*?,\s*)?([^\s)]+)[\s)]+in\s+([^\s|]+)/);
@ -58,12 +73,25 @@
;
'Start End Add Update Remove Sort'.split(' ').forEach(function (name) {
options['on' + name] = options['on' + name] || function () {};
});
function _emitEvent(/**Event*/evt, /*Mixed*/item) {
var name = 'on' + evt.type.charAt(0).toUpperCase() + evt.type.substr(1);
/* jshint expr:true */
options[name] && options[name]({
model: item,
models: source && source.items(),
oldIndex: evt.oldIndex,
newIndex: evt.newIndex
});
}
function _sync(/**Event*/evt) {
if (!source) {
// Without ng-repeat
return;
}
function _sync(evt) {
var oldIndex = evt.oldIndex,
newIndex = evt.newIndex,
items = source.items();
@ -77,6 +105,7 @@
if (evt.clone) {
evt.from.removeChild(evt.clone);
removed = angular.copy(removed);
}
else {
prevItems.splice(oldIndex, 1);
@ -100,24 +129,27 @@
}, {
onStart: function (/**Event*/evt) {
nextSibling = evt.item.nextSibling;
options.onStart(source.items());
_emitEvent(evt);
scope.$apply();
},
onEnd: function () {
options.onEnd(source.items());
onEnd: function (/**Event*/evt) {
_emitEvent(evt, removed);
scope.$apply();
},
onAdd: function (/**Event*/evt) {
_sync(evt);
options.onAdd(source.items(), removed);
_emitEvent(evt, removed);
scope.$apply();
},
onUpdate: function (/**Event*/evt) {
_sync(evt);
options.onUpdate(source.items(), source.item(evt.item));
_emitEvent(evt, source && source.item(evt.item));
},
onRemove: function () {
options.onRemove(source.items(), removed);
onRemove: function (/**Event*/evt) {
_emitEvent(evt, removed);
},
onSort: function () {
options.onSort(source.items());
onSort: function (/**Event*/evt) {
_emitEvent(evt, source && source.item(evt.item));
}
}));

5
package.json

@ -23,8 +23,11 @@
"sortable",
"reorder",
"drag",
"meteor",
"angular",
"ng-srotable",
"angular"
"react",
"mixin"
],
"author": "Konstantin Lebedev <ibnRubaXa@gmail.com>",
"license": "MIT",

147
react-sortable-mixin.js vendored

@ -0,0 +1,147 @@
/**
* @author RubaXa <trash@rubaxa.org>
* @licence MIT
*/
(function (factory) {
'use strict';
if (typeof module != 'undefined' && typeof module.exports != 'undefined') {
module.exports = factory(require('./Sortable'));
}
else if (typeof define === 'function' && define.amd) {
define(['./Sortable'], factory);
}
else {
/* jshint sub:true */
window['SortableMixin'] = factory(Sortable);
}
})(function (/** Sortable */Sortable) {
'use strict';
var _nextSibling;
var _activeComponent;
var _defaultOptions = {
ref: 'list',
model: 'items',
animation: 100,
onStart: 'handleStart',
onEnd: 'handleEnd',
onAdd: 'handleAdd',
onUpdate: 'handleUpdate',
onRemove: 'handleRemove',
onSort: 'handleSort',
onFilter: 'handleFilter'
};
function _getModelName(component) {
return component.sortableOptions && component.sortableOptions.model || _defaultOptions.model;
}
function _getModelItems(component) {
var name = _getModelName(component),
items = component.state && component.state[name] || component.props[name];
return items.slice();
}
function _extend(dst, src) {
for (var key in src) {
if (src.hasOwnProperty(key)) {
dst[key] = src[key];
}
}
return dst;
}
/**
* Simple and easy mixin-wrapper for rubaxa/Sortable library, in order to
* make reorderable drag-and-drop lists on modern browsers and touch devices.
*
* @mixin
*/
var SortableMixin = {
sortableMixinVersion: '0.1.0',
/**
* @type {Sortable}
* @private
*/
_sortableInstance: null,
componentDidMount: function () {
var options = _extend(_extend({}, _defaultOptions), this.sortableOptions || {}),
copyOptions = _extend({}, options),
emitEvent = function (/** string */type, /** Event */evt) {
var method = this[options[type]];
method && method.call(this, evt, this._sortableInstance);
}.bind(this);
// Bind callbacks so that "this" refers to the component
'onStart onEnd onAdd onSort onUpdate onRemove onFilter'.split(' ').forEach(function (/** string */name) {
copyOptions[name] = function (evt) {
if (name === 'onStart') {
_nextSibling = evt.item.nextElementSibling;
_activeComponent = this;
}
else if (name === 'onAdd' || name === 'onUpdate') {
evt.from.insertBefore(evt.item, _nextSibling);
var newState = {},
remoteState = {},
oldIndex = evt.oldIndex,
newIndex = evt.newIndex,
items = _getModelItems(this),
remoteItems,
item;
if (name === 'onAdd') {
remoteItems = _getModelItems(_activeComponent);
item = remoteItems.splice(oldIndex, 1)[0];
items.splice(newIndex, 0, item);
remoteState[_getModelName(_activeComponent)] = remoteItems;
}
else {
items.splice(newIndex, 0, items.splice(oldIndex, 1)[0]);
}
newState[_getModelName(this)] = items;
this.setState(newState);
(this !== _activeComponent) && _activeComponent.setState(remoteState);
}
setTimeout(function () {
emitEvent(name, evt);
}, 0);
}.bind(this);
}, this);
/** @namespace this.refs — http://facebook.github.io/react/docs/more-about-refs.html */
this._sortableInstance = Sortable.create((this.refs[options.ref] || this).getDOMNode(), copyOptions);
},
componentWillUnmount: function () {
this._sortableInstance.destroy();
this._sortableInstance = null;
}
};
// Export
return SortableMixin;
});

37
st/app.js

@ -1,5 +1,41 @@
(function () {
'use strict';
var byId = function (id) { return document.getElementById(id); },
loadScripts = function (desc, callback) {
var deps = [], key, idx = 0;
for (key in desc) {
deps.push(key);
}
(function _next() {
var pid,
name = deps[idx],
script = document.createElement('script');
script.type = 'text/javascript';
script.src = desc[deps[idx]];
pid = setInterval(function () {
if (window[name]) {
clearTimeout(pid);
deps[idx++] = window[name];
if (deps[idx]) {
_next();
} else {
callback.apply(null, deps);
}
}
}, 30);
document.getElementsByTagName('head')[0].appendChild(script);
})()
},
console = window.console;
@ -162,6 +198,7 @@
})();
// Background
document.addEventListener("DOMContentLoaded", function () {
function setNoiseBackground(el, width, height, opacity) {

Loading…
Cancel
Save