1(function ($) { 2 if (!$) return; 3 4 // IITIX CSM v1.4.19: canonical Tickera seat identity + checkout menu autofill. 5 6 var CONFIG = window.IITIX_SEAT_FILTER_CONFIG || {}; 7 var EXTRA_CONFIG = window.IITIX_SEAT_EXTRA_FIELDS_CONFIG || {}; 8 var SEAT_FILTER_URL = CONFIG.seatFilterUrl || '/wp-json/tickera/v1/product-seat-filter'; 9 var DEBUG = !!CONFIG.debug; 10 var requestSeq = 0; 11 var extraBootTimer = null; 12 var isSeatModalClosing = false; 13 var seatModalClosingTimer = null; 14 var lastSeatModalCloseAt = 0; 15 var lastSeatVariationAjaxSentAt = 0; 16 17 function nowMs() { 18 return (new Date()).getTime(); 19 } 20 21 function log() { 22 if (!DEBUG || !window.console) return; 23 console.log.apply(console, arguments); 24 } 25 26 function warn() { 27 if (!window.console) return; 28 console.warn.apply(console, arguments); 29 } 30 31 function decodeHtml(raw) { 32 var txt = document.createElement('textarea'); 33 txt.innerHTML = raw || ''; 34 return txt.value; 35 } 36 37 function normalize(value) { 38 return String(value || '') 39 .trim() 40 .replace(/^['"]+|['"]+$/g, '') 41 .toLowerCase(); 42 } 43 44 function normalizeSeat(value) { 45 return String(value || '') 46 .trim() 47 .replace(/^['"]+|['"]+$/g, '') 48 .toUpperCase(); 49 } 50 51 // Tickera's modal title is rendered as "Seat FC4", while its checkout 52 // owner metadata uses the raw label "FC4". The visible prefix is not part 53 // of the mapping key; removing it keeps the chart + seat + label triple 54 // stable without ever falling back to attendee/index order. 55 function normalizeSeatIdentity(value) { 56 return normalizeSeat(value).replace(/^SEAT\s+/, ''); 57 } 58 59 function esc(value) { 60 return String(value == null ? '' : value) 61 .replace(/&/g, '&') 62 .replace(/</g, '<') 63 .replace(/>/g, '>') 64 .replace(/"/g, '"') 65 .replace(/'/g, '''); 66 } 67 68 /* ========================= 69 * Ambil seat number dari DOM 70 * ========================= */ 71 function getSeatNumberFromSeatId(seatId) { 72 try { 73 if (!seatId) return ''; 74 75 var el = document.getElementById(seatId); 76 if (!el) return ''; 77 78 var p = el.querySelector('span p'); 79 if (p && p.textContent) return normalizeSeat(p.textContent); 80 81 return normalizeSeat(el.textContent || ''); 82 } catch (e) { 83 return ''; 84 } 85 } 86 87 /* ========================= 88 * Parse query string / ajax body 89 * ========================= */ 90 function parseQS(qs) { 91 var obj = {}; 92 if (!qs || typeof qs !== 'string') return obj; 93 94 qs.split('&').forEach(function (part) { 95 if (!part) return; 96 97 var idx = part.indexOf('='); 98 var k = idx >= 0 ? part.slice(0, idx) : part; 99 var v = idx >= 0 ? part.slice(idx + 1) : ''; 100 101 k = decodeURIComponent(k.replace(/\+/g, ' ')); 102 v = decodeURIComponent(v.replace(/\+/g, ' ')); 103 104 obj[k] = v; 105 }); 106 107 return obj; 108 } 109 110 function getSeatModal() { 111 var modal = $('#tc-modal-woobridge'); 112 if (modal.length) return modal; 113 return $('.tc-modal-wrap').last(); 114 } 115 116 function getVariationForm() { 117 var modal = getSeatModal(); 118 var form = modal.find('.variations_form').first(); 119 if (form.length) return form; 120 return $('.variations_form').first(); 121 } 122 123 function isSeatModalActuallyVisible(modal) { 124 if (!modal || !modal.length) return false; 125 126 var dialog = modal.closest('.ui-dialog'); 127 128 if (!modal.is(':visible')) return false; 129 if (dialog.length && !dialog.is(':visible')) return false; 130 131 return true; 132 } 133 134 /* ========================= 135 * Ambil original variations JSON dari form 136 * Jangan filter dari hasil filter sebelumnya. 137 * ========================= */ 138 function getOriginalVariationsJSON() { 139 var form = getVariationForm(); 140 if (!form.length) return []; 141 142 var cached = form.data('iitixOriginalProductVariations'); 143 var raw = form.attr('data-product_variations'); 144 var parsed = []; 145 146 if (raw) { 147 try { 148 parsed = JSON.parse(decodeHtml(raw)); 149 } catch (e) { 150 warn('[IITIX] Parse variations error', e); 151 parsed = []; 152 } 153 } 154 155 // Keep the biggest/original payload to avoid cumulative filtering when another seat is selected. 156 if (!cached || (parsed.length && parsed.length >= cached.length)) { 157 form.data('iitixOriginalProductVariations', parsed); 158 return parsed; 159 } 160 161 return cached; 162 } 163 164 function getProductIdFromPage(payload) { 165 var form = getVariationForm(); 166 var modal = getSeatModal(); 167 var candidates = []; 168 169 if (payload) { 170 candidates.push(payload.product_id); 171 candidates.push(payload.tc_product_id); 172 candidates.push(payload.post_id); 173 } 174 175 if (form.length) { 176 candidates.push(form.data('product_id')); 177 candidates.push(form.attr('data-product_id')); 178 candidates.push(form.find('input[name="product_id"]').val()); 179 candidates.push(form.find('input[name="add-to-cart"]').val()); 180 } 181 182 candidates.push(modal.find('.tc_cart_button').data('product_id')); 183 candidates.push(modal.find('.tc_cart_button').attr('data-product_id')); 184 candidates.push(modal.find('.tc_cart_button[name="add-to-cart"]').val()); 185 candidates.push($('button[name="add-to-cart"]').val()); 186 candidates.push($('input[name="add-to-cart"]').val()); 187 188 var bodyClass = document.body && document.body.className ? document.body.className : ''; 189 var match = bodyClass.match(/(?:^|\s)postid-(\d+)(?:\s|$)/); 190 if (match) candidates.push(match[1]); 191 192 for (var i = 0; i < candidates.length; i++) { 193 var n = parseInt(candidates[i], 10); 194 if (n > 0) return n; 195 } 196 197 return 0; 198 } 199 200 function fetchSeatFilter(productId, chartId, seatNumber) { 201 return fetch(SEAT_FILTER_URL, { 202 method: 'POST', 203 credentials: 'same-origin', 204 headers: { 205 'Content-Type': 'application/json', 206 'Accept': 'application/json', 207 'X-WP-Nonce': CONFIG.nonce || '' 208 }, 209 body: JSON.stringify({ 210 product_id: parseInt(productId, 10), 211 chart_id: parseInt(chartId, 10), 212 seat_label: normalizeSeat(seatNumber) 213 }) 214 }).then(function (response) { 215 return response.json().catch(function () { 216 return {}; 217 }).then(function (json) { 218 if (!response.ok) { 219 var message = json && json.message ? json.message : ('Seat filter API error ' + response.status); 220 throw new Error(message); 221 } 222 return json; 223 }); 224 }); 225 } 226 227 function getVariationIdsFromApi(apiResponse) { 228 var ids = {}; 229 var groups = apiResponse && Array.isArray(apiResponse.data) ? apiResponse.data : []; 230 231 groups.forEach(function (group) { 232 var options = Array.isArray(group.options) ? group.options : []; 233 options.forEach(function (option) { 234 if (option && option.variation_id) { 235 ids[String(option.variation_id)] = true; 236 } 237 }); 238 }); 239 240 return ids; 241 } 242 243 function getAllowedValuesFromApi(apiResponse) { 244 var values = {}; 245 var categories = Array.isArray(apiResponse && apiResponse.categories) ? apiResponse.categories : []; 246 var groups = apiResponse && Array.isArray(apiResponse.data) ? apiResponse.data : []; 247
248 categories.forEach(function (category) { 249 values[normalize(category)] = true; 250 }); 251 252 groups.forEach(function (group) { 253 var options = Array.isArray(group.options) ? group.options : []; 254 options.forEach(function (option) { 255 if (option && option.value) { 256 values[normalize(option.value)] = true; 257 } 258 }); 259 }); 260 261 return values; 262 } 263 264 function objectHasKeys(obj) { 265 return Object.keys(obj || {}).length > 0; 266 } 267 268 function filterVariationsByApi(variations, apiResponse) { 269 var ids = getVariationIdsFromApi(apiResponse); 270 var allowedValues = getAllowedValuesFromApi(apiResponse); 271 272 if (objectHasKeys(ids)) { 273 return variations.filter(function (variation) { 274 return ids[String(variation.variation_id)] === true; 275 }); 276 } 277 278 // Defensive fallback if API returns categories but no variation_id. 279 if (objectHasKeys(allowedValues)) { 280 return variations.filter(function (variation) { 281 var attrs = variation.attributes || {}; 282 return Object.keys(attrs).some(function (key) { 283 return allowedValues[normalize(attrs[key])] === true; 284 }); 285 }); 286 } 287 288 return []; 289 } 290 291 function getReadableCategories(apiResponse) { 292 if (Array.isArray(apiResponse && apiResponse.categories)) { 293 return apiResponse.categories; 294 } 295 296 var out = []; 297 var groups = apiResponse && Array.isArray(apiResponse.data) ? apiResponse.data : []; 298 groups.forEach(function (group) { 299 var options = Array.isArray(group.options) ? group.options : []; 300 options.forEach(function (option) { 301 if (option && option.value && out.indexOf(option.value) === -1) { 302 out.push(option.value); 303 } 304 }); 305 }); 306 return out; 307 } 308 309 function applySeatFilterModalClass(form) { 310 var modal = getSeatModal(); 311 if (!modal.length) return; 312 313 var dialog = modal.closest('.ui-dialog'); 314 modal.addClass('iitix-seat-filter-modal'); 315 if (dialog.length) dialog.addClass('iitix-seat-filter-dialog'); 316 317 if (form && form.length) { 318 form.addClass('iitix-seat-filter-form'); 319 } 320 } 321 322 function setSeatFilterModalState(form, state) { 323 var modal = getSeatModal(); 324 if (!modal.length) return; 325 326 var dialog = modal.closest('.ui-dialog'); 327 var classes = 'iitix-seat-filter-loading iitix-seat-filter-ready iitix-seat-filter-error'; 328 329 applySeatFilterModalClass(form); 330 modal.removeClass(classes); 331 if (dialog.length) dialog.removeClass(classes); 332 333 if (state) { 334 modal.addClass('iitix-seat-filter-' + state); 335 if (dialog.length) dialog.addClass('iitix-seat-filter-' + state); 336 } 337 } 338 339 function applySeatFilterModalShellLoading() { 340 if (isSeatModalClosing) return; 341 342 var modal = getSeatModal(); 343 if (!modal.length) return; 344 345 var dialog = modal.closest('.ui-dialog'); 346 modal 347 .addClass('iitix-seat-filter-modal iitix-seat-filter-loading') 348 .removeClass('iitix-seat-filter-ready iitix-seat-filter-error'); 349 350 if (dialog.length) { 351 dialog 352 .addClass('iitix-seat-filter-dialog iitix-seat-filter-loading') 353 .removeClass('iitix-seat-filter-ready iitix-seat-filter-error'); 354 } 355 } 356 357 function clearSeatFilterModalState() { 358 var modal = getSeatModal(); 359 var dialog = modal.length ? modal.closest('.ui-dialog') : $('.ui-dialog.iitix-seat-filter-dialog'); 360 var classes = 'iitix-seat-filter-modal iitix-seat-filter-dialog iitix-seat-filter-loading iitix-seat-filter-ready iitix-seat-filter-error'; 361 362 if (modal.length) { 363 modal.removeClass(classes); 364 modal.find('form.variations_form').removeClass('iitix-seat-filter-form'); 365 } 366 367 if (dialog.length) { 368 dialog.removeClass(classes); 369 } 370 } 371 372 function setSeatFilterControlsLoading(form, isLoading) { 373 if (!form || !form.length) return; 374 375 form.find('select[name^="attribute_"]').each(function () { 376 var select = $(this); 377 378 if (isLoading) { 379 if (typeof select.data('iitix-seat-filter-was-disabled') === 'undefined') { 380 select.data('iitix-seat-filter-was-disabled', select.prop('disabled')); 381 } 382 383 select.prop('disabled', true); 384 select.addClass('iitix-seat-filter-control-loading'); 385 return; 386 } 387 388 if (select.hasClass('iitix-seat-filter-control-loading')) { 389 var wasDisabled = select.data('iitix-seat-filter-was-disabled') === true; 390 select.prop('disabled', wasDisabled); 391 select.removeClass('iitix-seat-filter-control-loading'); 392 select.removeData('iitix-seat-filter-was-disabled'); 393 } 394 }); 395 } 396 397 function ensureNotice(form) { 398 var notice = form.find('.iitix-seat-filter-notice'); 399 if (notice.length) return notice; 400
401 notice = $('<div class="iitix-seat-filter-notice" aria-live="polite" />'); 402 403 var table = form.find('.variations').first(); 404 if (table.length) { 405 notice.insertBefore(table); 406 } else { 407 form.prepend(notice); 408 } 409 410 return notice; 411 } 412 413 function renderCategoryChips(categories) { 414 if (!categories || !categories.length) return ''; 415 416 return categories.map(function (category) { 417 return '<span class="iitix-category-chip">' + esc(category) + '</span>'; 418 }).join(''); 419 } 420 421 function renderNotice(form, state) { 422 var notice = ensureNotice(form); 423 var categories = state.categories || []; 424 var variationCount = state.variationCount || 0; 425 var seat = state.seat || ''; 426 427 applySeatFilterModalClass(form); 428 notice.removeClass('is-loading is-ready is-error'); 429 430 if (state.loading) { 431 setSeatFilterModalState(form, 'loading'); 432 setSeatFilterControlsLoading(form, true); 433 434 notice.addClass('is-loading'); 435 notice.html( 436 '<div class="iitix-seat-filter-skeleton-card">' + 437 '<div class="iitix-shimmer-line iitix-shimmer-line-title"></div>' + 438 '<div class="iitix-shimmer-line iitix-shimmer-line-wide"></div>' + 439 '<div class="iitix-shimmer-line iitix-shimmer-line-short"></div>' + 440 '</div>' + 441 '<div class="iitix-seat-filter-loading-text">Checking available categories for Seat ' + esc(seat) + '...</div>' 442 ); 443 return; 444 } 445 446 if (state.error) { 447 setSeatFilterModalState(form, 'error'); 448 setSeatFilterControlsLoading(form, false); 449 450 notice.addClass('is-error'); 451 notice.html( 452 '<div class="iitix-seat-filter-kicker">Seat ' + esc(seat) + '</div>' + 453 '<div class="iitix-seat-filter-title">Category check failed</div>' + 454 '<div class="iitix-seat-filter-desc">Please try selecting the seat again. ' + esc(state.error) + '</div>' 455 ); 456 return; 457 } 458 459 setSeatFilterModalState(form, 'ready'); 460 461 if (!categories.length || !variationCount) { 462 notice.addClass('is-error'); 463 notice.html( 464 '<div class="iitix-seat-filter-kicker">Seat ' + esc(seat) + '</div>' + 465 '<div class="iitix-seat-filter-title">No available category</div>' + 466 '<div class="iitix-seat-filter-desc">This seat has no available category/variation from the API. Please choose another seat or update the chart rules.</div>' 467 ); 468 return; 469 } 470 471 notice.addClass('is-ready'); 472 notice.html( 473 '<div class="iitix-seat-filter-head">' + 474 '<div>' + 475 '<div class="iitix-seat-filter-kicker">Seat ' + esc(seat) + '</div>' + 476 '<div class="iitix-seat-filter-title">Available categories</div>' + 477 '</div>' + 478 '<span class="iitix-seat-filter-count">' + esc(categories.length) + ' option' + (categories.length > 1 ? 's' : '') + '</span>' + 479 '</div>' + 480 '<div class="iitix-seat-filter-chips">' + renderCategoryChips(categories) + '</div>' 481 ); 482 } 483 484 function syncSelectOptionsWithApi(form, apiResponse) { 485 var allowedValues = getAllowedValuesFromApi(apiResponse); 486 var hasAllowed = objectHasKeys(allowedValues); 487 488 form.find('select[name^="attribute_"]').each(function () { 489 var select = $(this); 490 var current = normalize(select.val()); 491 var currentAllowed = false; 492 var enabledCount = 0; 493 var firstAllowedValue = ''; 494 495 select.find('option').each(function () { 496 var option = $(this); 497 var rawValue = option.val(); 498 var value = normalize(rawValue); 499 var text = normalize(option.text()); 500 501 if (!value) { 502 option.prop('disabled', false).show(); 503 if (hasAllowed) option.text('Choose a category'); 504 return; 505 } 506 507 var allowed = hasAllowed && (allowedValues[value] === true || allowedValues[text] === true); 508 option.prop('disabled', !allowed); 509 option.toggle(allowed); 510 511 if (allowed) { 512 enabledCount++; 513 if (!firstAllowedValue) firstAllowedValue = rawValue; 514 if (current && normalize(current) === value) currentAllowed = true; 515 } 516 }); 517 518 if (!hasAllowed || !enabledCount) { 519 select.val(''); 520 } else if (enabledCount === 1 && firstAllowedValue) { 521 // Important: category-only seats often return exactly 1 category from 522 // /product-seat-filter. Auto-select it so the Add to Cart button can 523 // become active without making the user open an empty-looking dropdown. 524 select.val(firstAllowedValue); 525 } else if (current && !currentAllowed) { 526 select.val(''); 527 } 528 529 select.prop('disabled', !enabledCount); 530 select.trigger('change'); 531 }); 532 } 533 534 function applySingleVariationFallback(form, filteredVariations) { 535 if (!form || !form.length) return false;
536 if (!Array.isArray(filteredVariations) || filteredVariations.length !== 1) return false; 537 538 var variation = filteredVariations[0] || {}; 539 var attrs = variation.attributes || {}; 540 var applied = false; 541 542 Object.keys(attrs).forEach(function (key) { 543 var value = attrs[key]; 544 if (!value) return; 545 546 var select = form.find('select[name="' + key + '"], select[data-attribute_name="' + key + '"]').first(); 547 if (!select.length) return; 548 549 if (select.val() !== value) { 550 select.val(value); 551 applied = true; 552 } 553 }); 554 555 if (variation.variation_id) { 556 form.find('input.variation_id').val(variation.variation_id).trigger('change'); 557 applied = true; 558 } 559 560 form.find('select[name^="attribute_"]').trigger('change'); 561 form.trigger('found_variation', [variation]); 562 form.trigger('check_variations'); 563 564 stabilizeValidVariationUi(form); 565 scheduleStabilizeValidVariationUi(form); 566 567 // Only unlock the button automatically for normal category-only products. 568 // Whitelisted products still need Menu / Appliance / Watt validation. 569 if (!currentSeatAllowsExtraFields(form) && variation.variation_id) { 570 form.find('.tc_cart_button, .single_add_to_cart_button').first() 571 .removeClass('disabled wc-variation-selection-needed iitix-extra-invalid') 572 .prop('disabled', false) 573 .attr('aria-disabled', 'false') 574 .removeAttr('title'); 575 } 576 577 return applied; 578 } 579 580 581 function forceShowVariationControlsForValidSeat(form) { 582 if (!form || !form.length) return; 583 584 var variationId = $.trim(form.find('input.variation_id').val() || ''); 585 if (!variationId) return; 586 587 var table = form.find('table.variations').first(); 588 if (table.length) { 589 table.css('display', 'table').show(); 590 table.find('tr').css('display', 'table-row').show(); 591 table.find('th,td').css('display', 'table-cell').show(); 592 } 593 594 form.find('select[name^="attribute_"]').each(function () { 595 $(this) 596 .prop('disabled', false) 597 .removeClass('disabled') 598 .css('display', '') 599 .show(); 600 }); 601 } 602 603 function normalizeValidVariationButton(form) { 604 if (!form || !form.length) return; 605 606 var variationId = $.trim(form.find('input.variation_id').val() || ''); 607 if (!variationId) return; 608 609 var btn = form.find('.tc_cart_button, .single_add_to_cart_button').first(); 610 if (!btn.length) return; 611 612 var text = $.trim(btn.text() || '').toLowerCase(); 613 if (text.indexOf('currently not available') !== -1 || text.indexOf('not available') !== -1) { 614 btn.text('Add to Cart'); 615 } 616 617 btn.removeClass('tc-seat-unavailable tc_seat_unavailable seat-unavailable unavailable out-of-stock'); 618 btn.removeAttr('data-seat-unavailable data-unavailable'); 619 } 620 621 function stabilizeValidVariationUi(form) { 622 forceShowVariationControlsForValidSeat(form); 623 normalizeValidVariationButton(form); 624 } 625 626 function scheduleStabilizeValidVariationUi(form) { 627 if (!form || !form.length) return; 628 [20, 80, 180, 360, 700, 1200].forEach(function (delay) { 629 window.setTimeout(function () { 630 stabilizeValidVariationUi(form); 631 }, delay); 632 }); 633 } 634 635 function updateVariationsForm(filteredVariations, apiResponse, meta) { 636 var form = getVariationForm(); 637 if (!form.length) return; 638 639 form.attr('data-product_variations', JSON.stringify(filteredVariations)); 640 form.data('product_variations', filteredVariations); 641 642 setSeatFilterControlsLoading(form, false); 643 644 if ($.fn.wc_variation_form) { 645 form.wc_variation_form(); 646 form.trigger('reload_product_variations'); 647 form.trigger('reset_data'); 648 } 649 650 syncSelectOptionsWithApi(form, apiResponse); 651 applySingleVariationFallback(form, filteredVariations); 652 stabilizeValidVariationUi(form); 653 scheduleStabilizeValidVariationUi(form); 654 655 if ($.fn.wc_variation_form) { 656 form.trigger('check_variations'); 657 } 658 659 renderNotice(form, { 660 seat: meta.seatNumber, 661 categories: getReadableCategories(apiResponse), 662 variationCount: filteredVariations.length 663 }); 664 665 updateExtraFieldsVisibility(form); 666 validateExtraFields(form, false); 667 668 // WooCommerce can re-render variation state a few ms after reload_product_variations. 669 // Re-apply the single allowed category once more so category-only seats do not 670 // fall back to a blank dropdown / disabled Add to Cart button. 671 window.setTimeout(function () { 672 syncSelectOptionsWithApi(form, apiResponse); 673 applySingleVariationFallback(form, filteredVariations); 674 stabilizeValidVariationUi(form); 675 if ($.fn.wc_variation_form) form.trigger('check_variations'); 676 validateExtraFields(form, false); 677 }, 40); 678 679 log('[IITIX] variations updated from API', { 680 api: apiResponse, 681 filteredVariations: filteredVariations 682 }); 683 } 684 685 function restoreOriginalVariations() { 686 var form = getVariationForm(); 687 if (!form.length) return; 688 689 var original = form.data('iitixOriginalProductVariations') || []; 690 if (!original.length) return; 691 692 form.attr('data-product_variations', JSON.stringify(original)); 693 form.data('product_variations', original); 694 695 form.find('select[name^="attribute_"]').each(function () { 696 $(this).prop('disabled', false).find('option').prop('disabled', false).show(); 697 }); 698 699 if ($.fn.wc_variation_form) { 700 form.wc_variation_form(); 701 form.trigger('reload_product_variations'); 702 form.trigger('reset_data'); 703 } 704 } 705 706 function updateSeatLabel(seatNumber) { 707 if (!seatNumber) return; 708 $('#tc-modal-woobridge .tc_regular_modal_seat_label').text('Seat ' + seatNumber); 709 } 710 711 function shouldHandleTickeraVariationAjax(settings) {
712 if (!settings || !settings.data || typeof settings.data !== 'string') return false; 713 return settings.data.indexOf('action=tc_seat_chart_get_wc_variations') !== -1; 714 } 715 716 /* ========================= 717 * Extra fields: Menu + Appliance + Wattage 718 * ========================= */ 719 function normalizeCategoryProfileKey(value) { 720 return normalize(value) 721 .replace(/[^a-z0-9]+/g, '-') 722 .replace(/^-+|-+$/g, ''); 723 } 724 725 function getCategoryAttributeName() { 726 var attributeName = $.trim(String(EXTRA_CONFIG.categoryAttributeName || 'attribute_categories')); 727 return attributeName || 'attribute_categories'; 728 } 729 730 function getProfilesByCategory() { 731 var profiles = EXTRA_CONFIG.profilesByCategory || EXTRA_CONFIG.categoryProfiles || {}; 732 return profiles && typeof profiles === 'object' ? profiles : {}; 733 } 734 735 function getSeatCategoryProfileOverrides() { 736 var overrides = EXTRA_CONFIG.seatCategoryProfileOverrides || {}; 737 return overrides && typeof overrides === 'object' ? overrides : {}; 738 } 739 740 function getSeatCategoryProfileOverride(form, categoryKey) { 741 if (!categoryKey) return null; 742 743 var context = getIitixSeatContextPayload(form || getVariationForm()); 744 var chartId = $.trim(String(context && context.chartId || '')); 745 var seatLabel = normalizeSeatIdentity(context && context.seatLabel || ''); 746 var overrides = getSeatCategoryProfileOverrides(); 747 748 if (!chartId || !seatLabel || !overrides[chartId] || !overrides[chartId][seatLabel]) { 749 return null; 750 } 751 752 var seatOverrides = overrides[chartId][seatLabel]; 753 if (seatOverrides && Object.prototype.hasOwnProperty.call(seatOverrides, categoryKey)) { 754 return seatOverrides[categoryKey]; 755 } 756 757 return null; 758 } 759 760 function getCurrentExtraCategoryValue(form) { 761 form = form && form.length ? form : getVariationForm(); 762 763 var attributeName = getCategoryAttributeName(); 764 var selector = 'select[name="' + attributeName + '"], select[data-attribute_name="' + attributeName + '"]'; 765 var select = form && form.length ? form.find(selector).first() : $(); 766 767 // Tickera can briefly move/replace the select while rebuilding the variation 768 // form. Fall back to the active modal but never infer from product ID. 769 if (!select.length) { 770 select = getSeatModal().find(selector).first(); 771 } 772 773 return select.length ? $.trim(String(select.val() || '')) : ''; 774 } 775 776 function getCurrentExtraProfile(form) { 777 form = form && form.length ? form : getVariationForm(); 778 var categoryValue = getCurrentExtraCategoryValue(form); 779 var categoryKey = normalizeCategoryProfileKey(categoryValue); 780 var profiles = getProfilesByCategory(); 781 var seatOverride = getSeatCategoryProfileOverride(form, categoryKey); 782 783 if (seatOverride) return seatOverride; 784 785 if (categoryKey && Object.prototype.hasOwnProperty.call(profiles, categoryKey)) { 786 return profiles[categoryKey]; 787 } 788 789 return null; 790 } 791 792 function getProfileSections(profile) { 793 return profile && Array.isArray(profile.sections) ? profile.sections : []; 794 } 795 796 function getProfileSection(profile, matcher) { 797 var sections = getProfileSections(profile); 798 for (var i = 0; i < sections.length; i++) { 799 if (matcher(sections[i] || {})) return sections[i]; 800 } 801 return null; 802 } 803 804 function getProfileSectionByType(profile, type) { 805 return getProfileSection(profile, function (section) { 806 return section && section.type === type; 807 }); 808 } 809 810 function getProfileSectionByKey(profile, key) { 811 return getProfileSection(profile, function (section) { 812 return section && section.key === key; 813 }); 814 } 815 816 function getProfileField(profile, fieldName) { 817 var sections = getProfileSections(profile); 818 for (var i = 0; i < sections.length; i++) { 819 var fields = Array.isArray(sections[i].fields) ? sections[i].fields : []; 820 for (var j = 0; j < fields.length; j++) { 821 if (fields[j] && fields[j].name === fieldName) return fields[j]; 822 } 823 } 824 return null; 825 } 826 827 function getActiveApplianceSection(form) { 828 var profile = getCurrentExtraProfile(form || getVariationForm()); 829 return getProfileSectionByType(profile, 'appliance_slots'); 830 } 831 832 function getExtraMaxWattage(form) { 833 var profile = getCurrentExtraProfile(form || getVariationForm()); 834 var max = parseInt(profile && profile.max_total_wattage, 10); 835 if (max > 0) return max; 836 837 var section = getActiveApplianceSection(form || getVariationForm()); 838 max = parseInt(section && section.max_total_wattage, 10); 839 if (max > 0) return max; 840 841 max = parseInt(EXTRA_CONFIG.maxTotalWattage, 10); 842 return max > 0 ? max : 1000; 843 } 844 845 function getExtraCopy(key, fallback) { 846 var profile = getCurrentExtraProfile(getVariationForm()); 847 var copy = profile && profile.copy && typeof profile.copy === 'object' ? profile.copy : (EXTRA_CONFIG.copy || {}); 848 849 if (copy && typeof copy[key] !== 'undefined') return copy[key]; 850 851 if (profile) { 852 if (key === 'title' && profile.title) return profile.title; 853 if (key === 'description' && profile.description) return profile.description; 854 855 var menuField = getProfileField(profile, 'iitix_menu'); 856 if (menuField) { 857 if (key === 'menuLabel' && menuField.label) return menuField.label; 858 if (key === 'menuHelp' && menuField.help) return menuField.help; 859 if (key === 'menuPlaceholder' && menuField.placeholder) return menuField.placeholder; 860 } 861 862 var applianceSection = getProfileSectionByType(profile, 'appliance_slots'); 863 if (applianceSection) { 864 if (key === 'applianceIntro' && applianceSection.help) return applianceSection.help;
865 if (key === 'appliance1Label' && applianceSection.appliance_1_label) return applianceSection.appliance_1_label; 866 if (key === 'appliance2Label' && applianceSection.appliance_2_label) return applianceSection.appliance_2_label; 867 } 868 869 var guidelineSection = getProfileSectionByType(profile, 'guidelines'); 870 if (key === 'guidelineTitle' && guidelineSection && guidelineSection.title) return guidelineSection.title; 871 } 872 873 var legacyCopy = EXTRA_CONFIG.copy || {}; 874 return legacyCopy[key] || fallback; 875 } 876 877 function getExtraCopyArray(key, fallback) { 878 var profile = getCurrentExtraProfile(getVariationForm()); 879 880 if (profile) { 881 var guidelineSection = getProfileSectionByType(profile, 'guidelines'); 882 if (key === 'guidelines' && guidelineSection && Array.isArray(guidelineSection.items)) return guidelineSection.items; 883 } 884 885 var copy = profile && profile.copy && typeof profile.copy === 'object' ? profile.copy : (EXTRA_CONFIG.copy || {}); 886 if (Array.isArray(copy[key])) return copy[key]; 887 888 var legacyCopy = EXTRA_CONFIG.copy || {}; 889 return Array.isArray(legacyCopy[key]) ? legacyCopy[key] : (Array.isArray(fallback) ? fallback : []); 890 } 891 892 function formatExtraCopy(template, replacements) { 893 template = String(template || ''); 894 replacements = replacements || {}; 895 896 Object.keys(replacements).forEach(function (key) { 897 var pattern = new RegExp('\\{' + key + '\\}', 'g'); 898 template = template.replace(pattern, String(replacements[key])); 899 }); 900 901 return template; 902 } 903 904 function normalizeApplianceKey(value) { 905 return normalize(value) 906 .replace(/[^a-z0-9]+/g, '_') 907 .replace(/^_+|_+$/g, ''); 908 } 909 910 function getApplianceMinWatt(type) { 911 if (!type || normalize(type) === 'other' || normalize(type) === 'others') return 0; 912 913 var section = getActiveApplianceSection(getVariationForm()); 914 var map = section && section.options && typeof section.options === 'object' 915 ? section.options 916 : (EXTRA_CONFIG.applianceMinWatts || EXTRA_CONFIG.applianceMinimumWatts || {}); 917 918 if (!map || typeof map !== 'object') return 0; 919 920 if (Object.prototype.hasOwnProperty.call(map, type)) { 921 var direct = parseInt(map[type], 10); 922 return direct > 0 ? direct : 0; 923 } 924 925 var wanted = normalizeApplianceKey(type); 926 var keys = Object.keys(map); 927 for (var i = 0; i < keys.length; i++) { 928 if (normalizeApplianceKey(keys[i]) === wanted) { 929 var n = parseInt(map[keys[i]], 10); 930 return n > 0 ? n : 0; 931 } 932 } 933 934 return 0; 935 } 936 937 function getEffectiveApplianceWatt(type, wattage) { 938 wattage = parseInt(wattage, 10); 939 wattage = isNaN(wattage) || wattage < 0 ? 0 : wattage; 940 if (wattage <= 0) return 0; 941 942 var min = getApplianceMinWatt(type); 943 return min > 0 ? Math.max(wattage, min) : wattage; 944 } 945 946 function getEffectiveTotalWattage(data) { 947 return getEffectiveApplianceWatt(data.appliance1Type, data.appliance1Wattage) + 948 getEffectiveApplianceWatt(data.appliance2Type, data.appliance2Wattage); 949 } 950 951 function getEnabledCategoryProfileMap() { 952 var profiles = getProfilesByCategory(); 953 var map = {}; 954 955 Object.keys(profiles).forEach(function (key) { 956 var normalizedKey = normalizeCategoryProfileKey(key); 957 if (normalizedKey) map[normalizedKey] = true; 958 }); 959 960 return map; 961 } 962 963 function getCurrentModalProductId(form) { 964 var candidates = []; 965 966 if (form && form.length) { 967 candidates.push(form.data('iitix-current-product-id')); 968 candidates.push(form.data('product_id')); 969 candidates.push(form.attr('data-product_id')); 970 candidates.push(form.find('input[name="product_id"]').val()); 971 candidates.push(form.find('input[name="add-to-cart"]').val()); 972 } 973 974 var modal = getSeatModal(); 975 candidates.push(modal.find('.tc_regular_modal_ticket_type_id').val()); 976 candidates.push(modal.find('.tc_cart_button').data('product_id')); 977 candidates.push(modal.find('.tc_cart_button').attr('data-product_id')); 978 979 // Do not use previous window.iitixSeatCategoryState as the main source here. 980 // That state can belong to the previously selected seat/product and can make 981 // a normal category-only product look like a whitelisted extra-fields product. 982 if (!form || !form.length) { 983 var state = window.iitixSeatCategoryState || {}; 984 candidates.push(state.product_id); 985 } 986 987 for (var i = 0; i < candidates.length; i++) { 988 var n = parseInt(candidates[i], 10); 989 if (n > 0) return n; 990 } 991 992 return getProductIdFromPage(); 993 } 994 995 function currentSeatAllowsExtraFields(form) { 996 if (EXTRA_CONFIG.enabled === false) return false;
997 998 // Extra sections are category-driven. A product may expose many category 999 // variation values; only the currently selected mapped category gets a form. 1000 var selectedCategoryKey = normalizeCategoryProfileKey(getCurrentExtraCategoryValue(form)); 1001 if (!selectedCategoryKey) return false; 1002 1003 var enabledCategories = getEnabledCategoryProfileMap(); 1004 return !!enabledCategories[selectedCategoryKey] && !!getCurrentExtraProfile(form); 1005 } 1006 1007 function ensureExtraFieldsStyle() { 1008 // Styles are loaded from assets/seat-number-injector.css. 1009 return; 1010 } 1011 1012 function getApplianceOptionLabel(option) { 1013 var minWatt = getApplianceMinWatt(option); 1014 return minWatt > 0 ? (option + ' (' + minWatt + ' watt)') : option; 1015 } 1016 1017 function getApplianceOptionsHtml() { 1018 var section = getActiveApplianceSection(getVariationForm()); 1019 var options = []; 1020 1021 if (section && section.options && typeof section.options === 'object') { 1022 options = Object.keys(section.options); 1023 } else { 1024 options = Array.isArray(EXTRA_CONFIG.applianceOptions) ? EXTRA_CONFIG.applianceOptions : []; 1025 if (!options.length && EXTRA_CONFIG.applianceMinWatts && typeof EXTRA_CONFIG.applianceMinWatts === 'object') { 1026 options = Object.keys(EXTRA_CONFIG.applianceMinWatts); 1027 } 1028 } 1029 1030 if (!options.length) { 1031 options = ['Microwave', 'Convection oven', 'Induction Stove', 'Rice cooker', 'Air fryer', 'Fridge', 'Chiller', 'Display fridge under', 'Other']; 1032 } 1033 1034 var html = '<option value="">Choose appliance</option>'; 1035 options.forEach(function (option) { 1036 if (option && typeof option === 'object') option = option.label || option.name || option.value || ''; 1037 if (!option) return; 1038 html += '<option value="' + esc(option) + '">' + esc(getApplianceOptionLabel(option)) + '</option>'; 1039 }); 1040 return html; 1041 } 1042 1043 function getSectionStepHtml(section, fallbackNumber, fallbackTitle, fallbackBadge, fallbackClass) { 1044 section = section || {}; 1045 var number = section.step_number || fallbackNumber; 1046 var title = section.step_title || fallbackTitle; 1047 var badge = section.step_badge || fallbackBadge || 'Required'; 1048 var klass = section.heading_class || fallbackClass || 'iitix-step-heading'; 1049 var badgeClass = normalize(String(badge || '')) === 'optional' ? ' is-optional' : ''; 1050 1051 if (!number && !title) return ''; 1052 1053 return '' + 1054 '<div class="' + esc(klass) + '">' + 1055 '<span class="iitix-step-number">' + esc(number) + '</span>' + 1056 '<span class="iitix-step-body">' + 1057 '<span class="iitix-step-title">' + esc(title) + '</span>' + 1058 '<span class="iitix-step-badge' + badgeClass + '">' + esc(badge) + '</span>' + 1059 '</span>' + 1060 '</div>'; 1061 } 1062 1063 function getIitixFilePlaceholderHtml(field) { 1064 field = field || {}; 1065 var title = field.empty_preview_title || 'No image yet'; 1066 var subtitle = field.empty_preview_subtitle || 'Preview here'; 1067 var icon = field.icon || '🖼'; 1068 1069 return '' + 1070 '<div class="iitix-file-placeholder">' + 1071 '<span class="iitix-file-placeholder-icon">' + esc(icon) + '</span>' + 1072 '<strong>' + esc(title) + '</strong>' + 1073 '<small>' + esc(subtitle) + '</small>' + 1074 '</div>'; 1075 } 1076 1077 function renderProfileFieldInput(field) { 1078 field = field || {}; 1079 var type = field.type || 'text'; 1080 var name = field.name || ''; 1081 var inputClass = field.input_class || ''; 1082 var placeholder = field.placeholder || ''; 1083 var required = field.required ? ' data-iitix-required="1"' : ''; 1084 1085 if (!name) return ''; 1086 1087 if (type === 'textarea') { 1088 return '<textarea id="' + esc(name) + '" name="' + esc(name) + '" class="' + esc(inputClass) + '" data-iitix-field-name="' + esc(name) + '" placeholder="' + esc(placeholder) + '"' + required + '></textarea>'; 1089 } 1090 1091 if (type === 'file') { 1092 var accept = field.accept || 'image/png,image/jpeg,image/webp'; 1093 var maxSize = field.max_size_mb || 10; 1094 var minWidth = field.min_width || 0; 1095 var minHeight = field.min_height || 0; 1096 var aspectRatio = field.aspect_ratio || '';
1097 var aspectRatioTolerance = field.aspect_ratio_tolerance || ''; 1098 var fileTypesLabel = field.file_types_label || 'PNG, JPG, or WebP'; 1099 var uploadButtonLabel = field.placeholder || 'Upload image'; 1100 var fileUiClass = field.file_ui_class || ''; 1101 return '' + 1102 '<div class="iitix-file-upload ' + esc(fileUiClass) + '" data-iitix-file-upload data-iitix-field-name="' + esc(name) + '" data-iitix-max-size-mb="' + esc(maxSize) + '" data-iitix-min-width="' + esc(minWidth) + '" data-iitix-min-height="' + esc(minHeight) + '" data-iitix-aspect-ratio="' + esc(aspectRatio) + '" data-iitix-aspect-ratio-tolerance="' + esc(aspectRatioTolerance) + '" data-iitix-file-types-label="' + esc(fileTypesLabel) + '" data-iitix-empty-preview-title="' + esc(field.empty_preview_title || 'No image yet') + '" data-iitix-empty-preview-subtitle="' + esc(field.empty_preview_subtitle || 'Preview here') + '" data-iitix-empty-preview-icon="' + esc(field.icon || '🖼') + '">' + 1103 '<input type="file" id="' + esc(name) + '" name="' + esc(name) + '" class="iitix-file-input ' + esc(inputClass) + '" accept="' + esc(accept) + '" data-iitix-field-name="' + esc(name) + '"' + required + '>' + 1104 '<div class="iitix-file-preview is-empty" data-iitix-file-preview>' + getIitixFilePlaceholderHtml(field) + '</div>' + 1105 '<div class="iitix-file-actions">' + 1106 '<button type="button" class="iitix-file-trigger" data-iitix-file-trigger>' + esc(uploadButtonLabel) + '</button>' + 1107 '<button type="button" class="iitix-file-change" data-iitix-file-trigger>Change Image</button>' + 1108 '</div>' + 1109 '<div class="iitix-file-name" data-iitix-file-name></div>' + 1110 '</div>'; 1111 } 1112 1113 if (type === 'radio') { 1114 var options = field.options && typeof field.options === 'object' ? field.options : { no: 'No', yes: 'Yes' }; 1115 var defaultValue = typeof field.default !== 'undefined' ? String(field.default) : ''; 1116 var radioHtml = '<div class="iitix-radio-group" data-iitix-radio-group="' + esc(name) + '">'; 1117 Object.keys(options).forEach(function (value) { 1118 var id = name + '_' + value; 1119 var checked = String(value) === defaultValue ? ' checked' : ''; 1120 radioHtml += '<label class="iitix-radio-card"><input type="radio" id="' + esc(id) + '" name="' + esc(name) + '" value="' + esc(value) + '" class="' + esc(inputClass) + '" data-iitix-field-name="' + esc(name) + '"' + required + checked + '> <span>' + esc(options[value]) + '</span></label>'; 1121 }); 1122 radioHtml += '</div>'; 1123 return radioHtml; 1124 } 1125 1126 if (type === 'select') { 1127 var opts = field.options && typeof field.options === 'object' ? field.options : {}; 1128 var defaultOpt = typeof field.default !== 'undefined' ? String(field.default) : ''; 1129 var selectHtml = '<select id="' + esc(name) + '" name="' + esc(name) + '" class="' + esc(inputClass) + '" data-iitix-field-name="' + esc(name) + '"' + required + '>'; 1130 Object.keys(opts).forEach(function (value) { 1131 selectHtml += '<option value="' + esc(value) + '"' + (String(value) === defaultOpt ? ' selected' : '') + '>' + esc(opts[value]) + '</option>'; 1132 }); 1133 selectHtml += '</select>'; 1134 return selectHtml; 1135 } 1136 1137 if (type === 'checkbox') { 1138 return '<label class="iitix-checkbox-field"><input type="checkbox" name="' + esc(name) + '" value="1" class="' + esc(inputClass) + '" data-iitix-field-name="' + esc(name) + '"' + required + '> <span>' + esc(field.checkbox_label || field.label || '') + '</span></label>'; 1139 } 1140 1141 return '<input type="' + esc(type === 'email' ? 'email' : 'text') + '" id="' + esc(name) + '" name="' + esc(name) + '" class="' + esc(inputClass) + '" data-iitix-field-name="' + esc(name) + '" placeholder="' + esc(placeholder) + '"' + required + '>'; 1142 } 1143 1144 function renderProfileFieldsSection(section) { 1145 section = section || {}; 1146 var fields = Array.isArray(section.fields) ? section.fields : []; 1147 if (!fields.length) return ''; 1148 1149 var wrapperClass = section.wrapper_class || 'iitix-extra-section iitix-step-block'; 1150 var fieldsLayoutClass = section.fields_layout_class || section.fields_layout || ''; 1151 var html = '<div class="' + esc(wrapperClass) + '">'; 1152 html += getSectionStepHtml(section, '', section.title || '', section.step_badge || 'Required', section.heading_class || 'iitix-step-heading iitix-step-menu-heading'); 1153 if (fieldsLayoutClass) html += '<div class="' + esc(fieldsLayoutClass) + '">'; 1154 1155 fields.forEach(function (field) { 1156 field = field || {}; 1157 if (!field.name) return; 1158 html += '<div class="' + esc(field.wrapper_class || 'iitix-field-group') + '" data-iitix-field-wrapper="' + esc(field.name) + '">'; 1159 if (field.label) html += '<label class="iitix-extra-label" for="' + esc(field.name) + '">' + esc(field.label) + '</label>'; 1160 if (field.help) html += '<div class="iitix-extra-help">• ' + esc(field.help) + '</div>'; 1161 html += renderProfileFieldInput(field); 1162 html += '</div>'; 1163 }); 1164 1165 if (fieldsLayoutClass) html += '</div>'; 1166 html += '</div>'; 1167 return html; 1168 } 1169 1170 function renderApplianceSlotsSection(section, profile, form) { 1171 section = section || {}; 1172 if (!section || section.type !== 'appliance_slots') return ''; 1173 1174 var maxWatt = parseInt(section.max_total_wattage || (profile && profile.max_total_wattage) || getExtraMaxWattage(form), 10); 1175 maxWatt = maxWatt > 0 ? maxWatt : 1000; 1176 var optionsHtml = getApplianceOptionsHtml(); 1177 var wattageUnit = getExtraCopy('wattageUnit', 'W'); 1178 var policyNotice = getExtraCopy('powerPolicyNotice', 'If actual power usage at the venue exceeds the declared wattage or the 1000W limit, the organizer and venue may disconnect the electricity supply without refund or compensation.'); 1179 var introClass = section.wrapper_class || 'iitix-extra-section iitix-appliance-intro'; 1180 var introTitle = section.section_kicker || section.title || 'Appliances';
1181 var applianceOneTitle = section.appliance_1_label || 'Appliance 1'; 1182 var applianceTwoTitle = section.appliance_2_label || 'Appliance 2'; 1183 var stepHtml = getSectionStepHtml(section, '', section.step_title || 'Choose appliance and fill watt', section.step_badge || 'Required', section.heading_class || 'iitix-step-heading iitix-step-appliance-heading'); 1184 1185 return '' + 1186 '<div class="' + esc(introClass) + '">' + 1187 '<div class="iitix-section-kicker">' + esc(introTitle) + '</div>' + 1188 (section.help ? '<div class="iitix-extra-help">' + esc(section.help) + '</div>' : '') + 1189 '</div>' + 1190 1191 '<div class="iitix-appliance-card iitix-step-block iitix-step-appliance-required">' + 1192 stepHtml + 1193 '<div class="iitix-appliance-card-header">' + 1194 '<div>' + 1195 '<div class="iitix-appliance-card-title">' + esc(applianceOneTitle) + '</div>' + 1196 '<div class="iitix-appliance-card-subtitle">Choose appliance type and enter wattage manually.</div>' + 1197 '</div>' + 1198 '<span class="iitix-appliance-card-badge">Slot 1</span>' + 1199 '</div>' + 1200 '<div class="iitix-appliance-row">' + 1201 '<div class="iitix-field-group">' + 1202 '<label>Appliance</label>' + 1203 '<select name="iitix_appliance_1_type" class="iitix-appliance-select" data-iitix-slot="1">' + optionsHtml + '</select>' + 1204 '</div>' + 1205 '<div class="iitix-field-group">' + 1206 '<label>' + esc(getExtraCopy('wattageLabel', 'Watt')) + '</label>' + 1207 '<div class="iitix-input-with-unit iitix-watt-input-wrap">' + 1208 '<input type="number" name="iitix_appliance_1_wattage" class="iitix-wattage-input" min="0" step="1" inputmode="numeric" placeholder="0">' + 1209 '<span class="iitix-input-unit">' + esc(wattageUnit) + '</span>' + 1210 '</div>' + 1211 '</div>' + 1212 '</div>' + 1213 '<input type="text" name="iitix_appliance_1_other" class="iitix-other-appliance" data-iitix-other-slot="1" placeholder="' + esc(getExtraCopy('otherPlaceholder', 'Please specify other appliance')) + '" style="display:none;">' + 1214 '</div>' + 1215 1216 '<div class="iitix-appliance-card iitix-step-optional">' + 1217 '<div class="iitix-appliance-card-header">' + 1218 '<div>' + 1219 '<div class="iitix-appliance-card-title">' + esc(applianceTwoTitle) + '</div>' + 1220 '<div class="iitix-appliance-card-subtitle">Optional. Leave empty if you only use one appliance.</div>' + 1221 '</div>' + 1222 '<span class="iitix-appliance-card-badge">Slot 2</span>' + 1223 '</div>' + 1224 '<div class="iitix-appliance-row">' + 1225 '<div class="iitix-field-group">' + 1226 '<label>Appliance</label>' + 1227 '<select name="iitix_appliance_2_type" class="iitix-appliance-select" data-iitix-slot="2">' + optionsHtml + '</select>' + 1228 '</div>' + 1229 '<div class="iitix-field-group">' + 1230 '<label>' + esc(getExtraCopy('wattageLabel', 'Watt')) + '</label>' + 1231 '<div class="iitix-input-with-unit iitix-watt-input-wrap">' + 1232 '<input type="number" name="iitix_appliance_2_wattage" class="iitix-wattage-input" min="0" step="1" inputmode="numeric" placeholder="0">' + 1233 '<span class="iitix-input-unit">' + esc(wattageUnit) + '</span>' + 1234 '</div>' + 1235 '</div>' + 1236 '</div>' + 1237 '<input type="text" name="iitix_appliance_2_other" class="iitix-other-appliance" data-iitix-other-slot="2" placeholder="' + esc(getExtraCopy('otherPlaceholder', 'Please specify other appliance')) + '" style="display:none;">' + 1238 '</div>' + 1239 1240 '<input type="hidden" name="iitix_total_wattage" value="0">' + 1241 '<input type="hidden" name="iitix_max_wattage" value="' + esc(maxWatt) + '">' + 1242 1243 '<div class="iitix-watt-summary">' + 1244 '<span>Total Appliance 1 + Appliance 2</span>' + 1245 '<span class="iitix-watt-total"><span data-iitix-total-wattage>0</span>W / ' + esc(maxWatt) + 'W</span>' + 1246 '</div>' + 1247 (policyNotice ? '<div class="iitix-power-policy-alert" role="alert">
1247' + esc(policyNotice) + '</div>' : ''); 1248 } 1249 1250 function renderProfileNoticeSection(section) { 1251 section = section || {}; 1252 var wrapperClass = section.wrapper_class || 'iitix-profile-notice'; 1253 var title = $.trim(String(section.title || 'Important')); 1254 var message = $.trim(String(section.message || section.intro || '')); 1255 if (!title && !message) return ''; 1256 1257 return '' + 1258 '<div class="' + esc(wrapperClass) + '" role="alert">' + 1259 (title ? '<div class="iitix-profile-notice-title">' + esc(title) + '</div>' : '') + 1260 (message ? '<div class="iitix-profile-notice-message">' + esc(message) + '</div>' : '') + 1261 '</div>'; 1262 } 1263 1264 function renderProfileGuidelinesSection(section) { 1265 section = section || {}; 1266 var items = Array.isArray(section.items) ? section.items.filter(function (item) { 1267 return $.trim(String(item || '')) !== ''; 1268 }) : []; 1269 if (!items.length && !section.title && !section.intro) return ''; 1270 1271 var wrapperClass = section.wrapper_class || 'iitix-extra-guidelines'; 1272 var html = '<div class="' + esc(wrapperClass) + '">'; 1273 if (section.title) html += '<div class="iitix-extra-guidelines-title">' + esc(section.title) + '</div>'; 1274 if (section.intro) html += '<div class="iitix-extra-help">' + esc(section.intro) + '</div>'; 1275 if (items.length) { 1276 html += '<ol>' + items.map(function (item) { 1277 return '<li>' + esc(item) + '</li>'; 1278 }).join('') + '</ol>'; 1279 } 1280 html += '</div>'; 1281 return html; 1282 } 1283 1284 /* 1285 * Mobile action-layout structure. 1286 * 1287 * The normal WooCommerce DOM has category fields, profile fields and 1288 * .single_variation_wrap as siblings. A sticky action bar inside the same 1289 * scroll area overlays the profile while the modal is at the top. Instead, 1290 * keep category/profile content in one dedicated scrolling wrapper and make 1291 * the WooCommerce action area a real footer sibling. 1292 */ 1293 function ensureExtraModalActionLayout(form) { 1294 if (!form || !form.length) return $(); 1295 1296 var variationWrap = form.find('.single_variation_wrap').first(); 1297 if (!variationWrap.length) return $(); 1298 1299 var scrollContent = form.children('.iitix-extra-scroll-content').first(); 1300 if (!scrollContent.length) { 1301 scrollContent = $('<div class="iitix-extra-scroll-content" data-iitix-extra-scroll-content="1"></div>'); 1302 scrollContent.insertBefore(variationWrap); 1303 1304 // Only move visual fields. Hidden WooCommerce inputs must remain direct 1305 // form children so variation/add-to-cart serialization remains unchanged. 1306 form.children('.iitix-step-heading, .iitix-seat-filter-notice, table.variations, .iitix-extra-fields') 1307 .not(scrollContent) 1308 .not(variationWrap) 1309 .appendTo(scrollContent); 1310 } 1311 1312 form.addClass('iitix-extra-action-layout'); 1313 return scrollContent; 1314 } 1315 1316 function restoreExtraModalActionLayout(form) { 1317 if (!form || !form.length) return; 1318 1319 var scrollContent = form.children('.iitix-extra-scroll-content').first(); 1320 if (scrollContent.length) { 1321 // Put the original UI nodes back before the WooCommerce action area 1322 // when the user switches to a category without an extra profile. 1323 scrollContent.children().insertBefore(scrollContent); 1324 scrollContent.remove(); 1325 } 1326 1327 form.removeClass('iitix-extra-action-layout'); 1328 } 1329 1330 function ensureExtraFields(form) { 1331 if (!form || !form.length) return $(); 1332 ensureExtraFieldsStyle(); 1333 1334 var existing = form.find('.iitix-extra-fields').first(); 1335 if (existing.length) return existing; 1336 1337 var profile = getCurrentExtraProfile(form); 1338 if (!profile) return $(); 1339 1340 // data-iitix-profile must be category-specific. Shared profile templates 1341 // have one profile.key but different title/description per selected category. 1342 var selectedCategoryKey = normalizeCategoryProfileKey(getCurrentExtraCategoryValue(form)); 1343 var profileKey = selectedCategoryKey && profile 1344 ? (selectedCategoryKey + ':' + String(profile.key || profile.category_key || 'category-profile')) 1345 : (selectedCategoryKey || profile.category_key || profile.key || 'category-profile'); 1346 var wrapperId = profile.wrapper_id || ''; 1347 var wrapperClass = profile.wrapper_class || 'iitix-extra-fields'; 1348 var sections = getProfileSections(profile); 1349 var content = ''; 1350 1351 sections.forEach(function (section) { 1352 section = section || {}; 1353 if (section.type === 'fields') { 1354 content += renderProfileFieldsSection(section); 1355 } else if (section.type === 'appliance_slots') { 1356 content += renderApplianceSlotsSection(section, profile, form); 1357 } else if (section.type === 'guidelines') { 1358 content += renderProfileGuidelinesSection(section); 1359 } else if (section.type === 'notice') { 1360 content += renderProfileNoticeSection(section); 1361 } 1362 }); 1363 1364 var html = '' + 1365 '<div' + (wrapperId ? ' id="' + esc(wrapperId) + '"' : '') + ' class="' + esc(wrapperClass) + '" data-iitix-extra-fields="1" data-iitix-profile="' + esc(profileKey) + '" style="display:none;">' + 1366 '<div class="iitix-extra-title">' + esc(profile.title || 'Vendor Details') + '</div>' + 1367 (profile.description ? '<div class="iitix-extra-desc">' + esc(profile.description) + '</div>' : '') + 1368 content + 1369 '<div class="iitix-extra-message" data-iitix-extra-message></div>' + 1370 '</div>'; 1371 1372 var block = $(html); 1373 var scrollContent = form.children('.iitix-extra-scroll-content').first(); 1374 var variationWrap = form.find('.single_variation_wrap').first(); 1375 1376 if (scrollContent.length) { 1377 // In the mobile footer layout, profile fields belong to the dedicated 1378 // scroll body. This keeps Add to Cart below the form instead of over it. 1379 scrollContent.append(block); 1380 } else if (variationWrap.length) { 1381 block.insertBefore(variationWrap); 1382 } else { 1383 form.append(block); 1384 } 1385 1386 return block; 1387 } 1388 1389 function autoFillMinimumWattForSelect(form, select) { 1390 if (!form || !form.length || !select || !select.length) return; 1391 1392 var slot = select.data('iitix-slot'); 1393 var type = $.trim(select.val() || ''); 1394 var wattInput = form.find('[name="iitix_appliance_' + slot + '_wattage"]'); 1395 if (!wattInput.length) return; 1396 1397 var minWatt = getApplianceMinWatt(type); 1398 if (minWatt > 0) { 1399 wattInput.val(minWatt); 1400 return; 1401 } 1402 1403 if (!type || normalize(type) === 'other' || normalize(type) === 'others') { 1404 wattInput.val(''); 1405 } 1406 } 1407 1408 function toggleOtherInputs(form) { 1409 form.find('.iitix-appliance-select').each(function () { 1410 var select = $(this); 1411 var slot = select.data('iitix-slot'); 1412 var other = form.find('[data-iitix-other-slot="' + slot + '"]'); 1413 var isOther = (normalize(select.val()) === 'other' || normalize(select.val()) === 'others'); 1414 var minWatt = getApplianceMinWatt(select.val()); 1415 var wattInput = form.find('[name="iitix_appliance_' + slot + '_wattage"]'); 1416 1417 other.toggle(isOther); 1418 other.prop('disabled', !isOther); 1419 if (!isOther) other.val(''); 1420 1421 if (wattInput.length) { 1422 wattInput.attr('min', minWatt > 0 ? String(minWatt) : '0'); 1423 wattInput.attr('title', minWatt > 0 ? ('Minimum watt: ' + minWatt + 'W') : ''); 1424 } 1425 }); 1426 } 1427 1428 function ensureSeatModalBackdrop(enabled) { 1429 var id = 'iitix-seat-modal-backdrop'; 1430 var backdrop = document.getElementById(id); 1431 1432 if (!enabled) { 1433 if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop); 1434 document.documentElement.classList.remove('iitix-seat-modal-open'); 1435 document.body.classList.remove('iitix-seat-modal-open'); 1436 return; 1437 } 1438 1439 if (!backdrop) { 1440 backdrop = document.createElement('div'); 1441 backdrop.id = id; 1442 backdrop.className = 'iitix-seat-modal-backdrop'; 1443 document.body.appendChild(backdrop); 1444 } 1445 1446 document.documentElement.classList.add('iitix-seat-modal-open'); 1447 document.body.classList.add('iitix-seat-modal-open'); 1448 } 1449 1450 function forceWideModalInlineLayout(modal, dialog) { 1451 // Inline fallback is intentional: some WordPress/caching stacks keep old CSS, 1452 // while Tickera also writes width/top styles directly into the dialog. 1453 if (dialog && dialog.length) { 1454 dialog.css({ 1455 position: 'fixed', 1456 top: '0', 1457 right: '0', 1458 bottom: '0', 1459 left: '0', 1460 width: '100vw', 1461 maxWidth: '100vw', 1462 height: '100vh', 1463 minHeight: '100vh', 1464 margin: '0', 1465 padding: '24px', 1466 display: 'flex', 1467 alignItems: 'center', 1468 justifyContent: 'center', 1469 boxSizing: 'border-box', 1470 background: 'transparent', 1471 zIndex: 999991 1472 }); 1473 } 1474 1475 modal.css({ 1476 width: 'min(92vw, 1240px)', 1477 maxWidth: '1240px', 1478 height: 'min(90vh, 860px)', 1479 maxHeight: 'calc(100vh - 56px)', 1480 minHeight: '0', 1481 overflow: 'hidden', 1482 boxSizing: 'border-box', 1483 padding: '0', 1484 borderRadius: '24px', 1485 background: '#fff', 1486 boxShadow: '0 30px 90px rgba(15, 23, 42, .30)', 1487 position: 'relative', 1488 zIndex: 999992 1489 }); 1490 1491 modal.find('.tc-modal-woobridge').first().css({ 1492 width: '100%', 1493 height: '100%', 1494 maxHeight: '100%', 1495 minHeight: '0', 1496 boxSizing: 'border-box', 1497 padding: '24px 28px 0', 1498 display: 'flex', 1499 flexDirection: 'column', 1500 overflow: 'hidden', 1501 position: 'relative' 1502 }); 1503 1504 modal.find('.tc-modal-woobridge-inner').first().css({ 1505 width: '100%', 1506 flex: '1 1 auto', 1507 minHeight: '0', 1508 maxHeight: 'none', 1509 overflowY: 'auto', 1510 overflowX: 'hidden', 1511 padding: '0 10px 0 0', 1512 boxSizing: 'border-box' 1513 }); 1514 } 1515 1516 function clearWideModalInlineLayout(modal, dialog) {
1517 var props = 'position top right bottom left width max-width height min-height margin padding display align-items justify-content box-sizing background z-index'; 1518 if (dialog && dialog.length) dialog.css(props, ''); 1519 1520 modal.css('width max-width height max-height min-height overflow box-sizing padding border-radius background box-shadow position z-index', ''); 1521 modal.find('.tc-modal-woobridge').first().css('width height max-height min-height box-sizing padding display flex-direction overflow position', ''); 1522 modal.find('.tc-modal-woobridge-inner').first().css('width flex min-height max-height overflow-y overflow-x padding box-sizing', ''); 1523 } 1524 1525 function setWhitelistedModalLayout(form, enabled) { 1526 var modal = getSeatModal(); 1527 if (!modal.length) return; 1528 1529 var dialog = modal.closest('.ui-dialog'); 1530 1531 if (enabled) { 1532 // Jangan hidupkan lagi kalau modal sedang proses close. 1533 if (isSeatModalClosing) return; 1534 1535 // Jangan force display:flex kalau modal sebenarnya sudah hidden. 1536 if (!isSeatModalActuallyVisible(modal)) return; 1537 1538 if (!modal.data('iitix-original-style')) { 1539 modal.data('iitix-original-style', modal.attr('style') || ''); 1540 } 1541 1542 if (dialog.length && !dialog.data('iitix-original-style')) { 1543 dialog.data('iitix-original-style', dialog.attr('style') || ''); 1544 } 1545 1546 modal.addClass('iitix-extra-modal-wide'); 1547 if (dialog.length) dialog.addClass('iitix-extra-dialog-wide'); 1548 1549 ensureSeatModalBackdrop(true); 1550 forceWideModalInlineLayout(modal, dialog); 1551 1552 return; 1553 } 1554 1555 var hasWideState = modal.hasClass('iitix-extra-modal-wide') 1556 || (dialog.length && dialog.hasClass('iitix-extra-dialog-wide')) 1557 || typeof modal.data('iitix-original-style') !== 'undefined' 1558 || (dialog.length && typeof dialog.data('iitix-original-style') !== 'undefined') 1559 || !!document.getElementById('iitix-seat-modal-backdrop') 1560 || $('body').hasClass('iitix-seat-modal-open'); 1561 1562 if (hasWideState) { 1563 cleanupIitixWideModalState(); 1564 } 1565 } 1566 1567 1568 function forceHideSeatModalElements() { 1569 var modal = getSeatModal(); 1570 var dialog = modal.length ? modal.closest('.ui-dialog') : $(); 1571 1572 if (modal.length) modal.hide(); 1573 if (dialog.length) dialog.hide(); 1574 1575 $('.ui-widget-overlay').remove(); 1576 $('#iitix-seat-modal-backdrop').remove(); 1577 1578 $('html, body').removeClass('iitix-seat-modal-open modal-open').css({ 1579 overflow: '', 1580 paddingRight: '' 1581 }); 1582 1583 clearSeatFilterModalState(); 1584 } 1585 1586 function cleanupIitixWideModalState() { 1587 var modal = getSeatModal(); 1588 if (!modal.length) { 1589 ensureSeatModalBackdrop(false); 1590 $('.ui-widget-overlay').remove(); 1591 $('#iitix-seat-modal-backdrop').remove(); 1592 $('html, body').removeClass('iitix-seat-modal-open modal-open').css({ 1593 overflow: '', 1594 paddingRight: '' 1595 }); 1596 return; 1597 } 1598 1599 var dialog = modal.closest('.ui-dialog'); 1600 1601 ensureSeatModalBackdrop(false); 1602 1603 modal.removeClass('iitix-extra-modal-wide'); 1604 if (dialog.length) dialog.removeClass('iitix-extra-dialog-wide'); 1605 1606 var modalOriginalStyle = modal.data('iitix-original-style'); 1607 var dialogOriginalStyle = dialog.data('iitix-original-style'); 1608 1609 if (typeof modalOriginalStyle === 'string') { 1610 modal.attr('style', modalOriginalStyle); 1611 } else { 1612 modal.removeAttr('style'); 1613 } 1614 1615 if (dialog.length) { 1616 if (typeof dialogOriginalStyle === 'string') { 1617 dialog.attr('style', dialogOriginalStyle); 1618 } else { 1619 dialog.removeAttr('style'); 1620 } 1621 } 1622 1623 modal.removeData('iitix-original-style'); 1624 if (dialog.length) dialog.removeData('iitix-original-style'); 1625 1626 // Remove anything that can keep blocking clicks on the seating map after close. 1627 $('.ui-widget-overlay').remove(); 1628 $('#iitix-seat-modal-backdrop').remove(); 1629 $('.ui-dialog.iitix-extra-dialog-wide').removeClass('iitix-extra-dialog-wide'); 1630 $('#tc-modal-woobridge.iitix-extra-modal-wide').removeClass('iitix-extra-modal-wide'); 1631 1632 $('html, body') 1633 .removeClass('iitix-seat-modal-open modal-open') 1634 .css({ 1635 overflow: '', 1636 paddingRight: '' 1637 }); 1638 } 1639 1640 function closeTickeraSeatModal() { 1641 isSeatModalClosing = true; 1642 lastSeatModalCloseAt = nowMs(); 1643 1644 // Invalidate pending product-seat-filter responses. This prevents an old 1645 // response from rebuilding/reopening the modal after the user closes it. 1646 requestSeq++; 1647 1648 if (extraBootTimer) { 1649 window.clearTimeout(extraBootTimer); 1650 extraBootTimer = null; 1651 } 1652 1653 if (seatModalClosingTimer) { 1654 window.clearTimeout(seatModalClosingTimer); 1655 seatModalClosingTimer = null; 1656 } 1657 1658 var modal = getSeatModal(); 1659 1660 if (!modal.length) { 1661 cleanupIitixWideModalState(); 1662 seatModalClosingTimer = window.setTimeout(function () { 1663 isSeatModalClosing = false;
1664 }, 350); 1665 return false; 1666 } 1667 1668 try { 1669 if (typeof modal.dialog === 'function' && modal.hasClass('ui-dialog-content')) { 1670 modal.dialog('close'); 1671 } 1672 } catch (e) {} 1673 1674 cleanupIitixWideModalState(); 1675 forceHideSeatModalElements(); 1676 1677 // Extra guard for the old AJAX request that may complete shortly after close. 1678 // Do NOT keep suppressing for seconds, because that blocks the user's next 1679 // intentional seat click. Stop only stale reopen attempts that started before close. 1680 var closeAt = lastSeatModalCloseAt; 1681 [80, 180, 360].forEach(function (delay) { 1682 window.setTimeout(function () { 1683 var hasNewSeatRequest = lastSeatVariationAjaxSentAt > closeAt; 1684 if (isSeatModalClosing && lastSeatModalCloseAt === closeAt && !hasNewSeatRequest) { 1685 cleanupIitixWideModalState(); 1686 forceHideSeatModalElements(); 1687 } 1688 }, delay); 1689 }); 1690 1691 seatModalClosingTimer = window.setTimeout(function () { 1692 var hasNewSeatRequest = lastSeatVariationAjaxSentAt > closeAt; 1693 if (!hasNewSeatRequest) { 1694 cleanupIitixWideModalState(); 1695 forceHideSeatModalElements(); 1696 } 1697 1698 if (lastSeatModalCloseAt === closeAt) { 1699 isSeatModalClosing = false; 1700 } 1701 }, 420); 1702 1703 return false; 1704 } 1705 1706 1707 function installSeatModalCloseHandler() { 1708 if (window.__IITIX_SEAT_MODAL_CLOSE_HANDLER_BOUND__) return; 1709 window.__IITIX_SEAT_MODAL_CLOSE_HANDLER_BOUND__ = true; 1710 1711 // Capture phase so the close still works even when Tickera/jQuery UI 1712 // or the map layer intercepts bubbling clicks. 1713 document.addEventListener('click', function (event) { 1714 var target = event.target && event.target.closest 1715 ? event.target.closest('#tc-modal-woobridge .tc_modal_close_dialog, .ui-dialog.iitix-extra-dialog-wide .ui-dialog-titlebar-close') 1716 : null; 1717 1718 if (!target) return; 1719 1720 event.preventDefault(); 1721 event.stopPropagation(); 1722 if (event.stopImmediatePropagation) event.stopImmediatePropagation(); 1723 1724 closeTickeraSeatModal(); 1725 return false; 1726 }, true); 1727 1728 $(document) 1729 .off('keydown.iitixSeatModalClose') 1730 .on('keydown.iitixSeatModalClose', function (e) { 1731 if (e.key === 'Escape' || e.keyCode === 27) { 1732 if ($('#tc-modal-woobridge:visible').length || $('.ui-dialog.iitix-extra-dialog-wide:visible').length) { 1733 closeTickeraSeatModal(); 1734 } 1735 } 1736 }); 1737 } 1738 1739 installSeatModalCloseHandler(); 1740 1741 function buildIitixStepHeading(number, title, badge, extraClass) { 1742 return $( 1743 '<div class="iitix-step-heading ' + esc(extraClass || '') + '">' + 1744 '<span class="iitix-step-number">' + esc(number) + '</span>' + 1745 '<span class="iitix-step-body">' + 1746 '<span class="iitix-step-title">' + esc(title) + '</span>' + 1747 '<span class="iitix-step-badge">' + esc(badge || 'Required') + '</span>' + 1748 '</span>' + 1749 '</div>' 1750 ); 1751 } 1752 1753 function ensureNumberedRequiredSteps(form) { 1754 if (!form || !form.length) return; 1755 1756 var table = form.find('table.variations').first(); 1757 if (table.length && !table.prev('.iitix-step-heading.iitix-step-category').length) { 1758 buildIitixStepHeading('1', 'Choose category', 'Required', 'iitix-step-category').insertBefore(table); 1759 } 1760 } 1761 1762 function removeNumberedRequiredSteps(form) { 1763 if (!form || !form.length) return; 1764 form.find('.iitix-step-heading.iitix-step-category').remove(); 1765 } 1766 1767 function updateExtraFieldsVisibility(form) { 1768 if (!form || !form.length) return; 1769 1770 var profile = getCurrentExtraProfile(form); 1771 var shouldShow = currentSeatAllowsExtraFields(form); 1772 // Use the selected category identity, not only profile.key. Several standard 1773 // categories intentionally share the same schema key (standard_vendor_details) 1774 // but must still rebuild their heading/description when the user switches 1775 // e.g. Beauty & Wellness → Home & Living. 1776 var selectedCategoryIdentity = normalizeCategoryProfileKey(getCurrentExtraCategoryValue(form)); 1777 // Include the actual profile schema key. The category remains "Packaged F&B" 1778 // on the six Premium Center seats, but their restricted schema must replace 1779 // the ordinary Packaged F&B appliance schema whenever the seat changes. 1780 var expectedProfileKey = selectedCategoryIdentity && profile 1781 ? (selectedCategoryIdentity + ':' + String(profile.key || profile.category_key || 'category-profile')) 1782 : (selectedCategoryIdentity || (profile && (profile.category_key || profile.key) ? String(profile.category_key || profile.key) : '')); 1783 var block = form.find('.iitix-extra-fields').first(); 1784 1785 // Category-driven profile sections use the wide modal only when a mapped 1786 // category (Food Service / Packaged F&B / Fashion with Racks) is selected. 1787 setWhitelistedModalLayout(form, shouldShow); 1788 1789 // No mapped category selected: remove any old profile completely so file 1790 // fields, radio state, and validation state cannot leak into another 1791 // variation/category's AJAX add-to-cart request. 1792 if (!shouldShow) { 1793 // Restore the ordinary Tickera/WooCommerce structure before removing the 1794 // category profile, so an unmapped category keeps its original layout. 1795 restoreExtraModalActionLayout(form); 1796 removeNumberedRequiredSteps(form); 1797 clearExtraInvalidMarkers(form); 1798 renderExtraMessage(form, '', ''); 1799 1800 if (block.length) { 1801 block.remove(); 1802 } 1803 1804 setExtraButtonInvalid(form, false); 1805 return; 1806 } 1807 1808 // Create the scroll-body + action-footer structure before adding/rebuilding 1809 // a mapped category profile. The footer itself remains a WooCommerce child. 1810 ensureExtraModalActionLayout(form); 1811 1812 // A single WooCommerce product can expose multiple category values. Rebuild 1813 // the profile when the selected category changes (e.g. Packaged F&B -> 1814 // Food Service), otherwise old fields and fees could remain in the form. 1815 if (block.length && String(block.attr('data-iitix-profile') || '') !== expectedProfileKey) { 1816 clearExtraInvalidMarkers(form); 1817 renderExtraMessage(form, '', ''); 1818 block.remove(); 1819 block = $(); 1820 } 1821 1822 block = block.length ? block : ensureExtraFields(form); 1823 ensureNumberedRequiredSteps(form); 1824 form.attr('enctype', 'multipart/form-data'); 1825 block.show(); 1826 block.find('input, textarea, select, button').prop('disabled', false); 1827 toggleOtherInputs(form); 1828 } 1829 1830 function readIntInput(form, selector) { 1831 var raw = form.find(selector).val(); 1832 var n = parseInt(raw, 10); 1833 return isNaN(n) || n < 0 ? 0 : n; 1834 } 1835 1836 function collectExtraFields(form) { 1837 var fields = {}; 1838 form.find('.iitix-extra-fields [data-iitix-field-name]').each(function () { 1839 var input = $(this); 1840 var name = input.data('iitix-field-name') || input.attr('name') || ''; 1841 if (!name) return; 1842 1843 if (input.attr('type') === 'checkbox') { 1844 fields[name] = input.is(':checked') ? (input.val() || 'Yes') : ''; 1845 } else if (input.attr('type') === 'radio') { 1846 if (input.is(':checked')) fields[name] = $.trim(input.val() || ''); 1847 else if (typeof fields[name] === 'undefined') fields[name] = ''; 1848 } else if (input.attr('type') === 'file') { 1849 var nativeInput = input.get(0); 1850 fields[name] = nativeInput && nativeInput.files && nativeInput.files.length ? nativeInput.files[0].name : ''; 1851 } else { 1852 fields[name] = $.trim(input.val() || ''); 1853 } 1854 }); 1855 1856 return { 1857 fields: fields, 1858 menu: $.trim(form.find('[name="iitix_menu"]').val() || ''), 1859 instagramUsername: $.trim(form.find('[name="iitix_instagram_username"]').val() || ''), 1860 appliance1Type: $.trim(form.find('[name="iitix_appliance_1_type"]').val() || ''), 1861 appliance1Other: $.trim(form.find('[name="iitix_appliance_1_other"]').val() || ''), 1862 appliance1Wattage: readIntInput(form, '[name="iitix_appliance_1_wattage"]'), 1863 appliance2Type: $.trim(form.find('[name="iitix_appliance_2_type"]').val() || ''), 1864 appliance2Other: $.trim(form.find('[name="iitix_appliance_2_other"]').val() || ''), 1865 appliance2Wattage: readIntInput(form, '[name="iitix_appliance_2_wattage"]') 1866 }; 1867 } 1868 1869 function setExtraButtonInvalid(form, invalid, message) { 1870 var btn = form.find('.tc_cart_button, .single_add_to_cart_button').first(); 1871 if (!btn.length) return; 1872 1873 var variationId = $.trim(form.find('input.variation_id').val() || ''); 1874 var hasVariationSelect = form.find('select[name^="attribute_"]').length > 0; 1875 1876 if (variationId) { 1877 normalizeValidVariationButton(form); 1878 } 1879 1880 // WooCommerce still needs category/variation if variation_id is blank. 1881 // Extra-field errors stay clickable so the guard can scroll the user to the empty field. 1882 var wooStillNeedsVariation = hasVariationSelect && !variationId; 1883 var extraInvalid = !!invalid && !wooStillNeedsVariation; 1884 var shouldHardDisable = wooStillNeedsVariation; 1885 1886 // A missing WooCommerce variation can keep the button disabled, but it must not 1887 // look like a submitted validation error before the vendor clicks Add to Cart. 1888 btn.toggleClass('iitix-extra-invalid', !!invalid); 1889 1890 if (shouldHardDisable) { 1891 btn.addClass('disabled'); 1892 btn.prop('disabled', true); 1893 btn.attr('aria-disabled', 'true'); 1894 btn.attr('title', message || 'Please choose category first.'); 1895 return; 1896 } 1897 1898 if (extraInvalid) {
1899 btn.removeClass('disabled wc-variation-selection-needed'); 1900 btn.prop('disabled', false); 1901 btn.attr('aria-disabled', 'true'); 1902 btn.attr('title', message || 'Please complete the required fields.'); 1903 return; 1904 } 1905 1906 btn.removeClass('disabled wc-variation-selection-needed iitix-extra-invalid'); 1907 btn.prop('disabled', false); 1908 btn.attr('aria-disabled', 'false'); 1909 btn.removeAttr('title'); 1910 } 1911 1912 function renderExtraMessage(form, type, message) { 1913 var block = form.find('.iitix-extra-fields').first(); 1914 var messageEl = block.find('[data-iitix-extra-message]').first(); 1915 if (!messageEl.length) return; 1916 1917 messageEl.removeClass('is-error is-ok'); 1918 1919 if (!message) { 1920 messageEl.text('').hide(); 1921 return; 1922 } 1923 1924 messageEl.text(message).show().addClass(type === 'ok' ? 'is-ok' : 'is-error'); 1925 } 1926 1927 1928 1929 /* 1930 * Required-field highlighting 1931 * 1932 * Validation always collects every missing field in schema order, but UI only 1933 * highlights validationErrors[0]. The helper below resolves that field to the 1934 * visible UI area instead of the hidden native file input / raw form control. 1935 */ 1936 function getExtraInvalidVisualTarget(target) { 1937 if (!target || !target.length) return $(); 1938 1939 var fieldTarget = target; 1940 var uploadTarget = $(); 1941 var fieldWrapper = $(); 1942 var stepWrapper = $(); 1943 1944 if (target.is('input[type="file"]')) { 1945 uploadTarget = target.closest('[data-iitix-file-upload]'); 1946 } 1947 1948 fieldWrapper = target.closest( 1949 '.iitix-field-group, ' + 1950 '.iitix-field-menu, ' + 1951 '.iitix-field-instagram, ' + 1952 '.iitix-field-brand-logo, ' + 1953 '.iitix-field-promo-package, ' + 1954 '.iitix-field-remove-table, ' + 1955 '[data-iitix-field-wrapper], ' + 1956 '.iitix-radio-group, ' + 1957 '.iitix-watt-summary' 1958 ); 1959 1960 stepWrapper = target.closest( 1961 '.iitix-extra-section, ' + 1962 '.iitix-appliance-card, ' + 1963 'td.value' 1964 ); 1965 1966 // Prefer the complete visible step / card so it is obvious which required 1967 // item must be completed. For uploads, also outline the upload drop-zone. 1968 if (stepWrapper.length) { 1969 if (uploadTarget.length) return stepWrapper.add(uploadTarget); 1970 return stepWrapper; 1971 } 1972 1973 if (uploadTarget.length) return uploadTarget; 1974 if (fieldWrapper.length) return fieldWrapper; 1975 1976 return fieldTarget; 1977 } 1978 1979 function getExtraInvalidMessageContainer(target) { 1980 if (!target || !target.length) return $(); 1981 1982 var stepWrapper = target.closest( 1983 '.iitix-extra-section, ' + 1984 '.iitix-appliance-card, ' + 1985 'td.value' 1986 ); 1987 if (stepWrapper.length) return stepWrapper; 1988 1989 var visualTarget = getExtraInvalidVisualTarget(target); 1990 return visualTarget.length ? visualTarget.first() : target; 1991 } 1992 1993 /* 1994 * The red state belongs to the first failed Add to Cart attempt. It must not 1995 * disappear when WooCommerce, Tickera, MutationObserver, or a normal input 1996 * event performs a silent validation pass. 1997 * 1998 * Store a stable identity for the highlighted source control. A later silent 1999 * validation only clears the red state when that exact control has become 2000 * valid; it deliberately does not highlight the next missing field until the 2001 * vendor presses Add to Cart again. 2002 */ 2003 function getExtraInvalidTargetKey(target) { 2004 target = target && target.length ? target.first() : $(); 2005 if (!target.length) return ''; 2006 2007 var name = $.trim(target.attr('name') || ''); 2008 if (name) return 'name:' + name; 2009 2010 var fieldName = $.trim(target.attr('data-iitix-field-name') || ''); 2011 if (fieldName) return 'field:' + fieldName; 2012 2013 var radioGroup = $.trim(target.attr('data-iitix-radio-group') || ''); 2014 if (radioGroup) return 'radio:' + radioGroup; 2015 2016 var id = $.trim(target.attr('id') || ''); 2017 if (id) return 'id:' + id; 2018 2019 if (target.hasClass('iitix-watt-summary')) return 'class:iitix-watt-summary'; 2020 if (target.hasClass('iitix-extra-fields')) return 'class:iitix-extra-fields'; 2021 2022 var generatedKey = target.data('iitixExtraInvalidTargetKey'); 2023 if (!generatedKey) { 2024 generatedKey = 'node:' + nowMs() + ':' + Math.random().toString(36).slice(2); 2025 target.data('iitixExtraInvalidTargetKey', generatedKey); 2026 } 2027 2028 return generatedKey; 2029 } 2030 2031 function getActiveExtraInvalidState(form) { 2032 if (!form || !form.length) return null; 2033 2034 var state = form.data('iitixActiveExtraInvalidState'); 2035 return state && state.key ? state : null; 2036 } 2037 2038 function setActiveExtraInvalidState(form, target, message) { 2039 if (!form || !form.length || !target || !target.length) return; 2040 2041 form.data('iitixActiveExtraInvalidState', { 2042 key: getExtraInvalidTargetKey(target), 2043 target: target.first(), 2044 message: String(message || '') 2045 }); 2046 } 2047 2048 function findMatchingExtraValidationError(errors, activeState) { 2049 if (!activeState || !activeState.key || !Array.isArray(errors)) return null; 2050 2051 for (var i = 0; i < errors.length; i++) { 2052 var candidate = errors[i] || {}; 2053 if (getExtraInvalidTargetKey(candidate.target) === activeState.key) { 2054 return candidate; 2055 } 2056 } 2057 2058 return null; 2059 } 2060 2061 function clearExtraInvalidMarkers(form) { 2062 if (!form || !form.length) return; 2063 2064 form.find('.iitix-is-invalid').removeClass('iitix-is-invalid'); 2065 form.find('[aria-invalid="true"]').removeAttr('aria-invalid'); 2066 form.find('.iitix-inline-error').remove(); 2067 form.removeData('iitixActiveExtraInvalidState'); 2068 } 2069 2070 function reconcileActiveExtraInvalidState(form, validationErrors) { 2071 var activeState = getActiveExtraInvalidState(form); 2072 if (!activeState) return; 2073 2074 var matchingError = findMatchingExtraValidationError(validationErrors, activeState); 2075 2076 // The source field that was highlighted is now valid. Remove all red UI, 2077 // but do not automatically jump to or highlight the next missing field. 2078 if (!matchingError) { 2079 clearExtraInvalidMarkers(form); 2080 renderExtraMessage(form, '', ''); 2081 return; 2082 } 2083 2084 // Keep the same first failed field visible through silent re-validations. 2085 // Its wording can change (for example appliance type -> wattage), so retain 2086 // the current message while preserving the original user-driven target. 2087 activeState.target = matchingError.target && matchingError.target.length 2088 ? matchingError.target.first() 2089 : activeState.target; 2090 activeState.message = String(matchingError.message || activeState.message || ''); 2091 form.data('iitixActiveExtraInvalidState', activeState); 2092 2093 markExtraInvalidTarget(activeState.target, activeState.message); 2094 renderExtraMessage(form, 'error', activeState.message); 2095 } 2096 2097 function markExtraInvalidTarget(target, message) { 2098 if (!target || !target.length) return; 2099 2100 var inputType = String(target.attr('type') || '').toLowerCase(); 2101 var isNativeInput = target.is('input, textarea, select'); 2102 2103 // Native controls receive aria-invalid for accessibility. The file input is 2104 // hidden by design, so its visible upload zone is highlighted below instead. 2105 if (isNativeInput && inputType !== 'file') { 2106 target.attr('aria-invalid', 'true'); 2107 } 2108 2109 var visualTarget = getExtraInvalidVisualTarget(target); 2110 if (!visualTarget.length) visualTarget = target; 2111 2112 // This class drives the existing red border / red focus ring CSS. 2113 visualTarget.addClass('iitix-is-invalid'); 2114 2115 var messageContainer = getExtraInvalidMessageContainer(target); 2116 if (!messageContainer.length) messageContainer = visualTarget.first(); 2117
2118 if (message && !messageContainer.children('.iitix-inline-error').length) { 2119 messageContainer.append('<div class="iitix-inline-error">' + esc(message) + '</div>'); 2120 } 2121 } 2122 2123 function scrollToFirstInvalidField(form, preferredTarget) { 2124 if (!form || !form.length) return; 2125 2126 // preferredTarget comes directly from validationErrors[0], so the first 2127 // missing required item in visible step order is the only one highlighted. 2128 var target = preferredTarget && preferredTarget.length && preferredTarget.is(':visible') 2129 ? preferredTarget 2130 : $(); 2131 2132 if (!target.length) { 2133 target = form.find('[aria-invalid="true"]:visible, .iitix-is-invalid:visible, .iitix-inline-error:visible').first(); 2134 } 2135 if (!target.length) { 2136 target = form.find('select[name^="attribute_"], .iitix-step-heading, .iitix-extra-fields').filter(':visible').first(); 2137 } 2138 2139 var scrollTarget = getExtraInvalidMessageContainer(target); 2140 if (!scrollTarget.length) scrollTarget = getExtraInvalidVisualTarget(target); 2141 if (!scrollTarget.length) scrollTarget = target; 2142 2143 if (scrollTarget.length && scrollTarget.get(0) && typeof scrollTarget.get(0).scrollIntoView === 'function') { 2144 scrollTarget.get(0).scrollIntoView({ behavior: 'smooth', block: 'center' }); 2145 2146 var focusTarget = target; 2147 if (target.is('input[type="file"]')) { 2148 focusTarget = target.closest('[data-iitix-file-upload]').find('[data-iitix-file-trigger]').first(); 2149 } 2150 2151 if (focusTarget.length && focusTarget.is('input, textarea, select, button')) { 2152 window.setTimeout(function () { 2153 try { focusTarget.trigger('focus'); } catch (e) {} 2154 }, 320); 2155 } 2156 } 2157 } 2158 2159 function formatIitixFileSize(bytes) { 2160 bytes = parseInt(bytes, 10) || 0; 2161 if (bytes <= 0) return '0 KB'; 2162 if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '') + ' MB'; 2163 return Math.ceil(bytes / 1024) + ' KB'; 2164 } 2165 2166 function getFieldDefByName(profile, fieldName) { 2167 return getProfileField(profile, fieldName) || {}; 2168 } 2169 2170 function getIitixFileValidationKey(file) { 2171 if (!file) return ''; 2172 return [file.name || '', file.size || 0, file.type || '', file.lastModified || 0].join('|'); 2173 } 2174 2175 function parseIitixAspectRatio(value) { 2176 value = $.trim(String(value || '')); 2177 if (!value) return 0; 2178 2179 if (value.indexOf(':') !== -1) { 2180 var parts = value.split(':'); 2181 var width = parseFloat(parts[0]); 2182 var height = parseFloat(parts[1]); 2183 return width > 0 && height > 0 ? width / height : 0; 2184 } 2185 2186 var ratio = parseFloat(value); 2187 return ratio > 0 ? ratio : 0; 2188 } 2189 2190 function hasIitixImageDimensionRule(field) { 2191 field = field || {}; 2192 return (parseInt(field.min_width, 10) || 0) > 0 || 2193 (parseInt(field.min_height, 10) || 0) > 0 || 2194 parseIitixAspectRatio(field.aspect_ratio) > 0; 2195 } 2196 2197 function inspectIitixImageDimensions(input, field) { 2198 if (!input || !input.length || !hasIitixImageDimensionRule(field)) return null; 2199 2200 var el = input.get(0); 2201 var file = el && el.files && el.files.length ? el.files[0] : null; 2202 if (!file) { 2203 input.removeData('iitixImageDimensionState'); 2204 return null; 2205 } 2206 2207 var fileKey = getIitixFileValidationKey(file); 2208 var cached = input.data('iitixImageDimensionState'); 2209 if (cached && cached.key === fileKey) return cached; 2210 2211 var initialState = { key: fileKey, status: 'loading', width: 0, height: 0, message: '' }; 2212 input.data('iitixImageDimensionState', initialState); 2213 2214 if (!window.URL || typeof window.URL.createObjectURL !== 'function') { 2215 initialState.status = 'error'; 2216 initialState.message = 'Image dimensions could not be checked in this browser.'; 2217 input.data('iitixImageDimensionState', initialState); 2218 return initialState; 2219 } 2220 2221 var objectUrl = window.URL.createObjectURL(file); 2222 var image = new Image(); 2223 var finish = function (state) { 2224 try { window.URL.revokeObjectURL(objectUrl); } catch (e) {} 2225 input.data('iitixImageDimensionState', state); 2226 2227 var form = input.closest('form.variations_form'); 2228 2229 // When the user has just selected/uploaded a file, validation must happen 2230 // immediately and the preview must only appear after the file passes all 2231 // client-side checks. Silent validation is kept for normal form refreshes. 2232 if (input.data('iitixFileValidateOnChange')) { 2233 validateIitixFileSelectionOnChange(input, true); 2234 } else if (form.length) { 2235 validateExtraFields(form, false); 2236 } 2237 }; 2238 2239 image.onload = function () { 2240 finish({ key: fileKey, status: 'ready', width: image.naturalWi
2240dth || image.width || 0, height: image.naturalHeight || image.height || 0, message: '' }); 2241 }; 2242 image.onerror = function () { 2243 finish({ key: fileKey, status: 'error', width: 0, height: 0, message: 'Image dimensions could not be read. Please choose a valid image file.' }); 2244 }; 2245 image.src = objectUrl; 2246 2247 return initialState; 2248 } 2249 2250 function validateClientFileInput(input, field) { 2251 field = field || {}; 2252 if (!input || !input.length) return { valid: true, message: '' }; 2253 2254 var el = input.get(0); 2255 var file = el && el.files && el.files.length ? el.files[0] : null; 2256 var label = field.label || field.meta_label || 'Image'; 2257 var required = !!field.required; 2258 2259 if (!file) { 2260 input.removeData('iitixImageDimensionState'); 2261 return required ? { valid: false, message: 'Please upload ' + label + '.' } : { valid: true, message: '' }; 2262 } 2263 2264 var upload = input.closest('[data-iitix-file-upload]'); 2265 var maxMb = parseInt(field.max_size_mb || upload.data('iitix-max-size-mb') || 10, 10) || 10; 2266 if (file.size > maxMb * 1024 * 1024) { 2267 return { valid: false, message: label + ' maximum file size is ' + maxMb + ' MB.' }; 2268 } 2269 2270 var accepted = String(field.accept || input.attr('accept') || 'image/png,image/jpeg,image/webp').split(',').map(function (item) { 2271 return $.trim(item).toLowerCase(); 2272 }); 2273 var fileType = String(file.type || '').toLowerCase(); 2274 var fileName = String(file.name || '').toLowerCase(); 2275 var typeAllowed = accepted.some(function (accept) { 2276 if (!accept) return false; 2277 if (accept.charAt(0) === '.') return fileName.slice(-accept.length) === accept; 2278 if (accept.slice(-2) === '/*') return fileType.indexOf(accept.slice(0, -1)) === 0; 2279 return fileType === accept; 2280 }); 2281 2282 if (!typeAllowed) { 2283 var fileTypesLabel = field.file_types_label || upload.data('iitix-file-types-label') || 'PNG, JPG, or WebP'; 2284 return { valid: false, message: label + ' must be a ' + fileTypesLabel + ' image.' }; 2285 } 2286 2287 if (hasIitixImageDimensionRule(field)) { 2288 var dimensionState = inspectIitixImageDimensions(input, field); 2289 if (!dimensionState || dimensionState.status === 'loading') { 2290 return { valid: false, message: 'Checking ' + label + ' dimensions. Please wait.' }; 2291 } 2292 if (dimensionState.status === 'error') { 2293 return { valid: false, message: dimensionState.message || (label + ' dimensions could not be checked.') }; 2294 } 2295 2296 var minWidth = parseInt(field.min_width || upload.data('iitix-min-width') || 0, 10) || 0; 2297 var minHeight = parseInt(field.min_height || upload.data('iitix-min-height') || 0, 10) || 0; 2298 if (minWidth > 0 && dimensionState.width < minWidth) { 2299 return { valid: false, message: label + ' minimum width is ' + minWidth + ' px.' }; 2300 } 2301 if (minHeight > 0 && dimensionState.height < minHeight) { 2302 return { valid: false, message: label + ' minimum height is ' + minHeight + ' px.' }; 2303 } 2304 2305 var expectedRatio = parseIitixAspectRatio(field.aspect_ratio || upload.data('iitix-aspect-ratio') || ''); 2306 if (expectedRatio > 0 && dimensionState.width > 0 && dimensionState.height > 0) { 2307 var actualRatio = dimensionState.width / dimensionState.height; 2308 var tolerance = parseFloat(field.aspect_ratio_tolerance || upload.data('iitix-aspect-ratio-tolerance') || 0.01); 2309 tolerance = tolerance > 0 ? tolerance : 0.01; 2310 if (Math.abs(actualRatio - expectedRatio) > tolerance) { 2311 return { valid: false, message: label + ' must use a ' + (field.aspect_ratio || upload.data('iitix-aspect-ratio')) + ' aspect ratio.' }; 2312 } 2313 } 2314 } 2315 2316 return { valid: true, message: '' }; 2317 } 2318 2319 function renderIitixFilePreview(input) { 2320 var upload = input.closest('[data-iitix-file-upload]'); 2321 var preview = upload.find('[data-iitix-file-preview]').first(); 2322 var fileNameEl = upload.find('[data-iitix-file-name]').first(); 2323 var el = input.get(0); 2324 var file = el && el.files && el.files.length ? el.files[0] : null; 2325 var previewField = { 2326 empty_preview_title: upload.attr('data-iitix-empty-preview-title') || 'No image yet', 2327 empty_preview_subtitle: upload.attr('data-iitix-empty-preview-subtitle') || 'Preview here', 2328 icon: upload.attr('data-iitix-empty-preview-icon') || '🖼' 2329 }; 2330 2331 upload.removeClass('has-file'); 2332 preview.addClass('is-empty').html(getIitixFilePlaceholderHtml(previewField)); 2333 fileNameEl.text(''); 2334 2335 if (!file) return; 2336 2337 fileNameEl.text(file.name + ' • ' + formatIitixFileSize(file.size)); 2338 2339 if (window.FileReader && file.type && file.type.indexOf('image/') === 0) { 2340 var reader = new FileReader(); 2341 reader.onload = function (event) { 2342 upload.addClass('has-file'); 2343 preview.removeClass('is-empty').html('<img src="' + esc(event.target.result) + '" alt="' + esc(file.name) + '">'); 2344 };
2345 reader.readAsDataURL(file); 2346 } else { 2347 upload.addClass('has-file'); 2348 preview.removeClass('is-empty').html('<div class="iitix-file-placeholder"><strong>' + esc(file.name) + '</strong><small>' + esc(formatIitixFileSize(file.size)) + '</small></div>'); 2349 } 2350 } 2351 2352 2353 function resetIitixFilePreview(input, note) { 2354 var upload = input.closest('[data-iitix-file-upload]'); 2355 var preview = upload.find('[data-iitix-file-preview]').first(); 2356 var fileNameEl = upload.find('[data-iitix-file-name]').first(); 2357 var previewField = { 2358 empty_preview_title: upload.attr('data-iitix-empty-preview-title') || 'No image yet', 2359 empty_preview_subtitle: note || upload.attr('data-iitix-empty-preview-subtitle') || 'Preview here', 2360 icon: upload.attr('data-iitix-empty-preview-icon') || '🖼' 2361 }; 2362 2363 upload.removeClass('has-file'); 2364 preview.addClass('is-empty').html(getIitixFilePlaceholderHtml(previewField)); 2365 fileNameEl.text(''); 2366 } 2367 2368 function setIitixFileCheckingState(input) { 2369 var upload = input.closest('[data-iitix-file-upload]'); 2370 var fileNameEl = upload.find('[data-iitix-file-name]').first(); 2371 var el = input.get(0); 2372 var file = el && el.files && el.files.length ? el.files[0] : null; 2373 2374 resetIitixFilePreview(input, 'Checking image dimensions...'); 2375 if (file) { 2376 fileNameEl.text(file.name + ' • ' + formatIitixFileSize(file.size) + ' • Checking dimensions...'); 2377 } 2378 } 2379 2380 function clearIitixFileInput(input) { 2381 input.val(''); 2382 input.removeData('iitixImageDimensionState'); 2383 input.removeData('iitixFileValidateOnChange'); 2384 resetIitixFilePreview(input); 2385 } 2386 2387 function showIitixUploadValidationAlert(message) { 2388 var text = message || 'The selected image does not match the upload requirements.'; 2389 2390 if (typeof Swal !== 'undefined' && Swal && typeof Swal.fire === 'function') { 2391 Swal.fire({ 2392 icon: 'warning', 2393 title: 'Invalid upload', 2394 text: text, 2395 confirmButtonText: 'OK' 2396 }); 2397 return; 2398 } 2399 2400 alert(text); 2401 } 2402 2403 function markIitixFileUploadInvalid(input, message) { 2404 var form = input.closest('form.variations_form'); 2405 2406 if (form.length) { 2407 clearExtraInvalidMarkers(form); 2408 renderExtraMessage(form, '', ''); 2409 markExtraInvalidTarget(input, message); 2410 setActiveExtraInvalidState(form, input, message); 2411 renderExtraMessage(form, 'error', message); 2412 } 2413 } 2414 2415 function getIitixFileFieldDefinition(form, input) { 2416 var profile = getCurrentExtraProfile(form); 2417 var fieldName = $.trim(input.attr('name') || input.attr('data-iitix-field-name') || ''); 2418 return getFieldDefByName(profile, fieldName) || {}; 2419 } 2420 2421 function validateIitixFileSelectionOnChange(input, dimensionReady) { 2422 if (!input || !input.length) return; 2423 2424 var form = input.closest('form.variations_form'); 2425 var field = getIitixFileFieldDefinition(form, input); 2426 var el = input.get(0); 2427 var file = el && el.files && el.files.length ? el.files[0] : null; 2428 2429 if (!file) { 2430 input.removeData('iitixFileValidateOnChange'); 2431 input.removeData('iitixImageDimensionState'); 2432 resetIitixFilePreview(input); 2433 if (form.length) validateExtraFields(form, false); 2434 return; 2435 } 2436 2437 input.data('iitixFileValidateOnChange', true); 2438 var result = validateClientFileInput(input, field); 2439 2440 // Dimension checks are asynchronous. Until the image is decoded, keep the 2441 // placeholder visible and do not show preview yet. 2442 if (!result.valid && /^Checking\s/.test(result.message || '') && !dimensionReady) { 2443 setIitixFileCheckingState(input); 2444 return; 2445 } 2446 2447 if (!result.valid) { 2448 var message = result.message || 'The selected image does not match the upload requirements.'; 2449 clearIitixFileInput(input); 2450 markIitixFileUploadInvalid(input, message); 2451 showIitixUploadValidationAlert(message); 2452 return; 2453 } 2454 2455 input.removeData('iitixFileValidateOnChange'); 2456 renderIitixFilePreview(input); 2457 if (form.length) validateExtraFields(form, false); 2458 } 2459 2460 /* ========================= 2461 * Add to cart success UX 2462 * ========================= */ 2463 var iitixAddToCartPending = false;
2464 var iitixAddToCartPendingAt = 0; 2465 var iitixAddToCartDoneTimer = null; 2466 2467 function getIitixToastRoot() { 2468 var root = document.getElementById('iitix-seat-toast-root'); 2469 if (!root) { 2470 root = document.createElement('div'); 2471 root.id = 'iitix-seat-toast-root'; 2472 document.body.appendChild(root); 2473 } 2474 return root; 2475 } 2476 2477 function showIitixSeatToast(message, type) { 2478 var root = getIitixToastRoot(); 2479 var toast = document.createElement('div'); 2480 toast.className = 'iitix-seat-toast ' + (type === 'error' ? 'is-error' : 'is-success'); 2481 toast.innerHTML = '' + 2482 '<span class="iitix-seat-toast-icon">' + (type === 'error' ? '!' : '✓') + '</span>' + 2483 '<span class="iitix-seat-toast-text">' + esc(message || 'Added to cart successfully.') + '</span>'; 2484 2485 root.appendChild(toast); 2486 2487 window.setTimeout(function () { 2488 toast.classList.add('is-visible'); 2489 }, 20); 2490 2491 window.setTimeout(function () { 2492 toast.classList.remove('is-visible'); 2493 window.setTimeout(function () { 2494 if (toast.parentNode) toast.parentNode.removeChild(toast); 2495 }, 260); 2496 }, 2600); 2497 } 2498 2499 function markIitixAddToCartPending(form) { 2500 iitixAddToCartPending = true; 2501 iitixAddToCartPendingAt = nowMs(); 2502 2503 if (iitixAddToCartDoneTimer) { 2504 window.clearTimeout(iitixAddToCartDoneTimer); 2505 iitixAddToCartDoneTimer = null; 2506 } 2507 2508 if (form && form.length) { 2509 form.addClass('iitix-add-to-cart-pending'); 2510 } 2511 } 2512 2513 function clearIitixAddToCartPending() { 2514 iitixAddToCartPending = false; 2515 iitixAddToCartPendingAt = 0; 2516 $('#tc-modal-woobridge form.variations_form').removeClass('iitix-add-to-cart-pending'); 2517 } 2518 2519 function finishIitixAddToCartSuccess() { 2520 if (!iitixAddToCartPending) return; 2521 2522 clearIitixAddToCartPending(); 2523 showIitixSeatToast('Added to cart successfully.', 'success'); 2524 2525 // Give WooCommerce/Tickera a short moment to update fragments/subtotal, 2526 // then close the seat modal cleanly so the user returns to the seating chart. 2527 window.setTimeout(function () { 2528 closeTickeraSeatModal(); 2529 }, 450); 2530 } 2531 2532 function scheduleIitixAddToCartSuccess() { 2533 if (!iitixAddToCartPending) return; 2534 2535 if (iitixAddToCartDoneTimer) { 2536 window.clearTimeout(iitixAddToCartDoneTimer); 2537 } 2538 2539 iitixAddToCartDoneTimer = window.setTimeout(function () { 2540 iitixAddToCartDoneTimer = null; 2541 finishIitixAddToCartSuccess(); 2542 }, 350); 2543 } 2544 2545 function isLikelyWooFragmentRequest(settings) { 2546 var url = String(settings && settings.url || ''); 2547 var data = String(settings && settings.data || ''); 2548 2549 return url.indexOf('wc-ajax=get_refreshed_fragments') !== -1 || 2550 data.indexOf('action=woocommerce_get_refreshed_fragments') !== -1 || 2551 data.indexOf('wc-ajax=get_refreshed_fragments') !== -1; 2552 } 2553 2554 function getAjaxSettingValue(settings, key) { 2555 if (!settings || !key) return ''; 2556 2557 var data = settings.data; 2558 if (window.FormData && data instanceof FormData) { 2559 var formDataValue = data.get(key); 2560 return formDataValue == null ? '' : String(formDataValue); 2561 } 2562 2563 if ($.isPlainObject(data)) { 2564 var objectValue = data[key]; 2565 if (Array.isArray(objectValue)) return objectValue.length ? String(objectValue[0]) : ''; 2566 return objectValue == null ? '' : String(objectValue); 2567 } 2568 2569 if (typeof data === 'string') { 2570 return String(parseQS(data)[key] || ''); 2571 } 2572 2573 return ''; 2574 } 2575 2576 function isTickeraFinalSeatAddRequest(settings) { 2577 var action = $.trim(getAjaxSettingValue(settings, 'action') || ''); 2578 if (action !== 'tc_woo_update_cart_seats') return false; 2579 2580 // Removal sends quantity 0 and must not be blocked by missing upload fields. 2581 var quantity = getAjaxSettingValue(settings, 'quantity'); 2582 return quantity === '' || parseInt(quantity, 10) !== 0; 2583 } 2584 2585 function isLikelyAddToCartRequest(settings) { 2586 var url = String(settings && settings.url || ''); 2587 var data = settings && settings.data; 2588 var dataString = typeof data === 'string' ? data : ''; 2589 2590 if (shouldHandleTickeraVariationAjax(settings)) return false;
2591 if (isTickeraFinalSeatAddRequest(settings)) return true; 2592 2593 // Keep support for a standard WooCommerce AJAX add-to-cart flow. Do not 2594 // treat Tickera availability probes as add-to-cart requests. 2595 return url.indexOf('wc-ajax=add_to_cart') !== -1 || 2596 dataString.indexOf('add-to-cart=') !== -1 || 2597 dataString.indexOf('add_to_cart') !== -1; 2598 } 2599 2600 2601 2602 /* ========================= 2603 * Add custom extra fields into AJAX Add to Cart payload 2604 * ========================= */ 2605 function getIitixSeatContextPayload(form) { 2606 var modal = form && form.length ? form.closest('.tc-modal-wrap') : $(); 2607 if (!modal.length) modal = getSeatModal(); 2608 2609 var seatId = $.trim(modal.find('.tc_regular_modal_seat_id').first().val() || ''); 2610 var seatLabel = normalizeSeatIdentity(modal.find('.tc_regular_modal_seat_label').first().text() || ''); 2611 var chartId = $.trim(modal.find('.tc_regular_modal_seating_chart_id').first().val() || ''); 2612 var ticketTypeId = $.trim(modal.find('.tc_regular_modal_ticket_type_id').first().val() || ''); 2613 var variationId = $.trim(form.find('input[name="variation_id"]').first().val() || ''); 2614 2615 if (!ticketTypeId) ticketTypeId = variationId; 2616 2617 return { 2618 seatId: seatId, 2619 seatLabel: seatLabel, 2620 chartId: chartId, 2621 ticketTypeId: ticketTypeId, 2622 variationId: variationId 2623 }; 2624 } 2625 2626 function getIitixFinalSubmissionToken(form) { 2627 var token = form.data('iitixFinalSubmissionToken'); 2628 if (token) return String(token); 2629 2630 token = 'iitix-' + nowMs() + '-' + Math.random().toString(36).slice(2, 12); 2631 form.data('iitixFinalSubmissionToken', token); 2632 return token; 2633 } 2634 2635 function getIitixExtraFieldsPayload(form) { 2636 if (!form || !form.length || !currentSeatAllowsExtraFields(form)) return []; 2637 2638 // Keep hidden total wattage updated before the AJAX request is sent. 2639 validateExtraFields(form, false); 2640 2641 var fields = form.find('[name^="iitix_"]').not(':file').serializeArray(); 2642 var categoryAttributeName = getCategoryAttributeName(); 2643 var categoryValue = getCurrentExtraCategoryValue(form); 2644 var seatContext = getIitixSeatContextPayload(form); 2645 2646 // Tickera/WooBridge normally serializes variation attributes itself. Add the 2647 // selected category defensively when it is absent so PHP resolves the same 2648 // category profile as the visible form. 2649 if (categoryValue && !fields.some(function (field) { 2650 return field && field.name === categoryAttributeName; 2651 })) { 2652 fields.push({ name: categoryAttributeName, value: categoryValue }); 2653 } 2654 2655 // This explicit context prevents a multi-seat cart from ever relying on 2656 // attendee order/index. PHP persists it with the exact cart line. 2657 fields.push({ name: 'iitix_csm_submission_context', value: 'tickera_woo_seat_add' }); 2658 fields.push({ name: 'iitix_csm_submission_token', value: getIitixFinalSubmissionToken(form) }); 2659 fields.push({ name: 'iitix_csm_seat_id', value: seatContext.seatId }); 2660 fields.push({ name: 'iitix_csm_seat_label', value: seatContext.seatLabel }); 2661 fields.push({ name: 'iitix_csm_chart_id', value: seatContext.chartId }); 2662 fields.push({ name: 'iitix_csm_ticket_type_id', value: seatContext.ticketTypeId }); 2663 2664 return fields; 2665 } 2666 2667 function getIitixExtraFilesPayload(form) { 2668 var files = []; 2669 if (!form || !form.length || !currentSeatAllowsExtraFields(form)) return files; 2670 2671 form.find('input[type="file"][name^="iitix_"]:not(:disabled)').each(function () { 2672 var input = this; 2673 if (input.files && input.files.length) { 2674 files.push({ name: input.name, file: input.files[0] }); 2675 } 2676 }); 2677 2678 return files; 2679 } 2680 2681 function appendQueryStringToFormData(formData, queryString) { 2682 String(queryString || '').split('&').forEach(function (part) { 2683 if (!part) return; 2684 var idx = part.indexOf('='); 2685 var key = idx >= 0 ? part.slice(0, idx) : part; 2686 var value = idx >= 0 ? part.slice(idx + 1) : ''; 2687 try { 2688 key = decodeURIComponent(key.replace(/\+/g, ' ')); 2689 value = decodeURIComponent(value.replace(/\+/g, ' ')); 2690 } catch (e) {} 2691 if (key) formData.append(key, value); 2692 }); 2693 } 2694 2695 function appendObjectToFormData(formData, obj) { 2696 Object.keys(obj || {}
2696).forEach(function (key) { 2697 var value = obj[key]; 2698 if (Array.isArray(value)) { 2699 var arrayKey = /\[\]$/.test(key) ? key : key + '[]'; 2700 value.forEach(function (item) { formData.append(arrayKey, item == null ? '' : item); }); 2701 } else { 2702 formData.append(key, value == null ? '' : value); 2703 } 2704 }); 2705 } 2706 2707 function replaceIitixFormDataValue(formData, name, value, filename) { 2708 if (typeof formData.delete === 'function') { 2709 formData.delete(name); 2710 } 2711 2712 if (typeof filename === 'string' && filename) { 2713 formData.append(name, value, filename); 2714 } else { 2715 formData.append(name, value == null ? '' : value); 2716 } 2717 } 2718 2719 function appendIitixFieldsAndFilesToFormData(formData, fields, files) { 2720 fields.forEach(function (field) { 2721 replaceIitixFormDataValue(formData, field.name, field.value); 2722 }); 2723 2724 files.forEach(function (item) { 2725 // Some handlers have already appended an empty file input. Replace it 2726 // instead of trusting FormData.has(), otherwise PHP sees UPLOAD_ERR_NO_FILE. 2727 replaceIitixFormDataValue(formData, item.name, item.file, item.file.name); 2728 }); 2729 } 2730 2731 function ajaxDataAlreadyHasField(dataString, fieldName) { 2732 dataString = String(dataString || ''); 2733 var rawKey = fieldName + '='; 2734 var encodedKey = encodeURIComponent(fieldName) + '='; 2735 2736 return dataString.indexOf(rawKey) !== -1 || dataString.indexOf(encodedKey) !== -1; 2737 } 2738 2739 function appendIitixExtraFieldsToAjaxSettings(settings) { 2740 if (!settings || !isLikelyAddToCartRequest(settings)) return; 2741 2742 var form = getVariationForm(); 2743 if (!form || !form.length) return; 2744 2745 var fields = getIitixExtraFieldsPayload(form); 2746 var files = getIitixExtraFilesPayload(form); 2747 if (!fields.length && !files.length) return; 2748 2749 // If there is a file upload, force FormData so PHP receives $_FILES. 2750 if (files.length && window.FormData) { 2751 var fd = settings.data instanceof FormData ? settings.data : new FormData(); 2752 2753 if (!(settings.data instanceof FormData)) { 2754 if (typeof settings.data === 'string') { 2755 appendQueryStringToFormData(fd, settings.data); 2756 } else if ($.isPlainObject(settings.data)) { 2757 appendObjectToFormData(fd, settings.data); 2758 } 2759 } 2760 2761 appendIitixFieldsAndFilesToFormData(fd, fields, files); 2762 settings.data = fd; 2763 settings.processData = false; 2764 settings.contentType = false;
2765 settings.enctype = 'multipart/form-data'; 2766 log('[IITIX] extra fields/files appended to FormData add-to-cart payload', { fields: fields, files: files.map(function (item) { return item.name; }) }); 2767 return; 2768 } 2769 2770 // Case 1: FormData payload. 2771 if (window.FormData && settings.data instanceof FormData) { 2772 appendIitixFieldsAndFilesToFormData(settings.data, fields, files); 2773 log('[IITIX] extra fields appended to FormData add-to-cart payload', fields); 2774 return; 2775 } 2776 2777 // Case 2: query string payload. This is the usual Tickera/WooBridge admin-ajax.php flow. 2778 if (typeof settings.data === 'string') { 2779 var existing = parseQS(settings.data); 2780 fields.forEach(function (field) { 2781 existing[field.name] = field.value; 2782 }); 2783 settings.data = $.param(existing); 2784 2785 log('[IITIX] extra fields appended to string add-to-cart payload', fields); 2786 return; 2787 } 2788 2789 // Case 3: plain object payload (Tickera's $.post uses this shape). 2790 if ($.isPlainObject(settings.data)) { 2791 fields.forEach(function (field) { 2792 settings.data[field.name] = field.value; 2793 }); 2794 2795 log('[IITIX] extra fields appended to object add-to-cart payload', fields); 2796 return; 2797 } 2798 } 2799 2800 function addIitixValidationError(errors, target, message) { 2801 if (!message) return; 2802 errors.push({ 2803 target: target && target.length ? target : $(), 2804 message: String(message) 2805 }); 2806 } 2807 2808 function validateExtraFields(form, showValidationFeedback) { 2809 if (!form || !form.length || !currentSeatAllowsExtraFields(form)) { 2810 clearExtraInvalidMarkers(form); 2811 renderExtraMessage(form, '', ''); 2812 setExtraButtonInvalid(form, false); 2813 return { valid: true, message: '', total: 0, max: 0, errors: [], invalidTarget: $() }; 2814 } 2815 2816 var block = form.find('.iitix-extra-fields').first(); 2817 2818 if (!block.length || !block.is(':visible')) { 2819 updateExtraFieldsVisibility(form); 2820 block = form.find('.iitix-extra-fields').first(); 2821 } 2822 2823 if (!block.length || !block.is(':visible')) { 2824 clearExtraInvalidMarkers(form); 2825 renderExtraMessage(form, '', ''); 2826 setExtraButtonInvalid(form, false); 2827 return { valid: true, message: '', total: 0, max: 0, errors: [], invalidTarget: $() }; 2828 } 2829 2830 var data = collectExtraFields(form); 2831 var profile = getCurrentExtraProfile(form); 2832 var max = getExtraMaxWattage(form); 2833 var total = getEffectiveTotalWattage(data); 2834 var validationErrors = []; 2835 2836 // Normal form opening and typing should remain clean. Do not clear an 2837 // already-highlighted required field here: this function is also called by 2838 // silent WooCommerce/Tickera re-validations after the user clicks Add to Cart. 2839 // The active marker is reconciled after validationErrors are fully built. 2840 2841 block.find('[name="iitix_total_wattage"]').val(total); 2842 block.find('[data-iitix-total-wattage]').text(total); 2843 2844 /* 2845 * STEP 1 - CATEGORY 2846 * Keep this first so validationErrors[0] always respects the visible step order. 2847 */ 2848 var categorySelect = form.find('select[name^="attribute_"]').first(); 2849 var categoryValue = categorySelect.length ? $.trim(categorySelect.val() || '') : ''; 2850 var variationId = $.trim(form.find('input.variation_id').val() || ''); 2851 2852 if (categorySelect.length && !categoryValue) { 2853 addIitixValidationError(validationErrors, categorySelect, 'Step 1 is required: please choose category first.'); 2854 } 2855 2856 /* 2857 * Dynamic required profile fields, e.g. Instagram and Brand Logo for Packaged F&B. 2858 * We collect every issue in order, then display and scroll only to index 0. 2859 */ 2860 if (profile) { 2861 var sections = getProfileSections(profile); 2862 for (var s = 0; s < sections.length; s++) { 2863 var section = sections[s] || {}; 2864 var fields = Array.isArray(section.fields) ? section.fields : []; 2865 2866 for (var f = 0; f < fields.length; f++) { 2867 var field = fields[f] || {}; 2868 if (!field.name || field.name === 'iitix_menu') continue; 2869 2870 var input = form.find('[name="' + field.name + '"]').first(); 2871 var value = data.fields && typeof data.fields[field.name] !== 'undefined' 2872 ? data.fields[field.name] 2873 : $.trim(input.val() || ''); 2874 var stepPrefix = section.step_number ? ('Step ' + section.step_number + ' is required: ') : ''; 2875 var fieldType = String(field.type || 'text').toLowerCase(); 2876 2877 if (fieldType === 'file') { 2878 var fileValidation = validateClientFileInput(input, field); 2879 if (!fileValidation.valid) { 2880 addIitixValidationError(validationErrors, input, stepPrefix + fileValidation.message); 2881 } 2882 continue; 2883 } 2884 2885 if (field.required && !value) { 2886 addIitixValidationError( 2887 validationErrors, 2888 input.length ? input : form.find('[data-iitix-radio-group="' + field.name + '"]').first(), 2889 stepPrefix + 'please fill ' + (field.label || field.meta_label || field.name) + '.' 2890 ); 2891 } 2892 } 2893 } 2894 } 2895 2896 /* 2897 * MENU 2898 */ 2899 var menuRequired = profile 2900 ? !!(getProfileField(profile, 'iitix_menu') && getProfileField(profile, 'iitix_menu').required) 2901 : (EXTRA_CONFIG.menuRequired === true); 2902 if (menuRequired && !data.menu) { 2903 addIitixValidationError( 2904 validationErrors, 2905 form.find('[name="iitix_menu"]').first(), 2906 'Step ' + ((getProfileSectionByKey(profile, 'menu') || {}
2906).step_number || 2) + ' is required: please fill the menu list first.' 2907 ); 2908 } 2909 2910 /* 2911 * APPLIANCE 1 2912 */ 2913 var applianceSection = getActiveApplianceSection(form) || {}; 2914 var applianceRequired = profile 2915 ? (!!profile.appliance_required || !!applianceSection.required_first_slot) 2916 : (EXTRA_CONFIG.applianceRequired === true); 2917 var applianceStep = applianceSection.step_number || 3; 2918 2919 if (applianceRequired && !data.appliance1Type) { 2920 addIitixValidationError( 2921 validationErrors, 2922 form.find('[name="iitix_appliance_1_type"]').first(), 2923 'Step ' + applianceStep + ' is required: please select Appliance 1.' 2924 ); 2925 } else if (data.appliance1Type && data.appliance1Wattage <= 0) { 2926 addIitixValidationError( 2927 validationErrors, 2928 form.find('[name="iitix_appliance_1_wattage"]').first(), 2929 'Step ' + applianceStep + ' is required: please input wattage for Appliance 1.' 2930 ); 2931 } else if (!data.appliance1Type && data.appliance1Wattage > 0) { 2932 addIitixValidationError( 2933 validationErrors, 2934 form.find('[name="iitix_appliance_1_type"]').first(), 2935 'Step ' + applianceStep + ' is required: please select appliance type for Appliance 1.' 2936 ); 2937 } else if ((normalize(data.appliance1Type) === 'other' || normalize(data.appliance1Type) === 'others') && !data.appliance1Other) { 2938 addIitixValidationError( 2939 validationErrors, 2940 form.find('[name="iitix_appliance_1_other"]').first(), 2941 'Step ' + applianceStep + ' is required: please type the other appliance name for Appliance 1.' 2942 ); 2943 } else { 2944 var appliance1MinWatt = getApplianceMinWatt(data.appliance1Type); 2945 if (data.appliance1Type && normalize(data.appliance1Type) !== 'other' && normalize(data.appliance1Type) !== 'others' && appliance1MinWatt > 0 && data.appliance1Wattage > 0 && data.appliance1Wattage < appliance1MinWatt) { 2946 addIitixValidationError( 2947 validationErrors, 2948 form.find('[name="iitix_appliance_1_wattage"]').first(), 2949 formatExtraCopy(getExtraCopy('belowMinimumMessage', 'Minimum watt for {appliance} is {min}W. Please enter at least {min}W.'), { 2950 appliance: data.appliance1Type, 2951 min: appliance1MinWatt 2952 }) 2953 ); 2954 } 2955 } 2956 2957 /* 2958 * OPTIONAL APPLIANCE 2 2959 * If the vendor starts filling it, type and wattage must be complete. 2960 */ 2961 if (data.appliance2Type && data.appliance2Wattage <= 0) { 2962 addIitixValidationError(validationErrors, form.find('[name="iitix_appliance_2_wattage"]').first(), 'Please input wattage for Appliance 2.'); 2963 } else if (!data.appliance2Type && data.appliance2Wattage > 0) { 2964 addIitixValidationError(validationErrors, form.find('[name="iitix_appliance_2_type"]').first(), 'Please select appliance type for Appliance 2.'); 2965 } else if ((normalize(data.appliance2Type) === 'other' || normalize(data.appliance2Type) === 'others') && !data.appliance2Other) { 2966 addIitixValidationError(validationErrors, form.find('[name="iitix_appliance_2_other"]').first(), 'Please type the other appliance name for Appliance 2.'); 2967 } else { 2968 var appliance2MinWatt = getApplianceMinWatt(data.appliance2Type); 2969 if (data.appliance2Type && normalize(data.appliance2Type) !== 'other' && normalize(data.appliance2Type) !== 'others' && appliance2MinWatt > 0 && data.appliance2Wattage > 0 && data.appliance2Wattage < appliance2MinWatt) { 2970 addIitixValidationError( 2971 validationErrors, 2972 form.find('[name="iitix_appliance_2_wattage"]').first(), 2973 formatExtraCopy(getExtraCopy('belowMinimumMessage', 'Minimum watt for {appliance} is {min}W. Please enter at least {min}W.'), { 2974 appliance: data.appliance2Type, 2975 min: appliance2MinWatt 2976 }) 2977 ); 2978 } 2979 } 2980 2981 /* 2982 * TOTAL WATT LIMIT 2983 */ 2984 if (total > max) { 2985 addIitixValidationError( 2986 validationErrors, 2987 form.find('.iitix-watt-summary').first(), 2988 getExtraCopy('limitMessage', 'Total wattage cannot be more than 1000 watt.') 2989 ); 2990 } 2991 2992 /* 2993 * Final WooCommerce safety: a selected category may still be waiting for 2994 * variation_id. It belongs after the visible category requirement. 2995 */ 2996 if (categorySelect.length && categoryValue && !variationId) { 2997 addIitixValidationError(validationErrors, categorySelect, 'Step 1 is required: please choose category first.'); 2998 } 2999 3000 var firstError = validationErrors.length ? validationErrors[0] : null;
3001 var valid = !firstError; 3002 var message = firstError ? firstError.message : ''; 3003 var invalidTarget = firstError ? firstError.target : $(); 3004 3005 if (showValidationFeedback) { 3006 // A real Add to Cart attempt always starts a fresh first-error cycle. 3007 // Clear the previous marker first, then paint only validationErrors[0]. 3008 clearExtraInvalidMarkers(form); 3009 renderExtraMessage(form, '', ''); 3010 setExtraButtonInvalid(form, !valid, message); 3011 3012 if (!valid) { 3013 markExtraInvalidTarget(invalidTarget, message); 3014 setActiveExtraInvalidState(form, invalidTarget, message); 3015 renderExtraMessage(form, 'error', message); 3016 } else if (total > 0) { 3017 renderExtraMessage(form, 'ok', 'Total wattage is valid: ' + total + 'W / ' + max + 'W.'); 3018 } 3019 } else { 3020 // Do not surface a new required error while the vendor is typing. 3021 // Keep a previous Add to Cart marker until that exact field is valid. 3022 setExtraButtonInvalid(form, false); 3023 reconcileActiveExtraInvalidState(form, validationErrors); 3024 } 3025 3026 stabilizeValidVariationUi(form); 3027 3028 return { 3029 valid: valid, 3030 message: message, 3031 total: total, 3032 max: max, 3033 errors: validationErrors, 3034 invalidTarget: invalidTarget 3035 }; 3036 } 3037 3038 function bootExtraFieldsForCurrentForm() { 3039 if (isSeatModalClosing) return; 3040 3041 var modal = getSeatModal(); 3042 if (!modal.length) return; 3043 if (!isSeatModalActuallyVisible(modal)) return; 3044 3045 var form = modal.find('.variations_form').first(); 3046 if (!form.length) return; 3047 3048 updateExtraFieldsVisibility(form); 3049 validateExtraFields(form, false); 3050 } 3051 3052 3053 function scheduleBootExtraFields(delay) { 3054 if (isSeatModalClosing) return; 3055 3056 if (extraBootTimer) window.clearTimeout(extraBootTimer); 3057 3058 extraBootTimer = window.setTimeout(function () { 3059 extraBootTimer = null; 3060 3061 if (isSeatModalClosing) return; 3062 3063 var modal = getSeatModal(); 3064 if (!isSeatModalActuallyVisible(modal)) return; 3065 3066 bootExtraFieldsForCurrentForm(); 3067 }, typeof delay === 'number' ? delay : 80); 3068 } 3069 3070 3071 $('body').on('click', '#tc-modal-woobridge [data-iitix-file-trigger]', function (event) { 3072 event.preventDefault(); 3073 var upload = $(this).closest('[data-iitix-file-upload]'); 3074 upload.find('input[type="file"]').first().trigger('click'); 3075 }); 3076 3077 $('body').on('change', '#tc-modal-woobridge input[type="file"][name^="iitix_"]', function () { 3078 var input = $(this); 3079 3080 // Validate immediately when a file is selected. Invalid files are cleared, 3081 // never previewed, and the user sees the error before Add to Cart. 3082 input.removeData('iitixImageDimensionState'); 3083 input.data('iitixFileValidateOnChange', true); 3084 validateIitixFileSelectionOnChange(input, false); 3085 }); 3086 3087 function formatIitixCurrency(amount) { 3088 amount = parseInt(amount, 10) || 0; 3089 return 'Rp' + String(amount).replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 3090 } 3091 3092 function setIitixPromoPackageChoice(form, value) { 3093 if (!form || !form.length) return; 3094 3095 var radio = form.find('input[name="iitix_promo_package"][value="' + value + '"]').first(); 3096 if (!radio.length) return; 3097 3098 radio.prop('checked', true).trigger('change', [{ iitixPromoProgrammatic: true }]); 3099 } 3100 3101 function openIitixPromoPackageConfirmation(form) { 3102 var profile = getCurrentExtraProfile(form) || {}; 3103 var field = getProfileField(profile, 'iitix_promo_package') || {}; 3104 var fee = parseInt(field.addon_fee, 10) || 0; 3105 var feeLabel = formatIitixCurrency(fee); 3106 var promoLabel = field.addon_label || field.label || 'IG Story & LED Backdrop'; 3107 3108 var content = '' + 3109 '<div style="text-align:left;line-height:1.55">' + 3110 '<strong>' + esc(promoLabel) + '</strong>' + 3111 '<div style="margin-top:8px">Additional fee: <strong>' + esc(feeLabel) + '</strong></div>' + 3112 '<div style="margin-top:8px;color:#64748b">This fee is charged for this vendor slot / booth and will appear in Cart and Checkout.</div>' + 3113 '</div>'; 3114 3115 var applyChoice = function (confirmed) { 3116 setIitixPromoPackageChoice(form, confirmed ? 'yes' : 'no'); 3117 }; 3118 3119 if (typeof Swal !== 'undefined' && Swal && typeof Swal.fire === 'function') { 3120 Swal.fire({ 3121 icon: 'question', 3122 title: 'Add Promo Package?', 3123 html: content, 3124 showCancelButton: true, 3125 confirmButtonText: 'Yes, Add Promo Package', 3126 cancelButtonText: 'Cancel', 3127 reverseButtons: true, 3128 focusCancel: true 3129 }).then(function (result) { 3130 applyChoice(!!(result && result.isConfirmed)); 3131 }); 3132 return; 3133 } 3134 3135 // Fallback for pages where SweetAlert is blocked by an optimization plugin. 3136 applyChoice(window.confirm('Add ' + promoLabel + ' for ' + feeLabel + ' to this vendor slot / booth?')); 3137 } 3138 3139 $('body')
3140 .off('change.iitixPromoPackageConfirmation') 3141 .on('change.iitixPromoPackageConfirmation', '#tc-modal-woobridge input[name="iitix_promo_package"]', function (event, detail) { 3142 var radio = $(this); 3143 var form = radio.closest('form.variations_form'); 3144 3145 // This is the final state after the modal has been confirmed or cancelled. 3146 if (detail && detail.iitixPromoProgrammatic) return; 3147 if (String(radio.val() || '').toLowerCase() !== 'yes') return; 3148 if (!radio.is(':checked')) return; 3149 3150 // Do not let the Yes state become final until the vendor explicitly confirms. 3151 // Return the visual selection to No while the confirmation dialog is open. 3152 form.find('input[name="iitix_promo_package"][value="no"]').first().prop('checked', true); 3153 event.stopImmediatePropagation(); 3154 3155 openIitixPromoPackageConfirmation(form); 3156 }); 3157 3158 $('body').on('change input', '#tc-modal-woobridge .iitix-extra-fields input, #tc-modal-woobridge .iitix-extra-fields textarea, #tc-modal-woobridge .iitix-extra-fields select', function (event) { 3159 var target = $(this); 3160 var form = target.closest('form.variations_form'); 3161 3162 if (event.type === 'change' && target.hasClass('iitix-appliance-select')) { 3163 autoFillMinimumWattForSelect(form, target); 3164 } 3165 3166 toggleOtherInputs(form); 3167 validateExtraFields(form, false); 3168 }); 3169 3170 $('body') 3171 .off('submit.iitixExtraRequiredGuard') 3172 .on('submit.iitixExtraRequiredGuard', '#tc-modal-woobridge form.variations_form', function (e) { 3173 var form = $(this); 3174 var result = validateExtraFields(form, true); 3175 3176 var variationId = $.trim(form.find('input.variation_id').val() || ''); 3177 var hasVariationSelect = form.find('select[name^="attribute_"]').length > 0; 3178 3179 if (!result.valid || (hasVariationSelect && !variationId)) { 3180 e.preventDefault(); 3181 e.stopPropagation(); 3182 3183 if (e.stopImmediatePropagation) { 3184 e.stopImmediatePropagation(); 3185 } 3186 3187 scrollToFirstInvalidField(form, result.invalidTarget); 3188 3189 return false; 3190 } 3191 }); 3192 3193 $('body').on('change', '#tc-modal-woobridge select[name^="attribute_"]', function () { 3194 var form = $(this).closest('form.variations_form'); 3195 3196 updateExtraFieldsVisibility(form); 3197 validateExtraFields(form, false); 3198 3199 // WooCommerce biasanya set variation_id sedikit setelah select berubah. 3200 // Jadi validasi ulang setelah WC selesai proses variation. 3201 window.setTimeout(function () { 3202 validateExtraFields(form, false); 3203 }, 150); 3204}); 3205 3206 if ($.ajaxPrefilter) { 3207 $.ajaxPrefilter(function (options) { 3208 appendIitixExtraFieldsToAjaxSettings(options); 3209 }); 3210 } 3211 3212 /* ========================= 3213 * Track Tickera variation request start time. 3214 * This lets us distinguish stale AJAX that started before close 3215 * from a fresh user click after close. 3216 * ========================= */ 3217 $(document).ajaxSend(function (event, xhr, settings) { 3218 // Important: Tickera/WooBridge add-to-cart AJAX does not always serialize 3219 // every custom field from the modal form. Append the iitix_* fields here 3220 // before the request is sent so PHP can store them in cart item data. 3221 appendIitixExtraFieldsToAjaxSettings(settings); 3222 3223 if (!shouldHandleTickeraVariationAjax(settings)) { 3224 return; 3225 } 3226 3227 var sentAt = nowMs(); 3228 lastSeatVariationAjaxSentAt = sentAt; 3229 settings.__iitixSeatVariationAjaxSentAt = sentAt; 3230 3231 // The first Tickera screen says "Loading options..." before the variation 3232 // form exists. Apply the shell class immediately so the loading modal keeps 3233 // the same width/centered position as the ready modal. 3234 applySeatFilterModalShellLoading(); 3235 window.setTimeout(applySeatFilterModalShellLoading, 20); 3236 window.setTimeout(applySeatFilterModalShellLoading, 90); 3237 }); 3238 3239 /* ========================= 3240 * AFTER AJAX SUCCESS 3241 * ========================= */ 3242 $(document).ajaxComplete(function (event, xhr, settings) { 3243 if (!shouldHandleTickeraVariationAjax(settings)) { 3244 return; 3245 } 3246 3247 var sentAt = parseInt(settings.__iitixSeatVariationAjaxSentAt || 0, 10) || 0; 3248 3249 // This is the main close/reopen fix: 3250 // If the request was started before the modal was closed, ignore it. 3251 // If the request was started after close, it is a real second user click, so allow it. 3252 if (lastSeatModalCloseAt && sentAt && sentAt <= lastSeatModalCloseAt) { 3253 requestSeq++; 3254 cleanupIitixWideModalState();
3255 forceHideSeatModalElements(); 3256 return; 3257 } 3258 3259 isSeatModalClosing = false; 3260 3261 if (seatModalClosingTimer) { 3262 window.clearTimeout(seatModalClosingTimer); 3263 seatModalClosingTimer = null; 3264 } 3265 3266 var payload = parseQS(settings.data); 3267 var chartId = parseInt(payload.chart_id || '', 10) || 0; 3268 var seatIdFromPayload = payload.seat_id || ''; 3269 var selected = $('.ui-selected').last(); 3270 var seatId = selected.attr('id') || seatIdFromPayload; 3271 var seatNumber = getSeatNumberFromSeatId(seatId); 3272 var productId = getProductIdFromPage(payload); 3273 var variations = getOriginalVariationsJSON(); 3274 var form = getVariationForm(); 3275 var seq = ++requestSeq; 3276 3277 if (form.length && productId > 0) { 3278 form.data('iitix-current-product-id', productId); 3279 applySeatFilterModalClass(form); 3280 } 3281 3282 updateSeatLabel(seatNumber); 3283 3284 log('[IITIX] Tickera seat selected', { 3285 product_id: productId, 3286 chart_id: chartId, 3287 seat_id: seatId, 3288 seat_number: seatNumber 3289 }); 3290 3291 if (!chartId || !seatNumber || !productId) { 3292 warn('[IITIX] Missing product_id/chart_id/seat_number, cannot call seat filter API', { 3293 product_id: productId, 3294 chart_id: chartId, 3295 seat_number: seatNumber 3296 }); 3297 return; 3298 } 3299 3300 if (!variations.length) { 3301 warn('[IITIX] No WooCommerce variations found on form'); 3302 } 3303 3304 if (form.length) { 3305 renderNotice(form, { loading: true, seat: seatNumber }); 3306 updateExtraFieldsVisibility(form); 3307 } 3308 3309 fetchSeatFilter(productId, chartId, seatNumber) 3310 .then(function (apiResponse) { 3311 if (seq !== requestSeq) return; 3312 3313 var filteredVariations = filterVariationsByApi(variations, apiResponse); 3314 3315 window.iitixSeatCategoryState = { 3316 source: 'tickera-store-api-seating-sync', 3317 endpoint: SEAT_FILTER_URL, 3318 product_id: productId, 3319 chart_id: chartId, 3320 seat_id: seatId, 3321 seat_number: seatNumber, 3322 categories: getReadableCategories(apiResponse), 3323 api_response: apiResponse, 3324 variations: filteredVariations 3325 }; 3326 3327 updateVariationsForm(filteredVariations, apiResponse, { 3328 seatNumber: seatNumber 3329 }); 3330 }) 3331 .catch(function (err) { 3332 if (seq !== requestSeq) return; 3333 3334 warn('[IITIX] seat filter API failed', err); 3335 restoreOriginalVariations(); 3336 3337 if (form.length) { 3338 setSeatFilterControlsLoading(form, false); 3339 renderNotice(form, { 3340 error: err && err.message ? err.message : String(err), 3341 seat: seatNumber 3342 }); 3343 updateExtraFieldsVisibility(form); 3344 } 3345 }); 3346 }); 3347 3348 3349 // Standard WooCommerce success event. 3350 $('body') 3351 .off('added_to_cart.iitixSeatSuccessClose') 3352 .on('added_to_cart.iitixSeatSuccessClose', function () { 3353 scheduleIitixAddToCartSuccess(); 3354 }); 3355 3356 // Some Tickera/WooCommerce flows do not emit added_to_cart, but they always 3357 // refresh WooCommerce fragments after the cart is updated. 3358 $(document) 3359 .off('ajaxComplete.iitixSeatAddToCartSuccess') 3360 .on('ajaxComplete.iitixSeatAddToCartSuccess', function (event, xhr, settings) { 3361 if (!iitixAddToCartPending) return; 3362 if (!xhr || xhr.status < 200 || xhr.status >= 300) return; 3363 3364 var age = nowMs() - iitixAddToCartPendingAt; 3365 if (age < 0 || age > 12000) { 3366 clearIitixAddToCartPending(); 3367 return; 3368 } 3369 3370 if (isLikelyWooFragmentRequest(settings) || isLikelyAddToCartRequest(settings)) { 3371 scheduleIitixAddToCartSuccess(); 3372 } 3373 }); 3374 3375 $(document) 3376 .off('ajaxError.iitixSeatAddToCartError') 3377 .on('ajaxError.iitixSeatAddToCartError', function (event, xhr, settings) { 3378 if (!iitixAddToCartPending) return; 3379 if (!isLikelyAddToCartRequest(settings)) return; 3380 3381 clearIitixAddToCartPending(); 3382 showIitixSeatToast('Add to cart failed. Please try again.', 'error'); 3383 }); 3384 3385 /* ========================= 3386 * Checkout: Food Service Menu -> Tickera Goods to sell 3387 * ========================= */ 3388 var MENU_AUTOFILL_CONFIG = window.IITIX_TICKERA_MENU_AUTOFILL_CONFIG || {}; 3389 var tickeraMenuAutofillTimer = null; 3390 3391 function normalizeTickeraMenuIdentity(value) { 3392 return String(value || '') 3393 .replace(/\s+/g, ' ') 3394 .trim() 3395 .toUpperCase(); 3396 } 3397 3398 function getTickeraMenuAutofillItems() { 3399 return MENU_AUTOFILL_CONFIG && Array.isArray(MENU_AUTOFILL_CONFIG.items) 3400 ? MENU_AUTOFILL_CONFIG.items 3401 : []; 3402 } 3403 3404 function getTickeraOwnerSeatIdentity(owner) { 3405 return { 3406 seatId: $.trim(owner.find('input[name^="owner_data_seat_id_post_meta"]').first().val() || ''), 3407 seatLabel: normalizeSeatIdentity(owner.find('input[name^="owner_data_seat_label_post_meta"]').first().val() || ''), 3408 chartId: $.trim(owner.find('input[name^="owner_data_chart_id_post_meta"]').first().val() || '') 3409 }; 3410 } 3411 3412 function findTickeraGoodsToSellField(owner) { 3413 var direct = owner.find('[name^="owner_data_"][name*="goodstosell"], [name^="owner_data_"][name*="goods_to_sell"]').filter('input, textarea, select').first(); 3414 if (direct.length) return direct; 3415 3416 var found = $(); 3417 owner.find('.fields-wrap').each(function () { 3418 if (found.length) return; 3419 var wrap = $(this); 3420 var label = String(wrap.find('label').first().text() || '').replace(/\s+/g, '').toLowerCase(); 3421 if (label.indexOf('goodstosell') === -1) return; 3422 found = wrap.find('input, textarea, select').filter('[name^="owner_data_"]').first(); 3423 }); 3424 3425 return found; 3426 } 3427 3428 function findTickeraMenuMapping(identity) { 3429 if (!identity || !identity.seatId || !identity.seatLabel || !identity.chartId) return null; 3430 3431 var items = getTickeraMenuAutofillItems(); 3432 for (var i = 0; i < items.length; i++) { 3433 var item = items[i] || {}; 3434 if (
3435 String(item.seat_id || '') === String(identity.seatId) && 3436 normalizeSeatIdentity(item.seat_label || '') === String(identity.seatLabel) && 3437 String(item.chart_id || '') === String(identity.chartId) 3438 ) { 3439 return item; 3440 } 3441 } 3442 3443 return null; 3444 } 3445 3446 function renderTickeraMenuAutofillNote(field, mapping, identity) { 3447 if (!field || !field.length || !mapping) return; 3448 3449 var wrap = field.closest('.fields-wrap'); 3450 if (!wrap.length) wrap = field.parent(); 3451 if (!wrap.length) return; 3452 3453 var note = wrap.find('[data-iitix-tickera-menu-autofill-note]').first(); 3454 var template = MENU_AUTOFILL_CONFIG.note || 'Auto-filled from the Food Service menu for seat {seat}. You can still edit this field if needed.'; 3455 var message = String(template).replace('{seat}', identity.seatLabel || mapping.seat_label || ''); 3456 3457 if (!note.length) { 3458 note = $('<div class="iitix-tickera-menu-autofill-note" data-iitix-tickera-menu-autofill-note="1" />'); 3459 field.after(note); 3460 } 3461 3462 note.text(message); 3463 } 3464 3465 function applyTickeraMenuAutofill() { 3466 if (!MENU_AUTOFILL_CONFIG || MENU_AUTOFILL_CONFIG.enabled === false) return; 3467 if (!getTickeraMenuAutofillItems().length) return; 3468 3469 $('.tickera_owner_info .owner-info-wrap, .tc-form-ticket-fields-wrap .owner-info-wrap').each(function () { 3470 var owner = $(this); 3471 var identity = getTickeraOwnerSeatIdentity(owner); 3472 var mapping = findTickeraMenuMapping(identity); 3473 if (!mapping || !$.trim(mapping.menu || '')) return; 3474 3475 var field = findTickeraGoodsToSellField(owner); 3476 if (!field.length) return; 3477 3478 // Manual input always wins. This makes Food Service + Fashion with Racks 3479 // carts safe: only the exact Food/Menu seat gets a value. 3480 if (!$.trim(field.val() || '')) { 3481 field.val(mapping.menu).attr('data-iitix-menu-autofilled', '1').trigger('input').trigger('change'); 3482 renderTickeraMenuAutofillNote(field, mapping, identity); 3483 } else if (field.attr('data-iitix-menu-autofilled') === '1' || $.trim(field.val() || '') === $.trim(mapping.menu || '')) { 3484 renderTickeraMenuAutofillNote(field, mapping, identity); 3485 } 3486 }); 3487 } 3488 3489 function scheduleTickeraMenuAutofill(delay) { 3490 if (tickeraMenuAutofillTimer) window.clearTimeout(tickeraMenuAutofillTimer); 3491 tickeraMenuAutofillTimer = window.setTimeout(function () { 3492 tickeraMenuAutofillTimer = null; 3493 applyTickeraMenuAutofill(); 3494 }, typeof delay === 'number' ? delay : 60); 3495 } 3496 3497 if (getTickeraMenuAutofillItems().length) { 3498 $(function () { 3499 scheduleTickeraMenuAutofill(60); 3500 scheduleTickeraMenuAutofill(400); 3501 }); 3502 3503 $(document.body).on('updated_checkout updated_wc_div', function () { 3504 scheduleTickeraMenuAutofill(80); 3505 }); 3506 3507 if (window.MutationObserver && document.body) { 3508 var tickeraCheckoutMenuObserver = new MutationObserver(function () { 3509 scheduleTickeraMenuAutofill(80); 3510 }); 3511 tickeraCheckoutMenuObserver.observe(document.body, { childList: true, subtree: true }); 3512 } 3513 } 3514 3515 /* ========================= 3516 * Collapsible Cart / Checkout detail cards 3517 * ========================= 3518 * Delegated binding is deliberate: WooCommerce can replace checkout/cart 3519 * fragments after a payment method or quantity change. 3520 */ 3521 $(document) 3522 .off('click.iitixCartExtraCollapse') 3523 .on('click.iitixCartExtraCollapse', '[data-iitix-cart-extra-card] > .iitix-cart-extra-card-summary', function () { 3524 var toggle = $(this); 3525 var card = toggle.closest('[data-iitix-cart-extra-card]'); 3526 var body = card.children('.iitix-cart-extra-card-body').first(); 3527 if (!body.length) return; 3528 3529 var isOpen = toggle.attr('aria-expanded') === 'true'; 3530 toggle.attr('aria-expanded', isOpen ? 'false' : 'true'); 3531 card.toggleClass('is-open', !isOpen); 3532 body.prop('hidden', isOpen); 3533 }); 3534 3535 /* ========================= 3536 * Mutation observer: modal markup is injected dynamically by Tickera. 3537 * ========================= */ 3538 if (window.MutationObserver && document.body) { 3539 var mo = new MutationObserver(function () { 3540 if (isSeatModalClosing) return; 3541 3542 var modal = getSeatModal(); 3543 if (!isSeatModalActuallyVisible(modal)) return; 3544 3545 if ($('#tc-modal-woobridge .variations_form').length) { 3546 scheduleBootExtraFields(120); 3547 } 3548 }); 3549 mo.observe(document.body, { childList: true, subtree: true }); 3550 } 3551 3552 $(function () { 3553 scheduleBootExtraFields(250); 3554 }); 3555 3556 /* ========================= 3557 * Override alert bawaan WooCommerce 3558 * ========================= */ 3559 (function () { 3560 if (typeof Swal === 'undefined') return; 3561 3562 window.alert = function (msg) { 3563 Swal.fire({ 3564 icon: 'warning', 3565 title: 'Select a variation first', 3566 text: msg, 3567 confirmButtonText: 'OK' 3568 }); 3569 }; 3570 })(); 3571 3572 /* ========================= 3573 * Intercept klik Add to Cart 3574 * ========================= */ 3575 3576 $('body') 3577 .off('click.iitixSeatModalClose') 3578 .on( 3579 'click.iitixSeatModalClose',
3580 '#tc-modal-woobridge .tc_modal_close_dialog, .ui-dialog.iitix-extra-dialog-wide .ui-dialog-titlebar-close', 3581 function (e) { 3582 e.preventDefault(); 3583 e.stopPropagation(); 3584 e.stopImmediatePropagation(); 3585 return closeTickeraSeatModal(); 3586 } 3587 ); 3588 3589 $('body') 3590 .off('click.iitixExtraRequiredClick') 3591 .on('click.iitixExtraRequiredClick', '.tc-modal-wrap .tc_cart_button', function (e) { 3592 var btn = $(this); 3593 var form = btn.closest('form.variations_form'); 3594 3595 if (form.length) { 3596 var extraValidation = validateExtraFields(form, true); 3597 var variationId = $.trim(form.find('input.variation_id').val() || ''); 3598 var hasVariationSelect = form.find('select[name^="attribute_"]').length > 0; 3599 3600 if (!extraValidation.valid || (hasVariationSelect && !variationId)) { 3601 e.preventDefault(); 3602 e.stopImmediatePropagation(); 3603 3604 scrollToFirstInvalidField(form, extraValidation.invalidTarget); 3605 3606 if (typeof Swal !== 'undefined') { 3607 Swal.fire({ 3608 icon: 'warning', 3609 title: 'Please complete the highlighted field', 3610 text: extraValidation.message || 'Please complete all required steps before adding this item to cart.', 3611 confirmButtonText: 'OK' 3612 }); 3613 } else { 3614 alert(extraValidation.message || 'Please complete all required steps before adding this item to cart.'); 3615 } 3616 3617 return false; 3618 } 3619 } 3620 3621 if ( 3622 btn.hasClass('wc-variation-selection-needed') || 3623 (btn.hasClass('disabled') && !btn.hasClass('iitix-extra-invalid')) 3624 ) { 3625 e.preventDefault(); 3626 e.stopImmediatePropagation(); 3627 3628 if (typeof Swal !== 'undefined') { 3629 Swal.fire({ 3630 icon: 'warning', 3631 title: 'Select a variation first', 3632 text: 'Please choose a category or variation before adding this item to the cart.', 3633 confirmButtonText: 'OK' 3634 }); 3635 } else { 3636 alert('Please choose a category or variation before adding this item to the cart.'); 3637 } 3638 3639 return false; 3640 } 3641 3642 markIitixAddToCartPending(form); 3643 3644 // Fallback: if the site finishes add-to-cart without firing the standard 3645 // Woo event, fragment refresh/ajaxComplete handler above will still close it. 3646 window.setTimeout(function () { 3647 if (iitixAddToCartPending && nowMs() - iitixAddToCartPendingAt > 9000) { 3648 clearIitixAddToCartPending(); 3649 } 3650 }, 9500); 3651 }); 3652})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.