diff --git a/.gitignore b/.gitignore index a6ed6fb..0436547 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ .vscode/ node_modules/ *.tgz -lib \ No newline at end of file diff --git a/doc/Development.md b/doc/Development.md index 258f3ff..db890a1 100644 --- a/doc/Development.md +++ b/doc/Development.md @@ -4,15 +4,15 @@ ## Requirements -* npm - I use v1.4.3. Different versions can store packages in very different ways, so be aware of that if you use something different. -* node - 0.10.26. Note that the package in the apt repos is much older, and there are much newer ones available. Newer 10.x versions are probably ok. +* npm - tested with npm v6.4.1 - Different versions can store packages in very different ways, so be aware of that if you use something different. +* node - 0.10.26, tested up to v10.14.1 - Note that the package in the apt repos is much older, and there are much newer ones available. Newer 10.x versions are probably ok. * gulp - Part of the required packages, but is useful to have a global version too. `npm install -g gulp` ## Git and npm Use the standard git-branch-merge workflow to update master, then `npm publish` -This package is published to the public npm repositories, then referenced from other places by version there. So follow (semver)[http://semver.org/] policies with the package name, then npm publish when you have something working. +This package is published to the public npm repositories, then referenced from other places by version there. So follow [semver](http://semver.org/) policies with the package name, then npm publish when you have something working. Depending on the features added, you may need to then update the reference in these places: diff --git a/doc/VersionHistory.md b/doc/VersionHistory.md index 1c77519..7d9f169 100644 --- a/doc/VersionHistory.md +++ b/doc/VersionHistory.md @@ -1,5 +1,8 @@ # Version History +# 2.2.x +- Testing support for bid adjustments + # 2.2.1 - Fix a bug where applying a value to a multiselect field where some of those values aren't options would sometimes not add all new values as options. diff --git a/lib/building.js b/lib/building.js new file mode 100644 index 0000000..807a3e5 --- /dev/null +++ b/lib/building.js @@ -0,0 +1,224 @@ +var CoffeeScript, ModelGroup, Mustache, _, globals, jiff, throttledAlert, vm; + +CoffeeScript = require('coffee-script'); + +Mustache = require('mustache'); + +_ = require('underscore'); + +vm = require('vm'); + +jiff = require('jiff'); + +globals = require('./globals'); + +ModelGroup = require('./modelGroup'); + +globals = require('./globals'); + +if (typeof alert !== "undefined" && alert !== null) { + throttledAlert = _.throttle(alert, 500); +} + + +/* + An array of functions that can test a built model. + Model code may add tests to this array during build. The tests themselves will not be run at the time, but are + made avaiable via this export so processes can run the tests when appropriate. + Tests may modify the model state, so the model should be rebuilt prior to running each test. + */ + +exports.modelTests = []; + +exports.fromCode = function(code, data, element, imports, isImport) { + var assert, emit, newRoot, test; + data = (function() { + switch (typeof data) { + case 'object': + return jiff.clone(data); + case 'string': + return JSON.parse(data); + default: + return {}; + } + })(); + globals.runtime = false; + exports.modelTests = []; + test = function(func) { + return exports.modelTests.push(func); + }; + assert = function(bool, message) { + if (message == null) { + message = "A model test has failed"; + } + if (!bool) { + return globals.handleError(message); + } + }; + emit = function(name, context) { + if (element && $) { + return element.trigger($.Event(name, context)); + } + }; + newRoot = new ModelGroup(); + newRoot.recalculating = false; + newRoot.recalculateCycle = function() {}; + (function(root) { + var field, group, sandbox, validate; + field = newRoot.field.bind(newRoot); + group = newRoot.group.bind(newRoot); + root = newRoot.root; + validate = newRoot.validate; + if (typeof window === "undefined" || window === null) { + sandbox = { + field: field, + group: group, + root: root, + validate: validate, + data: data, + imports: imports, + test: test, + assert: assert, + Mustache: Mustache, + emit: emit, + _: _, + console: { + log: function() {}, + error: function() {} + }, + print: function() {} + }; + return vm.runInNewContext('"use strict";' + code, sandbox); + } else { + return eval('"use strict";' + code); + } + })(null); + newRoot.postBuild(); + globals.runtime = true; + newRoot.applyData(data); + newRoot.getChanges = exports.getChanges.bind(null, newRoot); + newRoot.setDirty(newRoot.id, 'multiple'); + newRoot.recalculateCycle = function() { + var results; + results = []; + while (!this.recalculating && this.dirty) { + this.recalculating = true; + this.recalculateRelativeProperties(); + results.push(this.recalculating = false); + } + return results; + }; + newRoot.recalculateCycle(); + newRoot.on('change:isValid', function() { + if (!isImport) { + return emit('validate', { + isValid: newRoot.isValid + }); + } + }); + newRoot.on('recalculate', function() { + if (!isImport) { + return emit('change'); + } + }); + newRoot.trigger('change:isValid'); + newRoot.trigger('recalculate'); + newRoot.styles = false; + return newRoot; +}; + +exports.fromCoffee = function(code, data, element, imports, isImport) { + return exports.fromCode(CoffeeScript.compile(code), data, element, imports, isImport); +}; + +exports.fromPackage = function(pkg, data, element) { + var buildModelWithRecursiveImports; + buildModelWithRecursiveImports = function(p, el, isImport) { + var buildImport, builtImports, f, form; + form = ((function() { + var i, len, ref, results; + ref = p.forms; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + f = ref[i]; + if (f.formid === p.formid) { + results.push(f); + } + } + return results; + })())[0]; + if (form == null) { + return; + } + builtImports = {}; + buildImport = function(impObj) { + return builtImports[impObj.namespace] = buildModelWithRecursiveImports({ + formid: impObj.importformid, + data: data, + forms: p.forms + }, element, true); + }; + if (form.imports) { + form.imports.forEach(buildImport); + } + return exports.fromCoffee(form.model, data, el, builtImports, isImport); + }; + if (typeof pkg.formid === 'string') { + pkg.formid = parseInt(pkg.formid); + } + data = _.extend(pkg.data || {}, data || {}); + return buildModelWithRecursiveImports(pkg, element, false); +}; + +exports.getChanges = function(modelAfter, beforeData) { + var after, before, changedPath, changedPaths, changedPathsUniqObject, changedPathsUnique, changes, i, internalPatch, j, key, len, len1, modelBefore, outputPatch, p, path, val; + modelBefore = modelAfter.cloneModel(); + modelBefore.applyData(beforeData, true); + internalPatch = jiff.diff(modelBefore.buildOutputData(void 0, true), modelAfter.buildOutputData(void 0, true), { + invertible: false + }); + outputPatch = jiff.diff(modelBefore.buildOutputData(), modelAfter.buildOutputData(), { + invertible: false + }); + changedPaths = (function() { + var i, len, results; + results = []; + for (i = 0, len = internalPatch.length; i < len; i++) { + p = internalPatch[i]; + results.push(p.path.replace(/\/[0-9]+$/, '')); + } + return results; + })(); + changedPathsUniqObject = {}; + for (i = 0, len = changedPaths.length; i < len; i++) { + val = changedPaths[i]; + changedPathsUniqObject[val] = val; + } + changedPathsUnique = (function() { + var results; + results = []; + for (key in changedPathsUniqObject) { + results.push(key); + } + return results; + })(); + changes = []; + for (j = 0, len1 = changedPathsUnique.length; j < len1; j++) { + changedPath = changedPathsUnique[j]; + path = changedPath.slice(1); + before = modelBefore.child(path); + after = modelAfter.child(path); + if (!_.isEqual(before != null ? before.value : void 0, after != null ? after.value : void 0)) { + changes.push({ + name: changedPath, + title: after.title, + before: before.buildOutputData(void 0, true), + after: after.buildOutputData(void 0, true) + }); + } + } + return { + changes: changes, + patch: outputPatch + }; +}; diff --git a/lib/formbuilder.js b/lib/formbuilder.js new file mode 100644 index 0000000..664035c --- /dev/null +++ b/lib/formbuilder.js @@ -0,0 +1,47 @@ +var Backbone, ModelBase, building, globals; + +if (typeof window !== "undefined" && window !== null) { + window.formbuilder = exports; +} + +Backbone = require('backbone'); + +ModelBase = require('./modelBase'); + +building = require('./building'); + +globals = require('./globals'); + +exports.fromCode = building.fromCode; + +exports.fromCoffee = building.fromCoffee; + +exports.fromPackage = building.fromPackage; + +exports.getChanges = building.getChanges; + +exports.mergeData = globals.mergeData; + +exports.applyData = function(modelObject, inData, clear, purgeDefaults) { + return modelObject.applyData(inData, clear, purgeDefaults); +}; + +exports.buildOutputData = function(model) { + return model.buildOutputData(); +}; + +Object.defineProperty(exports, 'modelTests', { + get: function() { + return building.modelTests; + } +}); + +Object.defineProperty(exports, 'handleError', { + get: function() { + return globals.handleError; + }, + set: function(f) { + return globals.handleError = f; + }, + enumerable: true +}); diff --git a/lib/globals.js b/lib/globals.js new file mode 100644 index 0000000..d301218 --- /dev/null +++ b/lib/globals.js @@ -0,0 +1,35 @@ +module.exports = { + runtime: false, + handleError: function(err) { + if (!(err instanceof Error)) { + err = new Error(err); + } + throw err; + }, + makeErrorMessage: function(model, propName, err) { + var nameStack, node, stack; + stack = []; + node = model; + while (node.name != null) { + stack.push(node.name); + node = node.parent; + } + stack.reverse(); + nameStack = stack.join('.'); + return "The '" + propName + "' function belonging to the field named '" + nameStack + "' threw an error with the message '" + err.message + "'"; + }, + mergeData: function(a, b) { + var key, value; + if ((b != null ? b.constructor : void 0) === Object) { + for (key in b) { + value = b[key]; + if ((a[key] != null) && a[key].constructor === Object && (value != null ? value.constructor : void 0) === Object) { + module.exports.mergeData(a[key], value); + } else { + a[key] = value; + } + } + } + return a; + } +}; diff --git a/lib/modelBase.js b/lib/modelBase.js new file mode 100644 index 0000000..1a0ab51 --- /dev/null +++ b/lib/modelBase.js @@ -0,0 +1,417 @@ + +/* + * Attributes common to groups and fields. + */ +var Backbone, ModelBase, Mustache, _, getBoolOrFunctionResult, globals, moment, newid, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +Backbone = require('backbone'); + +_ = require('underscore'); + +globals = require('./globals'); + +moment = require('moment'); + +Mustache = require('mustache'); + +newid = (function() { + var incId; + incId = 0; + return function() { + incId++; + return "fbid_" + incId; + }; +})(); + + +/* Some properties may be booleans or functions that return booleans + Use this function to determine final boolean value. + prop - the property to evaluate, which may be something primitive or a function + deflt - the value to return if the property is undefined + */ + +getBoolOrFunctionResult = function(prop, deflt) { + if (deflt == null) { + deflt = true; + } + if (typeof prop === 'function') { + return !!prop(); + } + if (prop === void 0) { + return deflt; + } + return !!prop; +}; + +module.exports = ModelBase = (function(superClass) { + extend(ModelBase, superClass); + + function ModelBase() { + return ModelBase.__super__.constructor.apply(this, arguments); + } + + ModelBase.prototype.modelClassName = 'ModelBase'; + + ModelBase.prototype.initialize = function() { + var fn, key, ref, val; + this.setDefault('visible', true); + this.set('isVisible', true); + this.setDefault('disabled', false); + this.set('isDisabled', false); + this.setDefault('onChangePropertiesHandlers', []); + this.set('id', newid()); + this.setDefault('parent', void 0); + this.setDefault('root', void 0); + this.setDefault('name', this.get('title')); + this.setDefault('title', this.get('name')); + ref = this.attributes; + fn = (function(_this) { + return function(key) { + return Object.defineProperty(_this, key, { + get: function() { + return this.get(key); + }, + set: function(newValue) { + if ((this.get(key)) !== newValue) { + return this.set(key, newValue); + } + } + }); + }; + })(this); + for (key in ref) { + val = ref[key]; + fn(key); + } + this.bindPropFunctions('visible'); + this.bindPropFunctions('disabled'); + this.makePropArray('onChangePropertiesHandlers'); + this.bindPropFunctions('onChangePropertiesHandlers'); + return this.on('change', function() { + var ch, changeFunc, i, len, ref1; + if (!globals.runtime) { + return; + } + ref1 = this.onChangePropertiesHandlers; + for (i = 0, len = ref1.length; i < len; i++) { + changeFunc = ref1[i]; + changeFunc(); + } + ch = this.changedAttributes(); + if (ch === false) { + ch = 'multiple'; + } + this.root.setDirty(this.id, ch); + return this.root.recalculateCycle(); + }); + }; + + ModelBase.prototype.postBuild = function() {}; + + ModelBase.prototype.setDefault = function(field, val) { + if (this.get(field) == null) { + return this.set(field, val); + } + }; + + ModelBase.prototype.text = function(message) { + return this.field(message, { + type: 'info' + }); + }; + + ModelBase.prototype.bindPropFunction = function(propName, func) { + var model; + model = this; + return function() { + var err, message; + try { + if (this instanceof ModelBase) { + model = this; + } + return func.apply(model, arguments); + } catch (error) { + err = error; + message = globals.makeErrorMessage(model, propName, err); + return globals.handleError(message); + } + }; + }; + + ModelBase.prototype.bindPropFunctions = function(propName) { + var i, index, ref, results; + if (Array.isArray(this[propName])) { + results = []; + for (index = i = 0, ref = this[propName].length; 0 <= ref ? i < ref : i > ref; index = 0 <= ref ? ++i : --i) { + results.push(this[propName][index] = this.bindPropFunction(propName, this[propName][index])); + } + return results; + } else if (typeof this[propName] === 'function') { + return this.set(propName, this.bindPropFunction(propName, this[propName]), { + silent: true + }); + } + }; + + ModelBase.prototype.makePropArray = function(propName) { + if (!Array.isArray(this.get(propName))) { + return this.set(propName, [this.get(propName)]); + } + }; + + ModelBase.prototype.buildParamObject = function(params, paramPositions) { + var i, key, len, param, paramIndex, paramObject, ref, val; + paramObject = {}; + paramIndex = 0; + for (i = 0, len = params.length; i < len; i++) { + param = params[i]; + if (((ref = typeof param) === 'string' || ref === 'number' || ref === 'boolean') || Array.isArray(param)) { + paramObject[paramPositions[paramIndex++]] = param; + } else if (Object.prototype.toString.call(param) === '[object Object]') { + for (key in param) { + val = param[key]; + paramObject[key] = val; + } + } + } + paramObject.parent = this; + paramObject.root = this.root; + return paramObject; + }; + + ModelBase.prototype.dirty = ''; + + ModelBase.prototype.setDirty = function(id, whatChanged) { + var ch, drt, keys; + ch = typeof whatChanged === 'string' ? whatChanged : (keys = Object.keys(whatChanged), keys.length === 1 ? id + ":" + keys[0] : 'multiple'); + drt = this.dirty === ch || this.dirty === '' ? ch : "multiple"; + return this.dirty = drt; + }; + + ModelBase.prototype.setClean = function() { + return this.dirty = ''; + }; + + ModelBase.prototype.shouldCallTriggerFunctionFor = function(dirty, attrName) { + return dirty && dirty !== (this.id + ":" + attrName); + }; + + ModelBase.prototype.recalculateRelativeProperties = function() { + var dirty; + dirty = this.dirty; + this.setClean(); + if (this.shouldCallTriggerFunctionFor(dirty, 'isVisible')) { + this.isVisible = getBoolOrFunctionResult(this.visible); + } + if (this.shouldCallTriggerFunctionFor(dirty, 'isDisabled')) { + this.isDisabled = getBoolOrFunctionResult(this.disabled, false); + } + return this.trigger('recalculate'); + }; + + ModelBase.prototype.onChangeProperties = function(f, trigger) { + if (trigger == null) { + trigger = true; + } + this.onChangePropertiesHandlers.push(this.bindPropFunction('onChangeProperties', f)); + if (trigger) { + this.trigger('change'); + } + return this; + }; + + ModelBase.prototype.validate = { + required: function(value) { + if (value == null) { + value = this.value || ''; + } + if (((function() { + switch (typeof value) { + case 'number': + case 'boolean': + return false; + case 'string': + return value.length === 0; + case 'object': + return Object.keys(value).length === 0; + default: + return true; + } + })())) { + return "This field is required"; + } + }, + minLength: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length < n) { + return "Must be at least " + n + " characters long"; + } + }; + }, + maxLength: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length > n) { + return "Can be at most " + n + " characters long"; + } + }; + }, + number: function(value) { + if (value == null) { + value = this.value || ''; + } + if (isNaN(+value)) { + return "Must be an integer or decimal number. (ex. 42 or 1.618)"; + } + }, + date: function(value, format) { + if (value == null) { + value = this.value || ''; + } + if (format == null) { + format = this.format; + } + if (value === '') { + return; + } + if (!moment(value, format, true).isValid()) { + return "Not a valid date or does not match the format " + format; + } + }, + email: function(value) { + if (value == null) { + value = this.value || ''; + } + if (!value.match(/^[a-z0-9!\#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!\#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/)) { + return "Must be a valid email"; + } + }, + url: function(value) { + if (value == null) { + value = this.value || ''; + } + if (!value.match(/^(([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)\#?(?:[\w]*))?$/)) { + return "Must be a URL"; + } + }, + dollars: function(value) { + if (value == null) { + value = this.value || ''; + } + if (!value.match(/^\$(\d+\.\d\d|\d+)$/)) { + return "Must be a dollar amount (ex. $3.99)"; + } + }, + minSelections: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length < n) { + return "Please select at least " + n + " options"; + } + }; + }, + maxSelections: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length > n) { + return "Please select at most " + n + " options"; + } + }; + }, + selectedIsVisible: function(field) { + var i, len, opt, ref; + if (field == null) { + field = this; + } + ref = field.options; + for (i = 0, len = ref.length; i < len; i++) { + opt = ref[i]; + if (opt.selected && !opt.isVisible) { + return "A selected option is not currently available. Please make a new choice from available options."; + } + } + }, + template: function() { + var e, template; + if (!this.template) { + return; + } + if (typeof this.template === 'object') { + template = this.template.value; + } else { + template = this.parent.child(this.template).value; + } + try { + Mustache.render(template, this.root.data); + } catch (error) { + e = error; + return "Template field does not contain valid Mustache"; + } + } + }; + + ModelBase.prototype.cloneModel = function(newRoot, constructor, excludeAttributes) { + var childClone, filteredAttributes, i, key, len, modelObj, myClone, newVal, ref, ref1, val; + if (newRoot == null) { + newRoot = this.root; + } + if (constructor == null) { + constructor = this.constructor; + } + if (excludeAttributes == null) { + excludeAttributes = []; + } + filteredAttributes = {}; + ref = this.attributes; + for (key in ref) { + val = ref[key]; + if (indexOf.call(excludeAttributes, key) < 0) { + filteredAttributes[key] = val; + } + } + myClone = new constructor(filteredAttributes); + ref1 = myClone.attributes; + for (key in ref1) { + val = ref1[key]; + if (key === 'root') { + myClone.set(key, newRoot); + } else if (val instanceof ModelBase && (key !== 'root' && key !== 'parent')) { + myClone.set(key, val.cloneModel(newRoot)); + } else if (Array.isArray(val)) { + newVal = []; + if (val[0] instanceof ModelBase && key !== 'value') { + for (i = 0, len = val.length; i < len; i++) { + modelObj = val[i]; + childClone = modelObj.cloneModel(newRoot); + if (childClone.parent === this) { + childClone.parent = myClone; + if (key === 'options' && childClone.selected) { + myClone.addOptionValue(childClone.value); + } + } + newVal.push(childClone); + } + } else { + newVal = _.clone(val); + } + myClone.set(key, newVal); + } + } + return myClone; + }; + + return ModelBase; + +})(Backbone.Model); diff --git a/lib/modelField.js b/lib/modelField.js new file mode 100644 index 0000000..c482d77 --- /dev/null +++ b/lib/modelField.js @@ -0,0 +1,528 @@ +var ModelBase, ModelField, ModelOption, Mustache, globals, jiff, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelBase = require('./modelBase'); + +ModelOption = require('./modelOption'); + +globals = require('./globals'); + +Mustache = require('mustache'); + +jiff = require('jiff'); + + +/* + A ModelField represents a model object that render as a DOM field + NOTE: The following field types are subclasses: image, tree, date + */ + +module.exports = ModelField = (function(superClass) { + extend(ModelField, superClass); + + function ModelField() { + return ModelField.__super__.constructor.apply(this, arguments); + } + + ModelField.prototype.modelClassName = 'ModelField'; + + ModelField.prototype.initialize = function() { + var ref2, ref3; + this.setDefault('type', 'text'); + this.setDefault('options', []); + this.setDefault('value', (function() { + switch (this.get('type')) { + case 'multiselect': + return []; + case 'bool': + return false; + case 'info': + case 'button': + return void 0; + default: + return (this.get('defaultValue')) || ''; + } + }).call(this)); + this.setDefault('defaultValue', this.get('value')); + this.set('isValid', true); + this.setDefault('validators', []); + this.setDefault('onChangeHandlers', []); + this.setDefault('dynamicValue', null); + this.setDefault('template', null); + this.setDefault('autocomplete', null); + this.setDefault('beforeInput', function(val) { + return val; + }); + this.setDefault('beforeOutput', function(val) { + return val; + }); + ModelField.__super__.initialize.apply(this, arguments); + if ((ref2 = this.type) !== 'info' && ref2 !== 'text' && ref2 !== 'url' && ref2 !== 'email' && ref2 !== 'tel' && ref2 !== 'time' && ref2 !== 'date' && ref2 !== 'textarea' && ref2 !== 'bool' && ref2 !== 'tree' && ref2 !== 'color' && ref2 !== 'select' && ref2 !== 'multiselect' && ref2 !== 'image' && ref2 !== 'button' && ref2 !== 'number') { + return globals.handleError("Bad field type: " + this.type); + } + this.bindPropFunctions('dynamicValue'); + while ((Array.isArray(this.value)) && (this.type !== 'multiselect') && (this.type !== 'tree') && (this.type !== 'button')) { + this.value = this.value[0]; + } + if (typeof this.value === 'string' && (this.type === 'multiselect')) { + this.value = [this.value]; + } + if (this.type === 'bool' && typeof this.value !== 'bool') { + this.value = !!this.value; + } + this.makePropArray('validators'); + this.bindPropFunctions('validators'); + this.makePropArray('onChangeHandlers'); + this.bindPropFunctions('onChangeHandlers'); + if (this.optionsFrom != null) { + this.ensureSelectType(); + if ((this.optionsFrom.url == null) || (this.optionsFrom.parseResults == null)) { + return globals.handleError('When fetching options remotely, both url and parseResults properties are required'); + } + if (typeof ((ref3 = this.optionsFrom) != null ? ref3.url : void 0) === 'function') { + this.optionsFrom.url = this.bindPropFunction('optionsFrom.url', this.optionsFrom.url); + } + if (typeof this.optionsFrom.parseResults !== 'function') { + return globals.handleError('optionsFrom.parseResults must be a function'); + } + this.optionsFrom.parseResults = this.bindPropFunction('optionsFrom.parseResults', this.optionsFrom.parseResults); + } + this.updateOptionsSelected(); + this.on('change:value', function() { + var changeFunc, j, len1, ref4; + ref4 = this.onChangeHandlers; + for (j = 0, len1 = ref4.length; j < len1; j++) { + changeFunc = ref4[j]; + changeFunc(); + } + return this.updateOptionsSelected(); + }); + return this.on('change:type', function() { + if (this.type === 'multiselect') { + this.value = this.value.length > 0 ? [this.value] : []; + } else if (this.previousAttributes().type === 'multiselect') { + this.value = this.value.length > 0 ? this.value[0] : ''; + } + if (this.options.length > 0 && !this.isSelectType()) { + return this.type = 'select'; + } + }); + }; + + ModelField.prototype.getOptionsFrom = function() { + var ref2, url; + if (this.optionsFrom == null) { + return; + } + url = typeof this.optionsFrom.url === 'function' ? this.optionsFrom.url() : this.optionsFrom.url; + if (this.prevUrl === url) { + return; + } + this.prevUrl = url; + return typeof window !== "undefined" && window !== null ? (ref2 = window.formbuilderproxy) != null ? ref2.getFromProxy({ + url: url, + method: this.optionsFrom.method || 'get', + headerKey: this.optionsFrom.headerKey + }, (function(_this) { + return function(error, data) { + var j, len1, mappedResults, opt, results1; + if (error) { + return globals.handleError(globals.makeErrorMessage(_this, 'optionsFrom', error)); + } + mappedResults = _this.optionsFrom.parseResults(data); + if (!Array.isArray(mappedResults)) { + return globals.handleError('results of parseResults must be an array of option parameters'); + } + _this.options = []; + results1 = []; + for (j = 0, len1 = mappedResults.length; j < len1; j++) { + opt = mappedResults[j]; + results1.push(_this.option(opt)); + } + return results1; + }; + })(this)) : void 0 : void 0; + }; + + ModelField.prototype.validityMessage = void 0; + + ModelField.prototype.field = function() { + var obj, ref2; + obj = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return (ref2 = this.parent).field.apply(ref2, obj); + }; + + ModelField.prototype.group = function() { + var obj, ref2; + obj = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return (ref2 = this.parent).group.apply(ref2, obj); + }; + + ModelField.prototype.option = function() { + var newOption, nextOpts, opt, optionObject, optionParams; + optionParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + optionObject = this.buildParamObject(optionParams, ['title', 'value', 'selected', 'bidAdj', 'bidAdjFlag']); + this.ensureSelectType(); + nextOpts = (function() { + var j, len1, ref2, results1; + ref2 = this.options; + results1 = []; + for (j = 0, len1 = ref2.length; j < len1; j++) { + opt = ref2[j]; + if (opt.title !== optionObject.title) { + results1.push(opt); + } + } + return results1; + }).call(this); + newOption = new ModelOption(optionObject); + nextOpts.push(newOption); + this.options = nextOpts; + if (newOption.selected) { + this.addOptionValue(newOption.value); + } + return this; + }; + + ModelField.prototype.postBuild = function() { + this.defaultValue = this.value; + return this.updateOptionsSelected(); + }; + + ModelField.prototype.updateOptionsSelected = function() { + var bid, i, len, opt, ref, ref1, results; + ref = this.options; + results = []; + i = 0; + len = ref.length; + while (i < len) { + opt = ref[i]; + if ((ref1 = this.type) === 'multiselect' || ref1 === 'tree') { + bid = this.hasValue(opt.value); + if (bid.bidValue) { + opt.bidAdj = bid.bidValue.lastIndexOf('/') !== -1 ? bid.bidValue.split("/").pop() : this.bidAdj; + } + results.push(opt.selected = bid.selectStatus); + } else { + results.push(opt.selected = this.hasValue(opt.value)); + } + i++; + } + return results; + }; + + ModelField.prototype.isSelectType = function() { + var ref2; + return (ref2 = this.type) === 'select' || ref2 === 'multiselect' || ref2 === 'image' || ref2 === 'tree'; + }; + + ModelField.prototype.ensureSelectType = function() { + if (!this.isSelectType()) { + return this.type = 'select'; + } + }; + + ModelField.prototype.child = function(value) { + var j, len1, o, ref2; + if (Array.isArray(value)) { + value = value.shift(); + } + ref2 = this.options; + for (j = 0, len1 = ref2.length; j < len1; j++) { + o = ref2[j]; + if (o.value === value) { + return o; + } + } + }; + + ModelField.prototype.validator = function(func) { + this.validators.push(this.bindPropFunction('validator', func)); + this.trigger('change'); + return this; + }; + + ModelField.prototype.onChange = function(f) { + this.onChangeHandlers.push(this.bindPropFunction('onChange', f)); + this.trigger('change'); + return this; + }; + + ModelField.prototype.setDirty = function(id, whatChanged) { + var j, len1, opt, ref2; + ref2 = this.options; + for (j = 0, len1 = ref2.length; j < len1; j++) { + opt = ref2[j]; + opt.setDirty(id, whatChanged); + } + return ModelField.__super__.setDirty.call(this, id, whatChanged); + }; + + ModelField.prototype.setClean = function(all) { + var j, len1, opt, ref2, results1; + ModelField.__super__.setClean.apply(this, arguments); + if (all) { + ref2 = this.options; + results1 = []; + for (j = 0, len1 = ref2.length; j < len1; j++) { + opt = ref2[j]; + results1.push(opt.setClean(all)); + } + return results1; + } + }; + + ModelField.prototype.recalculateRelativeProperties = function() { + var dirty, j, k, len1, len2, opt, ref2, ref3, results1, validator, validityMessage, value; + dirty = this.dirty; + ModelField.__super__.recalculateRelativeProperties.apply(this, arguments); + if (this.shouldCallTriggerFunctionFor(dirty, 'isValid')) { + validityMessage = void 0; + if (this.template) { + validityMessage || (validityMessage = this.validate.template.call(this)); + } + if (this.type === 'number') { + validityMessage || (validityMessage = this.validate.number.call(this)); + } + if (!validityMessage) { + ref2 = this.validators; + for (j = 0, len1 = ref2.length; j < len1; j++) { + validator = ref2[j]; + if (typeof validator === 'function') { + validityMessage = validator.call(this); + } + if (typeof validityMessage === 'function') { + return globals.handleError("A validator on field '" + this.name + "' returned a function"); + } + if (validityMessage) { + break; + } + } + } + this.validityMessage = validityMessage; + this.set({ + isValid: validityMessage == null + }); + } + if (this.template && this.shouldCallTriggerFunctionFor(dirty, 'value')) { + this.renderTemplate(); + } else { + if (typeof this.dynamicValue === 'function' && this.shouldCallTriggerFunctionFor(dirty, 'value')) { + value = this.dynamicValue(); + if (typeof value === 'function') { + return globals.handleError("dynamicValue on field '" + this.name + "' returned a function"); + } + this.set('value', value); + } + } + if (this.shouldCallTriggerFunctionFor(dirty, 'options')) { + this.getOptionsFrom(); + } + ref3 = this.options; + results1 = []; + for (k = 0, len2 = ref3.length; k < len2; k++) { + opt = ref3[k]; + results1.push(opt.recalculateRelativeProperties()); + } + return results1; + }; + + ModelField.prototype.addOptionValue = function(val, bidAdj) { + var findMatch, ref; + findMatch = void 0; + ref = void 0; + if ((ref = this.type) === 'multiselect' || ref === 'tree') { + if (!Array.isArray(this.value)) { + this.value = [this.value]; + } + findMatch = this.value.findIndex(function(e) { + if (typeof e === 'string') { + return e.search(val) !== -1; + } else { + return e === val; + } + }); + if (findMatch !== -1) { + if (bidAdj) { + return this.value[findMatch] = val + '/' + bidAdj; + } + } else { + if (bidAdj) { + return this.value.push(val + '/' + bidAdj); + } else { + return this.value.push(val); + } + } + } else { + return this.value = val; + } + }; + + ModelField.prototype.removeOptionValue = function(val) { + var ref; + ref = void 0; + if ((ref = this.type) === 'multiselect' || ref === 'tree') { + return this.value = this.value.filter(function(e) { + if (typeof e === 'string') { + return e.search(val) === -1; + } else { + return e !== val; + } + }); + } else if (this.value === val) { + return this.value = ''; + } + }; + + ModelField.prototype.hasValue = function(val) { + var findMatch, ref; + findMatch = void 0; + ref = void 0; + if ((ref = this.type) === 'multiselect' || ref === 'tree') { + findMatch = this.value.findIndex(function(e) { + if (typeof e === 'string') { + return e.search(val) !== -1; + } else { + return e === val; + } + }); + if (findMatch !== -1) { + return { + 'bidValue': this.value[findMatch], + 'selectStatus': true + }; + } else { + return { + 'selectStatus': false + }; + } + } else { + return val === this.value; + } + }; + + ModelField.prototype.buildOutputData = function(_, skipBeforeOutput) { + var out, value; + value = (function() { + switch (this.type) { + case 'number': + out = +this.value; + if (isNaN(out)) { + return null; + } else { + return out; + } + break; + case 'info': + case 'button': + return void 0; + case 'bool': + return !!this.value; + default: + return this.value; + } + }).call(this); + if (skipBeforeOutput) { + return value; + } else { + return this.beforeOutput(value); + } + }; + + ModelField.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (purgeDefaults) { + return this.value = (function() { + switch (this.type) { + case 'multiselect': + return []; + case 'bool': + return false; + default: + return ''; + } + }).call(this); + } else { + return this.value = this.defaultValue; + } + }; + + ModelField.prototype.ensureValueInOptions = function() { + var existingOption, j, k, l, len1, len2, len3, o, ref2, ref3, ref4, results1, v; + if (!this.isSelectType()) { + return; + } + if (typeof this.value === 'string') { + ref2 = this.options; + for (j = 0, len1 = ref2.length; j < len1; j++) { + o = ref2[j]; + if (o.value === this.value) { + existingOption = o; + } + } + if (!existingOption) { + return this.option(this.value, { + selected: true + }); + } + } else if (Array.isArray(this.value)) { + ref3 = this.value; + results1 = []; + for (k = 0, len2 = ref3.length; k < len2; k++) { + v = ref3[k]; + existingOption = null; + ref4 = this.options; + for (l = 0, len3 = ref4.length; l < len3; l++) { + o = ref4[l]; + if (o.value === v) { + existingOption = o; + } + } + if (!existingOption) { + results1.push(this.option(v, { + selected: true + })); + } else { + results1.push(void 0); + } + } + return results1; + } + }; + + ModelField.prototype.applyData = function(inData, clear, purgeDefaults) { + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (clear) { + this.clear(purgeDefaults); + } + if (inData != null) { + return this.value = this.beforeInput(jiff.clone(inData)); + } + }; + + ModelField.prototype.renderTemplate = function() { + var template; + if (typeof this.template === 'object') { + template = this.template.value; + } else { + template = this.parent.child(this.template).value; + } + try { + return this.value = Mustache.render(template, this.root.data); + } catch (error1) { + + } + }; + + return ModelField; + +})(ModelBase); diff --git a/lib/modelFieldDate.js b/lib/modelFieldDate.js new file mode 100644 index 0000000..c8f3aa7 --- /dev/null +++ b/lib/modelFieldDate.js @@ -0,0 +1,44 @@ +var ModelField, ModelFieldDate, moment, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +ModelField = require('./modelField'); + +moment = require('moment'); + +module.exports = ModelFieldDate = (function(superClass) { + extend(ModelFieldDate, superClass); + + function ModelFieldDate() { + return ModelFieldDate.__super__.constructor.apply(this, arguments); + } + + ModelFieldDate.prototype.initialize = function() { + this.setDefault('format', 'M/D/YYYY'); + ModelFieldDate.__super__.initialize.apply(this, arguments); + return this.validator(this.validate.date); + }; + + ModelFieldDate.prototype.dateToString = function(date, format) { + if (date == null) { + date = this.value; + } + if (format == null) { + format = this.format; + } + return moment(date).format(format); + }; + + ModelFieldDate.prototype.stringToDate = function(str, format) { + if (str == null) { + str = this.value; + } + if (format == null) { + format = this.format; + } + return moment(str, format, true).toDate(); + }; + + return ModelFieldDate; + +})(ModelField); diff --git a/lib/modelFieldImage.js b/lib/modelFieldImage.js new file mode 100644 index 0000000..89f9a7f --- /dev/null +++ b/lib/modelFieldImage.js @@ -0,0 +1,101 @@ +var ModelField, ModelFieldImage, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelField = require('./modelField'); + +module.exports = ModelFieldImage = (function(superClass) { + extend(ModelFieldImage, superClass); + + function ModelFieldImage() { + return ModelFieldImage.__super__.constructor.apply(this, arguments); + } + + ModelFieldImage.prototype.initialize = function() { + this.setDefault('value', {}); + this.setDefault('allowUpload', false); + this.setDefault('imagesPerPage', 4); + this.setDefault('minWidth', 0); + this.setDefault('maxWidth', 0); + this.setDefault('minHeight', 0); + this.setDefault('maxHeight', 0); + this.setDefault('minSize', 0); + this.setDefault('maxSize', 0); + this.set('optionsChanged', false); + return ModelFieldImage.__super__.initialize.apply(this, arguments); + }; + + ModelFieldImage.prototype.option = function() { + var optionObject, optionParams; + optionParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + optionObject = this.buildParamObject(optionParams, ['fileID', 'fileUrl', 'thumbnailUrl']); + if (optionObject.fileID == null) { + optionObject.fileID = optionObject.fileUrl; + } + if (optionObject.thumbnailUrl == null) { + optionObject.thumbnailUrl = optionObject.fileUrl; + } + optionObject.value = { + fileID: optionObject.fileID, + fileUrl: optionObject.fileUrl, + thumbnailUrl: optionObject.thumbnailUrl + }; + if (optionObject.title == null) { + optionObject.title = optionObject.fileID; + } + this.optionsChanged = true; + return ModelFieldImage.__super__.option.call(this, optionObject); + }; + + ModelFieldImage.prototype.child = function(fileID) { + var i, len, o, ref; + if (Array.isArray(fileID)) { + fileID = fileID.shift(); + } + if (typeof fileID === 'object') { + fileID = fileID.fileID; + } + ref = this.options; + for (i = 0, len = ref.length; i < len; i++) { + o = ref[i]; + if (o.fileID === fileID) { + return o; + } + } + }; + + ModelFieldImage.prototype.removeOptionValue = function(val) { + if (this.value.fileID === val.fileID) { + return this.value = {}; + } + }; + + ModelFieldImage.prototype.hasValue = function(val) { + return val.fileID === this.value.fileID && val.thumbnailUrl === this.value.thumbnailUrl && val.fileUrl === this.value.fileUrl; + }; + + ModelFieldImage.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + return this.value = purgeDefaults ? {} : this.defaultValue; + }; + + ModelFieldImage.prototype.ensureValueInOptions = function() { + var existingOption, i, len, o, ref; + ref = this.options; + for (i = 0, len = ref.length; i < len; i++) { + o = ref[i]; + if (o.attributes.fileID === this.value.fileID) { + existingOption = o; + } + } + if (!existingOption) { + return this.option(this.value); + } + }; + + return ModelFieldImage; + +})(ModelField); diff --git a/lib/modelFieldTree.js b/lib/modelFieldTree.js new file mode 100644 index 0000000..68b1bee --- /dev/null +++ b/lib/modelFieldTree.js @@ -0,0 +1,45 @@ +var ModelField, ModelFieldTree, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelField = require('./modelField'); + +module.exports = ModelFieldTree = (function(superClass) { + extend(ModelFieldTree, superClass); + + function ModelFieldTree() { + return ModelFieldTree.__super__.constructor.apply(this, arguments); + } + + ModelFieldTree.prototype.initialize = function() { + this.setDefault('value', []); + return ModelFieldTree.__super__.initialize.apply(this, arguments); + }; + + ModelFieldTree.prototype.option = function() { + var optionObject, optionParams; + optionParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + optionObject = this.buildParamObject(optionParams, ['path', 'value', 'selected', 'bidAdj', 'bidAdjFlag']); + if (optionObject.value == null) { + optionObject.value = optionObject.id; + } + if (optionObject.value === null && Array.isArray(optionObject.path)) { + if (optionObject.value == null) { + optionObject.value = optionObject.path.join(' > '); + } + optionObject.title = optionObject.path.join('>'); + } + return ModelFieldTree.__super__.option.call(this, optionObject); + }; + + ModelFieldTree.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + return this.value = purgeDefaults ? [] : this.defaultValue; + }; + + return ModelFieldTree; + +})(ModelField); diff --git a/lib/modelGroup.js b/lib/modelGroup.js new file mode 100644 index 0000000..ea30836 --- /dev/null +++ b/lib/modelGroup.js @@ -0,0 +1,409 @@ +var ModelBase, ModelField, ModelFieldDate, ModelFieldImage, ModelFieldTree, ModelGroup, RepeatingModelGroup, globals, jiff, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelBase = require('./modelBase'); + +ModelFieldImage = require('./modelFieldImage'); + +ModelFieldTree = require('./modelFieldTree'); + +ModelFieldDate = require('./modelFieldDate'); + +ModelField = require('./modelField'); + +globals = require('./globals'); + +jiff = require('jiff'); + + +/* + A ModelGroup is a model object that can contain any number of other groups and fields + */ + +module.exports = ModelGroup = (function(superClass) { + extend(ModelGroup, superClass); + + function ModelGroup() { + return ModelGroup.__super__.constructor.apply(this, arguments); + } + + ModelGroup.prototype.modelClassName = 'ModelGroup'; + + ModelGroup.prototype.initialize = function() { + this.setDefault('children', []); + this.setDefault('root', this); + this.set('isValid', true); + this.set('data', null); + this.setDefault('beforeInput', function(val) { + return val; + }); + this.setDefault('beforeOutput', function(val) { + return val; + }); + return ModelGroup.__super__.initialize.apply(this, arguments); + }; + + ModelGroup.prototype.postBuild = function() { + var child, i, len, ref, results; + ref = this.children; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + results.push(child.postBuild()); + } + return results; + }; + + ModelGroup.prototype.field = function() { + var fieldObject, fieldParams, fld; + fieldParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + fieldObject = this.buildParamObject(fieldParams, ['title', 'name', 'type', 'value']); + if (fieldObject.disabled == null) { + fieldObject.disabled = this.disabled; + } + fld = (function() { + switch (fieldObject.type) { + case 'image': + return new ModelFieldImage(fieldObject); + case 'tree': + return new ModelFieldTree(fieldObject); + case 'date': + return new ModelFieldDate(fieldObject); + default: + return new ModelField(fieldObject); + } + })(); + this.children.push(fld); + this.trigger('change'); + return fld; + }; + + ModelGroup.prototype.group = function() { + var groupObject, groupParams, grp, key, ref, val; + groupParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + grp = {}; + if (((ref = groupParams[0].constructor) != null ? ref.name : void 0) === 'ModelGroup') { + grp = groupParams[0].cloneModel(this.root); + groupParams.shift(); + groupObject = this.buildParamObject(groupParams, ['title', 'name', 'description']); + if (groupObject.name == null) { + groupObject.name = groupObject.title; + } + if (groupObject.title == null) { + groupObject.title = groupObject.name; + } + for (key in groupObject) { + val = groupObject[key]; + grp.set(key, val); + } + } else { + groupObject = this.buildParamObject(groupParams, ['title', 'name', 'description']); + if (groupObject.disabled == null) { + groupObject.disabled = this.disabled; + } + if (groupObject.repeating) { + grp = new RepeatingModelGroup(groupObject); + } else { + grp = new ModelGroup(groupObject); + } + } + this.children.push(grp); + this.trigger('change'); + return grp; + }; + + ModelGroup.prototype.child = function(path) { + var c, child, i, len, name, ref; + if (!(Array.isArray(path))) { + path = path.split(/[.\/]/); + } + name = path.shift(); + ref = this.children; + for (i = 0, len = ref.length; i < len; i++) { + c = ref[i]; + if (c.name === name) { + child = c; + } + } + if (path.length === 0) { + return child; + } else { + return child.child(path); + } + }; + + ModelGroup.prototype.setDirty = function(id, whatChanged) { + var child, i, len, ref; + ref = this.children; + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + child.setDirty(id, whatChanged); + } + return ModelGroup.__super__.setDirty.call(this, id, whatChanged); + }; + + ModelGroup.prototype.setClean = function(all) { + var child, i, len, ref, results; + ModelGroup.__super__.setClean.apply(this, arguments); + if (all) { + ref = this.children; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + results.push(child.setClean(all)); + } + return results; + } + }; + + ModelGroup.prototype.recalculateRelativeProperties = function(collection) { + var child, dirty, i, len, newValid; + if (collection == null) { + collection = this.children; + } + dirty = this.dirty; + ModelGroup.__super__.recalculateRelativeProperties.apply(this, arguments); + newValid = true; + for (i = 0, len = collection.length; i < len; i++) { + child = collection[i]; + child.recalculateRelativeProperties(); + newValid && (newValid = child.isValid); + } + return this.isValid = newValid; + }; + + ModelGroup.prototype.buildOutputData = function(group, skipBeforeOutput) { + var obj; + if (group == null) { + group = this; + } + obj = {}; + group.children.forEach(function(child) { + var childData; + childData = child.buildOutputData(void 0, skipBeforeOutput); + if (childData !== void 0) { + return obj[child.name] = childData; + } + }); + if (skipBeforeOutput) { + return obj; + } else { + return group.beforeOutput(obj); + } + }; + + ModelGroup.prototype.buildOutputDataString = function() { + return JSON.stringify(this.buildOutputData()); + }; + + ModelGroup.prototype.clear = function(purgeDefaults) { + var child, i, j, key, len, len1, ref, ref1, results; + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (this.data) { + ref = Object.keys(this.data); + for (i = 0, len = ref.length; i < len; i++) { + key = ref[i]; + delete this.data[key]; + } + } + ref1 = this.children; + results = []; + for (j = 0, len1 = ref1.length; j < len1; j++) { + child = ref1[j]; + results.push(child.clear(purgeDefaults)); + } + return results; + }; + + ModelGroup.prototype.applyData = function(inData, clear, purgeDefaults) { + var finalInData, key, ref, results, value; + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (clear) { + this.clear(purgeDefaults); + } + finalInData = this.beforeInput(jiff.clone(inData)); + + /* + This section preserves a link to the initially applied data object and merges subsequent applies on top + of it in-place. This is necessary for two reasons. + First, the scope of the running model code also references the applied data through the 'data' variable. + Every applied data must be available even though the runtime is not re-evaluated each time. + Second, templated fields use this data as the input to their Mustache evaluation. See @renderTemplate() + */ + if (this.data) { + globals.mergeData(this.data, inData); + this.trigger('change'); + } else { + this.data = inData; + } + results = []; + for (key in finalInData) { + value = finalInData[key]; + results.push((ref = this.child(key)) != null ? ref.applyData(value) : void 0); + } + return results; + }; + + return ModelGroup; + +})(ModelBase); + + +/* + Encapsulates a group of form objects that can be added or removed to the form together multiple times + */ + +RepeatingModelGroup = (function(superClass) { + extend(RepeatingModelGroup, superClass); + + function RepeatingModelGroup() { + return RepeatingModelGroup.__super__.constructor.apply(this, arguments); + } + + RepeatingModelGroup.prototype.modelClassName = 'RepeatingModelGroup'; + + RepeatingModelGroup.prototype.initialize = function() { + this.setDefault('defaultValue', this.get('value') || []); + this.set('value', []); + return RepeatingModelGroup.__super__.initialize.apply(this, arguments); + }; + + RepeatingModelGroup.prototype.postBuild = function() { + var c, i, len, ref; + ref = this.children; + for (i = 0, len = ref.length; i < len; i++) { + c = ref[i]; + c.postBuild(); + } + return this.clear(); + }; + + RepeatingModelGroup.prototype.setDirty = function(id, whatChanged) { + var i, len, ref, val; + ref = this.value; + for (i = 0, len = ref.length; i < len; i++) { + val = ref[i]; + val.setDirty(id, whatChanged); + } + return RepeatingModelGroup.__super__.setDirty.call(this, id, whatChanged); + }; + + RepeatingModelGroup.prototype.setClean = function(all) { + var i, len, ref, results, val; + RepeatingModelGroup.__super__.setClean.apply(this, arguments); + if (all) { + ref = this.value; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + val = ref[i]; + results.push(val.setClean(all)); + } + return results; + } + }; + + RepeatingModelGroup.prototype.recalculateRelativeProperties = function() { + return RepeatingModelGroup.__super__.recalculateRelativeProperties.call(this, this.value); + }; + + RepeatingModelGroup.prototype.buildOutputData = function(_, skipBeforeOutput) { + var tempOut; + tempOut = this.value.map(function(instance) { + return RepeatingModelGroup.__super__.buildOutputData.call(this, instance); + }); + if (skipBeforeOutput) { + return tempOut; + } else { + return this.beforeOutput(tempOut); + } + }; + + RepeatingModelGroup.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + this.value = []; + if (!purgeDefaults) { + if (this.defaultValue) { + return this.addEachSimpleObject(this.defaultValue); + } + } + }; + + RepeatingModelGroup.prototype.applyData = function(inData, clear, purgeDefaults) { + var finalInData; + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + finalInData = this.beforeInput(jiff.clone(inData)); + if (finalInData) { + this.value = []; + } else { + if (clear) { + this.clear(purgeDefaults); + } + } + return this.addEachSimpleObject(finalInData, clear, purgeDefaults); + }; + + RepeatingModelGroup.prototype.addEachSimpleObject = function(o, clear, purgeDefaults) { + var added, i, key, len, obj, results, value; + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + results = []; + for (i = 0, len = o.length; i < len; i++) { + obj = o[i]; + added = this.add(); + results.push((function() { + var ref, results1; + results1 = []; + for (key in obj) { + value = obj[key]; + results1.push((ref = added.child(key)) != null ? ref.applyData(value, clear, purgeDefaults) : void 0); + } + return results1; + })()); + } + return results; + }; + + RepeatingModelGroup.prototype.cloneModel = function(root, constructor) { + var clone, excludeAttributes; + excludeAttributes = (constructor != null ? constructor.name : void 0) === 'ModelGroup' ? ['value', 'beforeInput', 'beforeOutput', 'description'] : []; + clone = RepeatingModelGroup.__super__.cloneModel.call(this, root, constructor, excludeAttributes); + clone.title = ''; + return clone; + }; + + RepeatingModelGroup.prototype.add = function() { + var clone; + clone = this.cloneModel(this.root, ModelGroup); + this.value.push(clone); + this.trigger('change'); + return clone; + }; + + RepeatingModelGroup.prototype["delete"] = function(index) { + this.value.splice(index, 1); + return this.trigger('change'); + }; + + return RepeatingModelGroup; + +})(ModelGroup); diff --git a/lib/modelOption.js b/lib/modelOption.js new file mode 100644 index 0000000..17b689b --- /dev/null +++ b/lib/modelOption.js @@ -0,0 +1,31 @@ +var ModelBase, ModelOption, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +ModelBase = require('./modelBase'); + +module.exports = ModelOption = (function(superClass) { + extend(ModelOption, superClass); + + function ModelOption() { + return ModelOption.__super__.constructor.apply(this, arguments); + } + + ModelOption.prototype.initialize = function() { + this.setDefault('value', this.get('title')); + this.setDefault('title', this.get('value')); + this.setDefault('selected', false); + this.setDefault('path', []); + ModelOption.__super__.initialize.apply(this, arguments); + return this.on('change:selected', function() { + if (this.selected) { + return this.parent.addOptionValue(this.value, this.bidAdj); + } else { + return this.parent.removeOptionValue(this.value); + } + }); + }; + + return ModelOption; + +})(ModelBase); diff --git a/lib_old/building.js b/lib_old/building.js new file mode 100644 index 0000000..807a3e5 --- /dev/null +++ b/lib_old/building.js @@ -0,0 +1,224 @@ +var CoffeeScript, ModelGroup, Mustache, _, globals, jiff, throttledAlert, vm; + +CoffeeScript = require('coffee-script'); + +Mustache = require('mustache'); + +_ = require('underscore'); + +vm = require('vm'); + +jiff = require('jiff'); + +globals = require('./globals'); + +ModelGroup = require('./modelGroup'); + +globals = require('./globals'); + +if (typeof alert !== "undefined" && alert !== null) { + throttledAlert = _.throttle(alert, 500); +} + + +/* + An array of functions that can test a built model. + Model code may add tests to this array during build. The tests themselves will not be run at the time, but are + made avaiable via this export so processes can run the tests when appropriate. + Tests may modify the model state, so the model should be rebuilt prior to running each test. + */ + +exports.modelTests = []; + +exports.fromCode = function(code, data, element, imports, isImport) { + var assert, emit, newRoot, test; + data = (function() { + switch (typeof data) { + case 'object': + return jiff.clone(data); + case 'string': + return JSON.parse(data); + default: + return {}; + } + })(); + globals.runtime = false; + exports.modelTests = []; + test = function(func) { + return exports.modelTests.push(func); + }; + assert = function(bool, message) { + if (message == null) { + message = "A model test has failed"; + } + if (!bool) { + return globals.handleError(message); + } + }; + emit = function(name, context) { + if (element && $) { + return element.trigger($.Event(name, context)); + } + }; + newRoot = new ModelGroup(); + newRoot.recalculating = false; + newRoot.recalculateCycle = function() {}; + (function(root) { + var field, group, sandbox, validate; + field = newRoot.field.bind(newRoot); + group = newRoot.group.bind(newRoot); + root = newRoot.root; + validate = newRoot.validate; + if (typeof window === "undefined" || window === null) { + sandbox = { + field: field, + group: group, + root: root, + validate: validate, + data: data, + imports: imports, + test: test, + assert: assert, + Mustache: Mustache, + emit: emit, + _: _, + console: { + log: function() {}, + error: function() {} + }, + print: function() {} + }; + return vm.runInNewContext('"use strict";' + code, sandbox); + } else { + return eval('"use strict";' + code); + } + })(null); + newRoot.postBuild(); + globals.runtime = true; + newRoot.applyData(data); + newRoot.getChanges = exports.getChanges.bind(null, newRoot); + newRoot.setDirty(newRoot.id, 'multiple'); + newRoot.recalculateCycle = function() { + var results; + results = []; + while (!this.recalculating && this.dirty) { + this.recalculating = true; + this.recalculateRelativeProperties(); + results.push(this.recalculating = false); + } + return results; + }; + newRoot.recalculateCycle(); + newRoot.on('change:isValid', function() { + if (!isImport) { + return emit('validate', { + isValid: newRoot.isValid + }); + } + }); + newRoot.on('recalculate', function() { + if (!isImport) { + return emit('change'); + } + }); + newRoot.trigger('change:isValid'); + newRoot.trigger('recalculate'); + newRoot.styles = false; + return newRoot; +}; + +exports.fromCoffee = function(code, data, element, imports, isImport) { + return exports.fromCode(CoffeeScript.compile(code), data, element, imports, isImport); +}; + +exports.fromPackage = function(pkg, data, element) { + var buildModelWithRecursiveImports; + buildModelWithRecursiveImports = function(p, el, isImport) { + var buildImport, builtImports, f, form; + form = ((function() { + var i, len, ref, results; + ref = p.forms; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + f = ref[i]; + if (f.formid === p.formid) { + results.push(f); + } + } + return results; + })())[0]; + if (form == null) { + return; + } + builtImports = {}; + buildImport = function(impObj) { + return builtImports[impObj.namespace] = buildModelWithRecursiveImports({ + formid: impObj.importformid, + data: data, + forms: p.forms + }, element, true); + }; + if (form.imports) { + form.imports.forEach(buildImport); + } + return exports.fromCoffee(form.model, data, el, builtImports, isImport); + }; + if (typeof pkg.formid === 'string') { + pkg.formid = parseInt(pkg.formid); + } + data = _.extend(pkg.data || {}, data || {}); + return buildModelWithRecursiveImports(pkg, element, false); +}; + +exports.getChanges = function(modelAfter, beforeData) { + var after, before, changedPath, changedPaths, changedPathsUniqObject, changedPathsUnique, changes, i, internalPatch, j, key, len, len1, modelBefore, outputPatch, p, path, val; + modelBefore = modelAfter.cloneModel(); + modelBefore.applyData(beforeData, true); + internalPatch = jiff.diff(modelBefore.buildOutputData(void 0, true), modelAfter.buildOutputData(void 0, true), { + invertible: false + }); + outputPatch = jiff.diff(modelBefore.buildOutputData(), modelAfter.buildOutputData(), { + invertible: false + }); + changedPaths = (function() { + var i, len, results; + results = []; + for (i = 0, len = internalPatch.length; i < len; i++) { + p = internalPatch[i]; + results.push(p.path.replace(/\/[0-9]+$/, '')); + } + return results; + })(); + changedPathsUniqObject = {}; + for (i = 0, len = changedPaths.length; i < len; i++) { + val = changedPaths[i]; + changedPathsUniqObject[val] = val; + } + changedPathsUnique = (function() { + var results; + results = []; + for (key in changedPathsUniqObject) { + results.push(key); + } + return results; + })(); + changes = []; + for (j = 0, len1 = changedPathsUnique.length; j < len1; j++) { + changedPath = changedPathsUnique[j]; + path = changedPath.slice(1); + before = modelBefore.child(path); + after = modelAfter.child(path); + if (!_.isEqual(before != null ? before.value : void 0, after != null ? after.value : void 0)) { + changes.push({ + name: changedPath, + title: after.title, + before: before.buildOutputData(void 0, true), + after: after.buildOutputData(void 0, true) + }); + } + } + return { + changes: changes, + patch: outputPatch + }; +}; diff --git a/lib_old/formbuilder.js b/lib_old/formbuilder.js new file mode 100644 index 0000000..664035c --- /dev/null +++ b/lib_old/formbuilder.js @@ -0,0 +1,47 @@ +var Backbone, ModelBase, building, globals; + +if (typeof window !== "undefined" && window !== null) { + window.formbuilder = exports; +} + +Backbone = require('backbone'); + +ModelBase = require('./modelBase'); + +building = require('./building'); + +globals = require('./globals'); + +exports.fromCode = building.fromCode; + +exports.fromCoffee = building.fromCoffee; + +exports.fromPackage = building.fromPackage; + +exports.getChanges = building.getChanges; + +exports.mergeData = globals.mergeData; + +exports.applyData = function(modelObject, inData, clear, purgeDefaults) { + return modelObject.applyData(inData, clear, purgeDefaults); +}; + +exports.buildOutputData = function(model) { + return model.buildOutputData(); +}; + +Object.defineProperty(exports, 'modelTests', { + get: function() { + return building.modelTests; + } +}); + +Object.defineProperty(exports, 'handleError', { + get: function() { + return globals.handleError; + }, + set: function(f) { + return globals.handleError = f; + }, + enumerable: true +}); diff --git a/lib_old/globals.js b/lib_old/globals.js new file mode 100644 index 0000000..d301218 --- /dev/null +++ b/lib_old/globals.js @@ -0,0 +1,35 @@ +module.exports = { + runtime: false, + handleError: function(err) { + if (!(err instanceof Error)) { + err = new Error(err); + } + throw err; + }, + makeErrorMessage: function(model, propName, err) { + var nameStack, node, stack; + stack = []; + node = model; + while (node.name != null) { + stack.push(node.name); + node = node.parent; + } + stack.reverse(); + nameStack = stack.join('.'); + return "The '" + propName + "' function belonging to the field named '" + nameStack + "' threw an error with the message '" + err.message + "'"; + }, + mergeData: function(a, b) { + var key, value; + if ((b != null ? b.constructor : void 0) === Object) { + for (key in b) { + value = b[key]; + if ((a[key] != null) && a[key].constructor === Object && (value != null ? value.constructor : void 0) === Object) { + module.exports.mergeData(a[key], value); + } else { + a[key] = value; + } + } + } + return a; + } +}; diff --git a/lib_old/modelBase.js b/lib_old/modelBase.js new file mode 100644 index 0000000..1a0ab51 --- /dev/null +++ b/lib_old/modelBase.js @@ -0,0 +1,417 @@ + +/* + * Attributes common to groups and fields. + */ +var Backbone, ModelBase, Mustache, _, getBoolOrFunctionResult, globals, moment, newid, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +Backbone = require('backbone'); + +_ = require('underscore'); + +globals = require('./globals'); + +moment = require('moment'); + +Mustache = require('mustache'); + +newid = (function() { + var incId; + incId = 0; + return function() { + incId++; + return "fbid_" + incId; + }; +})(); + + +/* Some properties may be booleans or functions that return booleans + Use this function to determine final boolean value. + prop - the property to evaluate, which may be something primitive or a function + deflt - the value to return if the property is undefined + */ + +getBoolOrFunctionResult = function(prop, deflt) { + if (deflt == null) { + deflt = true; + } + if (typeof prop === 'function') { + return !!prop(); + } + if (prop === void 0) { + return deflt; + } + return !!prop; +}; + +module.exports = ModelBase = (function(superClass) { + extend(ModelBase, superClass); + + function ModelBase() { + return ModelBase.__super__.constructor.apply(this, arguments); + } + + ModelBase.prototype.modelClassName = 'ModelBase'; + + ModelBase.prototype.initialize = function() { + var fn, key, ref, val; + this.setDefault('visible', true); + this.set('isVisible', true); + this.setDefault('disabled', false); + this.set('isDisabled', false); + this.setDefault('onChangePropertiesHandlers', []); + this.set('id', newid()); + this.setDefault('parent', void 0); + this.setDefault('root', void 0); + this.setDefault('name', this.get('title')); + this.setDefault('title', this.get('name')); + ref = this.attributes; + fn = (function(_this) { + return function(key) { + return Object.defineProperty(_this, key, { + get: function() { + return this.get(key); + }, + set: function(newValue) { + if ((this.get(key)) !== newValue) { + return this.set(key, newValue); + } + } + }); + }; + })(this); + for (key in ref) { + val = ref[key]; + fn(key); + } + this.bindPropFunctions('visible'); + this.bindPropFunctions('disabled'); + this.makePropArray('onChangePropertiesHandlers'); + this.bindPropFunctions('onChangePropertiesHandlers'); + return this.on('change', function() { + var ch, changeFunc, i, len, ref1; + if (!globals.runtime) { + return; + } + ref1 = this.onChangePropertiesHandlers; + for (i = 0, len = ref1.length; i < len; i++) { + changeFunc = ref1[i]; + changeFunc(); + } + ch = this.changedAttributes(); + if (ch === false) { + ch = 'multiple'; + } + this.root.setDirty(this.id, ch); + return this.root.recalculateCycle(); + }); + }; + + ModelBase.prototype.postBuild = function() {}; + + ModelBase.prototype.setDefault = function(field, val) { + if (this.get(field) == null) { + return this.set(field, val); + } + }; + + ModelBase.prototype.text = function(message) { + return this.field(message, { + type: 'info' + }); + }; + + ModelBase.prototype.bindPropFunction = function(propName, func) { + var model; + model = this; + return function() { + var err, message; + try { + if (this instanceof ModelBase) { + model = this; + } + return func.apply(model, arguments); + } catch (error) { + err = error; + message = globals.makeErrorMessage(model, propName, err); + return globals.handleError(message); + } + }; + }; + + ModelBase.prototype.bindPropFunctions = function(propName) { + var i, index, ref, results; + if (Array.isArray(this[propName])) { + results = []; + for (index = i = 0, ref = this[propName].length; 0 <= ref ? i < ref : i > ref; index = 0 <= ref ? ++i : --i) { + results.push(this[propName][index] = this.bindPropFunction(propName, this[propName][index])); + } + return results; + } else if (typeof this[propName] === 'function') { + return this.set(propName, this.bindPropFunction(propName, this[propName]), { + silent: true + }); + } + }; + + ModelBase.prototype.makePropArray = function(propName) { + if (!Array.isArray(this.get(propName))) { + return this.set(propName, [this.get(propName)]); + } + }; + + ModelBase.prototype.buildParamObject = function(params, paramPositions) { + var i, key, len, param, paramIndex, paramObject, ref, val; + paramObject = {}; + paramIndex = 0; + for (i = 0, len = params.length; i < len; i++) { + param = params[i]; + if (((ref = typeof param) === 'string' || ref === 'number' || ref === 'boolean') || Array.isArray(param)) { + paramObject[paramPositions[paramIndex++]] = param; + } else if (Object.prototype.toString.call(param) === '[object Object]') { + for (key in param) { + val = param[key]; + paramObject[key] = val; + } + } + } + paramObject.parent = this; + paramObject.root = this.root; + return paramObject; + }; + + ModelBase.prototype.dirty = ''; + + ModelBase.prototype.setDirty = function(id, whatChanged) { + var ch, drt, keys; + ch = typeof whatChanged === 'string' ? whatChanged : (keys = Object.keys(whatChanged), keys.length === 1 ? id + ":" + keys[0] : 'multiple'); + drt = this.dirty === ch || this.dirty === '' ? ch : "multiple"; + return this.dirty = drt; + }; + + ModelBase.prototype.setClean = function() { + return this.dirty = ''; + }; + + ModelBase.prototype.shouldCallTriggerFunctionFor = function(dirty, attrName) { + return dirty && dirty !== (this.id + ":" + attrName); + }; + + ModelBase.prototype.recalculateRelativeProperties = function() { + var dirty; + dirty = this.dirty; + this.setClean(); + if (this.shouldCallTriggerFunctionFor(dirty, 'isVisible')) { + this.isVisible = getBoolOrFunctionResult(this.visible); + } + if (this.shouldCallTriggerFunctionFor(dirty, 'isDisabled')) { + this.isDisabled = getBoolOrFunctionResult(this.disabled, false); + } + return this.trigger('recalculate'); + }; + + ModelBase.prototype.onChangeProperties = function(f, trigger) { + if (trigger == null) { + trigger = true; + } + this.onChangePropertiesHandlers.push(this.bindPropFunction('onChangeProperties', f)); + if (trigger) { + this.trigger('change'); + } + return this; + }; + + ModelBase.prototype.validate = { + required: function(value) { + if (value == null) { + value = this.value || ''; + } + if (((function() { + switch (typeof value) { + case 'number': + case 'boolean': + return false; + case 'string': + return value.length === 0; + case 'object': + return Object.keys(value).length === 0; + default: + return true; + } + })())) { + return "This field is required"; + } + }, + minLength: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length < n) { + return "Must be at least " + n + " characters long"; + } + }; + }, + maxLength: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length > n) { + return "Can be at most " + n + " characters long"; + } + }; + }, + number: function(value) { + if (value == null) { + value = this.value || ''; + } + if (isNaN(+value)) { + return "Must be an integer or decimal number. (ex. 42 or 1.618)"; + } + }, + date: function(value, format) { + if (value == null) { + value = this.value || ''; + } + if (format == null) { + format = this.format; + } + if (value === '') { + return; + } + if (!moment(value, format, true).isValid()) { + return "Not a valid date or does not match the format " + format; + } + }, + email: function(value) { + if (value == null) { + value = this.value || ''; + } + if (!value.match(/^[a-z0-9!\#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!\#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/)) { + return "Must be a valid email"; + } + }, + url: function(value) { + if (value == null) { + value = this.value || ''; + } + if (!value.match(/^(([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)\#?(?:[\w]*))?$/)) { + return "Must be a URL"; + } + }, + dollars: function(value) { + if (value == null) { + value = this.value || ''; + } + if (!value.match(/^\$(\d+\.\d\d|\d+)$/)) { + return "Must be a dollar amount (ex. $3.99)"; + } + }, + minSelections: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length < n) { + return "Please select at least " + n + " options"; + } + }; + }, + maxSelections: function(n) { + return function(value) { + if (value == null) { + value = this.value || ''; + } + if (value.length > n) { + return "Please select at most " + n + " options"; + } + }; + }, + selectedIsVisible: function(field) { + var i, len, opt, ref; + if (field == null) { + field = this; + } + ref = field.options; + for (i = 0, len = ref.length; i < len; i++) { + opt = ref[i]; + if (opt.selected && !opt.isVisible) { + return "A selected option is not currently available. Please make a new choice from available options."; + } + } + }, + template: function() { + var e, template; + if (!this.template) { + return; + } + if (typeof this.template === 'object') { + template = this.template.value; + } else { + template = this.parent.child(this.template).value; + } + try { + Mustache.render(template, this.root.data); + } catch (error) { + e = error; + return "Template field does not contain valid Mustache"; + } + } + }; + + ModelBase.prototype.cloneModel = function(newRoot, constructor, excludeAttributes) { + var childClone, filteredAttributes, i, key, len, modelObj, myClone, newVal, ref, ref1, val; + if (newRoot == null) { + newRoot = this.root; + } + if (constructor == null) { + constructor = this.constructor; + } + if (excludeAttributes == null) { + excludeAttributes = []; + } + filteredAttributes = {}; + ref = this.attributes; + for (key in ref) { + val = ref[key]; + if (indexOf.call(excludeAttributes, key) < 0) { + filteredAttributes[key] = val; + } + } + myClone = new constructor(filteredAttributes); + ref1 = myClone.attributes; + for (key in ref1) { + val = ref1[key]; + if (key === 'root') { + myClone.set(key, newRoot); + } else if (val instanceof ModelBase && (key !== 'root' && key !== 'parent')) { + myClone.set(key, val.cloneModel(newRoot)); + } else if (Array.isArray(val)) { + newVal = []; + if (val[0] instanceof ModelBase && key !== 'value') { + for (i = 0, len = val.length; i < len; i++) { + modelObj = val[i]; + childClone = modelObj.cloneModel(newRoot); + if (childClone.parent === this) { + childClone.parent = myClone; + if (key === 'options' && childClone.selected) { + myClone.addOptionValue(childClone.value); + } + } + newVal.push(childClone); + } + } else { + newVal = _.clone(val); + } + myClone.set(key, newVal); + } + } + return myClone; + }; + + return ModelBase; + +})(Backbone.Model); diff --git a/lib_old/modelField.js b/lib_old/modelField.js new file mode 100644 index 0000000..c482d77 --- /dev/null +++ b/lib_old/modelField.js @@ -0,0 +1,528 @@ +var ModelBase, ModelField, ModelOption, Mustache, globals, jiff, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelBase = require('./modelBase'); + +ModelOption = require('./modelOption'); + +globals = require('./globals'); + +Mustache = require('mustache'); + +jiff = require('jiff'); + + +/* + A ModelField represents a model object that render as a DOM field + NOTE: The following field types are subclasses: image, tree, date + */ + +module.exports = ModelField = (function(superClass) { + extend(ModelField, superClass); + + function ModelField() { + return ModelField.__super__.constructor.apply(this, arguments); + } + + ModelField.prototype.modelClassName = 'ModelField'; + + ModelField.prototype.initialize = function() { + var ref2, ref3; + this.setDefault('type', 'text'); + this.setDefault('options', []); + this.setDefault('value', (function() { + switch (this.get('type')) { + case 'multiselect': + return []; + case 'bool': + return false; + case 'info': + case 'button': + return void 0; + default: + return (this.get('defaultValue')) || ''; + } + }).call(this)); + this.setDefault('defaultValue', this.get('value')); + this.set('isValid', true); + this.setDefault('validators', []); + this.setDefault('onChangeHandlers', []); + this.setDefault('dynamicValue', null); + this.setDefault('template', null); + this.setDefault('autocomplete', null); + this.setDefault('beforeInput', function(val) { + return val; + }); + this.setDefault('beforeOutput', function(val) { + return val; + }); + ModelField.__super__.initialize.apply(this, arguments); + if ((ref2 = this.type) !== 'info' && ref2 !== 'text' && ref2 !== 'url' && ref2 !== 'email' && ref2 !== 'tel' && ref2 !== 'time' && ref2 !== 'date' && ref2 !== 'textarea' && ref2 !== 'bool' && ref2 !== 'tree' && ref2 !== 'color' && ref2 !== 'select' && ref2 !== 'multiselect' && ref2 !== 'image' && ref2 !== 'button' && ref2 !== 'number') { + return globals.handleError("Bad field type: " + this.type); + } + this.bindPropFunctions('dynamicValue'); + while ((Array.isArray(this.value)) && (this.type !== 'multiselect') && (this.type !== 'tree') && (this.type !== 'button')) { + this.value = this.value[0]; + } + if (typeof this.value === 'string' && (this.type === 'multiselect')) { + this.value = [this.value]; + } + if (this.type === 'bool' && typeof this.value !== 'bool') { + this.value = !!this.value; + } + this.makePropArray('validators'); + this.bindPropFunctions('validators'); + this.makePropArray('onChangeHandlers'); + this.bindPropFunctions('onChangeHandlers'); + if (this.optionsFrom != null) { + this.ensureSelectType(); + if ((this.optionsFrom.url == null) || (this.optionsFrom.parseResults == null)) { + return globals.handleError('When fetching options remotely, both url and parseResults properties are required'); + } + if (typeof ((ref3 = this.optionsFrom) != null ? ref3.url : void 0) === 'function') { + this.optionsFrom.url = this.bindPropFunction('optionsFrom.url', this.optionsFrom.url); + } + if (typeof this.optionsFrom.parseResults !== 'function') { + return globals.handleError('optionsFrom.parseResults must be a function'); + } + this.optionsFrom.parseResults = this.bindPropFunction('optionsFrom.parseResults', this.optionsFrom.parseResults); + } + this.updateOptionsSelected(); + this.on('change:value', function() { + var changeFunc, j, len1, ref4; + ref4 = this.onChangeHandlers; + for (j = 0, len1 = ref4.length; j < len1; j++) { + changeFunc = ref4[j]; + changeFunc(); + } + return this.updateOptionsSelected(); + }); + return this.on('change:type', function() { + if (this.type === 'multiselect') { + this.value = this.value.length > 0 ? [this.value] : []; + } else if (this.previousAttributes().type === 'multiselect') { + this.value = this.value.length > 0 ? this.value[0] : ''; + } + if (this.options.length > 0 && !this.isSelectType()) { + return this.type = 'select'; + } + }); + }; + + ModelField.prototype.getOptionsFrom = function() { + var ref2, url; + if (this.optionsFrom == null) { + return; + } + url = typeof this.optionsFrom.url === 'function' ? this.optionsFrom.url() : this.optionsFrom.url; + if (this.prevUrl === url) { + return; + } + this.prevUrl = url; + return typeof window !== "undefined" && window !== null ? (ref2 = window.formbuilderproxy) != null ? ref2.getFromProxy({ + url: url, + method: this.optionsFrom.method || 'get', + headerKey: this.optionsFrom.headerKey + }, (function(_this) { + return function(error, data) { + var j, len1, mappedResults, opt, results1; + if (error) { + return globals.handleError(globals.makeErrorMessage(_this, 'optionsFrom', error)); + } + mappedResults = _this.optionsFrom.parseResults(data); + if (!Array.isArray(mappedResults)) { + return globals.handleError('results of parseResults must be an array of option parameters'); + } + _this.options = []; + results1 = []; + for (j = 0, len1 = mappedResults.length; j < len1; j++) { + opt = mappedResults[j]; + results1.push(_this.option(opt)); + } + return results1; + }; + })(this)) : void 0 : void 0; + }; + + ModelField.prototype.validityMessage = void 0; + + ModelField.prototype.field = function() { + var obj, ref2; + obj = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return (ref2 = this.parent).field.apply(ref2, obj); + }; + + ModelField.prototype.group = function() { + var obj, ref2; + obj = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return (ref2 = this.parent).group.apply(ref2, obj); + }; + + ModelField.prototype.option = function() { + var newOption, nextOpts, opt, optionObject, optionParams; + optionParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + optionObject = this.buildParamObject(optionParams, ['title', 'value', 'selected', 'bidAdj', 'bidAdjFlag']); + this.ensureSelectType(); + nextOpts = (function() { + var j, len1, ref2, results1; + ref2 = this.options; + results1 = []; + for (j = 0, len1 = ref2.length; j < len1; j++) { + opt = ref2[j]; + if (opt.title !== optionObject.title) { + results1.push(opt); + } + } + return results1; + }).call(this); + newOption = new ModelOption(optionObject); + nextOpts.push(newOption); + this.options = nextOpts; + if (newOption.selected) { + this.addOptionValue(newOption.value); + } + return this; + }; + + ModelField.prototype.postBuild = function() { + this.defaultValue = this.value; + return this.updateOptionsSelected(); + }; + + ModelField.prototype.updateOptionsSelected = function() { + var bid, i, len, opt, ref, ref1, results; + ref = this.options; + results = []; + i = 0; + len = ref.length; + while (i < len) { + opt = ref[i]; + if ((ref1 = this.type) === 'multiselect' || ref1 === 'tree') { + bid = this.hasValue(opt.value); + if (bid.bidValue) { + opt.bidAdj = bid.bidValue.lastIndexOf('/') !== -1 ? bid.bidValue.split("/").pop() : this.bidAdj; + } + results.push(opt.selected = bid.selectStatus); + } else { + results.push(opt.selected = this.hasValue(opt.value)); + } + i++; + } + return results; + }; + + ModelField.prototype.isSelectType = function() { + var ref2; + return (ref2 = this.type) === 'select' || ref2 === 'multiselect' || ref2 === 'image' || ref2 === 'tree'; + }; + + ModelField.prototype.ensureSelectType = function() { + if (!this.isSelectType()) { + return this.type = 'select'; + } + }; + + ModelField.prototype.child = function(value) { + var j, len1, o, ref2; + if (Array.isArray(value)) { + value = value.shift(); + } + ref2 = this.options; + for (j = 0, len1 = ref2.length; j < len1; j++) { + o = ref2[j]; + if (o.value === value) { + return o; + } + } + }; + + ModelField.prototype.validator = function(func) { + this.validators.push(this.bindPropFunction('validator', func)); + this.trigger('change'); + return this; + }; + + ModelField.prototype.onChange = function(f) { + this.onChangeHandlers.push(this.bindPropFunction('onChange', f)); + this.trigger('change'); + return this; + }; + + ModelField.prototype.setDirty = function(id, whatChanged) { + var j, len1, opt, ref2; + ref2 = this.options; + for (j = 0, len1 = ref2.length; j < len1; j++) { + opt = ref2[j]; + opt.setDirty(id, whatChanged); + } + return ModelField.__super__.setDirty.call(this, id, whatChanged); + }; + + ModelField.prototype.setClean = function(all) { + var j, len1, opt, ref2, results1; + ModelField.__super__.setClean.apply(this, arguments); + if (all) { + ref2 = this.options; + results1 = []; + for (j = 0, len1 = ref2.length; j < len1; j++) { + opt = ref2[j]; + results1.push(opt.setClean(all)); + } + return results1; + } + }; + + ModelField.prototype.recalculateRelativeProperties = function() { + var dirty, j, k, len1, len2, opt, ref2, ref3, results1, validator, validityMessage, value; + dirty = this.dirty; + ModelField.__super__.recalculateRelativeProperties.apply(this, arguments); + if (this.shouldCallTriggerFunctionFor(dirty, 'isValid')) { + validityMessage = void 0; + if (this.template) { + validityMessage || (validityMessage = this.validate.template.call(this)); + } + if (this.type === 'number') { + validityMessage || (validityMessage = this.validate.number.call(this)); + } + if (!validityMessage) { + ref2 = this.validators; + for (j = 0, len1 = ref2.length; j < len1; j++) { + validator = ref2[j]; + if (typeof validator === 'function') { + validityMessage = validator.call(this); + } + if (typeof validityMessage === 'function') { + return globals.handleError("A validator on field '" + this.name + "' returned a function"); + } + if (validityMessage) { + break; + } + } + } + this.validityMessage = validityMessage; + this.set({ + isValid: validityMessage == null + }); + } + if (this.template && this.shouldCallTriggerFunctionFor(dirty, 'value')) { + this.renderTemplate(); + } else { + if (typeof this.dynamicValue === 'function' && this.shouldCallTriggerFunctionFor(dirty, 'value')) { + value = this.dynamicValue(); + if (typeof value === 'function') { + return globals.handleError("dynamicValue on field '" + this.name + "' returned a function"); + } + this.set('value', value); + } + } + if (this.shouldCallTriggerFunctionFor(dirty, 'options')) { + this.getOptionsFrom(); + } + ref3 = this.options; + results1 = []; + for (k = 0, len2 = ref3.length; k < len2; k++) { + opt = ref3[k]; + results1.push(opt.recalculateRelativeProperties()); + } + return results1; + }; + + ModelField.prototype.addOptionValue = function(val, bidAdj) { + var findMatch, ref; + findMatch = void 0; + ref = void 0; + if ((ref = this.type) === 'multiselect' || ref === 'tree') { + if (!Array.isArray(this.value)) { + this.value = [this.value]; + } + findMatch = this.value.findIndex(function(e) { + if (typeof e === 'string') { + return e.search(val) !== -1; + } else { + return e === val; + } + }); + if (findMatch !== -1) { + if (bidAdj) { + return this.value[findMatch] = val + '/' + bidAdj; + } + } else { + if (bidAdj) { + return this.value.push(val + '/' + bidAdj); + } else { + return this.value.push(val); + } + } + } else { + return this.value = val; + } + }; + + ModelField.prototype.removeOptionValue = function(val) { + var ref; + ref = void 0; + if ((ref = this.type) === 'multiselect' || ref === 'tree') { + return this.value = this.value.filter(function(e) { + if (typeof e === 'string') { + return e.search(val) === -1; + } else { + return e !== val; + } + }); + } else if (this.value === val) { + return this.value = ''; + } + }; + + ModelField.prototype.hasValue = function(val) { + var findMatch, ref; + findMatch = void 0; + ref = void 0; + if ((ref = this.type) === 'multiselect' || ref === 'tree') { + findMatch = this.value.findIndex(function(e) { + if (typeof e === 'string') { + return e.search(val) !== -1; + } else { + return e === val; + } + }); + if (findMatch !== -1) { + return { + 'bidValue': this.value[findMatch], + 'selectStatus': true + }; + } else { + return { + 'selectStatus': false + }; + } + } else { + return val === this.value; + } + }; + + ModelField.prototype.buildOutputData = function(_, skipBeforeOutput) { + var out, value; + value = (function() { + switch (this.type) { + case 'number': + out = +this.value; + if (isNaN(out)) { + return null; + } else { + return out; + } + break; + case 'info': + case 'button': + return void 0; + case 'bool': + return !!this.value; + default: + return this.value; + } + }).call(this); + if (skipBeforeOutput) { + return value; + } else { + return this.beforeOutput(value); + } + }; + + ModelField.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (purgeDefaults) { + return this.value = (function() { + switch (this.type) { + case 'multiselect': + return []; + case 'bool': + return false; + default: + return ''; + } + }).call(this); + } else { + return this.value = this.defaultValue; + } + }; + + ModelField.prototype.ensureValueInOptions = function() { + var existingOption, j, k, l, len1, len2, len3, o, ref2, ref3, ref4, results1, v; + if (!this.isSelectType()) { + return; + } + if (typeof this.value === 'string') { + ref2 = this.options; + for (j = 0, len1 = ref2.length; j < len1; j++) { + o = ref2[j]; + if (o.value === this.value) { + existingOption = o; + } + } + if (!existingOption) { + return this.option(this.value, { + selected: true + }); + } + } else if (Array.isArray(this.value)) { + ref3 = this.value; + results1 = []; + for (k = 0, len2 = ref3.length; k < len2; k++) { + v = ref3[k]; + existingOption = null; + ref4 = this.options; + for (l = 0, len3 = ref4.length; l < len3; l++) { + o = ref4[l]; + if (o.value === v) { + existingOption = o; + } + } + if (!existingOption) { + results1.push(this.option(v, { + selected: true + })); + } else { + results1.push(void 0); + } + } + return results1; + } + }; + + ModelField.prototype.applyData = function(inData, clear, purgeDefaults) { + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (clear) { + this.clear(purgeDefaults); + } + if (inData != null) { + return this.value = this.beforeInput(jiff.clone(inData)); + } + }; + + ModelField.prototype.renderTemplate = function() { + var template; + if (typeof this.template === 'object') { + template = this.template.value; + } else { + template = this.parent.child(this.template).value; + } + try { + return this.value = Mustache.render(template, this.root.data); + } catch (error1) { + + } + }; + + return ModelField; + +})(ModelBase); diff --git a/lib_old/modelFieldDate.js b/lib_old/modelFieldDate.js new file mode 100644 index 0000000..c8f3aa7 --- /dev/null +++ b/lib_old/modelFieldDate.js @@ -0,0 +1,44 @@ +var ModelField, ModelFieldDate, moment, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +ModelField = require('./modelField'); + +moment = require('moment'); + +module.exports = ModelFieldDate = (function(superClass) { + extend(ModelFieldDate, superClass); + + function ModelFieldDate() { + return ModelFieldDate.__super__.constructor.apply(this, arguments); + } + + ModelFieldDate.prototype.initialize = function() { + this.setDefault('format', 'M/D/YYYY'); + ModelFieldDate.__super__.initialize.apply(this, arguments); + return this.validator(this.validate.date); + }; + + ModelFieldDate.prototype.dateToString = function(date, format) { + if (date == null) { + date = this.value; + } + if (format == null) { + format = this.format; + } + return moment(date).format(format); + }; + + ModelFieldDate.prototype.stringToDate = function(str, format) { + if (str == null) { + str = this.value; + } + if (format == null) { + format = this.format; + } + return moment(str, format, true).toDate(); + }; + + return ModelFieldDate; + +})(ModelField); diff --git a/lib_old/modelFieldImage.js b/lib_old/modelFieldImage.js new file mode 100644 index 0000000..89f9a7f --- /dev/null +++ b/lib_old/modelFieldImage.js @@ -0,0 +1,101 @@ +var ModelField, ModelFieldImage, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelField = require('./modelField'); + +module.exports = ModelFieldImage = (function(superClass) { + extend(ModelFieldImage, superClass); + + function ModelFieldImage() { + return ModelFieldImage.__super__.constructor.apply(this, arguments); + } + + ModelFieldImage.prototype.initialize = function() { + this.setDefault('value', {}); + this.setDefault('allowUpload', false); + this.setDefault('imagesPerPage', 4); + this.setDefault('minWidth', 0); + this.setDefault('maxWidth', 0); + this.setDefault('minHeight', 0); + this.setDefault('maxHeight', 0); + this.setDefault('minSize', 0); + this.setDefault('maxSize', 0); + this.set('optionsChanged', false); + return ModelFieldImage.__super__.initialize.apply(this, arguments); + }; + + ModelFieldImage.prototype.option = function() { + var optionObject, optionParams; + optionParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + optionObject = this.buildParamObject(optionParams, ['fileID', 'fileUrl', 'thumbnailUrl']); + if (optionObject.fileID == null) { + optionObject.fileID = optionObject.fileUrl; + } + if (optionObject.thumbnailUrl == null) { + optionObject.thumbnailUrl = optionObject.fileUrl; + } + optionObject.value = { + fileID: optionObject.fileID, + fileUrl: optionObject.fileUrl, + thumbnailUrl: optionObject.thumbnailUrl + }; + if (optionObject.title == null) { + optionObject.title = optionObject.fileID; + } + this.optionsChanged = true; + return ModelFieldImage.__super__.option.call(this, optionObject); + }; + + ModelFieldImage.prototype.child = function(fileID) { + var i, len, o, ref; + if (Array.isArray(fileID)) { + fileID = fileID.shift(); + } + if (typeof fileID === 'object') { + fileID = fileID.fileID; + } + ref = this.options; + for (i = 0, len = ref.length; i < len; i++) { + o = ref[i]; + if (o.fileID === fileID) { + return o; + } + } + }; + + ModelFieldImage.prototype.removeOptionValue = function(val) { + if (this.value.fileID === val.fileID) { + return this.value = {}; + } + }; + + ModelFieldImage.prototype.hasValue = function(val) { + return val.fileID === this.value.fileID && val.thumbnailUrl === this.value.thumbnailUrl && val.fileUrl === this.value.fileUrl; + }; + + ModelFieldImage.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + return this.value = purgeDefaults ? {} : this.defaultValue; + }; + + ModelFieldImage.prototype.ensureValueInOptions = function() { + var existingOption, i, len, o, ref; + ref = this.options; + for (i = 0, len = ref.length; i < len; i++) { + o = ref[i]; + if (o.attributes.fileID === this.value.fileID) { + existingOption = o; + } + } + if (!existingOption) { + return this.option(this.value); + } + }; + + return ModelFieldImage; + +})(ModelField); diff --git a/lib_old/modelFieldTree.js b/lib_old/modelFieldTree.js new file mode 100644 index 0000000..68b1bee --- /dev/null +++ b/lib_old/modelFieldTree.js @@ -0,0 +1,45 @@ +var ModelField, ModelFieldTree, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelField = require('./modelField'); + +module.exports = ModelFieldTree = (function(superClass) { + extend(ModelFieldTree, superClass); + + function ModelFieldTree() { + return ModelFieldTree.__super__.constructor.apply(this, arguments); + } + + ModelFieldTree.prototype.initialize = function() { + this.setDefault('value', []); + return ModelFieldTree.__super__.initialize.apply(this, arguments); + }; + + ModelFieldTree.prototype.option = function() { + var optionObject, optionParams; + optionParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + optionObject = this.buildParamObject(optionParams, ['path', 'value', 'selected', 'bidAdj', 'bidAdjFlag']); + if (optionObject.value == null) { + optionObject.value = optionObject.id; + } + if (optionObject.value === null && Array.isArray(optionObject.path)) { + if (optionObject.value == null) { + optionObject.value = optionObject.path.join(' > '); + } + optionObject.title = optionObject.path.join('>'); + } + return ModelFieldTree.__super__.option.call(this, optionObject); + }; + + ModelFieldTree.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + return this.value = purgeDefaults ? [] : this.defaultValue; + }; + + return ModelFieldTree; + +})(ModelField); diff --git a/lib_old/modelGroup.js b/lib_old/modelGroup.js new file mode 100644 index 0000000..ea30836 --- /dev/null +++ b/lib_old/modelGroup.js @@ -0,0 +1,409 @@ +var ModelBase, ModelField, ModelFieldDate, ModelFieldImage, ModelFieldTree, ModelGroup, RepeatingModelGroup, globals, jiff, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ModelBase = require('./modelBase'); + +ModelFieldImage = require('./modelFieldImage'); + +ModelFieldTree = require('./modelFieldTree'); + +ModelFieldDate = require('./modelFieldDate'); + +ModelField = require('./modelField'); + +globals = require('./globals'); + +jiff = require('jiff'); + + +/* + A ModelGroup is a model object that can contain any number of other groups and fields + */ + +module.exports = ModelGroup = (function(superClass) { + extend(ModelGroup, superClass); + + function ModelGroup() { + return ModelGroup.__super__.constructor.apply(this, arguments); + } + + ModelGroup.prototype.modelClassName = 'ModelGroup'; + + ModelGroup.prototype.initialize = function() { + this.setDefault('children', []); + this.setDefault('root', this); + this.set('isValid', true); + this.set('data', null); + this.setDefault('beforeInput', function(val) { + return val; + }); + this.setDefault('beforeOutput', function(val) { + return val; + }); + return ModelGroup.__super__.initialize.apply(this, arguments); + }; + + ModelGroup.prototype.postBuild = function() { + var child, i, len, ref, results; + ref = this.children; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + results.push(child.postBuild()); + } + return results; + }; + + ModelGroup.prototype.field = function() { + var fieldObject, fieldParams, fld; + fieldParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + fieldObject = this.buildParamObject(fieldParams, ['title', 'name', 'type', 'value']); + if (fieldObject.disabled == null) { + fieldObject.disabled = this.disabled; + } + fld = (function() { + switch (fieldObject.type) { + case 'image': + return new ModelFieldImage(fieldObject); + case 'tree': + return new ModelFieldTree(fieldObject); + case 'date': + return new ModelFieldDate(fieldObject); + default: + return new ModelField(fieldObject); + } + })(); + this.children.push(fld); + this.trigger('change'); + return fld; + }; + + ModelGroup.prototype.group = function() { + var groupObject, groupParams, grp, key, ref, val; + groupParams = 1 <= arguments.length ? slice.call(arguments, 0) : []; + grp = {}; + if (((ref = groupParams[0].constructor) != null ? ref.name : void 0) === 'ModelGroup') { + grp = groupParams[0].cloneModel(this.root); + groupParams.shift(); + groupObject = this.buildParamObject(groupParams, ['title', 'name', 'description']); + if (groupObject.name == null) { + groupObject.name = groupObject.title; + } + if (groupObject.title == null) { + groupObject.title = groupObject.name; + } + for (key in groupObject) { + val = groupObject[key]; + grp.set(key, val); + } + } else { + groupObject = this.buildParamObject(groupParams, ['title', 'name', 'description']); + if (groupObject.disabled == null) { + groupObject.disabled = this.disabled; + } + if (groupObject.repeating) { + grp = new RepeatingModelGroup(groupObject); + } else { + grp = new ModelGroup(groupObject); + } + } + this.children.push(grp); + this.trigger('change'); + return grp; + }; + + ModelGroup.prototype.child = function(path) { + var c, child, i, len, name, ref; + if (!(Array.isArray(path))) { + path = path.split(/[.\/]/); + } + name = path.shift(); + ref = this.children; + for (i = 0, len = ref.length; i < len; i++) { + c = ref[i]; + if (c.name === name) { + child = c; + } + } + if (path.length === 0) { + return child; + } else { + return child.child(path); + } + }; + + ModelGroup.prototype.setDirty = function(id, whatChanged) { + var child, i, len, ref; + ref = this.children; + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + child.setDirty(id, whatChanged); + } + return ModelGroup.__super__.setDirty.call(this, id, whatChanged); + }; + + ModelGroup.prototype.setClean = function(all) { + var child, i, len, ref, results; + ModelGroup.__super__.setClean.apply(this, arguments); + if (all) { + ref = this.children; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + results.push(child.setClean(all)); + } + return results; + } + }; + + ModelGroup.prototype.recalculateRelativeProperties = function(collection) { + var child, dirty, i, len, newValid; + if (collection == null) { + collection = this.children; + } + dirty = this.dirty; + ModelGroup.__super__.recalculateRelativeProperties.apply(this, arguments); + newValid = true; + for (i = 0, len = collection.length; i < len; i++) { + child = collection[i]; + child.recalculateRelativeProperties(); + newValid && (newValid = child.isValid); + } + return this.isValid = newValid; + }; + + ModelGroup.prototype.buildOutputData = function(group, skipBeforeOutput) { + var obj; + if (group == null) { + group = this; + } + obj = {}; + group.children.forEach(function(child) { + var childData; + childData = child.buildOutputData(void 0, skipBeforeOutput); + if (childData !== void 0) { + return obj[child.name] = childData; + } + }); + if (skipBeforeOutput) { + return obj; + } else { + return group.beforeOutput(obj); + } + }; + + ModelGroup.prototype.buildOutputDataString = function() { + return JSON.stringify(this.buildOutputData()); + }; + + ModelGroup.prototype.clear = function(purgeDefaults) { + var child, i, j, key, len, len1, ref, ref1, results; + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (this.data) { + ref = Object.keys(this.data); + for (i = 0, len = ref.length; i < len; i++) { + key = ref[i]; + delete this.data[key]; + } + } + ref1 = this.children; + results = []; + for (j = 0, len1 = ref1.length; j < len1; j++) { + child = ref1[j]; + results.push(child.clear(purgeDefaults)); + } + return results; + }; + + ModelGroup.prototype.applyData = function(inData, clear, purgeDefaults) { + var finalInData, key, ref, results, value; + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + if (clear) { + this.clear(purgeDefaults); + } + finalInData = this.beforeInput(jiff.clone(inData)); + + /* + This section preserves a link to the initially applied data object and merges subsequent applies on top + of it in-place. This is necessary for two reasons. + First, the scope of the running model code also references the applied data through the 'data' variable. + Every applied data must be available even though the runtime is not re-evaluated each time. + Second, templated fields use this data as the input to their Mustache evaluation. See @renderTemplate() + */ + if (this.data) { + globals.mergeData(this.data, inData); + this.trigger('change'); + } else { + this.data = inData; + } + results = []; + for (key in finalInData) { + value = finalInData[key]; + results.push((ref = this.child(key)) != null ? ref.applyData(value) : void 0); + } + return results; + }; + + return ModelGroup; + +})(ModelBase); + + +/* + Encapsulates a group of form objects that can be added or removed to the form together multiple times + */ + +RepeatingModelGroup = (function(superClass) { + extend(RepeatingModelGroup, superClass); + + function RepeatingModelGroup() { + return RepeatingModelGroup.__super__.constructor.apply(this, arguments); + } + + RepeatingModelGroup.prototype.modelClassName = 'RepeatingModelGroup'; + + RepeatingModelGroup.prototype.initialize = function() { + this.setDefault('defaultValue', this.get('value') || []); + this.set('value', []); + return RepeatingModelGroup.__super__.initialize.apply(this, arguments); + }; + + RepeatingModelGroup.prototype.postBuild = function() { + var c, i, len, ref; + ref = this.children; + for (i = 0, len = ref.length; i < len; i++) { + c = ref[i]; + c.postBuild(); + } + return this.clear(); + }; + + RepeatingModelGroup.prototype.setDirty = function(id, whatChanged) { + var i, len, ref, val; + ref = this.value; + for (i = 0, len = ref.length; i < len; i++) { + val = ref[i]; + val.setDirty(id, whatChanged); + } + return RepeatingModelGroup.__super__.setDirty.call(this, id, whatChanged); + }; + + RepeatingModelGroup.prototype.setClean = function(all) { + var i, len, ref, results, val; + RepeatingModelGroup.__super__.setClean.apply(this, arguments); + if (all) { + ref = this.value; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + val = ref[i]; + results.push(val.setClean(all)); + } + return results; + } + }; + + RepeatingModelGroup.prototype.recalculateRelativeProperties = function() { + return RepeatingModelGroup.__super__.recalculateRelativeProperties.call(this, this.value); + }; + + RepeatingModelGroup.prototype.buildOutputData = function(_, skipBeforeOutput) { + var tempOut; + tempOut = this.value.map(function(instance) { + return RepeatingModelGroup.__super__.buildOutputData.call(this, instance); + }); + if (skipBeforeOutput) { + return tempOut; + } else { + return this.beforeOutput(tempOut); + } + }; + + RepeatingModelGroup.prototype.clear = function(purgeDefaults) { + if (purgeDefaults == null) { + purgeDefaults = false; + } + this.value = []; + if (!purgeDefaults) { + if (this.defaultValue) { + return this.addEachSimpleObject(this.defaultValue); + } + } + }; + + RepeatingModelGroup.prototype.applyData = function(inData, clear, purgeDefaults) { + var finalInData; + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + finalInData = this.beforeInput(jiff.clone(inData)); + if (finalInData) { + this.value = []; + } else { + if (clear) { + this.clear(purgeDefaults); + } + } + return this.addEachSimpleObject(finalInData, clear, purgeDefaults); + }; + + RepeatingModelGroup.prototype.addEachSimpleObject = function(o, clear, purgeDefaults) { + var added, i, key, len, obj, results, value; + if (clear == null) { + clear = false; + } + if (purgeDefaults == null) { + purgeDefaults = false; + } + results = []; + for (i = 0, len = o.length; i < len; i++) { + obj = o[i]; + added = this.add(); + results.push((function() { + var ref, results1; + results1 = []; + for (key in obj) { + value = obj[key]; + results1.push((ref = added.child(key)) != null ? ref.applyData(value, clear, purgeDefaults) : void 0); + } + return results1; + })()); + } + return results; + }; + + RepeatingModelGroup.prototype.cloneModel = function(root, constructor) { + var clone, excludeAttributes; + excludeAttributes = (constructor != null ? constructor.name : void 0) === 'ModelGroup' ? ['value', 'beforeInput', 'beforeOutput', 'description'] : []; + clone = RepeatingModelGroup.__super__.cloneModel.call(this, root, constructor, excludeAttributes); + clone.title = ''; + return clone; + }; + + RepeatingModelGroup.prototype.add = function() { + var clone; + clone = this.cloneModel(this.root, ModelGroup); + this.value.push(clone); + this.trigger('change'); + return clone; + }; + + RepeatingModelGroup.prototype["delete"] = function(index) { + this.value.splice(index, 1); + return this.trigger('change'); + }; + + return RepeatingModelGroup; + +})(ModelGroup); diff --git a/lib_old/modelOption.js b/lib_old/modelOption.js new file mode 100644 index 0000000..17b689b --- /dev/null +++ b/lib_old/modelOption.js @@ -0,0 +1,31 @@ +var ModelBase, ModelOption, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +ModelBase = require('./modelBase'); + +module.exports = ModelOption = (function(superClass) { + extend(ModelOption, superClass); + + function ModelOption() { + return ModelOption.__super__.constructor.apply(this, arguments); + } + + ModelOption.prototype.initialize = function() { + this.setDefault('value', this.get('title')); + this.setDefault('title', this.get('value')); + this.setDefault('selected', false); + this.setDefault('path', []); + ModelOption.__super__.initialize.apply(this, arguments); + return this.on('change:selected', function() { + if (this.selected) { + return this.parent.addOptionValue(this.value, this.bidAdj); + } else { + return this.parent.removeOptionValue(this.value); + } + }); + }; + + return ModelOption; + +})(ModelBase); diff --git a/package.json b/package.json index df294ba..ca67915 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "balihoo-form-builder-model", - "version": "2.2.1", + "version": "2.2.3", "description": "Standalone code for building form builder models", "author": "Balihoo", "main": "./formbuilder.js", diff --git a/src/modelField.coffee b/src/modelField.coffee index 059853b..6fc6fef 100644 --- a/src/modelField.coffee +++ b/src/modelField.coffee @@ -116,7 +116,7 @@ module.exports = class ModelField extends ModelBase @parent.group obj... option: (optionParams...) -> - optionObject = @buildParamObject optionParams, ['title', 'value', 'selected'] + optionObject = @buildParamObject optionParams, ['title', 'value', 'selected', 'bidAdj', 'bidAdjFlag'] # when adding an option to a field, make sure it is a *select type @ensureSelectType() @@ -140,8 +140,22 @@ module.exports = class ModelField extends ModelBase @updateOptionsSelected() updateOptionsSelected: -> - for opt in @options - opt.selected = @hasValue opt.value + ref = @options + results = [] + i = 0 + len = ref.length + + while i < len + opt = ref[i] + if (ref1 = @type) == 'multiselect' or ref1 == 'tree' + bid = @hasValue(opt.value) + if bid.bidValue + opt.bidAdj = if bid.bidValue.lastIndexOf('/') != -1 then bid.bidValue.split("/").pop() else @bidAdj + results.push opt.selected = bid.selectStatus + else + results.push opt.selected = @hasValue(opt.value) + i++ + results # returns true if this type is one where a value is selected. Otherwise false isSelectType: -> @@ -222,28 +236,61 @@ module.exports = class ModelField extends ModelBase for opt in @options opt.recalculateRelativeProperties() - addOptionValue: (val) -> - if @type in ['multiselect','tree'] - unless Array.isArray @value - @value = [@value] - if not (val in @value) - @value.push val - else #single-select - @value = val + addOptionValue: (val, bidAdj) -> + findMatch = undefined + ref = undefined + if (ref = @type) == 'multiselect' or ref == 'tree' + if !Array.isArray(@value) + @value = [ @value ] + findMatch = @value.findIndex((e) -> + if typeof e == 'string' + e.search(val) != -1 + else + e == val + ) + if findMatch != -1 + if bidAdj + return @value[findMatch] = val + '/' + bidAdj + else + if bidAdj + return @value.push(val + '/' + bidAdj) + else + return @value.push(val) + else + return @value = val + return removeOptionValue: (val) -> - if @type in ['multiselect','tree'] - if val in @value - @value = @value.filter (v) -> v isnt val - else if @value is val #single-select - @value = '' - - #determine if the value is or contains the provided value. + ref = undefined + if (ref = @type) == 'multiselect' or ref == 'tree' + return @value = @value.filter((e) -> + if typeof e == 'string' + e.search(val) == -1 + else + e != val + ) + else if @value == val + return @value = '' + return hasValue: (val) -> - if @type in ['multiselect','tree'] - val in @value + findMatch = undefined + ref = undefined + if (ref = @type) == 'multiselect' or ref == 'tree' + findMatch = @value.findIndex((e) -> + if typeof e == 'string' + e.search(val) != -1 + else + e == val + ) + if findMatch != -1 + { + 'bidValue': @value[findMatch] + 'selectStatus': true + } + else + { 'selectStatus': false } else - val is @value + val == @value buildOutputData: (_, skipBeforeOutput) -> value = switch @type @@ -273,7 +320,7 @@ module.exports = class ModelField extends ModelBase @option @value, selected:true else if Array.isArray @value for v in @value - existingOption = null #required to clear out previously found values + existingOption = null existingOption = o for o in @options when o.value is v unless existingOption @option v, selected:true @@ -282,7 +329,8 @@ module.exports = class ModelField extends ModelBase @clear purgeDefaults if clear if inData? @value = @beforeInput jiff.clone inData - @ensureValueInOptions() + #HUB-2766 this is no longer necessary as we now have biding changing option + #@ensureValueInOptions() renderTemplate: () -> if typeof @template is 'object' @@ -291,5 +339,4 @@ module.exports = class ModelField extends ModelBase template = @parent.child(@template).value try @value = Mustache.render template, @root.data - catch #just don't crash. Validator will display error later. - + catch \ No newline at end of file diff --git a/src/modelFieldTree.coffee b/src/modelFieldTree.coffee index 2e33f15..8898410 100644 --- a/src/modelFieldTree.coffee +++ b/src/modelFieldTree.coffee @@ -6,10 +6,12 @@ module.exports = class ModelFieldTree extends ModelField super option: (optionParams...) -> - optionObject = @buildParamObject optionParams, ['path', 'value', 'selected'] + optionObject = @buildParamObject optionParams, ['path', 'value', 'selected', 'bidAdj', 'bidAdjFlag'] optionObject.value ?= optionObject.id - optionObject.value ?= optionObject.path.join ' > ' - optionObject.title = optionObject.path.join '>' #use path as the key since that is what is rendered. + if optionObject.value == null && Array.isArray(optionObject.path) + optionObject.value ?= optionObject.path.join ' > ' + optionObject.title = optionObject.path.join '>' #use path as the key since that is what is rendered. + super optionObject clear: (purgeDefaults=false) -> diff --git a/src/modelOption.coffee b/src/modelOption.coffee index c713655..4aeba65 100644 --- a/src/modelOption.coffee +++ b/src/modelOption.coffee @@ -8,6 +8,7 @@ module.exports = class ModelOption extends ModelBase @setDefault 'title', @get 'value' # selected is used to set default value and also to store current value. @setDefault 'selected', false + # set default bid adjustment @setDefault 'path', [] #for tree. Might should move to subclass super @@ -15,6 +16,6 @@ module.exports = class ModelOption extends ModelBase # this change likely comes from parent value changing, so be careful not to infinitely recurse. @on 'change:selected', -> if @selected - @parent.addOptionValue @value + @parent.addOptionValue @value, @bidAdj else # not selected @parent.removeOptionValue @value \ No newline at end of file