1/** 2 * LEI form frontend: conditional fields, toggle buttons, validation (Parsley), 3 * AJAX submission and document uploads. Replaces caldera-forms-front.js, 4 * conditionals.js and the cf2 uploader for the application form. 5 * 6 * Events (jQuery, bubbling from the form element): 7 * lf.add conditional field(s) inserted into the DOM (old cf.add) 8 * lf.remove conditional field(s) removed 9 * lf.form.submit fired when a submit attempt starts (old cf.form.submit) 10 */ 11(function ($) { 12 'use strict'; 13 14 // Runs immediately: this is a footer script, the form markup is always 15 // parsed by now, and theme code clicks form elements from DOMContentLoaded 16 // handlers that would fire before a jQuery-ready callback could bind ours 17 // (e.g. the RENEW tab pre-selection on the renewal page template). 18 (function () { 19 var $grid = $('#lei-form-grid'); 20 var $form = $('#lei-application-form'); 21 if (!$form.length) { 22 return; 23 } 24 25 var config = JSON.parse(document.getElementById('lf-config').textContent || '{}'); 26 var conditions = config.conditions || {}; 27 28 // Authenticate REST calls for logged-in users (LEIAdmin portals) 29 var ajaxHeaders = config.restNonce ? {'X-WP-Nonce': config.restNonce} : {}; 30 31 /* ------------------------------------------------ field values */ 32 33 function fieldPresent(name) { 34 return $form.find('[name="' + name + '"], [name^="' + name + '["]').length > 0; 35 } 36 37 function fieldValue(name) { 38 var $els = $form.find('[name="' + name + '"]'); 39 if (!$els.length) { 40 return null; 41 } 42 var first = $els.get(0); 43 if (first.type === 'radio') { 44 var $checked = $els.filter(':checked'); 45 return $checked.length ? $checked.val() : null; 46 } 47 return $els.val(); 48 } 49 50 /* ------------------------------------------------ conditions engine */ 51 52 function ruleMatches(name) { 53 var rule = conditions[name]; 54 if (!rule) { 55 return true; 56 } 57 var controllerVisible = !conditions[rule.field] || ruleMatches(rule.field); 58 var value = controllerVisible && fieldPresent(rule.field) ? fieldValue(rule.field) : null; 59 if ('equals' in rule) { 60 return value === rule.equals; 61 } 62 if ('not' in rule) { 63 return value !== rule.not; 64 } 65 return true; 66 } 67 68 function applyConditions() { 69 var changed = false; 70 $.each(conditions, function (name) { 71 var $anchor = $('#lf-cond-' + name); 72 if (!$anchor.length) { 73 return; 74 } 75 var shouldShow = ruleMatches(name); 76 var isShown = $anchor.children().length > 0; 77 if (shouldShow && !isShown) { 78 var tmpl = document.getElementById('lf-tmpl-' + name); 79 if (tmpl) { 80 $anchor.html(tmpl.innerHTML); 81 $anchor.trigger('lf.add'); 82 changed = true; 83 } 84 } else if (!shouldShow && isShown) { 85 $anchor.empty(); 86 $anchor.trigger('lf.remove'); 87 changed = true; 88 } 89 }); 90 // Inserted fields may control other fields (cascades) â settle, bounded. 91 if (changed) { 92 applyConditions(); 93 } 94 } 95 96 /* ------------------------------------------------ toggle buttons */ 97 98 // Anchor buttons mirror their hidden radios (same mechanics Caldera had). 99 $grid.on('click', '.cf-toggle-group-buttons a.btn', function (e) { 100 e.preventDefault(); 101 var $btn = $(this); 102 var name = $btn.data('field'); 103 $btn.siblings('a.btn').addBack().removeClass($btn.data('active')).addClass($btn.data('default')); 104 $btn.removeClass($btn.data('default')).addClass($btn.data('active')); 105 var $radio = $form.find('input[type="radio"][name="' + name + '"][value="' + $btn.data('value') + '"]'); 106 if ($radio.length && !$radio.prop('checked')) { 107 $radio.prop('checked', true).trigger('change'); 108 } 109 }); 110 111 // Keep anchor classes in sync when radios are driven programmatically 112 // (theme JS clicks the hidden radios directly). 113 $grid.on('change click', '.cf-toggle-group-radio', function () { 114 var $radio = $(this); 115 if (!$radio.prop('checked')) { 116 $radio.prop('checked', true); 117 } 118 var $anchor = $('#' + $radio.data('ref')); 119 if ($anchor.length) { 120 $anchor.siblings('a.btn').addBack().removeClass($anchor.data('active')).addClass($anchor.data('default')); 121 $anchor.removeClass($anchor.data('default')).addClass($anchor.data('active')); 122 } 123 $radio.trigger('lf.toggle'); 124 }); 125 126 // Re-evaluate visibility whenever any input changes 127 $grid.on('change', 'input, select, textarea', function () { 128 applyConditions(); 129 }); 130 131 applyConditions(); 132 133 /* ------------------------------------------------ uploads */ 134 135 var maxUploadBytes = parseInt(config.maxUploadBytes, 10) || 0; 136 137 function uploadErrorItem($list, fileName, msg) { 138 var $item = $('<div class="cf-uploader-file lf-upload-error"></div>').text(fileName); 139 $item.append(' <span class="lf-upload-error-text">' + msg + '</span>'); 140 $item.append(' <button type="button" class="cf-uploader-remove" aria-label="Remove">×</button>'); 141 $list.append($item); 142 } 143 144 $grid.on('change', 'input.cf-multi-uploader', function () { 145 var input = this; 146 var $input = $(input); 147 var name = $input.data('field'); 148 var $urls = $('#lf-' + name + '-urls'); 149 var $validator = $('#lf-' + name + '-validator'); 150 var $list = $('#lf-' + name + '-file-list'); 151 var endpoint = $form.data('upload-endpoint'); 152 153 $.each(input.files, function (i, file) { 154 // Reject oversized files BEFORE uploading: past nginx's 155 // client_max_body_size the request dies with an HTML 413 that has no 156 // JSON error to show, which used to surface as the generic 157 // "submission failed" message (or a Cloudflare 502 page). 158 if (maxUploadBytes && file.size > maxUploadBytes) { 159 uploadErrorItem($list, file.name, config.i18n.maxUploadError); 160 return; 161 } 162 163 var data = new FormData(); 164 data.append('file', file); 165 166 var $item = $('<div class="cf-uploader-file lf-uploading"></div>').text(file.name); 167 $list.append($item); 168 169 $.ajax({ 170 url: endpoint, 171 method: 'POST', 172 headers: ajaxHeaders, 173 data: data, 174 processData: false, 175 contentType: false 176 }).done(function (response) { 177 $item.removeClass('lf-uploading'); 178 $item.attr('data-url', response.url); 179 $item.append(' <button type="button" class="cf-uploader-remove" aria-label="Remove">×</button>'); 180 $urls.val(($urls.val() + ' ' + response.url).trim()); 181 $validator.val($urls.val()); 182 $validator.parsley && $validator.parsley().validate(); 183 }).fail(function (xhr) { 184 $item.removeClass('lf-uploading'); 185 $item.addClass('lf-upload-error'); 186 var msg; 187 if (xhr.status === 413) { 188 msg = config.i18n.maxUploadError; 189 } else if (xhr.responseJSON && xhr.responseJSON.error) { 190 msg = xhr.responseJSON.error; 191 } else { 192 msg = config.i18n.submitFailed; 193 } 194 $item.append(' <span class="lf-upload-error-text">' + msg + '</span>'); 195 $item.append(' <button type="button" class="cf-uploader-remove" aria-label="Remove">×</button>'); 196 }); 197 }); 198 199 // allow re-selecting the same file 200 input.value = ''; 201 }); 202 203 $grid.on('click', '.cf-uploader-remove', function () { 204 var $item = $(this).closest('.cf-uploader-file'); 205 var url = $item.attr('data-url'); 206 var $wrap = $item.closest('.form-group'); 207 var $urls = $wrap.find('input[type="hidden"]'); 208 var $validator = $wrap.find('input[id$="-validator"]'); 209 if (url) { 210 $urls.val($urls.val().split(/\s+/).filter(function (u) { 211 return u && u !== url; 212 }).join(' ')); 213 $validator.val($urls.val()); 214 } 215 $item.remove(); 216 }); 217 218 // The visible trigger button + the theme's dropzone proxy to the file input 219 $grid.on('click', '.cf-uploader-trigger, .file-upload-custom', function (e) { 220 e.preventDefault(); 221 $(this).closest('.form-group').find('input[type="file"]').trigger('click'); 222 }); 223 224 /* ------------------------------------------------ validation + submit */ 225 226 // Parsley is bound lazily on the first submit attempt (Caldera did the 227 // same) so programmatic field.validate() calls before that â e.g. the 228 // India registration-number checker â don't paint errors on page load. 229 var parsleyForm = null; 230 231 function bindParsley() { 232 if (parsleyForm) { 233 return parsleyForm; 234 } 235 parsleyForm = $form.parsley({ 236 errorsWrapper: '<span class="help-block caldera_ajax_error_block"></span>', 237 errorTemplate: '<span></span>', 238 errorsContainer: function (field) { 239 return field.$element.closest('.form-group'); 240 } 241 }); 242 parsleyForm.on('field:error', function () { 243 this.$element.closest('.form-group').addClass('has-error'); 244 }); 245 parsleyForm.on('field:success', function () { 246 this.$element.closest('.form-group').removeClass('has-error'); 247 }); 248 return parsleyForm; 249 } 250 251 var submitting = false;
252 253 $form.on('submit', function (e) { 254 e.preventDefault(); 255 256 $(document).trigger('lf.form.submit', {form: $form}); 257 258 // Phone OTP (India): this handler is bound before the widget's own 259 // submit blocker, so ask its API directly and bail â the widget's 260 // handler still runs after this one and paints the "verify first" 261 // message. The server enforces the same gate via leiform_validate. 262 var otpBlocked = false; 263 $form.find('input').each(function () { 264 var api = $(this).data('leiOtpApi'); 265 if (api && !api.isVerified()) { 266 otpBlocked = true; 267 return false; 268 } 269 }); 270 if (otpBlocked) { 271 return; 272 } 273 274 bindParsley().validate(); 275 if (!parsleyForm.isValid()) { 276 return; 277 } 278 // Never submit with uploads still in flight (the file URLs would be 279 // missing) or with failed uploads (Caldera's cf2 uploader blocked on 280 // both; leiform silently submitted without the documents). 281 if ($grid.find('.lf-uploading').length) { 282 $('#lf-notices').html('<div class="alert alert-error caldera_ajax_error_block">' + config.i18n.uploadPending + '</div>'); 283 return; 284 } 285 if ($grid.find('.lf-upload-error').length) { 286 $('#lf-notices').html('<div class="alert alert-error caldera_ajax_error_block">' + config.i18n.uploadFailed + '</div>'); 287 return; 288 } 289 if (submitting) { 290 return; 291 } 292 submitting = true; 293 $grid.addClass('cf_processing'); 294 295 $.ajax({ 296 url: $form.data('endpoint'), 297 method: 'POST', 298 headers: ajaxHeaders, 299 data: $form.serialize() 300 }).done(function (response) { 301 if (response && response.redirect) { 302 // The theme's page-loader watches for this notice (old Caldera behavior) 303 $('#lf-notices').html('<div class="alert alert-success">' + config.i18n.processing + '</div>'); 304 window.location.href = response.redirect; 305 return; 306 } 307 submitting = false; 308 $grid.removeClass('cf_processing'); 309 }).fail(function (xhr) { 310 submitting = false; 311 $grid.removeClass('cf_processing'); 312 var fields = xhr.responseJSON && xhr.responseJSON.fields; 313 if (fields) { 314 $.each(fields, function (name, message) { 315 var $wrap = $('#lf-' + name + '-wrap'); 316 $wrap.addClass('has-error'); 317 if (!$wrap.find('.caldera_ajax_error_block').length) { 318 $wrap.find('> div').first().append('<span class="help-block caldera_ajax_error_block"><span>' + message + '</span></span>'); 319 } 320 }); 321 } else { 322 $('#lf-notices').html('<div class="alert alert-error caldera_ajax_error_block">' + config.i18n.submitFailed + '</div>'); 323 } 324 }); 325 }); 326 })(); 327})(jQuery);
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.