mirror of https://github.com/RubaXa/Sortable.git
RubaXa
9 years ago
27 changed files with 0 additions and 1068 deletions
@ -1,34 +0,0 @@ |
|||||||
base64@1.0.3 |
|
||||||
binary-heap@1.0.3 |
|
||||||
blaze@2.1.2 |
|
||||||
blaze-tools@1.0.3 |
|
||||||
callback-hook@1.0.3 |
|
||||||
check@1.0.5 |
|
||||||
dburles:mongo-collection-instances@0.3.4 |
|
||||||
ddp@1.1.0 |
|
||||||
deps@1.0.7 |
|
||||||
ejson@1.0.6 |
|
||||||
geojson-utils@1.0.3 |
|
||||||
html-tools@1.0.4 |
|
||||||
htmljs@1.0.4 |
|
||||||
id-map@1.0.3 |
|
||||||
jquery@1.11.3_2 |
|
||||||
json@1.0.3 |
|
||||||
lai:collection-extensions@0.1.4 |
|
||||||
local-test:rubaxa:sortable@1.2.1 |
|
||||||
logging@1.0.7 |
|
||||||
meteor@1.1.6 |
|
||||||
minifiers@1.1.5 |
|
||||||
minimongo@1.0.8 |
|
||||||
mongo@1.1.0 |
|
||||||
observe-sequence@1.0.6 |
|
||||||
ordered-dict@1.0.3 |
|
||||||
random@1.0.3 |
|
||||||
reactive-var@1.0.5 |
|
||||||
retry@1.0.3 |
|
||||||
rubaxa:sortable@1.2.1 |
|
||||||
spacebars-compiler@1.0.6 |
|
||||||
templating@1.1.1 |
|
||||||
tinytest@1.0.5 |
|
||||||
tracker@1.0.7 |
|
||||||
underscore@1.0.3 |
|
@ -1,133 +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://wiki.dandascalescu.com/essays/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: |
|
||||||
|
|
||||||
*Client:* |
|
||||||
|
|
||||||
```handlebars |
|
||||||
{{#sortable items=<collection|cursor|array> sortField="order"}} |
|
||||||
``` |
|
||||||
|
|
||||||
*Server:* |
|
||||||
|
|
||||||
```js |
|
||||||
Sortable.collections = <collectionName>; // the name, not the variable |
|
||||||
``` |
|
||||||
|
|
||||||
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). |
|
||||||
|
|
||||||
Remember to declare on the server which collections you want to be reorderable from the client. |
|
||||||
Otherwise, the library will error because the client would be able to modify numerical fields in |
|
||||||
any collection, which represents a security risk. |
|
||||||
|
|
||||||
|
|
||||||
## 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 |
|
||||||
}; |
|
||||||
} |
|
||||||
}); |
|
||||||
``` |
|
||||||
|
|
||||||
#### Meteor-specific options |
|
||||||
|
|
||||||
* `selector` - you can specify a collection selector if your list operates only on a subset of the collection. Example: |
|
||||||
|
|
||||||
```js |
|
||||||
Template.myTemplate.helpers({ |
|
||||||
playerOptions: function() { |
|
||||||
return { |
|
||||||
selector: { city: 'San Francisco' } |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
``` |
|
||||||
|
|
||||||
|
|
||||||
## 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 |
|
||||||
* [GitHub issues](https://github.com/RubaXa/Sortable/labels/%E2%98%84%20meteor) |
|
@ -1,8 +0,0 @@ |
|||||||
# This file contains information which helps Meteor properly upgrade your |
|
||||||
# app when you run 'meteor update'. You should check it into version control |
|
||||||
# with your project. |
|
||||||
|
|
||||||
notices-for-0.9.0 |
|
||||||
notices-for-0.9.1 |
|
||||||
0.9.4-platform-file |
|
||||||
notices-for-facebook-graph-api-2 |
|
@ -1,7 +0,0 @@ |
|||||||
# This file contains a token that is unique to your project. |
|
||||||
# Check it into your repository along with the rest of this directory. |
|
||||||
# It can be used for purposes such as: |
|
||||||
# - ensuring you don't accidentally deploy one app on top of another |
|
||||||
# - providing package authors with aggregated statistics |
|
||||||
|
|
||||||
ir0jg2douy3yo5mehw |
|
@ -1,10 +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 |
|
||||||
fezvrasta:bootstrap-material-design |
|
@ -1,53 +0,0 @@ |
|||||||
autopublish@1.0.3 |
|
||||||
autoupdate@1.2.1 |
|
||||||
base64@1.0.3 |
|
||||||
binary-heap@1.0.3 |
|
||||||
blaze@2.1.2 |
|
||||||
blaze-tools@1.0.3 |
|
||||||
boilerplate-generator@1.0.3 |
|
||||||
callback-hook@1.0.3 |
|
||||||
check@1.0.5 |
|
||||||
dburles:mongo-collection-instances@0.3.4 |
|
||||||
ddp@1.1.0 |
|
||||||
deps@1.0.7 |
|
||||||
ejson@1.0.6 |
|
||||||
fastclick@1.0.3 |
|
||||||
fezvrasta:bootstrap-material-design@0.3.0 |
|
||||||
geojson-utils@1.0.3 |
|
||||||
html-tools@1.0.4 |
|
||||||
htmljs@1.0.4 |
|
||||||
http@1.1.0 |
|
||||||
id-map@1.0.3 |
|
||||||
insecure@1.0.3 |
|
||||||
jquery@1.11.3_2 |
|
||||||
json@1.0.3 |
|
||||||
lai:collection-extensions@0.1.4 |
|
||||||
launch-screen@1.0.2 |
|
||||||
livedata@1.0.13 |
|
||||||
logging@1.0.7 |
|
||||||
meteor@1.1.6 |
|
||||||
meteor-platform@1.2.2 |
|
||||||
minifiers@1.1.5 |
|
||||||
minimongo@1.0.8 |
|
||||||
mobile-status-bar@1.0.3 |
|
||||||
mongo@1.1.0 |
|
||||||
observe-sequence@1.0.6 |
|
||||||
ordered-dict@1.0.3 |
|
||||||
random@1.0.3 |
|
||||||
reactive-dict@1.1.0 |
|
||||||
reactive-var@1.0.5 |
|
||||||
reload@1.1.3 |
|
||||||
retry@1.0.3 |
|
||||||
routepolicy@1.0.5 |
|
||||||
rubaxa:sortable@1.2.1 |
|
||||||
session@1.1.0 |
|
||||||
spacebars@1.0.6 |
|
||||||
spacebars-compiler@1.0.6 |
|
||||||
templating@1.1.1 |
|
||||||
tracker@1.0.7 |
|
||||||
twbs:bootstrap@3.3.5 |
|
||||||
ui@1.0.6 |
|
||||||
underscore@1.0.3 |
|
||||||
url@1.0.4 |
|
||||||
webapp@1.2.0 |
|
||||||
webapp-hashing@1.0.3 |
|
@ -1,59 +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, with the help of the run script: |
|
||||||
|
|
||||||
### Windows |
|
||||||
|
|
||||||
git clone https://github.com/RubaXa/Sortable.git |
|
||||||
cd Sortable |
|
||||||
# git checkout dev # optional |
|
||||||
meteor\example\run.bat |
|
||||||
|
|
||||||
### Elsewhere |
|
||||||
|
|
||||||
git clone https://github.com/RubaXa/Sortable.git |
|
||||||
cd Sortable |
|
||||||
# git checkout dev # optional |
|
||||||
meteor/example/run.sh |
|
||||||
|
|
||||||
|
|
||||||
## [Prior art](http://slides.com/dandv/prior-art) |
|
||||||
|
|
||||||
### Differential |
|
||||||
|
|
||||||
Differential wrote [a blog post on reorderable lists with |
|
||||||
Meteor](http://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,7 +0,0 @@ |
|||||||
@echo off |
|
||||||
REM Sanity check: make sure we're in the directory of the script |
|
||||||
set DIR=%~dp0 |
|
||||||
cd %DIR% |
|
||||||
|
|
||||||
set PACKAGE_DIRS=..\..\ |
|
||||||
meteor run %* |
|
@ -1,5 +0,0 @@ |
|||||||
# sanity check: make sure we're in the root directory of the example |
|
||||||
cd "$( dirname "$0" )" |
|
||||||
|
|
||||||
# let Meteor find the local package |
|
||||||
PACKAGE_DIRS=../../ meteor run "$@" |
|
@ -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,3 +0,0 @@ |
|||||||
'use strict'; |
|
||||||
|
|
||||||
Sortable.collections = ['attributes']; |
|
@ -1,16 +0,0 @@ |
|||||||
'use strict'; |
|
||||||
|
|
||||||
Meteor.methods({ |
|
||||||
/** |
|
||||||
* Update the sortField 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, sortField, incDec) { |
|
||||||
var selector = {_id: {$in: ids}}, modifier = {$inc: {}}; |
|
||||||
modifier.$inc[sortField] = incDec; |
|
||||||
Mongo.Collection.get(collectionName).update(selector, modifier, {multi: true}); |
|
||||||
} |
|
||||||
}); |
|
@ -1,31 +0,0 @@ |
|||||||
'use strict'; |
|
||||||
|
|
||||||
Sortable = {}; |
|
||||||
Sortable.collections = []; // array of collection names that the client is allowed to reorder
|
|
||||||
|
|
||||||
Meteor.methods({ |
|
||||||
/** |
|
||||||
* Update the sortField 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, sortField, incDec) { |
|
||||||
check(collectionName, String); |
|
||||||
// don't allow the client to modify just any collection
|
|
||||||
if (!Sortable || !Array.isArray(Sortable.collections)) { |
|
||||||
throw new Meteor.Error(500, 'Please define Sortable.collections'); |
|
||||||
} |
|
||||||
if (Sortable.collections.indexOf(collectionName) === -1) { |
|
||||||
throw new Meteor.Error(403, 'Collection <' + collectionName + '> is not Sortable. Please add it to Sortable.collections in server code.'); |
|
||||||
} |
|
||||||
|
|
||||||
check(ids, [String]); |
|
||||||
check(sortField, String); |
|
||||||
check(incDec, Number); |
|
||||||
var selector = {_id: {$in: ids}}, modifier = {$inc: {}}; |
|
||||||
modifier.$inc[sortField] = incDec; |
|
||||||
Mongo.Collection.get(collectionName).update(selector, modifier, {multi: true}); |
|
||||||
} |
|
||||||
}); |
|
@ -1,85 +0,0 @@ |
|||||||
// Package metadata file for Meteor.js
|
|
||||||
'use strict'; |
|
||||||
|
|
||||||
var packageName = 'rubaxa:sortable'; // https://atmospherejs.com/rubaxa/sortable
|
|
||||||
var gitHubPath = 'RubaXa/Sortable'; // https://github.com/RubaXa/Sortable
|
|
||||||
var npmPackageName = 'sortablejs'; // https://www.npmjs.com/package/sortablejs - optional but recommended; used as fallback if GitHub fails
|
|
||||||
|
|
||||||
/* All of the below is just to get the version number of the 3rd party library. |
|
||||||
* First we'll try to read it from package.json. This works when publishing or testing the package |
|
||||||
* but not when running an example app that uses a local copy of the package because the current
|
|
||||||
* directory will be that of the app, and it won't have package.json. Finding the path of a file is hard: |
|
||||||
* http://stackoverflow.com/questions/27435797/how-do-i-obtain-the-path-of-a-file-in-a-meteor-package
|
|
||||||
* Therefore, we'll fall back to GitHub (which is more frequently updated), and then to NPMJS. |
|
||||||
* We also don't have the HTTP package at this stage, and if we use Package.* in the request() callback, |
|
||||||
* it will error that it must be run in a Fiber. So we'll use Node futures. |
|
||||||
*/ |
|
||||||
var request = Npm.require('request'); |
|
||||||
var Future = Npm.require('fibers/future'); |
|
||||||
|
|
||||||
var fut = new Future; |
|
||||||
var version; |
|
||||||
|
|
||||||
if (!version) try { |
|
||||||
var packageJson = JSON.parse(Npm.require('fs').readFileSync('../package.json')); |
|
||||||
version = packageJson.version; |
|
||||||
} catch (e) { |
|
||||||
// if the file was not found, fall back to GitHub
|
|
||||||
console.warn('Could not find ../package.json to read version number from; trying GitHub...'); |
|
||||||
var url = 'https://api.github.com/repos/' + gitHubPath + '/tags'; |
|
||||||
request.get({ |
|
||||||
url: url, |
|
||||||
headers: { |
|
||||||
'User-Agent': 'request' // GitHub requires it
|
|
||||||
} |
|
||||||
}, function (error, response, body) { |
|
||||||
if (!error && response.statusCode === 200) { |
|
||||||
var versions = JSON.parse(body).map(function (version) { |
|
||||||
return version['name'].replace(/^\D+/, '') // trim leading non-digits from e.g. "v4.3.0"
|
|
||||||
}).sort(); |
|
||||||
fut.return(versions[versions.length -1]); |
|
||||||
} else { |
|
||||||
// GitHub API rate limit reached? Fall back to npmjs.
|
|
||||||
console.warn('GitHub request to', url, 'failed:\n ', response && response.statusCode, response && response.body, error || '', '\nTrying NPMJS...'); |
|
||||||
url = 'http://registry.npmjs.org/' + npmPackageName + '/latest'; |
|
||||||
request.get(url, function (error, response, body) { |
|
||||||
if (!error && response.statusCode === 200) |
|
||||||
fut.return(JSON.parse(body).version); |
|
||||||
else |
|
||||||
fut.throw('Could not get version information from ' + url + ' either (incorrect package name?):\n' + (response && response.statusCode || '') + (response && response.body || '') + (error || '')); |
|
||||||
}); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
version = fut.wait(); |
|
||||||
} |
|
||||||
|
|
||||||
// Now that we finally have an accurate version number...
|
|
||||||
Package.describe({ |
|
||||||
name: packageName, |
|
||||||
summary: 'Sortable: reactive minimalist reorderable drag-and-drop lists on modern browsers and touch devices', |
|
||||||
version: version, |
|
||||||
git: 'https://github.com/RubaXa/Sortable.git', |
|
||||||
documentation: '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.3.4'); // to watch collections getting created
|
|
||||||
api.export('Sortable'); // exported on the server too, as a global to hold the array of sortable collections (for security)
|
|
||||||
api.addFiles([ |
|
||||||
'../Sortable.js', |
|
||||||
'template.html', // the HTML comes first, so reactivize.js can refer to the template in it
|
|
||||||
'reactivize.js' |
|
||||||
], 'client'); |
|
||||||
api.addFiles('methods-client.js', 'client'); |
|
||||||
api.addFiles('methods-server.js', 'server'); |
|
||||||
}); |
|
||||||
|
|
||||||
Package.onTest(function (api) { |
|
||||||
api.use(packageName, 'client'); |
|
||||||
api.use('tinytest', 'client'); |
|
||||||
|
|
||||||
api.addFiles('test.js', 'client'); |
|
||||||
}); |
|
@ -1,26 +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 directory of the script |
|
||||||
cd "$( dirname "$0" )" |
|
||||||
|
|
||||||
# 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=$? |
|
||||||
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 have an error. Please post it at https://github.com/RubaXa/Sortable/issues" |
|
||||||
fi |
|
||||||
|
|
||||||
exit $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 of 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 = templateInstance.options.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 () { |
|
||||||
if(this.sortable) this.sortable.destroy(); |
|
||||||
}; |
|
@ -1,8 +0,0 @@ |
|||||||
@echo off |
|
||||||
REM Test Meteor package before publishing to Atmospherejs.com |
|
||||||
|
|
||||||
REM Sanity check: make sure we're in the directory of the script |
|
||||||
set DIR=%~dp0 |
|
||||||
cd %DIR% |
|
||||||
|
|
||||||
meteor test-packages ./ %* |
|
@ -1,35 +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 directory of the script |
|
||||||
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 |
|
||||||
|
|
||||||
|
|
||||||
EXIT_CODE=0 |
|
||||||
|
|
||||||
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 ./ |
|
||||||
EXIT_CODE=$(( $EXIT_CODE + $? )) |
|
||||||
fi |
|
||||||
|
|
||||||
exit $EXIT_CODE |
|
@ -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