Dynamic Inline Quick View Grid

Google Pixel 8 pro
Original price was: $960.00.$580.00Current price is: $580.00.

Apple iPhone 13 pro
$75.00

SAMSUNG S24 Ultra
Price range: $250.00 through $1,350.00

Nothing Phone 2
Original price was: $960.00.$580.00Current price is: $580.00.

LG Velvet
$10.99

OnePlus 12R
$679.00

Sony Xperia 1 V 5G
Original price was: $50.00.$25.00Current price is: $25.00.

Huawei P30 Pro
Original price was: $960.00.$580.00Current price is: $580.00.

Xiaomi Redmi Note 13
Original price was: $960.00.$580.00Current price is: $580.00.
/* active style */
.quick-view-grid--active .dwc-quickview-grid__card.active{
border: solid var(--active-border-color) var(--active-border-width);
border-bottom: var(--active-border-bottom);
box-shadow: var(--active-box-shadow);
background-color: var(--active-bg);
}
.dwc-quickview-grid__card{
border: solid var(--qv-card-border-color) var(--qv-card-border-width);
background-color: var(--qv-card-bg)
}
.dwc-quick-view__inner-wrap--grid {
height: 100%;
/* max-height: 95dvb;
overflow-y: scroll;*/
overflow-x: hidden;
overscroll-behavior: contain;
background-color: var(--fetch-bg)
}
/* caret */
.dwc-quickview-grid__card .caret {
inline-size: 0;
block-size: 0;
border-inline-start: var(--qv-caret-size) solid transparent;
border-inline-end: var(--qv-caret-size) solid transparent;
border-block-start: var(--qv-caret-size) solid var(--qv-caret-color);
position: absolute;
inset-block-end: calc(var(--qv-caret-size) * -0.8);
inset-inline-start: 50%;
transform: translateX(-50%);
}
/*quick view*/
.dwc-expanded-content{
border-inline: solid var(--active-border-color) var(--active-border-width)
}
/*SCROLLBARS*/
.dwc-quick-view__inner-wrap::-webkit-scrollbar-track {
border-radius: 5px;
background-color: transparent;
}
.dwc-quick-view__inner-wrap::-webkit-scrollbar {
width: 5px;
background-color: transparent;
}
.dwc-quick-view__inner-wrap::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: gray;
}
/* SCROLLBARS END */
/**
* Default configuration (internal settings)
* This contains all the technical configuration that rarely changes
*/
const quickViewConfig = {
// Selectors
selectors: {
card: '.dwc-quickview-grid__card',
quickViewBtn: '.dwc-quickview-grid-btn',
expandedContainer: '.dwc-expanded-container',
expandedContent: '.dwc-expanded-content',
closeBtn: '.dwc-quickview-grid__close-btn',
nextBtn: '.dwc-quickview-grid__next-button',
prevBtn: '.dwc-quickview-grid__prev-button',
nextPrevContainer: '.dwc-quickview-grid__next-prev-buttons',
miniCart: '.brxe-woocommerce-mini-cart',
cartDetail: '.cart-detail',
wooGallery: '.woocommerce-product-gallery',
wooGalleryImage: '.woocommerce-product-gallery__image img',
variationsForm: '.variations_form',
variationId: 'input.variation_id',
qtyInput: 'input.qty',
plusBtn: '.plus',
minusBtn: '.minus',
addToCartBtn: '.brx_ajax_add_to_cart, .single_add_to_cart_button',
wcTabs: '.wc-tabs, .tabs',
wcTabsWrapper: '.wc-tabs-wrapper, .woocommerce-tabs',
addedToCartLink: 'a.added_to_cart',
bricksStyleLinks: '[id^="bricks-post-"]',
bricksFrontendStyle: '#bricks-frontend-inline-inline-css',
brxQueryTrail: '.brx-query-trail'
},
// Timeouts
timeouts: {
focus: 500,
heightUpdate: 50,
scroll: 100,
resetOrder: 500,
wooInit: 100,
bricksInit: 150,
urlChange: 1500,
cartRevert: 3000,
resizeDebounce: 300,
styleInjection: 50
},
// CSS Classes
classes: {
active: 'active',
open: 'open',
inactive: 'inactive',
caret: 'caret',
quickViewActive: 'quick-view-grid--active',
macOS: 'dwc-macOS',
loading: 'loading',
showCartDetails: 'show-cart-details'
},
// CSS Custom Properties
cssVars: {
contentHeight: '--dwc-grid-content-height'
},
// Callbacks (optional, for extensibility)
callbacks: {
onOpen: null,
onClose: null,
onNavigate: null,
onAddToCart: null
}
};
// ============================================
// UTILITY FUNCTIONS
// ============================================
const Utils = {
/**
* Debounce function to limit execution rate
* @param {Function} func - Function to debounce
* @param {number} timeout - Delay in milliseconds
* @returns {Function} Debounced function
*/
debounce(func, timeout) {
let timer;
return function(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => func.apply(context, args), timeout);
};
},
/**
* Safely get element with null check
* @param {string} selector - CSS selector
* @param {Element} context - Context element (default: document)
* @returns {Element|null} Element or null
*/
getElementSafely(selector, context = document) {
try {
return context.querySelector(selector);
} catch (error) {
console.error(`Error selecting element: ${selector}`, error);
return null;
}
},
/**
* Safely get all elements with null check
* @param {string} selector - CSS selector
* @param {Element} context - Context element (default: document)
* @returns {NodeList} NodeList or empty array
*/
getAllElementsSafely(selector, context = document) {
try {
return context.querySelectorAll(selector) || [];
} catch (error) {
console.error(`Error selecting elements: ${selector}`, error);
return [];
}
},
/**
* Deep merge configuration objects
* @param {Object} defaults - Default configuration
* @param {Object} userConfig - User configuration
* @returns {Object} Merged configuration
*/
mergeConfig(defaults, userConfig) {
const result = { ...defaults };
for (const key in userConfig) {
if (userConfig.hasOwnProperty(key)) {
if (typeof userConfig[key] === 'object' && !Array.isArray(userConfig[key]) && userConfig[key] !== null) {
result[key] = this.mergeConfig(defaults[key] || {}, userConfig[key]);
} else {
result[key] = userConfig[key];
}
}
}
return result;
},
/**
* Check if running on macOS
* @returns {boolean} True if macOS
*/
isMacOS() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
}
};
// ============================================
// CONTENT MANAGER CLASS
// ============================================
/**
* Manages content fetching, caching, and replacement
* Uses Map for in-memory caching instead of localStorage
* @class ContentManager
*/
class ContentManager {
constructor(config) {
this.config = config;
this.cache = new Map(); // Replace localStorage with Map
this.tempDiv = document.createElement('div');
this.tempStyle = document.createElement('div');
this.currentQuickView = null;
this.isInitialFetchComplete = false;
}
/**
* Fetch initial content from source URL
* @returns {Promise<void>}
*/
fetchInitialContent() {
return new Promise((resolve, reject) => {
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', this.config.sourceUrl);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
this._processInitialResponse(xhr.responseText);
resolve();
} else {
reject(new Error(`XHR failed with status: ${xhr.status}`));
}
}
};
xhr.onerror = () => reject(new Error('XHR request failed'));
xhr.send();
} catch (error) {
console.error('Error fetching initial content:', error);
reject(error);
}
});
}
/**
* Process initial XHR response
* @param {string} responseText - Response HTML
* @private
*/
_processInitialResponse(responseText) {
try {
this.tempStyle.innerHTML = responseText;
// Inject styles
this._injectBricksStyles();
// Parse and sanitize
const sanitizedContent = this._sanitizeContent(responseText);
// Cache in Map instead of localStorage
this.cache.set(this.config.sourceUrl, sanitizedContent);
this.isInitialFetchComplete = true;
} catch (error) {
console.error('Error processing initial response:', error);
}
}
/**
* Inject Bricks styles into document head
* @private
*/
_injectBricksStyles() {
try {
// Import frontend style links
const bricksStyleLinks = this.tempStyle.querySelectorAll(this.config.selectors.bricksStyleLinks);
const tenthHeadElement = document.head.children[9];
if (tenthHeadElement) {
bricksStyleLinks.forEach(link => {
document.head.insertBefore(link.cloneNode(true), tenthHeadElement.nextSibling);
});
}
// Import frontend stylesheet
const quickViewStyles = this.tempStyle.querySelector(this.config.selectors.bricksFrontendStyle);
if (quickViewStyles) {
quickViewStyles.id += '-2';
document.head.appendChild(quickViewStyles);
}
} catch (error) {
console.error('Error injecting Bricks styles:', error);
}
}
/**
* Sanitize HTML content
* @param {string} html - Raw HTML
* @returns {string} Sanitized HTML
* @private
*/
_sanitizeContent(html) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Remove unwanted elements
const brxQueryTrailDiv = doc.querySelector(this.config.selectors.brxQueryTrail);
if (brxQueryTrailDiv && brxQueryTrailDiv.parentNode) {
brxQueryTrailDiv.parentNode.removeChild(brxQueryTrailDiv);
}
return doc.documentElement.innerHTML;
} catch (error) {
console.error('Error sanitizing content:', error);
return html;
}
}
/**
* Get cached content from Map
* @param {string} key - Cache key
* @returns {string|null} Cached content
*/
getCachedContent(key) {
return this.cache.get(key) || null;
}
/**
* Replace content in expanded container
* @param {string} dataId - Data ID of content to display
* @param {Element} clickedButton - Button that was clicked
* @returns {boolean} Success status
*/
replaceContent(dataId, clickedButton) {
try {
const cachedContent = this.getCachedContent(this.config.sourceUrl);
if (!cachedContent) {
console.error('No cached content available');
return false;
}
this.tempDiv.innerHTML = cachedContent;
// Find content with matching data-id
const fetchedQuickView = this.tempDiv.querySelector(`[data-id="${dataId}"]`);
if (!fetchedQuickView) {
console.error(`Content with data-id="${dataId}" not found`);
return false;
}
// Make focusable
fetchedQuickView.setAttribute('tabindex', '0');
// Get container
const container = Utils.getElementSafely(this.config.selectors.expandedContainer);
if (!container) {
console.error('Expanded container not found');
return false;
}
// Remove previous content
if (this.currentQuickView && this.currentQuickView.parentNode) {
container.removeChild(this.currentQuickView);
}
// Append new content
container.appendChild(fetchedQuickView);
this.currentQuickView = fetchedQuickView;
// Add active class to body
if (!document.body.classList.contains(this.config.classes.quickViewActive)) {
document.body.classList.add(this.config.classes.quickViewActive);
}
return true;
} catch (error) {
console.error('Error replacing content:', error);
return false;
}
}
/**
* Get current quick view element
* @returns {Element|null}
*/
getCurrentQuickView() {
return this.currentQuickView;
}
/**
* Clear cache
*/
clearCache() {
this.cache.clear();
}
/**
* Cleanup
*/
destroy() {
this.clearCache();
this.currentQuickView = null;
this.tempDiv = null;
this.tempStyle = null;
}
}
// ============================================
// NAVIGATION CONTROLLER CLASS
// ============================================
/**
* Manages navigation (next/prev), button states, keyboard controls
* @class NavigationController
*/
class NavigationController {
constructor(config, contentManager, gridController) {
this.config = config;
this.contentManager = contentManager;
this.gridController = gridController;
// Cache DOM elements
this.nextButton = null;
this.prevButton = null;
this.expandedContainer = null;
this.quickViewButtons = [];
// Store bound handlers for cleanup
this.handlers = {
next: this._handleNext.bind(this),
prev: this._handlePrev.bind(this),
keyboard: this._handleKeyboard.bind(this),
escape: this._handleEscape.bind(this)
};
}
/**
* Initialize navigation
*/
init() {
try {
// Cache elements
this.nextButton = Utils.getElementSafely(this.config.selectors.nextBtn);
this.prevButton = Utils.getElementSafely(this.config.selectors.prevBtn);
this.expandedContainer = Utils.getElementSafely(this.config.selectors.expandedContainer);
this.quickViewButtons = Array.from(Utils.getAllElementsSafely(this.config.selectors.quickViewBtn));
if (!this.nextButton || !this.prevButton || !this.expandedContainer) {
console.warn('Navigation buttons or container not found');
return;
}
// Attach event listeners
this.nextButton.addEventListener('click', this.handlers.next);
this.prevButton.addEventListener('click', this.handlers.prev);
this.expandedContainer.addEventListener('keydown', this.handlers.keyboard);
document.addEventListener('keydown', this.handlers.escape);
} catch (error) {
console.error('Error initializing navigation:', error);
}
}
/**
* Handle next button click
* @private
*/
_handleNext() {
this._navigateQuickView('next');
}
/**
* Handle previous button click
* @private
*/
_handlePrev() {
this._navigateQuickView('prev');
}
/**
* Handle keyboard navigation (arrow keys)
* @param {KeyboardEvent} event
* @private
*/
_handleKeyboard(event) {
if (event.key === 'ArrowLeft' && this.prevButton) {
this.prevButton.click();
} else if (event.key === 'ArrowRight' && this.nextButton) {
this.nextButton.click();
}
}
/**
* Handle escape key
* @param {KeyboardEvent} event
* @private
*/
_handleEscape(event) {
if (event.key === 'Escape') {
this._closeQuickView(event);
}
}
/**
* Navigate to next or previous quick view
* @param {string} direction - 'next' or 'prev'
* @private
*/
_navigateQuickView(direction) {
try {
const expandedContent = Utils.getElementSafely(this.config.selectors.expandedContent);
if (!expandedContent) return;
const currentDataId = expandedContent.dataset.id;
const currentIndex = this.quickViewButtons.findIndex(
btn => btn.getAttribute('data-id') === currentDataId
);
if (currentIndex === -1) return;
// Determine target index
let targetIndex;
if (direction === 'next') {
targetIndex = currentIndex + 1;
if (targetIndex >= this.quickViewButtons.length) return;
} else if (direction === 'prev') {
targetIndex = currentIndex - 1;
if (targetIndex < 0) return;
}
// Update button states BEFORE navigation
this.updateButtonStates(targetIndex, this.quickViewButtons.length);
// Navigate
this.quickViewButtons[targetIndex].click();
// Call callback if provided
if (this.config.callbacks.onNavigate) {
this.config.callbacks.onNavigate(targetIndex, direction);
}
} catch (error) {
console.error('Error navigating quick view:', error);
}
}
/**
* Update button states based on current position
* DRY solution - single reusable method for all button state logic
* @param {number} currentIndex - Current item index
* @param {number} totalItems - Total number of items
*/
updateButtonStates(currentIndex, totalItems) {
if (!this.nextButton || !this.prevButton) return;
const isFirstItem = currentIndex <= 0;
const isLastItem = currentIndex >= totalItems - 1;
if (isLastItem) {
this.nextButton.classList.add(this.config.classes.inactive);
this.prevButton.classList.remove(this.config.classes.inactive);
} else if (isFirstItem) {
this.prevButton.classList.add(this.config.classes.inactive);
this.nextButton.classList.remove(this.config.classes.inactive);
} else {
this.nextButton.classList.remove(this.config.classes.inactive);
this.prevButton.classList.remove(this.config.classes.inactive);
}
}
/**
* Get current quick view index
* @returns {number} Current index or -1 if not found
*/
getCurrentIndex() {
try {
const expandedContent = Utils.getElementSafely(this.config.selectors.expandedContent);
if (!expandedContent) return -1;
const currentDataId = expandedContent.dataset.id;
return this.quickViewButtons.findIndex(
btn => btn.getAttribute('data-id') === currentDataId
);
} catch (error) {
console.error('Error getting current index:', error);
return -1;
}
}
/**
* Focus target button
* @param {Event} event
*/
focusTargetButton(event) {
try {
const currentDiv = Utils.getElementSafely(this.config.selectors.expandedContent);
if (!currentDiv) return;
const currentId = currentDiv.getAttribute('data-id');
const targetBtn = document.querySelector(`[data-id='${currentId}']`);
if (targetBtn && event.relatedTarget !== targetBtn) {
targetBtn.focus();
}
} catch (error) {
console.error('Error focusing target button:', error);
}
}
/**
* Close quick view and remove active class
* @param {Event} event
* @private
*/
_closeQuickView(event) {
try {
// Reset button states
if (this.prevButton) this.prevButton.classList.remove(this.config.classes.inactive);
if (this.nextButton) this.nextButton.classList.remove(this.config.classes.inactive);
// Remove active class from body
if (document.body.classList.contains(this.config.classes.quickViewActive)) {
document.body.classList.remove(this.config.classes.quickViewActive);
// Return focus
const currentDiv = Utils.getElementSafely(this.config.selectors.expandedContent);
if (currentDiv) {
const currentId = currentDiv.getAttribute('data-id');
const targetBtn = document.querySelector(`[data-id='${currentId}']`);
const closeButton = Utils.getElementSafely(this.config.selectors.closeBtn);
if (targetBtn && event.relatedTarget !== targetBtn) {
targetBtn.focus();
} else if (closeButton && event.relatedTarget !== closeButton) {
closeButton.focus();
}
}
}
// Close container via grid controller
if (this.gridController) {
this.gridController.closeContainer();
}
} catch (error) {
console.error('Error closing quick view:', error);
}
}
/**
* Refresh button references (called after URL changes)
*/
refreshButtons() {
this.quickViewButtons = Array.from(Utils.getAllElementsSafely(this.config.selectors.quickViewBtn));
}
/**
* Cleanup
*/
destroy() {
try {
if (this.nextButton) this.nextButton.removeEventListener('click', this.handlers.next);
if (this.prevButton) this.prevButton.removeEventListener('click', this.handlers.prev);
if (this.expandedContainer) this.expandedContainer.removeEventListener('keydown', this.handlers.keyboard);
document.removeEventListener('keydown', this.handlers.escape);
this.nextButton = null;
this.prevButton = null;
this.expandedContainer = null;
this.quickViewButtons = [];
} catch (error) {
console.error('Error destroying navigation controller:', error);
}
}
}
// ============================================
// GRID CONTROLLER CLASS
// ============================================
/**
* Manages grid cards, container open/close, and layout
* @class GridController
*/
class GridController {
constructor(config) {
this.config = config;
this.cards = [];
this.expandedContainer = null;
this.resizeHandler = null;
this.resetOrderTimeout = null;
}
/**
* Initialize grid
*/
init() {
try {
// Setup cards
this.cards = Array.from(Utils.getAllElementsSafely(this.config.selectors.card));
this.cards.forEach((card, index) => {
card.id = `card${index}`;
card.style.order = index;
});
// Setup expanded container
this.expandedContainer = Utils.getElementSafely(this.config.selectors.expandedContainer);
if (this.expandedContainer) {
this.expandedContainer.inert = true;
this.expandedContainer.setAttribute('aria-hidden', 'true');
this.expandedContainer.style.order = this.cards.length;
}
// Setup next/prev buttons container position
this._setupNavButtonsPosition();
// Setup resize handler
this.resizeHandler = Utils.debounce(() => {
if (this.expandedContainer && this.expandedContainer.classList.contains(this.config.classes.open)) {
this.updateContentHeight();
}
}, this.config.timeouts.resizeDebounce);
window.addEventListener('resize', this.resizeHandler);
} catch (error) {
console.error('Error initializing grid:', error);
}
}
/**
* Setup navigation buttons position
* @private
*/
_setupNavButtonsPosition() {
const nextPrevContainer = Utils.getElementSafely(this.config.selectors.nextPrevContainer);
if (nextPrevContainer) {
nextPrevContainer.classList.add(this.config.navArrowsPosition);
}
}
/**
* Show container for clicked card
* @param {Element} clickedCard
*/
showContainer(clickedCard) {
try {
if (!this.expandedContainer || !clickedCard) return;
// Update active card and caret
this.updateActiveCard(clickedCard);
// Get clicked card ID
const clickedCardId = parseInt(clickedCard.id.replace('card', ''), 10);
// Toggle or open container
if (this.expandedContainer.style.order == `${clickedCardId}`) {
this.expandedContainer.classList.toggle(this.config.classes.open);
} else {
this.expandedContainer.classList.add(this.config.classes.open);
this.expandedContainer.style.order = `${clickedCardId}`;
}
// Update aria and inert
const isOpen = this.expandedContainer.classList.contains(this.config.classes.open);
if (isOpen) {
this.expandedContainer.inert = false;
this.expandedContainer.setAttribute('aria-hidden', 'false');
this.expandedContainer.focus({ preventScroll: true });
this.updateContentHeight();
// Scroll into view
setTimeout(() => {
const scrollY = this.expandedContainer.getBoundingClientRect().top +
window.pageYOffset - this.config.scrollOffset;
window.scrollTo({
top: scrollY,
behavior: 'smooth'
});
}, this.config.timeouts.scroll);
// Call callback
if (this.config.callbacks.onOpen) {
this.config.callbacks.onOpen(clickedCard);
}
} else {
this.expandedContainer.inert = true;
this.expandedContainer.setAttribute('aria-hidden', 'true');
document.body.style.setProperty(this.config.cssVars.contentHeight, '0');
}
this.resetOrder();
} catch (error) {
console.error('Error showing container:', error);
}
}
/**
* Close container
*/
closeContainer() {
try {
if (!this.expandedContainer) return;
this.removeActiveClass();
this.resetOrder();
this.expandedContainer.inert = true;
this.expandedContainer.classList.remove(this.config.classes.open);
this.expandedContainer.setAttribute('aria-hidden', 'true');
document.body.style.setProperty(this.config.cssVars.contentHeight, '0');
// Call callback
if (this.config.callbacks.onClose) {
this.config.callbacks.onClose();
}
} catch (error) {
console.error('Error closing container:', error);
}
}
/**
* Update active card and add caret
* @param {Element} clickedCard
*/
updateActiveCard(clickedCard) {
try {
const isActive = clickedCard.classList.contains(this.config.classes.active);
// Remove active class from all cards
this.removeActiveClass();
// Add active class and caret if not already active
if (!isActive) {
clickedCard.classList.add(this.config.classes.active);
const caret = document.createElement('div');
caret.classList.add(this.config.classes.caret);
clickedCard.style.position = 'relative';
clickedCard.appendChild(caret);
}
} catch (error) {
console.error('Error updating active card:', error);
}
}
/**
* Remove active class and caret from all cards
*/
removeActiveClass() {
try {
this.cards.forEach(card => {
card.classList.remove(this.config.classes.active);
const existingCaret = card.querySelector(`.${this.config.classes.caret}`);
if (existingCaret) {
existingCaret.remove();
}
});
} catch (error) {
console.error('Error removing active class:', error);
}
}
/**
* Reset container order when all cards are inactive
*/
resetOrder() {
// Clear any existing timeout
if (this.resetOrderTimeout) {
clearTimeout(this.resetOrderTimeout);
}
this.resetOrderTimeout = setTimeout(() => {
try {
const noActiveCard = !this.cards.some(card =>
card.classList.contains(this.config.classes.active)
);
if (noActiveCard && this.expandedContainer) {
this.expandedContainer.style.order = this.cards.length;
document.body.classList.remove(this.config.classes.quickViewActive);
}
} catch (error) {
console.error('Error resetting order:', error);
}
}, this.config.timeouts.resetOrder);
}
/**
* Update content height CSS variable
*/
updateContentHeight() {
setTimeout(() => {
try {
if (this.expandedContainer) {
const contentHeight = this.expandedContainer.scrollHeight;
document.body.style.setProperty(
this.config.cssVars.contentHeight,
`${contentHeight}px`
);
}
} catch (error) {
console.error('Error updating content height:', error);
}
}, this.config.timeouts.heightUpdate);
}
/**
* Cleanup
*/
destroy() {
try {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
if (this.resetOrderTimeout) {
clearTimeout(this.resetOrderTimeout);
}
this.cards = [];
this.expandedContainer = null;
} catch (error) {
console.error('Error destroying grid controller:', error);
}
}
}
// ============================================
// WOOCOMMERCE INTEGRATION CLASS
// ============================================
/**
* Handles all WooCommerce functionality using jQuery
* @class WooCommerceIntegration
*/
class WooCommerceIntegration {
constructor(config, gridController) {
this.config = config;
this.gridController = gridController;
this.initialized = false;
}
/**
* Initialize WooCommerce components
* @param {number} timeout - Initialization delay
*/
init(timeout = 100) {
if (!this.config.initializeWooScripts) return;
setTimeout(() => {
try {
this.setupQuantityControls();
this.setupAddToCart();
this.setupVariationChange();
this.initializeProductTabs();
this.initializeVariationForms();
this.initializeProductGallery();
jQuery(document.body).trigger('wc_fragment_refresh');
this.initialized = true;
} catch (error) {
console.error('Error initializing WooCommerce:', error);
}
}, timeout);
}
/**
* Setup quantity increment/decrement controls
*/
setupQuantityControls() {
try {
jQuery(document).off('click', this.config.selectors.plusBtn)
.on('click', this.config.selectors.plusBtn, () => {
const $input = jQuery(this.config.selectors.expandedContainer + ' ' + this.config.selectors.qtyInput);
const currentVal = parseInt($input.val());
if (!isNaN(currentVal)) {
$input.val(currentVal + 1);
$input.attr('value', currentVal + 1);
}
});
jQuery(document).off('click', this.config.selectors.minusBtn)
.on('click', this.config.selectors.minusBtn, () => {
const $input = jQuery(this.config.selectors.expandedContainer + ' ' + this.config.selectors.qtyInput);
const currentVal = parseInt($input.val());
if (!isNaN(currentVal) && currentVal > 1) {
$input.val(currentVal - 1);
$input.attr('value', currentVal - 1);
}
});
jQuery(document).on('input', this.config.selectors.qtyInput, function() {
const $input = jQuery(this);
$input.attr('value', $input.val());
});
} catch (error) {
console.error('Error setting up quantity controls:', error);
}
}
/**
* Setup AJAX add to cart functionality
*/
setupAddToCart() {
try {
jQuery(document).off('click', this.config.selectors.addToCartBtn)
.on('click', this.config.selectors.addToCartBtn, (e) => {
e.preventDefault();
const $button = jQuery(e.currentTarget);
const $form = $button.closest(this.config.selectors.variationsForm);
const $span = $button.find('span');
const originalText = 'Add again';
const isVariable = $form.length > 0;
const quantity = isVariable ?
$form.find(this.config.selectors.qtyInput).val() || 1 :
$button.closest('.brx-loop-product-form').find(this.config.selectors.qtyInput).val() || 1;
const data = {
action: 'woocommerce_add_to_cart',
product_id: $button.data('product_id'),
quantity: quantity,
product_type: isVariable ? 'variable' : 'simple'
};
if (isVariable) {
const variation_id = $form.find(this.config.selectors.variationId).val();
if (!variation_id || variation_id === "0") {
return;
}
data.variation_id = variation_id;
jQuery.each($form.serializeArray(), (index, item) => {
if (item.name !== 'product_id') {
data[item.name] = item.value;
}
});
}
// Update button text
this._updateButtonText($button, $span, 'Adding...');
// AJAX request
jQuery.ajax({
type: 'POST',
url: wc_add_to_cart_params.ajax_url,
data: data,
success: (response) => {
this._handleAddToCartSuccess(response, $button, $span, originalText);
},
error: () => {
console.error('Failed to add to cart');
this._revertButtonText($button, $span, originalText);
}
});
});
} catch (error) {
console.error('Error setting up add to cart:', error);
}
}
/**
* Update button text during add to cart
* @private
*/
_updateButtonText($button, $span, text) {
$button.addClass(this.config.classes.loading);
if ($span.length) {
$span.text(text);
} else {
$button.text(text);
}
}
/**
* Handle successful add to cart
* @private
*/
_handleAddToCartSuccess(response, $button, $span, originalText) {
try {
if (response.fragments && response.cart_hash) {
jQuery(document.body).trigger('added_to_cart', [response.fragments, response.cart_hash, $button]);
this._updateButtonText($button, $span, 'Added');
$button.removeClass(this.config.classes.loading);
if (typeof bricksWooRefreshCartFragments === 'function') {
bricksWooRefreshCartFragments();
}
} else {
this._updateButtonText($button, $span, 'Added');
$button.removeClass(this.config.classes.loading);
if (typeof bricksWooRefreshCartFragments === 'function') {
bricksWooRefreshCartFragments();
}
}
setTimeout(() => {
this._revertButtonText($button, $span, originalText);
}, this.config.timeouts.cartRevert);
this._cartToggle();
// Call callback
if (this.config.callbacks.onAddToCart) {
this.config.callbacks.onAddToCart(response);
}
} catch (error) {
console.error('Error handling add to cart success:', error);
}
}
/**
* Revert button text after add to cart
* @private
*/
_revertButtonText($button, $span, originalText) {
if ($span.length) {
$span.remove();
$button.text(originalText);
} else {
$button.text(originalText);
}
this._appendViewCartLink($button);
}
/**
* Append view cart link
* @private
*/
_appendViewCartLink($button) {
try {
if (!$button.next(this.config.selectors.addedToCartLink).length) {
const currentSiteUrl = window.location.origin + '/cart/';
const viewCartLink = `<a href="${currentSiteUrl}" class="added_to_cart wc-forward" title="View cart">View cart</a>`;
$button.after(viewCartLink);
}
} catch (error) {
console.error('Error appending view cart link:', error);
}
}
/**
* Toggle mini cart display
* @private
*/
_cartToggle() {
try {
const miniCart = Utils.getElementSafely(this.config.selectors.miniCart);
if (miniCart) {
miniCart.classList.add(this.config.classes.showCartDetails);
const cartDetail = miniCart.querySelector(this.config.selectors.cartDetail);
if (cartDetail) {
cartDetail.classList.add(this.config.classes.active);
}
}
} catch (error) {
console.error('Error toggling cart:', error);
}
}
/**
* Setup variation change handlers
*/
setupVariationChange() {
try {
jQuery(document).on('woocommerce_variation_select_change', () => {
this._updateVariationImage();
});
jQuery(document).on('found_variation', (event, variation) => {
if (variation && variation.image && variation.image.src) {
const newImageSrc = variation.image.src;
const newImageSrcset = variation.image.srcset;
// Update main product image
const $mainImage = jQuery(this.config.selectors.wooGalleryImage);
$mainImage.attr('src', newImageSrc);
$mainImage.attr('srcset', newImageSrcset);
// Reinitialize gallery
if (typeof jQuery.fn.wc_product_gallery === 'function') {
jQuery(this.config.selectors.wooGallery).wc_product_gallery();
}
// Update container height
if (this.gridController) {
this.gridController.updateContentHeight();
}
}
});
} catch (error) {
console.error('Error setting up variation change:', error);
}
}
/**
* Update variation image
* @private
*/
_updateVariationImage() {
jQuery(this.config.selectors.variationsForm).each(function() {
const $form = jQuery(this);
if (typeof $form.wc_variation_form === 'function') {
$form.wc_variation_form().trigger('check_variations');
}
});
}
/**
* Initialize WooCommerce product tabs
*/
initializeProductTabs() {
try {
jQuery(this.config.selectors.wcTabs).trigger('init');
jQuery(this.config.selectors.wcTabsWrapper).on('init', function() {
const $activeTab = jQuery(this).find('ul.wc-tabs li.active');
if ($activeTab.length) {
jQuery('#' + $activeTab.find('a').attr('href').replace('#', '')).show();
}
}).trigger('init');
} catch (error) {
console.error('Error initializing product tabs:', error);
}
}
/**
* Initialize variation forms
*/
initializeVariationForms() {
try {
jQuery(this.config.selectors.variationsForm).each(function() {
if (typeof jQuery(this).wc_variation_form === 'function') {
jQuery(this).wc_variation_form().trigger('check_variations');
}
});
} catch (error) {
console.error('Error initializing variation forms:', error);
}
}
/**
* Initialize product gallery
*/
initializeProductGallery() {
try {
jQuery(this.config.selectors.wooGallery).each(function() {
if (typeof jQuery(this).wc_product_gallery === 'function') {
jQuery(this).wc_product_gallery();
}
});
} catch (error) {
console.error('Error initializing product gallery:', error);
}
}
/**
* Cleanup
*/
destroy() {
try {
// Remove jQuery event handlers
jQuery(document).off('click', this.config.selectors.plusBtn);
jQuery(document).off('click', this.config.selectors.minusBtn);
jQuery(document).off('click', this.config.selectors.addToCartBtn);
jQuery(document).off('input', this.config.selectors.qtyInput);
jQuery(document).off('woocommerce_variation_select_change');
jQuery(document).off('found_variation');
this.initialized = false;
} catch (error) {
console.error('Error destroying WooCommerce integration:', error);
}
}
}
// ============================================
// BRICKS INTEGRATION CLASS
// ============================================
/**
* Handles Bricks Builder element initialization
* @class BricksIntegration
*/
class BricksIntegration {
constructor(config) {
this.config = config;
this.initialized = false;
}
/**
* Initialize Bricks elements
* @param {number} timeout - Initialization delay
*/
init(timeout = 100) {
if (!this.config.initializeBricksScripts) return;
setTimeout(() => {
try {
this._initializeElements();
this.initialized = true;
} catch (error) {
console.error('Error initializing Bricks elements:', error);
}
}, timeout);
}
/**
* Initialize all Bricks elements
* @private
*/
_initializeElements() {
const bricksFunctions = [
'bricksAccordion',
'bricksTabs',
'bricksSplide',
'bricksSwiper',
'bricksVideo',
'bricksToggle',
'bricksWooProductGallery',
'bricksWooRefreshCartFragments'
];
bricksFunctions.forEach(funcName => {
try {
if (typeof window[funcName] === 'function') {
window[funcName]();
}
} catch (error) {
console.error(`Error calling ${funcName}:`, error);
}
});
}
/**
* Cleanup
*/
destroy() {
this.initialized = false;
}
}
// ============================================
// MAIN ORCHESTRATOR CLASS
// ============================================
/**
* Main QuickView Grid orchestrator class
* Coordinates all subcomponents and manages lifecycle
* @class QuickViewGrid
*/
class QuickViewGrid {
constructor(userConfig = {}) {
// Merge user config with defaults
this.config = Utils.mergeConfig(quickViewConfig, userConfig);
// Initialize components
this.contentManager = null;
this.navigationController = null;
this.gridController = null;
this.wooCommerceIntegration = null;
this.bricksIntegration = null;
// State
this.isInitialized = false;
this.quickViewButtons = [];
// Bound handlers for cleanup
this.handlers = {
quickViewClick: this._handleQuickViewClick.bind(this),
closeClick: this._handleCloseClick.bind(this),
closeFocusOut: this._handleCloseFocusOut.bind(this),
urlChange: this._handleUrlChange.bind(this),
resize: null // Will be set with debounced function
};
// URL change tracking
this._setupHistoryTracking();
}
/**
* Initialize QuickView Grid
* @returns {Promise<void>}
*/
async init() {
if (this.isInitialized) {
console.warn('QuickView Grid already initialized');
return;
}
try {
// Detect and add macOS class
if (Utils.isMacOS()) {
document.body.classList.add(this.config.classes.macOS);
}
// Initialize content manager and fetch initial content
this.contentManager = new ContentManager(this.config);
await this.contentManager.fetchInitialContent();
// Initialize grid controller
this.gridController = new GridController(this.config);
this.gridController.init();
// Initialize navigation controller
this.navigationController = new NavigationController(
this.config,
this.contentManager,
this.gridController
);
this.navigationController.init();
// Initialize WooCommerce integration
this.wooCommerceIntegration = new WooCommerceIntegration(
this.config,
this.gridController
);
this.wooCommerceIntegration.init(this.config.timeouts.wooInit);
// Initialize Bricks integration
this.bricksIntegration = new BricksIntegration(this.config);
this.bricksIntegration.init(this.config.timeouts.bricksInit);
// Setup quick view button click handlers
this._setupQuickViewButtons();
// Setup resize handler with debounce
this.handlers.resize = Utils.debounce(() => {
this.wooCommerceIntegration.init(this.config.timeouts.wooInit);
this.bricksIntegration.init(this.config.timeouts.bricksInit);
}, this.config.timeouts.resizeDebounce);
window.addEventListener('resize', this.handlers.resize);
// Setup URL change listeners
this._setupUrlChangeListeners();
this.isInitialized = true;
console.log('QuickView Grid initialized successfully');
} catch (error) {
console.error('Error initializing QuickView Grid:', error);
throw error;
}
}
/**
* Setup quick view button click handlers
* @private
*/
_setupQuickViewButtons() {
this.quickViewButtons = Array.from(
Utils.getAllElementsSafely(this.config.selectors.quickViewBtn)
);
this.quickViewButtons.forEach((btn, index) => {
// Store index on button for easy reference
btn.dataset.index = index;
// Remove existing listener if any (prevent duplicates)
btn.removeEventListener('click', this.handlers.quickViewClick);
// Add click listener
btn.addEventListener('click', this.handlers.quickViewClick);
});
}
/**
* Handle quick view button click
* @param {Event} event
* @private
*/
_handleQuickViewClick(event) {
try {
const btn = event.currentTarget;
const dataId = btn.getAttribute('data-id');
const index = parseInt(btn.dataset.index);
// Replace content
const success = this.contentManager.replaceContent(dataId, btn);
if (!success) return;
// Show container
const card = btn.closest(this.config.selectors.card);
if (card) {
this.gridController.showContainer(card);
}
// Reinitialize WooCommerce and Bricks
this.wooCommerceIntegration.init(this.config.timeouts.wooInit);
this.bricksIntegration.init(this.config.timeouts.bricksInit);
// Focus on quick view
setTimeout(() => {
const quickView = this.contentManager.getCurrentQuickView();
if (quickView) {
quickView.focus({ preventScroll: true });
}
}, this.config.timeouts.focus);
// Setup close button
this._setupCloseButton();
// Update navigation button states
this.navigationController.updateButtonStates(
index,
this.quickViewButtons.length
);
} catch (error) {
console.error('Error handling quick view click:', error);
}
}
/**
* Setup close button handlers
* @private
*/
_setupCloseButton() {
const closeButton = Utils.getElementSafely(this.config.selectors.closeBtn);
if (!closeButton) return;
// Remove existing listeners
closeButton.removeEventListener('click', this.handlers.closeClick);
closeButton.removeEventListener('focusout', this.handlers.closeFocusOut);
// Add new listeners
closeButton.addEventListener('click', this.handlers.closeClick);
closeButton.addEventListener('focusout', this.handlers.closeFocusOut);
}
/**
* Handle close button click
* @param {Event} event
* @private
*/
_handleCloseClick(event) {
this.gridController.closeContainer();
this.navigationController._closeQuickView(event);
}
/**
* Handle close button focus out
* @param {Event} event
* @private
*/
_handleCloseFocusOut(event) {
this.navigationController.focusTargetButton(event);
}
/**
* Setup history tracking for URL changes
* @private
*/
_setupHistoryTracking() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...args) => {
if (typeof history.onpushstate === "function") {
history.onpushstate({ state: args[0] });
}
const result = originalPushState.apply(history, args);
this._handleUrlChange();
return result;
};
history.replaceState = (...args) => {
if (typeof history.onreplacestate === "function") {
history.onreplacestate({ state: args[0] });
}
const result = originalReplaceState.apply(history, args);
this._handleUrlChange();
return result;
};
}
/**
* Setup URL change listeners
* @private
*/
_setupUrlChangeListeners() {
window.addEventListener('popstate', this.handlers.urlChange);
window.addEventListener('hashchange', this.handlers.urlChange);
}
/**
* Handle URL change - refresh buttons without adding duplicate listeners
* @private
*/
_handleUrlChange() {
setTimeout(() => {
try {
// Refresh quick view buttons
this._setupQuickViewButtons();
// Refresh navigation controller button references
this.navigationController.refreshButtons();
} catch (error) {
console.error('Error handling URL change:', error);
}
}, this.config.timeouts.urlChange);
}
/**
* Get current configuration
* @returns {Object} Current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update configuration
* @param {Object} newConfig - New configuration values
*/
updateConfig(newConfig) {
this.config = Utils.mergeConfig(this.config, newConfig);
console.log('Configuration updated');
}
/**
* Destroy and cleanup QuickView Grid
*/
destroy() {
try {
// Remove quick view button listeners
this.quickViewButtons.forEach(btn => {
btn.removeEventListener('click', this.handlers.quickViewClick);
});
// Remove URL change listeners
window.removeEventListener('popstate', this.handlers.urlChange);
window.removeEventListener('hashchange', this.handlers.urlChange);
window.removeEventListener('resize', this.handlers.resize);
// Destroy components
if (this.contentManager) this.contentManager.destroy();
if (this.navigationController) this.navigationController.destroy();
if (this.gridController) this.gridController.destroy();
if (this.wooCommerceIntegration) this.wooCommerceIntegration.destroy();
if (this.bricksIntegration) this.bricksIntegration.destroy();
// Clear references
this.contentManager = null;
this.navigationController = null;
this.gridController = null;
this.wooCommerceIntegration = null;
this.bricksIntegration = null;
this.quickViewButtons = [];
this.isInitialized = false;
console.log('QuickView Grid destroyed');
} catch (error) {
console.error('Error destroying QuickView Grid:', error);
}
}
}
// ============================================
// INITIALIZATION
// ============================================
document.addEventListener('DOMContentLoaded', () => {
try {
// Map external variables to user settings
// These variables should be declared elsewhere in your WordPress theme
if (typeof url !== 'undefined') {
quickViewUserSettings.sourceUrl = url;
}
if (typeof scrollOffset !== 'undefined') {
quickViewUserSettings.scrollOffset = scrollOffset;
}
if (typeof navArrowsPosition !== 'undefined') {
quickViewUserSettings.navArrowsPosition = navArrowsPosition;
}
if (typeof initializeWooScripts !== 'undefined') {
quickViewUserSettings.initializeWooScripts = Boolean(initializeWooScripts);
}
if (typeof initializeBricksScripts !== 'undefined') {
quickViewUserSettings.initializeBricksScripts = Boolean(initializeBricksScripts);
}
// Merge user settings with default config
const finalConfig = Utils.mergeConfig(quickViewConfig, quickViewUserSettings);
// Initialize QuickView Grid
const quickViewGrid = new QuickViewGrid(finalConfig);
quickViewGrid.init().catch(error => {
console.error('Failed to initialize QuickView Grid:', error);
});
// Expose to window for debugging and external access
window.quickViewGrid = quickViewGrid;
} catch (error) {
console.error('Error in QuickView Grid initialization:', error);
}
});