PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
← All changes | includes/widgets/Form_Builder/script.js +835 -46 51.1.39 → 51.1.83 View file →
@@ -5,15 +5,15 @@
5 5 elementorFrontend.elementsHandler.addHandler(elementorModules.frontend.handlers.Base.extend({
6 6 onInit: function onInit() {
7 7 let $scope = this.$element;
8 8
9 - console.log("FORM BUILDER");
9 + // console.log("FORM BUILDER");
10 10
11 11 var formContent = {};
12 12
13 13 var fileUrl = {};
14 14
15 - if ($('body').find('.king-addons-form-field-type-recaptcha-v3').length > 0) {
15 + if ($scope.find('#g-recaptcha-response').length > 0 && $scope.find('#g-recaptcha-response').data('site-key')) {
16 16 var script = document.createElement('script');
17 17 script.src = 'https://www.google.com/recaptcha/api.js?render=' + $scope.find('#g-recaptcha-response').data('site-key') + '';
18 18 document.body.appendChild(script);
19 19 }
@@ -19,9 +19,9 @@
19 19 }
20 20
21 21 var currentTab = 0;
22 22 if (0 < $scope.find('.king-addons-fb-step-tab').length) {
23 - console.log(currentTab);
23 + // console.log(currentTab);
24 24 showTab(currentTab);
25 25
26 26 $scope.find('.king-addons-fb-step-prev').each(function () {
27 27 $(this).on('click', function () {
@@ -37,8 +37,13 @@
37 37 }
38 38
39 39 var actions = $scope.find('.king-addons-form-field-type-submit').data('actions');
40 40
41 + initExtraFields($scope);
42 + initConditionalLogic($scope);
43 + initCalculations($scope);
44 + initPartialEntries($scope);
45 +
41 46 $scope.find('input[type="file"]').on('change', function (e) {
42 47 var files = this.files;
43 48 var thisInput = $(this);
44 49 var eventType = e.type;
@@ -108,9 +113,9 @@
108 113 }
109 114 })
110 115 .catch((error) => {
111 116
112 - console.error(error);
117 + // console.error(error);
113 118 });
114 119 } else {
115 120 createFormContent();
116 121
@@ -125,22 +130,17 @@
125 130
126 131 e.preventDefault();
127 132
128 133 let responsesArray = [];
134 + let paymentRedirect = '';
129 135
130 136 $scope.find('.king-addons-button>span').addClass('king-addons-loader-hidden');
131 137 $scope.find('.king-addons-button').find('.king-addons-double-bounce').removeClass('king-addons-loader-hidden');
132 138
133 - if ($scope.find('.king-addons-submit-error')) {
134 - $scope.find('.king-addons-submit-error').remove();
135 - }
139 + $scope.find('.king-addons-submit-error, .king-addons-submit-success, .king-addons-submit-warning').remove();
136 140
137 - if ($scope.find('.king-addons-submit-success')) {
138 - $scope.find('.king-addons-submit-success').remove();
139 - }
140 -
141 141 function processRecaptcha(callback) {
142 - if ($scope.find('#g-recaptcha-response').length > 0) {
142 + if ($scope.find('#g-recaptcha-response').length > 0 && $scope.find('#g-recaptcha-response').data('site-key')) {
143 143 grecaptcha.ready(function () {
144 144 grecaptcha.execute(KingAddonsFormBuilderData.recaptcha_v3_site_key, {action: 'submit'}).then(function (token) {
145 145
146 146 $scope.find('#g-recaptcha-response').val(token);
@@ -150,13 +150,14 @@
150 150 type: 'POST',
151 151 url: KingAddonsFormBuilderData.ajaxurl,
152 152 data: {
153 153 action: 'king_addons_verify_recaptcha',
154 - 'g-recaptcha-response': token
154 + 'g-recaptcha-response': token,
155 + nonce: KingAddonsFormBuilderData.nonce
155 156 },
156 157 success: function (response) {
157 158 if (!response.success) {
158 - console.log(response);
159 + // console.log(response);
159 160 setTimeout(function () {
160 161 $scope.find('.king-addons-button').find('.king-addons-double-bounce').addClass('king-addons-loader-hidden');
161 162 $scope.find('.king-addons-button>span').removeClass('king-addons-loader-hidden');
162 163 $scope.find('form').append('<p class="king-addons-submit-error">' + KingAddonsFormBuilderData.recaptcha_error + '</p>');
@@ -162,14 +163,14 @@
162 163 $scope.find('form').append('<p class="king-addons-submit-error">' + KingAddonsFormBuilderData.recaptcha_error + '</p>');
163 164 }, 500);
164 165 callback(false);
165 166 } else {
166 - console.log(response);
167 + // console.log(response);
167 168 callback(true);
168 169 }
169 170 },
170 171 error: function (error) {
171 - console.log(error);
172 + // console.log(error);
172 173 setTimeout(function () {
173 174 $scope.find('.king-addons-button').find('.king-addons-double-bounce').addClass('king-addons-loader-hidden');
174 175 $scope.find('.king-addons-button>span').removeClass('king-addons-loader-hidden');
175 176 $scope.find('form').append('<p class="king-addons-submit-error">' + KingAddonsFormBuilderData.recaptcha_error + '</p>');
@@ -192,26 +193,42 @@
192 193 var actionsObject = {
193 194 emailPromise: sendEmail,
194 195 submissionsPromise: createPost,
195 196 mailchimpPromise: subscribeMailchimp,
196 - webhookPromise: sendWebhook
197 + webhookPromise: sendWebhook,
198 + integrationsPromise: sendIntegrations
197 199 }
198 200
199 -
201 + var firstActions = actions.filter(function (action) {
202 + return action !== 'payment';
203 + });
204 +
200 205 Promise.all(
201 - actions.map((action) => {
206 + firstActions.map((action) => {
202 207 try {
203 208 if (actionsObject[action + 'Promise']) {
204 209 return actionsObject[action + 'Promise']();
205 210 }
206 211 } catch (error) {
207 - console.error(error);
208 212 return Promise.reject(error);
209 213 }
210 214 })
211 215 )
212 216 .then((responses) => {
213 - console.log(responses);
217 + if (!actions.includes('payment')) {
218 + return responses;
219 + }
220 + var created = responses.find(function (response) {
221 + return response && response.data && response.data.action === 'king_addons_form_builder_submissions';
222 + });
223 + var submissionId = created && created.data ? created.data.post_id : 0;
224 + var accessSecret = created && created.data ? created.data.access_secret : '';
225 + return startPayment(submissionId, accessSecret).then(function (payResponse) {
226 + return responses.concat([payResponse]);
227 + });
228 + })
229 + .then((responses) => {
230 + // console.log(responses);
214 231
215 232
216 233 const createPostResponse = responses.find((response) => response && response.data.action === 'king_addons_form_builder_submissions');
217 234
@@ -244,18 +261,73 @@
244 261 }
245 262 });
246 263
247 264 return Promise.all(updateMetaPromises).then(() => {
248 - if (responsesArray.includes('error')) {
249 - var sanitizedErrorMessage = $('<div>').text($scope.data('settings').error_message).html();
250 - $scope.find('form').append('<p class="king-addons-submit-error">' + sanitizedErrorMessage + '</p>');
251 - } else {
265 + var anySuccess = responsesArray.includes('success');
266 + var anyError = responsesArray.includes('error');
267 + var paymentConfigured = actions.includes('payment');
268 + var paymentOk = responses.some(function (response) {
269 + return response && response.data
270 + && response.data.action === 'king_addons_form_builder_payment'
271 + && response.data.status === 'success';
272 + });
273 + var paymentFailed = paymentConfigured && !paymentOk;
274 +
275 + function firstActionMessage() {
276 + var note = '';
277 + responses.forEach(function (response) {
278 + if (note || !response || !response.data || !response.data.message) {
279 + return;
280 + }
281 + if (response.data.status === 'error' || response.success === false) {
282 + note = String(response.data.message);
283 + }
284 + });
285 + return note || $scope.data('settings').error_message;
286 + }
287 +
288 + // A broken Slack key must not hide a sent email. A failed
289 + // payment must, or the visitor thinks they have paid.
290 + if (anySuccess && !paymentFailed) {
291 + var successCopy = $scope.data('settings').success_message;
292 + // Provider None (and any payment with no checkout URL)
293 + // still answers success; show that copy, not the generic
294 + // “Submission successful”, or the visitor thinks they paid.
295 + if (paymentConfigured && !paymentRedirect) {
296 + responses.forEach(function (response) {
297 + if (response && response.data
298 + && response.data.action === 'king_addons_form_builder_payment'
299 + && response.data.status === 'success'
300 + && response.data.message) {
301 + successCopy = String(response.data.message);
302 + }
303 + });
304 + }
252 305 $scope.find('form').append(
253 - $('<p class="king-addons-submit-success"></p>').text($scope.data('settings').success_message)
306 + $('<p class="king-addons-submit-success"></p>').text(successCopy)
254 307 );
255 -
308 + $scope.find('form').trigger('king-addons/form/submitted');
309 +
256 310 $scope.find('button').attr('disabled', true);
257 311 $scope.find('button').css('opacity', 0.6);
312 +
313 + if (anyError) {
314 + var failedNotes = [];
315 + responses.forEach(function (response) {
316 + if (response && response.data && response.data.status === 'error' && response.data.message) {
317 + failedNotes.push(String(response.data.message));
318 + }
319 + });
320 + if (failedNotes.length) {
321 + $scope.find('form').append(
322 + $('<p class="king-addons-submit-warning"></p>').text(failedNotes.join(' '))
323 + );
324 + }
325 + }
326 + } else {
327 + $scope.find('form').append(
328 + $('<p class="king-addons-submit-error"></p>').text(firstActionMessage())
329 + );
258 330 }
259 331 });
260 332
261 333 })
@@ -260,9 +332,9 @@
260 332
261 333 })
262 334 .catch((error) => {
263 335
264 - console.error(error);
336 + // console.error(error);
265 337 })
266 338 .then(() => {
267 339
268 340 setTimeout(function () {
@@ -269,8 +341,16 @@
269 341
270 342 $scope.find('.king-addons-button').find('.king-addons-double-bounce').addClass('king-addons-loader-hidden');
271 343 $scope.find('.king-addons-button>span').removeClass('king-addons-loader-hidden');
272 344 setTimeout(function () {
345 + // Paying comes first: a redirect
346 + // set on the form would otherwise
347 + // navigate away from the checkout.
348 + if (paymentRedirect && !responsesArray.includes('error')) {
349 + $(location).prop('href', paymentRedirect);
350 + return;
351 + }
352 +
273 353 if (actions.includes('redirect') && responsesArray.includes('success')) {
274 354
275 355 $(location).prop('href', $scope.find('.king-addons-form-field-type-submit').data('redirect-url'))
276 356 }
@@ -278,9 +358,9 @@
278 358 }, 500);
279 359 })
280 360 .catch((error) => {
281 361
282 - console.error(error);
362 + // console.error(error);
283 363 });
284 364 } else {
285 365
286 366 return false;
@@ -325,12 +405,12 @@
325 405 data: {
326 406 action: 'king_addons_form_builder_email',
327 407 nonce: KingAddonsFormBuilderData.nonce,
328 408 form_content: data,
329 - king_addons_form_id: $scope.find('input[name="form_id"]').val(),
409 + ...spamPayload(),
330 410 },
331 411 success: function (response) {
332 - console.log(response);
412 + // console.log(response);
333 413 if (!response.success) {
334 414
335 415
336 416
@@ -347,8 +427,60 @@
347 427 }
348 428 });
349 429 }
350 430
431 + /**
432 + * Hand the submission to the Integrations action, which
433 + * decides on the server which services to forward it to.
434 + *
435 + * @return {jqXHR} The request.
436 + */
437 + function sendIntegrations() {
438 + return $.ajax({
439 + type: 'POST',
440 + url: KingAddonsFormBuilderData.ajaxurl,
441 + data: {
442 + ...spamPayload(),
443 + action: 'king_addons_form_builder_integrations',
444 + nonce: KingAddonsFormBuilderData.nonce,
445 + form_content: formContent
446 + }
447 + });
448 + }
449 +
450 + /**
451 + * Ask the server to open a payment and note where to
452 + * send the visitor once everything else has run.
453 + *
454 + * @param {number|string} submissionId Submission created by this submit, if any.
455 + * @param {string} accessSecret Secret issued with that submission.
456 + * @return {jqXHR} The request.
457 + */
458 + function startPayment(submissionId, accessSecret) {
459 + return $.ajax({
460 + type: 'POST',
461 + url: KingAddonsFormBuilderData.ajaxurl,
462 + data: {
463 + ...spamPayload(),
464 + action: 'king_addons_form_builder_payment',
465 + nonce: KingAddonsFormBuilderData.nonce,
466 + form_content: formContent,
467 + king_addons_form_id: $scope.find('input[name="form_id"]').val(),
468 + form_id: $scope.find('input[name="form_id"]').val(),
469 + form_name: $scope.find('form').attr('name'),
470 + form_page: $scope.find('form').attr('page'),
471 + form_page_id: $scope.find('form').attr('page_id'),
472 + submission_id: submissionId || 0,
473 + access_secret: accessSecret || ''
474 + },
475 + success: function (response) {
476 + if (response && response.success && response.data && response.data.redirect) {
477 + paymentRedirect = response.data.redirect;
478 + }
479 + }
480 + });
481 + }
482 +
351 483 function sendWebhook() {
352 484 var data = deepCopy(formContent);
353 485
354 486 for (let key in data) {
@@ -364,8 +496,9 @@
364 496 return $.ajax({
365 497 type: 'POST',
366 498 url: KingAddonsFormBuilderData.ajaxurl,
367 499 data: {
500 + ...spamPayload(),
368 501 action: 'king_addons_form_builder_webhook',
369 502 nonce: KingAddonsFormBuilderData.nonce,
370 503 form_content: data,
371 504 king_addons_form_id: $scope.find('input[name="form_id"]').val(),
@@ -371,9 +504,9 @@
371 504 king_addons_form_id: $scope.find('input[name="form_id"]').val(),
372 505 form_name: $scope.find('form').attr('name')
373 506 },
374 507 success: function (response) {
375 - console.log(response);
508 + // console.log(response);
376 509 if (!response.success) {
377 510
378 511
379 512
@@ -383,9 +516,9 @@
383 516
384 517 }
385 518 },
386 519 error: function (error) {
387 - console.log(error);
520 + // console.log(error);
388 521
389 522
390 523
391 524 }
@@ -391,14 +524,36 @@
391 524 }
392 525 });
393 526 }
394 527
528 + function flattenChoiceFields(data) {
529 + var copy = deepCopy(data);
530 + for (var key in copy) {
531 + if (!copy[key] || (copy[key][0] !== 'radio' && copy[key][0] !== 'checkbox')) {
532 + continue;
533 + }
534 + if (!Array.isArray(copy[key][1])) {
535 + continue;
536 + }
537 + var picked = copy[key][1]
538 + .filter(function (row) {
539 + return row && row[1] === true;
540 + })
541 + .map(function (row) {
542 + return row[0];
543 + });
544 + copy[key][1] = picked.join(', ');
545 + }
546 + return copy;
547 + }
548 +
395 549 function createPost() {
396 550
397 551 var data = {
552 + ...spamPayload(),
398 553 action: 'king_addons_form_builder_submissions',
399 554 nonce: KingAddonsFormBuilderData.nonce,
400 - form_content: formContent,
555 + form_content: flattenChoiceFields(formContent),
401 556 status: 'publish',
402 557 form_name: $scope.find('form').attr('name'),
403 558 form_id: $scope.find('input[name="form_id"]').val(),
404 559 form_page: $scope.find('form').attr('page'),
@@ -409,15 +564,15 @@
409 564 type: 'POST',
410 565 url: KingAddonsFormBuilderData.ajaxurl,
411 566 data: data,
412 567 success: function (response) {
413 - console.log(response);
568 + // console.log(response);
414 569
415 570
416 571
417 572 },
418 573 error: function (error) {
419 - console.log(error)
574 + // console.log(error)
420 575
421 576
422 577
423 578 }
@@ -450,8 +605,9 @@
450 605 return $.ajax({
451 606 url: KingAddonsFormBuilderData.ajaxurl,
452 607 method: 'POST',
453 608 data: {
609 + ...spamPayload(),
454 610 action: 'king_addons_form_builder_mailchimp',
455 611 nonce: KingAddonsFormBuilderData.nonce,
456 612 form_data: formData,
457 613 listId: submitButton.data('list-id')
@@ -460,9 +616,9 @@
460 616 beforeSend: function () {
461 617 submitButton.prop('disabled', true);
462 618 },
463 619 success: function (response) {
464 - console.log(response);
620 + // console.log(response);
465 621 if (!response.success) {
466 622
467 623
468 624
@@ -473,9 +629,9 @@
473 629 }
474 630
475 631 },
476 632 error: function (jqXHR, textStatus, errorThrown) {
477 - console.log(errorThrown);
633 + // console.log(errorThrown);
478 634
479 635
480 636
481 637 },
@@ -485,18 +641,621 @@
485 641 });
486 642 }
487 643 });
488 644
645 +
646 + /**
647 + * Wire up the field types that are more than a plain input: range, rating,
648 + * signature and acceptance.
649 + *
650 + * @param {jQuery} $scope Widget scope.
651 + */
652 + function initExtraFields($scope) {
653 + var root = $scope && $scope[0] ? $scope[0] : document;
654 +
655 + // Range: keep the printed value in step with the handle.
656 + root.querySelectorAll('.king-addons-form-range').forEach(function (wrap) {
657 + var input = wrap.querySelector('input[type="range"]');
658 + var output = wrap.querySelector('.king-addons-form-range__value');
659 + if (!input) {
660 + return;
661 + }
662 +
663 + var sync = function () {
664 + if (output) {
665 + output.textContent = input.value;
666 + }
667 + };
668 +
669 + input.addEventListener('input', sync);
670 + input.addEventListener('change', sync);
671 + sync();
672 + });
673 +
674 + // Rating: buttons drive a hidden input so the collector sees one value.
675 + root.querySelectorAll('[data-ka-rating]').forEach(function (wrap) {
676 + if (wrap.dataset.kaRatingReady === '1') {
677 + return;
678 + }
679 + wrap.dataset.kaRatingReady = '1';
680 +
681 + var input = wrap.querySelector('input');
682 + var items = wrap.querySelectorAll('.king-addons-form-rating__item');
683 +
684 + var paint = function (upto) {
685 + items.forEach(function (item, index) {
686 + item.classList.toggle('is-on', index < upto);
687 + });
688 + };
689 +
690 + items.forEach(function (item) {
691 + item.addEventListener('click', function () {
692 + var value = parseInt(item.dataset.value || '0', 10);
693 + // Clicking the current value again clears it.
694 + if (input && String(value) === input.value) {
695 + value = 0;
696 + }
697 + if (input) {
698 + input.value = value ? String(value) : '';
699 + }
700 + paint(value);
701 + wrap.classList.remove('king-addons-form-error');
702 + });
703 +
704 + item.addEventListener('mouseenter', function () {
705 + paint(parseInt(item.dataset.value || '0', 10));
706 + });
707 + });
708 +
709 + wrap.addEventListener('mouseleave', function () {
710 + paint(input && input.value ? parseInt(input.value, 10) : 0);
711 + });
712 + });
713 +
714 + // Signature: draw on a canvas, store a PNG data URL.
715 + root.querySelectorAll('[data-ka-signature]').forEach(function (wrap) {
716 + if (wrap.dataset.kaSignatureReady === '1') {
717 + return;
718 + }
719 + wrap.dataset.kaSignatureReady = '1';
720 +
721 + var canvas = wrap.querySelector('canvas');
722 + var input = wrap.querySelector('input');
723 + var clear = wrap.querySelector('.king-addons-form-signature__clear');
724 + if (!canvas || !input) {
725 + return;
726 + }
727 +
728 + var ctx = canvas.getContext('2d');
729 + var drawing = false;
730 + var dirty = false;
731 +
732 + var resize = function () {
733 + // Re-sizing wipes the canvas, so only do it while it is empty.
734 + if (dirty) {
735 + return;
736 + }
737 + var width = wrap.clientWidth || canvas.clientWidth;
738 + if (width > 0) {
739 + canvas.width = width;
740 + }
741 + ctx.lineWidth = 2;
742 + ctx.lineCap = 'round';
743 + ctx.lineJoin = 'round';
744 + ctx.strokeStyle = getComputedStyle(canvas).color || '#000';
745 + };
746 +
747 + var point = function (event) {
748 + var rect = canvas.getBoundingClientRect();
749 + var source = event.touches && event.touches[0] ? event.touches[0] : event;
750 + return {
751 + x: (source.clientX - rect.left) * (canvas.width / rect.width),
752 + y: (source.clientY - rect.top) * (canvas.height / rect.height)
753 + };
754 + };
755 +
756 + var start = function (event) {
757 + event.preventDefault();
758 + drawing = true;
759 + var p = point(event);
760 + ctx.beginPath();
761 + ctx.moveTo(p.x, p.y);
762 + };
763 +
764 + var move = function (event) {
765 + if (!drawing) {
766 + return;
767 + }
768 + event.preventDefault();
769 + var p = point(event);
770 + ctx.lineTo(p.x, p.y);
771 + ctx.stroke();
772 + dirty = true;
773 + };
774 +
775 + var stop = function () {
776 + if (!drawing) {
777 + return;
778 + }
779 + drawing = false;
780 + if (dirty) {
781 + input.value = canvas.toDataURL('image/png');
782 + wrap.classList.remove('king-addons-form-error');
783 + }
784 + };
785 +
786 + resize();
787 + window.addEventListener('resize', resize);
788 +
789 + canvas.addEventListener('mousedown', start);
790 + canvas.addEventListener('mousemove', move);
791 + document.addEventListener('mouseup', stop);
792 + canvas.addEventListener('touchstart', start, { passive: false });
793 + canvas.addEventListener('touchmove', move, { passive: false });
794 + canvas.addEventListener('touchend', stop);
795 +
796 + if (clear) {
797 + clear.addEventListener('click', function () {
798 + ctx.clearRect(0, 0, canvas.width, canvas.height);
799 + input.value = '';
800 + dirty = false;
801 + resize();
802 + });
803 + }
804 + });
805 + }
806 +
807 +
808 + /**
809 + * Show or hide fields according to their conditional logic rules.
810 + *
811 + * @param {jQuery} $scope Widget scope.
812 + */
813 + function initConditionalLogic($scope) {
814 + var root = $scope && $scope[0] ? $scope[0] : document;
815 + var form = root.querySelector('form');
816 + var groups = [];
817 +
818 + root.querySelectorAll('[data-ka-cond]').forEach(function (group) {
819 + var config;
820 + try {
821 + config = JSON.parse(group.getAttribute('data-ka-cond'));
822 + } catch (e) {
823 + return;
824 + }
825 +
826 + if (config && config.rules && config.rules.length) {
827 + groups.push({ el: group, config: config });
828 + }
829 + });
830 +
831 + if (!groups.length || !form) {
832 + return;
833 + }
834 +
835 + /**
836 + * Current value of the watched field, as a string.
837 + *
838 + * @param {string} fieldId Field ID as set in the panel.
839 + * @return {string} Value.
840 + */
841 + function readValue(fieldId) {
842 + var byId = form.querySelector('#form-field-' + CSS.escape(fieldId));
843 + if (byId) {
844 + if (byId.type === 'checkbox' || byId.type === 'radio') {
845 + return byId.checked ? (byId.value || '1') : '';
846 + }
847 + return byId.value || '';
848 + }
849 +
850 + // Radio and checkbox groups share a name rather than an id.
851 + var named = form.querySelectorAll('[name="form_fields[' + fieldId + ']"], [name="form_fields[' + fieldId + '][]"]');
852 + var values = [];
853 + Array.prototype.forEach.call(named, function (input) {
854 + if (input.type === 'checkbox' || input.type === 'radio') {
855 + if (input.checked) {
856 + values.push(input.value);
857 + }
858 + } else if (input.value) {
859 + values.push(input.value);
860 + }
861 + });
862 +
863 + return values.join(', ');
864 + }
865 +
866 + /**
867 + * Test one rule.
868 + *
869 + * @param {Object} rule Rule.
870 + * @return {boolean} Whether it matches.
871 + */
872 + function test(rule) {
873 + var actual = String(readValue(rule.field));
874 + var expected = String(rule.value === undefined ? '' : rule.value);
875 +
876 + switch (rule.op) {
877 + case 'is':
878 + return actual === expected;
879 + case 'is_not':
880 + return actual !== expected;
881 + case 'contains':
882 + return actual.toLowerCase().indexOf(expected.toLowerCase()) !== -1;
883 + case 'not_contains':
884 + return actual.toLowerCase().indexOf(expected.toLowerCase()) === -1;
885 + case 'is_empty':
886 + return actual.trim() === '';
887 + case 'is_not_empty':
888 + return actual.trim() !== '';
889 + case 'gt':
890 + return parseFloat(actual) > parseFloat(expected);
891 + case 'lt':
892 + return parseFloat(actual) < parseFloat(expected);
893 + }
894 +
895 + return false;
896 + }
897 +
898 + /**
899 + * Re-evaluate every conditional field.
900 + */
901 + function apply() {
902 + groups.forEach(function (entry) {
903 + var results = entry.config.rules.map(test);
904 + var matched = 'any' === entry.config.relation
905 + ? results.some(Boolean)
906 + : results.every(Boolean);
907 +
908 + var visible = 'hide' === entry.config.action ? !matched : matched;
909 +
910 + entry.el.classList.toggle('king-addons-cond-hidden', !visible);
911 + if (!visible) {
912 + entry.el.querySelectorAll('.king-addons-form-error').forEach(function (el) {
913 + el.classList.remove('king-addons-form-error');
914 + });
915 + entry.el.querySelectorAll('.king-addons-submit-error').forEach(function (el) {
916 + el.remove();
917 + });
918 + }
919 + });
920 + }
921 +
922 + form.addEventListener('input', apply);
923 + form.addEventListener('change', apply);
924 + apply();
925 + }
926 +
927 +
928 + /**
929 + * Evaluate an arithmetic expression without eval().
930 + *
931 + * Shunting-yard into RPN, then a stack machine. Anything the tokenizer does
932 + * not recognise makes the whole expression fail rather than fall through to
933 + * something surprising.
934 + *
935 + * @param {string} expression Expression with numbers and + - * / ( ).
936 + * @return {number|null} Result, or null when the expression is not valid.
937 + */
938 + function evaluateExpression(expression) {
939 + var tokens = String(expression).match(/\d+\.?\d*|[-+*/()]/g);
940 + if (!tokens || !tokens.length) {
941 + return null;
942 + }
943 +
944 + // Reject anything the tokenizer skipped over: letters, symbols, stray
945 + // characters. Only then is what is left safe to fold.
946 + if (tokens.join('').length !== String(expression).replace(/\s+/g, '').length) {
947 + return null;
948 + }
949 +
950 + var precedence = { '+': 1, '-': 1, '*': 2, '/': 2 };
951 + var output = [];
952 + var operators = [];
953 + var previous = null;
954 +
955 + for (var i = 0; i < tokens.length; i++) {
956 + var token = tokens[i];
957 +
958 + if (/^\d/.test(token)) {
959 + output.push(parseFloat(token));
960 + } else if ('(' === token) {
961 + operators.push(token);
962 + } else if (')' === token) {
963 + while (operators.length && operators[operators.length - 1] !== '(') {
964 + output.push(operators.pop());
965 + }
966 + if (!operators.length) {
967 + return null;
968 + }
969 + operators.pop();
970 + } else {
971 + // A minus with nothing usable in front of it is a sign, not an
972 + // operator: turn "-5" into "0 - 5".
973 + if ('-' === token && (null === previous || '(' === previous || precedence[previous])) {
974 + output.push(0);
975 + }
976 +
977 + while (
978 + operators.length &&
979 + operators[operators.length - 1] !== '(' &&
980 + precedence[operators[operators.length - 1]] >= precedence[token]
981 + ) {
982 + output.push(operators.pop());
983 + }
984 + operators.push(token);
985 + }
986 +
987 + previous = token;
988 + }
989 +
990 + while (operators.length) {
991 + var last = operators.pop();
992 + if ('(' === last) {
993 + return null;
994 + }
995 + output.push(last);
996 + }
997 +
998 + var stack = [];
999 + for (var j = 0; j < output.length; j++) {
1000 + var item = output[j];
1001 +
1002 + if ('number' === typeof item) {
1003 + stack.push(item);
1004 + continue;
1005 + }
1006 +
1007 + if (stack.length < 2) {
1008 + return null;
1009 + }
1010 +
1011 + var right = stack.pop();
1012 + var left = stack.pop();
1013 +
1014 + switch (item) {
1015 + case '+': stack.push(left + right); break;
1016 + case '-': stack.push(left - right); break;
1017 + case '*': stack.push(left * right); break;
1018 + case '/':
1019 + if (0 === right) {
1020 + return null;
1021 + }
1022 + stack.push(left / right);
1023 + break;
1024 + default: return null;
1025 + }
1026 + }
1027 +
1028 + if (1 !== stack.length || !isFinite(stack[0])) {
1029 + return null;
1030 + }
1031 +
1032 + return stack[0];
1033 + }
1034 +
1035 + /**
1036 + * Keep calculation fields in step with the fields they reference.
1037 + *
1038 + * @param {jQuery} $scope Widget scope.
1039 + */
1040 + function initCalculations($scope) {
1041 + var root = $scope && $scope[0] ? $scope[0] : document;
1042 + var form = root.querySelector('form');
1043 + var fields = root.querySelectorAll('[data-ka-calc]');
1044 +
1045 + if (!form || !fields.length) {
1046 + return;
1047 + }
1048 +
1049 + /**
1050 + * Numeric value of one referenced field.
1051 + *
1052 + * @param {string} fieldId Field ID.
1053 + * @return {number} Value, 0 when there is nothing usable.
1054 + */
1055 + function numberOf(fieldId) {
1056 + var input = form.querySelector('#form-field-' + CSS.escape(fieldId));
1057 + var raw = '';
1058 +
1059 + if (input) {
1060 + if (input.type === 'checkbox' || input.type === 'radio') {
1061 + raw = input.checked ? input.value : '0';
1062 + } else {
1063 + raw = input.value;
1064 + }
1065 + } else {
1066 + var checked = form.querySelector('[name="form_fields[' + fieldId + ']"]:checked, [name="form_fields[' + fieldId + '][]"]:checked');
1067 + if (checked) {
1068 + raw = checked.value;
1069 + }
1070 + }
1071 +
1072 + var value = parseFloat(String(raw).replace(',', '.'));
1073 +
1074 + return isFinite(value) ? value : 0;
1075 + }
1076 +
1077 + function apply() {
1078 + Array.prototype.forEach.call(fields, function (wrap) {
1079 + var input = wrap.querySelector('input');
1080 + var formula = wrap.getAttribute('data-formula') || '';
1081 +
1082 + if (!input || '' === formula.trim()) {
1083 + return;
1084 + }
1085 +
1086 + var resolved = formula.replace(/\[([^\]]+)\]/g, function (match, fieldId) {
1087 + return '(' + numberOf(fieldId.trim()) + ')';
1088 + });
1089 +
1090 + var result = evaluateExpression(resolved);
1091 +
1092 + if (null === result) {
1093 + input.value = '';
1094 + return;
1095 + }
1096 +
1097 + var decimals = parseInt(wrap.getAttribute('data-decimals') || '2', 10);
1098 + var text = result.toFixed(isFinite(decimals) ? decimals : 2);
1099 +
1100 + input.value = (wrap.getAttribute('data-prefix') || '') + text + (wrap.getAttribute('data-suffix') || '');
1101 + });
1102 + }
1103 +
1104 + form.addEventListener('input', apply);
1105 + form.addEventListener('change', apply);
1106 + apply();
1107 + }
1108 +
1109 +
1110 + /**
1111 + * Honeypot value and captcha token for this form.
1112 + *
1113 + * Every submit action is its own request, and each one is
1114 + * checked on the server, so the challenge has to travel with
1115 + * all of them.
1116 + *
1117 + * @return {Object} Extra request fields.
1118 + */
1119 + function spamPayload() {
1120 + var extra = {
1121 + king_addons_form_id: $scope.find('input[name="form_id"]').val()
1122 + };
1123 +
1124 + var honeypot = $scope.find('.king-addons-form-honeypot__input');
1125 + if (honeypot.length) {
1126 + extra[honeypot.attr('name')] = honeypot.val();
1127 + }
1128 +
1129 + var captcha = $scope.find('[data-ka-captcha]');
1130 + if (captcha.length) {
1131 + var token = captcha.find('[name="cf-turnstile-response"], [name="h-captcha-response"], [name="g-recaptcha-response"]').val();
1132 + extra.ka_captcha_token = token || '';
1133 + }
1134 +
1135 + return extra;
1136 + }
1137 +
1138 +
1139 + /**
1140 + * Save what the visitor has typed, before they submit.
1141 + *
1142 + * @param {jQuery} $scope Widget scope.
1143 + */
1144 + function initPartialEntries($scope) {
1145 + var submitGroup = $scope.find('.king-addons-form-field-type-submit');
1146 +
1147 + if ('1' !== String(submitGroup.data('partial-entries') || '')) {
1148 + return;
1149 + }
1150 +
1151 + var form = $scope.find('form');
1152 + if (!form.length) {
1153 + return;
1154 + }
1155 +
1156 + var formId = $scope.find('input[name="form_id"]').val();
1157 + var storageKey = 'kaPartial:' + formId;
1158 + var entryKey = '';
1159 +
1160 + try {
1161 + entryKey = window.sessionStorage.getItem(storageKey) || '';
1162 + } catch (e) {
1163 + entryKey = '';
1164 + }
1165 +
1166 + if (!entryKey) {
1167 + // One key per visit, so a returning visitor updates their own row
1168 + // instead of starting another.
1169 + entryKey = 'ka' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
1170 + try {
1171 + window.sessionStorage.setItem(storageKey, entryKey);
1172 + } catch (e) {
1173 + // Private browsing: the entry simply will not be reused.
1174 + }
1175 + }
1176 +
1177 + var timer = null;
1178 + var lastSent = '';
1179 +
1180 + /**
1181 + * Send the current values, at most once every couple of seconds.
1182 + */
1183 + function save() {
1184 + createFormContent();
1185 +
1186 + var snapshot = JSON.stringify(formContent);
1187 + if (snapshot === lastSent || '{}' === snapshot) {
1188 + return;
1189 + }
1190 + lastSent = snapshot;
1191 +
1192 + // Send a copy: formContent is shared with the submit path and is
1193 + // rebuilt from scratch there.
1194 + var payload = JSON.parse(snapshot);
1195 +
1196 + $.ajax({
1197 + type: 'POST',
1198 + url: KingAddonsFormBuilderData.ajaxurl,
1199 + data: {
1200 + action: 'king_addons_form_builder_partial',
1201 + nonce: KingAddonsFormBuilderData.nonce,
1202 + king_addons_form_id: formId,
1203 + form_page_id: form.attr('page_id'),
1204 + entry_key: entryKey,
1205 + form_content: payload
1206 + }
1207 + });
1208 + }
1209 +
1210 + form.on('input change', function () {
1211 + window.clearTimeout(timer);
1212 + timer = window.setTimeout(save, 2000);
1213 + });
1214 +
1215 + // A finished submission is the record; the draft is no longer wanted.
1216 + form.on('king-addons/form/submitted', function () {
1217 + try {
1218 + window.sessionStorage.removeItem(storageKey);
1219 + } catch (e) {
1220 + // Nothing to clean up.
1221 + }
1222 +
1223 + $.ajax({
1224 + type: 'POST',
1225 + url: KingAddonsFormBuilderData.ajaxurl,
1226 + data: {
1227 + action: 'king_addons_form_builder_partial',
1228 + nonce: KingAddonsFormBuilderData.nonce,
1229 + king_addons_form_id: formId,
1230 + form_page_id: form.attr('page_id'),
1231 + entry_key: entryKey,
1232 + finalise: 1
1233 + }
1234 + });
1235 + });
1236 + }
1237 +
489 1238 function createFormContent() {
490 1239 $scope.find('.king-addons-form-field, .king-addons-form-field-type-radio, .king-addons-form-field-type-checkbox, .king-addons-fb-step-input').each(function () {
491 1240
492 - var label = '';
493 - if ($(this).prev('label')) {
494 - label = $(this).prev('label').text().trim();
495 - } else {
496 - label = '';
1241 + // A field hidden by conditional logic is not part of
1242 + // the submission.
1243 + if ($(this).closest('.king-addons-cond-hidden').length) {
1244 + return;
497 1245 }
498 1246
1247 + // prev('label') always returns a jQuery object, so the
1248 + // old else branch never ran; and the newer field types
1249 + // wrap their input, which puts the label out of reach
1250 + // of prev(). Fall back to the field group's own label.
1251 + var label = $(this).prev('label').text().trim();
1252 + if ('' === label) {
1253 + label = $(this).closest('.king-addons-field-group')
1254 + .children('.king-addons-form-field-label')
1255 + .first().text().trim();
1256 + }
1257 +
499 1258 if ('textarea' !== $(this).prop('tagName').toLowerCase()) {
500 1259 if ($(this).hasClass('king-addons-select-wrap')) {
501 1260 var selectValue = $(this).find('select').val();
502 1261 if (Array.isArray($(this).find('select').val())) {
@@ -523,8 +1282,12 @@
523 1282
524 1283 if (checkedField.length > 0) {
525 1284 formContent[$(this).find('.king-addons-form-field-option').data('key').replace('-', '_')] = [type, valuesArray, inputLabel];
526 1285 }
1286 + } else if ($(this).attr('type') === 'checkbox' && $(this).closest('.king-addons-field-group').hasClass('king-addons-form-field-type-acceptance')) {
1287 + // A lone consent checkbox: val() would report
1288 + // its value attribute whether it is ticked or not.
1289 + formContent[$(this).attr('id').replace('-', '_')] = ['acceptance', $(this).is(':checked') ? $(this).val() : '', label];
527 1290 } else if ($(this).hasClass('king-addons-fb-step-input')) {
528 1291 formContent[$(this).attr('id').replace('-', '_')] = [$(this).attr('type'), '', $(this).val(), label];
529 1292 } else {
530 1293 if ($(this).attr('type') == 'file') {
@@ -583,15 +1346,15 @@
583 1346 contentType: false,
584 1347 success: function (response) {
585 1348 if (response.success) {
586 1349
587 - console.log(response);
1350 + // console.log(response);
588 1351 if (eventType == 'click') {
589 1352 fileUrl[thisId][i] = response.data.url;
590 1353 }
591 1354 resolve(response);
592 1355 } else {
593 - console.error('Error:', response);
1356 + // console.error('Error:', response);
594 1357 if (response.data) {
595 1358 if ('filesize' === response.data.cause) {
596 1359 let maxFileNotice = thisInput.data('maxfs-notice') ? thisInput.data('maxfs-notice') : response.data.message;
597 1360 thisInput.closest('.king-addons-field-group').append('<p class="king-addons-submit-error">' + maxFileNotice + '</p>');
@@ -613,9 +1376,9 @@
613 1376
614 1377 if ('filetype' == error.cause) {
615 1378 thisInput.closest('.king-addons-field-group').append('<p class="king-addons-submit-error">' + error.message + '</p>');
616 1379 }
617 - console.log(error);
1380 + // console.log(error);
618 1381 reject(error);
619 1382 },
620 1383 });
621 1384 }),
@@ -678,8 +1441,12 @@
678 1441 }
679 1442 var $types = ['text', 'email', 'password', 'file', 'url', 'tel', 'number', 'date', 'datetime-local', 'time', 'week', 'month', 'color'];
680 1443
681 1444 $stepTab.eq(currentTab).find('input, select, textarea').each(function () {
1445 + if ($(this).closest('.king-addons-cond-hidden').length) {
1446 + return;
1447 + }
1448 +
682 1449 const type = $(this).attr('type');
683 1450
684 1451 var requiredField = $(this).closest('.king-addons-field-group').find('.king-addons-form-field').attr('required') === 'required' || $(this).closest('.king-addons-field-group').find('.king-addons-form-field-textual').attr('required') === 'required';
685 1452
@@ -712,8 +1479,30 @@
712 1479
713 1480 $(this).addClass("king-addons-form-error");
714 1481
715 1482 valid = false;
1483 + }
1484 + });
1485 +
1486 + // Rating, signature and acceptance carry their value in a
1487 + // hidden input or a lone checkbox, so the loop above never
1488 + // sees them as required.
1489 + $stepTab.eq(currentTab).find('[data-ka-rating][data-ka-required], [data-ka-signature][data-ka-required]').not('.king-addons-cond-hidden *').each(function () {
1490 + var holder = $(this).find('input');
1491 + if (!holder.length || '' === String(holder.val() || '').trim()) {
1492 + $(this).addClass('king-addons-form-error');
1493 + valid = false;
1494 + } else {
1495 + $(this).removeClass('king-addons-form-error');
1496 + }
1497 + });
1498 +
1499 + $stepTab.eq(currentTab).find('.king-addons-form-field-type-acceptance:not(.king-addons-cond-hidden) input[type="checkbox"][required]').each(function () {
1500 + if (!$(this).is(':checked')) {
1501 + $(this).addClass('king-addons-form-error');
1502 + valid = false;
1503 + } else {
1504 + $(this).removeClass('king-addons-form-error');
716 1505 }
717 1506 });
718 1507
719 1508 if (!valid) {