1document.addEventListener("DOMContentLoaded", function () { 2 3 function showLoader() { 4 const loader = document.createElement("div"); 5 loader.className = "msf-loader-overlay"; 6 7 loader.innerHTML = ` 8 <svg class="msf-loader-spinner" viewBox="0 0 50 50"> 9 <circle cx="25" cy="25" r="20"></circle> 10 </svg> 11 `; 12 13 document.body.appendChild(loader); 14 } 15 16 17 18 const form = document.querySelector(".listing-manager-form"); 19 if (!form) return; 20 21 const allSections = Array.from(form.querySelectorAll(".add-listing-section")); 22 23const submitPageDiv = document.querySelector(".submit-page"); 24 25if (!submitPageDiv || !submitPageDiv.classList.contains("multi-step-form")) { 26 return; // or throw new Error('Required class not found'); 27} 28 29 30 const submitButton = form.querySelector('button[name="submit_listing"]'); 31 32 if (!submitButton || allSections.length <= 1) { 33 return; 34 } 35 36 // --- Start of Your Custom Configuration --- 37 let stepConfiguration = []; 38 39 try { 40 const rawStepsEncoded = document.getElementById( 41 "listeo_form_steps_json" 42 )?.value; 43 44 if (rawStepsEncoded) { 45 const textarea = document.createElement("textarea"); 46 textarea.innerHTML = rawStepsEncoded; 47 const rawSteps = textarea.value; 48 49 stepConfiguration = JSON.parse(rawSteps); 50 } 51 } catch (e) { 52 console.error("Failed to parse step configuration", e); 53 } 54 55 // if stepConfiguration is empty or not an array, disable multi-step 56 if (!Array.isArray(stepConfiguration) || stepConfiguration.length === 0) { 57 return; 58 } 59 60 allSections.forEach((section) => { 61 section.removeAttribute("style"); 62 }); 63 // const stepConfiguration = [ 64 // { 65 // title: "Listing Essentials", 66 // selectors: [".basic_info", ".location", ".gallery", ".details"], 67 // }, 68 // { 69 // title: "Offerings & Schedule", 70 // selectors: [ 71 // ".opening_hours", 72 // ".menu", 73 // ".booking", 74 // ".slots", 75 // ".basic_prices", 76 // ".availability_calendar", 77 // ], 78 // }, 79 // { 80 // title: "Final Details", 81 // selectors: [".faq", ".my_listings_section"], 82 // }, 83 // ]; 84 // --- End of Your Custom Configuration --- 85 86 let currentStep = 0; 87 let highestStepReached = 0; 88 const steps = []; 89 90 // Reworked Step Creation Logic 91 stepConfiguration.forEach((groupInfo) => { 92 const groupSections = []; 93 groupInfo.selectors.forEach((selector) => { 94 const foundSections = form.querySelectorAll(selector); 95 foundSections.forEach((section) => { 96 // Ensure we only add sections that actually exist in the form 97 if (allSections.includes(section)) { 98 groupSections.push(section); 99 } 100 }); 101 }); 102 103 // Also check for custom fields sections using both ID and class selectors 104 groupInfo.selectors.forEach((selector) => { 105 if (selector.startsWith('#custom-fields-') || selector === '.custom-term-features') { 106 // For ID selectors, look for specific custom section 107 if (selector.startsWith('#custom-fields-')) { 108 const customSection = document.querySelector(selector); 109 if (customSection && !groupSections.includes(customSection)) { 110 groupSections.push(customSection); 111 } 112 } 113 // For class selector, get all custom sections 114 else if (selector === '.custom-term-features') { 115 const customSections = document.querySelectorAll(selector); 116 customSections.forEach((customSection) => { 117 if (!groupSections.includes(customSection)) { 118 groupSections.push(customSection); 119 } 120 }); 121 } 122 } 123 }); 124 125 if (groupSections.length > 0) { 126 steps.push(groupSections); 127 } 128 }); 129 130 const progressContainer = document.createElement("div"); 131 progressContainer.className = "form-progress-container"; 132 const navContainer = document.createElement("div"); 133 navContainer.className = "form-navigation"; 134 const prevButton = document.createElement("button"); 135 prevButton.type = "button"; 136 prevButton.className = "btn-prev button"; 137 prevButton.textContent = listeo_core.prev; 138 const nextButton = document.createElement("button"); 139 nextButton.type = "button"; 140 nextButton.className = "btn-next button"; 141 nextButton.textContent = listeo_core.next; 142 form.insertBefore(progressContainer, form.firstChild); 143 const lastSection = allSections[allSections.length - 1]; 144 lastSection.parentNode.insertBefore(navContainer, lastSection.nextSibling); 145 navContainer.appendChild(prevButton); 146 navContainer.appendChild(nextButton); 147 navContainer.appendChild(submitButton); 148 if (submitButton.parentElement.nodeName === "P") { 149 submitButton.parentElement.style.display = "block"; 150 submitButton.parentElement.style.margin = "0"; 151 submitButton.parentElement.classList.add("form-navigation-submit"); 152 } 153 154 // Reworked Progress Bar Creation 155 steps.forEach((stepSections, index) => { 156 const stepElement = document.createElement("div"); 157 stepElement.className = "form-progress-step"; 158 const icon = document.createElement("div"); 159 icon.className = "progress-step-icon"; 160 const titleElement = document.createElement("div"); 161 titleElement.className = "progress-step-title"; 162 163 // Use the title from the new configuration 164 titleElement.textContent = 165 stepConfiguration[index].title || `Step ${index + 1}`; 166 167 stepElement.appendChild(icon); 168 stepElement.appendChild(titleElement); 169 progressContainer.appendChild(stepElement); 170 }); 171 const progressSteps = progressContainer.querySelectorAll( 172 ".form-progress-step" 173 ); 174 175 function smoothScrollToTop() { 176 window.scrollTo({ top: 0, behavior: "smooth" }); 177 } 178 179 function isFieldRequired(label) { 180 if (!label) return false;
181 const iTags = label.querySelectorAll("i"); 182 for (const iTag of iTags) { 183 if (iTag.textContent.trim() === "*") return true; 184 } 185 return false; 186 } 187 188 function validateStep(stepIndex) { 189 let isStepValid = true; 190 const sectionsToValidate = steps[stepIndex]; 191 for (const section of sectionsToValidate) { 192 const standardFields = section.querySelectorAll( 193 'input[required]:not([type="hidden"]), select[required], textarea[required]' 194 ); 195 for (const field of standardFields) { 196 // Special handling for Select2 fields 197 if (field.tagName === 'SELECT' && field.classList.contains('select2-hidden-accessible')) { 198 const isValid = field.value && field.value !== '-1' && field.value !== ''; 199 const select2Container = field.nextElementSibling; 200 201 if (!isValid) { 202 isStepValid = false; 203 field.classList.add("msf-input-error"); 204 if (select2Container && select2Container.classList.contains('select2-container')) { 205 select2Container.classList.add("msf-input-error"); 206 } 207 } else { 208 field.classList.remove("msf-input-error"); 209 if (select2Container && select2Container.classList.contains('select2-container')) { 210 select2Container.classList.remove("msf-input-error"); 211 } 212 } 213 } else { 214 // Standard HTML5 validation for non-Select2 fields 215 if (!field.checkValidity()) { 216 isStepValid = false; 217 field.classList.add("msf-input-error"); 218 } else { 219 field.classList.remove("msf-input-error"); 220 } 221 } 222 } 223 const drilldownMenus = section.querySelectorAll("div.drilldown-menu"); 224 for (const menu of drilldownMenus) { 225 const name = menu.getAttribute("data-name"); 226 if (!name) continue; 227 const label = section.querySelector( 228 `label[for="${name}"], .label-${name}` 229 ); 230 if (!isFieldRequired(label)) continue; 231 const hasValue = 232 menu.querySelector("input.drilldown-generated") !== null; 233 const menuToggle = menu.querySelector(".menu-toggle"); 234 if (menuToggle) { 235 if (!hasValue) { 236 isStepValid = false; 237 menuToggle.classList.add("msf-input-error"); 238 } else { 239 menuToggle.classList.remove("msf-input-error"); 240 } 241 } 242 } 243 const galleryUploader = section.querySelector("#media-uploader"); 244 if ( 245 galleryUploader && 246 galleryUploader.getAttribute("data-required") === "required" 247 ) { 248 const hasImages = 249 galleryUploader.querySelector(".dz-image-preview") !== null; 250 const existingError = galleryUploader.nextElementSibling; 251 if (!hasImages) { 252 isStepValid = false; 253 if ( 254 !existingError || 255 !existingError.classList.contains("msf-gallery-error-message") 256 ) { 257 const errorDiv = document.querySelector( 258 ".msf-gallery-error-message" 259 ); 260 if (errorDiv) { 261 errorDiv.style.display = "block"; 262 } 263 } 264 } else { 265 if ( 266 existingError && 267 existingError.classList.contains("msf-gallery-error-message") 268 ) { 269 existingError.remove(); 270 } 271 } 272 } 273 if (typeof tinymce !== "undefined") { 274 const textareas = section.querySelectorAll("textarea"); 275 for (const textarea of textareas) { 276 const editorId = textarea.id; 277 if (!editorId) continue; 278 const editor = tinymce.get(editorId); 279 if (!editor) continue; 280 const label = section.querySelector(`label[for="${editorId}"]`); 281 if (!isFieldRequired(label)) continue; 282 const content = editor.getContent({ format: "text" }).trim(); 283 const editorContainer = editor.getContainer(); 284 if (content === "") { 285 isStepValid = false; 286 if (editorContainer) 287 editorContainer.classList.add("msf-input-error"); 288 } else { 289 if (editorContainer) 290 editorContainer.classList.remove("msf-input-error"); 291 } 292 } 293 } 294 const fileInputs = section.querySelectorAll('input[type="file"]'); 295 for (const fileInput of fileInputs) { 296 const inputId = fileInput.id; 297 if (!inputId) continue; 298 const label = section.querySelector(`label[for="${inputId}"]`); 299 if (!isFieldRequired(label)) continue; 300 const fieldContainer = fileInput.closest( 301 ".form-field-container-type-file" 302 ); 303 const uploadWrapper = fileInput.closest(".uploadButton"); 304 if (uploadWrapper && fieldContainer) { 305 const uploadButtonLabel = uploadWrapper.querySelector( 306 ".uploadButton-button" 307 ); 308 const hasExistingPreview = 309 fieldContainer.querySelector(".listeo-uploaded-file-preview") !== 310 null; 311 const hasNewFile = fileInput.files.length > 0; 312 const isFieldValid = hasNewFile || hasExistingPreview; 313 if (uploadButtonLabel) { 314 if (!isFieldValid) { 315 isStepValid = false;
316 uploadButtonLabel.classList.add("msf-input-error"); 317 } else { 318 uploadButtonLabel.classList.remove("msf-input-error"); 319 } 320 } 321 } 322 } 323 } 324 return isStepValid; 325 } 326 327 function updateStepStatus(stepIndex) { 328 const isStepValid = validateStep(stepIndex); 329 const stepElement = progressSteps[stepIndex]; 330 if (isStepValid) { 331 stepElement.classList.remove("step-warning"); 332 } else { 333 stepElement.classList.add("step-warning"); 334 } 335 } 336 337 // Unified visibility controller 338 function shouldSectionBeVisible(section) { 339 // Check if section belongs to current step 340 const isInCurrentStep = steps[currentStep] && steps[currentStep].includes(section); 341 342 // Check if section is booking-dependent 343 const isBookingDependent = section.classList.contains('availability_calendar') || 344 section.classList.contains('slots') || 345 section.classList.contains('basic_prices'); 346 347 // Check if booking is enabled 348 const bookingInput = form.querySelector( 349 '.booking input[name*="booking_status"], .booking select[name*="booking_status"]' 350 ); 351 let isBookingEnabled = false; 352 353 if (bookingInput) { 354 if (bookingInput.type === "checkbox" || bookingInput.type === "radio") { 355 isBookingEnabled = bookingInput.checked || bookingInput.value === "enabled"; 356 } else if (bookingInput.tagName === "SELECT") { 357 isBookingEnabled = bookingInput.value === "enabled"; 358 } 359 } 360 361 // Apply unified visibility logic 362 if (isBookingDependent) { 363 return isInCurrentStep && isBookingEnabled; 364 } 365 return isInCurrentStep; 366 } 367 368 function updateBookingDependencies() { 369 const bookingInput = form.querySelector( 370 '.booking input[name*="booking_status"], .booking select[name*="booking_status"]' 371 ); 372 if (!bookingInput) return; 373 374 let isEnabled = false; 375 376 if (bookingInput.type === "checkbox" || bookingInput.type === "radio") { 377 isEnabled = bookingInput.checked || bookingInput.value === "enabled"; 378 } else if (bookingInput.tagName === "SELECT") { 379 isEnabled = bookingInput.value === "enabled"; 380 } 381 382 // Update the booking switcher visual state 383 const bookingSection = form.querySelector('.booking'); 384 if (bookingSection) { 385 if (isEnabled) { 386 bookingSection.classList.add("switcher-on"); 387 } else { 388 bookingSection.classList.remove("switcher-on"); 389 } 390 } 391 392 // Trigger form UI update to apply unified visibility logic 393 updateFormUI(); 394 } 395 396 // Set up initial state and watch for changes 397 updateBookingDependencies(); 398 const bookingInput = form.querySelector( 399 '.booking input[name*="booking_status"]' 400 ); 401 if (bookingInput) { 402 bookingInput.addEventListener("change", updateBookingDependencies); 403 } 404 405 // Modify the updateFormUI function to use unified visibility logic 406 function updateFormUI() { 407 if (typeof window.listeoCurrentStep !== "undefined") { 408 window.listeoCurrentStep = currentStep; 409 } 410 411 // Hide all sections first 412 allSections.forEach((section) => { 413 section.classList.remove("active"); 414 section.style.display = "none"; 415 }); 416 417 // Hide all custom fields sections 418 const customTermFeatures = document.querySelectorAll(".custom-term-features"); 419 customTermFeatures.forEach(function (element) { 420 element.classList.remove("active"); 421 element.style.display = "none"; 422 }); 423 424 // Clean up empty custom sections before showing the current step 425 const emptyCustomSections = document.querySelectorAll(".custom-term-features"); 426 emptyCustomSections.forEach(function (element) { 427 const content = element.querySelector('.custom-term-features-content'); 428 if (content && content.children.length === 0) { 429 console.log("Removing empty custom section from updateFormUI:", element.id); 430 element.remove(); 431 } 432 }); 433 434 if (steps[currentStep]) { 435 const activeSections = steps[currentStep]; 436 let needsResize = false; 437 438 // Show sections using unified visibility logic 439 activeSections.forEach((section) => { 440 // Check if the section still exists in the DOM (might have been removed as empty) 441 if (!document.contains(section)) { 442 return; 443 } 444 445 // Use unified visibility controller 446 if (shouldSectionBeVisible(section)) { 447 section.classList.add("active"); 448 section.style.display = ""; 449 450 if ( 451 section.querySelector("#submit_map") || 452 section.querySelector(".fullCalendar, .fc") 453 ) { 454 needsResize = true; 455 } 456 } 457 }); 458 459 if (needsResize) { 460 setTimeout(() => { 461 window.dispatchEvent(new Event("resize")); 462 }, 50); 463 } 464 } 465 466 // FIXED: Check custom sections based on current step configuration 467 const customSections = document.querySelectorAll(".custom-term-features"); 468 customSections.forEach(function (element) {
469 var sectionId = element.id; 470 var shouldBeVisible = false; 471 472 // Check if this section's selector is in current step configuration 473 if (stepConfiguration[currentStep] && stepConfiguration[currentStep].selectors) { 474 // Check for both ID selector and class selector 475 var idSelector = "#" + sectionId; 476 var classSelector = ".custom-term-features"; 477 478 shouldBeVisible = 479 stepConfiguration[currentStep].selectors.indexOf(idSelector) !== -1 || 480 stepConfiguration[currentStep].selectors.indexOf(classSelector) !== -1; 481 } 482 483 // Only show sections that have content 484 const content = element.querySelector('.custom-term-features-content'); 485 const hasContent = content && content.children.length > 0; 486 487 if (shouldBeVisible && hasContent) { 488 element.classList.add("active"); 489 element.style.display = ""; 490 console.log("Showing custom section:", sectionId, "in step:", currentStep); 491 } else { 492 console.log("Hiding custom section:", sectionId, "in step:", currentStep, "hasContent:", hasContent); 493 } 494 }); 495 496 progressSteps.forEach((step, index) => { 497 const icon = step.querySelector(".progress-step-icon"); 498 step.classList.remove("active", "completed"); 499 if ( 500 index <= highestStepReached && 501 !step.classList.contains("step-warning") 502 ) { 503 step.classList.add("completed"); 504 } 505 if (index === currentStep) { 506 step.classList.add("active"); 507 step.classList.remove("completed"); 508 } 509 if ( 510 step.classList.contains("completed") && 511 !step.classList.contains("active") 512 ) { 513 icon.textContent = ""; 514 } else { 515 icon.textContent = index + 1; 516 } 517 }); 518 prevButton.style.display = currentStep === 0 ? "none" : "inline-block"; 519 nextButton.style.display = 520 currentStep === steps.length - 1 ? "none" : "inline-block"; 521 submitButton.style.display = 522 currentStep === steps.length - 1 ? "inline-block" : "none"; 523} 524 525// Add this function after the step creation logic (around line 120) 526// Add a function to refresh steps when custom sections are added 527window.refreshStepsConfiguration = function() { 528 // Get the updated step configuration from the hidden input 529 const stepsInput = document.getElementById('listeo_form_steps_json'); 530 if (!stepsInput) return; 531 532 try { 533 const textarea = document.createElement("textarea"); 534 textarea.innerHTML = stepsInput.value; 535 const updatedStepConfiguration = JSON.parse(textarea.value); 536 537 // Rebuild the steps array 538 steps.length = 0; // Clear existing steps 539 540 updatedStepConfiguration.forEach((groupInfo) => { 541 const groupSections = []; 542 groupInfo.selectors.forEach((selector) => { 543 // Use querySelectorAll on the entire document for custom sections 544 const foundSections = document.querySelectorAll(selector); 545 foundSections.forEach((section) => { 546 // Check if it's in allSections or if it's a custom section 547 if (allSections.includes(section) || section.classList.contains('custom-term-features')) { 548 groupSections.push(section); 549 } 550 }); 551 }); 552 553 if (groupSections.length > 0) { 554 steps.push(groupSections); 555 } 556 }); 557 558 // Update the step configuration reference 559 stepConfiguration = updatedStepConfiguration; 560 561 console.log('Steps configuration refreshed:', steps); 562 563 // DON'T force update the UI here - let it happen naturally 564 // This was causing the section to be removed before content was added 565 // updateFormUI(); 566 567 } catch (e) { 568 console.error('Error refreshing step configuration:', e); 569 } 570}; 571 572// Also add debugging to the step navigation to see what's happening 573nextButton.addEventListener("click", () => { 574 updateStepStatus(currentStep); 575 if (currentStep < steps.length - 1) { 576 currentStep++; 577 highestStepReached = Math.max(highestStepReached, currentStep); 578 console.log("Moving to step:", currentStep, "with sections:", steps[currentStep]); 579 updateFormUI(); 580 smoothScrollToTop(); 581 } 582}); 583 584prevButton.addEventListener("click", () => { 585 updateStepStatus(currentStep); 586 if (currentStep > 0) { 587 currentStep--; 588 console.log("Moving to step:", currentStep, "with sections:", steps[currentStep]); 589 updateFormUI(); 590 smoothScrollToTop(); 591 } 592}); 593 594progressSteps.forEach((step, index) => { 595 step.addEventListener("click", () => { 596 if (index === currentStep) return; 597 if (index > currentStep) { 598 for (let i = currentStep; i < index; i++) { 599 updateStepStatus(i); 600 } 601 } else { 602 updateStepStatus(currentStep); 603 } 604 currentStep = index;
605 highestStepReached = Math.max(highestStepReached, currentStep); 606 console.log("Clicking to step:", currentStep, "with sections:", steps[currentStep]); 607 updateFormUI(); 608 smoothScrollToTop(); 609 }); 610}); 611 submitButton.addEventListener("click", (e) => { 612 const isPreview = 613 e.target.name === "submit_listing" || 614 e.target.value.toLowerCase().includes("preview") || 615 e.target.textContent.toLowerCase().includes("preview"); 616 617 // Always validate steps 618 for (let i = 0; i < steps.length; i++) { 619 updateStepStatus(i); 620 } 621 622 let firstInvalidStep = -1; 623 progressSteps.forEach((step, index) => { 624 if (step.classList.contains("step-warning") && firstInvalidStep === -1) { 625 firstInvalidStep = index; 626 } 627 }); 628 629 if (firstInvalidStep !== -1) { 630 e.preventDefault(); // prevent even preview from submitting 631 currentStep = firstInvalidStep; 632 highestStepReached = Math.max(highestStepReached, currentStep); 633 updateFormUI(); 634 smoothScrollToTop(); 635 636 const firstInvalidField = steps[firstInvalidStep][0].querySelector( 637 ".msf-input-error, .msf-gallery-error-message" 638 ); 639 if (firstInvalidField) firstInvalidField.focus({ preventScroll: true }); 640 // alert( 641 // "Please correct the errors on the highlighted step before continuing." 642 // ); 643 return; 644 } 645 646 // If valid 647 if (isPreview) { 648 showLoader(); 649 allSections.forEach((section) => { 650 section.classList.add("active", "msf-preview-hidden"); 651 }); 652 653 progressContainer.style.display = "none"; 654 navContainer.style.display = "none"; 655 656 console.log("Preview button clicked - showing all sections"); 657 return; 658 } 659 660 e.preventDefault(); 661 console.log("Form is valid. Submitting..."); 662 form.submit(); 663 }); 664 665 666 const previewButton = form.querySelector( 667 'button[name="preview_listing"], input[name="preview_listing"]' 668 ); 669 if (previewButton) { 670 previewButton.addEventListener("click", (e) => { 671 // Don't prevent default - let preview work normally 672 673 // Temporarily show all sections for preview 674 allSections.forEach((section) => { 675 section.classList.add("active"); 676 section.style.display = "block"; 677 }); 678 679 // Let the preview submission proceed without validation 680 console.log("Preview button clicked - showing all sections"); 681 }); 682 } 683 684 // Alternative: If preview button is identified differently, try this selector 685 const previewButtons = form.querySelectorAll( 686 'button[value*="preview"], input[value*="preview"], [name*="preview"]' 687 ); 688 previewButtons.forEach((button) => { 689 button.addEventListener("click", (e) => { 690 allSections.forEach((section) => { 691 section.classList.add("active"); 692 section.style.display = "block"; 693 }); 694 console.log("Preview button clicked - showing all sections"); 695 }); 696 }); 697 698 const urlParams = new URLSearchParams(window.location.search); 699 if (urlParams.get("action") === "edit") { 700 console.log("Edit mode detected. All steps will be marked as completed."); 701 highestStepReached = steps.length - 1; 702 } 703 updateBookingDependencies(); 704 updateFormUI(); 705 706 // Remove error styling when user selects a value in Select2 fields 707 document.addEventListener('change', function(e) { 708 const target = e.target; 709 710 // Check if the changed element is a required select with Select2 711 if (target.tagName === 'SELECT' && 712 target.hasAttribute('required') && 713 target.classList.contains('select2-hidden-accessible')) { 714 715 const value = target.value; 716 const container = target.nextElementSibling; 717 718 // If a valid value is selected, remove error styling 719 if (value && value !== '-1' && value !== '') { 720 target.classList.remove('msf-input-error'); 721 if (container && container.classList.contains('select2-container')) { 722 container.classList.remove('msf-input-error'); 723 } 724 } 725 } 726 }); 727 728 // Expose updateFormUI globally for coordination with frontend.js 729 window.listeoUpdateFormUI = updateFormUI; 730});
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.