PageSourceSearch

https://montyroberts.com/wp-content/plugins/power-coupons/public/assets/js/checkout-drawer.js?ver=1.0.7

js montyroberts.com collected 2026-10-02 06:46:17 UTC 15,707 bytes, 611 lines download raw bytes

1/**
2 * Power Coupons - Checkout Drawer JavaScript
3 *
4 * @param {Object} $ jQuery object
5 * @package
6 * @since 1.0.0
7 */
8( function ( $ ) {
9	'use strict';
10
11	const PowerCouponsDrawer = {
12		renderedTriggerButton: false,
13		eventsBound: false,
14		lastFocusedElement: null,
15		focusableElements: null,
16		firstFocusableElement: null,
17		lastFocusableElement: null,
18
19		/**
20		 * Sync every cart-dependent UI after the server-side cart changed.
21		 *
22		 * Delegates to the shared cart-refresh helper so the drawer never
23		 * reloads the page: a reload resets the in-page state that multi-step
24		 * checkout plugins such as CartFlows and FunnelKit hold in the
25		 * browser. Reloading stays available as an opt-in via the
26		 * `power_coupons_reload_page_after_coupon_is_applied` filter.
27		 *
28		 * @since 1.0.6
29		 * @return {Promise} Resolves once the cart UI is in sync.
30		 */
31		syncCartUi() {
32			if ( powerCouponsData.reloadPageAfterCouponApplied ) {
33				window.location.reload();
34
35				// Never resolves: the document is being torn down.
36				return new Promise( function () {} );
37			}
38
39			if ( ! window.PowerCouponsCartRefresh ) {
40				return Promise.resolve( false );
41			}
42
43			return window.PowerCouponsCartRefresh.refresh( {
44				source: 'checkout-drawer',
45			} );
46		},
47
48		/**
49		 * Initialize
50		 */
51		init() {
52			this.drawer = $( '#power-coupons-drawer' );
53			this.triggerBtn = $( '#power-coupons-view-coupons-btn' );
54			this.closeBtn = $( '.power-coupons-drawer-close' );
55			this.overlay = $( '.power-coupons-drawer-overlay' );
56			this.loadingEl = $( '.power-coupons-drawer-loading' );
57			this.couponsListEl = $( '.power-coupons-drawer-coupons-list' );
58			this.isOpen = false;
59			this.couponsLoaded = false;
60
61			if ( this.drawer.length && powerCouponsDrawer.displayMode ) {
62				this.drawer.attr(
63					'data-display-mode',
64					powerCouponsDrawer.displayMode
65				);
66			}
67
68			this.bindEvents();
69
70			// Check if WooCommerce Blocks API is available
71			if (
72				window &&
73				window.wc &&
74				window.wc.blocksCheckout &&
75				this.triggerBtn.length === 0 &&
76				typeof window.wc.blocksCheckout.registerCheckoutFilters ===
77					'function'
78			) {
79				// Register filters for blocks checkout
80				window.wc.blocksCheckout.registerCheckoutFilters(
81					'powerCouponsInjectDrawerButton',
82					{
83						itemName( defaultValue ) {
84							if (
85								true ===
86								PowerCouponsDrawer.renderedTriggerButton
87							) {
88								return defaultValue;
89							}
90
91							const wcPageType =
92								null !==
93								document.querySelector(
94									'.wp-block-woocommerce-cart'
95								)
96									? 'cart'
97									: 'checkout';
98
99							if (
100								'' === powerCouponsDrawer.showOnCart &&
101								'cart' === wcPageType
102							) {
103								return defaultValue;
104							}
105
106							if (
107								'' === powerCouponsDrawer.showOnCheckout &&
108								'checkout' === wcPageType
109							) {
110								return defaultValue;
111							}
112
113							const couponFormBlock = document.querySelector(
114								`.wp-block-woocommerce-${ wcPageType }-order-summary-coupon-form-block`
115							);
116
117							if ( ! couponFormBlock ) {
118								return defaultValue;
119							}
120
121							// Insert after BOGO notifications if present, otherwise after coupon form.
122							const bogoNotifications =
123								couponFormBlock.parentElement.querySelector(
124									'.power-coupons-bogo-notifications'
125								);
126							const targetElement =
127								bogoNotifications || couponFormBlock;
128
129							const tempDiv = document.createElement( 'div' );
130							tempDiv.classList =
131								'power-coupons-drawer-button-wrapper wc-block-components-totals-wrapper';
132							tempDiv.innerHTML =
133								powerCouponsDrawer.html.drawerButton;
134
135							targetElement.insertAdjacentElement(
136								'afterend',
137								tempDiv
138							);
139
140							// Mark the button as rendered BEFORE re-initialising.
141							// init() can re-enter this filter, and the flag is
142							// what stops the button being injected twice.
143							PowerCouponsDrawer.renderedTriggerButton = true;
144
145							// Re-initialize element references against the
146							// markup we just inserted. Event handlers are
147							// delegated and bound once — see bindEvents().
148							PowerCouponsDrawer.init();
149
150							return defaultValue;
151						},
152					}
153				);
154			}
155		},
156
157		/**
158		 * Bind events
159		 *
160		 * Every handler below is delegated from `document`, so binding twice
161		 * would leave two live handlers per selector and fire two apply/remove
162		 * AJAX requests for a single click. init() is deliberately called more
163		 * than once — the Blocks cart/checkout re-runs it after injecting the
164		 * trigger button — so the guard lives here rather than at the call site.
165		 */
166		bindEvents() {
167			const self = this;
168
169			if ( this.eventsBound ) {
170				return;
171			}
172			this.eventsBound = true;
173
174			// Open drawer - use event delegation to handle dynamically replaced buttons
175			$( document ).on(
176				'click',
177				'#power-coupons-view-coupons-btn',
178				function ( e ) {
179					e.preventDefault();
180					self.openDrawer();
181				}
182			);
183
184			// Close drawer - use event delegation
185			$( document ).on(
186				'click',
187				'.power-coupons-drawer-close',
188				function ( e ) {
189					e.preventDefault();
190					self.closeDrawer();
191				}
192			);
193
194			// Close on overlay click - use event delegation
195			$( document ).on(
196				'click',
197				'.power-coupons-drawer-overlay',
198				function () {
199					self.closeDrawer();
200				}
201			);
202
203			// Close on ESC key and implement focus trap
204			$( document ).on( 'keydown', function ( e ) {
205				if ( ! self.isOpen ) {
206					return;
207				}
208
209				// ESC key closes drawer
210				if ( e.key === 'Escape' ) {
211					e.preventDefault();
212					self.closeDrawer();
213					return;
214				}
215
216				// Tab key - implement focus trap
217				if ( e.key === 'Tab' ) {
218					if (
219						! self.focusableElements ||
220						self.focusableElements.length === 0
221					) {
222						return;
223					}
224
225					const activeElement =
226						self.drawer[ 0 ].ownerDocument.activeElement;
227
228					if ( e.shiftKey ) {
229						// Shift + Tab
230						if ( activeElement === self.firstFocusableElement ) {
231							e.preventDefault();
232							self.lastFocusableElement.focus();
233						}
234					} else if ( activeElement === self.lastFocusableElement ) {
235						// Tab
236						e.preventDefault();
237						self.firstFocusableElement.focus();
238					}
239				}
240			} );
241
242			// Handle apply coupon clicks (delegated)
243			$( document ).on(
244				'click',
245				'.power-coupon-apply-btn',
246				function ( e ) {
247					e.preventDefault();
248					const couponCode = $( this ).data( 'coupon-code' );
249					self.applyCoupon( couponCode, $( this ) );
250				}
251			);
252
253			// Handle remove coupon clicks (delegated)
254			$( document ).on(
255				'click',
256				'.power-coupon-remove-btn',
257				function ( e ) {
258					e.preventDefault();
259					const couponCode = $( this ).data( 'coupon-code' );
260					self.removeCoupon( couponCode, $( this ) );
261				}
262			);
263		},
264
265		/**
266		 * Open drawer
267		 */
268		openDrawer() {
269			// Store the element that had focus before opening
270			this.lastFocusedElement =
271				this.drawer[ 0 ].ownerDocument.activeElement;
272
273			this.isOpen = true;
274			this.drawer.addClass( 'is-open' );
275			this.drawer.attr( 'aria-hidden', 'false' );
276			this.triggerBtn.attr( 'aria-expanded', 'true' );
277			$( 'body' ).css( 'overflow', 'hidden' );
278
279			// Load coupons if not already loaded
280			if ( ! this.couponsLoaded ) {
281				this.loadCoupons();
282			}
283
284			// Set up focus trap
285			this.setupFocusTrap();
286
287			// Move focus to close button after a short delay
288			setTimeout( () => {
289				this.closeBtn.focus();
290			}, 100 );
291		},
292
293		/**
294		 * Setup focus trap for drawer
295		 */
296		setupFocusTrap() {
297			// Get all focusable elements within the drawer
298			const focusableSelectors =
299				'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
300			this.focusableElements = this.drawer
301				.find( focusableSelectors )
302				.filter( ':visible' )
303				.get();
304
305			if ( this.focusableElements.length > 0 ) {
306				this.firstFocusableElement = this.focusableElements[ 0 ];
307				this.lastFocusableElement =
308					this.focusableElements[ this.focusableElements.length - 1 ];
309			}
310		},
311
312		/**
313		 * Close drawer
314		 */
315		closeDrawer() {
316			this.isOpen = false;
317			this.drawer.removeClass( 'is-open' );
318			this.drawer.attr( 'aria-hidden', 'true' );
319			this.triggerBtn.attr( 'aria-expanded', 'false' );
320			$( 'body' ).css( 'overflow', '' );
321
322			// Restore focus to the element that opened the drawer
323			if ( this.lastFocusedElement ) {
324				this.lastFocusedElement.focus();
325				this.lastFocusedElement = null;
326			}
327		},
328
329		/**
330		 * Load coupons via AJAX
331		 */
332		loadCoupons() {
333			const self = this;
334
335			this.loadingEl.show();
336			this.couponsListEl.hide().removeClass( 'loaded' );
337
338			$.ajax( {
339				url: powerCouponsDrawer.ajaxUrl,
340				type: 'POST',
341				data: {
342					action: 'power_coupons_get_drawer_coupons',
343					nonce: powerCouponsDrawer.nonce,
344				},
345				success( response ) {
346					if ( response.success && response.data.html ) {
347						self.couponsListEl.html( response.data.html );
348						self.couponsListEl.addClass( 'loaded' ).show();
349						self.loadingEl.hide();
350						self.couponsLoaded = true;
351					} else {
352						self.showError(
353							'Failed to load coupons. Please try again.'
354						);
355					}
356				},
357				error() {
358					self.showError( 'Connection error. Please try again.' );
359				},
360			} );
361		},
362
363		/**
364		 * Reload coupons (for refresh after apply/remove)
365		 */
366		reloadCoupons() {
367			this.couponsLoaded = false;
368			this.loadCoupons();
369			// Refresh focus trap after loading new content
370			setTimeout( () => {
371				this.setupFocusTrap();
372			}, 100 );
373		},
374
375		/**
376		 * Apply coupon
377		 *
378		 * @param {string} couponCode Coupon code to apply
379		 * @param {Object} $button    jQuery button element
380		 */
381		applyCoupon( couponCode, $button ) {
382			const self = this;
383
384			if ( ! couponCode ) {
385				return;
386			}
387
388			// Disable button and show loading state
389			$button.prop( 'disabled', true ).addClass( 'loading' );
390
391			$.ajax( {
392				url: powerCouponsData.ajaxUrl.applyCoupon,
393				type: 'POST',
394				data: {
395					coupon_code: couponCode,
396					nonce: powerCouponsData.nonce,
397				},
398				success( response ) {
399					if ( response.success ) {
400						// Show success message
401						self.showNotice(
402							powerCouponsData.text.successMessage ||
403								'Coupon applied successfully!',
404							'success'
405						);
406
407						// Sync cart totals in place, then re-render the
408						// drawer against the fresh cart.
409						self.syncCartUi().then( function () {
410							self.reloadCoupons();
411						} );
412					} else {
413						const errorMessage =
414							response.data && response.data.message
415								? response.data.message
416								: powerCouponsData.text.genericErrorMessage ||
417								  'Failed to apply coupon.';
418						self.showNotice( errorMessage, 'error' );
419						$button
420							.prop( 'disabled', false )
421							.removeClass( 'loading' );
422					}
423				},
424				error() {
425					self.showNotice(
426						powerCouponsData.text.networkErrorMessage ||
427							'Connection error.',
428						'error'
429					);
430					$button.prop( 'disabled', false ).removeClass( 'loading' );
431				},
432			} );
433		},
434
435		/**
436		 * Remove coupon
437		 *
438		 * @param {string} couponCode Coupon code to remove
439		 * @param {Object} $button    jQuery button element
440		 */
441		removeCoupon( couponCode, $button ) {
442			const self = this;
443
444			if ( ! couponCode ) {
445				return;
446			}
447
448			// Disable button and show loading state
449			$button.prop( 'disabled', true ).addClass( 'loading' );
450
451			$.ajax( {
452				url: powerCouponsData.ajaxUrl.removeCoupon,
453				type: 'POST',
454				data: {
455					coupon_code: couponCode,
456					nonce: powerCouponsData.nonce,
457				},
458				success( response ) {
459					if ( response.success ) {
460						// Show success message
461						self.showNotice(
462							powerCouponsData.text.couponRemovedMessage ||
463								'Coupon removed.',
464							'success'
465						);
466
467						// Sync cart totals in place, then re-render the
468						// drawer against the fresh cart.
469						self.syncCartUi().then( function () {
470							self.reloadCoupons();
471						} );
472					} else {
473						const errorMessage =
474							response.data && response.data.message
475								? response.data.message
476								: powerCouponsData.text.genericErrorMessage ||
477								  'Failed to remove coupon.';
478						self.showNotice( errorMessage, 'error' );
479						$button
480							.prop( 'disabled', false )
481							.removeClass( 'loading' );
482					}
483				},
484				error() {
485					self.showNotice(
486						powerCouponsData.text.networkErrorMessage ||
487							'Connection error.',
488						'error'
489					);
490					$button.prop( 'disabled', false ).removeClass( 'loading' );
491				},
492			} );
493		},
494
495		/**
496		 * Show error message in drawer
497		 *
498		 * @param {string} message Error message to display
499		 */
500		showError( message ) {
501			this.loadingEl.hide();
502			this.couponsListEl
503				.html(
504					'<div class="power-coupons-no-coupons" role="alert"><p>' +
505						message +
506						'</p></div>'
507				)
508				.addClass( 'loaded' )
509				.show();
510		},
511
512		/**
513		 * Show notice/toast message
514		 *
515		 * @param {string} message Notice message to display
516		 * @param {string} type    Notice type (success or error)
517		 */
518		showNotice( message, type ) {
519			// Use role="status" (polite) for success, role="alert" (assertive) for errors.
520			const noticeRole = type === 'error' ? 'alert' : 'status';
521			const ariaLive = type === 'error' ? 'assertive' : 'polite';
522
523			// Use WooCommerce notices if available
524			const noticeWrapper = $( '.woocommerce-notices-wrapper' ).first();
525
526			if ( noticeWrapper.length ) {
527				const noticeClass =
528					type === 'success'
529						? 'woocommerce-message'
530						: 'woocommerce-error';
531				const notice = $( '<div>' )
532					.addClass( noticeClass )
533					.attr( 'role', noticeRole )
534					.attr( 'aria-live', ariaLive )
535					.attr( 'aria-atomic', 'true' )
536					.text( message );
537
538				noticeWrapper.html( notice );
539
540				// Scroll to notice
541				$( 'html, body' ).animate(
542					{
543						scrollTop: noticeWrapper.offset().top - 100,
544					},
545					500
546				);
547			} else {
548				// Fallback: create a temporary inline notice instead of disruptive alert().
549				const fallbackNotice = $( '<div>' )
550					.addClass( 'power-coupons-inline-notice' )
551					.attr( 'role', noticeRole )
552					.attr( 'aria-atomic', 'true' )
553					.text( message )
554					.css( {
555						position: 'fixed',
556						top: '20px',
557						right: '20px',
558						zIndex: 999999,
559						padding: '12px 20px',
560						borderRadius: '4px',
561						maxWidth: '400px',
562						background: type === 'error' ? '#fee2e2' : '#f0fdf4',
563						color: type === 'error' ? '#991b1b' : '#166534',
564						border:
565							'1px solid ' +
566							( type === 'error' ? '#fecaca' : '#86efac' ),
567						boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
568						fontSize: '14px',
569						lineHeight: '1.5',
570					} );
571
572				$( 'body' ).append( fallbackNotice );
573
574				setTimeout( function () {
575					fallbackNotice.fadeOut( function () {
576						$( this ).remove();
577					} );
578				}, 5000 );
579			}
580		},
581	};
582
583	// Initialize on document ready
584	$( document ).ready( function () {
585		// Check if drawer elements exist
586		if ( $( '#power-coupons-drawer' ).length ) {
587			PowerCouponsDrawer.init();
588		}
589	} );
590
591	// Re-initialize after WooCommerce updates that replace DOM elements
592	// These events fire when WooCommerce refreshes cart/checkout content
593	$( document.body ).on(
594		'updated_checkout updated_cart_totals updated_wc_div',
595		function () {
596			// Re-initialize drawer references if button was replaced
597			if ( $( '#power-coupons-view-coupons-btn' ).length ) {
598				PowerCouponsDrawer.triggerBtn = $(
599					'#power-coupons-view-coupons-btn'
600				);
601				// Update aria-expanded attribute if drawer is open
602				if ( PowerCouponsDrawer.isOpen ) {
603					PowerCouponsDrawer.triggerBtn.attr(
604						'aria-expanded',
605						'true'
606					);
607				}
608			}
609		}
610	);
611} )( jQuery );

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.