PageSourceSearch

https://salvadoreantours.com/wp-content/plugins/ninja-forms/assets/js/min/front-end.js?ver=3.4.34.2

js salvadoreantours.com collected 2026-10-02 17:42:54 UTC 199,431 bytes, 5,741 lines download raw bytes

vendor: 4,507 bytes, lines 1-110
1(function () {
2/**
3 * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved.
4 * Available via the MIT or new BSD license.
5 * see: http://github.com/jrburke/almond for details
6 */
7//Going sloppy to avoid 'use strict' string cost, but strict practices should
8//be followed.
9/*jslint sloppy: true */
10/*global setTimeout: false */
11
12var requirejs, require, define;
13(function (undef) {
14    var main, req, makeMap, handlers,
15        defined = {},
16        waiting = {},
17        config = {},
18        defining = {},
19        hasOwn = Object.prototype.hasOwnProperty,
20        aps = [].slice,
21        jsSuffixRegExp = /\.js$/;
22
23    function hasProp(obj, prop) {
24        return hasOwn.call(obj, prop);
25    }
26
27    /**
28     * Given a relative module name, like ./something, normalize it to
29     * a real name that can be mapped to a path.
30     * @param {String} name the relative name
31     * @param {String} baseName a real name that the name arg is relative
32     * to.
33     * @returns {String} normalized name
34     */
35    function normalize(name, baseName) {
36        var nameParts, nameSegment, mapValue, foundMap, lastIndex,
37            foundI, foundStarMap, starI, i, j, part,
38            baseParts = baseName && baseName.split("/"),
39            map = config.map,
40            starMap = (map && map['*']) || {};
41
42        //Adjust any relative paths.
43        if (name && name.charAt(0) === ".") {
44            //If have a base name, try to normalize against it,
45            //otherwise, assume it is a top-level require that will
46            //be relative to baseUrl in the end.
47            if (baseName) {
48                name = name.split('/');
49                lastIndex = name.length - 1;
50
51                // Node .js allowance:
52                if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
53                    name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
54                }
55
56                //Lop off the last part of baseParts, so that . matches the
57                //"directory" and not name of the baseName's module. For instance,
58                //baseName of "one/two/three", maps to "one/two/three.js", but we
59                //want the directory, "one/two" for this normalization.
60                name = baseParts.slice(0, baseParts.length - 1).concat(name);
61
62                //start trimDots
63                for (i = 0; i < name.length; i += 1) {
64                    part = name[i];
65                    if (part === ".") {
66                        name.splice(i, 1);
67                        i -= 1;
68                    } else if (part === "..") {
69                        if (i === 1 && (name[2] === '..' || name[0] === '..')) {
70                            //End of the line. Keep at least one non-dot
71                            //path segment at the front so it can be mapped
72                            //correctly to disk. Otherwise, there is likely
73                            //no path mapping for a path starting with '..'.
74                            //This can still fail, but catches the most reasonable
75                            //uses of ..
76                            break;
77                        } else if (i > 0) {
78                            name.splice(i - 1, 2);
79                            i -= 2;
80                        }
81                    }
82                }
83                //end trimDots
84
85                name = name.join("/");
86            } else if (name.indexOf('./') === 0) {
87                // No baseName, so this is ID is resolved relative
88                // to baseUrl, pull off the leading dot.
89                name = name.substring(2);
90            }
91        }
92
93        //Apply map config if available.
94        if ((baseParts || starMap) && map) {
95            nameParts = name.split('/');
96
97            for (i = nameParts.length; i > 0; i -= 1) {
98                nameSegment = nameParts.slice(0, i).join("/");
99
100                if (baseParts) {
101                    //Find the longest baseName segment match in the config.
102                    //So, do joins on the biggest to smallest lengths of baseParts.
103                    for (j = baseParts.length; j > 0; j -= 1) {
104                        mapValue = map[baseParts.slice(0, j).join('/')];
105
106                        //baseName segment has  config, find if it has one for
107                        //this name.
108                        if (mapValue) {
109                            mapValue = mapValue[nameSegment];
110                            if (mapValue) {
vendor: 8,126 bytes, lines 111-348
111                                //Match, update name to the new value.
112                                foundMap = mapValue;
113                                foundI = i;
114                                break;
115                            }
116                        }
117                    }
118                }
119
120                if (foundMap) {
121                    break;
122                }
123
124                //Check for a star map match, but just hold on to it,
125                //if there is a shorter segment match later in a matching
126                //config, then favor over this star map.
127                if (!foundStarMap && starMap && starMap[nameSegment]) {
128                    foundStarMap = starMap[nameSegment];
129                    starI = i;
130                }
131            }
132
133            if (!foundMap && foundStarMap) {
134                foundMap = foundStarMap;
135                foundI = starI;
136            }
137
138            if (foundMap) {
139                nameParts.splice(0, foundI, foundMap);
140                name = nameParts.join('/');
141            }
142        }
143
144        return name;
145    }
146
147    function makeRequire(relName, forceSync) {
148        return function () {
149            //A version of a require function that passes a moduleName
150            //value for items that may need to
151            //look up paths relative to the moduleName
152            var args = aps.call(arguments, 0);
153
154            //If first arg is not require('string'), and there is only
155            //one arg, it is the array form without a callback. Insert
156            //a null so that the following concat is correct.
157            if (typeof args[0] !== 'string' && args.length === 1) {
158                args.push(null);
159            }
160            return req.apply(undef, args.concat([relName, forceSync]));
161        };
162    }
163
164    function makeNormalize(relName) {
165        return function (name) {
166            return normalize(name, relName);
167        };
168    }
169
170    function makeLoad(depName) {
171        return function (value) {
172            defined[depName] = value;
173        };
174    }
175
176    function callDep(name) {
177        if (hasProp(waiting, name)) {
178            var args = waiting[name];
179            delete waiting[name];
180            defining[name] = true;
181            main.apply(undef, args);
182        }
183
184        if (!hasProp(defined, name) && !hasProp(defining, name)) {
185            throw new Error('No ' + name);
186        }
187        return defined[name];
188    }
189
190    //Turns a plugin!resource to [plugin, resource]
191    //with the plugin being undefined if the name
192    //did not have a plugin prefix.
193    function splitPrefix(name) {
194        var prefix,
195            index = name ? name.indexOf('!') : -1;
196        if (index > -1) {
197            prefix = name.substring(0, index);
198            name = name.substring(index + 1, name.length);
199        }
200        return [prefix, name];
201    }
202
203    /**
204     * Makes a name map, normalizing the name, and using a plugin
205     * for normalization if necessary. Grabs a ref to plugin
206     * too, as an optimization.
207     */
208    makeMap = function (name, relName) {
209        var plugin,
210            parts = splitPrefix(name),
211            prefix = parts[0];
212
213        name = parts[1];
214
215        if (prefix) {
216            prefix = normalize(prefix, relName);
217            plugin = callDep(prefix);
218        }
219
220        //Normalize according
221        if (prefix) {
222            if (plugin && plugin.normalize) {
223                name = plugin.normalize(name, makeNormalize(relName));
224            } else {
225                name = normalize(name, relName);
226            }
227        } else {
228            name = normalize(name, relName);
229            parts = splitPrefix(name);
230            prefix = parts[0];
231            name = parts[1];
232            if (prefix) {
233                plugin = callDep(prefix);
234            }
235        }
236
237        //Using ridiculous property names for space reasons
238        return {
239            f: prefix ? prefix + '!' + name : name, //fullName
240            n: name,
241            pr: prefix,
242            p: plugin
243        };
244    };
245
246    function makeConfig(name) {
247        return function () {
248            return (config && config.config && config.config[name]) || {};
249        };
250    }
251
252    handlers = {
253        require: function (name) {
254            return makeRequire(name);
255        },
256        exports: function (name) {
257            var e = defined[name];
258            if (typeof e !== 'undefined') {
259                return e;
260            } else {
261                return (defined[name] = {});
262            }
263        },
264        module: function (name) {
265            return {
266                id: name,
267                uri: '',
268                exports: defined[name],
269                config: makeConfig(name)
270            };
271        }
272    };
273
274    main = function (name, deps, callback, relName) {
275        var cjsModule, depName, ret, map, i,
276            args = [],
277            callbackType = typeof callback,
278            usingExports;
279
280        //Use name if no relName
281        relName = relName || name;
282
283        //Call the callback to define the module, if necessary.
284        if (callbackType === 'undefined' || callbackType === 'function') {
285            //Pull out the defined dependencies and pass the ordered
286            //values to the callback.
287            //Default to [require, exports, module] if no deps
288            deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
289            for (i = 0; i < deps.length; i += 1) {
290                map = makeMap(deps[i], relName);
291                depName = map.f;
292
293                //Fast path CommonJS standard dependencies.
294                if (depName === "require") {
295                    args[i] = handlers.require(name);
296                } else if (depName === "exports") {
297                    //CommonJS module spec 1.1
298                    args[i] = handlers.exports(name);
299                    usingExports = true;
300                } else if (depName === "module") {
301                    //CommonJS module spec 1.1
302                    cjsModule = args[i] = handlers.module(name);
303                } else if (hasProp(defined, depName) ||
304                           hasProp(waiting, depName) ||
305                           hasProp(defining, depName)) {
306                    args[i] = callDep(depName);
307                } else if (map.p) {
308                    map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
309                    args[i] = defined[depName];
310                } else {
311                    throw new Error(name + ' missing ' + depName);
312                }
313            }
314
315            ret = callback ? callback.apply(defined[name], args) : undefined;
316
317            if (name) {
318                //If setting exports via "module" is in play,
319                //favor that over return value and exports. After that,
320                //favor a non-undefined return value over exports use.
321                if (cjsModule && cjsModule.exports !== undef &&
322                        cjsModule.exports !== defined[name]) {
323                    defined[name] = cjsModule.exports;
324                } else if (ret !== undef || !usingExports) {
325                    //Use the return value from the function.
326                    defined[name] = ret;
327                }
328            }
329        } else if (name) {
330            //May just be an object definition for the module. Only
331            //worry about defining if have a module name.
332            defined[name] = callback;
333        }
334    };
335
336    requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
337        if (typeof deps === "string") {
338            if (handlers[deps]) {
339                //callback in this case is really relName
340                return handlers[deps](callback);
341            }
342            //Just return the module wanted. In this scenario, the
343            //deps arg is the module name, and second arg (if passed)
344            //is just the relName.
345            //Normalize module name, if it contains . or ..
346            return callDep(makeMap(deps, callback).f);
347        } else if (!deps.splice) {
348            //deps is a config object, not an array.
349            config = deps;
350            if (config.deps) {
351                req(config.deps, config.callback);
352            }
353            if (!callback) {
354                return;
355            }
356
357            if (callback.splice) {
358                //callback is an array, which means it is a dependency list.
359                //Adjust args if there are dependencies
360                deps = callback;
361                callback = relName;
362                relName = null;
363            } else {
364                deps = undef;
365            }
366        }
367
368        //Support require(['a'])
369        callback = callback || function () {};
370
371        //If relName is a function, it is an errback handler,
372        //so remove it.
373        if (typeof relName === 'function') {
374            relName = forceSync;
375            forceSync = alt;
376        }
377
378        //Simulate async callback;
379        if (forceSync) {
380            main(undef, deps, callback, relName);
381        } else {
382            //Using a non-zero value because of concern for what old browsers
383            //do, and latest browsers "upgrade" to 4 if lower value is used:
384            //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
385            //If want a value immediately, use require('id') instead -- something
386            //that works in almond on the global level, but not guaranteed and
387            //unlikely to work in other AMD implementations.
388            setTimeout(function () {
389                main(undef, deps, callback, relName);
390            }, 4);
391        }
392
393        return req;
394    };
395
396    /**
397     * Just drops the config on the floor, but returns req in case
398     * the config return value is used.
399     */
400    req.config = function (cfg) {
401        return req(cfg);
402    };
403
404    /**
405     * Expose module registry for debugging and tooling
406     */
407    requirejs._defined = defined;
408
409    define = function (name, deps, callback) {
410        if (typeof name !== 'string') {
411            throw new Error('See almond README: incorrect module build, no module name');
412        }
413
414        //This module may not have dependencies
415        if (!deps.splice) {
416            //deps is not an array, so probably means
417            //an object literal or factory function for
418            //the value. Adjust args.
419            callback = deps;
420            deps = [];
421        }
422
423        if (!hasProp(defined, name) && !hasProp(waiting, name)) {
424            waiting[name] = [name, deps, callback];
425        }
426    };
427
428    define.amd = {
429        jQuery: true
430    };
431}());
432
433define("../lib/almond", function(){});
434
435define( 'models/fieldErrorModel',[], function() {
436	var model = Backbone.Model.extend( {
437
438	} );
439	
440	return model;
441} );
442define( 'models/fieldErrorCollection',['models/fieldErrorModel'], function( errorModel ) {
443	var collection = Backbone.Collection.extend( {
444		model: errorModel
445	} );
446	return collection;
447} );
448define( 'models/fieldModel',['models/fieldErrorCollection'], function( fieldErrorCollection ) {
449	var model = Backbone.Model.extend( {
450		defaults: {
451			placeholder: '',
452			value: '',
453			label_pos: '',
454			classes: 'ninja-forms-field',
455			reRender: false,
456			mirror_field: false,
457			confirm_field: false,
458			clean: true,
459			disabled: '',
460			visible: true,
461			invalid: false
462		},
463
464		initialize: function() {
465			var type = this.get('type');
466
467			this.set( 'formID', this.collection.options.formModel.get( 'id' ) );
468			this.listenTo( nfRadio.channel( 'form-' + this.get( 'formID' ) ), 'reset', this.resetModel );
469
470    		this.bind( 'change', this.changeModel, this );
471    		this.bind( 'change:value', this.changeValue, this );
472    		this.set( 'errors', new fieldErrorCollection() );
473
474			if (type === 'listimage') {
475				this.get = this.listimageGet;
476				this.set = this.listimageSet;
477			}
478
479    		/*
480			 * Trigger an init event on two channels:
481			 * 
482			 * fields
483			 * field-type
484			 *
485			 * This lets specific field types modify model attributes before anything uses them.
486			 */
487			nfRadio.channel( 'fields' ).trigger( 'init:model', this );
488			nfRadio.channel( this.get( 'type' ) ).trigger( 'init:model', this );
489			nfRadio.channel( 'fields-' + this.get( 'type' ) ).trigger( 'init:model', this );
490
491			if( 'undefined' != this.get( 'parentType' ) ){
492				nfRadio.channel( this.get( 'parentType' ) ).trigger( 'init:model', this );
493			}
494
495			/*
496			 * When we load our form, fire another event for this field.
497			 */
498			this.listenTo( nfRadio.channel( 'form-' + this.get( 'formID' ) ), 'loaded', this.formLoaded );
499		
500			/*
501			 * Before we submit our form, send out a message so that this field can be modified if necessary.
502			 */
503			this.listenTo( nfRadio.channel( 'form-' + this.get( 'formID' ) ), 'before:submit', this.beforeSubmit );
504		},
505
506		listimageGet: function(attr) {
507            if(attr === 'options') {
508					attr = 'image_options';
509			}
510
511            return Backbone.Model.prototype.get.call(this, attr);
512		},
513		
514		listimageSet: function(attributes, options) {
515			if ('options' === attributes) {
516				attributes = 'image_options';
517			}
518			return Backbone.Model.prototype.set.call(this, attributes, options);
519		},
520
521		changeModel: function() {
522			nfRadio.channel( 'field-' + this.get( 'id' ) ).trigger( 'change:model', this );
523			nfRadio.channel( this.get( 'type' ) ).trigger( 'change:model', this );
524			nfRadio.channel( 'fields' ).trigger( 'change:model', this );
525		},
526
527		changeValue: function() {
528			nfRadio.channel( 'field-' + this.get( 'id' ) ).trigger( 'change:modelValue', this );
529			nfRadio.channel( this.get( 'type' ) ).trigger( 'change:modelValue', this );
530			nfRadio.channel( 'fields' ).trigger( 'change:modelValue', this );
531		},
532
533		addWrapperClass: function( cl ) {
534			this.set( 'addWrapperClass', cl );
535		},
536
537		removeWrapperClass: function( cl ) {
538			this.set( 'removeWrapperClass', cl );
539		},
540
541		setInvalid: function( invalid ) {
542			this.set( 'invalid', invalid );
543		},
544
545		formLoaded: function() {
546			nfRadio.channel( 'fields' ).trigger( 'formLoaded', this );
547			nfRadio.channel( 'fields-' + this.get( 'type' ) ).trigger( 'formLoaded', this );
548		},
549
550		beforeSubmit: function( formModel ) {
551			nfRadio.channel( this.get( 'type' ) ).trigger( 'before:submit', this );
552			nfRadio.channel( 'fields' ).trigger( 'before:submit', this );
553		}
554
555	} );
556
557	return model;
558} );
559
560define( 'models/fieldCollection',['models/fieldModel'], function( fieldModel ) {
561	var collection = Backbone.Collection.extend( {
562		model: fieldModel,
563		comparator: 'order',
564
565		initialize: function( models, options ) {
566			this.options = options;
567            this.on( 'reset', function( fieldCollection ){
568                nfRadio.channel( 'fields' ).trigger( 'reset:collection', fieldCollection );
569            }, this );
570		},
571
572		validateFields: function() {
573			_.each( this.models, function( fieldModel ) {
574				// added here for help with multi-part part validation
575				fieldModel.set( 'clean', false );
576				nfRadio.channel( 'submit' ).trigger( 'validate:field', fieldModel );
577			}, this );
578		},
579
580		showFields: function() {
581			this.invoke( 'set', { visible: true } );
582            this.invoke( function() {
583                this.trigger( 'change:value', this );
584            });
585		},
586
587		hideFields: function() {
588			this.invoke( 'set', { visible: false } );
589            this.invoke( function() {
590                this.trigger( 'change:value', this );
591            });
592		}
593	} );
594	return collection;
595} );
596
597define( 'models/formErrorModel',[], function() {
598	var model = Backbone.Model.extend( {
599
600	} );
601	
602	return model;
603} );
604define( 'models/formErrorCollection',['models/formErrorModel'], function( errorModel ) {
605	var collection = Backbone.Collection.extend( {
606		model: errorModel
607	} );
608	return collection;
609} );
610define( 'models/formModel',[
611	'models/fieldCollection',
612	'models/formErrorCollection'
613	], function(
614		FieldCollection,
615		ErrorCollection
616	) {
617	var model = Backbone.Model.extend({
618		defaults: {
619			beforeForm: '',
620			afterForm: '',
621			beforeFields: '',
622			afterFields: '',
623			wrapper_class: '',
624			element_class: '',
625			hp: '',
626			fieldErrors: {},
627			extra: {}
628		},
629
630		initialize: function() {
631			// Loop over settings and map to attributes
632			_.each( this.get( 'settings' ), function( value, setting ) {
633				this.set( setting, value );
634			}, this );
635
636			this.set( 'loadedFields', this.get( 'fields' ) );
637			this.set( 'fields', new FieldCollection( this.get( 'fields' ), { formModel: this } ) );
638			this.set( 'errors', new ErrorCollection() );
639
640			/*
641			 * Send out a radio message so that anyone who wants to filter our content data can register their filters.
642			 */
643			nfRadio.channel( 'form' ).trigger( 'before:filterData', this );
644
645			/*
646			 * Set our formContentData to our form setting 'formContentData'
647			 */
648			var formContentData = this.get( 'formContentData' );
649
650			/*
651			 * The formContentData variable used to be fieldContentsData.
652			 * If we don't have a 'formContentData' setting, check to see if we have an old 'fieldContentsData'.
653			 * 
654			 * TODO: This is for backwards compatibility and should be removed eventually. 
655			 */
656			if ( ! formContentData ) {
657				formContentData = this.get( 'fieldContentsData' );
658			}
659			
660			var formContentLoadFilters = nfRadio.channel( 'formContent' ).request( 'get:loadFilters' );
661			/* 
662			* Get our first filter, this will be the one with the highest priority.
663			*/
664			var sortedArray = _.without( formContentLoadFilters, undefined );
665			var callback = _.first( sortedArray );
666			formContentData = callback( formContentData, this, this );
667			
668			this.set( 'formContentData', formContentData );
669
670			nfRadio.channel( 'forms' ).trigger( 'init:model', this );
671			nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'init:model', this );
672
673			// Fields
674			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'get:fieldByKey', this.getFieldByKey, this );
675
676			// Form Errors
677			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'add:error',    this.addError, this    );
678			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'remove:error', this.removeError, this );
679
680			// Extra Data
681			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'get:extra',    this.getExtra,    this );
682			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'add:extra',    this.addExtra,    this );
683			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'remove:extra', this.removeExtra, this );
684		
685			// Respond to requests to get this model.
686			nfRadio.channel( 'form-' + this.get( 'id' ) ).reply( 'get:form', 	 this.getForm, 	   this );
687
688			nfRadio.channel( 'form' ).trigger( 'loaded', this );
689			nfRadio.channel( 'form' ).trigger( 'after:loaded', this );
690			nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'loaded', 	 this );
691		},
692
693		/*
694		 |--------------------------------------------------------------------------
695		 | Fields
696		 |--------------------------------------------------------------------------
697		 */
698
699		getFieldByKey: function( key ) {
700			return this.get( 'fields' ).findWhere( { key: key } );
701		},
702
703		/*
704		 |--------------------------------------------------------------------------
705		 | Form Errors
706		 |--------------------------------------------------------------------------
707		 */
708
709		addError: function( id, msg ) {
710			var errors = this.get( 'errors' );
711			errors.add( { id: id, msg: msg } );
712			nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'add:error', this, id, msg );
713		},
714
715		removeError: function( id ) {
716			var errors = this.get( 'errors' );
717			var errorModel = errors.get( id );
718			errors.remove( errorModel );
719			nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'remove:error', this, id );
720		},
721
722		/*
723		 |--------------------------------------------------------------------------
724		 | Extra Data
725		 |--------------------------------------------------------------------------
726		 */
727
728		getExtra: function( key ) {
729			var extraData = this.get( 'extra' );
730			if( 'undefined' == typeof key ) return extraData;
731			return extraData[ key ];
732		},
733
734		addExtra: function( key, value ) {
735			var extraData = this.get( 'extra' );
736			extraData[ key ] = value;
737			nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'add:extra', this, key, value );
738		},
739
740		removeExtra: function( key ) {
741			var extraData = this.get( 'extra' );
742			delete extraData[ key ];
743			nfRadio.channel( 'form-' + this.get( 'id' ) ).trigger( 'remove:extra', this, key );
744		},
745
746		/*
747		 |--------------------------------------------------------------------------
748		 | Get this form
749		 |--------------------------------------------------------------------------
750		 */
751		getForm: function() {
752			return this;
753		}
754	} );
755
756	return model;
757} );
758define( 'models/formCollection',['models/formModel'], function( formModel ) {
759	var collection = Backbone.Collection.extend( {
760		model: formModel
761	} );
762	return collection;
763} );
764/*
765 * Handles setting up our form.
766 *
767 * Holds a collection of our fields.
768 * Replies to requests for field data.
769 * Updates field models.
770 */
771define('controllers/formData',['models/formModel', 'models/formCollection', 'models/fieldCollection', 'models/formErrorCollection'], function( FormModel, FormCollection, FieldCollection, ErrorCollection ) {
772	var controller = Marionette.Object.extend( {
773		initialize: function() {
774
775			/*
776			 * Setup our field collections.
777			 */
778			var that = this;
779
780			/*
781			 * Initialize our form collection (incase we have multiple forms on the page)
782			 */
783			this.collection = new FormCollection( nfForms );
784
785			nfRadio.channel( 'forms' ).trigger( 'loaded', this.collection );
786			nfRadio.channel( 'app' ).trigger( 'forms:loaded', this.collection );
787
788			nfRadio.channel( 'app' ).reply( 'get:form', this.getForm, this );
789			nfRadio.channel( 'app' ).reply( 'get:forms', this.getForms, this );
790
791			nfRadio.channel( 'fields' ).reply( 'get:field', this.getField, this );
792		},
793
794		getForm: function( id ) {
795			return this.collection.get( id );
796		},
797
798		getForms: function() {
799			return this.collection;
800		},
801
802		getField: function( id ) {
803			var model = false;
804			
805			_.each( this.collection.models, function( form ) {
806				if ( ! model ) {
807					model = form.get( 'fields' ).get( id );	
808				}			
809			} );
810			return model;
811		}
812	});
813
814	return controller;
815} );
816
817define('controllers/fieldError',['models/fieldErrorModel'], function( fieldErrorModel ) {
818	var controller = Marionette.Object.extend( {
819		initialize: function() {
820			nfRadio.channel( 'fields' ).reply( 'add:error', this.addError );
821			nfRadio.channel( 'fields' ).reply( 'remove:error', this.removeError );
822			nfRadio.channel( 'fields' ).reply( 'get:error', this.getError );
823		},
824
825		addError: function( targetID, id, msg ) {
826			var model = nfRadio.channel( 'fields' ).request( 'get:field', targetID );
827
828			if( 'undefined' == typeof model ) return;
829
830			var errors = model.get( 'errors' );
831			errors.add( { 'id': id, 'msg' : msg } );
832			model.set( 'errors', errors );
833			model.trigger( 'change:errors', model );
834			model.set( 'clean', false );
835			nfRadio.channel( 'fields' ).trigger( 'add:error', model, id, msg );
836		},
837
838		removeError: function( targetID, id ) {
839			var model = nfRadio.channel( 'fields' ).request( 'get:field', targetID );
840
841			if( 'undefined' == typeof model ) return;
842
843			var errors = model.get( 'errors' );
844			var targetError = errors.get( id );
845			if ( 'undefined' != typeof targetError ) {
846				errors.remove( targetError );
847				model.set( 'errors', errors );
848				model.trigger( 'change:errors', model );
849				nfRadio.channel( 'fields' ).trigger( 'remove:error', model, id );
850			}
851		},
852
853		getError: function( targetID, id ) {
854			var model = nfRadio.channel( 'fields' ).request( 'get:field', targetID );
855			var errors = model.get( 'errors' );
856			var targetError = errors.get( id );
857			if ( 'undefined' != targetError ) {
858				return targetError;
859			} else {
860				return false;
861			}
862		}
863	});
864
865	return controller;
866} );
867/**
868 * Controller responsible for replying to a Radio request stating that a field has been changed.
869 *
870 * This controller sends out a message to the field-specific channel, the field type channel,
871 * and the public fields channel so that the data model can be updated.
872 */
873
874define('controllers/changeField',[], function() {
875	var controller = Marionette.Object.extend( {
876
877		initialize: function() {
878			/*
879			 * Reply to our request for changing a field.
880			 */
881			nfRadio.channel( 'nfAdmin' ).reply( 'change:field', this.changeField );
882
883			/*
884			 * If we blur our field, set the model attribute of 'clean' to false.
885			 * 'clean' tracks whether or not the user has every interacted with this element.
886			 * Some validation, like required, uses this to decide whether or not to add an error.
887			 */
888			this.listenTo( nfRadio.channel( 'fields' ), 'blur:field', this.blurField );
889		},
890
891		changeField: function( el, model ) {
892			// Get our current value.
893			var value = nfRadio.channel( model.get( 'type' ) ).request( 'before:updateField', el, model );
894			value = ( 'undefined' != typeof value ) ? value : nfRadio.channel( model.get( 'parentType' ) ).request( 'before:updateField', el, model );
895			value = ( 'undefined' != typeof value ) ? value : jQuery( el ).val();
896
897			// Set our 'isUpdated' flag to false.
898			model.set( 'isUpdated', false );
899
900			// Set our 'clean' flag to false.
901			model.set( 'clean', false );
902
903			/*
904			 * Send out a message saying that we've changed a field.
905			 * The first channel is field id/key specific.
906			 * The second channel is the field type, i.e. text, email, radio
907			 * The third channel is a generic 'field' channel.
908			 *
909			 * If the submitted value you wish to store in the data model isn't the same as the value received above,
910			 * you can set that model in the actions below and set the 'isUpdated' model attribute to true.
911			 * i.e. model.set( 'isUpdated', true );
912			 */
913			nfRadio.channel( 'field-' + model.get( 'id' ) ).trigger( 'change:field', el, model );
914			nfRadio.channel( model.get( 'type' ) ).trigger( 'change:field', el, model );
915			nfRadio.channel( 'fields' ).trigger( 'change:field', el, model );
916
917			/*
918			 * Send a request out on our nfAdmin channel to update our field model.
919			 * If the field model has a 'isUpdated' property of false, nothing will be updated.
920			 */
921			nfRadio.channel( 'nfAdmin' ).request( 'update:field', model, value );
922		},
923
924		blurField: function( el, model ) {
925			// Set our 'clean' flag to false.
926			model.set( 'clean', false );
927		}
928	});
929
930	return controller;
931} );
932define('controllers/changeEmail',[], function() {
933	var radioChannel = nfRadio.channel( 'email' );
934	// var emailReg = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
935	var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
936	var errorID = 'invalid-email';
937
938	var controller = Marionette.Object.extend( {
939
940		initialize: function() {
941			this.listenTo( radioChannel, 'change:modelValue', this.onChangeModelValue );
942			this.listenTo( radioChannel, 'keyup:field', this.emailKeyup );
943			this.listenTo( radioChannel, 'blur:field', this.onBlurField );
944		},
945
946		onChangeModelValue: function( model ) {
947			var value = model.get( 'value' );
948			var fieldID = model.get( 'id' );
949			this.emailChange( value, fieldID );
950		},
951
952		onBlurField: function( el, model ) {
953			var value = jQuery( el ).val();
954			var fieldID = model.get( 'id' );
955			this.emailChange( value, fieldID );
956		},
957
958		emailChange: function( value, fieldID ) {
959			if ( 0 < value.length ) {
960				if( emailReg.test( value ) ) {
961					nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
962				} else {
963					var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
964					var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
965					nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeEmailErrorMsg );
966				}				
967			} else {
968				nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
969			}
970		},
971
972		/**
973		 * When a user types inside of an email field, track their keypresses and add the appropriate class.
974		 * If the value validates as an email, add a class of nf-pass
975		 * If the value does not validate as email, add a class of nf-fail
976		 * 
977		 * @since  3.0
978		 * @param  {object} el    Element that triggered the keyup event.
979		 * @param  {object} model Model connected to the element that triggered the event
980		 * @return {void}
981		 */
982		emailKeyup: function( el, model, keyCode ) {
983			
984			/*
985			 * If we pressed the 'tab' key to get to this field, return false.
986			 */
987			if ( 9 == keyCode ) {
988				return false;
989			}
990			/*
991			 * Get the current value from our element.
992			 */
993			var value = jQuery( el ).val();
994
995			/*
996			 * Get our current ID
997			 */
998			var fieldID = model.get( 'id' );
999
1000			/*
1001			 * Check our value to see if it is a valid email.
1002			 */
1003			if ( 0 == value.length ) {
1004				nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
1005			} else if ( ! emailReg.test( value ) && ! model.get( 'clean' ) ) {
1006
1007				var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
1008				var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1009				nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeEmailErrorMsg );
1010
1011				model.removeWrapperClass( 'nf-pass' );
1012			} else if ( emailReg.test( value ) ) {
1013				nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
1014				/*
1015				 * Add nf-pass class to the wrapper.
1016				 */
1017				model.addWrapperClass( 'nf-pass' );
1018				model.set( 'clean', false );
1019			}
1020		}
1021	});
1022
1023	return controller;
1024} );
1025define('controllers/changeDate',[], function() {
1026	var radioChannel = nfRadio.channel( 'date' );
1027	var errorID = 'invalid-date';
1028
1029	var controller = Marionette.Object.extend( {
1030
1031		initialize: function() {
1032			this.listenTo( radioChannel, 'change:modelValue', this.onChangeModelValue );
1033			this.listenTo( radioChannel, 'keyup:field', this.dateKeyup );
1034			this.listenTo( radioChannel, 'blur:field', this.onBlurField );
1035		},
1036
1037		onChangeModelValue: function( model ) {
1038			this.dateChange( model );
1039		},
1040
1041		onBlurField: function( el, model ) {
1042			this.dateChange( model );
1043		},
1044
1045		dateChange: function( model ) {
1046			var fieldID = model.get( 'id' );
1047			var value = model.get( 'value' );
1048			var format = model.get( 'date_format' );
1049
1050			if( 'default' === format) {
1051				format = nfi18n.dateFormat;
1052			}
1053
1054			if ( 0 < value.length ) {
1055				// use moment's isValid to check against the fields format setting
1056				if( moment( value, format ).isValid() ) {
1057					nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
1058				} else {
1059					var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
1060					var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1061					nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeDateErrorMsg );
1062				}
1063			} else {
1064				nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
1065			}
1066		},
1067
1068		/**
1069		 * When a user types inside of an dat field, track their keypresses
1070		 * and add the appropriate class.
1071		 * If the value validates as an date, add a class of nf-pass
1072		 * If the value does not validate as date, add a class of nf-fail
1073		 *
1074		 * @since  3.0
1075		 * @param  {object} el    Element that triggered the keyup event.
1076		 * @param  {object} model Model connected to the element that triggered the event
1077		 * @return {void}
1078		 */
1079		dateKeyup: function( el, model, keyCode ) {
1080
1081			/*
1082			 * If we pressed the 'tab' key to get to this field, return false.
1083			 */
1084			if ( 9 == keyCode ) {
1085				return false;
1086			}
1087			/*
1088			 * Get the current value from our element.
1089			 */
1090			var value = jQuery( el ).val();
1091
1092			/*
1093			 * Get our current ID
1094			 */
1095			var fieldID = model.get( 'id' );
1096
1097			/*
1098			* Get our current date format
1099			 */
1100			var format = model.get( 'date_format' );
1101
1102			if( 'default' === format) {
1103				format = nfi18n.dateFormat;
1104			}
1105
1106			/*
1107			 * Check our value to see if it is a valid email.
1108			 */
1109			if ( 0 == value.length ) {
1110				nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
1111			}
1112			// use moment's isValid to check against the fields format setting
1113			else if ( ! moment( value, format ).isValid() && ! model.get( 'clean' ) ) {
1114
1115				var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
1116				var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1117				nfRadio.channel( 'fields' ).request( 'add:error', fieldID, errorID, formModel.get( 'settings' ).changeDateErrorMsg );
1118
1119				model.removeWrapperClass( 'nf-pass' );
1120			}
1121			// use moment's isValid to check against the fields format setting
1122			else if ( moment( value, format ).isValid() ) {
1123				nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, errorID );
1124				/*
1125				 * Add nf-pass class to the wrapper.
1126				 */
1127				model.addWrapperClass( 'nf-pass' );
1128				model.set( 'clean', false );
1129			}
1130		}
1131	});
1132
1133	return controller;
1134} );
1135define('controllers/fieldCheckbox',[], function() {
1136	var controller = Marionette.Object.extend( {
1137		initialize: function() {
1138			/*
1139			 * When we init our checkbox model, register our renderClasses() function
1140			 */
1141			this.listenTo( nfRadio.channel( 'checkbox' ), 'init:model', this.registerRenderClasses );
1142
1143			nfRadio.channel( 'checkbox' ).reply( 'validate:required', this.validateRequired );
1144			nfRadio.channel( 'checkbox' ).reply( 'validate:modelData', this.validateModelData );
1145            nfRadio.channel( 'checkbox' ).reply( 'before:updateField', this.beforeUpdateField, this );
1146            nfRadio.channel( 'checkbox' ).reply( 'get:calcValue', this.getCalcValue, this );
1147		},
1148
1149		beforeUpdateField: function( el, model ) {
1150			var checked = jQuery( el ).prop( 'checked' );
1151			if ( checked ) {
1152				var value = 1;
1153				jQuery( el ).addClass( 'nf-checked' );
1154				jQuery( el ).closest( '.field-wrap' ).find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' );
1155			} else {
1156				var value = 0;
1157				jQuery( el ).removeClass( 'nf-checked' );
1158				jQuery( el ).closest( '.field-wrap' ).find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).removeClass( 'nf-checked-label' );
1159			}
1160
1161			return value;
1162		},
1163
1164		validateRequired: function( el, model ) {
1165			return el[0].checked;
1166		},
1167
1168		validateModelData: function( model ) {
1169			return model.get( 'value' ) != 0;
1170		},
1171
1172		getCalcValue: function( fieldModel ) {
1173			if ( 1 == fieldModel.get( 'value' ) ) {
1174				calcValue = fieldModel.get( 'checked_calc_value' );
1175			} else {
1176				calcValue = fieldModel.get( 'unchecked_calc_value' );
1177			}
1178
1179			return calcValue;
1180		},
1181
1182		registerRenderClasses: function( model ) {
1183			if ( 'checked' == model.get( 'default_value' ) ) {
1184				model.set( 'value', 1 );
1185			} else {
1186				model.set( 'value', 0 );
1187			}
1188			model.set( 'customClasses', this.customClasses );
1189			model.set( 'customLabelClasses', this.customLabelClasses );
1190			model.set( 'maybeChecked', this.maybeChecked );
1191		},
1192
1193		customClasses: function( classes ) {
1194			if ( 1 == this.value || ( this.clean && 'undefined' != typeof this.default_value && 'checked' == this.default_value ) ) {
1195				classes += ' nf-checked';
1196			} else {
1197				classes.replace( 'nf-checked', '' );
1198			}
1199			return classes;
1200		},
1201
1202		customLabelClasses: function( classes ) {
1203			if ( 1 == this.value || ( this.clean && 'undefined' != typeof this.default_value && 'checked' == this.default_value ) ) {
1204				classes += ' nf-checked-label';
1205			} else {
1206				classes.replace( 'nf-checked-label', '' );
1207			}
1208			return classes;
1209		},
1210
1211		maybeChecked: function() {
1212			if ( 1 == this.value || ( this.clean && 'undefined' != typeof this.default_value && 'checked' == this.default_value ) ) {
1213				return ' checked';
1214			} else {
1215				return '';
1216			}
1217		}
1218	});
1219
1220	return controller;
1221} );
1222define('controllers/fieldCheckboxList',[], function() {
1223    var controller = Marionette.Object.extend( {
1224        initialize: function() {
1225            this.listenTo( nfRadio.channel( 'listcheckbox' ), 'init:model', this.register );
1226            this.listenTo( nfRadio.channel( 'terms' ), 'init:model', this.register );
1227            nfRadio.channel( 'listcheckbox' ).reply( 'before:updateField', this.beforeUpdateField, this );
1228            nfRadio.channel( 'terms' ).reply( 'before:updateField', this.beforeUpdateField, this );
1229            nfRadio.channel( 'listcheckbox' ).reply( 'get:calcValue', this.getCalcValue, this );
1230            nfRadio.channel( 'terms' ).reply( 'get:calcValue', this.getCalcValue, this );
1231        },
1232
1233        register: function( model ) {
1234            model.set( 'renderOptions', this.renderOptions );
1235            model.set( 'renderOtherText', this.renderOtherText );
1236            model.set( 'selected', [] );
1237
1238            /*
1239             * When we init a model, we need to set our 'value' to the selected option's value.
1240             * This is the list equivalent of a 'default value'.
1241             */ 
1242            if ( 0 != model.get( 'options' ).length ) {
1243                var selected = _.filter( model.get( 'options' ), function( opt ) { return 1 == opt.selected } );
1244                selected = _.map( selected, function( opt ) { return opt.value } );
1245            }
1246
1247            /*
1248            * This part is re-worked to take into account custom user-meta
1249            * values for fields.
1250             */
1251	        var savedVal = model.get( 'value' );
1252	        if( 'undefined' !== typeof savedVal && Array.isArray( savedVal ) ) {
1253		        model.set( 'value', savedVal );
1254	        } else if ( 'undefined' != typeof selected ) {
1255		        model.set( 'value', selected );
1256	        }
1257        },
1258
1259        renderOptions: function() {
1260            var html = '';
1261
1262            if ( '' == this.value || ( Array.isArray( this.value ) && 0 < this.value.length )
1263                || 0 < this.value.length ) {
1264                var valueFound = true;
1265            } else {
1266                var valueFound = false;
1267            }
1268
1269            _.each( this.options, function( option, index ) {
1270                if( Array.isArray( this.value ) ) {
1271                	if( Array.isArray( this.value[ 0 ] ) && -1 !== _.indexOf( this.value[ 0 ], option.value ) ) {
1272                		valueFound = true;
1273	                }
1274                    else if( _.indexOf( this.value, option.value ) ) {
1275                        valueFound = true;
1276	                }
1277                }
1278
1279                if ( option.value == this.value ) {
1280                    valueFound = true;
1281                }
1282
1283                /*
1284                 * TODO: This is a bandaid fix for making sure that each option has a "visible" property.
1285                 * This should be moved to creation so that when an option is added, it has a visible property by default.
1286                 */
1287                if ( 'undefined' == typeof option.visible ) {
1288                    option.visible = true;
1289                }
1290
1291                option.fieldID = this.id;
1292                option.classes = this.classes;
1293                option.index = index;
1294
1295                var selected = false;
1296				/*
1297				* This part has been re-worked to account for values passed in
1298				* via custom user-meta ( a la User Mgmt add-on)
1299				 */
1300	            if( Array.isArray( this.value ) && 0 < this.value.length ) {
1301	            	if ( -1 !== _.indexOf( this.value[ 0 ].split( ',' ), option.value )
1302		                || -1 !== _.indexOf( this.value, option.value ) ) {
1303			            selected = true;
1304	            	}
1305	            } else if ( ! _.isArray( this.value ) && option.value == this.value ) {
1306		            selected = true;
1307	            } else if ( ( 1 == option.selected && this.clean ) && 'undefined' === typeof this.value ) {
1308		            selected = true;
1309	            }
1310
1311
1312                // else if( ( option.selected && "0" != option.selected ) && this.clean ){
1313	            //     isSelected = true;
1314	            // } else {
1315	            //     var testValues = _.map( this.value, function( value ) {
1316	            //         return value.toString();
1317	            //     } );
1318	            //
1319	            //     option.isSelected = ( -1 != testValues.indexOf( option.value.toString() ) );
1320	            // }
1321	            option.selected = selected;
1322	            option.isSelected = selected;
1323	            option.required = this.required;
1324                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listcheckbox-option' );
1325                html += template( option );
1326            }, this );
1327
1328            if ( 1 == this.show_other ) {
1329                if ( 'nf-other' == this.value ) {
1330                    valueFound = false;
1331                }
1332                var data = {
1333                    fieldID: this.id,
1334                    classes: this.classes,
1335                    currentValue: this.value,
1336                    renderOtherText: this.renderOtherText,
1337                    valueFound: valueFound
1338                };
1339
1340                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listcheckbox-other' );
1341                html += template( data );
1342
1343            }
1344
1345            return html;
1346        },
1347
1348        renderOtherText: function() {
1349            if ( 'nf-other' == this.currentValue || ! this.valueFound ) {
1350                if ( 'nf-other' == this.currentValue ) {
1351                    this.currentValue = '';
1352                }
1353                var data = {
1354                    fieldID: this.fieldID,
1355                    classes: this.classes,
1356                    currentValue: this.currentValue
1357                };
1358                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listcheckbox-other-text' );
1359                return template( data );
1360            }
1361        },
1362
1363        getCalcValue: function( fieldModel ) {
1364            var calc_value = 0;
1365            var options = fieldModel.get( 'options' );
1366            if ( 0 != options.length ) {
1367                _.each( fieldModel.get( 'value' ), function( val ) {
1368                    var tmp_opt = _.find( options, function( opt ) { return opt.value == val } );
1369                    calc_value = Number( calc_value ) + Number( tmp_opt.calc );
1370                } );
1371            }
1372            return calc_value;
1373        },
1374
1375        beforeUpdateField: function( el, model ) {
1376            var selected = model.get( 'value' ) || [];
1377            if ( typeof selected == 'string' ) selected = [ selected ];
1378
1379            var value = jQuery( el ).val();
1380            var checked = jQuery( el ).prop( 'checked' );
1381            if ( checked ) {
1382                selected.push( value );
1383                jQuery( el ).addClass( 'nf-checked' );
1384                jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' );
1385            } else {
1386                jQuery( el ).removeClass( 'nf-checked' );
1387                jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).removeClass( 'nf-checked-label' );
1388                var i = selected.indexOf( value );
1389                if( -1 != i ){
1390                    selected.splice( i, 1 );
1391                } else if ( Array.isArray( selected ) ) {
1392                	var optionArray = selected[0].split( ',' );
1393                	var valueIndex = optionArray.indexOf( value );
1394                	if( -1 !== valueIndex) {
1395                		optionArray.splice( valueIndex, 1 );
1396	                }
1397                	selected = optionArray.join( ',' );
1398                }
1399            }
1400
1401            // if ( 1 == model.get( 'show_other' ) ) {
1402            //     model.set( 'reRender', true );
1403            // }
1404
1405            return _.clone( selected );
1406        }
1407    });
1408
1409    return controller;
1410} );
1411define('controllers/fieldImageList',[], function() {
1412    var controller = Marionette.Object.extend( {
1413        initialize: function() {
1414            this.listenTo( nfRadio.channel( 'listimage' ), 'init:model', this.register );
1415            nfRadio.channel( 'listimage' ).reply( 'before:updateField', this.beforeUpdateField, this );
1416            nfRadio.channel( 'listimage' ).reply( 'get:calcValue', this.getCalcValue, this );
1417        },
1418
1419        register: function( model ) {
1420            model.set( 'renderOptions', this.renderOptions );
1421            model.set( 'renderOtherText', this.renderOtherText );
1422            model.set( 'selected', [] );
1423
1424            /*
1425             * When we init a model, we need to set our 'value' to the selected option's value.
1426             * This is the list equivalent of a 'default value'.
1427             */ 
1428            if ( 0 != model.get( 'image_options' ).length ) {
1429                var selected = _.filter( model.get( 'image_options' ), function( opt ) { return 1 == opt.selected } );
1430                selected = _.map( selected, function( opt ) { return opt.value } );
1431            }
1432
1433            /*
1434            * This part is re-worked to take into account custom user-meta
1435            * values for fields.
1436             */
1437	        var savedVal = model.get( 'value' );
1438	        if( 'undefined' !== typeof savedVal && Array.isArray( savedVal ) ) {
1439		        model.set( 'value', savedVal );
1440	        } else if ( 'undefined' != typeof selected ) {
1441		        model.set( 'value', selected );
1442	        }
1443        },
1444
1445        renderOptions: function() {
1446            var html = '';
1447            
1448            if ( '' == this.value || ( Array.isArray( this.value ) && 0 < this.value.length )
1449                || 0 < this.value.length ) {
1450                var valueFound = true;
1451            } else {
1452                var valueFound = false;
1453            }
1454
1455            if (this.allow_multi_select === 1) {
1456                this.old_classname = 'list-checkbox';
1457                this.image_type = 'checkbox';
1458            } else {
1459                this.image_type = 'radio';
1460            }
1461
1462            if(this.list_orientation === 'horizontal') {
1463                this.flex_direction = 'row';
1464            } else {
1465                this.flex_direction = 'column';
1466            }
1467            var that = this;
1468
1469            var num_columns = parseInt(this.num_columns) || 1;
1470            var current_column = 1;
1471            var current_row = 1;
1472            
1473            _.each( this.image_options, function( image, index ) {
1474                if (!this.show_option_labels) {
1475                    image.label = '';
1476                }
1477                if( Array.isArray( this.value ) ) {
1478                	if( Array.isArray( this.value[ 0 ] ) && -1 !== _.indexOf( this.value[ 0 ], image.value ) ) {
1479                		valueFound = true;
1480	                }
1481                    else if( _.indexOf( this.value, image.value ) ) {
1482                        valueFound = true;
1483	                }
1484                }
1485
1486                if ( image.value == this.value ) {
1487                    valueFound = true;
1488                }
1489
1490                /*
1491                 * TODO: This is a bandaid fix for making sure that each option has a "visible" property.
1492                 * This should be moved to creation so that when an option is added, it has a visible property by default.
1493                 */
1494                if ( 'undefined' == typeof image.visible ) {
1495                    image.visible = true;
1496                }
1497                
1498                if(that.list_orientation === 'horizontal' && current_column <= num_columns) {
1499                    image.styles = "margin:auto;grid-column: " + current_column + "; grid-row = " + current_row;
1500
1501                    if(current_column === num_columns) {
1502                        current_column = 1;
1503                        current_row += 1;
1504                    } else {
1505                        current_column += 1;
1506                    }
1507                }
1508
1509                image.image_type = that.image_type; 
1510                image.fieldID = this.id;
1511                image.classes = this.classes;
1512                image.index = index;
1513
1514                var selected = false;
1515				/*
1516				* This part has been re-worked to account for values passed in
1517				* via custom user-meta ( a la User Mgmt add-on)
1518				 */
1519	            if( Array.isArray( this.value ) && 0 < this.value.length ) {
1520	            	if ( -1 !== _.indexOf( this.value[ 0 ].split( ',' ), image.value )
1521		                || -1 !== _.indexOf( this.value, image.value ) ) {
1522			            selected = true;
1523	            	}
1524	            } else if ( ! _.isArray( this.value ) && image.value == this.value ) {
1525		            selected = true;
1526	            } else if ( ( 1 == image.selected && this.clean ) && ('undefined' === typeof this.value || '' === this.value)) {
1527		            selected = true;
1528	            }
1529
1530	            image.selected = selected;
1531	            image.isSelected = selected;
1532	            image.required = this.required;
1533                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listimage-option' );
1534                html += template( image );
1535            }, this );
1536
1537            if ( 1 == this.show_other ) {
1538                if ( 'nf-other' == this.value ) {
1539                    valueFound = false;
1540                }
1541                var data = {
1542                    fieldID: this.id,
1543                    classes: this.classes,
1544                    value: this.value,
1545                    currentValue: this.value,
1546                    renderOtherText: this.renderOtherText,
1547                    valueFound: valueFound
1548                };
1549
1550                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listimage-other' );
1551                html += template( data );
1552
1553            }
1554
1555            return html;
1556        },
1557
1558        renderOtherText: function() {
1559            if ( 'nf-other' == this.currentValue || ! this.valueFound ) {
1560                if ( 'nf-other' == this.currentValue ) {
1561                    this.currentValue = '';
1562                }
1563                var data = {
1564                    fieldID: this.fieldID,
1565                    classes: this.classes,
1566                    currentValue: this.currentValue
1567                };
1568                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listimage-other-text' );
1569                return template( data );
1570            }
1571        },
1572
1573        getCalcValue: function( fieldModel ) {
1574			var calc_value = 0;
1575			var options = fieldModel.get( 'options' );
1576			if ( 0 != options.length ) {
1577				/*
1578				 * Check to see if this is a multi-select list.
1579				 */
1580				if ( 1 == parseInt( fieldModel.get( 'allow_multi_select' ) ) ) {
1581					/*
1582					 * We're using a multi-select, so we need to check out any selected options and add them together.
1583					 */
1584					_.each( fieldModel.get( 'value' ), function( val ) {
1585						var tmp_opt = _.find( options, function( opt ) { return opt.value == val } );
1586						calc_value += Number( tmp_opt.calc );
1587					} );
1588				} else {
1589					/*
1590					 * We are using a single select, so our selected option is in the 'value' attribute.
1591					 */
1592					var selected = _.find( options, function( opt ) { return fieldModel.get( 'value' ) == opt.value } );
1593					/*
1594					 * If we have a selcted value, use it.
1595					 */
1596					if ( 'undefined' !== typeof selected ) {
1597                        calc_value = selected.calc;
1598					}	
1599				}
1600			}
1601			return calc_value;
1602        },
1603
1604        beforeUpdateField: function( el, model ) {
1605
1606            if(model.get('allow_multi_select') !== 1) {
1607                var selected = jQuery( el ).val();
1608                var options = model.get('image_options');
1609                _.each(options, function(option, index) {
1610                    if(option.value === selected) {
1611                        option.isSelected = true;
1612                        option.selected = true;
1613                    } else {
1614                        option.isSelected = false;
1615                        option.selected = false;
1616                    }
1617                    if(!option.isSelected) {
1618                        option.selected = false;
1619                        jQuery("#nf-field-" + option.fieldID + "-" + index).removeClass('nf-checked');
1620                        jQuery("#nf-label-field-" + option.fieldID + "-" + index).removeClass('nf-checked-label');
1621                    } else {
1622                        jQuery("#nf-field-" + option.fieldID + "-" + index).addClass('nf-checked');
1623                        jQuery("#nf-label-field-" + option.fieldID + "-" + index).addClass('nf-checked-label');
1624                    }
1625                });
1626            } else {
1627                var selected = model.get( 'value' ) || [];
1628                if ( typeof selected == 'string' ) selected = [ selected ];
1629                var value = jQuery( el ).val();
1630                var checked = jQuery( el ).prop( 'checked' );
1631                if ( checked ) {
1632                    selected.push( value );
1633                    jQuery( el ).addClass( 'nf-checked' );
1634                    jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' );
1635                } else {
1636                    jQuery( el ).removeClass( 'nf-checked' );
1637                    jQuery( el ).parent().find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).removeClass( 'nf-checked-label' );
1638                    var i = selected.indexOf( value );
1639                    if( -1 != i ){
1640                        selected.splice( i, 1 );
1641                    } else if ( Array.isArray( selected ) ) {
1642                        var optionArray = selected[0].split( ',' );
1643                        var valueIndex = optionArray.indexOf( value );
1644                        if( -1 !== valueIndex) {
1645                            optionArray.splice( valueIndex, 1 );
1646                        }
1647                        selected = optionArray.join( ',' );
1648                    }
1649                }
1650            }
1651
1652            return _.clone( selected );
1653        }
1654    });
1655
1656    return controller;
1657} );
1658define('controllers/fieldRadio',[], function() {
1659	var controller = Marionette.Object.extend( {
1660		initialize: function() {
1661			this.listenTo( nfRadio.channel( 'listradio' ), 'change:modelValue', this.changeModelValue );
1662			this.listenTo( nfRadio.channel( 'listradio' ), 'init:model', this.register );
1663			nfRadio.channel( 'listradio' ).reply( 'get:calcValue', this.getCalcValue, this );
1664			
1665			this.listenTo( nfRadio.channel( 'listradio' ), 'change:field', this.updateCheckedClass, this );
1666		},
1667
1668		register: function( model ) {
1669			model.set( 'renderOptions', this.renderOptions );
1670			model.set( 'renderOtherText', this.renderOtherText );
1671			/*
1672			 * When we init a model, we need to set our 'value' to the selected option's value.
1673			 * This is the list equivalent of a 'default value'.
1674			 */ 
1675			if ( 0 != model.get( 'options' ).length ) {
1676				/*
1677				 * Check to see if we have a selected value.
1678				 */
1679				var selected = _.find( model.get( 'options' ), function( opt ) { return 1 == opt.selected } );
1680
1681				if ( 'undefined' != typeof selected ) {
1682					model.set( 'value', selected.value );
1683				}
1684			}
1685		},
1686
1687		changeModelValue: function( model ) {
1688			if ( 1 == model.get( 'show_other' ) ) {
1689				// model.set( 'reRender', true );
1690				model.trigger( 'reRender');
1691			}
1692		},
1693
1694		renderOptions: function() {
1695			var html = '';
1696			if ( '' == this.value ) {
1697				var valueFound = true;
1698			} else {
1699				var valueFound = false;
1700			}
1701			
1702			_.each( this.options, function( option, index ) {
1703				if ( option.value == this.value ) {
1704					valueFound = true;
1705				}
1706
1707				/*
1708                 * TODO: This is a bandaid fix for making sure that each option has a "visible" property.
1709                 * This should be moved to creation so that when an option is added, it has a visible property by default.
1710                 */
1711                if ( 'undefined' == typeof option.visible ) {
1712                    option.visible = true;
1713                }
1714
1715                option.selected = false;
1716				option.fieldID = this.id;
1717				option.classes = this.classes;
1718				option.currentValue = this.value;
1719				option.index = index;
1720				option.required = this.required;
1721
1722				/*
1723				 * If we haven't edited this field yet, use the default checked
1724				 */
1725				if ( this.clean && 1 == this.selected ) {
1726					option.selected = true;
1727				} else if ( this.value == option.value ) {
1728					option.selected = true;
1729				} else {
1730					option.selected = false;
1731				}
1732
1733				var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listradio-option' );
1734
1735				html += template( option );
1736			}, this );
1737
1738			if ( 1 == this.show_other ) {
1739				if ( 'nf-other' == this.value ) {
1740					valueFound = false;
1741				}
1742				var data = {
1743					fieldID: this.id,
1744					classes: this.classes,
1745					currentValue: this.value,
1746					renderOtherText: this.renderOtherText,
1747					valueFound: valueFound
1748				};
1749				var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listradio-other' );
1750				html += template( data );
1751			}
1752
1753			return html;
1754		},
1755
1756		renderOtherText: function() {
1757			if ( 'nf-other' == this.currentValue || ! this.valueFound ) {
1758				if ( 'nf-other' == this.currentValue ) {
1759					this.currentValue = '';
1760				}
1761				var data = {
1762					fieldID: this.fieldID,
1763					classes: this.classes,
1764					currentValue: this.currentValue
1765				};
1766				var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listradio-other-text' );
1767				return template( data );
1768			}
1769		},
1770
1771		getCalcValue: function( fieldModel ) {
1772			
1773            /*
1774             * Default to 0, in case we have no selection.
1775             */
1776            var calc_value = 0;
1777            
1778			if ( 0 != fieldModel.get( 'options' ).length ) {
1779				/*
1780				 * Check to see if we have a selected value.
1781				 */
1782				var selected = _.find( fieldModel.get( 'options' ), function( opt ) { return fieldModel.get( 'value' ) == opt.value } );
1783				if ( 'undefined' !== typeof selected ) {
1784                    calc_value = selected.calc;
1785				}
1786
1787			}
1788			return calc_value;
1789		},
1790
1791		updateCheckedClass: function( el, model ) {
1792			jQuery( '[name="' + jQuery( el ).attr( 'name' ) + '"]' ).removeClass( 'nf-checked' );
1793			jQuery( el ).closest( 'ul' ).find( 'label' ).removeClass( 'nf-checked-label' );
1794			jQuery( el ).addClass( 'nf-checked' );
1795			jQuery( el ).closest( 'li' ).find( 'label[for="' + jQuery( el ).prop( 'id' ) + '"]' ).addClass( 'nf-checked-label' );
1796
1797
1798		}
1799
1800	});
1801
1802	return controller;
1803} );
1804define('controllers/fieldNumber',[], function() {
1805    var controller = Marionette.Object.extend( {
1806        initialize: function() {
1807            this.listenTo( nfRadio.channel( 'number' ), 'init:model', this.maybeMinDefault );
1808            this.listenTo( nfRadio.channel( 'number' ), 'keyup:field', this.validateMinMax );
1809        },
1810
1811        maybeMinDefault: function( model ) {
1812
1813            if( '' == model.get( 'value' ) && '' == model.get( 'placeholder' ) ){
1814                var min = model.get( 'num_min' );
1815                model.set( 'placeholder', min );
1816            }
1817        },
1818
1819        validateMinMax: function( el, model ) {
1820            var $el = jQuery( el );
1821            var value = parseFloat( $el.val() );
1822            var min = $el.attr( 'min' );
1823            var max = $el.attr( 'max' );
1824            var step = parseFloat( $el.attr( 'step' ) );
1825
1826            if( min && value < min ){
1827                var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'id' ) );
1828                var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1829                nfRadio.channel( 'fields' ).request( 'add:error', model.get( 'id' ), 'number-min', formModel.get( 'settings' ).fieldNumberNumMinError );
1830            } else {
1831                nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'number-min' );
1832            }
1833
1834            if ( max && value > max ){
1835                var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'id' ) );
1836                var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1837                nfRadio.channel( 'fields' ).request( 'add:error', model.get( 'id' ), 'number-max', formModel.get( 'settings' ).fieldNumberNumMaxError );
1838            } else {
1839                nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'number-max' );
1840            }
1841
1842            var testValue = Math.round( parseFloat( value ) * 1000000000 );
1843            var testStep = Math.round( parseFloat( step ) * 1000000000  );
1844
1845            if( value && 0 !== testValue % testStep ){
1846                var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'id' ) );
1847                var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1848                nfRadio.channel( 'fields' ).request( 'add:error', model.get( 'id' ), 'number-step', formModel.get( 'settings' ).fieldNumberIncrementBy + step );
1849            } else {
1850                nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'number-step' );
1851            }
1852        }
1853
1854    });
1855
1856    return controller;
1857} );
1858define( 'controllers/mirrorField',[], function() {
1859	var radioChannel = nfRadio.channel( 'fields' );
1860
1861	var controller = Marionette.Object.extend( {
1862		listeningModel: '',
1863
1864		initialize: function() {
1865			this.listenTo( radioChannel, 'init:model', this.registerMirror );
1866		},
1867
1868		registerMirror: function( model ) {
1869			if ( model.get( 'mirror_field' ) ) {
1870				this.listeningModel = model;
1871				var targetID = model.get( 'mirror_field' );
1872				this.listenTo( nfRadio.channel( 'field-' + targetID ), 'change:modelValue', this.changeValue );
1873			}
1874		},
1875
1876		changeValue: function( targetModel ) {
1877			this.listeningModel.set( 'value', targetModel.get( 'value' ) );
1878			// this.listeningModel.set( 'reRender', true );
1879			this.listeningModel.trigger( 'reRender' );
1880		}
1881	});
1882
1883	return controller;
1884} );
1885define( 'controllers/confirmField',[], function() {
1886	var radioChannel = nfRadio.channel( 'fields' );
1887	var errorID = 'confirm-mismatch';
1888
1889	var controller = Marionette.Object.extend( {
1890
1891		initialize: function() {
1892			this.listenTo( radioChannel, 'init:model', this.registerConfirm );
1893			this.listenTo( radioChannel, 'keyup:field', this.confirmKeyup );
1894		},
1895
1896		registerConfirm: function( confirmModel ) {
1897			if ( ! confirmModel.get( 'confirm_field' ) ) return;
1898
1899			this.listenTo( nfRadio.channel( 'form' ), 'loaded', function( formModal ){
1900				this.registerConfirmListeners( confirmModel );
1901			});
1902		},
1903
1904		registerConfirmListeners: function( confirmModel ) {
1905			
1906			var targetModel = nfRadio.channel( 'form-' + confirmModel.get( 'formID' ) ).request( 'get:fieldByKey', confirmModel.get( 'confirm_field' ) );
1907
1908			//TODO: Add better handling for password confirm fields on the front end.
1909			if( 'undefined' == typeof targetModel ) return;
1910
1911			targetModel.set( 'confirm_with', confirmModel.get( 'id' ) );
1912			this.listenTo( nfRadio.channel( 'field-' + targetModel.get( 'id' ) ), 'change:modelValue', this.changeValue );
1913			this.listenTo( nfRadio.channel( 'field-' + confirmModel.get( 'id' ) ), 'change:modelValue', this.changeValue );
1914		},
1915
1916		changeValue: function( model ) {
1917			if ( 'undefined' == typeof model.get( 'confirm_with' ) ) {
1918				var confirmModel = model;
1919				var targetModel = nfRadio.channel( 'form-' + model.get( 'formID' ) ).request( 'get:fieldByKey', confirmModel.get( 'confirm_field' ) );
1920			} else {
1921				var targetModel = model;
1922				var confirmModel = radioChannel.request( 'get:field', targetModel.get( 'confirm_with' ) );
1923			}
1924			var targetID = targetModel.get( 'id' );
1925			var confirmID = confirmModel.get( 'id' );
1926
1927			if ( '' == confirmModel.get( 'value' ) || confirmModel.get( 'value' ) == targetModel.get( 'value' ) ) {
1928				nfRadio.channel( 'fields' ).request( 'remove:error', confirmID, errorID );
1929			} else {
1930				var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', confirmID );
1931				var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1932				nfRadio.channel( 'fields' ).request( 'add:error', confirmID, errorID, formModel.get( 'settings' ).confirmFieldErrorMsg );
1933			}
1934		},
1935		
1936		confirmKeyup: function( el, model, keyCode ) {
1937
1938			var currentValue = jQuery( el ).val();
1939			if ( model.get( 'confirm_field' ) ) {
1940				var confirmModel = model;
1941				var confirmID = model.get( 'id' );
1942				var targetModel = nfRadio.channel( 'form-' + model.get( 'formID' ) ).request( 'get:fieldByKey', confirmModel.get( 'confirm_field' ) );
1943				var compareValue = targetModel.get( 'value' );
1944				var confirmValue = currentValue;
1945			} else if ( model.get( 'confirm_with' ) ) {
1946				var confirmModel = nfRadio.channel( 'fields' ).request( 'get:field', model.get( 'confirm_with' ) );
1947				var confirmID = confirmModel.get( 'id' );
1948				var confirmValue = confirmModel.get( 'value' );
1949				var compareValue = confirmValue;
1950			}
1951
1952			if ( 'undefined' !== typeof confirmModel ) {
1953				if ( '' == confirmValue ) {
1954					nfRadio.channel( 'fields' ).request( 'remove:error', confirmID, errorID );
1955				} else if ( currentValue == compareValue ) {
1956					nfRadio.channel( 'fields' ).request( 'remove:error', confirmID, errorID );
1957				} else {
1958					var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', confirmID );
1959					var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  fieldModel.get( 'formID' ) );
1960					nfRadio.channel( 'fields' ).request( 'add:error', confirmID, errorID, formModel.get( 'settings' ).confirmFieldErrorMsg );
1961				}
1962			}
1963		}
1964	});
1965
1966	return controller;
1967} );
1968define('controllers/updateFieldModel',[], function() {
1969	var controller = Marionette.Object.extend( {
1970		initialize: function() {
1971			nfRadio.channel( 'nfAdmin' ).reply( 'update:field', this.updateField );
1972		},
1973
1974		updateField: function( model, value ) {
1975			if ( ! model.get( 'isUpdated' ) ) {
1976				model.set( 'value', value );
1977				model.set( 'isUpdated', true );
1978				/*
1979				 * If we're working with an array, it won't trigger a change event on the value attribute.
1980				 * Instead, we have to manually trigger a change event.
1981				 */ 
1982				if ( _.isArray( value ) ) {
1983					model.trigger( 'change:value', model );
1984				}
1985			}
1986		}
1987	});
1988
1989	return controller;
1990} );
1991define('controllers/submitButton',['controllers/submitButton'], function( submitButton ) {
1992	var controller = Marionette.Object.extend( {
1993		bound: {},
1994
1995		initialize: function() {
1996			this.listenTo( nfRadio.channel( 'submit' ), 'init:model', this.registerHandlers );
1997		},
1998
1999		registerHandlers: function( fieldModel ) {
2000			if ( 'undefined' != typeof this.bound[ fieldModel.get( 'id' ) ] ) {
2001				return false;
2002			}
2003
2004			this.listenTo( nfRadio.channel( 'field-' + fieldModel.get( 'id' ) ), 'click:field', this.click, this );
2005			/*
2006			 * Register an interest in the 'before:submit' event of our form.
2007			 */
2008			fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', this.beforeSubmit, fieldModel );
2009			fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'submit:failed', this.resetLabel, fieldModel );
2010			fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'submit:response', this.resetLabel, fieldModel );
2011			fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'enable:submit', this.maybeEnable, fieldModel );
2012			fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'disable:submit', this.maybeDisable, fieldModel );
2013			fieldModel.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'processingLabel', this.processingLabel, fieldModel );
2014
2015			fieldModel.listenTo( nfRadio.channel( 'fields' ), 'add:error', this.maybeDisable, fieldModel );
2016			fieldModel.listenTo( nfRadio.channel( 'fields' ), 'remove:error', this.maybeEnable, fieldModel );
2017			
2018			this.bound[ fieldModel.get( 'id') ] = true;
2019		},
2020
2021		click: function( e, fieldModel ) {
2022			var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) );
2023			nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'submit', formModel );
2024		},
2025
2026		beforeSubmit: function() {
2027			this.set( 'disabled', true );
2028			nfRadio.channel( 'form-' + this.get( 'formID' ) ).trigger( 'processingLabel', this );
2029		},
2030
2031		maybeDisable: function( fieldModel ) {
2032
2033			if( 'undefined' != typeof fieldModel && fieldModel.get( 'formID' ) != this.get( 'formID' ) ) return;
2034
2035			this.set( 'disabled', true );
2036			this.trigger( 'reRender' );
2037		},
2038
2039		maybeEnable: function( fieldModel ) {
2040			/*
2041			 * If the field reporting the error is not on the same form as the submit button, return false;
2042			 */
2043			if ( 'undefined' != typeof fieldModel && fieldModel.get( 'formID' ) != this.get( 'formID' ) ) {
2044				return false;
2045			}
2046			
2047			var formModel = nfRadio.channel( 'app' ).request( 'get:form', this.get( 'formID' ) );
2048			if ( 0 == _.size( formModel.get( 'fieldErrors' ) ) ) {
2049				this.set( 'disabled', false );
2050				this.trigger( 'reRender' );
2051			}
2052		},
2053
2054		processingLabel: function() {
2055			if ( this.get( 'label' ) == this.get( 'processing_label' ) ) return false;
2056
2057			this.set( 'oldLabel', this.get( 'label' ) );
2058			this.set( 'label', this.get( 'processing_label' ) );
2059			this.trigger( 'reRender' );
2060		},
2061
2062		resetLabel: function( response ) {
2063			if ( 'undefined' != typeof response.errors &&
2064				 'undefined' != typeof response.errors.nonce &&
2065				 _.size( response.errors.nonce ) > 0 ) {
2066				if( 'undefined' != typeof response.errors.nonce.new_nonce && 'undefined' != typeof response.errors.nonce.nonce_ts ) {
2067					// Do not reset label for nonce errors, which will re-submit the form.
2068					return;
2069				}
2070			}
2071			if ( 'undefined' != typeof this.get( 'oldLabel' ) ) {
2072				this.set( 'label', this.get( 'oldLabel' ) );
2073			}
2074			this.set( 'disabled', false );
2075			this.trigger( 'reRender' );
2076		}
2077
2078	});
2079
2080	return controller;
2081} );
2082define('controllers/submitDebug',[], function() {
2083    var controller = Marionette.Object.extend( {
2084        initialize: function() {
2085            this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.submitDebug );
2086        },
2087
2088        submitDebug: function( response, textStatus, jqXHR, formID ) {
2089
2090            if( 'undefined' == typeof response.debug ) return;
2091
2092            /* Form Debug Messages */
2093            if( 'undefined' != typeof response.debug.form ) {
2094                var debugMessages = document.createElement( 'span' );
2095                _.each(response.debug.form, function (message, index) {
2096                    var messageText = document.createTextNode( message );
2097                    debugMessages.appendChild( messageText );
2098                    debugMessages.appendChild(
2099                        document.createElement( 'br' )
2100                    );
2101                });
2102                jQuery('.nf-debug-msg').html( debugMessages );
2103            }
2104
2105            /* Console Debug Messages */
2106            if( 'undefined' != typeof response.debug.console ) {
2107                var style = '';
2108                console.log( '%c%s', style, 'NINJA SUPPORT' );
2109                _.each(response.debug.console, function (message, index) {
2110                    console.log( message );
2111                });
2112                console.log( '%c%s', style, 'END NINJA SUPPORT' );
2113            }
2114        }
2115
2116    });
2117
2118    return controller;
2119} );
2120
2121define('controllers/getFormErrors',[], function() {
2122	var radioChannel = nfRadio.channel( 'fields' );
2123	var controller = Marionette.Object.extend( {
2124		initialize: function( model ) {
2125			nfRadio.channel( 'form' ).reply( 'get:errors', this.getFormErrors );
2126		},
2127
2128		getFormErrors: function( formID ) {
2129			var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID );
2130			var errors = false;
2131			
2132			if ( formModel ) {
2133				/*
2134				 * Check to see if we have any errors on our form model.
2135				 */
2136				if ( 0 !== formModel.get( 'errors' ).length ) {
2137					_.each( formModel.get( 'errors' ).models, function( error ) {
2138						errors = errors || {};
2139						errors[ error.get( 'id' ) ] = error.get( 'msg' );
2140					} );						
2141				}
2142
2143				_.each( formModel.get( 'fields' ).models, function( field ) {
2144					if ( field.get( 'type' ) != 'submit' && field.get( 'errors' ).length > 0 ) {
2145						errors = errors || {};
2146						errors[ field.get( 'id' ) ] = field.get( 'errors' );
2147					}
2148				} );
2149			}
2150			return errors;
2151		},
2152	});
2153
2154	return controller;
2155} );
2156define('controllers/validateRequired',[], function() {
2157	var controller = Marionette.Object.extend( {
2158		initialize: function() {
2159			this.listenTo( nfRadio.channel( 'fields' ), 'blur:field', this.validateRequired );
2160			this.listenTo( nfRadio.channel( 'fields' ), 'change:field', this.validateRequired );
2161			this.listenTo( nfRadio.channel( 'fields' ), 'keyup:field', this.validateKeyup );
2162
2163			this.listenTo( nfRadio.channel( 'fields' ), 'change:modelValue', this.validateModelData );
2164			this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateModelData );
2165		},
2166		
2167		validateKeyup: function( el, model, keyCode ) {
2168			if ( 1 != model.get( 'required' ) ) {
2169				return false;
2170			}
2171
2172			if ( ! model.get( 'clean' ) ) {
2173				this.validateRequired( el, model );
2174			}
2175		},
2176
2177		validateRequired: function( el, model ) {
2178			if ( 1 != model.get( 'required' ) || ! model.get( 'visible' ) ) {
2179				return false;
2180			}
2181
2182			var currentValue = jQuery( el ).val();
2183			var customReqValidation = nfRadio.channel( model.get( 'type' ) ).request( 'validate:required', el, model );
2184			var defaultReqValidation = true;
2185
2186			var maskPlaceholder = model.get( 'mask' );
2187			if ( maskPlaceholder ) {
2188				maskPlaceholder = maskPlaceholder.replace( /9/g, '_' );
2189				maskPlaceholder = maskPlaceholder.replace( /a/g, '_' );
2190				maskPlaceholder = maskPlaceholder.replace( /\*/g, '_' );
2191			}
2192
2193            // If the field has a mask...
2194            // AND that mask is equal to the current value...            
2195            if ( maskPlaceholder && currentValue === maskPlaceholder ) {
2196                // If we have a pre-existing error...
2197                if ( 0 < model.get( 'errors' ).length ) {
2198                    // Persist that error.
2199                    defaultReqValidation = false;
2200                }
2201            }
2202            // If our value is an empty string...
2203            if ( ! jQuery.trim( currentValue ) ) {
2204                // Throw an error.
2205                defaultReqValidation = false;
2206            }
2207
2208			if ( 'undefined' !== typeof customReqValidation ) {
2209				var valid = customReqValidation;
2210			} else {
2211				var valid = defaultReqValidation;
2212			}
2213
2214			this.maybeError( valid, model );
2215		},
2216
2217		validateModelData: function( model ) {
2218
2219			if ( 1 != model.get( 'required' ) || ! model.get( 'visible' ) || model.get( 'clean' ) ) {
2220				return false;
2221			}
2222
2223			/*
2224			 * If we already have a required error on this model, return false
2225			 */
2226			if ( model.get( 'errors' ).get( 'required-error' ) ) {
2227				return false;
2228			}
2229
2230			currentValue = model.get( 'value' );
2231
2232			var defaultReqValidation = true;
2233
2234			if ( ! jQuery.trim( currentValue ) ) {
2235				defaultReqValidation = false;
2236			}
2237
2238			var customReqValidation = nfRadio.channel( model.get( 'type' ) ).request( 'validate:modelData', model );
2239			if ( 'undefined' !== typeof customReqValidation ) {
2240				var valid = customReqValidation;
2241			} else {
2242				var valid = defaultReqValidation;
2243			}
2244
2245			this.maybeError( valid, model );
2246
2247		},
2248
2249		maybeError: function( valid, model ) {
2250			if ( ! valid ) {
2251
2252				var formModel  = nfRadio.channel( 'form-' + model.get( 'formID' ) ).request( 'get:form' );
2253
2254				if( 'undefined' != typeof formModel ) {
2255					nfRadio.channel('fields').request('add:error', model.get('id'), 'required-error', formModel.get('settings').validateRequiredField);
2256				}
2257			} else {
2258				nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'required-error' );
2259			}			
2260		}
2261	});
2262
2263	return controller;
2264} );
2265
2266define('controllers/submitError',[], function() {
2267	var controller = Marionette.Object.extend( {
2268		initialize: function() {
2269			this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.submitErrors );
2270		},
2271
2272		submitErrors: function( response, textStatus, jqXHR, formID ) {
2273
2274			// Check for nonce error.
2275			if ( _.size( response.errors.nonce ) > 0 ) {
2276				if( 'undefined' != typeof response.errors.nonce.new_nonce && 'undefined' != typeof response.errors.nonce.nonce_ts ) {
2277					// Update nonce from response.
2278					nfFrontEnd.ajaxNonce = response.errors.nonce.new_nonce;
2279					nfFrontEnd.nonce_ts = response.errors.nonce.nonce_ts;
2280					// Re-submit form.
2281					var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID );
2282					nfRadio.channel( 'form-' + formID ).request( 'submit', formModel );
2283				}
2284			}
2285
2286			if ( _.size( response.errors.fields ) > 0 ) {
2287				_.each( response.errors.fields, function( data, fieldID ) {
2288                    if ( typeof( data ) === 'object' ) {
2289                        nfRadio.channel( 'fields' ).request( 'add:error', fieldID, data.slug, data.message );
2290                    } else {
2291                        nfRadio.channel( 'fields' ).request( 'add:error', fieldID, 'required-error', data );
2292                    }
2293				} );
2294			}
2295
2296			if ( _.size( response.errors.form ) > 0 ) {
2297				_.each( response.errors.form, function( msg, errorID ) {
2298					nfRadio.channel( 'form-' + formID ).request( 'remove:error', errorID );
2299					nfRadio.channel( 'form-' + formID ).request( 'add:error', errorID, msg );
2300				} );
2301			}
2302
2303			if ( 'undefined' != typeof response.errors.last ) {
2304				if( 'undefined' != typeof response.errors.last.message ) {
2305					var style = 'background: rgba( 255, 207, 115, .5 ); color: #FFA700; display: block;';
2306					console.log( '%c NINJA FORMS SUPPORT: SERVER ERROR', style );
2307					console.log( response.errors.last.message );
2308					console.log( '%c END SERVER ERROR MESSAGE', style );
2309				}
2310			}
2311
2312			/**
2313			 * TODO: This needs to be re-worked for backbone. It's not dynamic enough.
2314			 */
2315			/*
2316			 * Re-show any hidden fields during a form submission re-start.
2317			 */
2318			jQuery( '#nf-form-' + formID + '-cont .nf-field-container' ).show();
2319		}
2320
2321	});
2322
2323	return controller;
2324} );
2325
2326define('controllers/actionRedirect',[], function() {
2327	var controller = Marionette.Object.extend( {
2328		initialize: function() {
2329			this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.actionRedirect );
2330		},
2331
2332		actionRedirect: function( response ) {
2333
2334			if ( 'undefined' != typeof response.data.halt && 'undefined' != typeof response.data.halt.redirect && '' != response.data.halt.redirect ) {
2335				window.location = response.data.halt.redirect;
2336			}
2337
2338			if ( _.size( response.errors ) == 0 && 'undefined' != typeof response.data.actions ) {
2339
2340				if ( 'undefined' != typeof response.data.actions.redirect && '' != response.data.actions.redirect ) {
2341					window.location = response.data.actions.redirect;
2342				}
2343			}
2344		}
2345
2346	});
2347
2348	return controller;
2349} );
2350define('controllers/actionSuccess',[], function() {
2351	var controller = Marionette.Object.extend( {
2352		initialize: function() {
2353			this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.actionSubmit );
2354		},
2355
2356		actionSubmit: function( response ) {
2357			if ( _.size( response.errors ) == 0 && 'undefined' != typeof response.data.actions ) {
2358				if ( 'undefined' != typeof response.data.actions.success_message && '' != response.data.actions.success_message ) {
2359					var form_id = response.data.form_id;
2360					var success_message = jQuery( '#nf-form-' + form_id + '-cont .nf-response-msg' );
2361					
2362					success_message.html( response.data.actions.success_message ).show();
2363					
2364					//Let's check if the success message is already fully visible in the viewport without scrolling
2365					var top_of_success_message = success_message.offset().top;
2366					var bottom_of_success_message = success_message.offset().top + success_message.outerHeight();
2367					var bottom_of_screen = jQuery(window).scrollTop() + jQuery(window).height();
2368					var top_of_screen = jQuery(window).scrollTop();
2369
2370					var the_element_is_visible = ((bottom_of_screen > bottom_of_success_message) && (top_of_screen < top_of_success_message));
2371
2372					if(!the_element_is_visible){
2373						//The element isn't visible, so let's scroll to the success message as in the previous release, but with a short animation
2374						jQuery('html, body').animate({
2375							scrollTop: ( success_message.offset().top - 50 )
2376						}, 300 );
2377					}	
2378				}
2379			}
2380		}
2381
2382	});
2383
2384	return controller;
2385} );
2386
2387define('controllers/fieldSelect',[], function() {
2388	var controller = Marionette.Object.extend( {
2389		initialize: function() {
2390
2391			this.listenTo( nfRadio.channel( 'fields' ), 'init:model', function( model ){
2392				if( 'list' == model.get( 'parentType' ) ) this.register( model );
2393			}, this );
2394
2395			nfRadio.channel( 'listselect' ).reply( 'get:calcValue', this.getCalcValue, this );
2396			nfRadio.channel( 'listmultiselect' ).reply( 'get:calcValue', this.getCalcValue, this );
2397		},
2398
2399		register: function( model ) {
2400			model.set( 'renderOptions', this.renderOptions );
2401			model.set( 'renderOtherAttributes', this.renderOtherAttributes );
2402			/*
2403			 * When we init a model, we need to set our 'value' to the selected option's value.
2404			 * This is the list equivalent of a 'default value'.
2405			 */ 
2406			if ( 0 != model.get( 'options' ).length ) {
2407				//Check to see if there is a value set for the field
2408				var savedVal = model.get( 'value' );
2409
2410				/*
2411				 * Check to see if this is a multi-select list.
2412				 */
2413				if ( 'listmultiselect' == model.get( 'type' ) ) {
2414					/*
2415					 * We're using a multi-select, so we need to check out any selected options and add them together.
2416					 */
2417					var selected = _.filter( model.get( 'options' ), function( opt ) { return 1 == opt.selected } );
2418					selected = _.map( selected, function( opt ) { return opt.value } );
2419					var value = selected;
2420				} else if ( 'listradio' !== model.get( 'type' ) ) {
2421					/*
2422					 * Check to see if we have a selected value.
2423					 */
2424					var selected = _.find( model.get( 'options' ), function( opt ) { return 1 == opt.selected } );
2425					/*
2426					 * We don't have a selected value, so use our first option.
2427					 */
2428					if ( 'undefined' == typeof selected ) {
2429						selected = _.first( model.get( 'options' ) );
2430					}
2431
2432					if ( 'undefined' != typeof selected
2433						&& 'undefined' != typeof selected.value ) {
2434						var value = selected.value;
2435					} else if ( 'undefined' != typeof selected ) {
2436						var value = selected.label;
2437					}	
2438				}
2439
2440				/*
2441	            * This part is re-worked to take into account custom user-meta
2442	            * values for fields.
2443	             */
2444				if( 'undefined' !== typeof savedVal && '' !== savedVal
2445					&& Array.isArray( savedVal ) ) {
2446					model.set( 'value', savedVal );
2447				} else if ( 'undefined' != typeof selected ) {
2448					model.set( 'value', value );
2449				}
2450			}
2451		},
2452
2453		renderOptions: function() {
2454			var html = '';
2455
2456			_.each( this.options, function( option ) {
2457				/*
2458				* This part has been re-worked to account for values passed in
2459				* via custom user-meta ( a la User Mgmt add-on)
2460				 */
2461				if ( _.isArray( this.value ) ) {
2462                    // If we have a multiselect list...
2463                    // AND it has selected values...
2464					if( 'listmultiselect' === this.type && 0 < this.value.length &&
2465						-1 != _.indexOf( this.value[ 0 ].split( ',' ), option.value ) ) {
2466						var selected = true;
2467					} else if( -1 != _.indexOf( this.value, option.value ) ) {
2468						var selected = true;
2469					}
2470				} else if ( ! _.isArray( this.value ) && option.value == this.value ) {
2471					var selected = true;
2472				} else if ( ( 1 == option.selected && this.clean )
2473					&& 'undefined' === typeof this.value ) {
2474					var selected = true;
2475				} else {
2476					var selected = false;
2477				}
2478
2479				/*
2480                 * TODO: This is a bandaid fix for making sure that each option has a "visible" property.
2481                 * This should be moved to creation so that when an option is added, it has a visible property by default.
2482                 */
2483                if ( 'undefined' == typeof option.visible ) {
2484                    option.visible = true;
2485                }
2486
2487				option.selected = selected;
2488				option.fieldID = this.id;
2489				option.classes = this.classes;
2490				option.currentValue = this.value;
2491
2492				var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-listselect-option' );
2493				html += template( option );
2494			}, this );
2495
2496			return html;
2497		},
2498
2499		renderOtherAttributes: function() {
2500			var otherAttributes = '';
2501
2502			if( 'listmultiselect' == this.type ){
2503				otherAttributes = otherAttributes + ' multiple';
2504
2505				var multiSize = this.multi_size || 5;
2506				otherAttributes = otherAttributes + ' size="' + multiSize + '"';
2507			}
2508
2509			return otherAttributes;
2510		},
2511
2512		getCalcValue: function( fieldModel ) {
2513			var calc_value = 0;
2514			var options = fieldModel.get( 'options' );
2515			if ( 0 != options.length ) {
2516				/*
2517				 * Check to see if this is a multi-select list.
2518				 */
2519				if ( 'listmultiselect' == fieldModel.get( 'type' ) ) {
2520					/*
2521					 * We're using a multi-select, so we need to check out any selected options and add them together.
2522					 */
2523					_.each( fieldModel.get( 'value' ), function( val ) {
2524						var tmp_opt = _.find( options, function( opt ) { return opt.value == val } );
2525						calc_value += Number( tmp_opt.calc );
2526					} );
2527				} else {
2528					/*
2529					 * We are using a single select, so our selected option is in the 'value' attribute.
2530					 */
2531					var selected = _.find( options, function( opt ) { return fieldModel.get( 'value' ) == opt.value } );
2532					/*
2533					 * We don't have a selected value, so use our first option.
2534					 */
2535					if ( 'undefined' == typeof selected ) {
2536						selected = fieldModel.get( 'options' )[0];
2537					}		
2538					calc_value  = selected.calc;			
2539				}
2540			}
2541			return calc_value;
2542		}
2543
2544	});
2545
2546	return controller;
2547} );
2548
2549define('controllers/coreSubmitResponse',[], function() {
2550	var controller = Marionette.Object.extend( {
2551		initialize: function() {
2552			this.listenTo( nfRadio.channel( 'forms' ), 'submit:response', this.actionSubmit );
2553		},
2554
2555		actionSubmit: function( response ) {
2556			var formModel = nfRadio.channel( 'app' ).request( 'get:form', response.data.form_id );
2557			/*
2558			 * If we have errors, don't hide or clear.
2559			 */
2560			if ( 0 != _.size( response.errors ) ) {
2561				return false;
2562			}
2563
2564			if ( 1 == response.data.settings.clear_complete ) {
2565				// nfRadio.channel( 'form-' + response.data.form_id ).trigger( 'reset' );
2566				formModel.get( 'fields' ).reset( formModel.get( 'loadedFields' ) );
2567                if ( 1 != response.data.settings.hide_complete ) {
2568                    nfRadio.channel( 'captcha' ).trigger( 'reset' );
2569                }
2570			}
2571
2572			if ( 1 == response.data.settings.hide_complete ) {
2573				/**
2574				 * TODO: This needs to be re-worked for backbone. It's not dynamic enough.
2575				 */
2576				formModel.trigger( 'hide' );
2577				// jQuery( '.nf-fields' ).hide();
2578				// jQuery( '.nf-form-title' ).hide();
2579			}
2580		}
2581
2582	});
2583
2584	return controller;
2585} );
2586define('controllers/fieldProduct',[], function() {
2587    var controller = Marionette.Object.extend( {
2588        initialize: function() {
2589            this.listenTo( nfRadio.channel( 'product' ), 'init:model', this.register );
2590            nfRadio.channel( 'product' ).reply( 'get:calcValue', this.getCalcValue, this );
2591        },
2592
2593        register: function( model ) {
2594            model.set( 'renderProductQuantity', this.renderProductQuantity );
2595            model.set( 'renderProduct', this.renderProduct );
2596            model.set( 'renderOptions', this.renderOptions );
2597        },
2598
2599        renderProduct: function(){
2600            switch( this.product_type ) {
2601                case 'user':
2602                    var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-textbox' );
2603                    return template( this );
2604                    break;
2605                case 'hidden':
2606                    var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-hidden' );
2607                    return template( this );
2608                    break;
2609
2610                case 'dropdown':
2611                    var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-product-dropdown' );
2612                    return template( this );
2613                    break;
2614                default:
2615                    var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-product-single' );
2616                    return template( this );
2617            }
2618        },
2619
2620        renderProductQuantity: function(){
2621            if ( 1 == this.product_use_quantity ) {
2622                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-product-quantity' );
2623                return template( this );
2624            }
2625        },
2626
2627        renderOptions: function() {
2628            var that = this;
2629            var html = '';
2630            _.each( this.options, function( option ) {
2631                if ( 1 == option.selected ) {
2632                    var selected = true;
2633                } else {
2634                    var selected = false;
2635                }
2636
2637                option.selected = selected;
2638                option.fieldID = that.id;
2639                option.classes = that.classes;
2640                option.currentValue = that.value;
2641
2642                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-product-' + that.product_type + '-option' );
2643                html += template( option );
2644            } );
2645
2646            return html;
2647        },
2648
2649        getCalcValue: function( fieldModel ) {
2650
2651            var product_price = fieldModel.get( 'product_price' );
2652            var product_quantity = fieldModel.get( 'value' );
2653
2654            return product_price * product_quantity;
2655        }
2656    });
2657
2658    return controller;
2659} );
2660
2661define('controllers/fieldTotal',[], function() {
2662    var controller = Marionette.Object.extend( {
2663
2664        totalModel: {},
2665
2666        productTotals: {},
2667
2668        initialize: function() {
2669            this.listenTo( nfRadio.channel( 'total' ), 'init:model', this.register );
2670            this.listenTo( nfRadio.channel( 'shipping' ), 'init:model', this.registerShipping );
2671        },
2672
2673        register: function( totalModel ){
2674            this.totalModel = totalModel;
2675
2676            var formID = totalModel.get( 'formID' );
2677            this.listenTo( nfRadio.channel( 'form-' + formID ), 'loaded', this.onFormLoaded );
2678
2679            this.listenTo( nfRadio.channel( 'product' ), 'change:modelValue', this.onChangeProduct );
2680            this.listenTo( nfRadio.channel( 'quantity' ), 'change:modelValue', this.onChangeQuantity );
2681        },
2682
2683        registerShipping: function( shippingModel ){
2684            this.shippingCost = shippingModel.get( 'shipping_cost' );
2685        },
2686
2687        onFormLoaded: function( formModel ){
2688
2689            var fieldModels = formModel.get( 'fields' ).models;
2690
2691            var productFields = {};
2692            var quantityFields = {};
2693
2694            for( var model in fieldModels ){
2695
2696                var field = fieldModels[ model ];
2697                var fieldID = field.get( 'id' );
2698
2699                // TODO: Maybe use switch
2700                if( 'product' == field.get( 'type' ) ){
2701                    productFields[ fieldID ] = field;
2702                } else if( 'quantity' == field.get( 'type' ) ){
2703                    var productID = field.get( 'product_assignment' );
2704                    quantityFields[ productID ] = field;
2705                }
2706            }
2707
2708            for( var productID in productFields ){
2709
2710                var product = productFields[ productID ];
2711
2712                var productPrice = Number( product.get( 'product_price' ) );
2713
2714                if( quantityFields[ productID ] ){
2715
2716                    productPrice *= quantityFields[ productID ].get( 'value' );
2717
2718                } else if( 1 == product.get( 'product_use_quantity' ) ){
2719
2720                    productPrice *= product.get( 'value' );
2721
2722                }
2723
2724                this.productTotals[ productID ] = productPrice;
2725            }
2726
2727            this.updateTotal();
2728        },
2729
2730        onChangeProduct: function( model ){
2731            var productID = model.get( 'id' );
2732            var productPrice = Number( model.get( 'product_price' ) );
2733            var productQuantity = Number( model.get( 'value' ) );
2734            var newTotal = productQuantity * productPrice;
2735            this.productTotals[ productID ] = newTotal;
2736
2737            this.updateTotal();
2738        },
2739
2740        onChangeQuantity: function( model ){
2741            var productID = model.get( 'product_assignment' );
2742            var productField = nfRadio.channel( 'fields' ).request( 'get:field', productID );
2743            var productPrice = Number( productField.get( 'product_price' ) );
2744
2745            var quantity = Number( model.get( 'value' ) );
2746
2747            var newTotal = quantity * productPrice;
2748
2749            this.productTotals[ productID ] = newTotal;
2750
2751            this.updateTotal();
2752        },
2753
2754        updateTotal: function(){
2755
2756            var newTotal = 0;
2757
2758            for( var product in this.productTotals ){
2759                newTotal += Number( this.productTotals[ product ] );
2760            }
2761
2762            if( newTotal && this.shippingCost ) {
2763                // Only add shipping if there is a cost.
2764                newTotal += Number(this.shippingCost);
2765            }
2766
2767            this.totalModel.set( 'value', newTotal.toFixed( 2 ) );
2768            this.totalModel.trigger( 'reRender' );
2769        }
2770    });
2771
2772    return controller;
2773});
2774define('controllers/fieldQuantity',[], function() {
2775    var controller = Marionette.Object.extend( {
2776
2777        initialize: function() {
2778            this.listenTo( nfRadio.channel( 'quantity' ), 'init:model', this.registerQuantity );
2779        },
2780
2781        registerQuantity: function( model ){
2782            var productID = model.get( 'product_assignment' );
2783            var product = nfRadio.channel( 'fields' ).request( 'get:field', productID );
2784
2785            if( product ) {
2786                product.set('product_use_quantity', 0);
2787            }
2788        },
2789
2790    });
2791
2792    return controller;
2793});
2794/**
2795 * Model that represents a calculation.
2796 *
2797 * On init, we trigger a radio message so that controllers can do things when a calc model inits.
2798 */
2799define( 'models/calcModel',[], function() {
2800	var model = Backbone.Model.extend( {
2801		initialize: function() {
2802			// Set our form id
2803			this.set( 'formID', this.collection.options.formModel.get( 'id' ) );
2804			// Set our initial fields object to empty. This will hold our key/value pairs.
2805			this.set( 'fields', {} );
2806			// Trigger a radio message to let controllers know we've inited this model.
2807			nfRadio.channel( 'calc' ).trigger( 'init:model', this );
2808			// When we change the value of this calculation, send out a radio message
2809			this.on( 'change:value', this.changeValue, this );
2810		},
2811
2812		/**
2813		 * Trigger a radio message when a field present in our calculation changes
2814		 *
2815		 * The listener that triggers/calls this function is in controllers/calculations
2816		 * 
2817		 * @since  3.0
2818		 * @return void
2819		 */
2820		changeField: function( fieldModel ) {
2821			nfRadio.channel( 'calc' ).trigger( 'change:field', this, fieldModel );
2822		},
2823
2824		changeCalc: function( targetCalcModel ) {
2825			nfRadio.channel( 'calc' ).trigger( 'change:calc', this, targetCalcModel );
2826		},
2827
2828		changeValue: function() {
2829			nfRadio.channel( 'calc' ).trigger( 'change:value', this );
2830		}
2831	} );
2832
2833	return model;
2834} );
2835
2836define( 'models/calcCollection',['models/calcModel'], function( CalcModel ) {
2837	var collection = Backbone.Collection.extend( {
2838		model: CalcModel,
2839		comparator: 'order',
2840
2841		initialize: function( models, options ) {
2842			this.options = options;
2843            _.each( models, function( model ) {
2844            	if( 'undefined' == typeof model.dec ) return;
2845                if ( '' === model.dec.toString().trim() ) model.dec = 2;
2846                model.dec = parseInt( model.dec );
2847            } );
2848			/*
2849			 * Respond to requests for our calc model
2850			 */
2851			nfRadio.channel( 'form-' + options.formModel.get( 'id' ) ).reply( 'get:calc', this.getCalc, this );
2852		},
2853
2854		getCalc: function( key ) {
2855			return this.findWhere( { name: key } );
2856		}
2857	} );
2858	return collection;
2859} );
2860/**
2861 * Controller responsible for keeping up with calculations.
2862 */
2863define('controllers/calculations',['models/calcCollection'], function( CalcCollection ) {
2864	var controller = Marionette.Object.extend( {
2865		initialize: function() {
2866			this.calcs = {};
2867			this.displayFields = {};
2868			// When our form initialises, check to see if there are any calculations that need to be tracked.
2869			this.listenTo( nfRadio.channel( 'form' ), 'loaded', this.registerCalcs );
2870            
2871            // When our collection gets reset, reset calculation tracking as well.
2872            this.listenTo( nfRadio.channel( 'fields' ), 'reset:collection', this.resetCalcs );
2873
2874			// When a calc model is initialised, run a setup function.
2875			// this.listenTo( nfRadio.channel( 'calc' ), 'init:model', this.setupCalc );
2876
2877			// When a field referenced by a calc model changes, update our calc.
2878			this.listenTo( nfRadio.channel( 'calc' ), 'change:field', this.changeField );
2879
2880			// When a calculation referenced by a calc model changes, update our calc.
2881			this.listenTo( nfRadio.channel( 'calc' ), 'change:calc', this.changeCalc );
2882
2883			/*
2884			 * Listen to our field model init for fields that want to display calc values.
2885			 * If that field has a calc merge tag, replace it with the default calc value.
2886			 */
2887			var that = this;
2888			_.each( nfFrontEnd.use_merge_tags.calculations, function( fieldType ) {
2889				that.listenTo( nfRadio.channel( 'fields-' + fieldType ), 'init:model', that.initDisplayField );
2890			} );
2891			
2892			// When we change our calc value, update any display fields.
2893			this.listenTo( nfRadio.channel( 'calc' ), 'change:value', this.updateDisplayFields );
2894
2895			// Set an init variable so that we only call reRender on the display field on change, not on init.
2896			this.init = {};
2897		},
2898        
2899        /**
2900         * Passthrough function to reset tracking of calculations when the fieldCollection is reset.
2901         * 
2902         * @since 3.2
2903         * @param backbone.collection fieldCollection
2904         * @return void
2905         */
2906        resetCalcs: function( fieldCollection ) {
2907            if( 'undefined' != typeof( fieldCollection.options.formModel ) ) {
2908                this.registerCalcs( fieldCollection.options.formModel );  
2909            }
2910        },
2911
2912		/**
2913		 * When our form loads, create a collection out of any calculations.
2914		 * 
2915		 * @since  3.0
2916		 * @param  backbone.model formModel
2917		 * @return void
2918		 */
2919		registerCalcs: function( formModel ) {
2920			var calcCollection = new CalcCollection( formModel.get( 'settings' ).calculations, { formModel: formModel } );
2921			this.calcs[ formModel.get( 'id' ) ] = calcCollection;
2922			var that = this;
2923
2924			_.each( calcCollection.models, function( calcModel ) {
2925				/*
2926				 * We set a property on our init variable for the calc model we're looping over.
2927				 * This property is set to true so that when we make changes to the calc model on the next line
2928				 * the field view doesn't try to redraw itself.
2929				 * If we don't do this, the 'reRender' attribute of the model will be set before the view is initialized,
2930				 * which means that setting 'reRender' to true will never re-render the view.
2931				 */
2932				that.init[ calcModel.get( 'name' ) ] = true;
2933				// Setup our calculation models with initial values and register listeners for calc-related fields.
2934				that.setupCalc( calcModel );
2935			} );
2936		},
2937
2938		/**
2939		 * When a calculation model is instantiated from the registerCalcs function:
2940		 *
2941		 * Use a regex to get an array of the field keys
2942		 * Setup an initial key/values array
2943		 * Check for any references to other calculations
2944		 * Set the initial value of our calculation
2945		 * 
2946		 * @since  3.0
2947		 * @param  backbone.model calcModel
2948		 * @return void
2949		 */
2950		setupCalc: function( calcModel ) {
2951			// Setup our that var so we can access 'this' context in our loop.
2952			var that = this;
2953			// Get our equation
2954			var eq = calcModel.get( 'eq' );
2955			// We want to keep our original eq intact, so we use a different var for string replacment.
2956			var eqValues = eq;
2957            // Store the name for debugging later.
2958            var calcName = calcModel.get( 'name' );
2959
2960			/* TODO:
2961			 * It might be possible to refactor these two if statements.
2962			 * The difficulty is that each has a different method of retreiving the specific data model.
2963			 */
2964			// Check to see if we have any field merge tags in our equation.
2965			var fields = eq.match( new RegExp( /{field:(.*?)}/g ) );
2966			if ( fields ) {
2967				/*
2968				 * fields is now an array of field keys that looks like:
2969				 * ['{field:key'], ['{field:key'], etc.
2970				 *
2971				 * We need to run a function with each of our field keys to setup our field key array and hook up our field change listner.
2972				 */
2973				
2974				fields = fields.map( function( field ) {
2975					// field will be {field:key}
2976					var key = field.replace( ':calc}', '' ).replace( '}', '' ).replace( '{field:', '' );
2977
2978					// Get our field model
2979					fieldModel = nfRadio.channel( 'form-' + calcModel.get( 'formID' ) ).request( 'get:fieldByKey', key );
2980
2981                    if( 'undefined' == typeof fieldModel ) return;
2982
2983                    fieldModel.set( 'clean', false );
2984
2985					// Register a listener in our field model for value changes.
2986					fieldModel.on( 'change:value', calcModel.changeField, calcModel );
2987					// Get our calc value from our field model.
2988					var calcValue = that.getCalcValue( fieldModel );
2989					// Add this field to our internal key/value object.
2990					that.updateCalcFields( calcModel, key, calcValue );
2991					// Update the string tracking our merged eq with the calc value.
2992					eqValues = that.replaceKey( 'field', key, calcValue, eqValues );
2993				} );
2994			}
2995
2996			// Check to see if we have any calc merge tags in our equation.
2997			var calcs = eq.match( new RegExp( /{calc:(.*?)}/g ) );
2998			if ( calcs ) {
2999				/*
3000				 * calcs is now an array of calc keys that looks like:
3001				 * ['{calc:key'], ['{calc:key'], etc.
3002				 *
3003				 * We need to run a function with each of our calc keys to setup our calc key array and hook up our calc change listner.
3004				 */
3005				
3006				calcs = calcs.map( function( calc ) {
3007					// calc will be {calc:name}
3008					var name = calc.replace( '}', '' ).replace( '{calc:', '' );
3009					// Get our calc model
3010					var targetCalcModel = calcModel.collection.findWhere( { name: name } );
3011
3012					if( 'undefined' == typeof targetCalcModel ) return;
3013
3014					// Listen for changes on our calcluation, since we need to update our calc when it changes.
3015					targetCalcModel.on( 'change:value', calcModel.changeCalc, calcModel );
3016					// // Get our calc value from our calc model.
3017					var calcValue = targetCalcModel.get( 'value' );
3018					// Update the string tracking our merged eq with the calc value.
3019					eqValues = that.replaceKey( 'calc', name, calcValue, eqValues );
3020				} );
3021
3022			}
3023
3024            // Scrub unmerged tags (ie deleted/nox-existent fields/calcs, etc).
3025            eqValues = eqValues.replace( /{([a-zA-Z0-9]|:|_|-)*}/g, 0 );
3026            // Scrub line breaks.
3027            eqValues = eqValues.replace( /\r?\n|\r/g, '' );
3028			// Evaluate the equation and update the value of this model.
3029			try {
3030				this.debug('Calculation Decoder ' + eqValues + ' -> ' + this.localeDecodeEquation(eqValues) + ' (Setup)');
3031				calcModel.set( 'value', Number( mexp.eval( this.localeDecodeEquation(eqValues) ) ).toFixed( calcModel.get( 'dec' ) ) );
3032			} catch( e ) {
3033                //console.log( calcName );
3034				console.log( e );
3035			}
3036            
3037            // If for whatever reason, we got NaN, reset that to 0.
3038            if( calcModel.get( 'value' ) === 'NaN' ) calcModel.set( 'value', '0' );
3039
3040			// Debugging console statement.
3041			// console.log( eqValues + ' = ' + calcModel.get( 'value' ) );
3042		},
3043
3044		/**
3045		 * Update an item in our key/value pair that represents our fields and calc values.
3046		 * 
3047		 * @since  3.0
3048		 * @param  backbone.model 	calcModel
3049		 * @param  string 			key
3050		 * @param  string 			calcValue
3051		 * @return void
3052		 */
3053		updateCalcFields: function( calcModel, key, calcValue ) {
3054			var fields = calcModel.get( 'fields' );
3055			fields[ key ] = calcValue;
3056			calcModel.set( 'fields', fields );
3057		},
3058
3059		/**
3060		 * Get a calc value from a field model.
3061		 *
3062		 * Sends a request to see if there's a special calc value
3063		 * Uses the value of the field if there is not.
3064		 * 
3065		 * @since  3.0
3066		 * @param  backbone.model fieldModel
3067		 * @return value
3068		 */
3069		getCalcValue: function( fieldModel ) {
3070			/*
3071			 * Send out a request on the field type and parent type channel asking if they need to modify the calc value.
3072			 * This is helpful for fields like lists that can have a different calc_value than selected value.
3073			 */
3074			var value = nfRadio.channel( fieldModel.get( 'type' ) ).request( 'get:calcValue', fieldModel );
3075
3076			var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point);
3077			
3078
3079			var calcValue = value || fieldModel.get( 'value' );
3080			var machineNumber = localeConverter.numberDecoder(calcValue);
3081			var formattedNumber = localeConverter.numberEncoder(calcValue);
3082
3083			if ( 'undefined' !== typeof machineNumber && jQuery.isNumeric( machineNumber ) ) {
3084				value = formattedNumber;
3085			} else {
3086				value = 0;
3087			}
3088			// }
3089
3090			if ( ! fieldModel.get( 'visible' ) ) {
3091				value = 0;
3092			}
3093		
3094			return value;
3095		},
3096
3097		/**
3098		 * Replace instances of key with calcValue. This is used to replace one key at a time.
3099		 *
3100		 * If no eq is passed, use calcModel eq.
3101		 *
3102		 * Returns a string with instances of key replaced with calcValue.
3103		 * 
3104		 * @since  version
3105		 * @param  string 	key       
3106		 * @param  string 	calcValue 
3107		 * @param  string 	eq        
3108		 * @return string 	eq      
3109		 */
3110		replaceKey: function( type, key, calcValue, eq ) {
3111			eq = eq || calcModel.get( 'eq' );
3112
3113			tag = '{' + type + ':' + key + '}';
3114			var reTag = new RegExp( tag, 'g' );
3115
3116			calcTag = '{' + type + ':' + key + ':calc}';
3117			var reCalcTag = new RegExp( calcTag, 'g' );
3118
3119			eq = eq.replace( reTag, calcValue );
3120			eq = eq.replace( reCalcTag, calcValue );
3121
3122			return eq;
3123		},
3124
3125		/**
3126		 * Takes a calcModel and returns a string eq with all keys replaced by their appropriate calcValues.
3127		 * 
3128		 * @since  3.0
3129		 * @param  backbone.model 	calcModel
3130		 * @return string			eq
3131		 */
3132		replaceAllKeys: function( calcModel ) {
3133			var eq = calcModel.get( 'eq' );
3134			var that = this;
3135			_.each( calcModel.get( 'fields' ), function( value, key ) {
3136				eq = that.replaceKey( 'field', key, value, eq );
3137			} );
3138
3139			// If we have any calc merge tags, replace those as well.
3140			var calcs = eq.match( new RegExp( /{calc:(.*?)}/g ) );
3141			if ( calcs ) {
3142				_.each( calcs, function( calc ) {
3143					// calc will be {calc:key}
3144					var name = calc.replace( '}', '' ).replace( '{calc:', '' );
3145					var targetCalcModel = calcModel.collection.findWhere( { name: name } );
3146                    if( 'undefined' == typeof targetCalcModel ) return;
3147					var re = new RegExp( calc, 'g' );
3148					eq = eq.replace( re, targetCalcModel.get( 'value' ) );
3149				} );
3150			}
3151
3152			return eq;
3153		},
3154
3155		/**
3156		 * Function that's called when a field within the calculation changes.
3157		 * 
3158		 * @since  3.0
3159		 * @param  backbone.model calcModel
3160		 * @param  backbone.model fieldModel
3161		 * @return void
3162		 */
3163		changeField: function( calcModel, fieldModel ) {
3164		
3165			var key = fieldModel.get( 'key' );
3166			var value = this.getCalcValue( fieldModel );
3167			
3168			this.updateCalcFields( calcModel, key, value );
3169			var eqValues = this.replaceAllKeys( calcModel );
3170
3171            // Scrub unmerged tags (ie deleted/nox-existent fields/calcs, etc).
3172            eqValues = eqValues.replace( /{([a-zA-Z0-9]|:|_|-)*}/g, '0' );
3173            eqValues = eqValues.replace( /\r?\n|\r/g, '' );
3174            try {
3175				this.debug('Calculation Decoder ' + eqValues + ' -> ' + this.localeDecodeEquation(eqValues) + ' (Change Field)');
3176			     calcModel.set( 'value', Number( mexp.eval( this.localeDecodeEquation(eqValues) ) ).toFixed( calcModel.get( 'dec' ) ) );
3177            } catch( e ) {
3178                if(this.debug())console.log( e );
3179            }
3180            if( calcModel.get( 'value' ) === 'NaN' ) calcModel.set( 'value', '0' );
3181
3182			// Debugging console statement.
3183			// console.log( eqValues + ' = ' + calcModel.get( 'value' ) );		
3184		},
3185
3186		initDisplayField: function( fieldModel ) {
3187
3188			if( ! fieldModel.get( 'default' ) || 'string' != typeof fieldModel.get( 'default' ) ) return;
3189
3190			var calcs = fieldModel.get( 'default' ).match( new RegExp( /{calc:(.*?)}/g ) );
3191			if ( calcs ) {
3192				_.each( calcs, function( calcName ) {
3193					calcName = calcName.replace( '{calc:', '' ).replace( '}', '' ).replace( ':2', '' );
3194					this.displayFields[ calcName ] = this.displayFields[ calcName ] || [];
3195					this.displayFields[ calcName ].push( fieldModel );
3196				}, this );
3197			}
3198		},
3199
3200		updateDisplayFields: function( calcModel ) {
3201			var that = this;
3202			if ( 'undefined' != typeof this.displayFields[ calcModel.get( 'name' ) ] ) {
3203				_.each( this.displayFields[ calcModel.get( 'name' ) ], function( fieldModel ) {
3204
3205					var value = '';
3206
3207					/**
3208					 * if we have a html field, we want to use the actual
3209					 * value and re-evaluate
3210				    **/
3211					if( "html" === fieldModel.get( 'type' ) ) {
3212						value = fieldModel.get( 'value' );
3213					} else {
3214						// if not a html field, use default to re-evaluate
3215						value = fieldModel.get( 'default' );
3216					}
3217
3218					/*
3219					 This is a fix for the issue of the merge tags being
3220					 display'd
3221					 */
3222
3223					// Find spans with calc data-key values
3224					var spans = value.match( new RegExp( /<span data-key="calc:(.*?)<\/span>/g ));
3225					_.each( spans, function( spanVar ) {
3226						// transform the span back into a merge tag
3227						var tmpCalcTag = "{" + spanVar.replace("<span" +
3228							" data-key=\"", "" ).replace( /">(.*?)<\/span>/, "" ) + "}";
3229
3230						value = value.replace( spanVar, tmpCalcTag );
3231					} );
3232					var calcs = value.match( new RegExp( /{calc:(.*?)}/g ) );
3233					_.each( calcs, function( calc ) {
3234//						var rounding = false;
3235						// calc will be {calc:key} or {calc:key:2}
3236						var name = calc.replace( '}', '' ).replace( '{calc:', '' ).replace( ':2', '' );
3237
3238						/*
3239						 * TODO: Bandaid for rounding calculations to two decimal places when displaying the merge tag.
3240						 * Checks to see if we have a :2. If we do, remove it and set our rounding variable to true.
3241						 */
3242//						if ( -1 != name.indexOf( ':2' ) ) {
3243//							rounding = true;
3244//							name = name.replace( ':2', '' );
3245//						}
3246
3247						var calcModel = that.calcs[ fieldModel.get( 'formID' ) ].findWhere( { name: name } );
3248						var re = new RegExp( calc, 'g' );
3249						var calcValue = calcModel.get( 'value' ) ;
3250//						if ( rounding ) {
3251//							calcValue = calcValue.toFixed( 2 );
3252//							rounding = false;
3253//						}
3254						
3255                        if( 'undefined' != typeof( calcValue ) ) {
3256                            calcValue = that.applyLocaleFormatting( calcValue, calcModel );
3257						}
3258                        /*
3259                         * We replace the merge tag with the value
3260						 * surrounded by a span so that we can still find it
3261						 * and not affect itself or other field merge tags
3262						 *
3263						 * Unless this isn't a html field, then we just set
3264						  * value to calcValue
3265						*/
3266                        if( "html" === fieldModel.get( 'type' ) ) {
3267	                        value = value.replace(re, "<span data-key=\"calc:" + name + "\">"
3268		                        + calcValue + "</span>");
3269                        } else {
3270                        	value = calcValue;
3271                        }
3272					} );
3273					
3274					fieldModel.set( 'value', value );
3275					if ( ! that.init[ calcModel.get( 'name' ) ] ) {
3276						// fieldModel.set( 'reRender', true );
3277						fieldModel.trigger( 'reRender' );
3278					}
3279					that.init[ calcModel.get( 'name' ) ] = false;
3280				} );
3281			}
3282		},
3283
3284		getCalc: function( name, formID ) {
3285			return this.calcs[ formID ].findWhere( { name: name } );
3286		},
3287
3288		changeCalc: function( calcModel, targetCalcModel ) {
3289			var eqValues = this.replaceAllKeys( calcModel );
3290			
3291			eqValues = eqValues.replace( '[', '' ).replace( ']', '' );
3292            eqValues = eqValues.replace( /\r?\n|\r/g, '' );
3293            try {
3294				this.debug('Calculation Decoder ' + eqValues + ' -> ' + this.localeDecodeEquation(eqValues) + ' (Change Calc)');
3295			     calcModel.set( 'value', Number( mexp.eval( this.localeDecodeEquation( eqValues ) ) ).toFixed( calcModel.get( 'dec' ) ) );
3296            } catch( e ) {
3297                console.log( e );
3298            }
3299            if( calcModel.get( 'value' ) === 'NaN' ) calcModel.set( 'value', '0' );
3300		},
3301        
3302        /**
3303         * Function to apply Locale Formatting to Calculations
3304         * @since Version 3.1
3305         * @param Str number
3306         * 
3307         * @return Str
3308         */
3309        applyLocaleFormatting: function( number, calcModel ) {
3310
3311			var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point);
3312
3313			var formattedNumber = localeConverter.numberEncoder(number, calcModel.get('dec'));
3314            
3315            // // Split our string on the decimal to preserve context.
3316            // var splitNumber = number.split('.');
3317            // // If we have more than one element (if we had a decimal point)...
3318            // if ( splitNumber.length > 1 ) {
3319            //     // Update the thousands and remerge the array.
3320            //     splitNumber[ 0 ] = splitNumber[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, nfi18n.thousands_sep );
3321            //     var formattedNumber = splitNumber.join( nfi18n.decimal_point );
3322            // }
3323            // // Otherwise (we had no decimal point)...
3324            // else {
3325            //     // Update the thousands.
3326            //     var formattedNumber = number.replace( /\B(?=(\d{3})+(?!\d))/g, nfi18n.thousands_sep );
3327            // }
3328            return formattedNumber;
3329		},
3330		
3331		localeDecodeEquation: function( eq ) {
3332			var result = '';
3333			var expression = '';
3334			var pattern = /[0-9.,]/;
3335			var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point);
3336			// This pattern accounts for all whitespace characters (including thin space).
3337			eq = eq.replace( /\s/g, '' );
3338			eq = eq.replace( /&nbsp;/g, '' );
3339			var characters = eq.split('');
3340			// foreach ( characters as character ) {
3341			characters.forEach( function( character ) {
3342				// If the character is numeric or '.' or ','
3343				if (pattern.test(character)) {
3344					expression = expression + character;
3345				} else {
3346					// If we reach an operator char, append the expression to the result
3347					if ( 0 < expression.length ) {
3348						result = result + localeConverter.numberDecoder( expression );
3349						expression = '';
3350					}
3351					result = result + character;
3352				}
3353			});
3354			// The following catches the case of the last character being a digit.
3355			if ( 0 < expression.length ) {
3356				result = result + localeConverter.numberDecoder( expression );
3357			}
3358			return result;
3359		},
3360
3361		debug: function(message) {
3362			if ( window.nfCalculationsDebug || false ) console.log(message);
3363		}
3364	
3365	});
3366
3367	return controller;
3368} );
3369
3370define('controllers/dateBackwardsCompat',[], function() {
3371    var controller = Marionette.Object.extend({
3372
3373        initialize: function () {
3374            this.listenTo( Backbone.Radio.channel( 'pikaday-bc' ), 'init', this.dateBackwardsCompat );	
3375        },
3376
3377        dateBackwardsCompat: function( dateObject, fieldModel ) {
3378           
3379            /**
3380             * Start backwards compatibility for old pikaday customisation
3381             */
3382            // Legacy properties
3383            dateObject.pikaday = {};
3384            dateObject.pikaday._o = {};
3385
3386            //Old hook for Pikaday Custom code
3387            nfRadio.channel( 'pikaday' ).trigger( 'init', dateObject, fieldModel );
3388
3389            // If we've set a disableDayFn property in custom code, hook it up to Flatpickr
3390            if ( typeof dateObject.pikaday._o.disableDayFn !== 'undefined') {
3391                dateObject.set( 'disable', [ dateObject.pikaday._o.disableDayFn ] );
3392            }
3393
3394            //Compatibility for i18n pikaday function
3395            if ( typeof dateObject.pikaday._o.i18n !== 'undefined' || typeof dateObject.pikaday._o.firstDay !== 'undefined') {
3396
3397                let locale = dateObject.config.locale;
3398
3399                if ( typeof dateObject.pikaday._o.firstDay !== 'undefined') {
3400                    locale.firstDayOfWeek = dateObject.pikaday._o.firstDay;
3401                }
3402
3403                if ( typeof dateObject.pikaday._o.i18n !== 'undefined') {
3404                    if ( typeof dateObject.pikaday._o.i18n.weekdays !== 'undefined') {
3405                        locale.weekdays.longhand = dateObject.pikaday._o.i18n.weekdays;
3406                    }
3407
3408                    if ( typeof dateObject.pikaday._o.i18n.weekdaysShort !== 'undefined') {
3409                        locale.weekdays.shorthand = dateObject.pikaday._o.i18n.weekdaysShort;
3410                    }
3411                    
3412                    if ( typeof dateObject.pikaday._o.i18n.months !== 'undefined') {
3413                        jQuery( '.flatpickr-monthDropdown-months > option' ).each( function() {
3414                            this.text = dateObject.pikaday._o.i18n.months[ this.value ];
3415                        } );
3416                    }
3417                }
3418
3419                dateObject.set( 'locale', locale );
3420                
3421            }
3422
3423            if ( Object.keys(dateObject.pikaday._o).length > 0 ) {
3424                console.log("%cDeprecated Ninja Forms Pikaday custom code detected.", "color: Red; font-size: large");
3425                console.log("You are using deprecated Ninja Forms Pikaday custom code. Support for this custom code will be removed in a future version of Ninja Forms. Please contact Ninja Forms support for more details.");
3426            }
3427
3428        }
3429
3430    });
3431
3432    return controller;
3433});
3434define('controllers/fieldDate',[], function() {
3435    var controller = Marionette.Object.extend({
3436
3437        initialize: function () {
3438            this.listenTo( nfRadio.channel( 'date' ), 'render:view', this.initDatepicker );
3439        },
3440
3441        initDatepicker: function ( view ) {
3442            var dateFormat = view.model.get( 'date_format' );
3443    
3444            // For "default" date format, convert PHP format to JS compatible format.
3445            if( '' == dateFormat || 'default' == dateFormat ){
3446                dateFormat = this.convertDateFormat( nfi18n.dateFormat );
3447            }
3448
3449            var el = jQuery( view.el ).find( '.nf-element' )[0];
3450            var dateSettings = {
3451                classes: jQuery( el ).attr( "class" ),
3452                placeholder: view.model.get( 'placeholder' ),
3453                parseDate: function (datestr, format) {
3454                    return moment(datestr, format, true).toDate();
3455                },
3456                formatDate: function (date, format, locale) {
3457                    return moment(date).format(format);
3458                },
3459                dateFormat: dateFormat,
3460                altFormat: dateFormat,
3461                altInput: true,
3462                ariaDateFormat: dateFormat,
3463                mode: "single",
3464                disableMobile: "true",
3465                locale: {
3466                    months: {
3467                        shorthand: nfi18n.monthsShort,
3468                        longhand: nfi18n.months
3469                    },
3470                    weekdays: {
3471                        shorthand: nfi18n.weekdaysShort,
3472                        longhand: nfi18n.weekdays
3473                    },
3474                    firstDayOfWeek: nfi18n.startOfWeek,
3475                }
3476            };
3477           
3478            var dateObject = flatpickr( el, dateSettings );
3479
3480            if ( 1 == view.model.get( 'date_default' ) ) {
3481                dateObject.setDate( moment().format(dateFormat) );
3482            }
3483
3484            //Trigger Pikaday backwards compatibility
3485            nfRadio.channel( 'pikaday-bc' ).trigger( 'init', dateObject, view.model );
3486
3487            nfRadio.channel( 'flatpickr' ).trigger( 'init', dateObject, view.model );
3488        },
3489
3490        getYearRange: function( fieldModel ) {
3491            var yearRange      = 10;
3492            var yearRangeStart = fieldModel.get( 'year_range_start' );
3493            var yearRangeEnd   = fieldModel.get( 'year_range_end'   );
3494
3495            if( yearRangeStart && yearRangeEnd ){
3496                return [ yearRangeStart, yearRangeEnd ];
3497            } else if( yearRangeStart ) {
3498                yearRangeEnd = yearRangeStart + yearRange;
3499                return [ yearRangeStart, yearRangeEnd ];
3500            } else if( yearRangeEnd ) {
3501                yearRangeStart = yearRangeEnd - yearRange;
3502                return [ yearRangeStart, yearRangeEnd ];
3503            }
3504
3505            return yearRange;
3506        },
3507
3508        getMinDate: function( fieldModel ) {
3509            var minDate = null;
3510            var yearRangeStart = fieldModel.get( 'year_range_start' );
3511
3512            if( yearRangeStart ) {
3513                return new Date( yearRangeStart, 0, 1 );
3514            }
3515
3516            return minDate;
3517        },
3518
3519        getMaxDate: function( fieldModel ) {
3520            var maxDate = null;
3521            var yearRangeEnd   = fieldModel.get( 'year_range_end' );
3522
3523            if( yearRangeEnd ) {
3524                return new Date( yearRangeEnd, 11, 31 );
3525            }
3526
3527            return maxDate;
3528        },
3529        
3530        convertDateFormat: function( dateFormat ) {
3531            // http://php.net/manual/en/function.date.php
3532            // https://github.com/dbushell/Pikaday/blob/master/README.md#formatting  **** Switched to flatpickr ***
3533            // Note: Be careful not to add overriding replacements. Order is important here.
3534
3535            /** Day */
3536            dateFormat = dateFormat.replace( 'D', 'ddd' ); // @todo Ordering issue?
3537            dateFormat = dateFormat.replace( 'd', 'DD' );
3538            dateFormat = dateFormat.replace( 'l', 'dddd' );
3539            dateFormat = dateFormat.replace( 'j', 'D' );
3540            dateFormat = dateFormat.replace( 'N', '' ); // Not Supported
3541            dateFormat = dateFormat.replace( 'S', '' ); // Not Supported
3542            dateFormat = dateFormat.replace( 'w', 'd' );
3543            dateFormat = dateFormat.replace( 'z', '' ); // Not Supported
3544
3545            /** Week */
3546            dateFormat = dateFormat.replace( 'W', 'W' );
3547
3548            /** Month */
3549            dateFormat = dateFormat.replace( 'M', 'MMM' ); // "M" before "F" or "m" to avoid overriding.
3550            dateFormat = dateFormat.replace( 'F', 'MMMM' );
3551            dateFormat = dateFormat.replace( 'm', 'MM' );
3552            dateFormat = dateFormat.replace( 'n', 'M' );
3553            dateFormat = dateFormat.replace( 't', '' );  // Not Supported
3554
3555            // Year
3556            dateFormat = dateFormat.replace( 'L', '' ); // Not Supported
3557            dateFormat = dateFormat.replace( 'o', 'YYYY' );
3558            dateFormat = dateFormat.replace( 'Y', 'YYYY' );
3559            dateFormat = dateFormat.replace( 'y', 'YY' );
3560
3561            // Time - Not supported
3562            dateFormat = dateFormat.replace( 'a', '' );
3563            dateFormat = dateFormat.replace( 'A', '' );
3564            dateFormat = dateFormat.replace( 'B', '' );
3565            dateFormat = dateFormat.replace( 'g', '' );
3566            dateFormat = dateFormat.replace( 'G', '' );
3567            dateFormat = dateFormat.replace( 'h', '' );
3568            dateFormat = dateFormat.replace( 'H', '' );
3569            dateFormat = dateFormat.replace( 'i', '' );
3570            dateFormat = dateFormat.replace( 's', '' );
3571            dateFormat = dateFormat.replace( 'u', '' );
3572            dateFormat = dateFormat.replace( 'v', '' );
3573
3574            // Timezone - Not supported
3575            dateFormat = dateFormat.replace( 'e', '' );
3576            dateFormat = dateFormat.replace( 'I', '' );
3577            dateFormat = dateFormat.replace( 'O', '' );
3578            dateFormat = dateFormat.replace( 'P', '' );
3579            dateFormat = dateFormat.replace( 'T', '' );
3580            dateFormat = dateFormat.replace( 'Z', '' );
3581
3582            // Full Date/Time - Not Supported
3583            dateFormat = dateFormat.replace( 'c', '' );
3584            dateFormat = dateFormat.replace( 'r', '' );
3585            dateFormat = dateFormat.replace( 'u', '' );
3586
3587            return dateFormat;
3588        }
3589    });
3590
3591    return controller;
3592});
3593
3594define('controllers/fieldRecaptcha',[], function() {
3595    var controller = Marionette.Object.extend({
3596
3597        initialize: function () {
3598            this.listenTo( nfRadio.channel( 'recaptcha' ), 'init:model',      this.initRecaptcha  );
3599            this.listenTo( nfRadio.channel( 'forms' ),     'submit:response', this.resetRecaptcha );
3600        },
3601
3602       	initRecaptcha: function ( model ) {
3603       		nfRadio.channel( 'recaptcha' ).reply( 'update:response', this.updateResponse, this, model.id );
3604        },
3605
3606        updateResponse: function( response, fieldID ) {
3607        	var model = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
3608			model.set( 'value', response );
3609            nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'required-error' );
3610        },
3611
3612        resetRecaptcha: function() {
3613			var recaptchaID = 0;
3614			jQuery( '.g-recaptcha' ).each( function() {
3615				try {
3616					grecaptcha.reset( recaptchaID );
3617				} catch( e ){
3618					console.log( 'Notice: Error trying to reset grecaptcha.' );
3619				}
3620				recaptchaID++;
3621			} );
3622        }
3623    });
3624
3625    return controller;
3626} );
3627define('controllers/fieldHTML',[], function() {
3628    var controller = Marionette.Object.extend({
3629
3630        htmlFields: [],
3631        trackedMergeTags: [],
3632
3633        initialize: function () {
3634            this.listenTo( Backbone.Radio.channel( 'fields-html' ), 'init:model', this.setupFieldMergeTagTracking );
3635        },
3636
3637        setupFieldMergeTagTracking: function( fieldModel ) {
3638            this.htmlFields.push( fieldModel );
3639
3640            var formID = fieldModel.get( 'formID' );
3641
3642            this.listenTo( nfRadio.channel( 'form-' + formID ), 'init:model', function( formModel ){
3643
3644                var mergeTags = fieldModel.get( 'default' ).match( new RegExp( /{field:(.*?)}/g ) );
3645                if ( ! mergeTags ) return;
3646
3647                _.each( mergeTags, function( mergeTag ) {
3648                    var fieldKey = mergeTag.replace( '{field:', '' ).replace( '}', '' );
3649                    var fieldModel = formModel.get( 'fields' ).findWhere({ key: fieldKey });
3650                    if( 'undefined' == typeof fieldModel ) return;
3651
3652                    this.trackedMergeTags.push( fieldModel );
3653                    this.listenTo( nfRadio.channel( 'field-' + fieldModel.get( 'id' ) ), 'change:modelValue', this.updateFieldMergeTags );
3654                }, this );
3655
3656                // Let's get this party started!
3657                this.updateFieldMergeTags();
3658            }, this );
3659        },
3660
3661        updateFieldMergeTags: function( fieldModel ) {
3662            _.each( this.htmlFields, function( htmlFieldModel ){
3663                var value = htmlFieldModel.get( 'value' );
3664               _.each( this.trackedMergeTags, function( fieldModel ){
3665
3666                   /* Search the value for any spans with mergetag data-key
3667                   * values
3668                   */
3669                   var spans = value.match( new RegExp( /<span data-key="field:(.*?)<\/span>/g ) );
3670	               _.each( spans, function( spanVar ) {
3671	                   /* See if the span string contains the current
3672                       * fieldModel's key. If so replace the span with a
3673                       * merge tag for evaluation.
3674                       */
3675                       if( -1 < spanVar.indexOf( "data-key=\"field:" + fieldModel.get( 'key' ) ) ) {
3676	                       value = value.replace( spanVar, "{field:" + fieldModel.get( 'key' ) + "}" );
3677                       }
3678	               } );
3679
3680                    var mergeTag = '{field:' + fieldModel.get( 'key' ) + '}';
3681	               /* We replace the merge tag with the value
3682	               * surrounded by a span so that we can still find it
3683	               * and not affect itself or other field merge tags
3684	               */
3685                    value = value.replace( mergeTag, "<span data-key=\"field:"
3686                        + fieldModel.get( 'key' ) + "\">"
3687                        + fieldModel.get( 'value' ) + "</span>" );
3688               }, this ) ;
3689               htmlFieldModel.set( 'value', value );
3690               htmlFieldModel.trigger( 'reRender' );
3691            }, this );
3692        }
3693
3694    });
3695
3696    return controller;
3697});
3698
3699/**
3700 * When a form is loaded, enable any help text that appears on the page.
3701 */
3702define('controllers/helpText',[], function() {
3703	var controller = Marionette.Object.extend( {
3704		initialize: function() {
3705			this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.initHelpText );
3706
3707			nfRadio.channel( 'form' ).reply( 'init:help', this.initHelpText );
3708		},
3709
3710		initHelpText: function( view ) {
3711			jQuery( view.el ).find( '.nf-help' ).each( function() {
3712				var jBox = jQuery( this ).jBox( 'Tooltip', {
3713					theme: 'TooltipBorder',
3714					content: jQuery( this ).data( 'text' )
3715				});
3716			} );
3717		}
3718	});
3719
3720	return controller;
3721} );
3722define('controllers/fieldTextbox',[], function() {
3723	var controller = Marionette.Object.extend( {
3724		initialize: function() {
3725            nfRadio.channel( 'textbox' ).reply( 'get:calcValue', this.getCalcValue, this );
3726		},
3727
3728		getCalcValue: function( fieldModel ) {
3729            if('currency' == fieldModel.get('mask')){
3730                var form = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) );
3731                var currencySymbol = ('undefined' !== typeof form) ? form.get( 'currencySymbol' ) : '';
3732                var currencySymbolDecoded = jQuery('<textarea />').html(currencySymbol).text();
3733                return fieldModel.get( 'value' ).replace(currencySymbolDecoded, '');
3734            }
3735
3736			return fieldModel.get( 'value' );
3737		},
3738	});
3739
3740	return controller;
3741} );
3742/**
3743 * When a form is loaded, enable any rtes in textareas.
3744 */
3745define('controllers/fieldTextareaRTE',[], function() {
3746	var controller = Marionette.Object.extend( {
3747		initialize: function() {
3748			this.listenTo( nfRadio.channel( 'textarea' ), 'render:view', this.initTextareaRTEs );
3749			this.listenTo( nfRadio.channel( 'textarea' ), 'click:extra', this.clickExtra );
3750
3751			// Instantiates the variable that holds the media library frame.
3752			this.meta_image_frame;
3753
3754			this.currentContext = {};
3755
3756			if( 'undefined' == typeof jQuery.summernote ) return;
3757
3758			jQuery.summernote.options.icons = {
3759		        'align': 'dashicons dashicons-editor-alignleft',
3760		        'alignCenter': 'dashicons dashicons-editor-aligncenter',
3761		        'alignJustify': 'dashicons dashicons-editor-justify',
3762		        'a
3762lignLeft': 'dashicons dashicons-editor-alignleft',
3763		        'alignRight': 'dashicons dashicons-editor-alignright',
3764		        'indent': 'dashicons dashicons-editor-indent',
3765		        'outdent': 'dashicons dashicons-editor-outdent',
3766		        // 'arrowsAlt': 'dashicons fa-arrows-alt',
3767		        'bold': 'dashicons dashicons-editor-bold',
3768		        'caret': 'dashicons dashicons-arrow-down',
3769		        // 'circle': 'dashicons fa-circle',
3770		        'close': 'dashicons dashicons-dismiss',
3771		        'code': 'dashicons dashicons-editor-code',
3772		        'eraser': 'dashicons dashicons-editor-removeformatting',
3773		        // 'font': 'dashicons fa-font',
3774		        // 'frame': 'dashicons fa-frame',
3775		        'italic': 'dashicons dashicons-editor-italic',
3776		        'link': 'dashicons dashicons-admin-links',
3777		        'unlink': 'dashicons dashicons-editor-unlink',
3778		        'magic': 'dashicons dashicons-editor-paragraph',
3779		        // 'menuCheck': 'dashicons fa-check',
3780		        'minus': 'dashicons dashicons-minus',
3781		        'orderedlist': 'dashicons dashicons-editor-ol',
3782		        // 'pencil': 'dashicons fa-pencil',
3783		        // 'picture': 'dashicons fa-picture-o',
3784		        // 'question': 'dashicons fa-question',
3785		        'redo': 'dashicons dashicons-redo',
3786		        'square': 'dashicons fa-square',
3787		        // 'strikethrough': 'dashicons fa-strikethrough',
3788		        // 'subscript': 'dashicons fa-subscript',
3789		        // 'superscript': 'dashicons fa-superscript',
3790		        'table': 'dashicons dashicons-editor-table',
3791		        // 'textHeight': 'dashicons fa-text-height',
3792		        // 'trash': 'dashicons fa-trash',
3793		        'underline': 'dashicons dashicons-editor-underline',
3794		        'undo': 'dashicons dashicons-undo',
3795		        'unorderedlist': 'dashicons dashicons-editor-ul',
3796		        // 'video': 'dashicons fa-youtube-play'
3797		      };
3798
3799		},
3800
3801		initTextareaRTEs: function( view ) {
3802			if ( 1 != view.model.get( 'textarea_rte' ) ) {
3803				return false;
3804			}
3805			/*
3806			 * Custom Button for links
3807			 */
3808			var that = this;
3809			// var linkButton = this.linkButton();
3810			var linkButton = function( context ) {
3811				return that.linkButton( context );
3812			}
3813			var mediaButton = function( context ) {
3814				return that.mediaButton( context );
3815			}
3816
3817			var toolbar = [
3818				[ 'paragraphStyle', ['style'] ],
3819				[ 'fontStyle', [ 'bold', 'italic', 'underline','clear' ] ],
3820				[ 'lists', [ 'ul', 'ol' ] ],
3821			    [ 'paragraph', [ 'paragraph' ] ],
3822			    [ 'customGroup', [ 'linkButton', 'unlink' ] ],
3823			    [ 'table', [ 'table' ] ],
3824			    [ 'actions', [ 'undo', 'redo' ] ],
3825			];
3826
3827			if ( 1 == view.model.get( 'textarea_media' ) && 0 != userSettings.uid ) {
3828				toolbar.push( [ 'tools', [ 'mediaButton' ] ] );
3829			}
3830
3831			jQuery( view.el ).find( '.nf-element' ).summernote( {
3832				toolbar: toolbar,
3833				buttons: {
3834					linkButton: linkButton,
3835					mediaButton: mediaButton
3836				},
3837				height: 150,   //set editable area's height
3838				codemirror: { // codemirror options
3839				    theme: 'monokai',
3840				    lineNumbers: true
3841				},
3842				prettifyHtml: true,
3843				callbacks: {
3844					onChange: function( e ) {
3845						view.model.set( 'value', jQuery( this ).summernote( 'code' ) );
3846					}
3847				}
3848			} );
3849
3850			var linkMenu = jQuery( view.el ).find( '.link-button' ).next( '.dropdown-menu' ).find( 'button' );
3851			linkMenu.replaceWith(function () {
3852			    return jQuery( '<div/>', {
3853			        class: jQuery( linkMenu ).attr( 'class' ),
3854			        html: this.innerHTML
3855			    } );
3856			} );
3857		},
3858
3859		linkButton: function( context ) {
3860			var that = this;
3861			var ui = jQuery.summernote.ui;
3862			var linkButton = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-rte-link-button' );
3863			var linkDropdown = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-rte-link-dropdown' );
3864			return ui.buttonGroup([
3865				ui.button({
3866	            className: 'dropdown-toggle link-button',
3867	            contents: linkButton({}),
3868	            tooltip: nfi18n.fieldTextareaRTEInsertLink,
3869	            click: function( e ) {
3870	            	that.clickLinkButton( e, context );
3871	            },
3872	            data: {
3873	              toggle: 'dropdown'
3874	            }
3875	          }),
3876				ui.dropdown([
3877	            ui.buttonGroup({
3878	              children: [
3879	                ui.button({
3880	                  contents: linkDropdown({}),
3881	                  tooltip: ''
3882	                }),
3883	              ]
3884	            })
3885	          ])
3886			]).render();
3887		},
3888
3889		mediaButton: function( context ) {
3890			var that = this;
3891			var ui = jQuery.summernote.ui;
3892			var mediaButton = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-rte-media-button' );
3893			return ui.button({
3894	            className: 'dropdown-toggle',
3895	            contents: mediaButton({}),
3896	            tooltip: nfi18n.fieldTextareaRTEInsertMedia,
3897	            click: function( e ) {
3898	            	that.openMediaManager( e, context );
3899	            }
3900	          }).render();
3901		},
3902
3903		openMediaManager: function( e, context ) {
3904			context.invoke( 'editor.saveRange' );
3905			// If the frame already exists, re-open it.
3906			if ( this.meta_image_frame ) {
3907				this.meta_image_frame.open();
3908				return;
3909			}
3910
3911			// Sets up the media library frame
3912			this.meta_image_frame = wp.media.frames.meta_image_frame = wp.media({
3913				title: nfi18n.fieldTextareaRTESelectAFile,
3914				button: { text:  'insert' }
3915			});
3916
3917			var that = this;
3918
3919			// Runs when an image is selected.
3920			this.meta_image_frame.on('select', function(){
3921
3922				// Grabs the attachment selection and creates a JSON representation of the model.
3923				var media_attachment = that.meta_image_frame.state().get('selection').first().toJSON();
3924				that.insertMedia( media_attachment, context );
3925			});
3926
3927			// Opens the media library frame.
3928			this.meta_image_frame.open();
3929		},
3930
3931		clickLinkButton: function ( e, context ) {
3932			var range = context.invoke( 'editor.createRange' );
3933			context.invoke( 'editor.saveRange' );
3934			var text = range.toString()
3935			this.currentContext = context;
3936
3937			jQuery( e.target ).closest( '.note-customGroup > .note-btn-group' ).on ('hide.bs.dropdown', function ( e ) {
3938				return false;
3939			});
3940
3941			jQuery( e.target ).closest( '.note-customGroup > .note-btn-group' ).on ('shown.bs.dropdown', function ( e ) {
3942				jQuery( e.target ).parent().parent().find( '.link-text' ).val( text );
3943				jQuery( e.target ).parent().parent().find( '.link-url' ).focus();
3944			});
3945		},
3946
3947		clickExtra: function( e ) {
3948			var textEl = jQuery( e.target ).parent().find( '.link-text' );
3949			var urlEl = jQuery( e.target ).parent().find( '.link-url' );
3950			var isNewWindowEl = jQuery( e.target ).parent().find( '.link-new-window' );
3951			this.currentContext.invoke( 'editor.restoreRange' );
3952			if ( jQuery( e.target ).hasClass( 'insert-link' ) ) {
3953				var text = textEl.val();
3954				var url = urlEl.val();
3955				var isNewWindow = ( isNewWindowEl.prop( 'checked' ) ) ? true: false;
3956				if ( 0 != text.length && 0 != url.length ) {
3957					this.currentContext.invoke( 'editor.createLink', { text:text, url: url, isNewWindow: isNewWindow } );
3958				}
3959			}
3960			textEl.val( '' );
3961			urlEl.val( '' );
3962			isNewWindowEl.prop( 'checked', false );
3963			jQuery( e.target ).closest( 'div.note-btn-group.open' ).removeClass( 'open' );
3964		},
3965
3966		insertMedia: function( media, context ) {
3967			context.invoke( 'editor.restoreRange' );
3968			if ( 'image' == media.type ) {
3969				context.invoke( 'editor.insertImage', media.url );
3970			} else {
3971				context.invoke( 'editor.createLink', { text: media.filename, url: media.url } );
3972			}
3973
3974		}
3975	});
3976
3977	return controller;
3978} );
3979define('controllers/fieldStarRating',[], function() {
3980    var controller = Marionette.Object.extend( {
3981
3982        initialize: function() {
3983        	this.listenTo( nfRadio.channel( 'starrating' ), 'init:model', this.register );
3984            this.listenTo( nfRadio.channel( 'starrating' ), 'render:view', this.initRating );
3985        },
3986
3987        register: function( model ) {
3988			model.set( 'renderRatings', this.renderRatings );
3989		},
3990
3991        initRating: function( view ){
3992            jQuery( view.el ).find( '.starrating' ).rating();
3993
3994        },
3995
3996        renderRatings: function() {
3997        	var html = document.createElement( 'span' );
3998        	// changed from 'default' to 'number_of_stars'
3999        	for (var i = 0; i <= this.number_of_stars - 1; i++) {
4000                var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-starrating-star' );
4001                var num = i + 1;
4002                var checked = '';
4003
4004                // Check to see if current 'star' matches the default value
4005		        if ( this.value == num ) {
4006		        	checked = 'checked';
4007		        }
4008                var htmlFragment = template( { id: this.id, classes: this.classes, num: num, checked: checked, required: this.required } );
4009                html.appendChild(
4010                    document.createRange().createContextualFragment( htmlFragment )
4011                );
4012        	}
4013        	return html.innerHTML;
4014        }
4015
4016    });
4017
4018    return controller;
4019});
4020
4021define('controllers/fieldTerms',[], function() {
4022    var controller = Marionette.Object.extend( {
4023        initialize: function() {
4024            this.listenTo( nfRadio.channel( 'terms' ), 'init:model', this.register );
4025        },
4026
4027        register: function( model ) {
4028            // nfRadio.channel( 'field-' + this.model.get( 'id' ) ).trigger( 'click:extra', e, this.model );
4029            this.listenTo( nfRadio.channel( 'field-' + model.get( 'id' ) ), 'click:extra', this.clickExtra );
4030            this.listenTo( nfRadio.channel( 'field-' + model.get( 'id' ) ), 'keyup:field', this.keyUpExtra );
4031        },
4032        
4033        clickExtra: function( e, model ) {
4034            var el = jQuery( e.currentTarget );
4035            var value = el.parent().find( '.extra-value' ).val();
4036            this.addOption( model, value );
4037        },
4038
4039        keyUpExtra: function( el, model, keyCode ) {
4040            if( 13 != keyCode ) return;
4041            this.addOption( model, el.val() );
4042        },
4043
4044        addOption: function( model, value ) {
4045            if( ! value ) return;
4046            var options = model.get( 'options' );
4047            var new_option = {
4048                label: value,
4049                value: value,
4050                selected: 0,
4051            };
4052            options.push( new_option );
4053
4054            var selected = model.get( 'value' );
4055            selected.push( value );
4056
4057            // model.set( 'reRender', true );
4058            model.trigger( 'reRender' );
4059        }
4060        
4061    });
4062
4063    return controller;
4064} );
4065/**
4066 * Before we display our form content, ask if anyone wants to give us a different view.
4067 * Before we do anything with the data, pass it through any loading filters.
4068 * 
4069 * @package Ninja Forms builder
4070 * @subpackage Main App
4071 * @copyright (c) 2016 WP Ninjas
4072 * @since 3.0
4073 */
4074define( 'controllers/formContentFilters',[], function() {
4075	var controller = Marionette.Object.extend( {
4076		initialize: function() {
4077			/*
4078			 * Init our fieldContent view and load filter arrays.
4079			 */
4080			this.viewFilters = [];
4081			this.loadFilters = [];
4082
4083			/*
4084			 * Listen for requests to add new formContent filters.
4085			 */
4086			nfRadio.channel( 'formContent' ).reply( 'add:viewFilter', this.addViewFilter, this );
4087			nfRadio.channel( 'formContent' ).reply( 'add:loadFilter', this.addLoadFilter, this );
4088
4089			/*
4090			 * Listen for requests to get our formContent filters.
4091			 */
4092			nfRadio.channel( 'formContent' ).reply( 'get:viewFilters', this.getViewFilters, this );
4093			nfRadio.channel( 'formContent' ).reply( 'get:loadFilters', this.getLoadFilters, this );
4094
4095			/*
4096			 * -- DEPRECATED RADIO REPLIES --
4097			 * 
4098			 * The 'fieldContents' channel has been deprecated as of 3.0 (it was present in the RC) in favour of 'formContent'.
4099			 * Listen for requests to add new fieldContent filters.
4100			 * 
4101			 * TODO: These radio listeners on the 'fieldContents' channels are here for backwards compatibility and should be removed eventually.
4102			 */
4103			nfRadio.channel( 'fieldContents' ).reply( 'add:viewFilter', this.addViewFilter, this );
4104			nfRadio.channel( 'fieldContents' ).reply( 'add:loadFilter', this.addLoadFilter, this );
4105
4106			/*
4107			 * Listen for requests to get our fieldContent filters.
4108			 * TODO: Remove eventually.
4109			 */
4110			nfRadio.channel( 'fieldContents' ).reply( 'get:viewFilters', this.getViewFilters, this );
4111			nfRadio.channel( 'fieldContents' ).reply( 'get:loadFilters', this.getLoadFilters, this );
4112		
4113			/*
4114			 * -- END DEPRECATED --
4115			 */
4116		},
4117
4118		addViewFilter: function( callback, priority ) {
4119			this.viewFilters[ priority ] = callback;
4120		},
4121
4122		getViewFilters: function() {
4123			return this.viewFilters;
4124		},
4125
4126		addLoadFilter: function( callback, priority ) {
4127			this.loadFilters[ priority ] = callback;
4128		},
4129
4130		getLoadFilters: function() {
4131			return this.loadFilters;
4132		}
4133
4134	});
4135
4136	return controller;
4137} );
4138define( 'views/fieldItem',[], function() {
4139	var view = Marionette.ItemView.extend({
4140		tagName: 'div',
4141
4142		initialize: function() {
4143    		this.listenTo( this.model, 'reRender', this.render, this );
4144    		this.listenTo( this.model, 'change:addWrapperClass', this.addWrapperClass, this );
4145    		this.listenTo( this.model, 'change:removeWrapperClass', this.removeWrapperClass, this );
4146    		this.listenTo( this.model, 'change:invalid', this.toggleAriaInvalid, this );
4147
4148    		this.template = '#tmpl-nf-field-' + this.model.get( 'wrap_template' );
4149		},
4150
4151		test: function( model ) {
4152			console.log( 'firing from trigger 1' );
4153		},
4154
4155		addWrapperClass: function() {
4156			var cl = this.model.get( 'addWrapperClass' );
4157			if ( '' != cl ) {
4158				jQuery( this.el ).addClass( cl );
4159				this.model.set( 'addWrapperClass', '' );
4160			}
4161		},
4162
4163		removeWrapperClass: function() {
4164			var cl = this.model.get( 'removeWrapperClass' );
4165			if ( '' != cl ) {
4166				jQuery( this.el ).removeClass( cl );
4167				this.model.set( 'removeWrapperClass', '' );
4168			}
4169		},
4170
4171		toggleAriaInvalid: function() {
4172			var invalid = this.model.get( 'invalid' );
4173			jQuery( '[aria-invalid]', this.el ).attr( 'aria-invalid', JSON.stringify( invalid ) );
4174		},
4175
4176		onRender: function() {
4177			this.$el = this.$el.children();
4178			this.$el.unwrap();
4179			this.setElement( this.$el );
4180
4181	   		/*
4182    		 * If we have an input mask, init that mask.
4183    		 * TODO: Move this to a controller so that the logic isn't in the view.
4184    		 */
4185    		if ( 'undefined' != typeof this.model.get( 'mask' ) && '' != jQuery.trim( this.model.get( 'mask' ) ) ) {
4186    			if ( 'custom' == this.model.get( 'mask') ) {
4187    				var mask = this.model.get( 'custom_mask' );
4188    			} else {
4189    				var mask = this.model.get( 'mask' );
4190    			}
4191
4192				/* POLYFILL */ Number.isInteger = Number.isInteger || function(value) { return typeof value === "number" && isFinite(value) && Math.floor(value) === value; };
4193    			if ( Number.isInteger( mask ) ) {
4194    				mask = mask.toString();
4195    			}
4196
4197				if ( 'currency' == mask ) {
4198					var form = nfRadio.channel( 'app' ).request( 'get:form', this.model.get( 'formID' ) );
4199
4200					var thousands_sep = form.get( 'thousands_sep' );
4201					/*
4202					 * TODO: if we have a &nbsp; , replace it with a string with a space.
4203					 */
4204					if ( '&nbsp;' == thousands_sep ) {
4205						thousands_sep = ' ';
4206					}
4207					var currencySymbol = jQuery( '<div/>' ).html( form.get( 'currencySymbol' ) ).text();
4208					thousands_sep = jQuery( '<div/>' ).html( thousands_sep ).text();
4209					var decimal_point = jQuery( '<div/>' ).html( form.get( 'decimal_point' ) ).text();
4210					
4211					/*
4212					 * TODO: Currently, these options use the plugin-wide defaults for locale.
4213					 * When per-form locales are implemented, these will need to be revisited.
4214					 */
4215					var autoNumericOptions = {
4216					    digitGroupSeparator        : thousands_sep,
4217					    decimalCharacter           : decimal_point,
4218					    currencySymbol             : currencySymbol
4219					};
4220
4221					// Initialization
4222					var autoN_el = jQuery(jQuery( this.el ).find( '.nf-element' )[ 0 ]);
4223					new AutoNumeric( jQuery( this.el ).find( '.nf-element' )[ 0 ], autoNumericOptions );
4224					// update the value for the model so it gets saved to
4225					// the database properly
4226					var context = this;
4227					autoN_el.on( 'change', function( e ) {
4228						context.model.set( 'value', e.target.value );
4229					})
4230				} else {
4231					jQuery( this.el ).find( '.nf-element' ).mask( mask );
4232				} 			
4233	   		}
4234
4235			nfRadio.channel( this.model.get( 'type' ) ).trigger( 'render:view', this );
4236			nfRadio.channel( 'fields' ).trigger( 'render:view', this );
4237		},
4238
4239		templateHelpers: function () {
4240			var that = this;
4241	    	return {
4242
4243				renderElement: function(){
4244					var tmpl = _.find( this.element_templates, function( tmpl ) {
4245						if ( 0 < jQuery( '#tmpl-nf-field-' + tmpl ).length ) {
4246							return true;
4247						}
4248					} );
4249					var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-' + tmpl );
4250					return template( this );
4251				},
4252
4253				renderLabel: function() {
4254					var template = nfRadio.channel( 'app' ).request( 'get:template',  '#tmpl-nf-field-label' );
4255					return template( this );
4256				},
4257
4258				renderLabelClasses: function () {
4259					var classes = '';
4260					if ( 'undefined' != typeof this.customLabelClasses ) {
4261						classes = this.customLabelClasses( classes );
4262					}
4263					return classes;
4264				},
4265
4266				renderPlaceholder: function() {
4267					var placeholder = this.placeholder;
4268
4269					if ( 'undefined' != typeof this.customPlaceholder ) {
4270						placeholder = this.customPlaceholder( placeholder );
4271					}
4272
4273					if( '' != jQuery.trim( placeholder ) ) {
4274						return 'placeholder="' + placeholder + '"';
4275					} else {
4276						return '';
4277					}
4278				},
4279
4280				renderWrapClass: function() {
4281					var wrapClass = 'field-wrap ' + this.type + '-wrap';
4282
4283					// Check if type and parentType are different. If, so
4284					// then add appropriate parentType wrap class
4285					if ( this.type !== this.parentType ) {
4286						wrapClass = wrapClass + ' ' + this.parentType + '-wrap';
4287					}
4288					// If we have an old_classname defined, output wrap class for backward compatibility
4289					if ( 'undefined' != typeof this.old_classname && 0 < jQuery.trim( this.old_classname ).length ) {
4290						wrapClass += ' ' + this.old_classname + '-wrap';
4291					}
4292
4293					if ( 'undefined' != typeof customWrapClass ) {
4294						wrapClass = customWrapClass( wrapClass );
4295					}
4296
4297					return wrapClass;
4298				},
4299
4300				renderClasses: function() {
4301					var classes = this.classes;
4302
4303					if ( this.error ) {
4304						classes += ' nf-error';
4305					} else {
4306						classes = classes.replace( 'nf-error', '' );
4307					}
4308
4309					if ( 'undefined' != typeof this.element_class && 0 < jQuery.trim( this.element_class ).length ) {
4310						classes += ' ' + this.element_class;
4311					}
4312
4313					/*
4314					 * If we have a function for adding extra classes, add those.
4315					 */
4316
4317					if ( 'undefined' != typeof this.customClasses ) {
4318						classes = this.customClasses( classes );
4319					}
4320
4321					return classes;
4322				},
4323
4324				maybeDisabled: function() {
4325					if ( 1 == this.disable_input ) {
4326						return 'disabled';
4327					} else {
4328						return '';
4329					}
4330				},
4331                
4332                maybeRequired: function() {
4333                    if ( 1 == this.required ) {
4334                        return 'required';
4335                    } else {
4336                        return '';
4337                    }
4338                },
4339
4340				maybeDisableAutocomplete: function() {
4341					if ( 1 == this.disable_browser_autocomplete ) {
4342						return 'autocomplete="off"';
4343					} else {
4344						return '';
4345					}
4346				},
4347
4348				maybeInputLimit: function() {
4349					if ( 'characters' == this.input_limit_type && '' != jQuery.trim( this.input_limit ) ) {
4350						return 'maxlength="' + this.input_limit + '"';
4351					} else {
4352						return '';
4353					}
4354				},
4355
4356				getHelpText: function() {
4357					// this.help_text = jQuery( this.help_text ).html();
4358					// return ( 'undefined' != typeof this.help_text ) ? this.help_text.replace(/"/g, "&quot;") : '';
4359					return ( 'undefined' != typeof this.help_text ) ? this.help_text : '';
4360				},
4361
4362				maybeRenderHelp: function() {
4363
4364					// use jQuery().text() to make sure help_text has actual
4365					// text and not just HTML tags.
4366					var check_text_par = document.createElement( 'p' );
4367                    check_text_par.innerHTML = this.help_text;
4368
4369                    var shouldRenderHelpText = false;
4370                    // Check for text or image tags
4371					if ( 0 != jQuery.trim( jQuery( check_text_par ).text() ).length
4372						|| 0 < jQuery( check_text_par ).find('img').length ) {
4373                    	shouldRenderHelpText = true;
4374                    }
4375
4376					if ( 'undefined' != typeof this.help_text && shouldRenderHelpText ) {
4377						var icon = document.createElement( 'span' );
4378						icon.classList.add( 'fa', 'fa-info-circle', 'nf-help' );
4379						icon.setAttribute( 'data-text', this.getHelpText() );
4380						return icon.outerHTML;
4381					} else {
4382						return '';
4383					}
4384				},
4385
4386				renderDescText: function() {
4387					if ( 'undefined' == typeof this.desc_text ) {
4388						return '';
4389					}
4390
4391					// Creates an element so we can check to see if the text is empty.
4392					var text = document.createElement( 'p' );
4393					text.innerHTML = this.desc_text;
4394					if( 0 == jQuery.trim( text.innerText ).length ) return '';
4395
4396                    var check, checkText;
4397					checkText = document.createTextNode( this.desc_text );
4398					check = document.createElement( 'p' );
4399					check.appendChild( checkText );
4400					if ( 0 != jQuery.trim( jQuery( check ).text() ).length ) {
4401						var descriptionText, fieldDescription;
4402                        descriptionText  = document.createRange().createContextualFragment( this.desc_text );
4403                        fieldDescription  = document.createElement( 'div' );
4404						fieldDescription.classList.add( 'nf-field-description' );
4405						fieldDescription.appendChild( descriptionText );
4406						return fieldDescription.outerHTML;
4407					} else {
4408						return '';
4409					}
4410				},
4411                
4412                renderNumberDefault: function() {
4413                    // If the field is clean...
4414                    if ( this.clean ) {
4415                        // If we have a default...
4416                        if ( this.default ) {
4417                            return this.default;
4418                        } // If we do not have a placeholder...
4419                        else if ( ! this.placeholder ) {
4420                            return this.value;
4421                        } // Otherwise...
4422                        else {
4423                            return '';
4424                        }
4425                    } // Otherwise... (The field is not clean.)
4426                    else {
4427                        return this.value;
4428                    }
4429                },
4430
4431				renderCurrencyFormatting: function( number ) {
4432					/*
4433					 * Our number will have a . as a decimal point. We want to replace that with our locale decimal, nfi18n.decimal_point.
4434					 */
4435					var replacedDecimal = number.toString().replace( '.', '||' );
4436					/*
4437					 * Add thousands separator. Our original number var won't have thousands separators.
4438					 */
4439					var replacedThousands = replacedDecimal.replace( /\B(?=(\d{3})+(?!\d))/g, nfi18n.thousands_sep );
4440					var formattedNumber = replacedThousands.replace( '||', nfi18n.decimal_point );
4441
4442					var form = nfRadio.channel( 'app' ).request( 'get:form', that.model.get( 'formID' ) );
4443					var currency_symbol = form.get( 'settings' ).currency_symbol;
4444					return currency_symbol + formattedNumber;
4445				}
4446			};
4447		},
4448
4449		events: {
4450			'change .nf-element': 'fieldChange',
4451			'keyup .nf-element': 'fieldKeyup',
4452			'click .nf-element': 'fieldClick',
4453			'click .extra': 'extraClick',
4454			'blur .nf-element': 'fieldBlur'
4455		},
4456
4457		fieldChange: function( e ) {
4458			var el = jQuery( e.currentTarget );
4459			var response = nfRadio.channel( 'nfAdmin' ).request( 'change:field', el, this.model );
4460		},
4461
4462		fieldKeyup: function( e ) {
4463			var el = jQuery( e.currentTarget );
4464			var keyCode = e.keyCode;
4465			nfRadio.channel( 'field-' + this.model.get( 'id' ) ).trigger( 'keyup:field', el, this.model, keyCode );
4466			nfRadio.channel( this.model.get( 'type' ) ).trigger( 'keyup:field', el, this.model, keyCode );
4467			nfRadio.channel( 'fields' ).trigger( 'keyup:field', el, this.model, keyCode );
4468		},
4469
4470		fieldClick: function( e ) {
4471			var el = jQuery( e.currentTarget );
4472			nfRadio.channel( 'field-' + this.model.get( 'id' ) ).trigger( 'click:field', el, this.model );
4473			nfRadio.channel( this.model.get( 'type' ) ).trigger( 'click:field', el, this.model );
4474			nfRadio.channel( 'fields' ).trigger( 'click:field', el, this.model );
4475		},
4476
4477		extraClick: function( e ) {
4478			nfRadio.channel( 'field-' + this.model.get( 'id' ) ).trigger( 'click:extra', e, this.model );
4479			nfRadio.channel( this.model.get( 'type' ) ).trigger( 'click:extra', e, this.model );
4480			nfRadio.channel( 'fields' ).trigger( 'click:extra', e, this.model );
4481		},
4482
4483		fieldBlur: function( e ) {
4484			var el = jQuery( e.currentTarget );
4485			nfRadio.channel( 'field-' + this.model.get( 'id' ) ).trigger( 'blur:field', el, this.model );
4486			nfRadio.channel( this.model.get( 'type' ) ).trigger( 'blur:field', el, this.model );
4487			nfRadio.channel( 'fields' ).trigger( 'blur:field', el, this.model );
4488		},
4489
4490		onAttach: function() {
4491			nfRadio.channel( this.model.get( 'type' ) ).trigger( 'attach:view', this );
4492		}
4493	});
4494
4495	return view;
4496} );
4497
4498define( 'views/beforeField',[], function() {
4499    var view = Marionette.ItemView.extend({
4500        tagName: 'nf-section',
4501        template: '#tmpl-nf-field-before'
4502    });
4503
4504    return view;
4505} );
4506define( 'views/fieldErrorItem',[], function() {
4507	var view = Marionette.ItemView.extend({
4508		tagName: 'nf-section',
4509		template: '#tmpl-nf-field-error',
4510
4511		onRender: function() {
4512			this.$el = this.$el.children();
4513			this.$el.unwrap();
4514			this.setElement( this.$el );
4515		},
4516	});
4517
4518	return view;
4519} );
4520define( 'views/fieldErrorCollection',['views/fieldErrorItem'], function( fieldErrorItem ) {
4521	var view = Marionette.CollectionView.extend({
4522		tagName: "nf-errors",
4523		childView: fieldErrorItem,
4524
4525		initialize: function( options ) {
4526			this.fieldModel = options.fieldModel;
4527		},
4528
4529		onRender: function() {
4530			if ( 0 == this.fieldModel.get( 'errors' ).models.length ) {
4531                this.fieldModel.removeWrapperClass( 'nf-error' );
4532                this.fieldModel.removeWrapperClass( 'nf-fail' );
4533                this.fieldModel.addWrapperClass( 'nf-pass' );
4534                this.fieldModel.setInvalid( false );
4535            } else {
4536                this.fieldModel.removeWrapperClass( 'nf-pass' );
4537                this.fieldModel.addWrapperClass( 'nf-fail' );
4538                this.fieldModel.addWrapperClass( 'nf-error' );
4539                this.fieldModel.setInvalid( true );
4540            }
4541
4542		}
4543	});
4544
4545	return view;
4546} );
4547
4548define( 'views/inputLimit',[], function() {
4549    var view = Marionette.ItemView.extend({
4550        tagName: 'nf-section',
4551        template: '#tmpl-nf-field-input-limit',
4552
4553        initialize: function() {
4554        	this.listenTo( nfRadio.channel( 'field-' + this.model.get( 'id' ) ), 'keyup:field', this.updateCount );
4555        	this.count = this.model.get( 'input_limit' );
4556        	this.render();
4557        },
4558
4559        updateCount: function( el, model ) {
4560            var value = jQuery( el ).val();
4561            var regex = /\s+/gi;
4562            var words = value.trim().replace(regex, ' ').split(' ');
4563            var wordCount = words.length;
4564            var charCount = value.length;
4565            
4566            /**
4567             * PHP Config has 'char' instead of 'characters', so I changed it to
4568             * 'characters', but added 'char' here so existing form fields will
4569             * act correctly
4570             **/
4571            if ( 'characters' == this.model.get( 'input_limit_type' )
4572                    || 'char' == this.model.get( 'input_limit_type' ) ) {
4573                jQuery( el ).attr( 'maxlength', this.model.get( 'input_limit' ) );
4574                this.count = this.model.get( 'input_limit' ) - charCount;
4575            } else {
4576                this.count = this.model.get( 'input_limit' ) - wordCount;
4577                var limit = this.model.get( 'input_limit' );
4578                if( wordCount > limit ){
4579                    jQuery( el ).val( words.slice( 0, limit).join( ' ' ) );
4580                }
4581            }
4582
4583        	this.render();
4584        },
4585
4586        templateHelpers: function() {
4587        	var that = this;
4588        	return {
4589        		currentCount: function() {
4590        			return that.count;
4591        		}
4592        	}
4593        }
4594
4595    });
4596
4597    return view;
4598} );
4599define( 'views/afterField',['views/fieldErrorCollection', 'views/inputLimit'], function( fieldErrorCollection, InputLimitView ) {
4600    var view = Marionette.ItemView.extend({
4601        tagName: 'nf-section',
4602        template: '#tmpl-nf-field-after',
4603
4604        initialize: function() {
4605    		this.model.on( 'change:errors', this.changeError, this );
4606        },
4607
4608        onRender: function() {
4609        	/*
4610        	 * If we have an error, render our error view.
4611        	 * TODO: Perhaps move to a controller?
4612        	 */
4613        	var errorEl = jQuery( this.el ).children( '.nf-error-wrap' );
4614    		this.errorCollectionView = new fieldErrorCollection( { el: errorEl, collection: this.model.get( 'errors' ), fieldModel: this.model } );
4615            if ( 0 < this.model.get( 'errors' ).length ) {
4616               this.errorCollectionView.render(); 
4617            }
4618            
4619    		/*
4620    		 * If we have an input limit set, render the view that contains our counter
4621    		 * TODO: Move this to a controller so that the logic isn't in the view.
4622    		 */
4623    		if ( 'undefined' != typeof this.model.get( 'input_limit' ) && '' != jQuery.trim( this.model.get( 'input_limit' ) ) ){
4624    			var inputLimitEl = jQuery( this.el ).children( '.nf-input-limit');
4625    			this.inputLimitView = new InputLimitView( { el: inputLimitEl, model: this.model } );
4626    		}
4627        },
4628
4629        changeError: function() {
4630			this.errorCollectionView.render();
4631		},
4632
4633    });
4634
4635    return view;
4636} );
4637define( 'views/fieldLayout',['views/fieldItem', 'views/beforeField', 'views/afterField'], function( fieldItem, beforeField, afterField ) {
4638
4639    var view = Marionette.LayoutView.extend({
4640        tagName: 'nf-field',
4641
4642        regions: {
4643            beforeField: '.nf-before-field',
4644            field: '.nf-field',
4645            afterField: '.nf-after-field',
4646        },
4647
4648        initialize: function() {
4649            this.listenTo( this.model, 'change:visible', this.render, this );
4650        },
4651
4652        getTemplate: function() {
4653            if ( this.model.get( 'visible' ) ) {
4654                return '#tmpl-nf-field-layout';
4655            } else {
4656                return '#tmpl-nf-empty';
4657            }
4658        },
4659
4660        onRender: function() {
4661            if ( this.model.get( 'visible' ) ) {
4662                this.beforeField.show( new beforeField( { model: this.model } ) );
4663                this.field.show( new fieldItem( { model: this.model } ) );
4664                this.afterField.show( new afterField( { model: this.model } ) );
4665            }
4666        },
4667
4668        templateHelpers: function() {
4669            return {
4670                renderContainerClass: function() {
4671                    var containerClass = ' label-' + this.label_pos + ' ';
4672                    // If we have a description position, add that to our container.
4673                    if ( 'undefined' != typeof this.desc_pos ) {
4674                        containerClass += 'desc-' + this.desc_pos + ' ';
4675                    }
4676                    // if we have a container_class field setting, add that to our container.
4677                    if ( 'undefined' != typeof this.container_class && 0 < jQuery.trim( this.container_class ).length ) {
4678                        containerClass += this.container_class + ' ';
4679                    }
4680
4681                    //check if the parent type and type are different. If
4682                    // so, add parent type container styling
4683                    
4684                    if( this.type !== this.parentType ) {
4685                        containerClass += ' ' + this.parentType + '-container';
4686                    }
4687                    return containerClass;
4688                }
4689            }
4690        }
4691
4692    });
4693
4694    return view;
4695} );
4696
4697/**
4698 * Return views that might be used in extensions.
4699 * These are un-instantiated views.
4700 * 
4701 * @package Ninja Forms builder
4702 * @subpackage Main App
4703 * @copyright (c) 2015 WP Ninjas
4704 * @since 3.0
4705 */
4706define( 'controllers/loadViews',['views/fieldItem', 'views/fieldLayout'], function( fieldItemView, fieldLayoutView ) {
4707	var controller = Marionette.Object.extend( {
4708		initialize: function() {
4709			// Reply to requests for our field item view.
4710			nfRadio.channel( 'views' ).reply( 'get:fieldItem', this.getFieldItem );
4711
4712			nfRadio.channel( 'views' ).reply( 'get:fieldLayout', this.getFieldLayout );
4713		},
4714
4715		getFieldItem: function( model ) {
4716			return fieldItemView;
4717		},
4718
4719		getFieldLayout: function() {
4720			return fieldLayoutView;
4721		}
4722
4723	});
4724
4725	return controller;
4726} );
4727/**
4728 * If a form has at least one field error, we should disable the submit button and add a form error.
4729 * If a form had errors, but all the field errors have been removed, we should remove the form error.
4730 *
4731 * @since 3.0
4732 */
4733define('controllers/formErrors',[], function() {
4734	var controller = Marionette.Object.extend( {
4735		initialize: function() {
4736			/*
4737			 * Listen for error messages being added to and removed from fields.
4738			 */
4739			this.listenTo( nfRadio.channel( 'fields' ), 'add:error', this.addError );
4740			this.listenTo( nfRadio.channel( 'fields' ), 'remove:error', this.removeError );
4741
4742			/*
4743			 * Respond to requests to get form errors
4744			 */
4745			nfRadio.channel( 'form' ).reply( 'get:errors', this.getFormErrors );
4746		},
4747
4748		addError: function( fieldModel, errorID, errorMsg ) {
4749			var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) );
4750			/*
4751			 * We store our errors in this object by field ID so that we don't have to loop over all our fields when we're testing for errors.
4752			 * They are stored as an object within an array, using the field ID as the key.
4753			 *
4754			 * If we haven't setup an array item for this field, set it as an object.
4755			 */
4756			if ( 'undefined' == typeof formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ] ) {
4757				formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ] = {};
4758			}
4759			// Add an error to our tracking array
4760			formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ][ errorID ] = errorMsg;
4761			/*
4762			 * We have at least one field error, so submmission should be prevented.
4763			 * Add a form error.
4764			 */
4765			nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'add:error', 'field-errors', formModel.get( 'settings' ).formErrorsCorrectErrors );
4766		},
4767
4768		removeError: function( fieldModel, errorID ) {
4769			var formModel = nfRadio.channel( 'app' ).request( 'get:form', fieldModel.get( 'formID' ) );
4770			// Remove this error ID from our tracking array.
4771			formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ] = _.omit( formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ], errorID );
4772			/*
4773			 * If we don't have any more error IDs on this field, then we need to remove this field from the array.
4774			 *
4775			 * Then, if the fieldErrors tracking array has a length of 0, we remove our form error, because all field errors have been dealt with.
4776			 */
4777			if ( 0 == _.size( formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ] ) ) {
4778				delete formModel.get( 'fieldErrors' )[ fieldModel.get( 'id' ) ];
4779			}
4780
4781			if ( 0 == _.size( formModel.get( 'fieldErrors' ) ) ) {
4782				// Remove our form error.
4783				nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'remove:error', 'field-errors' );
4784			}
4785		},
4786
4787		getFormErrors: function( formID ) {
4788			var formModel = nfRadio.channel( 'app' ).request( 'get:form', formID );
4789			var errors = false;
4790			
4791			if ( formModel ) {
4792				/*
4793				 * Check to see if we have any errors on our form model.
4794				 */
4795				if ( 0 !== formModel.get( 'errors' ).length ) {
4796					_.each( formModel.get( 'errors' ).models, function( error ) {
4797						errors = errors || {};
4798						errors[ error.get( 'id' ) ] = error.get( 'msg' );
4799					} );						
4800				}
4801
4802				
4803			}
4804			return errors;
4805		}
4806	});
4807
4808	return controller;
4809} );
4810/**
4811 * Handles submission of our form.
4812 */
4813define('controllers/submit',[], function() {
4814	var controller = Marionette.Object.extend( {
4815		initialize: function() {
4816			this.listenTo( nfRadio.channel( 'forms' ), 'init:model', this.registerSubmitHandler );
4817		},
4818
4819		/**
4820		 * Register the submission handler function.
4821		 *
4822		 * @since  3.0
4823		 * @param  Backbone.model 	formModel
4824		 * @return void
4825		 */
4826		registerSubmitHandler: function( formModel ) {
4827			nfRadio.channel( 'form-' + formModel.get( 'id' ) ).reply( 'submit', this.submit );
4828		},
4829
4830		/**
4831		 * Handles the actual submission of our form.
4832		 * When we submit:
4833		 *
4834		 * 1) Send out a message saying that we're about to begin form submission.
4835		 * 2) Check the form for errors
4836		 * 3) Submit the data
4837		 * 4) Send out a message with our response
4838		 *
4839		 * @since  3.0
4840		 * @param  Backbone.model 	formModel
4841		 * @return void
4842		 */
4843		submit: function( formModel ) {
4844
4845			/*
4846			 * Send out a radio message saying that we're about to begin submitting.
4847			 * First we send on the generic forms channel, and then on the form-specific channel.
4848			 */
4849			nfRadio.channel( 'forms' ).trigger( 'before:submit', formModel );
4850			nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'before:submit', formModel );
4851
4852			/*
4853			 * Validate our field models.
4854			 */
4855			var validate = nfRadio.channel( 'forms' ).request( 'maybe:validate', formModel );
4856		 	if( false !== validate ){
4857
4858                // When validating all fields, set clean to false to force validation.
4859                _.each( formModel.get( 'fields' ).models, function( fieldModel ) {
4860                    fieldModel.set( 'clean', false );
4861                } );
4862
4863				/*
4864				 * This method is defined in our models/fieldCollection.js file,
4865				 * except where overridden by an add-on (ie Layout & Styles).
4866				 */
4867				formModel.get( 'formContentData' ).validateFields();
4868			}
4869
4870			var submit = nfRadio.channel( 'form-' + formModel.get( 'id' ) ).request( 'maybe:submit', formModel );
4871			if ( false == submit ) {
4872				nfRadio.channel( 'forms' ).trigger( 'submit:cancel', formModel );
4873				nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'submit:cancel', formModel );
4874				return;
4875			}
4876
4877			if( false !== validate ){
4878
4879				// Ignore non-blocking errors.
4880				var blockingFormErrors = _.filter( formModel.get( 'errors' ).models, function( error ){
4881
4882					// Ignore email action related errors.
4883					if( 'invalid_email' == error.get( 'id' ) || 'email_not_sent' == error.get( 'id' ) ) return false;
4884
4885					return true; // Error is blocking.
4886				});
4887
4888				/*
4889				 * Make sure we don't have any form errors before we submit.
4890				 * Return false if we do.
4891				 */
4892				if ( 0 != _.size( blockingFormErrors ) ) {
4893					nfRadio.channel( 'forms' ).trigger( 'submit:failed', formModel );
4894					nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'submit:failed', formModel );
4895					return false;
4896				}
4897			}
4898
4899			/*
4900			 * Send out a radio message saying that we're about to begin submitting.
4901			 * First we send on the generic forms channel, and then on the form-specific channel.
4902			 */
4903			nfRadio.channel( 'forms' ).trigger( 'after:submitValidation', formModel );
4904			nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'after:submitValidation', formModel );
4905
4906			/*
4907			 * Actually submit our form, and send out a message with our response.
4908			 */
4909
4910 			var formID = formModel.get( 'id' );
4911			var fields = {};
4912			_.each( formModel.get( 'fields' ).models, function( field ) {
4913				var fieldDataDefaults = { value:field.get( 'value' ), id:field.get( 'id' ) };
4914
4915				// Add field data at the field ID for efficient access.
4916				fields[ field.get( 'id' ) ] = nfRadio.channel( field.get( 'type' ) ).request( 'get:submitData', fieldDataDefaults, field ) || fieldDataDefaults;;
4917			} );
4918			var extra = formModel.get( 'extra' );
4919			var settings = formModel.get( 'settings' );
4920			delete settings.formContentData;
4921			var formData = JSON.stringify( { id: formID, fields: fields, settings: settings, extra: extra } );
4922			var data = {
4923				'action': 'nf_ajax_submit',
4924				'security': nfFrontEnd.ajaxNonce,
4925				'nonce_ts': nfFrontEnd.nonce_ts,
4926				'formData': formData
4927			}
4928
4929			var that = this;
4930
4931			jQuery.ajax({
4932			    url: nfFrontEnd.adminAjax,
4933			    type: 'POST',
4934			    data: data,
4935			    cache: false,
4936			   	success: function( data, textStatus, jqXHR ) {
4937			   		try {
4938				   		var response = data;
4939				        nfRadio.channel( 'forms' ).trigger( 'submit:response', response, textStatus, jqXHR, formModel.get( 'id' ) );
4940				    	nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'submit:response', response, textStatus, jqXHR );
4941				    	jQuery( document ).trigger( 'nfFormSubmitResponse', { response: response, id: formModel.get( 'id' ) } );
4942			   		} catch( e ) {
4943			   			console.log( e );
4944			   			console.log( 'Parse Error' );
4945						console.log( e );
4946			   		}
4947
4948			    },
4949			    error: function( jqXHR, textStatus, errorThrown ) {
4950			        // Handle errors here
4951			        console.log('ERRORS: ' + errorThrown);
4952					console.log( jqXHR );
4953
4954					try {
4955						var response = jQuery.parseJSON( jqXHR.responseText );
4956						nfRadio.channel( 'forms' ).trigger( 'submit:response', response, textStatus, jqXHR, formModel.get( 'id' ) );
4957						nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'submit:response', response, textStatus, jqXHR );
4958					} catch( e ) {
4959						console.log( 'Parse Error' );
4960					}
4961
4962			        // STOP LOADING SPINNER
4963					nfRadio.channel( 'forms' ).trigger( 'submit:response', 'error', textStatus, jqXHR, errorThrown );
4964			    }
4965			});
4966
4967		}
4968
4969	});
4970
4971	return controller;
4972} );
4973
4974define( 'views/fieldCollection',['views/fieldLayout'], function( fieldLayout ) {
4975	var view = Marionette.CollectionView.extend({
4976		tagName: 'nf-fields-wrap',
4977		childView: fieldLayout
4978
4979	});
4980
4981	return view;
4982} );
4983/**
4984 * Default filters
4985 * 
4986 * @package Ninja Forms builder
4987 * @subpackage Main App
4988 * @copyright (c) 2015 WP Ninjas
4989 * @since 3.0
4990 */
4991define( 'controllers/defaultFilters',[ 'views/fieldCollection', 'models/fieldCollection' ], function( FieldCollectionView, FieldCollection ) {
4992	var controller = Marionette.Object.extend( {
4993		initialize: function() {
4994			this.listenTo( nfRadio.channel( 'form' ), 'before:filterData', this.registerDefaultDataFilter );
4995		},
4996
4997		registerDefaultDataFilter: function( formModel ) {
4998			/*
4999			 * Set our default formContent load filter
5000			 */
5001			nfRadio.channel( 'formContent' ).request( 'add:loadFilter', this.defaultFormContentLoad, 10, this );
5002			/*
5003			 * Set our default formContentView.
5004			 */
5005			nfRadio.channel( 'formContent' ).request( 'add:viewFilter', this.defaultFormContentView, 10, this );
5006		},
5007
5008		defaultFormContentLoad: function( formContentData, formModel, context ) {
5009			var fieldCollection = formModel.get( 'fields' );
5010			/*
5011			 * If we only have one load filter, we can just return the field collection.
5012			 */
5013			var formContentLoadFilters = nfRadio.channel( 'formContent' ).request( 'get:loadFilters' );
5014			var sortedArray = _.without( formContentLoadFilters, undefined );
5015			if ( 1 == sortedArray.length || 'undefined' == typeof formContentData || true === formContentData instanceof Backbone.Collection ) return formModel.get( 'fields' );
5016
5017        	var fieldModels = _.map( formContentData, function( key ) {
5018        		return formModel.get( 'fields' ).findWhere( { key: key } );
5019        	}, this );
5020
5021        	var currentFieldCollection = new FieldCollection( fieldModels );
5022
5023        	fieldCollection.on( 'reset', function( collection ) {
5024        		var resetFields = [];
5025        		currentFieldCollection.each( function( fieldModel ) {
5026        			if ( 'submit' != fieldModel.get( 'type' ) ) {
5027        				resetFields.push( collection.findWhere( { key: fieldModel.get( 'key' ) } ) );
5028        			} else {
5029        				resetFields.push( fieldModel );
5030        			}
5031        		} );
5032
5033                currentFieldCollection.options = { formModel: formModel };
5034        		currentFieldCollection.reset( resetFields );
5035        	} );
5036
5037        	return currentFieldCollection;
5038        },
5039
5040        defaultFormContentView: function() {
5041        	return FieldCollectionView;
5042        }
5043
5044	});
5045
5046	return controller;
5047} );
5048/**
5049 * Controller responsible for removing unique field errors.
5050 */
5051
5052define('controllers/uniqueFieldError',[], function() {
5053	var controller = Marionette.Object.extend( {
5054
5055		initialize: function() {
5056			/*
5057			 * Listen to keyup and field changes.
5058			 *
5059			 * If those fields have a unique field error, remove that error.
5060			 */
5061			this.listenTo( nfRadio.channel( 'fields' ), 'change:modelValue', this.removeError );
5062			this.listenTo( nfRadio.channel( 'fields' ), 'keyup:field', this.removeError );
5063			this.listenTo( nfRadio.channel( 'fields' ), 'blur:field', this.removeError );
5064
5065		},
5066
5067		removeError: function( el, model ) {
5068			model = model || el;
5069			/*
5070			 * Remove any unique field errors.
5071			 */
5072			nfRadio.channel( 'fields' ).request( 'remove:error', model.get( 'id' ), 'unique_field' );
5073		},
5074
5075	});
5076
5077	return controller;
5078} );
5079define(
5080	'controllers/loadControllers',[
5081		'controllers/formData',
5082		'controllers/fieldError',
5083		'controllers/changeField',
5084		'controllers/changeEmail',
5085		'controllers/changeDate',
5086		'controllers/fieldCheckbox',
5087		'controllers/fieldCheckboxList',
5088		'controllers/fieldImageList',
5089		'controllers/fieldRadio',
5090		'controllers/fieldNumber',
5091		'controllers/mirrorField',
5092		'controllers/confirmField',
5093		'controllers/updateFieldModel',
5094		'controllers/submitButton',
5095		'controllers/submitDebug',
5096		'controllers/getFormErrors',
5097		'controllers/validateRequired',
5098		'controllers/submitError',
5099		'controllers/actionRedirect',
5100		'controllers/actionSuccess',
5101		'controllers/fieldSelect',
5102		'controllers/coreSubmitResponse',
5103		'controllers/fieldProduct',
5104		'controllers/fieldTotal',
5105		'controllers/fieldQuantity',
5106		'controllers/calculations',
5107		'controllers/dateBackwardsCompat',
5108		'controllers/fieldDate',
5109		'controllers/fieldRecaptcha',
5110		'controllers/fieldHTML',
5111		'controllers/helpText',
5112		'controllers/fieldTextbox',
5113		'controllers/fieldTextareaRTE',
5114		'controllers/fieldStarRating',
5115		'controllers/fieldTerms',
5116		'controllers/formContentFilters',
5117		'controllers/loadViews',
5118		'controllers/formErrors',
5119		'controllers/submit',
5120		'controllers/defaultFilters',
5121		'controllers/uniqueFieldError'
5122	],
5123	function(
5124		FormData,
5125		FieldError,
5126		ChangeField,
5127		ChangeEmail,
5128		ChangeDate,
5129		FieldCheckbox,
5130		FieldCheckboxList,
5131		FieldImageList,
5132		FieldRadio,
5133		FieldNumber,
5134		MirrorField,
5135		ConfirmField,
5136		UpdateFieldModel,
5137		SubmitButton,
5138		SubmitDebug,
5139		GetFormErrors,
5140		ValidateRequired,
5141		SubmitError,
5142		ActionRedirect,
5143		ActionSuccess,
5144		FieldSelect,
5145		CoreSubmitResponse,
5146		FieldProduct,
5147		FieldTotal,
5148		FieldQuantity,
5149		Calculations,
5150		DateBackwardsCompat,
5151		FieldDate,
5152		FieldRecaptcha,
5153		FieldHTML,
5154		HelpText,
5155		FieldTextbox,
5156		FieldTextareaRTE,
5157		FieldStarRating,
5158		FieldTerms,
5159		FormContentFilters,
5160		LoadViews,
5161		FormErrors,
5162		Submit,
5163		DefaultFilters,
5164		UniqueFieldError
5165	) {
5166		var controller = Marionette.Object.extend( {
5167			initialize: function() {
5168
5169				/**
5170				 * App Controllers
5171				 */
5172				new LoadViews();
5173				new FormErrors();
5174				new Submit();
5175				
5176				/**
5177				 * Field type controllers
5178				 */
5179				new FieldCheckbox();
5180				new FieldCheckboxList();
5181				new FieldImageList();
5182				new FieldRadio();
5183				new FieldNumber();
5184				new FieldSelect();
5185				new FieldProduct();
5186				new FieldTotal();
5187				new FieldQuantity();
5188				new FieldRecaptcha();
5189				new FieldHTML();
5190				new HelpText();
5191				new FieldTextbox();
5192				new FieldTextareaRTE();
5193				new FieldStarRating();
5194				new FieldTerms();
5195				new FormContentFilters();
5196				new UniqueFieldError();
5197				/**
5198				 * Misc controllers
5199				 */
5200				new FieldError();
5201				new ChangeField();
5202				new ChangeEmail();
5203				new ChangeDate();
5204				
5205				new MirrorField();
5206				new ConfirmField();
5207				new UpdateFieldModel();
5208				new SubmitButton();
5209				new SubmitDebug();
5210				new GetFormErrors();
5211				new ValidateRequired();
5212				new SubmitError();
5213				new ActionRedirect();
5214				new ActionSuccess();
5215				
5216				new CoreSubmitResponse();
5217				new Calculations();
5218
5219				new DefaultFilters();
5220
5221				/**
5222				 * Data controllers
5223				 */
5224				new DateBackwardsCompat();
5225				new FieldDate();
5226				new FormData();
5227				
5228			}
5229		});
5230
5231		return controller;
5232} );
5233
5234define( 'views/beforeForm',[], function( ) {
5235
5236	var view = Marionette.ItemView.extend({
5237		tagName: "nf-section",
5238		template: "#tmpl-nf-before-form",
5239
5240	});
5241
5242	return view;
5243} );
5244define( 'views/formErrorItem',[], function() {
5245	var view = Marionette.ItemView.extend({
5246		tagName: 'nf-section',
5247		template: '#tmpl-nf-form-error',
5248
5249		onRender: function() {
5250			// this.$el = this.$el.children();
5251			// this.$el.unwrap();
5252			// this.setElement( this.$el );
5253		},
5254	});
5255
5256	return view;
5257} );
5258define( 'views/formErrorCollection',['views/formErrorItem'], function( formErrorItem ) {
5259	var view = Marionette.CollectionView.extend({
5260		tagName: "nf-errors",
5261		childView: formErrorItem
5262	});
5263
5264	return view;
5265} );
5266define( 'views/honeyPot',[], function() {
5267    var view = Marionette.ItemView.extend({
5268        tagName: 'nf-section',
5269        template: '#tmpl-nf-form-hp',
5270
5271        events: {
5272        	'keyup .nf-field-hp': 'maybeError',
5273            'change .nf-field-hp': 'maybeError'
5274        },
5275
5276        maybeError: function( e ) {
5277            /*
5278             * If we have an empty honeyPot field, remove the honeypot form error.
5279             * If we do not have an empty honeyPot field, add the honeypot form error.
5280             */
5281            if ( 0 == jQuery( e.target ).val().length ) {
5282                nfRadio.channel( 'form-' + this.model.get( 'id' ) ).request( 'remove:error', 'honeyPot' );
5283            } else {
5284                var formModel  = nfRadio.channel( 'app'    ).request( 'get:form',  this.model.get( 'id' ) );
5285                nfRadio.channel( 'form-' + this.model.get( 'id' ) ).request( 'add:error', 'honeyPot', formModel.get( 'settings' ).honeypotHoneypotError );
5286            }
5287        }
5288    });
5289
5290    return view;
5291} );
5292define( 'views/afterFormContent',['views/formErrorCollection', 'views/honeyPot'], function( FormErrors, HoneyPot ) {
5293
5294    var view = Marionette.LayoutView.extend({
5295        tagName: "nf-section",
5296        template: "#tmpl-nf-after-fields",
5297
5298		regions: {
5299			errors: ".nf-form-errors",
5300            hp: ".nf-form-hp"
5301		},
5302
5303        onShow: function() {
5304        	this.errors.show( new FormErrors( { collection: this.model.get( 'errors' ) } ) );
5305            this.hp.show( new HoneyPot( { model: this.model } ) );
5306        }
5307
5308    });
5309
5310    return view;
5311} );
5312define( 'views/beforeFormContent',[], function( ) {
5313
5314    var view = Marionette.ItemView.extend({
5315        tagName: "nf-section",
5316        template: "#tmpl-nf-before-fields",
5317
5318        templateHelpers: function () {
5319            return {
5320
5321                renderFieldsMarkedRequired: function() {
5322
5323                    var requiredFields = this.fields.filter( { required: 1 } );
5324                    return ( requiredFields.length ) ? this.fieldsMarkedRequired : '';
5325                },
5326            };
5327        },
5328
5329    });
5330
5331    return view;
5332} );
5333define( 'views/formLayout',[ 'views/afterFormContent', 'views/beforeFormContent', 'models/fieldCollection' ], function( AfterForm
5333Content, BeforeFormContent, FieldCollection ) {
5334
5335	var view = Marionette.LayoutView.extend({
5336		tagName: "nf-section",
5337		template: "#tmpl-nf-form-layout",
5338
5339		regions: {
5340			beforeFormContent: ".nf-before-form-content",
5341			formContent: ".nf-form-content",
5342			afterFormContent: ".nf-after-form-content"
5343		},
5344
5345		initialize: function() {
5346			nfRadio.channel( 'form-' + this.model.get( 'id' ) ).reply( 'get:el', this.getEl, this );
5347			
5348			/*
5349			 * If we need to hide a form, set the visibility of this form to hidden.
5350			 */
5351			 this.listenTo( this.model, 'hide', this.hide );
5352		},
5353
5354		onRender: function() {
5355			this.$el = this.$el.children();
5356			this.$el.unwrap();
5357			this.setElement( this.$el );
5358		},
5359
5360		onShow: function() {
5361			this.beforeFormContent.show( new BeforeFormContent( { model: this.model } ) );
5362			
5363			/*
5364			 * Set our formContentData to our form setting 'formContentData'
5365			 */
5366			var formContentData = this.model.get( 'formContentData' );
5367			
5368			/*
5369			 * Check our fieldContentViewsFilter to see if we have any defined.
5370			 * If we do, overwrite our default with the view returned from the filter.
5371			 */
5372			var formContentViewFilters = nfRadio.channel( 'formContent' ).request( 'get:viewFilters' );
5373			
5374			/* 
5375			* Get our first filter, this will be the one with the highest priority.
5376			*/
5377			var sortedArray = _.without( formContentViewFilters, undefined );
5378			var callback = _.first( sortedArray );
5379			formContentView = callback();
5380			
5381			var options = {
5382				data: formContentData,
5383				formModel: this.model
5384			};
5385			
5386			/*
5387			 * If we have a collection, pass the returned data as the collection.
5388			 *
5389			 * If we have a model, pass the returned data as the collection.
5390			 */
5391			if ( false !== formContentData instanceof Backbone.Collection ) {
5392				options.collection = formContentData;
5393			} else if ( false !== formContentData instanceof Backbone.Model ) {
5394				options.model = formContentData;
5395			}
5396
5397			this.formContent.show( new formContentView( options ) );
5398			this.afterFormContent.show( new AfterFormContent( { model: this.model } ) );
5399		},
5400
5401		getEl: function() {
5402			return this.el;
5403		},
5404
5405        templateHelpers: function () {
5406            return {
5407
5408                renderClasses: function() {
5409                    return '';
5410                }
5411
5412            };
5413        },
5414
5415        hide: function() {
5416        	jQuery( this.el ).hide();
5417        }
5418
5419	});
5420
5421	return view;
5422} );
5423define( 'views/afterForm',[], function( ) {
5424
5425	var view = Marionette.ItemView.extend({
5426		tagName: "nf-section",
5427		template: "#tmpl-nf-after-form",
5428		
5429	});
5430
5431	return view;
5432} );
5433define( 'views/mainLayout',['views/beforeForm', 'views/formLayout', 'views/afterForm'], function( BeforeForm, FormLayout, AfterForm ) {
5434
5435	var view = Marionette.LayoutView.extend({
5436		template: '#tmpl-nf-layout',
5437
5438		regions: {
5439			responseMsg: '.nf-response-msg',
5440			beforeForm: '.nf-before-form',
5441			formLayout: '.nf-form-layout',
5442			afterForm: '.nf-after-form'
5443		},
5444
5445		initialize: function() {
5446			this.$el = jQuery( '#nf-form-' + this.model.id + '-cont' );
5447			this.el = '#nf-form-' + this.model.id + '-cont';
5448
5449			this.render();
5450
5451			this.beforeForm.show( new BeforeForm( { model: this.model } ) );
5452			this.formLayout.show( new FormLayout( { model: this.model, fieldCollection: this.options.fieldCollection } ) );
5453			this.afterForm.show( new AfterForm( { model: this.model } ) );
5454
5455			/*
5456			 * If we need to hide a form, set the visibility of this form to hidden.
5457			 */
5458			 this.listenTo( this.model, 'hide', this.hide );
5459		},
5460
5461        hide: function() {
5462        	jQuery( this.el ).find( '.nf-form-title' ).hide();
5463        }
5464
5465	});
5466
5467	return view;
5468} );
5469// const Intl = require('intl');
5470
5471// class nfLocaleConverter {
5472var nfLocaleConverter = function(newLocale, thousands_sep, decimal_sep) {
5473
5474    // constructor(newLocale = 'en-US', thousands_sep, decimal_sep) {
5475        if ('undefined' !== typeof newLocale && 0 < newLocale.length) {
5476            this.locale = newLocale.split('_').join('-');
5477        } else {
5478            this.locale = 'en-US';
5479        }
5480
5481        this.thousands_sep = thousands_sep || ',';
5482        this.decimal_sep = decimal_sep || '.';
5483    // }
5484
5485    this.uniqueElememts = function( value, index, self ) {
5486        return self.indexOf(value) === index;
5487    }
5488
5489    this.numberDecoder = function(num) {
5490        num = num.toString();
5491        // let thousands_sep = ',';
5492        var formatted = '';
5493
5494        // Account for negative numbers.
5495        var negative = false;
5496        
5497        if ('-' === num.charAt(0)) {
5498            negative = true;
5499            num = num.replace( '-', '' );
5500        }
5501        
5502        // Account for a space as the thousands separator.
5503        // This pattern accounts for all whitespace characters (including thin space).
5504        num = num.replace( /\s/g, '' );
5505        num = num.replace( /&nbsp;/g, '' );
5506
5507        // Determine what our existing separators are.
5508        var myArr = num.split('');
5509        var separators = myArr.filter(function(el) {
5510            return !el.match(/[0-9]/);
5511          });
5512          
5513        var final_separators = separators.filter(this.uniqueElememts);
5514        
5515        switch( final_separators.length ) {
5516            case 0:
5517                formatted = num;
5518                break;
5519            case 1:
5520                var replacer = '';
5521                if ( 1 == separators.length ) {
5522                    separator = separators.pop();
5523                    var sides = num.split(separator);
5524                    var last = sides.pop();
5525                    if ( 3 == last.length && separator == this.thousands_sep ) {
5526                        replacer = '';
5527                    } else {
5528                        replacer = '.';
5529                    }
5530                } else {
5531                    separator = final_separators.pop();
5532                }
5533
5534                formatted = num.split(separator).join(replacer);
5535                break;
5536            case 2:
5537                var find_one = final_separators[0];
5538                var re_one;
5539                if('.' === find_one) {
5540                    re_one = new RegExp('[.]', 'g');
5541                } else {
5542                    re_one = new RegExp(find_one, 'g');
5543                }
5544                formatted = num.replace(re_one, '');
5545                
5546                var find_two = final_separators[1];
5547                
5548                var re_two;
5549                if('.' === find_two) {
5550                    re_two = new RegExp('[.]', 'g');
5551                } else {
5552                    re_two = new RegExp(find_two, 'g');
5553                }
5554                formatted = formatted.replace(re_two, '.' );
5555                break;
5556            default:
5557            return 'NaN';
5558        }
5559
5560        if ( negative ) {
5561            formatted = '-' + formatted;
5562        }
5563        this.debug('Number Decoder ' + num + ' -> ' + formatted );
5564        return formatted;
5565    }
5566
5567    this.numberEncoder = function(num, percision) {
5568        num = this.numberDecoder(num);
5569
5570        return Intl.NumberFormat(this.locale, { minimumFractionDigits: percision, maximumFractionDigits: percision }).format(num);
5571    }
5572
5573    this.debug = function(message) {
5574        if ( window.nfLocaleConverterDebug || false ) console.log(message);
5575    }
5576}
5577
5578// module.exports = nfLocaleConverter;
5579define("../nfLocaleConverter", function(){});
5580
5581/*
5582 * Because our backbone listens to .change() events on elements, changes made using jQuery .val() don't bubble properly.
5583 * This patch overwrites the default behaviour of jQuery .val() so that IF the item has an nf-element class, we fire a change event.
5584 */
5585( function( jQuery ) {
5586	/*
5587	 * Store our original .val() function.
5588	 */
5589    var originalVal = jQuery.fn.val;
5590    /*
5591     * Create our own .val() function.
5592     */
5593    jQuery.fn.val = function(){
5594        var prev;
5595        /* 
5596         * Store a copy of the results of the original .val() call.
5597         * We use this to make sure that we've actually changed something.
5598         */
5599        if( arguments.length > 0 ){
5600            prev = originalVal.apply( this,[] );
5601        }
5602        /*
5603         * Get the results of the original .val() call. 
5604         */
5605        var result = originalVal.apply( this, arguments );
5606
5607        /*
5608         * If we have arguments, we have actually made a change, AND this has the nf-element class, trigger .change().
5609         */
5610        if( arguments.length > 0 && prev != originalVal.apply( this, [] ) && jQuery( this ).hasClass( 'nf-element' ) ) {
5611			jQuery(this).change();
5612        }
5613
5614        return result;
5615    };
5616} ) ( jQuery );
5617
5618jQuery( document ).ready( function( $ ) {
5619	require( [ 'models/formCollection', 'models/formModel', 'models/fieldCollection', 'controllers/loadControllers', 'views/mainLayout', '../nfLocaleConverter'], function( formCollection, FormModel, FieldCollection, LoadControllers, mainLayout ) {
5620
5621		if( 'undefined' == typeof nfForms ) {
5622			/*
5623			 * nfForms is not defined. This means that something went wrong loading the form data.
5624			 * Bail form setup and empty the form containers to remove any loading animations.
5625			 */
5626			jQuery( '.nf-form-cont' ).empty();
5627			return;
5628		}
5629
5630		var NinjaForms = Marionette.Application.extend({
5631			forms: {},
5632			initialize: function( options ) {
5633				var that = this;
5634				Marionette.Renderer.render = function(template, data){
5635					var template = that.template( template );
5636					return template( data );
5637				};
5638
5639				// Underscore one-liner for getting URL Parameters
5640				this.urlParameters = _.object(_.compact(_.map(location.search.slice(1).split('&'), function(item) {  if (item) return item.split('='); })));
5641
5642				if( 'undefined' != typeof this.urlParameters.nf_resume ) {
5643					this.listenTo(nfRadio.channel('form-' + this.urlParameters.nf_resume), 'loaded', this.restart);
5644				}
5645
5646				nfRadio.channel( 'app' ).reply( 'locale:decodeNumber', this.decodeNumber);
5647
5648				nfRadio.channel( 'app' ).reply( 'locale:encodeNumber',this.encodeNumber);
5649
5650				var loadControllers = new LoadControllers();
5651				nfRadio.channel( 'app' ).trigger( 'after:loadControllers' );
5652
5653				nfRadio.channel( 'app' ).reply( 'get:template', this.template );			},
5654			
5655			onStart: function() {
5656				var formCollection = nfRadio.channel( 'app' ).request( 'get:forms' );
5657				_.each( formCollection.models, function( form, index ) {
5658					var layoutView = new mainLayout( { model: form, fieldCollection: form.get( 'fields' ) } );			
5659					nfRadio.channel( 'form' ).trigger( 'render:view', layoutView );
5660					jQuery( document ).trigger( 'nfFormReady', layoutView );
5661				} );
5662			},
5663
5664			restart: function( formModel ) {
5665				if( 'undefined' != typeof this.urlParameters.nf_resume ){
5666					var data = {
5667						'action': 'nf_ajax_submit',
5668						'security': nfFrontEnd.ajaxNonce,
5669						'nf_resume': this.urlParameters
5670					};
5671
5672					nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'disable:submit' );
5673					nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'processingLabel' );
5674
5675					this.listenTo( nfRadio.channel( 'form' ), 'render:view', function() {
5676						/**
5677						 * TODO: This needs to be re-worked for backbone. It's not dynamic enough.
5678						 */
5679						/*
5680						 * Hide form fields (but not the submit button).
5681						 */
5682						jQuery( '#nf-form-' + formModel.get( 'id' ) + '-cont .nf-field-container:not(.submit-container)' ).hide();
5683					});
5684
5685					// TODO: Refactor Duplication
5686					jQuery.ajax({
5687						url: nfFrontEnd.adminAjax,
5688						type: 'POST',
5689						data: data,
5690						cache: false,
5691						success: function( data, textStatus, jqXHR ) {
5692							try {
5693						   		var response = data;
5694						        nfRadio.channel( 'forms' ).trigger( 'submit:response', response, textStatus, jqXHR, formModel.get( 'id' ) );
5695						    	nfRadio.channel( 'form-' + formModel.get( 'id' ) ).trigger( 'submit:response', response, textStatus, jqXHR );
5696							} catch( e ) {
5697								console.log( 'Parse Error' );
5698							}
5699
5700					    },
5701					    error: function( jqXHR, textStatus, errorThrown ) {
5702					        // Handle errors here
5703					        console.log('ERRORS: ' + textStatus);
5704					        // STOP LOADING SPINNER
5705							nfRadio.channel( 'forms' ).trigger( 'submit:response', 'error', textStatus, jqXHR, errorThrown );
5706					    }
5707					});
5708				}
5709			},
5710
5711			template: function( template ) {
5712				return _.template( $( template ).html(),  {
5713					evaluate:    /<#([\s\S]+?)#>/g,
5714					interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
5715					escape:      /\{\{([^\}]+?)\}\}(?!\})/g,
5716					variable:    'data'
5717				} );
5718			},
5719
5720			encodeNumber: function(num) {
5721				var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point);
5722
5723				return localeConverter.numberEncoder(num);
5724			},
5725
5726			decodeNumber: function(num) {
5727				var localeConverter = new nfLocaleConverter(nfi18n.siteLocale, nfi18n.thousands_sep, nfi18n.decimal_point);
5728
5729				return localeConverter.numberDecoder(num);
5730			}
5731		});
5732	
5733		var ninjaForms = new NinjaForms();
5734		ninjaForms.start();		
5735	} );
5736} );
5737
5738define("main", function(){});
5739
5740}());
5741//# sourceMappingURL=front-end.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.