PageSourceSearch

https://lifehappens.org/wp-content/plugins/ninja-forms-multi-part/assets/js/min/front-end.js?ver=3.0.31

js lifehappens.org collected 2026-10-01 11:37:15 UTC 62,289 bytes, 1,778 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( 'views/header',[], function() {
436	var view = Marionette.ItemView.extend( {
437		template: "#tmpl-nf-mp-header",
438 		fullProgressBar: false,
439
440 		initialize: function( options ) {
441			this.listenTo( this.collection, 'change:part', this.reRender );
442			this.listenTo( this.collection, 'change:errors', this.reRender );
443
444			this.listenTo( nfRadio.channel( 'forms' ), 'before:submit', this.fillProgressBar );
445		},
446
447		reRender: function() {
448			this.model = this.collection.getElement();
449			this.render();
450		},
451
452		templateHelpers: function() {
453			var that = this;
454			return {
455				renderPartTitle: function() {
456					if ( 'undefined' == typeof that.collection.formModel.get( 'settings' ).mp_display_titles || 0 == that.collection.formModel.get( 'settings' ).mp_display_titles ) return '';
457					
458					var template = Backbone.Radio.channel( 'app' ).request( 'get:template', '#tmpl-nf-mp-part-title' );
459					var parts = _.invoke( that.collection.getVisibleParts(), 'pick', [ 'title', 'errors', 'visible' ] )
460					if ( 1 < parts.length ) {
461						return template( { title: this.title } );
462					} else {
463						return ''; 
464					}
465				},
466
467				renderBreadcrumbs: function() {
468					if ( 'undefined' == typeof that.collection.formModel.get( 'settings' ).mp_breadcrumb || 0 == that.collection.formModel.get( 'settings' ).mp_breadcrumb ) return '';
469
470					var template = Backbone.Radio.channel( 'app' ).request( 'get:template', '#tmpl-nf-mp-breadcrumbs' );
471					var parts = _.invoke( that.collection.getVisibleParts(), 'pick', [ 'title', 'errors', 'visible' ] )
472					if ( 1 < parts.length ) {
473						return template( { parts: parts, currentIndex: that.collection.getVisibleParts().indexOf( that.model ) } );
474					} else {
475						return ''; 
476					}
477				},
478
479				renderProgressBar: function() {
480					if ( 'undefined' == typeof that.collection.formModel.get( 'settings' ).mp_progress_bar || 0 == that.collection.formModel.get( 'settings' ).mp_progress_bar ) return '';
481					
482					var template = Backbone.Radio.channel( 'app' ).request( 'get:template', '#tmpl-nf-mp-progress-bar' );
483					var currentIndex = that.collection.getVisibleParts().indexOf( that.model );
484					var percent = ( that.fullProgressBar ) ? 100 : currentIndex / that.collection.getVisibleParts().length * 100;
485					if ( 1 < that.collection.getVisibleParts().length ) {
486						return template( { percent: percent } );
487					} else {
488						return '';
489					}
490				}
491			}
492		},
493
494		events: {
495			'click .nf-breadcrumb': 'clickBreadcrumb'
496		},
497
498		clickBreadcrumb: function( e ) {
499			e.preventDefault();
500			this.collection.setElement( this.collection.getVisibleParts()[ jQuery( e.target ).data( 'index' ) ] );
501		},
502
503		fillProgressBar: function( formModel ) {
504			this.fullProgressBar = true;
505			this.render();
506			this.fullProgressBar = false;
507		}
508	} );
509
510	return view;
511} );
512define( 'views/footer',[], function() {
513	var view = Marionette.ItemView.extend( {
514		template: "#tmpl-nf-mp-footer",
515
516		initialize: function( options ) {
517			this.listenTo( this.collection, 'change:part', this.reRender );
518		},
519
520		reRender: function() {
521			this.model = this.collection.getElement();
522			this.render();
523		},
524
525		templateHelpers: function() {
526			var that = this;
527			return {
528				renderNextPrevious: function() {
529					var template = Backbone.Radio.channel( 'app' ).request( 'get:template', '#tmpl-nf-mp-next-previous' );
530					var showNext = false;
531					var showPrevious = false;
532					var visibleParts = that.collection.where( { visible: true } );
533					
534					/*
535					 * If our collection pointer isn't on the last visible part, show Next navigation.
536					 */
537					if ( visibleParts.indexOf( that.model ) != visibleParts.length - 1 ) {
538						showNext = true;
539					}
540
541					/*
542					 * If our collection pointer isn't on the first visible part, show Previous navigation.
543					 */
544					if ( visibleParts.indexOf( that.model ) != 0 ) {
545						showPrevious = true;
546					}
547
548					if ( ! showNext && ! showPrevious ) return '';
549
550					var prevLabel = that.collection.formModel.get( 'mp_prev_label' ) || nfMPSettings.prevLabel;
551					var nextLabel = that.collection.formModel.get( 'mp_next_label' ) || nfMPSettings.nextLabel;
552
553					return template( { showNext: showNext, showPrevious: showPrevious, prevLabel: prevLabel, nextLabel: nextLabel } );
554				},
555			}
556		}
557
558	} );
559
560	return view;
561} );
562
563define( 'views/formContent',[ 'views/header', 'views/footer' ], function( HeaderView, FooterView ) {
564	var view = Marionette.LayoutView.extend( {
565		template: "#tmpl-nf-mp-form-content",
566
567		regions: {
568			header: '.nf-mp-header',
569			body: '.nf-mp-body',
570			footer: '.nf-mp-footer'
571		},
572
573		initialize: function( options ) {
574			this.formModel = options.formModel;
575			this.collection = options.data;
576			this.listenTo( this.collection, 'change:part', this.changePart );
577			this.listenTo( this.collection, 'change:visible', this.renderHeaderFooter );
578		},
579
580		onRender: function() {
581			this.header.show( new HeaderView( { collection: this.collection, model: this.collection.getElement() } ) );
582
583			/*
584			 * Check our fieldContentViewsFilter to see if we have any defined.
585			 * If we do, overwrite our default with the view returned from the filter.
586			 */
587			var formContentViewFilters = nfRadio.channel( 'formContent' ).request( 'get:viewFilters' );
588			
589			/* 
590			* Get our first filter, this will be the one with the highest priority.
591			*/
592			var sortedArray = _.without( formContentViewFilters, undefined );
593			var callback = sortedArray[1];
594			this.formContentView = callback();
595			
596			this.body.show(  new this.formContentView( { collection: this.collection.getElement().get( 'formContentData' ) } ) );
597			this.footer.show( new FooterView( { collection: this.collection, model: this.collection.getElement() } ) );
598		},
599
600		renderHeaderFooter: function() {
601			this.header.show( new HeaderView( { collection: this.collection, model: this.collection.getElement() } ) );
602			this.footer.show( new FooterView( { collection: this.collection, model: this.collection.getElement() } ) );
603		},
604
605		changePart: function() {
606			this.body.show(  new this.formContentView( { collection: this.collection.getElement().get( 'formContentData' ) } ) );
607			/*
608			 * Scroll the page to the top of the form.
609			 */
610			var formTop = jQuery( this.body.el ).closest( '.nf-form-cont' ).offset().top;
611			if ( jQuery( window ).scrollTop() > formTop - 50 ) {
612				jQuery( window ).scrollTop( formTop - 50 );
613			}
614		},
615
616		events: {
617			'click .nf-next': 'clickNext',
618			'click .nf-previous': 'clickPrevious'
619		},
620
621		clickNext: function( e ) {
622			e.preventDefault();
623			this.collection.next();
624		},
625
626		clickPrevious: function( e ) {
627			e.preventDefault();
628			this.collection.previous();
629		}
630
631	} );
632
633	return view;
634} );
635define( 'models/partModel',[], function() {
636	var model = Backbone.Model.extend( {
637		fieldErrors: {},
638
639		defaults: {
640			errors: false,
641			visible: true,
642			title: ''
643		},
644
645		initialize: function() {
646			this.filterFormContentData();
647			this.listenTo( this.get( 'formContentData' ), 'change:errors', this.maybeChangeActivePart );
648			// Repeater children live outside formContentData, so their error changes
649			// never reach maybeChangeActivePart above. Without this, a Repeater's
650			// required-field error only clears on the next Next/Submit click instead
651			// of live, the moment the user corrects it — unlike every other field type.
652			this.listenTo( nfRadio.channel( 'fields' ), 'add:error', this.maybeRepeaterError );
653			this.listenTo( nfRadio.channel( 'fields' ), 'remove:error', this.maybeRepeaterError );
654			this.fieldErrors[ this.cid ] = [];
655			this.on( 'change:visible', this.changeVisible, this );
656            // Cast order as a number to avoid string comparison.
657            this.set( 'order', Number( this.get( 'order' ) ) );
658		},
659
660		filterFormContentData: function() {
661			if ( ! this.get( 'formContentData' ) ) return;
662			var formContentData = this.get( 'formContentData' );
663			/*
664			 * Update our formContentData by running it through our fromContentData filter
665			 */
666			var formContentLoadFilters = nfRadio.channel( 'formContent' ).request( 'get:loadFilters' );
667			/* 
668			* Get our second filter, this will be the one with the highest priority after MP Forms.
669			*/
670			var sortedArray = _.without( formContentLoadFilters, undefined );
671			var callback = sortedArray[ 1 ];
672			/*
673			 * If our formContentData is an empty array, we want to pass the "empty" flag as true so that filters know it's purposefully empty.
674			 */
675			var empty = ( 0 == formContentData.length ) ? true : false;
676			/*
677			 * TODO: This is a bandaid fix to prevent forms with layouts and parts from freaking out of layouts & styles are deactivated.
678			 * If Layouts is deactivated, it will try to grab the layout data and show the fields on the appropriate parts.
679			 */
680			if ( 'undefined' == typeof formContentLoadFilters[4] && _.isArray( formContentData ) && 0 != formContentData.length && 'undefined' != typeof formContentData[0].cells ) {
681				/*
682				 * We need to get our field keys from our layout data.
683				 * Layout data looks like:
684				 * Rows
685				 *   Row
686				 *     Cells
687				 *       Cell
688				 *         Fields
689				 *       Cell
690				 *         Fields
691				 *   Row
692				 *     Cells
693				 *       Cell
694				 *         Fields  
695				 */
696				var partFields = [];
697				var cells = _.pluck( formContentData, 'cells' );
698				_.each( cells, function( cell ) {
699					var fields = _.flatten( _.pluck( cell, 'fields' ) );
700					partFields = _.union( partFields, fields );
701				} );
702
703				formContentData = partFields;
704
705				this.set( 'formContentData', formContentData );
706			}
707
708			this.set( 'formContentData', callback( formContentData, this.collection.formModel, empty, formContentData ) );
709		},
710
711		maybeChangeActivePart: function( fieldModel ) {
712			/*
713			 * If we have an error on this part, add an error to our part model.
714			 *
715			 * If we are on a part that has a higher index than the current part, set this as current.
716			 */
717			if ( 0 < fieldModel.get( 'errors' ).length ) {
718				this.set( 'errors', true );
719				this.fieldErrors[ this.cid ].push( fieldModel.get( 'key' ) );
720				// this.set( 'fieldErrors', fieldModel.get( 'key' ) );
721				if (
722					this.collection.getElement() != this &&
723					this.collection.indexOf( this.collection.getElement() ) > this.collection.indexOf( this )
724
725				) {
726					this.collection.setElement( this );
727				}
728			} else {
729				this.fieldErrors[ this.cid ] = _.without( this.fieldErrors[ this.cid ], fieldModel.get( 'key' ) );
730				if ( 0 == this.fieldErrors[ this.cid ].length ) {
731					this.set( 'errors', false );
732				}
733			}
734		},
735
736		validateFields: function() {
737			this.get( 'formContentData' ).validateFields();
738		},
739
740		changeVisible: function() {
741			if ( this.get( 'visible' ) ) {
742				this.get( 'formContentData' ).showFields();
743				this.cascadeRepeaterChildrenVisible( true );
744			} else {
745				this.get( 'formContentData' ).hideFields();
746				this.cascadeRepeaterChildrenVisible( false );
747			}
748		},
749
750		// Returns the `key` of every field directly in this part's formContentData
751		// (not Repeater children, which live outside it — see getRepeaterFieldIds).
752		// Handles both flat fieldCollection (no Layouts) and Row→Cell structure
753		// (with Layouts), same as getRepeaterFieldIds below. Used by
754		// conditionalLogic.js to scope its post-show/hide re-check to only the
755		// Conditional Logic rules that actually reference a field this part just
756		// forced visible/hidden via showFields()/hideFields() (#270) — instead of
757		// blindly re-checking every condition in the form, including unrelated
758		// ones whose own field-value-driven status may not have settled yet.
759		getFieldKeys: function() {
760			var keys = [];
761			var formContentData = this.get( 'formContentData' );
762			if ( ! formContentData || ! formContentData.each ) return keys;
763
764			formContentData.each( function( rowOrFieldModel ) {
765				// Flat case: each model in the collection is a field model
766				if ( rowOrFieldModel.get( 'key' ) ) {
767					keys.push( rowOrFieldModel.get( 'key' ) );
768					return;
769				}
770
771				// Layouts case: row model has a 'cells' Backbone Collection
772				var cells = rowOrFieldModel.get( 'cells' );
773				if ( ! cells ) return;
774				var cellItems = cells.models ? cells.models : cells;
775				_.each( cellItems, function( cell ) {
776					if ( ! cell ) return;
777					var fields = cell.get ? cell.get( 'fields' ) : cell.fields;
778					var fieldItems = fields && fields.models ? fields.models : ( f
778ields || [] );
779					_.each( fieldItems, function( fieldData ) {
780						if ( ! fieldData ) return;
781						var fKey = fieldData.get ? fieldData.get( 'key' ) : fieldData.key;
782						if ( fKey ) keys.push( fKey );
783					} );
784				} );
785			} );
786
787			return keys;
788		},
789
790		// Returns all Repeater field IDs in this part's formContentData.
791		// Handles both flat fieldCollection (no Layouts) and Row→Cell structure (with Layouts).
792		// Layouts stores fields inside Backbone Collection cells, not as direct models.
793		getRepeaterFieldIds: function() {
794			var ids = [];
795			var formContentData = this.get( 'formContentData' );
796			if ( ! formContentData || ! formContentData.each ) return ids;
797
798			formContentData.each( function( rowOrFieldModel ) {
799				// Flat case: each model in the collection is a field model
800				if ( 'repeater' === rowOrFieldModel.get( 'type' ) ) {
801					ids.push( String( rowOrFieldModel.get( 'id' ) ) );
802					return;
803				}
804
805				// Layouts case: row model has a 'cells' Backbone Collection
806				// Use .models — Backbone Collections don't expose numeric indices
807				var cells = rowOrFieldModel.get( 'cells' );
808				if ( ! cells ) return;
809				var cellItems = cells.models ? cells.models : cells;
810				_.each( cellItems, function( cell ) {
811					if ( ! cell ) return;
812					var fields = cell.get ? cell.get( 'fields' ) : cell.fields;
813					var fieldItems = fields && fields.models ? fields.models : ( fields || [] );
814					_.each( fieldItems, function( fieldData ) {
815						if ( ! fieldData ) return;
816						var fType = fieldData.get ? fieldData.get( 'type' ) : fieldData.type;
817						var fId   = fieldData.get ? fieldData.get( 'id' )   : fieldData.id;
818						if ( 'repeater' === fType ) {
819							ids.push( String( fId ) );
820						}
821					} );
822				} );
823			} );
824
825			return ids;
826		},
827
828		// Returns child field models for Repeater fields that are individually
829		// hidden (e.g., via conditional logic) within this part's formContentData.
830		// Used by partErrors() to suppress them during validation so validateRequired
831		// skips them, and to scrub their stale errors from formModel.fieldErrors.
832		getHiddenRepeaterChildModels: function() {
833			var children = [];
834			var formContentData = this.get( 'formContentData' );
835			if ( ! formContentData || ! formContentData.each ) return children;
836
837			formContentData.each( function( rowOrFieldModel ) {
838				// Flat case: each model in the collection is a field model
839				if ( 'repeater' === rowOrFieldModel.get( 'type' ) ) {
840					if ( ! rowOrFieldModel.get( 'visible' ) ) {
841						var fieldset = nfRadio.channel( 'field-repeater' ).request( 'get:repeaterFields', String( rowOrFieldModel.get( 'id' ) ) );
842						if ( fieldset && fieldset.fields ) {
843							children = children.concat( fieldset.fields );
844						}
845					}
846					return;
847				}
848
849				// Layouts case: row model has a 'cells' Backbone Collection
850				var cells = rowOrFieldModel.get( 'cells' );
851				if ( ! cells ) return;
852				var cellItems = cells.models ? cells.models : cells;
853				_.each( cellItems, function( cell ) {
854					if ( ! cell ) return;
855					var fields = cell.get ? cell.get( 'fields' ) : cell.fields;
856					var fieldItems = fields && fields.models ? fields.models : ( fields || [] );
857					_.each( fieldItems, function( fieldData ) {
858						if ( ! fieldData ) return;
859						var fType    = fieldData.get ? fieldData.get( 'type' )    : fieldData.type;
860						var fId      = fieldData.get ? fieldData.get( 'id' )      : fieldData.id;
861						var fVisible = fieldData.get ? fieldData.get( 'visible' ) : fieldData.visible;
862						if ( 'repeater' === fType && ! fVisible ) {
863							var fieldset = nfRadio.channel( 'field-repeater' ).request( 'get:repeaterFields', String( fId ) );
864							if ( fieldset && fieldset.fields ) {
865								children = children.concat( fieldset.fields );
866							}
867						}
868					} );
869				} );
870			} );
871
872			return children;
873		},
874
875		// Fires on every field's add:error/remove:error, form-wide. Cheap no-op for
876		// fields outside this part; for a Repeater child that belongs to this part,
877		// re-syncs this.errors immediately so the part-tab UI updates live instead
878		// of waiting for the next Next/Submit click.
879		maybeRepeaterError: function( fieldModel ) {
880			var formModel = this.collection && this.collection.formModel;
881			if ( ! formModel || ! fieldModel || 'undefined' === typeof fieldModel.get( 'formID' ) ) return;
882
883			if ( this.hasRepeaterChildErrors( formModel ) ) {
884				this.set( 'errors', true );
885			} else if ( 0 === this.fieldErrors[ this.cid ].length ) {
886				this.set( 'errors', false );
887			}
888		},
889
890		// Returns true if formModel.fieldErrors contains errors for any Repeater
891		// child that belongs to this part. Used by partCollection.partErrors() to
892		// block Next navigation when mp_validate=1 and a Repeater child is invali
892d.
893		hasRepeaterChildErrors: function( formModel ) {
894			var fieldErrors = formModel.get( 'fieldErrors' );
895			if ( 0 === _.size( fieldErrors ) ) return false;
896
897			return _.any( this.getRepeaterFieldIds(), function( repId ) {
898				var prefix = repId + '.';
899				return _.any( Object.keys( fieldErrors ), function( key ) {
900					return 0 === key.indexOf( prefix );
901				} );
902			} );
903		},
904
905		cascadeRepeaterChildrenVisible: function( visible ) {
906			var formModel = this.collection && this.collection.formModel;
907			var fieldErrors = formModel && formModel.get( 'fieldErrors' );
908
909			_.each( this.getRepeaterFieldIds(), function( repeaterFieldId ) {
910				// nfAllRepeaterFieldsModels persists across view re-renders;
911				// fieldModel.get('sets') may be cleared when the view is destroyed.
912				var fieldset = nfRadio.channel( 'field-repeater' ).request( 'get:repeaterFields', repeaterFieldId );
913				if ( fieldset && fieldset.fields ) {
914					_.each( fieldset.fields, function( childModel ) {
915						childModel.set( 'visible', visible );
916						if ( ! visible ) {
917							var errors = childModel.get( 'errors' );
918							if ( errors && errors.get( 'required-error' ) ) {
919								errors.remove( errors.get( 'required-error' ) );
920								childModel.trigger( 'change:errors', childModel );
921							}
922						}
923					} );
924				}
925
926				// Scrub formModel.fieldErrors by prefix — the channel-based remove:error
927				// silently no-ops when stale instances accumulate in nfAllRepeaterFieldsModels.
928				if ( ! visible && fieldErrors ) {
929					var prefix = repeaterFieldId + '.';
930					_.each( Object.keys( fieldErrors ), function( key ) {
931						if ( 0 === key.indexOf( prefix ) ) {
932							delete fieldErrors[ key ];
933						}
934					} );
935				}
936			} );
937
938			if ( ! visible && formModel && 0 === _.size( formModel.get( 'fieldErrors' ) ) ) {
939				var formId = formModel.get( 'id' );
940				nfRadio.channel( 'form-' + formId ).request( 'remove:error', 'field-errors' );
941				// Guard: during early initialization the form may not yet be registered
942				// with the app channel. Skip the trigger — the submit button is enabled
943				// by default so there is nothing to re-enable at that point.
944				if ( nfRadio.channel( 'app' ).request( 'get:form', formId ) ) {
945					nfRadio.channel( 'form-' + formId ).trigger( 'enable:submit' );
946				}
947			}
948		}
949	} );
950
951	return model;
952} );
953
954define( 'models/partCollection',[ 'models/partModel' ], function( PartModel ) {
955	var collection = Backbone.Collection.extend( {
956		model: PartModel,
957		currentElement: false,
958
959		initialize: function( models, options ){
960			this.formModel = options.formModel;
961		},
962		
963		getElement: function() {
964			/*
965			 * If we haven't set an element yet, set it to the first one.
966			 */
967			if ( ! this.currentElement ) {
968				this.setElement( this.at( 0 ), true );
969			}
970			return this.currentElement;
971		},
972
973		setElement: function( model, silent ) {
974			silent = silent || false;
975
976			/*
977			 * If we have part errors and aren't updating silently, check for part errors.
978			 */
979			if ( ! silent ) {
980				if ( this.partErrors() ) return;
981			}
982			
983			this.currentElement = model;
984			if ( ! silent ) {
985				this.trigger( 'change:part', this );
986				nfRadio.channel( 'nfMP' ).trigger( 'change:part', this );
987			} 
988		},
989		  
990		// check for errors on Next click if we have validate parts on
991		setNextElement: function( model, silent ) {
992			silent = silent || false;
993
994			/*
995			 * If we have part errors and aren't updating silently, check for part errors.
996			 */
997			if ( ! silent ) {
998				if ( this.partErrors() ) return;
999			}
1000			
1001			this.currentElement = model;
1002			if ( ! silent ) {
1003				this.trigger( 'change:part', this );
1004				nfRadio.channel( 'nfMP' ).trigger( 'change:part', this );
1005			} 
1006		},
1007
1008		// We don't want to stop user from moving back in the form if there are errors
1009		setPreviousElement: function( model, silent ) {
1010			silent = silent || false;
1011			
1012			this.currentElement = model;
1013			if ( ! silent ) {
1014				this.trigger( 'change:part', this );
1015				nfRadio.channel( 'nfMP' ).trigger( 'change:part', this );
1016			}
1017		},
1018		
1019		next: function (){
1020			/*
1021			 * If this isn't the last visible part, move forward.
1022			 */
1023			if ( this.getVisibleParts().length - 1 != this.getVisibleParts().indexOf( this.getElement() ) ) {
1024				this.setNextElement( this.getVisibleParts()[ this.getVisibleParts().indexOf( this.getElement() ) + 1 ] );
1025			}
1026			
1027			return this;
1028		},
1029
1030		previous: function() {
1031			/*
1032			 * If this isn't the first visible part, move backward.
1033			 */
1034			if ( 0 != this.getVisibleParts().indexOf( this.getElement() ) ) {
1035				this.setPreviousElement( this.getVisibleParts()[ this.getVisibleParts().indexOf( this.getElement() ) - 1 ] );	
1036			}
1037			
1038			return this;
1039		},
1040
1041		partErrors: function() {
1042			if ( 'undefined' == typeof this.formModel.get( 'settings' ).mp_validate || 0 == this.formModel.get( 'settings' ).mp_validate ) return false;
1043			this._cascadeHiddenPartRepeaterChildren();
1044
1045			// Two suppression groups — both set visible=false silently before validation,
1046			// restored after. validateRequired gates on visible, so suppressed children
1047			// are skipped. Group 1: children from other visible parts (Layouts fetches ALL
1048			// form children via get:repeaterFieldsModelsArrayByForm). Group 2: children of
1049			// Repeater fields that are individually hidden by CL within the current part —
1050			// the Repeater add-on does not cascade visibility from the field to its children.
1051			var nonCurrentChildren = this._getNonCurrentPartRepeaterChildren();
1052			var hiddenRepeaterChildren = this.currentElement.getHiddenRepeaterChildModels();
1053			var suppressedChildren = nonCurrentChildren.concat( hiddenRepeaterChildren );
1054			// Capture each child's real visibility before suppressing so it can be restored
1055			// afterward, rather than assumed. hiddenRepeaterChildren are always visible:false
1056			// here by construction, but nonCurrentChildren can be either — blindly restoring
1057			// to true (the previous behavior) silently and permanently un-hides a Repeater
1058			// that CL had correctly hidden the moment any validation runs against this part.
1059			var priorVisibility = _.map( suppressedChildren, function( child ) { return child.get( 'visible' ); } );
1060			_.each( suppressedChildren, function( child ) { child.set( 'visible', false, { silent: true } ); } );
1061
1062			this.currentElement.validateFields();
1063
1064			_.each( suppressedChildren, function( child, i ) { child.set( 'visible', priorVisibility[ i ], { silent: true } ); } );
1065
1066			// Scrub any pre-existing fieldErrors for hidden-Repeater children in the current
1067			// part. Suppression above blocks new errors during this run; this removes stale
1068			// ones left from a previous Next attempt before the Repeater was hidden.
1069			var formFieldErrors = this.formModel.get( 'fieldErrors' );
1070			if ( formFieldErrors ) {
1071				_.each( hiddenRepeaterChildren, function( child ) {
1072					var id = child.get ? child.get( 'id' ) : child.id;
1073					if ( id ) delete formFieldErrors[ String( id ) ];
1074				} );
1075			}
1076
1077			// Repeater child errors don't bubble up to partModel.errors via maybeChangeActivePart.
1078			// Evaluate them explicitly and sync the errors flag each time so Next blocks/unblocks
1079			// correctly even after a failed attempt where the user then fills in the field.
1080			var hasRepeaterErrors = this.currentElement.hasRepeaterChildErrors( this.formModel );
1081
1082			if ( hasRepeaterErrors ) {
1083				this.currentElement.set( 'errors', true );
1084				return true;
1085			}
1086
1087			// No Repeater child errors. If the standard field-error tracker is also empty,
1088			// reset the part's errors flag — it may be stale from a previous failed attempt.
1089			if ( 0 === this.currentElement.fieldErrors[ this.currentElement.cid ].length ) {
1090				this.currentElement.set( 'errors', false );
1091			}
1092
1093			return this.currentElement.get( 'errors' );
1094		},
1095
1096		validateFields: function() {
1097			this._cascadeHiddenPartRepeaterChildren();
1098
1099			// Collect children of Repeater fields individually hidden (via CL) within
1100			// each visible part. The Repeater add-on does not cascade visibility from
1101			// the field to its children, so without suppression validateRequired would
1102			// treat them as required and block submission.
1103			var hiddenRepeaterChildren = [];
1104			_.each( this.getVisibleParts(), function( partModel ) {
1105				hiddenRepeaterChildren = hiddenRepeaterChildren.concat( partModel.getHiddenRepeaterChildModels() );
1106			} );
1107			_.each( hiddenRepeaterChildren, function( child ) { child.set( 'visible', false, { silent: true } ); } );
1108
1109			_.each( this.getVisibleParts(), function( partModel ) { partModel.validateFields(); } );
1110
1111			// hiddenRepeaterChildren are always visible:false prior to suppression (that's how
1112			// getHiddenRepeaterChildModels() selects them), so restore to false here, not true —
1113			// see the matching fix in partErrors() above for the full explanation.
1114			_.each( hiddenRepeaterChildren, function( child ) { child.set( 'visible', false, { silent: true } ); } );
1115
1116			var formFieldErrors = this.formModel && this.formModel.get( 'fieldErrors' );
1117			if ( formFieldErrors ) {
1118				_.each( hiddenRepeaterChildren, function( child ) {
1119					var id = child.get ? child.get( 'id' ) : child.id;
1120					if ( id ) delete formFieldErrors[ String( id ) ];
1121				} );
1122			}
1123		},
1124
1125		_cascadeHiddenPartRepeaterChildren: function() {
1126			this.each( function( partModel ) {
1127				if ( ! partModel.get( 'visible' ) ) {
1128					partModel.cascadeRepeaterChildrenVisible( false );
1129				}
1130			} );
1131		},
1132
1133		// Returns the child field models for all Repeaters that live in visible parts
1134		// other than the current element. Used to temporarily suppress their visibility
1135		// during current-part validation so Layouts' global child lookup doesn't validate them.
1136		_getNonCurrentPartRepeaterChildren: function() {
1137			var self = this;
1138			var children = [];
1139			this.each( function( partModel ) {
1140				if ( partModel === self.currentElement ) return;
1141				if ( ! partModel.get( 'visible' ) ) return;
1142				_.each( partModel.getRepeaterFieldIds(), function( repeaterFieldId ) {
1143					var fieldset = nfRadio.channel( 'field-repeater' ).request( 'get:repeaterFields', repeaterFieldId );
1144					if ( fieldset && fieldset.fields ) {
1145						children = children.concat( fieldset.fields );
1146					}
1147				} );
1148			} );
1149			return children;
1150		},
1151
1152		getVisibleParts: function() {
1153			return this.where( { visible: true } );
1154		}
1155	} );
1156
1157	return collection;
1158} );
1159/**
1160 * Interprets formContent data when a form is loaded.
1161 * Also returns our MP Layout View to use in place of the standard form layout view.
1162 * 
1163 * @package Ninja Forms Front-End
1164 * @subpackage Main App
1165 * @copyright (c) 2016 WP Ninjas
1166 * @since 3.0
1167 */
1168define( 'controllers/loadFilters',[ 'views/formContent', 'models/partCollection' ], function( FormContentView, PartCollection ) {
1169	var controller = Marionette.Object.extend( {
1170		initialize: function() {
1171			nfRadio.channel( 'formContent' ).request( 'add:viewFilter', this.getformContentView, 1 );
1172			nfRadio.channel( 'formContent' ).request( 'add:loadFilter', this.formContentLoad, 1 );
1173		},
1174
1175		/**
1176		 * Return the MP Content View
1177		 * 
1178		 * @since  3.0
1179		 * @param  {Backbon.Collection} collection formContentData
1180		 * @return {Backbone.View}            Our MP Content View
1181		 */
1182		getformContentView: function( formContentData ) {
1183			return FormContentView;
1184		},
1185
1186		/**
1187		 * When we load our front-end view, we filter the formContentData.
1188		 * This turns the saved object into a Backbone Collection.
1189		 * 
1190		 * @since  3.0
1191		 * @param  array formContentData current value of our formContentData.
1192		 * @return Backbone.Collection
1193		 */
1194		formContentLoad: function( formContentData, formModel ) {
1195			/*
1196			 * If the data has already been converted, just return it.
1197			 */
1198			if ( true === formContentData instanceof PartCollection ) return formContentData;
1199			/*
1200			 * If the data isn't converted, but is an array, let's make sure it's part data.
1201			 */
1202			if ( _.isArray( formContentData ) && 0 != _.isArray( formContentData ).length  && 'undefined' != typeof _.first( formContentData ) && 'part' == _.first( formContentData ).type ) {
1203				/*
1204				 * We have multi-part data. Let's convert it to a collection.
1205				 */
1206				var partCollection = new PartCollection( formContentData, { formModel: formModel } );
1207			} else {
1208				/*
1209				 * We have unknown data. Create a new part collection and use the data as the content.
1210				 */
1211				var partCollection = new PartCollection( { formContentData: formContentData }, { formModel: formModel } );
1212			}
1213
1214			return partCollection;
1215		}
1216	});
1217
1218	return controller;
1219} );
1220/**
1221 * Handles showing and hiding parts in response to Conditional Logic triggers.
1222 * 
1223 * @package Ninja Forms Front-End
1224 * @subpackage Main App
1225 * @copyright (c) 2016 WP Ninjas
1226 * @since 3.0
1227 */
1228define( 'controllers/conditionalLogic',[], function() {
1229	var controller = Marionette.Object.extend( {
1230		initialize: function() {
1231			nfRadio.channel( 'condition:trigger' ).reply( 'show_part', this.showPart, this );
1232			nfRadio.channel( 'condition:trigger' ).reply( 'hide_part', this.hidePart, this );
1233			/*
1234			 * Monotonic counter backing the Multi-Part Reset Flag below. Date.now() has
1235			 * millisecond resolution, and nested synchronous changePartVisibility() calls
1236			 * within the same then[] array can complete inside a single millisecond, causing
1237			 * an inner call to coincidentally match the outer call's flag and reset
1238			 * alreadyTriggered prematurely (#270). A per-instance counter can never collide.
1239			 */
1240			this._mpCallCounter = 0;
1241		},
1242
1243		showPart: function( conditionModel, then ) {
1244			conditionModel.set( 'alreadyTriggered', true );
1245			this.changePartVisibility( conditionModel, then, true );
1246			conditionModel.set( 'alreadyTriggered', false );
1247		},
1248
1249		hidePart: function( conditionModel, then ) {
1250			conditionModel.set( 'alreadyTriggered', true );
1251			this.changePartVisibility( conditionModel, then, false );
1252			conditionModel.set( 'alreadyTriggered', false );
1253		},
1254
1255		changePartVisibility: function( conditionModel, then, visible ) {
1256
1257
1258            /**
1259			 * Multi-Part Reset Flag
1260			 *   Identifies the initial request of a nested loop of conditions
1261			 *   so that the alreadyTriggered flags can be cleared.
1262             */
1263			var mpResetFlag = ++this._mpCallCounter;
1264			if( ! conditionModel.collection.mpResetFlag ){
1265                conditionModel.collection.mpResetFlag = mpResetFlag;
1266			}
1267
1268			var partCollection = conditionModel.collection.formModel.get( 'formContentData' );
1269			var partModel = partCollection.findWhere( { key: then.key } );
1270			partModel.set( 'visible', visible );
1271
1272			/*
1273			 * Field keys this part's own showFields()/hideFields() (triggered by the
1274			 * set('visible', ...) above, via partModel.changeVisible()) just forced to
1275			 * visible:true or visible:false, bypassing each field's own Conditional
1276			 * Logic status. Only conditions that reference one of these fields need
1277			 * re-checking below.
1278			 */
1279			var partFieldKeys = partModel.getFieldKeys ? partModel.getFieldKeys() : [];
1280
1281			/*
1282			 * Check our conditions again because we have just shown/hidden a part that
1283			 * could have conditions on it — scoped to conditions that actually reference
1284			 * a field inside this part (see partFieldKeys above), not the whole form.
1285			 *
1286			 * This scoping matters beyond efficiency: a condition unrelated to this part
1287			 * (e.g. one watching a field in a different part entirely) can have a stale
1288			 * cached `when` status at this exact moment. Backbone dispatches every
1289			 * listener for a field's change:value synchronously, in registration order —
1290			 * if THIS cascade was itself triggered by that same change:value event, an
1291			 * unrelated condition on the same field may not have run its own status
1292			 * recompute yet, so calling its checkWhen() here would act on stale data and
1293			 * can fire its then/else actions out of order (#270). Restricting to
1294			 * conditions tied to fields actually inside this part avoids ever touching
1295			 * those unrelated conditions from here at all.
1296			 */
1297			conditionModel.collection.each( function( model ) {
1298				// Avoid triggering a model's own conditions.
1299				if( model == conditionModel ) return;
1300				// Avoid re-triggering conditions, which may cause an infinite loop.
1301				if( model.get( 'alreadyTriggered' ) ) return;
1302				// Skip conditions that don't reference any field inside this part.
1303				var whenKeys = model.get( 'when' ) ? model.get( 'when' ).pluck( 'key' ) : [];
1304				if ( ! _.intersection( whenKeys, partFieldKeys ).length ) return;
1305				// Trigger conditions.
1306                model.checkWhen();
1307                // Set a flag to avoid re-triggering these conditions.
1308                model.set( 'alreadyTriggered', true );
1309			} );
1310
1311            /**
1312			 * Reset Flag Check
1313			 *   If is the initial request of the nested loop of conditions
1314			 *   then clear the previously set alreadyTriggered flags.
1315             */
1316			if( mpResetFlag == conditionModel.collection.mpResetFlag ){
1317				// Clear all alreadyTriggered flags.
1318                conditionModel.collection.invoke( 'set', { 'alreadyTriggered': false } );
1319                // Clear the Reset Flag.
1320                conditionModel.collection.mpResetFlag = false;
1321			}
1322		}
1323	});
1324
1325	return controller;
1326} );
1327
1328/**
1329 * Handles making sure that any google recaptcha fields render if they are on this part.
1330 * 
1331 * @package Ninja Forms Front-End
1332 * @subpackage Main App
1333 * @copyright (c) 2016 WP Ninjas
1334 * @since 3.0
1335 */
1336define( 'controllers/renderRecaptcha',[], function() {
1337	var controller = Marionette.Object.extend( {
1338		initialize: function() {
1339			this.listenTo( nfRadio.channel( 'nfMP' ), 'change:part', this.changePart, this );
1340		},
1341
1342		changePart: function( conditionModel, then ) {
1343			jQuery( '.g-recaptcha' ).each( function() {
1344                var callback = jQuery( this ).data( 'callback' );
1345                var fieldID = jQuery( this ).data( 'fieldid' );
1346                if ( typeof window[ callback ] !== 'function' ){
1347                    window[ callback ] = function( response ) {
1348                        nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
1349                    };
1350                }
1351				var opts = {
1352					theme: jQuery( this ).data( 'theme' ),
1353					sitekey: jQuery( this ).data( 'sitekey' ),
1354					callback: callback
1355				};
1356
1357				// Skip if no sitekey or already rendered (mirrors renderTurnstile.js pattern)
1358				if ( ! opts.sitekey || jQuery( this ).children().length > 0 ) {
1359					return;
1360				}
1361
1362				grecaptcha.render( jQuery( this )[0], opts );
1363			} );
1364		},
1365
1366	});
1367
1368	return controller;
1369} );
1370/**
1371 * Handles making sure that any Cloudflare Turnstile fields render if they are on this part.
1372 * 
1373 * @package Ninja Forms Multi-Part
1374 * @subpackage Front-End Controllers
1375 * @copyright (c) 2024 WP Ninjas
1376 * @since 3.0
1377 */
1378define( 'controllers/renderTurnstile',[], function() {
1379	var controller = Marionette.Object.extend( {
1380		initialize: function() {
1381			this.listenTo( nfRadio.channel( 'nfMP' ), 'change:part', this.changePart, this );
1382		},
1383
1384		changePart: function( conditionModel, then ) {
1385			// Wait for part transition to complete before re-rendering
1386			setTimeout( function() {
1387				jQuery( '.cf-turnstile, .nf-cf-turnstile' ).each( function() {
1388					var element = this;
1389					var sitekey = jQuery( element ).data( 'sitekey' );
1390					var fieldID = jQuery( element ).data( 'fieldid' );
1391					
1392					// Skip if no sitekey or already rendered
1393					if ( ! sitekey || jQuery( element ).children().length > 0 ) {
1394						return;
1395					}
1396
1397					// Ensure turnstile API is loaded
1398					if ( typeof turnstile === 'undefined' ) {
1399						return;
1400					}
1401
1402					try {
1403						// Create callback function for this specific field
1404						var callbackName = 'nfTurnstileCallback_' + fieldID;
1405						
1406						// Define the callback function
1407						window[ callbackName ] = function( token ) {
1408							// Update the hidden input field
1409							var input = document.getElementById( 'nf-field-' + fieldID );
1410							if ( input ) {
1411								input.value = token;
1412								jQuery( input ).trigger( 'change' );
1413								
1414								// Remove error states
1415								jQuery( input ).closest( '.field-wrap' ).removeClass( 'nf-error' );
1416								jQuery( input ).closest( '.field-wrap' ).find( '.nf-error-msg' ).remove();
1417							}
1418
1419							// Update via radio channel if available
1420							if ( typeof nfRadio !== 'undefined' && nfRadio.channel ) {
1421								try {
1422									nfRadio.channel( 'turnstile' ).request( 'update:response', token, fieldID );
1423								} catch( e ) {
1424									// Silent fail
1425								}
1426							}
1427						};
1428
1429						// Render the turnstile widget
1430						turnstile.render( element, {
1431							sitekey: sitekey,
1432							theme: jQuery( element ).data( 'theme' ) || 'auto',
1433							size: jQuery( element ).data( 'size' ) || 'normal',
1434							callback: callbackName
1435						} );
1436						
1437					} catch( e ) {
1438						// Silent fail for turnstile render errors
1439						console.warn( 'Turnstile render error:', e );
1440					}
1441				} );
1442			}, 100 ); // Small delay to ensure DOM is ready
1443		}
1444	});
1445
1446	return controller;
1447} );
1448/**
1449 * Handles making sure that any help text render if they are on this part.
1450 *
1451 * @package Ninja Forms Front-End
1452 * @subpackage Main App
1453 * @copyright (c) 2016 WP Ninjas
1454 * @since 3.0
1455 */
1456define( 'controllers/renderHelpText',[], function() {
1457    var controller = Marionette.Object.extend( {
1458        initialize: function() {
1459            this.listenTo( nfRadio.channel( 'nfMP' ), 'change:part', this.changePart, this );
1460        },
1461
1462        changePart: function( conditionModel, then ) {
1463            jQuery( '.nf-help' ).each( function() {
1464                var jBox = jQuery( this ).jBox( 'Tooltip', {
1465                    theme: 'TooltipBorder',
1466                    content: jQuery( this ).data( 'text' )
1467                });
1468            } );
1469        },
1470
1471    });
1472
1473    return controller;
1474} );
1475/**
1476 * Field Value Preserver Controller
1477 *
1478 * Preserves date/time field values across part navigation.
1479 *
1480 * @package Ninja Forms Multi-Part
1481 * @since 1.x.x
1482 * @see Issue #207, #206, #225
1483 */
1484define( 'controllers/fieldValuePreserver',[], function() {
1485	var controller = Marionette.Object.extend( {
1486
1487		/**
1488		 * Cache of field values indexed by field ID
1489		 */
1490		fieldCache: {},
1491
1492		/**
1493		 * Cache of date-only field values (Issue #225)
1494		 */
1495		dateOnlyCache: {},
1496
1497		initialize: function() {
1498			var self = this;
1499
1500			// Listen for changes on time select elements
1501			jQuery( document ).on( 'change', '.hour, .minute, .ampm', function( e ) {
1502				self.onTimeSelectChange( e );
1503			} );
1504
1505			// Also cache on initial page interaction (in case selects have default values)
1506			// Use mousedown to capture values BEFORE click handlers destroy the DOM
1507			jQuery( document ).on( 'mousedown', '.nf-next, .nf-previous', function( e ) {
1508				self.cacheAllVisibleTimeFields();
1509				self.cacheDateOnlyFields();  // Issue #225
1510			} );
1511
1512			// Intercept form submission at the earliest point
1513			this.listenTo( nfRadio.channel( 'forms' ), 'before:submit', this.onBeforeSubmit );
1514		},
1515
1516		/**
1517		 * Cache all currently visible time field values
1518		 */
1519		cacheAllVisibleTimeFields: function() {
1520			var self = this;
1521
1522			jQuery( '.nf-field-container' ).each( function() {
1523				var $container = jQuery( this );
1524				var $hour = $container.find( '.hour' );
1525
1526				if ( ! $hour.length ) return;
1527
1528				var fieldId = $container.data( 'id' );
1529				if ( ! fieldId ) {
1530					var $input = $container.find( '.nf-element' ).first();
1531					if ( $input.length && $input.attr( 'id' ) ) {
1532						fieldId = $input.attr( 'id' ).replace( /^nf-field-/, '' );
1533					}
1534				}
1535
1536				if ( ! fieldId ) return;
1537
1538				var $minute = $container.find( '.minute' );
1539				var $ampm = $container.find( '.ampm' );
1540
1541				// Also get the date value
1542				var $dateInput = $container.find( 'input[type="hidden"], input.nf-element' ).first();
1543				var dateValue = $dateInput.length ? $dateInput.val() : '';
1544
1545				self.fieldCache[ fieldId ] = {
1546					date: dateValue,
1547					hour: $hour.val() || null,
1548					minute: $minute.val() || null,
1549					ampm: $ampm.val() || null
1550				};
1551			} );
1552		},
1553
1554		/**
1555		 * Cache date-only field values from hidden inputs (Issue #225)
1556		 * This captures the formatted date before DOM is destroyed during part navigation
1557		 */
1558		cacheDateOnlyFields: function() {
1559			var self = this;
1560
1561			jQuery( '.nf-field-container' ).each( function() {
1562				var $container = jQuery( this );
1563
1564				// Find the hidden input with the formatted date value
1565				var $hiddenInput = $container.find( 'input[type="hidden"].datepicker' );
1566				if ( ! $hiddenInput.length ) {
1567					// Try alternate selector - any hidden input with datepicker-related class
1568					$hiddenInput = $container.find( 'input[type="hidden"].flatpickr-input' );
1569				}
1570				if ( ! $hiddenInput.length ) return; // Not a date field
1571
1572				// Skip if this has time selects (not date_only)
1573				if ( $container.find( '.hour' ).length ) return;
1574
1575				var fieldId = $container.data( 'id' );
1576				if ( ! fieldId ) {
1577					// Try to get ID from the input
1578					var inputId = $hiddenInput.attr( 'id' );
1579					if ( inputId ) {
1580						fieldId = inputId.replace( /^nf-field-/, '' );
1581					}
1582				}
1583
1584				if ( ! fieldId ) return;
1585
1586				// Cache the formatted date value from the hidden input
1587				var formattedDate = $hiddenInput.val();
1588				if ( formattedDate ) {
1589					self.dateOnlyCache[ String( fieldId ) ] = formattedDate;
1590				}
1591			} );
1592		},
1593
1594		/**
1595		 * Called when any time select changes
1596		 */
1597		onTimeSelectChange: function( e ) {
1598			var $select = jQuery( e.target );
1599			var $container = $select.closest( '.nf-field-container' );
1600
1601			if ( ! $container.length ) return;
1602
1603			var fieldId = $container.data( 'id' );
1604			if ( ! fieldId ) {
1605				var $input = $container.find( '.nf-element' ).first();
1606				if ( $input.length && $input.attr( 'id' ) ) {
1607					fieldId = $input.attr( 'id' ).replace( /^nf-field-/, '' );
1608				}
1609			}
1610
1611			if ( ! fieldId ) return;
1612
1613			var $hour = $container.find( '.hour' );
1614			var $minute = $container.find( '.minute' );
1615			var $ampm = $container.find( '.ampm' );
1616
1617			// Get date value
1618			var $dateInput = $container.find( 'input[type="hidden"], input.nf-element' ).first();
1619			var dateValue = $dateInput.length ? $dateInput.val() : '';
1620
1621			this.fieldCache[ fieldId ] = {
1622				date: dateValue,
1623				hour: $hour.val() || null,
1624				minute: $minute.val() || null,
1625				ampm: $ampm.val() || null
1626			};
1627		},
1628
1629		/**
1630		 * Before form submission - patch all date field models with cached values
1631		 */
1632		onBeforeSubmit: function( formModel ) {
1633			var self = this;
1634			var fields = formModel.get( 'fields' );
1635
1636			if ( ! fields || ! fields.models ) return;
1637
1638			fields.models.forEach( function( fieldModel ) {
1639				if ( 'date' !== fieldModel.get( 'type' ) ) return;
1640
1641				var dateMode = fieldModel.get( 'date_mode' );
1642				var fieldId = fieldModel.get( 'id' );
1643
1644				// Handle date_only fields (Issue #225)
1645				if ( 'date_only' === dateMode ) {
1646					var cachedDate = self.dateOnlyCache[ String( fieldId ) ];
1647					if ( cachedDate ) {
1648						// Use cached formatted date instead of Date object
1649						// Use silent:true to prevent triggering validation
1650						fieldModel.set( 'value', cachedDate, { silent: true } );
1651					}
1652					return;
1653				}
1654
1655				var cached = self.fieldCache[ fieldId ];
1656
1657				if ( ! cached ) return;
1658
1659				// Check if field DOM is available
1660				var el = fieldModel.get( 'el' );
1661				var $el = jQuery( el );
1662				var domAvailable = $el.length && $el.find( '.hour' ).length && $el.closest( 'body' ).length;
1663
1664				if ( domAvailable ) {
1665					// DOM exists, let normal processing handle it
1666					return;
1667				}
1668
1669				// DOM is not available - we need to set the value directly
1670				// The beforeSubmit in fieldDate.js won't be able to read DOM
1671				// So we set the model value to what it should be
1672
1673				if ( 'time_only' === dateMode ) {
1674					var timeValue = '';
1675					if ( cached.hour && cached.minute ) {
1676						timeValue = cached.hour + ':' + cached.minute;
1677						if ( cached.ampm ) {
1678							timeValue += ' ' + cached.ampm;
1679						}
1680					}
1681					fieldModel.set( 'value', timeValue );
1682				} else {
1683					// date_and_time - set as object that matches what beforeSubmit creates
1684					fieldModel.set( 'value', {
1685						date: cached.date || '',
1686						hour: cached.hour || '',
1687						minute: cached.minute || '',
1688						ampm: cached.ampm || ''
1689					} );
1690				}
1691
1692				// Override getValue for this submission
1693				var originalGetValue = fieldModel.getValue;
1694				fieldModel.getValue = function() {
1695					var val = this.get( 'value' );
1696
1697					if ( typeof val === 'object' && val !== null ) {
1698						var result = val.date || '';
1699						if ( val.hour && val.minute ) {
1700							result += ' ' + val.hour + ':' + val.minute;
1701						}
1702						if ( val.ampm ) {
1703							result += ' ' + val.ampm;
1704						}
1705						return result;
1706					}
1707
1708					return val;
1709				};
1710			} );
1711		}
1712
1713	} );
1714
1715	return controller;
1716} );
1717
1718define(
1719	'controllers/loadControllers',[
1720		'controllers/conditionalLogic',
1721		'controllers/renderRecaptcha',
1722		'controllers/renderTurnstile',
1723		'controllers/renderHelpText',
1724		'controllers/fieldValuePreserver'
1725	],
1726	function
1727	(
1728		ConditionalLogic,
1729		RenderRecaptcha,
1730		RenderTurnstile,
1731		RenderHelpText,
1732		FieldValuePreserver
1733	)
1734	{
1735	var controller = Marionette.Object.extend( {
1736		initialize: function() {
1737			new ConditionalLogic();
1738			new RenderRecaptcha();
1739			new RenderTurnstile();
1740			new RenderHelpText();
1741			new FieldValuePreserver();
1742		}
1743
1744	});
1745
1746	return controller;
1747} );
1748var nfRadio = Backbone.Radio;
1749
1750require( [ 'controllers/loadFilters', 'controllers/loadControllers' ], function( LoadFilters, LoadControllers ) {
1751
1752	var NFMultiPart = Marionette.Application.extend( {
1753
1754		initialize: function( options ) {
1755			// console.log( 'init mp' );
1756			this.listenTo( nfRadio.channel( 'form' ), 'before:filterData', this.loadFilters );
1757			this.listenTo( nfRadio.channel( 'form' ), 'loaded', this.loadControllers );
1758		},
1759
1760		loadFilters: function( formModel ) {
1761			new LoadFilters();
1762		},
1763
1764		loadControllers: function( formModel ) {
1765			new LoadControllers();
1766		},
1767
1768		onStart: function() {
1769		}
1770	} );
1771
1772	var nfMultiPart = new NFMultiPart();
1773	nfMultiPart.start();
1774} );
1775define("main", function(){});
1776
1777}());
1778//# 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.