forked from SortableJS/Sortable
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathreactivize.js
201 lines (181 loc) · 10.3 KB
/
reactivize.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
/*
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 () {
this.sortable.destroy();
};