https://poppsee.com/wp-content/themes/poppsee_theme/customization/js/variant_manager.js?v=1791442013
1/** 2 * Variant Manager â Design Studio 3 * Ticket #9486478256: Artist product variations upon saving initial products 4 * 5 * Manages the variant selection panel, per-variant canvas resizing, 6 * design capture, background submission, and retry logic. 7 */ 8var VariantManager = (function ($) { 9 'use strict'; 10 11 /* ------------------------------------------------------------------ */ 12 /* State */ 13 /* ------------------------------------------------------------------ */ 14 var variants = []; // array of variant objects 15 var activeIndex = -1; // currently-selected variant index 16 var designData = {}; // variantId -> { thumbnail, additional_data, custom_style_value, background_color } 17 var selectedIds = {}; // variantId -> true once that size is SAVED (2026-09-23: the box reports saves, it is not a control) 18 var lastSavedVid = null; // variation_id of the most recently saved design 19 // The capture currently running, or null. variantScreenshot() reads the 20 // LIVE #designcanvas, so a size switch must wait for it rather than race 21 // it -- racing banks one size's artwork against another, which is how 36 22 // live products came to share a print file (see saveCurrentVariantDesign). 23 var pendingCapture = null; 24 var productId = ''; 25 // Other surfaces this design could also be published onto, as answered 26 // by poppsee_offerable_surfaces. Empty until the panel asks, and empty 27 // for a design with no artwork id. 28 var offerableSurfaces = []; 29 var catId = ''; 30 var postId = ''; 31 var userId = ''; 32 var productTitle = ''; 33 // One label, one place. The button reads "Publish" because the artist is 34 // publishing sizes of a product, not filing a submission. 35 var SUBMIT_LABEL = 'Publish'; 36 var panelOpen = false; 37 var isProcessing = false; 38 39 /* ------------------------------------------------------------------ */ 40 /* Initialisation */ 41 /* ------------------------------------------------------------------ */ 42 /** 43 * Drop every scrap of the PREVIOUS product's rendered design. 44 * 45 * These hidden fields are written from #previewImg inside an AJAX success 46 * handler, so until that has run for the NEW product they still hold the 47 * previous product's values. saveCurrentVariantDesign() reads them as 48 * 49 * $('#post_thumbnail').val() || $('#previewImg').attr('src') || '' 50 * 51 * and a stale value is truthy, so it WINS. Publish a product, pick another 52 * without reloading the page, save a variant, and it was saved with the old 53 * product's artwork (reported 2026-08-22). That is silent data corruption on 54 * a live listing, which is far worse than an error. 55 * 56 * Clearing at init() makes the stale read impossible: init() runs on every 57 * product switch, so from here the value must be freshly generated or it is 58 * empty, and empty is caught by the capture-complete gate rather than 59 * shipped. 60 */ 61 function clearCarriedOverDesignState() { 62 // Every field the save path reads, including the numbered siblings that 63 // are written in the same breath as the primary one. 64 ['#post_thumbnail', '#post_thumbnail1', '#post_thumbnail2', '#post_thumbnail3', 65 '#additional_data', '#custom_style_value'].forEach(function (sel) { 66 var $el = $(sel); 67 if ($el.length) { $el.val(''); } 68 }); 69 // The preview itself is the source those fields are copied from, so it 70 // has to go too, or the very next capture re-seeds them from the old 71 // product. 72 $('#previewImg').attr('src', ''); 73 if (typeof window.previewImgB64 !== 'undefined') { window.previewImgB64 = ''; } 74 } 75 76 function init(opts) { 77 // Reset state for re-initialization (e.g. artist selects a new product) 78 variants = []; 79 activeIndex = -1; 80 designData = {}; 81 selectedIds = {}; 82 lastSavedVid = null; 83 isProcessing = false;
84 panelOpen = false; 85 $('#variantPanel').html('').removeClass('open'); 86 $('.vp-toggle-btn').removeClass('visible'); 87 setVariantsRow(false, 'Select a product first'); 88 clearCarriedOverDesignState(); 89 90 productId = opts.productId || ''; 91 catId = opts.catId || ''; 92 postId = opts.postId || ''; 93 userId = opts.userId || ''; 94 productTitle = opts.productTitle || ''; 95 var autoOpen = !!opts.autoOpen; 96 // The size already loaded in the canvas, when the caller knows it 97 // (editing an existing product). Pre-selects that row once the list 98 // arrives, instead of leaving every size looking untouched. 99 var activeVariationId = opts.variationId || ''; 100 101 if (!productId) return; 102 103 // Fetch available variations for the product 104 $.ajax({ 105 url: admin_url, 106 type: 'POST', 107 dataType: 'json', 108 data: { 109 action: 'get_product_variations_for_panel', 110 product_id: productId 111 }, 112 success: function (res) { 113 if (res && res.success && res.data && res.data.length > 0) { 114 variants = res.data; 115 buildPanel(); 116 $('.vp-toggle-btn').addClass('visible'); 117 // The left-rail row is driven from the same moment as the 118 // existing toggle, so the two can never disagree about 119 // whether variants are available. 120 setVariantsRow(true, variants.length + (variants.length === 1 ? ' size ready' : ' sizes ready')); 121 if (autoOpen) openPanel(); 122 if (activeVariationId) { 123 markVariantActive(findVariantIndex(activeVariationId)); 124 } 125 console.log('[VariantManager] Loaded ' + variants.length + ' variants'); 126 } else { 127 console.log('[VariantManager] Product has no variants â panel hidden'); 128 setVariantsRow(false, 'This product has no variants'); 129 } 130 }, 131 error: function () { 132 console.log('[VariantManager] Failed to fetch variants'); 133 setVariantsRow(false, 'Could not load variants'); 134 } 135 }); 136 } 137 138 /* ------------------------------------------------------------------ */ 139 /* Build the side-panel HTML */ 140 /* ------------------------------------------------------------------ */ 141 function buildPanel() { 142 var html = ''; 143 html += '<div class="vp-header">'; 144 html += ' <h3>Choose your size variants</h3>'; 145 html += ' <button class="vp-close" id="vpClose" title="Hide this panel and keep designing">×</button>'; 146 html += '</div>'; 147 148 // Which size the studio is editing right now, and the fact that the 149 // studio IS still editable. Reported 2026-08-22: once you reach the 150 // variants step nothing signals that the left-hand tools still work, so 151 // it reads as a dead end. The behaviour was already right -- pick a 152 // size, edit it, Save Current Variant -- it was simply never stated. 153 html += '<div class="vp-context" id="vpContext">'; 154 html += ' <div class="vp-context-line" id="vpContextLine">Pick a size below to start designing it.</div>'; 155 html += '</div>'; 156 157 // The product title is inherited from the base product and was never 158 // editable here, so showing a greyed-out box only added noise. It still 159 // rides along on submit via `productTitle`. 160 161 // progress 162 html += '<div class="vp-progress">'; 163 html += ' <div class="vp-progress-bar"><div class="vp-progress-fill" id="vpProgressFill"></div></div>'; 164 html += ' <div class="vp-progress-text" id="vpProgressText">0 of ' + variants.length + ' selected · 0 designed</div>'; 165 html += '</div>'; 166 167 // list â ONE row per distinct size (the server groups all same-size 168 // color/frame combinations behind each row; they ride along on submit) 169 html += '<ul class="vp-list" id="vpList">'; 170 for (var i = 0; i < variants.length; i++) { 171 var v = variants[i]; 172 html += '<li class="vp-item" data-index="' + i + '" data-variation-id="' + v.variation_id + '">'; 173 html += ' <div class="vp-check" title="Ticks itself once this size is saved" aria-hidden="true"><i class="fa fa-check"></i></div>'; 174 html += ' <div class="vp-info">'; 175 html += ' <div class="vp-name">' + escHtml(v.label) + '</div>'; 176 if (v.sublabel) { 177 html += ' <div class="vp-sublabel">' + escHtml(v.sublabel) + '</div>'; 178 } 179 html += ' <div class="vp-dims">' + escHtml(v.dimensions) + '</div>'; 180 html += ' <div class="vp-status pending" id="vpStatus' + i + '">Pending</div>'; 181 html += ' </div>'; 182 html += ' <div class="vp-thumb" id="vpThumb' + i + '"></div>'; 183 html += '</li>'; 184 } 185 html += '</ul>'; 186
187 // Other surfaces this same artwork can go on. Empty until the server 188 // answers, and absent entirely for a design with no artwork id -- a 189 // draft-loaded design has none, and offering surfaces for it would 190 // publish products that nothing can ever link together. 191 html += '<div class="vp-surfaces" id="vpSurfaces"></div>'; 192 193 // actions 194 html += '<div class="vp-actions">'; 195 html += ' <button class="vp-btn vp-btn-primary" id="vpSubmitAll" disabled>' + SUBMIT_LABEL + '</button>'; 196 html += ' <div class="vp-submit-hint" id="vpSubmitHint"></div>'; 197 html += ' <button class="vp-btn vp-btn-secondary" id="vpSaveCurrent" disabled>Save Current Variant</button>'; 198 html += '</div>'; 199 200 $('#variantPanel').html(html); 201 bindPanelEvents(); 202 updateProgress(); 203 loadOfferableSurfaces(); 204 } 205 206 /* ------------------------------------------------------------------ */ 207 /* Event bindings */ 208 /* ------------------------------------------------------------------ */ 209 function bindPanelEvents() { 210 $('#vpClose').on('click', function () { 211 closePanel(); 212 }); 213 214 // No handler for .vp-check on purpose. Chris, 2026-09-23: the boxes 215 // "should be not clickable and they should check themselves when they 216 // are saved". A click anywhere on the row, the tick included, opens 217 // that size for editing -- which is what an artist means by it. 218 $('#vpList').on('click', '.vp-item', function () { 219 var idx = parseInt($(this).data('index'), 10); 220 selectVariant(idx); 221 }); 222 223 $('#vpSaveCurrent').on('click', function () { 224 saveCurrentVariantDesign(); 225 }); 226 227 $('#vpSubmitAll').on('click', function () { 228 submitAllVariants(); 229 }); 230 231 $('#vpList').on('click', '.vp-retry', function (e) { 232 e.stopPropagation(); 233 var idx = parseInt($(this).closest('.vp-item').data('index'), 10); 234 retryVariant(idx); 235 }); 236 } 237 238 /* ------------------------------------------------------------------ */ 239 /* The left-rail Variants row */ 240 /* ------------------------------------------------------------------ */ 241 242 /** 243 * Enable or disable the rail row, and say WHY in its subtitle. 244 * 245 * The row is always present, never hidden: a control that disappears 246 * cannot teach an artist that it exists, which is the discoverability 247 * problem this row was added to solve. So it stays put and explains 248 * itself instead. 249 */ 250 function setVariantsRow(enabled, subtitle) { 251 var $row = $('#spVariantsRow'); 252 if (!$row.length) return; // studio markup predates this row 253 $row.prop('disabled', !enabled).attr('aria-disabled', enabled ? 'false' : 'true'); 254 if (subtitle) $('#spVariantsRowSub').text(subtitle); 255 if (!enabled) $row.removeClass('is-open'); 256 } 257 258 /** Mirror the drawer's state onto the row, so "open" is visible from the rail. */ 259 function markVariantsRowOpen(open) { 260 $('#spVariantsRow').toggleClass('is-open', !!open); 261 } 262 263 /* ------------------------------------------------------------------ */ 264 /* Panel open / close */ 265 /* ------------------------------------------------------------------ */ 266 function togglePanel() { 267 if (panelOpen) closePanel(); 268 else openPanel(); 269 } 270 function openPanel() { 271 $('#variantPanel').addClass('open'); 272 // The panel is a fixed 380px drawer. Without this the studio simply 273 // sits UNDERNEATH it, which is a large part of why the step felt like a 274 // separate screen rather than a sidebar you design alongside. 275 $('body').addClass('vp-panel-open'); 276 markVariantsRowOpen(true); 277 panelOpen = true; 278 } 279 function closePanel() { 280 markVariantsRowOpen(false); 281 $('#variantPanel').removeClass('open'); 282 $('body').removeClass('vp-panel-open'); 283 panelOpen = false;
284 } 285 286 /** Update the "what am I editing" line. Safe before the panel is built. */ 287 function setContextLine(html) { 288 var el = document.getElementById('vpContextLine'); 289 if (el) { el.innerHTML = html; } 290 } 291 292 /* ------------------------------------------------------------------ */ 293 /* Other surfaces this artwork can be published onto */ 294 /* ------------------------------------------------------------------ */ 295 296 /** 297 * Ask the server which surfaces sell something the shape the artist is 298 * working at, and offer them. 299 * 300 * NOTHING IS OFFERED WITHOUT AN ARTWORK ID. The whole point is that the 301 * products end up linked, and the id is best effort: the studio knows it 302 * for a design built in this session and not for one loaded from a draft. 303 * Publishing extra surfaces that nothing can ever link together would be 304 * worse than not offering, because the artist would believe it worked. 305 */ 306 function loadOfferableSurfaces() { 307 var $box = $('#vpSurfaces'); 308 if (!$box.length) { return; } 309 if (!window.poppseeLastArtId) { 310 $box.empty(); 311 return; 312 } 313 var active = activeIndex >= 0 ? variants[activeIndex] : variants[0]; 314 $.ajax({ 315 type: 'post', 316 url: (window.ajaxurl || (window.wc_add_to_cart_params || {}).ajax_url), 317 data: { 318 action: 'poppsee_offerable_surfaces', 319 product_id: productId, 320 size_label: active ? (active.size_label || active.label || '') : '', 321 _ajax_nonce: (window.poppseeAjax || {}).nonce || '' 322 }, 323 success: function (res) { 324 offerableSurfaces = (res && res.success && res.data) ? res.data : []; 325 renderOfferableSurfaces(); 326 }, 327 error: function () { 328 // A surface list that cannot be fetched is simply not offered. 329 // The ordinary single-surface publish must never be blocked by 330 // it, so this fails quiet rather than loud. 331 offerableSurfaces = []; 332 renderOfferableSurfaces(); 333 } 334 }); 335 } 336 337 function renderOfferableSurfaces() { 338 var $box = $('#vpSurfaces'); 339 if (!$box.length) { return; } 340 if (!offerableSurfaces.length) { 341 $box.empty(); 342 return; 343 } 344 var html = '<div class="vp-surfaces-head">Also publish this design on</div>'; 345 html += '<ul class="vp-surface-list">'; 346 offerableSurfaces.forEach(function (s, i) { 347 html += '<li class="vp-surface">' 348 + '<label><input type="checkbox" class="vp-surface-cb" data-surface="' + s.id + '" data-index="' + i + '"> ' 349 + escHtml(s.name) 350 + ' <span class="vp-surface-sizes">' + s.matching_sizes + ' size' 351 + (s.matching_sizes === 1 ? '' : 's') + ' fit</span>' 352 + '</label></li>'; 353 }); 354 html += '</ul>'; 355 // Said plainly, because it is the part an artist would otherwise 356 // discover by waiting: each surface is a separate render pass. 357 html += '<div class="vp-surfaces-note">Each one is rendered against its own template, so this takes longer than a single publish.</div>'; 358 $box.html(html); 359 } 360 361 /** The surfaces the artist ticked, as the plan module wants them. */ 362 function selectedSurfaces() { 363 var out = []; 364 $('#vpSurfaces .vp-surface-cb:checked').each(function () { 365 var s = offerableSurfaces[parseInt($(this).data('index'), 10)]; 366 if (s) { out.push(s); } 367 }); 368 return out; 369 } 370 371 /* ------------------------------------------------------------------ */ 372 /* Select a variant â resize canvas and load its template image */ 373 /* ------------------------------------------------------------------ */ 374 /** 375 * Point the canvas at ONE variation of ONE product and load its template. 376 * 377 * Extracted unchanged from selectVariant() so the same path can be aimed 378 * at another PRODUCT, which is what multi-surface publish needs: the 379 * endpoint is literally called select_different_product_action and has 380 * always taken both ids -- only the caller assumed one product. 381 * 382 * `restoreFrom` is a callback rather than a value so it is evaluated at
383 * the same moment the original code evaluated it, after the response 384 * lands. For an ordinary size switch it returns that size's own saved 385 * design; for a cross-surface render it returns the SOURCE design, which 386 * is how the artwork comes to sit on the new surface's template. 387 * 388 * `onDone` fires once the canvas holds the loaded variation. 389 */ 390 function loadVariationIntoCanvas(pid, vid, label, restoreFrom, onDone) { 391 // Load variation into the canvas via existing AJAX endpoint 392 var frmdata = new FormData(); 393 frmdata.append('product_id', pid); 394 frmdata.append('variation_id', vid); 395 frmdata.append('action', 'select_different_product_action'); 396 397 $.ajax({ 398 url: admin_url, 399 method: 'post', 400 processData: false, 401 contentType: false, 402 cache: false, 403 data: frmdata, 404 success: function (response) { 405 try { 406 var res = (typeof response === 'string') ? JSON.parse(response) : response; 407 408 // Update the hidden variation_id fields 409 $('.variation_id').val(vid); 410 $('#currVariationID').text(vid); 411
412 // Resize canvas with existing updateBoundingBox logic 413 var canvasHeight = res.pallet_height; 414 var canvasWidth = res.pallet_width; 415 var previewW = res.preview_widthDS; 416 var previewH = res.preview_heightDS; 417 var mwwTW = res.mww_template_width; 418 var mwwTH = res.mww_template_height; 419 var mwwSW = res.mww_sewn_product_width; 420 var mwwSH = res.mww_sewn_product_height; 421 var mwwSZW = res.mww_safeZone_product_width; 422 var mwwSZH = res.mww_safeZone_product_height; 423 var catIdVal = $('#currCatID').text(); 424 425 // Switching size must also update the PRINT TEMPLATE spans. 426 // Only changeBaseProduct() ever wrote these, so they kept 427 // the first-loaded variation's values for the whole 428 // session. activePreview() sizes #imgcan from them, so 429 // every later size was composited at the FIRST size's 430 // template â and when the two differ in ORIENTATION the 431 // art lands in a wrong-shaped box. 432 // 433 // Seen on Area Rugs, whose sizes are both different and 434 // differently oriented (10'x8' -> 11904x9600 landscape, 435 // 5'x7' -> 9300x12900 portrait): the saved design was 436 // exactly right (105.92% wide, 100.9% tall, centred) while 437 // the published image showed the artwork squashed into the 438 // top third. Products whose sizes share one template 439 // (most of the catalogue) were unaffected, which is why 440 // this stayed invisible. 441 if (mwwTW) { $('#currTemplateWidth').text(mwwTW); } 442 if (mwwTH) { $('#currTemplateHeight').text(mwwTH); } 443 444 // And the SEWN dimensions, which are what actually size the 445 // capture. activePreview() does: 446 // 447 // let modTemplWidth = mwwSewnWidth; 448 // let modTemplHeight = mwwSewnHeight; 449 // if (modTemplWidth == 0 && ...) { modTemplWidth = $('#currTemplateWidth').text(); } 450 // 451 // so #currTemplateWidth is only the FALLBACK, used when the 452 // sewn globals are 0. Those globals were set once for the 453 // product's default variation and never updated on a size 454 // switch, so every other size was captured at the default 455 // size â a landscape 3'x2' rug published as a 9000x12600 456 // portrait image with the artwork squashed into a corner. 457 // 458 // Measured on prod: spans 5550x3750 (correct) while 459 // mwwSewnWidth/Height were still 9000x12600. Setting them 460 // to the size's own values produced a correct 5550x3750 461 // landscape render with full-bleed artwork (product 72982). 462 // 463 // Only products whose sizes differ in template are affected 464 // â 23 of 42 â which is why the first size of each product 465 // always looked right and the rest did not. 466 if (mwwSW) { window.mwwSewnWidth = mwwSW; } 467 if (mwwSH) { window.mwwSewnHeight = mwwSH; } 468 469 // Determine shape type 470 var shapeType = 'square'; 471 if (canvasWidth && canvasHeight) { 472 var w = parseFloat(canvasWidth); 473 var h = parseFloat(canvasHeight); 474 if (w > h) shapeType = 'rectangleH'; 475 else if (h > w) shapeType = 'rectangleV'; 476 } 477 478 if (typeof updateBoundingBox === 'function') { 479 updateBoundingBox(canvasWidth, canvasHeight, previewW, previewH, shapeType, catIdVal, 480 mwwTW, mwwTH, mwwSW, mwwSH, mwwSZW, mwwSZH, vid); 481 } 482 483 // Load the variation image as product SVG if available 484 if (res.imgarray && res.imgarray.length > 0) { 485 var imgUrl = res.imgarray[0]; 486 var previewBlock = $('.block_img.img0'); 487 if (previewBlock.length) { 488 previewBlock.find('img').attr('src', imgUrl); 489 } 490 } 491 492 // Restore this size's design: what was SAVED if it has ever
493 // been saved, otherwise the working canvas it was left in. 494 var restoreHtml = restoreFrom ? restoreFrom() : ''; 495 if (restoreHtml) { 496 $('#designcanvas').html(restoreHtml); 497 } 498 499 // Handle safe-zone display 500 if (typeof showSafeAreas === 'function') { 501 var safeVal = $('#safeZoneSet').length ? $('#safeZoneSet').text() : 'N'; 502 showSafeAreas(safeVal); 503 } 504 505 if (label) { showToast('Switched to: ' + label, 'info'); } 506 507 // The canvas now holds this variation on its own template. 508 // Called INSIDE the try so a loader that threw never 509 // reports success to a caller that is about to capture. 510 if (typeof onDone === 'function') { onDone(true); } 511 } catch (e) { 512 console.log('[VariantManager] Error parsing variant response', e); 513 showToast('Error loading variant', 'error'); 514 // A caller waiting on this must hear about it, or a 515 // cross-surface run would hang on a canvas that never 516 // loaded. 517 if (typeof onDone === 'function') { onDone(false); } 518 } 519 }, 520 error: function () { 521 showToast('Failed to load variant data', 'error'); 522 if (typeof onDone === 'function') { onDone(false); } 523 } 524 }); 525 } 526 527 function selectVariant(idx) { 528 if (idx < 0 || idx >= variants.length) return; 529 if (isProcessing) return; 530 531 // A save is still reading the canvas. Wait for it rather than swapping 532 // the canvas underneath it: variantScreenshot() renders whatever 533 // #designcanvas holds at that moment, so switching mid-capture banks 534 // this size's artwork against the one being saved. The artist is not 535 // blocked -- the click is honoured as soon as the capture lands. 536 if (pendingCapture) { 537 var want = idx; 538 pendingCapture.then(function () { selectVariant(want); }); 539 return; 540 } 541 542 // Save current design first if we already had one active 543 if (activeIndex >= 0 && activeIndex !== idx) { 544 captureCurrentDesign(activeIndex); 545 } 546 547 activeIndex = idx; 548 var v = variants[idx]; 549 550 // UI highlight 551 $('#vpList .vp-item').removeClass('active'); 552 $('#vpList .vp-item[data-index="' + idx + '"]').addClass('active'); 553 updateStatus(idx, 'Designing...', 'active'); 554 555 // Name the size the left-hand tools now affect. Without this the artist 556 // cannot tell whether adding artwork applies to this size, to all of 557 // them, or to nothing -- which is what made the step feel like a dead 558 // end even though editing worked the whole time. 559 setContextLine( 560 'Editing <strong>' + escHtml(v.label) + '</strong> — use the tools on the left to add ' 561 + 'artwork or text, then <strong>Save Current Variant</strong>.' 562 ); 563 564 // Enable save button 565 $('#vpSaveCurrent').prop('disabled', false); 566 567 loadVariationIntoCanvas( 568 productId, 569 v.variation_id, 570 v.label, 571 function () { return designHtmlFor(designData[v.variation_id]); }, 572 null 573 ); 574 } 575 576 /** 577 * Point the panel at the size the canvas is ALREADY showing. 578 * 579 * selectVariant() exists to *change* the canvas, so it re-fetches the 580 * variation and re-runs updateBoundingBox. When the studio has just loaded 581 * a variation itself â editing an existing product, where loadDraft() has 582 * restored the artwork into #designcanvas â that round trip would fight the 583 * restore for no gain. This only updates panel state. 584 */ 585 function markVariantActive(idx) { 586 if (idx < 0 || idx >= variants.length) return; 587 588 activeIndex = idx; 589 var v = variants[idx]; 590 var $item = $('#vpList .vp-item[data-index="' + idx + '"]'); 591 592 $('#vpList .vp-item').removeClass('active'); 593 $item.addClass('active'); 594 updateStatus(idx, 'Designing...', 'active'); 595 596 // Opening a size does NOT tick it. The tick means "saved" now, and a 597 // size that has only been opened has nothing banked against it. 598 599 setContextLine( 600 'Editing <strong>' + escHtml(v.label) + '</strong> — use the tools on the left to change ' 601 + 'the artwork or text, then <strong>Save Current Variant</strong>.' 602 ); 603 604 $('#vpSaveCurrent').prop('disabled', false); 605 updateProgress(); 606 } 607 608 /** 609 * Which snapshot a size comes back as. 610 * 611 * `savedHtml` is frozen by a save and only a later save replaces it. 612 * `canvasHtml` is the working copy, rewritten every time the artist leaves 613 * the size. Before this existed they were the same field, so moving a 614 * design to suit another size's proportions and coming back showed the 615 * moved version -- the saved one was gone. Chris, 2026-09-23: it must 616 * "always show what was saved when you return". 617 */ 618 function designHtmlFor(rec) { 619 if (!rec) return ''; 620 return rec.savedHtml || rec.canvasHtml || ''; 621 } 622 623 /* ------------------------------------------------------------------ */ 624 /* Capture the current canvas design for the active variant */ 625 /* ------------------------------------------------------------------ */ 626 function captureCurrentDesign(idx) { 627 if (idx < 0 || idx >= variants.length) return; 628 var v = variants[idx]; 629 var canvas = document.getElementById('designcanvas'); 630 if (!canvas) return; 631 632 designData[v.variation_id] = designData[v.variation_id] || {}; 633 designData[v.variation_id].canvasHtml = canvas.innerHTML; 634 } 635 636 /* ------------------------------------------------------------------ */ 637 /* Save current variant design (capture screenshot + data) */ 638 /* ------------------------------------------------------------------ */ 639 function saveCurrentVariantDesign() { 640 if (activeIndex < 0) return; 641 642 var v = variants[activeIndex]; 643 var idx = activeIndex; 644 645 // Freeze the artwork NOW, at the moment the artist pressed save, and 646 // tick the row for it. Everything after this is the screenshot 647 // pipeline, which the artist should not have to watch. Chris, 648 // 2026-09-23: "saving should happen in the background without having 649 // to wait." If the capture fails, markVariantFailed() below takes the 650 // tick back, so an optimistic tick can never outlive a failed save. 651 designData[v.variation_id] = designData[v.variation_id] || {}; 652 designData[v.variation_id].savedHtml = document.getElementById('designcanvas').innerHTML; 653 // Count it as saved straight away, so the tick, the progress line and 654 // the Publish button all say the same thing the instant the artist 655 // presses save. A failed capture takes all three back together 656 // (markVariantUnsaved), so they can never disagree. 657 designData[v.variation_id].saved = true; 658 markVariantComplete(idx); 659 updateStatus(idx, 'Saving...', 'active'); 660 661 // Use the existing saveasimg flow to generate the thumbnail / additional_data 662 // We wrap it in a try-catch since saveasimg returns a Promise 663 try { 664 // Clear the capture targets BEFORE rendering this size. 665 // 666 // These fields persist across sizes. When a size's preview cannot 667 // be generated (no print template, so buildpsdimg returns 668 // psd_template_missing) they still hold the PREVIOUS size's image, 669 // and the capture below banks that instead of failing â so the 670 // size publishes silently carrying another size's print file. 671 // 672 // Measured on the live catalogue: 36 products across 13 lines 673 // shared a print file with a different size, including Cloth 674 // Napkins 22" and 10", Wall Tapestries 36"x26" and 60"x51", and
675 // Area Rugs 2'x3' and 3'x2' (different orientations). Only the 676 // very first size of a product ever failed loudly, because it had 677 // nothing to inherit. 678 // 679 // Blanking them first makes the existing `if (!thumbVal)` guard 680 // below do its job: refuse the save rather than bank the wrong 681 // artwork. A visible "could not capture" beats a wrong print file. 682 $('#post_thumbnail').val(''); 683 $('#additional_data').val(''); 684 $('#previewImg').attr('src', ''); 685 686 // Trigger the existing design capture pipeline 687 if (typeof productDesign === 'function') { 688 productDesign(); 689 } 690 691 // Use variantScreenshot to capture. The promise is parked in 692 // pendingCapture so a size switch can wait for it (selectVariant), 693 // and cleared in both outcomes below so a failure cannot wedge the 694 // panel into queueing every later click forever. 695 if (typeof variantScreenshot === 'function') { 696 pendingCapture = variantScreenshot().then(function () { 697 // Now capture values from the hidden fields 698 var thumbVal = $('#post_thumbnail').val() || $('#previewImg').attr('src') || ''; 699 var addDataVal = $('#additional_data').val() || $('#previewImg').attr('src') || ''; 700 var styleVal = $('#custom_style_value').val() || ''; 701 var bgColor = $('#background_color_val2').length ? $('#background_color_val2').val() : ''; 702 703 // Nothing to save means the capture did not produce anything 704 // for THIS product. Refuse rather than bank an empty design, 705 // which would publish a listing with no artwork. Saving 706 // something wrong quietly is the failure we are removing 707 // here; failing loudly is the point. 708 if (!thumbVal) { 709 // Take the optimistic tick back: this size is NOT saved. 710 markVariantUnsaved(idx); 711 updateStatus(idx, 'Not saved', 'error'); 712 showToast('Could not capture the design for ' + v.label + '. Try again.', 'error'); 713 $('#vpSaveCurrent').prop('disabled', false); 714 return; 715 } 716 717 designData[v.variation_id] = designData[v.variation_id] || {}; 718 designData[v.variation_id].post_thumbnail = thumbVal; 719 designData[v.variation_id].additional_data = addDataVal; 720 designData[v.variation_id].custom_style_value = styleVal; 721 designData[v.variation_id].background_color_val2 = bgColor; 722 designData[v.variation_id].canvasHtml = document.getElementById('designcanvas').innerHTML; 723 designData[v.variation_id].saved = true; 724 725 // Update thumbnail preview in panel 726 if (thumbVal) { 727 $('#vpThumb' + idx).html('<img src="' + thumbVal + '" />'); 728 } 729 730 pendingCapture = null; 731 markVariantComplete(idx); 732 showToast(v.label + ' design saved', 'success'); 733 $('#vpSaveCurrent').prop('disabled', false); 734 }).catch(function () { 735 pendingCapture = null; 736 markVariantUnsaved(idx); 737 updateStatus(idx, 'Save failed', 'error'); 738 showToast('Failed to capture design', 'error'); 739 $('#vpSaveCurrent').prop('disabled', false); 740 }); 741 } else { 742 // Fallback: just capture the raw canvas HTML 743 var canvas = document.getElementById('designcanvas'); 744 designData[v.variation_id] = designData[v.variation_id] || {}; 745 designData[v.variation_id].canvasHtml = canvas ? canvas.innerHTML : ''; 746 designData[v.variation_id].post_thumbnail = $('#previewImg').attr('src') || ''; 747 designData[v.variation_id].additional_data = $('#previewImg').attr('src') || ''; 748 designData[v.variation_id].custom_style_value = ''; 749 designData[v.variation_id].background_color_val2 = ''; 750 designData[v.variation_id].saved = true; 751 markVariantComplete(idx); 752 showToast(v.label + ' design saved (basic)', 'success'); 753 $('#vpSaveCurrent').prop('disabled', false); 754 } 755 } catch (e) { 756 console.log('[VariantManager] Error saving variant design', e); 757 pendingCapture = null; 758 markVariantUnsaved(idx); 759 updateStatus(idx, 'Save failed', 'error'); 760 showToast('Error saving variant design', 'error'); 761 $('#vpSaveCurrent').prop('disabled', false); 762 } 763 } 764 765 /* ------------------------------------------------------------------ */ 766 /* Un-tick â the save the tick promised did not land */ 767 /* ------------------------------------------------------------------ */ 768 function markVariantUnsaved(idx) { 769 if (idx < 0 || idx >= variants.length) return; 770 var vid = variants[idx].variation_id; 771 delete selectedIds[vid]; 772 if (designData[vid]) { 773 designData[vid].saved = false;
774 delete designData[vid].savedHtml; 775 } 776 if (lastSavedVid === vid) lastSavedVid = null; 777 $('#vpList .vp-item[data-index="' + idx + '"]').removeClass('selected completed'); 778 updateProgress(); 779 } 780 781 /* ------------------------------------------------------------------ */ 782 /* Mark variant complete / update progress */ 783 /* ------------------------------------------------------------------ */ 784 function markVariantComplete(idx) { 785 var $item = $('#vpList .vp-item[data-index="' + idx + '"]'); 786 $item.addClass('completed').removeClass('failed'); 787 // The tick IS the save. Nothing else sets it and nothing but a failed 788 // capture takes it back, so what an artist sees ticked is exactly what 789 // publishing will send. 790 lastSavedVid = variants[idx].variation_id; 791 selectedIds[lastSavedVid] = true; 792 $item.addClass('selected'); 793 updateStatus(idx, 'Complete', 'done'); 794 updateProgress(); 795 } 796 797 function markVariantFailed(idx, msg) { 798 var $item = $('#vpList .vp-item[data-index="' + idx + '"]'); 799 $item.addClass('failed').removeClass('completed'); 800 updateStatus(idx, msg || 'Failed', 'error'); 801 802 // Add retry button 803 var $status = $('#vpStatus' + idx); 804 if (!$status.find('.vp-retry').length) { 805 $status.after('<button class="vp-retry">Retry</button>'); 806 } 807 updateProgress(); 808 } 809 810 function updateStatus(idx, text, cls) { 811 var $el = $('#vpStatus' + idx); 812 $el.text(text).removeClass('pending active done error').addClass(cls); 813 } 814 815 function updateProgress() { 816 var designed = 0, selected = 0; 817 for (var i = 0; i < variants.length; i++) { 818 var vid = variants[i].variation_id; 819 if (designData[vid] && designData[vid].saved) designed++; 820 if (selectedIds[vid]) selected++; 821 } 822 var pct = variants.length > 0 ? Math.round((designed / variants.length) * 100) : 0; 823 $('#vpProgressFill').css('width', pct + '%'); 824 $('#vpProgressText').text(designed + ' of ' + variants.length + ' saved'); 825 826 // Publishing sends the saved sizes, so one save is the whole gate. 827 var canSubmit = designed >= 1; 828 $('#vpSubmitAll').prop('disabled', !canSubmit); 829 var hint = ''; 830 if (!canSubmit) { 831 hint = 'Save a size ("Save Current Variant") to publish it'; 832 } 833 $('#vpSubmitHint').text(hint).toggle(hint !== ''); 834 } 835 836 /* ------------------------------------------------------------------ */ 837 /* Submit â enqueue a background job and close immediately */ 838 /* ------------------------------------------------------------------ */ 839 840 // Refresh the AJAX nonce (long studio sessions outlive the original one). 841 function refreshNonce() { 842 return $.post(admin_url, { action: 'poppsee_refresh_nonce' }).then(function (res) { 843 if (res && res.success && res.data && res.data.nonce) { 844 window.poppseeSec = window.poppseeSec || {}; 845 poppseeSec.nonce = res.data.nonce; 846 return res.data.nonce; 847 } 848 return null; 849 }); 850 } 851 852 // POST the payload; on a 403 (stale nonce) refresh the token and retry once. 853 function postSubmission(payload, onDone, onFail, retried) { 854 payload.nonce = (window.poppseeSec && poppseeSec.nonce) || ''; 855 $.ajax({ 856 url: admin_url, 857 type: 'POST', 858 data: payload, 859 dataType: 'json', 860 timeout: 30000, 861 success: function (res) { 862 if (res && res.success) onDone(res); 863 else onFail(res && res.data && res.data.message ? res.data.message : 'Submission failed'); 864 }, 865 error: function (xhr, status, err) { 866 if (xhr && xhr.status === 403 && !retried) { 867 refreshNonce().then(function (nonce) { 868 if (nonce) postSubmission(payload, onDone, onFail, true); 869 else onFail('Your session expired â please reload the page and try again.'); 870 }); 871 return; 872 } 873 // PHP drops $_POST wholesale when the body exceeds 874 // post_max_size, so admin-ajax answers a bare "0" with HTTP 875 // 400/413 and jQuery reports a JSON parse error. Surfacing 876 // that as "Network error" told the artist nothing â name the 877 // real problem instead. 878 if (xhr && (xhr.status === 413 || (xhr.status === 400 && String(xhr.responseText).trim() === '0'))) { 879 onFail('That submission was too large for the server to accept. ' 880 + 'Submit fewer sizes at once, or re-save your design and try again.'); 881 return; 882 } 883 if (status === 'timeout') { 884 onFail('The server took too long to respond. Your designs may still be processing â ' 885 + 'check your dashboard notifications before resubmitting.'); 886 return; 887 } 888 onFail('Network error: ' + (err || status)); 889 } 890 }); 891 } 892 893 // Upload the heavy design blobs ONE PER REQUEST and collect their keys. 894 // Doing this before the submit keeps every request small and constant, so
895 // the body can't cross post_max_size no matter how many designs the artist 896 // used. Sequential on purpose: parallel uploads of multi-MB base64 blobs 897 // just trade one limit for another. 898 // Slice size. The server's hard ceiling is post_max_size (8M) and it applies 899 // to the WHOLE encoded body, so this leaves generous room for the other 900 // fields plus form-encoding overhead (base64 roughly doubles once escaped). 901 // Pieces are cut from the base64 body, so this must stay a multiple of 4. 902 var CHUNK_CHARS = 1500000; 903 904 function randomStageId() { 905 var abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', s = ''; 906 for (var i = 0; i < 20; i++) { s += abc.charAt(Math.floor(Math.random() * abc.length)); } 907 return s; 908 } 909 910 // Base64 of the UTF-8 bytes. The design travels encoded because prod's 911 // MalCare firewall reads canvas_html as an attack: the studio's own 912 // onmouseleave= handlers 403'd Lisa's second piece on every Publish 913 // (2026-09-16). Base64 carries no markup, spaces or SQL, so no rule can 914 // match. See tests/stage-chunks-survive-the-firewall.test.js. 915 function utf8ToBase64(str) { 916 var bytes = new TextEncoder().encode(str); 917 var out = ''; 918 for (var i = 0; i < bytes.length; i += 0x8000) { 919 out += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000)); 920 } 921 return btoa(out); 922 } 923 924 // Upload the designs one at a time, and each design in slices. A single 925 // high-resolution design can exceed post_max_size on its own, so slicing â 926 // not just separating â is what removes the size ceiling entirely.
927 function stageDesigns(designs, onReady, onFail) { 928 var keys = []; 929 930 function uploadDesign(i, done, fail) { 931 var body = utf8ToBase64(JSON.stringify(designs[i])); 932 var total = Math.max(1, Math.ceil(body.length / CHUNK_CHARS)); 933 var stageId = randomStageId(); 934 935 function sendChunk(idx) { 936 var label = 'Uploading design ' + (i + 1) + ' of ' + designs.length 937 + (total > 1 ? ' (' + (idx + 1) + '/' + total + ')' : '') + 'â¦'; 938 $('#vpSubmitAll').text(label); 939 $.ajax({ 940 url: admin_url, 941 type: 'POST', 942 dataType: 'json', 943 timeout: 120000, 944 data: { 945 action: 'poppsee_stage_variant_design', 946 nonce: (window.poppseeSec && poppseeSec.nonce) || '', 947 stage_id: stageId, 948 chunk_index: idx, 949 chunk_total: total, 950 enc: 'b64', 951 chunk: body.substr(idx * CHUNK_CHARS, CHUNK_CHARS) 952 }, 953 success: function (res) { 954 if (!res || !res.success) { 955 fail((res && res.data && res.data.message) ? res.data.message 956 : 'Could not upload design ' + (i + 1)); 957 return; 958 } 959 if (idx < total - 1) { sendChunk(idx + 1); return; } 960 if (res.data && res.data.design_key) { done(res.data.design_key); } 961 else { fail('The server did not confirm design ' + (i + 1)); } 962 }, 963 error: function (xhr, status) { 964 fail('Could not upload design ' + (i + 1) 965 + (status ? ' (' + status + ')' : '') + '. Please try again.'); 966 } 967 }); 968 } 969 sendChunk(0); 970 } 971 972 function next(i) { 973 if (i >= designs.length) { onReady(keys); return; } 974 uploadDesign(i, function (key) { keys.push(key); next(i + 1); }, onFail); 975 } 976 next(0); 977 } 978 979 function submitAllVariants() { 980 if (isProcessing) return; 981 982 // Use locked init title â panel field is read-only 983 var title = productTitle 984 || $('#TemplateForm input[name="title"]').val() 985 || 'Untitled Product'; 986 var desc = $('#TemplateForm textarea[name="content"]').val() || ''; 987 988 // Build the payload from the SAVED sizes. Until 2026-09-23 the tick 989 // was a control, so a ticked size with no design of its own published 990 // using a "donor" â the active size's save, else the most recent one. 991 // The tick now reports a save instead, which makes that borrowing both 992 // unreachable and undesirable: every published size carries the 993 // artwork the artist actually approved at that size's proportions. 994 // Each row still carries ALL same-size variation ids, so every 995 // colour/frame combination of that size goes with it. 996 997 // De-duplicate heavy design data. Multiple checked sizes reusing the 998 // same design would otherwise embed the SAME base64 screenshot once 999 // PER size and blow past the server's post_max_size (8M): PHP then 1000 // silently drops $_POST and admin-ajax answers "0" with HTTP 400. 1001 // Send each unique design ONCE in `designs`; each size references it 1002 // by index. Server rehydrates before queuing, so the job is unchanged. 1003 var designs = []; 1004 var designIndexByVid = {}; 1005 function internDesign(srcVid, d) { 1006 if (designIndexByVid[srcVid] !== undefined) { return designIndexByVid[srcVid]; } 1007 var idx = designs.length; 1008 designs.push({ 1009 post_thumbnail: d.post_thumbnail || '', 1010 additional_data: d.additional_data || '', 1011 custom_style_value: d.custom_style_value || '', 1012 background_color_val2: d.background_color_val2 || '', 1013 // Hidden layers must never reach the server: the job queue 1014 // rehydrates canvas_html, and display:none is only a client 1015 // rendering fact. Strip from this COPY only â the live canvas 1016 // and drafts keep hidden layers so the artist gets them back. 1017 canvas_html: (window.LayersPanel 1018 ? window.LayersPanel.stripHidden(d.canvasHtml || '') 1019 : (d.canvasHtml || '')) 1020 }); 1021 designIndexByVid[srcVid] = idx; 1022 return idx; 1023 } 1024 1025 var variationsPayload = []; 1026 for (var i = 0; i < variants.length; i++) { 1027 var v = variants[i]; 1028 var own = designData[v.variation_id]; 1029 if (!own || !own.saved) { 1030 continue; 1031 } 1032 variationsPayload.push({ 1033 variation_id: v.variation_id, 1034 variation_ids: v.variation_ids || [v.variation_id], 1035 size_label: v.size_label || v.label || '', 1036 design_index: internDesign(v.variation_id, own) 1037 }); 1038 } 1039 1040 if (variationsPayload.length === 0) { 1041 showToast('Save at least one size before publishing.', 'error'); 1042 return; 1043 } 1044 1045 if (!confirm('Publish ' + variationsPayload.length + ' saved size(s)?\n\nTitle: ' + title)) { 1046 return; 1047 } 1048 1049 isProcessing = true; 1050 $('#vpSubmitAll').prop('disabled', true).text('Uploading designsâ¦'); 1051 showToast('Uploading your designsâ¦', 'info'); 1052 1053 function finish(msg, ok, res) { 1054 isProcessing = false;
1055 $('#vpSubmitAll').prop('disabled', false).text(SUBMIT_LABEL); 1056 if (ok) { 1057 for (var j = 0; j < variationsPayload.length; j++) { 1058 var matchIdx = findVariantIndex(variationsPayload[j].variation_id); 1059 if (matchIdx >= 0) updateStatus(matchIdx, 'Queued', 'done'); 1060 } 1061 closePanel(); 1062 showToast((res && res.data && res.data.message) 1063 ? res.data.message 1064 : 'Designs queued! You\'ll get a notification when they\'re done.', 'success'); 1065 } else { 1066 openPanel(); 1067 showToast(msg, 'error'); 1068 updateProgress(); 1069 } 1070 } 1071 1072 // Stage the heavy blobs first, then submit a payload that carries only 1073 // their keys. The panel stays open through the upload so the artist can 1074 // see progress â the old code closed instantly and then had to re-open 1075 // itself on failure. 1076 stageDesigns(designs, function (designKeys) { 1077 $('#vpSubmitAll').text('Submittingâ¦'); 1078 postSubmission({ 1079 action: 'submit_artist_product_variations', 1080 user_id: userId, 1081 cat_id: catId, 1082 post_id: postId, 1083 title: title, 1084 description: desc, 1085 product_id: productId, 1086 // Best effort: present for a design built in this session, absent 1087 // for one loaded from a draft. Absent simply means no surface picker. 1088 artwork_id: (window.poppseeLastArtId || ''), 1089 variations: JSON.stringify(variationsPayload), 1090 design_keys: JSON.stringify(designKeys) 1091 }, function (res) { 1092 finish('', true, res); 1093 }, function (msg) { 1094 finish(msg, false); 1095 }); 1096 }, function (msg) { 1097 finish(msg, false); 1098 }); 1099 } 1100 1101 /* ------------------------------------------------------------------ */ 1102 /* Retry a single failed variant */ 1103 /* ------------------------------------------------------------------ */ 1104 function retryVariant(idx) { 1105 if (isProcessing) return; 1106 var v = variants[idx]; 1107 var d = designData[v.variation_id]; 1108 1109 if (!d || !d.saved) { 1110 showToast('Design this variant first before retrying.', 'error'); 1111 return; 1112 } 1113 1114 updateStatus(idx, 'Retrying...', 'active'); 1115 $('#vpList .vp-item[data-index="' + idx + '"]').removeClass('failed'); 1116 $('#vpList .vp-item[data-index="' + idx + '"]').find('.vp-retry').remove(); 1117 1118 var title = productTitle 1119 || $('#TemplateForm input[name="title"]').val() 1120 || 'Untitled Product'; 1121 var desc = $('#TemplateForm textarea[name="content"]').val() || ''; 1122 1123 var singlePayload = [{ 1124 variation_id: v.variation_id, 1125 variation_ids: v.variation_ids || [v.variation_id], 1126 size_label: v.size_label || v.label || '', 1127 post_thumbnail: d.post_thumbnail || '', 1128 additional_data: d.additional_data || '', 1129 custom_style_value: d.custom_style_value || '', 1130 background_color_val2: d.background_color_val2 || '', 1131 // Hidden layers must never reach the server: the job queue 1132 // rehydrates canvas_html, and display:none is only a client 1133 // rendering fact. Strip from this COPY only â the live canvas 1134 // and drafts keep hidden layers so the artist gets them back. 1135 canvas_html: (window.LayersPanel 1136 ? window.LayersPanel.stripHidden(d.canvasHtml || '') 1137 : (d.canvasHtml || '')) 1138 }]; 1139 1140 postSubmission({ 1141 action: 'submit_artist_product_variations', 1142 user_id: userId, 1143 cat_id: catId, 1144 post_id: postId, 1145 title: title, 1146 description: desc, 1147 product_id: productId, 1148 // Best effort: present for a design built in this session, absent 1149 // for one loaded from a draft. Absent simply means no surface picker. 1150 artwork_id: (window.poppseeLastArtId || ''), 1151 variations: JSON.stringify(singlePayload) 1152 }, function () { 1153 updateStatus(idx, 'Queued', 'done'); 1154 showToast(v.label + ' re-queued â you\'ll be notified when it\'s done.', 'success'); 1155 }, function (msg) { 1156 markVariantFailed(idx, msg || 'Server error'); 1157 showToast(v.label + ' retry failed.', 'error'); 1158 }); 1159 } 1160 1161 /* ------------------------------------------------------------------ */ 1162 /* Helpers */ 1163 /* ------------------------------------------------------------------ */ 1164 // A row is ONE size, standing in for every same-size color/frame
1165 // combination (see get_product_variations_for_panel). `variation_id` is 1166 // only the representative of that group, so an id handed to us from 1167 // elsewhere in the studio â e.g. the variation an artist is editing â is 1168 // just as likely to be a non-representative member. Match on the whole 1169 // group or the lookup silently misses on any product with combinations. 1170 function findVariantIndex(variationId) { 1171 var needle = String(variationId); 1172 for (var i = 0; i < variants.length; i++) { 1173 if (String(variants[i].variation_id) === needle) return i; 1174 var ids = variants[i].variation_ids || []; 1175 for (var j = 0; j < ids.length; j++) { 1176 if (String(ids[j]) === needle) return i; 1177 } 1178 } 1179 return -1; 1180 } 1181 1182 function escHtml(str) { 1183 if (!str) return ''; 1184 return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 1185 } 1186 1187 function showToast(msg, type) { 1188 var $toast = $('#vpToast'); 1189 if (!$toast.length) { 1190 $('body').append('<div class="vp-toast" id="vpToast"></div>'); 1191 $toast = $('#vpToast'); 1192 } 1193 $toast.text(msg).removeClass('show error success').addClass(type || '').addClass('show'); 1194 clearTimeout($toast.data('timer')); 1195 $toast.data('timer', setTimeout(function () { 1196 $toast.removeClass('show'); 1197 }, 4500)); 1198 } 1199 1200 function showProcessingOverlay(show) { 1201 var $ov = $('#vpProcessingOverlay'); 1202 if (!$ov.length) { 1203 $('body').append( 1204 '<div class="vp-processing-overlay" id="vpProcessingOverlay">' + 1205 ' <div class="vp-processing-box">' + 1206 ' <div class="vp-spinner"></div>' + 1207 ' <h4>Processing Variants</h4>' + 1208 ' <p>Submitting your designs to the server. You will be notified by email when complete.</p>' + 1209 ' </div>' + 1210 '</div>' 1211 ); 1212 $ov = $('#vpProcessingOverlay'); 1213 } 1214 if (show) $ov.addClass('show'); 1215 else $ov.removeClass('show'); 1216 } 1217 1218 /* ------------------------------------------------------------------ */ 1219 /* Public API */ 1220 /* ------------------------------------------------------------------ */ 1221 return { 1222 init: init, 1223 openPanel: openPanel, 1224 closePanel: closePanel, 1225 togglePanel: togglePanel, 1226 selectVariant: selectVariant, 1227 getDesignData: function () { return designData; }, 1228 getVariants: function () { return variants; }, 1229 getActiveIndex: function () { return activeIndex; }, 1230 isOpen: function () { return panelOpen; } 1231 }; 1232 1233})(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.