PageSourceSearch

https://www.leicode.hk/wp-content/plugins/leiform/assets/js/leiform-front.js?ver=1.0.0

js leicode.hk collected 2026-10-07 19:23:38 UTC 12,291 bytes, 327 lines download raw bytes

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">&times;</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">&times;</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">&times;</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.