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.