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,535 lines (1,357 loc) 140 kB
/*jshint -W004 */ // duplicate variables /*jshint -W083 */ // inline functions are used safely /** * Alpaca forms engine for jQuery */ (function($) { /** * Renders an Alpaca field instance that is bound to a DOM element. * * The basic syntax is: * * <code> * <pre> * Alpaca(el, config); * </pre> * </code> * * The full syntax is: * * <code> * <pre> * Alpaca(el, { * "data" : {Any} field data (optional), * "schema": {Object} field schema (optional), * "options" : {Object} field options (optional), * "view": {Object|String} field view (object or id reference) (optional), * "render": {Function} callback function for replacing default rendering method (optional), * "postRender": {Function} callback function for post-rendering (optional), * "error": {Function} callback function for error handling (optional), * "connector": {Alpaca.Connector} connector for retrieving or storing data, schema, options, view and templates. (optional) * }); * </pre> * </code> * * @returns {*} */ var Alpaca = function() { var args = Alpaca.makeArray(arguments); if (args.length === 0) { // illegal return Alpaca.throwDefaultError("You must supply at least one argument. This argument can either be a DOM element against which Alpaca will generate a form or it can be a function name. See http://www.alpacajs.org for more details."); } // element is the first argument (either a string or a DOM element) var el = args[0]; if (el && Alpaca.isString(el)) { el = $("#" + el); } // other arguments we may want to figure out var data = null; var schema = null; var options = null; var view = null; var callback = null; var renderedCallback = null; var errorCallback = null; var connector = null; var notTopLevel = false; var initialSettings = {}; // if these options are provided, then data, schema, options and source are loaded via connector var dataSource = null; var schemaSource = null; var optionsSource = null; var viewSource = null; /** * Finds the Alpaca field instance bound to the dom element. * * First considers the immediate dom element and then looks 1 level deep to children and then up to parent. * * @returns {*} */ var findExistingAlpacaBinding = function(domElement, skipPivot) { var existing = null; // look at "data-alpaca-field-id" var alpacaFieldId = $(domElement).attr("data-alpaca-field-id"); if (alpacaFieldId) { var alpacaField = Alpaca.fieldInstances[alpacaFieldId]; if (alpacaField) { existing = alpacaField; } } // if not found, look at "data-alpaca-form-id" if (!existing) { var formId = $(domElement).attr("data-alpaca-form-id"); if (formId) { var subElements = $(domElement).find(":first"); if (subElements.length > 0) { var subFieldId = $(subElements[0]).attr("data-alpaca-field-id"); if (subFieldId) { var subField = Alpaca.fieldInstances[subFieldId]; if (subField) { existing = subField; } } } } } // if not found, check for children 0th element if (!existing && !skipPivot) { var childDomElements = $(el).find(":first"); if (childDomElements.length > 0) { var childField = findExistingAlpacaBinding(childDomElements[0], true); if (childField) { existing = childField; } } } // if not found, check parent if (!existing && !skipPivot) { var parentEl = $(el).parent(); if (parentEl) { var parentField = findExistingAlpacaBinding(parentEl, true); if (parentField) { existing = parentField; } } } return existing; }; var specialFunctionNames = ["get", "exists", "destroy"]; var isSpecialFunction = (args.length > 1 && Alpaca.isString(args[1]) && (specialFunctionNames.indexOf(args[1]) > -1)); var existing = findExistingAlpacaBinding(el); if (existing || isSpecialFunction) { if (isSpecialFunction) { // second argument must be a special function name var specialFunctionName = args[1]; if ("get" === specialFunctionName) { return existing; } else if ("exists" === specialFunctionName) { return (existing ? true : false); } else if ("destroy" === specialFunctionName) { if (existing) { existing.destroy(); } return; } return Alpaca.throwDefaultError("Unknown special function: " + specialFunctionName); } return existing; } else { var config = null; // just a dom element, no other args? if (args.length === 1) { // grab the data inside of the element and use that for config var jsonString = $(el).text(); config = JSON.parse(jsonString); $(el).html(""); } else { if (Alpaca.isObject(args[1])) { config = args[1]; } else if (Alpaca.isFunction(args[1])) { config = args[1](); } else { config = { "data": args[1] }; } } if (!config) { return Alpaca.throwDefaultError("Unable to determine Alpaca configuration"); } data = config.data; schema = config.schema; options = config.options; view = config.view; callback = config.render; if (config.callback) { callback = config.callback; } renderedCallback = config.postRender; errorCallback = config.error; connector = config.connector; // sources dataSource = config.dataSource; schemaSource = config.schemaSource; optionsSource = config.optionsSource; viewSource = config.viewSource; // other if (config.ui) { initialSettings["ui"] = config.ui; } if (config.type) { initialSettings["type"] = config.type; } if (!Alpaca.isEmpty(config.notTopLevel)) { notTopLevel = config.notTopLevel; } } // if no error callback is provided, we fall back to a browser alert if (Alpaca.isEmpty(errorCallback)) { errorCallback = Alpaca.defaultErrorCallback; } // instantiate the connector (if not already instantiated) // if config is passed in (as object), we instantiate if (!connector || !connector.connect) { var connectorId = "default"; var connectorConfig = {}; if (Alpaca.isString(connector)) { connectorId = connector; } else if (Alpaca.isObject(connector) && connector.id) { connectorId = connector.id; if (connector.config) { connectorConfig = connector.config; } } var ConnectorClass = Alpaca.getConnectorClass(connectorId); if (!ConnectorClass) { ConnectorClass = Alpaca.getConnectorClass("default"); } connector = new ConnectorClass(connectorId, connectorConfig); } // For second or deeper level of fields, default loader should be the one to do loadAll // since schema, data, options and view should have already been loaded. // Unless we want to load individual fields (other than the templates) using the provided // loader, this should be good enough. The benefit is saving time on loader format checking. var loadAllConnector = connector; if (notTopLevel) { var LoadAllConnectorClass = Alpaca.getConnectorClass("default"); loadAllConnector = new LoadAllConnectorClass("default"); } if (!options) { options = {}; } // resets the hideInitValidationError back to default state after first render var _resetInitValidationError = function(field) { // if this is the top-level alpaca field, then we call for validation state to be recalculated across // all child fields if (!field.parent) { // final call to update validation state // only do this if we're not supposed to suspend initial validation errors if (!field.hideInitValidationError) { field.refreshValidationState(true); } // force hideInitValidationError to false for field and all children if (field.view.type !== 'view') { Alpaca.fieldApplyFieldAndChildren(field, function(field) { // set to false after first validation (even if in CREATE mode, we only force init validation error false on first render) field.hideInitValidationError = false; }); } } }; // wrap rendered callback to allow for UI treatment (dom focus, etc) var _renderedCallback = function(field) { // if top level, apply a unique observable scope id if (!field.parent) { field.observableScope = Alpaca.generateId(); } // if we are the top-most control // fire "ready" event on every control // go down depth first and fire to lowest controls before trickling back up if (!field.parent) { Alpaca.fireReady(field); } // if top level and focus has not been specified, then auto-set if (Alpaca.isUndefined(options.focus) && !field.parent) { options.focus = Alpaca.defaultFocus; } // auto-set the focus? if (options && options.focus) { window.setTimeout(function() { var doFocus = function(__field) { __field.suspendBlurFocus = true; __field.focus(); __field.suspendBlurFocus = false; }; if (options.focus) { if (field.isControlField && field.isAutoFocusable()) { // just focus on this one doFocus(field); } else if (field.isContainerField) { // if focus = true, then focus on the first child control if it is auto-focusable // and not read-only if (options.focus === true) { // pick first element in form if (field.children && field.children.length > 0) { /* for (var z = 0; z < field.children.length; z++) { if (field.children[z].isControlField) { if (field.children[z].isAutoFocusable() && !field.children[z].options.readonly) { doFocus(field.children[z]); break; } } } */ doFocus(field); } } else if (typeof(options.focus) === "string") { // assume it is a path to the child var child = field.getControlByPath(options.focus); if (child && child.isControlField && child.isAutoFocusable()) { doFocus(child); } } } _resetInitValidationError(field); } }, 500); } else { _resetInitValidationError(field); } if (renderedCallback) { renderedCallback(field); } }; loadAllConnector.loadAll({ "data": data, "schema": schema, "options": options, "view": view, "dataSource": dataSource, "schemaSource": schemaSource, "optionsSource": optionsSource, "viewSource": viewSource }, function(loadedData, loadedOptions, loadedSchema, loadedView) { // for cases where things could not be loaded via source loaders, fall back to what may have been passed // in directly as values loadedData = loadedData ? loadedData : data; loadedSchema = loadedSchema ? loadedSchema: schema; loadedOptions = loadedOptions ? loadedOptions : options; loadedView = loadedView ? loadedView : view; // some defaults for the case where data is null // if schema + options are not provided, we assume a text field if (Alpaca.isEmpty(loadedData)) { if (Alpaca.isEmpty(loadedSchema) && (Alpaca.isEmpty(loadedOptions) || Alpaca.isEmpty(loadedOptions.type))) { loadedData = ""; if (Alpaca.isEmpty(loadedOptions)) { loadedOptions = "text"; } else if (options && Alpaca.isObject(options)) { loadedOptions.type = "text"; } } } if (loadedOptions.view && !view) { loadedView = loadedOptions.view; } // init alpaca return Alpaca.init(el, loadedData, loadedOptions, loadedSchema, loadedView, initialSettings, callback, _renderedCallback, connector, errorCallback); }, function (loadError) { errorCallback(loadError); return null; }); }; /** * @namespace Namespace for all Alpaca Field Class Implementations. */ Alpaca.Fields = { }; /** * @namespace Namespace for all Alpaca Connector Class Implementations. */ Alpaca.Connectors = { }; Alpaca.Extend = $.extend; Alpaca.Create = function() { var args = Array.prototype.slice.call(arguments); args.unshift({}); return $.extend.apply(this, args); }; // static methods and properties Alpaca.Extend(Alpaca, /** @lends Alpaca */ { /** * Makes an array. * * @param {Any} nonArray A non-array variable. * @returns {Array} Array out of the non-array variable. */ makeArray : function(nonArray) { return Array.prototype.slice.call(nonArray); }, /** * Finds whether the type of a variable is function. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is a function, false otherwise. */ isFunction: function(obj) { return Object.prototype.toString.call(obj) === "[object Function]"; }, /** * Finds whether the type of a variable is string. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is a string, false otherwise. */ isString: function(obj) { return (typeof obj === "string"); }, /** * Finds whether the type of a variable is object. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is an object, false otherwise. */ isObject: function(obj) { return !Alpaca.isUndefined(obj) && Object.prototype.toString.call(obj) === '[object Object]'; }, /** * Finds whether the type of a variable is a plain, non-prototyped object. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is a plain object, false otherwise. */ isPlainObject: function(obj) { return $.isPlainObject(obj); }, /** * Finds whether the type of a variable is number. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is a number, false otherwise. */ isNumber: function(obj) { return (typeof obj === "number"); }, /** * Finds whether the type of a variable is array. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is an array, false otherwise. */ isArray: function(obj) { return Object.prototype.toString.call(obj) == "[object Array]"; }, /** * Finds whether the type of a variable is boolean. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is a boolean, false otherwise. */ isBoolean: function(obj) { return (typeof obj === "boolean"); }, /** * Finds whether the type of a variable is undefined. * @param {Any} obj The variable being evaluated. * @returns {Boolean} True if the variable is a undefined, false otherwise. */ isUndefined: function(obj) { return (typeof obj == "undefined"); }, /** * Strips any excess whitespace characters from the given text. * Returns the trimmed string. * * @param str * * @return trimmed string */ trim: function(text) { var trimmed = text; if (trimmed && Alpaca.isString(trimmed)) { trimmed = trimmed.replace(/^\s+|\s+$/g, ''); } return trimmed; }, /** * Provides a safe conversion of an HTML textual string into a DOM object. * * @param x * @return {*} */ safeDomParse: function(x) { if (x && Alpaca.isString(x)) { x = Alpaca.trim(x); // convert to dom var converted = null; try { converted = $(x); } catch (e) { // make another attempt to account for safety in some browsers x = "<div>" + x + "</div>"; converted = $(x).children(); } return converted; } return x; }, /** * Finds whether a variable is empty. * @param {Any} obj The variable being evaluated. * @param [boolean] includeFunctions whether to include functions in any counts * @returns {Boolean} True if the variable is empty, false otherwise. */ isEmpty: function(obj, includeFunctions) { var self = this; if (Alpaca.isUndefined(obj)) { return true; } else if (obj === null) { return true; } if (obj && Alpaca.isObject(obj)) { var count = self.countProperties(obj, includeFunctions); if (count === 0) { return true; } } return false; }, /** * Counts the number of properties in an object. * * @param obj * @param includeFunctions * * @returns {number} */ countProperties: function(obj, includeFunctions) { var count = 0; if (obj && Alpaca.isObject(obj)) { for (var k in obj) { if (obj.hasOwnProperty(k)) { if (includeFunctions) { count++; } else { if (typeof(obj[k]) !== "function") { count++; } } } } } return count; }, /** * Produces a copy of the given JS value. * * If the value is a simple array or a simple object, then a pure copy is produced. * * If it's a complex object or a function, then the reference is copied (i.e. not truly a copy). * * @param thing * @return {*} */ copyOf: function(thing) { var copy = thing; if (Alpaca.isArray(thing)) { copy = []; for (var i = 0; i < thing.length; i++) { copy.push(Alpaca.copyOf(thing[i])); } } else if (Alpaca.isObject(thing)) { if (thing instanceof Date) { // date return new Date(thing.getTime()); } else if (thing instanceof RegExp) { // regular expression return new RegExp(thing); } else if (thing.nodeType && "cloneNode" in thing) { // DOM node copy = thing.cloneNode(true); } else if ($.isPlainObject(thing)) { copy = {}; for (var k in thing) { if (thing.hasOwnProperty(k)) { copy[k] = Alpaca.copyOf(thing[k]); } } } else { // otherwise, it's some other kind of object so we just do a referential copy // in other words, not a copy } } return copy; }, copyInto: function(target, source) { for (var i in source) { if (source.hasOwnProperty(i) && !this.isFunction(this[i])) { target[i] = source[i]; } } }, /** * Retained for legacy purposes. Alias for copyOf(). * * @param object * @returns {*} */ cloneObject: function(object) { return Alpaca.copyOf(object); }, /** * Splices a string. * * @param {String} source Source string to be spliced. * @param {Integer} splicePoint Splice location. * @param {String} splice String to be spliced in. * @returns {String} Spliced string */ spliceIn: function(source, splicePoint, splice) { return source.substring(0, splicePoint) + splice + source.substring(splicePoint, source.length); }, /** * Compacts an array. * * @param {Array} arr Source array to be compacted. * @returns {Array} Compacted array. */ compactArray: function(arr) { var n = [], l = arr.length,i; for (i = 0; i < l; i++) { if (!lang.isNull(arr[i]) && !lang.isUndefined(arr[i])) { n.push(arr[i]); } } return n; }, /** * Removes accents from a string. * * @param {String} str Source string. * @returns {String} Cleaned string without accents. */ removeAccents: function(str) { return str.replace(/[àáâãäå]/g, "a").replace(/[èéêë]/g, "e").replace(/[ìíîï]/g, "i").replace(/[òóôõö]/g, "o").replace(/[ùúûü]/g, "u").replace(/[ýÿ]/g, "y").replace(/[ñ]/g, "n").replace(/[ç]/g, "c").replace(/[œ]/g, "oe").replace(/[æ]/g, "ae"); }, /** * @private * @param el * @param arr * @param fn */ indexOf: function(el, arr, fn) { var l = arr.length,i; if (!Alpaca.isFunction(fn)) { /** * @ignore * @param elt * @param arrElt */ fn = function(elt, arrElt) { return elt === arrElt; }; } for (i = 0; i < l; i++) { if (fn.call({}, el, arr[i])) { return i; } } return -1; }, /** * Static counter for generating a unique ID. */ uniqueIdCounter: 0, /** * Default Locale. */ defaultLocale: "en_US", /** * Whether to set focus by default */ defaultFocus: true, /** * The default sort function to use for enumerations. */ defaultSort: function(a, b) { if (a.text > b.text) { return 1; } else if (a.text < b.text) { return -1; } return 0; }, /** * Sets the default Locale. * * @param {String} locale New default locale. */ setDefaultLocale: function(locale) { this.defaultLocale = locale; }, /** * Field Type to Schema Type Mappings. */ defaultSchemaFieldMapping: {}, /** * Registers a field type to schema data type mapping. * * @param {String} schemaType Schema data type. * @param {String} fieldType Field type. */ registerDefaultSchemaFieldMapping: function(schemaType, fieldType) { if (schemaType && fieldType) { this.defaultSchemaFieldMapping[schemaType] = fieldType; } }, /** * Field Type to Schema Format Mappings. */ defaultFormatFieldMapping: {}, /** * Registers a field type to schema format mapping. * * @param {String} format Schema format. * @param {String} fieldType Field type. */ registerDefaultFormatFieldMapping: function(format, fieldType) { if (format && fieldType) { this.defaultFormatFieldMapping[format] = fieldType; } }, /** * Gets schema type of a variable. * * @param {Any} data The variable. * @returns {String} Schema type of the variable. */ getSchemaType: function(data) { var schemaType = null; // map data types to default field types if (Alpaca.isEmpty(data)) { schemaType = "string"; } else if (Alpaca.isArray(data)) { schemaType = "array"; } else if (Alpaca.isObject(data)) { schemaType = "object"; } else if (Alpaca.isString(data)) { schemaType = "string"; } else if (Alpaca.isNumber(data)) { schemaType = "number"; } else if (Alpaca.isBoolean(data)) { schemaType = "boolean"; } // Last check for data that carries functions -- GitanaConnector case. if (!schemaType && (typeof data === 'object')) { schemaType = "object"; } return schemaType; }, /** * Makes a best guess at the options field type if none provided. * * @param schema * @returns {string} the field type */ guessOptionsType: function(schema) { var type = null; if (schema && typeof(schema["enum"]) !== "undefined") { if (schema["enum"].length > 3) { type = "select"; } else { type = "radio"; } } else { type = Alpaca.defaultSchemaFieldMapping[schema.type]; } // check if it has format defined if (schema.format && Alpaca.defaultFormatFieldMapping[schema.format]) { type = Alpaca.defaultFormatFieldMapping[schema.format]; } return type; }, /** * Alpaca Views. */ views: {}, /** * Generates a valid view id. * * @returns {String} A valid unique view id. */ generateViewId : function () { return "view-" + this.generateId(); }, /** * Registers a view with the framework. * * @param viewObject */ registerView: function(viewObject) { var viewId = viewObject.id; if (!viewId) { return Alpaca.throwDefaultError("Cannot register view with missing view id: " + viewId); } var existingView = this.views[viewId]; if (existingView) { Alpaca.mergeObject(existingView, viewObject); } else { this.views[viewId] = viewObject; if (!viewObject.templates) { viewObject.templates = {}; } // if we have any precompiled views, flag them var engineIds = Alpaca.TemplateEngineRegistry.ids(); for (var i = 0; i < engineIds.length; i++) { var engineId = engineIds[i]; var engine = Alpaca.TemplateEngineRegistry.find(engineId); if (engine) { // ask the engine if it has any cache keys for view templates for this view var cacheKeys = engine.findCacheKeys(viewId); for (var z = 0; z < cacheKeys.length; z++) { var parts = Alpaca.splitCacheKey(cacheKeys[z]); // mark as precompiled viewObject.templates[parts.templateId] = { "type": engineId, "template": true, "cacheKey": cacheKeys[z] }; } } } } }, /** * Retrieves a normalized view by view id. * * @param viewId * @return {*} */ getNormalizedView: function(viewId) { return this.normalizedViews[viewId]; }, /** * Resolves which view handles a given theme and type of operation. * * @param {String} ui * @param {String} type * * @returns {String} the view id */ lookupNormalizedView: function(ui, type) { var theViewId = null; for (var viewId in this.normalizedViews) { var view = this.normalizedViews[viewId]; if (view.ui === ui && view.type === type) { theViewId = viewId; break; } } return theViewId; }, /** * Registers a template to a view. * * @param {String} templateId Template id. * @param {String|Object} template Either the text of the template or an object containing { "type": "<templateEngineIdentifier>", "template": "<markup>" } * @param [String] viewId the optional view id. If none is provided, then all registrations are to the default view. */ registerTemplate: function(templateId, template, viewId) { // if no view specified, fall back to the base view which is "base" if (!viewId) { viewId = "base"; } if (!this.views[viewId]) { this.views[viewId] = {}; this.views[viewId].id = viewId; } if (!this.views[viewId].templates) { this.views[viewId].templates = {}; } this.views[viewId].templates[templateId] = template; // if normalized views have already been computed, then wipe them down // this allows them to be re-computed on the next render and allows this template to participate if (Alpaca.countProperties(Alpaca.normalizedViews) > 0) { Alpaca.normalizedViews = {}; } }, /** * Registers list of templates to a view. * * @param {Array} templates Templates being registered * @param {String} viewId Id of the view that the templates being registered to. */ registerTemplates: function(templates, viewId) { for (var templateId in templates) { this.registerTemplate(templateId, templates[templateId], viewId); } }, /** * Registers a message to a view. * * @param {String} messageId Id of the message being registered. * @param {String} message Message to be registered * @param {String} viewId Id of the view that the message being registered to. */ registerMessage: function(messageId, message, viewId) { // if no view specified, fall back to the base view which is "base" if (!viewId) { viewId = "base"; } if (!this.views[viewId]) { this.views[viewId] = {}; this.views[viewId].id = viewId; } if (!this.views[viewId].messages) { this.views[viewId].messages = {}; } this.views[viewId].messages[messageId] = message; }, /** * Registers messages with a view. * * @param {Array} messages Messages to be registered. * @param {String} viewId Id of the view that the messages being registered to. */ registerMessages: function(messages, viewId) { for (var messageId in messages) { if (messages.hasOwnProperty(messageId)) { this.registerMessage(messageId, messages[messageId], viewId); } } }, /** * Default date format. */ defaultDateFormat: "MM/DD/YYYY", /** * Default time format. */ defaultTimeFormat: "HH:mm:ss", /** * Regular expressions for fields. */ regexps: { "email": /^[a-z0-9!\#\$%&'\*\-\/=\?\+\-\^_`\{\|\}~]+(?:\.[a-z0-9!\#\$%&'\*\-\/=\?\+\-\^_`\{\|\}~]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,6}$/i, "url": /^(http|https|ftp):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(\:[0-9]{1,5})?(\/.*)?$/i, "intranet-url": /^(http|https|ftp):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\:[0-9]{1,5})?(\/.*)?$/i, "password": /^[0-9a-zA-Z\x20-\x7E]*$/, "date": /^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.]\d\d$/, "integer": /^([\+\-]?([1-9]\d*)|0)$/, "number":/^([\+\-]?((([0-9]+(\.)?)|([0-9]*\.[0-9]+))([eE][+-]?[0-9]+)?))$/, "phone":/^(\D?(\d{3})\D?\D?(\d{3})\D?(\d{4}))?$/, "ipv4":/^(?:1\d?\d?|2(?:[0-4]\d?|[6789]|5[0-5]?)?|[3-9]\d?|0)(?:\.(?:1\d?\d?|2(?:[0-4]\d?|[6789]|5[0-5]?)?|[3-9]\d?|0)){3}$/, "zipcode-five": /^(\d{5})?$/, "zipcode-nine": /^(\d{5}(-\d{4})?)?$/, "whitespace": /^\s+$/ }, /** * Map of instantiated fields. */ fieldInstances: {}, /** * Maps of field types to field class implementations. */ fieldClassRegistry: {}, /** * Registers an implementation class for a type of field. * * @param {String} type Field type. * @param {Alpaca.Field} fieldClass Field class. */ registerFieldClass: function(type, fieldClass) { this.fieldClassRegistry[type] = fieldClass; }, /** * Returns the implementation class for a type of field. * * @param {String} type Field type. * * @returns {Alpaca.Field} Field class mapped to field type. */ getFieldClass: function(type) { return this.fieldClassRegistry[type]; }, /** * Gets the field type id for a given field implementation class. * * @param {Alpaca.Field} fieldClass Field class. * * @returns {String} Field type of the field class. */ getFieldClassType: function(fieldClass) { for (var type in this.fieldClassRegistry) { if (this.fieldClassRegistry.hasOwnProperty(type)) { if (this.fieldClassRegistry[type] === fieldClass) { return type; } } } return null; }, /** * Maps of connector types to connector class implementations. */ connectorClassRegistry: {}, /** * Registers an implementation class for a connector type. * * @param {String} type cConnect type * @param {Alpaca.Connector} connectorClass Connector class. */ registerConnectorClass: function(type, connectorClass) { this.connectorClassRegistry[type] = connectorClass; }, /** * Returns the implementation class for a connector type. * * @param {String} type Connect type. * @returns {Alpaca.Connector} Connector class mapped to connect type. */ getConnectorClass: function(type) { return this.connectorClassRegistry[type]; }, /** * Replaces each substring of this string that matches the given regular expression with the given replacement. * * @param {String} text Source string being replaced. * @param {String} replace Regular expression for replacing. * @param {String} with_this Replacement. * * @returns {String} Replaced string. */ replaceAll: function(text, replace, with_this) { return text.replace(new RegExp(replace, 'g'), with_this); }, /** * Creates an element with a given tag name, dom/style attributes and class names. * * @param {String} tag Tag name. * @param {Array} domAttributes DOM attributes. * @param {Array} styleAttributes Style attributes. * @param {Array} classNames Class names. * * @returns {Object} New element with the tag name and all other provided attributes. */ element: function(tag, domAttributes, styleAttributes, classNames) { var el = $("<" + tag + "/>"); if (domAttributes) { el.attr(domAttributes); } if (styleAttributes) { el.css(styleAttributes); } if (classNames) { for (var className in classNames) { el.addClass(className); } } }, /** * Replaces a template with list of replacements. * * @param {String} template Template being processed. * @param {String} substitutions List of substitutions. * * @returns {String} Replaced template. */ elementFromTemplate: function(template, substitutions) { var html = template; if (substitutions) { for (var x in substitutions) { html = Alpaca.replaceAll(html, "${" + x + "}", substitutions[x]); } } return $(html); }, /** * Generates a unique alpaca id. * * @returns {String} The unique alpaca id. */ generateId: function() { Alpaca.uniqueIdCounter++; return "alpaca" + Alpaca.uniqueIdCounter; }, /** * Helper function to provide YAHOO later like capabilities. */ later: function(when, o, fn, data, periodic) { when = when || 0; o = o || {}; var m = fn, d = $.makeArray(data), f, r; if (typeof fn === "string") { m = o[fn]; } if (!m) { // Throw an error about the method throw { name: 'TypeError', message: "The function is undefined." }; } /** * @ignore */ f = function() { m.apply(o, d); }; r = (periodic) ? setInterval(f, when) : setTimeout(f, when); return { id: r, interval: periodic, cancel: function() { if (this.interval) { clearInterval(r); } else { clearTimeout(r); } } }; }, /** * Finds if an string ends with a given suffix. * * @param {String} text The string being evaluated. * @param {String} suffix Suffix. * @returns {Boolean} True if the string ends with the given suffix, false otherwise. */ endsWith : function(text, suffix) { return text.indexOf(suffix, text.length - suffix.length) !== -1; }, /** * Finds if an string starts with a given prefix. * * @param {String} text The string being evaluated. * @param {String} prefix Prefix * @returns {Boolean} True if the string starts with the given prefix, false otherwise. */ startsWith : function(text, prefix) { //return (text.match("^" + prefix) == prefix); return text.substr(0, prefix.length) === prefix; }, /** * Finds if a variable is a URI. * * @param {Object} obj The variable being evaluated. * @returns {Boolean} True if the variable is a URI, false otherwise. */ isUri : function(obj) { return Alpaca.isString(obj) && (Alpaca.startsWith(obj, "http://") || Alpaca.startsWith(obj, "https://") || Alpaca.startsWith(obj, "/") || Alpaca.startsWith(obj, "./") || Alpaca.startsWith(obj, "../")); }, /** * Picks a sub-element from an object using a keys array. * * @param {Object} object Object to be traversed * @param {String|Array} keys Either an array of tokens or a dot-delimited string (i.e. "data.user.firstname") * @param {String} subprop Optional subproperty to traverse (i.e.. "data.properties.user.properties.firstname") * * @returns {Object} Sub element mapped to the given key path */ traverseObject : function(object, keys, subprop) { if (Alpaca.isString(keys)) { keys = keys.split("."); } var element = null; var current = object; var key = null; do { key = keys.shift(); if (subprop && key === subprop) { key = keys.shift(); } if (!Alpaca.isEmpty(current[key])) { current = current[key]; if (keys.length === 0) { element = current; } } else { keys = []; } } while (keys.length > 0); return element; }, /** * Helper function that executes the given function upon each element in the array * The element of the array becomes the "this" variable in the function * * @param {Array|Object} data Either an array or an object * @param {Function} func Function to be executed. */ each : function(data, func) { if (Alpaca.isArray(data)) { for (var i = 0; i < data.length; i++) { func.apply(data[i]); } } else if (Alpaca.isObject(data)) { for (var key in data) { func.apply(data[key]); } } }, /** * Merges json obj2 into obj1 using a recursive approach. * * @param {Object} obj1 Destination object. * @param {Object} obj2 Source object. * @param {Function} validKeyFunction Function used to determine whether to include a given key or not. * * @returns {Object} Merged object. */ merge : function(obj1, obj2, validKeyFunction) { if (!obj1) { obj1 = {}; } for (var key in obj2) { var valid = true; if (validKeyFunction) { valid = validKeyFunction(key); } if (valid) { if (Alpaca.isEmpty(obj2[key])) { obj1[key] = obj2[key]; } else { if (Alpaca.isObject(obj2[key])) { if (!obj1[key]) { obj1[key] = {}; } obj1[key] = Alpaca.merge(obj1[key], obj2[key]); } else { obj1[key] = obj2[key]; } } } } return obj1; }, /** * Merges json "source" into "target" using a recursive approach. The merge will include empty values * of obj2 properties. * * @param {Object} target Target object. * @param {Object} source Source object. * * @returns {Object} Merged object */ mergeObject : function(target, source) { if (!target) { target = {}; } if (!source) { source = {}; } this.mergeObject2(source, target); return target;