1/* ========================================================== 2 Zendesk Theme Core JS 3 Last updated: 17 Oct 2025 4 Purpose: Core UI scripts + all original utilities + Recipes Explorers 5 ========================================================== */ 6 7hljs.initHighlightingOnLoad(); 8 9var HC_SETTINGS = { 10 css: { 11 activeClass: 'is-active', 12 hiddenClass: 'is-hidden' 13 } 14}; 15 16$(function() { 17 // ========================================================= 18 // ð¹ Global UI (menus, overlay, parallax, scroll-to-top) 19 // ========================================================= 20 SearchResultsFilters.init(); 21 22 var $window = $(window); 23 var $body = $('body'); 24 var $topbar = $('[data-topbar]'); 25 var $scrollToTop = $('[data-scroll-to-top]'); 26 var $mainMenu = $('[data-menu]'); 27 var $mobileSandwich = $('[data-toggle-menu]'); 28 var $overlay = $('<div class="body-overlay"></div>'); 29 30 var bodyEl = document.querySelector('body'); 31 var shouldShowSideNav = !!document.querySelector('[data-show-sidenav]'); 32 var sidenavBox = document.querySelector('[data-sidenav-box]'); 33 34 if (shouldShowSideNav) { 35 bodyEl.classList.add('sidenav-layout'); 36 sidenavBox.classList.remove(HC_SETTINGS.css.hiddenClass); 37 } 38 39 // Parallax for hero elements 40 var $heroElement = $('.hero__element'); 41 var evenEl = $heroElement.filter(':even'); 42 var oddEl = $heroElement.filter(':odd'); 43 var VELOCITY = 5; 44 var wScrollTop; 45 46 $body.append($overlay[0]); 47 48 $scrollToTop.click(function() { 49 $('html, body').animate({ scrollTop: 0 }, 1000); 50 return false; 51 }); 52 53 $mobileSandwich.click(function() { 54 $(this).add($mainMenu).add($topbar).toggleClass(HC_SETTINGS.css.activeClass); 55 $mainMenu.slideToggle(300); 56 $body.toggleClass('menu-opened'); 57 }); 58 59 $overlay.on('click', mobileMenuClose); 60 61 $window.on('resize', function() { 62 if ($window.width() > 991) { 63 mobileMenuClose(null, true); 64 } 65 }); 66 67 function mobileMenuClose(e, desktop) { 68 if ($mobileSandwich.hasClass(HC_SETTINGS.css.activeClass)) { 69 $mobileSandwich.add($mainMenu).add($topbar).removeClass(HC_SETTINGS.css.activeClass); 70 $mainMenu.slideUp(desktop ? 0 : 300); 71 $body.removeClass('menu-opened'); 72 } 73 } 74 75 // Scroll & parallax 76 $window.on('scroll', function() { 77 wScrollTop = $window.scrollTop(); 78 79 (wScrollTop > 400) 80 ? $scrollToTop.addClass(HC_SETTINGS.css.activeClass) 81 : $scrollToTop.removeClass(HC_SETTINGS.css.activeClass); 82 83 if ($heroElement.length && $window.width() > 1024) { 84 evenEl.css('transform', 'translateY(' + (wScrollTop / VELOCITY) + 'px)'); 85 oddEl.css('transform', 'translateY(' + (wScrollTop / (VELOCITY * -2)) + 'px)'); 86 } 87 }); 88 89 // ========================================================= 90 // ð¹ Social share + dropdowns + search helpers 91 // ========================================================= 92 $('.share a').click(function(e) { 93 e.preventDefault(); 94 window.open(this.href, '', 'height=500,width=500'); 95 }); 96 97 $('.share-label').on('click', function(e) { 98 e.stopPropagation(); 99 var isSelected = this.getAttribute('aria-selected') === 'true'; 100 this.setAttribute('aria-selected', !isSelected); 101 $('.share-label').not(this).attr('aria-selected', 'false'); 102 }); 103 104 $(document).on('click', function() { 105 $('.share-label').attr('aria-selected', 'false'); 106 }); 107 108 $('.dropdown-toggle').on('click', function(e) { e.preventDefault(); }); 109 110 // Submit search on select change / input enter 111 $('#request-status-select, #request-organization-select').on('change', search); 112 $('#quick-search').on('keypress', function(e) { if (e.which === 13) search(); }); 113 114 function search() { 115 window.location.search = $.param({ 116 query: $('#quick-search').val(), 117 status: $('#request-status-select').val(), 118 organization_id: $('#request-organization-select').val() 119 }); 120 } 121 122 // ========================================================= 123 // ð¹ Old Lightbox (Magnific Popup) â keep for legacy content 124 // ========================================================= 125 $('.image-with-lightbox').magnificPopup({ 126 type: 'image', 127 closeOnContentClick: true, 128 closeBtnInside: false, 129 fixedContentPos: true, 130 mainClass: 'mfp-with-zoom', 131 image: { verticalFit: true }, 132 zoom: { enabled: true, duration: 300 } 133 }); 134 135 $('.image-with-video-icon').magnificPopup({ 136 disableOn: 700, 137 type: 'iframe', 138 mainClass: 'mfp-fade', 139 removalDelay: 160, 140 preloader: false,
141 fixedContentPos: false 142 }); 143 144 // ========================================================= 145 // ð¹ Accordion / Tabs / Icons 146 // ========================================================= 147 $('.accordion__item-title').on('click', function() { 148 var $title = $(this); 149 $title.toggleClass('accordion__item-title--active'); 150 $title.parents('.accordion__item').find('.accordion__item-content').slideToggle(); 151 }); 152 153 $('.tabs-link').click(function(e) { 154 e.preventDefault(); 155 var $link = $(this); 156 var tabIndex = $link.index(); 157 var $tab = $link.parents('.tabs').find('.tab').eq(tabIndex); 158 $link.addClass(HC_SETTINGS.css.activeClass).siblings().removeClass(HC_SETTINGS.css.activeClass); 159 $tab.removeClass(HC_SETTINGS.css.hiddenClass).siblings('.tab').addClass(HC_SETTINGS.css.hiddenClass); 160 }); 161 162 // Fix animated icons 163 $('.fa-spin').empty(); 164 165 // ========================================================= 166 // ð¹ Asset part / underline / SVG replacement / Category tree 167 // ========================================================= 168 var assetPart = $('[property]').attr('content'), 169 $assetElement = $('[data-asset]'); 170 171 if (assetPart) { 172 assetPart = assetPart.slice(0, -5); 173 $assetElement.each(function() { 174 var $this = $(this), link = $this.data('asset'); 175 $this.attr('src', assetPart + link).css('opacity', 1); 176 }); 177 } 178 179 // Underline matching word 180 $('[data-underline]').each(function() { 181 var $text = $(this); 182 var word = $text.data('underline'); 183 var html = $text.html(); 184 $text.html(html.replace(new RegExp(word, 'g'), '<span class="underline">' + word + '</span>')); 185 }); 186 187 // Replace SVG images (inline) 188 $('img.custom-block__image').each(function() { 189 var $img = $(this); 190 var imgID = $img.attr('id'); 191 var imgClass = $img.attr('class'); 192 var imgURL = $img.attr('src') + '?reset'; 193 194 $.get(imgURL, function(data) { 195 var $svg = $(data).find('svg'); 196 if (typeof imgID !== 'undefined') $svg = $svg.attr('id', imgID); 197 if (typeof imgClass !== 'undefined') $svg = $svg.attr('class', imgClass + ' replaced-svg'); 198 $svg = $svg.removeAttr('xmlns:a'); 199 $img.replaceWith($svg); 200 }, 'xml'); 201 }); 202 203 // Category tree 204 $('[data-tree]').each(function(i, element) { 205 var $item = $(element); 206 var $link = $item.find('[data-tree-link]'); 207 var $box = $item.find('[data-tree-box]'); 208 $link.on('click', function(e) { 209 e.preventDefault(); 210 $link.add($item).add($box).toggleClass(HC_SETTINGS.css.activeClass); 211 $box.slideToggle(300); 212 }); 213 }); 214 215 // Add custom links to side navigation 216 $(window).on('sidenavRendered', function(e, sidenav) { 217 if (!window.LotusConfig || !Array.isArray(LotusConfig.sidenavLinks)) return; 218 LotusConfig.sidenavLinks.forEach((el)=>{ 219 let linkList = ''; 220 el.links.forEach((link)=> { linkList += link; }); 221 $('<span class="sidenav__custom-links">' + linkList + '</span>') 222 .insertAfter('[data-sidenav-category-id="' + el.categoryId + '"] .sidenav__category-name'); 223 }); 224 }); 225 226 // ========================================================= 227 // ð¹ FAQ Expand/Collapse 228 // ========================================================= 229 $('.faq-question').on('click', function () { 230 const $question = $(this); 231 const $item = $question.closest('.faq-item'); 232 const $answer = $item.find('.faq-answer'); 233 const $icon = $question.find('.faq-toggle-icon'); 234 $answer.toggleClass('active'); 235 $icon.text($answer.hasClass('active') ? 'â' : '+'); 236 }); 237 238 // ========================================================= 239 // ð¹ Generative AI access check (agent/admin reveal) 240 // ========================================================= 241 console.log("Script is loaded â "); 242 fetch('/hc/en-us/articles/41196780578707-Generative-AI-search-access-check-do-not-delete') 243 .then(response => { 244 console.log("Fetch response status:", response.status); 245 if (response.status === 200) { 246 console.log("Agent/admin confirmed â "); 247 document.querySelector('.ai-disclaimer')?.classList.remove('hidden'); 248 document.querySelector('.generative-answers-wrapper')?.classList.remove('hidden'); 249 } else { 250 console.log("User is not agent/admin."); 251 } 252 }) 253 .catch(error => { 254 console.error("Agent detection failed:", error); 255 }); 256 257 // ========================================================= 258 // ð¹ New Lightbox (pure JS) â for elements with [data-fullsize] 259 // (kept alongside Magnific to avoid breaking legacy content) 260 // ========================================================= 261 (function lightboxNew() { 262 const modal = document.createElement("div"); 263 modal.className = "lightbox-modal"; 264 modal.setAttribute("role", "dialog"); 265 modal.setAttribute("aria-modal", "true"); 266 267 const content = document.createElement("div"); 268 content.className = "lightbox-content"; 269 270 const closeBtn = document.createElement("button"); 271 closeBtn.className = "lightbox-close"; 272 closeBtn.type = "button"; 273 closeBtn.setAttribute("aria-label", "Close"); 274 closeBtn.textContent = "Ã"; 275 276 const scrollWrap = document.createElement("div"); 277 scrollWrap.className = "lightbox-scroll"; 278 279 const img = document.createElement("img"); 280 img.alt = "Full-size preview"; 281 282 scrollWrap.appendChild(img); 283 content.appendChild(closeBtn); 284 content.appendChild(scrollWrap); 285 modal.appendChild(content); 286 document.body.appendChild(modal); 287 288 const open = (src) => { 289 const temp = new Image(); 290 temp.crossOrigin = "use-credentials"; 291 temp.onload = temp.onerror = () => { 292 img.src = src; 293 modal.classList.add("active"); 294 document.body.classList.add("lb-lock"); 295 closeBtn.focus(); 296 }; 297 temp.src = src; 298 }; 299 300 const close = () => { 301 modal.classList.remove("active"); 302 document.body.classList.remove("lb-lock"); 303 img.removeAttribute("src"); 304 }; 305 306 document.addEventListener("click", (e) => { 307 const thumb = e.target.closest("[data-fullsize]"); 308 if (thumb) { 309 e.preventDefault(); 310 const full = thumb.getAttribute("data-fullsize") || thumb.src; 311 if (full) open(full); 312 } else if (e.target === modal) { 313 close(); 314 } 315 }); 316 317 closeBtn.addEventListener("click", close); 318 document.addEventListener("keydown", (e) => { 319 if (e.key === "Escape" && modal.classList.contains("active")) close(); 320 }); 321 })(); 322 323 // ========================================================= 324 // ð¹ Copy Button (Updated for "Pro" Icon-Only CSS) 325 // ========================================================= 326 document.querySelectorAll("pre").forEach(pre => { 327 const codeBlock = pre.querySelector("code") || pre; 328 329 // Prevent duplicates 330 if (pre.querySelector(".copy-btn")) return; 331 332 const button = document.createElement("button"); 333 button.className = "copy-btn"; 334 335 // ACCESSIBILITY ONLY: We don't set .textContent because CSS hides it. 336 // We use aria-label so screen readers still know what it is. 337 button.setAttribute("aria-label", "Copy code"); 338 339 button.addEventListener("click", () => { 340 navigator.clipboard.writeText(codeBlock.innerText).then(() => { 341
342 // 1. ADD CLASS: This triggers the Green Checkmark Icon in CSS 343 button.classList.add("copied"); 344 button.setAttribute("aria-label", "Copied!"); 345 346 // 2. REMOVE CLASS: After 2s, reverts to Gray Clipboard Icon 347 setTimeout(() => { 348 button.classList.remove("copied"); 349 button.setAttribute("aria-label", "Copy code"); 350 }, 2000); 351 }); 352 }); 353 354 // Ensure the button stays in the top-right corner 355 pre.style.position = "relative"; 356 pre.prepend(button); 357 358 }); 359 360 361// ========================================================= 362// Recipe Studio Explorer V2 363// Last Updated: 7 Apr 2026 364// 365// Updates: Fixed detection order (lists before code), 366// improved mixed-content handling for JSON in descriptions 367// 368// Sheet Columns: ID, Title, Short Desc, Full Desc, 369// Requirements, Inputs, Use Cases, Vertical, Lifecycle, 370// Priority, Published, Slug, Feed Type 371// ========================================================= 372 373(function recipeExplorer() { 374 // ---------- DOM Hooks ---------- 375 const appEl = document.getElementById('recipes-app'); 376 const listEl = document.getElementById('recipes-list'); 377 const pagerEl = document.getElementById('recipes-pagination'); 378 const searchEl = document.getElementById('recipes-search'); 379 const pageSizeEl = document.getElementById('recipes-page-size'); 380 const lifeCycleEl = document.getElementById('recipes-lifecycle-filter'); 381 const verticalEl = document.getElementById('recipes-vertical-filter'); 382 const modalOverlay = document.getElementById('details-modal'); 383 const modalTitle = document.getElementById('modal-title'); 384 const modalContentArea = document.getElementById('modal-content-area'); 385 const modalCloseBtn = modalOverlay?.querySelector('.modal-close-btn'); 386 const modalTagVertical = document.getElementById('modal-tag-vertical'); 387 const modalTagLifecycle = document.getElementById('modal-tag-lifecycle'); 388 389 if (!appEl || !listEl || !pagerEl || !modalOverlay) { 390 return console.error('[recipes] Missing essential DOM elements.'); 391 } 392 393 // ---------- Config ---------- 394 395 const DEBUG = false; 396 const SHEET_ID = (appEl.dataset.sheetId || '').trim(); 397 const TAB_NAME = (appEl.dataset.tab || 'All_Recipes').trim(); 398 const IS_PREVIEW = appEl.dataset.preview === 'true'; 399 const FALLBACK_MSG = appEl.dataset.fallback || 'No recipes available.'; 400 401 const DEFAULT_DESKTOP_SIZE = 12; 402const DEFAULT_NON_DESKTOP_SIZE = 6; 403const isNonDesktop = () => window.matchMedia('(max-width: 1024px)').matches; 404 405 // ---------- State ---------- 406let rawRows = []; 407let page = 1; 408 409// â set pageSize 410let pageSize = isNonDesktop() 411 ? DEFAULT_NON_DESKTOP_SIZE 412 : parseInt(pageSizeEl?.value || String(DEFAULT_DESKTOP_SIZE), 10); 413 414// â then override dropdown on mobile 415if (isNonDesktop() && pageSizeEl) { 416 pageSizeEl.value = String(DEFAULT_NON_DESKTOP_SIZE); 417} 418 419let userSetPageSize = false; 420 421 422 423 // ---------- Utility Functions ---------- 424 const debounce = (fn, ms = 400) => { 425 let timer; 426 return (...args) => { 427 clearTimeout(timer); 428 timer = setTimeout(() => fn(...args), ms); 429 }; 430 }; 431 432 const parseGviz = (txt) => { 433 const start = txt.indexOf('{'); 434 const end = txt.lastIndexOf('}') + 1; 435 return JSON.parse(txt.slice(start, end)); 436 }; 437 438 const getSafeString = (val) => String(val || '').trim(); 439 440 const escapeHtml = (str) => 441 String(str).replace(/[&<>"']/g, m => ({ 442 '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' 443 }[m])); 444 445 const toSentenceCase = (str) => { 446 if (!str) return ''; 447 return str.charAt(0).toUpperCase() + str.slice(1); 448 }; 449 450 // ---------- Text Processing Helpers (FIXED ORDER) ---------- 451 452 // Parse numbered or bulleted lists 453 const parseList = (text) => { 454 if (!text) return null; 455 456 // Split by numbered pattern (1. 2. 3.) OR bullet patterns (- ⢠*) 457 const splitPattern = /(?=\d+\.\s+|[-â¢*]\s+)/g; 458 const items = text.split(splitPattern) 459 .map(item => item.replace(/^(\d+\.|[-â¢*])\s*/, '').trim()) 460 .filter(item => item.length > 0); 461 462 // Must have at least 2 items to be a list 463 if (items.length < 2) return null; 464 465 return items; 466 }; 467 468 // Try to parse and pretty-print JSON, with fallback formatting 469const tryFormatJSON = (text) => { 470 try { 471 const parsed = JSON.parse(text); 472 return JSON.stringify(parsed, null, 2); 473 } catch (e) { 474 // JSON parsing failed - try to at least format it nicely 475 return text 476 .replace(/\s*,\s*/g, ',\n ') // Add line breaks after commas 477 .replace(/\{\s*/g, '{\n ') // Open brace + newline 478 .replace(/\s*\}/g, '\n}') // Close brace on new line 479 .replace(/:\s*/g, ': '); // Space after colons 480 } 481}; 482 483 // Detect if ENTIRE text is pure code/JSON (not just contains it) 484 const isPureCode = (text) => { 485 if (!text) return false;
486 const trimmed = text.trim(); 487 488 // Must START with JSON structure 489 if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || 490 (trimmed.startsWith('[') && trimmed.endsWith(']'))) { 491 return true; 492 } 493 494 return false; 495 }; 496 497 // Format text based on detected type (CRITICAL: CHECK LISTS FIRST!) 498 const formatText = (text, isDescription = false) => { 499 if (!text) return ''; 500 501 // Helper: Add period if text doesn't end with punctuation 502 const ensurePeriod = (str) => { 503 const trimmed = str.trim(); 504 if (/[.!?;]$/.test(trimmed)) return trimmed; 505 return trimmed + '.'; 506 }; 507 508 // Special handling for descriptions with mixed content (text + JSON) 509 if (isDescription) { 510 // Look for JSON blocks within text 511 const jsonMatch = text.match(/\{[^}]+\}/s); 512 if (jsonMatch) { 513 const jsonPart = jsonMatch[0]; 514 const beforeJSON = text.substring(0, jsonMatch.index).trim(); 515 const afterJSON = text.substring(jsonMatch.index + jsonPart.length).trim(); 516 517 let html = ''; 518 if (beforeJSON) html += `<p>${escapeHtml(ensurePeriod(beforeJSON))}</p>`; 519 html += `<pre><code>${escapeHtml(tryFormatJSON(jsonPart))}</code></pre>`; 520 if (afterJSON) html += `<p>${escapeHtml(ensurePeriod(afterJSON))}</p>`; 521 return html; 522 } 523 return `<p>${escapeHtml(ensurePeriod(text))}</p>`; 524 } 525 526 // For non-descriptions: CHECK LISTS FIRST (before code detection) 527 const listItems = parseList(text); 528 if (listItems) { 529 // Multi-item list - format as ordered list 530 const itemsHtml = listItems.map(item => `<li>${escapeHtml(ensurePeriod(item))}</li>`).join(''); 531 return `<ol>${itemsHtml}</ol>`; 532 } 533 534 // Check if text LOOKS like a single numbered/bulleted item 535 const singleItemPattern = /^(\d+\.|[-â¢*])\s+(.+)$/s; 536 const singleMatch = text.match(singleItemPattern); 537 if (singleMatch) { 538 // Strip the number/bullet and treat as paragraph 539 return `<p>${escapeHtml(ensurePeriod(singleMatch[2]))}</p>`; 540 } 541 542 // Then check if ENTIRE content is pure code/JSON 543 if (isPureCode(text)) { 544 const formatted = tryFormatJSON(text); 545 return `<pre><code>${escapeHtml(formatted)}</code></pre>`; 546 } 547 548 // Default to paragraph 549 return `<p>${escapeHtml(ensurePeriod(text))}</p>`; 550 }; 551 552 // ---------- Data Normalization ---------- 553 554 // Map sheet columns (A-K = 0-10) 555 const normalizeRow = (row) => ({ 556 id: getSafeString(row.c?.[0]?.v), // ID 557 title: getSafeString(row.c?.[1]?.v), // Title 558 shortDesc: getSafeString(row.c?.[2]?.v), // Short Description 559 fullDesc: getSafeString(row.c?.[3]?.v), // Full Description 560 requirements: getSafeString(row.c?.[4]?.v), // Requirements 561 inputs: getSafeString(row.c?.[5]?.v), // Inputs 562 useCases: getSafeString(row.c?.[6]?.v), // Use Cases 563 vertical: getSafeString(row.c?.[7]?.v), // Vertical 564 lifeCycle: getSafeString(row.c?.[8]?.v), // Lifecycle Stage 565 priority: Number(row.c?.[9]?.v) || 5, // Priority 566 publish: getSafeString(row.c?.[10]?.v).trim().toLowerCase(), // Published 567 slug: getSafeString(row.c?.[14]?.v), // ð Column O (15th column, index 14) 568 feedType: getSafeString(row.c?.[15]?.v).toLowerCase() // Feed Type (Column P) 569}); 570 571 572 573 const isValid = (row) => row.title && row.publish === 'yes'; 574 575 const matchesFilters = (row, query, lifeCycle, vertical) => { 576 const searchText = `${row.title} ${row.shortDesc} ${row.fullDesc}`.toLowerCase(); 577 // â Enhanced multi-word & fuzzy search 578function fuzzyMatch(text, term) { 579 const pattern = term.split('').join('.{0,2}'); // allows small typos 580 return new RegExp(pattern, 'i').test(text); 581} 582 583const terms = query.toLowerCase().split(/\s+/).filter(Boolean); 584const searchMatch = !query || terms.every(term => fuzzyMatch(searchText, term)); 585 586 const lifeCycleMatch = !lifeCycle || row.lifeCycle === lifeCycle; 587 const verticalMatch = !vertical || row.vertical === vertical; 588 589 return searchMatch && lifeCycleMatch && verticalMatch; 590 }; 591 592 // ---------- Pagination ---------- 593 594 const slicePage = (rows, currentPage, size) => { 595 const total = rows.length; 596 const pages = Math.max(1, Math.ceil(total / size)); 597 const cur = Math.min(Math.max(1, currentPage), pages); 598 const start = (cur - 1) * size; 599 const pageRows = rows.slice(start, start + size); 600 601 return { pageRows, cur, pages, total }; 602 }; 603 604 // ---------- Data Fetching ---------- 605async function fetchSheet(sheetId, tabName) { 606 if (!sheetId) { 607 throw new Error('Missing data-sheet-id on #recipes-app'); 608 } 609 610 // â Use cache only if not in preview mode 611 const cached = sessionStorage.getItem('recipesCache'); 612 if (cached && !IS_PREVIEW) { 613 if (DEBUG) console.log('[recipes] Using cached data'); 614 return JSON.parse(cached); 615 } 616 617 const cacheBuster = IS_PREVIEW ? `&_=${Date.now()}` : ''; 618 const baseUrl = `https://docs.google.com/spreadsheets/d/${encodeURIComponent(sheetId)}/gviz/tq`; 619 const url = `${baseUrl}?tqx=out:json&sheet=${encodeURIComponent(tabName)}${cacheBuster}`; 620 621 if (DEBUG) console.log(`[recipes] Fetching: ${url}`); 622 623 try { 624 const response = await fetch(url, { cache: 'no-store' }); 625 if (!response.ok) throw new Error(`HTTP ${response.status}`); 626 627 const jsonData = parseGviz(await response.text()); 628 const allRows = jsonData.table?.rows || []; 629 const validRows = allRows.map(normalizeRow).filter(isValid); 630 631 if (DEBUG) { 632 console.log('[recipes] Total rows:', allRows.length); 633 console.log('[recipes] Valid published rows:', validRows.length); 634 console.log('[recipes] Sample valid row:', validRows[0]); 635 } 636 637 if (validRows.length === 0 && allRows.length > 0 && DEBUG) { 638 console.warn('[recipes] No valid rows found. Check Title (col B) and Published=yes (col K)'); 639 } 640 641 if (!IS_PREVIEW) { 642 sessionStorage.setItem('recipesCache', JSON.stringify(validRows)); 643 } 644 645 return validRows; 646 } catch (err) { 647 if (DEBUG) console.error('[recipes] Initialization failed:', err); 648 return []; 649 } 650} 651 652 // -----------Render for feed recipe lists -------------- 653 654 function renderFeedRecipeList(list, containerId) { 655 const container = document.getElementById(containerId); 656 if (!container) return; 657 658 const PAGE_SIZE = 10; 659 let currentPage = 1; 660 661 function renderPage() { 662 const start = (currentPage - 1) * PAGE_SIZE; 663 const pageItems = list.slice(start, start + PAGE_SIZE); 664 665 container.innerHTML = pageItems.map(r => ` 666 <button class="feed-recipe-link" data-id="${r.id}"> 667 ${r.title} 668 </button> 669 `).join('') + renderPager(); 670 671 attachClicks(); 672 } 673 674 function renderPager() { 675 const totalPages = Math.ceil(list.length / PAGE_SIZE); 676 if (totalPages <= 1) return ''; 677 678 return ` 679 <div class="feed-pager"> 680 <button ${currentPage === 1 ? 'disabled' : ''} data-prev>Prev</button>
681 <span>${currentPage} / ${totalPages}</span> 682 <button ${currentPage === totalPages ? 'disabled' : ''} data-next>Next</button> 683 </div> 684 `; 685 } 686 687 function attachClicks() { 688 container.querySelectorAll('.feed-recipe-link').forEach(btn => { 689 btn.addEventListener('click', () => { 690 const recipe = rawRows.find(r => r.id === btn.dataset.id); 691 if (recipe) showModal(recipe); // ð reuse modal 692 }); 693 }); 694 695 const prev = container.querySelector('[data-prev]'); 696 const next = container.querySelector('[data-next]'); 697 698 if (prev) prev.onclick = () => { currentPage--; renderPage(); }; 699 if (next) next.onclick = () => { currentPage++; renderPage(); }; 700 } 701 702 renderPage(); 703} 704 705 706 707 // ---------- Modal Functions ---------- 708 709const showModal = (recipe) => { 710 if (!modalOverlay || !modalTitle || !modalContentArea) { 711 return console.error('[recipes] Modal elements missing at runtime.'); 712 } 713 714 // 1. Build the sections HTML, running content through formatText() immediately 715 const sections = [ 716 { title: 'Description', content: recipe.fullDesc, isDescription: true }, 717 { title: 'Requirements', content: recipe.requirements }, 718 { title: 'Input / Configuration', content: recipe.inputs }, 719 { title: 'Suggested Use Cases', content: recipe.useCases } 720 ]; 721 722 const sectionsHtml = sections 723 .filter(section => section.content && formatText(section.content, section.isDescription)) // Filter out empty content after formatting 724 .map(section => ` 725 <div class="modal-section"> 726 <h3>${section.title}</h3> 727 ${formatText(section.content, section.isDescription)} 728 </div> 729 `) 730 .join(''); 731 732 // If sectionsHtml is empty, show a fallback message instead of crashing
733 if (!sectionsHtml) { 734 modalContentArea.innerHTML = `<p>Error: Recipe details are missing or could not be formatted correctly.</p>`; 735 } else { 736 modalContentArea.innerHTML = sectionsHtml; 737 } 738 739 // 2. Update modal header content 740 modalTitle.textContent = toSentenceCase(recipe.title); 741 742 // Safely set tag content 743 if (modalTagLifecycle) { 744 modalTagLifecycle.textContent = toSentenceCase(recipe.lifeCycle || 'N/A'); 745 modalTagLifecycle.style.display = recipe.lifeCycle ? 'inline-block' : 'none'; 746 } 747 if (modalTagVertical) { 748 modalTagVertical.textContent = toSentenceCase(recipe.vertical || 'N/A'); 749 modalTagVertical.style.display = recipe.vertical ? 'inline-block' : 'none'; 750 } 751 752 // 3. Show the modal 753 modalOverlay.classList.remove('hidden'); 754 document.body.style.overflow = 'hidden'; 755}; 756 757const hideModal = () => { 758 modalOverlay.classList.add('hidden'); 759 document.body.style.overflow = ''; 760 const baseUrl = window.location.origin + window.location.pathname; 761const qs = new URLSearchParams(window.location.search); 762qs.delete('slug'); 763const newUrl = qs.toString() ? `${baseUrl}?${qs.toString()}` : baseUrl; 764window.history.replaceState({}, '', newUrl); 765 766}; 767 768 // ---------- Rendering ---------- 769 770 function showLoading() { 771 listEl.innerHTML = '<div class="recipes-loading">Loading recipesâ¦</div>'; 772 pagerEl.innerHTML = ''; 773 } 774 775 function renderCards(rows) { 776 if (!rows.length) { 777 const query = searchEl?.value?.trim() || ''; 778const lifeCycle = lifeCycleEl?.value?.trim() || ''; 779const vertical = verticalEl?.value?.trim() || ''; 780 781const isFilterActive = query || lifeCycle || vertical; 782 783const message = isFilterActive 784 ? `Try adjusting your filters or search to discover other recommendation recipes.` 785 : FALLBACK_MSG; 786 787const title = isFilterActive 788 ? 'No recommendation recipes found' 789 : 'No recommendation recipes published yet'; 790 791listEl.innerHTML = ` 792 <div class="recipes-empty"> 793 <h3>${title}</h3> 794 <p>${message}</p> 795 </div>`; 796 797 798 pagerEl.innerHTML = ''; 799 return; 800 } 801 802 listEl.innerHTML = rows.map(recipe => ` 803 <article class="recipe-card-v2" data-recipe-id="${escapeHtml(recipe.id)}" data-slug="${escapeHtml(recipe.slug)}"> 804 <div> 805 <h3 class="recipe-card-title">${escapeHtml(toSentenceCase(recipe.title))}</h3> 806 <p class="recipe-card-desc">${escapeHtml(recipe.shortDesc)}</p> 807 </div> 808 <div class="recipe-card-tags"> 809 <span class="tag tag-lifecycle">${escapeHtml(toSentenceCase(recipe.lifeCycle))}</span> 810 <span class="tag tag-vertical">${escapeHtml(toSentenceCase(recipe.vertical))}</span> 811 </div> 812 </article> 813 `).join(''); 814 815 // Attach click handlers 816 listEl.querySelectorAll('.recipe-card-v2').forEach(card => { 817 card.addEventListener('click', () => { 818 const recipeId = card.dataset.recipeId; 819 const recipe = rawRows.find(r => r.id === recipeId); 820 if (recipe) { 821 showModal(recipe); 822 if (recipe.slug) { 823 const baseUrl = window.location.origin + window.location.pathname; 824const qs = new URLSearchParams(window.location.search); 825qs.set('slug', recipe.slug); 826window.history.pushState({}, '', `${baseUrl}?${qs.toString()}`); 827 828 } 829} 830 831 }); 832 }); 833 } 834 835 function renderPagination(current, total, itemCount) { 836 if (total <= 1) { 837 pagerEl.innerHTML = ''; 838 return; 839 } 840 841 const btn = (label, targetPage, disabled = false) => 842 `<button class="pg-btn" data-page="${targetPage}" ${disabled ? 'disabled' : ''}>${label}</button>`; 843 844 pagerEl.innerHTML = ` 845 <div class="pager"> 846 ${btn('« First', 1, current <= 1)} 847 ${btn('â¹ Prev', current - 1, current <= 1)} 848 <span class="pg-status">Page ${current} of ${total} · ${itemCount} items</span> 849 ${btn('Next âº', current + 1, current >= total)} 850 ${btn('Last »', total, current >= total)} 851 </div>`; 852 } 853 854 // ------------ Render functions for feed lists --------- 855 856 function renderFeedRecipeGroups() { 857 const userProductRecipes = rawRows 858 .filter(r => r.feedType === 'user-product') 859 .sort((a, b) => a.title.localeCompare(b.title)); 860 861 const productProductRecipes = rawRows 862 .filter(r => r.feedType === 'product-product') 863 .sort((a, b) => a.title.localeCompare(b.title)); 864 865 renderFeedRecipeList(userProductRecipes, 'feed-user-recipes'); 866 renderFeedRecipeList(productProductRecipes, 'feed-product-recipes'); 867} 868 869 // ---------- Apply View ---------- 870 871 function applyView() { 872 const query = searchEl?.value?.trim() || ''; 873 const lifeCycle = lifeCycleEl?.value?.trim() || ''; 874 const vertical = verticalEl?.value?.trim() || ''; 875 876 const filtered = rawRows.filter(r => matchesFilters(r, query, lifeCycle, vertical)); 877 const { pageRows, cur, pages, total } = slicePage(filtered, page, pageSize); 878 879 renderCards(pageRows); 880 renderPagination(cur, pages, total); 881 } 882 883 // ---------- Event Handlers ---------- 884 885 // Modal events 886 if (modalCloseBtn) { 887 modalCloseBtn.addEventListener('click', hideModal); 888 } 889 890 if (modalOverlay) { 891 modalOverlay.addEventListener('click', (e) => { 892 if (e.target === modalOverlay) hideModal(); 893 }); 894 } 895 896 // Filter events 897 if (searchEl) { 898 searchEl.addEventListener('input', debounce(() => { 899 page = 1; 900 applyView(); 901 })); 902 } 903 904 if (lifeCycleEl) { 905 lifeCycleEl.addEventListener('change', () => { 906 page = 1; 907 applyView(); 908 }); 909 } 910 911 if (verticalEl) { 912 verticalEl.addEventListener('change', () => { 913 page = 1; 914 applyView(); 915 }); 916 } 917 918 if (pageSizeEl) { 919 pageSizeEl.addEventListener('change', () => { 920 userSetPageSize = true; 921 pageSize = parseInt(pageSizeEl.value, 10) || DEFAULT_DESKTOP_SIZE; 922 page = 1; 923 applyView(); 924 }); 925} 926 927 // â Reset All Button: clears search, filters, page size, and resets view 928 const clearBtn = document.getElementById('recipes-clear-filters'); 929 if (clearBtn) { 930 clearBtn.addEventListener('click', () => { 931 // Clear search input 932 searchEl.value = ''; 933 934 // Reset dropdown filters 935 lifeCycleEl.value = ''; 936 verticalEl.value = ''; 937 938 // Reset page size based on screen type 939const resetSize = isNonDesktop() ? DEFAULT_NON_DESKTOP_SIZE : DEFAULT_DESKTOP_SIZE;
940pageSizeEl.value = String(resetSize); 941pageSize = resetSize; 942 943 944 // Reset pagination 945 page = 1; 946 947 // Re-render 948 applyView(); 949 }); 950 } 951 952 953 // Pagination events 954pagerEl.addEventListener('click', (e) => { 955 const btn = e.target.closest('button[data-page]'); 956 if (btn) { 957 page = parseInt(btn.dataset.page, 10); 958 applyView(); 959 960 // Scroll to recipe list with reliable positioning 961 setTimeout(() => { 962 const rect = listEl.getBoundingClientRect(); 963 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 964 const targetPosition = rect.top + scrollTop - 300; // 300px above the list 965 966 window.scrollTo({ 967 top: targetPosition, 968 behavior: 'smooth' 969 }); 970 }, 50); // Small delay to ensure content is rendered 971 } 972}); 973 974 // ---------- Initialize ---------- 975(async function init() { 976 try { 977 if (!SHEET_ID) { 978 throw new Error('Missing data-sheet-id on #recipes-app'); 979 } 980 981 showLoading(); 982 rawRows = await fetchSheet(SHEET_ID, TAB_NAME); 983 renderFeedRecipeGroups(); 984 985 // â Deep-link + filter support (Zendesk-friendly: ?slug=) 986const urlParams = new URLSearchParams(window.location.search); 987 988const slugFromUrl = urlParams.get('slug'); 989if (slugFromUrl) { 990 const matched = rawRows.find(r => r.slug === slugFromUrl); 991 if (matched) showModal(matched); 992} 993 994if (pageSizeEl) { 995 pageSizeEl.value = String(pageSize); 996} 997 998// â Apply filters from URL parameters 999const presetVertical = urlParams.get('vertical'); 1000const presetLifecycle = urlParams.get('lifecycle'); 1001const presetSearch = urlParams.get('q'); 1002 1003if (presetVertical && verticalEl) verticalEl.value = presetVertical; 1004if (presetLifecycle && lifeCycleEl) lifeCycleEl.value = presetLifecycle; 1005if (presetSearch && searchEl) searchEl.value = presetSearch; 1006 1007page = 1; 1008applyView(); 1009 1010 1011 1012 // â Render vertical sections after data load 1013 renderVerticalSections(); 1014 1015 /* for SEO */ 1016 setTimeout(renderCrawlableList, 2000); // delay SEO block load 1017
1018 // renderCrawlableList(); // 1019// autoCycleModals(); // disable automatic modal cycling 1020 1021 1022 } catch (error) { 1023 if (DEBUG) console.error('[recipes] Initialization failed:', error); 1024 listEl.innerHTML = ` 1025 <div class="recipes-empty"> 1026 <h3>Failed to Load</h3> 1027 <p>${FALLBACK_MSG}</p> 1028 </div>`; 1029 pagerEl.innerHTML = ''; 1030 } 1031})(); 1032 1033 1034 // ========================================================= 1035// Render Vertical Sections (Retail / Marketplaces / etc.) 1036// This uses column H (Vertical) and column J (Priority). 1037// Priority order: 1â4 first, then fallback rows in sheet order. 1038// Renders max 4 cards per vertical section and shows 1039// fallback message if no recipes found. 1040// ========================================================= 1041function renderVerticalSections() { 1042 document.querySelectorAll('.recipe-section-x[data-vertical]').forEach(section => { 1043 const verticalName = section.dataset.vertical || ''; 1044 const listEl = section.querySelector('.recipe-cards-x'); 1045 1046 // Step 1: Filter all rows for this vertical 1047 const verticalRows = rawRows.filter(r => r.vertical === verticalName); 1048 1049 // Step 2: Get priority 1â4 rows in order 1050 const priorityRows = [1, 2, 3, 4] 1051 .map(p => verticalRows.find(r => r.priority === p)) 1052 .filter(Boolean); 1053 1054 // Step 3: Fill remaining slots with other rows (excluding already selected) 1055 const remainingRows = verticalRows 1056 .filter(r => !priorityRows.includes(r)) 1057 .slice(0, 4 - priorityRows.length); 1058 1059 // Step 4: Final selection (max 4, no duplicates) 1060 const rowsForVertical = [...priorityRows, ...remainingRows]; 1061 1062 // Step 5: Debug logs 1063 if (DEBUG) console.log(`[recipes] Vertical: ${verticalName}`); 1064 if (DEBUG) console.log('Priority rows:', priorityRows); 1065 if (DEBUG) console.log('Remaining rows:', remainingRows); 1066 if (DEBUG) console.log('Selected rows:', rowsForVertical); 1067 1068 // Step 6: Fallback message if nothing to show 1069 if (!rowsForVertical.length) { 1070 listEl.innerHTML = ` 1071 <div class="recipes-empty"> 1072 <h3>No recommendation recipes published yet</h3> 1073 <p>Weâre updating recommendation recipes soon. Check back later.</p> 1074 </div>`; 1075 return; 1076 } 1077 1078 // Step 7: Render recipe cards 1079 listEl.innerHTML = rowsForVertical.map(recipe => ` 1080 <article class="recipe-card-v2" data-recipe-id="${escapeHtml(recipe.id)}" data-slug="${escapeHtml(recipe.slug)}"> 1081 <div> 1082 <h3 class="recipe-card-title">${escapeHtml(recipe.title)}</h3> 1083 <p class="recipe-card-desc">${escapeHtml(recipe.shortDesc)}</p> 1084 </div> 1085 <div class="recipe-card-tags"> 1086 <span class="tag tag-lifecycle">${escapeHtml(recipe.lifeCycle)}</span> 1087 <span class="tag tag-vertical">${escapeHtml(recipe.vertical)}</span> 1088 </div> 1089 </article> 1090 `).join(''); 1091 1092 // Step 8: Modal click handlers 1093 listEl.querySelectorAll('.recipe-card-v2').forEach(card => { 1094 card.addEventListener('click', () => { 1095 const recipeId = card.dataset.recipeId; 1096 const recipe = rawRows.find(r => r.id === recipeId); 1097 if (recipe) { 1098 showModal(recipe); 1099 if (recipe.slug) { 1100 const baseUrl = window.location.origin + window.location.pathname; 1101const qs = new URLSearchParams(window.location.search); 1102qs.set('slug', recipe.slug); 1103window.history.pushState({}, '', `${baseUrl}?${qs.toString()}`); 1104 1105 } 1106} 1107 1108 }); 1109 }); 1110 }); 1111} 1112 1113// ========================================================= 1114// Hidden Crawlable List + Auto Modal Cycle (for SEO) 1115// =========================================================
1116function renderCrawlableList() { 1117 const container = document.getElementById('recipes-crawlable'); 1118 if (!container) return; 1119 1120 const ul = container.querySelector('ul'); 1121 if (!ul) return; 1122 1123 ul.innerHTML = rawRows 1124 .map(r => `<li><a href="?slug=${r.slug}">${escapeHtml(r.title)} (${escapeHtml(r.vertical)})</a></li>`) 1125 .join(''); 1126 1127if (DEBUG) console.log(`[recipes] Crawlable list generated with ${rawRows.length} entries`); 1128} 1129 1130function autoCycleModals() { 1131 const links = document.querySelectorAll('#recipes-crawlable a'); 1132 if (!links.length) return; 1133 1134 let index = 0; 1135 const openNext = () => { 1136 if (index >= links.length) return; 1137 const slug = new URL(links[index].href).searchParams.get('slug'); 1138 const recipe = rawRows.find(r => r.slug === slug); 1139 1140 if (recipe) { 1141 showModal(recipe); 1142 if (DEBUG) console.log(`[SEO] Opened modal for: ${slug}`); 1143 } 1144 1145 index++; 1146 setTimeout(() => { 1147 hideModal(); 1148 setTimeout(openNext, 300); 1149 }, 300); 1150 }; 1151 1152 // Run this only in preview mode 1153 if (IS_PREVIEW) setTimeout(openNext, 1000); 1154} 1155 1156 1157 1158})(); 1159 1160 1161 1162 1163 1164 1165 1166}); // â end $(function(){ ... }) 1167 1168 1169 1170// ========================================================= 1171 // Deliverability Glossary (bs-gl) 1172 // Feb 2026 · v8 1173 // Auto-sort, auto-group, auto alpha nav 1174 // HTML is just a flat bag of entries â JS does everything 1175 // ========================================================= 1176 (function() { 1177 var $list = $('#bsGlList'); 1178 if (!$list.length) return; 1179 1180 var $search = $('#bsGlSearch'); 1181 var $clearBtn = $('#bsGlClear'); 1182 var $alpha = $('#bsGlAlpha'); 1183 var $empty = $('#bsGlEmpty'); 1184 var $info = $('#bsGlInfo'); 1185 var $count = $('#bsGlCount'); 1186 var $controls = $('#bsGlControls'); 1187 var $topBtn = $('#bsGlTop'); 1188 1189 // Toast: append to body so it doesn't affect article layout 1190 var $toast = $('<div class="bs-gl-toast" id="bsGlToast" aria-live="polite"></div>'); 1191 $('body').append($toast); 1192 1193 var H = 'bs-gl-hidden'; 1194 1195 // âââ Step 1: Read all entries and sort âââ 1196 1197 var entries = []; 1198 $list.find('.bs-gl-entry').each(function() { 1199 var $el = $(this); 1200 var term = ($el.data('term') || '') + ''; 1201 var full = ($el.data('full') || '') + ''; 1202 var displayName = full || term; 1203 var firstChar = displayName.charAt(0).toUpperCase(); 1204 var group = /[A-Z]/.test(firstChar) ? firstChar : 'num'; 1205 1206 entries.push({ 1207 $el: $el, 1208 term: term, 1209 full: full, 1210 displayName: displayName, 1211 group: group, 1212 sortKey: displayName.toLowerCase() 1213 }); 1214 }); 1215 1216 // Sort: non-alpha group first, then A-Z, alphabetical within each group 1217 entries.sort(function(a, b) { 1218 if (a.group === 'num' && b.group !== 'num') return -1; 1219 if (a.group !== 'num' && b.group === 'num') return 1; 1220 if (a.group !== b.group) return a.group < b.group ? -1 : 1; 1221 return a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0; 1222 }); 1223 1224 // âââ Step 2: Rebuild DOM with letter headers âââ 1225 1226 $list.empty(); 1227 var currentGroup = null; 1228 var hasNonAlpha = entries.some(function(e) { return e.group === 'num'; }); 1229 1230 entries.forEach(function(entry) { 1231 if (entry.group !== currentGroup) { 1232 currentGroup = entry.group; 1233 var display = currentGroup === 'num' ? '#' : currentGroup; 1234 var $header = $('<div class="bs-gl-letter" id="letter-' + currentGroup + '"><span>' + display + '</span></div>'); 1235 $list.append($header); 1236 } 1237 $list.append(entry.$el); 1238 }); 1239 1240 // âââ Step 3: Re-query DOM after rebuild âââ 1241 1242 var $entries = $list.find('.bs-gl-entry'); 1243 var $letters = $list.find('.bs-gl-letter'); 1244 var $children = $list.children(); 1245 var total = $entries.length; 1246 1247 // Auto-count 1248 $count.text(total + ' terms'); 1249 1250 // âââ Step 4: Build search index âââ 1251 1252 var idx = []; 1253 $entries.each(function() { 1254 var $el = $(this); 1255 var term = ($el.data('term') || '') + ''; 1256 var full = ($el.data('full') || '') + ''; 1257 var def = $el.find('p').not('.bs-gl-term-ex').not(':has(.bs-gl-term-link)').last().text() || ''; 1258 idx.push({ 1259 $el: $el, 1260 text: (term + ' ' + full + ' ' + def).toLowerCase() 1261 }); 1262 }); 1263 1264 // âââ Stemmer âââ 1265 1266 function stem(w) { 1267 if (w.length < 4) return w; 1268 return w.replace(/(ations?|tions?|ments?|ing|ed|ly|ic|al|es|s)$/, ''); 1269 } 1270 1271 // Multi-layer match 1272 function matches(hay, needle) { 1273 if (hay.indexOf(needle) >= 0) return true; 1274 var root = stem(needle); 1275 if (root.length >= 3 && hay.indexOf(root) >= 0) return true; 1276 if (needle.length >= 4) { 1277 try { 1278 if (new RegExp(needle.split('').join('.?'), 'i').test(hay)) return true; 1279 } catch(e) {} 1280 } 1281 return false;
1282 } 1283 1284 // âââ Separators (JS-managed) âââ 1285 1286 function updateSeps() { 1287 $entries.removeClass('bs-gl-sep'); 1288 var prev = false; 1289 $children.each(function() { 1290 var $c = $(this); 1291 if ($c.hasClass('bs-gl-letter')) { 1292 prev = $c.hasClass(H) ? prev : false; 1293 } else if ($c.hasClass('bs-gl-entry') && !$c.hasClass(H)) { 1294 if (prev) $c.addClass('bs-gl-sep'); 1295 prev = true; 1296 } 1297 }); 1298 } 1299 1300 // âââ Alpha nav âââ 1301 1302 function buildAlpha() { 1303 var abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); 1304 var active = {}; 1305 $letters.not('.' + H).each(function() { active[this.id.replace('letter-', '')] = true; }); 1306 1307 var html = ''; 1308 1309 // # button â only if non-alpha entries exist 1310 if (hasNonAlpha) { 1311 html += '<a href="#letter-num"' + (active['num'] ? '' : ' class="bs-gl-off"') + '>#</a>'; 1312 } 1313 1314 // A-Z â always shown 1315 html += abc.map(function(c) { 1316 return '<a href="#letter-' + c + '"' + (active[c] ? '' : ' class="bs-gl-off"') + '>' + c + '</a>'; 1317 }).join(''); 1318 1319 $alpha.html(html); 1320 } 1321 1322 // âââ Search âââ 1323 1324 function doSearch(q) { 1325 q = (q || '').trim().toLowerCase(); 1326 var terms = q ? q.split(/\s+/) : []; 1327 var found = 0; 1328 1329 // Clear highlights 1330 $entries.find('mark').each(function() { 1331 var p = this.parentNode; 1332 p.replaceChild(document.createTextNode(this.textContent), this); 1333 p.normalize(); 1334 }); 1335 $entries.find('span').not('.bs-gl-badge').each(function() { 1336 if (!this.className && this.childNodes.length) $(this).replaceWith($(this).contents()); 1337 }); 1338 1339 // Show/hide 1340 idx.forEach(function(item) { 1341 var ok = !terms.length || terms.every(function(t) { return matches(item.text, t); }); 1342 item.$el.toggleClass(H, !ok); 1343 if (ok) found++; 1344 }); 1345 1346 // Highlight 1347 if (q && found) { 1348 try { 1349 var re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi'); 1350 $entries.not('.' + H).find('p').each(function() { 1351 $(this).contents().filter(function() { return this.nodeType === 3; }).each(function() { 1352 re.lastIndex = 0; 1353 if (re.test(this.textContent)) { 1354 re.lastIndex = 0; 1355 var s = document.createElement('span'); 1356 s.innerHTML = this.textContent.replace(re, '<mark>$1</mark>'); 1357 this.parentNode.replaceChild(s, this); 1358 } 1359 }); 1360 }); 1361 } catch(e) {} 1362 } 1363 1364 // Letter headers â hide if no visible entries beneath 1365 $letters.each(function() { 1366 var $h = $(this); 1367 $h.toggleClass(H, !$h.nextUntil('.bs-gl-letter', '.bs-gl-entry').not('.' + H).length); 1368 }); 1369 1370 buildAlpha(); 1371 updateSeps(); 1372 1373 // Count logic 1374 if (terms.length) { 1375 $count.hide(); 1376 $info.html('<b>' + found + '</b> of ' + total + ' terms').addClass('bs-gl-show'); 1377 $empty.toggle(found === 0); 1378 } else { 1379 $count.show(); 1380 $info.removeClass('bs-gl-show').empty(); 1381 $empty.hide(); 1382 } 1383 } 1384 1385 // âââ Events âââ 1386 1387 $search.on('input', function() { 1388 $clearBtn.toggleClass('bs-gl-show', this.value.length > 0); 1389 doSearch(this.value); 1390 }); 1391 1392 $clearBtn.on('click', function() { $search.val('').trigger('input').focus(); }); 1393 $(document).on('click', '#bsGlClearLink', function(e) { e.preventDefault(); $search.val('').trigger('input'); }); 1394 1395 // Keyboard 1396 $(document).on('keydown', function(e) { 1397 if (e.key === '/' && !$(e.target).is('input,textarea,select')) { e.preventDefault(); $search.focus(); } 1398 if (e.key === 'Escape' && $search.is(':focus')) { $search.val('').trigger('input').blur(); } 1399 }); 1400 1401 // âââ Clipboard âââ 1402 1403 function copyText(text, ok, fail) { 1404 if (navigator.clipboard && navigator.clipboard.writeText) { 1405 navigator.clipboard.writeText(text).then(ok).catch(function() { fbCopy(text, ok, fail); }); 1406 } else { 1407 fbCopy(text, ok, fail); 1408 } 1409 } 1410 1411 function fbCopy(text, ok, fail) {
1412 try { 1413 var ta = document.createElement('textarea'); 1414 ta.value = text; 1415 ta.style.cssText = 'position:fixed;left:-9999px'; 1416 document.body.appendChild(ta); 1417 ta.select(); 1418 document.execCommand('copy'); 1419 document.body.removeChild(ta); 1420 ok(); 1421 } catch(e) { if (fail) fail(); } 1422 } 1423 1424 function toast(msg) { 1425 $toast.text(msg).addClass('bs-gl-show'); 1426 setTimeout(function() { $toast.removeClass('bs-gl-show'); }, 1800); 1427 } 1428 1429 // âââ Permalink âââ 1430 1431 $list.on('click', '.bs-gl-hash', function(e) { 1432 e.preventDefault(); 1433 var hash = $(this).attr('href'); 1434 var url = location.href.split('#')[0] + hash; 1435 history.replaceState(null, null, hash); 1436 copyText(url, function() { toast('Link copied'); }, function() { toast('URL updated'); }); 1437 }); 1438 1439 // âââ Back-to-top: smooth brake at content boundary âââ 1440 1441 // Move button inside list so it can use absolute positioning relative to list 1442 $list.append($topBtn); 1443 1444 function positionTopBtnFixed() { 1445 var $anchor = $list.closest('.article-body, .article__body, [role="main"]'); 1446 if (!$anchor.length) $anchor = $list; 1447 var rect = $anchor[0].getBoundingClientRect(); 1448 var right = rect.right - 16; 1449 $topBtn.css({ 1450 position: 'fixed', 1451 bottom: '24px', 1452 top: 'auto', 1453 left: (right - $topBtn.outerWidth()) + 'px', 1454 right: 'auto' 1455 }); 1456 } 1457 1458 function positionTopBtnAbsolute() { 1459 // Park just below the last entry 1460 $topBtn.css({ 1461 position: 'absolute', 1462 bottom: '0', 1463 top: 'auto', 1464 left: 'auto', 1465 right: '8px' 1466 }); 1467 } 1468 1469 // âââ Scroll: sticky shadow + back-to-top + scroll-spy âââ 1470 1471 $(window).on('scroll', function() { 1472 var st = $(window).scrollTop(); 1473 $controls.toggleClass('bs-gl-scrolled', st > 20); 1474 1475 // Back to top with brake 1476 if (st > 400) { 1477 $topBtn.addClass('bs-gl-visible'); 1478 1479 // Check if fixed position would overflow past parking zone 1480 var listBottom = $list.offset().top + $list.outerHeight(); 1481 var winH = $(window).height(); 1482 var fixedBtnBottom = st + winH - 24; // where btn bottom edge would be if fixed 1483 1484 if (fixedBtnBottom > listBottom - 14) { 1485 // Past content: park in padding zone (absolute) 1486 positionTopBtnAbsolute(); 1487 } else { 1488 // Inside content: float (fixed) 1489 positionTopBtnFixed(); 1490 } 1491 } else { 1492 $topBtn.removeClass('bs-gl-visible'); 1493 } 1494 1495 var cur = ''; 1496 $letters.not('.' + H).each(function() { 1497 if ($(this).offset().top - 180 <= st) cur = this.id.replace('letter-', ''); 1498 }); 1499 $alpha.find('a').removeClass('bs-gl-on'); 1500 if (cur) $alpha.find('a[href="#letter-' + cur + '"]').addClass('bs-gl-on'); 1501 }); 1502 1503 $(window).on('resize', function() { 1504 if ($topBtn.hasClass('bs-gl-visible')) { 1505 $(window).trigger('scroll'); // recalculate position 1506 } 1507 }); 1508 1509 $topBtn.on('click', function() { 1510 $('html, body').animate({ scrollTop: 0 }, 400); 1511 }); 1512 1513 // âââ Init âââ 1514 1515 // Assign IDs to entries 1516 $entries.each(function() { 1517 var t = ($(this).data('term') || '') + ''; 1518 var slug = t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, ''); 1519 if (slug) this.id = 'term-' + slug; 1520 }); 1521 1522 buildAlpha(); 1523 updateSeps(); 1524 1525 // Deep link 1526 if (location.hash && location.hash.indexOf('#term-') === 0) { 1527 var $t = $(location.hash); 1528 if ($t.length) { 1529 setTimeout(function() { 1530 $('html, body').animate({ scrollTop: $t.offset().top - 150 }, 300); 1531 $t.css('background', '#EBF2FF'); 1532 setTimeout(function() { $t.css('transition', 'background 1s').css('background', ''); }, 100); 1533 }, 400); 1534 } 1535 } 1536 })(); 1537 1538 1539/* ========================================================================== 1540 FAQs and troubleshooting â back-to-top with brake 1541 Append to bottom of existing script.js. 1542 ========================================================================== */ 1543(function () { 1544 var $topBtn = $('#bsFaqTop'); 1545 var $page = $('#bs-faq-page'); 1546 1547 if (!$topBtn.length || !$page.length) return; 1548 1549 $page.css('position', 'relative').append($topBtn); 1550 1551 function positionFixed() { 1552 var $anchor = $page.closest('.article-body, .article__body, [role="main"]'); 1553 if (!$anchor.length) $anchor = $page; 1554 var rect = $anchor[0].getBoundingClientRect(); 1555 var right = rect.right - 16; 1556 $topBtn.css({ 1557 position: 'fixed', 1558 bottom: '24px', 1559 top: 'auto', 1560 left: (right - $topBtn.outerWidth()) + 'px', 1561 right: 'auto' 1562 }); 1563 } 1564 1565 function positionAbsolute() { 1566 $topBtn.css({ 1567 position: 'absolute', 1568 bottom: '0', 1569 top: 'auto', 1570 left: 'auto', 1571 right: '8px' 1572 }); 1573 } 1574 1575 $(window).on('scroll.bsfaq resize.bsfaq', function () { 1576 var st = $(window).scrollTop(); 1577 1578 if (st > 400) { 1579 $topBtn.addClass('bs-gl-visible'); 1580 1581 var pageBottom = $page.offset().top + $page.outerHeight(); 1582 var fixedBtnBottom = st + $(window).height() - 24; 1583 1584 if (fixedBtnBottom > pageBottom - 14) { 1585 positionAbsolute(); 1586 } else { 1587 positionFixed(); 1588 } 1589 } else { 1590 $topBtn.removeClass('bs-gl-visible'); 1591 } 1592 }); 1593 1594 $topBtn.on('click', function () { 1595 $('html, body').animate({ scrollTop: 0 }, 400); 1596 }); 1597}()); 1598 1599 1600// ========================================================= 1601// MCP Tools Explorer â Script 1602// Created: 07 Mar 2026 1603// Last Updated: 07 Mar 2026 1604// 1605// Changelog: 1606// 07 Mar 2026 â v1. Initial working version. 1607// 07 Mar 2026 â v3. Added fuzzy search, search highlight, 1608// intro count badge, status range, category 1609// counts in dropdown, improved filter reset. 1610// 1611// Sheet columns: Tool (A), Description (B), Category (C), 1612// Type (D), Published (E) 1613// ========================================================= 1614 1615(function mcpToolsExplorer() { 1616 var appEl = document.getElementById('mcp-tools-app'); 1617 var tbodyEl = document.getElementById('mcp-tbody'); 1618 var searchEl = document.getElementById('mcp-search'); 1619 var searchClearEl = document.getElementById('mcp-search-clear'); 1620 var catFilterEl = document.getElementById('mcp-filter-category'); 1621 var typeFilterEl = document.getElementById('mcp-filter-type'); 1622 var clearBtn = document.getElementById('mcp-clear'); 1623 var countEl = document.getElementById('mcp-count'); 1624 var introCountEl = document.getElementById('mcp-intro-count'); 1625 var pageSizeEl = document.getElementById('mcp-page-size'); 1626 var paginationEl = document.getElementById('mcp-pagination'); 1627 var theadCols = document.querySelectorAll('.mcp-table th[data-col]'); 1628 1629 if (!appEl || !tbodyEl) return; 1630 1631 var SHEET_ID = (appEl.dataset.sheetId || '').trim(); 1632 var TAB_NAME = (appEl.dataset.tab || 'MCP Tools').trim(); 1633 var IS_PREVIEW = appEl.dataset.preview === 'true'; 1634 1635 var allRows = []; 1636 var filtered = []; 1637 var page = 1; 1638 var pageSize = parseInt(pageSizeEl ? pageSizeEl.value : '25', 10) || 25; 1639 var sortCol = 'tool'; 1640 var sortDir = 'asc'; 1641 1642 function debounce(fn, ms) { 1643 var timer; 1644 return function() { 1645 clearTimeout(timer); 1646 timer = setTimeout(fn, ms || 300); 1647 }; 1648 } 1649 1650 function escapeHtml(str) { 1651 return String(str) 1652 .replace(/&/g, '&') 1653 .replace(/</g, '<') 1654 .replace(/>/g, '>') 1655 .replace(/"/g, '"'); 1656 } 1657 1658 function fuzzyMatch(text, term) { 1659 var pattern = term.split('').join('.{0,2}'); 1660 return new RegExp(pattern, 'i').test(text); 1661 } 1662 1663 function matchesSearch(row, query) { 1664 if (!query) return true; 1665 var searchText = (row.tool + ' ' + row.description + ' ' + row.category).toLowerCase();
1666 var terms = query.toLowerCase().split(/\s+/).filter(Boolean); 1667 return terms.every(function(term) { 1668 return fuzzyMatch(searchText, term); 1669 }); 1670 } 1671 1672 function highlightText(text, query) { 1673 if (!query) return escapeHtml(text); 1674 var escaped = escapeHtml(text); 1675 var terms = query.trim().split(/\s+/).filter(Boolean); 1676 terms.forEach(function(term) { 1677 var safe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 1678 escaped = escaped.replace(new RegExp('(' + safe + ')', 'gi'), 1679 '<span class="mcp-highlight">$1</span>'); 1680 }); 1681 return escaped; 1682 } 1683 1684 function syncSearchClear() { 1685 if (!searchClearEl) return; 1686 if ((searchEl.value || '').trim()) { 1687 searchClearEl.classList.add('is-visible'); 1688 } else { 1689 searchClearEl.classList.remove('is-visible'); 1690 } 1691 } 1692 1693 function fetchSheet(cb) { 1694 var cached = sessionStorage.getItem('mcpToolsV3'); 1695 if (cached && !IS_PREVIEW) return cb(JSON.parse(cached)); 1696 1697 var bust = IS_PREVIEW ? '&_=' + Date.now() : ''; 1698 var url = 'https://docs.google.com/spreadsheets/d/' + SHEET_ID + 1699 '/gviz/tq?tqx=out:json&sheet=' + encodeURIComponent(TAB_NAME) + bust; 1700 1701 fetch(url, { cache: 'no-store' }) 1702 .then(function(r) { return r.text(); }) 1703 .then(function(txt) { 1704 var s = txt.indexOf('{'); 1705 var e = txt.lastIndexOf('}') + 1; 1706 var data = JSON.parse(txt.slice(s, e)); 1707 var rows = (data.table && data.table.rows) || []; 1708 var tools = []; 1709 1710 for (var i = 0; i < rows.length; i++) { 1711 var c = rows[i].c; 1712 var tool = String(c && c[0] && c[0].v || '').trim(); 1713 var desc = String(c && c[1] && c[1].v || '').trim(); 1714 var cat = String(c && c[2] && c[2].v || '').trim(); 1715 var typ = String(c && c[3] && c[3].v || '').trim(); 1716 var pub = String(c && c[4] && c[4].v || '').trim().toLowerCase(); 1717 if (tool && pub === 'yes') { 1718 tools.push({ tool: tool, description: desc, category: cat, type: typ }); 1719 } 1720 } 1721 1722 if (!IS_PREVIEW) { 1723 sessionStorage.setItem('mcpToolsV3', JSON.stringify(tools)); 1724 } 1725 cb(tools); 1726 }) 1727 .catch(function(err) { 1728 tbodyEl.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:2rem;color:#dc2626;">' + 1729 'Failed to load tools: ' + err.message + '</td></tr>'; 1730 }); 1731 } 1732 1733 function populateFilters(rows) { 1734 var cats = {}; 1735 var types = {}; 1736 1737 for (var i = 0; i < rows.length; i++) { 1738 cats[rows[i].category] = (cats[rows[i].category] || 0) + 1; 1739 types[rows[i].type] = (types[rows[i].type] || 0) + 1; 1740 } 1741 1742 var catKeys = Object.keys(cats).sort(); 1743 for (var j = 0; j < catKeys.length; j++) { 1744 var opt = document.createElement('option'); 1745 opt.value = catKeys[j]; 1746 opt.textContent = catKeys[j] + ' (' + cats[catKeys[j]] + ')'; 1747 catFilterEl.appendChild(opt); 1748 } 1749 1750 var typeKeys = Object.keys(types).sort(); 1751 for (var k = 0; k < typeKeys.length; k++) { 1752 var opt2 = document.createElement('option'); 1753 opt2.value = typeKeys[k]; 1754 opt2.textContent = typeKeys[k] + ' (' + types[typeKeys[k]] + ')'; 1755 typeFilterEl.appendChild(opt2); 1756 } 1757 1758 if (introCountEl) { 1759 introCountEl.textContent = rows.length + ' tools'; 1760 } 1761 } 1762 1763 function applyFilters() { 1764 var query = (searchEl.value || '').trim(); 1765 var catVal = catFilterEl.value; 1766 var typeVal = typeFilterEl.value; 1767 1768 filtered = allRows.filter(function(r) { 1769 var matchSearch = matchesSearch(r, query); 1770 var matchCat = !catVal || r.category === catVal; 1771 var matchType = !typeVal || r.type === typeVal; 1772 return matchSearch && matchCat && matchType; 1773 }); 1774 1775 filtered.sort(function(a, b) { 1776 var aVal = (a[sortCol] || '').toLowerCase(); 1777 var bVal = (b[sortCol] || '').toLowerCase(); 1778 if (aVal < bVal) return sortDir === 'asc' ? -1 : 1; 1779 if (aVal > bVal) return sortDir === 'asc' ? 1 : -1; 1780 return 0; 1781 }); 1782 1783 page = 1; 1784 render(); 1785 } 1786 1787 function render() { 1788 var query = (searchEl.value || '').trim();
1789 var total = filtered.length; 1790 var totalPages = Math.max(1, Math.ceil(total / pageSize)); 1791 if (page > totalPages) page = totalPages; 1792 1793 var start = (page - 1) * pageSize; 1794 var end = Math.min(start + pageSize, total); 1795 var pageRows = filtered.slice(start, end); 1796 1797 if (countEl) { 1798 if (total === allRows.length) { 1799 countEl.textContent = total + ' tools'; 1800 } else { 1801 countEl.textContent = total + ' of ' + allRows.length + ' tools'; 1802 } 1803 } 1804 1805 if (!pageRows.length) { 1806 tbodyEl.innerHTML = '<tr><td colspan="4">' + 1807 '<div class="mcp-empty"><h3>No tools found</h3>' + 1808 '<p>Try adjusting your search or filters.</p></div></td></tr>'; 1809 } else { 1810 var html = ''; 1811 for (var i = 0; i < pageRows.length; i++) { 1812 var r = pageRows[i]; 1813 var badge = r.type === 'Write' 1814 ? '<span class="mcp-badge mcp-badge-write">Write</span>' 1815 : '<span class="mcp-badge mcp-badge-read">Read</span>'; 1816 1817 html += '<tr>'; 1818 html += '<td><code>' + highlightText(r.tool, query) + '</code></td>'; 1819 html += '<td>' + highlightText(r.description, query) + '</td>'; 1820 html += '<td>' + highlightText(r.category, query) + '</td>'; 1821 html += '<td>' + badge + '</td>'; 1822 html += '</tr>'; 1823 } 1824 tbodyEl.innerHTML = html; 1825 } 1826 1827 theadCols.forEach(function(th) { 1828 var icon = th.querySelector('.sort-icon'); 1829 if (th.dataset.col === sortCol) { 1830 icon.textContent = sortDir === 'asc' ? '\u25B2' : '\u25BC'; 1831 icon.classList.add('active'); 1832 } else { 1833 icon.textContent = '\u25B2\u25BC'; 1834 icon.classList.remove('active'); 1835 } 1836 }); 1837 1838 renderPagination(totalPages, total, start + 1, end); 1839 } 1840 1841 function renderPagination(totalPages, total, from, to) { 1842 if (totalPages <= 1) { 1843 paginationEl.innerHTML = ''; 1844 return; 1845 } 1846 1847 var html = ''; 1848 html += pgBtn('First', 1, page <= 1); 1849 html += pgBtn('Previous', page - 1, page <= 1); 1850 1851 var startPg = Math.max(1, page - 2); 1852 var endPg = Math.min(totalPages, startPg + 4); 1853 if (endPg - startPg < 4) startPg = Math.max(1, endPg - 4); 1854 1855 for (var i = startPg; i <= endPg; i++) { 1856 html += '<button class="mcp-pg-btn' + (i === page ? ' active' : '') + '" data-pg="' + i + '">' + i + '</button>'; 1857 } 1858 1859 html += pgBtn('Next', page + 1, page >= totalPages); 1860 html += pgBtn('Last', totalPages, page >= totalPages); 1861 html += '<span class="mcp-pg-info">' + from + 'â' + to + ' of ' + total + '</span>'; 1862 1863 paginationEl.innerHTML = html; 1864 } 1865 1866 function pgBtn(label, pg, disabled) { 1867 return '<button class="mcp-pg-btn" data-pg="' + pg + '"' + 1868 (disabled ? ' disabled' : '') + '>' + label + '</button>'; 1869 } 1870 1871 searchEl.addEventListener('input', debounce(function() { 1872 syncSearchClear(); 1873 applyFilters(); 1874 }, 300)); 1875 1876 if (searchClearEl) { 1877 searchClearEl.addEventListener('click', function() { 1878 searchEl.value = ''; 1879 syncSearchClear(); 1880 applyFilters(); 1881 searchEl.focus(); 1882 }); 1883 } 1884 1885 catFilterEl.addEventListener('change', applyFilters); 1886 typeFilterEl.addEventListener('change', applyFilters); 1887 1888 pageSizeEl.addEventListener('change', function() { 1889 pageSize = parseInt(pageSizeEl.value, 10) || 25; 1890 if (pageSize > 200) pageSize = allRows.length; 1891 page = 1; 1892 render(); 1893 }); 1894 1895 clearBtn.addEventListener('click', function() { 1896 searchEl.value = ''; 1897 catFilterEl.value = ''; 1898 typeFilterEl.value = ''; 1899 sortCol = 'tool'; 1900 sortDir = 'asc'; 1901 if (pageSizeEl) pageSizeEl.value = '10'; // Change from '25' to '10' 1902 pageSize = 10; // Change from 25 to 10 1903 syncSearchClear(); 1904 applyFilters(); 1905 }); 1906 1907 theadCols.forEach(function(th) { 1908 th.addEventListener('click', function() { 1909 var col = th.dataset.col; 1910 if (sortCol === col) { 1911 sortDir = sortDir === 'asc' ? 'desc' : 'asc'; 1912 } else { 1913 sortCol = col; 1914 sortDir = 'asc'; 1915 } 1916 applyFilters(); 1917 }); 1918 }); 1919 1920 paginationEl.addEventListener('click', function(e) { 1921 var btn = e.target.closest('[data-pg]'); 1922 if (!btn || btn.disabled) return; 1923 page = parseInt(btn.dataset.pg, 10); 1924 render(); 1925 appEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1926 }); 1927 1928 tbodyEl.innerHTML = '<tr><td colspan="4" class="mcp-loading">Loading toolsâ¦</td></tr>'; 1929 1930 fetchSheet(function(rows) { 1931 allRows = rows; 1932 populateFilters(rows); 1933 applyFilters(); 1934 syncSearchClear(); 1935 }); 1936})(); 1937 1938// ====================== // 1939// Added March 18, 2026: Intercepts same-page target="_blank" links to prevent new tabs and scroll smoothly. 1940// ========================= // 1941 1942document.addEventListener('click', function(e) { 1943 // Find the closest anchor tag that was clicked 1944 const link = e.target.closest('a[target="_blank"]'); 1945 if (!link) return; 1946 1947 // SAFETY CHECK: Skip links meant for downloading files. 1948 // Checks for the 'download' attribute OR common file extensions in the URL.
1949 const isDownload = link.hasAttribute('download') || link.pathname.match(/\.(pdf|zip|csv|doc|docx|xls|xlsx)$/i); 1950 if (isDownload) return; // Let the browser handle the download natively 1951 1952 // Get current page URL and the link's URL (ignoring the #hash) 1953 const currentUrl = window.location.origin + window.location.pathname; 1954 const linkUrl = link.origin + link.pathname; 1955 1956 // Check if it is the EXACT same page 1957 if (currentUrl === linkUrl) { 1958 1959 // Stop the new tab from opening immediately 1960 e.preventDefault(); 1961 1962 if (link.hash) { 1963 // SCENARIO 1: There IS an anchor tag (e.g., #h_01MULTIPLE...) 1964 const targetElement = document.querySelector(link.hash); 1965 if (targetElement) { 1966 targetElement.scrollIntoView({ behavior: 'smooth' }); 1967 history.pushState(null, null, link.hash); // Update the URL bar 1968 } 1969 } else { 1970 // SCENARIO 2: There is NO anchor tag. Just a link to the current page. 1971 // Smoothly scroll the user back to the very top of the page 1972 window.scrollTo({ top: 0, behavior: 'smooth' }); 1973 1974 // Clean up the URL bar to remove any old hashes 1975 history.pushState(null, null, currentUrl); 1976 } 1977 } 1978}); 1979 1980// ====================== // 1981// Added March 18, 2026: Simple collapsible section using details tag 1982// ========================= // 1983 1984document.addEventListener("DOMContentLoaded", function () { 1985 const reportFieldDetails = document.querySelectorAll(".report-fields-details"); 1986 1987 reportFieldDetails.forEach((details) => { 1988 const summary = details.querySelector("summary"); 1989 1990 function updateSummaryText() { 1991 summary.innerHTML = details.open 1992 ? "<strong>Hide report fields</strong>" 1993 : "<strong>Click to view report fields</strong>"; 1994 } 1995 1996 updateSummaryText(); 1997 details.addEventListener("toggle", updateSummaryText); 1998 }); 1999}); 2000 2001// Added - 19 March 2026 - Fix: Reposition instant search dropdown below search bar 2002// Fix: Reposition instant search dropdown below search bar (device agnostic) 2003(function() { 2004 var observer = new MutationObserver(function() { 2005 var dropdown = document.querySelector('zd-autocomplete[role="listbox"]'); 2006 if (!dropdown) return; 2007 2008 var input = document.querySelector('.search-box input[type="search"]') 2009 || document.querySelector('.search-box input[type="text"]'); 2010 if (!input) return; 2011 2012 var rect = input.getBoundingClientRect(); 2013 var newTop = rect.bottom + window.scrollY; 2014 var availableHeight = window.innerHeight - rect.bottom - 20; 2015 2016 if (parseFloat(dropdown.style.top) < newTop - 10) { 2017 dropdown.style.setProperty('top', newTop + 'px', 'important'); 2018 dropdown.style.setProperty('left', rect.left + 'px', 'important'); 2019 dropdown.style.setProperty('width', rect.width + 'px', 'important'); 2020 } 2021 2022 dropdown.style.setProperty('max-height', availableHeight + 'px', 'important'); 2023 dropdown.style.setProperty('overflow-y', 'auto', 'important'); 2024 }); 2025 2026 observer.observe(document.body, { 2027 childList: true, 2028 subtree: true, 2029 attributes: true, 2030 attributeFilter: ['style'] 2031 }); 2032})(); 2033 2034// Universal doc navigation behavior â simplified 2026-03-29 2035document.querySelectorAll('.doc-nav').forEach((nav) => { 2036 const prev = nav.querySelector('.doc-nav-link.prev'); 2037 const next = nav.querySelector('.doc-nav-link.next'); 2038 2039 const isEmpty = (link) => { 2040 if (!link) return true; 2041 const href = (link.getAttribute('href') || '').trim(); 2042 const text = (link.textContent || '').trim(); 2043 return !href || !text; 2044 }; 2045 2046 const prevEmpty = isEmpty(prev); 2047 const nextEmpty = isEmpty(next); 2048 2049 if (!prevEmpty && prev) { 2050 prev.setAttribute('aria-label', `Previous: ${prev.textContent.trim()}`); 2051 } 2052 2053 if (!nextEmpty && next) { 2054 next.setAttribute('aria-label', `Next: ${next.textContent.trim()}`); 2055 } 2056 2057 if (prevEmpty && prev) prev.remove(); 2058 if (nextEmpty && next) next.remove(); 2059 2060 if (prevEmpty && !nextEmpty) { 2061 nav.classList.add('is-next-only'); 2062 } else if (!prevEmpty && nextEmpty) { 2063 nav.classList.add('is-prev-only'); 2064 } else if (prevEmpty && nextEmpty) { 2065 nav.style.display = 'none'; 2066 } 2067}); 2068 2069/* ========================================================================== 2070 PROMPT COPY BLOCKS â 31 Mar 2026 2071 Copies .lp-prompt__text content to clipboard on button click. 2072 Uses event delegation â works with any number of prompts. 2073 Icon swap handled by CSS background-image + .is-copied class. 2074 Smooth crossfade: fade out â swap icon â fade in â hold â reverse. 2075 ========================================================================== */ 2076document.addEventListener('click', function (e) { 2077 var btn = e.target.closest('.lp-prompt__copy'); 2078 if (!btn) return; 2079 var box = btn.closest('.lp-prompt__box'); 2080 if (!box) return; 2081 var text = box.querySelector('.lp-prompt__text'); 2082 if (!text) return; 2083 navigator.clipboard.writeText(text.textContent.trim()).then(function () { 2084 btn.style.opacity = '0'; 2085 setTimeout(function () { 2086 btn.classList.add('is-copied'); 2087 btn.setAttribute('aria-label', 'Copied'); 2088 btn.style.opacity = '1'; 2089 }, 200); 2090 setTimeout(function () { 2091 btn.style.opacity = '0'; 2092 setTimeout(function () { 2093 btn.classList.remove('is-copied'); 2094 btn.setAttribute('aria-label', 'Copy prompt'); 2095 btn.style.opacity = ''; 2096 }, 200); 2097 }, 1500); 2098 }); 2099}); 2100 2101/** 2102 * HC â Ask AI / Markdown 2103 * 2104 * ALL article pages: 2105 * Ask AI dropdown with Claude + ChatGPT (no loading wait) 2106 * 2107 * MARKDOWN-ENABLED pages (hardcoded list): 2108 * Same dropdown + Copy Markdown / View as Markdown (waits for full render) 2109 */ 2110 2111(function () { 2112 "use strict"; 2113 2114 // ââ CONFIG âââââââââââââââââââââââââââââââââââââââââââââââââââ 2115 // Pages that also get markdown options 2116 var MARKDOWN_URLS = [ 2117 "/hc/en-us/articles/49713147943187-The-Blueshift-MCP-Server-Beta", 2118 "/hc/en-us/articles/49714422787347-Blueshift-MCP-Server-tools" 2119 ]; 2120 2121 var QUIET_PERIOD = 3000; 2122 2123 var AI_PROMPTS = { 2124 claude: "Hi Claude! Can you please read [this page]({url}) and prepare to answer questions about it?", 2125 chatgpt: "Hi ChatGPT! Can you please read [this page]({url}) and prepare to answer questions about it?" 2126 }; 2127 2128 // ââ PAGE DETECTION âââââââââââââââââââââââââââââââââââââââââââ 2129 var articleBody = document.querySelector( 2130 ".article-body, article.article-body, .article__body" 2131 ); 2132 if (!articleBody) return; // Not an article page 2133 2134 var currentPath = window.location.pathname; 2135 var hasMarkdown = MARKDOWN_URLS.some(function (url) { 2136 return currentPath.indexOf(url) !== -1; 2137 }); 2138 2139 // ââ STATE ââââââââââââââââââââââââââââââââââââââââââââââââââââ 2140 var turndownLoaded = false;
2141 var markdownReady = false; 2142 2143 // ââ LOAD TURNDOWN (only if markdown enabled) ââââââââââââââââââ 2144 function loadTurndown(cb) { 2145 if (window.TurndownService) { turndownLoaded = true; return cb && cb(); } 2146 var s = document.createElement("script"); 2147 s.src = "https://cdnjs.cloudflare.com/ajax/libs/turndown/7.2.0/turndown.min.js"; 2148 s.onload = function () { turndownLoaded = true; if (cb) cb(); }; 2149 document.head.appendChild(s); 2150 } 2151 2152 // ââ EXTRACT + CONVERT ââââââââââââââââââââââââââââââââââââââââ 2153 function getArticleTitle() { 2154 var el = document.querySelector( 2155 ".article-title, .article__title, h1.article-header" 2156 ); 2157 return el ? el.textContent.trim() : document.title.split("â")[0].trim(); 2158 } 2159 2160 function getArticleHTML() { 2161 var clone = articleBody.cloneNode(true); 2162 [ 2163 "script", "style", "noscript", 2164 ".article-votes", ".article-comments", ".article-footer", 2165 ".article-sidebar", ".article-relatives", ".article-attachments", 2166 "[data-purpose='vote']", ".share-links", ".article-subscribe", 2167 "#hc-md-wrap" 2168 ].forEach(function (sel) { 2169 clone.querySelectorAll(sel).forEach(function (el) { el.remove(); }); 2170 }); 2171 return clone.innerHTML; 2172 } 2173 2174 function toMarkdown() { 2175 var html = getArticleHTML(); 2176 if (!html) return null; 2177 2178 var td = new window.TurndownService({ 2179 headingStyle: "atx", 2180 codeBlockStyle: "fenced", 2181 bulletListMarker: "-", 2182 emDelimiter: "*", 2183 strongDelimiter: "**", 2184 linkStyle: "inlined" 2185 }); 2186 2187 td.addRule("callouts", { 2188 filter: function (node) { 2189 return node.nodeName === "DIV" && 2190 /callout|note|tip|warning|info|important/i.test(node.className); 2191 }, 2192 replacement: function (content) { 2193 return "\n> " + content.trim().replace(/\n/g, "\n> ") + "\n"; 2194 } 2195 }); 2196 2197 td.addRule("details", { 2198 filter: ["details"], 2199 replacement: function (content, node) { 2200 var summary = node.querySelector("summary"); 2201 var title = summary ? summary.textContent.trim() : "Details"; 2202 var body = content.replace(title, "").trim(); 2203 return "\n**" + title + "**\n\n" + body + "\n"; 2204 } 2205 }); 2206 2207 var md = td.turndown(html); 2208 md = md.replace(/\n{3,}/g, "\n\n").trim(); 2209 return "# " + getArticleTitle() + "\n\n" + md; 2210 } 2211 2212 function withTurndown(fn) { 2213 if (turndownLoaded) return fn(); 2214 loadTurndown(fn); 2215 } 2216 2217 // ââ TOAST ââââââââââââââââââââââââââââââââââââââââââââââââââââ 2218 function showToast(message) { 2219 var existing = document.getElementById("hc-md-toast"); 2220 if (existing) existing.remove(); 2221 2222 var toast = document.createElement("div"); 2223 toast.id = "hc-md-toast"; 2224 toast.textContent = message; 2225 document.body.appendChild(toast); 2226 2227 setTimeout(function () { toast.classList.add("hc-md-toast-show"); }, 10); 2228 setTimeout(function () { 2229 toast.classList.remove("hc-md-toast-show"); 2230 setTimeout(function () { toast.remove(); }, 300); 2231 }, 2500); 2232 } 2233 2234 // ââ ACTIONS ââââââââââââââââââââââââââââââââââââââââââââââââââ 2235 function openAI(platform, baseUrl, paramName) { 2236 var prompt = AI_PROMPTS[platform].replace("{url}", window.location.href); 2237 var chatUrl = baseUrl + "?" + paramName + "=" + encodeURIComponent(prompt); 2238 var tab = window.open(chatUrl, "_blank"); 2239 if (!tab) showToast("Pop-up blocked â please allow pop-ups for this site"); 2240 } 2241 2242 function doCopy() { 2243 withTurndown(function () { 2244 var md = toMarkdown(); 2245 if (!md) return; 2246 navigator.clipboard.writeText(md).then(function () { 2247 showToast("Markdown copied to clipboard"); 2248 }); 2249 }); 2250 } 2251 2252 function doView() { 2253 withTurndown(function () { 2254 var md = toMarkdown(); 2255 if (!md) return; 2256 2257 var title = getArticleTitle(); 2258 var tokens = Math.ceil(md.length / 4); 2259 2260 var html = [ 2261 "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'>", 2262 "<title>" + title + " â Markdown</title>", 2263 "<style>", 2264 "* { margin:0; padding:0; box-sizing:border-box; }", 2265 "body { font-family:'SF Mono','Fira Code','Consolas',monospace; background:#f8f9fa; color:#2d3748; }", 2266 ".bar { position:sticky; top:0; z-index:10; background:#fff; border-bottom:1px solid #e2e8f0; padding:12px 24px; display:flex; align-items:center; gap:12px; }", 2267 ".bar-title { font-size:13px; font-weight:600;
2267 color:#1a202c; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }", 2268 ".bar-meta { font-size:11px; color:#a0aec0; white-space:nowrap; }", 2269 ".bar-btn { padding:6px 14px; font-size:12px; font-weight:600; font-family:inherit; border-radius:6px; cursor:pointer; border:1px solid #3182ce; background:#3182ce; color:#fff; }", 2270 ".bar-btn:hover { background:#2b6cb0; }", 2271 "pre { padding:24px; font-size:13px; line-height:1.7; white-space:pre-wrap; word-break:break-word; min-height:calc(100vh - 50px); }", 2272 "</style></head><body>", 2273 "<div class='bar'>", 2274 "<div class='bar-title'>" + title + "</div>", 2275 "<div class='bar-meta'>~" + tokens.toLocaleString() + " tokens · " + md.length.toLocaleString() + " chars</div>", 2276 "<button class='bar-btn' onclick='copyMd()'>Copy</button>", 2277 "</div>", 2278 "<pre id='md'></pre>", 2279 "<script>function copyMd(){navigator.clipboard.writeText(document.getElementById(\"md\").textContent).then(function(){var b=document.querySelector(\".bar-btn\");b.textContent=\"\\u2713 Copied\";setTimeout(function(){b.textContent=\"Copy\"},2000)})}<\/script>", 2280 "</body></html>" 2281 ].join("\n"); 2282 2283 var tab = window.open("", "_blank"); 2284 if (!tab) { showToast("Pop-up blocked â please allow pop-ups"); return; } 2285 tab.document.write(html); 2286 tab.document.getElementById("md").textContent = md; 2287 tab.document.close(); 2288 }); 2289 } 2290 2291 // ââ ICONS ââââââââââââââââââââââââââââââââââââââââââââââââââââ 2292 var ICONS = { 2293 sparkle: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.5 2l1.5 4.5L15.5 8l-4.5 1.5L9.5 14l-1.5-4.5L3.5 8l4.5-1.5zm7 8l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/></svg>', 2294 claude: '<svg width="14" height="14" viewBox="0 0 100 100" fill="currentColor"><path d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/></svg>', 2295 chatgpt: '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm
22951.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>', 2296 copy: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>', 2297 md: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M7 15V9l3 3 3-3v6"/><path d="M17 9v6m0-6l-2 3m2-3l2 3"/></svg>', 2298 spinner: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" class="hc-md-spin"><path d="M12 2v4m0 12v4m-10-10h4m12 0h4m-3.5-7.5l-2.8 2.8m-7.4 7.4l-2.8 2.8m0-13l2.8 2.8m7.4 7.4l2.8 2.8"/></svg>', 2299 ext: '<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 1h6v6M11 1L4.5 7.5"/></svg>', 2300 chev: '<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5l3 3 3-3"/></svg>' 2301 }; 2302 2303 // ââ STYLES âââââââââââââââââââââââââââââââââââââââââââââââââââ 2304 function injectStyles() { 2305 var style = document.createElement("style"); 2306 style.textContent = [ 2307 "#hc-md-wrap { position:relative; display:inline-block; margin:0 0 20px 0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }", 2308 2309 "#hc-md-btn { display:inline-flex; align-items:center; gap:6px; padding:8px 14px; font-size:13px; font-weight:600; background:#fff; color:#374151; border:1px solid #d1d5db; border-radius:8px; cursor:pointer; font-family:inherit; transition:all 0.15s; }", 2310 "#hc-md-btn:hover { border-color:#9ca3af; background:#f9fafb; }", 2311 2312 "#hc-md-menu { display:none; position:absolute; top:calc(100% + 4px); left:0; min-width:180px; background:#fff; border:1px solid #e5e7eb; border-radius:10px; box-shadow:0 4px 16px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.06); z-index:9999; padding:4px 0; }", 2313 "#hc-md-menu.open { display:block; }", 2314 2315 ".hc-md-sep { height:1px; background:#f3f4f6; margin:4px 0; }", 2316 2317 ".hc-md-item { display:flex; align-items:center; gap:10px; padding:9px 14px; font-size:13px; font-weight:500; color:#374151; cursor:pointer; border:none; background:none; width:100%; text-align:left; font-family:inherit; transition:background 0.1s; }", 2318 ".hc-md-item:hover { background:#f3f4f6; }", 2319 ".hc-md-item:disabled { opacity:0.35; cursor:not-allowed; }", 2320 ".hc-md-item:disabled:hover { background:none; }", 2321 ".hc-md-item .ic { display:flex; align-items:center; color:#6b7280; }", 2322 ".hc-md-item .lb { flex:1; }", 2323 ".hc-md-item .ex { display:flex; align-items:center; color:#9ca3af; }", 2324 2325 "@keyframes hc-md-spin { to { transform:rotate(360deg); } }", 2326 ".hc-md-spin { animation:hc-md-spin 1s linear infinite; }", 2327 2328 "#hc-md-toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); padding:10px 20px; font-size:13px; font-weight:500; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#1f2937; color:#fff; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.15); opacity:0; transition:opacity 0.3s; z-index:99999; }", 2329 "#hc-md-toast.hc-md-toast-show { opacity:1; }" 2330 ].join("\n"); 2331 document.head.appendChild(style); 2332 } 2333 2334 // ââ BUILD DROPDOWN âââââââââââââââââââââââââââââââââââââââââââ 2335 function injectDropdown() { 2336 var wrap = document.createElement("div"); 2337 wrap.id = "hc-md-wrap"; 2338 2339 // Trigger button â always enabled (Ask AI works immediately) 2340 var btn = document.createElement("button"); 2341 btn.id = "hc-md-btn"; 2342 btn.innerHTML = ICONS.sparkle + " Ask AI " + ICONS.chev; 2343 2344 // Menu 2345 var menu = document.createElement("div"); 2346 menu.id = "hc-md-menu"; 2347 2348 // AI options (always present, always enabled) 2349 var menuHTML = 2350 '<button class="hc-md-item" data-action="claude">' + 2351 '<span class="ic">' + ICONS.claude + '</span>' + 2352 '<span class="lb">Claude</span>' + 2353 '<span class="ex">' + ICONS.ext + '</span>' + 2354 '</button>' + 2355 '<button class="hc-md-item" data-action="chatgpt">' + 2356 '<span class="ic">' + ICONS.chatgpt + '</span>' + 2357 '<span class="lb">ChatGPT</span>' + 2358 '<span class="ex">' + ICONS.ext + '</span>' + 2359 '</button>'; 2360
2361 // Markdown options (only on enabled pages, start disabled with spinner) 2362 if (hasMarkdown) { 2363 menuHTML += 2364 '<div class="hc-md-sep"></div>' + 2365 '<button class="hc-md-item hc-md-needs-load" data-action="copy" disabled>' + 2366 '<span class="ic">' + ICONS.spinner + '</span>' + 2367 '<span class="lb">Copy Markdown</span>' + 2368 '</button>' + 2369 '<button class="hc-md-item hc-md-needs-load" data-action="view" disabled>' + 2370 '<span class="ic">' + ICONS.spinner + '</span>' + 2371 '<span class="lb">View as Markdown</span>' + 2372 '</button>'; 2373 } 2374 2375 menu.innerHTML = menuHTML; 2376 2377 wrap.appendChild(btn); 2378 wrap.appendChild(menu); 2379 2380 articleBody.parentNode.insertBefore(wrap, articleBody); 2381 2382 // Toggle menu 2383 btn.addEventListener("click", function (e) { 2384 e.stopPropagation(); 2385 menu.classList.toggle("open"); 2386 }); 2387 2388 // Close on outside click 2389 document.addEventListener("click", function () { 2390 menu.classList.remove("open"); 2391 }); 2392 2393 // Menu item clicks 2394 menu.addEventListener("click", function (e) { 2395 e.stopPropagation(); 2396 var item = e.target.closest(".hc-md-item"); 2397 if (!item || item.disabled) return; 2398 2399 menu.classList.remove("open"); 2400 2401 switch (item.dataset.action) { 2402 case "claude": 2403 openAI("claude", "https://claude.ai/new", "q"); 2404 break; 2405 case "chatgpt": 2406 openAI("chatgpt", "https://chatgpt.com", "prompt"); 2407 break; 2408 case "copy": 2409 doCopy(); 2410 break; 2411 case "view": 2412 doView(); 2413 break; 2414 } 2415 }); 2416 } 2417 2418 // ââ ENABLE MARKDOWN OPTIONS ââââââââââââââââââââââââââââââââââ 2419 function enableMarkdown() { 2420 markdownReady = true; 2421 2422 var items = document.querySelectorAll(".hc-md-needs-load"); 2423 items.forEach(function (item) { 2424 item.disabled = false; 2425 var ic = item.querySelector(".ic"); 2426 if (item.dataset.action === "copy") ic.innerHTML = ICONS.copy; 2427 if (item.dataset.action === "view") ic.innerHTML = ICONS.md; 2428 }); 2429 } 2430 2431 // ââ WAIT FOR FULL RENDER (markdown pages only) âââââââââââââââ 2432 function waitForReady() { 2433 var timer = null; 2434 var observer = new MutationObserver(function () { 2435 clearTimeout(timer); 2436 timer = setTimeout(function () { 2437 observer.disconnect(); 2438 enableMarkdown(); 2439 }, QUIET_PERIOD); 2440 }); 2441 2442 observer.observe(articleBody, { 2443 childList: true, subtree: true, 2444 characterData: true, attributes: true 2445 }); 2446 2447 timer = setTimeout(function () { 2448 observer.disconnect(); 2449 enableMarkdown(); 2450 }, QUIET_PERIOD); 2451 } 2452 2453 // ââ INIT ââââââââââââââââââââââââââââââââââââââââââââââââââââ 2454 function init() { 2455 injectStyles(); 2456 injectDropdown(); 2457 2458 // Only load Turndown + wait for render on markdown-enabled pages 2459 if (hasMarkdown) { 2460 loadTurndown(null); 2461 2462 if (document.readyState === "complete") { 2463 waitForReady(); 2464 } else { 2465 window.addEventListener("load", waitForReady); 2466 } 2467 } 2468 } 2469 2470 if (document.readyState === "loading") { 2471 document.addEventListener("DOMContentLoaded", init); 2472 } else { 2473 init(); 2474 } 2475})(); 2476 2477/* ========================================================= 2478 Feed Recipe Callout 2479 Renders feed recipes on any page with the feed containers 2480 ========================================================= */ 2481 2482(function feedRecipeCallout() { 2483 const userEl = document.getElementById('feed-user-recipes'); 2484 const productEl = document.getElementById('feed-product-recipes'); 2485 2486 // Modal hooks 2487 const modalOverlay = document.getElementById('details-modal'); 2488 const modalTitle = document.getElementById('modal-title'); 2489 const modalContentArea = document.getElementById('modal-content-area'); 2490 const modalCloseBtn = modalOverlay?.querySelector('.modal-close-btn'); 2491 const modalTagVertical = document.getElementById('modal-tag-vertical'); 2492 const modalTagLifecycle = document.getElementById('modal-tag-lifecycle'); 2493 2494 if (!userEl && !productEl) return; 2495 2496 const SHEET_ID = '15bhdBYVol09wC9air6kU-HfdDbf-_bSNzJWJJJlFfTQ'; 2497 const TAB_NAME = 'All_Recipes'; 2498 const PAGE_SIZE = 10; 2499 2500 let allRows = []; 2501 2502 function parseGviz(txt) { 2503 const start = txt.indexOf('{'); 2504 const end = txt.lastIndexOf('}') + 1; 2505 return JSON.parse(txt.slice(start, end)); 2506 } 2507 2508 function getSafeString(val) { 2509 return String(val || '').trim(); 2510 } 2511 2512 function escapeHtml(str) { 2513 return String(str).replace(/[&<>"']/g, m => ({ 2514 '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' 2515 }[m])); 2516 } 2517 2518 function toSentenceCase(str) { 2519 if (!str) return ''; 2520 return str.charAt(0).toUpperCase() + str.slice(1); 2521 } 2522 2523 function parseList(text) { 2524 if (!text) return null; 2525 const splitPattern = /(?=\d+\.\s+|[-â¢*]\s+)/g; 2526 const items = text.split(splitPattern) 2527 .map(item => item.replace(/^(\d+\.|[-â¢*])\s*/, '').trim()) 2528 .filter(Boolean); 2529 return items.length >= 2 ? items : null; 2530 } 2531 2532 function tryFormatJSON(text) { 2533 try { 2534 const parsed = JSON.parse(text); 2535 return JSON.stringify(parsed, null, 2); 2536 } catch (e) { 2537 return text 2538 .replace(/\s*,\s*/g, ',\n ') 2539 .replace(/\{\s*/g, '{\n ') 2540 .replace(/\s*\}/g, '\n}') 2541 .replace(/:\s*/g, ': '); 2542 } 2543 } 2544 2545 function isPureCode(text) { 2546 if (!text) return false;
2547 const trimmed = text.trim(); 2548 return ( 2549 (trimmed.startsWith('{') && trimmed.endsWith('}')) || 2550 (trimmed.startsWith('[') && trimmed.endsWith(']')) 2551 ); 2552 } 2553 2554 function formatText(text, isDescription = false) { 2555 if (!text) return ''; 2556 2557 const ensurePeriod = (str) => { 2558 const trimmed = str.trim(); 2559 return /[.!?;]$/.test(trimmed) ? trimmed : trimmed + '.'; 2560 }; 2561 2562 if (isDescription) { 2563 const jsonMatch = text.match(/\{[^}]+\}/s); 2564 if (jsonMatch) { 2565 const jsonPart = jsonMatch[0]; 2566 const beforeJSON = text.substring(0, jsonMatch.index).trim(); 2567 const afterJSON = text.substring(jsonMatch.index + jsonPart.length).trim(); 2568 2569 let html = ''; 2570 if (beforeJSON) html += `<p>${escapeHtml(ensurePeriod(beforeJSON))}</p>`; 2571 html += `<pre><code>${escapeHtml(tryFormatJSON(jsonPart))}</code></pre>`; 2572 if (afterJSON) html += `<p>${escapeHtml(ensurePeriod(afterJSON))}</p>`; 2573 return html; 2574 } 2575 return `<p>${escapeHtml(ensurePeriod(text))}</p>`; 2576 } 2577 2578 const listItems = parseList(text); 2579 if (listItems) { 2580 return `<ol>${listItems.map(item => `<li>${escapeHtml(ensurePeriod(item))}</li>`).join('')}</ol>`; 2581 } 2582 2583 const singleItemPattern = /^(\d+\.|[-â¢*])\s+(.+)$/s; 2584 const singleMatch = text.match(singleItemPattern); 2585 if (singleMatch) { 2586 return `<p>${escapeHtml(ensurePeriod(singleMatch[2]))}</p>`; 2587 } 2588 2589 if (isPureCode(text)) { 2590 return `<pre><code>${escapeHtml(tryFormatJSON(text))}</code></pre>`; 2591 } 2592 2593 return `<p>${escapeHtml(ensurePeriod(text))}</p>`; 2594 } 2595 2596 function normalizeRow(row) { 2597 return { 2598 id: getSafeString(row.c?.[0]?.v), 2599 title: getSafeString(row.c?.[1]?.v), 2600 shortDesc: getSafeString(row.c?.[2]?.v), 2601 fullDesc: getSafeString(row.c?.[3]?.v), 2602 requirements: getSafeString(row.c?.[4]?.v), 2603 inputs: getSafeString(row.c?.[5]?.v), 2604 useCases: getSafeString(row.c?.[6]?.v), 2605 vertical: getSafeString(row.c?.[7]?.v), 2606 lifeCycle: getSafeString(row.c?.[8]?.v), 2607 priority: Number(row.c?.[9]?.v) || 5, 2608 publish: getSafeString(row.c?.[10]?.v).toLowerCase(), 2609 slug: getSafeString(row.c?.[14]?.v), 2610 feedType: getSafeString(row.c?.[15]?.v).toLowerCase() 2611 }; 2612 } 2613 2614 function showModal(recipe) { 2615 if (!modalOverlay || !modalTitle || !modalContentArea) return; 2616 2617 const sections = [ 2618 { title: 'Description', content: recipe.fullDesc, isDescription: true }, 2619 { title: 'Requirements', content: recipe.requirements }, 2620 { title: 'Input / Configuration', content: recipe.inputs }, 2621 { title: 'Suggested Use Cases', content: recipe.useCases } 2622 ]; 2623 2624 const sectionsHtml = sections 2625 .filter(section => section.content && formatText(section.content, section.isDescription)) 2626 .map(section => ` 2627 <div class="modal-section"> 2628 <h3>${section.title}</h3> 2629 ${formatText(section.content, section.isDescription)} 2630 </div> 2631 `) 2632 .join(''); 2633 2634 modalTitle.textContent = toSentenceCase(recipe.title); 2635 modalContentArea.innerHTML = sectionsHtml || '<p>Recipe details are unavailable.</p>'; 2636 2637 if (modalTagLifecycle) { 2638 modalTagLifecycle.textContent = toSentenceCase(recipe.lifeCycle || ''); 2639 modalTagLifecycle.style.display = recipe.lifeCycle ? 'inline-block' : 'none'; 2640 } 2641 2642 if (modalTagVertical) { 2643 modalTagVertical.textContent = toSentenceCase(recipe.vertical || ''); 2644 modalTagVertical.style.display = recipe.vertical ? 'inline-block' : 'none'; 2645 } 2646 2647 modalOverlay.classList.remove('hidden'); 2648 document.body.style.overflow = 'hidden'; 2649 } 2650 2651 function hideModal() { 2652 if (!modalOverlay) return; 2653 modalOverlay.classList.add('hidden'); 2654 document.body.style.overflow = ''; 2655 } 2656 2657 if (modalCloseBtn) { 2658 modalCloseBtn.addEventListener('click', hideModal); 2659 } 2660 2661 if (modalOverlay) { 2662 modalOverlay.addEventListener('click', (e) => { 2663 if (e.target === modalOverlay) hideModal(); 2664 }); 2665 } 2666 2667 function renderFeedRecipeList(list, container) { 2668 if (!container) return; 2669 2670 if (!list.length) { 2671 container.innerHTML = '<span>No recipes available.</span>'; 2672 return; 2673 } 2674 2675 let currentPage = 1; 2676 const totalPages = Math.max(1, Math.ceil(list.length / PAGE_SIZE)); 2677 2678 function renderPage() { 2679 const start = (currentPage - 1) * PAGE_SIZE; 2680 const pageItems = list.slice(start, start + PAGE_SIZE); 2681 2682 const itemsHtml = `
2683 <ul class="feed-recipe-items"> 2684 ${pageItems.map(recipe => ` 2685 <li> 2686 <button 2687 type="button" 2688 class="feed-recipe-link" 2689 data-recipe-id="${escapeHtml(recipe.id)}" 2690 title="${escapeHtml(recipe.title)}"> 2691 ${escapeHtml(recipe.title)} 2692 </button> 2693 </li> 2694 `).join('')} 2695 </ul> 2696`; 2697 2698 const pagerHtml = totalPages > 1 ? ` 2699 <div class="feed-pager"> 2700 <button type="button" class="feed-pager-btn" data-feed-prev ${currentPage === 1 ? 'disabled' : ''}>Prev</button> 2701 <span class="feed-pager-status">${currentPage} / ${totalPages}</span> 2702 <button type="button" class="feed-pager-btn" data-feed-next ${currentPage === totalPages ? 'disabled' : ''}>Next</button> 2703 </div> 2704 ` : ''; 2705 2706 container.innerHTML = itemsHtml + pagerHtml; 2707 2708 container.querySelectorAll('.feed-recipe-link').forEach(btn => { 2709 btn.addEventListener('click', () => { 2710 const recipe = allRows.find(r => r.id === btn.dataset.recipeId); 2711 if (recipe) showModal(recipe); 2712 }); 2713 }); 2714 2715 const prevBtn = container.querySelector('[data-feed-prev]'); 2716 const nextBtn = container.querySelector('[data-feed-next]'); 2717 2718 if (prevBtn) { 2719 prevBtn.addEventListener('click', () => { 2720 if (currentPage > 1) { 2721 currentPage -= 1; 2722 renderPage(); 2723 } 2724 }); 2725 } 2726 2727 if (nextBtn) { 2728 nextBtn.addEventListener('click', () => { 2729 if (currentPage < totalPages) { 2730 currentPage += 1; 2731 renderPage(); 2732 } 2733 }); 2734 } 2735 } 2736 2737 renderPage(); 2738} 2739 2740 function renderFeedRecipeGroups() { 2741 const userProductRecipes = allRows 2742 .filter(r => r.feedType === 'user-product') 2743 .sort((a, b) => a.title.localeCompare(b.title)); 2744 2745 const productProductRecipes = allRows 2746 .filter(r => r.feedType === 'product-product') 2747 .sort((a, b) => a.title.localeCompare(b.title)); 2748 2749 renderFeedRecipeList(userProductRecipes, userEl); 2750 renderFeedRecipeList(productProductRecipes, productEl); 2751 } 2752 2753 fetch(`https://docs.google.com/spreadsheets/d/${encodeURIComponent(SHEET_ID)}/gviz/tq?tqx=out:json&sheet=${encodeURIComponent(TAB_NAME)}`, { cache: 'no-store' }) 2754 .then(res => res.text()) 2755 .then(txt => { 2756 const data = parseGviz(txt); 2757 allRows = (data.table?.rows || []) 2758 .map(normalizeRow) 2759 .filter(r => r.title && r.publish === 'yes'); 2760 2761 renderFeedRecipeGroups(); 2762 }) 2763 .catch(() => { 2764 if (userEl) userEl.innerHTML = 'Unable to load'; 2765 if (productEl) productEl.innerHTML = 'Unable to load'; 2766 }); 2767})(); 2768 2769 2770// --------------------- Recommendations Studio Sections Reorder --------------- 2771 2772(function () { 2773 var SECTION_ID = '360000298254'; 2774 2775 console.log("Reorder Script: Loaded and checking for menu..."); 2776 2777 function attemptReorder() { 2778 // Look for the specific menu anywhere on the current page 2779 var parentUl = document.querySelector('ul[data-sidenav-parent-section="' + SECTION_ID + '"]'); 2780 2781 if (!parentUl) { 2782 return false; // Menu isn't on the page yet 2783 } 2784 2785 // Prevent infinite loops if the menu is already reordered 2786 if (parentUl.getAttribute('data-reordered') === 'true') { 2787 return true; 2788 } 2789 2790 console.log("Reorder Script: Menu found! Starting reorder."); 2791 2792 var DESIRED_ORDER = [ 2793 { type: 'article', id: '45868378331667' }, // The Blueshift Recommendations Studio 2794 { type: 'article', id: '45874935379475' }, // Explore Blueshift recommendation recipes 2795 { type: 'article', id: '9063525871891' }, // How Recommendations Studio works 2796 { type: 'article', id: '115002726694' }, // Recommendations Studio interface 2797 { type: 'section', id: '50603236413587' }, // --> Build recommendation schemes (MOVED HERE) 2798 { type: 'article', id: '9063463203219' }, // Add recommendations to messages 2799 { type: 'article', id: '4815071670291' } // Measure performance 2800 ]; 2801 2802 var itemMap = {}; 2803 var listItems = Array.from(parentUl.children); 2804 2805 listItems.forEach(function(li) { 2806 if (li.hasAttribute('data-sidenav-article-id')) { 2807 itemMap['article_' + li.getAttribute('data-sidenav-article-id')] = li; 2808 } else if (li.hasAttribute('data-sidenav-section-id')) { 2809 itemMap['section_' + li.getAttribute('data-sidenav-section-id')] = li; 2810 } 2811 }); 2812 2813 // Re-append in the exact order requested
2814 DESIRED_ORDER.forEach(function(item) { 2815 var key = item.type + '_' + item.id; 2816 if (itemMap[key]) { 2817 parentUl.appendChild(itemMap[key]); 2818 delete itemMap[key]; // Mark as moved 2819 } 2820 }); 2821 2822 // Append any leftovers (like 'Import recommendations') to the bottom 2823 Object.keys(itemMap).forEach(function(key) { 2824 parentUl.appendChild(itemMap[key]); 2825 }); 2826 2827 // Mark as finished so we don't keep reordering it 2828 parentUl.setAttribute('data-reordered', 'true'); 2829 console.log("Reorder Script: Success!"); 2830 return true; 2831 } 2832 2833 // Check every 500ms for the menu. Stop checking after 10 seconds. 2834 var attempts = 0; 2835 var checkInterval = setInterval(function() { 2836 attempts++; 2837 if (attemptReorder() || attempts > 20) { 2838 clearInterval(checkInterval); 2839 if (attempts > 20) console.log("Reorder Script: Gave up waiting for menu. Menu not found on this page."); 2840 } 2841 }, 500); 2842})(); 2843 2844/* ââ Back to top (global) -- updated 15-April-2026 ââ */ 2845(function() { 2846 var btn = document.getElementById('bsftTopBtn'); 2847 if (!btn) return; 2848 2849 if (document.getElementById('bsGlTop')) { 2850 btn.style.display = 'none'; 2851 return; 2852 } 2853 2854 function getAnchor() { 2855 return document.querySelector('.article__body') || 2856 document.querySelector('[role="main"]'); 2857 } 2858 2859 function positionFixed() { 2860 var anchor = getAnchor(); 2861 if (!anchor) return; 2862 var rect = anchor.getBoundingClientRect(); 2863 btn.style.position = 'fixed'; 2864 btn.style.bottom = '24px'; 2865 btn.style.top = 'auto'; 2866 btn.style.left = (rect.right - btn.offsetWidth - 16) + 'px'; 2867 btn.style.right = 'auto'; 2868 } 2869 2870 function positionAbsolute() { 2871 var anchor = getAnchor(); 2872 if (!anchor) return; 2873 anchor.style.position = 'relative'; 2874 btn.style.position = 'absolute'; 2875 btn.style.bottom = '0'; 2876 btn.style.top = 'auto'; 2877 btn.style.left = 'auto'; 2878 btn.style.right = '8px'; 2879 anchor.appendChild(btn); 2880 } 2881 2882 function update() { 2883 var anchor = getAnchor(); 2884 if (!anchor) return; 2885 var st = window.scrollY; 2886 2887 if (st > 400) { 2888 btn.classList.add('is-visible'); 2889 var anchorBottom = anchor.getBoundingClientRect().bottom + st; 2890 var fixedBtnBottom = st + window.innerHeight - 24; 2891 if (fixedBtnBottom > anchorBottom - 14) { 2892 positionAbsolute(); 2893 } else { 2894 positionFixed(); 2895 } 2896 } else { 2897 btn.classList.remove('is-visible'); 2898 } 2899 } 2900 2901 window.addEventListener('scroll', update); 2902 window.addEventListener('resize', update); 2903 2904 btn.addEventListener('click', function() { 2905 window.scrollTo({ top: 0, behavior: 'smooth' }); 2906 }); 2907 2908 update(); 2909})(); 2910 2911/* --------------------- Launchpad x Compass - Move Release notes to top --------------- 2912Updated - 30-June-2026 ------------------- */ 2913 2914(function () { 2915 var SECTION_ID = '50363396358419'; // Launchpad x Compass (Beta) 2916 var RELEASE_NOTES_SECTION_ID = '53035039589267'; // Release notes 2917 2918 function attemptReorderLaunchpad() { 2919 var parentUl = document.querySelector( 2920 'ul[data-sidenav-parent-section="' + SECTION_ID + '"]' 2921 ); 2922 2923 if (!parentUl) { 2924 return false; 2925 } 2926 2927 if (parentUl.getAttribute('data-launchpad-reordered') === 'true') { 2928 return true; 2929 } 2930 2931 var releaseNotesItem = parentUl.querySelector( 2932 'li[data-sidenav-section-id="' + RELEASE_NOTES_SECTION_ID + '"]' 2933 ); 2934 2935 if (!releaseNotesItem) { 2936 return false; 2937 } 2938 2939 // Move Release notes before the direct articles 2940 parentUl.insertBefore(releaseNotesItem, parentUl.firstElementChild); 2941 2942 parentUl.setAttribute('data-launchpad-reordered', 'true'); 2943 return true; 2944 } 2945 2946 var attempts = 0; 2947 var checkInterval = setInterval(function () { 2948 attempts++; 2949 2950 if (attemptReorderLaunchpad() || attempts > 20) { 2951 clearInterval(checkInterval); 2952 } 2953 }, 500); 2954})(); 2955 2956/* ---------- Launchpad Release Notes: sort section + prev/next nav ---------- 2957Updated - 30-June-2026 ------------------- */ 2958 2959/* Clubbed together with latest release note load below ------ 2960 2961(function () { 2962 var RELEASE_NOTES_SECTION_ID = "53035039589267"; 2963 2964 function getLocale() { 2965 var m = window.location.pathname.match(/\/hc\/([^/]+)/); 2966 return m ? m[1] : "en-us"; 2967 } 2968 function getCurrentArticleId() { 2969 var m = window.location.pathname.match(/\/articles\/(\d+)/); 2970 return m ? m[1] : null; 2971 } 2972 function isReleaseSectionPage() { 2973 return window.location.pathname.indexOf("/sections/" + RELEASE_NOTES_SECTION_ID) !== -1; 2974 } 2975 2976 function getDateFromTitle(title) { 2977 var months = { january:0, february:1, march:2, april:3, may:4, june:5, 2978 july:6, august:7, september:8, october:9, november:10, december:11 }; 2979 var m = title.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s*(\d{4})\b/i); 2980 if (!m) return null; 2981 return new Date(Number(m[3]), months[m[1].toLowerCase()], Number(m[2])); 2982 } 2983 2984 function fetchAll(url, acc) { 2985 acc = acc || []; 2986 return fetch(url) 2987 .then(function (r) { return r.json(); }) 2988 .then(function (data) { 2989 acc = acc.concat(data.articles || []); 2990 return data.next_page ? fetchAll(data.next_page, acc) : acc; 2991 }); 2992 } 2993 2994 function getSorted() { 2995 var url = "/api/v2/help_center/" + getLocale() + 2996 "/sections/" + RELEASE_NOTES_SECTION_ID + "/articles.json?per_page=100"; 2997 return fetchAll(url).then(function (articles) { 2998 return articles 2999 .filter(function (a) { return getDateFromTitle(a.title); }) 3000 .sort(function (a, b) { return getDateFromTitle(b.title) - getDateFromTitle(a.title); }); 3001 }); 3002 } 3003 3004 function sortSectionPage(articles) { 3005 if (!isReleaseSectionPage()) return; 3006 var list = document.getElementById("article-list") || 3007 document.querySelector(".article-list"); 3008 if (!list) return; 3009 list.innerHTML = "";
3010 articles.forEach(function (a) { 3011 var li = document.createElement("li"); 3012 li.className = "article-list__item"; 3013 var link = document.createElement("a"); 3014 link.className = "article-list__item-link link-black"; 3015 link.href = a.html_url; 3016 link.textContent = a.title; 3017 li.appendChild(link); 3018 list.appendChild(li); 3019 }); 3020 } 3021 3022 function addBottomNav(articles) { 3023 var id = getCurrentArticleId(); 3024 if (!id) return; 3025 var idx = articles.findIndex(function (a) { return String(a.id) === String(id); }); 3026 if (idx === -1) return; // not a release note 3027 if (articles.length <= 1) return; // only one: no nav 3028 3029 var older = articles[idx + 1]; // previous = older 3030 var newer = articles[idx - 1]; // next = newer 3031 3032 var nav = document.createElement("nav"); 3033 nav.className = "release-notes-bottom-nav"; 3034 3035 var left = document.createElement("div"); 3036 left.className = "release-notes-bottom-nav__left"; 3037 var right = document.createElement("div"); 3038 right.className = "release-notes-bottom-nav__right"; 3039 3040 if (older) { 3041 var prev = document.createElement("a"); 3042 prev.href = older.html_url; 3043 prev.innerHTML = "â Previous release note<br><span></span>"; 3044 prev.querySelector("span").textContent = older.title; 3045 left.appendChild(prev); 3046 } 3047 if (newer) { 3048 var next = document.createElement("a"); 3049 next.href = newer.html_url; 3050 next.innerHTML = "Next release note â<br><span></span>"; 3051 next.querySelector("span").textContent = newer.title; 3052 right.appendChild(next); 3053 } 3054 3055 nav.appendChild(left); 3056 nav.appendChild(right); 3057 3058 var body = document.querySelector(".article-body") || 3059 document.querySelector(".article__body") || 3060 document.querySelector(".article-content") || 3061 document.querySelector(".article"); 3062 if (body) body.appendChild(nav); 3063 } 3064 3065 document.addEventListener("DOMContentLoaded", function () { 3066 getSorted() 3067 .then(function (articles) { 3068 sortSectionPage(articles); 3069 addBottomNav(articles); 3070 }) 3071 .catch(function (e) { console.log("Release notes nav error:", e); }); 3072 }); 3073})(); 3074 3075*/ 3076 3077// --------------------- Launchpad Release Notes: Sort articles by title date --------------- updated 30-June-2026 3078 3079(function () { 3080 var RELEASE_NOTES_SECTION_ID = "53035039589267"; 3081 3082 function getDateFromTitle(title) { 3083 var months = { 3084 january: 0, 3085 february: 1, 3086 march: 2, 3087 april: 3, 3088 may: 4, 3089 june: 5, 3090 july: 6, 3091 august: 7, 3092 september: 8, 3093 october: 9, 3094 november: 10, 3095 december: 11 3096 }; 3097 3098 var match = title.match( 3099 /\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s*(\d{4})\b/i 3100 ); 3101 3102 if (!match) return 0; 3103 3104 return new Date( 3105 Number(match[3]), 3106 months[match[1].toLowerCase()], 3107 Number(match[2]) 3108 ).getTime(); 3109 } 3110 3111 function sortListItems(parent) { 3112 if (!parent) return false; 3113 3114 var items = Array.from(parent.children); 3115 3116 items.sort(function (a, b) { 3117 return getDateFromTitle(b.textContent.trim()) - getDateFromTitle(a.textContent.trim()); 3118 }); 3119 3120 items.forEach(function (item) { 3121 parent.appendChild(item); 3122 }); 3123 3124 return true; 3125 } 3126 3127 function sortReleaseNotesSectionPage() { 3128 if (!window.location.pathname.includes("/sections/" + RELEASE_NOTES_SECTION_ID)) { 3129 return; 3130 } 3131 3132 var articleList = document.getElementById("article-list"); 3133 sortListItems(articleList); 3134 } 3135 3136 function sortReleaseNotesSidebar() { 3137 var sidebarList = document.querySelector( 3138 'ul[data-sidenav-parent-section="' + RELEASE_NOTES_SECTION_ID + '"]' 3139 ); 3140 3141 if (!sidebarList) return false; 3142 3143 if (sidebarList.getAttribute("data-release-notes-sorted") === "true") { 3144 return true; 3145 } 3146 3147 sortListItems(sidebarList); 3148 sidebarList.setAttribute("data-release-notes-sorted", "true"); 3149 3150 return true; 3151 } 3152 3153 document.addEventListener("DOMContentLoaded", function () { 3154 // Sort main Release notes section page 3155 sortReleaseNotesSectionPage(); 3156 3157 // Sort sidebar after sidenav loads 3158 var attempts = 0; 3159 var interval = setInterval(function () { 3160 attempts++; 3161 3162 if (sortReleaseNotesSidebar() || attempts > 20) { 3163 clearInterval(interval); 3164 } 3165 }, 500); 3166 }); 3167})(); 3168 3169 3170
3171/* Launchpad Release Notes: sort + prev/next nav + #latest redirect | 01-July-2026 */ 3172(function () { 3173 var RELEASE_NOTES_SECTION_ID = "53035039589267"; 3174 3175 var WANTS_LATEST = window.location.hash === "#latest" && 3176 window.location.pathname.indexOf("/sections/" + RELEASE_NOTES_SECTION_ID) !== -1; 3177 3178 /* instantly hide everything + paint a spinner overlay ON <html> (no TOC flash) */ 3179 if (WANTS_LATEST) { 3180 var st = document.createElement("style"); 3181 st.id = "rn-latest-hide"; 3182 st.textContent = 3183 "body{display:none!important;}" + /* kill the section page immediately */ 3184 "html{background:#fff;}" + 3185 "#rn-latest-loader{position:fixed;inset:0;z-index:2147483647;background:#fff;" + 3186 "display:flex;flex-direction:column;align-items:center;justify-content:center;" + 3187 "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}" + 3188 "#rn-latest-loader .rn-spin{width:34px;height:34px;border-radius:50%;" + 3189 "border:3px solid #e3e7ee;border-top-color:#2790ff;animation:rnspin .7s linear infinite;}" + 3190 "#rn-latest-loader .rn-msg{margin-top:16px;font-size:14px;color:#4c6072;}" + 3191 "@keyframes rnspin{to{transform:rotate(360deg);}}"; 3192 (document.head || document.documentElement).appendChild(st); 3193 3194 // mount spinner directly on <html> so it shows even before <body> exists 3195 var loader = document.createElement("div"); 3196 loader.id = "rn-latest-loader"; 3197 loader.innerHTML = '<div class="rn-spin"></div>' + 3198 '<div class="rn-msg">Taking you to the latest release\u2026</div>'; 3199 document.documentElement.appendChild(loader); 3200 } 3201 3202 function getLocale() { 3203 var m = window.location.pathname.match(/\/hc\/([^/]+)/); 3204 return m ? m[1] : "en-us"; 3205 } 3206 function getCurrentArticleId() { 3207 var m = window.location.pathname.match(/\/articles\/(\d+)/); 3208 return m ? m[1] : null; 3209 } 3210 function isReleaseSectionPage() { 3211 return window.location.pathname.indexOf("/sections/" + RELEASE_NOTES_SECTION_ID) !== -1; 3212 } 3213 3214 function getDateFromTitle(title) { 3215 var months = { january:0, february:1, march:2, april:3, may:4, june:5, 3216 july:6, august:7, september:8, october:9, november:10, december:11 }; 3217 var m = title.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s*(\d{4})\b/i); 3218 if (!m) return null; 3219 return new Date(Number(m[3]), months[m[1].toLowerCase()], Number(m[2])); 3220 } 3221 3222 function fetchAll(url, acc) { 3223 acc = acc || []; 3224 return fetch(url) 3225 .then(function (r) { return r.json(); }) 3226 .then(function (data) { 3227 acc = acc.concat(data.articles || []); 3228 return data.next_page ? fetchAll(data.next_page, acc) : acc; 3229 }); 3230 } 3231 3232 function getSorted() { 3233 var url = "/api/v2/help_center/" + getLocale() + 3234 "/sections/" + RELEASE_NOTES_SECTION_ID + "/articles.json?per_page=100"; 3235 return fetchAll(url).then(function (articles) { 3236 return articles 3237 .filter(function (a) { return getDateFromTitle(a.title); }) 3238 .sort(function (a, b) { return getDateFromTitle(b.title) - getDateFromTitle(a.title); }); 3239 }); 3240 } 3241 3242 /* fast path: single page, sorted by the API, newest first by title date */ 3243 function getLatestFast() { 3244 var url = "/api/v2/help_center/" + getLocale() + 3245 "/sections/" + RELEASE_NOTES_SECTION_ID + 3246 "/articles.json?per_page=30&sort_by=created_at&sort_order=desc"; 3247 return fetch(url) 3248 .then(function (r) { return r.json(); }) 3249 .then(function (data) { 3250 var arts = (data.articles || []).filter(function (a) { return getDateFromTitle(a.title); }); 3251 arts.sort(function (a, b) { return getDateFromTitle(b.title) - getDateFromTitle(a.title); }); 3252 return arts.length ? arts[0] : null; 3253 }); 3254 } 3255 3256 var LATEST_CACHE_KEY = "rn_latest_url_" + RELEASE_NOTES_SECTION_ID; 3257 3258 function revealPage() { 3259 var h = document.getElementById("rn-latest-hide"); 3260 if (h) h.remove(); 3261 var o = document.getElementById("rn-latest-loader"); 3262 if (o) o.remove(); 3263 } 3264 3265 /* #latest redirect, cache-first for an instant repeat experience */ 3266 function redirectToLatestIfRequested() { 3267 if (!WANTS_LATEST) return false;
3268 3269 // 1) instant path: use cached URL if we have one 3270 var cached = null; 3271 try { cached = window.localStorage.getItem(LATEST_CACHE_KEY); } catch (e) {} 3272 3273 if (cached) { 3274 window.location.replace(cached); // instant, no API wait 3275 getLatestFast().then(function (a) { // refresh cache in background 3276 if (a) { try { window.localStorage.setItem(LATEST_CACHE_KEY, a.html_url); } catch (e) {} } 3277 }).catch(function () {}); 3278 return true; 3279 } 3280 3281 // first visit: fast single-page fetch, redirect, cache 3282 getLatestFast().then(function (a) { 3283 if (a) { 3284 try { window.localStorage.setItem(LATEST_CACHE_KEY, a.html_url); } catch (e) {} 3285 window.location.replace(a.html_url); 3286 } else { 3287 revealPage(); 3288 } 3289 }).catch(function () { 3290 revealPage(); 3291 }); 3292 return true; 3293 } 3294 3295 function sortSectionPage(articles) { 3296 if (!isReleaseSectionPage()) return; 3297 var list = document.getElementById("article-list") || 3298 document.querySelector(".article-list"); 3299 if (!list) return; 3300 list.innerHTML = ""; 3301 articles.forEach(function (a) { 3302 var li = document.createElement("li"); 3303 li.className = "article-list__item"; 3304 var link = document.createElement("a"); 3305 link.className = "article-list__item-link link-black"; 3306 link.href = a.html_url; 3307 link.textContent = a.title; 3308 li.appendChild(link); 3309 list.appendChild(li); 3310 }); 3311 } 3312 3313 function addBottomNav(articles) { 3314 var id = getCurrentArticleId(); 3315 if (!id) return; 3316 var idx = articles.findIndex(function (a) { return String(a.id) === String(id); }); 3317 if (idx === -1) return; 3318 if (articles.length <= 1) return; 3319 3320 var older = articles[idx + 1]; 3321 var newer = articles[idx - 1]; 3322 3323 var nav = document.createElement("nav"); 3324 nav.className = "release-notes-bottom-nav"; 3325 var left = document.createElement("div"); 3326 left.className = "release-notes-bottom-nav__left"; 3327 var right = document.createElement("div"); 3328 right.className = "release-notes-bottom-nav__right"; 3329 3330 if (older) { 3331 var prev = document.createElement("a"); 3332 prev.href = older.html_url; 3333 prev.innerHTML = "â Previous release note<br><span></span>"; 3334 prev.querySelector("span").textContent = older.title; 3335 left.appendChild(prev); 3336 } 3337 if (newer) { 3338 var next = document.createElement("a"); 3339 next.href = newer.html_url; 3340 next.innerHTML = "Next release note â<br><span></span>"; 3341 next.querySelector("span").textContent = newer.title; 3342 right.appendChild(next); 3343 } 3344 3345 nav.appendChild(left); 3346 nav.appendChild(right); 3347 3348 var body = document.querySelector(".article-body") || 3349 document.querySelector(".article__body") || 3350 document.querySelector(".article-content") || 3351 document.querySelector(".article"); 3352 if (body) body.appendChild(nav); 3353 } 3354 3355 /* start the #latest redirect as early as possible, not waiting for DOMContentLoaded */ 3356 if (WANTS_LATEST) { 3357 redirectToLatestIfRequested(); 3358 } 3359 3360 document.addEventListener("DOMContentLoaded", function () { 3361 if (WANTS_LATEST) return; // redirect already in flight 3362 3363 getSorted() 3364 .then(function (articles) { 3365 sortSectionPage(articles); 3366 addBottomNav(articles); 3367 }) 3368 .catch(function (e) { console.log("Release notes nav error:", e); }); 3369 }); 3370})(); 3371 3372 3373// --------------------- Advertising: LiveRamp first, rest alphabetical --------------- updated 06-July-2026 3374(function () { 3375 var ADVERTISING_SECTION_ID = "360011629493"; 3376 var PINNED_FIRST_TITLE = "liveramp"; 3377 3378 function sortKey(title) { 3379 var t = title.trim().toLowerCase(); 3380 // Pinned title always sorts first, everything else alphabetical after it 3381 if (t === PINNED_FIRST_TITLE) return ""; 3382 return t; 3383 } 3384 3385 function sortListItems(parent) { 3386 if (!parent) return false; 3387 var items = Array.from(parent.children); 3388 items.sort(function (a, b) { 3389 var ka = sortKey(a.textContent.trim()); 3390 var kb = sortKey(b.textContent.trim()); 3391 if (ka < kb) return -1; 3392 if (ka > kb) return 1; 3393 return 0; 3394 });
3395 items.forEach(function (item) { 3396 parent.appendChild(item); 3397 }); 3398 return true; 3399 } 3400 3401 function sortAdvertisingSectionPage() { 3402 if (!window.location.pathname.includes("/sections/" + ADVERTISING_SECTION_ID)) { 3403 return; 3404 } 3405 var articleList = document.getElementById("article-list"); 3406 sortListItems(articleList); 3407 } 3408 3409 function sortAdvertisingSidebar() { 3410 var sidebarList = document.querySelector( 3411 'ul[data-sidenav-parent-section="' + ADVERTISING_SECTION_ID + '"]' 3412 ); 3413 if (!sidebarList) return false; 3414 if (sidebarList.getAttribute("data-advertising-sorted") === "true") { 3415 return true; 3416 } 3417 sortListItems(sidebarList); 3418 sidebarList.setAttribute("data-advertising-sorted", "true"); 3419 return true; 3420 } 3421 3422 document.addEventListener("DOMContentLoaded", function () { 3423 // Sort main Advertising section page 3424 sortAdvertisingSectionPage(); 3425 // Sort sidebar after sidenav loads 3426 var attempts = 0; 3427 var interval = setInterval(function () { 3428 attempts++; 3429 if (sortAdvertisingSidebar() || attempts > 20) { 3430 clearInterval(interval); 3431 } 3432 }, 500); 3433 }); 3434})();
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.