(function ($) {
'use strict';
$(document).ready(function () {
var $wrap = $('.studiou-fpp-upload-wrap');
if (!$wrap.length) {
return;
}
// ==========================================
// Config + data
// ==========================================
var cfg = {
productId: parseInt($wrap.data('product-id'), 10) || 0,
maxFileSize: parseInt($wrap.data('max-file-size'), 10) || 50,
maxUploads: parseInt($wrap.data('max-uploads'), 10) || 0,
chunkSize: parseInt(studiouWcfppFront.chunkSize, 10) || 1048576
};
var i18n = studiouWcfppFront.i18n || {};
var data = window.studiouFppCardData || {variants: [], batchFiles: []};
var variants = data.variants || [];
var $dropzone = $('#studiou-fpp-dropzone');
var $fileInput = $('#studiou-fpp-file-input');
var $cards = $('#studiou-fpp-cards');
var $messages = $wrap.find('.studiou-fpp-messages');
var originalGallery = {};
// ==========================================
// Bootstrap
// ==========================================
initExistingCards();
initDropzone();
initCardDelegation();
initOverviewDelegation();
applyHeroImage(data.heroPreviewUrl);
// ==========================================
// Card construction
// ==========================================
function initExistingCards() {
(data.batchFiles || []).forEach(function (f) {
var $card = buildCard({
fileRecordId: f.file_record_id,
attachmentId: f.attachment_id,
fileName: f.file_name,
thumbUrl: f.thumb_url,
variationId: f.variation_id || 0,
uploading: false
});
$cards.append($card);
});
refreshMaxReachedState();
}
function buildCard(opts) {
var $card = $('
');
$card.attr('data-file-record-id', opts.fileRecordId || '');
$card.attr('data-attachment-id', opts.attachmentId || '');
if (opts.uploading) $card.addClass('studiou-fpp-card-uploading');
// Remove-upload button (×)
$card.append('');
// Thumb column
var $thumb = $('');
if (opts.thumbUrl) {
$thumb.append($('
').attr('src', opts.thumbUrl).attr('alt', opts.fileName || ''));
} else {
$thumb.append('');
}
var $progress = $('');
$progress.append('');
$progress.append('0%');
$thumb.append($progress);
$card.append($thumb);
// Body column
var $body = $('');
$body.append($('').text(opts.fileName || ''));
var $controls = $('');
$controls.append(buildVariantSelect(opts.variationId || 0));
$controls.append(buildQtyStepper());
$body.append($controls);
var $priceRow = $('');
$priceRow.append('—');
$priceRow.append('');
$priceRow.append('');
$body.append($priceRow);
$card.append($body);
if (!opts.uploading) {
updateCardPrice($card);
}
return $card;
}
function buildVariantSelect(initialVariationId) {
var $select = $('');
$select.append('');
for (var i = 0; i < variants.length; i++) {
var v = variants[i];
var priceLabel = v.base_price ? ' — ' + formatPrice(v.base_price) : '';
var $opt = $('')
.val(v.id)
.text(v.label + priceLabel)
.attr('data-base-price', v.base_price);
if (!v.in_stock) {
$opt.prop('disabled', true).text(v.label + priceLabel + ' — ' + (i18n.outOfStock || 'out of stock'));
}
$select.append($opt);
}
if (initialVariationId) {
$select.val(initialVariationId);
}
return $select;
}
function buildQtyStepper() {
var $stepper = $('');
$stepper.append('');
$stepper.append('');
$stepper.append('');
return $stepper;
}
// ==========================================
// Card live updates
// ==========================================
function initCardDelegation() {
$cards.on('change', '.studiou-fpp-card-variant', function () {
updateCardPrice($(this).closest('.studiou-fpp-card'));
});
$cards.on('input change keyup', '.studiou-fpp-qty-input', function () {
updateCardPrice($(this).closest('.studiou-fpp-card'));
});
$cards.on('click', '.studiou-fpp-qty-plus', function () {
var $input = $(this).closest('.studiou-fpp-card-qty').find('.studiou-fpp-qty-input');
$input.val((parseInt($input.val(), 10) || 1) + 1).trigger('change');
});
$cards.on('click', '.studiou-fpp-qty-minus', function () {
var $input = $(this).closest('.studiou-fpp-card-qty').find('.studiou-fpp-qty-input');
var v = Math.max(1, (parseInt($input.val(), 10) || 1) - 1);
$input.val(v).trigger('change');
});
$cards.on('click', '.studiou-fpp-card-addtocart', function () {
onAddToCartClick($(this).closest('.studiou-fpp-card'));
});
$cards.on('click', '.studiou-fpp-card-remove', function () {
onRemoveUploadClick($(this).closest('.studiou-fpp-card'));
});
}
function findVariant(variationId) {
variationId = parseInt(variationId, 10) || 0;
if (!variationId) return null;
for (var i = 0; i < variants.length; i++) {
if (variants[i].id === variationId) return variants[i];
}
return null;
}
function resolveTier(tiers, qty) {
if (!tiers || !tiers.length) return null;
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 updateCardPrice($card) {
var $price = $card.find('.studiou-fpp-card-price');
var $badge = $card.find('.studiou-fpp-card-badge');
var $btn = $card.find('.studiou-fpp-card-addtocart');
var variationId = parseInt($card.find('.studiou-fpp-card-variant').val(), 10) || 0;
var qty = Math.max(1, parseInt($card.find('.studiou-fpp-qty-input').val(), 10) || 1);
var variant = findVariant(variationId);
if (!variant || !variant.base_price) {
$price.html('—');
$badge.hide();
$btn.prop('disabled', true);
return;
}
var tier = resolveTier(variant.tiers || [], qty);
var unit = variant.base_price;
if (tier && tier.percent > 0) {
unit = variant.base_price * (1 - (tier.percent / 100));
$badge.show().text('−' + formatPercent(tier.percent));
} else {
$badge.hide();
}
var lineTotal = unit * qty;
$price.html(formatPriceHtml(lineTotal));
var uploading = $card.hasClass('studiou-fpp-card-uploading');
$btn.prop('disabled', uploading);
}
// ==========================================
// Upload dropzone
// ==========================================
function initDropzone() {
$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;
enqueueFiles(files);
});
$dropzone.on('click', function () {
if ($dropzone.hasClass('studiou-fpp-dropzone-disabled')) return;
$fileInput.trigger('click');
});
$fileInput.on('change', function () {
enqueueFiles(this.files);
$fileInput.val('');
});
}
var uploadQueue = [];
var activeUpload = false;
function enqueueFiles(fileList) {
if (!fileList || !fileList.length) return;
clearMessages();
if (cfg.maxUploads > 0) {
var existing = $cards.children('.studiou-fpp-card').length;
var available = Math.max(0, cfg.maxUploads - existing - uploadQueue.length);
if (available <= 0) {
showMessage(formatI18n(i18n.maxUploadsReached, cfg.maxUploads), 'error');
return;
}
if (fileList.length > available) {
showMessage(formatI18n(i18n.maxUploadsReached, cfg.maxUploads), 'error');
}
for (var i = 0; i < Math.min(fileList.length, available); i++) {
pushFile(fileList[i]);
}
} else {
for (var j = 0; j < fileList.length; j++) {
pushFile(fileList[j]);
}
}
refreshMaxReachedState();
drainQueue();
}
function pushFile(file) {
var maxBytes = cfg.maxFileSize * 1024 * 1024;
if (file.size > maxBytes) {
showMessage(formatI18n(i18n.fileTooLarge, cfg.maxFileSize), 'error');
return;
}
var $card = buildCard({
fileRecordId: 0,
attachmentId: 0,
fileName: file.name,
thumbUrl: '',
variationId: 0,
uploading: true
});
$cards.append($card);
uploadQueue.push({file: file, $card: $card});
}
function drainQueue() {
if (activeUpload || !uploadQueue.length) return;
activeUpload = true;
var next = uploadQueue.shift();
uploadOne(next.file, next.$card, function () {
activeUpload = false;
drainQueue();
});
}
function uploadOne(file, $card, done) {
var totalChunks = Math.ceil(file.size / cfg.chunkSize);
var uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
var currentChunk = 0;
var $fill = $card.find('.studiou-fpp-card-progress-fill');
var $text = $card.find('.studiou-fpp-card-progress-text');
$card.find('.studiou-fpp-card-progress').show();
function next() {
var start = currentChunk * cfg.chunkSize;
var end = Math.min(start + cfg.chunkSize, file.size);
var blob = file.slice(start, end);
var form = new FormData();
form.append('action', 'studiou_wcfpp_upload_chunk');
form.append('nonce', studiouWcfppFront.nonce);
form.append('product_id', cfg.productId);
form.append('upload_id', uploadId);
form.append('chunk_index', currentChunk);
form.append('total_chunks', totalChunks);
form.append('file_name', file.name);
form.append('file_size', file.size);
form.append('chunk', blob, 'chunk');
var isLast = (currentChunk === totalChunks - 1);
if (isLast) {
$fill.css('width', '100%');
$text.text(i18n.processing || 'Processing...');
}
$.ajax({
url: studiouWcfppFront.ajaxUrl,
type: 'POST',
data: form,
processData: false,
contentType: false,
timeout: isLast ? 120000 : 30000,
success: function (response) {
if (!response.success) {
failUpload($card, response.data ? response.data.message : (i18n.uploadError || 'Upload failed.'));
done();
return;
}
if (!response.data.complete) {
currentChunk++;
var pct = Math.round((currentChunk / totalChunks) * 100);
$fill.css('width', pct + '%');
$text.text(pct + '%');
next();
} else {
finishUpload($card, response.data);
done();
}
},
error: function () {
failUpload($card, i18n.uploadError || 'Upload failed.');
done();
}
});
}
next();
}
function finishUpload($card, res) {
$card.removeClass('studiou-fpp-card-uploading');
$card.find('.studiou-fpp-card-progress').hide();
$card.attr('data-file-record-id', res.file_record_id);
$card.attr('data-attachment-id', res.attachment_id);
var $img = $card.find('.studiou-fpp-card-thumb img');
if ($img.length) {
$img.attr('src', res.thumbnail_url);
} else {
$card.find('.studiou-fpp-card-thumb-placeholder').replaceWith(
$('
').attr('src', res.thumbnail_url).attr('alt', res.file_name)
);
}
updateCardPrice($card);
refreshMaxReachedState();
// If this is now the first/only card, swap the hero gallery image
if ($cards.children('.studiou-fpp-card').index($card) === 0) {
applyHeroImage(res.preview_url || res.thumbnail_url);
}
}
function failUpload($card, message) {
$card.remove();
showMessage(message, 'error');
refreshMaxReachedState();
}
function refreshMaxReachedState() {
if (cfg.maxUploads <= 0) return;
var reached = $cards.children('.studiou-fpp-card').length >= cfg.maxUploads;
$dropzone.toggleClass('studiou-fpp-dropzone-disabled', reached);
}
// ==========================================
// Remove upload (per-card × button)
// ==========================================
function onRemoveUploadClick($card) {
var fileRecordId = parseInt($card.attr('data-file-record-id'), 10) || 0;
if (!fileRecordId) {
// Still uploading — just cancel locally
$card.remove();
refreshMaxReachedState();
return;
}
$.ajax({
url: studiouWcfppFront.ajaxUrl,
type: 'POST',
data: {
action: 'studiou_wcfpp_remove_upload',
nonce: studiouWcfppFront.nonce,
file_record_id: fileRecordId
},
success: function (response) {
if (response.success) {
var wasFirst = ($cards.children('.studiou-fpp-card').index($card) === 0);
$card.remove();
refreshMaxReachedState();
if (wasFirst) {
var $nextFirst = $cards.children('.studiou-fpp-card').first();
if ($nextFirst.length) {
var nextImg = $nextFirst.find('.studiou-fpp-card-thumb img').attr('src');
applyHeroImage(nextImg || '');
} else {
restoreHeroImage();
}
}
} else {
showMessage(response.data ? response.data.message : (i18n.error || 'Error'), 'error');
}
},
error: function () {
showMessage(i18n.error || 'Error', 'error');
}
});
}
// ==========================================
// Add to cart
// ==========================================
function onAddToCartClick($card) {
var fileRecordId = parseInt($card.attr('data-file-record-id'), 10) || 0;
var variationId = parseInt($card.find('.studiou-fpp-card-variant').val(), 10) || 0;
var qty = Math.max(1, parseInt($card.find('.studiou-fpp-qty-input').val(), 10) || 1);
if (!fileRecordId) {
showMessage(i18n.uploadFile || 'Please upload a file.', 'error');
return;
}
if (!variationId) {
showMessage(i18n.selectVariant || 'Please select a variant.', 'error');
return;
}
var $btn = $card.find('.studiou-fpp-card-addtocart');
$btn.prop('disabled', true);
$.ajax({
url: studiouWcfppFront.ajaxUrl,
type: 'POST',
data: {
action: 'studiou_wcfpp_add_file_to_cart',
nonce: studiouWcfppFront.nonce,
file_record_id: fileRecordId,
variation_id: variationId,
quantity: qty
},
success: function (response) {
if (response.success) {
showMessage(i18n.addedToCart || 'Added to cart.', 'success');
if (response.data && response.data.overview_html) {
updateOverview(response.data.overview_html);
}
} else {
showMessage(response.data ? response.data.message : (i18n.addToCartError || 'Could not add to cart.'), 'error');
}
},
error: function () {
showMessage(i18n.addToCartError || 'Could not add to cart.', 'error');
},
complete: function () {
$btn.prop('disabled', false);
}
});
}
// ==========================================
// Order overview
// ==========================================
function initOverviewDelegation() {
// Remove-line on overview panel (cart-only, upload stays)
$(document).on('click', '.studiou-fpp-overview-remove', function (e) {
var $link = $(this);
var href = $link.attr('href');
// Only intercept if the link has a nonce arg — otherwise let browser navigate
if (!href || href.indexOf('studiou-fpp-remove-line') < 0) return;
e.preventDefault();
var $line = $link.closest('.studiou-fpp-overview-line');
var key = $line.data('cart-item-key');
if (!key) return;
$.ajax({
url: studiouWcfppFront.ajaxUrl,
type: 'POST',
data: {
action: 'studiou_wcfpp_remove_cart_line',
nonce: studiouWcfppFront.nonce,
cart_item_key: key
},
success: function (response) {
if (response.success && response.data && response.data.overview_html) {
updateOverview(response.data.overview_html);
} else if (!response.success) {
showMessage(response.data ? response.data.message : (i18n.error || 'Error'), 'error');
}
},
error: function () {
showMessage(i18n.error || 'Error', 'error');
}
});
});
}
function updateOverview(html) {
var $new = $(html);
var $existing = $('#studiou-fpp-overview');
if ($existing.length) {
$existing.replaceWith($new);
} else {
$wrap.after($new);
}
$(document).trigger('studiou-fpp:cart-updated');
}
// ==========================================
// Hero image
// ==========================================
function applyHeroImage(url) {
if (!url) 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 (!originalGallery.src) {
originalGallery.src = $img.attr('src');
originalGallery.srcset = $img.attr('srcset') || '';
originalGallery.sizes = $img.attr('sizes') || '';
var $link = $img.closest('a');
if ($link.length) originalGallery.href = $link.attr('href');
}
$img.attr('src', url).removeAttr('srcset').removeAttr('sizes');
var $a = $img.closest('a');
if ($a.length) $a.attr('href', url);
}
function restoreHeroImage() {
if (!originalGallery.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', originalGallery.src);
if (originalGallery.srcset) $img.attr('srcset', originalGallery.srcset);
if (originalGallery.sizes) $img.attr('sizes', originalGallery.sizes);
if (originalGallery.href) $img.closest('a').attr('href', originalGallery.href);
originalGallery = {};
}
// ==========================================
// Helpers
// ==========================================
function showMessage(text, type) {
var cls = type === 'error' ? 'studiou-fpp-msg-error'
: type === 'success' ? 'studiou-fpp-msg-success'
: 'studiou-fpp-msg-info';
$messages.html('' + escHtml(text) + '
');
}
function clearMessages() { $messages.empty(); }
function escHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str == null ? '' : String(str)));
return div.innerHTML;
}
function escAttr(str) {
return String(str == null ? '' : str).replace(/"/g, '"');
}
function formatI18n(tpl, val) {
if (!tpl) return String(val);
return tpl.replace('%d', val).replace('%s', val);
}
function formatPercent(p) {
var n = parseFloat(p) || 0;
var str = n.toFixed(2).replace(/\.?0+$/, '');
return str + ' %';
}
function formatPrice(amount) {
return formatPriceHtml(amount);
}
function formatPriceHtml(amount) {
var c = studiouWcfppFront.currency || {};
var decimals = parseInt(c.decimals, 10); if (isNaN(decimals)) decimals = 2;
var decSep = c.decimalSeparator || '.';
var thouSep = c.thousandSeparator || ',';
var fmt = c.priceFormat || '%1$s%2$s';
var symbol = c.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 sym = '' + symbol + '';
return '' +
fmt.replace('%1$s', sym).replace('%2$s', numStr) +
'';
}
});
})(jQuery);