snappymail/vendors/knockout-sortable/build/knockout-sortable.js

377 lines
17 KiB
JavaScript

// knockout-sortable 1.2.0 | (c) 2019 Ryan Niemeyer | http://www.opensource.org/licenses/mit-license
(function(ko, $) {
var ITEMKEY = "ko_sortItem",
INDEXKEY = "ko_sourceIndex",
LISTKEY = "ko_sortList",
PARENTKEY = "ko_parentList",
DRAGKEY = "ko_dragItem",
unwrap = ko.utils.unwrapObservable,
dataGet = ko.utils.domData.get,
dataSet = ko.utils.domData.set;
//internal afterRender that adds meta-data to children
var addMetaDataAfterRender = function(elements, data) {
ko.utils.arrayForEach(elements, function(element) {
if (element.nodeType === 1) {
dataSet(element, ITEMKEY, data);
dataSet(element, PARENTKEY, dataGet(element.parentNode, LISTKEY));
}
});
};
//prepare the proper options for the template binding
var prepareTemplateOptions = function(valueAccessor, dataName) {
var result = {},
options = {},
actualAfterRender;
//build our options to pass to the template engine
if (ko.utils.peekObservable(valueAccessor()).data) {
options = unwrap(valueAccessor() || {});
result[dataName] = options.data;
if (options.hasOwnProperty("template")) {
result.name = options.template;
}
} else {
result[dataName] = valueAccessor();
}
ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove",
"includeDestroyed", "templateEngine", "templateOptions", "nodes"], function (option) {
if (options.hasOwnProperty(option)) {
result[option] = options[option];
} else if (ko.bindingHandlers.sortable.hasOwnProperty(option)) {
result[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;
};
var updateIndexFromDestroyedItems = function(index, items) {
var unwrapped = unwrap(items);
if (unwrapped) {
for (var i = 0; i <= index; i++) {
//add one for every destroyed item we find before the targetIndex in the target array
if (unwrapped[i] && unwrap(unwrapped[i]._destroy)) {
index++;
}
}
}
return index;
};
//remove problematic leading/trailing whitespace from templates
var stripTemplateWhitespace = function(element, name) {
var templateSource,
templateElement;
//process named templates
if (name) {
templateElement = document.getElementById(name);
if (templateElement) {
templateSource = new ko.templateSources.domElement(templateElement);
templateSource.text($.trim(templateSource.text()));
}
}
else {
//remove leading/trailing non-elements from anonymous templates
$(element).contents().each(function() {
if (this && this.nodeType !== 1) {
element.removeChild(this);
}
});
}
};
//connect items with observableArrays
ko.bindingHandlers.sortable = {
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
var $element = $(element),
value = unwrap(valueAccessor()) || {},
templateOptions = prepareTemplateOptions(valueAccessor, "foreach"),
sortable = {},
startActual, updateActual;
stripTemplateWhitespace(element, templateOptions.name);
//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 = unwrap(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;
//ensure draggable table row cells maintain their width while dragging (unless a helper is provided)
if ( !sortable.options.helper ) {
sortable.options.helper = function(e, ui) {
if (ui.is("tr")) {
ui.children().each(function() {
$(this).width($(this).width());
});
}
return ui;
};
}
//initialize sortable binding after template binding has rendered in update function
var createTimeout = setTimeout(function() {
var dragItem;
var originalReceive = sortable.options.receive;
$element.sortable(ko.utils.extend(sortable.options, {
start: function(event, ui) {
//track original index
var el = ui.item[0];
dataSet(el, INDEXKEY, ko.utils.arrayIndexOf(ui.item.parent().children(), el));
//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) {
//optionally apply an existing receive handler
if (typeof originalReceive === "function") {
originalReceive.call(this, event, ui);
}
dragItem = dataGet(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, sourceIndex, targetIndex, arg,
el = ui.item[0],
parentEl = ui.item.parent()[0],
item = dataGet(el, ITEMKEY) || dragItem;
if (!item) {
$(el).remove();
}
dragItem = null;
//make sure that moves only run once, as update fires on multiple containers
if (item && (this === parentEl)) {
//identify parents
sourceParent = dataGet(el, PARENTKEY);
sourceIndex = dataGet(el, INDEXKEY);
targetParent = dataGet(el.parentNode, LISTKEY);
targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el);
//take destroyed items into consideration
if (!templateOptions.includeDestroyed) {
sourceIndex = updateIndexFromDestroyedItems(sourceIndex, sourceParent);
targetIndex = updateIndexFromDestroyedItems(targetIndex, targetParent);
}
//build up args for the callbacks
if (sortable.beforeMove || sortable.afterMove) {
arg = {
item: item,
sourceParent: sourceParent,
sourceParentNode: sourceParent && ui.sender || el.parentNode,
sourceIndex: sourceIndex,
targetParent: targetParent,
targetIndex: targetIndex,
cancelDrop: false
};
//execute the configured callback prior to actually moving items
if (sortable.beforeMove) {
sortable.beforeMove.call(this, arg, event, ui);
}
}
//call cancel on the correct list, so KO can take care of DOM manipulation
if (sourceParent) {
$(sourceParent === targetParent ? this : ui.sender || this).sortable("cancel");
}
//for a draggable item just remove the element
else {
$(el).remove();
}
//if beforeMove told us to cancel, then we are done
if (arg && arg.cancelDrop) {
return;
}
//if the strategy option is unset or false, employ the order strategy involving removal and insertion of items
if (!sortable.hasOwnProperty("strategyMove") || sortable.strategyMove === false) {
//do the actual move
if (targetIndex >= 0) {
if (sourceParent) {
sourceParent.splice(sourceIndex, 1);
//if using deferred updates plugin, force updates
if (ko.processAllDeferredBindingUpdates) {
ko.processAllDeferredBindingUpdates();
}
//if using deferred updates on knockout 3.4, force updates
if (ko.options && ko.options.deferUpdates) {
ko.tasks.runEarly();
}
}
targetParent.splice(targetIndex, 0, item);
}
//rendering is handled by manipulating the observableArray; ignore dropped element
dataSet(el, ITEMKEY, null);
}
else { //employ the strategy of moving items
if (targetIndex >= 0) {
if (sourceParent) {
if (sourceParent !== targetParent) {
// moving from one list to another
sourceParent.splice(sourceIndex, 1);
targetParent.splice(targetIndex, 0, item);
//rendering is handled by manipulating the observableArray; ignore dropped element
dataSet(el, ITEMKEY, null);
ui.item.remove();
}
else {
// moving within same list
var underlyingList = unwrap(sourceParent);
// notify 'beforeChange' subscribers
if (sourceParent.valueWillMutate) {
sourceParent.valueWillMutate();
}
// move from source index ...
underlyingList.splice(sourceIndex, 1);
// ... to target index
underlyingList.splice(targetIndex, 0, item);
// notify subscribers
if (sourceParent.valueHasMutated) {
sourceParent.valueHasMutated();
}
}
}
else {
// drop new element from outside
targetParent.splice(targetIndex, 0, item);
//rendering is handled by manipulating the observableArray; ignore dropped element
dataSet(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(unwrap(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("ui-sortable") || $element.data("sortable")) {
$element.sortable("destroy");
}
ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, false);
//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
dataSet(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: {}
};
})(this.ko, this.jQuery);