Просмотр исходного кода

Keep upload persistent, disable add-to-cart until photo uploaded — v1.0.6 -> v1.0.7

- Session no longer cleared after add-to-cart — same photo reusable
  for multiple variants
- Upload preview persists after adding to cart
- All add-to-cart buttons disabled on page load via CSS + disabled attr
  + pointer-events; capture-phase listener as fallback
- MutationObserver re-disables dynamically added buttons
- Session state restored on page reload (server passes data to JS)
- Session only cleared on explicit Remove click
- Update documentation and translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba 3 месяцев назад
Родитель
Сommit
b7c0aa7423

+ 12 - 10
studiou-wc-free-photo-product/CLAUDE.md

@@ -2,7 +2,7 @@
 
 ## Project Overview
 
-WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.6.
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.7.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 
 ## Initial Requirements
@@ -44,15 +44,17 @@ Allows publishing special variable products where customers upload custom raw im
 ## Frontend Flow
 
 1. Upload area rendered below variation table via `woocommerce_single_product_summary` (priority 35)
-2. Customer uploads photo via chunked AJAX
-3. On upload complete, JS calls `studiou_wcfpp_set_upload_session` to store file ref in WC session
-4. Product gallery image replaced with uploaded photo preview; restored on remove
-5. JS intercepts all add-to-cart buttons/links/forms — blocks if no photo uploaded
-6. Customer selects variant and clicks any Add to Cart button (works with any theme/layout)
-7. `woocommerce_add_to_cart_validation` checks WC session for uploaded file
-8. `woocommerce_add_cart_item_data` reads file data from WC session, clears session after use
-9. Cart/order item thumbnails replaced with uploaded photo (`woocommerce_cart_item_thumbnail`, `woocommerce_admin_order_item_thumbnail`)
-10. On checkout, file record is linked to order via `woocommerce_checkout_order_processed`
+2. All add-to-cart buttons disabled on page load (CSS + disabled + pointer-events + capture-phase listener + MutationObserver)
+3. On page load, existing session state restored (preview, gallery image, buttons enabled)
+4. Customer uploads photo via chunked AJAX
+5. On upload complete, JS stores file ref in WC session, enables add-to-cart buttons
+6. Product gallery image replaced with uploaded photo preview; restored on remove
+7. Customer selects variant and clicks any Add to Cart button (works with any theme/layout)
+8. `woocommerce_add_to_cart_validation` checks WC session for uploaded file (server-side fallback)
+9. `woocommerce_add_cart_item_data` reads file data from WC session (session NOT cleared — same photo reusable for multiple variants)
+10. Cart/order item thumbnails replaced with uploaded photo (`woocommerce_cart_item_thumbnail`, `woocommerce_admin_order_item_thumbnail`)
+11. On checkout, file record is linked to order via `woocommerce_checkout_order_processed`
+12. Session only cleared when user clicks Remove
 
 ## Coding Conventions
 

+ 7 - 0
studiou-wc-free-photo-product/assets/css/frontend.css

@@ -236,6 +236,13 @@
     cursor: not-allowed;
 }
 
+/* Disabled add-to-cart buttons */
+.studiou-fpp-disabled {
+    opacity: 0.4 !important;
+    cursor: not-allowed !important;
+    pointer-events: none !important;
+}
+
 /* Messages */
 .studiou-fpp-msg {
     padding: 10px 15px;

+ 105 - 65
studiou-wc-free-photo-product/assets/js/frontend.js

@@ -14,7 +14,20 @@
         var uploadedAttachmentId = null;
         var uploadedFileRecordId = null;
         var isUploading = false;
-        var originalGalleryImages = {}; // Store original src/srcset to restore on remove
+        var originalGalleryImages = {};
+
+        // Broad selectors for add-to-cart elements
+        var addToCartSelectors = [
+            '.single_add_to_cart_button',
+            '.add_to_cart_button',
+            'button[name="add-to-cart"]',
+            'input[name="add-to-cart"]',
+            'a[data-product_id]',
+            'button[data-product_id]',
+            'a[href*="add-to-cart"]',
+            '.product form.cart button[type="submit"]',
+            '.product form.cart input[type="submit"]'
+        ].join(', ');
 
         // Upload area elements
         var $dropzone = $uploadWrap.find('#studiou-fpp-dropzone');
@@ -28,51 +41,80 @@
         var $removeBtn = $uploadWrap.find('.studiou-fpp-remove-file');
         var $messages = $uploadWrap.find('.studiou-fpp-messages');
 
-        // Block all add-to-cart actions until photo is uploaded
-        // Intercept form submissions
-        $(document).on('submit', 'form.cart, form.variations_form', function (e) {
-            if (!uploadedAttachmentId) {
-                e.preventDefault();
-                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
-                scrollToUpload();
-                return false;
-            }
-        });
+        // ==========================================
+        // Disable/enable add-to-cart buttons
+        // ==========================================
+        function disableAddToCart() {
+            $(addToCartSelectors).each(function () {
+                var $el = $(this);
+                $el.addClass('studiou-fpp-disabled');
+                if ($el.is('a')) {
+                    if (!$el.data('studiou-fpp-href')) {
+                        $el.data('studiou-fpp-href', $el.attr('href'));
+                    }
+                    $el.attr('href', '#');
+                } else {
+                    $el.prop('disabled', true);
+                }
+            });
+        }
+
+        function enableAddToCart() {
+            $(addToCartSelectors).each(function () {
+                var $el = $(this);
+                $el.removeClass('studiou-fpp-disabled');
+                if ($el.is('a')) {
+                    var origHref = $el.data('studiou-fpp-href');
+                    if (origHref) {
+                        $el.attr('href', origHref);
+                    }
+                } else {
+                    $el.prop('disabled', false);
+                }
+            });
+        }
 
-        // Intercept add-to-cart button clicks (covers AJAX and non-AJAX buttons)
-        $(document).on('click', '.single_add_to_cart_button, .add_to_cart_button, button[name="add-to-cart"], input[name="add-to-cart"]', function (e) {
+        // Fallback: intercept clicks on disabled elements (capture phase)
+        document.addEventListener('click', function (e) {
             if (!uploadedAttachmentId) {
-                e.preventDefault();
-                e.stopImmediatePropagation();
-                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
-                scrollToUpload();
-                return false;
+                var el = e.target.closest(addToCartSelectors.replace(/,\s*/g, ', '));
+                if (el) {
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                    showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
+                    $('html, body').animate({ scrollTop: $uploadWrap.offset().top - 100 }, 400);
+                    return false;
+                }
             }
-        });
+        }, true); // capture phase - fires before bubble
 
-        // Intercept add-to-cart links (e.g. ?add-to-cart=ID)
-        $(document).on('click', 'a[href*="add-to-cart"]', function (e) {
+        // Disable buttons on page load
+        disableAddToCart();
+
+        // Also watch for dynamically added buttons (e.g. AJAX-loaded variations)
+        var observer = new MutationObserver(function () {
             if (!uploadedAttachmentId) {
-                e.preventDefault();
-                e.stopImmediatePropagation();
-                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
-                scrollToUpload();
-                return false;
+                disableAddToCart();
             }
         });
+        observer.observe(document.querySelector('.product') || document.body, {
+            childList: true, subtree: true
+        });
 
-        function scrollToUpload() {
-            $('html, body').animate({
-                scrollTop: $uploadWrap.offset().top - 100
-            }, 400);
+        // ==========================================
+        // 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);
+            enableAddToCart();
         }
 
-        // Reset upload after successful WC add-to-cart
-        $(document.body).on('added_to_cart', function () {
-            resetUpload();
-        });
-
+        // ==========================================
         // Drag and drop
+        // ==========================================
         $dropzone.on('dragover dragenter', function (e) {
             e.preventDefault();
             e.stopPropagation();
@@ -109,10 +151,11 @@
             removeUploadedFile();
         });
 
+        // ==========================================
+        // Upload logic
+        // ==========================================
         function handleFile(file) {
-            if (isUploading) {
-                return;
-            }
+            if (isUploading) return;
 
             var maxBytes = maxFileSize * 1024 * 1024;
             if (file.size > maxBytes) {
@@ -149,7 +192,6 @@
                 formData.append('file_size', file.size);
                 formData.append('chunk', blob, 'chunk');
 
-                // Last chunk triggers server-side assembly - needs longer timeout
                 var isLastChunk = (currentChunk === totalChunks - 1);
                 if (isLastChunk) {
                     $progressFill.css('width', '100%');
@@ -176,7 +218,6 @@
                                 uploadedAttachmentId = response.data.attachment_id;
                                 uploadedFileRecordId = response.data.file_record_id;
 
-                                // Store in WC session so any add-to-cart button picks it up
                                 storeInSession(
                                     response.data.attachment_id,
                                     response.data.file_record_id,
@@ -185,6 +226,7 @@
 
                                 showPreview(response.data.thumbnail_url, response.data.file_name);
                                 updateProductGallery(response.data.preview_url || response.data.thumbnail_url);
+                                enableAddToCart();
                                 $progress.hide();
                                 isUploading = false;
                             } else {
@@ -203,6 +245,9 @@
             uploadNextChunk();
         }
 
+        // ==========================================
+        // Session management
+        // ==========================================
         function storeInSession(attachmentId, fileRecordId, fileName) {
             $.ajax({
                 url: studiouWcfppFront.ajaxUrl,
@@ -228,18 +273,12 @@
             });
         }
 
-        function uploadFailed(message) {
-            isUploading = false;
-            $progress.hide();
-            $dropzone.show();
-            showMessage(message, 'error');
-            $fileInput.val('');
-        }
-
+        // ==========================================
+        // Product gallery image replacement
+        // ==========================================
         function updateProductGallery(imageUrl) {
             if (!imageUrl) return;
 
-            // Find the main product image (try common WC / theme selectors)
             var selectors = [
                 '.woocommerce-product-gallery__image img',
                 '.product .wp-post-image',
@@ -257,19 +296,16 @@
 
             if (!$img || !$img.length) return;
 
-            // Save original attributes for restoration
             if (!originalGalleryImages.src) {
                 originalGalleryImages.src = $img.attr('src');
                 originalGalleryImages.srcset = $img.attr('srcset') || '';
                 originalGalleryImages.sizes = $img.attr('sizes') || '';
-                // Also save the parent link href if it's a gallery link
                 var $parentLink = $img.closest('a');
                 if ($parentLink.length) {
                     originalGalleryImages.href = $parentLink.attr('href');
                 }
             }
 
-            // Replace with uploaded image
             $img.attr('src', imageUrl);
             $img.removeAttr('srcset');
             $img.removeAttr('sizes');
@@ -301,23 +337,27 @@
             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.srcset) $img.attr('srcset', originalGalleryImages.srcset);
+            if (originalGalleryImages.sizes) $img.attr('sizes', originalGalleryImages.sizes);
 
             if (originalGalleryImages.href) {
-                var $parentLink = $img.closest('a');
-                if ($parentLink.length) {
-                    $parentLink.attr('href', 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();
@@ -326,12 +366,11 @@
             }
             $previewName.text(fileName);
             $preview.show();
+            $dropzone.hide();
         }
 
         function removeUploadedFile() {
-            if (!uploadedFileRecordId) {
-                return;
-            }
+            if (!uploadedFileRecordId) return;
 
             $.ajax({
                 url: studiouWcfppFront.ajaxUrl,
@@ -352,6 +391,7 @@
             uploadedFileRecordId = null;
             clearSession();
             restoreProductGallery();
+            disableAddToCart();
             $preview.hide();
             $previewImg.attr('src', '');
             $previewName.text('');

+ 1 - 5
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-cart.php

@@ -71,11 +71,7 @@ class Studiou_WC_FPP_Cart {
             'variation_id' => $actual_variation_id,
         ));
 
-        // Clear session data after reading (so next add-to-cart is clean)
-        WC()->session->set('studiou_fpp_attachment_id', null);
-        WC()->session->set('studiou_fpp_file_record_id', null);
-        WC()->session->set('studiou_fpp_file_name', null);
-
+        // Keep session data so the same photo can be used for multiple variants
         return $cart_item_data;
     }
 

+ 19 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-single-product.php

@@ -29,6 +29,25 @@ class Studiou_WC_FPP_Single_Product {
         $product_id = $product->get_id();
         $max_file_size = Studiou_WC_FPP_Product::get_product_max_file_size($product_id);
 
+        // Pass existing session state to JS for restore on page load
+        $session_data = array();
+        if (WC()->session) {
+            $att_id = WC()->session->get('studiou_fpp_attachment_id');
+            if ($att_id) {
+                $thumb = wp_get_attachment_image_src($att_id, 'thumbnail');
+                $preview = wp_get_attachment_image_src($att_id, 'woocommerce_single');
+                $session_data = array(
+                    'attachment_id'  => $att_id,
+                    'file_record_id' => WC()->session->get('studiou_fpp_file_record_id'),
+                    'file_name'      => WC()->session->get('studiou_fpp_file_name'),
+                    'thumbnail_url'  => $thumb ? $thumb[0] : '',
+                    'preview_url'    => $preview ? $preview[0] : ($thumb ? $thumb[0] : ''),
+                );
+            }
+        }
+
+        wp_localize_script('studiou-wcfpp-frontend', 'studiouWcfppSession', $session_data);
+
         include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-upload.php';
     }
 

BIN
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product-cs_CZ.mo


+ 1 - 1
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product-cs_CZ.po

@@ -3,7 +3,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.6\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.7\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: 2026-04-02 00:00+0000\n"

+ 1 - 1
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product.pot

@@ -2,7 +2,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.6\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.7\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"

+ 13 - 5
studiou-wc-free-photo-product/readme.md

@@ -31,12 +31,20 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 
 When a variable product has Free Photo enabled, the upload area automatically appears on the WooCommerce single product page **below** the product summary (below the variation table and Add to Cart buttons). The customer workflow is:
 
-1. Upload a photo file via drag-and-drop or file browser (below the variation table)
-2. The product gallery image updates to show the uploaded photo preview
-3. Select a variant from the product variation table
-4. Click the variant's **Add to Cart** button
+1. All Add to Cart buttons are disabled until a photo is uploaded
+2. Upload a photo file via drag-and-drop or file browser (below the variation table)
+3. The product gallery image updates to show the uploaded photo preview
+4. Add to Cart buttons become enabled
+5. Select a variant from the product variation table and click its **Add to Cart** button
+6. The uploaded photo stays — the same photo can be used for multiple variants without re-uploading
 
-The uploaded file reference is stored in the WooCommerce session, so it works with any add-to-cart method (form POST, AJAX buttons, individual variation buttons). The plugin validates that a photo is uploaded before allowing add-to-cart — both server-side (`woocommerce_add_to_cart_validation`) and client-side (JS intercepts all add-to-cart buttons/links/forms). The upload area is rendered via `woocommerce_single_product_summary` hook (priority 35, after the add-to-cart section). When the photo is removed, the original product image is restored.
+The uploaded file reference is stored in the WooCommerce session, so it works with any add-to-cart method (form POST, AJAX buttons, individual variation buttons). The session persists across add-to-cart actions, allowing the same photo to be used for multiple variants. The session is only cleared when the user explicitly removes the uploaded photo.
+
+Add-to-cart is blocked until a photo is uploaded:
+- **Client-side**: all add-to-cart buttons/links are disabled on page load (CSS + disabled attribute + pointer-events). A capture-phase event listener provides a fallback intercept. A MutationObserver re-disables dynamically added buttons.
+- **Server-side**: `woocommerce_add_to_cart_validation` checks the WC session for uploaded file data.
+
+On page reload, existing session state is restored — the upload preview, product gallery image, and enabled buttons are all reconstructed from the server-side session data. When the photo is removed, the original product image is restored and buttons are disabled again.
 
 ### Shortcode
 

+ 2 - 2
studiou-wc-free-photo-product/studiou-wc-free-photo-product.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Free Photo Product
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-free-photo-product
  * Description: Allows publishing special WooCommerce products with variants and custom raw image upload for photo printing
- * Version: 1.0.6
+ * Version: 1.0.7
  * Requires at least: 6.9.4
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -21,7 +21,7 @@ if (!defined('WPINC')) {
     die;
 }
 
-if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.6');
+if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.7');
 if (!defined('STUDIOU_WCFPP_PLUGIN_DIR')) define('STUDIOU_WCFPP_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCFPP_PLUGIN_URL')) define('STUDIOU_WCFPP_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCFPP_PLUGIN_BASENAME')) define('STUDIOU_WCFPP_PLUGIN_BASENAME', plugin_basename(__FILE__));