PageSourceSearch

https://itchelm.pl/wp-content/plugins/power-coupons/public/assets/js/frontend.js?ver=1.0.3

js itchelm.pl collected 2026-10-02 21:37:39 UTC 16,405 bytes, 624 lines download raw bytes

1/**
2 * Power Coupons Public JavaScript
3 *
4 * Handles coupon apply/remove actions and refreshes on the frontend.
5 * Uses jQuery for DOM manipulation and AJAX requests.
6 *
7 * @param {Object} $ jQuery object
8 * @package
9 * @since 1.0.0
10 */
11
12( function ( $ ) {
13	'use strict';
14
15	/**
16	 * Power Coupons Main Object
17	 *
18	 * Manages all coupon-related functionality on the frontend.
19	 */
20	const PowerCoupons = {
21		/**
22		 * Active AJAX requests tracker
23		 * Prevents duplicate requests from multiple clicks
24		 *
25		 * @type {Object}
26		 */
27		activeRequests: {
28			apply: null,
29			remove: null,
30			refresh: null,
31		},
32
33		/**
34		 * Initialize the Power Coupons functionality
35		 *
36		 * Sets up event handlers and WooCommerce integrations.
37		 *
38		 * @since 1.0.0
39		 */
40		init() {
41			// Validate required data is available
42			if ( ! this.validateDependencies() ) {
43				console.error( 'Power Coupons: Required data not available' );
44				return;
45			}
46
47			// Setup WooCommerce event handlers
48			this.handleWooCommerceEvents();
49
50			// Bind click events for apply/remove buttons
51			this.bindEvents();
52		},
53
54		maybeReloadPageOnSuccess() {
55			if ( powerCouponsData.reloadPageAfterCouponApplied ) {
56				window.location.href = window.location.href;
57			}
58		},
59
60		/**
61		 * Validate that all required dependencies are available
62		 *
63		 * Checks for powerCouponsData object and required AJAX URLs.
64		 *
65		 * @since 1.0.0
66		 * @return {boolean} True if all dependencies are available
67		 */
68		validateDependencies() {
69			if ( typeof powerCouponsData === 'undefined' ) {
70				return false;
71			}
72
73			if ( ! powerCouponsData.ajaxUrl || ! powerCouponsData.nonce ) {
74				return false;
75			}
76
77			return true;
78		},
79
80		/**
81		 * AJAX function to refresh coupons HTML
82		 *
83		 * Fetches updated coupon list from server and replaces existing list.
84		 * Uses modern DOM APIs for better performance.
85		 * @param {Event}  event      Click event.
86		 * @param {string} couponCode Removed coupon code.
87		 */
88		ajaxRefreshCouponsHTML( event, couponCode ) {
89			// Cancel any pending refresh request
90			if ( PowerCoupons.activeRequests.refresh ) {
91				PowerCoupons.activeRequests.refresh.abort();
92			}
93
94			const dataContext = $( '.power-coupons-list' ).attr(
95				'data-context'
96			);
97
98			// Make AJAX request to get fresh coupon HTML
99			PowerCoupons.activeRequests.refresh = $.ajax( {
100				type: 'GET',
101				url: powerCouponsData.ajaxUrl.getCouponsHtml,
102				data: {
103					nonce: powerCouponsData.nonce,
104					context: dataContext || 'ajax',
105					couponCode,
106				},
107				success( response ) {
108					if ( parseInt( response.coupon_id, 10 ) > 0 ) {
109						const couponCards = document.querySelectorAll(
110							`.power-coupons-apply-coupon-btn[data-coupon="${ CSS.escape(
111								response.coupon_code
112							) }"]`
113						);
114
115						const template = document.createElement( 'template' );
116						template.innerHTML = response.html.trim();
117
118						couponCards.forEach( function ( couponCard ) {
119							const newNode =
120								template.content.firstElementChild.cloneNode(
121									true
122								);
123							couponCard.parentElement.parentElement.replaceWith(
124								newNode
125							);
126						} );
127
128						return;
129					}
130
131					const drawerList = document.querySelector(
132						'.power-coupons-drawer-coupons-list'
133					);
134
135					if ( drawerList ) {
136						drawerList.innerHTML = response.html;
137
138						// Trigger custom event for extensions.
139						jQuery( document ).trigger( 'powerCoupons:refreshed' );
140					}
141				},
142				error( jqXHR, textStatus ) {
143					// Only reload if request wasn't aborted
144					if ( textStatus !== 'abort' ) {
145						console.error(
146							'Power Coupons: Failed to refresh coupons list'
147						);
148						// Fallback to page reload as last resort
149						location.reload();
150					}
151				},
152				complete() {
153					// Clear request reference
154					PowerCoupons.activeRequests.refresh = null;
155				},
156			} );
157		},
158
159		/**
160		 * Handle WooCommerce-specific events
161		 *
162		 * Refreshes coupon list when coupons are applied/removed via:
163		 * - Classic WooCommerce cart/checkout forms
164		 * - WooCommerce Blocks (cart/checkout blocks)
165		 *
166		 * @since 1.0.0
167		 */
168		handleWooCommerceEvents() {
169			// ============================================
170			// Classic WooCommerce Cart/Checkout Events
171			// ============================================
172
173			// Coupon applied in classic cart
174			$( 'body' ).on(
175				'applied_coupon',
176				PowerCoupons.ajaxRefreshCouponsHTML
177			);
178
179			// Coupon removed in classic cart
180			$( 'body' ).on(
181				'removed_coupon',
182				PowerCoupons.ajaxRefreshCouponsHTML
183			);
184
185			// Coupon applied in classic checkout
186			$( 'body' ).on(
187				'applied_coupon_in_checkout',
188				PowerCoupons.ajaxRefreshCouponsHTML
189			);
190
191			// Coupon removed in classic checkout
192			$( 'body' ).on(
193				'removed_coupon_in_checkout',
194				PowerCoupons.ajaxRefreshCouponsHTML
195			);
196
197			// Coupon removed in classic checkout
198			$( 'body' ).on(
199				'updated_cart_totals',
200				PowerCoupons.ajaxRefreshCouponsHTML
201			);
202
203			// ============================================
204			// WooCommerce Blocks Integration
205			// ============================================
206
207			// Check if WooCommerce Blocks API is available
208			if (
209				window &&
210				window.wc &&
211				window.wc.blocksCheckout &&
212				typeof window.wc.blocksCheckout.registerCheckoutFilters ===
213					'function'
214			) {
215				// Register filters for blocks checkout
216				window.wc.blocksCheckout.registerCheckoutFilters(
217					'powerCouponsRefreshCoupons',
218					{
219						totalValue( defaultValue ) {
220							PowerCoupons.ajaxRefreshCouponsHTML();
221							return defaultValue;
222						},
223
224						/**
225						 * Filter for apply coupon notice
226						 * Refreshes coupon list when coupon is applied in blocks
227						 *
228						 * @param {boolean} defaultValue - Whether to show notice
229						 * @return {boolean} The default value (pass through)
230						 */
231						showApplyCouponNotice( defaultValue ) {
232							PowerCoupons.ajaxRefreshCouponsHTML();
233							return defaultValue;
234						},
235
236						/**
237						 * Filter for remove coupon notice
238						 * Refreshes coupon list when coupon is removed in blocks
239						 *
240						 * @param {boolean} defaultValue - Whether to show notice
241						 * @return {boolean} The default value (pass through)
242						 */
243						showRemoveCouponNotice( defaultValue ) {
244							PowerCoupons.ajaxRefreshCouponsHTML();
245							return defaultValue;
246						},
247					}
248				);
249			}
250		},
251
252		/**
253		 * Bind click events to coupon action buttons
254		 *
255		 * Uses event delegation for better performance and to handle
256		 * dynamically added elements (after AJAX refresh).
257		 *
258		 * @since 1.0.0
259		 */
260		bindEvents() {
261			const self = this;
262
263			// Apply coupon button click
264			// Simpler: clicking a coupon card applies it (unless user clicks a button in it)
265			$( document ).on(
266				'click',
267				'.power-coupons-list .power-coupon-card',
268				function ( e ) {
269					if (
270						! $( e.target ).closest(
271							'.apply-coupon, .remove-coupon'
272						).length
273					) {
274						$( this )
275							.find( '.apply-coupon' )
276							.first()
277							.trigger( 'click' );
278					}
279				}
280			);
281
282			$( document ).on(
283				'click',
284				'.power-coupons-apply-coupon-btn',
285				function ( e ) {
286					self.applyCoupon.call( this, e, self );
287				}
288			);
289
290			// Remove coupon button click
291			$( document ).on( 'click', '.remove-coupon', function ( e ) {
292				self.removeCoupon.call( this, e, self );
293			} );
294		},
295
296		/**
297		 * Apply coupon to cart
298		 *
299		 * Sends AJAX request to apply coupon and updates UI accordingly.
300		 *
301		 * @since 1.0.0
302		 * @param {Event}  e    - Click event object
303		 * @param {Object} self - Reference to PowerCoupons object
304		 */
305		applyCoupon( e, self ) {
306			e.preventDefault();
307
308			const $button = $( this );
309			const couponCode = $button.data( 'coupon' );
310
311			// Validate coupon code
312			if ( ! couponCode ) {
313				console.error( 'Power Coupons: No coupon code found' );
314				return;
315			}
316
317			// Prevent duplicate requests
318			if ( self.activeRequests.apply ) {
319				return;
320			}
321
322			const originalText = $button.html();
323
324			// Update button state - show loading
325			$button
326				.prop( 'disabled', true )
327				.attr( 'aria-disabled', 'true' )
328				.attr( 'aria-busy', 'true' )
329				.addClass( 'pc-loading' )
330				.find( '.power-coupons-coupon-status' )
331				.text( powerCouponsData.text?.applyingText || 'Applying...' );
332
333			// Send AJAX request
334			self.activeRequests.apply = $.ajax( {
335				type: 'POST',
336				url: powerCouponsData.ajaxUrl.applyCoupon,
337				data: {
338					coupon_code: couponCode,
339					nonce: powerCouponsData.nonce,
340					billing_email: $( 'input[name="billing_email"]' ).val(),
341				},
342				success( response ) {
343					if ( response.success ) {
344						// Trigger WooCommerce events for cart/checkout updates
345						self.triggerWooCommerceUpdates();
346
347						PowerCoupons.maybeReloadPageOnSuccess();
348
349						// Refresh coupon list after a short delay
350						setTimeout( function () {
351							PowerCoupons.ajaxRefreshCouponsHTML();
352						}, 500 );
353					} else {
354						// Failed - show error message
355						const errorMessage =
356							response.data?.message ||
357							powerCouponsData.text?.applyErrorText ||
358							'Failed to apply coupon.';
359
360						self.showNotice( errorMessage, 'error' );
361
362						// Refresh coupon list after a short delay
363						setTimeout( function () {
364							PowerCoupons.ajaxRefreshCouponsHTML();
365						}, 500 );
366					}
367				},
368				error( jqXHR, textStatus ) {
369					// Network error or server error
370					if ( textStatus !== 'abort' ) {
371						const errorMessage =
372							powerCouponsData.text?.applyErrorText ||
373							'Failed to apply coupon. Please try again.';
374
375						self.showNotice( errorMessage, 'error' );
376
377						// Restore button state
378						$button
379							.prop( 'disabled', false )
380							.attr( 'aria-disabled', 'false' )
381							.attr( 'aria-busy', 'false' )
382							.removeClass( 'pc-loading' )
383							.text( originalText );
384					}
385				},
386				complete() {
387					// Clear request reference
388					self.activeRequests.apply = null;
389				},
390			} );
391		},
392
393		/**
394		 * Remove coupon from cart
395		 *
396		 * Sends AJAX request to remove coupon and updates UI accordingly.
397		 *
398		 * @since 1.0.0
399		 * @param {Event}  e    - Click event object
400		 * @param {Object} self - Reference to PowerCoupons object
401		 */
402		removeCoupon( e, self ) {
403			e.preventDefault();
404
405			const $button = $( this );
406			const couponCode = $button.data( 'coupon' );
407
408			// Validate coupon code
409			if ( ! couponCode ) {
410				console.error( 'Power Coupons: No coupon code found' );
411				return;
412			}
413
414			// Prevent duplicate requests
415			if ( self.activeRequests.remove ) {
416				return;
417			}
418
419			const originalText = $button.text();
420
421			// Update button state - show loading
422			$button
423				.prop( 'disabled', true )
424				.attr( 'aria-disabled', 'true' )
425				.attr( 'aria-busy', 'true' )
426				.addClass( 'pc-loading' )
427				.text( powerCouponsData.text?.removingText || 'Removing...' );
428
429			// Send AJAX request
430			self.activeRequests.remove = $.ajax( {
431				type: 'POST',
432				url: powerCouponsData.ajaxUrl.removeCoupon,
433				data: {
434					coupon_code: couponCode,
435					nonce: powerCouponsData.nonce,
436				},
437				success( response ) {
438					if ( response.success ) {
439						// Success - reload page to update cart totals
440						// WooCommerce will show its own success message
441						PowerCoupons.maybeReloadPageOnSuccess();
442					} else {
443						// Failed - show error message
444						const errorMessage =
445							response.data?.message ||
446							powerCouponsData.text?.removeErrorText ||
447							'Failed to remove coupon.';
448
449						self.showNotice( errorMessage, 'error' );
450
451						// Restore button state
452						$button
453							.prop( 'disabled', false )
454							.attr( 'aria-disabled', 'false' )
455							.attr( 'aria-busy', 'false' )
456							.removeClass( 'pc-loading' )
457							.text( originalText );
458					}
459				},
460				error( jqXHR, textStatus ) {
461					// Network error or server error
462					if ( textStatus !== 'abort' ) {
463						const errorMessage =
464							powerCouponsData.text?.removeErrorText ||
465							'Failed to remove coupon. Please try again.';
466
467						self.showNotice( errorMessage, 'error' );
468
469						// Restore button state
470						$button
471							.prop( 'disabled', false )
472							.attr( 'aria-disabled', 'false' )
473							.attr( 'aria-busy', 'false' )
474							.removeClass( 'pc-loading' )
475							.text( originalText );
476					}
477				},
478				complete() {
479					// Clear request reference
480					self.activeRequests.remove = null;
481				},
482			} );
483		},
484
485		/**
486		 * Trigger WooCommerce cart/checkout updates
487		 *
488		 * Triggers appropriate WooCommerce events to refresh cart/checkout
489		 * displays after coupon application.
490		 *
491		 * @since 1.0.0
492		 */
493		triggerWooCommerceUpdates() {
494			const isBlockCart =
495				document.querySelector( '.wc-block-cart' ) !== null;
496			const isBlockCheckout =
497				document.querySelector( '.wc-block-checkout' ) !== null;
498
499			if ( isBlockCart || isBlockCheckout ) {
500				// Block pages: reload to get fresh server-rendered notifications.
501				window.location.reload();
502				return;
503			}
504
505			// Classic cart/checkout pages.
506			const isCartPage = $( '.woocommerce-cart-form' ).length > 0;
507			const isCheckoutPage = $( 'form.checkout' ).length > 0;
508
509			if ( isCartPage ) {
510				// Cart page: Trigger cart totals update
511				$( document.body ).trigger( 'updated_cart_totals' );
512				$( document.body ).trigger( 'wc_fragment_refresh' );
513
514				// Also trigger update_checkout if checkout is on same page
515				if ( isCheckoutPage ) {
516					$( document.body ).trigger( 'update_checkout' );
517				}
518			} else if ( isCheckoutPage ) {
519				// Checkout page: Trigger checkout update
520				$( document.body ).trigger( 'update_checkout' );
521			}
522
523			// Always trigger applied_coupon event (WooCommerce standard event)
524			$( document.body ).trigger( 'applied_coupon' );
525			$( document.body ).trigger( 'applied_coupon_in_checkout' );
526		},
527
528		/**
529		 * Show notice message to user
530		 *
531		 * Attempts to use WooCommerce notice system if available,
532		 * falls back to browser alert as last resort.
533		 *
534		 * @since 1.0.0
535		 * @param {string} message - Message to display
536		 * @param {string} type    - Notice type: 'success', 'error', 'notice'
537		 */
538		showNotice( message, type ) {
539			type = type || 'notice';
540
541			// Use role="status" (polite) for success, role="alert" (assertive) for errors.
542			const noticeRole = type === 'error' ? 'alert' : 'status';
543			const ariaLive = type === 'error' ? 'assertive' : 'polite';
544
545			// Try to use WooCommerce notices if available
546			const noticeContainer = $( '.woocommerce-notices-wrapper' ).first();
547
548			if ( noticeContainer.length ) {
549				// Create WooCommerce-style notice
550				const noticeClass =
551					type === 'error'
552						? 'woocommerce-error'
553						: 'woocommerce-message';
554				const notice = $( '<div>' )
555					.addClass( noticeClass )
556					.attr( 'role', noticeRole )
557					.attr( 'aria-live', ariaLive )
558					.attr( 'aria-atomic', 'true' )
559					.text( message );
560
561				// Clear existing notices and add new one
562				noticeContainer.empty().append( notice );
563
564				// Scroll to notice
565				$( 'html, body' ).animate(
566					{
567						scrollTop: noticeContainer.offset().top - 100,
568					},
569					500
570				);
571
572				// Auto-dismiss success notices after 10 seconds (increased from 5 for accessibility)
573				if ( type === 'success' ) {
574					setTimeout( function () {
575						notice.fadeOut( function () {
576							$( this ).remove();
577						} );
578					}, 10000 );
579				}
580			} else {
581				// Fallback: create a temporary inline notice instead of disruptive alert().
582				const fallbackNotice = $( '<div>' )
583					.addClass( 'power-coupons-inline-notice' )
584					.attr( 'role', noticeRole )
585					.attr( 'aria-atomic', 'true' )
586					.text( message )
587					.css( {
588						position: 'fixed',
589						top: '20px',
590						right: '20px',
591						zIndex: 999999,
592						padding: '12px 20px',
593						borderRadius: '4px',
594						maxWidth: '400px',
595						background: type === 'error' ? '#fee2e2' : '#f0fdf4',
596						color: type === 'error' ? '#991b1b' : '#166534',
597						border:
598							'1px solid ' +
599							( type === 'error' ? '#fecaca' : '#86efac' ),
600						boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
601						fontSize: '14px',
602						lineHeight: '1.5',
603					} );
604
605				$( 'body' ).append( fallbackNotice );
606
607				setTimeout( function () {
608					fallbackNotice.fadeOut( function () {
609						$( this ).remove();
610					} );
611				}, 5000 );
612			}
613		},
614	};
615
616	/**
617	 * Initialize on DOM ready
618	 *
619	 * Waits for jQuery and DOM to be ready before initializing.
620	 */
621	$( document ).ready( function () {
622		PowerCoupons.init();
623	} );
624} )( 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.