Skip to content

Commit b1cee4d

Browse files
committed
v0.1.0: initital
0 parents  commit b1cee4d

File tree

5 files changed

+304
-0
lines changed

5 files changed

+304
-0
lines changed

.editorconfig

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# editorconfig.org
2+
root = true
3+
4+
[*]
5+
indent_style = tab
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.md]
12+
trim_trailing_whitespace = false

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
mock.png
3+
.*.sw*
4+
.build*
5+
jquery.fn.*

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# knockout-sortablejs
2+
A Knockout.js binding to [SortableJS](https://github.com/RubaXa/Sortable/).
3+
4+
Demo: http://rubaxa.github.io/Sortable/
5+
6+
### Support KnockoutJS
7+
Include [knockout-sortable.js](knockout-sortable.js)
8+
9+
```html
10+
<div data-bind="sortable: {foreach: yourObservableArray, options: {/* sortable options here */}}">
11+
<!-- optional item template here -->
12+
</div>
13+
14+
<div data-bind="draggable: {foreach: yourObservableArray, options: {/* sortable options here */}}">
15+
<!-- optional item template here -->
16+
</div>
17+
```
18+
19+
Using this bindingHandler sorts the observableArray when the user sorts the HTMLElements.
20+
21+
The sortable/draggable bindingHandlers supports the same syntax as Knockouts built in [template](http://knockoutjs.com/documentation/template-binding.html) binding except for the `data` option, meaning that you could supply the name of a template or specify a separate templateEngine. The difference between the sortable and draggable handlers is that the draggable has the sortable `group` option set to `{pull:'clone',put: false}` and the `sort` option set to false by default (overridable).
22+
23+
#### Other attributes are:
24+
* options: an object that contains settings for the underlaying sortable, ie `group`,`handle`, events etc.
25+
* collection: if your `foreach` array is a computed then you would supply the underlaying observableArray that you would like to sort here.
26+
* manuallyHandleUpdateEvents: a boolean to turn off the change events on update that other polymer elements listen to.

knockout-sortable.js

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*global ko*/
2+
3+
(function (factory) {
4+
"use strict";
5+
//get ko ref via global or require
6+
var koRef;
7+
if (typeof ko !== 'undefined') {
8+
//global ref already defined
9+
koRef = ko;
10+
}
11+
else if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
12+
//commonjs / node.js
13+
koRef = require('knockout');
14+
}
15+
//get sortable ref via global or require
16+
var sortableRef;
17+
if (typeof Sortable !== 'undefined') {
18+
//global ref already defined
19+
sortableRef = Sortable;
20+
}
21+
else if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
22+
//commonjs / node.js
23+
sortableRef = require('sortablejs');
24+
}
25+
//use references if we found them
26+
if (koRef !== undefined && sortableRef !== undefined) {
27+
factory(koRef, sortableRef);
28+
}
29+
//if both references aren't found yet, get via AMD if available
30+
else if (typeof define === 'function' && define.amd) {
31+
//we may have a reference to only 1, or none
32+
if (koRef !== undefined && sortableRef === undefined) {
33+
define(['./Sortable'], function (amdSortableRef) {
34+
factory(koRef, amdSortableRef);
35+
});
36+
}
37+
else if (koRef === undefined && sortableRef !== undefined) {
38+
define(['knockout'], function (amdKnockout) {
39+
factory(amdKnockout, sortableRef);
40+
});
41+
}
42+
else if (koRef === undefined && sortableRef === undefined) {
43+
define(['knockout', './Sortable'], factory);
44+
}
45+
}
46+
//no more routes to get references
47+
else {
48+
//report specific error
49+
if (koRef !== undefined && sortableRef === undefined) {
50+
throw new Error('knockout-sortable could not get reference to Sortable');
51+
}
52+
else if (koRef === undefined && sortableRef !== undefined) {
53+
throw new Error('knockout-sortable could not get reference to Knockout');
54+
}
55+
else if (koRef === undefined && sortableRef === undefined) {
56+
throw new Error('knockout-sortable could not get reference to Knockout or Sortable');
57+
}
58+
}
59+
})(function (ko, Sortable) {
60+
"use strict";
61+
62+
var init = function (element, valueAccessor, allBindings, viewModel, bindingContext, sortableOptions) {
63+
64+
var options = buildOptions(valueAccessor, sortableOptions);
65+
66+
// It's seems that we cannot update the eventhandlers after we've created
67+
// the sortable, so define them in init instead of update
68+
['onStart', 'onEnd', 'onRemove', 'onAdd', 'onUpdate', 'onSort', 'onFilter', 'onMove', 'onClone'].forEach(function (e) {
69+
if (options[e] || eventHandlers[e])
70+
options[e] = function (eventType, parentVM, parentBindings, handler, e) {
71+
var itemVM = ko.dataFor(e.item),
72+
// All of the bindings on the parent element
73+
bindings = ko.utils.peekObservable(parentBindings()),
74+
// The binding options for the draggable/sortable binding of the parent element
75+
bindingHandlerBinding = bindings.sortable || bindings.draggable,
76+
// The collection that we should modify
77+
collection = bindingHandlerBinding.collection || bindingHandlerBinding.foreach;
78+
if (handler)
79+
handler(e, itemVM, parentVM, collection, bindings);
80+
if (eventHandlers[eventType])
81+
eventHandlers[eventType](e, itemVM, parentVM, collection, bindings);
82+
}.bind(undefined, e, viewModel, allBindings, options[e]);
83+
});
84+
85+
var sortableElement = Sortable.create(element, options);
86+
87+
// Destroy the sortable if knockout disposes the element it's connected to
88+
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
89+
sortableElement.destroy();
90+
});
91+
return ko.bindingHandlers.template.init(element, valueAccessor);
92+
},
93+
update = function (element, valueAccessor, allBindings, viewModel, bindingContext, sortableOptions) {
94+
95+
// There seems to be some problems with updating the options of a sortable
96+
// Tested to change eventhandlers and the group options without any luck
97+
98+
return ko.bindingHandlers.template.update(element, valueAccessor, allBindings, viewModel, bindingContext);
99+
},
100+
eventHandlers = (function (handlers) {
101+
102+
var moveOperations = [],
103+
tryMoveOperation = function (e, itemVM, parentVM, collection, parentBindings) {
104+
// A move operation is the combination of a add and remove event,
105+
// this is to make sure that we have both the target and origin collections
106+
var currentOperation = { event: e, itemVM: itemVM, parentVM: parentVM, collection: collection, parentBindings: parentBindings },
107+
existingOperation = moveOperations.filter(function (op) {
108+
return op.itemVM === currentOperation.itemVM;
109+
})[0];
110+
111+
if (!existingOperation) {
112+
moveOperations.push(currentOperation);
113+
}
114+
else {
115+
// We're finishing the operation and already have a handle on
116+
// the operation item meaning that it's safe to remove it
117+
moveOperations.splice(moveOperations.indexOf(existingOperation), 1);
118+
119+
var removeOperation = currentOperation.event.type === 'remove' ? currentOperation : existingOperation,
120+
addOperation = currentOperation.event.type === 'add' ? currentOperation : existingOperation;
121+
122+
moveItem(itemVM, removeOperation.collection, addOperation.collection, addOperation.event.clone, addOperation.event);
123+
}
124+
},
125+
// Moves an item from the "from" collection to the "to" collection, these
126+
// can be references to the same collection which means it's a sort.
127+
// clone indicates if we should move or copy the item into the new collection
128+
moveItem = function (itemVM, from, to, clone, e) {
129+
// Unwrapping this allows us to manipulate the actual array
130+
var fromArray = from(),
131+
// It's not certain that the items actual index is the same
132+
// as the index reported by sortable due to filtering etc.
133+
originalIndex = fromArray.indexOf(itemVM),
134+
newIndex = e.newIndex;
135+
136+
// We have to find out the actual desired index of the to array,
137+
// as this might be a computed array. We could otherwise potentially
138+
// drop an item above the 3rd visible item, but the 2nd visible item
139+
// has an actual index of 5.
140+
if (e.item.previousElementSibling) {
141+
newIndex = to().indexOf(ko.dataFor(e.item.previousElementSibling)) + 1;
142+
}
143+
144+
// Remove sortables "unbound" element
145+
e.item.parentNode.removeChild(e.item);
146+
147+
// This splice is necessary for both clone and move/sort
148+
// In sort/move since it shouldn't be at this index/in this array anymore
149+
// In clone since we have to work around knockouts valuHasMutated
150+
// when manipulating arrays and avoid a "unbound" item added by sortable
151+
fromArray.splice(originalIndex, 1);
152+
// Update the array, this will also remove sortables "unbound" clone
153+
from.valueHasMutated();
154+
if (clone && from !== to) {
155+
// Read the item
156+
fromArray.splice(originalIndex, 0, itemVM);
157+
// Force knockout to update
158+
from.valueHasMutated();
159+
}
160+
// Force deferred tasks to run now, registering the removal
161+
ko.tasks.runEarly();
162+
// Insert the item on its new position
163+
to().splice(newIndex, 0, itemVM);
164+
// Make sure to tell knockout that we've modified the actual array.
165+
to.valueHasMutated();
166+
};
167+
168+
handlers.onRemove = tryMoveOperation;
169+
handlers.onAdd = tryMoveOperation;
170+
handlers.onUpdate = function (e, itemVM, parentVM, collection, parentBindings) {
171+
// This will be performed as a sort since the to/from collections
172+
// reference the same collection and clone is set to false
173+
moveItem(itemVM, collection, collection, false, e);
174+
};
175+
176+
return handlers;
177+
})({}),
178+
// bindingOptions are the options set in the "data-bind" attribute in the ui.
179+
// options are custom options, for instance draggable/sortable specific options
180+
buildOptions = function (bindingOptions, options) {
181+
// deep clone/copy of properties from the "from" argument onto
182+
// the "into" argument and returns the modified "into"
183+
var merge = function (into, from) {
184+
for (var prop in from) {
185+
if (Object.prototype.toString.call(from[prop]) === '[object Object]') {
186+
if (Object.prototype.toString.call(into[prop]) !== '[object Object]') {
187+
into[prop] = {};
188+
}
189+
into[prop] = merge(into[prop], from[prop]);
190+
}
191+
else
192+
into[prop] = from[prop];
193+
}
194+
195+
return into;
196+
},
197+
// unwrap the supplied options
198+
unwrappedOptions = ko.utils.peekObservable(bindingOptions()).options || {};
199+
200+
// Make sure that we don't modify the provided settings object
201+
options = merge({}, options);
202+
203+
// group is handled differently since we should both allow to change
204+
// a draggable to a sortable (and vice versa), but still be able to set
205+
// a name on a draggable without it becoming a drop target.
206+
if (unwrappedOptions.group && Object.prototype.toString.call(unwrappedOptions.group) !== '[object Object]') {
207+
// group property is a name string declaration, convert to object.
208+
unwrappedOptions.group = { name: unwrappedOptions.group };
209+
}
210+
211+
return merge(options, unwrappedOptions);
212+
};
213+
214+
ko.bindingHandlers.draggable = {
215+
sortableOptions: {
216+
group: { pull: 'clone', put: false },
217+
sort: false
218+
},
219+
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
220+
return init(element, valueAccessor, allBindings, viewModel, bindingContext, ko.bindingHandlers.draggable.sortableOptions);
221+
},
222+
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
223+
return update(element, valueAccessor, allBindings, viewModel, bindingContext, ko.bindingHandlers.draggable.sortableOptions);
224+
}
225+
};
226+
227+
ko.bindingHandlers.sortable = {
228+
sortableOptions: {
229+
group: { pull: true, put: true }
230+
},
231+
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
232+
return init(element, valueAccessor, allBindings, viewModel, bindingContext, ko.bindingHandlers.sortable.sortableOptions);
233+
},
234+
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
235+
return update(element, valueAccessor, allBindings, viewModel, bindingContext, ko.bindingHandlers.sortable.sortableOptions);
236+
}
237+
};
238+
});

package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "knockout-sortablejs",
3+
"version": "0.1.0",
4+
"description": "A Knockout.js binding to SortableJS.",
5+
"main": "knockout-sortable.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/SortableJS/knockout-sortablejs.git"
12+
},
13+
"keywords": [
14+
"knockout",
15+
"sortable"
16+
],
17+
"author": "srosengren <sebastian@rosengren.me>",
18+
"license": "MIT",
19+
"bugs": {
20+
"url": "https://github.com/SortableJS/knockout-sortablejs/issues"
21+
},
22+
"homepage": "https://github.com/SortableJS/knockout-sortablejs#readme"
23+
}

0 commit comments

Comments
 (0)