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
493 lines (401 loc) • 17.8 kB
JavaScript
(function($) {
var Alpaca = $.alpaca;
Alpaca.Fields.OptionTreeField = Alpaca.Fields.TextField.extend(
/**
* @lends Alpaca.Fields.OptionTree.prototype
*/
{
/**
* @see Alpaca.Fields.ObjectField#getFieldType
*/
getFieldType: function() {
return "optiontree";
},
/**
* @private
* @see Alpaca.Fields.ObjectField#setup
*/
setup: function()
{
var self = this;
this.base();
if (!this.options.tree)
{
this.options.tree = {};
}
if (!this.options.tree.selectors)
{
this.options.tree.selectors = {};
}
if (!this.options.tree.order)
{
this.options.tree.order = [];
}
// ensure all options have schema
for (var k in this.options.tree.selectors)
{
if (!this.options.tree.selectors[k].schema) {
Alpaca.logError("OptionTree selector for: " + k + " is missing schema");
return;
}
if (!this.options.tree.selectors[k].options) {
this.options.tree.selectors[k].options = {};
}
}
if (!this.options.tree.data)
{
this.options.tree.data = [];
}
// walk data
for (var i = 0; i < this.options.tree.data.length; i++)
{
var item = this.options.tree.data[i];
if (item.attributes)
{
for (var k in item.attributes)
{
if (!this.options.tree.selectors[k])
{
this.options.tree.selectors[k] = {};
}
if (!this.options.tree.selectors[k].label)
{
this.options.tree.selectors[k].options.noneLabel = "Choose...";
}
if (!this.options.tree.selectors[k].type)
{
this.options.tree.selectors[k].options.type = "select";
}
}
}
}
// assume the order from the options if not otherwise provided
if (!self.options.tree.order)
{
self.options.tree.order = [];
for (var k in self.options.tree.selectors)
{
self.options.tree.order.push(self.options.tree.selectors[k]);
}
}
if (typeof(self.options.tree.horizontal) === "undefined")
{
self.options.tree.horizontal = true;
}
// create a lookup list for option list key/value based on location
this.locationValueLists = {};
this.locationValues = {};
for (var i = 0; i < self.options.tree.data.length; i++)
{
if (self.options.tree.data[i].attributes)
{
var location = "root";
for (var k in self.options.tree.data[i].attributes)
{
var v = self.options.tree.data[i].attributes[k];
var array = this.locationValueLists[location];
if (!array) {
array = [];
this.locationValueLists[location] = array;
}
var exists = false;
for (var x = 0; x < array.length; x++)
{
if (array[x].value === v) {
exists = true;
break;
}
}
if (!exists)
{
array.push({
"text": v,
"value": v
});
}
if (location.length > 0) {
location += "~";
}
location += k + "=" + v;
}
this.locationValues[location] = self.options.tree.data[i].value;
}
}
this.currentAttributes = {};
this.controls = {};
},
toLocation: function(attrs)
{
var location = "root";
for (var k in attrs)
{
var v = attrs[k];
if (location.length > 0) {
location += "~";
}
location += k + "=" + v;
}
return location;
},
existsLocationWithPrefix: function(prefix)
{
var match = false;
for (var k in this.locationValueLists)
{
if (k.indexOf(prefix) > -1)
{
match = true;
break;
}
}
return match;
},
/**
* @see Alpaca.Field#afterRenderControl
*/
afterRenderControl: function(model, callback) {
var self = this;
self.optionTreeHolder = $(self.field).find(".optiontree");
if (self.options.tree.horizontal)
{
$(self.field).addClass("optiontree-horizontal");
}
this.base(model, function() {
self.refreshOptionTreeControls(function() {
callback();
});
});
},
refreshOptionTreeControls: function(callback)
{
var self = this;
// hide all of the controls
for (var k in self.controls)
{
self.controls[k].hide();
}
// find the index of the last option for which we are missing a value
var displayUpToIndex = 0;
for (var i = 0; i < self.options.tree.order.length; i++)
{
var selectorId = self.options.tree.order[i];
if (typeof(self.currentAttributes[selectorId]) !== "undefined" && self.currentAttributes[selectorId] !== null && self.currentAttributes[selectorId] !== "")
{
displayUpToIndex++;
}
}
// walk through order and construct render functions
var location = "root";
var fns = [];
var displayCount = 0;
var i = 0;
do
{
if (i < self.options.tree.order.length)
{
var selectorId = self.options.tree.order[i];
var hasMatches = (i == self.options.tree.order.length - 1) || (self.existsLocationWithPrefix(location + "~" + selectorId + "="));
if (hasMatches)
{
if (displayCount <= displayUpToIndex)
{
if (self.controls[selectorId])
{
// show this one
self.controls[selectorId].show();
location += "~" + selectorId + "=" + self.currentAttributes[selectorId];
}
else
{
var selector = self.options.tree.selectors[selectorId];
var last = (i + 1 === self.options.tree.order.length);
var fn = function(index, selectorId, selector, controls, optionTreeHolder, last) {
return function(done) {
var alpacaSchema = selector.schema;
var alpacaOptions = selector.options;
if (!alpacaOptions) {
alpacaOptions = {};
}
if (!alpacaOptions.type) {
alpacaOptions.type = "select";
}
if (alpacaOptions.type === "select") {
alpacaOptions.dataSource = function(callback) {
var currentLocation = self.toLocation(self.currentAttributes);
var currentValueList = self.locationValueLists[currentLocation];
callback(currentValueList);
};
}
// render via alpaca
var domEl = $("<div class='optiontree-selector'></div>");
$(domEl).alpaca({
"schema": alpacaSchema,
"options": alpacaOptions,
"postRender": function(control) {
controls[selectorId] = control;
// append to the holder element
$(optionTreeHolder).append(domEl);
control.selectorId = selectorId;
// when the value of this control changes, we record it into our
// current attribute set
control.on("change", function() {
var selectorId = this.selectorId;
// set our attribute value
self.currentAttributes[selectorId] = this.getValue();
// clear out everything past our index value
for (var i = 0; i < self.options.tree.order.length; i++)
{
if (i > index)
{
var selectorId = self.options.tree.order[i];
delete self.currentAttributes[selectorId];
if (controls[selectorId])
{
controls[selectorId].destroy();
delete controls[selectorId];
}
}
}
if (last)
{
// find the match
var val = null;
for (var i = 0; i < self.options.tree.data.length; i++)
{
var match = true;
var attrs = self.options.tree.data[i].attributes;
for (var k in self.currentAttributes)
{
if (attrs[k] !== self.currentAttributes[k])
{
match = false;
break;
}
}
if (match)
{
val = self.options.tree.data[i].value;
}
}
if (val)
{
self.setValue(val);
}
}
self.refreshOptionTreeControls();
});
// show by default
control.show();
done();
}
});
}
}(i, selectorId, selector, self.controls, self.optionTreeHolder, last);
fns.push(fn);
location += "~" + selectorId + "=" + self.currentAttributes[selectorId];
}
displayCount++;
}
else
{
if (self.controls[selectorId])
{
self.controls[selectorId].destroy();
delete self.controls[selectorId];
}
}
}
else
{
if (self.controls[selectorId])
{
self.controls[selectorId].destroy();
delete self.controls[selectorId];
}
}
}
i++;
}
while (i < self.options.tree.order.length);
Alpaca.series(fns, function() {
if (callback)
{
callback();
}
});
},
/**
* @see Alpaca.Fields.ObjectField#getType
*/
getType: function() {
return "any";
}
/* builder_helpers */
,
/**
* @see Alpaca.Fields.ObjectField#getTitle
*/
getTitle: function() {
return "Option Tree";
},
/**
* @see Alpaca.Fields.ObjectField#getDescription
*/
getDescription: function() {
return "Option Tree";
},
/**
* @private
* @see Alpaca.Fields.ObjectField#getSchemaOfOptions
*/
getSchemaOfOptions: function() {
return Alpaca.merge(this.base(), {
"properties": {
"tree": {
"type": "object",
"properties": {
"options": {
"type": "object"
},
"order": {
"type": "array",
"items": {
"type": "string"
}
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "any"
},
"attributes": {
"type": "object"
}
}
}
},
"horizontal": {
"type": "boolean"
}
}
}
}
});
},
/**
* @private
* @see Alpaca.Fields.ObjectField#getOptionsForOptions
*/
getOptionsForOptions: function() {
return Alpaca.merge(this.base(), {
"fields": {
}
});
}
/* end_builder_helpers */
});
Alpaca.registerFieldClass("optiontree", Alpaca.Fields.OptionTreeField);
})(jQuery);