1function toggleDeveloperMenu() { 2 const menu = document.getElementById('dev-doc-menu'); 3 document.body.classList.toggle('menu-developer-documentation-is-collapsed'); 4 menu.classList.toggle('collapsed'); 5} 6 7function toggleDeveloperMenuMobile() { 8 document.body.classList.toggle('developer-documentation-menu-opened'); 9} 10 11function expandDeveloperMenu(event) { 12 event.preventDefault(); 13 14 const link = event.target.closest('a'); 15 const section = event.target.closest('.menu-developer-documentation-section'); 16 const list = section.querySelector('.menu-developer-documentation-subitems'); 17 18 const menu = document.getElementById('dev-doc-menu'); 19 if (menu.classList.contains('collapsed')) { 20 toggleDeveloperMenu(); 21 22 link.classList.add('expanded'); 23 section.classList.add('expanded'); 24 list.classList.add('expanded'); 25 26 return; 27 } 28 29 link.classList.toggle('expanded'); 30 section.classList.toggle('expanded'); 31 list.classList.toggle('expanded'); 32} 33 34/* TABS */ 35const developerDocumentationTabsScrollContainer = document.querySelector('.developer-documentation-tabs__container'); 36const developerDocumentationTabs = document.querySelector('.developer-documentation-tabs'); 37 38if (developerDocumentationTabsScrollContainer) { 39 developerDocumentationTabsScrollContainer.addEventListener('scroll', () => { 40 adjustDeveloperDocumentationTabsScroll(); 41 }); 42 43 window.addEventListener('resize', () => { 44 adjustDeveloperDocumentationTabsScroll(); 45 adjustDeveloperDocumentationTabsActiveScroll(); 46 }); 47 48 window.addEventListener('load', function() { 49 adjustDeveloperDocumentationTabsScroll(); 50 adjustDeveloperDocumentationTabsActiveScroll(); 51 }); 52 53 // Add drag scrolling functionality 54 let isDown = false; 55 let startX; 56 let scrollLeft; 57 let moved = false; 58 // Minimum horizontal distance (px) the pointer must travel before a 59 // mousedown+move is treated as a drag instead of a click. Increase to make 60 // clicks more forgiving, decrease to make dragging more sensitive. 61 const DRAG_THRESHOLD = 10; 62 63 // Prevent default drag behavior on all links 64 const tabLinks = developerDocumentationTabsScrollContainer.querySelectorAll('a'); 65 tabLinks.forEach(link => { 66 link.addEventListener('dragstart', (e) => { 67 e.preventDefault(); 68 }); 69 }); 70 71 developerDocumentationTabsScrollContainer.addEventListener('mousedown', (e) => { 72 if (!isDeveloperDocumentationTabsScrollable()) return; 73 isDown = true; 74 moved = false; 75 developerDocumentationTabsScrollContainer.classList.add('active-drag'); 76 startX = e.pageX - developerDocumentationTabsScrollContainer.offsetLeft; 77 scrollLeft = developerDocumentationTabsScrollContainer.scrollLeft; 78 }); 79 80 developerDocumentationTabsScrollContainer.addEventListener('mouseleave', () => { 81 isDown = false; 82 developerDocumentationTabsScrollContainer.classList.remove('active-drag'); 83 }); 84 85 developerDocumentationTabsScrollContainer.addEventListener('mouseup', () => { 86 isDown = false; 87 developerDocumentationTabsScrollContainer.classList.remove('active-drag'); 88 // Reset moved flag after a short delay to allow click event to check it 89 setTimeout(() => { 90 moved = false; 91 }, 10); 92 }); 93 94 developerDocumentationTabsScrollContainer.addEventListener('mousemove', (e) => { 95 if (!isDown) return; 96 const x = e.pageX - developerDocumentationTabsScrollContainer.offsetLeft; 97 const walk = (x - startX) * 2; 98 // Only treat as a drag once the pointer has moved past the threshold, 99 // so small jitters between mousedown/mouseup still count as a click. 100 if (!moved && Math.abs(x - startX) < DRAG_THRESHOLD) return; 101 e.preventDefault(); 102 moved = true; 103 developerDocumentationTabsScrollContainer.scrollLeft = scrollLeft - walk; 104 }); 105 106 // Prevent clicks on links when dragging - use capture phase 107 tabLinks.forEach(link => { 108 link.addEventListener('click', (e) => { 109 if (moved) { 110 e.preventDefault(); 111 e.stopPropagation(); 112 e.stopImmediatePropagation(); 113 } 114 }, { capture: true }); 115 }); 116 117 // Touch scrolling works natively with overflow-x: auto 118} 119 120function isDeveloperDocumentationTabsScrollable() { 121 return developerDocumentationTabsScrollContainer.scrollWidth > developerDocumentationTabsScrollContainer.clientWidth + 1; 122} 123 124function adjustDeveloperDocumentationTabsScroll() { 125 // Toggle scrollable state so the grab cursor/drag only apply when scrolling 126 // is actually possible (independent of current scroll position). 127 developerDocumentationTabs.classList.toggle('is-scrollable', isDeveloperDocumentationTabsScrollable()); 128 129 // Check if the user has scrolled to the very end (also true when not scrollable) 130 // This calculation might need slight adjustments depending on padding/borders 131 if (developerDocumentationTabsScrollContainer.scrollLeft + developerDocumentationTabsScrollContainer.clientWidth >= developerDocumentationTabsScrollContainer.scrollWidth - 1) { 132 developerDocumentationTabs.classList.add('no-overflow'); 133 } else { 134 developerDocumentationTabs.classList.remove('no-overflow'); 135 } 136} 137 138function adjustDeveloperDocumentationTabsActiveScroll() { 139 const activeTab = developerDocumentationTabsScrollContainer.querySelector('.developer-documentation-tabs__item.active'); 140 if (activeTab) { 141 // Calculate the position to scroll to center the active tab 142 const scrollLeft = activeTab.offsetLeft - (developerDocumentationTabsScrollContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2); 143 144 // Scroll only the container, not the entire page 145 developerDocumentationTabsScrollContainer.scrollTo({ 146 left: scrollLeft, 147 behavior: 'smooth' 148 }); 149 } 150} 151 152/* TOC */ 153class ElementorTOCActiveHandler { 154 constructor(options = {}) { 155 this.tocSelector = options.tocSelector || '.elementor-toc__list-item';
156 this.linkSelector = options.linkSelector || '.elementor-toc__list-item-text'; 157 this.activeClass = options.activeClass || 'active'; 158 this.rootMargin = options.rootMargin || '0px 0px -10% 0px'; 159 this.threshold = options.threshold || [0, 0.25, 0.5, 0.75, 1]; 160 161 this.tocItems = []; 162 this.targetElements = new Map(); 163 this.visibilityRatios = new Map(); 164 this.observer = null; 165 166 this.init(); 167 } 168 169 init() { 170 // Wait for DOM to be ready, then wait for TOC to be rendered 171 if (document.readyState === 'loading') { 172 document.addEventListener('DOMContentLoaded', () => this.waitForTOC()); 173 } else { 174 this.waitForTOC(); 175 } 176 } 177 178 waitForTOC() { 179 // First check if the developer documentation TOC exists 180 const checkDevDocTOC = () => { 181 return document.querySelector('.developer-documentation-toc'); 182 }; 183 184 // Check if TOC elements exist within the dev doc TOC 185 const checkTOC = () => { 186 const devDocTOC = checkDevDocTOC(); 187 if (!devDocTOC) { 188 return false; 189 } 190 191 const tocElements = devDocTOC.querySelectorAll(this.tocSelector); 192 const tocLinks = devDocTOC.querySelectorAll(this.linkSelector); 193 194 if (tocElements.length > 0 && tocLinks.length > 0) { 195 this.setup(); 196 return true; 197 } 198 return false; 199 }; 200 201 // Try immediately first 202 if (checkTOC()) { 203 return; 204 } 205 206 // If not found, use multiple strategies to wait for TOC 207 let attempts = 0; 208 const maxAttempts = 50; // 10 seconds maximum 209 210 const pollForTOC = () => { 211 attempts++; 212 213 if (checkTOC()) { 214 return; // Found it! 215 } 216 217 if (attempts < maxAttempts) { 218 setTimeout(pollForTOC, 200); 219 } 220 }; 221 222 // Start polling 223 setTimeout(pollForTOC, 200); 224 225 // Also listen for various events that might indicate TOC is ready 226 const eventListeners = [ 227 'load', 228 'elementor/frontend/init', 229 'elementor/frontend/ready' 230 ]; 231 232 eventListeners.forEach(eventName => { 233 window.addEventListener(eventName, () => { 234 setTimeout(() => { 235 if (!this.tocItems.length) { 236 checkTOC(); 237 } 238 }, 100); 239 }); 240 }); 241 242 // Watch for DOM mutations (when Elementor adds the TOC) 243 const observer = new MutationObserver((mutations) => { 244 mutations.forEach((mutation) => { 245 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 246 mutation.addedNodes.forEach((node) => { 247 if (node.nodeType === 1) { // Element node 248 // Check if the added node is or contains our dev doc TOC 249 const isDevDocTOC = node.classList.contains('developer-documentation-toc') || 250 node.querySelector && node.querySelector('.developer-documentation-toc'); 251 252 if (isDevDocTOC && !this.tocItems.length) { 253 setTimeout(() => { 254 checkTOC(); 255 }, 100); 256 } 257 } 258 }); 259 } 260 }); 261 }); 262 263 observer.observe(document.body, { 264 childList: true, 265 subtree: true 266 }); 267 268 // Clean up observer after TOC is found 269 const originalSetup = this.setup.bind(this); 270 this.setup = function() { 271 observer.disconnect(); 272 originalSetup(); 273 }; 274 } 275 276 setup() { 277 this.collectTOCItems(); 278 this.setupIntersectionObserver(); 279 this.observeTargetElements(); 280 281 // Set initial active state 282 setTimeout(() => this.updateActiveStates(), 100); 283 } 284 285 collectTOCItems() { 286 // Make sure we're only looking within the developer documentation TOC 287 const devDocTOC = document.querySelector('.developer-documentation-toc'); 288 if (!devDocTOC) { 289 return; 290 } 291 292 const tocElements = devDocTOC.querySelectorAll(this.tocSelector); 293
294 tocElements.forEach(tocItem => { 295 const link = tocItem.querySelector(this.linkSelector); 296 if (!link) return; 297 298 const href = link.getAttribute('href'); 299 if (!href || !href.startsWith('#')) return; 300 301 const targetId = href.substring(1); 302 const directTarget = document.getElementById(targetId); 303 304 if (directTarget) { 305 // Find the parent .elementor-element with data-element_type="container" 306 let targetElement = directTarget.closest('.elementor-element[data-element_type="container"]'); 307 308 // Fallback: if no container parent, try any .elementor-element 309 if (!targetElement) { 310 targetElement = directTarget.closest('.elementor-element'); 311 } 312 313 // Final fallback: use the direct target 314 if (!targetElement) { 315 targetElement = directTarget; 316 } 317 318 this.tocItems.push({ 319 tocItem, 320 link, 321 targetElement, 322 targetId, 323 directTarget // Keep reference to original target 324 }); 325 326 this.targetElements.set(targetElement, { 327 tocItem, 328 targetId, 329 visibilityRatio: 0, 330 directTarget 331 }); 332 333 this.visibilityRatios.set(targetId, 0); 334 } 335 }); 336 } 337 338 setupIntersectionObserver() { 339 const options = { 340 root: null, 341 rootMargin: this.rootMargin, 342 threshold: this.threshold 343 }; 344 345 this.observer = new IntersectionObserver((entries) => { 346 entries.forEach(entry => { 347 const targetData = this.targetElements.get(entry.target); 348 if (targetData) { 349 targetData.visibilityRatio = entry.intersectionRatio; 350 this.visibilityRatios.set(targetData.targetId, entry.intersectionRatio); 351 } 352 }); 353 354 this.updateActiveStates(); 355 }, options); 356 } 357 358 observeTargetElements() { 359 this.targetElements.forEach((data, element) => { 360 this.observer.observe(element); 361 }); 362 } 363 364 updateActiveStates() { 365 // Remove all active classes first 366 this.tocItems.forEach(({ tocItem }) => { 367 tocItem.classList.remove(this.activeClass); 368 }); 369 370 // Find the element with highest visibility ratio 371 let maxVisibilityRatio = 0; 372 let mostVisibleElement = null; 373 374 // Check for fully visible elements first 375 for (const [targetId, ratio] of this.visibilityRatios.entries()) { 376 if (ratio >= 0.99) { // Consider 99%+ as fully visible 377 const targetData = Array.from(this.targetElements.values()) 378 .find(data => data.targetId === targetId); 379 380 if (targetData) { 381 mostVisibleElement = targetData; 382 break; 383 } 384 } 385 } 386 387 // If no fully visible element, find the one with highest visibility 388 if (!mostVisibleElement) { 389 for (const [targetId, ratio] of this.visibilityRatios.entries()) { 390 if (ratio > maxVisibilityRatio) { 391 maxVisibilityRatio = ratio; 392 const targetData = Array.from(this.targetElements.values()) 393 .find(data => data.targetId === targetId); 394 395 if (targetData) { 396 mostVisibleElement = targetData; 397 } 398 } 399 } 400 } 401 402 // Apply active class to the most visible element 403 if (mostVisibleElement && (mostVisibleElement.visibilityRatio > 0 || maxVisibilityRatio > 0)) { 404 mostVisibleElement.tocItem.classList.add(this.activeClass); 405 406 // Optional: scroll the TOC item into view if it's outside viewport 407 this.scrollTOCItemIntoView(mostVisibleElement.tocItem); 408 } 409 410 // Fallback: if no elements are visible, activate the first one 411 if (maxVisibilityRatio === 0 && this.tocItems.length > 0) { 412 // Check if we're at the top of the page 413 if (window.scrollY < 100) { 414 this.tocItems[0].tocItem.classList.add(this.activeClass); 415 } 416 } 417 } 418 419 scrollTOCItemIntoView(tocItem) { 420 const tocContainer = tocItem.closest('.elementor-toc__body, .elementor-widget-table-of-contents'); 421 if (tocContainer) { 422 const tocRect = tocContainer.getBoundingClientRect(); 423 const itemRect = tocItem.getBoundingClientRect(); 424 425 // Check if item is outside the visible TOC area 426 if (itemRect.top < tocRect.top || itemRect.bottom > tocRect.bottom) { 427 tocItem.scrollIntoView({ 428 behavior: 'smooth', 429 block: 'nearest',
430 inline: 'nearest' 431 }); 432 } 433 } 434 } 435 436 // Method to manually refresh the active states 437 refresh() { 438 this.updateActiveStates(); 439 } 440 441 // Method to destroy the observer 442 destroy() { 443 if (this.observer) { 444 this.observer.disconnect(); 445 } 446 447 // Remove all active classes 448 this.tocItems.forEach(({ tocItem }) => { 449 tocItem.classList.remove(this.activeClass); 450 }); 451 } 452} 453 454// Auto-initialize when script loads 455(function() { 456 let tocHandler = null; 457 458 const initializeTOC = () => { 459 if (tocHandler) { 460 tocHandler.destroy(); 461 } 462 tocHandler = new ElementorTOCActiveHandler(); 463 window.tocActiveHandler = tocHandler; 464 }; 465 466 // Initialize immediately 467 initializeTOC(); 468 469 // Re-initialize on various Elementor events 470 const elementorEvents = [ 471 'elementor/frontend/init', 472 'elementor/frontend/ready', 473 'elementor/loaded' 474 ]; 475 476 elementorEvents.forEach(eventName => { 477 document.addEventListener(eventName, () => { 478 setTimeout(initializeTOC, 500); 479 }); 480 }); 481 482 // Listen for Elementor frontend hooks if available 483 if (typeof elementorFrontend !== 'undefined') { 484 elementorFrontend.hooks.addAction('frontend/element_ready/widget', function(scope) { 485 const tocWidget = scope.find('.elementor-widget-table-of-contents'); 486 if (tocWidget.length > 0) { 487 setTimeout(initializeTOC, 300); 488 } 489 }); 490 } else { 491 // If elementorFrontend isn't available yet, wait for it 492 const waitForElementor = () => { 493 if (typeof elementorFrontend !== 'undefined') { 494 elementorFrontend.hooks.addAction('frontend/element_ready/widget', function(scope) { 495 const tocWidget = scope.find('.elementor-widget-table-of-contents'); 496 if (tocWidget.length > 0) { 497 setTimeout(initializeTOC, 300); 498 } 499 }); 500 } else { 501 setTimeout(waitForElementor, 100); 502 } 503 }; 504 setTimeout(waitForElementor, 100); 505 } 506 507 // Fallback: Watch for specific Elementor classes being added 508 const observer = new MutationObserver((mutations) => { 509 mutations.forEach((mutation) => { 510 if (mutation.type === 'attributes' && 511 mutation.target.classList && 512 mutation.target.classList.contains('elementor-widget-table-of-contents')) { 513 setTimeout(initializeTOC, 200); 514 } 515 }); 516 }); 517 518 observer.observe(document.body, { 519 attributes: true, 520 attributeFilter: ['class'], 521 subtree: true 522 }); 523})(); 524 525// Export for module usage 526if (typeof module !== 'undefined' && module.exports) { 527 module.exports = ElementorTOCActiveHandler; 528}
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.