1/** 2 * Read More Without Refresh Pro - Frontend (v5.0) 3 * 4 * Modules: toggle + animations, accordion groups, truncate ellipsis, lazy 5 * <template> rendering, conditional display, deep linking, remember state, 6 * expand/collapse all, A/B variants, content locker (email/share), engaged 7 * CTA injection, sections (Wikipedia mode) and REST analytics. 8 * 9 * No console output in production: debug logs only when WP_DEBUG is on. 10 * 11 * @package ReadMoreWithoutRefreshPro 12 * @version 5.0.0 13 */ 14 15(function () { 16 'use strict'; 17 18 var settings = window.rmwrSettings || {}; 19 20 function debug() { 21 if (settings.debug && window.console) { 22 console.log.apply(console, ['RMWR:'].concat(Array.prototype.slice.call(arguments))); 23 } 24 } 25 26 /* --------------------------------------------------------------------- 27 * Storage helpers (fail silently in private mode) 28 * ------------------------------------------------------------------ */ 29 30 function storeGet(key) { 31 try { return window.localStorage.getItem(key); } catch (e) { return null; } 32 } 33 34 function storeSet(key, value) { 35 try { window.localStorage.setItem(key, value); } catch (e) { /* noop */ } 36 } 37 38 /* --------------------------------------------------------------------- 39 * Analytics 40 * ------------------------------------------------------------------ */ 41 42 function track(key, postId, event, variant) { 43 if (!settings.analytics || !settings.restUrl || !key) { 44 return; 45 } 46 47 try { 48 window.fetch(settings.restUrl.replace(/\/$/, '') + '/track', { 49 method: 'POST', 50 keepalive: true, 51 headers: { 'Content-Type': 'application/json' }, 52 body: JSON.stringify({ k: key, p: parseInt(postId, 10) || 0, e: event, v: variant || '' }) 53 }).catch(function () { /* never disturb the visitor */ }); 54 } catch (e) { /* noop */ } 55 56 // Ad / analytics conversion events (GA4, Meta Pixel, Google Ads). 57 fireAdEvents(key, postId, event); 58 59 // Public hook for custom integrations. 60 try { 61 window.dispatchEvent(new CustomEvent('rmwr:' + event, { 62 detail: { instanceKey: key, postId: postId, variant: variant || '', timestamp: new Date().toISOString() } 63 })); 64 } catch (e) { /* noop */ } 65 } 66 67 /** 68 * Send a high-intent interaction to the site's ad / analytics platforms so 69 * engaged readers can be measured and retargeted. Fires only for the 70 * meaningful signals (expand, unlock, cta, share) and only for the 71 * platforms the admin enabled. All calls are wrapped so a missing or 72 * broken third-party tag can never disturb the visitor. 73 * 74 * @param {string} key Instance key. 75 * @param {number} postId Post ID. 76 * @param {string} event Interaction name. 77 */ 78 function fireAdEvents(key, postId, event) { 79 var cfg = settings.adEvents; 80 if (!cfg || !cfg.enabled) { 81 return; 82 } 83 if (['expand', 'unlock', 'cta', 'share'].indexOf(event) === -1) { 84 return; 85 } 86 87 var payload = { 88 rmwr_event: event, 89 rmwr_key: key, 90 rmwr_post_id: parseInt(postId, 10) || 0 91 }; 92 93 // Google Analytics 4 (gtag). 94 if (cfg.ga4 && typeof window.gtag === 'function') { 95 try { window.gtag('event', cfg.gaEventName || ('rmwr_' + event), payload); } catch (e) { /* noop */ } 96 } 97 98 // Meta / Facebook Pixel (custom event). 99 if (cfg.metaPixel && typeof window.fbq === 'function') { 100 try { window.fbq('trackCustom', cfg.metaEventName || 'RMWR_Engaged', payload); } catch (e) { /* noop */ } 101 } 102 103 // Google Ads conversion (needs a send_to id like 'AW-123456789/AbC-D_efG'). 104 if (cfg.googleAds && cfg.googleAdsSendTo && typeof window.gtag === 'function') { 105 try { window.gtag('event', 'conversion', { send_to: cfg.googleAdsSendTo }); } catch (e) { /* noop */ } 106 } 107 108 // Generic dataLayer push for GTM users. 109 if (cfg.dataLayer && window.dataLayer && typeof window.dataLayer.push === 'function') { 110 try { window.dataLayer.push({ event: 'rmwr_' + event, rmwrKey: key, rmwrPostId: payload.rmwr_post_id }); } catch (e) { /* noop */ } 111 } 112 } 113 114 /* --------------------------------------------------------------------- 115 * A/B variants 116 * ------------------------------------------------------------------ */ 117 118 var abVariant = ''; 119 120 function initAB() { 121 var variants = settings.abVariants || []; 122 if (!variants.length || variants.length < 2) { 123 return; 124 } 125 126 var idx = parseInt(storeGet('rmwr_ab_idx'), 10); 127 if (isNaN(idx) || idx < 0 || idx >= variants.length) { 128 idx = Math.floor(Math.random() * variants.length); 129 storeSet('rmwr_ab_idx', String(idx)); 130 } 131 132 abVariant = variants[idx]; 133 134 document.querySelectorAll('.read-link[data-ab="1"]').forEach(function (link) { 135 var textEl = link.querySelector('.rmwr-text'); 136 if (textEl && link.getAttribute('aria-expanded') !== 'true') { 137 textEl.textContent = abVariant; 138 } 139 link.dataset.openText = abVariant; 140 }); 141 142 debug('A/B variant assigned:', abVariant); 143 } 144 145 /* --------------------------------------------------------------------- 146 * Animations (expand/collapse) 147 * ------------------------------------------------------------------ */ 148 149 function clearAnimStyles(el) { 150 el.style.maxHeight = ''; 151 el.style.overflow = ''; 152 el.style.transform = ''; 153 el.style.perspective = ''; 154 el.style.transformOrigin = ''; 155 el.style.transition = ''; 156 el.style.opacity = ''; 157 } 158 159 function expandEl(element, wrapper, instant) { 160 element.style.display = 'block'; 161 162 var animation = instant ? 'none' : ((wrapper && wrapper.dataset.animation) || 'none'); 163 var duration = parseInt(wrapper ? wrapper.dataset.duration : 0, 10) || 300; 164 165 if (animation === 'none') { 166 return; 167 } 168 169 element.style.overflow = 'hidden'; 170 element.style.transition = 'all ' + duration + 'ms ease-in-out'; 171 172 switch (animation) { 173 case 'fade': 174 case 'slide': 175 element.style.opacity = '0'; 176 element.style.maxHeight = '0'; 177 void element.offsetHeight; 178 element.style.opacity = '1'; 179 element.style.maxHeight = element.scrollHeight + 'px'; 180 break; 181 case 'flip': 182 element.style.opacity = '0'; 183 element.style.transform = 'rotateX(-90deg)'; 184 void element.offsetHeight; 185 element.style.opacity = '1'; 186 element.style.transform = 'rotateX(0deg)'; 187 break; 188 case 'zoom': 189 element.style.opacity = '0'; 190 element.style.transform = 'scale(0.8)'; 191 void element.offsetHeight; 192 element.style.opacity = '1'; 193 element.style.transform = 'scale(1)'; 194 break; 195 case 'bounce': 196 element.style.opacity = '0'; 197 element.style.transform = 'translateY(-20px)'; 198 void element.offsetHeight; 199 element.style.transition = 'all ' + duration + 'ms cubic-bezier(0.68, -0.55, 0.265, 1.55)'; 200 element.style.opacity = '1'; 201 element.style.transform = 'translateY(0)'; 202 break; 203 case 'rotate': 204 element.style.opacity = '0'; 205 element.style.transform = 'rotate(-180deg) scale(0.8)'; 206 void element.offsetHeight; 207 element.style.opacity = '1'; 208 element.style.transform = 'rotate(0deg) scale(1)'; 209 break; 210 case 'scale': 211 element.style.opacity = '0'; 212 element.style.transform = 'scaleY(0)'; 213 element.style.transformOrigin = 'top'; 214 void element.offsetHeight; 215 element.style.opacity = '1'; 216 element.style.transform = 'scaleY(1)'; 217 break; 218 case 'elastic': 219 element.style.opacity = '0'; 220 element.style.maxHeight = '0'; 221 void element.offsetHeight; 222 element.style.transition = 'all ' + duration + 'ms cubic-bezier(0.68, -0.55, 0.265, 1.55)'; 223 element.style.opacity = '1'; 224 element.style.maxHeight = element.scrollHeight + 'px'; 225 break; 226 default: 227 clearAnimStyles(element); 228 return; 229 } 230 231 window.setTimeout(function () { clearAnimStyles(element); }, duration); 232 } 233 234 function collapseEl(element, wrapper, instant) { 235 var animation = instant ? 'none' : ((wrapper && wrapper.dataset.animation) || 'none'); 236 var duration = parseInt(wrapper ? wrapper.dataset.duration : 0, 10) || 300; 237 238 if (animation === 'none') { 239 element.style.display = 'none'; 240 return; 241 } 242 243 element.style.transition = 'all ' + duration + 'ms ease-in-out'; 244 element.style.overflow = 'hidden'; 245 246 switch (animation) { 247 case 'fade': 248 case 'slide': 249 case 'elastic': 250 element.style.opacity = '1'; 251 element.style.maxHeight = element.scrollHeight + 'px'; 252 void element.offsetHeight; 253 element.style.opacity = '0'; 254 element.style.maxHeight = '0'; 255 break; 256 case 'flip': 257 void element.offsetHeight; 258 element.style.opacity = '0'; 259 element.style.transform = 'rotateX(90deg)'; 260 break; 261 case 'zoom': 262 void element.offsetHeight; 263 element.style.opacity = '0'; 264 element.style.transform = 'scale(0.8)'; 265 break; 266 case 'bounce': 267 void element.offsetHeight; 268 element.style.opacity = '0'; 269 element.style.transform = 'translateY(-20px)'; 270 break; 271 case 'rotate': 272 void element.offsetHeight; 273 element.style.opacity = '0'; 274 element.style.transform = 'rotate(180deg) scale(0.8)'; 275 break; 276 case 'scale': 277 element.style.transformOrigin = 'top'; 278 void element.offsetHeight; 279 element.style.opacity = '0'; 280 element.style.transform = 'scaleY(0)'; 281 break; 282 } 283 284 window.setTimeout(function () { 285 element.style.display = 'none'; 286 clearAnimStyles(element); 287 }, duration); 288 } 289 290 /* --------------------------------------------------------------------- 291 * Core toggle 292 * ------------------------------------------------------------------ */ 293 294 function getParts(wrapper) { 295 var id = wrapper.dataset.id; 296 return { 297 id: id, 298 key: wrapper.dataset.key || id, 299 postId: wrapper.dataset.post || 0, 300 link: document.getElementById('readlink' + id), 301 content: document.getElementById('read' + id) 302 }; 303 } 304 305 function isExpanded(content) { 306 return content.style.display !== 'none' && content.getAttribute('aria-hidden') !== 'true'; 307 } 308 309 function setButtonText(link, text) { 310 var textEl = link.querySelector('.rmwr-text'); 311 if (textEl) { 312 textEl.textContent = text; 313 } else { 314 link.textContent = text; 315 } 316 } 317 318 function materializeLazy(content) { 319 var tpl = content.querySelector('template.rmwr-tpl'); 320 if (tpl) { 321 content.appendChild(tpl.content.cloneNode(true)); 322 tpl.remove(); 323 } 324 } 325 326 function rememberState(key, expanded) { 327 if (!settings.remember) { 328 return; 329 } 330 var map; 331 try { map = JSON.parse(storeGet('rmwr_state') || '{}'); } catch (e) { map = {}; } 332 if (expanded) { 333 map[key] = 1; 334 } else { 335 delete map[key]; 336 }
337 storeSet('rmwr_state', JSON.stringify(map)); 338 } 339 340 function toggle(wrapper, opts) { 341 opts = opts || {}; 342 var parts = getParts(wrapper); 343 if (!parts.link || !parts.content) { 344 return; 345 } 346 347 var expanded = isExpanded(parts.content); 348 var openText = parts.link.dataset.openText || 'Read More'; 349 var closeText = parts.link.dataset.closeText || 'Read Less'; 350 351 if (!expanded) { 352 // Locker interception: show the unlock UI instead of the content. 353 if (wrapper.dataset.lock && !isUnlocked(parts.key)) { 354 showLocker(wrapper, parts); 355 return; 356 } 357 358 // Accordion: collapse the other items of the group first. 359 if (wrapper.dataset.mode === 'accordion' && wrapper.dataset.accordionId) { 360 document.querySelectorAll('[data-accordion-id="' + wrapper.dataset.accordionId + '"]').forEach(function (other) { 361 if (other !== wrapper) { 362 var op = getParts(other); 363 if (op.link && op.content && isExpanded(op.content)) { 364 collapseInstance(other, op); 365 } 366 } 367 }); 368 } 369 370 materializeLazy(parts.content); 371 expandEl(parts.content, wrapper, opts.instant); 372 setButtonText(parts.link, closeText); 373 parts.link.setAttribute('aria-expanded', 'true'); 374 parts.content.setAttribute('aria-hidden', 'false'); 375 376 hideEllipsis(parts.id, true); 377 378 if (!opts.instant && wrapper.dataset.smoothScroll !== 'false') { 379 smoothScrollTo(parts.content, parseInt(wrapper.dataset.scrollOffset || '0', 10)); 380 } 381 382 if (!opts.silent) { 383 var variant = parts.link.dataset.ab === '1' ? abVariant : ''; 384 track(parts.key, parts.postId, 'expand', variant); 385 } 386 387 maybeShowCta(wrapper, parts); 388 rememberState(parts.key, true); 389 } else { 390 collapseInstance(wrapper, parts, opts); 391 rememberState(parts.key, false); 392 } 393 } 394 395 function collapseInstance(wrapper, parts, opts) { 396 opts = opts || {}; 397 var openText = parts.link.dataset.openText || 'Read More'; 398 collapseEl(parts.content, wrapper, opts.instant); 399 setButtonText(parts.link, openText); 400 parts.link.setAttribute('aria-expanded', 'false'); 401 parts.content.setAttribute('aria-hidden', 'true'); 402 hideEllipsis(parts.id, false); 403 } 404 405 function hideEllipsis(id, hidden) { 406 var el = document.querySelector('.rmwr-ellipsis[data-for="' + id + '"]'); 407 if (el) { 408 el.style.display = hidden ? 'none' : ''; 409 } 410 } 411 412 function smoothScrollTo(element, offset) { 413 if (!element) { return; } 414 var top = element.getBoundingClientRect().top + window.pageYOffset - (offset || 0); 415 window.scrollTo({ top: top, behavior: 'smooth' }); 416 } 417 418 /* --------------------------------------------------------------------- 419 * Content Locker 420 * ------------------------------------------------------------------ */ 421 422 function isUnlocked(key) { 423 return storeGet('rmwr_unlock_' + key) === '1'; 424 } 425 426 function markUnlocked(key) { 427 storeSet('rmwr_unlock_' + key, '1'); 428 } 429 430 function showLocker(wrapper, parts) { 431 var existing = wrapper.querySelector('.rmwr-lock-box'); 432 if (existing) { 433 existing.style.display = ''; 434 return; 435 } 436 437 var tpl = wrapper.querySelector('template.rmwr-lock-tpl'); 438 if (!tpl) { 439 return; 440 } 441 442 wrapper.appendChild(tpl.content.cloneNode(true)); 443 var box = wrapper.querySelector('.rmwr-lock-box'); 444 if (!box) { 445 return; 446 } 447 448 var form = box.querySelector('.rmwr-lock-form'); 449 if (form) { 450 form.addEventListener('submit', function (e) { 451 e.preventDefault(); 452 submitLockerForm(form, wrapper, parts, box); 453 }); 454 } 455 456 box.querySelectorAll('.rmwr-share-btn').forEach(function (btn) { 457 btn.addEventListener('click', function () { 458 openShareWindow(btn.dataset.network); 459 markUnlocked(parts.key); 460 track(parts.key, parts.postId, 'share'); 461 box.remove(); 462 toggle(wrapper, { silent: true });
463 }); 464 }); 465 } 466 467 function submitLockerForm(form, wrapper, parts, box) { 468 var locker = settings.locker || {}; 469 var emailInput = form.querySelector('.rmwr-lock-email'); 470 var errorEl = form.querySelector('.rmwr-lock-error'); 471 var submitBtn = form.querySelector('.rmwr-lock-submit'); 472 var consentInput = form.querySelector('input[name="rmwr_consent"]'); 473 var email = emailInput ? emailInput.value.trim() : ''; 474 475 function fail(message) { 476 if (errorEl) { 477 errorEl.textContent = message; 478 errorEl.hidden = false; 479 } 480 if (submitBtn) { submitBtn.disabled = false; } 481 } 482 483 if (!email || email.indexOf('@') < 1) { 484 fail(locker.invalidEmail || 'Please enter a valid email address.'); 485 return; 486 } 487 488 if (submitBtn) { submitBtn.disabled = true; } 489 if (errorEl) { errorEl.hidden = true; } 490 491 window.fetch(settings.restUrl.replace(/\/$/, '') + '/unlock', { 492 method: 'POST', 493 headers: { 'Content-Type': 'application/json' }, 494 body: JSON.stringify({ 495 email: email, 496 k: parts.key, 497 p: parseInt(parts.postId, 10) || 0, 498 consent: !!(consentInput && consentInput.checked) 499 }) 500 }).then(function (response) { 501 return response.json().then(function (data) { return { ok: response.ok, data: data }; }); 502 }).then(function (result) { 503 if (result.ok && result.data && result.data.ok) { 504 markUnlocked(parts.key); 505 track(parts.key, parts.postId, 'unlock'); 506 box.remove(); 507 toggle(wrapper, { silent: true }); 508 } else { 509 fail((settings.locker || {}).genericError || 'Something went wrong. Please try again.'); 510 } 511 }).catch(function () { 512 fail((settings.locker || {}).genericError || 'Something went wrong. Please try again.'); 513 }); 514 } 515 516 function openShareWindow(network) { 517 var url = encodeURIComponent(window.location.href); 518 var title = encodeURIComponent(document.title); 519 var shareUrl = ''; 520 521 if (network === 'facebook') { 522 shareUrl = 'https://www.facebook.com/sharer/sharer.php?u=' + url; 523 } else if (network === 'x') { 524 shareUrl = 'https://twitter.com/intent/tweet?url=' + url + '&text=' + title; 525 } else if (network === 'linkedin') { 526 shareUrl = 'https://www.linkedin.com/sharing/share-offsite/?url=' + url; 527 } 528 529 if (shareUrl) { 530 window.open(shareUrl, 'rmwr-share', 'width=600,height=500,noopener'); 531 } 532 } 533 534 /* --------------------------------------------------------------------- 535 * Engaged-user CTA 536 * ------------------------------------------------------------------ */ 537 538 function maybeShowCta(wrapper, parts) { 539 var cta = settings.cta || {}; 540 if (wrapper.dataset.cta !== '1' || !cta.html || wrapper.dataset.ctaShown === '1') { 541 return; 542 } 543 wrapper.dataset.ctaShown = '1'; 544 545 window.setTimeout(function () { 546 var box = document.createElement('div'); 547 box.className = 'rmwr-cta-box'; 548 box.innerHTML = cta.html; 549 550 var close = document.createElement('button'); 551 close.type = 'button'; 552 close.className = 'rmwr-cta-close'; 553 close.setAttribute('aria-label', 'Close'); 554 close.innerHTML = '×'; 555 close.addEventListener('click', function () { box.remove(); }); 556 box.appendChild(close); 557 558 box.addEventListener('click', function (e) { 559 if (e.target.closest('a, button:not(.rmwr-cta-close)')) { 560 track(parts.key, parts.postId, 'cta'); 561 } 562 }); 563 564 parts.content.insertAdjacentElement('afterend', box); 565 }, (parseInt(cta.delay, 10) || 0) * 1000); 566 } 567 568 /* --------------------------------------------------------------------- 569 * Sections (Wikipedia mode) 570 * ------------------------------------------------------------------ */ 571 572 function toggleSection(btn, forceOpen) { 573 var panel = document.getElementById(btn.getAttribute('aria-controls')); 574 if (!panel) { 575 return; 576 } 577 var open = btn.getAttribute('aria-expanded') === 'true'; 578 if (forceOpen && open) { 579 return; 580 } 581 var next = forceOpen ? true : !open; 582 btn.setAttribute('aria-expanded', next ? 'true' : 'false'); 583 panel.hidden = !next; 584 } 585 586 /* --------------------------------------------------------------------- 587 * Expand/Collapse all 588 * ------------------------------------------------------------------ */ 589 590 function toggleAll(button) { 591 var group = button.dataset.group || ''; 592 var wrappers = group 593 ? document.querySelectorAll('.rmwr-wrapper[data-accordion-id="' + group + '"]') 594 : document.querySelectorAll('.rmwr-wrapper'); 595 596 var anyCollapsed = false;
597 wrappers.forEach(function (wrapper) { 598 var parts = getParts(wrapper); 599 if (parts.content && !isExpanded(parts.content)) { 600 anyCollapsed = true; 601 } 602 }); 603 604 wrappers.forEach(function (wrapper) { 605 var parts = getParts(wrapper); 606 if (!parts.content || !parts.link) { 607 return; 608 } 609 if (anyCollapsed && !isExpanded(parts.content)) { 610 toggle(wrapper, { silent: true, instant: true }); 611 } else if (!anyCollapsed && isExpanded(parts.content)) { 612 toggle(wrapper, { silent: true, instant: true }); 613 } 614 }); 615 616 // Sections too, when no specific group is set. 617 if (!group) { 618 document.querySelectorAll('.rmwr-sec-btn').forEach(function (btn) { 619 toggleSection(btn, anyCollapsed); 620 }); 621 } 622 623 button.textContent = anyCollapsed 624 ? (button.dataset.closeText || 'Collapse all') 625 : (button.dataset.openText || 'Expand all'); 626 } 627 628 /* --------------------------------------------------------------------- 629 * Conditional display 630 * ------------------------------------------------------------------ */ 631 632 function applyConditionalDisplay(wrapper) { 633 var device = wrapper.dataset.device || 'all'; 634 if (device !== 'all') { 635 var isMobile = window.innerWidth <= 768; 636 if ((device === 'mobile' && !isMobile) || (device === 'desktop' && isMobile)) { 637 wrapper.style.display = 'none'; 638 return; 639 } 640 } 641 642 var showAfter = wrapper.dataset.showAfter; 643 if (showAfter) { 644 var seconds = parseInt(String(showAfter).replace('seconds', ''), 10) || 0; 645 if (seconds > 0) { 646 wrapper.style.visibility = 'hidden'; 647 wrapper.style.opacity = '0'; 648 window.setTimeout(function () { 649 wrapper.style.transition = 'opacity 0.5s ease'; 650 wrapper.style.visibility = ''; 651 wrapper.style.opacity = '1'; 652 }, seconds * 1000); 653 } 654 } 655 656 if (wrapper.dataset.scroll === 'true') { 657 wrapper.style.opacity = '0'; 658 wrapper.style.transition = 'opacity 0.5s ease'; 659 var reveal = function () { 660 var rect = wrapper.getBoundingClientRect(); 661 var viewport = window.innerHeight || document.documentElement.clientHeight; 662 if (rect.top < viewport - 100) { 663 wrapper.style.opacity = '1'; 664 window.removeEventListener('scroll', reveal); 665 } 666 }; 667 window.addEventListener('scroll', reveal, { passive: true }); 668 reveal(); 669 } 670 } 671 672 /* --------------------------------------------------------------------- 673 * Deep linking 674 * ------------------------------------------------------------------ */ 675 676 function handleHash() { 677 if (!settings.deepLink || !window.location.hash) { 678 return; 679 } 680 681 var hash = window.location.hash.slice(1); 682 if (!hash) { 683 return; 684 } 685 686 var target = null; 687 try { target = document.getElementById(hash); } catch (e) { return; } 688 if (!target) { 689 return; 690 } 691 692 // Section heading anchor (#rmwr-sec-slug). 693 if (target.classList.contains('rmwr-sec-title')) { 694 var btn = target.querySelector('.rmwr-sec-btn'); 695 if (btn) { 696 toggleSection(btn, true); 697 window.setTimeout(function () { smoothScrollTo(target, 60); }, 50); 698 } 699 return; 700 } 701 702 // Wrapper / content id (#rmwr-p12-1 or #readrmwr-p12-1). 703 var wrapper = target.closest ? target.closest('.rmwr-wrapper') : null; 704 if (!wrapper) { 705 wrapper = document.querySelector('.rmwr-wrapper[data-id="' + hash + '"]'); 706 } 707 if (wrapper) { 708 var parts = getParts(wrapper); 709 if (parts.content && !isExpanded(parts.content)) { 710 toggle(wrapper, { silent: true });
711 } 712 window.setTimeout(function () { smoothScrollTo(wrapper, 60); }, 50); 713 return; 714 } 715 716 // Element hidden inside a collapsed area or section panel. 717 var container = target.closest ? (target.closest('.read_div') || target.closest('.rmwr-sec-panel')) : null; 718 if (container) { 719 if (container.classList.contains('read_div')) { 720 var w = container.closest('.rmwr-wrapper'); 721 if (w) { toggle(w, { silent: true }); } 722 } else { 723 var sbtn = document.querySelector('.rmwr-sec-btn[aria-controls="' + container.id + '"]'); 724 if (sbtn) { toggleSection(sbtn, true); } 725 } 726 window.setTimeout(function () { smoothScrollTo(target, 60); }, 50); 727 } 728 } 729 730 /* --------------------------------------------------------------------- 731 * Init 732 * ------------------------------------------------------------------ */ 733 734 function restoreRememberedState() { 735 if (!settings.remember) { 736 return; 737 } 738 var map; 739 try { map = JSON.parse(storeGet('rmwr_state') || '{}'); } catch (e) { map = {}; } 740 741 document.querySelectorAll('.rmwr-wrapper').forEach(function (wrapper) { 742 var parts = getParts(wrapper); 743 if (parts.key && map[parts.key] && parts.content && !isExpanded(parts.content) && !wrapper.dataset.lock) { 744 toggle(wrapper, { silent: true, instant: true }); 745 } 746 }); 747 } 748 749 function init() { 750 document.querySelectorAll('.rmwr-wrapper').forEach(applyConditionalDisplay); 751 initAB(); 752 restoreRememberedState(); 753 handleHash(); 754 755 debug('initialized', document.querySelectorAll('.rmwr-wrapper').length, 'instances'); 756 757 document.addEventListener('click', function (e) { 758 var secBtn = e.target.closest('.rmwr-sec-btn'); 759 if (secBtn) { 760 e.preventDefault(); 761 toggleSection(secBtn); 762 return; 763 } 764 765 var allBtn = e.target.closest('.rmwr-toggle-all'); 766 if (allBtn) { 767 e.preventDefault(); 768 toggleAll(allBtn); 769 return; 770 } 771 772 var tocLink = e.target.closest('.rmwr-toc a'); 773 if (tocLink) { 774 // Let the browser set the hash; expansion follows on hashchange. 775 window.setTimeout(handleHash, 30); 776 return; 777 } 778 779 var link = e.target.closest('.read-link'); 780 if (link && link.id && link.id.indexOf('readlink') === 0) { 781 e.preventDefault(); 782 var wrapper = link.closest('.rmwr-wrapper'); 783 if (wrapper) { 784 toggle(wrapper); 785 } 786 } 787 }); 788 789 window.addEventListener('hashchange', handleHash); 790 791 if (settings.printExpand !== false) { 792 window.addEventListener('beforeprint', function () { 793 document.querySelectorAll('.read_div').forEach(function (content) { 794 materializeLazy(content); 795 content.style.display = 'block'; 796 content.setAttribute('aria-hidden', 'false'); 797 }); 798 document.querySelectorAll('.rmwr-sec-panel').forEach(function (panel) { 799 panel.hidden = false; 800 }); 801 }); 802 } 803 804 // Support content injected after load (AJAX pagination, quick view...). 805 if (typeof MutationObserver !== 'undefined' && document.body) { 806 var observer = new MutationObserver(function (mutations) { 807 mutations.forEach(function (mutation) { 808 Array.prototype.forEach.call(mutation.addedNodes, function (node) { 809 if (node.nodeType !== 1) { 810 return; 811 } 812 var wrappers = node.classList && node.classList.contains('rmwr-wrapper') 813 ? [node] 814 : (node.querySelectorAll ? node.querySelectorAll('.rmwr-wrapper') : []); 815 Array.prototype.forEach.call(wrappers, applyConditionalDisplay); 816 }); 817 }); 818 }); 819 observer.observe(document.body, { childList: true, subtree: true });
820 } 821 822 initReadingProgress(); 823 } 824 825 /* --------------------------------------------------------------------- 826 * Reading progress bar + "continue reading" resume (free) 827 * ------------------------------------------------------------------ */ 828 829 var progressStylesInjected = false; 830 831 function injectProgressStyles() { 832 if (progressStylesInjected) { 833 return; 834 } 835 progressStylesInjected = true; 836 var css = '.rmwr-progress{position:fixed;top:0;left:0;right:0;height:3px;z-index:99999;pointer-events:none;background:transparent}' 837 + '.rmwr-progress-fill{display:block;height:100%;width:0;background:var(--rmwr-progress-color,#7c3aed);transition:width .1s linear}' 838 + '.rmwr-resume{position:fixed;right:16px;bottom:16px;z-index:99999;background:var(--rmwr-progress-color,#7c3aed);color:#fff;border:0;border-radius:999px;padding:10px 16px;font:600 13px/1 system-ui,sans-serif;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.2)}' 839 + '.rmwr-resume:hover{filter:brightness(.92)}'; 840 var style = document.createElement('style'); 841 style.textContent = css; 842 document.head.appendChild(style); 843 } 844 845 function initReadingProgress() { 846 var cfg = settings.readingProgress; 847 if (!cfg || (!cfg.bar && !cfg.resume)) { 848 return; 849 } 850 851 injectProgressStyles(); 852 853 if (cfg.bar) { 854 var bar = document.createElement('div'); 855 bar.className = 'rmwr-progress'; 856 bar.setAttribute('role', 'progressbar'); 857 bar.setAttribute('aria-hidden', 'true'); 858 var fill = document.createElement('span'); 859 fill.className = 'rmwr-progress-fill'; 860 bar.appendChild(fill); 861 document.body.appendChild(bar); 862 863 var barTicking = false; 864 var updateBar = function () { 865 var h = document.documentElement; 866 var max = (h.scrollHeight - h.clientHeight) || 1; 867 var top = h.scrollTop || window.pageYOffset || 0; 868 fill.style.width = Math.min(100, Math.max(0, (top / max) * 100)) + '%'; 869 barTicking = false; 870 }; 871 window.addEventListener('scroll', function () { 872 if (!barTicking) { barTicking = true; window.requestAnimationFrame(updateBar); } 873 }, { passive: true }); 874 updateBar(); 875 } 876 877 if (cfg.resume) { 878 initResume(); 879 } 880 } 881 882 function initResume() { 883 var storeKey = 'rmwr_pos_' + location.pathname; 884 885 var resumeTicking = false; 886 window.addEventListener('scroll', function () { 887 if (resumeTicking) { return; } 888 resumeTicking = true; 889 window.requestAnimationFrame(function () { 890 try { window.localStorage.setItem(storeKey, String(window.pageYOffset || 0)); } catch (e) { /* noop */ } 891 resumeTicking = false; 892 }); 893 }, { passive: true }); 894 895 var saved = 0; 896 try { saved = parseInt(window.localStorage.getItem(storeKey), 10) || 0; } catch (e) { /* noop */ } 897 if (saved < 400) { 898 return; // Not worth offering for a tiny scroll. 899 } 900 901 var pill = document.createElement('button'); 902 pill.type = 'button'; 903 pill.className = 'rmwr-resume'; 904 pill.textContent = settings.resumeText || 'Continue reading'; 905 document.body.appendChild(pill); 906 907 var dismiss = function () { 908 if (pill.parentNode) { pill.parentNode.removeChild(pill); } 909 }; 910 pill.addEventListener('click', function () { 911 try { 912 window.scrollTo({ top: saved, behavior: 'smooth' }); 913 } catch (e) { 914 window.scrollTo(0, saved); 915 } 916 dismiss(); 917 }); 918 window.setTimeout(dismiss, 8000); // Auto-hide if ignored. 919 } 920 921 // Public API for theme developers. 922 window.RMWR = { 923 toggle: function (idOrKey) { 924 var wrapper = document.querySelector('.rmwr-wrapper[data-id="' + idOrKey + '"]') 925 || document.querySelector('.rmwr-wrapper[data-key="' + idOrKey + '"]'); 926 if (wrapper) { 927 toggle(wrapper); 928 } 929 }, 930 expandAll: function () { 931 document.querySelectorAll('.rmwr-wrapper').forEach(function (wrapper) { 932 var parts = getParts(wrapper); 933 if (parts.content && !isExpanded(parts.content)) { 934 toggle(wrapper, { silent: true, instant: true });
935 } 936 }); 937 }, 938 track: track 939 }; 940 941 if (document.readyState === 'loading') { 942 document.addEventListener('DOMContentLoaded', init); 943 } else { 944 init(); 945 } 946})();
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.