PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.1
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.1
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
merchant / assets / js / modules / quick-view / quick-view.js

quick-view.js in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.1, at assets/js/modules/quick-view/quick-view.js

424 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 'use strict';
2
3 var merchant = merchant || {};
4 merchant.modules = merchant.modules || {};
5 (function ($) {
6 merchant.modules.quickView = {
7 init: function init() {
8 var self = this;
9 var $modals = $('.merchant-quick-view-modal');
10 if (!$modals.length) {
11 return;
12 }
13 $modals.each(function () {
14 var $modal = $(this);
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');
18 var $inner = $modal.find('.merchant-quick-view-inner');
19 var $content = $modal.find('.merchant-quick-view-content');
20 var $overlay = $modal.find('.merchant-quick-view-overlay');
21 var isOpen = false;
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) {
36 $content.empty();
37 $modal.addClass('merchant-show');
38 $modal.addClass('merchant-loading');
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 }
51 $.post(window.merchant.setting.ajax_url, {
52 action: 'merchant_quick_view_content',
53 nonce: window.merchant.setting.nonce,
54 product_id: productId
55 }, function (response) {
56 if (response.success && isOpen) {
57 $content.html(response.data);
58 $inner.addClass('merchant-show');
59 $modal.removeClass('merchant-loading');
60 updateNavState(productId);
61 var $gallery = $content.find('.woocommerce-product-gallery');
62 wc_single_product_params.zoom_enabled = window.merchant.setting.quick_view_zoom;
63 if (typeof $.fn.wc_product_gallery === 'function' && $gallery.length) {
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';
68 $gallery.wc_product_gallery(wc_single_product_params);
69 $gallery.trigger('wc-product-gallery-after-init', [$gallery.get(0), wc_single_product_params]);
70 }
71 var $variations = $content.find('.variations_form');
72 if (typeof $.fn.wc_variation_form === 'function' && $variations.length) {
73 $variations.each(function () {
74 $(this).wc_variation_form();
75 });
76 }
77 if (window.botiga && window.botiga.productSwatch) {
78 window.botiga.productSwatch.init();
79 }
80 if (window.botiga && window.botiga.qtyButton) {
81 window.botiga.qtyButton.init('quick-view');
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 }
88 } else {
89 $content.html(response.data);
90 $inner.addClass('merchant-show');
91 $modal.removeClass('merchant-loading');
92 $navPrev.removeClass('merchant-quick-view-nav-visible');
93 $navNext.removeClass('merchant-quick-view-nav-visible');
94 }
95 window.dispatchEvent(new Event('merchant.quickview.ajax.loaded'));
96 }).fail(function (xhr, textStatus) {
97 $content.html(textStatus);
98 $inner.addClass('merchant-show');
99 $modal.removeClass('merchant-loading');
100 $navPrev.removeClass('merchant-quick-view-nav-visible');
101 $navNext.removeClass('merchant-quick-view-nav-visible');
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');
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 });
123 $overlay.on('click', function (e) {
124 e.preventDefault();
125 $closeButton.trigger('click');
126 });
127 $(document).on('keydown', function (e) {
128 if ('Escape' === e.key && isOpen) {
129 $closeButton.trigger('click');
130 }
131 });
132 $closeButton.on('click', function (e) {
133 e.preventDefault();
134 isOpen = false;
135 $modal.removeClass('merchant-show');
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 }
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'
418 });
419 }
420 };
421 $(document).ready(function () {
422 merchant.modules.quickView.init();
423 });
424 })(jQuery);