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.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 51.1.46 All 39 releases
← All changes | includes/widgets/Form_Builder/script.js +833 -45 51.1.6451.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);
@@ -155,9 +155,9 @@
155 155 nonce: KingAddonsFormBuilderData.nonce
156 156 },
157 157 success: function (response) {
158 158 if (!response.success) {
159 - console.log(response);
159 + // console.log(response);
160 160 setTimeout(function () {
161 161 $scope.find('.king-addons-button').find('.king-addons-double-bounce').addClass('king-addons-loader-hidden');
162 162 $scope.find('.king-addons-button>span').removeClass('king-addons-loader-hidden');
163 163 $scope.find('form').append('<p class="king-addons-submit-error">' + KingAddonsFormBuilderData.recaptcha_error + '</p>');
@@ -163,14 +163,14 @@
163 163 $scope.find('form').append('<p class="king-addons-submit-error">' + KingAddonsFormBuilderData.recaptcha_error + '</p>');
164 164 }, 500);
165 165 callback(false);
166 166 } else {
167 - console.log(response);
167 + // console.log(response);
168 168 callback(true);
169 169 }
170 170 },
171 171 error: function (error) {
172 - console.log(error);
172 + // console.log(error);
173 173 setTimeout(function () {
174 174 $scope.find('.king-addons-button').find('.king-addons-double-bounce').addClass('king-addons-loader-hidden');
175 175 $scope.find('.king-addons-button>span').removeClass('king-addons-loader-hidden');
176 176 $scope.find('form').append('<p class="king-addons-submit-error">' + KingAddonsFormBuilderData.recaptcha_error + '</p>');
@@ -193,26 +193,42 @@
193 193 var actionsObject = {
194 194 emailPromise: sendEmail,
195 195 submissionsPromise: createPost,
196 196 mailchimpPromise: subscribeMailchimp,
197 - webhookPromise: sendWebhook
197 + webhookPromise: sendWebhook,
198 + integrationsPromise: sendIntegrations
198 199 }
199 200
200 -
201 + var firstActions = actions.filter(function (action) {
202 + return action !== 'payment';
203 + });
204 +
201 205 Promise.all(
202 - actions.map((action) => {
206 + firstActions.map((action) => {
203 207 try {
204 208 if (actionsObject[action + 'Promise']) {
205 209 return actionsObject[action + 'Promise']();
206 210 }
207 211 } catch (error) {
208 - console.error(error);
209 212 return Promise.reject(error);
210 213 }
211 214 })
212 215 )
213 216 .then((responses) => {
214 - 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);
215 231
216 232
217 233 const createPostResponse = responses.find((response) => response && response.data.action === 'king_addons_form_builder_submissions');
218 234
@@ -245,18 +261,73 @@
245 261 }
246 262 });
247 263
248 264 return Promise.all(updateMetaPromises).then(() => {
249 - if (responsesArray.includes('error')) {
250 - var sanitizedErrorMessage = $('<div>').text($scope.data('settings').error_message).html();
251 - $scope.find('form').append('<p class="king-addons-submit-error">' + sanitizedErrorMessage + '</p>');
252 - } 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 + }
253 305 $scope.find('form').append(
254 - $('<p class="king-addons-submit-success"></p>').text($scope.data('settings').success_message)
306 + $('<p class="king-addons-submit-success"></p>').text(successCopy)
255 307 );
256 -
308 + $scope.find('form').trigger('king-addons/form/submitted');
309 +
257 310 $scope.find('button').attr('disabled', true);
258 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 + );
259 330 }
260 331 });
261 332
262 333 })
@@ -261,9 +332,9 @@
261 332
262 333 })
263 334 .catch((error) => {
264 335
265 - console.error(error);
336 + // console.error(error);
266 337 })
267 338 .then(() => {
268 339
269 340 setTimeout(function () {
@@ -270,8 +341,16 @@
270 341
271 342 $scope.find('.king-addons-button').find('.king-addons-double-bounce').addClass('king-addons-loader-hidden');
272 343 $scope.find('.king-addons-button>span').removeClass('king-addons-loader-hidden');
273 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 +
274 353 if (actions.includes('redirect') && responsesArray.includes('success')) {
275 354
276 355 $(location).prop('href', $scope.find('.king-addons-form-field-type-submit').data('redirect-url'))
277 356 }
@@ -279,9 +358,9 @@
279 358 }, 500);
280 359 })
281 360 .catch((error) => {
282 361
283 - console.error(error);
362 + // console.error(error);
284 363 });
285 364 } else {
286 365
287 366 return false;
@@ -326,12 +405,12 @@
326 405 data: {
327 406 action: 'king_addons_form_builder_email',
328 407 nonce: KingAddonsFormBuilderData.nonce,
329 408 form_content: data,
330 - king_addons_form_id: $scope.find('input[name="form_id"]').val(),
409 + ...spamPayload(),
331 410 },
332 411 success: function (response) {
333 - console.log(response);
412 + // console.log(response);
334 413 if (!response.success) {
335 414
336 415
337 416
@@ -348,8 +427,60 @@
348 427 }
349 428 });
350 429 }
351 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 +
352 483 function sendWebhook() {
353 484 var data = deepCopy(formContent);
354 485
355 486 for (let key in data) {
@@ -365,8 +496,9 @@
365 496 return $.ajax({
366 497 type: 'POST',
367 498 url: KingAddonsFormBuilderData.ajaxurl,
368 499 data: {
500 + ...spamPayload(),
369 501 action: 'king_addons_form_builder_webhook',
370 502 nonce: KingAddonsFormBuilderData.nonce,
371 503 form_content: data,
372 504 king_addons_form_id: $scope.find('input[name="form_id"]').val(),
@@ -372,9 +504,9 @@
372 504 king_addons_form_id: $scope.find('input[name="form_id"]').val(),
373 505 form_name: $scope.find('form').attr('name')
374 506 },
375 507 success: function (response) {
376 - console.log(response);
508 + // console.log(response);
377 509 if (!response.success) {
378 510
379 511
380 512
@@ -384,9 +516,9 @@
384 516
385 517 }
386 518 },
387 519 error: function (error) {
388 - console.log(error);
520 + // console.log(error);
389 521
390 522
391 523
392 524 }
@@ -392,14 +524,36 @@
392 524 }
393 525 });
394 526 }
395 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 +
396 549 function createPost() {
397 550
398 551 var data = {
552 + ...spamPayload(),
399 553 action: 'king_addons_form_builder_submissions',
400 554 nonce: KingAddonsFormBuilderData.nonce,
401 - form_content: formContent,
555 + form_content: flattenChoiceFields(formContent),
402 556 status: 'publish',
403 557 form_name: $scope.find('form').attr('name'),
404 558 form_id: $scope.find('input[name="form_id"]').val(),
405 559 form_page: $scope.find('form').attr('page'),
@@ -410,15 +564,15 @@
410 564 type: 'POST',
411 565 url: KingAddonsFormBuilderData.ajaxurl,
412 566 data: data,
413 567 success: function (response) {
414 - console.log(response);
568 + // console.log(response);
415 569
416 570
417 571
418 572 },
419 573 error: function (error) {
420 - console.log(error)
574 + // console.log(error)
421 575
422 576
423 577
424 578 }
@@ -451,8 +605,9 @@
451 605 return $.ajax({
452 606 url: KingAddonsFormBuilderData.ajaxurl,
453 607 method: 'POST',
454 608 data: {
609 + ...spamPayload(),
455 610 action: 'king_addons_form_builder_mailchimp',
456 611 nonce: KingAddonsFormBuilderData.nonce,
457 612 form_data: formData,
458 613 listId: submitButton.data('list-id')
@@ -461,9 +616,9 @@
461 616 beforeSend: function () {
462 617 submitButton.prop('disabled', true);
463 618 },
464 619 success: function (response) {
465 - console.log(response);
620 + // console.log(response);
466 621 if (!response.success) {
467 622
468 623
469 624
@@ -474,9 +629,9 @@
474 629 }
475 630
476 631 },
477 632 error: function (jqXHR, textStatus, errorThrown) {
478 - console.log(errorThrown);
633 + // console.log(errorThrown);
479 634
480 635
481 636
482 637 },
@@ -486,18 +641,621 @@
486 641 });
487 642 }
488 643 });
489 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 +
490 1238 function createFormContent() {
491 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 () {
492 1240
493 - var label = '';
494 - if ($(this).prev('label')) {
495 - label = $(this).prev('label').text().trim();
496 - } else {
497 - 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;
498 1245 }
499 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 +
500 1258 if ('textarea' !== $(this).prop('tagName').toLowerCase()) {
501 1259 if ($(this).hasClass('king-addons-select-wrap')) {
502 1260 var selectValue = $(this).find('select').val();
503 1261 if (Array.isArray($(this).find('select').val())) {
@@ -524,8 +1282,12 @@
524 1282
525 1283 if (checkedField.length > 0) {
526 1284 formContent[$(this).find('.king-addons-form-field-option').data('key').replace('-', '_')] = [type, valuesArray, inputLabel];
527 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];
528 1290 } else if ($(this).hasClass('king-addons-fb-step-input')) {
529 1291 formContent[$(this).attr('id').replace('-', '_')] = [$(this).attr('type'), '', $(this).val(), label];
530 1292 } else {
531 1293 if ($(this).attr('type') == 'file') {
@@ -584,15 +1346,15 @@
584 1346 contentType: false,
585 1347 success: function (response) {
586 1348 if (response.success) {
587 1349
588 - console.log(response);
1350 + // console.log(response);
589 1351 if (eventType == 'click') {
590 1352 fileUrl[thisId][i] = response.data.url;
591 1353 }
592 1354 resolve(response);
593 1355 } else {
594 - console.error('Error:', response);
1356 + // console.error('Error:', response);
595 1357 if (response.data) {
596 1358 if ('filesize' === response.data.cause) {
597 1359 let maxFileNotice = thisInput.data('maxfs-notice') ? thisInput.data('maxfs-notice') : response.data.message;
598 1360 thisInput.closest('.king-addons-field-group').append('<p class="king-addons-submit-error">' + maxFileNotice + '</p>');
@@ -614,9 +1376,9 @@
614 1376
615 1377 if ('filetype' == error.cause) {
616 1378 thisInput.closest('.king-addons-field-group').append('<p class="king-addons-submit-error">' + error.message + '</p>');
617 1379 }
618 - console.log(error);
1380 + // console.log(error);
619 1381 reject(error);
620 1382 },
621 1383 });
622 1384 }),
@@ -679,8 +1441,12 @@
679 1441 }
680 1442 var $types = ['text', 'email', 'password', 'file', 'url', 'tel', 'number', 'date', 'datetime-local', 'time', 'week', 'month', 'color'];
681 1443
682 1444 $stepTab.eq(currentTab).find('input, select, textarea').each(function () {
1445 + if ($(this).closest('.king-addons-cond-hidden').length) {
1446 + return;
1447 + }
1448 +
683 1449 const type = $(this).attr('type');
684 1450
685 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';
686 1452
@@ -713,8 +1479,30 @@
713 1479
714 1480 $(this).addClass("king-addons-form-error");
715 1481
716 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');
717 1505 }
718 1506 });
719 1507
720 1508 if (!valid) {