(function ($) { 'use strict'; $(document).ready(function () { var $uploadWrap = $('.studiou-fpp-upload-wrap'); if (!$uploadWrap.length) { return; } var productId = $uploadWrap.data('product-id'); var maxFileSize = parseInt($uploadWrap.data('max-file-size'), 10) || 50; var chunkSize = parseInt(studiouWcfppFront.chunkSize, 10) || 1048576; var uploadedAttachmentId = null; var uploadedFileRecordId = null; var isUploading = false; var originalGalleryImages = {}; // Upload area elements var $dropzone = $uploadWrap.find('#studiou-fpp-dropzone'); var $fileInput = $uploadWrap.find('#studiou-fpp-file-input'); var $progress = $uploadWrap.find('.studiou-fpp-progress'); var $progressFill = $uploadWrap.find('.studiou-fpp-progress-fill'); var $progressText = $uploadWrap.find('.studiou-fpp-progress-text'); var $preview = $uploadWrap.find('.studiou-fpp-preview'); var $previewImg = $preview.find('img'); var $previewName = $uploadWrap.find('.studiou-fpp-preview-name'); var $removeBtn = $uploadWrap.find('.studiou-fpp-remove-file'); var $messages = $uploadWrap.find('.studiou-fpp-messages'); // ========================================== // Restore session state on page load // ========================================== if (typeof studiouWcfppSession !== 'undefined' && studiouWcfppSession.attachment_id) { uploadedAttachmentId = studiouWcfppSession.attachment_id; uploadedFileRecordId = studiouWcfppSession.file_record_id; showPreview(studiouWcfppSession.thumbnail_url, studiouWcfppSession.file_name); updateProductGallery(studiouWcfppSession.preview_url || studiouWcfppSession.thumbnail_url); } // ========================================== // Drag and drop // ========================================== $dropzone.on('dragover dragenter', function (e) { e.preventDefault(); e.stopPropagation(); $(this).addClass('studiou-fpp-dragover'); }); $dropzone.on('dragleave drop', function (e) { e.preventDefault(); e.stopPropagation(); $(this).removeClass('studiou-fpp-dragover'); }); $dropzone.on('drop', function (e) { var files = e.originalEvent.dataTransfer.files; if (files.length > 0) { handleFile(files[0]); } }); $dropzone.on('click', function () { if (!isUploading) { $fileInput.trigger('click'); } }); $fileInput.on('change', function () { if (this.files.length > 0) { handleFile(this.files[0]); } }); // Remove file $removeBtn.on('click', function () { removeUploadedFile(); }); // ========================================== // Upload logic // ========================================== function handleFile(file) { if (isUploading) return; var maxBytes = maxFileSize * 1024 * 1024; if (file.size > maxBytes) { showMessage(studiouWcfppFront.i18n.fileTooLarge.replace('%s', maxFileSize), 'error'); return; } clearMessages(); startChunkedUpload(file); } function startChunkedUpload(file) { isUploading = true; var totalChunks = Math.ceil(file.size / chunkSize); var uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); var currentChunk = 0; $dropzone.hide(); $progress.show(); function uploadNextChunk() { var start = currentChunk * chunkSize; var end = Math.min(start + chunkSize, file.size); var blob = file.slice(start, end); var formData = new FormData(); formData.append('action', 'studiou_wcfpp_upload_chunk'); formData.append('nonce', studiouWcfppFront.nonce); formData.append('product_id', productId); formData.append('upload_id', uploadId); formData.append('chunk_index', currentChunk); formData.append('total_chunks', totalChunks); formData.append('file_name', file.name); formData.append('file_size', file.size); formData.append('chunk', blob, 'chunk'); var isLastChunk = (currentChunk === totalChunks - 1); if (isLastChunk) { $progressFill.css('width', '100%'); $progressText.text(studiouWcfppFront.i18n.processing || 'Processing...'); } $.ajax({ url: studiouWcfppFront.ajaxUrl, type: 'POST', data: formData, processData: false, contentType: false, timeout: isLastChunk ? 120000 : 30000, success: function (response) { if (response.success) { if (!isLastChunk) { currentChunk++; var percent = Math.round((currentChunk / totalChunks) * 100); $progressFill.css('width', percent + '%'); $progressText.text(percent + '%'); } if (response.data.complete) { uploadedAttachmentId = response.data.attachment_id; uploadedFileRecordId = response.data.file_record_id; storeInSession( response.data.attachment_id, response.data.file_record_id, response.data.file_name ); showPreview(response.data.thumbnail_url, response.data.file_name); updateProductGallery(response.data.preview_url || response.data.thumbnail_url); $progress.hide(); isUploading = false; } else { uploadNextChunk(); } } else { uploadFailed(response.data ? response.data.message : studiouWcfppFront.i18n.uploadError); } }, error: function () { uploadFailed(studiouWcfppFront.i18n.uploadError); } }); } uploadNextChunk(); } // ========================================== // Session management // ========================================== function storeInSession(attachmentId, fileRecordId, fileName) { $.ajax({ url: studiouWcfppFront.ajaxUrl, type: 'POST', data: { action: 'studiou_wcfpp_set_upload_session', nonce: studiouWcfppFront.nonce, attachment_id: attachmentId, file_record_id: fileRecordId, file_name: fileName } }); } function clearSession() { $.ajax({ url: studiouWcfppFront.ajaxUrl, type: 'POST', data: { action: 'studiou_wcfpp_clear_upload_session', nonce: studiouWcfppFront.nonce } }); } // ========================================== // Product gallery image replacement // ========================================== function updateProductGallery(imageUrl) { if (!imageUrl) return; var selectors = [ '.woocommerce-product-gallery__image img', '.product .wp-post-image', '.product-image img', '.product-thumbnail img', '.entry-summary img.attachment-woocommerce_thumbnail', '.product-gallery img:first' ]; var $img = null; for (var i = 0; i < selectors.length; i++) { $img = $(selectors[i]).first(); if ($img.length) break; } if (!$img || !$img.length) return; if (!originalGalleryImages.src) { originalGalleryImages.src = $img.attr('src'); originalGalleryImages.srcset = $img.attr('srcset') || ''; originalGalleryImages.sizes = $img.attr('sizes') || ''; var $link = $img.closest('a'); if ($link.length) originalGalleryImages.href = $link.attr('href'); } $img.attr('src', imageUrl).removeAttr('srcset').removeAttr('sizes'); var $link = $img.closest('a'); if ($link.length) $link.attr('href', imageUrl); } function restoreProductGallery() { if (!originalGalleryImages.src) return; var selectors = [ '.woocommerce-product-gallery__image img', '.product .wp-post-image', '.product-image img', '.product-thumbnail img', '.entry-summary img.attachment-woocommerce_thumbnail', '.product-gallery img:first' ]; var $img = null; for (var i = 0; i < selectors.length; i++) { $img = $(selectors[i]).first(); if ($img.length) break; } if (!$img || !$img.length) return; $img.attr('src', originalGalleryImages.src); if (originalGalleryImages.srcset) $img.attr('srcset', originalGalleryImages.srcset); if (originalGalleryImages.sizes) $img.attr('sizes', originalGalleryImages.sizes); if (originalGalleryImages.href) $img.closest('a').attr('href', originalGalleryImages.href); originalGalleryImages = {}; } // ========================================== // UI helpers // ========================================== function uploadFailed(message) { isUploading = false; $progress.hide(); $dropzone.show(); showMessage(message, 'error'); $fileInput.val(''); } function showPreview(thumbnailUrl, fileName) { if (thumbnailUrl) { $previewImg.attr('src', thumbnailUrl).show(); } else { $previewImg.hide(); } $previewName.text(fileName); $preview.show(); $dropzone.hide(); } function removeUploadedFile() { if (!uploadedFileRecordId) return; $.ajax({ url: studiouWcfppFront.ajaxUrl, type: 'POST', data: { action: 'studiou_wcfpp_remove_upload', nonce: studiouWcfppFront.nonce, file_record_id: uploadedFileRecordId }, success: function () { resetUpload(); } }); } function resetUpload() { uploadedAttachmentId = null; uploadedFileRecordId = null; clearSession(); restoreProductGallery(); $preview.hide(); $previewImg.attr('src', ''); $previewName.text(''); $dropzone.show(); $fileInput.val(''); clearMessages(); } function showMessage(text, type) { var cls = type === 'error' ? 'studiou-fpp-msg-error' : 'studiou-fpp-msg-success'; $messages.html('
' + escHtml(text) + '
'); } function clearMessages() { $messages.empty(); } function escHtml(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; } // ========================================== // Quantity discount tier display + live price update // ========================================== initQtyTiers(); initVariantTableTiers(); function initVariantTableTiers() { // Handles custom variant-table plugins (e.g. pvtfw / product-variant-table-for-woocommerce) // where each variation is rendered as a with its own qty input and price. var tierData = (typeof window.studiouFppTierData === 'object' && window.studiouFppTierData) ? window.studiouFppTierData : null; if (!tierData) { return; } var $table = $('table.variant').first(); if (!$table.length) { return; } var i18n = studiouWcfppFront.i18n; $table.find('tbody > tr').each(function () { var $row = $(this); var $cartBtn = $row.find('.pvtfw_variant_table_cart_btn').first(); var variantId = $cartBtn.length ? String($cartBtn.data('variant')) : ''; if (!variantId) { var $qtyInput = $row.find('input.qty').first(); variantId = $qtyInput.attr('id') || ''; } if (!variantId || !tierData[variantId]) { return; } var cfg = tierData[variantId]; var tiers = cfg.tiers || []; var basePrice = parseFloat(cfg.base_price) || 0; if (tiers.length === 0 || basePrice <= 0) { return; } var $priceCell = $row.find('td[data-title="Price"]').first(); if (!$priceCell.length) $priceCell = $row.find('.woocommerce-Price-amount').first().closest('td'); var $priceEl = $priceCell.find('.woocommerce-Price-amount').first(); var $qty = $row.find('input.qty').first(); if (!$priceEl.length || !$qty.length) { return; } // Append a compact tier summary beneath the price var tierListHtml = '
'; tierListHtml += ''; tierListHtml += ''; for (var i = 0; i < tiers.length; i++) { var t = tiers[i]; tierListHtml += '' + t.from_qty + '+: −' + formatPercent(t.percent) + ''; } tierListHtml += '
'; $priceCell.append(tierListHtml); // Store the original price HTML so we can revert when qty falls below the first tier if (!$priceEl.data('studiou-fpp-base-html')) { $priceEl.data('studiou-fpp-base-html', $priceEl.html()); } // Bind qty change handlers to update this row's price display var handler = function () { updateRowPrice($row, tiers, basePrice, $priceEl, $qty); }; $qty.on('input change keyup', handler); // pvtfw ± buttons sit next to the input; they dispatch change events on the input, // but some themes intercept them — bind click too $row.find('.qty-count').on('click', function () { setTimeout(handler, 0); }); // Initial update (qty=1 usually, just to install the "Save X%" indicator if applicable) updateRowPrice($row, tiers, basePrice, $priceEl, $qty); }); } function updateRowPrice($row, tiers, basePrice, $priceEl, $qty) { var qty = parseInt($qty.val(), 10) || 1; var tier = resolveTierSimple(tiers, qty); $row.find('.studiou-fpp-row-save-badge').remove(); if (!tier || tier.percent <= 0) { // Revert to original HTML var base = $priceEl.data('studiou-fpp-base-html'); if (base) { $priceEl.html(base); } return; } var unit = basePrice * (1 - tier.percent / 100); $priceEl.html(formatPriceInner(unit)); var badge = '−' + formatPercent(tier.percent) + ''; $priceEl.after(badge); } function resolveTierSimple(tiers, qty) { var match = null; for (var i = 0; i < tiers.length; i++) { if (tiers[i].from_qty <= qty) match = tiers[i]; else break; } return match; } function formatPercent(percent) { var p = parseFloat(percent) || 0; var str = p.toFixed(2).replace(/\.?0+$/, ''); return str + ' %'; } function formatPriceInner(amount) { var curr = studiouWcfppFront.currency || {}; var decimals = parseInt(curr.decimals, 10); if (isNaN(decimals)) decimals = 2; var decSep = curr.decimalSeparator || '.'; var thouSep = curr.thousandSeparator || ','; var fmt = curr.priceFormat || '%1$s%2$s'; var symbol = curr.symbol || ''; var fixed = parseFloat(amount).toFixed(decimals); var parts = fixed.split('.'); var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouSep); var decPart = parts.length > 1 ? parts[1] : ''; var numStr = decPart ? intPart + decSep + decPart : intPart; var symbolHtml = '' + symbol + ''; return fmt.replace('%1$s', symbolHtml).replace('%2$s', numStr); } function initQtyTiers() { var $container = $('.studiou-fpp-qty-tiers-display'); var $form = $('form.variations_form').first(); if (!$container.length || !$form.length) { return; } var currentTiers = []; var currentBasePrice = 0; $form.on('found_variation', function (event, variation) { currentTiers = (variation && variation.studiou_fpp_qty_tiers) ? variation.studiou_fpp_qty_tiers : []; currentBasePrice = (variation && variation.studiou_fpp_base_price) ? parseFloat(variation.studiou_fpp_base_price) : 0; // WC re-renders .single_variation on every selection, so defer a tick to run after it setTimeout(function () { renderTierTable(); updateDisplayedPrice(); }, 0); }); $form.on('reset_data', function () { currentTiers = []; currentBasePrice = 0; $container.hide().empty(); }); // Live price update on quantity change — broad listener so themes with non-standard // qty input placement still trigger the update. $(document).on('input change keyup', 'input.qty, input[name="quantity"]', function () { updateDisplayedPrice(); }); function renderTierTable() { if (!currentTiers || currentTiers.length === 0) { $container.hide().empty(); return; } var i18n = studiouWcfppFront.i18n; var html = '
'; html += '

' + escHtml(i18n.qtyTiersTitle || 'Quantity Discount') + '

'; html += ''; html += ''; html += ''; html += ''; html += ''; for (var i = 0; i < currentTiers.length; i++) { var t = currentTiers[i]; var unit = computeDiscounted(currentBasePrice, t.percent); html += ''; html += ''; html += ''; html += ''; html += ''; } html += '
' + escHtml(i18n.qtyTiersFromQty || 'From Qty') + '' + escHtml(i18n.qtyTiersDiscount || 'Discount') + '' + escHtml(i18n.qtyTiersUnitPrice || 'Unit Price') + '
' + t.from_qty + '+' + formatPercent(t.percent) + '' + formatPrice(unit) + '
'; $container.html(html).show(); } function findPriceElement() { var selectors = [ '.single_variation .woocommerce-variation-price .woocommerce-Price-amount', 'form.variations_form .woocommerce-variation-price .woocommerce-Price-amount', '.single_variation_wrap .woocommerce-Price-amount', 'form.variations_form .price .woocommerce-Price-amount' ]; for (var i = 0; i < selectors.length; i++) { var $el = $(selectors[i]).first(); if ($el.length) return $el; } return $(); } function findQtyInput() { var $qty = $form.find('input.qty').first(); if (!$qty.length) $qty = $('input.qty').first(); if (!$qty.length) $qty = $('input[name="quantity"]').first(); return $qty; } function updateDisplayedPrice() { if (!currentBasePrice || !currentTiers || currentTiers.length === 0) { highlightActiveTierRow(0); return; } var $qty = findQtyInput(); var qty = $qty.length ? (parseInt($qty.val(), 10) || 1) : 1; var tier = resolveTier(currentTiers, qty); var unit = tier ? computeDiscounted(currentBasePrice, tier.percent) : currentBasePrice; var $wcPrice = findPriceElement(); if ($wcPrice.length) { $wcPrice.html(formatPriceInner(unit)); } highlightActiveTierRow(tier ? tier.from_qty : 0); } function highlightActiveTierRow(fromQty) { $container.find('tr').removeClass('studiou-fpp-qty-tier-active'); if (fromQty > 0) { $container.find('tr[data-from-qty="' + fromQty + '"]').addClass('studiou-fpp-qty-tier-active'); } } function resolveTier(tiers, qty) { var match = null; for (var i = 0; i < tiers.length; i++) { if (tiers[i].from_qty <= qty) { match = tiers[i]; } else { break; } } return match; } function computeDiscounted(basePrice, percent) { var p = parseFloat(percent) || 0; if (p <= 0) return basePrice; return basePrice * (1 - p / 100); } function formatPercent(percent) { var p = parseFloat(percent) || 0; // Strip trailing zeros var str = p.toFixed(2).replace(/\.?0+$/, ''); return str + ' %'; } function formatPrice(amount) { return '' + formatPriceInner(amount) + ''; } function formatPriceInner(amount) { var curr = studiouWcfppFront.currency || {}; var decimals = parseInt(curr.decimals, 10); if (isNaN(decimals)) decimals = 2; var decSep = curr.decimalSeparator || '.'; var thouSep = curr.thousandSeparator || ','; var fmt = curr.priceFormat || '%1$s%2$s'; var symbol = curr.symbol || ''; var fixed = parseFloat(amount).toFixed(decimals); var parts = fixed.split('.'); var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouSep); var decPart = parts.length > 1 ? parts[1] : ''; var numStr = decPart ? intPart + decSep + decPart : intPart; var symbolHtml = '' + symbol + ''; return fmt.replace('%1$s', symbolHtml).replace('%2$s', numStr); } } }); })(jQuery);