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.