Skip to content
Browse files

fixed sortables

  • Loading branch information...
1 parent 5e1f251 commit 51fd89c10c30fab25016102c11b87c588eb80dd2 @haldun committed Jan 9, 2013
Showing with 309 additions and 42 deletions.
  1. +2 −1 editor.html
  2. +0 −41 editor.js
  3. +307 −0 lib/knockout-sortable.js
View
3 editor.html
@@ -9,6 +9,7 @@
<script src="lib/jquery-ui.min.js"></script>
<script src="lib/knockout-latest.debug.js"></script>
<script src="lib/knockout.mapping-latest.js"></script>
+ <script src="lib/knockout-sortable.js"></script>
<script src="lib/bootstrap-tabs.js"></script>
<script src="lib/jquery.hotkeys.js"></script>
<script src="editor.js"></script>
@@ -45,7 +46,7 @@
<h2 data-bind="text: name"></h2>
<p data-bind="text: description"></p>
</div>
- <form data-bind="template: {name: 'tmpl-field-preview', foreach: fields}, sortableList: fields"
+ <form data-bind="sortable: {template: 'tmpl-field-preview', data: fields}"
class="form-stacked"></form>
<div data-bind="ifnot: hasFields">
<div class="alert-message block-message warning">
View
41 editor.js
@@ -305,44 +305,3 @@ ko.bindingHandlers.tab = {
}
}
};
-
-(function() {
- var __hasProp = Object.prototype.hasOwnProperty;
- var data_key = "sortable_data";
-
- ko.bindingHandlers['sortableList'] = {
- init: function(element, valueAccessor) {
- $(element).mousedown(function() {
- // Keep track of the order of all child nodes (including text/comments)
- $(this).data("preSortChildren", ko.utils.makeArray(this.childNodes));
- });
-
- return $(element).sortable({
- update: function(event, ui) {
- // Figure out what data item was moved, from where, and to where
- var movedDataItem = $(ui.item).data(data_key);
- var possiblyObservableArray = valueAccessor();
- var array = ko.utils.unwrapObservable(possiblyObservableArray);
- var previousIndex = ko.utils.arrayIndexOf(array, movedDataItem);
- var newIndex = $(element).children().index(ui.item);
-
- // Restore the order of child nodes (including text/comments)
- this.innerHTML = "";
- $(this).append($(this).data("preSortChildren"));
-
- // Update the underlying collection to reflect the data item movement
- array.splice(previousIndex, 1);
- array.splice(newIndex, 0, movedDataItem);
- if (typeof possiblyObservableArray.valueHasMutated === 'function')
- possiblyObservableArray.valueHasMutated();
- }
- });
- }
- };
-
- ko.bindingHandlers['sortableItem'] = {
- init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
- return $(element).data(data_key, ko.utils.unwrapObservable(valueAccessor()));
- }
- };
-}).call(this);
View
307 lib/knockout-sortable.js
@@ -0,0 +1,307 @@
+(function(factory) {
+ if (typeof define === "function" && define.amd) {
+ // AMD anonymous module
+ define(["knockout", "jquery", "jquery.ui.sortable"], factory);
+ } else {
+ // No module loader (plain <script> tag) - put directly in global namespace
+ factory(window.ko, jQuery);
+ }
+})(function(ko, $, undefined) {
+ var ITEMKEY = "ko_sortItem",
+ LISTKEY = "ko_sortList",
+ PARENTKEY = "ko_parentList",
+ DRAGKEY = "ko_dragItem";
+
+ //internal afterRender that adds meta-data to children
+ var addMetaDataAfterRender = function(elements, data) {
+ ko.utils.arrayForEach(elements, function(element) {
+ if (element.nodeType === 1) {
+ ko.utils.domData.set(element, ITEMKEY, data);
+ ko.utils.domData.set(element, PARENTKEY, ko.utils.domData.get(element.parentNode, LISTKEY));
+ }
+ });
+ };
+
+ //prepare the proper options for the template binding
+ var prepareTemplateOptions = function(valueAccessor, dataName) {
+ var result = {},
+ options = ko.utils.unwrapObservable(valueAccessor()),
+ actualAfterRender;
+
+ //build our options to pass to the template engine
+ if (options.data) {
+ result[dataName] = options.data;
+ result.name = options.template;
+ } else {
+ result[dataName] = valueAccessor();
+ }
+
+ ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove", "includeDestroyed", "templateEngine", "templateOptions"], function (option) {
+ result[option] = options[option] || ko.bindingHandlers.sortable[option];
+ });
+
+ //use an afterRender function to add meta-data
+ if (dataName === "foreach") {
+ if (result.afterRender) {
+ //wrap the existing function, if it was passed
+ actualAfterRender = result.afterRender;
+ result.afterRender = function(element, data) {
+ addMetaDataAfterRender.call(data, element, data);
+ actualAfterRender.call(data, element, data);
+ };
+ } else {
+ result.afterRender = addMetaDataAfterRender;
+ }
+ }
+
+ //return options to pass to the template binding
+ return result;
+ };
+
+ //connect items with observableArrays
+ ko.bindingHandlers.sortable = {
+ init: function(element, valueAccessor, allBindingsAccessor, data, context) {
+ var $element = $(element),
+ value = ko.utils.unwrapObservable(valueAccessor()) || {},
+ templateOptions = prepareTemplateOptions(valueAccessor, "foreach"),
+ sortable = {},
+ startActual, updateActual;
+
+ //remove leading/trailing text nodes from anonymous templates
+ ko.utils.arrayForEach(element.childNodes, function(node) {
+ if (node && node.nodeType === 3) {
+ node.parentNode.removeChild(node);
+ }
+ });
+
+ //build a new object that has the global options with overrides from the binding
+ $.extend(true, sortable, ko.bindingHandlers.sortable);
+ if (value.options && sortable.options) {
+ ko.utils.extend(sortable.options, value.options);
+ delete value.options;
+ }
+ ko.utils.extend(sortable, value);
+
+ //if allowDrop is an observable or a function, then execute it in a computed observable
+ if (sortable.connectClass && (ko.isObservable(sortable.allowDrop) || typeof sortable.allowDrop == "function")) {
+ ko.computed({
+ read: function() {
+ var value = ko.utils.unwrapObservable(sortable.allowDrop),
+ shouldAdd = typeof value == "function" ? value.call(this, templateOptions.foreach) : value;
+ ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, shouldAdd);
+ },
+ disposeWhenNodeIsRemoved: element
+ }, this);
+ } else {
+ ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop);
+ }
+
+ //wrap the template binding
+ ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+
+ //keep a reference to start/update functions that might have been passed in
+ startActual = sortable.options.start;
+ updateActual = sortable.options.update;
+
+ //initialize sortable binding after template binding has rendered in update function
+ var createTimeout = setTimeout(function() {
+ var dragItem;
+ $element.sortable(ko.utils.extend(sortable.options, {
+ start: function(event, ui) {
+ //make sure that fields have a chance to update model
+ ui.item.find("input:focus").change();
+ if (startActual) {
+ startActual.apply(this, arguments);
+ }
+ },
+ receive: function(event, ui) {
+ dragItem = ko.utils.domData.get(ui.item[0], DRAGKEY);
+ if (dragItem) {
+ //copy the model item, if a clone option is provided
+ if (dragItem.clone) {
+ dragItem = dragItem.clone();
+ }
+
+ //configure a handler to potentially manipulate item before drop
+ if (sortable.dragged) {
+ dragItem = sortable.dragged.call(this, dragItem, event, ui) || dragItem;
+ }
+ }
+ },
+ update: function(event, ui) {
+ var sourceParent, targetParent, targetIndex, i, targetUnwrapped, arg,
+ el = ui.item[0],
+ parentEl = ui.item.parent()[0],
+ item = ko.utils.domData.get(el, ITEMKEY) || dragItem;
+
+ dragItem = null;
+
+ //make sure that moves only run once, as update fires on multiple containers
+ if (item && (this === parentEl || $.contains(this, parentEl))) {
+ //identify parents
+ sourceParent = ko.utils.domData.get(el, PARENTKEY);
+ targetParent = ko.utils.domData.get(el.parentNode, LISTKEY);
+ targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el);
+
+ //take destroyed items into consideration
+ if (!templateOptions.includeDestroyed) {
+ targetUnwrapped = targetParent();
+ for (i = 0; i < targetIndex; i++) {
+ //add one for every destroyed item we find before the targetIndex in the target array
+ if (targetUnwrapped[i] && targetUnwrapped[i]._destroy) {
+ targetIndex++;
+ }
+ }
+ }
+
+ if (sortable.beforeMove || sortable.afterMove) {
+ arg = {
+ item: item,
+ sourceParent: sourceParent,
+ sourceParentNode: sourceParent && el.parentNode,
+ sourceIndex: sourceParent && sourceParent.indexOf(item),
+ targetParent: targetParent,
+ targetIndex: targetIndex,
+ cancelDrop: false
+ };
+ }
+
+ if (sortable.beforeMove) {
+ sortable.beforeMove.call(this, arg, event, ui);
+ if (arg.cancelDrop) {
+ //call cancel on the correct list
+ if (arg.sourceParent) {
+ $(arg.sourceParent === arg.targetParent ? this : ui.sender).sortable('cancel');
+ }
+ //for a draggable item just remove the element
+ else {
+ $(el).remove();
+ }
+
+ return;
+ }
+ }
+
+ if (targetIndex >= 0) {
+ if (sourceParent) {
+ sourceParent.remove(item);
+
+ //if using deferred updates plugin, force updates
+ if (ko.processAllDeferredBindingUpdates) {
+ ko.processAllDeferredBindingUpdates();
+ }
+ }
+
+ targetParent.splice(targetIndex, 0, item);
+ }
+
+ //rendering is handled by manipulating the observableArray; ignore dropped element
+ ko.utils.domData.set(el, ITEMKEY, null);
+ ui.item.remove();
+
+ //if using deferred updates plugin, force updates
+ if (ko.processAllDeferredBindingUpdates) {
+ ko.processAllDeferredBindingUpdates();
+ }
+
+ //allow binding to accept a function to execute after moving the item
+ if (sortable.afterMove) {
+ sortable.afterMove.call(this, arg, event, ui);
+ }
+ }
+
+ if (updateActual) {
+ updateActual.apply(this, arguments);
+ }
+ },
+ connectWith: sortable.connectClass ? "." + sortable.connectClass : false
+ }));
+
+ //handle enabling/disabling sorting
+ if (sortable.isEnabled !== undefined) {
+ ko.computed({
+ read: function() {
+ $element.sortable(ko.utils.unwrapObservable(sortable.isEnabled) ? "enable" : "disable");
+ },
+ disposeWhenNodeIsRemoved: element
+ });
+ }
+ }, 0);
+
+ //handle disposal
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+ //only call destroy if sortable has been created
+ if ($element.data("sortable")) {
+ $element.sortable("destroy");
+ }
+
+ //do not create the sortable if the element has been removed from DOM
+ clearTimeout(createTimeout);
+ });
+
+ return { 'controlsDescendantBindings': true };
+ },
+ update: function(element, valueAccessor, allBindingsAccessor, data, context) {
+ var templateOptions = prepareTemplateOptions(valueAccessor, "foreach");
+
+ //attach meta-data
+ ko.utils.domData.set(element, LISTKEY, templateOptions.foreach);
+
+ //call template binding's update with correct options
+ ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+ },
+ connectClass: 'ko_container',
+ allowDrop: true,
+ afterMove: null,
+ beforeMove: null,
+ options: {}
+ };
+
+ //create a draggable that is appropriate for dropping into a sortable
+ ko.bindingHandlers.draggable = {
+ init: function(element, valueAccessor, allBindingsAccessor, data, context) {
+ var value = ko.utils.unwrapObservable(valueAccessor()) || {},
+ options = value.options || {},
+ draggableOptions = ko.utils.extend({}, ko.bindingHandlers.draggable.options),
+ templateOptions = prepareTemplateOptions(valueAccessor, "data"),
+ connectClass = value.connectClass || ko.bindingHandlers.draggable.connectClass,
+ isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.draggable.isEnabled;
+
+ value = value.data || value;
+
+ //set meta-data
+ ko.utils.domData.set(element, DRAGKEY, value);
+
+ //override global options with override options passed in
+ ko.utils.extend(draggableOptions, options);
+
+ //setup connection to a sortable
+ draggableOptions.connectToSortable = connectClass ? "." + connectClass : false;
+
+ //initialize draggable
+ $(element).draggable(draggableOptions);
+
+ //handle enabling/disabling sorting
+ if (isEnabled !== undefined) {
+ ko.computed({
+ read: function() {
+ $(element).draggable(ko.utils.unwrapObservable(isEnabled) ? "enable" : "disable");
+ },
+ disposeWhenNodeIsRemoved: element
+ });
+ }
+
+ return ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+ },
+ update: function(element, valueAccessor, allBindingsAccessor, data, context) {
+ var templateOptions = prepareTemplateOptions(valueAccessor, "data");
+
+ return ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+ },
+ connectClass: ko.bindingHandlers.sortable.connectClass,
+ options: {
+ helper: "clone"
+ }
+ };
+
+});

0 comments on commit 51fd89c

Please sign in to comment.
Something went wrong with that request. Please try again.