update playlist drag and drop

This commit is contained in:
Luke Pulverenti 2016-07-19 16:23:28 -04:00
parent dc69bc6055
commit a694661cc1
43 changed files with 74 additions and 2715 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,2 +0,0 @@
browser
server

View File

@ -1 +0,0 @@
METEOR@1.1.0.3

View File

@ -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

View File

@ -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;
}

View File

@ -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">&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>

View File

@ -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);
}
}
});

View File

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

View File

@ -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 %*

View File

@ -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 "$@"

View File

@ -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.');
}
});

View File

@ -1,3 +0,0 @@
'use strict';
Sortable.collections = ['attributes'];

View File

@ -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});
}
});

View File

@ -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});
}
});

View File

@ -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');
});

View File

@ -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

View File

@ -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();
};

View File

@ -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 ./ %*

View File

@ -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

View File

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

View File

@ -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');
});

View File

@ -1,239 +0,0 @@
html {
background-image: -webkit-linear-gradient(bottom, #F4E2C9 20%, #F4D7C9 100%);
background-image: -ms-linear-gradient(bottom, #F4E2C9 20%, #F4D7C9 100%);
background-image: linear-gradient(to bottom, #F4E2C9 20%, #F4D7C9 100%);
}
html, body {
margin: 0;
padding: 0;
position: relative;
color: #464637;
min-height: 100%;
font-size: 20px;
font-family: 'Roboto', sans-serif;
font-weight: 300;
}
h1 {
color: #FF3F00;
font-size: 20px;
font-family: 'Roboto', sans-serif;
font-weight: 300;
text-align: center;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
.container {
width: 80%;
margin: auto;
min-width: 1100px;
max-width: 1300px;
position: relative;
}
@media (min-width: 750px) and (max-width: 970px){
.container {
width: 100%;
min-width: 750px;
}
}
.sortable-ghost {
opacity: .2;
}
img {
border: 0;
vertical-align: middle;
}
.logo {
top: 55px;
left: 30px;
position: absolute;
}
.title {
color: #fff;
padding: 3px 10px;
display: inline-block;
position: relative;
background-color: #FF7373;
z-index: 1000;
}
.title_xl {
padding: 3px 15px;
font-size: 40px;
}
.tile {
width: 22%;
min-width: 245px;
color: #FF7270;
padding: 10px 30px;
text-align: center;
margin-top: 15px;
margin-left: 5px;
margin-right: 30px;
background-color: #fff;
display: inline-block;
vertical-align: top;
}
.tile__name {
cursor: move;
padding-bottom: 10px;
border-bottom: 1px solid #FF7373;
}
.tile__list {
margin-top: 10px;
}
.tile__list:last-child {
margin-right: 0;
min-height: 80px;
}
.tile__list img {
cursor: move;
margin: 10px;
border-radius: 100%;
}
.block {
opacity: 1;
position: absolute;
}
.block__list {
padding: 20px 0;
max-width: 360px;
margin-top: -8px;
margin-left: 5px;
background-color: #fff;
}
.block__list-title {
margin: -20px 0 0;
padding: 10px;
text-align: center;
background: #5F9EDF;
}
.block__list li { cursor: move; }
.block__list_words li {
background-color: #fff;
padding: 10px 40px;
}
.block__list_words .sortable-ghost {
opacity: 0.4;
background-color: #F4E2C9;
}
.block__list_words li:first-letter {
text-transform: uppercase;
}
.block__list_tags {
padding-left: 30px;
}
.block__list_tags:after {
clear: both;
content: '';
display: block;
}
.block__list_tags li {
color: #fff;
float: left;
margin: 8px 20px 10px 0;
padding: 5px 10px;
min-width: 10px;
background-color: #5F9EDF;
text-align: center;
}
.block__list_tags li:first-child:first-letter {
text-transform: uppercase;
}
#editable {}
#editable li {
position: relative;
}
#editable i {
-webkit-transition: opacity .2s;
transition: opacity .2s;
opacity: 0;
display: block;
cursor: pointer;
color: #c00;
top: 10px;
right: 40px;
position: absolute;
font-style: normal;
}
#editable li:hover i {
opacity: 1;
}
#filter {}
#filter button {
color: #fff;
width: 100%;
border: none;
outline: 0;
opacity: .5;
margin: 10px 0 0;
transition: opacity .1s ease;
cursor: pointer;
background: #5F9EDF;
padding: 10px 0;
font-size: 20px;
}
#filter button:hover {
opacity: 1;
}
#filter .block__list {
padding-bottom: 0;
}
.drag-handle {
margin-right: 10px;
font: bold 20px Sans-Serif;
color: #5F9EDF;
display: inline-block;
cursor: move;
cursor: -webkit-grabbing; /* overrides 'move' */
}
#todos input {
padding: 5px;
font-size: 14px;
font-family: 'Roboto', sans-serif;
font-weight: 300;
}
#nested ul li {
background-color: rgba(0,0,0,.05);
}

View File

@ -1,226 +0,0 @@
(function () {
'use strict';
var byId = function (id) { return document.getElementById(id); },
loadScripts = function (desc, callback) {
var deps = [], key, idx = 0;
for (key in desc) {
deps.push(key);
}
(function _next() {
var pid,
name = deps[idx],
script = document.createElement('script');
script.type = 'text/javascript';
script.src = desc[deps[idx]];
pid = setInterval(function () {
if (window[name]) {
clearTimeout(pid);
deps[idx++] = window[name];
if (deps[idx]) {
_next();
} else {
callback.apply(null, deps);
}
}
}, 30);
document.getElementsByTagName('head')[0].appendChild(script);
})()
},
console = window.console;
if (!console.log) {
console.log = function () {
alert([].join.apply(arguments, ' '));
};
}
Sortable.create(byId('foo'), {
group: "words",
animation: 150,
store: {
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group);
return order ? order.split('|') : [];
},
set: function (sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group, order.join('|'));
}
},
onAdd: function (evt){ console.log('onAdd.foo:', [evt.item, evt.from]); },
onUpdate: function (evt){ console.log('onUpdate.foo:', [evt.item, evt.from]); },
onRemove: function (evt){ console.log('onRemove.foo:', [evt.item, evt.from]); },
onStart:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onSort:function(evt){ console.log('onStart.foo:', [evt.item, evt.from]);},
onEnd: function(evt){ console.log('onEnd.foo:', [evt.item, evt.from]);}
});
Sortable.create(byId('bar'), {
group: "words",
animation: 150,
onAdd: function (evt){ console.log('onAdd.bar:', evt.item); },
onUpdate: function (evt){ console.log('onUpdate.bar:', evt.item); },
onRemove: function (evt){ console.log('onRemove.bar:', evt.item); },
onStart:function(evt){ console.log('onStart.foo:', evt.item);},
onEnd: function(evt){ console.log('onEnd.foo:', evt.item);}
});
// Multi groups
Sortable.create(byId('multi'), {
animation: 150,
draggable: '.tile',
handle: '.tile__name'
});
[].forEach.call(byId('multi').getElementsByClassName('tile__list'), function (el){
Sortable.create(el, {
group: 'photo',
animation: 150
});
});
// Editable list
var editableList = Sortable.create(byId('editable'), {
animation: 150,
filter: '.js-remove',
onFilter: function (evt) {
evt.item.parentNode.removeChild(evt.item);
}
});
byId('addUser').onclick = function () {
Ply.dialog('prompt', {
title: 'Add',
form: { name: 'name' }
}).done(function (ui) {
var el = document.createElement('li');
el.innerHTML = ui.data.name + '<i class="js-remove">✖</i>';
editableList.el.appendChild(el);
});
};
// Advanced groups
[{
name: 'advanced',
pull: true,
put: true
},
{
name: 'advanced',
pull: 'clone',
put: false
}, {
name: 'advanced',
pull: false,
put: true
}].forEach(function (groupOpts, i) {
Sortable.create(byId('advanced-' + (i + 1)), {
sort: (i != 1),
group: groupOpts,
animation: 150
});
});
// 'handle' option
Sortable.create(byId('handle-1'), {
handle: '.drag-handle',
animation: 150
});
// Angular example
angular.module('todoApp', ['ng-sortable'])
.constant('ngSortableConfig', {onEnd: function() {
console.log('default onEnd()');
}})
.controller('TodoController', ['$scope', function ($scope) {
$scope.todos = [
{text: 'learn angular', done: true},
{text: 'build an angular app', done: false}
];
$scope.addTodo = function () {
$scope.todos.push({text: $scope.todoText, done: false});
$scope.todoText = '';
};
$scope.remaining = function () {
var count = 0;
angular.forEach($scope.todos, function (todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.archive = function () {
var oldTodos = $scope.todos;
$scope.todos = [];
angular.forEach(oldTodos, function (todo) {
if (!todo.done) $scope.todos.push(todo);
});
};
}])
.controller('TodoControllerNext', ['$scope', function ($scope) {
$scope.todos = [
{text: 'learn Sortable', done: true},
{text: 'use ng-sortable', done: false},
{text: 'Enjoy', done: false}
];
$scope.remaining = function () {
var count = 0;
angular.forEach($scope.todos, function (todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.sortableConfig = { group: 'todo', animation: 150 };
'Start End Add Update Remove Sort'.split(' ').forEach(function (name) {
$scope.sortableConfig['on' + name] = console.log.bind(console, name);
});
}]);
})();
// Background
document.addEventListener("DOMContentLoaded", function () {
function setNoiseBackground(el, width, height, opacity) {
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
for (var i = 0; i < width; i++) {
for (var j = 0; j < height; j++) {
var val = Math.floor(Math.random() * 255);
context.fillStyle = "rgba(" + val + "," + val + "," + val + "," + opacity + ")";
context.fillRect(i, j, 1, 1);
}
}
el.style.background = "url(" + canvas.toDataURL("image/png") + ")";
}
setNoiseBackground(document.getElementsByTagName('body')[0], 50, 50, 0.02);
}, false);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"/>
<!-- List with handle -->
<div id="listWithHandle" class="list-group">
<div class="list-group-item">
<span class="badge">14</span>
<span class="glyphicon glyphicon-move" aria-hidden="true"></span>
Drag me by the handle
</div>
<div class="list-group-item">
<span class="badge">2</span>
<span class="glyphicon glyphicon-move" aria-hidden="true"></span>
You can also select text
</div>
<div class="list-group-item">
<span class="badge">1</span>
<span class="glyphicon glyphicon-move" aria-hidden="true"></span>
Best of both worlds!
</div>
</div>
</body>
</html>

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>IFrame playground</title>
</head>
<body>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"/>
<!-- Latest Sortable -->
<script src="../../Sortable.js"></script>
<!-- Simple List -->
<div id="simpleList" class="list-group">
<div class="list-group-item">This is <a href="http://rubaxa.github.io/Sortable/">Sortable</a></div>
<div class="list-group-item">It works with Bootstrap...</div>
<div class="list-group-item">...out of the box.</div>
<div class="list-group-item">It has support for touch devices.</div>
<div class="list-group-item">Just drag some elements around.</div>
</div>
<script>
(function () {
Sortable.create(simpleList, {group: 'shared'});
var iframe = document.createElement('iframe');
iframe.src = 'frame.html';
iframe.width = '100%';
iframe.onload = function () {
var doc = iframe.contentDocument,
list = doc.getElementById('listWithHandle');
Sortable.create(list, {group: 'shared'});
};
document.body.appendChild(iframe);
})();
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,4 +1,4 @@
define(['itemShortcuts', 'connectionManager', 'layoutManager', 'browser', 'dom', 'registerElement'], function (itemShortcuts, connectionManager, layoutManager, browser, dom) {
define(['itemShortcuts', 'connectionManager', 'layoutManager', 'browser', 'dom', 'loading', 'registerElement'], function (itemShortcuts, connectionManager, layoutManager, browser, dom, loading) {
var ItemsContainerProtoType = Object.create(HTMLDivElement.prototype);
@ -105,6 +105,76 @@
});
};
function onDrop(evt, itemsContainer) {
var playlistId = itemsContainer.getAttribute('data-playlistid');
loading.show();
var el = evt.item;
var newIndex = evt.newIndex;
var itemId = el.getAttribute('data-playlistitemid');
var serverId = el.getAttribute('data-serverid');
var apiClient = connectionManager.getApiClient(serverId);
apiClient.ajax({
url: apiClient.getUrl('Playlists/' + playlistId + '/Items/' + itemId + '/Move/' + newIndex),
type: 'POST'
}).then(function () {
el.setAttribute('data-index', newIndex);
loading.hide();
}, function () {
loading.hide();
itemsContainer.dispatchEvent(new CustomEvent('needsrefresh', {
detail: {},
cancelable: false,
bubbles: true
}));
});
}
ItemsContainerProtoType.enableDragReordering = function (enabled) {
var current = this.sortable;
if (!enabled) {
if (current) {
current.destroy();
this.sortable = null;
}
return;
}
if (current) {
return;
}
var self = this;
require(['sortable'], function (Sortable) {
self.sortable = new Sortable(self, {
draggable: ".listItem",
handle: '.listViewDragHandle',
// dragging ended
onEnd: function (/**Event*/evt) {
onDrop(evt, self);
}
});
});
};
ItemsContainerProtoType.attachedCallback = function () {
this.addEventListener('click', onClick);
@ -130,6 +200,7 @@
this.enableHoverMenu(false);
this.enableMultiSelect(false);
this.enableDragReordering(false);
this.removeEventListener('click', onClick);
this.removeEventListener('contextmenu', onContextMenu);
this.removeEventListener('contextmenu', disableEvent);

View File

@ -76,22 +76,6 @@
elem.setAttribute('data-playlistid', item.Id);
elem.innerHTML = html;
var listParent = elem;
require(['sortable'], function (Sortable) {
var sortable = new Sortable(listParent, {
draggable: ".listItem",
handle: '.listViewDragHandle',
// dragging ended
onEnd: function (/**Event*/evt) {
onDrop(evt, page, item);
}
});
});
ImageLoader.lazyChildren(elem);
$('.btnNextPage', elem).on('click', function () {
@ -108,37 +92,12 @@
});
}
function onDrop(evt, page, item) {
Dashboard.showLoadingMsg();
var el = evt.item;
var newIndex = evt.newIndex;
var itemId = el.getAttribute('data-playlistitemid');
ApiClient.ajax({
url: ApiClient.getUrl('Playlists/' + item.Id + '/Items/' + itemId + '/Move/' + newIndex),
type: 'POST'
}).then(function () {
el.setAttribute('data-index', newIndex);
Dashboard.hideLoadingMsg();
}, function () {
Dashboard.hideLoadingMsg();
reloadItems(page, item);
});
}
function init(page, item) {
var elem = page.querySelector('#childrenContent .itemsContainer');
elem.enableDragReordering(true);
elem.addEventListener('needsrefresh', function () {
reloadItems(page, item);