alpaca
Version:
Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide
1,385 lines (1,178 loc) • 72 kB
JavaScript
/*jshint -W083 */ // inline functions are used safely
(function($) {
var Alpaca = $.alpaca;
Alpaca.Fields.ArrayField = Alpaca.ContainerField.extend(
/**
* @lends Alpaca.Fields.ArrayField.prototype
*/
{
/**
* @see Alpaca.Field#getFieldType
*/
getFieldType: function() {
return "array";
},
/**
* @see Alpaca.ContainerField#setup
*/
setup: function()
{
var self = this;
this.base();
var containerItemTemplateType = self.resolveContainerItemTemplateType();
if (!containerItemTemplateType)
{
return Alpaca.throwErrorWithCallback("Unable to find template descriptor for container item: " + self.getFieldType());
}
this.containerItemTemplateDescriptor = self.view.getTemplateDescriptor("container-" + containerItemTemplateType + "-item", self);
if (typeof(this.options.dragAndDrop) === "undefined") {
this.options.dragAndDrop = Alpaca.isEmpty(this.view.dragAndDrop) ? Alpaca.defaultDragAndDrop : this.view.dragAndDrop;
}
if (!this.options.toolbarStyle) {
this.options.toolbarStyle = Alpaca.isEmpty(this.view.toolbarStyle) ? "button" : this.view.toolbarStyle;
}
if (!this.options.toolbarStyle) {
this.options.toolbarStyle = "button";
}
if (!this.options.actionbarStyle) {
this.options.actionbarStyle = Alpaca.isEmpty(this.view.actionbarStyle) ? "top" : this.view.actionbarStyle;
}
if (!this.options.actionbarStyle) {
this.options.actionbarStyle = "top";
}
if (!this.options.toolbarPosition) {
this.options.toolbarPosition = Alpaca.isEmpty(this.view.toolbarPosition) ? "top" : this.view.toolbarPosition;
}
if (!this.options.toolbarPosition) {
this.options.toolbarPosition = "top";
}
if (!this.schema.items)
{
this.schema.items = {};
}
if (!this.options.items)
{
this.options.items = {};
}
// offer some backward compability here as older version of Alpaca used to incorrectly look for
// maxItems, minItems and uniqueItems on the schema.items subobject.
// if not defined properly, we offer some automatic forward migration of these properties
if (this.schema.items && this.schema.items.maxItems && typeof(this.schema.maxItems) === "undefined") {
this.schema.maxItems = this.schema.items.maxItems;
delete this.schema.items.maxItems;
}
if (this.schema.items && this.schema.items.minItems && typeof(this.schema.minItems) === "undefined") {
this.schema.minItems = this.schema.items.minItems;
delete this.schema.items.minItems;
}
if (this.schema.items && this.schema.items.uniqueItems && typeof(this.schema.uniqueItems) === "undefined") {
this.schema.uniqueItems = this.schema.items.uniqueItems;
delete this.schema.items.uniqueItems;
}
// determine whether we are using "ruby on rails" compatibility mode
this.options.rubyrails = false;
if (this.parent && this.parent.options && this.parent.options.form && this.parent.options.form.attributes)
{
if (!Alpaca.isEmpty(this.parent.options.form.attributes.rubyrails))
{
this.options.rubyrails = true;
}
}
var toolbarSticky = Alpaca.defaultToolbarSticky;
if (!Alpaca.isEmpty(this.view.toolbarSticky))
{
toolbarSticky = this.view.toolbarSticky;
}
if (!Alpaca.isEmpty(this.options.toolbarSticky))
{
toolbarSticky = this.options.toolbarSticky;
}
this.options.toolbarSticky = toolbarSticky;
// by default, hide toolbar when children.count > 0
if (typeof(self.options.hideToolbarWithChildren) === "undefined")
{
self.options.hideToolbarWithChildren = true;
}
// Enable forceRevalidation option so that any change in children will trigger parent's revalidation.
if (this.schema.items && this.schema.uniqueItems)
{
Alpaca.mergeObject(this.options, {
"forceRevalidation" : true
});
}
if (Alpaca.isEmpty(this.data) || this.data === "")
{
this.data = [];
}
if (Alpaca.isString(this.data))
{
// assume to be a serialized array or object, convert
try
{
var parsedJSON = Alpaca.parseJSON(this.data);
if (!Alpaca.isArray(parsedJSON) && !Alpaca.isObject(parsedJSON))
{
Alpaca.logWarn("ArrayField parsed string data but it was not an array: " + this.data);
return;
}
this.data = parsedJSON;
}
catch (e)
{
// assume just a string value, put into array
this.data = [this.data];
}
}
if (!Alpaca.isArray(this.data) && !Alpaca.isObject(this.data))
{
return Alpaca.logWarn("ArrayField data is not an array: " + JSON.stringify(this.data, null, " "));
}
//
// ACTIONS
//
var applyAction = function(actions, key, actionConfig) {
var action = self.findAction(actions, key);
if (!action) {
action = {
"core": true
};
actions.push(action);
}
for (var k in actionConfig) {
if (!action[k]) {
action[k] = actionConfig[k];
}
}
};
var cleanupActions = function(actions, showLabels) {
var i = 0;
do {
// assume enabled by default
if (typeof(actions[i].enabled) === "undefined") {
actions[i].enabled = true;
}
// hide label if global disable
if (!showLabels) {
delete actions[i].label;
}
if (!actions[i].enabled) {
actions.splice(i, 1);
} else {
i++;
}
} while (i < actions.length);
// sort so that core actions appear first
actions.sort(function(a, b) {
if (a.core && !b.core) {
return -1;
}
if (!a.core && b.core) {
return 1;
}
return 0;
});
};
// set up default actions for the top array toolbar
self.toolbar = {};
if (self.options.toolbar)
{
for (var k in self.options.toolbar) {
self.toolbar[k] = Alpaca.copyOf(self.options.toolbar[k]);
}
}
if (typeof(self.toolbar.showLabels) === "undefined") {
self.toolbar.showLabels = true;
}
if (!self.toolbar.actions) {
self.toolbar.actions = [];
}
applyAction(self.toolbar.actions, "add", {
"label": self.getMessage("addItemButtonLabel"),
"action": "add",
"iconClass": self.view.getStyle("addIcon"),
"click": function(key, action) {
self.handleToolBarAddItemClick(function(item) {
// done
});
}
});
cleanupActions(self.toolbar.actions, self.toolbar.showLabels);
// determine which actions to add into the per-item actionbar
self.actionbar = {};
if (self.options.actionbar)
{
for (var k2 in self.options.actionbar) {
self.actionbar[k2] = Alpaca.copyOf(self.options.actionbar[k2]);
}
}
if (typeof(self.actionbar.showLabels) === "undefined") {
self.actionbar.showLabels = false;
}
if (!self.actionbar.actions) {
self.actionbar.actions = [];
}
applyAction(self.actionbar.actions, "add", {
"label": self.getMessage("addButtonLabel"),
"action": "add",
"iconClass": self.view.getStyle("addIcon"),
"click": function(key, action, itemIndex) {
self.handleActionBarAddItemClick(itemIndex, function(item) {
// done
});
}
});
applyAction(self.actionbar.actions, "remove", {
"label": self.getMessage("removeButtonLabel"),
"action": "remove",
"iconClass": self.view.getStyle("removeIcon"),
"click": function(key, action, itemIndex) {
self.handleActionBarRemoveItemClick(itemIndex, function(item) {
// done
});
}
});
applyAction(self.actionbar.actions, "up", {
"label": self.getMessage("upButtonLabel"),
"action": "up",
"iconClass": self.view.getStyle("upIcon"),
"click": function(key, action, itemIndex) {
self.handleActionBarMoveItemUpClick(itemIndex, function() {
// done
});
}
});
applyAction(self.actionbar.actions, "down", {
"label": self.getMessage("downButtonLabel"),
"action": "down",
"iconClass": self.view.getStyle("downIcon"),
"click": function(key, action, itemIndex) {
self.handleActionBarMoveItemDownClick(itemIndex, function() {
// done
});
}
});
cleanupActions(self.actionbar.actions, self.actionbar.showLabels);
var len = this.data.length;
var data = $.extend(true, {}, this.data);
data.length = len;
this.data = Array.prototype.slice.call(data);
},
/**
* Picks apart the array and set onto child fields.
* @see Alpaca.ContainerField#setup
*/
setValue: function(data)
{
var self = this;
if (!data) {
data = [];
}
if (!Alpaca.isArray(data))
{
return;
}
// set fields
var i = 0;
do
{
if (i < self.children.length)
{
var childField = self.children[i];
if (data.length > i)
{
childField.setValue(data[i]);
i++;
}
else
{
self.removeItem(i, null, true);
}
}
}
while (i < self.children.length);
// if the number of items in the data is greater than the number of existing child elements
// then we need to add the new fields
if (i < data.length)
{
self.resolveItemSchemaOptions(function(itemSchema, itemOptions, circular) {
if (!itemSchema)
{
Alpaca.logDebug("Unable to resolve schema for item: " + i);
}
// we only allow addition if the resolved schema isn't circularly referenced
// or the schema is optional
if (circular)
{
return Alpaca.throwErrorWithCallback("Circular reference detected for schema: " + JSON.stringify(itemSchema), self.errorCallback);
}
// waterfall functions
var funcs = [];
while (i < data.length)
{
var f = (function(i, data)
{
return function(_done)
{
self.addItem(i, itemSchema, itemOptions, data[i], function() {
_done();
});
};
})(i, data);
funcs.push(f);
i++;
}
Alpaca.parallel(funcs, function() {
// nothing
});
});
}
},
/**
* @see Alpaca.ContainerField#getContainerValue
*/
getContainerValue: function()
{
// if we're empty and we're also not required, then we hand back empty set
if (this.children.length === 0 && !this.isRequired())
{
return [];
}
// otherwise, construct an array and hand it back
var o = [];
for (var i = 0; i < this.children.length; i++)
{
var v = this.children[i].getValue();
if(v !== v) {
// NaN
v = undefined;
}
if (typeof(v) !== "undefined")
{
o.push(v);
}
}
return o;
},
/**
* @override
*
* Creates sub-items for this object.
*
* @param callback
*/
createItems: function(callback)
{
var self = this;
var items = [];
if (self.data && self.data.length > 0)
{
var totalItemCount = self.data.length;
var itemsByIndex = {};
// all items within the array have the same schema and options
// so we only need to load this once
self.resolveItemSchemaOptions(function(itemSchema, itemOptions, circular) {
// we only allow addition if the resolved schema isn't circularly referenced
// or the schema is optional
if (circular)
{
return Alpaca.throwErrorWithCallback("Circular reference detected for schema: " + JSON.stringify(itemSchema), self.errorCallback);
}
// waterfall functions
var funcs = [];
for (var index = 0; index < self.data.length; index++)
{
var value = self.data[index];
var pf = (function(index, value)
{
return function(_done)
{
self.createItem(index, itemSchema, itemOptions, value, function(item) {
itemsByIndex[index] = item;
_done();
});
};
})(index, value);
funcs.push(pf);
}
Alpaca.parallel(funcs, function(err) {
// restore intended order
for (var i = 0; i < totalItemCount; i++)
{
var item = itemsByIndex[i];
if (item) {
items.push(item);
}
}
callback(items);
});
});
}
else
{
callback(items);
}
},
/**
* Workhorse method for createItem.
*
* @param index
* @param itemSchema
* @param itemOptions
* @param itemData
* @param postRenderCallback
* @return {*}
* @private
*/
createItem: function(index, itemSchema, itemOptions, itemData, postRenderCallback)
{
var self = this;
if (self._validateEqualMaxItems())
{
var formEl = $("<div></div>");
formEl.alpaca({
"data" : itemData,
"options": itemOptions,
"schema" : itemSchema,
"view" : this.view.id ? this.view.id : this.view,
"connector": this.connector,
"error": function(err)
{
self.destroy();
self.errorCallback.call(self, err);
},
"notTopLevel":true,
"render": function(fieldControl, cb) {
// render
fieldControl.parent = self;
// setup item path
fieldControl.path = self.path + "[" + index + "]";
//fieldControl.nameCalculated = true;
fieldControl.render(null, function() {
if (cb) {
cb();
}
});
},
"postRender": function(control)
{
// alpaca finished
// render the outer container
var containerItemEl = Alpaca.tmpl(self.containerItemTemplateDescriptor, {
"id": self.getId(),
"name": control.name,
"parentFieldId": self.getId(),
"actionbarStyle": self.options.actionbarStyle,
"toolbarLocation": self.options.toolbarLocation,
"dragAndDrop": self.options.dragAndDrop,
"view": self.view,
"data": itemData
});
// find the insertion point
var insertionPointEl = $(containerItemEl).find("." + Alpaca.MARKER_CLASS_CONTAINER_FIELD_ITEM_FIELD);
if (insertionPointEl.length === 0)
{
if ($(containerItemEl).hasClass(Alpaca.MARKER_CLASS_CONTAINER_FIELD_ITEM_FIELD)) {
insertionPointEl = $(containerItemEl);
}
}
if (insertionPointEl.length === 0)
{
self.errorCallback.call(self, {
"message": "Cannot find insertion point for field: " + self.getId()
});
return;
}
// copy into place
$(insertionPointEl).before(control.getFieldEl());
$(insertionPointEl).remove();
control.containerItemEl = containerItemEl;
// TODO: verify, as per: https://github.com/emircal/alpaca/commit/4061c33787bd7a2b86fb613317374d365d9acc92
// Reset hideInitValidationError after render
Alpaca.fieldApplyFieldAndChildren(control, function(_control) {
_control.hideInitValidationError = false;
});
// PR: https://github.com/gitana/alpaca/pull/124
if (Alpaca.isFunction(self.options.items.postRender))
{
self.options.items.postRender.call(control, insertionPointEl);
}
if (postRenderCallback)
{
postRenderCallback(control);
}
}
});
}
},
/**
* Determines the schema and options to utilize for items within this array.
*
* @param callback
*/
resolveItemSchemaOptions: function(callback)
{
var _this = this;
var completionFunction = function(resolvedItemSchema, resolvedItemOptions, circular)
{
// special caveat: if we're in read-only mode, the child must also be in read-only mode
if (_this.options.readonly) {
resolvedItemOptions.readonly = true;
}
callback(resolvedItemSchema, resolvedItemOptions, circular);
};
var itemOptions;
// legacy support for options.fields.item
if (!itemOptions && _this.options && _this.options.fields && _this.options.fields.item) {
itemOptions = _this.options.fields.item;
}
if (!itemOptions && _this.options && _this.options.items) {
itemOptions = _this.options.items;
}
var itemSchema;
if (_this.schema && _this.schema.items) {
itemSchema = _this.schema.items;
}
// handle $ref
var schemaReferenceId = null;
if (itemSchema) {
schemaReferenceId = itemSchema["$ref"];
}
var optionsReferenceId = null;
if (itemOptions) {
optionsReferenceId = itemOptions["$ref"];
}
if (schemaReferenceId || optionsReferenceId)
{
// walk up to find top field
var topField = this;
var fieldChain = [topField];
while (topField.parent)
{
topField = topField.parent;
fieldChain.push(topField);
}
var originalItemSchema = itemSchema;
var originalItemOptions = itemOptions;
Alpaca.loadRefSchemaOptions(topField, schemaReferenceId, optionsReferenceId, function(itemSchema, itemOptions) {
// walk the field chain to see if we have any circularity (for schema)
var refCount = 0;
for (var i = 0; i < fieldChain.length; i++)
{
if (fieldChain[i].schema)
{
if (schemaReferenceId)
{
if ((fieldChain[i].schema.id === schemaReferenceId) || (fieldChain[i].schema.id === "#" + schemaReferenceId))
{
refCount++;
}
else if ((fieldChain[i].schema["$ref"] === schemaReferenceId))
{
refCount++;
}
}
}
}
// use a higher limit for arrays, perhaps 10
//var circular = (refCount > 1);
var circular = (refCount > 10);
var resolvedItemSchema = {};
if (originalItemSchema) {
Alpaca.mergeObject(resolvedItemSchema, originalItemSchema);
}
if (itemSchema) {
Alpaca.mergeObject(resolvedItemSchema, itemSchema);
}
delete resolvedItemSchema.id;
var resolvedItemOptions = {};
if (originalItemOptions) {
Alpaca.mergeObject(resolvedItemOptions, originalItemOptions);
}
if (itemOptions) {
Alpaca.mergeObject(resolvedItemOptions, itemOptions);
}
Alpaca.nextTick(function() {
completionFunction(resolvedItemSchema, resolvedItemOptions, circular);
});
});
}
else
{
Alpaca.nextTick(function() {
completionFunction(itemSchema, itemOptions);
});
}
},
/**
* @see Alpaca.ContainerField#handleValidate
*/
handleValidate: function()
{
var baseStatus = this.base();
var valInfo = this.validation;
var status = this._validateUniqueItems();
valInfo["valueNotUnique"] = {
"message": status ? "" : this.getMessage("valueNotUnique"),
"status": status
};
status = this._validateMaxItems();
valInfo["tooManyItems"] = {
"message": status ? "" : Alpaca.substituteTokens(this.getMessage("tooManyItems"), [this.schema.maxItems]),
"status": status
};
status = this._validateMinItems();
valInfo["notEnoughItems"] = {
"message": status ? "" : Alpaca.substituteTokens(this.getMessage("notEnoughItems"), [this.schema.minItems]),
"status": status
};
return baseStatus && valInfo["valueNotUnique"]["status"] && valInfo["tooManyItems"]["status"] && valInfo["notEnoughItems"]["status"];
},
/**
* Validates if the number of items has been reached to maxItems.
* @returns {Boolean} true if the number of items has been reached to maxItems
*/
_validateEqualMaxItems: function()
{
if (this.schema.maxItems && this.schema.maxItems >= 0)
{
if (this.getSize() >= this.schema.maxItems)
{
return false;
}
}
return true;
},
/**
* Validates if the number of items has been reached to minItems.
* @returns {Boolean} true if number of items has been reached to minItems
*/
_validateEqualMinItems: function()
{
if (this.schema.minItems && this.schema.minItems >= 0)
{
if (this.getSize() <= this.schema.minItems)
{
return false;
}
}
return true;
},
/**
* Validates if number of items has been less than minItems.
* @returns {Boolean} true if number of items has been less than minItems
*/
_validateMinItems: function()
{
if (this.schema.minItems && this.schema.minItems >= 0)
{
if (this.getSize() < this.schema.minItems)
{
return false;
}
}
return true;
},
/**
* Validates if number of items has been over maxItems.
* @returns {Boolean} true if number of items has been over maxItems
*/
_validateMaxItems: function()
{
if (this.schema.maxItems && this.schema.maxItems >= 0)
{
if (this.getSize() > this.schema.maxItems)
{
return false;
}
}
return true;
},
/**
* Validates if all items are unique.
* @returns {Boolean} true if all items are unique.
*/
_validateUniqueItems: function()
{
if (this.schema.items && this.schema.uniqueItems)
{
var hash = {};
for (var i = 0; i < this.children.length; i++)
{
var key = this.children[i].getValue();
if (!key) {
key = "";
}
key = Alpaca.hashCode(key);
if (hash[key])
{
return false;
}
hash[key] = true;
}
}
return true;
},
findAction: function(actionsArray, actionKey)
{
var action = null;
$.each(actionsArray, function(i, v) {
if (v.action === actionKey) // jshint ignore:line
{
action = v;
}
});
return action;
},
postRender: function(callback)
{
var self = this;
this.base(function() {
// if there are zero children, show the array toolbar
self.updateToolbars();
callback();
});
},
/*
afterApplyCreatedItems: function(model, callback)
{
var self = this;
// if there are zero children, show the array toolbar
self.updateToolbars();
callback();
},
*/
/**
* Returns number of children.
*/
getSize: function() {
return this.children.length;
},
/**
* @OVERRIDE
*
* Adjust the path and name ahead of refreshing the DOM.
*/
updateDOMElement: function()
{
this.updatePathAndName();
this.base();
},
/**
* This method gets invoked after items are dynamically added, removed or moved around in the child chain.
* It adjusts classes on child DOM elements to make sure they're correct.
*/
updatePathAndName: function()
{
var self = this;
var updateChildrenPathAndName = function(parent)
{
if (parent.children)
{
$.each(parent.children, function(i, v) {
if (parent.prePath && Alpaca.startsWith(v.path, parent.prePath))
{
v.prePath = v.path;
v.path = v.path.replace(parent.prePath, parent.path);
}
// re-calculate name
if (parent.preName && Alpaca.startsWith(v.name, parent.preName))
{
v.preName = v.name;
v.name = v.name.replace(parent.preName, parent.name);
if (v.field)
{
$(v.field).attr("name", v.name);
}
}
updateChildrenPathAndName(v);
});
}
};
if (this.children && this.children.length > 0)
{
$.each(this.children, function(i, v) {
var idx = v.path.lastIndexOf('/');
var lastSegment = v.path.substring(idx+1);
var lastIndex = -1;
if (lastSegment.indexOf("[") > 0 && lastSegment.indexOf("]") > 0)
{
lastIndex = parseInt(lastSegment.substring(lastSegment.indexOf("[") + 1, lastSegment.indexOf("]")));
}
if (lastIndex !== i)
{
v.prePath = v.path;
v.path = v.path.substring(0, idx) + "/" + lastSegment.substring(0, lastSegment.indexOf("[")) + "[" + i + "]";
}
// re-calculate name
if (v.nameCalculated)
{
v.preName = v.name;
if (v.parent && v.parent.name && v.path)
{
v.name = v.parent.name + "_" + i;
}
else
{
if (v.path)
{
v.name = v.path.replace(/\//g, "").replace(/\[/g, "_").replace(/\]/g, "");
}
}
if (this.parent.options.rubyrails )
{
$(v.field).attr("name", v.parent.name);
}
else
{
$(v.field).attr("name", v.name);
}
}
if (!v.prePath)
{
v.prePath = v.path;
}
updateChildrenPathAndName(v);
});
}
},
/**
* Updates the status of array item action toolbar buttons.
*/
updateToolbars: function()
{
var self = this;
// if we're in display mode, we do not do this
if (this.view.type === "display")
{
return;
}
// if we're in readonly mode, don't do this
if (this.schema.readonly)
{
return;
}
// fire callbacks to view to remove and create toolbar
if (self.toolbar)
{
self.fireCallback("arrayToolbar", true);
self.fireCallback("arrayToolbar");
}
// fire callbacks to view to remove and create an actionbar for each item
if (self.actionbar)
{
self.fireCallback("arrayActionbars", true);
self.fireCallback("arrayActionbars");
}
//
// TOOLBAR
//
var toolbarEl = $(this.getFieldEl()).find(".alpaca-array-toolbar[data-alpaca-array-toolbar-field-id='" + self.getId() + "']");
if (this.children.length > 0 && self.options.hideToolbarWithChildren)
{
// hide toolbar
$(toolbarEl).hide();
}
else
{
// show toolbar
$(toolbarEl).show();
// CLICK: array toolbar buttons
$(toolbarEl).find("[data-alpaca-array-toolbar-action]").each(function() {
var actionKey = $(this).attr("data-alpaca-array-toolbar-action");
var action = self.findAction(self.toolbar.actions, actionKey);
if (action)
{
$(this).off().click(function(e) {
e.preventDefault();
action.click.call(self, actionKey, action);
});
}
});
}
//
// DRAG AND DROP
//
if (self.options.dragAndDrop)
{
// enable drag and drop
document.addEventListener("dragenter", function (event) {
event.preventDefault();
}, false);
document.addEventListener("dragover", function (event) {
event.preventDefault();
}, false);
$(self.getFieldEl()).off().on("drop", function(ev) {
ev.preventDefault();
var parentFieldId = ev.originalEvent.dataTransfer.getData("parentFieldId");
if (parentFieldId == self.getId())
{
var closestItem = ev.target.closest(".alpaca-container-item[data-alpaca-container-item-parent-field-id='" + parentFieldId + "']");
if (closestItem) {
var targetIndex = closestItem.dataset.alpacaContainerItemIndex;
var sourceIndex = ev.originalEvent.dataTransfer.getData("sourceIndex");
self.moveItem(sourceIndex, targetIndex);
ev.stopPropagation();
}
}
});
var items = self.getFieldEl().find(".alpaca-container-item[data-alpaca-container-item-parent-field-id='" + self.getId() + "']");
$(items).each(function(itemIndex) {
var target = null;
$(this).attr("draggable", true);
$(this).off().on("mousedown", function(ev)
{
ev.stopPropagation();
target = ev.target;
});
$(this).on("dragstart", function(ev)
{
ev.stopPropagation();
// if this item's move icon contains the target
var dragHandle = $(this).children(".alpaca-array-item-move")[0];
if (dragHandle && dragHandle.contains(target))
{
var event = ev.originalEvent;
event.dataTransfer.setData("sourceIndex", this.dataset.alpacaContainerItemIndex);
event.dataTransfer.setData("parentFieldId", this.dataset.alpacaContainerItemParentFieldId);
// find droppable area and highlight
$(this).siblings(".alpaca-container-item").each(function() {
$(this).children(".alpaca-array-item-move").css({
"color": "#2CEAA3"
});
});
}
else
{
ev.preventDefault();
}
});
$(this).on("dragend", function(ev)
{
ev.stopPropagation();
$(".alpaca-array-item-move").css({
"color": "#333"
});
});
});
}
//
// ACTIONBAR
//
// if we're not using the "sticky" toolbar, then show and hide the item action buttons when hovered
if (typeof(this.options.toolbarSticky) === "undefined" || this.options.toolbarSticky === null)
{
// find each item
var items = this.getFieldEl().find(".alpaca-container-item[data-alpaca-container-item-parent-field-id='" + self.getId() + "']");
$(items).each(function(itemIndex) {
// find the actionbar for this item
// find from containerItemEl
var actionbarEl = $(self.getFieldEl()).find(".alpaca-array-actionbar[data-alpaca-array-actionbar-parent-field-id='" + self.getId() + "'][data-alpaca-array-actionbar-item-index='" + itemIndex + "']");
if (actionbarEl && actionbarEl.length > 0)
{
$(this).hover(function() {
$(actionbarEl).show();
}, function() {
$(actionbarEl).hide();
});
$(actionbarEl).hide();
}
});
}
else if (this.options.toolbarSticky)
{
// always show the actionbars
$(self.getFieldEl()).find(".alpaca-array-actionbar[data-alpaca-array-actionbar-parent-field-id='" + self.getId() + "']").css("display", "inline-block");
}
else if (!this.options.toolbarSticky)
{
// always hide the actionbars
$(self.getFieldEl()).find(".alpaca-array-actionbar[data-alpaca-array-actionbar-parent-field-id='" + self.getId() + "']").hide();
}
// CLICK: actionbar buttons
// NOTE: actionbarEls size should be 0 or 1
var actionbarEls = $(self.getFieldEl()).find(".alpaca-array-actionbar[data-alpaca-array-actionbar-parent-field-id='" + self.getId() + "']");
$(actionbarEls).each(function() {
var targetIndex = $(this).attr("data-alpaca-array-actionbar-item-index");
if (typeof(targetIndex) === "string")
{
targetIndex = parseInt(targetIndex, 10);
}
// bind button click handlers
$(this).children("[data-alpaca-array-actionbar-action]").each(function() {
var actionKey = $(this).attr("data-alpaca-array-actionbar-action");
var action = self.findAction(self.actionbar.actions, actionKey);
if (action)
{
$(this).off().click(function(e) {
e.preventDefault();
action.click.call(self, actionKey, action, targetIndex);
});
}
});
// if we're at max capacity, disable "add" buttons
if (self._validateEqualMaxItems())
{
$(this).children("[data-alpaca-array-toolbar-action='add']").each(function(index) {
$(this).removeClass('alpaca-button-disabled');
self.fireCallback("enableButton", this);
});
$(this).children("[data-alpaca-array-actionbar-action='add']").each(function(index) {
$(this).removeClass('alpaca-button-disabled');
self.fireCallback("enableButton", this);
});
}
else
{
$(this).children("[data-alpaca-array-toolbar-action='add']").each(function(index) {
$(this).addClass('alpaca-button-disabled');
self.fireCallback("disableButton", this);
});
$(this).children("[data-alpaca-array-actionbar-action='add']").each(function(index) {
$(this).addClass('alpaca-button-disabled');
self.fireCallback("disableButton", this);
});
}
// if we're at min capacity, disable "remove" buttons
if (self._validateEqualMinItems())
{
$(this).children("[data-alpaca-array-actionbar-action='remove']").each(function(index) {
$(this).removeClass('alpaca-button-disabled');
self.fireCallback("enableButton", this);
});
}
else
{
$(this).children("[data-alpaca-array-actionbar-action='remove']").each(function(index) {
$(this).addClass('alpaca-button-disabled');
self.fireCallback("disableButton", this);
});
}
});
// first actionbar has its "move up" button disabled
$(actionbarEls).first().children("[data-alpaca-array-actionbar-action='up']").each(function() {
$(this).addClass('alpaca-button-disabled');
self.fireCallback("disableButton", this);
});
// last actionbar has its "move down" button disabled
$(actionbarEls).last().children("[data-alpaca-array-actionbar-action='down']").each(function() {
$(this).addClass('alpaca-button-disabled');
self.fireCallback("disableButton", this);
});
},
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// DYNAMIC METHODS
//
///////////////////////////////////////////////////////////////////////////////////////////////////
doResolveItemContainer: function()
{
var self = this;
return $(self.container);
},
handleToolBarAddItemClick: function(callback)
{
var self = this;
self.resolveItemSchemaOptions(function(itemSchema, itemOptions, circular) {
// we only allow addition if the resolved schema isn't circularly referenced
// or the schema is optional
if (circular)
{
return Alpaca.throwErrorWithCallback("Circular reference detected for schema: " + JSON.stringify(itemSchema), self.errorCallback);
}
// how many children do we have currently?
var insertionPoint = self.children.length;
var itemData = Alpaca.createEmptyDataInstance(itemSchema);
self.addItem(insertionPoint, itemSchema, itemOptions, itemData, function(item) {
if (callback) {
callback(item);
}
});
});
},
handleActionBarAddItemClick: function(itemIndex, callback)
{
var self = this;
self.resolveItemSchemaOptions(function(itemSchema, itemOptions, circular) {
// we only allow addition if the resolved schema isn't circularly referenced
// or the schema is optional
if (circular)
{
return Alpaca.throwErrorWithCallback("Circular reference detected for schema: " + JSON.stringify(itemSchema), self.errorCallback);
}
var arrayValues = self.getValue();
var itemData = Alpaca.createEmptyDataInstance(itemSchema);
self.addItem(itemIndex + 1, itemSchema, itemOptions, itemData, function(item) {
// this is necessary because some underlying fields require their data to be reset
// in order for the display to work out properly (radio fields)
arrayValues.splice(itemIndex + 1, 0, item.getValue());
self.setValue(arrayValues);
if (callback) {
callback(item);
}
});
});
},
handleActionBarRemoveItemClick: function(itemIndex, callback)
{
var self = this;
self.removeItem(itemIndex, function() {
if (callback) {
callback();
}
});
},
handleActionBarMoveItemUpClick: function(itemIndex, callback)
{
var self = this;
self.swapItem(itemIndex, itemIndex - 1, self.options.animate, function() {
if (callback) {
callback();
}
});
},
handleActionBarMoveItemDownClick: function(itemIndex, callback)
{
var self = this;
self.swapItem(itemIndex, itemIndex + 1, self.options.animate, function() {
if (callback) {
callback();
}
});
},
doAddItem: function(index, item, callback)
{
var self = this;
var addItemContainer = self.doResolveItemContainer();
// insert into dom
if (index === 0)
{
// insert first into container
$(addItemContainer).append(item.containerItemEl);
}
else
{
// insert at a specific index
var existingElement = addItemContainer.children("[data-alpaca-container-item-index='" + (index-1) + "']");
if (existingElement && existingElement.length > 0)
{
// insert after
existingElement.after(item.containerItemEl);
}
}
self.doAfterAddItem(item, function(err) {
// trigger ready
Alpaca.fireReady(item);
callback(err);
});
},
doAfterAddItem: function(item, callback)
{
callback();
},
/**
* Adds an item to the array.
*
* This gets called from the toolbar when items are added via the user interface. The method can also
* be called programmatically to insert items on the fly.
*
* @param