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