UNPKG

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
/*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.