1function removeKeysFromDataString(dataString, keysToRemove) { 2 // Convert the query string into an array of key-value pairs 3 const params = new URLSearchParams(dataString); 4 5 // Remove specified keys 6 keysToRemove.forEach(key => { 7 if (params.has(key)) { 8 params.delete(key); 9 } 10 }); 11 12 // Convert back to query string 13 return params.toString(); 14} 15 16// --- Signature debug (remove this whole block when finished) --- 17// Toggle: set DEBUG_SIGNATURE_FORM = true below, or in the browser console: window.DEBUG_SIGNATURE_FORM = true 18var DEBUG_SIGNATURE_FORM = true; 19 20function isSignatureDebugEnabled() { 21 return DEBUG_SIGNATURE_FORM === true || 22 (typeof window !== 'undefined' && window.DEBUG_SIGNATURE_FORM === true); 23} 24 25function debugLogSignatureField(info) { 26 if (!isSignatureDebugEnabled()) { 27 return; 28 } 29 console.log('[BloqForms signature]', info); 30} 31// --- end signature debug --- 32 33// Function to convert query string to JSON 34function queryStringToJson(queryString) { 35 const params = new URLSearchParams(queryString); 36 const jsonObject = {}; 37 for (const [key, value] of params.entries()) { 38 // If the key already exists, convert to an array to handle duplicates 39 if (jsonObject[key]) { 40 if (!Array.isArray(jsonObject[key])) { 41 jsonObject[key] = [jsonObject[key]]; 42 } 43 jsonObject[key].push(value); 44 } else { 45 jsonObject[key] = value; 46 } 47 } 48 return jsonObject; 49} 50 51// Function to toggle visibility based on rowId 52function toggleVisibility(rowId, action) { 53 const element = document.getElementById(rowId); 54 if (element) { 55 if (action === 'show') { 56 element.classList.remove('hidden'); 57 58 const requiredFields = element.querySelectorAll('[data-required="true"]'); 59 requiredFields.forEach(field => { 60 field.setAttribute('required', 'required'); 61 }); 62 63 restoreChildRows(element); 64 65 // ONLY if signature pad exists 66 const signatureContainers = element.querySelectorAll('.signature-container'); 67 68 if (signatureContainers.length > 0) { 69 setTimeout(() => { 70 reinitializeSignaturePad(element); 71 }, 50); 72 } 73 } else if (action === 'hide') { 74 element.classList.add('hidden'); 75 76 const formFields = element.querySelectorAll('[required]'); 77 formFields.forEach(field => { 78 field.removeAttribute('required'); 79 80 if (!field.hasAttribute('data-required')) { 81 field.setAttribute('data-required', 'true'); 82 } 83 }); 84 85 hideChildRows(rowId); 86 } 87 } 88} 89 90function hideChildRows(parentRowId, clearReferences = true) { 91 const element = document.getElementById(parentRowId); 92 if (!element) return; 93 94 const selects = element.querySelectorAll('select'); 95 const radios = element.querySelectorAll('input[type="radio"]:checked'); 96 const checkboxes = element.querySelectorAll('input[type="checkbox"]:checked'); 97 98 // Handle selects 99 selects.forEach(select => { 100 const prevRowId = select.dataset.prevRowId; 101 if (prevRowId && prevRowId !== 'none') { 102 // Hide this child row 103 const childElement = document.getElementById(prevRowId); 104 if (childElement) { 105 childElement.classList.add('hidden'); 106 107 hideChildRows(prevRowId, clearReferences); 108 } 109 } 110 }); 111 112 // Handle radios 113 radios.forEach(radio => { 114 const prevRowId = radio.dataset.prevRowId; 115 if (prevRowId && prevRowId !== 'none') { 116 // Hide this child row 117 const childElement = document.getElementById(prevRowId); 118 if (childElement) { 119 childElement.classList.add('hidden'); 120 121 hideChildRows(prevRowId, clearReferences); 122 } 123 } 124 }); 125 126 // Handle checkboxes
127 checkboxes.forEach(checkbox => { 128 const prevRowId = checkbox.dataset.prevRowId; 129 if (prevRowId && prevRowId !== 'none') { 130 // Hide this child row 131 const childElement = document.getElementById(prevRowId); 132 if (childElement) { 133 childElement.classList.add('hidden'); 134 135 hideChildRows(prevRowId, clearReferences); 136 } 137 } 138 }); 139} 140 141function restoreChildRows(element) { 142 if (!element) return; 143 144 const selects = element.querySelectorAll('select'); 145 const radios = element.querySelectorAll('input[type="radio"]:checked'); 146 const checkboxes = element.querySelectorAll('input[type="checkbox"]:checked'); 147 148 selects.forEach(select => { 149 const prevRowId = select.dataset.prevRowId; 150 if (prevRowId && prevRowId !== 'none') { 151 toggleVisibility(prevRowId, 'show'); 152 } 153 }); 154 155 radios.forEach(radio => { 156 const prevRowId = radio.dataset.prevRowId; 157 if (prevRowId && prevRowId !== 'none') { 158 toggleVisibility(prevRowId, 'show'); 159 } 160 }); 161 162 checkboxes.forEach(checkbox => { 163 const prevRowId = checkbox.dataset.prevRowId; 164 if (prevRowId && prevRowId !== 'none') { 165 toggleVisibility(prevRowId, 'show'); 166 } 167 }); 168} 169 170// Function to initialize form functionality 171function initializeFormBehavior(formData, bloqFormId = null) { 172 const selectRowIdMappings = {}; 173 const radioRowIdMappings = {}; 174 const checkboxRowIdMappings = {}; 175 176 var containerForm = document.getElementById(`containerForm`); 177 if (bloqFormId) { 178 containerForm = document.getElementById(`containerForm_${bloqFormId}`); 179 } 180 181 if (formData) { 182 let initializedForms = new Set(); // Store processed form IDs 183 // Loop through form data to find 'Drop Down' or 'Radio Button' or 'Checkbox' components 184 formData.forEach(data => { 185 const jsonData = JSON.parse(data.json_data); 186 187 if (data.component_name === 'Drop Down' || data.component_name === 'Radio Button' || data.component_name === 'Checkbox') { 188 jsonData.components.forEach(component => { 189 if (component.options) { 190 // Iterate through each option to check the rowId 191 component.options.forEach(option => { 192 const rowId = option.rowId; 193 const element = containerForm.querySelector(`#${rowId}`); 194 195 if (rowId !== 'none') { 196 if (element) { 197 // Add the hidden class to the element if rowId matches 198 element.classList.add('hidden'); 199 200 const formFields = element.querySelectorAll('[required]'); 201 formFields.forEach(field => { 202 field.removeAttribute('required'); 203 204 if (!field.hasAttribute('data-required')) { 205 field.setAttribute('data-required', 'true'); 206 } 207 }); 208 } 209 210 if (component.tagName === 'select') { 211 if (!selectRowIdMappings[component.name]) { 212 selectRowIdMappings[component.name] = []; 213 } 214 selectRowIdMappings[component.name].push([rowId, option.label]); 215 } else if (component.tagName === 'input' && component.type === 'radio') { 216 radioRowIdMappings[option.name] = rowId; 217 } else if (component.tagName === 'input' && component.type === 'checkbox') { 218 checkboxRowIdMappings[option.name] = rowId; 219 } 220 } 221 }); 222 } 223 }); 224 } 225 226 if (data.component_name === 'Star Rating') { 227 // Initialize star rating for each form component 228 initializeStarRating(); 229 } 230 231 if (data.component_name === 'Range') { 232 // Initialize Range for each form component 233 initializeRange(); 234 } 235 236 if (data.component_name === 'File Upload') { 237 // Initialize Range for each form component 238 initializeFileuploader(); 239 } 240 241 if (data.component_name === 'Donation') { 242 let choiceValues = [];
243 jsonData.components.forEach(component => { 244 // Check if the component is of type 'donation-box' 245 if (component.type === 'donation-box') { 246 if (component.options) { 247 // Extract values from the options array 248 component.options.forEach(option => { 249 choiceValues.push(option.value); 250 }); 251 } 252 } 253 }); 254 255 // Initialize the donation box component 256 initializeDonationBox(choiceValues, data.f_x8_form_id, bloqFormId); 257 } 258 259 if (data.component_name === 'Signature') { 260 // Initialize the signature pad component 261 initializeSignaturePad(); 262 } 263 264 if (data.component_name === 'Item with options' || data.component_name === 'Item with Options (Card Style)' || data.component_name === 'Ticket with options') { 265 // Initialize the signature pad component 266 267 if (!initializedForms.has(data.f_x8_form_id)) { 268 initializeTicketCard(data.f_x8_form_id, bloqFormId); 269 initializedForms.add(data.f_x8_form_id); // Mark form as initialized 270 } 271 } 272 }); 273 } 274 275 // Add event listeners to checkbox buttons 276 containerForm.querySelectorAll('input[type="checkbox"]').forEach((checkbox, index) => { 277 if (checkbox) { 278 const checkboxId = checkbox.getAttribute('id'); 279 const rowId = checkboxRowIdMappings[checkboxId] || 'none'; 280 281 checkbox.dataset.prevRowId = rowId; 282 283 if (checkbox.checked && rowId !== 'none') { 284 toggleVisibility(rowId, 'show'); 285 } 286 287 // Add event listener for checkbox change 288 checkbox.addEventListener('change', function () { 289 // const rowId = checkboxRowIdMappings[checkboxId] || 'none'; 290 291 // Toggle visibility based on the checkbox state 292 if (this.checked) { 293 if (rowId !== 'none') { 294 toggleVisibility(rowId, 'show'); 295 } 296 } else { 297 if (rowId !== 'none') { 298 toggleVisibility(rowId, 'hide'); 299 } 300 } 301 302 // Store the new rowId for future reference 303 // this.dataset.prevRowId = rowId; 304 }); 305 } 306 }); 307 308 // Add event listeners to radio buttons 309 containerForm.querySelectorAll('input[type="radio"]').forEach(radio => { 310 const radioId = radio.getAttribute('id'); 311 const rowId = radioRowIdMappings[radioId] || 'none'; 312 313 radio.dataset.prevRowId = rowId; 314 315 radio.addEventListener('change', function() { 316 // const radioId = radio.getAttribute('id'); 317 // const rowId = radioRowIdMappings[radioId] || 'none'; 318 319 // Hide the previously selected rowId if available 320 const radios = containerForm.querySelectorAll(`[name="${this.name}"]`); 321 radios.forEach(radio => { 322 const prevRowId = radio.dataset.prevRowId || null; 323 if (prevRowId) { 324 toggleVisibility(prevRowId, 'hide'); 325 } 326 }); 327 328 // Toggle visibility based on the new selection's rowId 329 if (rowId !== 'none') { 330 toggleVisibility(rowId, 'show'); 331 } 332 333 // Store the new rowId for future reference 334 // this.dataset.prevRowId = rowId; 335 }); 336 }); 337 338 // Add event listeners to select dropdowns 339 containerForm.querySelectorAll('select').forEach(select => { 340 select.addEventListener('change', function() { 341 const selectedOptionLabel = this.options[this.selectedIndex].text; 342 const selectName = select.getAttribute('name'); 343 344 // Find the rowId associated with the selected label 345 const mappings = selectRowIdMappings[selectName] || []; 346 const matchedMapping = mappings.find(mapping => mapping[1].trim() === selectedOptionLabel.trim()); 347 const rowId = matchedMapping ? matchedMapping[0] : 'none'; 348 349 // Hide the previously selected rowId 350 const prevRowId = this.dataset.prevRowId || null; 351 if (prevRowId) { 352 toggleVisibility(prevRowId, 'hide'); 353 } 354 355 // Show the new rowId if it's not 'none' 356 if (rowId !== 'none') { 357 toggleVisibility(rowId, 'show'); 358 } 359 360 // Store the new rowId for future reference 361 this.dataset.prevRowId = rowId; 362 }); 363 }); 364 365 containerForm.querySelectorAll('.phone').forEach(function (phoneInput) { 366 phoneInput.addEventListener('input', function (e) { 367 let input = e.target.value; 368 input = input.replace(/\D/g, ''); // Remove all non-numeric characters 369 370 // Format the input value as (123) 456-7890 371 if (input.length > 0) { 372 input = '(' + input; 373 } 374 if (input.length > 4) { 375 input = input.slice(0, 4) + ') ' + input.slice(4); 376 } 377 if (input.length > 9) { 378 input = input.slice(0, 9) + '-' + input.slice(9); 379 } 380 e.target.value = input.slice(0, 14); // Limit input length 381 }); 382 }); 383 384 // Select all date and time input elements and show the date or time picker 385 containerForm.querySelectorAll('input[type="date"], input[type="time"]').forEach(input => { 386 input.addEventListener('click', function() { 387 this.showPicker(); 388 }); 389 }); 390 391 $('input[type="number"]').on('keydown input', function(event) { 392 // Prevent 'e', '.', and '-' keys on keydown 393 if (event.type === 'keydown' && (event.key.toLowerCase() === 'e' || event.key === '.' || event.key === '-')) { 394 event.preventDefault(); 395 } 396 }); 397 398 const formRows = document.querySelectorAll('.form-row'); 399 400 if (formRows) { 401 formRows.forEach(formRow => { 402 const formElems = formRow.querySelectorAll('.form-elem'); 403 404 if (formRow.classList.contains('sm:grid-cols-2')) {
405 formElems.forEach(elem => { 406 407 elem.classList.remove('sm:grid-cols-2'); 408 elem.classList.remove('md:grid-cols-3'); 409 elem.classList.remove('md:grid-cols-6'); 410 elem.classList.remove('md:grid-cols-12'); 411 412 const divs = elem.querySelectorAll('.form-ctrl'); 413 414 divs.forEach(div => { 415 div.className = ''; 416 div.classList.add('w-full', 'mb-4', 'col-span-1'); 417 }); 418 }); 419 } 420 }); 421 } 422} 423 424// Function to initialize star rating functionality 425function initializeStarRating() { 426 document.querySelectorAll('.star-rating').forEach((container,index) => { 427 const stars = container.querySelectorAll('.fa-star'); 428 const ratingInput = container.querySelector('#rating'); 429 430 // Make the input name dynamic based on index or unique identifier 431 if (ratingInput) { 432 const dynamicName = `rating_${index}`; // Dynamically generate the name attribute 433 ratingInput.setAttribute('name', dynamicName); // Set the dynamic name attribute 434 } 435 436 if (stars.length && ratingInput) { 437 stars.forEach((star, index) => { 438 star.addEventListener('click', function() { 439 ratingInput.value = star.getAttribute('data-value'); 440 441 // Reset the color of all stars 442 stars.forEach(s => s.classList.remove('text-yellow-500')); 443 stars.forEach(s => s.classList.add('text-gray-300')); 444 445 // Set the color of the selected stars 446 for (let i = 0; i <= index; i++) { 447 stars[i].classList.add('text-yellow-500'); 448 } 449 }); 450 }); 451 } 452 }); 453} 454 455// Function to initialize range functionality 456function initializeRange() { 457 document.querySelectorAll('.range-selector').forEach((container, index) => { 458 const rangeInput = container.querySelector('input[type="range"]'); 459 const rangeValueSpan = container.querySelector('#range_value'); 460 461 if (rangeInput) { 462 const originalName = rangeInput.getAttribute('name') || ''; // Get the original name if it exists 463 const dynamicName = `${originalName}_${index}`; // Append the index to the original name 464 rangeInput.setAttribute('name', dynamicName); // Set the dynamic name attribute 465 466 // Add an event listener to update the displayed value as the range input changes 467 rangeInput.addEventListener('input', function() { 468 rangeValueSpan.textContent = this.value; 469 }); 470 } 471 }); 472} 473 474// Function to initialize file upload functionality 475function initializeFileuploader() { 476 document.querySelectorAll('input[type="file"]').forEach((fileInput) => { 477 let selectedFiles = []; 478 479 fileInput.addEventListener('change', function () { 480 let newFiles = Array.from(this.files); 481 let validFiles = []; 482 let oversizedFiles = []; 483 let invalidFiles = []; 484 const dataTransfer = new DataTransfer(); 485 486 const notAllowedExtensions = /(\.exe)$/i; 487 488 // Validate file sizes (Max: 5MB) 489 newFiles.forEach((file) => { 490 if (file.size > 5 * 1024 * 1024) { // 5MB limit 491 oversizedFiles.push(`"${file.name}"`); 492 } else if (notAllowedExtensions.test(file.name)) { 493 invalidFiles.push(`"${file.name}"`); 494 } else { 495 validFiles.push(file); 496 dataTransfer.items.add(file); 497 } 498 }); 499 500 // If there are oversized and invalid files 501 if (oversizedFiles.length > 0) { 502 alert(`The following files exceed the 5MB limit and will be removed:\n\n${oversizedFiles.join("\n")}`); 503 } 504 if (invalidFiles.length > 0) { 505 alert(`The following files are not allowed (.exe files) and will be removed:\n\n${invalidFiles.join("\n")}`); 506 } 507 508 selectedFiles = validFiles; 509 this.files = dataTransfer.files; // Update input with valid files 510 511 // Find the closest #fileList 512 const fileListContainer = fileInput.parentElement.querySelector('#fileList'); 513 if (!fileListContainer) return; 514 515 updateFileList(selectedFiles, fileListContainer, this); 516 }); 517 }); 518 519 function updateFileList(files, fileListContainer, fileInput) { 520 fileListContainer.innerHTML = ""; 521 522 const ul = document.createElement('ul'); 523 ul.style.listStyle = 'none'; 524 ul.style.padding = '0'; 525
526 files.forEach((file, index) => { 527 const listItem = document.createElement("li"); 528 listItem.className = "flex gap-2 mb-2"; 529 530 listItem.innerHTML = ` 531 <span class="ml-4">${file.name} (${formatFileSize(file.size)})</span> 532 <button type="button" class="cursor-pointer text-red-500 text-xs remove-file" title="Remove file" data-index="${index}"> 533 <i class="fa-solid fa-trash-can"></i> 534 </button> 535 `; 536 537 ul.appendChild(listItem); 538 fileListContainer.appendChild(ul); 539 }); 540 541 attachRemoveHandlers(fileListContainer, files, fileInput); 542 } 543 544 function attachRemoveHandlers(fileListContainer, files, fileInput) { 545 fileListContainer.querySelectorAll(".remove-file").forEach(button => { 546 button.addEventListener("click", function () { 547 const index = parseInt(this.getAttribute("data-index")); 548 files.splice(index, 1); 549 550 updateFileList(files, fileListContainer, fileInput); 551 552 const dataTransfer = new DataTransfer(); 553 files.forEach(file => dataTransfer.items.add(file)); 554 fileInput.files = dataTransfer.files; 555 }); 556 }); 557 } 558 559 function formatFileSize(bytes) { 560 if (bytes === 0) return '0 Bytes'; 561 const k = 1024; 562 const sizes = ['Bytes', 'KB', 'MB', 'GB']; 563 const i = Math.floor(Math.log(bytes) / Math.log(k)); 564 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 565 } 566} 567 568// Initialize donation box for preset and custom amounts 569function initializeDonationBox(choiceValues, fX8FormId, bloqFormId) { 570 const donationContainers = document.querySelectorAll('.donation-container'); 571 572 donationContainers.forEach((container) => { 573 const presets = container.querySelectorAll('#preset1, #preset2, #preset3'); 574 // Adds event listeners to preset buttons to update the amount 575 presets.forEach((preset, index) => { 576 preset.addEventListener('click', () => { 577 updateSelectedAmount(container, choiceValues, choiceValues[index], fX8FormId, bloqFormId); 578 }); 579 }); 580 initializeCustomAmountHandler(container, choiceValues, fX8FormId, bloqFormId); 581 addAmountToDonate(container, choiceValues, fX8FormId, bloqFormId); 582 }); 583} 584 585// Updates the selected amount and highlights the corresponding preset button 586function updateSelectedAmount(container, choiceValues, amount, fX8FormId, bloqFormId) { 587 const otherAmountInput = container.querySelector('#other_amount'); 588 const presets = container.querySelectorAll('#preset1, #preset2, #preset3'); 589 590 // Set input value to the selected amount 591 otherAmountInput.value = amount; 592 593 // Reset the styling of all preset buttons 594 resetPresetStyles(container); 595 596 // Highlight the corresponding preset based on the selected amount 597 const presetIndex = choiceValues.indexOf(amount.toString()); 598 if (presetIndex !== -1) { 599 presets[presetIndex].classList.add('bg-blue-500', 'text-white'); 600 presets[presetIndex].classList.remove('bg-gray-200', 'text-gray-700'); 601 } 602 603 // Adds event listeners to preset buttons to update the amount 604 presets.forEach((preset, index) => { 605 preset.addEventListener('click', () => { 606 updateSelectedAmount(container, choiceValues, choiceValues[index], fX8FormId, bloqFormId); 607 }); 608 }); 609} 610 611// Adds event listener to the custom amount input field 612function initializeCustomAmountHandler(container, choiceValues, fX8FormId, bloqFormId) { 613 const otherAmountInput = container.querySelector('#other_amount'); 614 615 otherAmountInput.addEventListener('change', function () { 616 const amount = parseInt(this.value, 10); 617 618 if (choiceValues.includes(amount.toString())) { 619 updateSelectedAmount(container, choiceValues, amount, fX8FormId, bloqFormId); 620 } else { 621 resetPresetStyles(container); 622 } 623 }); 624} 625 626function addAmountToDonate(container, choiceValues, fX8FormId, bloqFormId) { 627 // console.log(amount ? amount : 'no'); 628 const donationBtn = container.querySelector('#donationBtn'); 629 630 // Attach event listener for add button 631 if (donationBtn) { 632 donationBtn.addEventListener('click', function () { 633 handleDonationAddToCart(this, container, choiceValues, fX8FormId, bloqFormId); 634 }); 635 } 636 637 // Handle adding item to the cart 638 function handleDonationAddToCart(donationBtn, container, choiceValues, fX8FormId, bloqFormId) { 639 let dFormDataId, donationName; 640 641 const presets = container.querySelectorAll('#preset1, #preset2, #preset3'); 642 const otherAmountInput = container.querySelector('#other_amount'); 643 var amount = otherAmountInput.value; 644 645 // Highlight the corresponding preset based on the selected amount
646 const presetIndex = choiceValues.indexOf(amount.toString()); 647 if (presetIndex !== -1) { 648 dFormDataId = presets[presetIndex].getAttribute('data-form-data-id'); 649 donationName = presets[presetIndex].getAttribute('data-name'); 650 } else { 651 dFormDataId = donationBtn.getAttribute('data-form-data-id'); 652 donationName = donationBtn.getAttribute('data-name'); 653 } 654 655 if (bloqFormId) { 656 var cartSection = document.getElementById(`cartSection_${bloqFormId}`); 657 } else { 658 var cartSection = document.getElementById('cartSection'); 659 } 660 const tableBody = cartSection.querySelector('tbody'); 661 662 amount = Number(amount); 663 664 const errorMsgContainer = container.nextElementSibling; 665 // Check and remove the error message if it exists 666 if (errorMsgContainer && errorMsgContainer.classList.contains('error-message')) { 667 errorMsgContainer.remove(); 668 } 669 670 if (amount > 0) { 671 const existingRow = findExistingDonationRow(donationName, amount, tableBody); 672 673 if (existingRow) { 674 const itemPrice = existingRow.querySelector('.itemPrice'); 675 const currentItemPrice = parseFloat(itemPrice.textContent.replace('$', '')); 676 const newItemPrice = (currentItemPrice + amount).toFixed(2); 677 itemPrice.textContent = `$${newItemPrice}`; 678 679 updateRowTotal(existingRow); 680 } else { 681 createNewRow(donationName, amount, null, 0, dFormDataId, tableBody, fX8FormId, bloqFormId); 682 } 683 684 var containerForm = bloqFormId ? $(`#containerForm_${fX8FormId}`) : $(`#containerForm`); 685 containerForm.find("#flashMessage").html( 686 '<div class="custom-msg-success" onclick="this.classList.add(\'hidden\')">The donation has been added to the cart.</div>' 687 ); 688 hideFlash(); 689 690 updateTotals(bloqFormId); 691 } else { 692 // Add a span with an error message after the item container if it doesn't already exist 693 const errorMessage = document.createElement('span'); 694 errorMessage.className = 'error-message text-red-500 text-sm'; 695 errorMessage.textContent = `Please enter an amount greater than 0.`; 696 container.after(errorMessage); 697 } 698 } 699 700 // Find an existing row in the cart based on item name and price 701 function findExistingDonationRow(donationName, amount, tableBody) { 702 return [...tableBody.querySelectorAll('tr')].find(row => { 703 const rowItemName = row.querySelector('.itemNameCell').textContent; 704 return rowItemName === donationName; 705 }); 706 } 707} 708 709// Resets the styling of preset buttons (if a custom amount is selected) 710function resetPresetStyles(container) { 711 const presets = container.querySelectorAll('#preset1, #preset2, #preset3'); 712 713 presets.forEach(preset => { 714 preset.classList.remove('bg-blue-500', 'text-white'); 715 preset.classList.add('bg-gray-200', 'text-gray-700'); 716 }); 717} 718 719function reinitializeSignaturePad(element) { 720 const containers = element.querySelectorAll('.signature-container'); 721 722 containers.forEach(container => { 723 const canvas = container.querySelector('.signature-pad'); 724 if (!canvas) return; 725 726 const ctx = canvas.getContext('2d'); 727
728 // Resize canvas after visible 729 canvas.width = container.offsetWidth; 730 canvas.height = container.offsetHeight; 731 732 // Optional: clear old drawing issue 733 ctx.clearRect(0, 0, canvas.width, canvas.height); 734 }); 735} 736 737// Function to initialize signature pad 738function initializeSignaturePad() { 739 document.querySelectorAll('.signature-container').forEach((container, index) => { 740 const canvas = container.querySelector('.signature-pad'); 741 const clearBtn = container.querySelector('.clear-signature'); 742 743 let signatureInput = container.querySelector('.signature-data'); 744 745 // Add a hidden input field to store the signature data if NOT exists 746 if (!signatureInput) { 747 signatureInput = document.createElement('input'); 748 signatureInput.type = 'hidden'; 749 signatureInput.name = 'signature_' + index; 750 signatureInput.className = 'signature-data'; 751 752 if (container.getAttribute('data-required') === '1') { 753 signatureInput.setAttribute('data-required', '1'); 754 } 755 756 container.appendChild(signatureInput); 757 } 758 759 // Get context and set up signature pad for each canvas 760 const ctx = canvas.getContext('2d'); 761 762 // Function to set canvas dimensions based on container size 763 function resizeCanvas() { 764 const prevWidth = canvas.width; 765 const prevHeight = canvas.height; 766 767 let data = null; 768 if (prevWidth > 0 && prevHeight > 0) { 769 try { 770 data = ctx.getImageData(0, 0, prevWidth, prevHeight); 771 } catch (e) { 772 console.warn('Canvas data could not be copied:', e); 773 } 774 } 775 776 canvas.width = container.offsetWidth; 777 canvas.height = container.offsetHeight; 778 779 if (data) { 780 ctx.putImageData(data, 0, 0); 781 } 782 } 783 784 // Set the canvas dimensions immediately after initialization 785 resizeCanvas(); 786 787 // Adjust the canvas size and maintain drawn content when the window is resized 788 window.addEventListener('resize', resizeCanvas); 789 790 let isDrawing = false; 791 let hasSignature = false; 792 793 // Start drawing when the mouse is pressed down 794 canvas.addEventListener('mousedown', (event) => { 795 isDrawing = true; 796 hasSignature = true; 797 const { x, y } = getCanvasCoordinates(event, canvas); 798 ctx.beginPath(); 799 ctx.moveTo(x, y); // Use offsetX and offsetY for correct coordinates 800 }); 801 802 // Stop drawing when the mouse is released 803 canvas.addEventListener('mouseup', () => { 804 isDrawing = false; 805 ctx.closePath(); 806 if (hasSignature) { 807 updateSignatureData(); 808 } 809 }); 810 811 // Mouse leaves canvas 812 canvas.addEventListener('mouseleave', () => { 813 if (isDrawing) { 814 isDrawing = false; 815 ctx.closePath(); 816 if (hasSignature) { 817 updateSignatureData(); 818 } 819 } 820 }); 821 822 // Draw the signature on the canvas 823 canvas.addEventListener('mousemove', (event) => { 824 if (!isDrawing) return; 825 const { x, y } = getCanvasCoordinates(event, canvas); 826 ctx.lineWidth = 2; 827 ctx.lineCap = 'round'; 828 ctx.strokeStyle = 'black'; 829 ctx.lineTo(x, y); // Use offsetX and offsetY for correct coordinates 830 ctx.stroke(); 831 }); 832 833 // Touch events support for mobile devices 834 canvas.addEventListener('touchstart', (event) => { 835 event.preventDefault(); 836 hasSignature = true; 837 isDrawing = true; 838 const touch = event.touches[0]; 839 const rect = canvas.getBoundingClientRect(); 840 const scaleX = canvas.width / rect.width; 841 const scaleY = canvas.height / rect.height; 842 843 const touchX = (touch.clientX - rect.left) * scaleX; 844 const touchY = (touch.clientY - rect.top) * scaleY; 845 ctx.beginPath(); 846 ctx.moveTo(touchX, touchY); 847 }); 848 849 canvas.addEventListener('touchmove', (event) => { 850 event.preventDefault(); 851 if (!isDrawing) return; 852 const touch = event.touches[0]; 853 const rect = canvas.getBoundingClientRect(); 854 const scaleX = canvas.width / rect.width; 855 const scaleY = canvas.height / rect.height; 856 857 const touchX = (touch.clientX - rect.left) * scaleX; 858 const touchY = (touch.clientY - rect.top) * scaleY; 859 ctx.lineWidth = 2; 860 ctx.lineCap = 'round'; 861 ctx.strokeStyle = 'black'; 862 ctx.lineTo(touchX, touchY); 863 ctx.stroke(); 864 }); 865 866 canvas.addEventListener('touchend', (event) => { 867 event.preventDefault(); 868 isDrawing = false;
869 ctx.closePath(); 870 if (hasSignature) { 871 updateSignatureData(); 872 } 873 }); 874 875 // Clear the signature when the Clear button is clicked 876 clearBtn.addEventListener('click', () => { 877 ctx.clearRect(0, 0, canvas.width, canvas.height); 878 hasSignature = false; 879 signatureInput.value = ''; 880 }); 881 882 // Function to update the hidden input with the signature data 883 function updateSignatureData() { 884 const signatureData = canvas.toDataURL('image/png'); 885 signatureInput.value = signatureData; 886 } 887 888 function getCanvasCoordinates(event, canvas) { 889 const rect = canvas.getBoundingClientRect(); 890 891 const scaleX = canvas.width / rect.width; 892 const scaleY = canvas.height / rect.height; 893 894 return { 895 x: (event.clientX - rect.left) * scaleX, 896 y: (event.clientY - rect.top) * scaleY 897 }; 898 } 899 }); 900} 901 902// Function to convert base64 to Blob 903function dataURLtoBlob(dataURL) { 904 const parts = dataURL.split(';base64,'); 905 const contentType = parts[0].split(':')[1]; 906 const raw = window.atob(parts[1]); 907 const rawLength = raw.length; 908 const uInt8Array = new Uint8Array(rawLength); 909 910 for (let i = 0; i < rawLength; ++i) { 911 uInt8Array[i] = raw.charCodeAt(i); 912 } 913 914 return new Blob([uInt8Array], { type: contentType }); 915} 916 917// Function to initialize Ticket Card 918function initializeTicketCard(fX8FormId, bloqFormId) { 919 document.querySelectorAll('.item').forEach((item, index) => { 920 const select = item.querySelector('#itemOptions'); 921 const itemAmountElement = item.querySelector('.itemAmount'); 922 const quantityInput = item.querySelector('input[id="itemQuantity"]'); 923 const addButton = item.querySelector('button'); 924 const itemName = item.querySelector('#itemTitle'); 925 926 if (bloqFormId) { 927 var cartSection = document.getElementById(`cartSection_${bloqFormId}`); 928 var errorMsg = document.getElementById(`errorMsg_${bloqFormId}`); 929 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 930 } else { 931 var cartSection = document.getElementById('cartSection'); 932 var errorMsg = document.getElementById('errorMsg'); 933 var formPaymentContainer = $(`#formPaymentContainer`); 934 } 935 const tableBody = cartSection.querySelector('tbody'); 936 const tableBodyRows = cartSection.querySelectorAll('tbody tr'); 937 938 // Check if there are any rows in the cart (tbody) 939 if (cartSection) { 940 if (tableBodyRows.length === 0) { 941 cartSection.classList.add('hidden'); 942 errorMsg.classList.remove('hidden'); 943 // formPaymentContainer.find('#formPaymentForm #payNowBtn').prop('disabled', true); 944 } else { 945 cartSection.classList.remove('hidden'); 946 errorMsg.classList.add('hidden'); 947 // formPaymentContainer.find('#formPaymentForm #payNowBtn').prop('disabled', false); 948 } 949 } 950 951 // Set dynamic id for quantity input 952 if (quantityInput) { 953 quantityInput.setAttribute('id', `itemQuantity_${index}`); 954 955 // Get max value from the input's attribute 956 let maxQuantity = parseInt(quantityInput.getAttribute('max')) || Infinity; 957 958 const totalPriceElement = item.querySelector('#totalPrice'); 959 let availableQuantity = parseInt(totalPriceElement.textContent); 960 961 // Ensure quantity input does not exceed max value 962 quantityInput.addEventListener('input', function () { 963 let enteredValue = parseInt(this.value); 964 965 const errorMsgContainer = item.nextElementSibling; 966 // Check and remove the error message if it exists 967 if (errorMsgContainer && errorMsgContainer.classList.contains('error-message')) { 968 errorMsgContainer.remove(); 969 } 970 971 if (enteredValue > maxQuantity) { 972 this.value = maxQuantity; 973 974 const errorMessage = document.createElement('span'); 975 errorMessage.className = 'error-message text-red-500 text-sm'; 976 errorMessage.textContent = `This item is limited to ${maxQuantity} per transaction.`; 977 item.after(errorMessage); 978 } else if (enteredValue < 1 || isNaN(enteredValue)) { 979 this.value = 1; 980 } else if (enteredValue > availableQuantity) { 981 this.value = availableQuantity; 982 983 const errorMessage = document.createElement('span'); 984 errorMessage.className = 'error-message text-red-500 text-sm'; 985 errorMessage.textContent = `Only ${availableQuantity} items are available.`; 986 item.after(errorMessage); 987 } 988 }); 989 } 990 991 let currentPriceText = itemAmountElement.textContent; 992 var currentPrice = parseFloat(currentPriceText.replace(/[^0-9.
992-]+/g,"")); 993 994 // Attach event listener for select dropdown to update price and reset quantity 995 if (select && itemAmountElement) { 996 select.addEventListener('change', function() { 997 const selectedValue = parseFloat(this.value); // Convert to float for decimal handling 998 itemAmountElement.textContent = `$${selectedValue.toFixed(2)}`; 999 1000 if (quantityInput) { 1001 quantityInput.value = 1; // Reset quantity input to 1 1002 } 1003 1004 currentPrice = this.value; 1005 }); 1006 } 1007 1008 getAvailableQuantity(item, fX8FormId); 1009 getCartItemsSummary(item, fX8FormId, bloqFormId, 'first-time-load'); 1010 1011 // Attach event listener for add button 1012 if (addButton && quantityInput) { 1013 addButton.addEventListener('click', () => handleAddToCart(quantityInput, currentPrice, itemName, tableBody, item, fX8FormId, bloqFormId)); 1014 } 1015 }); 1016 1017 // Initialize events for rows that already exist in tbody 1018 initializeCartRowEvents(bloqFormId); 1019 1020 function getAvailableQuantity(item, fX8FormId) { 1021 1022 const isTaxableInput = item.querySelector('input[id="is_taxable"]'); 1023 1024 if (isTaxableInput) { 1025 const formDataId = isTaxableInput.getAttribute('data-form-data-id'); 1026 $.ajax({ 1027 type: "GET", 1028 url: `/bloqs/bloq-forms/get-available-quantity/${fX8FormId}/${formDataId}/${bloqFormId}`, 1029 success: function(response) { 1030 const totalPriceElement = item.querySelector('#totalPrice'); 1031 1032 if (response && typeof response.difference !== 'undefined') { 1033 if (totalPriceElement) { 1034 totalPriceElement.textContent = response.difference; 1035 } 1036 } 1037 }, 1038 error: function() { 1039 console.error("An error occurred while fetching available quantity"); 1040 } 1041 }); 1042 } 1043 } 1044 1045 // Handle adding item to the cart 1046 function handleAddToCart(quantityInput, currentPrice, itemName, tableBody, item, fX8FormId, bloqFormId) { 1047 const quantity = parseInt(quantityInput.value, 10); 1048 const itemPrice = parseFloat(currentPrice); 1049 var itemNameText = itemName.textContent; 1050 const isTaxableInput = item.querySelector('input[id="is_taxable"]'); 1051 const isTaxable = isTaxableInput.value; 1052 const formDataId = isTaxableInput.getAttribute('data-form-data-id'); 1053 const itemOptions = item.querySelector('#itemOptions'); 1054 1055 if (window.getComputedStyle(itemOptions).display === 'block') { 1056 const selectedText = itemOptions.options[itemOptions.selectedIndex].text; 1057 // itemNameText = `${itemName.textContent} (${selectedText})`; 1058 itemNameText = `${itemName.textContent}: ${selectedText}`; 1059 } 1060 1061 if (quantity > 0) { 1062 const existingRow = findExistingRow(itemNameText, itemPrice, tableBody); 1063 1064 if (existingRow) { 1065 updateExistingRow(existingRow, quantity); 1066 } else { 1067 // Check and remove the error message if it exists 1068 const errorMessage = item.nextElementSibling; 1069 if (errorMessage && errorMessage.classList.contains('error-message')) { 1070 errorMessage.remove(); 1071 } 1072 1073 createNewRow(itemNameText, itemPrice, quantity, isTaxable, formDataId, tableBody, fX8FormId, bloqFormId); 1074 } 1075 1076 var containerForm = bloqFormId ? $(`#containerForm_${fX8FormId}`) : $(`#containerForm`); 1077 containerForm.find("#flashMessage").html( 1078 '<div class="custom-msg-success" onclick="this.classList.add(\'hidden\')">The item has been added to the cart.</div>' 1079 ); 1080 hideFlash(); 1081 1082 quantityInput.value = 1; 1083 1084 updateTotals(bloqFormId); 1085 } else { 1086 swal("Something went wrong!", "Please enter a quantity greater than 0.") 1087 .then(() => { 1088 quantityInput.value = 1; 1089 }); 1090 } 1091 } 1092 1093 // Find an existing row in the cart based on item name and price 1094 function findExistingRow(itemNameText, itemPrice, tableBody) { 1095 return [...tableBody.querySelectorAll('tr')].find(row => { 1096 const rowItemName = row.querySelector('.itemNameCell').textContent; 1097 const rowItemPrice = parseFloat(row.querySelector('.itemPrice').textContent.replace('$', '')); 1098 return rowItemName === itemNameText && rowItemPrice === itemPrice; 1099 }); 1100 } 1101 1102 // Update the quantity and total for an existing row 1103 function updateExistingRow(existingRow, quantity) { 1104 const quantityInput = existingRow.querySelector('.quantityInput'); 1105 quantityInput.value = parseInt(quantityInput.value, 10) + quantity; 1106 updateRowTotal(existingRow); 1107 } 1108} 1109 1110// Create a new row in the cart 1111function createNewRow(itemName, itemPrice, quantity = null, isTaxable = 0, formDataId, tableBody, fX8FormId, bloqFormId) { 1112 const newRow = document.createElement('tr'); 1113 newRow.id = `cartRow-${formDataId}`; 1114 newRow.innerHTML = ` 1115 <td class="p-2 itemNameCell">${itemName}</td> 1116 <td class="p-2 itemPrice">$${itemPrice.toFixed(2)}</td> 1117 <td class="p-2"> 1118 ${quantity !== null 1119 ? `<input type="number" class="quantityInput w-16 !m-0 !py-1 input-standard" value="${quantity}" min="1">` 1120 : `<span>-</span>`} 1121 </td> 1122 <td class="p-2 itemTotal"> 1123 $${quantity !== null 1124 ? (itemPrice * quantity).toFixed(2) 1125 : itemPrice.toFixed(2)}</td> 1126 <td class="p-2 hidden isTaxableCell">${isTaxable}</td> 1127 <td class="p-2 hidden formDataId">${formDataId}</td> 1128 <td class="p-2 hidden fX8FormId">${fX8FormId}</td> 1129 <td> 1130 <button type="button" class="text-red-500 hover:text-red-700 removeRow">
1130<i class="fas fa-trash"></i></button> 1131 </td> 1132 `; 1133 1134 tableBody.appendChild(newRow); 1135 1136 // Initialize delete and quantity events for the new row 1137 initializeRowEvents(newRow, bloqFormId); 1138} 1139 1140// Initialize row events for existing rows 1141function initializeCartRowEvents(bloqFormId) { 1142 if (bloqFormId) { 1143 var cartSection = document.getElementById(`cartSection_${bloqFormId}`); 1144 } else { 1145 var cartSection = document.getElementById('cartSection'); 1146 } 1147 cartSection.querySelectorAll('tbody tr').forEach(row => { 1148 initializeRowEvents(row, bloqFormId); 1149 }); 1150} 1151 1152// Initialize delete and quantity update events for a specific row 1153function initializeRowEvents(row, bloqFormId) { 1154 const removeButton = row.querySelector('.removeRow'); 1155 const quantityInput = row.querySelector('.quantityInput'); 1156 1157 if (removeButton) { 1158 // Attach event listener to remove the row on click 1159 removeButton.addEventListener('click', () => { 1160 row.remove(); 1161 1162 const rowId = row.id; 1163 const errorRow = document.getElementById(`${rowId}-error`); 1164 1165 // Remove the error message row if it exists 1166 if (errorRow) { 1167 errorRow.remove(); 1168 } 1169 1170 updateTotals(bloqFormId); 1171 }); 1172 1173 if (quantityInput) { 1174 // Attach event listener to update the row total when quantity changes 1175 quantityInput.addEventListener('input', () => { 1176 // Ensure the quantity is not less than 1 and not zero 1177 if (quantityInput.value <= 0) { 1178 quantityInput.value = 1; 1179 } 1180 updateRowTotal(row); 1181 updateTotals(bloqFormId); 1182 }); 1183 } 1184 } 1185} 1186 1187// Update the total for a specific row 1188function updateRowTotal(row) { 1189 const itemPrice = parseFloat(row.querySelector('.itemPrice').textContent.replace('$', '')); 1190 // const quantity = parseInt(row.querySelector('.quantityInput').value, 10); 1191 // const total = (itemPrice * quantity).toFixed(2); 1192 const quantityInput = row.querySelector('.quantityInput'); 1193 const quantity = quantityInput ? parseInt(quantityInput.value, 10) : null; 1194 const total = (quantity !== null ? itemPrice * quantity : itemPrice).toFixed(2); 1195 row.querySelector('.itemTotal').textContent = `$${total}`; 1196} 1197 1198// Update the total amounts 1199function updateTotals(bloqFormId) { 1200 let totalAmount = 0; 1201 const cartItems = []; 1202 1203 if (bloqFormId) { 1204 var cartSection = document.getElementById(`cartSection_${bloqFormId}`); 1205 var errorMsg = document.getElementById(`errorMsg_${bloqFormId}`); 1206 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1207 } else { 1208 var cartSection = document.getElementById('cartSection'); 1209 var errorMsg = document.getElementById('errorMsg'); 1210 var formPaymentContainer = $(`#formPaymentContainer`); 1211 } 1212 const tableBodyRows = cartSection.querySelectorAll('tbody tr'); 1213 1214 // Check if there are any rows in the cart (tbody) 1215 if (tableBodyRows.length === 0) { 1216 cartSection.classList.add('hidden'); 1217 errorMsg.classList.remove('hidden'); 1218 // formPaymentContainer.find('#formPaymentForm #payNowBtn').prop('disabled', true); 1219 } else { 1220 cartSection.classList.remove('hidden'); 1221 errorMsg.classList.add('hidden'); 1222 // formPaymentContainer.find('#formPaymentForm #payNowBtn').prop('disabled', false); 1223 } 1224 1225 tableBodyRows.forEach(row => { 1226 if (row.querySelector('.itemNameCell')) { 1227 const itemName = row.querySelector('.itemNameCell').textContent; 1228 const itemPrice = parseFloat(row.querySelector('.itemPrice').textContent.replace('$', '')); 1229 // const quantity = parseInt(row.querySelector('.quantityInput').value, 10); 1230 const quantityInput = row.querySelector('.quantityInput'); 1231 const quantity = quantityInput ? parseInt(quantityInput.value, 10) : null; 1232 1233 const total = parseFloat(row.querySelector('.itemTotal').textContent.replace('$', '')); 1234 const isTaxableValue = row.querySelector('.isTaxableCell').textContent; 1235 const isTaxable = isTaxableValue == 1 || isTaxableValue === 'true'; 1236 const formDataId = row.querySelector('.formDataId').textContent; 1237 const fX8FormId = row.querySelector('.fX8FormId').textContent; 1238 1239 totalAmount += total; 1240 1241 // Add item details to the cart array 1242 cartItems.push({ 1243 form_data_id: formDataId, 1244 name: itemName, 1245 price: itemPrice.toFixed(2), 1246 qty: quantity, 1247 is_taxable: isTaxable, 1248 form_id: fX8FormId 1249 }); 1250 } 1251 }); 1252 1253 cartSection.querySelector('.subTotal').textContent = `$${totalAmount.toFixed(2)}`; 1254 1255 // Prepare the cart data to be sent to the server 1256 const cartData = { 1257 items: cartItems 1258 }; 1259 1260 const fX8FormId = cartItems.length > 0 ? cartItems[0].form_id : null; 1261 1262 // Make the AJAX call to save cart data into the session 1263 $.ajax({ 1264 url: '/bloqs/bloq-forms/save-cart-items', 1265 method: 'POST', 1266 data: { 1267 bloq_form_id: bloqFormId, 1268 form_id: cartItems.length > 0 ? cartItems[0].form_id : null, 1269 cart: cartData 1270 }, 1271 headers: { 1272 'X-CSRF-Token': csrfToken 1273 }, 1274 success: function(response) { 1275 console.log('Cart saved successfully'); 1276 1277 document.querySelectorAll('.item').forEach((item, index) => { 1278 getCartItemsSummary(item, fX8FormId, bloqFormId); 1279 }); 1280 1281 if (bloqFormId) { 1282 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1283 } else { 1284 var formPaymentContainer = $(`#formPaymentContainer`); 1285 } 1286 1287 const couponInput = formPaymentContainer.find('input[name="coupon_code"]'); 1288 1289 if (couponInput.length > 0) { 1290 const couponCode = couponInput.val().trim(); 1291 1292 if (couponCode !== '') { 1293 checkCoupon(fX8FormId, bloqFormId); 1294 } 1295 } 1296 1297 const cartSessionData = response.cart_items; 1298 1299 formPaymentContainer.find('#formPaymentForm input[name="cart_data"]').val(cartSessionData); 1300 1301 // Parse cartSessionData to check differences 1302 const cartData = cartSessionData ? JSON.parse(cartSessionData) : null; 1303 1304 let hasNegativeDifference = false;
1305 1306 if (cartData && cartData.items) { 1307 hasNegativeDifference = cartData.items.some(item => item.difference <= 0); 1308 } 1309 1310 // Hide or show the Pay Now button and message 1311 if (hasNegativeDifference) { 1312 formPaymentContainer.find('#formPaymentForm #payNowBtn').hide(); 1313 formPaymentContainer.find('#formPaymentForm #payNowMessage').removeClass('hidden'); 1314 } else { 1315 formPaymentContainer.find('#formPaymentForm #payNowBtn').show(); 1316 formPaymentContainer.find('#formPaymentForm #payNowMessage').addClass('hidden'); 1317 } 1318 1319 var zipCodeValue = formPaymentContainer.find('#formPaymentForm #zipCode').val(); 1320 if (zipCodeValue.length > 0) { 1321 fetchSalesTax(zipCodeValue, bloqFormId); 1322 } else { 1323 updateSalesTax(0,bloqFormId); 1324 } 1325 }, 1326 error: function(xhr, status, error) { 1327 console.error('Error saving cart', error); 1328 } 1329 }); 1330} 1331 1332function getCartItemsSummary(item, fX8FormId, bloqFormId, action = null) { 1333 const isTaxableInput = item.querySelector('input[id="is_taxable"]'); 1334 const cartSummaryElement = item.querySelector('.cart-summary'); 1335 1336 if (isTaxableInput) { 1337 1338 const formDataId = isTaxableInput.getAttribute('data-form-data-id'); 1339 $.ajax({ 1340 type: "GET", 1341 url: `/bloqs/bloq-forms/get-cart-items-summary/${fX8FormId}/${formDataId}/${bloqFormId}`, 1342 success: function(response) { 1343 if (cartSummaryElement) { 1344 cartSummaryElement.innerHTML = ''; 1345 } 1346 1347 // Cart status for this item 1348 if (response.cart_status && Array.isArray(response.cart_status) && response.cart_status.length > 0) { 1349 let cart_data = ` 1350 <table class="w-full text-xs mt-4"> 1351 <thead class="bg-gray-50"> 1352 <tr> 1353 <th class="text-left">Product</th> 1354 <th class="text-left">Price</th> 1355 <th class="text-left">Qty</th> 1356 <th class="text-left">Total</th> 1357 <th></th> 1358 </tr> 1359 </thead> 1360 <tbody>`; 1361 1362 response.cart_status.forEach(item => { 1363 cart_data += ` 1364 <tr id="cartRow-${item.form_data_id}"> 1365 <td class="">${item.name}</td> 1366 <td class="">$${item.price}</td> 1367 <td class="">${item.qty}</td> 1368 <td class="">$${(item.price * item.qty).toFixed(2)}</td> 1369 <td class=""> 1370 <button type="button" class="text-red-500 hover:text-red-700 removeRow"><i class="fas fa-trash"></i></button> 1371 </td> 1372 </tr>`; 1373 }); 1374 1375 cart_data += `</tbody></table>`; 1376 1377 // Create a div element and insert the table HTML 1378 const cartElement = document.createElement("div"); 1379 cartElement.innerHTML = cart_data; 1380 1381 if (cartSummaryElement) { 1382 // cartSummaryElement.innerHTML = ''; 1383 cartSummaryElement.appendChild(cartElement); 1384 1385 cartSummaryElement.querySelectorAll('tbody tr').forEach(row => { 1386 const removeButton = row.querySelector('.removeRow'); 1387 1388 if (removeButton) { 1389 removeButton.addEventListener('click', () => { 1390 const rowId = row.id; 1391 row.remove(); 1392 1393 const cartSection = bloqFormId 1394 ? document.getElementById(`cartSection_${bloqFormId}`) 1395 : document.getElementById('cartSection'); 1396 1397 if (cartSection) { 1398 const matchingRow = cartSection.querySelector(`tbody tr#${rowId}`); 1399 if (matchingRow) { 1400 matchingRow.remove(); 1401 1402 updateTotals(bloqFormId); 1403 } 1404 } 1405 1406 // Check if there are any remaining rows in the cart summary 1407 if (cartSummaryElement.querySelectorAll('tbody tr').length === 0) { 1408 cartSummaryElement.innerHTML = ''; 1409 } 1410 }); 1411 } 1412 }); 1413 } 1414 1415 if (action) { 1416 const cartSection = bloqFormId 1417 ? document.getElementById(`cartSection_${bloqFormId}`) 1418 : document.getElementById('cartSection'); 1419 1420 if (!cartSection) return; 1421 1422 const tableBody = cartSection.querySelector('tbody'); 1423
1424 response.cart_status.forEach(cartItem => { 1425 const itemName = cartItem.name; 1426 const itemPrice = parseFloat(cartItem.price); 1427 const quantity = parseInt(cartItem.qty, 10); 1428 const isTaxable = cartItem.is_taxable; 1429 const formDataId = cartItem.form_data_id; 1430 1431 // Prevent duplicate rows 1432 const existingRow = tableBody.querySelector(`#cartRow-${formDataId}`); 1433 if (existingRow) return; 1434 1435 // Reuse existing function 1436 createNewRow(itemName, itemPrice, quantity, isTaxable, formDataId, tableBody, fX8FormId, bloqFormId); 1437 }); 1438 1439 // Update totals once after all rows added 1440 updateTotals(bloqFormId); 1441 1442 const cartSessionData = { 1443 items: response.cart_status 1444 }; 1445 1446 if (bloqFormId) { 1447 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1448 } else { 1449 var formPaymentContainer = $(`#formPaymentContainer`); 1450 } 1451 1452 formPaymentContainer.find('#formPaymentForm input[name="cart_data"]').val(JSON.stringify(cartSessionData)); 1453 } 1454 } 1455 1456 1457 }, 1458 error: function() { 1459 console.error("An error occurred while fetching available quantity"); 1460 } 1461 }); 1462 } 1463} 1464 1465function checkRequiredItems(targetForm, dataString, formId, type = null) { 1466 var formPaymentContainer = $(`#formPaymentContainer`); 1467 const cartSessionData = formPaymentContainer.find('#formPaymentForm input[name="cart_data"]').val(); 1468 1469 if ($('input[name="is_required"]').length > 0) { 1470 const requiredItems = []; 1471 const requiredFields = $('input[name="is_required"]').filter(function() { 1472 // return $(this).val() === '1'; 1473 const isRequired = $(this).val() === '1'; 1474 1475 const closestRow = $(this).closest('.form-row, [id^="row-"]'); 1476 const isRowHidden = closestRow.hasClass('hidden'); 1477 1478 return isRequired && !isRowHidden; 1479 }); 1480 1481 if (requiredFields.length > 0) { 1482 requiredFields.each(function() { 1483 const isRequired = $(this).val(); 1484 const formDataId = $(this).attr('data-form-data-id'); 1485 1486 // Add item details to the required items array 1487 requiredItems.push({ 1488 form_data_id: formDataId, 1489 is_required: isRequired 1490 }); 1491 }); 1492 1493 $.ajax({ 1494 url: '/bloqs/bloq-forms/check-required-items', 1495 method: 'POST', 1496 data: { 1497 required_items: requiredItems, 1498 form_id: formId, 1499 is_bloq_form_id: false 1500 }, 1501 headers: { 1502 'X-CSRF-Token': csrfToken 1503 }, 1504 success: function(response) { 1505 if (response.status == "success") { 1506 // togglePaymentFormslider(dataString); 1507 if (type == 'viewForm') { 1508 if (cartSessionData) { 1509 togglePaymentFormContainer(targetForm, dataString, null, formId); 1510 } else { 1511 saveBloqForm(targetForm, dataString, formId); 1512 } 1513 } else if (type == 'previewForm') { 1514 if (cartSessionData) { 1515 togglePaymentFormContainer(targetForm, dataString, null, formId, 'previewForm'); 1516 } else { 1517 formPaymentContainer.html(`<div class="mx-16 my-8 flex flex-col justify-center items-center"><div>Your Transaction Id: XXXXXXXX999 </div> 1518 <div>You can see your transaction <a href="/payments/plans" class="font-bold text-blue-600"> Here</a></div></div>`); 1519 1520 $(`#containerForm`).addClass('hidden'); 1521 $(`#thankYouContainerForm`).removeClass('hidden'); 1522 } 1523 } 1524 } else { 1525 const missingItems = response.missing_items;
1526 missingItems.forEach(function(formDataId) { 1527 const requiredInput = $(`input[name="is_required"][data-form-data-id="${formDataId}"]`); 1528 const itemContainer = requiredInput.closest('.item'); 1529 1530 // Add a span with an error message after the item container if it doesn't already exist 1531 if (itemContainer.next('.error-message').length === 0) { 1532 itemContainer.after('<span class="error-message text-red-500 text-sm">This item is required and must be added to the cart.</span>'); 1533 } 1534 }); 1535 } 1536 }, 1537 error: function(xhr, status, error) { 1538 console.error('Error saving cart', error); 1539 } 1540 }); 1541 } else { 1542 // togglePaymentFormslider(dataString); 1543 if (type == 'viewForm') { 1544 if (cartSessionData) { 1545 togglePaymentFormContainer(targetForm, dataString, null, formId); 1546 } else { 1547 saveBloqForm(targetForm, dataString, formId); 1548 } 1549 } else if (type == 'previewForm') { 1550 if (cartSessionData) { 1551 togglePaymentFormContainer(targetForm, dataString, null, formId, 'previewForm'); 1552 } else { 1553 formPaymentContainer.html(`<div class="mx-16 my-8 flex flex-col justify-center items-center"><div>Your Transaction Id: XXXXXXXX999 </div> 1554 <div>You can see your transaction <a href="/payments/plans" class="font-bold text-blue-600"> Here</a></div></div>`); 1555 1556 $(`#containerForm`).addClass('hidden'); 1557 $(`#thankYouContainerForm`).removeClass('hidden'); 1558 } 1559 } 1560 } 1561 } else { 1562 // togglePaymentFormslider(dataString); 1563 if (type == 'viewForm') { 1564 if (cartSessionData) { 1565 togglePaymentFormContainer(targetForm, dataString, null, formId); 1566 } else { 1567 saveBloqForm(targetForm, dataString, formId); 1568 } 1569 } else if (type == 'previewForm') { 1570 if (cartSessionData) { 1571 togglePaymentFormContainer(targetForm, dataString, null, formId, 'previewForm'); 1572 } else { 1573 formPaymentContainer.html(`<div class="mx-16 my-8 flex flex-col justify-center items-center"><div>Your Transaction Id: XXXXXXXX999 </div> 1574 <div>You can see your transaction <a href="/payments/plans" class="font-bold text-blue-600"> Here</a></div></div>`); 1575 1576 $(`#containerForm`).addClass('hidden'); 1577 $(`#thankYouContainerForm`).removeClass('hidden'); 1578 } 1579 } 1580 } 1581} 1582 1583function togglePaymentFormContainer(targetForm, dataString, bloqFormId, formId, type = null) { 1584 if (!validateTotalFileSize(targetForm)) { 1585 return false; 1586 } 1587 1588 if (bloqFormId) { 1589 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1590 $(`#containerForm_${bloqFormId}`).addClass('hidden'); 1591 } else { 1592 var formPaymentContainer = $(`#formPaymentContainer`); 1593 $(`#containerForm`).addClass('hidden'); 1594 } 1595 const formData = getFormPostData(targetForm); 1596 displayFormData(formData,formPaymentContainer); 1597 1598 const isSignatureValid = validateSignatureFields(targetForm); 1599 1600 if (!isSignatureValid) { 1601 if (bloqFormId) { 1602 $(`#containerForm_${bloqFormId}`).removeClass('hidden'); 1603 } else { 1604 $(`#containerForm`).removeClass('hidden'); 1605 } 1606 return false; 1607 } 1608 1609 formPaymentContainer.removeClass('hidden'); 1610 1611 $('html, body').animate({ 1612 scrollTop: formPaymentContainer.offset().top 1613 }, 100); 1614 1615 formPaymentContainer.find("#formErrors").html(''); 1616 1617 if (dataString) { 1618 formPaymentContainer.find('#formPaymentForm input[name="post_data"]').val(JSON.stringify(dataString)); 1619 1620 let totalAmount = formPaymentContainer.find('#itemSummary .totalAmount').text(); 1621 formPaymentContainer.find('#formPaymentForm #payNowBtn').text(`Pay Now (${totalAmount})`); 1622 formPaymentContainer.find('#formPaymentForm input[name="total"]').val(`${totalAmount}`); 1623 1624 const cartSessionData = formPaymentContainer.find('#formPaymentForm input[name="cart_data"]').val(); 1625 1626 const cartData = cartSessionData ? JSON.parse(cartSessionData) : null; 1627 1628 let hasNegativeDifference = false;
1629 1630 if (cartData && cartData.items) { 1631 hasNegativeDifference = cartData.items.some(item => item.difference <= 0); 1632 } 1633 1634 if (hasNegativeDifference) { 1635 formPaymentContainer.find('#formPaymentForm #payNowBtn').hide(); 1636 formPaymentContainer.find('#formPaymentForm #payNowMessage').removeClass('hidden'); 1637 } else { 1638 formPaymentContainer.find('#formPaymentForm #payNowBtn').show(); 1639 formPaymentContainer.find('#formPaymentForm #payNowMessage').addClass('hidden'); 1640 } 1641 1642 var zipCodeValue = formPaymentContainer.find('#formPaymentForm #zipCode').val(); 1643 if (zipCodeValue.length > 0) { 1644 fetchSalesTax(zipCodeValue, bloqFormId); 1645 } else { 1646 updateSalesTax(0, bloqFormId); 1647 } 1648 } 1649 1650 formPaymentContainer.find('#formPaymentForm #zipCode').blur(function() { 1651 var zipcode = $(this).val(); 1652 1653 if (zipcode.length > 0) { 1654 fetchSalesTax(zipcode, bloqFormId); 1655 } else { 1656 updateSalesTax(0, bloqFormId); 1657 } 1658 }); 1659 1660 formPaymentContainer.find('#formPaymentForm #cvv').inputmask('999'); 1661 formPaymentContainer.find('#formPaymentForm #cardNumber').inputmask("9999 9999 9999 9999"); 1662 formPaymentContainer.find('#formPaymentForm #phone').inputmask("(999) 999-9999"); 1663 formPaymentContainer.find('#formPaymentForm #zipCode').inputmask("99999"); 1664 1665 formPaymentContainer.find('#formPaymentForm #expiry').inputmask('99/99', { 1666 clearIncomplete: true, 1667 onincomplete: () => formPaymentContainer.find('#formPaymentForm #expiry-error').addClass('hidden') 1668 }); 1669 1670 formPaymentContainer.find('#formPaymentForm #expiry').on('input', function () {
1671 const month = $(this).val().substring(0, 2).replace(/\D/g, ''); 1672 1673 // Auto-format single digit months to two digits 1674 if (month.length === 1 && month > 1) { 1675 $(this).val('0' + month); 1676 } 1677 1678 if (month.length === 2) { 1679 const monthNum = parseInt(month, 10); 1680 formPaymentContainer.find('#formPaymentForm #expiry-error').toggleClass('hidden', monthNum >= 1 && monthNum <= 12); 1681 } 1682 }); 1683 1684 formPaymentContainer.find('#applyBtn').off('click').on('click', function () { 1685 checkCoupon(formId, bloqFormId); 1686 }); 1687 1688 formPaymentContainer.find('#formPaymentForm').off('submit').on('submit', function(e) { 1689 e.preventDefault(); 1690 var dataString = $(this).serialize(); 1691 1692 if (type) { 1693 formPaymentContainer.html(`<div class="mx-16 my-8 flex flex-col justify-center items-center"><div>Your Transaction Id: XXXXXXXX999 </div> 1694 <div>You can see your transaction <a href="/payments/plans" class="font-bold text-blue-600"> Here</a></div></div>`); 1695 1696 if (bloqFormId) { 1697 $(`#containerForm_${bloqFormId}`).addClass('hidden'); 1698 $(`#thankYouContainerForm_${bloqFormId}`).removeClass('hidden'); 1699 } else { 1700 $(`#containerForm`).addClass('hidden'); 1701 $(`#thankYouContainerForm`).removeClass('hidden'); 1702 } 1703 } else { 1704 const newFormData = new FormData(); 1705 1706 // Append form data 1707 newFormData.append('form_data', JSON.stringify(dataString)); 1708 1709 // Append files separately 1710 targetForm.find('input[type="file"]').each(function () { 1711 const files = this.files; 1712 const fieldName = $(this).attr("name"); 1713 1714 // Check if the input has the 'multiple' attribute 1715 const isMultiple = $(this).attr("multiple") !== undefined; 1716 1717 for (let i = 0; i < files.length; i++) { 1718 // If it's a single file upload, enforce array format manually 1719 const formattedFieldName = isMultiple ? `${fieldName}[]` : `${fieldName}[][]`; 1720 1721 newFormData.append(formattedFieldName, files[i]); // Ensure uniform structure 1722 } 1723 }); 1724 1725 // Add signature data as files 1726 targetForm.find('.signature-data').each(function(index) { 1727 const signatureData = $(this).val(); 1728 if (signatureData) { 1729 // Convert base64 signature to Blob 1730 const signatureBlob = dataURLtoBlob(signatureData); 1731 // Create a file from the blob 1732 const signatureFile = new File([signatureBlob], `signature_${index}.png`, { type: 'image/png' }); 1733 // Append to FormData 1734 newFormData.append('signature[][]', signatureFile); 1735 } 1736 }); 1737 1738 let isAnyCheckboxChecked = false; 1739 targetForm.find('input[name^="subscribe"]').each(function(index) { 1740 const checkbox = $(this); 1741 1742 if (checkbox.is(':checkbox') && checkbox.is(':checked')) { 1743 isAnyCheckboxChecked = true; 1744 } 1745 }); 1746 1747 if (isAnyCheckboxChecked) { 1748 newFormData.append('subscribe_checked', 'yes'); 1749 } 1750 1751 $.ajax({ 1752 type: "POST", 1753 url: $('#formPaymentPayFormArea #formPaymentForm').attr('action'), 1754 // data: dataString, 1755 data: newFormData, 1756 processData: false, 1757 contentType: false, 1758 headers: { 1759 'X-CSRF-Token': $('meta[name="csrfToken"]').attr('content') 1760 }, 1761 beforeSend: function () { 1762 const submitBtn = formPaymentContainer.find("#payNowBtn"); 1763 submitBtn.prop('disabled', true); 1764 submitBtn.html(`<svg aria-hidden="true" role="status" class="inline w-4 h-4 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> 1765 <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"></path> 1766 <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.
17661071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"></path> 1767 </svg>`); 1768 }, 1769 dataType: "json", 1770 success: function(response) { 1771 1772 if(response.status == 'success'){ 1773 formPaymentContainer.html(`<div class="mx-16 my-8 flex flex-col justify-center items-center"><div>Your Transaction Id: ${response.transaction_id} </div> 1774 <div>You can see your transaction <a href="/payments/plans" class="font-bold text-blue-600"> Here</a></div></div>`); 1775 1776 if (bloqFormId) { 1777 $(`#containerForm_${bloqFormId}`).addClass('hidden'); 1778 $(`#thankYouContainerForm_${bloqFormId}`).removeClass('hidden'); 1779 } else { 1780 $(`#containerForm`).addClass('hidden'); 1781 $(`#thankYouContainerForm`).removeClass('hidden'); 1782 } 1783 }else{ 1784 formPaymentContainer.find("#formErrors").html(response.message); 1785 var payAmount = $("#formPaymentForm input[name='total']").val(); 1786 formPaymentContainer.find("#formPaymentForm #payNowBtn").html('Pay Now ($' + payAmount + ')'); 1787 const submitBtn = formPaymentContainer.find('#payNowBtn'); 1788 submitBtn.prop('disabled', false); 1789 } 1790 } 1791 }); 1792 } 1793 }); 1794} 1795 1796function checkCoupon(formId, bloqFormId) { 1797 if (bloqFormId) { 1798 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1799 } else { 1800 var formPaymentContainer = $(`#formPaymentContainer`); 1801 } 1802 1803 let couponInput = formPaymentContainer.find('input[name="coupon_code"]'); 1804 let couponCode = couponInput.val().trim(); 1805 let errorMsg = formPaymentContainer.find('#couponErrorMsg'); 1806 1807 if (couponCode === '') { 1808 errorMsg.text('Please enter a coupon code.').removeClass('hidden'); 1809 return; 1810 } else { 1811 errorMsg.addClass('hidden'); 1812 } 1813 1814 1815 $.ajax({ 1816 url: '/bloqs/bloq-forms/check-coupon', 1817 type: 'POST', 1818 data: { 1819 coupon_code: couponCode, 1820 form_id: formId 1821 }, 1822 headers: { 1823 'X-CSRF-Token': $('meta[name="csrfToken"]').attr('content') 1824 }, 1825 success: function (response) { 1826 if (response.status == 'success') { 1827 formPaymentContainer.find('#formPaymentForm input[name="coupon_id"]').val(response.coupon.id); 1828 1829 errorMsg.addClass('hidden'); 1830 formPaymentContainer.find('#applyBtn').addClass('hidden'); 1831 formPaymentContainer.find('#removeCoupon').removeClass('hidden'); 1832 formPaymentContainer.find('#discountContainer').removeClass('hidden'); 1833 1834 couponInput.attr('readonly', true); 1835 1836 const subTotalElement = formPaymentContainer.find('#itemSummary .subTotal'); 1837 const currentSubTotalAmount = parseFloat(subTotalElement.text().replace('$', '')); 1838 1839 var couponText = `Discount`; 1840 var discountAmount = response.coupon.value; 1841 1842 if (response.coupon.type == 'percent') { 1843 couponText = `Discount (${response.coupon.value}%)`; 1844 discountAmount = (currentSubTotalAmount * response.coupon.value) / 100; 1845 } 1846 1847 formPaymentContainer.find('.discountText').text(couponText); 1848 formPaymentContainer.find('.discountPrice').text(`- $${discountAmount.toFixed(2)}`); 1849 1850 calculateTax(bloqFormId); 1851 1852 formPaymentContainer.find('#removeCoupon').off('click').on('click', function () { 1853 removeCoupon(discountAmount, bloqFormId); 1854 }); 1855 } else { 1856 errorMsg.text(response.message).removeClass('hidden'); 1857 } 1858 }, 1859 error: function () { 1860 errorMsg.text('Something went wrong. Please try again.').removeClass('hidden'); 1861 } 1862 }); 1863} 1864 1865function removeCoupon(discountAmount, bloqFormId) { 1866 if (bloqFormId) { 1867 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1868 } else { 1869 var formPaymentContainer = $(`#formPaymentContainer`); 1870 } 1871 1872 formPaymentContainer.find('input[name="coupon_code"]').val(''); 1873 formPaymentContainer.find('#discountContainer').addClass('hidden'); 1874 formPaymentContainer.find('.discountText').text('Discount'); 1875 formPaymentContainer.find('.discountPrice').text(''); 1876 formPaymentContainer.find('#formPaymentForm input[name="coupon_id"]').val(''); 1877 1878 const totalAmountElement = formPaymentContainer.find('#itemSummary .totalAmount'); 1879 const currentTotalAmount = parseFloat(totalAmountElement.text().replace('$', '')); 1880 const originalTotalAmount = (currentTotalAmount + discountAmount); 1881 1882 totalAmountElement.text(`$${originalTotalAmount.toFixed(2)}`); 1883 1884 formPaymentContainer.find('#formPaymentForm #discountAmount').text('').parent().addClass('hidden'); 1885 formPaymentContainer.find('#applyBtn').removeClass('hidden'); 1886 formPaymentContainer.find('#removeCoupon').addClass('hidden'); 1887 1888 formPaymentContainer.find('input[name="coupon_code"]').removeAttr('readonly'); 1889 1890 calculateTax(bloqFormId); 1891} 1892 1893function fetchSalesTax(zipcode, bloqFormId) { 1894 $.ajax({ 1895 url: '/payments/getsalesTax/' + zipcode, 1896 method: 'GET', 1897 dataType: "json", 1898 success: function(response) { 1899 if (response.status == 'success' && response.tax_data) { 1900 salesTax = response.tax_data.total_rate; 1901 1902 if (bloqFormId) { 1903 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1904 } else { 1905 var formPaymentContainer = $(`#formPaymentContainer`); 1906 } 1907 formPaymentContainer.find('#formPaymentForm input[name="tax_data"]').val(JSON.stringify(response)); 1908 updateSalesTax(salesTax, bloqFormId); 1909 } else { 1910 updateSalesTax(0,bloqFormId); 1911 } 1912 }, 1913 error: function(xhr, status, error) { 1914 console.log("Error: " + error); 1915 updateSalesTax(0, bloqFormId); 1916 } 1917 }); 1918} 1919 1920function updateSalesTax(taxRate, bloqFormId) {
1921 salesTax = taxRate; 1922 const salesTaxPercent = (salesTax * 100).toFixed(2); 1923 1924 if (bloqFormId) { 1925 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1926 } else { 1927 var formPaymentContainer = $(`#formPaymentContainer`); 1928 } 1929 formPaymentContainer.find("#itemSummary .taxRate").text(`(${salesTaxPercent}%)`); 1930 calculateTax(bloqFormId); 1931} 1932 1933function calculateTax(bloqFormId) { 1934 if (bloqFormId) { 1935 var formPaymentContainer = $(`#formPaymentContainer_${bloqFormId}`); 1936 } else { 1937 var formPaymentContainer = $(`#formPaymentContainer`); 1938 } 1939 const itemSummary = formPaymentContainer.find('#itemSummary'); 1940 const tableRows = itemSummary.find('tbody tr'); 1941 let taxAmount = 0.00; 1942 1943 // Get Transaction Fee Settings 1944 const transactionFeeElement = formPaymentContainer.find('#showTransactionFee'); 1945 1946 let includeTransactionFee = false; 1947 let transactionFeePercent = 0; 1948 1949 if (transactionFeeElement.length > 0) { 1950 includeTransactionFee = transactionFeeElement.data('include') == 1; 1951 transactionFeePercent = parseFloat(transactionFeeElement.data('fee-percent')) || 0; 1952 } 1953 1954 tableRows.each(function() { 1955 const row = $(this); 1956 const totalElement = row.find('.itemTotal'); 1957 const isTaxableCell = row.find('.isTaxableCell'); 1958 1959 if (totalElement && isTaxableCell) { 1960 const total = parseFloat(totalElement.text().replace('$', '')); 1961 const isTaxableValue = isTaxableCell.text().trim(); 1962 const isTaxable = isTaxableValue == 1 || isTaxableValue === 'true'; 1963 1964 if (isTaxable && salesTax > 0) { 1965 taxAmount += total * salesTax; 1966 } 1967 } 1968 }); 1969 1970 taxAmount = parseFloat(taxAmount.toFixed(2)); 1971 formPaymentContainer.find("#itemSummary .taxAmount").text(`$${taxAmount.toFixed(2)}`); 1972 formPaymentContainer.find('#formPaymentForm input[name="tax"]').val(taxAmount); 1973 1974 const subTotalElement = itemSummary.find('.subTotal'); 1975 const totalAmountElement = itemSummary.find('.totalAmount'); 1976 const currentSubTotalAmount = parseFloat(subTotalElement.text().replace('$', '')); 1977 1978 // var newTotalAmount = (currentSubTotalAmount + taxAmount).toFixed(2); 1979 var newTotalAmount = currentSubTotalAmount; 1980 1981 const discountPriceElement = itemSummary.find('.discountPrice'); 1982 if (discountPriceElement.length !== 0 && discountPriceElement.text().trim() !== '') { 1983 const currentdiscountPriceAmount = parseFloat(discountPriceElement.text().replace('- $', '')); 1984 formPaymentContainer.find('#formPaymentForm #discountAmount').text(discountPriceElement.text()).parent().removeClass('hidden'); 1985 1986 const totalAmountAfterDiscount = (currentSubTotalAmount - currentdiscountPriceAmount); 1987 // newTotalAmount = (totalAmountAfterDiscount + taxAmount).toFixed(2); 1988 newTotalAmount = totalAmountAfterDiscount; 1989 1990 if (newTotalAmount < 1) { 1991 // Remove the coupon and show error 1992 removeCoupon(currentdiscountPriceAmount, bloqFormId); 1993 formPaymentContainer.find('#couponErrorMsg').text('This coupon cannot be applied as it would reduce the total amount below $1.00.').removeClass('hidden'); 1994 return; 1995 } 1996 } 1997 1998 newTotalAmount = newTotalAmount + taxAmount; 1999 let summaryTotalAmount = newTotalAmount + taxAmount; 2000 // Apply Transaction Fee 2001 let transactionFeeAmount = 0; 2002 2003 if (includeTransactionFee && transactionFeePercent > 0) { 2004 transactionFeeAmount = (newTotalAmount * transactionFeePercent) / 100; 2005 } 2006 2007 newTotalAmount = newTotalAmount + transactionFeeAmount; 2008 2009 newTotalAmount = parseFloat(newTotalAmount.toFixed(2)); 2010 summaryTotalAmount = parseFloat(summaryTotalAmount.toFixed(2)); 2011 transactionFeeAmount = parseFloat(transactionFeeAmount.toFixed(2)); 2012 2013 if (includeTransactionFee && transactionFeePercent > 0) { 2014 transactionFeeElement.removeClass('hidden'); 2015 formPaymentContainer.find('#TransactionFeeTotal') 2016 .text(`$${transactionFeeAmount.toFixed(2)}`); 2017 } else { 2018 transactionFeeElement.addClass('hidden'); 2019 } 2020 2021 totalAmountElement.text(`$${summaryTotalAmount}`); 2022 2023 formPaymentContainer.find('#formPaymentForm #subTotal').text(`$${currentSubTotalAmount.toFixed(2)}`); 2024 formPaymentContainer.find('#formPaymentForm #totalTax').text(`$${taxAmount}`); 2025 formPaymentContainer.find('#formPaymentForm #totalAmount').text(`$${newTotalAmount}`); 2026 2027 formPaymentContainer.find('#formPaymentForm #payNowBtn').text(`Pay Now ($${newTotalAmount})`); 2028 formPaymentContainer.find('#formPaymentForm input[name="total"]').val(newTotalAmount); 2029} 2030 2031/* ------------------------------------------------------------------ 2032 * Anti-abuse helpers for Form Builder submissions. 2033 * The server (BloqFormsController::saveFormInfo) requires: 2034 * - form_token : signed token issued when the form page was rendered 2035 * - recaptcha_token : reCAPTCHA Enterprise token (only when a site key is configured) 2036 * Templates expose window.bloqsFormTokens[formId] and window.bloqsRecaptchaSiteKey. 2037 * ------------------------------------------------------------------ */ 2038const BLOQS_FORM_RECAPTCHA_ACTION = 'form_builder_submit'; 2039 2040function bloqsGetFormToken(formId) { 2041 if (window.bloqsFormTokens && formId && window.bloqsFormTokens[formId]) { 2042 return window.bloqsFormTokens[formId]; 2043 } 2044 return ''; 2045} 2046 2047function bloqsGetFormIdFromAction(targetForm) { 2048 const action = (targetForm && targetForm.attr) ? (targetForm.attr('action') || '') : ''; 2049 const match = action.match(/save-form-info\/(\d+)/); 2050 return match ? match[1] : null; 2051} 2052 2053function bloqsGetRecaptchaToken() { 2054 return new Promise(function(resolve) { 2055 const siteKey = window.bloqsRecaptchaSiteKey || ''; 2056 if (!siteKey || typeof grecaptcha === 'undefined' || !grecaptcha.enterprise) { 2057 resolve(null); 2058 return; 2059 } 2060 // Never leave the submit button stuck if Google is slow/unreachable 2061 let settled = false;
2062 const finish = function(value) { 2063 if (!settled) { 2064 settled = true; 2065 resolve(value); 2066 } 2067 }; 2068 setTimeout(function() { finish(null); }, 10000); 2069 try { 2070 grecaptcha.enterprise.ready(function() { 2071 grecaptcha.enterprise.execute(siteKey, { action: BLOQS_FORM_RECAPTCHA_ACTION }) 2072 .then(finish) 2073 .catch(function(err) { 2074 console.error('reCAPTCHA token generation failed', err); 2075 finish(null); 2076 }); 2077 }); 2078 } catch (e) { 2079 console.error('reCAPTCHA error', e); 2080 finish(null); 2081 } 2082 }); 2083} 2084 2085// Appends form_token + recaptcha_token to a FormData; resolves when ready to send. 2086function bloqsAppendAntiAbuseFields(formData, formId) { 2087 formData.append('form_token', bloqsGetFormToken(formId)); 2088 return bloqsGetRecaptchaToken().then(function(token) { 2089 if (token) { 2090 formData.append('recaptcha_token', token); 2091 } 2092 return formData; 2093 }); 2094} 2095 2096function postSaveFormInfoRequest(targetForm, dataString, formId, bloqFormId) { 2097 const newFormData = new FormData(); 2098 2099 newFormData.append('form_data', JSON.stringify(dataString)); 2100 2101 targetForm.find('input[type="file"]').each(function () { 2102 const files = this.files; 2103 const fieldName = $(this).attr('name'); 2104 const isMultiple = $(this).attr('multiple') !== undefined; 2105 2106 for (let i = 0; i < files.length; i++) { 2107 const formattedFieldName = isMultiple ? `${fieldName}[]` : `${fieldName}[][]`; 2108 newFormData.append(formattedFieldName, files[i]); 2109 } 2110 }); 2111 2112 targetForm.find('.signature-data').each(function(index) { 2113 const signatureData = $(this).val(); 2114 if (signatureData) { 2115 const signatureBlob = dataURLtoBlob(signatureData); 2116 const signatureFile = new File([signatureBlob], `signature_${index}.png`, { type: 'image/png' }); 2117 newFormData.append('signature[][]', signatureFile); 2118 } 2119 }); 2120 2121 var websiteValue = targetForm.find('input[name="website"]').val(); 2122 if (websiteValue === undefined || websiteValue === null) { 2123 websiteValue = $('#toEmailContainer input[name="website"]').val(); 2124 } 2125 newFormData.append('website', websiteValue || ''); 2126 2127 const submitBtn = targetForm.find('button[type="submit"]'); 2128 2129 if (submitBtn.data('bloqsOrigHtml') === undefined) { 2130 submitBtn.data('bloqsOrigHtml', submitBtn.html()); 2131 } 2132 submitBtn.prop('disabled', true); 2133 2134 bloqsAppendAntiAbuseFields(newFormData, formId).then(function() { 2135 $.ajax({ 2136 type: "POST", 2137 url: '/bloqs/bloq-forms/save-form-info/' + formId, 2138 data: newFormData, 2139 processData: false, 2140 contentType: false, 2141 dataType: "json", 2142 headers: { 2143 'X-CSRF-Token': csrfToken 2144 }, 2145 beforeSend: function () { 2146 submitBtn.html(`<svg aria-hidden="true" role="status" class="inline w-4 h-4 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> 2147 <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"></path> 2148 <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"></path> 2149 </svg>`); 2150 }, 2151 success: function(response) { 2152 if (response.status == "success") { 2153 if (bloqFormId) { 2154 $(`#containerForm_${bloqFormId}`).addClass('hidden'); 2155 $(`#thankYouContainerForm_${bloqFormId}`).removeClass('hidden'); 2156 } else { 2157 $(`#containerForm`).addClass('hidden'); 2158 $(`#thankYouContainerForm`).removeClass('hidden'); 2159 } 2160 } else { 2161 submitBtn.prop('disabled', false).html(submitBtn.data('bloqsOrigHtml')); 2162 alert(response.message); 2163 } 2164 }, 2165 error: function() { 2166 submitBtn.prop('disabled', false).html(submitBtn.data('bloqsOrigHtml')); 2167 alert("An error occurred. Please try again."); 2168 } 2169 }); 2170 }); 2171} 2172 2173function saveBloqForm(targetForm, dataString, formId, bloqFormId = null) { 2174 if (formId && dataString) { 2175 if (!validateTotalFileSize(targetForm)) { 2176 return false;
2177 } 2178 if (!validateSignatureFields(targetForm)) { 2179 return false; 2180 } 2181 postSaveFormInfoRequest(targetForm, dataString, formId, bloqFormId); 2182 } 2183} 2184 2185function hideFlash() { 2186 const successMessage = document.querySelector('.custom-msg-success'); 2187 if (successMessage) { 2188 // Hide the message after 5 seconds 2189 setTimeout(() => { 2190 setTimeout(() => successMessage.style.display = 'none', successMessage.style.opacity = 0, 500); 2191 }, 2000); 2192 } 2193} 2194 2195function getFormPostData(targetForm, formId = null, bloqFormId = null) { 2196 try { 2197 var formData = {}; 2198 2199 if (formId) { 2200 formData.form_id = formId; 2201 } 2202 2203 if (bloqFormId) { 2204 formData.bloq_form_id = bloqFormId; 2205 } 2206 2207 // List of fields to exclude 2208 const excludedFields = [ 2209 'is_required', 2210 'is_taxable', 2211 'other_amount', 2212 'quantity' 2213 ]; 2214 2215 // Process each form element 2216 targetForm.find('input, select, textarea').each(function() { 2217 var element = $(this); 2218 var name = element.attr('name'); 2219 2220 var type = element.attr('type'); 2221 var tag = element.prop('tagName').toLowerCase(); 2222 2223 var isCheckedOrSelected = false; 2224 2225 // Check if the element is checked or selected 2226 if ((type === 'checkbox' || type === 'radio') && element.is(':checked')) { 2227 isCheckedOrSelected = true; 2228 } else if (tag === 'select' && element.val()) { 2229 isCheckedOrSelected = true; 2230 } 2231 2232 // If it's checked/selected and has a prevRowId 2233 if (!isCheckedOrSelected) { 2234 var prevRowId = element.data('prev-row-id'); 2235 2236 if (prevRowId) { 2237 var fieldNames = []; 2238 2239 $('#' + prevRowId).find('input[name], select[name], textarea[name]').each(function () { 2240 var fieldName = $(this).attr('name'); 2241 if (fieldName) { 2242 fieldNames.push(fieldName); 2243 excludedFields.push(fieldName); 2244 } 2245 }); 2246 } 2247 } 2248 2249 var subscribeField = []; 2250 2251 $('input[name^="subscribe"]').each(function () { 2252 var fieldName = $(this).attr('name'); 2253 if (fieldName) { 2254 subscribeField.push(fieldName); 2255 excludedFields.push(fieldName); 2256 } 2257 }); 2258 2259 // Skip if no name attribute or hidden CSRF token 2260 if (!name || name === '_token' || excludedFields.includes(name)) return; 2261 2262 // Remove array notation from checkbox names 2263 var cleanName = name.replace('[]', ''); 2264 2265 // Find the label - first try to find group label 2266 var label = ''; 2267 2268 if (element.attr('id') && element.attr('type') !== 'checkbox' && element.attr('type') !== 'radio' && !element.is('select')) { 2269 label = $('label[for="' + element.attr('id') + '"]').first().text().trim(); 2270 } 2271 2272 var container = element.closest('.mb-6, .form-group'); 2273 if (container.length) { 2274 if (name.includes('agree')) { 2275 label = 'agree'; 2276 } else { 2277 if (!label) { 2278 // First, try to find a direct label element 2279 label = container.find('> label, > span').first().text().trim(); 2280 } 2281 2282 if (!label) { 2283 label = container.find('label:first, span:first').text().trim(); 2284 } 2285 } 2286 } 2287 2288 // If still no label found, try finding by input id 2289 // if (!label) { 2290 // label = $('label[for="' + element.attr('id') + '"]').first().text().trim(); 2291 // } 2292 2293 // Skip if no label found 2294 if (!label) { 2295 return; 2296 } 2297 2298 // Remove the asterisk from required fields 2299 label = label.replace('*', '').trim(); 2300 2301 // Initialize the data structure if it doesn't exist 2302 if (!formData[cleanName]) { 2303 formData[cleanName] = { 2304 label: label, 2305 value: null 2306 }; 2307 } 2308 2309 switch(element.attr('type')) { 2310 case 'file': 2311 // Handle multiple files 2312 var fileList = []; 2313 var files = element[0].files; 2314 for (var i = 0; i < files.length; i++) { 2315 fileList.push(files[i].name); 2316 } 2317 formData[cleanName].value = fileList; 2318 formData[cleanName].type = 'file'; 2319 break; 2320 2321 case 'checkbox': 2322 if (name.includes('agree')) { 2323 // Handle agree checkboxes 2324 formData[cleanName].value = element.is(':checked') ? "Yes" : "No"; 2325 } else if (!name.includes('switch')) { 2326 // Handle normal checkboxes 2327 if (!Array.isArray(formData[cleanName].value)) { 2328 formData[cleanName].value = []; 2329 formData[cleanName].options = []; 2330 } 2331 2332 // For checkboxes, collect all options in the same container 2333 var checkboxContainer = element.closest('.mb-6, .form-group'); 2334 if (!formData[cleanName].options.length) { 2335 checkboxContainer.find('input[type="checkbox"]').each(function() { 2336 var optionLabel = $('label[for="' + $(this).attr('id') + '"]').text().trim(); 2337 formData[cleanName].options.push(optionLabel); 2338 }); 2339 } 2340 2341 // Add to values if checked 2342 if (element.is(':checked')) { 2343 formData[cleanName].value.push(element.val()); 2344 } 2345 } 2346 // Handle switch type checkbox 2347 else { 2348 if (!formData[cleanName].options) { 2349 formData[cleanName].options = []; 2350 // Find switch labels 2351 var switchContainer = element.closest('label'); 2352 switchContainer.find('label').each(function() { 2353 if ($(this).text().trim() !== '') { 2354 formData[cleanName].options.push($(this).text().trim()); 2355 } 2356 }); 2357 } 2358 formData[cleanName].value = element.is(':checked') ? 2359 formData[cleanName].options[1] : 2360 formData[cleanName].options[0]; 2361 } 2362 break; 2363 2364 case 'radio': 2365 if (!formData[cleanName].options) { 2366 formData[cleanName].options = []; 2367 } 2368 2369 // For radio buttons, collect all options in the same container 2370 var radioContainer = element.closest('.mb-6, .form-group'); 2371 if (!formData[cleanName].options.length) { 2372 radioContainer.find('input[type="radio"]').each(function() { 2373 var optionLabel = $('label[for="' + $(this).attr('id') + '"]').text().trim(); 2374 formData[cleanName].options.push(optionLabel); 2375 }); 2376 } 2377 2378 // Set value if checked 2379 if (element.is(':checked')) { 2380 formData[cleanName].value = element.val(); 2381 } 2382 break; 2383 2384 case 'hidden':
2385 // Skip hidden inputs for switches as they're handled by the checkbox 2386 if (!element.next('input[type="checkbox"]').length) { 2387 formData[cleanName].value = element.val(); 2388 } 2389 break; 2390 2391 case 'textarea': 2392 formData[cleanName].value = element.val().trim(); 2393 break; 2394 2395 case 'time': 2396 // Convert 24-hour time to 12-hour format with AM/PM 2397 const timeVal = element.val(); 2398 if (timeVal) { 2399 const [hours, minutes] = timeVal.split(':'); 2400 const hour = parseInt(hours, 10); 2401 const ampm = hour >= 12 ? 'PM' : 'AM'; 2402 const hour12 = hour % 12 || 12; 2403 formData[cleanName].value = `${hour12}:${minutes} ${ampm}`; 2404 } else { 2405 formData[cleanName].value = timeVal; 2406 } 2407 break; 2408 2409 default: 2410 formData[cleanName].value = element.val(); 2411 } 2412 }); 2413 2414 // Convert checkbox arrays to single values if only one selection 2415 Object.keys(formData).forEach(key => { 2416 if (Array.isArray(formData[key].value) && formData[key].value.length === 1) { 2417 formData[key].value = formData[key].value[0]; 2418 } 2419 }); 2420 2421 return formData; 2422 } catch (error) { 2423 console.error('Error processing form data:', error); 2424 return null; 2425 } 2426} 2427 2428function collectMemberData(targetForm) { 2429 const result = []; 2430 2431 const containers = targetForm.find('.subscribe-container'); 2432 if (containers.length === 0) { 2433 return false; 2434 } 2435 2436 containers.each(function () { 2437 const container = $(this); 2438 const firstName = container.find('[name^="first_name"]').val() || ''; 2439 const lastName = container.find('[name^="last_name"]').val() || ''; 2440 const phone = container.find('[name^="phone_number"]').val() || ''; 2441 const email = container.find('[name^="email"]').val() || ''; 2442 2443 if (container.find('.subscribe-phone input[type="checkbox"]').is(':checked')) { 2444 result.push({ 2445 first_name: firstName, 2446 last_name: lastName, 2447 phone_number: phone, 2448 email: email, 2449 type: 'phone', 2450 sms_opt_in: 1 2451 }); 2452 } 2453 2454 if (container.find('.subscribe-email input[type="checkbox"]').is(':checked')) { 2455 result.push({ 2456 first_name: firstName, 2457 last_name: lastName, 2458 phone_number: phone, 2459 email: email, 2460 type: 'email', 2461 email_opt_in: 1 2462 }); 2463 } 2464 }); 2465 2466 return result; 2467} 2468 2469function saveStandardFormData(targetForm, bloqFormId = null, type = null) { 2470 const formData = getFormPostData(targetForm); 2471 const memberData = collectMemberData(targetForm); 2472 2473 if (!validateTotalFileSize(targetForm)) { 2474 return false; 2475 } 2476 2477 if (bloqFormId) { 2478 var toEmailContainer = $(`#toEmailContainer_${bloqFormId}`); 2479 var containerForm = $(`#containerForm_${bloqFormId}`); 2480 } else { 2481 var toEmailContainer = $(`#toEmailContainer`); 2482 var containerForm = $(`#containerForm`); 2483 } 2484 2485 const isSignatureValid = validateSignatureFields(targetForm); 2486 2487 if (isSignatureValid) { 2488 containerForm.addClass('hidden'); 2489 toEmailContainer.removeClass('hidden'); 2490 2491 var emailValue= ''; 2492 // Loop through all email inputs and pick the first non-empty one 2493 targetForm.find('input[type="email"]').each(function () { 2494 const val = $(this).val().trim(); 2495 if (val !== '') { 2496 emailValue = val; 2497 return false; 2498 } 2499 }); 2500 2501 // Scroll to the toEmailContainer smoothly 2502 $('html, body').animate({ 2503 scrollTop: toEmailContainer.offset().top 2504 }, 100); 2505 2506 toEmailContainer.find('input[name="to_email"]').each(function () { 2507 $(this).val(emailValue); 2508 }); 2509 2510 displayFormData(formData, toEmailContainer); 2511 } 2512 2513 toEmailContainer.find('#toEmailForm').off('submit').on('submit', function(e) { 2514 e.preventDefault(); 2515 2516 var dataString = $(this).serialize(); 2517 2518 if (type) { 2519 toEmailContainer.addClass('hidden'); 2520 2521 if (bloqFormId) { 2522 $(`#thankYouContainerForm_${bloqFormId}`).removeClass('hidden'); 2523 } else { 2524 $(`#thankYouContainerForm`).removeClass('hidden'); 2525 } 2526 } else { 2527 const newFormData = new FormData(); 2528 2529 // Append form data 2530 newFormData.append('form_data', JSON.stringify(formData)); 2531 newFormData.append('data_string', dataString); 2532 2533 // Append files separately 2534 targetForm.find('input[type="file"]').each(function () { 2535 const files = this.files; 2536 const fieldName = $(this).attr("name"); 2537 2538 // Check if the input has the 'multiple' attribute 2539 const isMultiple = $(this).attr("multiple") !== undefined; 2540 2541 for (let i = 0; i < files.length; i++) { 2542 // If it's a single file upload, enforce array format manually 2543 const formattedFieldName = isMultiple ? `${fieldName}[]` : `${fieldName}[][]`; 2544 2545 newFormData.append(formattedFieldName, files[i]); // Ensure uniform structure 2546 } 2547 }); 2548 2549 // Add signature data as files 2550 targetForm.find('.signature-data').each(function(index) { 2551 const signatureData = $(this).val(); 2552 if (signatureData) { 2553 // Convert base64 signature to Blob 2554 const signatureBlob = dataURLtoBlob(signatureData); 2555 // Create a file from the blob 2556 const signatureFile = new File([signatureBlob], `signature_${index}.png`, { type: 'image/png' }); 2557 // Append to FormData 2558 newFormData.append('signature[][]', signatureFile); 2559 } 2560 }); 2561 2562 if (memberData) { 2563 newFormData.append('member_data', JSON.stringify(memberData)); 2564 } 2565 2566 const fx8FormId = bloqsGetFormIdFromAction(targetForm); 2567 const submitBtn = toEmailContainer.find(".submitBtn"); 2568 if (submitBtn.data('bloqsOrigHtml') === undefined) { 2569 submitBtn.data('bloqsOrigHtml', submitBtn.html()); 2570 } 2571 submitBtn.prop('disabled', true); 2572 2573 bloqsAppendAntiAbuseFields(newFormData, fx8FormId).then(function() { 2574 $.ajax({ 2575 type: "POST", 2576 url: targetForm.attr('action'), 2577 data: newFormData, 2578 processData: false, 2579 contentType: false, 2580 dataType: "json", 2581 headers: { 2582 'X-CSRF-Token': csrfToken 2583 }, 2584 beforeSend: function () { 2585 submitBtn.html(`<svg aria-hidden="true" role="status" class="inline w-4 h-4 text-white animate-spin" viewB
2585ox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> 2586 <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"></path> 2587 <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"></path> 2588 </svg>`); 2589 }, 2590 success: function(response) { 2591 if (response.status == "success") { 2592 toEmailContainer.addClass('hidden'); 2593 2594 if (bloqFormId) { 2595 $(`#thankYouContainerForm_${bloqFormId}`).removeClass('hidden'); 2596 } else { 2597 $(`#thankYouContainerForm`).removeClass('hidden'); 2598 } 2599 } else { 2600 submitBtn.prop('disabled', false).html(submitBtn.data('bloqsOrigHtml')); 2601 alert(response.message); 2602 } 2603 }, 2604 error: function() { 2605 submitBtn.prop('disabled', false).html(submitBtn.data('bloqsOrigHtml')); 2606 alert("An error occurred. Please try again."); 2607 } 2608 }); 2609 }); 2610 } 2611 }); 2612} 2613 2614function displayFormData(formData, targetContainer = null) { 2615 // Create container 2616 const container = document.createElement('div'); 2617 container.classList.add('text-sm'); 2618 2619 // Iterate through form data 2620 for (const [key, value] of Object.entries(formData)) { 2621 // Create section for each field 2622 const section = document.createElement('div'); 2623 section.classList.add('mb-5', 'border-b-2', 'border-dotted'); 2624 2625 // Create label 2626 const label = document.createElement('div'); 2627 label.textContent = value.label; 2628 label.classList.add('block', 'font-medium', 'text-gray-700', 'mb-1'); 2629 2630 // Create value 2631 const valueElement = document.createElement('div'); 2632 valueElement.classList.add('text-gray-500', 'pb-2', 'ml-2'); 2633 2634 if (Array.isArray(value.value) && value.value.length > 0) { 2635 value.value.forEach(item => { 2636 const line = document.createElement('div'); // Create a new div for each item 2637 line.textContent = item; 2638 valueElement.appendChild(line); 2639 }); 2640 } else { 2641 valueElement.textContent = value.value; 2642 } 2643 2644 // Assemble section 2645 section.appendChild(label); 2646 section.appendChild(valueElement); 2647 container.appendChild(section); 2648 } 2649 2650 // Clear any existing content and append new display 2651 if(targetContainer){ 2652 const targetElement = targetContainer.find('#formSummary'); 2653 targetElement.empty(); 2654 targetElement.append(container); 2655 } 2656} 2657 2658function openInModal(object) { 2659 var url = object.getAttribute('data-url'); 2660 openModalWithAjax(url); 2661} 2662 2663function validateSignatureFields(targetForm) { 2664 let signatureError = false; 2665 2666 targetForm.find('.signature-data').each(function (index) { 2667 if($(this).closest('.hidden').length === 0){ 2668 const $input = $(this); 2669 const inputValue = $input.val(); 2670 const isRequired = $input.attr('data-required') === '1'; 2671 2672 $input.next('.signature-error').remove(); 2673 2674 const fieldInvalid = !inputValue && isRequired; 2675 2676 debugLogSignatureField({ 2677 index: index, 2678 name: $input.attr('name') || '(no name)', 2679 id: $input.attr('id') || '(no id)', 2680 required: isRequired, 2681 isEmpty: !inputValue, 2682 isValid: !fieldInvalid, 2683 }); 2684 2685 if (fieldInvalid) { 2686 $('<div class="signature-error text-red-500 text-sm mt-1">Signature is required.</div>').insertAfter($input); 2687 signatureError = true; 2688 } 2689 } 2690 }); 2691 2692 return signatureError ? false : true; 2693} 2694 2695function validateTotalFileSize(targetForm, maxSize = 5 * 1024 * 1024) { 2696 let totalSize = 0; 2697 2698 targetForm.find('input[type="file"]').each(function () { 2699 const files = this.files; 2700 for (let i = 0; i < files.length; i++) { 2701 totalSize += files[i].size; 2702 } 2703 }); 2704 2705 if (totalSize > maxSize) { 2706 alert(`Cannot submit form: Total file size (${formatFileSize(totalSize)}) exceeds the maximum limit of ${formatFileSize(maxSize)}.\n\nPlease remove some files and try again.`); 2707 return false;
2708 } 2709 2710 return true; 2711} 2712 2713function formatFileSize(bytes) { 2714 if (bytes === 0) return '0 Bytes'; 2715 const k = 1024; 2716 const sizes = ['Bytes', 'KB', 'MB', 'GB']; 2717 const i = Math.floor(Math.log(bytes) / Math.log(k)); 2718 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 2719} 2720 2721function closeModal(id){ 2722 document.getElementById(id).classList.add('hidden'); 2723} 2724 2725function handleSubscribeCheckbox(checkbox, formId) { 2726 // const chechboxId = checkbox.id; 2727 const parentElement = checkbox.closest('.subscribe-email, .subscribe-phone'); 2728 let subscribeType = ''; 2729 if (parentElement) { 2730 if (parentElement.classList.contains('subscribe-email')) { 2731 subscribeType = 'email'; 2732 } else if (parentElement.classList.contains('subscribe-phone')) { 2733 subscribeType = 'phone'; 2734 } 2735 } 2736 2737 document.querySelectorAll('.phone-container .phone').forEach(input => { 2738 if (input) { 2739 if (input.hasAttribute('required')) { 2740 input.dataset.originalRequired = 'true'; 2741 } 2742 } 2743 }); 2744 2745 if (checkbox.checked) { 2746 $.ajax({ 2747 type: "GET", 2748 url: '/bloqs/bloq-forms/member-details/' + subscribeType, 2749 headers: { 2750 'X-CSRF-Token': $('meta[name="csrfToken"]').attr('content') 2751 }, 2752 success: function(response) { 2753 $("#subscribeDetailsModal").removeClass('hidden').html(response); 2754 2755 setTimeout(() => { 2756 const cancelButton = document.querySelector('#subscribeDetailsModal .cancel-btn'); 2757 if (cancelButton) { 2758 cancelButton.onclick = function () { 2759 $('#subscribeDetailsModal').addClass('hidden'); 2760 checkbox.checked = false; 2761 2762 if (subscribeType == 'phone') { 2763 const phoneContainer = checkbox.closest('.phone-container'); 2764 if (phoneContainer) { 2765 const phoneInput = phoneContainer.querySelector('.phone'); 2766 if (phoneInput && !phoneInput.dataset.originalRequired) { 2767 phoneInput.removeAttribute('required'); 2768 } 2769 } 2770 } 2771 }; 2772 } 2773 2774 const agreeButton = document.querySelector('#subscribeDetailsModal .agree-btn'); 2775 if (agreeButton) { 2776 agreeButton.onclick = function () { 2777 $('#subscribeDetailsModal').addClass('hidden'); 2778 2779 if (subscribeType == 'phone') { 2780 const phoneContainer = checkbox.closest('.phone-container'); 2781 if (phoneContainer) { 2782 const phoneInput = phoneContainer.querySelector('.phone'); 2783 if (phoneInput && !phoneInput.dataset.originalRequired) { 2784 phoneInput.setAttribute('required', 'required'); 2785 } 2786 } 2787 } 2788 }; 2789 } 2790 2791 // const form = document.getElementById('member-details-form'); 2792 // const saveBtn = document.getElementById('save-details'); 2793 2794 // if (form && saveBtn) { 2795 // saveBtn.addEventListener('click', function (e) { 2796 // e.preventDefault(); 2797 2798 // if (form.checkValidity()) { 2799 // const parentDiv = checkbox.closest('.subscribe-email, .subscribe-phone'); 2800 // let type = ''; 2801 // if (parentDiv) { 2802 // if (parentDiv.classList.contains('subscribe-email')) { 2803 // type = 'email'; 2804 // } else if (parentDiv.classList.contains('subscribe-phone')) { 2805 // type = 'phone'; 2806 // } 2807 // } 2808 2809 // const formData = new FormData(form); 2810 // formData.append('clicked_checkbox_id', chechboxId); 2811 // formData.append('type', type); 2812 // formData.append('form_id', formId); 2813 2814 // $.ajax({ 2815 // url: '/bloqs/bloq-forms/save-member-details', 2816 // method: 'POST', 2817 // data: formData, 2818 // processData: false, 2819 // contentType: false, 2820 // headers: { 2821 // 'X-CSRF-Token': $('meta[name="csrfToken"]').attr('content') 2822 // }, 2823 // success: function(res) { 2824 // $('#subscribeDetailsModal').addClass('hidden'); 2825 // }, 2826 // error: function(xhr) { 2827 // console.error('Save failed:', xhr.responseText); 2828 // } 2829 // }); 2830 2831 // } else { 2832 // form.reportValidity(); 2833 // } 2834 // }); 2835 // } 2836 }, 0); 2837 }, 2838 error: function(xhr) { 2839 console.error('AJAX error:', xhr.responseText); 2840 } 2841 }); 2842 } else { 2843 if (subscribeType == 'phone') { 2844 const phoneContainer = checkbox.closest('.phone-container'); 2845 if (phoneContainer) { 2846 const phoneInput = phoneContainer.querySelector('.phone'); 2847 if (phoneInput && !phoneInput.dataset.originalRequired) { 2848 phoneInput.removeAttribute('required'); 2849 } 2850 } 2851 } 2852 // removeSessionData(chechboxId, formId); 2853 } 2854} 2855 2856function toggleAgreeModal(checkbox, type) { 2857 if (checkbox.checked) { 2858 if (type == 'email') { 2859 document.getElementById('emailConsentModal').classList.remove('hidden'); 2860 } else { 2861 document.getElementById('smsConsentModal').classList.remove('hidden'); 2862 } 2863 } 2864} 2865 2866function cancelConsent(type) { 2867 document.getElementById('iAgreeCheckbox').checked = false;
2868 document.getElementById(`${type}ConsentModal`).classList.add('hidden'); 2869} 2870 2871function removeSessionData(checkboxId, formId) { 2872 $.ajax({ 2873 url: '/bloqs/bloq-forms/remove-member-details', 2874 method: 'POST', 2875 data: { 2876 checkbox_id: checkboxId, 2877 form_id: formId 2878 }, 2879 headers: { 2880 'X-CSRF-Token': $('meta[name="csrfToken"]').attr('content') 2881 }, 2882 success: function (res) { 2883 console.log('Session cleared for:', checkboxId); 2884 }, 2885 error: function (xhr) { 2886 console.error('Session removal failed:', xhr.responseText); 2887 } 2888 }); 2889}
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.