1"use strict"; 2 3(function ($) { 4 'use strict'; 5 6 /** 7 * AM Announce Kit Frontend JavaScript 8 * Handles drawer functionality and user interactions. 9 */ 10 const AnnouncementDrawer = { 11 // Store timer references to clear them if needed 12 tooltipTimers: [], 13 14 /** 15 * Initialize the drawer functionality. 16 */ 17 init: function () { 18 this.bindEvents(); 19 this.setupDrawer(); 20 this.addAnnouncementCountBadge(); 21 this.initializeDismissableTooltip(); 22 }, 23 24 /** 25 * Cookie management utilities. 26 */ 27 cookies: { 28 set: function(name, value, days) { 29 const date = new Date(); 30 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 31 const expires = "expires=" + date.toUTCString(); 32 document.cookie = name + "=" + value + ";" + expires + ";path=/"; 33 }, 34 35 get: function(name) { 36 const nameEQ = name + "="; 37 const ca = document.cookie.split(';'); 38 for(let i = 0; i < ca.length; i++) { 39 let c = ca[i]; 40 while (c.charAt(0) === ' ') c = c.substring(1, c.length); 41 if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 42 } 43 return null; 44 }, 45 46 remove: function(name) { 47 document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;"; 48 } 49 }, 50 51 /** 52 * Add announcement count badge to drawer triggers. 53 */ 54 addAnnouncementCountBadge: function() { 55 // Only add badge if there are announcements 56 if (!amAnnounceKit.hasAnnouncements) { 57 return; 58 } 59 60 // Check if the announcement has been dismissed 61 const latestAnnouncement = amAnnounceKit.latestAnnouncement || null; 62 if (latestAnnouncement && latestAnnouncement.id) { 63 const cookieName = 'am_tooltip_dismissed_' + latestAnnouncement.id; 64 const isDismissed = this.cookies.get(cookieName); 65 66 // Don't show badge if tooltip has been dismissed 67 if (isDismissed) { 68 return; 69 } 70 } 71 72 // Find all drawer trigger elements 73 const triggers = document.querySelectorAll('.am-announce-kit-drawer-trigger, .am-announce-kit-drawer-trigger a'); 74 75 triggers.forEach(trigger => { 76 // Skip if this is a container and not the actual clickable element 77 if (trigger.tagName === 'LI' || trigger.tagName === 'DIV') { 78 return; 79 } 80 81 // For links within drawer trigger containers 82 if (!trigger.classList.contains('am-announce-kit-drawer-trigger')) { 83 const parent = trigger.closest('.am-announce-kit-drawer-trigger'); 84 if (parent) { 85 trigger.classList.add('am-announce-kit-drawer-trigger'); 86 } 87 } 88 89 // Check if badge already exists 90 if (trigger.querySelector('.am-announcement-count-badge')) { 91 return; 92 } 93 94 // Create and add the count badge 95 const badge = document.createElement('span'); 96 badge.className = 'am-announcement-count-badge'; 97 badge.textContent = '1'; 98 trigger.style.position = 'relative'; 99 trigger.appendChild(badge); 100 }); 101 }, 102 103 /** 104 * Initialize dismissable tooltip for drawer triggers. 105 */ 106 initializeDismissableTooltip: function() { 107 // First, get the latest announcement data from the localized script data 108 const latestAnnouncement = amAnnounceKit.latestAnnouncement || null; 109 110 if (!latestAnnouncement || !latestAnnouncement.title) { 111 return; // No announcement to show 112 } 113 114 // Find all drawer trigger elements (including links within trigger containers) 115 const triggers = document.querySelectorAll('.am-announce-kit-drawer-trigger, .am-announce-kit-drawer-trigger a'); 116 117 triggers.forEach(trigger => { 118 // Skip if this is a container and not the actual clickable element 119 if (trigger.tagName === 'LI' || trigger.tagName === 'DIV') { 120 return; 121 } 122 123 // For links within drawer trigger containers 124 if (!trigger.classList.contains('am-announce-kit-drawer-trigger')) { 125 const parent = trigger.closest('.am-announce-kit-drawer-trigger'); 126 if (parent) { 127 trigger.classList.add('am-announce-kit-drawer-trigger'); 128 } 129 } 130 131 // Check if this specific announcement tooltip has been dismissed 132 const cookieName = 'am_tooltip_dismissed_' + latestAnnouncement.id; 133 const isDismissed = this.cookies.get(cookieName); 134 135 if (!isDismissed) { 136 // Add 3-second delay before showing tooltip 137 const timer = setTimeout(() => { 138 // Double-check cookie wasn't set while we were waiting 139 if (!this.cookies.get(cookieName)) { 140 this.createDismissableTooltip(trigger, latestAnnouncement.title, latestAnnouncement.id); 141 } 142 }, 3000); 143 144 // Store timer reference so we can clear it if needed 145 this.tooltipTimers.push(timer); 146 } 147 }); 148 }, 149 150 /** 151 * Create a dismissable tooltip element. 152 */ 153 createDismissableTooltip: function(trigger, title, announcementId) { 154 // Remove any existing tooltip 155 const existingTooltip = trigger.querySelector('.am-dismissable-tooltip'); 156 if (existingTooltip) { 157 existingTooltip.remove(); 158 } 159 160 // Create tooltip container 161 const tooltip = document.createElement('div'); 162 tooltip.className = 'am-dismissable-tooltip'; 163 tooltip.setAttribute('role', 'tooltip'); 164 165 // Create tooltip content
166 const tooltipInner = document.createElement('div'); 167 tooltipInner.className = 'am-dismissable-tooltip__inner'; 168 169 // Add NEW badge 170 const newBadge = document.createElement('span'); 171 newBadge.className = 'am-dismissable-tooltip__new-badge'; 172 newBadge.textContent = 'NEW'; 173 tooltipInner.appendChild(newBadge); 174 175 // Add title 176 const tooltipTitle = document.createElement('span'); 177 tooltipTitle.className = 'am-dismissable-tooltip__title'; 178 tooltipTitle.textContent = title; 179 tooltipInner.appendChild(tooltipTitle); 180 181 // Add dismiss button 182 const dismissBtn = document.createElement('button'); 183 dismissBtn.className = 'am-dismissable-tooltip__dismiss'; 184 dismissBtn.setAttribute('aria-label', 'Dismiss tooltip'); 185 dismissBtn.innerHTML = '<span aria-hidden="true">×</span>'; 186 187 // Add arrow 188 const arrow = document.createElement('div'); 189 arrow.className = 'am-dismissable-tooltip__arrow'; 190 191 // Assemble tooltip 192 tooltip.appendChild(tooltipInner); 193 tooltip.appendChild(dismissBtn); 194 tooltip.appendChild(arrow); 195 196 // Position tooltip relative to trigger 197 trigger.style.position = 'relative'; 198 trigger.appendChild(tooltip); 199 200 // Show tooltip with animation 201 setTimeout(() => { 202 tooltip.classList.add('am-dismissable-tooltip--visible'); 203 }, 100); 204 205 // Auto-hide after 20 seconds (no cookie) 206 const autoHideTimer = setTimeout(() => { 207 tooltip.classList.remove('am-dismissable-tooltip--visible'); 208 setTimeout(() => { 209 tooltip.remove(); 210 }, 300); 211 }, 20000); 212 213 // Handle dismiss 214 dismissBtn.addEventListener('click', (e) => { 215 e.preventDefault(); 216 e.stopPropagation(); 217 218 // Clear auto-hide timer 219 clearTimeout(autoHideTimer); 220 221 // Set cookie to remember dismissal 222 const cookieName = 'am_tooltip_dismissed_' + (announcementId || 'default'); 223 this.cookies.set(cookieName, '1', 30); // Dismiss for 30 days 224 225 // Hide and remove tooltip 226 tooltip.classList.remove('am-dismissable-tooltip--visible'); 227 setTimeout(() => { 228 tooltip.remove(); 229 }, 300); 230 231 // Remove count badges from all triggers 232 const badges = document.querySelectorAll('.am-announcement-count-badge'); 233 badges.forEach(badge => badge.remove()); 234 235 // Track dismissal 236 this.trackEvent('tooltip_dismissed', { 237 announcement_id: announcementId 238 }); 239 }); 240 }, 241 242 /** 243 * Bind event handlers. 244 */ 245 bindEvents: function () { 246 // Open drawer when trigger is clicked 247 $(document).on('click', '.am-announce-kit-drawer-trigger', this.openDrawer.bind(this)); 248 249 // Close drawer when close button is clicked 250 $(document).on('click', '.am-announcement-drawer-widget__close', this.closeDrawer.bind(this)); 251 252 // Close drawer when clicking outside 253 $(document).on('click', function (e) { 254 if (!$(e.target).closest('.am-announcement-drawer-widget').length && 255 !$(e.target).closest('.am-announce-kit-drawer-trigger').length) { 256 AnnouncementDrawer.closeDrawer(); 257 } 258 259 // Close feedback form when clicking outside the feedback section 260 if (!$(e.target).closest('.am-announcement-item__feedback').length) { 261 $('.am-announcement-item__feedback').each(function() { 262 const $feedbackSection = $(this); 263 const $selectedReaction = $feedbackSection.find('.am-feedback-reaction--selected'); 264 265 if ($selectedReaction.length) { 266 // Remove selected state from all reactions 267 $feedbackSection.find('.am-feedback-reaction').removeClass('am-feedback-reaction--selected'); 268 269 // Hide the feedback form 270 $feedbackSection.find('.am-feedback-form').slideUp(300); 271 272 // Clear the reaction value 273 $feedbackSection.find('.am-feedback-form__reaction').val(''); 274 } 275 }); 276 } 277 }); 278 279 // Close drawer on escape key 280 $(document).on('keydown', function (e) { 281 if (e.key === 'Escape') { 282 AnnouncementDrawer.closeDrawer(); 283 } 284 }); 285 286 // Handle CTA button clicks 287 $(document).on('click', '.am-announcement-item__cta-button', this.handleCTAClick.bind(this)); 288 289 // Handle load more button clicks 290 $(document).on('click', '.am-announcement-drawer-widget__load-more-button', this.handleLoadMoreClick.bind(this)); 291 292 // Handle feedback reactions 293 $(document).on('click', '.am-feedback-reaction', this.handleFeedbackReaction.bind(this)); 294 295 // Handle feedback form submission 296 $(document).on('submit', '.am-feedback-form__container', this.handleFeedbackSubmission.bind(this)); 297 298 // Handle subscription component interactions 299 $(document).on('click', '#am-subscription-trigger', this.handleSubscriptionTriggerClick.bind(this)); 300 $(document).on('click', '#am-email-subscription-option', this.handleEmailSubscriptionClick.bind(this)); 301 $(document).on('click', '#am-push-subscription-option', this.handlePushSub
301scriptionClick.bind(this)); 302 $(document).on('click', '#am-email-form-close', this.handleEmailFormClose.bind(this)); 303 }, 304 305 /** 306 * Setup the drawer element. 307 */ 308 setupDrawer: function () { 309 const $drawer = $('#am-announcement-drawer'); 310 const $overlay = $('#am-announcement-drawer-overlay'); 311 312 if ($drawer.length && $overlay.length) { 313 // Initialize load more button with next page 314 const $loadMoreButton = $('#am-announcement-load-more-button'); 315 if ($loadMoreButton.length) { 316 $loadMoreButton.data('next-page', 2); 317 } 318 } 319 }, 320 321 /** 322 * Open the announcement drawer. 323 */ 324 openDrawer: function (e) { 325 e.preventDefault(); 326 327 const $drawer = $('#am-announcement-drawer'); 328 const $overlay = $('#am-announcement-drawer-overlay'); 329 330 if ($drawer.length) { 331 // Clear any pending tooltip timers to prevent them from showing 332 this.tooltipTimers.forEach(timer => clearTimeout(timer)); 333 this.tooltipTimers = []; 334 335 // Set cookie to dismiss tooltip when drawer is opened 336 if (amAnnounceKit.latestAnnouncement && amAnnounceKit.latestAnnouncement.id) { 337 const cookieName = 'am_tooltip_dismissed_' + amAnnounceKit.latestAnnouncement.id; 338 this.cookies.set(cookieName, '1', 30); // Dismiss for 30 days 339 340 // Remove any visible tooltips 341 const tooltips = document.querySelectorAll('.am-dismissable-tooltip'); 342 tooltips.forEach(tooltip => { 343 tooltip.classList.remove('am-dismissable-tooltip--visible'); 344 setTimeout(() => { 345 tooltip.remove(); 346 }, 300); 347 }); 348 349 // Remove count badges from all triggers 350 const badges = document.querySelectorAll('.am-announcement-count-badge'); 351 badges.forEach(badge => badge.remove()); 352 } 353 354 // Add loading state 355 $drawer.addClass('am-announcement-drawer-widget--loading'); 356 357 // Show overlay 358 $overlay.addClass('am-announcement-drawer-overlay--visible'); 359 360 // Open drawer 361 $drawer.addClass('am-announcement-drawer-widget--open'); 362 363 // Remove loading state after animation 364 setTimeout(function () { 365 $drawer.removeClass('am-announcement-drawer-widget--loading'); 366 }, 300); 367 368 // Prevent body scroll 369 $('body').addClass('am-announcement-drawer-open'); 370 371 // Track analytics if available 372 this.trackEvent('drawer_opened'); 373 } 374 }, 375 376 /** 377 * Close the announcement drawer. 378 */ 379 closeDrawer: function (e) { 380 if (e) { 381 e.preventDefault(); 382 } 383 384 const $drawer = $('#am-announcement-drawer'); 385 const $overlay = $('#am-announcement-drawer-overlay'); 386 387 if ($drawer.length) { 388 // Add closing animation class 389 $drawer.addClass('am-announcement-drawer-widget--closing'); 390 391 // Hide overlay 392 $overlay.removeClass('am-announcement-drawer-overlay--visible'); 393 394 // Close drawer after animation 395 $drawer.removeClass('am-announcement-drawer-widget--open am-announcement-drawer-widget--closing'); 396 397 // Re-enable body scroll 398 $('body').removeClass('am-announcement-drawer-open'); 399 400 // Track analytics if available 401 AnnouncementDrawer.trackEvent('drawer_closed'); 402 } 403 }, 404 405 /** 406 * Handle call-to-action button clicks. 407 */ 408 handleCTAClick: function (e) { 409 const $button = $(e.currentTarget); 410 const href = $button.attr('href'); 411 const announcementId = $button.closest('.am-announcement-item').data('announcement-id'); 412 413 // Track CTA click 414 this.trackEvent('cta_clicked', { 415 announcement_id: announcementId, 416 cta_url: href 417 }); 418 }, 419 420 /** 421 * Handle load more button clicks. 422 */ 423 handleLoadMoreClick: function (e) { 424 const $button = $(e.currentTarget); 425 const nextPage = $button.data('next-page') || 2; 426 427 this.loadMoreAnnouncements(nextPage); 428 }, 429 430 /** 431 * Load more announcements via AJAX. 432 */ 433 loadMoreAnnouncements: function (page) { 434 const $button = $('.am-announcement-drawer-widget__load-more-button'); 435 const $container = $('.am-announcement-drawer-widget__content'); 436 437 // Disable button and show loading 438 $button.prop('disabled', true).text(amAnnounceKit.strings.loading || 'Loading...'); 439 440 $.ajax({ 441 url: amAnnounceKit.ajaxUrl, 442 type: 'POST', 443 data: { 444 action: 'am_announce_kit_load_more', 445 page: page, 446 per_page: 5, 447 nonce: amAnnounceKit.nonce 448 }, 449 success: function (response) { 450 if (response.success) { 451 // Append new announcements if there are any 452 if (response.data.html) { 453 $container.append(response.data.html); 454 } 455 456 // Update button for next page 457 const nextPage = response.data.pagination.current_page + 1; 458 $button.data('next-page', nextPage); 459 460 // Disable button if no more pages but keep it visible 461 if (!response.data.pagination.has_more) { 462 $button.prop('disabled', true).text(amAnnounceKit.strings.noM
462oreAnnouncements || 'No More Announcements'); 463 } else { 464 $button.prop('disabled', false).text(amAnnounceKit.strings.loadMore || 'Load More'); 465 } 466 } else { 467 alert(amAnnounceKit.strings.error || 'An error occurred. Please try again.'); 468 $button.prop('disabled', false).text(amAnnounceKit.strings.loadMore || 'Load More'); 469 } 470 }, 471 error: function () { 472 alert(amAnnounceKit.strings.error || 'An error occurred. Please try again.'); 473 $button.prop('disabled', false).text(amAnnounceKit.strings.loadMore || 'Load More'); 474 } 475 }); 476 }, 477 478 /** 479 * Track events for analytics. 480 */ 481 trackEvent: function (eventName, data = {}) { 482 // Google Analytics 4 483 if (typeof gtag !== 'undefined') { 484 gtag('event', eventName, { 485 event_category: 'announcement_drawer', 486 ...data 487 }); 488 } 489 490 // Google Analytics Universal 491 if (typeof ga !== 'undefined') { 492 ga('send', 'event', 'announcement_drawer', eventName, data); 493 } 494 495 // Custom event for other tracking 496 $(document).trigger('am_announce_kit_event', { 497 event: eventName, 498 data: data 499 }); 500 }, 501 502 /** 503 * Handle feedback reaction clicks. 504 */ 505 handleFeedbackReaction: function (e) { 506 const $button = $(e.currentTarget); 507 const $feedbackSection = $button.closest('.am-announcement-item__feedback'); 508 const reaction = $button.data('reaction'); 509 const announcementId = $button.closest('.am-announcement-item').data('announcement-id'); 510 const wasSelected = $button.hasClass('am-feedback-reaction--selected'); 511 512 // If clicking the same emoji that's already selected, close everything 513 if (wasSelected) { 514 // Remove selected state from all reactions 515 $feedbackSection.find('.am-feedback-reaction').removeClass('am-feedback-reaction--selected'); 516 517 // Hide the feedback form 518 $feedbackSection.find('.am-feedback-form').slideUp(300); 519 520 // Clear the reaction value 521 $feedbackSection.find('.am-feedback-form__reaction').val(''); 522 523 // Track deselection 524 this.trackEvent('feedback_reaction_deselected', { 525 announcement_id: announcementId, 526 reaction: reaction 527 }); 528 529 return; 530 } 531 532 // Remove selected state from all reactions 533 $feedbackSection.find('.am-feedback-reaction').removeClass('am-feedback-reaction--selected'); 534 535 // Add selected state to clicked reaction 536 $button.addClass('am-feedback-reaction--selected'); 537 538 // Set the reaction value in the form 539 $feedbackSection.find('.am-feedback-form__reaction').val(reaction); 540 541 // Show the feedback form 542 $feedbackSection.find('.am-feedback-form').slideDown(300); 543 544 // Focus on the text input 545 setTimeout(() => { 546 $feedbackSection.find('.am-feedback-form__text-input').focus(); 547 }, 350); 548 549 // Track reaction selection 550 this.trackEvent('feedback_reaction_selected', { 551 announcement_id: announcementId, 552 reaction: reaction 553 }); 554 }, 555 556 /** 557 * Handle feedback form submission. 558 */ 559 handleFeedbackSubmission: function (e) { 560 e.preventDefault(); 561 562 const $form = $(e.currentTarget); 563 const $feedbackSection = $form.closest('.am-announcement-item__feedback'); 564 const announcementId = $form.data('announcement-id'); 565 const feedbackText = $form.find('.am-feedback-form__text-input').val().trim(); 566 const reaction = $form.find('.am-feedback-form__reaction').val(); 567 const userEmail = $form.find('.am-feedback-form__email-input').val().trim(); 568 569 // Validate required fields 570 if (!feedbackText) { 571 alert(amAnnounceKit.strings.feedbackRequired || 'Please enter your feedback.'); 572 return; 573 } 574 575 // Check if user is logged in 576 const isLoggedIn = amAnnounceKit.isLoggedIn || false; 577 578 // If not logged in, show email field if not already shown 579 if (!isLoggedIn && !$form.find('.am-feedback-form__email-group').is(':visible')) { 580 $form.find('.am-feedback-form__email-group').slideDown(300); 581 $form.find('.am-feedback-form__email-input').focus(); 582 return; 583 } 584 585 // If not logged in, validate email 586 if (!isLoggedIn && !userEmail) { 587 alert(amAnnounceKit.strings.emailRequired || 'Please enter your email address.'); 588 return; 589 } 590 591 // Disable submit button 592 const $submitButton = $form.find('.am-feedback-form__submit'); 593 $submitButton.prop('disabled', true); 594 595 // Prepare form data 596 const formData = { 597 action: 'am_announce_kit_submit_feedback', 598 announcement_id: announcementId, 599 feedback_text: feedbackText, 600 reaction: reaction, 601 user_email: userEmail, 602 nonce: amAnnounceKit.feedbackNonce 603 }; 604 605 // Submit feedback via AJAX 606 $.ajax({ 607 url: amAnnounceKit.ajaxUrl, 608 type: 'POST', 609 data: formData, 610 success: function (response) { 611 if (response.success) { 612 // Show success message 613 $feedbackSection.html('<div class="am-feedback-success"><p>' + (amAnnounceKit.strings.feedbackSuccess || 'Thank you for your feedback!') + '</p></div>'); 614 } else { 615 alert(amAnnounceKit.strings.feedbackError || 'Failed to submit feedback. Please try again.'); 616 } 617 }, 618 error: function () { 619 alert(amAnnounceKit.strings.feedbackError || 'Failed to submit feedback. Please try again.'); 620 }, 621 complete: function () { 622 $submitButton.prop('disabled', false); 623 } 624 }); 625 }, 626 627 /** 628 * Handle subscription trigger click. 629 */ 630 handleSubscriptionTriggerClick: function (e) { 631 e.preventDefault(); 632 const $options = $('#am-subscription-options'); 633 const $trigger = $('#am-subscription-trigger'); 634 635 if ($options.hasClass('am-announcement-drawer-widget__subscription-options--visible')) { 636 // Hide options 637 $options.removeClass('am-announcement-drawer-widget__subscription-options--visible'); 638 $trigger.removeClass('am-announcement-drawer-widget__subscription-trigger--active'); 639 } else { 640 // Show options 641 $options.addClass('am-announcement-drawer-widget__subscription-options--visible'); 642 $trigger.addClass('am-announcement-drawer-widget__subscription-trigger--active'); 643 } 644 }, 645 646 /** 647 * Handle email subscription option click. 648 */ 649 handleEmailSubscriptionClick: function (e) { 650 e.preventDefault(); 651 // Hide the buttons and show the form 652 const $buttons = $('#am-subscription-buttons'); 653 const $form = $('#am-email-subscription-form'); 654 655 if ($buttons.length && $form.length) { 656 $buttons.hide(); 657 $form.show(); 658 } 659 }, 660 661 /** 662 * Handle push subscription option click. 663 */
664 handlePushSubscriptionClick: function (e) { 665 e.preventDefault(); 666 // For now, just show a message that push notifications are not implemented yet 667 alert(amAnnounceKit.strings.pushNotificationsNotImplemented || 'Push notifications will be implemented in a future update.'); 668 }, 669 670 /** 671 * Handle email form close button click. 672 */ 673 handleEmailFormClose: function (e) { 674 e.preventDefault(); 675 // Show the buttons again and hide the form 676 const $buttons = $('#am-subscription-buttons'); 677 const $form = $('#am-email-subscription-form'); 678 679 if ($buttons.length && $form.length) { 680 $buttons.show(); 681 $form.hide(); 682 } 683 } 684 }; 685 686 // Initialize when document is ready 687 $(document).ready(function () { 688 AnnouncementDrawer.init(); 689 }); 690 691 // Make available globally 692 window.AnnouncementDrawer = AnnouncementDrawer; 693 694})(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.