mirror of https://github.com/RubaXa/Sortable.git
RubaXa
10 years ago
22 changed files with 0 additions and 941 deletions
@ -1,106 +0,0 @@ |
|||||||
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). |
|
||||||
|
|
||||||
|
|
||||||
# Usage |
|
||||||
|
|
||||||
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: |
|
||||||
|
|
||||||
<template name="myTemplate"> |
|
||||||
... |
|
||||||
{{sortable items=Players handle=".sortable-handle" ghostClass="sortable-ghost" options=playerOptions}} |
|
||||||
</template> |
|
||||||
|
|
||||||
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 |
|
||||||
|
|
||||||
* Array support |
|
||||||
* Tests |
|
||||||
* Misc. - see reactivize.js |
|
@ -1,12 +0,0 @@ |
|||||||
# 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,56 +0,0 @@ |
|||||||
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 |
|
@ -1,60 +0,0 @@ |
|||||||
# 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 |
|
@ -1,57 +0,0 @@ |
|||||||
.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; |
|
||||||
} |
|
@ -1,94 +0,0 @@ |
|||||||
<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">×</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> |
|
@ -1,101 +0,0 @@ |
|||||||
// 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); |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
@ -1,2 +0,0 @@ |
|||||||
Types = new Mongo.Collection('types'); |
|
||||||
Attributes = new Mongo.Collection('attributes'); |
|
@ -1,4 +0,0 @@ |
|||||||
mklink ..\..\package.js "meteor/package.js" |
|
||||||
mklink package.json "../../package.json" |
|
||||||
meteor run |
|
||||||
del ..\..\package.js package.json |
|
@ -1,15 +0,0 @@ |
|||||||
# 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 |
|
@ -1,75 +0,0 @@ |
|||||||
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.'); |
|
||||||
} |
|
||||||
}); |
|
@ -1,20 +0,0 @@ |
|||||||
'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}); |
|
||||||
} |
|
||||||
}); |
|
@ -1,34 +0,0 @@ |
|||||||
// package metadata file for Meteor.js
|
|
||||||
'use strict'; |
|
||||||
|
|
||||||
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: reactive minimalist reorderable drag-and-drop lists on modern browsers and touch devices', |
|
||||||
version: packageJson.version, |
|
||||||
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', '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', |
|
||||||
'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, 'client'); |
|
||||||
api.use('tinytest', 'client'); |
|
||||||
|
|
||||||
api.addFiles('meteor/test.js', 'client'); |
|
||||||
}); |
|
@ -1,40 +0,0 @@ |
|||||||
#!/bin/bash |
|
||||||
# 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. |
|
||||||
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 |
|
||||||
|
|
||||||
# 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.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. 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" |
|
||||||
fi |
|
||||||
|
|
||||||
# rm the temporary build files and package.js |
|
||||||
rm -rf ".build.$PACKAGE_NAME" versions.json package.js |
|
||||||
|
|
||||||
done |
|
||||||
|
|
||||||
exit $ALL_EXIT_CODE |
|
@ -1,201 +0,0 @@ |
|||||||
/* |
|
||||||
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(); |
|
||||||
}; |
|
@ -1,46 +0,0 @@ |
|||||||
#!/bin/sh |
|
||||||
# Test Meteor package before publishing to Atmospherejs.com |
|
||||||
|
|
||||||
# Make sure Meteor is installed, per https://www.meteor.com/install. |
|
||||||
# The curl'ed script is totally safe; takes 2 minutes to read its source and check. |
|
||||||
type meteor >/dev/null 2>&1 || { curl https://install.meteor.com/ | sh; } |
|
||||||
|
|
||||||
# sanity check: make sure we're in the root directory of the checkout |
|
||||||
cd "$( dirname "$0" )/.." |
|
||||||
|
|
||||||
|
|
||||||
# delete the temporary files even if Ctrl+C is pressed |
|
||||||
int_trap() { |
|
||||||
printf "\nTests interrupted. Cleaning up...\n\n" |
|
||||||
} |
|
||||||
trap int_trap INT |
|
||||||
|
|
||||||
|
|
||||||
ALL_EXIT_CODE=0 |
|
||||||
|
|
||||||
# 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 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 |
|
||||||
|
|
||||||
# delete temporary build files and package.js |
|
||||||
rm -rf .build.* versions.json package.js |
|
||||||
|
|
||||||
done |
|
||||||
|
|
||||||
exit $ALL_EXIT_CODES |
|
@ -1,5 +0,0 @@ |
|||||||
<template name="sortable"> |
|
||||||
{{#each items}} |
|
||||||
{{> Template.contentBlock this}} |
|
||||||
{{/each}} |
|
||||||
</template> |
|
@ -1,9 +0,0 @@ |
|||||||
'use strict'; |
|
||||||
|
|
||||||
Tinytest.add('Sortable.is', function (test) { |
|
||||||
var items = document.createElement('ul'); |
|
||||||
items.innerHTML = '<li data-id="one">item 1</li><li data-id="two">item 2</li><li data-id="three">item 3</li>'; |
|
||||||
var sortable = new Sortable(items); |
|
||||||
test.instanceOf(sortable, Sortable, 'Instantiation OK'); |
|
||||||
test.length(sortable.toArray(), 3, 'Three elements'); |
|
||||||
}); |
|
Loading…
Reference in new issue