import "@hotwired/turbo-rails" import { Application } from "@hotwired/stimulus" import LocaleController from "locale_controller" import ImageController from "image_controller" const application = Application.start() application.register("locale", LocaleController) application.register("image", ImageController) // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application } var animationElements var originalHeights document.addEventListener('turbo:render', (event) => { console.info('turbo:render') let errorElements = document.querySelectorAll('.field_with_errors') if (errorElements.length > 0) { animationElements = document.querySelectorAll('.animation-element'); gsap.set(animationElements, { opacity: 1 }) } }); // document.addEventListener("turbo:before-stream-render", animateElements) document.addEventListener("turbo:load", animateElements) function animateElements(event) { console.info(event.type) // Copy link to clipboard document.querySelectorAll('[data-share-title]').forEach((share_btn) => { share_btn.addEventListener('click', (e) => { if (navigator.share) { navigator.share({ title: e.currentTarget.getAttribute('data-share-title'), text: e.currentTarget.getAttribute('data-share-text'), url: e.currentTarget.getAttribute('data-share-url'), }).then(() => { console.log('Thanks for sharing!'); }) .catch(console.error); } else { navigator.clipboard.writeText(window.location.href) alert('URL copied to clipboard!') } }) }) originalHeights = [] animationElements = document.querySelectorAll('.animation-element'); animationElements.forEach((animationElement, index) => { const height = animationElement.offsetHeight originalHeights.push(height) // console.log(`Element ${index + 1} height: ${height}px`) }) gsap.set(animationElements, { height: 0, opacity: 0, overflow: 'hidden' }) let typewriterElements = document.querySelectorAll('.typewriter-text'); if (typewriterElements.length > 0) { gsap.set(typewriterElements, { opacity: 0 }) } // Start the sequential animation animateElementsSequentially(); } function animateElementsSequentially(index = 0) { if (index >= animationElements.length) { console.log('All animations complete'); return; } // 'margin-top': (index == (animationElements.length-1) ? "12px" : "8px"), gsap.to(animationElements[index], { height: originalHeights[index], opacity: 1, duration: (0.004 * originalHeights[index]), delay: (index > 0 ? 0.4 : 0), ease: "power2.out", onComplete: () => { // Typewriter const typewriterTexts = animationElements[index].querySelectorAll('.typewriter-text') if (typewriterTexts.length > 0) { const typewriter = typeWriterConsoleSequential(typewriterTexts[0].parentElement, { baseSpeed: 20, onAllComplete: () => { // Start next element after typewriter finishes animateElementsSequentially(index + 1); } }) typewriter.start() return } // % Answer Distributions const answerDistributions = animationElements[index].querySelectorAll('.answer-distribution') if (answerDistributions.length > 0 ) { const containerHeight = animationElements[index].offsetHeight - 8; // - padding const percentages = Array.from(answerDistributions).map(bar => parseInt(bar.dataset.value)); const maxPercentage = Math.max(...percentages); console.info(maxPercentage) answerDistributions.forEach((bar, i) => { const percentage = parseInt(bar.dataset.value); const relativePercentage = (percentage / maxPercentage) * 100; const calculatedHeight = (relativePercentage / 100) * containerHeight; const computedStyle = window.getComputedStyle(bar); const minHeight = parseInt(computedStyle.minHeight) || 0; const targetHeight = Math.max(calculatedHeight, minHeight); gsap.to(bar, { height: targetHeight, duration: 0.4, ease: "power2.out", delay: index * 0.1, onAllComplete: () => { if (bar.dataset.answered != undefined) { bar.classList.add('answered') } gsap.to(bar.querySelectorAll('.percentage'), { opacity: 1, duration: 0.4, onAllComplete: function() { const explanationElement = this.targets()[0].nextElementSibling if (explanationElement) { explanationElement.style.display = 'block' } } }) if (i == (answerDistributions.length - 1)) { setTimeout(() => animateElementsSequentially(index + 1), 1000); } } }); }) return } animateElementsSequentially(index + 1); } }); } function typeWriterConsoleSequential(container, options = {}) { const { selector = '.typewriter-text', baseSpeed = 60, divDelay = 600, showCursor = true, onDivComplete = null, onAllComplete = null } = options; const textDivs = container.querySelectorAll(selector); if (textDivs.length === 0) return null; let currentDivIndex = 0; let isActive = false; let animationId = null; let timeoutIds = new Set(); // Track all timeouts for cleanup // Cleanup function for Turbo navigation function cleanup() { isActive = false; if (animationId) { cancelAnimationFrame(animationId); animationId = null; } // Clear all timeouts timeoutIds.forEach(id => clearTimeout(id)); timeoutIds.clear(); } // Set up Turbo event listeners for cleanup function setupTurboListeners() { const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload']; turboEvents.forEach(eventName => { document.addEventListener(eventName, cleanup, { once: false }); }); // Store cleanup function on container for manual cleanup container._typewriterCleanup = cleanup; } // Enhanced setTimeout that tracks IDs function safeSetTimeout(callback, delay) { const id = setTimeout(() => { timeoutIds.delete(id); if (isActive) callback(); }, delay); timeoutIds.add(id); return id; } // Store original text and prepare divs function initializeDivs() { // Set container height to prevent layout shifts const containerHeight = container.offsetHeight; container.style.minHeight = `${containerHeight}px`; textDivs.forEach(div => { if (!div.dataset.originalText) { div.dataset.originalText = div.textContent.trim(); } // Use visibility instead of blanking content to maintain layout div.style.visibility = 'hidden'; div.innerHTML = ''; div.style.opacity = '1'; }); // Force reflow to ensure styles are applied container.offsetHeight; } function createCursorSpan() { // No longer needed - cursor is now handled via CSS return null; } function updateDivContent(div, text, withCursor = false) { // Use textContent for better performance div.textContent = text; // Toggle cursor class instead of adding DOM element if (withCursor && showCursor) { div.classList.add('typewriter-cursor'); } else { div.classList.remove('typewriter-cursor'); } } function animateNextDiv() { if (currentDivIndex >= textDivs.length || !isActive) { isActive = false; if (onAllComplete) { // Use setTimeout to avoid potential timing issues requestAnimationFrame(() => { safeSetTimeout(() => onAllComplete(container), 200); }); } return; } const currentDiv = textDivs[currentDivIndex]; const originalText = currentDiv.dataset.originalText; const isLastDiv = currentDivIndex === textDivs.length - 1; // Make div visible and prepare for typing currentDiv.style.visibility = 'visible'; updateDivContent(currentDiv, '', showCursor); currentDiv.classList.add('typing-active'); let charIndex = 0; let nextCharTime = performance.now() + 200; // Initial delay function typeFrame(currentTime) { if (!isActive) { if (animationId) { cancelAnimationFrame(animationId); animationId = null; } return; } if (currentTime >= nextCharTime && charIndex < originalText.length) { charIndex++; const currentText = originalText.substring(0, charIndex); updateDivContent(currentDiv, currentText, true); // Calculate delay for next character let delay = baseSpeed + Math.random() * 30; const char = originalText.charAt(charIndex - 1); if (char === '.' || char === '!' || char === '?') { delay += 250; } else if (char === ',' || char === ';') { delay += 120; } else if (char === ' ') { delay += 60; } nextCharTime = currentTime + delay; } // Check if current div is complete if (charIndex >= originalText.length) { currentDiv.classList.remove('typing-active'); // Final content update updateDivContent(currentDiv, originalText, showCursor); if (onDivComplete) { onDivComplete(currentDiv, currentDivIndex, container); } currentDivIndex++; if (isActive && currentDivIndex < textDivs.length) { // Move to next div after delay safeSetTimeout(() => { if (isActive) { // Remove cursor from previous div (except last one) if (!isLastDiv) { updateDivContent(currentDiv, originalText, false); } animateNextDiv(); } }, divDelay); } else if (isActive) { // This was the last div safeSetTimeout(() => { if (onAllComplete) { onAllComplete(container); } }, 200); } return; } // Continue animation if (isActive) { animationId = requestAnimationFrame(typeFrame); } } // Start the animation animationId = requestAnimationFrame(typeFrame); } // Initialize Turbo listeners setupTurboListeners(); // Add required CSS for cursor pseudo-element function addCursorStyles() { const styleId = 'typewriter-cursor-styles'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` .typewriter-cursor::after { content: '|'; animation: typewriter-blink 1s infinite; color: currentColor; } @keyframes typewriter-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } } `; document.head.appendChild(style); } } // Initialize styles addCursorStyles(); return { start: () => { if (isActive) return; // Prevent multiple starts initializeDivs(); isActive = true; currentDivIndex = 0; // Use requestAnimationFrame for smoother start requestAnimationFrame(() => { if (isActive) { animateNextDiv(); } }); }, stop: () => { cleanup(); }, reset: () => { cleanup(); textDivs.forEach(div => { if (div.dataset.originalText) { div.style.visibility = 'visible'; updateDivContent(div, div.dataset.originalText, false); div.classList.remove('typing-active'); div.classList.remove('typewriter-cursor'); } }); currentDivIndex = 0; }, // Manual cleanup method for explicit cleanup destroy: () => { cleanup(); // Remove Turbo event listeners const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload']; turboEvents.forEach(eventName => { document.removeEventListener(eventName, cleanup); }); // Remove cleanup reference delete container._typewriterCleanup; }, get isActive() { return isActive; } }; }