PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
← All changes | assets/js/modules/quick-view/quick-view.js +346 -4 1.8 → 2.3.2 View file →
@@ -11,33 +11,61 @@
11 11 return;
12 12 }
13 13 $modals.each(function () {
14 14 var $modal = $(this);
15 - var $openButton = $('.merchant-quick-view-button');
16 15 var $closeButton = $modal.find('.merchant-quick-view-close-button');
16 + var $navPrev = $modal.find('.merchant-quick-view-nav-prev');
17 + var $navNext = $modal.find('.merchant-quick-view-nav-next');
17 18 var $inner = $modal.find('.merchant-quick-view-inner');
18 19 var $content = $modal.find('.merchant-quick-view-content');
19 20 var $overlay = $modal.find('.merchant-quick-view-overlay');
20 21 var isOpen = false;
21 - $openButton.on('click', function (e) {
22 - e.preventDefault();
22 + var currentIndex = -1;
23 + var openedViaOwnPushState = false;
24 + function updateNavState(productId) {
25 + var productIds = self.getProductIds();
26 + currentIndex = productIds.indexOf(productId);
27 + if (productIds.length < 2 || -1 === currentIndex) {
28 + $navPrev.removeClass('merchant-quick-view-nav-visible');
29 + $navNext.removeClass('merchant-quick-view-nav-visible');
30 + return;
31 + }
32 + $navPrev.toggleClass('merchant-quick-view-nav-visible', currentIndex > 0);
33 + $navNext.toggleClass('merchant-quick-view-nav-visible', currentIndex < productIds.length - 1);
34 + }
35 + function loadProduct(productId, historyAction) {
23 36 $content.empty();
24 37 $modal.addClass('merchant-show');
25 38 $modal.addClass('merchant-loading');
26 39 isOpen = true;
40 + if (window.merchant.setting.sync_url_hash) {
41 + if ('push' === historyAction) {
42 + history.pushState({
43 + merchantQuickView: productId
44 + }, '', '#merchant-quick-view-' + productId);
45 + } else if ('replace' === historyAction) {
46 + history.replaceState({
47 + merchantQuickView: productId
48 + }, '', '#merchant-quick-view-' + productId);
49 + }
50 + }
27 51 $.post(window.merchant.setting.ajax_url, {
28 52 action: 'merchant_quick_view_content',
29 53 nonce: window.merchant.setting.nonce,
30 - product_id: $(this).data('product-id')
54 + product_id: productId
31 55 }, function (response) {
32 56 if (response.success && isOpen) {
33 57 $content.html(response.data);
34 58 $inner.addClass('merchant-show');
35 59 $modal.removeClass('merchant-loading');
60 + updateNavState(productId);
36 61 var $gallery = $content.find('.woocommerce-product-gallery');
37 62 wc_single_product_params.zoom_enabled = window.merchant.setting.quick_view_zoom;
38 63 if (typeof $.fn.wc_product_gallery === 'function' && $gallery.length) {
39 64 $gallery.trigger('wc-product-gallery-before-init', [$gallery.get(0), wc_single_product_params]);
65 +
66 + // Force-enable the gallery slider in case the theme/plugin has removed it using `remove_theme_support( 'wc-product-gallery-slider' )`
67 + wc_single_product_params['flexslider_enabled'] = '1';
40 68 $gallery.wc_product_gallery(wc_single_product_params);
41 69 $gallery.trigger('wc-product-gallery-after-init', [$gallery.get(0), wc_single_product_params]);
42 70 }
43 71 var $variations = $content.find('.variations_form');
@@ -51,29 +79,343 @@
51 79 }
52 80 if (window.botiga && window.botiga.qtyButton) {
53 81 window.botiga.qtyButton.init('quick-view');
54 82 }
83 +
84 + // Initialize AJAX add to cart if enabled.
85 + if (window.merchant.setting.ajax_add_to_cart) {
86 + self.initAjaxAddToCart($content, $modal);
87 + }
55 88 } else {
56 89 $content.html(response.data);
57 90 $inner.addClass('merchant-show');
58 91 $modal.removeClass('merchant-loading');
92 + $navPrev.removeClass('merchant-quick-view-nav-visible');
93 + $navNext.removeClass('merchant-quick-view-nav-visible');
59 94 }
95 + window.dispatchEvent(new Event('merchant.quickview.ajax.loaded'));
60 96 }).fail(function (xhr, textStatus) {
61 97 $content.html(textStatus);
62 98 $inner.addClass('merchant-show');
63 99 $modal.removeClass('merchant-loading');
100 + $navPrev.removeClass('merchant-quick-view-nav-visible');
101 + $navNext.removeClass('merchant-quick-view-nav-visible');
64 102 });
103 + }
104 + $(document).on('click', '.merchant-quick-view-button', function (e) {
105 + e.preventDefault();
106 + openedViaOwnPushState = !!window.merchant.setting.sync_url_hash;
107 + loadProduct($(this).data('product-id'), 'push');
65 108 });
109 + $navPrev.on('click', function (e) {
110 + e.preventDefault();
111 + var productIds = self.getProductIds();
112 + if (currentIndex > 0) {
113 + loadProduct(productIds[currentIndex - 1], 'replace');
114 + }
115 + });
116 + $navNext.on('click', function (e) {
117 + e.preventDefault();
118 + var productIds = self.getProductIds();
119 + if (currentIndex > -1 && currentIndex < productIds.length - 1) {
120 + loadProduct(productIds[currentIndex + 1], 'replace');
121 + }
122 + });
66 123 $overlay.on('click', function (e) {
67 124 e.preventDefault();
68 125 $closeButton.trigger('click');
69 126 });
127 + $(document).on('keydown', function (e) {
128 + if ('Escape' === e.key && isOpen) {
129 + $closeButton.trigger('click');
130 + }
131 + });
70 132 $closeButton.on('click', function (e) {
71 133 e.preventDefault();
72 134 isOpen = false;
73 135 $modal.removeClass('merchant-show');
74 136 $inner.removeClass('merchant-show');
137 + if (window.merchant.setting.sync_url_hash) {
138 + if (openedViaOwnPushState) {
139 + history.back();
140 + } else {
141 + history.replaceState(null, '', location.pathname + location.search);
142 + }
143 + openedViaOwnPushState = false;
144 + }
75 145 });
146 + if (window.merchant.setting.sync_url_hash) {
147 + window.addEventListener('popstate', function () {
148 + var match = location.hash.match(/^#merchant-quick-view-(\d+)$/);
149 + if (match) {
150 + loadProduct(parseInt(match[1], 10), 'none');
151 + } else if (isOpen) {
152 + isOpen = false;
153 + $modal.removeClass('merchant-show');
154 + $inner.removeClass('merchant-show');
155 + openedViaOwnPushState = false;
156 + }
157 + });
158 + var initialMatch = location.hash.match(/^#merchant-quick-view-(\d+)$/);
159 + if (initialMatch) {
160 + loadProduct(parseInt(initialMatch[1], 10), 'none');
161 + }
162 + }
163 + });
164 + },
165 + /**
166 + * Collect product IDs for every Quick View button currently in the DOM, in document order.
167 + *
168 + * @return {Array} Ordered, de-duplicated product IDs.
169 + */
170 + getProductIds: function getProductIds() {
171 + var ids = [];
172 + $('.merchant-quick-view-button[data-product-id]').each(function () {
173 + var id = $(this).data('product-id');
174 + if (id && ids.indexOf(id) === -1) {
175 + ids.push(id);
176 + }
177 + });
178 + return ids;
179 + },
180 + /**
181 + * Initialize AJAX add to cart handler.
182 + *
183 + * @param {jQuery} $content The modal content container.
184 + * @param {jQuery} $modal The modal container.
185 + */
186 + initAjaxAddToCart: function initAjaxAddToCart($content, $modal) {
187 + var self = this;
188 +
189 + // Remove any existing handlers to prevent duplicates.
190 + $content.off('submit', 'form.cart');
191 +
192 + // Handle add to cart form submission.
193 + $content.on('submit', 'form.cart', function (e) {
194 + // Access the native submitter.
195 + var submitter = e.originalEvent ? e.originalEvent.submitter : null;
196 + var $submitter = $(submitter);
197 + var isAddToCart = false;
198 +
199 + // 1. If triggered by Enter key (no submitter), assume Add to Cart (standard behavior)
200 + // Note: You can add extra checks here for document.activeElement if you want to be extra safe for "Enter on Buy Now",
201 + // but typically Enter in input fields implies the default action (Add to Cart).
202 + if (!submitter) {
203 + isAddToCart = true;
204 + }
205 + // 2. Check if submitter is explicitly the "Add to Cart" button
206 + // WooCommerce standard Add to Cart usually has name="add-to-cart" or class="single_add_to_cart_button"
207 + else if ($submitter.attr('name') === 'add-to-cart' || $submitter.hasClass('single_add_to_cart_button')) {
208 + isAddToCart = true;
209 + }
210 +
211 + // If it's NOT an Add to Cart action (e.g. it's a Buy Now button), allow default submission
212 + if (!isAddToCart) {
213 + return;
214 + }
215 + e.preventDefault();
216 + e.stopImmediatePropagation(); // Prevent other handlers from firing.
217 +
218 + var $form = $(this);
219 + var $button = $form.find('button[type="submit"], input[type="submit"]');
220 + var $addToCartButton = $form.find('.single_add_to_cart_button');
221 +
222 + // Use add to cart button if available, otherwise use first submit button.
223 + if (!$addToCartButton.length) {
224 + $addToCartButton = $button.first();
225 + }
226 +
227 + // Prevent duplicate submissions.
228 + if ($addToCartButton.hasClass('loading') || $addToCartButton.prop('disabled')) {
229 + return false;
230 + }
231 +
232 + // Get form data.
233 + var formData = $form.serializeArray();
234 + var data = {
235 + action: 'merchant_quick_view_add_to_cart',
236 + nonce: window.merchant.setting.nonce
237 + };
238 +
239 + // Check if this is a grouped product (has quantity fields with array notation like quantity[14]).
240 + var isGroupedProduct = false;
241 + $.each(formData, function (i, field) {
242 + if (field.name && field.name.indexOf('quantity[') === 0) {
243 + isGroupedProduct = true;
244 + return false; // Break the loop.
245 + }
246 + });
247 +
248 + // Convert form data to object, but handle quantity specially.
249 + $.each(formData, function (i, field) {
250 + // Skip standalone quantity field - we'll handle it separately for non-grouped products.
251 + if (field.name === 'quantity') {
252 + return;
253 + }
254 + // Skip add-to-cart field - we'll handle product ID separately.
255 + if (field.name === 'add-to-cart') {
256 + return;
257 + }
258 + // For grouped products, quantity[ID] fields are added as-is.
259 + // Variation attributes are sent as-is (attribute_pa_color, etc.).
260 + data[field.name] = field.value;
261 + });
262 +
263 + // Get product ID - try multiple sources (most reliable first).
264 + // 1. Extract from product wrapper div ID (format: product-{id}) - this is always present in the modal.
265 + var $productWrapper = $form.closest('[id^="product-"]');
266 + if ($productWrapper.length) {
267 + var productIdMatch = $productWrapper.attr('id').match(/product-(\d+)/);
268 + if (productIdMatch && productIdMatch[1]) {
269 + data.product_id = productIdMatch[1];
270 + }
271 + }
272 +
273 + // 2. Try to get it directly from the form input.
274 + if (!data.product_id) {
275 + var addToCartInput = $form.find('input[name="add-to-cart"]');
276 + if (addToCartInput.length && addToCartInput.val()) {
277 + data.product_id = addToCartInput.val();
278 + }
279 + }
280 +
281 + // Ensure product_id is set, otherwise we can't proceed.
282 + if (!data.product_id) {
283 + console.error('Quick View: Could not determine product ID');
284 + return false;
285 + }
286 +
287 + // Get variation ID if it's a variable product.
288 + if ($form.find('.variations_form').length) {
289 + var variationId = $form.find('input[name="variation_id"]').val();
290 + if (variationId) {
291 + data.variation_id = parseInt(variationId, 10);
292 + }
293 + }
294 +
295 + // Get quantity and ensure it's a number (not a string).
296 + // Only add standalone quantity for non-grouped products.
297 + // Grouped products use quantity[product_id] format which is already in the data object.
298 + if (!isGroupedProduct) {
299 + delete data.quantity; // Remove if it was included in serialized data.
300 + var quantityInput = $form.find('input[name="quantity"]');
301 + var quantity = quantityInput.length ? parseInt(quantityInput.val(), 10) : 1;
302 + if (isNaN(quantity) || quantity < 1) {
303 + quantity = 1;
304 + }
305 + data.quantity = quantity;
306 + } else {
307 + // For grouped products, ensure no standalone quantity field exists.
308 + delete data.quantity;
309 + }
310 +
311 + // Show loading state.
312 + var originalText = $addToCartButton.html();
313 + var originalDisabled = $addToCartButton.prop('disabled');
314 + $addToCartButton.addClass('loading').prop('disabled', true);
315 + if ($addToCartButton.is('button')) {
316 + $addToCartButton.data('original-text', originalText);
317 + $addToCartButton.html('<span class="spinner"></span>');
318 + }
319 +
320 + // Send AJAX request.
321 + $.ajax({
322 + type: 'POST',
323 + url: window.merchant.setting.ajax_url,
324 + data: data,
325 + success: function success(response) {
326 + self.handleAddToCartSuccess(response, $addToCartButton, originalText, originalDisabled, $content, $modal);
327 + },
328 + error: function error(xhr, textStatus, errorThrown) {
329 + self.handleAddToCartError(xhr, textStatus, errorThrown, $addToCartButton, originalText, originalDisabled, $content);
330 + }
331 + });
332 + return false;
333 + });
334 + },
335 + /**
336 + * Handle successful add to cart response.
337 + *
338 + * @param {Object} response AJAX response.
339 + * @param {jQuery} $button The add to cart button.
340 + * @param {string} originalText Original button text.
341 + * @param {boolean} originalDisabled Original disabled state.
342 + * @param {jQuery} $content The modal content container.
343 + * @param {jQuery} $modal The modal container.
344 + */
345 + handleAddToCartSuccess: function handleAddToCartSuccess(response, $button, originalText, originalDisabled, $content, $modal) {
346 + // Remove loading state.
347 + $button.removeClass('loading').prop('disabled', originalDisabled);
348 + if ($button.is('button') && $button.data('original-text')) {
349 + $button.html($button.data('original-text'));
350 + }
351 + if (response.success && response.data) {
352 + // Update cart fragments.
353 + if (response.data.fragments) {
354 + $.each(response.data.fragments, function (selector, html) {
355 + $(selector).replaceWith(html);
356 + });
357 + }
358 +
359 + // Trigger WooCommerce events.
360 + $(document.body).trigger('added_to_cart', [response.data.fragments, response.data.cart_hash, $button, 'quick-view']);
361 + $(document.body).trigger('wc_fragment_refresh');
362 +
363 + // Show success message.
364 + if (response.data.message) {
365 + // Remove existing notices.
366 + $content.find('.merchant-quick-view-inner .woocommerce-error, .woocommerce-message, .woocommerce-info').remove();
367 + // trigger event for third party integration
368 + $(document).trigger('merchant_quick_view_add_to_cart_success', [response.data.message, $content]);
369 + }
370 + } else {
371 + // Handle unexpected response.
372 + this.handleAddToCartError(null, 'error', 'Unexpected response format', $button, originalText, originalDisabled, $content);
373 + }
374 + },
375 + /**
376 + * Handle add to cart error response.
377 + *
378 + * @param {Object} xhr XMLHttpRequest object.
379 + * @param {string} textStatus Status text.
380 + * @param {string} errorThrown Error thrown.
381 + * @param {jQuery} $button The add to cart button.
382 + * @param {string} originalText Original button text.
383 + * @param {boolean} originalDisabled Original disabled state.
384 + * @param {jQuery} $content The modal content container.
385 + */
386 + handleAddToCartError: function handleAddToCartError(xhr, textStatus, errorThrown, $button, originalText, originalDisabled, $content) {
387 + // Remove loading state.
388 + $button.removeClass('loading').prop('disabled', originalDisabled);
389 + if ($button.is('button') && $button.data('original-text')) {
390 + $button.html($button.data('original-text'));
391 + }
392 +
393 + // Get error message.
394 + var errorMessage = '';
395 + if (xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
396 + errorMessage = xhr.responseJSON.data.message;
397 + } else if (xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.notices) {
398 + // Extract error from notices.
399 + var notices = xhr.responseJSON.data.notices;
400 + if (notices.error && notices.error.length > 0) {
401 + errorMessage = notices.error[0].notice;
402 + }
403 + } else {
404 + errorMessage = errorThrown || textStatus || 'An error occurred while adding the product to cart.';
405 + }
406 +
407 + // Remove existing notices.
408 + $content.find('.woocommerce-error, .woocommerce-message, .woocommerce-info').remove();
409 +
410 + // Add error message.
411 + var $error = $('<div class="woocommerce-error" role="alert"></div>').text(errorMessage);
412 + $content.find('form.cart').before($error);
413 +
414 + // Scroll to error if needed.
415 + $error[0].scrollIntoView({
416 + behavior: 'smooth',
417 + block: 'nearest'
76 418 });
77 419 }
78 420 };
79 421 $(document).ready(function () {