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 * Request timeout, in milliseconds. 35 * 36 * activeRequests gates apply/remove/refresh so a double click cannot 37 * fire twice, and each slot is only released in the request's 38 * `complete` handler. jQuery does not time a request out on its own, so 39 * a connection that stalls rather than fails leaves its slot occupied 40 * and the button inert for as long as the browser holds the socket. 41 * 42 * @type {number} 43 */ 44 requestTimeout: 30000, 45 46 /** 47 * Initialize the Power Coupons functionality 48 * 49 * Sets up event handlers and WooCommerce integrations. 50 * 51 * @since 1.0.0 52 */ 53 init() { 54 // Validate required data is available 55 if ( ! this.validateDependencies() ) { 56 console.error( 'Power Coupons: Required data not available' ); 57 return; 58 } 59 60 // Setup WooCommerce event handlers 61 this.handleWooCommerceEvents(); 62 63 // Bind click events for apply/remove buttons 64 this.bindEvents(); 65 }, 66 67 /** 68 * Sync every cart-dependent UI after the server-side cart changed. 69 * 70 * Refreshes in place through the shared cart-refresh helper, which 71 * picks the right mechanism for the current context (Blocks data 72 * store, classic checkout, classic cart). 73 * 74 * A full page reload is opt-in only, via the 75 * `power_coupons_reload_page_after_coupon_is_applied` filter: reloading 76 * discards the in-page state that multi-step checkout plugins such as 77 * CartFlows and FunnelKit hold in the browser, which sends the shopper 78 * back to step one. 79 * 80 * @since 1.0.6 81 * @return {Promise} Resolves once the cart UI is in sync. 82 */ 83 syncCartUi() { 84 if ( powerCouponsData.reloadPageAfterCouponApplied ) { 85 window.location.reload(); 86 87 // Never resolves: the document is being torn down, so no 88 // follow-up UI work should run. 89 return new Promise( function () {} ); 90 } 91 92 if ( ! window.PowerCouponsCartRefresh ) { 93 return Promise.resolve( false ); 94 } 95 96 return window.PowerCouponsCartRefresh.refresh( { 97 source: 'coupon-list', 98 } ); 99 }, 100 101 /** 102 * Validate that all required dependencies are available 103 * 104 * Checks for powerCouponsData object and required AJAX URLs. 105 * 106 * @since 1.0.0 107 * @return {boolean} True if all dependencies are available 108 */ 109 validateDependencies() { 110 if ( typeof powerCouponsData === 'undefined' ) { 111 return false; 112 } 113 114 if ( ! powerCouponsData.ajaxUrl || ! powerCouponsData.nonce ) { 115 return false; 116 } 117 118 return true; 119 }, 120 121 /** 122 * Escape a value for use inside an attribute selector. 123 * 124 * Extracted for readability; the fallback below is belt-and-braces and 125 * is not expected to run. The audit flagged the bare `CSS.escape` call 126 * as unpolyfilled, but this file already uses optional chaining and 127 * `??` and is shipped untranspiled, so any engine old enough to lack 128 * `CSS.escape` (pre-2016) fails to parse the file long before reaching 129 * this line â and `.browserslistrc` targets the last four versions of 130 * each major browser anyway. Keeping the guard costs nothing and makes 131 * the selector construction explicit. 132 * 133 * @since 1.0.7 134 * @param {string} value Raw value, typically a coupon code. 135 * @return {string} Value safe to interpolate into a selector. 136 */ 137 escapeForSelector( value ) { 138 const raw = String( value ?? '' ); 139 140 if ( 141 typeof window.CSS !== 'undefined' && 142 typeof window.CSS.escape === 'function' 143 ) { 144 return window.CSS.escape( raw ); 145 } 146 147 return raw.replace( /["'\\\]\[\s]/g, '\\$&' ); 148 }, 149 150 /** 151 * Pending deferred refresh timer, if one is queued. 152 * 153 * @type {?number} 154 */ 155 refreshTimer: null, 156 157 /** 158 * Queue a coupon-list refresh to run after the current task. 159 * 160 * WooCommerce Blocks calls its checkout filters during render and 161 * expects them to be pure value transforms. Calling 162 * ajaxRefreshCouponsHTML() straight from one issued an XHR from inside 163 * someone else's render pass; deferring by a tick moves the side effect
164 * out of it, and the single timer collapses any repeat calls within one 165 * pass into a single request. 166 * 167 * Measured on a real block cart, this is request-count neutral for a 168 * quantity change (8 requests over 4 changes, before and after) â the 169 * old code's abort-and-retry already collapsed most of the repeats. The 170 * point of this is the render-pass contract, not throughput. 171 * 172 * @since 1.0.7 173 * @return {void} 174 */ 175 scheduleCouponsRefresh() { 176 if ( null !== PowerCoupons.refreshTimer ) { 177 return; 178 } 179 180 PowerCoupons.refreshTimer = window.setTimeout( function () { 181 PowerCoupons.refreshTimer = null; 182 PowerCoupons.ajaxRefreshCouponsHTML(); 183 }, 0 ); 184 }, 185 186 /** 187 * Normalise a coupon code so both sources compare equal. 188 * 189 * The order review row carries the code as a `coupon-<code>` class run 190 * through WordPress's `sanitize_title()`, while the cards carry it raw 191 * in `data-coupon`. A code containing a space or symbol would otherwise 192 * never match itself and the two views would look permanently out of 193 * sync. 194 * 195 * @since 1.0.7 196 * @param {string} code Raw or sanitised coupon code. 197 * @return {string} Comparable form. 198 */ 199 normalizeCouponCode( code ) { 200 return String( code ?? '' ) 201 .toLowerCase() 202 .replace( /[^a-z0-9]+/g, '-' ) 203 .replace( /^-+|-+$/g, '' ); 204 }, 205 206 /** 207 * Coupons the cart says are applied, read from the order review. 208 * 209 * Read from the `tr.cart-discount` rows rather than any one plugin's 210 * remove control: WooCommerce renders 211 * `a.woocommerce-remove-coupon` and CartFlows renders 212 * `a.wcf-remove-coupon` inside the same row, but both put the code in a 213 * `coupon-<code>` class on the row itself. 214 * 215 * @since 1.0.7 216 * @return {string} Sorted, comma-separated coupon codes. 217 */ 218 appliedCouponSignature() { 219 const codes = []; 220 221 document 222 .querySelectorAll( '.cart-discount' ) 223 .forEach( function ( row ) { 224 const fromControl = row.querySelector( '[data-coupon]' ); 225 226 if ( fromControl && fromControl.dataset.coupon ) { 227 codes.push( 228 PowerCoupons.normalizeCouponCode( 229 fromControl.dataset.coupon 230 ) 231 ); 232 233 return; 234 } 235 236 row.classList.forEach( function ( className ) { 237 if ( 0 === className.indexOf( 'coupon-' ) ) { 238 codes.push( 239 PowerCoupons.normalizeCouponCode( 240 className.slice( 'coupon-'.length ) 241 ) 242 ); 243 } 244 } ); 245 } ); 246 247 return PowerCoupons.toSignature( codes ); 248 }, 249 250 /** 251 * Coupons the rendered cards currently show as applied. 252 * 253 * @since 1.0.7 254 * @return {string} Sorted, comma-separated coupon codes. 255 */ 256 renderedCouponSignature() { 257 const codes = []; 258 259 document 260 .querySelectorAll( 261 '.power-coupons-apply-coupon-btn[data-coupon-status="applied"]' 262 ) 263 .forEach( function ( button ) { 264 if ( button.dataset.coupon ) { 265 codes.push( 266 PowerCoupons.normalizeCouponCode( 267 button.dataset.coupon 268 ) 269 ); 270 } 271 } ); 272 273 return PowerCoupons.toSignature( codes ); 274 }, 275 276 /** 277 * Reduce a list of codes to a comparable signature. 278 * 279 * @since 1.0.7 280 * @param {Array<string>} codes Coupon codes. 281 * @return {string} Sorted, de-duplicated, comma-separated codes. 282 */ 283 toSignature( codes ) { 284 return Array.from( new Set( codes.filter( Boolean ) ) ) 285 .sort() 286 .join( ',' ); 287 }, 288 289 /** 290 * Re-render the coupon list when it disagrees with the cart. 291 * 292 * Compares two live views â what the order review says is applied 293 * against what the cards show â rather than diffing against a 294 * remembered value. A remembered baseline goes stale whenever the cart 295 * changes through a route that does not refresh both, and then 296 * suppresses the very refresh it exists to trigger. Comparing the two 297 * current views instead makes this self-correcting: it fires whenever 298 * they drift apart, however they got there, and stays quiet when they 299 * agree. 300 * 301 * That quiet matters â `updated_checkout` fires on address edits, 302 * shipping switches and quantity changes, none of which touch coupons. 303 * 304 * @since 1.0.7 305 * @return {void} 306 */ 307 refreshIfCouponsChanged() { 308 if ( 309 PowerCoupons.appliedCouponSignature() === 310 PowerCoupons.renderedCouponSignature() 311 ) { 312 return; 313 } 314 315 // No coupon code: take the whole-list branch, because this path 316 // only knows the views drifted, not which coupon moved. 317 PowerCoupons.ajaxRefreshCouponsHTML(); 318 }, 319 320 /** 321 * AJAX function to refresh coupons HTML 322 * 323 * Fetches updated coupon list from server and replaces existing list. 324 * Uses modern DOM APIs for better performance. 325 * @param {Event} event Click event. 326 * @param {string} couponCode Removed coupon code. 327 */ 328 ajaxRefreshCouponsHTML( event, couponCode ) {
329 // Cancel any pending refresh request 330 if ( PowerCoupons.activeRequests.refresh ) { 331 PowerCoupons.activeRequests.refresh.abort(); 332 } 333 334 const dataContext = $( '.power-coupons-list' ).attr( 335 'data-context' 336 ); 337 338 // Make AJAX request to get fresh coupon HTML 339 PowerCoupons.activeRequests.refresh = $.ajax( { 340 type: 'GET', 341 url: powerCouponsData.ajaxUrl.getCouponsHtml, 342 timeout: PowerCoupons.requestTimeout, 343 data: { 344 nonce: powerCouponsData.nonce, 345 context: dataContext || 'ajax', 346 couponCode, 347 }, 348 success( response ) { 349 if ( parseInt( response.coupon_id, 10 ) > 0 ) { 350 const selector = `.power-coupons-apply-coupon-btn[data-coupon="${ PowerCoupons.escapeForSelector( 351 response.coupon_code 352 ) }"]`; 353 354 const couponCards = 355 document.querySelectorAll( selector ); 356 357 const template = document.createElement( 'template' ); 358 template.innerHTML = response.html.trim(); 359 360 // The response is a whole `.power-coupons-list` wrapper 361 // containing just this coupon, but each coupon IS the 362 // button (see views/coupon-list.php), and every button 363 // shares one `.power-coupons-section` parent. Replacing 364 // the button's grandparent therefore swapped the entire 365 // list for a single card and silently dropped every 366 // other coupon â visible in the drawer, which renders 367 // them all. Swap the button itself instead. 368 const freshCard = 369 template.content.querySelector( selector ); 370 371 if ( ! freshCard ) { 372 return; 373 } 374 375 couponCards.forEach( function ( couponCard ) { 376 couponCard.replaceWith( 377 freshCard.cloneNode( true ) 378 ); 379 } ); 380 381 return; 382 } 383 384 const drawerList = document.querySelector( 385 '.power-coupons-drawer-coupons-list' 386 ); 387 388 if ( drawerList ) { 389 drawerList.innerHTML = response.html; 390 391 // Trigger custom event for extensions. 392 jQuery( document ).trigger( 'powerCoupons:refreshed' ); 393 } 394 }, 395 error( jqXHR, textStatus ) { 396 if ( textStatus !== 'abort' ) { 397 // Leave the stale list in place rather than reloading: 398 // the cart itself is already correct, and a reload 399 // would reset multi-step checkout progress. 400 console.error( 401 'Power Coupons: Failed to refresh coupons list' 402 ); 403 } 404 }, 405 complete() { 406 // Clear request reference 407 PowerCoupons.activeRequests.refresh = null; 408 }, 409 } ); 410 }, 411 412 /** 413 * Handle WooCommerce-specific events 414 * 415 * Refreshes coupon list when coupons are applied/removed via: 416 * - Classic WooCommerce cart/checkout forms 417 * - WooCommerce Blocks (cart/checkout blocks) 418 * 419 * @since 1.0.0 420 */ 421 handleWooCommerceEvents() { 422 // ============================================ 423 // Classic WooCommerce Cart/Checkout Events 424 // ============================================ 425 426 // Coupon applied in classic cart 427 $( 'body' ).on( 428 'applied_coupon', 429 PowerCoupons.ajaxRefreshCouponsHTML 430 ); 431 432 // Coupon removed in classic cart 433 $( 'body' ).on( 434 'removed_coupon', 435 PowerCoupons.ajaxRefreshCouponsHTML 436 ); 437 438 // Coupon applied in classic checkout 439 $( 'body' ).on( 440 'applied_coupon_in_checkout', 441 PowerCoupons.ajaxRefreshCouponsHTML 442 ); 443 444 // Coupon removed in classic checkout 445 $( 'body' ).on( 446 'removed_coupon_in_checkout', 447 PowerCoupons.ajaxRefreshCouponsHTML 448 ); 449 450 // Coupon removed in classic checkout 451 $( 'body' ).on( 452 'updated_cart_totals', 453 PowerCoupons.ajaxRefreshCouponsHTML 454 ); 455 456 // Checkouts that never fire WooCommerce's coupon events. 457 // 458 // CartFlows' step checkout applies and removes coupons through its 459 // own handlers and its own `a.wcf-remove-coupon` control, and on 460 // success triggers only `update_checkout` 461 // (cartflows/assets/js/checkout-template.js). None of the five 462 // events above fire, so removing a coupon from the step's order 463 // summary left the drawer still showing it as applied. 464 // 465 // Watching `updated_checkout` covers any such integration without 466 // needing to know about it, and comparing the applied-coupon set 467 // keeps the ordinary checkout refreshes free. 468 $( 'body' ).on( 'updated_checkout', function () { 469 PowerCoupons.refreshIfCouponsChanged(); 470 } ); 471 472 // ============================================ 473 // WooCommerce Blocks Integration 474 // ============================================ 475 476 // Check if WooCommerce Blocks API is available 477 if ( 478 window && 479 window.wc && 480 window.wc.blocksCheckout && 481 typeof window.wc.blocksCheckout.registerCheckoutFilters === 482 'function' 483 ) { 484 /* 485 * These are the only signal Blocks gives us that the cart 486 * changed, but Blocks treats them as pure value transforms and 487 * calls them during render â `totalValue` once per totals row. 488 * 489 * So each one records that a refresh is wanted and returns the 490 * value untouched. The refresh itself is queued out of the 491 * render pass by scheduleCouponsRefresh(), which also collapses 492 * a whole render's worth of calls into a single request. 493 */ 494 window.wc.blocksCheckout.registerCheckoutFilters( 495 'powerCouponsRefreshCoupons', 496 { 497 /** 498 * Cart totals changed. 499 * 500 * @param {string} defaultValue - The formatted total. 501 * @return {string} The default value (pass through) 502 */ 503 totalValue( defaultValue ) { 504 PowerCoupons.scheduleCouponsRefresh(); 505 return defaultValue; 506 }, 507 508 /** 509 * Filter for apply coupon notice 510 * Refreshes coupon list when coupon is applied in blocks 511 * 512 * @param {boolean} defaultValue - Whether to show notice 513 * @return {boolean} The default value (pass through) 514 */ 515 showApplyCouponNotice( defaultValue ) { 516 PowerCoupons.scheduleCouponsRefresh(); 517 return defaultValue; 518 }, 519 520 /** 521 * Filter for remove coupon notice 522 * Refreshes coupon list when coupon is removed in blocks 523 * 524 * @param {boolean} defaultValue - Whether to show notice 525 * @return {boolean} The default value (pass through) 526 */ 527 showRemoveCouponNotice( defaultValue ) { 528 PowerCoupons.scheduleCouponsRefresh(); 529 return defaultValue; 530 }, 531 } 532 ); 533 } 534 }, 535 536 /** 537 * Bind click events to coupon action buttons 538 * 539 * Uses event delegation for better performance and to handle 540 * dynamically added elements (after AJAX refresh). 541 * 542 * @since 1.0.0 543 */ 544 bindEvents() { 545 const self = this; 546 547 // Apply coupon button click 548 // Simpler: clicking a coupon card applies it (unless user clicks a button in it) 549 $( document ).on( 550 'click', 551 '.power-coupons-list .power-coupon-card', 552 function ( e ) { 553 if ( 554 ! $( e.target ).closest( 555 '.apply-coupon, .remove-coupon' 556 ).length 557 ) { 558 $( this ) 559 .find( '.apply-coupon' ) 560 .first() 561 .trigger( 'click' ); 562 } 563 } 564 ); 565 566 $( document ).on( 567 'click', 568 '.power-coupons-apply-coupon-btn', 569 function ( e ) { 570 self.applyCoupon.call( this, e, self ); 571 } 572 ); 573
574 // Remove coupon button click 575 $( document ).on( 'click', '.remove-coupon', function ( e ) { 576 self.removeCoupon.call( this, e, self ); 577 } ); 578 }, 579 580 /** 581 * Apply coupon to cart 582 * 583 * Sends AJAX request to apply coupon and updates UI accordingly. 584 * 585 * @since 1.0.0 586 * @param {Event} e - Click event object 587 * @param {Object} self - Reference to PowerCoupons object 588 */ 589 applyCoupon( e, self ) { 590 e.preventDefault(); 591 592 const $button = $( this ); 593 const couponCode = $button.data( 'coupon' ); 594 595 // Validate coupon code 596 if ( ! couponCode ) { 597 console.error( 'Power Coupons: No coupon code found' ); 598 return; 599 } 600 601 // Prevent duplicate requests 602 if ( self.activeRequests.apply ) { 603 return; 604 } 605 606 const originalText = $button.html(); 607 608 // Update button state - show loading 609 $button 610 .prop( 'disabled', true ) 611 .attr( 'aria-disabled', 'true' ) 612 .attr( 'aria-busy', 'true' ) 613 .addClass( 'pc-loading' ) 614 .find( '.power-coupons-coupon-status' ) 615 .text( powerCouponsData.text?.applyingText || 'Applying...' ); 616 617 // Send AJAX request 618 self.activeRequests.apply = $.ajax( { 619 type: 'POST', 620 url: powerCouponsData.ajaxUrl.applyCoupon, 621 timeout: PowerCoupons.requestTimeout, 622 data: { 623 coupon_code: couponCode, 624 nonce: powerCouponsData.nonce, 625 billing_email: $( 'input[name="billing_email"]' ).val(), 626 }, 627 success( response ) { 628 if ( response.success ) { 629 // Sync cart totals in place, then re-render the 630 // coupon cards against the fresh cart. 631 self.syncCartUi().then( function () { 632 PowerCoupons.ajaxRefreshCouponsHTML(); 633 } ); 634 } else { 635 // Failed - show error message 636 const errorMessage = 637 response.data?.message || 638 powerCouponsData.text?.applyErrorText || 639 'Failed to apply coupon.'; 640 641 self.showNotice( errorMessage, 'error' ); 642 643 // Refresh coupon list after a short delay 644 setTimeout( function () { 645 PowerCoupons.ajaxRefreshCouponsHTML(); 646 }, 500 ); 647 } 648 }, 649 error( jqXHR, textStatus ) { 650 // Network error or server error 651 if ( textStatus !== 'abort' ) { 652 const errorMessage = 653 powerCouponsData.text?.applyErrorText || 654 'Failed to apply coupon. Please try again.'; 655 656 self.showNotice( errorMessage, 'error' ); 657 658 // Restore button state 659 $button 660 .prop( 'disabled', false ) 661 .attr( 'aria-disabled', 'false' ) 662 .attr( 'aria-busy', 'false' ) 663 .removeClass( 'pc-loading' ) 664 .text( originalText ); 665 } 666 }, 667 complete() { 668 // Clear request reference 669 self.activeRequests.apply = null; 670 }, 671 } ); 672 }, 673 674 /** 675 * Remove coupon from cart 676 * 677 * Sends AJAX request to remove coupon and updates UI accordingly. 678 * 679 * @since 1.0.0 680 * @param {Event} e - Click event object 681 * @param {Object} self - Reference to PowerCoupons object 682 */ 683 removeCoupon( e, self ) { 684 e.preventDefault(); 685 686 const $button = $( this ); 687 const couponCode = $button.data( 'coupon' ); 688 689 // Validate coupon code 690 if ( ! couponCode ) { 691 console.error( 'Power Coupons: No coupon code found' ); 692 return; 693 } 694 695 // Prevent duplicate requests 696 if ( self.activeRequests.remove ) { 697 return; 698 } 699 700 const originalText = $button.text(); 701 702 // Update button state - show loading 703 $button 704 .prop( 'disabled', true ) 705 .attr( 'aria-disabled', 'true' ) 706 .attr( 'aria-busy', 'true' ) 707 .addClass( 'pc-loading' ) 708 .text( powerCouponsData.text?.removingText || 'Removing...' ); 709 710 // Send AJAX request 711 self.activeRequests.remove = $.ajax( { 712 type: 'POST', 713 url: powerCouponsData.ajaxUrl.removeCoupon, 714 timeout: PowerCoupons.requestTimeout, 715 data: { 716 coupon_code: couponCode, 717 nonce: powerCouponsData.nonce, 718 }, 719 success( response ) { 720 if ( response.success ) { 721 // Sync cart totals in place, then re-render the 722 // coupon cards against the fresh cart. 723 self.syncCartUi().then( function () { 724 PowerCoupons.ajaxRefreshCouponsHTML(); 725 } ); 726 } else { 727 // Failed - show error message 728 const errorMessage = 729 response.data?.message || 730 powerCouponsData.text?.removeErrorText || 731 'Failed to remove coupon.'; 732 733 self.showNotice( errorMessage, 'error' ); 734 735 // Restore button state 736 $button 737 .prop( 'disabled', false ) 738 .attr( 'aria-disabled', 'false' ) 739 .attr( 'aria-busy', 'false' ) 740 .removeClass( 'pc-loading' ) 741 .text( originalText ); 742 } 743 }, 744 error( jqXHR, textStatus ) { 745 // Network error or server error 746 if ( textStatus !== 'abort' ) { 747 const errorMessage = 748 powerCouponsData.text?.removeErrorText || 749 'Failed to remove coupon. Please try again.'; 750 751 self.showNotice( errorMessage, 'error' ); 752 753 // Restore button state 754 $button 755 .prop( 'disabled', false ) 756 .attr( 'aria-disabled', 'false' ) 757 .attr( 'aria-busy', 'false' ) 758 .removeClass( 'pc-loading' ) 759 .text( originalText ); 760 } 761 }, 762 complete() { 763 // Clear request reference 764 self.activeRequests.remove = null; 765 }, 766 } ); 767 }, 768 769 /** 770 * Show notice message to user 771 * 772 * Attempts to use WooCommerce notice system if available, 773 * falls back to browser alert as last resort. 774 * 775 * @since 1.0.0 776 * @param {string} message - Message to display 777 * @param {string} type - Notice type: 'success', 'error', 'notice' 778 */ 779 showNotice( message, type ) { 780 type = type || 'notice'; 781 782 // Use role="status" (polite) for success, role="alert" (assertive) for errors. 783 const noticeRole = type === 'error' ? 'alert' : 'status'; 784 const ariaLive = type === 'error' ? 'assertive' : 'polite'; 785 786 // Try to use WooCommerce notices if available 787 const noticeContainer = $( '.woocommerce-notices-wrapper' ).first(); 788 789 if ( noticeContainer.length ) { 790 // Create WooCommerce-style notice 791 const noticeClass = 792 type === 'error' 793 ? 'woocommerce-error' 794 : 'woocommerce-message';
795 const notice = $( '<div>' ) 796 .addClass( noticeClass ) 797 .attr( 'role', noticeRole ) 798 .attr( 'aria-live', ariaLive ) 799 .attr( 'aria-atomic', 'true' ) 800 .text( message ); 801 802 // Clear existing notices and add new one 803 noticeContainer.empty().append( notice ); 804 805 // Scroll to notice 806 $( 'html, body' ).animate( 807 { 808 scrollTop: noticeContainer.offset().top - 100, 809 }, 810 500 811 ); 812 813 // Auto-dismiss success notices after 10 seconds (increased from 5 for accessibility) 814 if ( type === 'success' ) { 815 setTimeout( function () { 816 notice.fadeOut( function () { 817 $( this ).remove(); 818 } ); 819 }, 10000 ); 820 } 821 } else { 822 // Fallback: create a temporary inline notice instead of disruptive alert(). 823 const fallbackNotice = $( '<div>' ) 824 .addClass( 'power-coupons-inline-notice' ) 825 .attr( 'role', noticeRole ) 826 .attr( 'aria-atomic', 'true' ) 827 .text( message ) 828 .css( { 829 position: 'fixed', 830 top: '20px', 831 right: '20px', 832 zIndex: 999999, 833 padding: '12px 20px', 834 borderRadius: '4px', 835 maxWidth: '400px', 836 background: type === 'error' ? '#fee2e2' : '#f0fdf4', 837 color: type === 'error' ? '#991b1b' : '#166534', 838 border: 839 '1px solid ' + 840 ( type === 'error' ? '#fecaca' : '#86efac' ), 841 boxShadow: '0 4px 12px rgba(0,0,0,0.15)', 842 fontSize: '14px', 843 lineHeight: '1.5', 844 } ); 845 846 $( 'body' ).append( fallbackNotice ); 847 848 setTimeout( function () { 849 fallbackNotice.fadeOut( function () { 850 $( this ).remove(); 851 } ); 852 }, 5000 ); 853 } 854 }, 855 }; 856 857 /** 858 * Initialize on DOM ready 859 * 860 * Waits for jQuery and DOM to be ready before initializing. 861 */ 862 $( document ).ready( function () { 863 PowerCoupons.init(); 864 } ); 865} )( 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.