Browse Source

Merge branch 'dev' into gh-pages

pull/191/head
RubaXa 10 years ago
parent
commit
32948c717c
  1. 13
      Gruntfile.js
  2. 24
      README.md
  3. 60
      Sortable.js
  4. 4
      Sortable.min.js
  5. 2
      bower.json
  6. 2
      component.json
  7. 2
      index.html
  8. 96
      meteor/README.md
  9. 1
      meteor/example/.meteor/.gitignore
  10. 12
      meteor/example/.meteor/packages
  11. 1
      meteor/example/.meteor/release
  12. 56
      meteor/example/.meteor/versions
  13. 60
      meteor/example/README.md
  14. 57
      meteor/example/client/define-object-type.css
  15. 94
      meteor/example/client/define-object-type.html
  16. 101
      meteor/example/client/define-object-type.js
  17. 2
      meteor/example/model.js
  18. 1
      meteor/example/package.json
  19. 1
      meteor/example/packages/Sortable
  20. 4
      meteor/example/run.bat
  21. 15
      meteor/example/run.sh
  22. 75
      meteor/example/server/fixtures.js
  23. 20
      meteor/methods.js
  24. 26
      meteor/package.js
  25. 66
      meteor/publish.sh
  26. 201
      meteor/reactivize.js
  27. 39
      meteor/runtests.sh
  28. 5
      meteor/template.html
  29. 7
      ng-sortable.js
  30. 9
      package.json

13
Gruntfile.js

@ -27,7 +27,7 @@ module.exports = function (grunt) {
}
},
shell: {
exec: {
'meteor-test': {
command: 'meteor/runtests.sh'
},
@ -42,15 +42,12 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-version');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-exec');
// 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');
grunt.registerTask('meteor-test', 'exec:meteor-test');
grunt.registerTask('meteor-publish', 'exec:meteor-publish');
grunt.registerTask('meteor', ['meteor-test', 'meteor-publish']);
grunt.registerTask('tests', ['jshint']);
grunt.registerTask('default', ['tests', 'version', 'uglify']);

24
README.md

@ -12,7 +12,8 @@ Demo: http://rubaxa.github.io/Sortable/
* Supports drag handles *and selectable text* (better than voidberg's html5sortable)
* Smart auto-scrolling
* Built using native HTML5 drag and drop API
* Supports [AngularJS](#ng) and and any CSS library, e.g. [Bootstrap](#bs)
* Supports [Meteor](meteor/README.md) and [AngularJS](#ng)
* Supports any CSS library, e.g. [Bootstrap](#bs)
* Simple API
* No jQuery
@ -387,11 +388,29 @@ Demo: http://jsbin.com/luxero/2/edit?html,js,output
</script>
```
---
### Static methods & properties
##### Sortable.create(el:`HTMLElement`[, options:`Object`]):`Sortable`
Create new instance.
---
##### Sortable.active:`Sortable`
Link to the active instance.
---
### Sortable.utils
##### Sortable.utils
* on(el`:HTMLElement`, event`:String`, fn`:Function`) — attach an event handler function
* off(el`:HTMLElement`, event`:String`, fn`:Function`) — remove an event handler
* css(el`:HTMLElement`)`:Object` — get the values of all the CSS properties
@ -400,6 +419,7 @@ Demo: http://jsbin.com/luxero/2/edit?html,js,output
* css(el`:HTMLElement`, props`:Object`) — set more CSS properties
* find(ctx`:HTMLElement`, tagName`:String`[, iterator`:Function`])`:Array` — get elements by tag name
* bind(ctx`:Mixed`, fn`:Function`)`:Function` — Takes a function and returns a new one that will always have a particular context
* is(el`:HTMLElement`, selector`:String`)`:Boolean` — check the current matched set of elements against a selector
* closest(el`:HTMLElement`, selector`:String`[, ctx`: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
* toggleClass(el`:HTMLElement`, name`:String`, state`:Boolean`) — add or remove one classes from each element

60
Sortable.js

@ -104,9 +104,7 @@
setData: function (dataTransfer, dragEl) {
dataTransfer.setData('Text', dragEl.textContent);
}
},
group = options.group;
};
// Set default options
@ -115,6 +113,8 @@
}
var group = options.group;
if (!group || typeof group != 'object') {
group = options.group = { name: group };
}
@ -165,20 +165,27 @@
constructor: Sortable,
_applyEffects: function () {
_dragStarted: function () {
// Apply effect
_toggleClass(dragEl, this.options.ghostClass, true);
Sortable.active = this;
// Drag start event
_dispatchEvent(rootEl, 'start', dragEl, rootEl, startIndex);
},
_onTapStart: function (/**Event|TouchEvent*/evt) {
var touch = evt.touches && evt.touches[0],
var type = evt.type,
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 || options.disabled) {
if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
return; // only left button or enabled
}
@ -215,14 +222,11 @@
}
}
// Prepare `dragstart`
if (target && !dragEl && (target.parentNode === el)) {
// IE 9 Support
if (target && evt.type == 'selectstart') {
if (target.tagName != 'A' && target.tagName != 'IMG') {
target.dragDrop();
}
}
(type === 'selectstart') && target.dragDrop();
if (target && !dragEl && (target.parentNode === el)) {
tapEvt = evt;
rootEl = this.el;
@ -269,17 +273,11 @@
}
// Drag start event
_dispatchEvent(rootEl, 'start', dragEl, rootEl, startIndex);
if (activeGroup.pull == 'clone') {
cloneEl = dragEl.cloneNode(true);
_css(cloneEl, 'display', 'none');
rootEl.insertBefore(cloneEl, dragEl);
}
Sortable.active = this;
}
},
@ -288,11 +286,13 @@
_css(ghostEl, 'display', 'none');
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
parent = target.parentNode,
parent = target && target.parentNode,
groupName = this.options.group.name,
i = touchDragOverListeners.length;
if (parent && (' ' + parent[expando] + ' ').indexOf(groupName) > -1) {
if (parent) {
do {
if ((' ' + parent[expando] + ' ').indexOf(groupName) > -1) {
while (i--) {
touchDragOverListeners[i]({
clientX: touchEvt.clientX,
@ -301,6 +301,14 @@
rootEl: parent
});
}
break;
}
target = parent; // store last element
}
/* jshint boss:true */
while (parent = parent.parentNode);
}
_css(ghostEl, 'display', '');
@ -370,8 +378,6 @@
_on(document, 'drop', this);
}
setTimeout(this._applyEffects, 0);
scrollEl = options.scroll;
if (scrollEl === true) {
@ -386,6 +392,8 @@
/* jshint boss:true */
} while (scrollEl = scrollEl.parentNode);
}
setTimeout(this._dragStarted, 0);
},
_onDrag: _throttle(function (/**Event*/evt) {
@ -601,7 +609,7 @@
_disableDraggable(dragEl);
_toggleClass(dragEl, this.options.ghostClass, false);
if (!rootEl.contains(dragEl)) {
if (rootEl !== dragEl.parentNode) {
// drag from one list and drop into another
_dispatchEvent(dragEl.parentNode, 'sort', dragEl, rootEl, startIndex, newIndex);
_dispatchEvent(rootEl, 'sort', dragEl, rootEl, startIndex, newIndex);
@ -621,7 +629,7 @@
}
// Drag end event
_dispatchEvent(rootEl, 'end', dragEl, rootEl, startIndex, newIndex);
Sortable.active && _dispatchEvent(rootEl, 'end', dragEl, rootEl, startIndex, newIndex);
}
// Set NULL
@ -641,7 +649,7 @@
Sortable.active = null;
// Save sorting
this.save()
this.save();
}
},
@ -975,7 +983,7 @@
};
Sortable.version = '0.7.3';
Sortable.version = '1.0.0';
/**

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.7.3",
"version": "1.0.0",
"homepage": "http://rubaxa.github.io/Sortable/",
"authors": [
"RubaXa <ibnRubaXa@gmail.com>"

2
component.json

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

2
index.html

@ -8,7 +8,7 @@
<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 modern browsers and touch devices (No jQuery). Support AngularJS."/>
<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="viewport" content="width=device-width, initial-scale=0.5"/>
<link href="//rubaxa.github.io/Ply/ply.css" rel="stylesheet" type="text/css"/>

96
meteor/README.md

@ -1,26 +1,106 @@
Packaging [Sortable](http://rubaxa.github.io/Sortable/) for [Meteor.js](http://meteor.com).
Reactive reorderable lists with [Sortable](http://rubaxa.github.io/Sortable/),
backed by [Meteor.js](http://meteor.com) collections:
* new elements arriving in the collection will update the list as you expect
* elements removed from the collection will be removed from the list
* drag and drop between lists updates collections accordingly
Demo: http://rubaxa-sortable.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
# Usage
If you encounter an issue while using this package, please CC @dandv when you file it in this repo.
Simplest invocation - order will be lost when the page is refreshed:
```handlebars
{{sortable <collection|cursor|array>}}
```
Persist the sort order in the 'order' field of each document in the collection:
```handlebars
{{sortable items=<collection|cursor|array> sortField="order"}}
```
Along with `items`, `sortField` is the only Meteor-specific option. If it's missing, the package will
assume there is a field called "order" in the collection, holding unique `Number`s such that every
`order` differs from that before and after it by at least 1. Basically, keep to 0, 1, 2, ... .
Try not to depend on a particular format for this field; it *is* though guaranteed that a `sort` will
produce lexicographical order, and that the order will be maintained after an arbitrary number of
reorderings, unlike with [naive solutions](http://programmers.stackexchange.com/questions/266451/maintain-ordered-collection-by-updating-as-few-order-fields-as-possible).
## Passing options to the Sortable library
{{sortable items=<collection|cursor|array> option1=value1 option2=value2...}}
{{sortable items=<collection|cursor|array> options=myOptions}}
For available options, please refer to [the main README](../README.md#options). You can pass them directly
or under the `options` object. Direct options (`key=value`) override those in `options`. It is best
to pass presentation-related options directly, and functionality-related settings in an `options`
object, as this will enable designers to work without needing to inspect the JavaScript code:
# DONE
<template name="myTemplate">
...
{{sortable items=Players handle=".sortable-handle" ghostClass="sortable-ghost" options=playerOptions}}
</template>
* Instantiation test
Define the options in a helper for the template that calls Sortable:
```js
Template.myTemplate.helpers({
playerOptions: function () {
return {
group: {
name: "league",
pull: true,
put: false
},
sort: false
};
}
});
```
## Events
All the original Sortable events are supported. In addition, they will receive
the data context in `event.data`. You can access `event.data.order` this way:
```handlebars
{{sortable items=players options=playersOptions}}
```
```js
Template.myTemplate.helpers({
playersOptions: function () {
return {
onSort: function(/**Event*/event) {
console.log('Moved player #%d from %d to %d',
event.data.order, event.oldIndex, event.newIndex
);
}
};
}
});
```
# Issues
If you encounter an issue while using this package, please CC @dandv when you file it in this repo.
# TODO
* Meteor collection backing
* Tests ensuring correct rendering with Meteor dynamic templates
* Array support
* Tests
* Misc. - see reactivize.js

1
meteor/example/.meteor/.gitignore vendored

@ -0,0 +1 @@
local

12
meteor/example/.meteor/packages

@ -0,0 +1,12 @@
# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-platform
autopublish
insecure
rubaxa:sortable
dburles:mongo-collection-instances
fezvrasta:bootstrap-material-design
# twbs:bootstrap

1
meteor/example/.meteor/release

@ -0,0 +1 @@
METEOR@1.0.1

56
meteor/example/.meteor/versions

@ -0,0 +1,56 @@
application-configuration@1.0.3
autopublish@1.0.1
autoupdate@1.1.3
base64@1.0.1
binary-heap@1.0.1
blaze-tools@1.0.1
blaze@2.0.3
boilerplate-generator@1.0.1
callback-hook@1.0.1
check@1.0.2
ctl-helper@1.0.4
ctl@1.0.2
dburles:mongo-collection-instances@0.2.5
ddp@1.0.12
deps@1.0.5
ejson@1.0.4
fastclick@1.0.1
fezvrasta:bootstrap-material-design@0.2.1
follower-livedata@1.0.2
geojson-utils@1.0.1
html-tools@1.0.2
htmljs@1.0.2
http@1.0.8
id-map@1.0.1
insecure@1.0.1
jquery@1.0.1
json@1.0.1
launch-screen@1.0.0
livedata@1.0.11
logging@1.0.5
meteor-platform@1.2.0
meteor@1.1.3
minifiers@1.1.2
minimongo@1.0.5
mobile-status-bar@1.0.1
mongo@1.0.9
observe-sequence@1.0.3
ordered-dict@1.0.1
random@1.0.1
reactive-dict@1.0.4
reactive-var@1.0.3
reload@1.1.1
retry@1.0.1
routepolicy@1.0.2
rubaxa:sortable@1.0.0
session@1.0.4
spacebars-compiler@1.0.3
spacebars@1.0.3
templating@1.0.9
tracker@1.0.3
twbs:bootstrap@3.3.1
ui@1.0.4
underscore@1.0.1
url@1.0.2
webapp-hashing@1.0.1
webapp@1.1.4

60
meteor/example/README.md

@ -0,0 +1,60 @@
# RubaXa:Sortable Meteor demo
This demo showcases the two-way integration between the reorderable list
widget [Sortable](https://github.com/RubaXa/Sortable/) and Meteor.js. Meteor
Mongo collections are updated when items are added, removed or reordered, and
the order is persisted.
It also shows list grouping and control over what lists can give or receive
elements. You can only drag elements from the list to the left onto the list
to the right.
## Usage
The example uses the local package from the checkout, so it needs to wire
up some files (`package.js` and `package.json`). This is done by the handy
run script:
### Windows
git clone git@github.com:RubaXa/Sortable.git
cd Sortable
git checkout dev
cd meteor\example
run.bat
### Elsewhere
git clone git@github.com:RubaXa/Sortable.git
cd Sortable
git checkout dev
meteor/example./run.sh
## Prior art
### Differential
Differential wrote [a blog post on reorderable lists with
Meteor](differential.com/blog/sortable-lists-in-meteor-using-jquery-ui) and
[jQuery UI Sortable](http://jqueryui.com/sortable/). It served as inspiration
for integrating [rubaxa:sortable](rubaxa.github.io/Sortable/),
which uses the HTML5 native drag&drop API (not without [its
limitations](https://github.com/RubaXa/Sortable/issues/106)).
The reordering method used by the Differential example can lead to data loss
though, because it calculates the new order of a dropped element as the
arithmetic mean of the elements before and after it. This [runs into limitations
of floating point precision](http://programmers.stackexchange.com/questions/266451/maintain-ordered-collection-by-updating-as-few-order-fields-as-possible)
in JavaScript after <50 reorderings.
### Todos animated
http://todos-dnd-animated.meteor.com/ ([source](https://github.com/nleush/meteor-todos-sortable-animation))
is based on old Meteor Blaze (back then Spark) API, and won't work with current versions.
It does showcase some neat features, such as animation when collection elements
are reordered by another client. It uses jQuery UI Sortable as well, which lacks
some features vs. rubaxa:Sortable, e.g. text selection within the item.
## TODO
* Animation
* Indication that an item is being edited

57
meteor/example/client/define-object-type.css

@ -0,0 +1,57 @@
.glyphicon {
vertical-align: baseline;
font-size: 80%;
margin-right: 0.5em;
}
[class^="mdi-"], [class*=" mdi-"] {
vertical-align: baseline;
font-size: 90%;
margin-right: 0.4em;
}
.list-pair {
display: flex; /* use the flexbox model */
flex-direction: row;
}
.sortable {
/* font-size: 2em;*/
}
.sortable.source {
/*background: #9FA8DA;*/
flex: 0 0 auto;
margin-right: 1em;
cursor: move;
cursor: -webkit-grabbing;
}
.sortable.target {
/*background: #3F51B5;*/
flex: 1 1 auto;
margin-left: 1em;
}
.target .well {
}
.sortable-handle {
cursor: move;
cursor: -webkit-grabbing;
}
.sortable-handle.pull-right {
margin-top: 0.3em;
}
.sortable-ghost {
opacity: 0.6;
}
/* show the remove button on hover */
.removable .close {
display: none;
}
.removable:hover .close {
display: block;
}

94
meteor/example/client/define-object-type.html

@ -0,0 +1,94 @@
<head>
<title>Reactive RubaXa:Sortable for Meteor</title>
</head>
<body>
{{> navbar}}
<div class="container">
<div class="page-header">
<h1>RubaXa:Sortable - reactive reorderable lists for Meteor</h1>
<h2>Drag attribute types from the left to define an object type on the right</h2>
<h3>Drag the <i class="sortable-handle mdi-action-view-headline"></i> handle to reorder elements</h3>
</div>
{{> typeDefinition}}
</div>
</body>
<template name="typeDefinition">
<div class="row">
<div class="list-pair col-sm-12">
<div class="sortable source list-group" id="types">
{{#sortable items=types options=typesOptions}}
<div class="list-group-item well well-sm">
{{{icon}}} {{name}}
</div>
{{/sortable}}
</div>
<div class="sortable target" id="object">
{{#sortable items=attributes animation="100" handle=".sortable-handle" ghostClass="sortable-ghost" options=attributesOptions}}
{{> sortableItemTarget}}
{{/sortable}}
</div>
</div>
</div>
</template>
<template name="sortableItemTarget">
<div data-id="{{order}}" class="sortable-item removable well well-sm">
{{{icon}}}
<i class="sortable-handle mdi-action-view-headline pull-right"></i>
<span class="name">{{name}}</span>
<span class="badge">{{order}}</span>
<button type="button" class="close" data-dismiss="alert">
<span aria-hidden="true">&times;</span><span class="sr-only">Close</span>
</button>
</div>
</template>
<template name="navbar">
<div class="navbar navbar-inverse">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-inverse-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="https://atmospherejs.com/rubaxa/sortable">RubaXa:Sortable</a>
</div>
<div class="navbar-collapse collapse navbar-inverse-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="http://rubaxa-sortable.meteor.com">Meteor Demo</a></li>
<li><a href="https://rubaxa.github.io/Sortable/" target="_blank">Sortable standalone demo</a></li>
<li class="dropdown">
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">Source <b class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown-header">GitHub</li>
<li><a href="https://github.com/RubaXa/Sortable/tree/dev/meteor/example" target="_blank">This demo</a></li>
<li><a href="https://github.com/RubaXa/Sortable/tree/dev/meteor" target="_blank">Meteor integration</a></li>
<li><a href="https://github.com/RubaXa/Sortable" target="_blank">rubaxa/sortable</a></li>
<li class="divider"></li>
<li><a href="https://atmospherejs.com/rubaxa/sortable">Star this package on Atmosphere!</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">Resources <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="http://www.meteorpedia.com/read/Packaging_existing_Libraries">Packaging 3rd party libraries for Meteor</a></li>
<li><a href="https://twitter.com/dandv">Author: @dandv</a></li>
</ul>
</li>
</ul>
</div>
</div>
</template>

101
meteor/example/client/define-object-type.js

@ -0,0 +1,101 @@
// Define an object type by dragging together attributes
Template.typeDefinition.helpers({
types: function () {
return Types.find({}, { sort: { order: 1 } });
},
typesOptions: {
sortField: 'order', // defaults to 'order' anyway
group: {
name: 'typeDefinition',
pull: 'clone',
put: false
},
sort: false // don't allow reordering the types, just the attributes below
},
attributes: function () {
return Attributes.find({}, {
sort: { order: 1 },
transform: function (doc) {
doc.icon = Types.findOne({name: doc.type}).icon;
return doc;
}
});
},
attributesOptions: {
group: {
name: 'typeDefinition',
put: true
},
onAdd: function (event) {
delete event.data._id; // Generate a new id when inserting in the Attributes collection. Otherwise, if we add the same type twice, we'll get an error that the ids are not unique.
delete event.data.icon;
event.data.type = event.data.name;
event.data.name = 'Rename me (double click)'
},
// event handler for reordering attributes
onSort: function (event) {
console.log('Item %s went from #%d to #%d',
event.data.name, event.oldIndex, event.newIndex
);
}
}
});
Template.sortableItemTarget.events({
'dblclick .name': function (event, template) {
// Make the name editable. We should use an existing component, but it's
// in a sorry state - https://github.com/arillo/meteor-x-editable/issues/1
var name = template.$('.name');
var input = template.$('input');
if (input.length) { // jQuery never returns null - http://stackoverflow.com/questions/920236/how-can-i-detect-if-a-selector-returns-null
input.show();
} else {
input = $('<input class="form-control" type="text" placeholder="' + this.name + '" style="display: inline">');
name.after(input);
}
name.hide();
input.focus();
},
'blur input[type=text]': function (event, template) {
// commit the change to the name, if any
var input = template.$('input');
input.hide();
template.$('.name').show();
// TODO - what is the collection here? We'll hard-code for now.
// https://github.com/meteor/meteor/issues/3303
if (this.name !== input.val() && this.name !== '')
Attributes.update(this._id, {$set: {name: input.val()}});
},
'keydown input[type=text]': function (event, template) {
if (event.which === 27) {
// ESC - discard edits and keep existing value
template.$('input').val(this.name);
event.preventDefault();
event.target.blur();
} else if (event.which === 13) {
// ENTER
event.preventDefault();
event.target.blur();
}
}
});
// you can add events to all Sortable template instances
Template.sortable.events({
'click .close': function (event, template) {
// `this` is the data context set by the enclosing block helper (#each, here)
template.collection.remove(this._id);
// custom code, working on a specific collection
if (Attributes.find().count() === 0) {
Meteor.setTimeout(function () {
Attributes.insert({
name: 'Not nice to delete the entire list! Add some attributes instead.',
type: 'String',
order: 0
})
}, 1000);
}
}
});

2
meteor/example/model.js

@ -0,0 +1,2 @@
Types = new Mongo.Collection('types');
Attributes = new Mongo.Collection('attributes');

1
meteor/example/package.json

@ -0,0 +1 @@
../../package.json

1
meteor/example/packages/Sortable

@ -0,0 +1 @@
../../../

4
meteor/example/run.bat

@ -0,0 +1,4 @@
mklink ..\..\package.js "meteor/package.js"
mklink package.json "../../package.json"
meteor run
del ..\..\package.js package.json

15
meteor/example/run.sh

@ -0,0 +1,15 @@
# sanity check: make sure we're in the root directory of the example
cd "$( dirname "$0" )"
# delete temp files even if Ctrl+C is pressed
int_trap() {
echo "Cleaning up..."
}
trap int_trap INT
ln -s "meteor/package.js" ../../package.js 2>/dev/null
ln -s "../../package.json" package.json 2>/dev/null
meteor run "$@"
rm ../../package.js package.json

75
meteor/example/server/fixtures.js

@ -0,0 +1,75 @@
Meteor.startup(function () {
if (Types.find().count() === 0) {
[
{
name: 'String',
icon: '<span class="glyphicon glyphicon-tag" aria-hidden="true"></span>'
},
{
name: 'Text, multi-line',
icon: '<i class="mdi-communication-message" aria-hidden="true"></i>'
},
{
name: 'Category',
icon: '<span class="glyphicon glyphicon-list" aria-hidden="true"></span>'
},
{
name: 'Number',
icon: '<i class="mdi-image-looks-one" aria-hidden="true"></i>'
},
{
name: 'Date',
icon: '<span class="glyphicon glyphicon-calendar" aria-hidden="true"></span>'
},
{
name: 'Hyperlink',
icon: '<span class="glyphicon glyphicon-link" aria-hidden="true"></span>'
},
{
name: 'Image',
icon: '<span class="glyphicon glyphicon-picture" aria-hidden="true"></span>'
},
{
name: 'Progress',
icon: '<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>'
},
{
name: 'Duration',
icon: '<span class="glyphicon glyphicon-time" aria-hidden="true"></span>'
},
{
name: 'Map address',
icon: '<i class="mdi-maps-place" aria-hidden="true"></i>'
},
{
name: 'Relationship',
icon: '<span class="glyphicon glyphicon-flash" aria-hidden="true"></span>'
}
].forEach(function (type, i) {
Types.insert({
name: type.name,
icon: type.icon,
order: i
});
}
);
console.log('Initialized attribute types.');
}
if (Attributes.find().count() === 0) {
[
{ name: 'Name', type: 'String' },
{ name: 'Created at', type: 'Date' },
{ name: 'Link', type: 'Hyperlink' },
{ name: 'Owner', type: 'Relationship' }
].forEach(function (attribute, i) {
Attributes.insert({
name: attribute.name,
type: attribute.type,
order: i
});
}
);
console.log('Created sample object type.');
}
});

20
meteor/methods.js

@ -0,0 +1,20 @@
'use strict';
Meteor.methods({
/**
* Update the orderField of documents with given ids in a collection, incrementing it by incDec
* @param {String} collectionName - name of the collection to update
* @param {String[]} ids - array of document ids
* @param {String} orderField - the name of the order field, usually "order"
* @param {Number} incDec - pass 1 or -1
*/
'rubaxa:sortable/collection-update': function (collectionName, ids, orderField, incDec) {
check(collectionName, String);
check(ids, [String]);
check(orderField, String);
check(incDec, Number);
var selector = {_id: {$in: ids}}, modifier = {$inc: {}};
modifier.$inc[orderField] = incDec;
Mongo.Collection.get(collectionName).update(selector, modifier, {multi: true});
}
});

26
meteor/package.js

@ -1,30 +1,34 @@
// 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 packageName = 'rubaxa:sortable'; // http://atmospherejs.com/rubaxa/sortable
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',
summary: 'Sortable: reactive minimalist reorderable drag-and-drop lists on modern browsers and touch devices',
version: packageJson.version,
git: 'https://github.com/RubaXa/Sortable.git'
git: 'https://github.com/RubaXa/Sortable.git',
readme: 'https://github.com/RubaXa/Sortable/blob/master/meteor/README.md'
});
Package.onUse(function (api) {
api.versionsFrom('METEOR@0.9.0');
api.versionsFrom(['METEOR@0.9.0', 'METEOR@1.0']);
api.use('templating', 'client');
api.use('dburles:mongo-collection-instances@0.2.5'); // to watch collections getting created
api.export('Sortable');
api.addFiles([
'Sortable.js'
], where
);
'Sortable.js',
'meteor/template.html', // the HTML comes first, so reactivize.js can refer to the template in it
'meteor/reactivize.js'
], 'client');
api.addFiles('meteor/methods.js'); // add to both client and server
});
Package.onTest(function (api) {
api.use(packageName, where);
api.use('tinytest', where);
api.use(packageName, 'client');
api.use('tinytest', 'client');
api.addFiles('meteor/test.js', where);
api.addFiles('meteor/test.js', 'client');
});

66
meteor/publish.sh

@ -1,72 +1,40 @@
#!/bin/bash
# Publish package on Meteor's Atmosphere.js
# Publish package to Meteor's repository, 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; }
# sanity check: make sure we're in the root directory of the checkout
cd "$( dirname "$0" )/.."
ALL_EXIT_CODE=0
function cleanup() {
# we copied the file as package.js, regardless of its original name
rm package.js
# temporary build files
rm -rf ".build.$PACKAGE_NAME" versions.json
}
# publish separately any package*.js files we have, e.g. package.js, package-compat.js
# test any package*.js packages we may 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/://}
PACKAGE_NAME=$(grep -i name package.js | head -1 | cut -d "'" -f 2)
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 --------------------------------------------------------
"
# Attempt to re-publish the package - the most common operation once the initial release has
# been made. If the package name was changed (rare), you'll have to pass the --create flag.
meteor publish "$@"; EXIT_CODE=$?
ALL_EXIT_CODE=$(( $ALL_EXIT_CODE + $EXIT_CODE ))
if (( $EXIT_CODE == 0 )); then
echo "Thanks for releasing a new version. You can see it at"
echo "https://atmospherejs.com/${PACKAGE_NAME/://}"
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
# rm the temporary build files and package.js
rm -rf ".build.$PACKAGE_NAME" versions.json package.js
done
exit $ALL_EXIT_CODE

201
meteor/reactivize.js

@ -0,0 +1,201 @@
/*
Make a Sortable reactive by binding it to a Mongo.Collection.
Calls `rubaxa:sortable/collection-update` on the server to update the sortField or affected records.
TODO:
* supply consecutive values if the `order` field doesn't have any
* .get(DOMElement) - return the Sortable object of a DOMElement
* create a new _id automatically onAdd if the event.from list had pull: 'clone'
* support arrays
* sparse arrays
* tests
* drop onto existing empty lists
* insert back into lists emptied by dropping
* performance on dragging into long list at the beginning
* handle failures on Collection operations, e.g. add callback to .insert
* when adding elements, update ranks just for the half closer to the start/end of the list
* revisit http://programmers.stackexchange.com/questions/266451/maintain-ordered-collection-by-updating-as-few-order-fields-as-possible
* reproduce the insidious bug where the list isn't always sorted (fiddle with dragging #1 over #2, then back, then #N before #1)
*/
'use strict';
Template.sortable.created = function () {
var templateInstance = this;
// `this` is a template instance that can store properties of our choice - http://docs.meteor.com/#/full/template_inst
if (templateInstance.setupDone) return; // paranoid: only run setup once
// this.data is the data context - http://docs.meteor.com/#/full/template_data
// normalize all options into templateInstance.options, and remove them from .data
templateInstance.options = templateInstance.data.options || {};
Object.keys(templateInstance.data).forEach(function (key) {
if (key === 'options' || key === 'items') return;
templateInstance.options[key] = templateInstance.data[key];
delete templateInstance.data[key];
});
templateInstance.options.sortField = templateInstance.options.sortField || 'order';
// We can get the collection via the .collection property of the cursor, but changes made that way
// will NOT be sent to the server - https://github.com/meteor/meteor/issues/3271#issuecomment-66656257
// Thus we need to use dburles:mongo-collection-instances to get a *real* collection
if (templateInstance.data.items && templateInstance.data.items.collection) {
// cursor passed via items=; its .collection works client-only and has a .name property
templateInstance.collectionName = templateInstance.data.items.collection.name;
templateInstance.collection = Mongo.Collection.get(templateInstance.collectionName);
} else if (templateInstance.data.items) {
// collection passed via items=; does NOT have a .name property, but _name
templateInstance.collection = templateInstance.data.items;
templateInstance.collectionName = templateInstance.collection._name;
} else if (templateInstance.data.collection) {
// cursor passed directly
templateInstance.collectionName = templateInstance.data.collection.name;
templateInstance.collection = Mongo.Collection.get(templateInstance.collectionName);
} else {
templateInstance.collection = templateInstance.data; // collection passed directly
templateInstance.collectionName = templateInstance.collection._name;
}
// TODO if (Array.isArray(templateInstance.collection))
// What if user filters some of the items in the cursor, instead of ordering the entire collection?
// Use case: reorder by preference movies of a given genre, a filter within all movies.
// A: Modify all intervening items **that are on the client**, to preserve the overall order
// TODO: update *all* orders via a server method that takes not ids, but start & end elements - mild security risk
delete templateInstance.data.options;
/**
* When an element was moved, adjust its orders and possibly the order of
* other elements, so as to maintain a consistent and correct order.
*
* There are three approaches to this:
* 1) Using arbitrary precision arithmetic and setting only the order of the moved
* element to the average of the orders of the elements around it -
* http://programmers.stackexchange.com/questions/266451/maintain-ordered-collection-by-updating-as-few-order-fields-as-possible
* The downside is that the order field in the DB will increase by one byte every
* time an element is reordered.
* 2) Adjust the orders of the intervening items. This keeps the orders sane (integers)
* but is slower because we have to modify multiple documents.
* TODO: we may be able to update fewer records by only altering the
* order of the records between the newIndex/oldIndex and the start/end of the list.
* 3) Use regular precision arithmetic, but when the difference between the orders of the
* moved item and the one before/after it falls below a certain threshold, adjust
* the order of that other item, and cascade doing so up or down the list.
* This will keep the `order` field constant in size, and will only occasionally
* require updating the `order` of other records.
*
* For now, we use approach #2.
*
* @param {String} itemId - the _id of the item that was moved
* @param {Number} orderPrevItem - the order of the item before it, or null
* @param {Number} orderNextItem - the order of the item after it, or null
*/
templateInstance.adjustOrders = function adjustOrders(itemId, orderPrevItem, orderNextItem) {
var orderField = templateInstance.options.sortField;
var selector = {}, modifier = {$set: {}};
var ids = [];
var startOrder = templateInstance.collection.findOne(itemId)[orderField];
if (orderPrevItem !== null) {
// Element has a previous sibling, therefore it was moved down in the list.
// Decrease the order of intervening elements.
selector[orderField] = {$lte: orderPrevItem, $gt: startOrder};
ids = _.pluck(templateInstance.collection.find(selector, {fields: {_id: 1}}).fetch(), '_id');
Meteor.call('rubaxa:sortable/collection-update', templateInstance.collectionName, ids, orderField, -1);
// Set the order of the dropped element to the order of its predecessor, whose order was decreased
modifier.$set[orderField] = orderPrevItem;
} else {
// element moved up the list, increase order of intervening elements
selector[orderField] = {$gte: orderNextItem, $lt: startOrder};
ids = _.pluck(templateInstance.collection.find(selector, {fields: {_id: 1}}).fetch(), '_id');
Meteor.call('rubaxa:sortable/collection-update', templateInstance.collectionName, ids, orderField, 1);
// Set the order of the dropped element to the order of its successor, whose order was increased
modifier.$set[orderField] = orderNextItem;
}
templateInstance.collection.update(itemId, modifier);
};
templateInstance.setupDone = true;
};
Template.sortable.rendered = function () {
var templateInstance = this;
var orderField = templateInstance.options.sortField;
// sorting was changed within the list
var optionsOnUpdate = templateInstance.options.onUpdate;
templateInstance.options.onUpdate = function sortableUpdate(/**Event*/event) {
var itemEl = event.item; // dragged HTMLElement
event.data = Blaze.getData(itemEl);
if (event.newIndex < event.oldIndex) {
// Element moved up in the list. The dropped element has a next sibling for sure.
var orderNextItem = Blaze.getData(itemEl.nextElementSibling)[orderField];
templateInstance.adjustOrders(event.data._id, null, orderNextItem);
} else if (event.newIndex > event.oldIndex) {
// Element moved down in the list. The dropped element has a previous sibling for sure.
var orderPrevItem = Blaze.getData(itemEl.previousElementSibling)[orderField];
templateInstance.adjustOrders(event.data._id, orderPrevItem, null);
} else {
// do nothing - drag and drop in the same location
}
if (optionsOnUpdate) optionsOnUpdate(event);
};
// element was added from another list
var optionsOnAdd = templateInstance.options.onAdd;
templateInstance.options.onAdd = function sortableAdd(/**Event*/event) {
var itemEl = event.item; // dragged HTMLElement
event.data = Blaze.getData(itemEl);
// let the user decorate the object with additional properties before insertion
if (optionsOnAdd) optionsOnAdd(event);
// Insert the new element at the end of the list and move it where it was dropped.
// We could insert it at the beginning, but that would lead to negative orders.
var sortSpecifier = {}; sortSpecifier[orderField] = -1;
event.data.order = templateInstance.collection.findOne({}, { sort: sortSpecifier, limit: 1 }).order + 1;
// TODO: this can obviously be optimized by setting the order directly as the arithmetic average, with the caveats described above
var newElementId = templateInstance.collection.insert(event.data);
event.data._id = newElementId;
if (itemEl.nextElementSibling) {
var orderNextItem = Blaze.getData(itemEl.nextElementSibling)[orderField];
templateInstance.adjustOrders(newElementId, null, orderNextItem);
} else {
// do nothing - inserted after the last element
}
// remove the dropped HTMLElement from the list because we have inserted it in the collection, which will update the template
itemEl.parentElement.removeChild(itemEl);
};
// element was removed by dragging into another list
var optionsOnRemove = templateInstance.options.onRemove;
templateInstance.options.onRemove = function sortableRemove(/**Event*/event) {
var itemEl = event.item; // dragged HTMLElement
event.data = Blaze.getData(itemEl);
// don't remove from the collection if group.pull is clone or false
if (typeof templateInstance.options.group === 'undefined'
|| typeof templateInstance.options.group.pull === 'undefined'
|| templateInstance.options.group.pull === true
) templateInstance.collection.remove(event.data._id);
if (optionsOnRemove) optionsOnRemove(event);
};
// just compute the `data` context
['onStart', 'onEnd', 'onSort', 'onFilter'].forEach(function (eventHandler) {
if (templateInstance.options[eventHandler]) {
var userEventHandler = templateInstance.options[eventHandler];
templateInstance.options[eventHandler] = function (/**Event*/event) {
var itemEl = event.item; // dragged HTMLElement
event.data = Blaze.getData(itemEl);
userEventHandler(event);
};
}
});
templateInstance.sortable = Sortable.create(templateInstance.firstNode.parentElement, templateInstance.options);
// TODO make the object accessible, e.g. via Sortable.getSortableById() or some such
};
Template.sortable.destroyed = function () {
this.sortable.destroy();
};

39
meteor/runtests.sh

@ -1,37 +1,46 @@
#!/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; }
# sanity check: make sure we're in the root directory of the checkout
cd "$( dirname "$0" )/.."
# run tests and delete the temporary package.js even if Ctrl+C is pressed
# delete the temporary files even if Ctrl+C is pressed
int_trap() {
echo
printf "Tests interrupted. Hopefully you verified in the browser that tests pass?\n\n"
printf "\nTests interrupted. Cleaning up...\n\n"
}
trap int_trap INT
# 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)
ALL_EXIT_CODE=0
echo "Testing $PACKAGE_NAME..."
# test any package*.js packages we may have, e.g. package.js, package-standalone.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
# Meteor expects package.js in the root dir of the checkout, so copy there our package file under that name, temporarily
cp $PACKAGE_FILE ./package.js
PACKAGE_NAME=$(grep -i name package.js | head -1 | cut -d "'" -f 2)
echo "### Testing $PACKAGE_NAME..."
# provide an invalid MONGO_URL so Meteor doesn't bog us down with an empty Mongo database
if [ $# -gt 0 ]; then
# interpret any parameter to mean we want an interactive test
MONGO_URL=mongodb:// meteor test-packages ./
else
# automated/CI test with phantomjs
./node_modules/.bin/spacejam --mongo-url mongodb:// test-packages ./
ALL_EXIT_CODES=$(( $ALL_EXIT_CODES + $? ))
fi
rm -rf ".build.$PACKAGE_NAME"
rm -rf ".build.local-test:$PACKAGE_NAME"
rm versions.json 2>/dev/null
rm package.js
# delete temporary build files and package.js
rm -rf .build.* versions.json package.js
done
exit $ALL_EXIT_CODES

5
meteor/template.html

@ -0,0 +1,5 @@
<template name="sortable">
{{#each items}}
{{> Template.contentBlock this}}
{{/each}}
</template>

7
ng-sortable.js

@ -3,7 +3,7 @@
* @licence MIT
*/
angular.module('ng-sortable', [])
.constant('$version', '0.3.1')
.constant('$version', '0.3.2')
.directive('ngSortable', ['$parse', function ($parse) {
'use strict';
@ -28,9 +28,6 @@ angular.module('ng-sortable', [])
},
items: function () {
return itemsExpr(scope);
},
upd: function () {
itemsExpr.assign(scope, this.items());
}
};
}
@ -66,14 +63,12 @@ angular.module('ng-sortable', [])
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();
}

9
package.json

@ -1,15 +1,16 @@
{
"name": "sortablejs",
"exportName": "Sortable",
"version": "0.7.3",
"version": "1.0.0",
"devDependencies": {
"grunt": "*",
"grunt-version": "*",
"grunt-shell": "*",
"grunt-exec": "*",
"grunt-contrib-jshint": "0.9.2",
"grunt-contrib-uglify": "*"
"grunt-contrib-uglify": "*",
"spacejam": "*"
},
"description": "Minimalist library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery.",
"description": "Minimalist JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery. Supports AngularJS and any CSS library, e.g. Bootstrap.",
"main": "Sortable.js",
"scripts": {
"test": "grunt"

Loading…
Cancel
Save