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,392 lines (1,186 loc) • 79.6 kB
JavaScript
/*jshint -W004 */ // duplicate variables
/*jshint -W083 */ // inline functions are used safely
(function($) {
var Alpaca = $.alpaca;
Alpaca.Fields.ObjectField = Alpaca.ContainerField.extend(
/**
* @lends Alpaca.Fields.ObjectField.prototype
*/
{
/**
* @see Alpaca.Field#getFieldType
*/
getFieldType: function() {
return "object";
},
/**
* @see Alpaca.ContainerField#setup
*/
setup: function()
{
var self = this;
this.base();
var containerItemTemplateType = self.resolveContainerItemTemplateType();
if (!containerItemTemplateType)
{
var x = self.resolveContainerItemTemplateType();
return Alpaca.throwErrorWithCallback("Unable to find template descriptor for container item: " + self.getFieldType());
}
this.containerItemTemplateDescriptor = self.view.getTemplateDescriptor("container-" + containerItemTemplateType + "-item", self);
if (Alpaca.isEmpty(this.data) || this.data === "")
{
return;
}
if (!Alpaca.isObject(this.data))
{
if (!Alpaca.isString(this.data))
{
return;
}
else
{
try
{
this.data = Alpaca.parseJSON(this.data);
if (!Alpaca.isObject(this.data))
{
Alpaca.logWarn("ObjectField parsed data but it was not an object: " + JSON.stringify(this.data));
return;
}
}
catch (e)
{
return;
}
}
}
},
/**
* Picks apart the data object and set onto child fields.
*
* @see Alpaca.Field#setValue
*/
setValue: function(data)
{
if (!data)
{
data = {};
}
// if not an object by this point, we don't handle it
if (!Alpaca.isObject(data))
{
return;
}
// sort existing fields by property id
var existingFieldsByPropertyId = {};
for (var fieldId in this.childrenById)
{
var propertyId = this.childrenById[fieldId].propertyId;
existingFieldsByPropertyId[propertyId] = this.childrenById[fieldId];
}
// new data mapped by property id
var newDataByPropertyId = {};
for (var k in data)
{
if (data.hasOwnProperty(k))
{
newDataByPropertyId[k] = data[k];
}
}
// walk through new property ids
// if a field exists, set value onto it and remove from newDataByPropertyId and existingFieldsByPropertyId
// if a field doesn't exist, let it remain in list
for (var propertyId in newDataByPropertyId)
{
var field = existingFieldsByPropertyId[propertyId];
if (field)
{
field.setValue(newDataByPropertyId[propertyId]);
delete existingFieldsByPropertyId[propertyId];
delete newDataByPropertyId[propertyId];
}
}
// anything left in existingFieldsByPropertyId describes data that is missing, null or empty
// we set those as undefined
for (var propertyId in existingFieldsByPropertyId)
{
var field = existingFieldsByPropertyId[propertyId];
field.setValue(null);
}
// anything left in newDataByPropertyId is new stuff that we need to add
// the object field doesn't support this since it runs against a schema
// so we drop this off
},
/**
* Reconstructs the data object from the child fields.
*
* @see Alpaca.ContainerField#getContainerValue
*/
getContainerValue: function()
{
// if we don't have any children and we're not required, hand back empty object
if (this.children.length === 0 && !this.isRequired())
{
return {};
}
// otherwise, hand back an object with our child properties in it
var o = {};
// walk through all of the properties object
// for each property, we insert it into a JSON object that we'll hand back as the result
// if the property has dependencies, then we evaluate those dependencies first to determine whether the
// resulting property should be included
for (var i = 0; i < this.children.length; i++)
{
// the property key and value
var propertyId = this.children[i].propertyId;
var fieldValue = this.children[i].getValue();
if(fieldValue !== fieldValue) {
// NaN
fieldValue = undefined;
}
if (typeof(fieldValue) !== "undefined")
{
if (this.determineAllDependenciesValid(propertyId))
{
var assignedValue = null;
if (typeof(fieldValue) === "boolean")
{
assignedValue = (fieldValue? true: false);
}
else if (Alpaca.isArray(fieldValue) || Alpaca.isObject(fieldValue) || Alpaca.isNumber(fieldValue))
{
assignedValue = fieldValue;
}
else if (fieldValue || fieldValue === 0)
{
assignedValue = fieldValue;
}
if (assignedValue !== null)
{
o[propertyId] = assignedValue;
}
}
}
}
return o;
},
/**
* @see Alpaca.Field#afterRenderContainer
*/
afterRenderContainer: function(model, callback) {
var self = this;
this.base(model, function() {
// Generates wizard if requested
if (self.isTopLevel())
{
if (self.view)
{
self.wizardConfigs = self.view.getWizard();
if (typeof(self.wizardConfigs) != "undefined")
{
if (!self.wizardConfigs || self.wizardConfigs === true)
{
self.wizardConfigs = {};
}
}
var layoutTemplateDescriptor = self.view.getLayout().templateDescriptor;
if (self.wizardConfigs && Alpaca.isObject(self.wizardConfigs))
{
if (!layoutTemplateDescriptor || self.wizardConfigs.bindings)
{
// run the automatic wizard
self.autoWizard();
}
else
{
// manual wizard based on layout
self.wizard();
}
}
}
}
callback();
});
},
/**
* @override
*
* Creates sub-items for this object.
*
* @param callback
*/
createItems: function(callback)
{
var self = this;
var items = [];
// we keep a map of all of the properties in our original data object
// as we render elements out of the schema, we remove from the extraDataProperties map
// whatever is leftover are the data properties that were NOT rendered because they were not part
// of the schema
//
// this is primarily maintained for debugging purposes, so as to inform the developer of mismatches
var extraDataProperties = {};
for (var dataKey in self.data) {
extraDataProperties[dataKey] = dataKey;
}
var properties = self.data;
if (self.schema && self.schema.properties) {
properties = self.schema.properties;
}
var cf = function()
{
// If the schema and the data line up perfectly, then there will be no properties in the data that are
// not also in the schema, and thus, extraDataProperties will be empty.
//
// On the other hand, if there are some properties in data that were not in schema, then they will
// remain in extraDataProperties and we can inform developers for debugging purposes
//
var extraDataKeys = [];
for (var extraDataKey in extraDataProperties) {
extraDataKeys.push(extraDataKey);
}
if (extraDataKeys.length > 0) {
Alpaca.logDebug("There were " + extraDataKeys.length + " extra data keys that were not part of the schema " + JSON.stringify(extraDataKeys));
}
callback(items);
};
// each property in the object can have a different schema and options so we need to process
// asynchronously and wait for all to complete
var itemsByPropertyId = {};
// wrap into waterfall functions
var propertyFunctions = [];
for (var propertyId in properties)
{
var itemData = null;
if (self.data)
{
if (self.data.hasOwnProperty(propertyId))
{
itemData = self.data[propertyId];
}
}
var pf = (function(self, propertyId, itemData, extraDataProperties)
{
return function(_done)
{
// only allow this if we have data, otherwise we end up with circular reference
self.resolvePropertySchemaOptions(propertyId, function (schema, options, 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(schema), self.errorCallback);
}
if (!schema) {
Alpaca.logDebug("Unable to resolve schema for property: " + propertyId);
}
self.createItem(propertyId, schema, options, itemData, null, function (addedItemControl) {
itemsByPropertyId[propertyId] = addedItemControl;
// remove from extraDataProperties helper
delete extraDataProperties[propertyId];
_done();
});
});
};
})(self, propertyId, itemData, extraDataProperties);
propertyFunctions.push(pf);
}
Alpaca.parallel(propertyFunctions, function(err) {
// build items array in correct property order
for (var propertyId in properties)
{
var item = itemsByPropertyId[propertyId];
if (item)
{
items.push(item);
}
}
// is there any order information in the items?
var hasOrderInformation = false;
for (var i = 0; i < items.length; i++) {
if (typeof(items[i].options.order) !== "undefined") {
hasOrderInformation = true;
break;
}
}
if (hasOrderInformation)
{
// sort by order?
items.sort(function (a, b) {
var orderA = a.options.order;
if (!orderA)
{
orderA = 0;
}
var orderB = b.options.order;
if (!orderB)
{
orderB = 0;
}
return (orderA - orderB);
});
}
cf();
});
},
/**
* Creates an sub-item for this object.
*
* The postRenderCallback method is called upon completion.
*
* @param {String} propertyId Child field property ID.
* @param {Object} itemSchema schema
* @param {Object} fieldOptions Child field options.
* @param {Any} value Child field value
* @param {String} insertAfterId Location where the child item will be inserted.
* @param [Function} postRenderCallback called once the item has been added
*/
createItem: function(propertyId, itemSchema, itemOptions, itemData, insertAfterId, postRenderCallback)
{
var self = this;
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;
// add the property Id
fieldControl.propertyId = propertyId;
// setup item path
if (self.path !== "/") {
fieldControl.path = self.path + "/" + propertyId;
} else {
fieldControl.path = self.path + propertyId;
}
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,
"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)
{
return self.errorCallback.call(self, {
"message": "Cannot find insertion point for field: " + self.getId()
});
}
// 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;
});
if (postRenderCallback)
{
postRenderCallback(control);
}
}
});
},
/**
* Determines the schema and options to utilize for sub-objects within this object.
*
* @param propertyId
* @param callback
*/
resolvePropertySchemaOptions: function(propertyId, callback)
{
var _this = this;
var completionFunction = function(resolvedPropertySchema, resolvedPropertyOptions, circular)
{
// special caveat: if we're in read-only mode, the child must also be in read-only mode
if (_this.options.readonly) {
resolvedPropertyOptions.readonly = true;
}
callback(resolvedPropertySchema, resolvedPropertyOptions, circular);
};
var propertySchema = null;
if (_this.schema && _this.schema.properties && _this.schema.properties[propertyId]) {
propertySchema = _this.schema.properties[propertyId];
}
var propertyOptions = {};
if (_this.options && _this.options.fields && _this.options.fields[propertyId]) {
propertyOptions = _this.options.fields[propertyId];
}
// handle $ref
var propertyReferenceId = null;
if (propertySchema) {
propertyReferenceId = propertySchema["$ref"];
}
var fieldReferenceId = null;
if (propertyOptions) {
fieldReferenceId = propertyOptions["$ref"];
}
if (propertyReferenceId || fieldReferenceId)
{
// walk up to find top field
var topField = this;
var fieldChain = [topField];
while (topField.parent)
{
topField = topField.parent;
fieldChain.push(topField);
}
var originalPropertySchema = propertySchema;
var originalPropertyOptions = propertyOptions;
Alpaca.loadRefSchemaOptions(topField, propertyReferenceId, fieldReferenceId, function(propertySchema, propertyOptions) {
// 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 (propertyReferenceId)
{
if (fieldChain[i].schema)
{
if ( (fieldChain[i].schema.id === propertyReferenceId) || (fieldChain[i].schema.id === "#" + propertyReferenceId))
{
refCount++;
}
else if ( (fieldChain[i].schema["$ref"] === propertyReferenceId))
{
refCount++;
}
}
}
}
var circular = (refCount > 1);
var resolvedPropertySchema = {};
if (originalPropertySchema) {
Alpaca.mergeObject(resolvedPropertySchema, originalPropertySchema);
}
if (propertySchema) {
Alpaca.mergeObject(resolvedPropertySchema, propertySchema);
}
// keep original id
if (originalPropertySchema && originalPropertySchema.id) {
resolvedPropertySchema.id = originalPropertySchema.id;
}
//delete resolvedPropertySchema.id;
var resolvedPropertyOptions = {};
if (originalPropertyOptions) {
Alpaca.mergeObject(resolvedPropertyOptions, originalPropertyOptions);
}
if (propertyOptions) {
Alpaca.mergeObject(resolvedPropertyOptions, propertyOptions);
}
Alpaca.nextTick(function() {
completionFunction(resolvedPropertySchema, resolvedPropertyOptions, circular);
});
});
}
else
{
Alpaca.nextTick(function() {
completionFunction(propertySchema, propertyOptions);
});
}
},
applyCreatedItems: function(model, callback)
{
var self = this;
this.base(model, function() {
var f = function(i)
{
if (i === model.items.length)
{
// done
callback();
return;
}
var item = model.items[i];
var propertyId = item.propertyId;
// HANDLE PROPERTY DEPENDENCIES (IF THE PROPERTY HAS THEM)
// if this property has dependencies, show or hide this added item right away
self.showOrHidePropertyBasedOnDependencies(propertyId);
// if this property has dependencies, bind update handlers to dependent fields
self.bindDependencyFieldUpdateEvent(propertyId);
// if this property has dependencies, trigger those to ensure it is in the right state
self.refreshDependentFieldStates(propertyId);
f(i+1);
};
f(0);
});
},
/**
* @see Alpaca.ContainerField#handleValidate
*/
handleValidate: function()
{
var baseStatus = this.base();
var valInfo = this.validation;
var status = this._validateMaxProperties();
valInfo["tooManyProperties"] = {
"message": status ? "" : Alpaca.substituteTokens(this.getMessage("tooManyProperties"), [this.schema.maxProperties]),
"status": status
};
status = this._validateMinProperties();
valInfo["tooFewProperties"] = {
"message": status ? "" : Alpaca.substituteTokens(this.getMessage("tooManyItems"), [this.schema.minProperties]),
"status": status
};
return baseStatus && valInfo["tooManyProperties"]["status"] && valInfo["tooFewProperties"]["status"];
},
/**
* Validate maxProperties schema property.
*
* @returns {Boolean} whether maxProperties is satisfied
*/
_validateMaxProperties: function()
{
if (typeof(this.schema["maxProperties"]) == "undefined")
{
return true;
}
var maxProperties = this.schema["maxProperties"];
// count the number of properties that we currently have
var propertyCount = 0;
for (var k in this.data)
{
propertyCount++;
}
return propertyCount <= maxProperties;
},
/**
* Validate maxProperties schema property.
*
* @returns {Boolean} whether maxProperties is satisfied
*/
_validateMinProperties: function()
{
if (typeof(this.schema["minProperties"]) == "undefined")
{
return true;
}
var minProperties = this.schema["minProperties"];
// count the number of properties that we currently have
var propertyCount = 0;
for (var k in this.data)
{
propertyCount++;
}
return propertyCount >= minProperties;
},
///////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DEPENDENCIES
//
///////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Shows or hides a property's field based on how its dependencies evaluate.
* If a property doesn't have dependencies, this no-ops.
*
* @param propertyId
*/
showOrHidePropertyBasedOnDependencies: function(propertyId)
{
var self = this;
var item = this.childrenByPropertyId[propertyId];
if (!item)
{
return Alpaca.throwErrorWithCallback("Missing property: " + propertyId, self.errorCallback);
}
var valid = this.determineAllDependenciesValid(propertyId);
if (valid)
{
item.show();
item.onDependentReveal();
}
else
{
item.hide();
item.onDependentConceal();
}
item.getFieldEl().trigger("fieldupdate");
},
/**
* Helper function for resolving dependencies for a child property.
* This takes into account JSON Schema v4 and also provides for legacy v3 support.
*
* @param propertyId
*/
getChildDependencies: function(propertyId)
{
// first, check for dependencies declared within the object (container)
var itemDependencies = null;
if (this.schema.dependencies)
{
itemDependencies = this.schema.dependencies[propertyId];
}
if (!itemDependencies)
{
// second, check for dependencies declared on the item itself
// this is to support legacy v3 json schema
var item = this.childrenByPropertyId[propertyId];
if (item)
{
itemDependencies = item.schema.dependencies;
}
}
return itemDependencies;
},
getChildConditionalDependencies: function(propertyId)
{
var itemConditionalDependencies = null;
// second, check for conditional dependencies declared on the item itself
// this is to support legacy v3 json options
var item = this.childrenByPropertyId[propertyId];
if (item)
{
itemConditionalDependencies = item.options.dependencies;
}
return itemConditionalDependencies;
},
/**
* Determines whether the dependencies for a property pass.
*
* @param propertyId
*/
determineAllDependenciesValid: function(propertyId)
{
var self = this;
var item = this.childrenByPropertyId[propertyId];
if (!item)
{
return Alpaca.throwErrorWithCallback("Missing property: " + propertyId, self.errorCallback);
}
// first check for dependencies declared within the object (container)
var itemDependencies = self.getChildDependencies(propertyId);;
if (!itemDependencies)
{
// no dependencies, so yes, we pass
return true;
}
var valid = true;
if (Alpaca.isString(itemDependencies))
{
valid = self.determineSingleDependencyValid(propertyId, itemDependencies);
}
else if (Alpaca.isArray(itemDependencies))
{
$.each(itemDependencies, function(index, value) {
valid = valid && self.determineSingleDependencyValid(propertyId, value);
});
}
return valid;
},
/**
* Binds field updates to any field dependencies.
*
* @param propertyId
*/
bindDependencyFieldUpdateEvent: function(propertyId)
{
var self = this;
var item = this.childrenByPropertyId[propertyId];
if (!item)
{
return Alpaca.throwErrorWithCallback("Missing property: " + propertyId, self.errorCallback);
}
var itemDependencies = self.getChildDependencies(propertyId);
if (!itemDependencies)
{
// no dependencies, so simple return
return true;
}
// helper function
var bindEvent = function(propertyId, dependencyPropertyId)
{
// dependencyPropertyId is the identifier for the property that the field "propertyId" is dependent on
var dependentField = Alpaca.resolveField(self, dependencyPropertyId);
if (dependentField)
{
dependentField.getFieldEl().bind("fieldupdate", (function(propertyField, dependencyField, propertyId, dependencyPropertyId) {
return function(event)
{
// the property "dependencyPropertyId" changed and affects target property ("propertyId")
// update UI state for target property
self.showOrHidePropertyBasedOnDependencies(propertyId);
propertyField.getFieldEl().trigger("fieldupdate");
};
})(item, dependentField, propertyId, dependencyPropertyId));
// trigger field update
dependentField.getFieldEl().trigger("fieldupdate");
}
};
if (Alpaca.isString(itemDependencies))
{
bindEvent(propertyId, itemDependencies);
}
else if (Alpaca.isArray(itemDependencies))
{
$.each(itemDependencies, function(index, value) {
bindEvent(propertyId, value);
});
}
},
refreshDependentFieldStates: function(propertyId)
{
var self = this;
var propertyField = this.childrenByPropertyId[propertyId];
if (!propertyField)
{
return Alpaca.throwErrorWithCallback("Missing property: " + propertyId, self.errorCallback);
}
var itemDependencies = self.getChildDependencies(propertyId);
if (!itemDependencies)
{
// no dependencies, so simple return
return true;
}
// helper function
var triggerFieldUpdateForProperty = function(otherPropertyId)
{
var dependentField = Alpaca.resolveField(self, otherPropertyId);
if (dependentField)
{
// trigger field update
dependentField.getFieldEl().trigger("fieldupdate");
}
};
if (Alpaca.isString(itemDependencies))
{
triggerFieldUpdateForProperty(itemDependencies);
}
else if (Alpaca.isArray(itemDependencies))
{
$.each(itemDependencies, function(index, value) {
triggerFieldUpdateForProperty(value);
});
}
},
/**
* Checks whether a single property's dependency is satisfied or not.
*
* In order to be valid, the property's dependency must exist (JSON schema) and optionally must satisfy
* any dependency options (value matches using an AND). Finally, the dependency field must be showing.
*
* @param {Object} propertyId Field property id.
* @param {Object} dependentOnPropertyId Property id of the dependency field.
*
* @returns {Boolean} True if all dependencies have been satisfied and the field needs to be shown,
* false otherwise.
*/
determineSingleDependencyValid: function(propertyId, dependentOnPropertyId)
{
var self = this;
// checks to see if the referenced "dependent-on" property has a value
// basic JSON-schema supports this (if it has ANY value, it is considered valid
// special consideration for boolean false
var dependentOnField = Alpaca.resolveField(self, dependentOnPropertyId);
if (!dependentOnField)
{
// no dependent-on field found, return false
return false;
}
var dependentOnData = dependentOnField.getValue();
// assume it isn't valid
var valid = false;
// go one of two directions depending on whether we have conditional dependencies or not
var conditionalDependencies = this.getChildConditionalDependencies(propertyId);
if (!conditionalDependencies || conditionalDependencies.length === 0)
{
//
// BASIC DEPENENDENCY CHECKING (CORE JSON SCHEMA)
//
// special case: if the field is a boolean field and we have no conditional dependency checking,
// then we set valid = false if the field data is a boolean false
if (dependentOnField.getType() === "boolean" && !this.childrenByPropertyId[propertyId].options.dependencies && !dependentOnData)
{
valid = false;
}
else
{
valid = !Alpaca.isValEmpty(dependentOnData);
}
}
else
{
//
// CONDITIONAL DEPENDENCY CHECKING (ALPACA EXTENSION VIA OPTIONS)
//
// Alpaca extends JSON schema by allowing dependencies to trigger only for specific values on the
// dependent fields. If options are specified to define this, we walk through and perform an
// AND operation across any fields
// do some data sanity cleanup
if (dependentOnField.getType() === "boolean" && !dependentOnData) {
dependentOnData = false;
}
var conditionalData = conditionalDependencies[dependentOnPropertyId];
// if the option is a function, then evaluate the function to determine whether to show
// the function evaluates regardless of whether the schema-based fallback determined we should show
if (!Alpaca.isEmpty(conditionalData) && Alpaca.isFunction(conditionalData))
{
valid = conditionalData.call(this, dependentOnData);
}
else
{
// assume true
valid = true;
// the conditional data is an array of values
if (Alpaca.isArray(conditionalData)) {
// check array value
if (!Alpaca.anyEquality(dependentOnData, conditionalData))
{
valid = false;
}
}
else
{
// check object value
if (!Alpaca.isEmpty(conditionalData) && !Alpaca.anyEquality(conditionalData, dependentOnData))
{
valid = false;
}
}
}
}
//
// NESTED HIDDENS DEPENDENCY HIDES (ALPACA EXTENSION)
//
// final check: only set valid if the dependentOnPropertyId is showing
if (dependentOnField && dependentOnField.isHidden())
{
valid = false;
}
return valid;
},
/**
* Gets child index.
*
* @param {Object} propertyId Child field property ID.
*/
getIndex: function(propertyId)
{
if (Alpaca.isEmpty(propertyId)) {
return -1;
}
for (var i = 0; i < this.children.length; i++) {
var pid = this.children[i].propertyId;
if (pid == propertyId) { // jshint ignore:line
return i;
}
}
return -1;
},
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// DYNAMIC METHODS
//
///////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Adds an item to the object.
*
* @param {String} propertyId Child field property ID.
* @param {Object} itemSchema schema
* @param {Object} fieldOptions Child field options.
* @param {Any} value Child field value
* @param {String} insertAfterId Location where the child item will be inserted.
* @param [Function} callback called once the item has been added
*/
addItem: function(propertyId, itemSchema, itemOptions, itemData, insertAfterId, callback)
{
var self = this;
this.createItem(propertyId, itemSchema, itemOptions, itemData, insertAfterId, function(child) {
var index = null;
if (insertAfterId && self.childrenById[insertAfterId])
{
for (var z = 0; z < self.children.length; z++)
{
if (self.children[z].getId() == insertAfterId)
{
index = z;
break;
}
}
}
// register the child
self.registerChild(child, ((index != null) ? index + 1 : 0));
// insert into dom
self.doAddItem(index, child);
// updates dom markers for this element and any siblings
self.handleRepositionDOMRefresh();
// update the array item toolbar state
//self.updateToolbars();
// refresh validation state
self.refreshValidationState(true, function() {
// dispatch event: add
self.trigger("add", child);
// trigger update
self.triggerUpdate();
// trigger "ready"
child.triggerWithPropagation.call(child, "ready", "down");
if (callback)
{
Alpaca.nextTick(function() {
callback();
});
}
});
});
},
doAddItem: function(index, item)
{
var self = this;
// insert into dom
if (!index)
{
// insert first into container
$(self.container).prepend(item.containerItemEl);
}
else
{
// insert at a specific index
var existingElement = self.getContainerEl().children("[data-alpaca-container-item-index='" + index + "']");
if (existingElement && existingElement.length > 0)
{
// insert after
existingElement.after(item.containerItemEl);
}
}
self.doAfterAddItem(item, function() {
// trigger ready
Alpaca.fireReady(item);
});
},
doAfterAddItem: function(item, callback)
{
callback();
},
doResolveItemContainer: function()
{
var self = this;
return $(self.container);
},
/**
* Removes an item from the object.
*
* @param propertyId
* @param callback
*/
removeItem: function(propertyId, callback)
{
var self = this;
var childField = this.childrenByPropertyId[propertyId];
if (childField)
{
this.children = $.grep(this.children, function (val, index) {
return (val.propertyId !== propertyId);
});
delete this.childrenByPropertyId[propertyId];
delete this.childrenById[childField.getId()];
// remove itemContainerEl from DOM
self.doRemoveItem(childField);
this.refreshValidationState(true, function () {
// updates dom markers for this element and any siblings
self.handleRepositionDOMRefresh();
// dispatch event: remove
self.trigger("remove", childField);
// trigger update handler
self.triggerUpdate();
if (callback)
{
Alpaca.nextTick(function() {
callback();
});
}
});
}
else
{
callback();
}
},
doRemoveItem: function(item)
{
var self = this;
var removeItemContainer = self.doResolveItemContainer();
removeItemContainer.children(".alpaca-container-item[data-alpaca-container-item-name='" + item.name + "']").remove();
// destroy child field itself
item.destroy();
},
///////////////////////////////////////////////////////////////////////////////////////////////////////
//
// WIZARD
//
///////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Wraps the current object into a wizard container and wires up the navigation and buttons so that
* wizard elements flip nicely.
*/
wizard: function()
{
var self = this;
// config-driven
var stepDescriptors = this.wizardConfigs.steps;
if (!stepDescriptors)
{
stepDescriptors = [];
}
var wizardTitle = this.wizardConfigs.title;
var wizardDescription = this.wizardConfigs.description;
var buttonDescriptors = this.wizardConfigs.buttons;
if (!buttonDescriptors)
{
buttonDescriptors = {};
}
if (!buttonDescriptors["previous"])
{
buttonDescriptors["previous"] = {}
}
if (!buttonDescriptors["previous"].title)
{
buttonDescriptors["previous"].title = "Previous";
}
if (!buttonDescriptors["previous"].align)
{
buttonDescriptors["previous"].align = "left";
}
if (!buttonDescriptors["previous"].type)
{
buttonDescriptors["previous"].type = "button";
}
if (!buttonDescriptors["next"])
{
buttonDescriptors["next"] = {}
}
if (!buttonDescriptors["next"].title)
{
buttonDescriptors["next"].title = "Next";
}
if (!buttonDescriptors["next"].align)
{
buttonDescriptors["next"].align = "right";
}
if (!buttonDescriptors["next"].type)
{
buttonDescriptors["next"].type = "button";
}
if (!this.wizardConfigs.hideSubmitButton)
{
if (!buttonDescriptors["submit"]) {
buttonDescriptors["submit"] = {}
}
if (!buttonDescriptors["submit"].title) {
buttonDescriptors["submit"].title = "Submit";
}
if (!buttonDescriptors["submit"].align) {
buttonDescriptors["submit"].align = "right";
}
if (!buttonDescriptors["submit"].type) {
buttonDescriptors["submit"].type = "button";
}
}
for (var buttonKey in buttonDescriptors)
{
if (!buttonDescriptors[buttonKey].type)
{
buttonDescriptors[buttonKey].type = "button";
}
}
var showSteps = this.wizardConfigs.showSteps;
if (typeof(showSteps) == "undefined")
{
showSteps = true;
}
var showProgressBar = this.wizardConfigs.showProgressBar;
var performValidation = this.wizardConfigs.validation;
if (typeof(performValidation) == "undefined")
{
performValidation = true;
}
// DOM-driven configuration
var wizardTitle = $(this.field).attr("data-alpaca-wizard-title");
var wizardDescription = $(this.field).attr("data-alpaca-wizard-description");
var _wizardValidation = $(this.field).attr("data-alpaca-wizard-validation");
if (typeof(_wizardValidation) != "undefined")
{
performValidation = _wizardValidation ? true : false;
}
var _wizardShowSteps = $(this.field).attr("data-alpaca-wizard-show-steps");
if (typeof(_wizardShowSteps) != "undefined")
{
showSteps = _wizardShowSteps ? true : false;
}
var _wizardShowProgressBar = $(this.field).attr("data-alpaca-wizard-show-progress-bar");
if (typeof(_wizardShowProgressBar) != "undefined")
{
showProgressBar = _wizardShowProgressBar ? true : false;
}
// find all of the steps
var stepEls = $(this.field).find("[data-alpaca-wizard-role='step']");
// DOM-driven configuration of step descriptors
if (stepDescriptors.length == 0)
{
stepEls.each(function(i) {
var stepDescriptor = {};
var stepTitle = $(this).attr("data-alpaca-wizard-step-title");
if (typeof(stepTitle) != "undefined")
{
stepDescriptor.title = stepTitle;
}
if (!stepDescriptor.title)
{
stepDescriptor.title = "Step " + i;
}
var stepDescription = $(this).attr("data-alpaca-wizard-step-description");
if (typeof(stepDescription) != "undefined")
{
stepDescriptor.description = stepDescription;
}
if (!stepDescriptor.description)
{
stepDescriptor.description = "Step " + i;
}
stepDescriptors.push(stepDescriptor);
});
}
// assume something for progress bar if not specified
if (typeof(showProgressBar) == "undefined")
{
if (stepDescriptors.length > 1)
{
showProgressBar = true;
}
}
// model for use in rendering the wizard
var model = {};
model.wizardTitle = wizardTitle;
model.wizardDescription = wizardDescription;
model.showSteps = showSteps;
model.performValidation = performValidation;
model.steps = stepDescriptors;
model.buttons = buttonDescriptors;
model.schema = self.schema;
model.options = self.options;
model.data = self.