|
@@ -3,207 +3,539 @@
|
|
|
|
|
|
|
|
$(document).ready(function () {
|
|
$(document).ready(function () {
|
|
|
|
|
|
|
|
- var $uploadWrap = $('.studiou-fpp-upload-wrap');
|
|
|
|
|
- if (!$uploadWrap.length) {
|
|
|
|
|
|
|
+ var $wrap = $('.studiou-fpp-upload-wrap');
|
|
|
|
|
+ if (!$wrap.length) {
|
|
|
return;
|
|
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');
|
|
|
|
|
|
|
+ // ==========================================
|
|
|
|
|
+ // 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);
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- // Restore session state on page load
|
|
|
|
|
|
|
+ // Card construction
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- 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);
|
|
|
|
|
|
|
+ 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 = $('<div class="studiou-fpp-card"></div>');
|
|
|
|
|
+ $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('<button type="button" class="studiou-fpp-card-remove" title="' +
|
|
|
|
|
+ escAttr(i18n.removeFile || 'Remove') + '" aria-label="' +
|
|
|
|
|
+ escAttr(i18n.removeFile || 'Remove') + '">×</button>');
|
|
|
|
|
+
|
|
|
|
|
+ // Thumb column
|
|
|
|
|
+ var $thumb = $('<div class="studiou-fpp-card-thumb"></div>');
|
|
|
|
|
+ if (opts.thumbUrl) {
|
|
|
|
|
+ $thumb.append($('<img>').attr('src', opts.thumbUrl).attr('alt', opts.fileName || ''));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $thumb.append('<div class="studiou-fpp-card-thumb-placeholder"></div>');
|
|
|
|
|
+ }
|
|
|
|
|
+ var $progress = $('<div class="studiou-fpp-card-progress"></div>');
|
|
|
|
|
+ $progress.append('<div class="studiou-fpp-card-progress-fill"></div>');
|
|
|
|
|
+ $progress.append('<span class="studiou-fpp-card-progress-text">0%</span>');
|
|
|
|
|
+ $thumb.append($progress);
|
|
|
|
|
+ $card.append($thumb);
|
|
|
|
|
+
|
|
|
|
|
+ // Body column
|
|
|
|
|
+ var $body = $('<div class="studiou-fpp-card-body"></div>');
|
|
|
|
|
+ $body.append($('<div class="studiou-fpp-card-name"></div>').text(opts.fileName || ''));
|
|
|
|
|
+
|
|
|
|
|
+ var $controls = $('<div class="studiou-fpp-card-controls"></div>');
|
|
|
|
|
+ $controls.append(buildVariantSelect(opts.variationId || 0));
|
|
|
|
|
+ $controls.append(buildQtyStepper());
|
|
|
|
|
+ $body.append($controls);
|
|
|
|
|
+
|
|
|
|
|
+ var $priceRow = $('<div class="studiou-fpp-card-price-row"></div>');
|
|
|
|
|
+ $priceRow.append('<span class="studiou-fpp-card-price">—</span>');
|
|
|
|
|
+ $priceRow.append('<span class="studiou-fpp-card-badge" style="display:none;"></span>');
|
|
|
|
|
+ $priceRow.append('<button type="button" class="button studiou-fpp-card-addtocart">' +
|
|
|
|
|
+ escHtml(i18n.addToCart || 'Add to cart') + '</button>');
|
|
|
|
|
+ $body.append($priceRow);
|
|
|
|
|
+
|
|
|
|
|
+ $card.append($body);
|
|
|
|
|
+
|
|
|
|
|
+ if (!opts.uploading) {
|
|
|
|
|
+ updateCardPrice($card);
|
|
|
|
|
+ }
|
|
|
|
|
+ return $card;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function buildVariantSelect(initialVariationId) {
|
|
|
|
|
+ var $select = $('<select class="studiou-fpp-card-variant"></select>');
|
|
|
|
|
+ $select.append('<option value="0">' + escHtml(i18n.selectVariant || 'Select variant') + '</option>');
|
|
|
|
|
+ for (var i = 0; i < variants.length; i++) {
|
|
|
|
|
+ var v = variants[i];
|
|
|
|
|
+ var priceLabel = v.base_price ? ' — ' + formatPrice(v.base_price) : '';
|
|
|
|
|
+ var $opt = $('<option></option>')
|
|
|
|
|
+ .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 = $('<div class="studiou-fpp-card-qty"></div>');
|
|
|
|
|
+ $stepper.append('<button type="button" class="studiou-fpp-qty-btn studiou-fpp-qty-minus">−</button>');
|
|
|
|
|
+ $stepper.append('<input type="number" class="studiou-fpp-qty-input" min="1" step="1" value="1">');
|
|
|
|
|
+ $stepper.append('<button type="button" class="studiou-fpp-qty-btn studiou-fpp-qty-plus">+</button>');
|
|
|
|
|
+ return $stepper;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- // Drag and drop
|
|
|
|
|
|
|
+ // Card live updates
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- $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]);
|
|
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- $dropzone.on('click', function () {
|
|
|
|
|
- if (!isUploading) {
|
|
|
|
|
- $fileInput.trigger('click');
|
|
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- $fileInput.on('change', function () {
|
|
|
|
|
- if (this.files.length > 0) {
|
|
|
|
|
- handleFile(this.files[0]);
|
|
|
|
|
|
|
+ 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;
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
|
|
|
|
|
- // Remove file
|
|
|
|
|
- $removeBtn.on('click', function () {
|
|
|
|
|
- removeUploadedFile();
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ 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 logic
|
|
|
|
|
|
|
+ // Upload dropzone
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- function handleFile(file) {
|
|
|
|
|
- if (isUploading) return;
|
|
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- var maxBytes = maxFileSize * 1024 * 1024;
|
|
|
|
|
|
|
+ function pushFile(file) {
|
|
|
|
|
+ var maxBytes = cfg.maxFileSize * 1024 * 1024;
|
|
|
if (file.size > maxBytes) {
|
|
if (file.size > maxBytes) {
|
|
|
- showMessage(studiouWcfppFront.i18n.fileTooLarge.replace('%s', maxFileSize), 'error');
|
|
|
|
|
|
|
+ showMessage(formatI18n(i18n.fileTooLarge, cfg.maxFileSize), 'error');
|
|
|
return;
|
|
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});
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- clearMessages();
|
|
|
|
|
- startChunkedUpload(file);
|
|
|
|
|
|
|
+ function drainQueue() {
|
|
|
|
|
+ if (activeUpload || !uploadQueue.length) return;
|
|
|
|
|
+ activeUpload = true;
|
|
|
|
|
+ var next = uploadQueue.shift();
|
|
|
|
|
+ uploadOne(next.file, next.$card, function () {
|
|
|
|
|
+ activeUpload = false;
|
|
|
|
|
+ drainQueue();
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function startChunkedUpload(file) {
|
|
|
|
|
- isUploading = true;
|
|
|
|
|
- var totalChunks = Math.ceil(file.size / chunkSize);
|
|
|
|
|
- var uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
|
|
|
|
|
+ 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 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...');
|
|
|
|
|
|
|
+ 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({
|
|
$.ajax({
|
|
|
url: studiouWcfppFront.ajaxUrl,
|
|
url: studiouWcfppFront.ajaxUrl,
|
|
|
type: 'POST',
|
|
type: 'POST',
|
|
|
- data: formData,
|
|
|
|
|
|
|
+ data: form,
|
|
|
processData: false,
|
|
processData: false,
|
|
|
contentType: false,
|
|
contentType: false,
|
|
|
- timeout: isLastChunk ? 120000 : 30000,
|
|
|
|
|
|
|
+ timeout: isLast ? 120000 : 30000,
|
|
|
success: function (response) {
|
|
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();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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 {
|
|
} else {
|
|
|
- uploadFailed(response.data ? response.data.message : studiouWcfppFront.i18n.uploadError);
|
|
|
|
|
|
|
+ finishUpload($card, response.data);
|
|
|
|
|
+ done();
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
error: function () {
|
|
error: function () {
|
|
|
- uploadFailed(studiouWcfppFront.i18n.uploadError);
|
|
|
|
|
|
|
+ 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(
|
|
|
|
|
+ $('<img>').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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- uploadNextChunk();
|
|
|
|
|
|
|
+ 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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- // Session management
|
|
|
|
|
|
|
+ // Remove upload (per-card × button)
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- function storeInSession(attachmentId, fileRecordId, fileName) {
|
|
|
|
|
|
|
+ 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({
|
|
$.ajax({
|
|
|
url: studiouWcfppFront.ajaxUrl,
|
|
url: studiouWcfppFront.ajaxUrl,
|
|
|
type: 'POST',
|
|
type: 'POST',
|
|
|
data: {
|
|
data: {
|
|
|
- action: 'studiou_wcfpp_set_upload_session',
|
|
|
|
|
- nonce: studiouWcfppFront.nonce,
|
|
|
|
|
- attachment_id: attachmentId,
|
|
|
|
|
- file_record_id: fileRecordId,
|
|
|
|
|
- file_name: fileName
|
|
|
|
|
|
|
+ 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');
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function clearSession() {
|
|
|
|
|
|
|
+ // ==========================================
|
|
|
|
|
+ // 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({
|
|
$.ajax({
|
|
|
url: studiouWcfppFront.ajaxUrl,
|
|
url: studiouWcfppFront.ajaxUrl,
|
|
|
type: 'POST',
|
|
type: 'POST',
|
|
|
data: {
|
|
data: {
|
|
|
- action: 'studiou_wcfpp_clear_upload_session',
|
|
|
|
|
- nonce: studiouWcfppFront.nonce
|
|
|
|
|
|
|
+ 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);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- // Product gallery image replacement
|
|
|
|
|
|
|
+ // Order overview
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- function updateProductGallery(imageUrl) {
|
|
|
|
|
- if (!imageUrl) return;
|
|
|
|
|
|
|
+ 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 = [
|
|
var selectors = [
|
|
|
'.woocommerce-product-gallery__image img',
|
|
'.woocommerce-product-gallery__image img',
|
|
|
'.product .wp-post-image',
|
|
'.product .wp-post-image',
|
|
@@ -212,7 +544,6 @@
|
|
|
'.entry-summary img.attachment-woocommerce_thumbnail',
|
|
'.entry-summary img.attachment-woocommerce_thumbnail',
|
|
|
'.product-gallery img:first'
|
|
'.product-gallery img:first'
|
|
|
];
|
|
];
|
|
|
-
|
|
|
|
|
var $img = null;
|
|
var $img = null;
|
|
|
for (var i = 0; i < selectors.length; i++) {
|
|
for (var i = 0; i < selectors.length; i++) {
|
|
|
$img = $(selectors[i]).first();
|
|
$img = $(selectors[i]).first();
|
|
@@ -220,22 +551,20 @@
|
|
|
}
|
|
}
|
|
|
if (!$img || !$img.length) return;
|
|
if (!$img || !$img.length) return;
|
|
|
|
|
|
|
|
- if (!originalGalleryImages.src) {
|
|
|
|
|
- originalGalleryImages.src = $img.attr('src');
|
|
|
|
|
- originalGalleryImages.srcset = $img.attr('srcset') || '';
|
|
|
|
|
- originalGalleryImages.sizes = $img.attr('sizes') || '';
|
|
|
|
|
|
|
+ if (!originalGallery.src) {
|
|
|
|
|
+ originalGallery.src = $img.attr('src');
|
|
|
|
|
+ originalGallery.srcset = $img.attr('srcset') || '';
|
|
|
|
|
+ originalGallery.sizes = $img.attr('sizes') || '';
|
|
|
var $link = $img.closest('a');
|
|
var $link = $img.closest('a');
|
|
|
- if ($link.length) originalGalleryImages.href = $link.attr('href');
|
|
|
|
|
|
|
+ if ($link.length) originalGallery.href = $link.attr('href');
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- $img.attr('src', imageUrl).removeAttr('srcset').removeAttr('sizes');
|
|
|
|
|
- var $link = $img.closest('a');
|
|
|
|
|
- if ($link.length) $link.attr('href', imageUrl);
|
|
|
|
|
|
|
+ $img.attr('src', url).removeAttr('srcset').removeAttr('sizes');
|
|
|
|
|
+ var $a = $img.closest('a');
|
|
|
|
|
+ if ($a.length) $a.attr('href', url);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function restoreProductGallery() {
|
|
|
|
|
- if (!originalGalleryImages.src) return;
|
|
|
|
|
-
|
|
|
|
|
|
|
+ function restoreHeroImage() {
|
|
|
|
|
+ if (!originalGallery.src) return;
|
|
|
var selectors = [
|
|
var selectors = [
|
|
|
'.woocommerce-product-gallery__image img',
|
|
'.woocommerce-product-gallery__image img',
|
|
|
'.product .wp-post-image',
|
|
'.product .wp-post-image',
|
|
@@ -244,377 +573,72 @@
|
|
|
'.entry-summary img.attachment-woocommerce_thumbnail',
|
|
'.entry-summary img.attachment-woocommerce_thumbnail',
|
|
|
'.product-gallery img:first'
|
|
'.product-gallery img:first'
|
|
|
];
|
|
];
|
|
|
-
|
|
|
|
|
var $img = null;
|
|
var $img = null;
|
|
|
for (var i = 0; i < selectors.length; i++) {
|
|
for (var i = 0; i < selectors.length; i++) {
|
|
|
$img = $(selectors[i]).first();
|
|
$img = $(selectors[i]).first();
|
|
|
if ($img.length) break;
|
|
if ($img.length) break;
|
|
|
}
|
|
}
|
|
|
if (!$img || !$img.length) return;
|
|
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 = {};
|
|
|
|
|
|
|
+ $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 = {};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
- // UI helpers
|
|
|
|
|
|
|
+ // 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) {
|
|
function showMessage(text, type) {
|
|
|
- var cls = type === 'error' ? 'studiou-fpp-msg-error' : 'studiou-fpp-msg-success';
|
|
|
|
|
|
|
+ var cls = type === 'error' ? 'studiou-fpp-msg-error'
|
|
|
|
|
+ : type === 'success' ? 'studiou-fpp-msg-success'
|
|
|
|
|
+ : 'studiou-fpp-msg-info';
|
|
|
$messages.html('<div class="studiou-fpp-msg ' + cls + '">' + escHtml(text) + '</div>');
|
|
$messages.html('<div class="studiou-fpp-msg ' + cls + '">' + escHtml(text) + '</div>');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function clearMessages() {
|
|
|
|
|
- $messages.empty();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ function clearMessages() { $messages.empty(); }
|
|
|
|
|
|
|
|
function escHtml(str) {
|
|
function escHtml(str) {
|
|
|
var div = document.createElement('div');
|
|
var div = document.createElement('div');
|
|
|
- div.appendChild(document.createTextNode(str));
|
|
|
|
|
|
|
+ div.appendChild(document.createTextNode(str == null ? '' : String(str)));
|
|
|
return div.innerHTML;
|
|
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 <tr> 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 = '<div class="studiou-fpp-row-tiers" title="' + escHtml(i18n.qtyTiersTitle) + '">';
|
|
|
|
|
- tierListHtml += '<span class="dashicons dashicons-tag"></span>';
|
|
|
|
|
- tierListHtml += '<span class="studiou-fpp-row-tiers-list">';
|
|
|
|
|
- for (var i = 0; i < tiers.length; i++) {
|
|
|
|
|
- var t = tiers[i];
|
|
|
|
|
- tierListHtml += '<span class="studiou-fpp-row-tier">' + t.from_qty + '+: −' + formatPercent(t.percent) + '</span>';
|
|
|
|
|
- }
|
|
|
|
|
- tierListHtml += '</span></div>';
|
|
|
|
|
- $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 escAttr(str) {
|
|
|
|
|
+ return String(str == null ? '' : str).replace(/"/g, '"');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 = '<span class="studiou-fpp-row-save-badge">−' + formatPercent(tier.percent) + '</span>';
|
|
|
|
|
- $priceEl.after(badge);
|
|
|
|
|
|
|
+ function formatI18n(tpl, val) {
|
|
|
|
|
+ if (!tpl) return String(val);
|
|
|
|
|
+ return tpl.replace('%d', val).replace('%s', val);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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+$/, '');
|
|
|
|
|
|
|
+ function formatPercent(p) {
|
|
|
|
|
+ var n = parseFloat(p) || 0;
|
|
|
|
|
+ var str = n.toFixed(2).replace(/\.?0+$/, '');
|
|
|
return str + ' %';
|
|
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 || '';
|
|
|
|
|
|
|
+ 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 fixed = parseFloat(amount).toFixed(decimals);
|
|
|
var parts = fixed.split('.');
|
|
var parts = fixed.split('.');
|
|
|
var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouSep);
|
|
var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouSep);
|
|
|
var decPart = parts.length > 1 ? parts[1] : '';
|
|
var decPart = parts.length > 1 ? parts[1] : '';
|
|
|
var numStr = decPart ? intPart + decSep + decPart : intPart;
|
|
var numStr = decPart ? intPart + decSep + decPart : intPart;
|
|
|
-
|
|
|
|
|
- var symbolHtml = '<span class="woocommerce-Price-currencySymbol">' + symbol + '</span>';
|
|
|
|
|
- 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 = '<div class="studiou-fpp-qty-tiers-box">';
|
|
|
|
|
- html += '<h4><span class="dashicons dashicons-tag"></span> ' + escHtml(i18n.qtyTiersTitle || 'Quantity Discount') + '</h4>';
|
|
|
|
|
- html += '<table class="studiou-fpp-qty-tiers-table-display"><thead><tr>';
|
|
|
|
|
- html += '<th>' + escHtml(i18n.qtyTiersFromQty || 'From Qty') + '</th>';
|
|
|
|
|
- html += '<th>' + escHtml(i18n.qtyTiersDiscount || 'Discount') + '</th>';
|
|
|
|
|
- html += '<th>' + escHtml(i18n.qtyTiersUnitPrice || 'Unit Price') + '</th>';
|
|
|
|
|
- html += '</tr></thead><tbody>';
|
|
|
|
|
-
|
|
|
|
|
- for (var i = 0; i < currentTiers.length; i++) {
|
|
|
|
|
- var t = currentTiers[i];
|
|
|
|
|
- var unit = computeDiscounted(currentBasePrice, t.percent);
|
|
|
|
|
- html += '<tr data-from-qty="' + t.from_qty + '">';
|
|
|
|
|
- html += '<td>' + t.from_qty + '+</td>';
|
|
|
|
|
- html += '<td>' + formatPercent(t.percent) + '</td>';
|
|
|
|
|
- html += '<td>' + formatPrice(unit) + '</td>';
|
|
|
|
|
- html += '</tr>';
|
|
|
|
|
- }
|
|
|
|
|
- html += '</tbody></table></div>';
|
|
|
|
|
-
|
|
|
|
|
- $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 '<span class="woocommerce-Price-amount amount">' + formatPriceInner(amount) + '</span>';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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 = '<span class="woocommerce-Price-currencySymbol">' + symbol + '</span>';
|
|
|
|
|
- return fmt.replace('%1$s', symbolHtml).replace('%2$s', numStr);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ var sym = '<span class="woocommerce-Price-currencySymbol">' + symbol + '</span>';
|
|
|
|
|
+ return '<span class="woocommerce-Price-amount amount">' +
|
|
|
|
|
+ fmt.replace('%1$s', sym).replace('%2$s', numStr) +
|
|
|
|
|
+ '</span>';
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
})(jQuery);
|
|
})(jQuery);
|