1/** 2 * Share Buttons & AI-powered Summaries Plugin - JavaScript 3 * Advanced functionality for tooltips, hover effects, responsive behavior, click tracking 4 * and the visitor-facing AI summary generation button (v2.0.0). 5 * Version: 2.5.0 6 */ 7 8/** 9 * Optional dark-background detection. 10 * 11 * Active only when the admin selects the "auto" dark-mode adaptation mode. 12 * Walks up from each share-buttons container looking for the first opaque 13 * background, computes its WCAG relative luminance, and toggles the 14 * .is-on-dark class accordingly. Runs once on DOMContentLoaded and again 15 * when the OS-level prefers-color-scheme flips (themes that wire dark mode 16 * to that media query change their background at that moment, so we need 17 * to re-measure). No MutationObserver is installed to keep runtime cost 18 * negligible. 19 */ 20(function() { 21 if ( ! window.ayudawpAissL10n || 'auto' !== window.ayudawpAissL10n.darkAdaptMode ) { 22 return; 23 } 24 25 function detect() { 26 var nodes = document.querySelectorAll( 27 '.ayudawp-share-buttons:not(.dark):not(.custom), .ayudawp-aiss-summary' 28 ); 29 for ( var i = 0; i < nodes.length; i++ ) { 30 var node = nodes[ i ].parentElement; 31 var bg = ''; 32 while ( node && node !== document.documentElement ) { 33 var computed = window.getComputedStyle( node ).backgroundColor; 34 if ( computed && 'rgba(0, 0, 0, 0)' !== computed && 'transparent' !== computed ) { 35 bg = computed; 36 break; 37 } 38 node = node.parentElement; 39 } 40 if ( ! bg ) { 41 continue; 42 } 43 var match = bg.match( /\d+(\.\d+)?/g ); 44 if ( ! match || match.length < 3 ) { 45 continue; 46 } 47 var r = parseFloat( match[ 0 ] ) / 255; 48 var g = parseFloat( match[ 1 ] ) / 255; 49 var b = parseFloat( match[ 2 ] ) / 255; 50 var lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; 51 nodes[ i ].classList.toggle( 'is-on-dark', lum < 0.5 ); 52 } 53 } 54 55 if ( 'loading' === document.readyState ) { 56 document.addEventListener( 'DOMContentLoaded', detect ); 57 } else { 58 detect(); 59 } 60 61 if ( window.matchMedia ) { 62 var mq = window.matchMedia( '(prefers-color-scheme: dark)' ); 63 if ( mq.addEventListener ) { 64 mq.addEventListener( 'change', detect ); 65 } else if ( mq.addListener ) { 66 mq.addListener( detect ); 67 } 68 } 69})(); 70 71/** 72 * Share buttons runtime (rewritten without jQuery in v2.5.0) 73 * 74 * Everything below used to run inside jQuery(document).ready. The plugin no
75 * longer declares a jQuery dependency, so block themes that do not load it on 76 * the front end stop pulling in ~85 KB to show share buttons. Three things 77 * went away rather than being ported, because they were doing work the 78 * stylesheet already does or the browser does better: 79 * 80 * - the hover handler that wrote `transform: translateY(-2px)` inline, which 81 * the CSS :hover rule had been applying since 1.6.0; 82 * - the resize handler that rewrote the icons-only grid with inline styles, 83 * now a media query; 84 * - the DOMNodeInserted listener, a Mutation Event removed from Chrome in 85 * version 127, replaced by a MutationObserver that only watches for share 86 * blocks arriving. 87 * 88 * @since 2.5.0 89 */ 90(function () { 91 'use strict'; 92 93 /** 94 * AI platforms with no way to pre-fill the prompt from the URL, so the 95 * prompt is copied to the clipboard and the site opened empty. 96 * 97 * Checked again on 12 Sep 2026 against each platform, with Perplexity's 98 * working ?q= as the positive control: Meta AI does pre-fill from 99 * ?prompt= and left this list in 2.5.0; Gemini, DeepSeek, Copilot and 100 * Qwen still do not. 101 */ 102 var copyPromptPlatforms = ['gemini', 'deepseek', 'copilot', 'qwen']; 103 104 /** 105 * Landing pages for the copy-prompt platforms. 106 */ 107 var platformBaseUrls = { 108 'gemini': 'https://gemini.google.com/app', 109 'deepseek': 'https://chat.deepseek.com/', 110 'copilot': 'https://copilot.microsoft.com/', 111 'qwen': 'https://chat.qwen.ai/' 112 }; 113 114 /** Platforms that open in a sized popup window instead of a tab. */ 115 var popupPlatforms = ['twitter', 'linkedin', 'facebook', 'telegram', 'reddit', 'bluesky']; 116 117 /** Buttons that act on the current page instead of opening a destination. */ 118 var actionPlatforms = ['copy_link', 'web_share']; 119 120 var L = window.ayudawpAissL10n || {}; 121 var tooltipEl = null; 122 123 function l10n(key, fallback) { 124 return (L && L[key]) ? L[key] : fallback; 125 } 126 127 /** 128 * Platform of a button, from its data attribute or, failing that, its class. 129 * 130 * @param {Element} btn Button element. 131 * @return {string} Platform key, '' when unknown. 132 */ 133 function detectPlatform(btn) { 134 var platform = btn.getAttribute('data-platform'); 135 if (platform) { 136 return platform; 137 } 138 var known = ['twitter', 'linkedin', 'facebook', 'telegram', 'whatsapp', 'email', 'raindrop', 139 'reddit', 'bluesky', 'line', 'mastodon', 'threads', 'pinterest', 140 'claude', 'chatgpt', 'google-ai', 'gemini', 'grok', 'perplexity', 141 'deepseek', 'mistral', 'copilot', 'qwen', 'meta-ai', 'copy-link', 'web-share']; 142 for (var i = 0; i < known.length; i++) { 143 if (btn.classList.contains(known[i])) { 144 return known[i].replace(/-/g, '_'); 145 } 146 } 147 return ''; 148 } 149 150 /** 151 * Tooltip text for a button, or '' when it needs none. 152 * 153 * @param {Element} btn Button element. 154 * @param {string} platform Platform key. 155 * @return {string} Tooltip text. 156 */ 157 function tooltipFor(btn, platform) { 158 if (copyPromptPlatforms.indexOf(platform) !== -1) { 159 var block = btn.closest('.ayudawp-share-buttons'); 160 var short = block && block.classList.contains('icons-only'); 161 return short
162 ? l10n(platform + 'TooltipShort', l10n('copyPromptShort', 'Copy prompt & open')) 163 : l10n(platform + 'TooltipLong', l10n('copyPromptLong', 'Copy prompt and open')); 164 } 165 var names = L.platformNames || {}; 166 return names[platform] || ''; 167 } 168 169 /** 170 * Single shared tooltip element. 171 * 172 * One node reused by every button, instead of one node plus two listeners 173 * per button as before 2.5.0 â a 24-button block was adding 24 divs to 174 * <body> on every page load. 175 * 176 * @return {Element} The tooltip element. 177 */ 178 function getTooltip() { 179 if (!tooltipEl) { 180 tooltipEl = document.createElement('div'); 181 tooltipEl.className = 'ayudawp-tooltip'; 182 tooltipEl.setAttribute('aria-hidden', 'true'); 183 document.body.appendChild(tooltipEl); 184 } 185 return tooltipEl; 186 } 187 188 function showTooltip(btn) { 189 var platform = detectPlatform(btn); 190 var text = tooltipFor(btn, platform); 191 if (!text) { 192 return; 193 } 194 var tip = getTooltip(); 195 tip.textContent = text; 196 tip.classList.add('active'); 197 198 var rect = btn.getBoundingClientRect(); 199 var top = rect.top + window.pageYOffset - tip.offsetHeight - 8; 200 var left = rect.left + window.pageXOffset + (rect.width / 2) - (tip.offsetWidth / 2); 201 tip.style.top = Math.max(0, top) + 'px'; 202 tip.style.left = Math.max(0, left) + 'px'; 203 } 204 205 function hideTooltip() { 206 if (tooltipEl) { 207 tooltipEl.classList.remove('active'); 208 } 209 } 210 211 /** 212 * Report a click to the analytics endpoint, fire and forget. 213 * 214 * sendBeacon since 2.5.0: the browser queues the request and delivers it 215 * even if the page is being replaced by the destination, which is exactly 216 * what happens when the click opens another site. 217 * 218 * @param {string} platform Platform key. 219 * @param {Element} btn Clicked button. 220 */ 221 function trackClick(platform, btn) { 222 if (!L.trackUrl || !platform) { 223 return; 224 } 225 226 var container = btn.closest('[data-post-id]'); 227 var postId = container ? container.getAttribute('data-post-id') : ''; 228 229 if (!postId) { 230 var article = document.querySelector('article[id^="post-"]'); 231 if (article) { 232 postId = article.id.replace('post-', ''); 233 } 234 } 235 236 var payload = JSON.stringify({ 237 platform: platform.replace(/-/g, '_'), 238 post_id: parseInt(postId, 10) || 0 239 }); 240 241 try { 242 if (navigator.sendBeacon) { 243 navigator.sendBeacon(L.trackUrl, new Blob([payload], { type: 'application/json' })); 244 return; 245 } 246 if (window.fetch) { 247 window.fetch(L.trackUrl, { 248 method: 'POST', 249 headers: { 'Content-Type': 'application/json' }, 250 body: payload, 251 keepalive: true, 252 credentials: 'same-origin' 253 }).catch(function () {}); 254 return; 255 } 256 var xhr = new XMLHttpRequest(); 257 xhr.open('POST', L.trackUrl, true); 258 xhr.setRequestHeader('Content-Type', 'application/json'); 259 xhr.send(payload); 260 } catch (err) { 261 // Tracking must never break sharing. 262 } 263 } 264 265 /** 266 * Copy text to the clipboard, resolving when it is done. 267 * 268 * @param {string} text Text to copy. 269 * @return {Promise} Resolves on success. 270 */ 271 function copyToClipboard(text) { 272 if (navigator.clipboard && window.isSecureContext) { 273 return navigator.clipboard.writeText(text); 274 } 275 276 return new Promise(function (resolve, reject) { 277 var area = document.createElement('textarea'); 278 area.value = text; 279 area.setAttribute('readonly', 'readonly'); 280 area.style.position = 'fixed'; 281 area.style.left = '-999999px'; 282 area.style.top = '-999999px'; 283 document.body.appendChild(area); 284 area.focus(); 285 area.select(); 286 try { 287 if (document.execCommand('copy')) { 288 resolve(); 289 } else { 290 reject(); 291 } 292 } catch (err) { 293 reject(err); 294 } finally { 295 area.remove(); 296 } 297 }); 298 } 299 300 /** 301 * Transient on-screen message. 302 * 303 * role="status" so screen readers announce it: before 2.5.0 the copy 304 * confirmation was visual only. 305 * 306 * @param {string} message Text to show. 307 */ 308 function showNotification(message) { 309 var existing = document.querySelector('.ayudawp-notification'); 310 if (existing) { 311 existing.remove(); 312 } 313 314 var note = document.createElement('div'); 315 note.className = 'ayudawp-notification'; 316 note.setAttribute('role', 'status'); 317 note.textContent = message; 318 document.body.appendChild(note); 319 320 window.setTimeout(function () { 321 note.classList.add('is-visible'); 322 }, 20); 323 324 window.setTimeout(function () { 325 note.classList.remove('is-visible'); 326 window.setTimeout(function () { 327 if (note.parentNode) { 328 note.parentNode.removeChild(note); 329 } 330 }, 400); 331 }, 2500); 332 } 333 334 /** 335 * Copy the prompt and open the platform empty. 336 * 337 * @param {string} url Button URL carrying the prompt. 338 * @param {string} platform Platform key. 339 */ 340 function handleCopyPromptClick(url, platform) { 341 var base = platformBaseUrls[platform] || url.split('?')[0]; 342 var prompt = ''; 343 344 try { 345 var query = url.split('?')[1] || ''; 346 var params = new URLSearchParams(query); 347 prompt = params.get('prompt') || params.get('q') || ''; 348 } catch (err) { 349 prompt = ''; 350 } 351 352 if (!prompt) { 353 window.open(base, '_blank', 'noopener,noreferrer'); 354 return; 355 } 356 357 copyToClipboard(decodeURIComponent(prompt)).then(function () { 358 showNotification(l10n('promptCopied', 'Prompt copied!')); 359 window.setTimeout(function () { 360 window.open(base, '_blank', 'noopener,noreferrer'); 361 }, 500); 362 }).catch(function () { 363 window.open(base, '_blank', 'noopener,noreferrer'); 364 }); 365 } 366 367 /** 368 * Copy the post link (v2.5.0). 369 * 370 * @param {string} url Link to copy. 371 */ 372 function handleCopyLink(url) { 373 copyToClipboard(url).then(function () { 374 showNotification(l10n('linkCopied', 'Link copied to clipboard!')); 375 }).catch(function () { 376 showNotification(l10n('copyFailed', 'Could not copy.')); 377 }); 378 } 379 380 /** 381 * Hand the post to the operating system share sheet (v2.5.0). 382 * 383 * @param {Element} btn Button element. 384 * @param {string} url Link to share. 385 */ 386 function handleWebShare(btn, url) { 387 // No API, or the browser refuses the call (it is only available in a 388 // secure context, so plain http never has it): fall back to copying 389 // the link rather than doing nothing, which is what a visitor who 390 // manages to click it would otherwise get. 391 if (!navigator.share) { 392 handleCopyLink(url); 393 return; 394 } 395 396 var data = { url: url }; 397 var title = btn.getAttribute('data-title'); 398 if (title) { 399 data.title = title; 400 } 401 402 try { 403 var shared = navigator.share(data);
404 if (shared && shared.catch) { 405 shared.catch(function (err) { 406 // AbortError is the visitor closing the sheet: not a failure. 407 if (err && 'AbortError' === err.name) { 408 return; 409 } 410 handleCopyLink(url); 411 }); 412 } 413 } catch (err) { 414 handleCopyLink(url); 415 } 416 } 417 418 /** 419 * Expand or collapse the mobile compact block (v2.5.0). 420 * 421 * @param {Element} toggle The toggle button. 422 */ 423 function handleCompactToggle(toggle) { 424 var block = toggle.closest('.ayudawp-share-buttons'); 425 if (!block) { 426 return; 427 } 428 429 var expanded = block.classList.toggle('is-expanded'); 430 toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); 431 432 var label = toggle.querySelector('.ayudawp-button-text'); 433 if (!label) { 434 return; 435 } 436 if (expanded) { 437 if (!toggle.hasAttribute('data-collapsed-label')) { 438 toggle.setAttribute('data-collapsed-label', label.textContent); 439 } 440 label.textContent = block.classList.contains('compact-single') 441 ? l10n('compactClose', 'Close') 442 : l10n('compactLess', 'Less'); 443 } else { 444 label.textContent = toggle.getAttribute('data-collapsed-label') || label.textContent; 445 } 446 } 447 448 /** 449 * Single delegated click handler for every share button on the page. 450 * 451 * @param {Event} e Click event. 452 */ 453 function onClick(e) { 454 var target = e.target; 455 if (!target || !target.closest) { 456 return; 457 } 458 459 var toggle = target.closest('.ayudawp-aiss-more-btn'); 460 if (toggle) { 461 e.preventDefault(); 462 handleCompactToggle(toggle); 463 return; 464 } 465 466 var btn = target.closest('.ayudawp-share-btn'); 467 if (!btn) { 468 return; 469 } 470 471 var url = btn.getAttribute('data-url') || btn.getAttribute('href'); 472 if (!url) { 473 return; 474 } 475 476 var platform = detectPlatform(btn); 477 478 e.preventDefault(); 479 trackClick(platform, btn); 480 481 if (actionPlatforms.indexOf(platform) !== -1) { 482 if ('copy_link' === platform) { 483 handleCopyLink(url); 484 } else { 485 handleWebShare(btn, url); 486 } 487 return; 488 } 489 490 if (copyPromptPlatforms.indexOf(platform) !== -1) { 491 handleCopyPromptClick(url, platform); 492 return; 493 } 494 495 if (popupPlatforms.indexOf(platform) !== -1) { 496 var width = 'reddit' === platform ? 900 : 600; 497 var height = 'reddit' === platform ? 600 : 400; 498 var left = (window.screen.width / 2) - (width / 2); 499 var top = (window.screen.height / 2) - (height / 2); 500 window.open(url, 'share', 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',scrollbars=yes,resizable=yes'); 501 return; 502 } 503 504 window.open(url, '_blank', 'noopener,noreferrer'); 505 } 506 507 /** 508 * Reveal the native share buttons where the browser supports the API. 509 * 510 * Rendered hidden by PHP so the markup is the same for every visitor (and 511 * therefore cacheable), and only shown here when navigator.share exists. 512 */ 513 function revealNativeShare() { 514 if (!navigator.share) { 515 return; 516 } 517 var buttons = document.querySelectorAll('.ayudawp-aiss-needs-share-api[hidden]'); 518 for (var i = 0; i < buttons.length; i++) { 519 buttons[i].removeAttribute('hidden'); 520 } 521 } 522 523 function init() { 524 document.addEventListener('click', onClick, false); 525 526 // Tooltips follow both pointer and keyboard focus. 527 document.addEventListener('mouseover', function (e) { 528 var btn = e.target && e.target.closest ? e.target.closest('.ayudawp-share-btn') : null; 529 if (btn && !btn.classList.contains('ayudawp-aiss-more-btn')) { 530 showTooltip(btn); 531 } 532 }, false); 533 document.addEventListener('mouseout', function (e) { 534 if (e.target && e.target.closest && e.target.closest('.ayudawp-share-btn')) { 535 hideTooltip(); 536 } 537 }, false); 538 document.addEventListener('focusin', function (e) { 539 var btn = e.target && e.target.closest ? e.target.closest('.ayudawp-share-btn') : null; 540 if (btn && !btn.classList.contains('ayudawp-aiss-more-btn')) { 541 showTooltip(btn); 542 } 543 }, false); 544 document.addEventListener('focusout', hideTooltip, false); 545 window.addEventListener('scroll', hideTooltip, { passive: true }); 546 547 revealNativeShare(); 548 549 // Share blocks injected later (AJAX pagination, page builders, infinite 550 // scroll) only need the native-share buttons revealing: clicks and 551 // tooltips are delegated on document and work without re-binding. 552 if (window.MutationObserver && document.body) { 553 var observer = new MutationObserver(function (mutations) { 554 for (var i = 0; i < mutations.length; i++) { 555 for (var j = 0; j < mutations[i].addedNodes.length; j++) { 556 var node = mutations[i].addedNodes[j]; 557 if (node.nodeType !== 1) { 558 continue; 559 } 560 if (node.classList && node.classList.contains('ayudawp-share-buttons')) { 561 revealNativeShare(); 562 return; 563 } 564 if (node.querySelector && node.querySelector('.ayudawp-share-buttons')) { 565 revealNativeShare(); 566 return; 567 } 568 } 569 } 570 }); 571 observer.observe(document.body, { childList: true, subtree: true }); 572 } 573 } 574 575 if ('loading' === document.readyState) { 576 document.addEventListener('DOMContentLoaded', init); 577 } else { 578 init(); 579 } 580})(); 581 582/** 583 * AI Summary frontend generation button (v2.0.0) 584 * 585 * Vanilla JS â runs outside jQuery so the handler survives aggressive 586 * defer/async optimizations from caching/perf plugins (e.g. wpo-tweaks, 587 * WP Rocket, LiteSpeed). Event delegation on document so it picks up 588 * placeholders rendered by shortcodes, page builders or AJAX-loaded 589 * content. Uses fetch() with credentials so it works for logged-in and 590 * anonymous visitors alike. 591 * 592 * @since 2.0.0 593 */ 594(function () { 595 'use strict'; 596 597 function ayudawpAissInitSummaryButton() { 598 if (document.body && document.body.__aissSummaryBtnBound) { 599 return; 600 } 601 if (document.body) { 602 document.body.__aissSummaryBtnBound = true; 603 } 604 605 document.addEventListener('click', function (e) { 606 var btn = e.target && e.target.closest ? e.target.closest('.ayudawp-aiss-summary-generate-btn') : null; 607 if (!btn) { 608 return; 609 } 610 e.preventDefault(); 611 612 var placeholder = btn.closest('.ayudawp-aiss-summary--placeholder'); 613 if (!placeholder) { 614 return; 615 } 616 var postId = parseInt(placeholder.getAttribute('data-post-id'), 10); 617 if (!postId || btn.disabled) { 618 return; 619 } 620 621 var L = window.ayudawpAissL10n || {}; 622 var labelEl = btn.querySelector('.ayudawp-aiss-summary-label'); 623 var originalLabel = labelEl ? labelEl.textContent : ''; 624 625 btn.disabled = true; 626 if (labelEl) { 627 labelEl.textContent = L.summaryGenerating || 'Generatingâ¦'; 628 } 629 630 var endpoint = L.publicGenerateUrl || (window.location.origin + '/wp-json/aiss/v1/summary/generate'); 631 632 fetch(endpoint, { 633 method: 'POST', 634 credentials: 'same-origin', 635 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, 636 body: JSON.stringify({ post_id: postId }) 637 }).then(function (response) { 638 return response.json().then(function (data) { 639 return { ok: response.ok, data: data }; 640 }); 641 }).then(function (result) { 642 if (result.ok && result.data && result.data.html) { 643 var wrapper = document.createElement('div'); 644 wrapper.innerHTML = result.data.html.trim(); 645 // The server response includes an inline <style> for the 646 // critical CSS plus the actual <aside> summary block. 647 // Picking firstChild would replace the placeholder with 648 // the invisible <style>, making the box disappear. Pick 649 // the real summary node instead (the .ayudawp-aiss-summary 650 // that is NOT the placeholder we're replacing). 651 var node = wrapper.querySelector('.ayudawp-aiss-summary:not(.ayudawp-aiss-summary--placeholder)'); 652 if (node && placeholder.parentNode) { 653 placeholder.parentNode.replaceChild(node, placeholder); 654 // A11y: open the freshly generated summary and move focus 655 // to its toggle so screen readers announce the new content 656 // that replaced the button the visitor just activated. 657 var details = node.querySelector('details.ayudawp-aiss-summary-details'); 658 if (details) { details.open = true; } 659 var toggle = node.querySelector('summary.ayudawp-aiss-summary-toggle') || node.querySelector('summary'); 660 if (toggle && typeof toggle.focus === 'function') { 661 try { toggle.focus(); } catch (e) {} 662 } 663 } 664 return; 665 } 666 var msg = (result.data && result.data.message) ? result.data.message : (L.summaryGenerateError || 'Generation failed. Try again.'); 667 if (labelEl) { labelEl.textContent = msg; } 668 window.setTimeout(function () { 669 btn.disabled = false;
670 if (labelEl) { labelEl.textContent = originalLabel; } 671 }, 3000); 672 }).catch(function () { 673 if (labelEl) { labelEl.textContent = L.summaryGenerateError || 'Generation failed. Try again.'; } 674 window.setTimeout(function () { 675 btn.disabled = false; 676 if (labelEl) { labelEl.textContent = originalLabel; } 677 }, 3000); 678 }); 679 }, false); 680 } 681 682 // Bind whenever the DOM is ready â works whether this script runs 683 // before or after DOMContentLoaded (covers defer/async loading). 684 if (document.readyState === 'loading') { 685 document.addEventListener('DOMContentLoaded', ayudawpAissInitSummaryButton); 686 } else { 687 ayudawpAissInitSummaryButton(); 688 } 689})();
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.